Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,4 @@ paket-files/
.sonarlint

# Auto-generated test configuration (produced from .runsettings at build time)
Tests/SocketTests/TestConfiguration.cs
Tests/**/TestConfiguration.cs
2 changes: 1 addition & 1 deletion .runsettings
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<!-- Configurations that affect the Test Framework -->
<RunConfiguration>
<ResultsDirectory>.\TestResults</ResultsDirectory><!-- Path relative to solution directory -->
<TestSessionTimeout>60000</TestSessionTimeout><!-- Milliseconds -->
<TestSessionTimeout>90000</TestSessionTimeout><!-- Milliseconds -->
<TargetFrameworkVersion>net48</TargetFrameworkVersion>
<TargetPlatform>x64</TargetPlatform>
</RunConfiguration>
Expand Down
2 changes: 1 addition & 1 deletion Tests/IPAddressTests/IPAddressTests.nfproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@
<ProjectConfigurationsDeclaredAsItems />
</ProjectCapabilities>
</ProjectExtensions>
</Project>
</Project>
2 changes: 1 addition & 1 deletion Tests/NetworkHelperTests/NetworkHelperTests.nfproj
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,4 @@
<ProjectConfigurationsDeclaredAsItems />
</ProjectCapabilities>
</ProjectExtensions>
</Project>
</Project>
181 changes: 181 additions & 0 deletions Tests/NetworkTestCompanion/CommandServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
Expand All @@ -20,6 +22,9 @@ namespace NetworkTestCompanion;
/// { "cmd": "stop", "port": N }
/// { "cmd": "stop_all" }
/// { "cmd": "connect_to", "host": "...", "port": N }
/// { "cmd": "start_tls_echo", "port": N }
/// { "cmd": "tls_connect_to", "host": "...", "port": N }
/// { "cmd": "tls_connect_echo", "host": "...", "port": N, "data": "base64..." }
/// </summary>
internal sealed class CommandServer : IDisposable
{
Expand Down Expand Up @@ -112,6 +117,9 @@ private string ProcessCommand(string json)
"stop" => Stop(node),
"stop_all" => StopAll(),
"connect_to" => ConnectTo(node),
"start_tls_echo" => StartTlsEcho(node),
"tls_connect_to" => TlsConnectTo(node),
"tls_connect_echo" => TlsConnectEcho(node),
_ => Error($"unknown command: {cmd}")
};
}
Expand Down Expand Up @@ -237,6 +245,179 @@ private string ConnectTo(JsonNode node)
}
}

private string StartTlsEcho(JsonNode node)
{
if (!TryGetPort(node, out var port, out var err)) return err!;

lock (_lock)
{
// Replace any stale server left registered on this port by a previous
// (possibly aborted) test run — the companion is long-lived across runs.
if (_activeServers.TryGetValue(port, out var existing))
{
existing.Dispose();
_activeServers.Remove(port);
Console.WriteLine($"[CMD] Replaced stale server on port {port}");
}

var server = new TlsEchoServer(_bindAddress, port, TestCertificates.ServerCert);
try
{
server.Start();
}
catch (Exception ex)
{
server.Dispose();
return Error(ex.Message);
}

_activeServers[port] = server;
}

Console.WriteLine($"[CMD] TLS echo started on port {port}");
return Ok();
}

