# Deterministic topology harness Issue #14 is verified at three layers. The layers are deliberately separate so the always-on gate remains deterministic while privileged CI workers can add a stronger operating-system topology without overstating what local emulation proves about the public Internet. ## Always-on public-process gate `TestClientProcessIntegrationTests` launches the built server and the same `FinalFactory.Rendezvous.TestClient` executable shipped to operators. Every child process uses `--script --json`, dynamic HTTP and UDP ports, bounded state-driven waits, and enforced process-tree cleanup. The suite proves: | Scenario | Required observation | | --- | --- | | Three-party happy path | register, presence-ready, browse, authorize, punch, authenticated LiteNetLib connection, direct ping/echo/ack/completion traffic, outcome report, disconnect, deregister | | Same-LAN candidate | the connected peer is reported as `loopback` or `private`, never inferred merely from an introduction callback | | Empty and missing selection | browse exits `11`; exact missing lookup exits `10` | | Wrong tenant/protocol | no listing is returned for an incompatible protocol; exact joins with either mismatch fail before `join.punch` | | Traversal timeout | an unreachable mediator produces typed `PunchTimedOut`, exits `12`, advertises the configured dedicated fallback, and never connects to it | | Caller cancellation | POSIX `SIGINT` exits `130`, deregisters the listing, and removes it from public lookup | | Abrupt host loss | the listing disappears after the presence window and before its lease expires; public exact lookup intentionally reports `NotFound` | | Bounded host without a peer | exits `13` and still deregisters | Captured output is parsed as the stable JSON v1 event schema. Publisher credentials and signing-key material are checked against all captured output. The direct traffic payload is handled only by the caller-owned host and client LiteNetLib managers; the HTTP service and mediator do not implement or observe the echo protocol. Run the always-on scenarios with: ```bash dotnet test Rendezvous.slnx --configuration Release --no-build \ --filter FullyQualifiedName~TestClientProcessIntegrationTests ``` ## Deterministic protocol and adverse-state gate The following real service-boundary tests cover conditions that a public CLI cannot safely manufacture by accepting raw capabilities or tickets: | Scenario | Test evidence | | --- | --- | | Same-NAT private candidates | `NatMediationProcessorTests.MatchedPeersReceiveOneIntroductionAndSameNatPrivateCandidates` | | Separate observed endpoints | `NatMediationProcessorTests.DifferentNatsAndInvalidLocalClaimsExposeOnlyObservedPublicEndpoints` | | One-time introduction and replay | `InMemoryEphemeralRendezvousStoreTests.AttemptCapabilitiesAndIntroductionAreOneTime` | | Direct ticket replay | `RendezvousCoordinatorIntegrationTests.CallerOwnedManagersCompleteAuthenticatedDirectConnectionAndRejectTicketReplay` | | Wrong tenant/protocol and stale presence | `InMemoryEphemeralRendezvousStoreTests.JoinRequiresExactScopeProtocolAndFreshHostPresence` | | Cancellation and late callbacks | `RendezvousCoordinatorBehaviorTests.CancellationCompletesExactlyOnceAndLateCallbacksCannotReopenTheAttempt` | | Mediator restart | both cases of `UdpMediatorServiceTests.NativeLiteNetLibRequestsIntroduceTheAuthorizedPair`; the restarted case rebinds the same UDP port and completes a native LiteNetLib introduction | These tests use fake monotonic clocks or state predicates where expiry and race ordering matter. They do not use fixed sleeps as proof of state. ## Privileged Linux namespace gate When a Linux CI worker can create network namespaces, the workflow sets `RENDEZVOUS_RUN_NETNS_TESTS=1` and reruns `PrivilegedLinuxNatNamespacesCompleteDirectTrafficAcrossSeparateObservedEndpoints`. The test creates a temporary WAN bridge, an isolated service namespace, two NAT router namespaces, and isolated host/client LAN namespaces. Each NAT has its own inside subnet and WAN address. Linux forwarding plus per-router MASQUERADE rules force the service to observe separate translated endpoints; the public TestClient processes must then complete authenticated direct traffic through those mappings using the public candidate. Namespaces, rules, veth pairs, bridge, processes, and sockets are removed in bounded async-disposal paths. A cleanup failure fails the test. If `ip netns add`/`iptables` is unavailable or the worker lacks `CAP_NET_ADMIN`, CI records the limitation and keeps the always-on loopback suite as the required gate. To request the privileged run explicitly: ```bash RENDEZVOUS_RUN_NETNS_TESTS=1 dotnet test Rendezvous.slnx \ --configuration Release --no-build \ --filter FullyQualifiedName~PrivilegedLinuxNatNamespacesCompleteDirectTrafficAcrossSeparateObservedEndpoints ``` ## What this does not prove Loopback, MASQUERADE, and namespace routing cannot reproduce every consumer router, carrier-grade NAT, firewall, IPv6 transition mechanism, symmetric NAT mapping, or real-world packet-loss pattern. The separate-observed-endpoint processor test proves that untrusted private claims are excluded and public candidates are selected; it is not presented as universal Internet traversal proof. Real network canaries and measured production readiness remain the scope of issue #23.