docker build -t $(httpStressImage) --build-arg SDK_BASE_IMAGE=$(sdkBaseImage) --build-arg CONFIGURATION=$(BUILD_CONFIGURATION) -f windows.Dockerfile .
displayName: Build HttpStress
-- bash: |
+- powershell: |
cd '$(HttpStressProject)'
docker-compose up --abort-on-container-exit --no-color
displayName: Run HttpStress
--- /dev/null
+trigger: none
+
+pr:
+ branches:
+ include:
+ - "*"
+
+schedules:
+- cron: "0 13 * * *" # 1PM UTC => 5 AM PST
+ displayName: SslStress nightly run
+ branches:
+ include:
+ - master
+
+pool:
+ name: Hosted Ubuntu 1604
+
+variables:
+ - template: ../variables.yml
+ - name: sslStressProject
+ value: $(sourcesRoot)/System.Net.Security/tests/StressTests/SslStress/
+ # Avoid duplication by referencing corefx build infrastructure hosted in HttpStress
+ # TODO move to eng/ folder.
+ - name: httpStressProject
+ value: $(sourcesRoot)/System.Net.Http/tests/StressTests/HttpStress/
+ - name: sdkBaseImage
+ value: sdk-corefx-current
+ - name: sslStressImage
+ value: sslstress
+
+steps:
+- checkout: self
+ clean: true
+ fetchDepth: 1
+ lfs: false
+
+- bash: |
+ docker build -t $(sdkBaseImage) --build-arg CONFIGURATION=$(BUILD_CONFIGURATION) --build-arg BUILD_SCRIPT_NAME=$(buildScriptFileName) -f $(httpStressProject)corefx.Dockerfile .
+ displayName: Build Libraries
+
+- bash: |
+ # we need to include the entire src/libraries folder in the build context due to Common/ dependencies
+ docker build -t $(sslStressImage) --build-arg SDK_BASE_IMAGE=$(sdkBaseImage) --build-arg CONFIGURATION=$(BUILD_CONFIGURATION) -f $(sslStressProject)/Dockerfile src/libraries
+ displayName: Build SslStress
+
+- bash: |
+ cd '$(sslStressProject)'
+ docker-compose up --abort-on-container-exit --no-color
+ displayName: Run SslStress
+ env:
+ HTTPSTRESS_IMAGE: $(sslStressImage)
--- /dev/null
+trigger: none
+
+pr:
+ branches:
+ include:
+ - "*"
+
+schedules:
+- cron: "0 13 * * *" # 1PM UTC => 5 AM PST
+ displayName: SslStress nightly run
+ branches:
+ include:
+ - master
+
+pool:
+ vmImage: 'windows-latest'
+
+variables:
+ - template: ../variables.yml
+ - name: sslStressProject
+ value: $(sourcesRoot)/System.Net.Security/tests/StressTests/SslStress/
+ # Avoid duplication by referencing corefx build infrastructure hosted in HttpStress
+ # TODO move to eng/ folder.
+ - name: httpStressProject
+ value: $(sourcesRoot)/System.Net.Http/tests/StressTests/HttpStress/
+ - name: sdkBaseImage
+ value: sdk-corefx-current
+ - name: sslStressImage
+ value: sslstress
+
+steps:
+- checkout: self
+ clean: true
+ fetchDepth: 1
+ lfs: false
+
+- powershell: |
+ .\libraries.cmd -ci -c $(BUILD_CONFIGURATION)
+ docker build -t $(sdkBaseImage) `
+ --build-arg CONFIGURATION=$(BUILD_CONFIGURATION) `
+ --build-arg TESTHOST_LOCATION=. `
+ -f src/libraries/System.Net.Http/tests/StressTests/HttpStress/corefx.windows.Dockerfile `
+ artifacts/bin/testhost
+
+ displayName: Build Libraries
+
+- powershell: |
+ # we need to include the entire src/libraries folder in the build context due to Common/ dependencies
+ docker build -t $(sslStressImage) --build-arg SDK_BASE_IMAGE=$(sdkBaseImage) --build-arg CONFIGURATION=$(BUILD_CONFIGURATION) -f $(sslStressProject)/windows.Dockerfile src/libraries
+ displayName: Build SslStress
+
+- powershell: |
+ cd '$(sslStressProject)'
+ docker-compose up --abort-on-container-exit --no-color
+ displayName: Run SslStress
+ env:
+ SSLTRESS_IMAGE: $(sslStressImage)
+
+- task: PublishBuildArtifacts@1
+ displayName: Publish Logs
+ inputs:
+ PathtoPublish: '$(Build.SourcesDirectory)/artifacts/log/$(BUILD_CONFIGURATION)'
+ PublishLocation: Container
+ ArtifactName: 'sslstress_$(Agent.Os)_$(Agent.JobName)'
+ continueOnError: true
+ condition: always()
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System;
+
public class CRC
{
// Table of CRCs of all 8-bit messages.
s_crc_table_computed = true;
}
- // Update a running CRC with the bytes buf[0..len-1]--the CRC
+ // Update a running CRC with the bytes --the CRC
// should be initialized to all 1's, and the transmitted value
// is the 1's complement of the final running CRC (see the
// crc() routine below)).
- private static ulong update_crc(ulong crc, byte[] buf, int len)
+ public static ulong UpdateCRC(ulong crc, ReadOnlySpan<byte> buf)
{
ulong c = crc;
int n;
if (!s_crc_table_computed)
make_crc_table();
- for (n = 0; n < len; n++)
+ for (n = 0; n < buf.Length; n++)
{
c = s_crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8);
}
return c;
}
- internal static string CalculateCRC(byte[] buf) => CalculateCRC(buf, buf.Length);
-
- // Return the CRC of the bytes buf[0..len-1].
- internal static string CalculateCRC(byte[] buf, int len) =>
- (update_crc(0xffffffffL, buf, len) ^ 0xffffffffL).ToString();
+ public static ulong CalculateCRC(ReadOnlySpan<byte> buf) => (UpdateCRC(0xffffffffL, buf) ^ 0xffffffffL);
}
entrystream.Read(buffer, 0, buffer.Length);
#if NETCOREAPP
uint zipcrc = entry.Crc32;
- Assert.Equal(CRC.CalculateCRC(buffer), zipcrc.ToString());
+ Assert.Equal(CRC.CalculateCRC(buffer), zipcrc);
#endif
if (file.Length != givenLength)
}
Assert.Equal(file.Length, buffer.Length);
- string crc = CRC.CalculateCRC(buffer);
- Assert.Equal(file.CRC, crc);
+ ulong crc = CRC.CalculateCRC(buffer);
+ Assert.Equal(file.CRC, crc.ToString());
}
if (checkTimes)
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Net;
+
+namespace SslStress
+{
+ [Flags]
+ public enum RunMode { server = 1, client = 2, both = server | client };
+
+ public class Configuration
+ {
+ public IPEndPoint ServerEndpoint { get; set; } = new IPEndPoint(IPAddress.Loopback, 0);
+ public RunMode RunMode { get; set; }
+ public int RandomSeed { get; set; }
+ public double CancellationProbability { get; set; }
+ public int MaxConnections { get; set; }
+ public int MaxBufferLength { get; set; }
+ public TimeSpan? MaxExecutionTime { get; set; }
+ public TimeSpan DisplayInterval { get; set; }
+ public TimeSpan MinConnectionLifetime { get; set; }
+ public TimeSpan MaxConnectionLifetime { get; set; }
+ public bool LogServer { get; set; }
+ }
+}
--- /dev/null
+<Project/>
--- /dev/null
+<Project/>
--- /dev/null
+ARG SDK_BASE_IMAGE=mcr.microsoft.com/dotnet/core/sdk:3.0.100-buster
+FROM $SDK_BASE_IMAGE
+
+WORKDIR /app
+COPY . .
+WORKDIR /app/System.Net.Security/tests/StressTests/SslStress
+
+ARG CONFIGURATION=Release
+RUN dotnet build -c $CONFIGURATION
+
+EXPOSE 5001
+
+ENV CONFIGURATION=$CONFIGURATION
+ENV SSLSTRESS_ARGS=''
+CMD dotnet run --no-build -c $CONFIGURATION -- $SSLSTRESS_ARGS
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.CommandLine;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Net;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using SslStress.Utils;
+
+namespace SslStress
+{
+ public static class Program
+ {
+ public enum ExitCode { Success = 0, StressError = 1, CliError = 2 };
+
+ public static async Task<int> Main(string[] args)
+ {
+ if (!TryParseCli(args, out Configuration? config))
+ {
+ return (int)ExitCode.CliError;
+ }
+
+ return (int)await Run(config);
+ }
+
+ private static async Task<ExitCode> Run(Configuration config)
+ {
+ if ((config.RunMode & RunMode.both) == 0)
+ {
+ Console.Error.WriteLine("Must specify a valid run mode");
+ return ExitCode.CliError;
+ }
+
+ static string GetAssemblyInfo(Assembly assembly) => $"{assembly.Location}, modified {new FileInfo(assembly.Location).LastWriteTime}";
+
+ Console.WriteLine(" .NET Core: " + GetAssemblyInfo(typeof(object).Assembly));
+ Console.WriteLine(" System.Net.Security: " + GetAssemblyInfo(typeof(System.Net.Security.SslStream).Assembly));
+ Console.WriteLine(" Server Endpoint: " + config.ServerEndpoint);
+ Console.WriteLine(" Concurrency: " + config.MaxConnections);
+ Console.WriteLine(" Max Execution Time: " + ((config.MaxExecutionTime != null) ? config.MaxExecutionTime.Value.ToString() : "infinite"));
+ Console.WriteLine(" Min Conn. Lifetime: " + config.MinConnectionLifetime);
+ Console.WriteLine(" Max Conn. Lifetime: " + config.MaxConnectionLifetime);
+ Console.WriteLine(" Random Seed: " + config.RandomSeed);
+ Console.WriteLine(" Cancellation Pb: " + 100 * config.CancellationProbability + "%");
+ Console.WriteLine();
+
+ StressServer? server = null;
+ if (config.RunMode.HasFlag(RunMode.server))
+ {
+ // Start the SSL web server in-proc.
+ Console.WriteLine($"Starting SSL server.");
+ server = new StressServer(config);
+ server.Start();
+
+ Console.WriteLine($"Server listening to {server.ServerEndpoint}");
+ }
+
+ StressClient? client = null;
+ if (config.RunMode.HasFlag(RunMode.client))
+ {
+ // Start the client.
+ Console.WriteLine($"Starting {config.MaxConnections} client workers.");
+ Console.WriteLine();
+
+ client = new StressClient(config);
+ client.Start();
+ }
+
+ await WaitUntilMaxExecutionTimeElapsedOrKeyboardInterrupt(config.MaxExecutionTime);
+
+ try
+ {
+ if (client != null)
+ {
+ await client.StopAsync();
+ Console.WriteLine("client stopped");
+ }
+
+ if (server != null)
+ {
+ await server.StopAsync();
+ Console.WriteLine("server stopped");
+ }
+ }
+ finally
+ {
+ client?.PrintFinalReport();
+ }
+
+ return client?.TotalErrorCount == 0 ? ExitCode.Success : ExitCode.StressError;
+
+ static async Task WaitUntilMaxExecutionTimeElapsedOrKeyboardInterrupt(TimeSpan? maxExecutionTime = null)
+ {
+ var tcs = new TaskCompletionSource<bool>();
+ Console.CancelKeyPress += (sender, args) => { Console.Error.WriteLine("Keyboard interrupt"); args.Cancel = true; tcs.TrySetResult(false); };
+ if (maxExecutionTime.HasValue)
+ {
+ Console.WriteLine($"Running for a total of {maxExecutionTime.Value.TotalMinutes:0.##} minutes");
+ var cts = new System.Threading.CancellationTokenSource(delay: maxExecutionTime.Value);
+ cts.Token.Register(() => { Console.WriteLine("Max execution time elapsed"); tcs.TrySetResult(false); });
+ }
+
+ await tcs.Task;
+ }
+ }
+
+ private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configuration? config)
+ {
+ var cmd = new RootCommand();
+ cmd.AddOption(new Option(new[] { "--help", "-h" }, "Display this help text."));
+ cmd.AddOption(new Option(new[] { "--mode", "-m" }, "Stress suite execution mode. Defaults to 'both'.") { Argument = new Argument<RunMode>("runMode", RunMode.both) });
+ cmd.AddOption(new Option(new[] { "--cancellation-probability", "-p"}, "Cancellation probability 0 <= p <= 1 for a given connection. Defaults to 0.1") { Argument = new Argument<double>("probability", 0.1)});
+ cmd.AddOption(new Option(new[] { "--num-connections", "-n" }, "Max number of connections to open concurrently.") { Argument = new Argument<int>("connections", Environment.ProcessorCount) });
+ cmd.AddOption(new Option(new[] { "--server-endpoint", "-e" }, "Endpoint to bind to if server, endpoint to listen to if client.") { Argument = new Argument<string>("ipEndpoint", "127.0.0.1:5002") });
+ cmd.AddOption(new Option(new[] { "--max-execution-time", "-t" }, "Maximum stress suite execution time, in minutes. Defaults to infinity.") { Argument = new Argument<double?>("minutes", null) });
+ cmd.AddOption(new Option(new[] { "--max-buffer-length", "-b" }, "Maximum buffer length to write on ssl stream. Defaults to 8192.") { Argument = new Argument<int>("bytes", 8192) });
+ cmd.AddOption(new Option(new[] { "--min-connection-lifetime", "-l" }, "Minimum duration for a single connection, in seconds. Defaults to 5 seconds.") { Argument = new Argument<double>("seconds", 5) });
+ cmd.AddOption(new Option(new[] { "--max-connection-lifetime", "-L" }, "Maximum duration for a single connection, in seconds. Defaults to 120 seconds.") { Argument = new Argument<double>("seconds", 120) });
+ cmd.AddOption(new Option(new[] { "--display-interval", "-i" }, "Client stats display interval, in seconds. Defaults to 5 seconds.") { Argument = new Argument<double>("seconds", 5) });
+ cmd.AddOption(new Option(new[] { "--log-server", "-S" }, "Print server logs to stdout."));
+ cmd.AddOption(new Option(new[] { "--seed", "-s" }, "Seed for generating pseudo-random parameters. Also depends on the -n argument.") { Argument = new Argument<int>("seed", (new Random().Next())) });
+
+ ParseResult parseResult = cmd.Parse(args);
+ if (parseResult.Errors.Count > 0 || parseResult.HasOption("-h"))
+ {
+ foreach (ParseError error in parseResult.Errors)
+ {
+ Console.WriteLine(error);
+ }
+ WriteHelpText();
+ config = null;
+ return false;
+ }
+
+ config = new Configuration()
+ {
+ RunMode = parseResult.ValueForOption<RunMode>("-m"),
+ MaxConnections = parseResult.ValueForOption<int>("-n"),
+ CancellationProbability = Math.Max(0, Math.Min(1, parseResult.ValueForOption<double>("-p"))),
+ ServerEndpoint = ParseEndpoint(parseResult.ValueForOption<string>("-e")),
+ MaxExecutionTime = parseResult.ValueForOption<double?>("-t")?.Pipe(TimeSpan.FromMinutes),
+ MaxBufferLength = parseResult.ValueForOption<int>("-b"),
+ MinConnectionLifetime = TimeSpan.FromSeconds(parseResult.ValueForOption<double>("-l")),
+ MaxConnectionLifetime = TimeSpan.FromSeconds(parseResult.ValueForOption<double>("-L")),
+ DisplayInterval = TimeSpan.FromSeconds(parseResult.ValueForOption<double>("-i")),
+ LogServer = parseResult.HasOption("-S"),
+ RandomSeed = parseResult.ValueForOption<int>("-s"),
+ };
+
+ if (config.MaxConnectionLifetime < config.MinConnectionLifetime)
+ {
+ Console.WriteLine("Max connection lifetime should be greater than or equal to min connection lifetime");
+ WriteHelpText();
+ config = null;
+ return false;
+ }
+
+ return true;
+
+ void WriteHelpText()
+ {
+ Console.WriteLine();
+ new HelpBuilder(new SystemConsole()).Write(cmd);
+ }
+
+ static IPEndPoint ParseEndpoint(string value)
+ {
+ try
+ {
+ return IPEndPoint.Parse(value);
+ }
+ catch (FormatException)
+ {
+ // support hostname:port endpoints
+ Match match = Regex.Match(value, "^([^:]+):([0-9]+)$");
+ if (match.Success)
+ {
+ string hostname = match.Groups[1].Value;
+ int port = int.Parse(match.Groups[2].Value);
+ switch(hostname)
+ {
+ case "+":
+ case "*":
+ return new IPEndPoint(IPAddress.Any, port);
+ default:
+ IPAddress[] addresses = Dns.GetHostAddresses(hostname);
+ return new IPEndPoint(addresses[0], port);
+ }
+ }
+
+ throw;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+## SslStress
+
+Stress testing suite for SslStream
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using SslStress.Utils;
+
+namespace SslStress
+{
+ public abstract partial class SslClientBase
+ {
+ private class StressResultAggregator
+ {
+ private long _totalConnections = 0;
+ private readonly long[] _successes, _failures, _cancellations;
+ private readonly ErrorAggregator _errors = new ErrorAggregator();
+ private readonly StreamCounter[] _currentCounters;
+ private readonly StreamCounter[] _aggregateCounters;
+
+ public StressResultAggregator(int workerCount)
+ {
+ _currentCounters = Enumerable.Range(0, workerCount).Select(_ => new StreamCounter()).ToArray();
+ _aggregateCounters = Enumerable.Range(0, workerCount).Select(_ => new StreamCounter()).ToArray();
+ _successes = new long[workerCount];
+ _failures = new long[workerCount];
+ _cancellations = new long[workerCount];
+ }
+
+ public long TotalConnections => _totalConnections;
+ public long TotalFailures => _failures.Sum();
+
+ public StreamCounter GetCounters(int workerId) => _currentCounters[workerId];
+
+ public void RecordSuccess(int workerId)
+ {
+ _successes[workerId]++;
+ Interlocked.Increment(ref _totalConnections);
+ UpdateCounters(workerId);
+ }
+
+ public void RecordCancellation(int workerId)
+ {
+ _cancellations[workerId]++;
+ Interlocked.Increment(ref _totalConnections);
+ UpdateCounters(workerId);
+ }
+
+ public void RecordFailure(int workerId, Exception exn)
+ {
+ _failures[workerId]++;
+ Interlocked.Increment(ref _totalConnections);
+ _errors.RecordError(exn);
+ UpdateCounters(workerId);
+
+ lock (Console.Out)
+ {
+ Console.ForegroundColor = ConsoleColor.DarkRed;
+ Console.WriteLine($"Worker #{workerId}: unhandled exception: {exn}");
+ Console.WriteLine();
+ Console.ResetColor();
+ }
+ }
+
+ private void UpdateCounters(int workerId)
+ {
+ // need to synchronize with GetCounterView to avoid reporting bad data
+ lock (_aggregateCounters)
+ {
+ _aggregateCounters[workerId].Append(_currentCounters[workerId]);
+ _currentCounters[workerId].Reset();
+ }
+ }
+
+ private (StreamCounter total, StreamCounter current)[] GetCounterView()
+ {
+ // generate a coherent view of counter state
+ lock (_aggregateCounters)
+ {
+ var view = new (StreamCounter total, StreamCounter current)[_aggregateCounters.Length];
+ for (int i = 0; i < _aggregateCounters.Length; i++)
+ {
+ StreamCounter current = _currentCounters[i].Clone();
+ StreamCounter total = _aggregateCounters[i].Clone().Append(current);
+ view[i] = (total, current);
+ }
+
+ return view;
+ }
+ }
+
+ public void PrintFailureTypes() => _errors.PrintFailureTypes();
+
+ public void PrintCurrentResults(TimeSpan elapsed, bool showAggregatesOnly)
+ {
+ (StreamCounter total, StreamCounter current)[] counters = GetCounterView();
+
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.Write($"[{DateTime.Now}]");
+ Console.ResetColor();
+ Console.WriteLine(" Elapsed: " + elapsed.ToString(@"hh\:mm\:ss"));
+ Console.ResetColor();
+
+ for (int i = 0; i < _currentCounters.Length; i++)
+ {
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.Write($"\tWorker #{i.ToString("N0")}:");
+ Console.ResetColor();
+
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.Write($"\tPass: ");
+ Console.ResetColor();
+ Console.Write(_successes[i].ToString("N0"));
+ Console.ForegroundColor = ConsoleColor.DarkYellow;
+ Console.Write("\tCancel: ");
+ Console.ResetColor();
+ Console.Write(_cancellations[i].ToString("N0"));
+ Console.ForegroundColor = ConsoleColor.DarkRed;
+ Console.Write("\tFail: ");
+ Console.ResetColor();
+ Console.Write(_failures[i].ToString("N0"));
+
+ if (!showAggregatesOnly)
+ {
+ Console.ForegroundColor = ConsoleColor.DarkBlue;
+ Console.Write($"\tCurr. Tx: ");
+ Console.ResetColor();
+ Console.Write(FmtBytes(counters[i].current.BytesWritten));
+ Console.ForegroundColor = ConsoleColor.DarkMagenta;
+ Console.Write($"\tCurr. Rx: ");
+ Console.ResetColor();
+ Console.Write(FmtBytes(counters[i].current.BytesRead));
+ }
+
+ Console.ForegroundColor = ConsoleColor.DarkBlue;
+ Console.Write($"\tTotal Tx: ");
+ Console.ResetColor();
+ Console.Write(FmtBytes(counters[i].total.BytesWritten));
+ Console.ForegroundColor = ConsoleColor.DarkMagenta;
+ Console.Write($"\tTotal Rx: ");
+ Console.ResetColor();
+ Console.Write(FmtBytes(counters[i].total.BytesRead));
+
+ Console.WriteLine();
+ }
+
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.Write("\tTOTAL : ");
+
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.Write($"\tPass: ");
+ Console.ResetColor();
+ Console.Write(_successes.Sum().ToString("N0"));
+ Console.ForegroundColor = ConsoleColor.DarkYellow;
+ Console.Write("\tCancel: ");
+ Console.ResetColor();
+ Console.Write(_cancellations.Sum().ToString("N0"));
+ Console.ForegroundColor = ConsoleColor.DarkRed;
+ Console.Write("\tFail: ");
+ Console.ResetColor();
+ Console.Write(_failures.Sum().ToString("N0"));
+
+ if (!showAggregatesOnly)
+ {
+ Console.ForegroundColor = ConsoleColor.DarkBlue;
+ Console.Write("\tCurr. Tx: ");
+ Console.ResetColor();
+ Console.Write(FmtBytes(counters.Select(c => c.current.BytesWritten).Sum()));
+ Console.ForegroundColor = ConsoleColor.DarkMagenta;
+ Console.Write($"\tCurr. Rx: ");
+ Console.ResetColor();
+ Console.Write(FmtBytes(counters.Select(c => c.current.BytesRead).Sum()));
+ }
+
+ Console.ForegroundColor = ConsoleColor.DarkBlue;
+ Console.Write("\tTotal Tx: ");
+ Console.ResetColor();
+ Console.Write(FmtBytes(counters.Select(c => c.total.BytesWritten).Sum()));
+ Console.ForegroundColor = ConsoleColor.DarkMagenta;
+ Console.Write($"\tTotal Rx: ");
+ Console.ResetColor();
+ Console.Write(FmtBytes(counters.Select(c => c.total.BytesRead).Sum()));
+
+ Console.WriteLine();
+ Console.WriteLine();
+
+ static string FmtBytes(long value) => HumanReadableByteSizeFormatter.Format(value);
+ }
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net.Security;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using SslStress.Utils;
+
+namespace SslStress
+{
+
+ public abstract partial class SslClientBase : IAsyncDisposable
+ {
+ protected readonly Configuration _config;
+
+ private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+ private readonly StressResultAggregator _aggregator;
+ private readonly Lazy<Task> _clientTask;
+ private readonly Stopwatch _stopwatch = new Stopwatch();
+
+ public SslClientBase(Configuration config)
+ {
+ if (config.MaxConnections < 1) throw new ArgumentOutOfRangeException(nameof(config.MaxConnections));
+
+ _config = config;
+ _aggregator = new StressResultAggregator(config.MaxConnections);
+ _clientTask = new Lazy<Task>(Task.Run(StartCore));
+ }
+
+ protected abstract Task HandleConnection(int workerId, long jobId, SslStream stream, TcpClient client, Random random, TimeSpan duration, CancellationToken token);
+
+ protected virtual async Task<SslStream> EstablishSslStream(Stream networkStream, Random random, CancellationToken token)
+ {
+ var sslStream = new SslStream(networkStream, leaveInnerStreamOpen: false);
+ var clientOptions = new SslClientAuthenticationOptions
+ {
+ ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http11 },
+ RemoteCertificateValidationCallback = ((x, y, z, w) => true),
+ TargetHost = SslServerBase.Hostname,
+ };
+
+ await sslStream.AuthenticateAsClientAsync(clientOptions, token);
+ return sslStream;
+ }
+
+ public ValueTask DisposeAsync() => StopAsync();
+
+ public void Start()
+ {
+ if (_cts.IsCancellationRequested) throw new ObjectDisposedException(nameof(SslClientBase));
+ _ = _clientTask.Value;
+ }
+
+ public async ValueTask StopAsync()
+ {
+ _cts.Cancel();
+ await _clientTask.Value;
+ }
+
+ public Task Task
+ {
+ get
+ {
+ if (!_clientTask.IsValueCreated) throw new InvalidOperationException("Client has not been started yet");
+ return _clientTask.Value;
+ }
+ }
+
+ public long TotalErrorCount => _aggregator.TotalFailures;
+
+ private Task StartCore()
+ {
+ _stopwatch.Start();
+
+ // Spin up a thread dedicated to outputting stats for each defined interval
+ new Thread(() =>
+ {
+ while (!_cts.IsCancellationRequested)
+ {
+ Thread.Sleep(_config.DisplayInterval);
+ lock (Console.Out) { _aggregator.PrintCurrentResults(_stopwatch.Elapsed, showAggregatesOnly: false); }
+ }
+ })
+ { IsBackground = true }.Start();
+
+ IEnumerable<Task> workers = CreateWorkerSeeds().Select(x => RunSingleWorker(x.workerId, x.random));
+ return Task.WhenAll(workers);
+
+ async Task RunSingleWorker(int workerId, Random random)
+ {
+ StreamCounter counter = _aggregator.GetCounters(workerId);
+
+ for (long jobId = 0; !_cts.IsCancellationRequested; jobId++)
+ {
+ TimeSpan connectionLifetime = _config.MinConnectionLifetime + random.NextDouble() * (_config.MaxConnectionLifetime - _config.MinConnectionLifetime);
+ TimeSpan cancellationDelay =
+ (random.NextBoolean(probability: _config.CancellationProbability)) ?
+ connectionLifetime * random.NextDouble() : // cancel in a random interval within the lifetime
+ connectionLifetime + TimeSpan.FromSeconds(10); // otherwise trigger cancellation 10 seconds after expected expiry
+
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
+ cts.CancelAfter(cancellationDelay);
+
+ bool isTestCompleted = false;
+ using var _ = cts.Token.Register(CheckForStalledConnection);
+
+ try
+ {
+ using var client = new TcpClient();
+ await client.ConnectAsync(_config.ServerEndpoint.Address, _config.ServerEndpoint.Port);
+ var stream = new CountingStream(client.GetStream(), counter);
+ using SslStream sslStream = await EstablishSslStream(stream, random, cts.Token);
+ await HandleConnection(workerId, jobId, sslStream, client, random, connectionLifetime, cts.Token);
+
+ _aggregator.RecordSuccess(workerId);
+ }
+ catch (OperationCanceledException) when (cts.IsCancellationRequested)
+ {
+ _aggregator.RecordCancellation(workerId);
+ }
+ catch (Exception e)
+ {
+ _aggregator.RecordFailure(workerId, e);
+ }
+ finally
+ {
+ isTestCompleted = true;
+ }
+
+ async void CheckForStalledConnection()
+ {
+ await Task.Delay(10_000);
+ if(!isTestCompleted)
+ {
+ lock (Console.Out)
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine($"Worker #{workerId} test #{jobId} has stalled, terminating the stress app.");
+ Console.WriteLine();
+ Console.ResetColor();
+ }
+ Environment.Exit(1);
+ }
+ }
+ }
+ }
+
+ IEnumerable<(int workerId, Random random)> CreateWorkerSeeds()
+ {
+ // deterministically generate random instance for each individual worker
+ Random random = new Random(_config.RandomSeed);
+ for (int workerId = 0; workerId < _config.MaxConnections; workerId++)
+ {
+ yield return (workerId, random.NextRandom());
+ }
+ }
+ }
+
+ public void PrintFinalReport()
+ {
+ lock (Console.Out)
+ {
+ Console.ForegroundColor = ConsoleColor.Magenta;
+ Console.WriteLine("SslStress Run Final Report");
+ Console.WriteLine();
+
+ _aggregator.PrintCurrentResults(_stopwatch.Elapsed, showAggregatesOnly: true);
+ _aggregator.PrintFailureTypes();
+ }
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Net.Security;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Threading;
+using System.Security.Cryptography.X509Certificates;
+using System.Security.Cryptography;
+using System.Runtime.InteropServices;
+
+namespace SslStress
+{
+ public abstract class SslServerBase : IAsyncDisposable
+ {
+ public const string Hostname = "contoso.com";
+
+ protected readonly Configuration _config;
+
+ private readonly X509Certificate2 _certificate;
+ private readonly TcpListener _listener;
+ private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+ private readonly SemaphoreSlim _acceptConnectionLatch = new SemaphoreSlim(initialCount: 1);
+ private readonly Lazy<Task> _serverTask;
+
+ public EndPoint ServerEndpoint => _listener.LocalEndpoint;
+
+ public SslServerBase(Configuration config)
+ {
+ if (config.MaxConnections < 1) throw new ArgumentOutOfRangeException(nameof(config.MaxConnections));
+
+ _config = config;
+ _certificate = CreateSelfSignedCertificate();
+ _listener = new TcpListener(config.ServerEndpoint);
+ _serverTask = new Lazy<Task>(Task.Run(StartCore));
+ }
+
+ protected abstract Task HandleConnection(SslStream sslStream, TcpClient client, CancellationToken token);
+
+ protected virtual async Task<SslStream> EstablishSslStream(Stream networkStream, CancellationToken token)
+ {
+ var sslStream = new SslStream(networkStream, leaveInnerStreamOpen: false);
+ var serverOptions = new SslServerAuthenticationOptions
+ {
+ ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http11, SslApplicationProtocol.Http2 },
+ ServerCertificate = _certificate,
+ };
+
+ await sslStream.AuthenticateAsServerAsync(serverOptions, token);
+ return sslStream;
+ }
+
+ public void Start()
+ {
+ if (_cts.IsCancellationRequested) throw new ObjectDisposedException(nameof(SslServerBase));
+ _ = _serverTask.Value;
+ }
+
+ public async ValueTask StopAsync()
+ {
+ _cts.Cancel();
+ await _serverTask.Value;
+ }
+
+ public Task Task
+ {
+ get
+ {
+ if (!_serverTask.IsValueCreated) throw new InvalidOperationException("Server has not been started yet");
+ return _serverTask.Value;
+ }
+ }
+
+ public ValueTask DisposeAsync() => StopAsync();
+
+ private async Task StartCore()
+ {
+ _listener.Start();
+ IEnumerable<Task> workers = Enumerable.Range(1, 2 * _config.MaxConnections).Select(_ => RunSingleWorker());
+ try
+ {
+ await Task.WhenAll(workers);
+ }
+ finally
+ {
+ _listener.Stop();
+ }
+
+ async Task RunSingleWorker()
+ {
+ while(!_cts.IsCancellationRequested)
+ {
+ try
+ {
+ using TcpClient client = await AcceptTcpClientAsync(_cts.Token);
+ using SslStream stream = await EstablishSslStream(client.GetStream(), _cts.Token);
+ await HandleConnection(stream, client, _cts.Token);
+ }
+ catch (OperationCanceledException) when (_cts.IsCancellationRequested)
+ {
+
+ }
+ catch (Exception e)
+ {
+ if (_config.LogServer)
+ {
+ lock (Console.Out)
+ {
+ Console.ForegroundColor = ConsoleColor.DarkRed;
+ Console.WriteLine($"Server: unhandled exception: {e}");
+ Console.WriteLine();
+ Console.ResetColor();
+ }
+ }
+ }
+ }
+ }
+
+ // workaround for TcpListener not accepting cancellation tokens
+ async Task<TcpClient> AcceptTcpClientAsync(CancellationToken token)
+ {
+ while (!token.IsCancellationRequested)
+ {
+ if (_listener.Pending())
+ {
+ // Need to ensure AcceptTcpClientAsync() returns immediately,
+ // so we synchronize here to avoid races between workers
+ await _acceptConnectionLatch.WaitAsync(token);
+ try
+ {
+ if (_listener.Pending())
+ return await _listener.AcceptTcpClientAsync();
+ }
+ finally
+ {
+ _acceptConnectionLatch.Release();
+ }
+ }
+
+ await Task.Delay(20);
+ }
+
+ token.ThrowIfCancellationRequested();
+ throw new Exception("internal error");
+ }
+ }
+
+ protected virtual X509Certificate2 CreateSelfSignedCertificate()
+ {
+ using var rsa = RSA.Create();
+ var certReq = new CertificateRequest($"CN={Hostname}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+ certReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
+ certReq.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
+ certReq.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
+ X509Certificate2 cert = certReq.CreateSelfSigned(DateTimeOffset.UtcNow.AddMonths(-1), DateTimeOffset.UtcNow.AddMonths(1));
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ cert = new X509Certificate2(cert.Export(X509ContentType.Pfx));
+ }
+
+ return cert;
+ }
+ }
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>netcoreapp3.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="..\..\..\..\Common\tests\System\IO\Compression\CRC.cs" Link="Utils\CRC.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19405.1" />
+ <PackageReference Include="System.IO.Pipelines" Version="4.6.0" />
+ </ItemGroup>
+</Project>
--- /dev/null
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27428.2002
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SslStress", "SslStress.csproj", "{802E12E4-7E4C-493D-B767-A69223AE7FB2}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {802E12E4-7E4C-493D-B767-A69223AE7FB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {802E12E4-7E4C-493D-B767-A69223AE7FB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {802E12E4-7E4C-493D-B767-A69223AE7FB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {802E12E4-7E4C-493D-B767-A69223AE7FB2}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {264DF521-D85C-41B0-B753-AB4C5C7505FB}
+ EndGlobalSection
+EndGlobal
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+// SslStream stress scenario
+//
+// * Client sends sequences of random data, accompanied with length and checksum information.
+// * Server echoes back the same data. Both client and server validate integrity of received data.
+// * Data is written using randomized combinations of the SslStream.Write* methods.
+// * Data is ingested using System.IO.Pipelines.
+
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net.Security;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using SslStress.Utils;
+
+namespace SslStress
+{
+ public struct DataSegment
+ {
+ private readonly byte[] _buffer;
+
+ public DataSegment(int length)
+ {
+ _buffer = ArrayPool<byte>.Shared.Rent(length);
+ Length = length;
+ }
+
+ public int Length { get; }
+ public Memory<byte> AsMemory() => new Memory<byte>(_buffer, 0, Length);
+ public Span<byte> AsSpan() => new Span<byte>(_buffer, 0, Length);
+
+ public ulong Checksum => CRC.CalculateCRC(AsSpan());
+ public void Return() => ArrayPool<byte>.Shared.Return(_buffer);
+
+ /// Create and populate a segment with random data
+ public static DataSegment CreateRandom(Random random, int maxLength)
+ {
+ int size = random.Next(0, maxLength);
+ var chunk = new DataSegment(size);
+ foreach (ref byte b in chunk.AsSpan())
+ {
+ b = s_bytePool[random.Next(255)];
+ }
+
+ return chunk;
+ }
+
+ private static readonly byte[] s_bytePool =
+ Enumerable
+ .Range(0, 256)
+ .Select(i => (byte)i)
+ .Where(b => b != (byte)'\n')
+ .ToArray();
+ }
+
+ public class DataMismatchException : Exception
+ {
+ public DataMismatchException(string message) : base(message) { }
+ }
+
+ // Serializes data segment using the following format: <length>,<checksum>,<data>
+ public class DataSegmentSerializer
+ {
+ private static readonly Encoding s_encoding = Encoding.ASCII;
+
+ private readonly byte[] _buffer = new byte[32];
+ private readonly char[] _charBuffer = new char[32];
+
+ public async Task SerializeAsync(Stream stream, DataSegment segment, Random? random = null, CancellationToken token = default)
+ {
+ // length
+ int numsize = s_encoding.GetBytes(segment.Length.ToString(), _buffer);
+ await stream.WriteAsync(_buffer.AsMemory().Slice(0, numsize), token);
+ stream.WriteByte((byte)',');
+ // checksum
+ numsize = s_encoding.GetBytes(segment.Checksum.ToString(), _buffer);
+ await stream.WriteAsync(_buffer.AsMemory().Slice(0, numsize), token);
+ stream.WriteByte((byte)',');
+ // payload
+ Memory<byte> source = segment.AsMemory();
+ // write the entire segment outright if not given random instance
+ if (random == null)
+ {
+ await stream.WriteAsync(source, token);
+ return;
+ }
+ // randomize chunking otherwise
+ while (source.Length > 0)
+ {
+ if (random.NextBoolean(probability: 0.05))
+ {
+ stream.WriteByte(source.Span[0]);
+ source = source.Slice(1);
+ }
+ else
+ {
+ // TODO consider non-uniform distribution for chunk sizes
+ int chunkSize = random.Next(source.Length);
+ Memory<byte> chunk = source.Slice(0, chunkSize);
+ source = source.Slice(chunkSize);
+
+ if (random.NextBoolean(probability: 0.9))
+ {
+ await stream.WriteAsync(chunk, token);
+ }
+ else
+ {
+ stream.Write(chunk.Span);
+ }
+ }
+
+ if (random.NextBoolean(probability: 0.3))
+ {
+ await stream.FlushAsync(token);
+ }
+ }
+ }
+
+ public DataSegment Deserialize(ReadOnlySequence<byte> buffer)
+ {
+ // length
+ SequencePosition? pos = buffer.PositionOf((byte)',');
+ if (pos == null)
+ {
+ throw new DataMismatchException("should contain comma-separated values");
+ }
+
+ ReadOnlySequence<byte> lengthBytes = buffer.Slice(0, pos.Value);
+ int numSize = s_encoding.GetChars(lengthBytes.ToArray(), _charBuffer);
+ int length = int.Parse(_charBuffer.AsSpan().Slice(0, numSize));
+ buffer = buffer.Slice(buffer.GetPosition(1, pos.Value));
+
+ // checksum
+ pos = buffer.PositionOf((byte)',');
+ if (pos == null)
+ {
+ throw new DataMismatchException("should contain comma-separated values");
+ }
+
+ ReadOnlySequence<byte> checksumBytes = buffer.Slice(0, pos.Value);
+ numSize = s_encoding.GetChars(checksumBytes.ToArray(), _charBuffer);
+ ulong checksum = ulong.Parse(_charBuffer.AsSpan().Slice(0, numSize));
+ buffer = buffer.Slice(buffer.GetPosition(1, pos.Value));
+
+ // payload
+ if (length != (int)buffer.Length)
+ {
+ throw new DataMismatchException("declared length does not match payload length");
+ }
+
+ var chunk = new DataSegment((int)buffer.Length);
+ buffer.CopyTo(chunk.AsSpan());
+
+ if (checksum != chunk.Checksum)
+ {
+ chunk.Return();
+ throw new DataMismatchException("declared checksum doesn't match payload checksum");
+ }
+
+ return chunk;
+ }
+ }
+
+ // Client implementation:
+ //
+ // Sends randomly generated data segments and validates data echoed back by the server.
+ // Applies backpressure if the difference between sent and received segments is too large.
+ public sealed class StressClient : SslClientBase
+ {
+ public StressClient(Configuration config) : base(config) { }
+
+ protected override async Task HandleConnection(int workerId, long jobId, SslStream stream, TcpClient client, Random random, TimeSpan duration, CancellationToken token)
+ {
+ // token used for signalling cooperative cancellation; do not pass this to SslStream methods
+ using var connectionLifetimeToken = new CancellationTokenSource(duration);
+
+ long messagesInFlight = 0;
+ DateTime lastWrite = DateTime.Now;
+ DateTime lastRead = DateTime.Now;
+
+ await StressTaskExtensions.WhenAllThrowOnFirstException(token, Sender, Receiver, Monitor);
+
+ async Task Sender(CancellationToken token)
+ {
+ var serializer = new DataSegmentSerializer();
+
+ while (!token.IsCancellationRequested && !connectionLifetimeToken.IsCancellationRequested)
+ {
+ await ApplyBackpressure();
+
+ DataSegment chunk = DataSegment.CreateRandom(random, _config.MaxBufferLength);
+ try
+ {
+ await serializer.SerializeAsync(stream, chunk, random, token);
+ stream.WriteByte((byte)'\n');
+ await stream.FlushAsync(token);
+ Interlocked.Increment(ref messagesInFlight);
+ lastWrite = DateTime.Now;
+ }
+ finally
+ {
+ chunk.Return();
+ }
+ }
+
+ // write an empty line to signal completion to the server
+ stream.WriteByte((byte)'\n');
+ await stream.FlushAsync(token);
+
+ /// Polls until number of in-flight messages falls below threshold
+ async Task ApplyBackpressure()
+ {
+ if (Volatile.Read(ref messagesInFlight) > 5000)
+ {
+ Stopwatch stopwatch = Stopwatch.StartNew();
+ bool isLogged = false;
+
+ while (!token.IsCancellationRequested && !connectionLifetimeToken.IsCancellationRequested && Volatile.Read(ref messagesInFlight) > 2000)
+ {
+ // only log if tx has been suspended for a while
+ if (!isLogged && stopwatch.ElapsedMilliseconds >= 1000)
+ {
+ isLogged = true;
+ lock (Console.Out)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"worker #{workerId}: applying backpressure");
+ Console.WriteLine();
+ Console.ResetColor();
+ }
+ }
+
+ await Task.Delay(20);
+ }
+
+ if(isLogged)
+ {
+ Console.WriteLine($"worker #{workerId}: resumed tx after {stopwatch.Elapsed}");
+ }
+ }
+ }
+ }
+
+ async Task Receiver(CancellationToken token)
+ {
+ var serializer = new DataSegmentSerializer();
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
+ await stream.ReadLinesUsingPipesAsync(Callback, cts.Token, separator: '\n');
+
+ Task Callback(ReadOnlySequence<byte> buffer)
+ {
+ if (buffer.Length == 0 && connectionLifetimeToken.IsCancellationRequested)
+ {
+ // server echoed back empty buffer sent by client,
+ // signal cancellation and complete the connection.
+ cts.Cancel();
+ return Task.CompletedTask;
+ }
+
+ // deserialize to validate the checksum, then discard
+ DataSegment chunk = serializer.Deserialize(buffer);
+ chunk.Return();
+ Interlocked.Decrement(ref messagesInFlight);
+ lastRead = DateTime.Now;
+ return Task.CompletedTask;
+ }
+ }
+
+ async Task Monitor(CancellationToken token)
+ {
+ do
+ {
+ await Task.Delay(500);
+
+ if((DateTime.Now - lastWrite) >= TimeSpan.FromSeconds(10))
+ {
+ throw new Exception($"worker #{workerId} job #{jobId} has stopped writing bytes to server");
+ }
+
+ if((DateTime.Now - lastRead) >= TimeSpan.FromSeconds(10))
+ {
+ throw new Exception($"worker #{workerId} job #{jobId} has stopped receiving bytes from server");
+ }
+ }
+ while(!token.IsCancellationRequested && !connectionLifetimeToken.IsCancellationRequested);
+ }
+ }
+ }
+
+ // Server implementation:
+ //
+ // Sets up a pipeline reader which validates checksums and echoes back data.
+ public sealed class StressServer : SslServerBase
+ {
+ public StressServer(Configuration config) : base(config) { }
+
+ protected override async Task HandleConnection(SslStream sslStream, TcpClient client, CancellationToken token)
+ {
+ using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token);
+ DateTime lastReadTime = DateTime.Now;
+
+ var serializer = new DataSegmentSerializer();
+
+ _ = Task.Run(Monitor);
+ await sslStream.ReadLinesUsingPipesAsync(Callback, cts.Token, separator: '\n');
+
+ async Task Callback(ReadOnlySequence<byte> buffer)
+ {
+ lastReadTime = DateTime.Now;
+
+ if (buffer.Length == 0)
+ {
+ // got an empty line, client is closing the connection
+ // echo back the empty line and tear down.
+ sslStream.WriteByte((byte)'\n');
+ await sslStream.FlushAsync(token);
+ cts.Cancel();
+ return;
+ }
+
+ DataSegment? chunk = null;
+ try
+ {
+ chunk = serializer.Deserialize(buffer);
+ await serializer.SerializeAsync(sslStream, chunk.Value, token: token);
+ sslStream.WriteByte((byte)'\n');
+ await sslStream.FlushAsync(token);
+ }
+ catch (DataMismatchException e)
+ {
+ if (_config.LogServer)
+ {
+ lock (Console.Out)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"Server: {e.Message}");
+ Console.ResetColor();
+ }
+ }
+ }
+ finally
+ {
+ chunk?.Return();
+ }
+ }
+
+ async Task Monitor()
+ {
+ do
+ {
+ await Task.Delay(1000);
+
+ if (DateTime.Now - lastReadTime >= TimeSpan.FromSeconds(10))
+ {
+ cts.Cancel();
+ }
+
+ } while (!cts.IsCancellationRequested);
+ }
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SslStress.Utils
+{
+ public class StreamCounter
+ {
+ public long BytesWritten = 0L;
+ public long BytesRead = 0L;
+
+ public void Reset()
+ {
+ BytesWritten = 0L;
+ BytesRead = 0L;
+ }
+
+ public StreamCounter Append(StreamCounter that)
+ {
+ BytesRead += that.BytesRead;
+ BytesWritten += that.BytesWritten;
+ return this;
+ }
+
+ public StreamCounter Clone() => new StreamCounter() { BytesRead = BytesRead, BytesWritten = BytesWritten };
+ }
+
+ public class CountingStream : Stream
+ {
+ private readonly Stream _stream;
+ private readonly StreamCounter _counter;
+
+ public CountingStream(Stream stream, StreamCounter counters)
+ {
+ _stream = stream;
+ _counter = counters;
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ _stream.Write(buffer, offset, count);
+ Interlocked.Add(ref _counter.BytesWritten, count);
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ int read = _stream.Read(buffer, offset, count);
+ Interlocked.Add(ref _counter.BytesRead, read);
+ return read;
+ }
+
+ public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ await _stream.WriteAsync(buffer, cancellationToken);
+ Interlocked.Add(ref _counter.BytesWritten, buffer.Length);
+ }
+
+ public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ int read = await _stream.ReadAsync(buffer, cancellationToken);
+ Interlocked.Add(ref _counter.BytesRead, read);
+ return read;
+ }
+
+ public override void WriteByte(byte value)
+ {
+ _stream.WriteByte(value);
+ Interlocked.Increment(ref _counter.BytesRead);
+ }
+
+ // route everything else to the inner stream
+
+ public override bool CanRead => _stream.CanRead;
+
+ public override bool CanSeek => _stream.CanSeek;
+
+ public override bool CanWrite => _stream.CanWrite;
+
+ public override long Length => _stream.Length;
+
+ public override long Position { get => _stream.Position; set => _stream.Position = value; }
+
+ public override void Flush() => _stream.Flush();
+
+ public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin);
+
+ public override void SetLength(long value) => _stream.SetLength(value);
+
+ public override void Close() => _stream.Close();
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace SslStress.Utils
+{
+ public interface IErrorType
+ {
+ string ErrorMessage { get; }
+
+ IReadOnlyCollection<(DateTime timestamp, string? metadata)> Occurrences { get; }
+ }
+
+ public sealed class ErrorAggregator
+ {
+ private readonly ConcurrentDictionary<(Type exception, string message, string callSite)[], ErrorType> _failureTypes;
+
+ public ErrorAggregator()
+ {
+ _failureTypes = new ConcurrentDictionary<(Type, string, string)[], ErrorType>(new StructuralEqualityComparer<(Type, string, string)[]>());
+ }
+
+ public int TotalErrorTypes => _failureTypes.Count;
+ public IReadOnlyCollection<IErrorType> ErrorTypes => ErrorTypes.ToArray();
+ public long TotalErrorCount => _failureTypes.Values.Select(c => (long)c.Occurrences.Count).Sum();
+
+ public void RecordError(Exception exception, string? metadata = null, DateTime? timestamp = null)
+ {
+ timestamp ??= DateTime.Now;
+
+ (Type, string, string)[] key = ClassifyFailure(exception);
+
+ ErrorType failureType = _failureTypes.GetOrAdd(key, _ => new ErrorType(exception.ToString()));
+ failureType.OccurencesQueue.Enqueue((timestamp.Value, metadata));
+
+ // classify exception according to type, message and callsite of itself and any inner exceptions
+ static (Type exception, string message, string callSite)[] ClassifyFailure(Exception exn)
+ {
+ var acc = new List<(Type exception, string message, string callSite)>();
+
+ for (Exception? e = exn; e != null;)
+ {
+ acc.Add((e.GetType(), e.Message ?? "", new StackTrace(e, true).GetFrame(0)?.ToString() ?? ""));
+ e = e.InnerException;
+ }
+
+ return acc.ToArray();
+ }
+ }
+
+ public void PrintFailureTypes()
+ {
+ if (_failureTypes.Count == 0)
+ return;
+
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine($"There were a total of {TotalErrorCount} failures classified into {TotalErrorTypes} different types:");
+ Console.WriteLine();
+ Console.ResetColor();
+
+ int i = 0;
+ foreach (ErrorType failure in _failureTypes.Values.OrderByDescending(x => x.Occurrences.Count))
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"Failure Type {++i}/{_failureTypes.Count}:");
+ Console.ResetColor();
+ Console.WriteLine(failure.ErrorMessage);
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ foreach (IGrouping<string?, (DateTime timestamp, string? metadata)> grouping in failure.Occurrences.GroupBy(o => o.metadata))
+ {
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.Write($"\t{(grouping.Key ?? "").PadRight(30)}");
+ Console.ResetColor();
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.Write("Fail: ");
+ Console.ResetColor();
+ Console.Write(grouping.Count());
+ Console.WriteLine($"\tTimestamps: {string.Join(", ", grouping.Select(x => x.timestamp.ToString("HH:mm:ss")))}");
+ }
+
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.Write("\t TOTAL".PadRight(31));
+ Console.ResetColor();
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.Write($"Fail: ");
+ Console.ResetColor();
+ Console.WriteLine(TotalErrorTypes);
+ Console.WriteLine();
+ }
+ }
+
+ /// <summary>Aggregate view of a particular stress failure type</summary>
+ private sealed class ErrorType : IErrorType
+ {
+ public string ErrorMessage { get; }
+ public ConcurrentQueue<(DateTime, string?)> OccurencesQueue = new ConcurrentQueue<(DateTime, string?)>();
+
+ public ErrorType(string errorText)
+ {
+ ErrorMessage = errorText;
+ }
+
+ public IReadOnlyCollection<(DateTime timestamp, string? metadata)> Occurrences => OccurencesQueue;
+ }
+
+ private class StructuralEqualityComparer<T> : IEqualityComparer<T> where T : IStructuralEquatable
+ {
+ public bool Equals(T left, T right) => left.Equals(right, StructuralComparisons.StructuralEqualityComparer);
+ public int GetHashCode(T value) => value.GetHashCode(StructuralComparisons.StructuralEqualityComparer);
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace SslStress.Utils
+{
+ public static class HumanReadableByteSizeFormatter
+ {
+ private static readonly string[] s_suffixes = { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB" };
+
+ public static string Format(long byteCount)
+ {
+ // adapted from https://stackoverflow.com/a/4975942
+ if (byteCount == 0)
+ {
+ return $"0{s_suffixes[0]}";
+ }
+
+ int position = (int)Math.Floor(Math.Log(Math.Abs(byteCount), 1024));
+ double renderedValue = byteCount / Math.Pow(1024, position);
+ return $"{renderedValue:0.#}{s_suffixes[position]}";
+ }
+ }
+}
--- /dev/null
+using System;
+
+namespace SslStress.Utils
+{
+ public static class MiscHelpers
+ {
+ // help transform `(foo != null) ? Bar(foo) : null` expressions into `foo?.Select(Bar)`
+ public static S Pipe<T, S>(this T value, Func<T, S> mapper) => mapper(value);
+ public static void Pipe<T>(this T value, Action<T> body) => body(value);
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+using System.IO;
+using System.IO.Pipelines;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SslStress.Utils
+{
+ public static class PipeExtensions
+ {
+ // Adapted from https://devblogs.microsoft.com/dotnet/system-io-pipelines-high-performance-io-in-net/
+ public static async Task ReadLinesUsingPipesAsync(this Stream stream, Func<ReadOnlySequence<byte>, Task> callback, CancellationToken token = default, char separator = '\n')
+ {
+ var pipe = new Pipe();
+
+ try
+ {
+ await StressTaskExtensions.WhenAllThrowOnFirstException(token, FillPipeAsync, ReadPipeAsync);
+ }
+ catch (OperationCanceledException) when (token.IsCancellationRequested)
+ {
+
+ }
+
+ async Task FillPipeAsync(CancellationToken token)
+ {
+ try
+ {
+ await stream.CopyToAsync(pipe.Writer, token);
+ }
+ catch (Exception e)
+ {
+ pipe.Writer.Complete(e);
+ throw;
+ }
+
+ pipe.Writer.Complete();
+ }
+
+ async Task ReadPipeAsync(CancellationToken token)
+ {
+ while (!token.IsCancellationRequested)
+ {
+ ReadResult result = await pipe.Reader.ReadAsync(token);
+ ReadOnlySequence<byte> buffer = result.Buffer;
+ SequencePosition? position;
+
+ do
+ {
+ position = buffer.PositionOf((byte)separator);
+
+ if (position != null)
+ {
+ await callback(buffer.Slice(0, position.Value));
+ buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
+ }
+ }
+ while (position != null);
+
+ pipe.Reader.AdvanceTo(buffer.Start, buffer.End);
+
+ if (result.IsCompleted)
+ {
+ break;
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace SslStress.Utils
+{
+ public static class RandomHelpers
+ {
+ public static Random NextRandom(this Random random) => new Random(Seed: random.Next());
+
+ public static bool NextBoolean(this Random random, double probability = 0.5)
+ {
+ if (probability < 0 || probability > 1)
+ throw new ArgumentOutOfRangeException(nameof(probability));
+
+ return random.NextDouble() < probability;
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SslStress.Utils
+{
+ public static class StressTaskExtensions
+ {
+
+ /// <summary>
+ /// Starts and awaits a collection of cancellable tasks.
+ /// Will surface the first exception that has occured (instead of AggregateException)
+ /// and trigger cancellation for all sibling tasks.
+ /// </summary>
+ /// <param name="token"></param>
+ /// <param name="tasks"></param>
+ /// <returns></returns>
+ public static async Task WhenAllThrowOnFirstException(CancellationToken token, params Func<CancellationToken, Task>[] tasks)
+ {
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
+ Exception? firstException = null;
+
+ await Task.WhenAll(tasks.Select(RunOne));
+
+ if (firstException != null)
+ {
+ ExceptionDispatchInfo.Capture(firstException).Throw();
+ }
+
+ async Task RunOne(Func<CancellationToken, Task> task)
+ {
+ try
+ {
+ await Task.Run(() => task(cts.Token));
+ }
+ catch (Exception e)
+ {
+ if (Interlocked.CompareExchange(ref firstException, e, null) == null)
+ {
+ cts.Cancel();
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+version: '3'
+services:
+ client:
+ image: ${SSLSTRESS_IMAGE:-sslstress}
+ links:
+ - server
+ environment:
+ - SSLSTRESS_ARGS=--mode client --server-endpoint server:5001 ${SSLSTRESS_CLIENT_ARGS}
+ server:
+ image: ${SSLSTRESS_IMAGE:-sslstress}
+ ports:
+ - "5001:5001"
+ environment:
+ - SSLSTRESS_ARGS=--mode server --server-endpoint 0.0.0.0:5001 ${SSLSTRESS_SERVER_ARGS}
--- /dev/null
+# escape=`
+ARG SDK_BASE_IMAGE=mcr.microsoft.com/dotnet/core/sdk:3.0.100-nanoserver-1809
+FROM $SDK_BASE_IMAGE
+
+# Use powershell as the default shell
+SHELL ["pwsh", "-Command"]
+
+WORKDIR /app
+COPY . .
+WORKDIR /app/System.Net.Security/tests/StressTests/SslStress
+
+ARG CONFIGURATION=Release
+RUN dotnet build -c $env:CONFIGURATION
+
+EXPOSE 5001
+
+ENV CONFIGURATION=$CONFIGURATION
+ENV SSLSTRESS_ARGS=""
+CMD dotnet run --no-build -c $env:CONFIGURATION -- $env:SSLSTRESS_ARGS.Split()