private string TlsConnectTo(JsonNode node)
{
var host = node["host"]?.GetValue<string>();
if (string.IsNullOrEmpty(host)) return Error("missing 'host'");
if (!TryGetPort(node, out var port, out var err)) return err!;

TcpClient? connectClient = null;
try
{
connectClient = new TcpClient();
if (!connectClient.ConnectAsync(host, port).Wait(TimeSpan.FromSeconds(5)))
{
connectClient.Dispose();
return Error($"connect to {host}:{port} timed out");
}

Console.WriteLine($"[CMD] tls_connect_to {host}:{port} TCP connected, starting TLS handshake in background");

// TLS handshake + keep-alive runs in background so the device
// can call Accept() and AuthenticateAsServer() after getting Ok.
var clientForBg = connectClient;
_ = Task.Run(async () =>
{
try
{
var sslStream = new SslStream(
clientForBg.GetStream(),
leaveInnerStreamOpen: false,
(_, _, _, _) => true);

await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
{
TargetHost = host,
EnabledSslProtocols = SslProtocols.Tls12
});

Console.WriteLine($"[CMD] tls_connect_to {host}:{port} TLS handshake succeeded");

await Task.Delay(2000);
sslStream.Dispose();
clientForBg.Dispose();
}
catch (Exception ex)
{
Console.Error.WriteLine($"[CMD] tls_connect_to {host}:{port} TLS failed: {ex.Message}");
clientForBg.Dispose();
}
});
Comment thread
josesimoes marked this conversation as resolved.

return Ok();
}
catch (Exception ex)
{
connectClient?.Dispose();
Console.Error.WriteLine($"[CMD] tls_connect_to {host}:{port} failed: {ex.Message}");
return Error(ex.Message);
}
}

private string TlsConnectEcho(JsonNode node)
{
var host = node["host"]?.GetValue<string>();
if (string.IsNullOrEmpty(host)) return Error("missing 'host'");
if (!TryGetPort(node, out var port, out var err)) return err!;
var dataB64 = node["data"]?.GetValue<string>();
if (string.IsNullOrEmpty(dataB64)) return Error("missing 'data'");

byte[] dataToSend;
try { dataToSend = Convert.FromBase64String(dataB64); }
catch { return Error("'data' is not valid base64"); }

TcpClient? connectClient = null;
try
{
connectClient = new TcpClient();
if (!connectClient.ConnectAsync(host, port).Wait(TimeSpan.FromSeconds(5)))
{
connectClient.Dispose();
return Error($"connect to {host}:{port} timed out");
}

Console.WriteLine($"[CMD] tls_connect_echo {host}:{port} TCP connected, starting TLS + echo in background");

// TLS handshake + echo runs in background so the device
// can call Accept() and AuthenticateAsServer() after getting Ok.
var clientForBg = connectClient;
_ = Task.Run(async () =>
{
try
{
var sslStream = new SslStream(
clientForBg.GetStream(),
leaveInnerStreamOpen: false,
(_, _, _, _) => true);

await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
{
TargetHost = host,
EnabledSslProtocols = SslProtocols.Tls12
});

await sslStream.WriteAsync(dataToSend);
await sslStream.FlushAsync();

Console.WriteLine($"[CMD] tls_connect_echo {host}:{port}: sent {dataToSend.Length} bytes, waiting for echo");

var buf = new byte[4096];
int totalRead = 0;
using var ms = new MemoryStream();

while (totalRead < dataToSend.Length)
{
int read = await sslStream.ReadAsync(buf);
if (read == 0) break;
ms.Write(buf, 0, read);
totalRead += read;
}

Console.WriteLine($"[CMD] tls_connect_echo {host}:{port}: received {totalRead} bytes echo");

sslStream.Dispose();
clientForBg.Dispose();
}
catch (Exception ex)
{
Console.Error.WriteLine($"[CMD] tls_connect_echo {host}:{port} TLS/echo failed: {ex.Message}");
clientForBg.Dispose();
}
});

return Ok();
}
catch (Exception ex)
{
connectClient?.Dispose();
Console.Error.WriteLine($"[CMD] tls_connect_echo {host}:{port} failed: {ex.Message}");
return Error(ex.Message);
}
}

private static bool TryGetPort(JsonNode node, out int port, out string? error)
{
port = 0;
Expand Down
2 changes: 1 addition & 1 deletion Tests/NetworkTestCompanion/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using NetworkTestCompanion;

const int DefaultControlPort = 11000;
int[] WellKnownTcpPorts = [DefaultControlPort, 7, 8, 9, 10, 80, 8080];
int[] WellKnownTcpPorts = [DefaultControlPort, 7, 8, 9, 10, 80, 8080, 7010, 7011, 7012, 7013, 7014, 7015];
int[] WellKnownUdpPorts = [7, 8, 9];

// Argument parsing
Expand Down
93 changes: 93 additions & 0 deletions Tests/NetworkTestCompanion/TestCertificates.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) .NET Foundation and Contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Security.Cryptography.X509Certificates;

