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.