Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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 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; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Project/>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Project/>
14 changes: 14 additions & 0 deletions src/System.Net.Security/tests/StressTests/SslStress/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ARG SDK_BASE_IMAGE=mcr.microsoft.com/dotnet/core/sdk:3.0.100-buster
FROM $SDK_BASE_IMAGE

WORKDIR /app
COPY . .

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
159 changes: 159 additions & 0 deletions src/System.Net.Security/tests/StressTests/SslStress/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// 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.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();

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();
if (server != null) await server.StopAsync();
}
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[] { "--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>("minutes", 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>("minutes", 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"),
ServerEndpoint = IPEndPoint.Parse(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);
}
}
}
}
3 changes: 3 additions & 0 deletions src/System.Net.Security/tests/StressTests/SslStress/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## SslStress

Stress testing suite for SslStream
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// 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;
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];
}

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 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.DarkRed;
Console.Write("\tFail: ");
Console.ResetColor();
Console.Write(_failures[i].ToString("N0"));

if (!showAggregatesOnly)
{
Console.ForegroundColor = ConsoleColor.DarkBlue;
Console.Write($"\tTx: ");
Console.ResetColor();
Console.Write(FmtBytes(counters[i].current.BytesWritten));
Console.ForegroundColor = ConsoleColor.DarkMagenta;
Console.Write($"\tRx: ");
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.DarkRed;
Console.Write("\tFail: ");
Console.ResetColor();
Console.Write(_failures.Sum().ToString("N0"));

if (!showAggregatesOnly)
{
Console.ForegroundColor = ConsoleColor.DarkBlue;
Console.Write("\tTx: ");
Console.ResetColor();
Console.Write(FmtBytes(counters.Select(c => c.current.BytesWritten).Sum()));
Console.ForegroundColor = ConsoleColor.DarkMagenta;
Console.Write($"\tRx: ");
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);
}
}
}
}
Loading