69 lines
3.8 KiB
Markdown
69 lines
3.8 KiB
Markdown
# 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.
|