Files
Rendezvous/docs/integration/topology-harness.md
T
KyuubiYoru 2ff7cd6d9d
quality-gate / quality (push) Failing after 1m3s
test(integration): add deterministic NAT topology harness (#14)
2026-07-16 11:50:53 +02:00

5.3 KiB

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:

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:

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.