22// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33
44using System . Net ;
5+ using System . Net . Security ;
56using System . Net . Sockets ;
7+ using System . Security . Authentication ;
68using System . Text ;
79using System . Text . Json ;
810using System . Text . Json . Nodes ;
@@ -20,6 +22,9 @@ namespace NetworkTestCompanion;
2022/// { "cmd": "stop", "port": N }
2123/// { "cmd": "stop_all" }
2224/// { "cmd": "connect_to", "host": "...", "port": N }
25+ /// { "cmd": "start_tls_echo", "port": N }
26+ /// { "cmd": "tls_connect_to", "host": "...", "port": N }
27+ /// { "cmd": "tls_connect_echo", "host": "...", "port": N, "data": "base64..." }
2328/// </summary>
2429internal sealed class CommandServer : IDisposable
2530{
@@ -112,6 +117,9 @@ private string ProcessCommand(string json)
112117 "stop" => Stop ( node ) ,
113118 "stop_all" => StopAll ( ) ,
114119 "connect_to" => ConnectTo ( node ) ,
120+ "start_tls_echo" => StartTlsEcho ( node ) ,
121+ "tls_connect_to" => TlsConnectTo ( node ) ,
122+ "tls_connect_echo" => TlsConnectEcho ( node ) ,
115123 _ => Error ( $ "unknown command: { cmd } ")
116124 } ;
117125 }
@@ -237,6 +245,179 @@ private string ConnectTo(JsonNode node)
237245 }
238246 }
239247
248+ private string StartTlsEcho ( JsonNode node )
249+ {
250+ if ( ! TryGetPort ( node , out var port , out var err ) ) return err ! ;
251+
252+ lock ( _lock )
253+ {
254+ // Replace any stale server left registered on this port by a previous
255+ // (possibly aborted) test run — the companion is long-lived across runs.
256+ if ( _activeServers . TryGetValue ( port , out var existing ) )
257+ {
258+ existing . Dispose ( ) ;
259+ _activeServers . Remove ( port ) ;
260+ Console . WriteLine ( $ "[CMD] Replaced stale server on port { port } ") ;
261+ }
262+
263+ var server = new TlsEchoServer ( _bindAddress , port , TestCertificates . ServerCert ) ;
264+ try
265+ {
266+ server . Start ( ) ;
267+ }
268+ catch ( Exception ex )
269+ {
270+ server . Dispose ( ) ;
271+ return Error ( ex . Message ) ;
272+ }
273+
274+ _activeServers [ port ] = server ;
275+ }
276+
277+ Console . WriteLine ( $ "[CMD] TLS echo started on port { port } ") ;
278+ return Ok ( ) ;
279+ }
280+
281+ private string TlsConnectTo ( JsonNode node )
282+ {
283+ var host = node [ "host" ] ? . GetValue < string > ( ) ;
284+ if ( string . IsNullOrEmpty ( host ) ) return Error ( "missing 'host'" ) ;
285+ if ( ! TryGetPort ( node , out var port , out var err ) ) return err ! ;
286+
287+ TcpClient ? connectClient = null ;
288+ try
289+ {
290+ connectClient = new TcpClient ( ) ;
291+ if ( ! connectClient . ConnectAsync ( host , port ) . Wait ( TimeSpan . FromSeconds ( 5 ) ) )
292+ {
293+ connectClient . Dispose ( ) ;
294+ return Error ( $ "connect to { host } :{ port } timed out") ;
295+ }
296+
297+ Console . WriteLine ( $ "[CMD] tls_connect_to { host } :{ port } TCP connected, starting TLS handshake in background") ;
298+
299+ // TLS handshake + keep-alive runs in background so the device
300+ // can call Accept() and AuthenticateAsServer() after getting Ok.
301+ var clientForBg = connectClient ;
302+ _ = Task . Run ( async ( ) =>
303+ {
304+ try
305+ {
306+ var sslStream = new SslStream (
307+ clientForBg . GetStream ( ) ,
308+ leaveInnerStreamOpen : false ,
309+ ( _ , _ , _ , _ ) => true ) ;
310+
311+ await sslStream . AuthenticateAsClientAsync ( new SslClientAuthenticationOptions
312+ {
313+ TargetHost = host ,
314+ EnabledSslProtocols = SslProtocols . Tls12
315+ } ) ;
316+
317+ Console . WriteLine ( $ "[CMD] tls_connect_to { host } :{ port } TLS handshake succeeded") ;
318+
319+ await Task . Delay ( 2000 ) ;
320+ sslStream . Dispose ( ) ;
321+ clientForBg . Dispose ( ) ;
322+ }
323+ catch ( Exception ex )
324+ {
325+ Console . Error . WriteLine ( $ "[CMD] tls_connect_to { host } :{ port } TLS failed: { ex . Message } ") ;
326+ clientForBg . Dispose ( ) ;
327+ }
328+ } ) ;
329+
330+ return Ok ( ) ;
331+ }
332+ catch ( Exception ex )
333+ {
334+ connectClient ? . Dispose ( ) ;
335+ Console . Error . WriteLine ( $ "[CMD] tls_connect_to { host } :{ port } failed: { ex . Message } ") ;
336+ return Error ( ex . Message ) ;
337+ }
338+ }
339+
340+ private string TlsConnectEcho ( JsonNode node )
341+ {
342+ var host = node [ "host" ] ? . GetValue < string > ( ) ;
343+ if ( string . IsNullOrEmpty ( host ) ) return Error ( "missing 'host'" ) ;
344+ if ( ! TryGetPort ( node , out var port , out var err ) ) return err ! ;
345+ var dataB64 = node [ "data" ] ? . GetValue < string > ( ) ;
346+ if ( string . IsNullOrEmpty ( dataB64 ) ) return Error ( "missing 'data'" ) ;
347+
348+ byte [ ] dataToSend ;
349+ try { dataToSend = Convert . FromBase64String ( dataB64 ) ; }
350+ catch { return Error ( "'data' is not valid base64" ) ; }
351+
352+ TcpClient ? connectClient = null ;
353+ try
354+ {
355+ connectClient = new TcpClient ( ) ;
356+ if ( ! connectClient . ConnectAsync ( host , port ) . Wait ( TimeSpan . FromSeconds ( 5 ) ) )
357+ {
358+ connectClient . Dispose ( ) ;
359+ return Error ( $ "connect to { host } :{ port } timed out") ;
360+ }
361+
362+ Console . WriteLine ( $ "[CMD] tls_connect_echo { host } :{ port } TCP connected, starting TLS + echo in background") ;
363+
364+ // TLS handshake + echo runs in background so the device
365+ // can call Accept() and AuthenticateAsServer() after getting Ok.
366+ var clientForBg = connectClient ;
367+ _ = Task . Run ( async ( ) =>
368+ {
369+ try
370+ {
371+ var sslStream = new SslStream (
372+ clientForBg . GetStream ( ) ,
373+ leaveInnerStreamOpen : false ,
374+ ( _ , _ , _ , _ ) => true ) ;
375+
376+ await sslStream . AuthenticateAsClientAsync ( new SslClientAuthenticationOptions
377+ {
378+ TargetHost = host ,
379+ EnabledSslProtocols = SslProtocols . Tls12
380+ } ) ;
381+
382+ await sslStream . WriteAsync ( dataToSend ) ;
383+ await sslStream . FlushAsync ( ) ;
384+
385+ Console . WriteLine ( $ "[CMD] tls_connect_echo { host } :{ port } : sent { dataToSend . Length } bytes, waiting for echo") ;
386+
387+ var buf = new byte [ 4096 ] ;
388+ int totalRead = 0 ;
389+ using var ms = new MemoryStream ( ) ;
390+
391+ while ( totalRead < dataToSend . Length )
392+ {
393+ int read = await sslStream . ReadAsync ( buf ) ;
394+ if ( read == 0 ) break ;
395+ ms . Write ( buf , 0 , read ) ;
396+ totalRead += read ;
397+ }
398+
399+ Console . WriteLine ( $ "[CMD] tls_connect_echo { host } :{ port } : received { totalRead } bytes echo") ;
400+
401+ sslStream . Dispose ( ) ;
402+ clientForBg . Dispose ( ) ;
403+ }
404+ catch ( Exception ex )
405+ {
406+ Console . Error . WriteLine ( $ "[CMD] tls_connect_echo { host } :{ port } TLS/echo failed: { ex . Message } ") ;
407+ clientForBg . Dispose ( ) ;
408+ }
409+ } ) ;
410+
411+ return Ok ( ) ;
412+ }
413+ catch ( Exception ex )
414+ {
415+ connectClient ? . Dispose ( ) ;
416+ Console . Error . WriteLine ( $ "[CMD] tls_connect_echo { host } :{ port } failed: { ex . Message } ") ;
417+ return Error ( ex . Message ) ;
418+ }
419+ }
420+
240421 private static bool TryGetPort ( JsonNode node , out int port , out string ? error )
241422 {
242423 port = 0 ;
0 commit comments