97 lines
4.9 KiB
Markdown
97 lines
4.9 KiB
Markdown
# UDP presence and NAT-punch contract v1
|
||
|
||
Tracking: #4, #11
|
||
|
||
The UDP mediator accepts the frozen bounded presence envelope below and native
|
||
LiteNetLib NAT-introduction requests. Both forms associate an authenticated
|
||
mediation handle with the packet's observed public source endpoint and the
|
||
sender's reported local endpoint. Neither form carries gameplay packets.
|
||
|
||
All multi-byte integers use network byte order. UUID bytes use the canonical
|
||
RFC 4122 textual order (the byte pairs from the 32 hexadecimal digits), not the
|
||
mixed-endian layout returned by `Guid.ToByteArray()`.
|
||
|
||
## Datagram layout
|
||
|
||
| Offset | Size | Field |
|
||
| ---: | ---: | --- |
|
||
| 0 | 2 | Magic bytes `52 56` (`RV`). |
|
||
| 2 | 1 | Contract version, exactly `01`. |
|
||
| 3 | 1 | Message type: host presence `01`, client presence `02`. |
|
||
| 4 | 1 | Flags, exactly `00` in v1. |
|
||
| 5 | 16 | Non-empty mediation-handle UUID. |
|
||
| 21 | 1 | Address family: IPv4 `04`, IPv6 `06`. |
|
||
| 22 | 1 | Address length: `04` for IPv4, `10` for IPv6. |
|
||
| 23 | 4 or 16 | Raw local IP address bytes. |
|
||
| next | 2 | Local UDP port, 1–65535. |
|
||
| next | 1 | Capability length, 1–192. |
|
||
| next | variable | ASCII base64url capability, without padding. |
|
||
|
||
No trailing bytes are permitted. The whole datagram is limited to 1,200 bytes,
|
||
well below common Internet path MTUs. The v1 capability limit is 192 characters,
|
||
which also keeps any value passed through LiteNetLib's 256-character NAT token
|
||
surface safely below that library boundary.
|
||
|
||
## Validation and failure behavior
|
||
|
||
Decoders return one stable failure category: oversized, truncated, invalid
|
||
magic, unsupported version, unknown message type, non-zero flags, invalid
|
||
handle, invalid address family, invalid address, invalid port, invalid
|
||
capability, or trailing data. Unknown versions and message types are rejected;
|
||
they are never interpreted as v1.
|
||
|
||
The address-family byte, encoded address length, and parsed address must agree.
|
||
The service derives the public endpoint from the UDP packet source and never
|
||
trusts a client-supplied public address. Reported local endpoints are candidates
|
||
only and grant no authority.
|
||
|
||
Capabilities are short-lived, single-purpose, scoped to one mediation handle,
|
||
and compared without exposing them in logs. A valid-looking packet does not
|
||
prove authorization until the capability is checked. Invalid packets receive
|
||
no UDP response, preventing the mediator from becoming an amplification oracle.
|
||
For the frozen envelope, `HostPresence` is resolved against either the listing's
|
||
host-presence capability or an attempt's host-role capability. `ClientPresence`
|
||
is resolved only against the attempt's client-role capability. Handles are
|
||
globally distinct in the active store, so this does not permit role confusion.
|
||
|
||
## Native LiteNetLib request token
|
||
|
||
A game using LiteNetLib sends `NatPunchModule.SendNatIntroduceRequest` from its
|
||
gameplay `NetManager`. The `additionalInfo` value is produced by
|
||
`NatPunchRequestTokenCodec` and is exactly 192 ASCII characters:
|
||
|
||
```text
|
||
rv1:<role>:<32 lowercase handle hex>:<43-character capability><dot padding>
|
||
```
|
||
|
||
`role` is `p` for listing host-presence refresh, `h` for the host side of a join
|
||
attempt, or `c` for its client side. Padding is canonical and leaves the token
|
||
below LiteNetLib's 256-character ceiling. Its fixed size also ensures that the
|
||
two authenticated introduction responses remain within the 2.0 response-byte
|
||
budget. Tokens with a wrong length, role, handle, capability, or padding receive
|
||
no response.
|
||
|
||
The mediator runs LiteNetLib in bounded manual-poll mode. Its packet layer admits
|
||
only the pinned native `NatIntroduceRequest` frame, consumes every inbound frame
|
||
before LiteNetLib can act on it, and uses `NatPunchModule` only to emit authorized
|
||
introductions. Native and frozen v1 inputs reach the same atomic role/capability
|
||
checks. Only the packet source is
|
||
used as the public endpoint. A claimed private candidate is retained only when
|
||
it is private unicast, matches the observed address family, and both authorized
|
||
peers were observed behind the same public address; otherwise the observed
|
||
public endpoint is substituted. IPv4 punching is required. IPv6 sources must be
|
||
observed global unicast and both roles must use IPv6; IPv6 NAT traversal remains
|
||
best-effort rather than a v1 release requirement.
|
||
|
||
The second valid contribution atomically consumes the introduction and starts
|
||
the connection-ticket lifetime. `NatIntroduce` is called once with the distinct
|
||
43-character connection ticket. Reordered and exact duplicate requests are
|
||
idempotent. Endpoint substitution, cross-role use, stale host presence, expired
|
||
or cancelled attempts, malformed packets, and gameplay payloads produce no
|
||
introduction and create no mediator queue or endpoint state.
|
||
|
||
Frozen envelopes may refresh listing presence over IPv6 because that operation
|
||
has no response. IPv6 attempt contributions must use the fixed-size native token;
|
||
the shorter frozen IPv6 envelope cannot fund two IPv6 introduction frames within
|
||
the 2.0 response-byte ceiling and is therefore dropped without response.
|