feat: implement authenticated NAT mediator (#11)
quality-gate / quality (push) Successful in 59s
quality-gate / quality (push) Successful in 59s
Closes #11
This commit is contained in:
@@ -60,12 +60,14 @@ the supplied ID.
|
||||
|
||||
### UDP presence
|
||||
|
||||
Only a structurally valid `HostPresence` datagram with the issued capability can
|
||||
refresh presence. The public endpoint is the UDP packet's observed source on the
|
||||
host's gameplay socket; the HTTP API never accepts one. The bounded local candidate
|
||||
comes from the authenticated datagram. Invalid, unknown, or client-presence packets
|
||||
receive no response. Presence expiry demotes public visibility but keeps the lease,
|
||||
so the same handle can restore visibility without changing session identity.
|
||||
Only a structurally valid frozen `HostPresence` envelope or native LiteNetLib
|
||||
host-presence request with the issued capability can refresh presence. The public
|
||||
endpoint is the UDP packet's observed source on the host's gameplay socket; the
|
||||
HTTP API never accepts one. The bounded local candidate comes from the authenticated
|
||||
packet. Invalid or unknown inputs receive no response. ADR 0009 defines the later
|
||||
attempt-role use of frozen `ClientPresence` and native host/client requests.
|
||||
Presence expiry demotes public visibility but keeps the lease, so the same handle
|
||||
can restore visibility without changing session identity.
|
||||
|
||||
Public listing responses contain bounded listing data only. They never contain
|
||||
public/local endpoints, lease tokens, presence capabilities, fingerprints, store
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# 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.
|
||||
@@ -11,6 +11,7 @@ decision requires a superseding ADR and corresponding contract/test updates.
|
||||
- [ADR 0006: bounded compatible session browser](0006-compatible-session-browser.md)
|
||||
- [ADR 0007: caller-owned .NET publisher and browser SDK](0007-caller-owned-dotnet-client-sdk.md)
|
||||
- [ADR 0008: scoped join attempts and one-time connection tickets](0008-scoped-join-attempts-and-tickets.md)
|
||||
- [ADR 0009: authenticated bounded LiteNetLib NAT mediator](0009-authenticated-litenet-nat-mediator.md)
|
||||
- [Threat model](../security/threat-model.md)
|
||||
- [Security promise and test matrix](../security/control-matrix.md)
|
||||
- [Versioned HTTP and UDP contracts](../contracts/README.md)
|
||||
|
||||
Reference in New Issue
Block a user