using System.Net; using System.Net.Sockets; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests; /// /// Gitea #676 — an in-test TCP forwarder that can STALL a connection without closing it: the shape of a /// docker pause, a hung switch port or a congested WAN link, where the socket stays open and simply stops /// carrying bytes. Each accepted connection gets a fresh upstream connection and two copy pumps; every pump awaits a /// shared gate before each write, so holds all traffic in both directions (bytes already read /// are held, the rest back up in the kernel buffers) and lets it all through in order. Nothing /// is ever closed by a pause — the outage is invisible at the TCP level, exactly as in production. /// internal sealed class TcpStallProxy : IAsyncDisposable { private readonly TcpListener _listener = new(IPAddress.Loopback, 0); private readonly int _targetPort; private readonly CancellationTokenSource _cts = new(); private readonly List _sockets = []; private readonly Lock _lock = new(); private TaskCompletionSource _gate = Open(); private volatile bool _refusing; private Task _acceptLoop = Task.CompletedTask; private TcpStallProxy(int targetPort) => _targetPort = targetPort; /// The loopback port clients connect to. public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port; /// Number of client connections accepted so far — a new one means the client re-connected. public int AcceptedConnections { get; private set; } /// Starts forwarding 127.0.0.1:{Port} to 127.0.0.1:{targetPort}. public static TcpStallProxy Start(int targetPort) { var proxy = new TcpStallProxy(targetPort); proxy._listener.Start(); proxy._acceptLoop = proxy.AcceptLoopAsync(); return proxy; } /// Rewrites the port of an opc.tcp URL to this proxy's. public string Through(string endpointUrl) => new UriBuilder(endpointUrl) { Port = Port }.Uri.ToString(); /// Stops all traffic in both directions; sockets stay open. public void Pause() { lock (_lock) if (_gate.Task.IsCompleted) _gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } /// /// The shape of a killed / stopped server process (#676 live gate): every open connection is RESET (linger 0 ⇒ /// RST, both sides) and new connections are refused (accepted and reset at once) until . /// public void HardClose() { List victims; lock (_lock) { _refusing = true; victims = [.. _sockets]; _sockets.Clear(); } foreach (var s in victims) Reset(s); } /// Accept and forward new connections again (the server is back); the reset ones stay dead. public void Reopen() => _refusing = false; /// Connections refused while was in force — a reconnect ATTEMPT by the client. public int RefusedConnections { get; private set; } private static void Reset(TcpClient s) { try { s.Client.LingerState = new LingerOption(true, 0); } catch { } try { s.Dispose(); } catch { } } /// Releases everything held since , in order. public void Resume() { lock (_lock) _gate.TrySetResult(); } private static TaskCompletionSource Open() { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); tcs.SetResult(); return tcs; } private Task CurrentGate() { lock (_lock) return _gate.Task; } private async Task AcceptLoopAsync() { while (!_cts.IsCancellationRequested) { TcpClient client; try { client = await _listener.AcceptTcpClientAsync(_cts.Token); } catch { return; } if (_refusing) { lock (_lock) RefusedConnections++; Reset(client); continue; } var upstream = new TcpClient(); try { await upstream.ConnectAsync(IPAddress.Loopback, _targetPort, _cts.Token); } catch { client.Dispose(); upstream.Dispose(); continue; } lock (_lock) { AcceptedConnections++; _sockets.Add(client); _sockets.Add(upstream); } _ = PumpAsync(client.GetStream(), upstream.GetStream()); _ = PumpAsync(upstream.GetStream(), client.GetStream()); } } private async Task PumpAsync(NetworkStream from, NetworkStream to) { var buffer = new byte[64 * 1024]; try { while (true) { var n = await from.ReadAsync(buffer, _cts.Token); if (n == 0) break; await CurrentGate().WaitAsync(_cts.Token); await to.WriteAsync(buffer.AsMemory(0, n), _cts.Token); } } catch { /* either side gone or the proxy disposed */ } // Half-close propagation is not needed: a real close (server stopped) tears the pair down on the next read. try { to.Socket.Shutdown(SocketShutdown.Send); } catch { } } public async ValueTask DisposeAsync() { Resume(); await _cts.CancelAsync(); _listener.Stop(); try { await _acceptLoop; } catch { } lock (_lock) { foreach (var s in _sockets) s.Dispose(); _sockets.Clear(); } _cts.Dispose(); } }