Files
Rendezvous/docs/architecture/0009-authenticated-litenet-nat-mediator.md
KyuubiYoru 6d076c281a
quality-gate / quality (push) Successful in 59s
feat: implement authenticated NAT mediator (#11)
Closes #11
2026-07-16 07:37:02 +02:00

3.8 KiB

ADR 0009: authenticated bounded LiteNetLib NAT mediator

  • Status: Accepted
  • Date: 2026-07-16
  • Tracking: #11

Decision

The server owns one LiteNetLib NetManager and its NatPunchModule on the configured UDP endpoint. It runs in manual mode with a configured maximum number of datagrams per poll and a short caller-owned poll interval. LiteNetLib events are unsynchronized so authenticated requests are processed immediately on that single polling path rather than accumulated in an unbounded event queue. The mediator never accepts a LiteNetLib gameplay connection or handles application payloads.

The packet layer also consumes the frozen v1 presence envelope on the same socket. Native NAT requests use a canonical fixed-size 192-character token that binds a role (HostPresence, attempt Host, or attempt Client), mediation handle, and the already-issued capability. Both transports enter one processor and the same atomic store operations. No transport-supplied public address is trusted; the socket source is authoritative.

LiteNetLib's native NAT packet family also contains introduction-response and punch frames that are appropriate for peers but unsafe on a public mediator: a forged response can name arbitrary destinations. The packet layer therefore decodes only the pinned NatIntroduceRequest wire shape and consumes every inbound packet before NatPunchModule sees it. The module is outbound-only and may send introductions solely from a completed authorized plan.

Listing presence refreshes authorize no response. Attempt contributions bind the first observed endpoint for exactly one capability role. Exact duplicates are idempotent; a different endpoint, the opposite role, an expired/cancelled attempt, or a stale listing presence cannot replace it. The introduction is consumed atomically only after both roles bind and their observed address families match, preventing concurrent attempts for one listing from cross-wiring.

A reported local candidate is eligible only when it is RFC 1918 IPv4 or IPv6 unique-local unicast, matches the observed family, and both peers have the same observed public address. Otherwise NatIntroduce receives the observed public endpoint in the local slot. Loopback, link-local, multicast, unspecified, documentation IPv6, global-address claims, and cross-family claims are never disclosed as local targets. IPv4 is required; observed global IPv6 can be used when both peers contribute IPv6, without claiming guaranteed IPv6 NAT traversal.

The introduction carries only the distinct connection ticket and is emitted at most once to each verified observed endpoint. The fixed authenticated native request and bounded frozen envelope keep the combined response bytes within the 2.0 verified amplification budget; unauthenticated inputs receive zero bytes. Malformed, truncated, oversized, spoofed, or unrelated LiteNetLib packets do not grow Rendezvous state. Raw endpoints and credentials are never logged or exposed through diagnostic string representations.

Frozen IPv6 listing-presence refresh remains valid because it emits no response. IPv6 attempt roles require the fixed-size native LiteNetLib request; accepting the short frozen envelope would exceed the 2.0 byte budget for two IPv6 introduction frames. The required IPv4 listen address and optional IPv6 listen address are configured separately so enabling one family never widens the other family to a wildcard bind.

Consequences

  • Hosts refresh listing presence and answer invitations from their actual gameplay socket; a separate mediator socket would observe the wrong mapping.
  • Caller-owned SDK coordination in #12 must poll the host invitation endpoint, send the corresponding native role token, and consume the returned ticket.
  • UDP loss can prevent traversal, but it cannot cause an arbitrary destination, replay, role substitution, or cross-attempt introduction.