using System.Net; using System.Net.Sockets; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests; /// /// Loopback TCP relay in front of the real Modbus fixture, so a test can make the device "disappear" /// (: every relayed connection is closed and new connects are refused) and "come back" /// (: listen again on the SAME port) without touching the shared container. /// internal sealed class TcpRelay : IAsyncDisposable { private readonly string _upstreamHost; private readonly int _upstreamPort; private readonly object _lock = new(); private readonly List _live = []; private TcpListener? _listener; private CancellationTokenSource _stop = new(); private int _accepted; public TcpRelay(string upstreamHost, int upstreamPort) { _upstreamHost = upstreamHost; _upstreamPort = upstreamPort; var probe = new TcpListener(IPAddress.Loopback, 0); probe.Start(); Port = ((IPEndPoint)probe.LocalEndpoint).Port; probe.Stop(); Restore(); } /// Gets the loopback port clients dial. public int Port { get; } /// Gets the number of client connections the relay has accepted. public int AcceptedConnections => Volatile.Read(ref _accepted); /// Starts (or resumes) relaying on . public void Restore() { _stop = new CancellationTokenSource(); var listener = new TcpListener(IPAddress.Loopback, Port); listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); listener.Start(); _listener = listener; var token = _stop.Token; _ = Task.Run(() => AcceptLoopAsync(listener, token)); } /// Closes every relayed connection and refuses new ones until . public void Cut() { try { _stop.Cancel(); } catch { } try { _listener?.Stop(); } catch { } _listener = null; lock (_lock) { foreach (var c in _live) { try { c.Close(); } catch { } } _live.Clear(); } } private async Task AcceptLoopAsync(TcpListener listener, CancellationToken token) { while (!token.IsCancellationRequested) { TcpClient downstream; try { downstream = await listener.AcceptTcpClientAsync(token); } catch { return; } Interlocked.Increment(ref _accepted); _ = Task.Run(() => RelayAsync(downstream, token)); } } private async Task RelayAsync(TcpClient downstream, CancellationToken token) { var upstream = new TcpClient(AddressFamily.InterNetwork); lock (_lock) { _live.Add(downstream); _live.Add(upstream); } try { var addr = (await Dns.GetHostAddressesAsync(_upstreamHost, token)) .First(a => a.AddressFamily == AddressFamily.InterNetwork); await upstream.ConnectAsync(addr, _upstreamPort, token); var a = downstream.GetStream(); var b = upstream.GetStream(); await Task.WhenAny(a.CopyToAsync(b, token), b.CopyToAsync(a, token)); } catch { /* cut or upstream gone */ } finally { try { downstream.Close(); } catch { } try { upstream.Close(); } catch { } } } public ValueTask DisposeAsync() { Cut(); return ValueTask.CompletedTask; } }