namespace NetworkTestCompanion;

/// <summary>
/// Provides the static test certificate used by the companion's TLS echo server.
///
/// This is a self-signed RSA-2048 test certificate (CN=nanoFramework Test Server).
/// The device connects to the companion's TLS echo server with certificate
/// verification disabled, so the certificate only needs to be valid enough for
/// the Windows SChannel server side to complete a handshake.
///
/// The SAME certificate and key are embedded on the device side (SslServerTests.cs)
/// for the reverse scenario (device acting as TLS server). Not a secret — a
/// throw-away test certificate.
/// </summary>
internal static class TestCertificates
{
private static readonly Lazy<X509Certificate2> _serverCert = new(LoadServerCertificate);

internal static X509Certificate2 ServerCert => _serverCert.Value;

private static X509Certificate2 LoadServerCertificate()
{
// Load cert + key from PEM, then re-import via PFX so Windows SChannel
// can use the private key (SChannel rejects the ephemeral key handle
// that CreateFromPem produces).
using var fromPem = X509Certificate2.CreateFromPem(ServerCertPem, ServerKeyPem);
var pfxBytes = fromPem.Export(X509ContentType.Pfx);
var cert = X509CertificateLoader.LoadPkcs12(pfxBytes, null, X509KeyStorageFlags.Exportable);

Console.WriteLine("[CERTS] Loaded static test server certificate");
return cert;
}

// Self-signed RSA-2048 test certificate, CN=nanoFramework Test Server.
// Valid 2026..2036. Throw-away test cert — not a secret.
internal const string ServerCertPem =
@"-----BEGIN CERTIFICATE-----
MIIDRTCCAi2gAwIBAgIUepRBLWtpFLvLv6rjIIUQxmkVkYkwDQYJKoZIhvcNAQEL
BQAwJDEiMCAGA1UEAwwZbmFub0ZyYW1ld29yayBUZXN0IFNlcnZlcjAeFw0yNjA3
MTYxMjA0MDJaFw0zNjA3MTMxMjA0MDJaMCQxIjAgBgNVBAMMGW5hbm9GcmFtZXdv
cmsgVGVzdCBTZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCu
+yM+X9ZcaawdwfhpJiWa4qlrA/1aV0CoENchMP6XOr4Eq7h/Y8jH+QlKdG2hFe31
wULiwLJq6QwTQ23a7vRFBgTZCZJSs5QY54o2r7O6pO37Y1w+/d0/4blFLNWd0PQq
Mm8TUKdK3J11dv+n/oY9++4vFHR6Bo3xjHFBvm03vcKETeF3UIX+g6J84lfNmdPs
A3UIFqkWXioC7a2+afnRczAHrrS0Py2KcSv+G5E94ZYQHs0VljY8CpOEV2maxh9S
Bjocv4o6HUejKoWvbXqkftuxztjYx77p++jhICpnNZjpNOb27rJhtGw3HPwtn8IY
I3jIZS72insBEQgKSxBhAgMBAAGjbzBtMB0GA1UdDgQWBBR7BiZTDR7gx4gl2fV1
Y9fjrb1mMjAfBgNVHSMEGDAWgBR7BiZTDR7gx4gl2fV1Y9fjrb1mMjAJBgNVHRME
AjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B
AQsFAAOCAQEAIeCjTx10sJiacjzajLUJ/dfgSq8VfMUhHQLxG5VF3bPMVbr8LaF+
sM77OKXOBKigDj65urMUwAoLKXF7UcewvVV239Og1p97acetDDiM3Q72duH8MFAF
K2V9qR6Cj8/jvQdSHWiJVOHE0u31PG6Xwa6aIwXA79VKbJaucvj9hXYz0O3HXTRc
RmvHemTge0p0VhuTL+wNv46mftEZhoSsPZa4S08nv5VX3EyWfQM6eX3ghnq6CsE+
MQw94r5CV1kZDA/R9IdXh4aRIVCyN0ZmMvfNNrmIJRdr/eLzQc/6DBeh5Wrg7Sc/
9oUAfyIiPqXG3sW0Txof2L5qbTmallrooQ==
-----END CERTIFICATE-----";

// PKCS#1 (traditional RSA) encoding — kept identical to the device-side key
// (SslServerTests.cs), which requires this format for mbedTLS SecureServerInit.
internal const string ServerKeyPem =
@"-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEArvsjPl/WXGmsHcH4aSYlmuKpawP9WldAqBDXITD+lzq+BKu4
f2PIx/kJSnRtoRXt9cFC4sCyaukME0Nt2u70RQYE2QmSUrOUGOeKNq+zuqTt+2Nc
Pv3dP+G5RSzVndD0KjJvE1CnStyddXb/p/6GPfvuLxR0egaN8YxxQb5tN73ChE3h
d1CF/oOifOJXzZnT7AN1CBapFl4qAu2tvmn50XMwB660tD8tinEr/huRPeGWEB7N
FZY2PAqThFdpmsYfUgY6HL+KOh1HoyqFr216pH7bsc7Y2Me+6fvo4SAqZzWY6TTm
9u6yYbRsNxz8LZ/CGCN4yGUu9op7AREICksQYQIDAQABAoIBAB8L2Bj9EB+dcDhn
bhfZ+NoeVUjzkEQzLvmi40i0VLeoaIaToUyY+8rfWNKpDbqDFZGBFMj+v6lQaCAS
2q75rsWAZ+PKWvfpfOFeU5uYWR9InCD6ZCeZC2SGPEUVy2EQ7gF+qU6YBNa3hgiN
cJbyBgeBZ6Vaz7/G4fB1prKvgtlcunjtXAwdme9nkHR2kuG+pGGtNs/qc71bQeOt
5gphHdls+lHX+D6QD/gB2biR1bSJW+Cegz0zNM0nUUz6cA3K9dUTMeyGyI4c8FNt
u/FuRZlri/I7yUAPngVnmq46rJg1Ih0OvXI+jIEHp7SI0e9MiRLnZhB4x3/76940
qAJ+nnECgYEA5692QqZ0eovLlqlWSrRNP0mG31HxCTrWYvfvfWkfhGPskdsJoOeQ
RXk5Mvfp84miIezv8aXxmQEquxQiP8HRtzQ0TrsvKdlM9XOGwUJULyqtFfSs6vmj
HTD16GGPxWqEy0xAcaQcM31pb8YwuZ5MCNRdhuL4ladWBfMR0z75GssCgYEAwVg8
hPsX+H4LzCfu32MQCMh+sAR+1xPTLSG3ydJFbx6PE9pX408ZJKc9CtkG8Xz29+y1
EZnymB1IUpNZxms/4pybyFaKXUa20SpPgSoIrBaL/wdgM4h4Pvs2FSceYN9qY7Z5
d4DhJEiuez+CAFVqQnrNaLJI7xD094SEv91nQAMCgYA+rq8dOzG6UgYj3e61yXA4
1ijCVMYUzDFil1fZI07en7ZKg+tn+B6FXVXHX2GRfUQ7T4Jfa5kg3zrzYHAftc2K
dnpMbsJE3UDAC6CCuvJRzIcFsKvz6tRhunRdib+/FqGU6y1oUZE7sQuMrR9TqOtD
XEltjAzbWGmitG+3Kot03wKBgQCunriaCgWOQpj5HB/b1aaHqDzzUDwWmCskGc3a
E3TudRUYAx1ZiPjWZ8zz3SsuM4UCSeEHMpkt1VSab8anM/oQ+wyflbmFoPZAVwxT
RdlrQznRbaHvKRQhHdWsqRYAvAdkY0u1KMsucA5V9fe9wWck/7BBHLROZmw4mJEk
kBxObQKBgQCCrcUNFhm3dxCdi+VgMwrhMOqiO5XYAGw4raQ/BbXwpb1PLbj4xtSZ
WVb3utTBP0WPhf58EcHc8ko4B+1xCMR4B9rntQACfngbUN4wETQ1Gz+bNgaHQyJo
gHYA38gnEOJurr2VLZFaqLgwj+7kpTRL2a0ZTDz8pCxlwDpsi4n1YA==
-----END RSA PRIVATE KEY-----";
}
Loading
Loading