using System.Net; using System.Net.Sockets; namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests; /// /// An in-test TCP forwarder in front of the S7 fixture that can be FROZEN: while frozen it keeps /// every socket open but forwards nothing in either direction — the frozen-but-established peer /// (cable pull, firewall DROP) that only a wire-op deadline can detect, produced without touching /// the shared fixture. It counts the ISO-on-TCP (TPKT) request frames the client sends while frozen, /// so a test can assert how many wire operations went out without timing anything. /// internal sealed class FreezableTcpProxy : IAsyncDisposable { private readonly string _targetHost; private readonly int _targetPort; private readonly TcpListener _listener = new(IPAddress.Loopback, 0); private readonly CancellationTokenSource _cts = new(); private readonly List _sockets = []; private volatile bool _frozen; private int _framesWhileFrozen; /// Starts a proxy forwarding to :. /// The fixture host. /// The fixture port. public FreezableTcpProxy(string targetHost, int targetPort) { _targetHost = targetHost; _targetPort = targetPort; _listener.Start(); _ = AcceptLoopAsync(); } /// The local port clients connect to. public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port; /// TPKT request frames the client sent while the proxy was frozen. public int FramesWhileFrozen => Volatile.Read(ref _framesWhileFrozen); /// Stops forwarding (sockets stay open). public void Freeze() => _frozen = true; /// Resumes forwarding for connections made from now on. public void Unfreeze() => _frozen = false; private async Task AcceptLoopAsync() { try { while (!_cts.IsCancellationRequested) { var client = await _listener.AcceptTcpClientAsync(_cts.Token).ConfigureAwait(false); var upstream = new TcpClient(); await upstream.ConnectAsync(_targetHost, _targetPort, _cts.Token).ConfigureAwait(false); lock (_sockets) { _sockets.Add(client); _sockets.Add(upstream); } // A connection accepted while frozen stays frozen for its whole life (its peer is "gone"). var frozenAtBirth = _frozen; _ = PumpAsync(client.GetStream(), upstream.GetStream(), countFrames: true, frozenAtBirth); _ = PumpAsync(upstream.GetStream(), client.GetStream(), countFrames: false, frozenAtBirth); } } catch { /* disposed */ } } private async Task PumpAsync(NetworkStream from, NetworkStream to, bool countFrames, bool frozenAtBirth) { var buffer = new byte[8192]; var pending = new List(); try { while (!_cts.IsCancellationRequested) { var n = await from.ReadAsync(buffer, _cts.Token).ConfigureAwait(false); if (n == 0) return; var frozen = _frozen || frozenAtBirth; if (countFrames && frozen) { // Count complete TPKT frames: 0x03 0x00 then a big-endian total length. pending.AddRange(buffer.AsSpan(0, n).ToArray()); while (pending.Count >= 4) { var len = (pending[2] << 8) | pending[3]; if (len < 4 || pending.Count < len) break; pending.RemoveRange(0, len); Interlocked.Increment(ref _framesWhileFrozen); } } if (frozen) continue; // swallow — the peer never answers await to.WriteAsync(buffer.AsMemory(0, n), _cts.Token).ConfigureAwait(false); } } catch { /* socket closed / disposed */ } } /// public ValueTask DisposeAsync() { _cts.Cancel(); _listener.Stop(); lock (_sockets) foreach (var s in _sockets) s.Dispose(); _cts.Dispose(); return ValueTask.CompletedTask; } }