4.7 KiB
ADR 0008: scoped join attempts and one-time connection tickets
- Status: Accepted
- Date: 2026-07-16
- Tracking: #10
Decision
Join creation is an unauthenticated public operation because v1 does not treat a Rendezvous caller as game identity. The HTTP source address is normalized and converted to a process-keyed opaque subject for idempotency and bounded policy accounting; raw addresses and the derived subject are never returned or logged. A successful request means only that this network client may try to connect to this active session. It does not reserve capacity or grant gameplay admission.
Creation validates the v1 contract, caller idempotency key, enabled tenant policy, exact gameplay protocol, listing scope, live lease, and fresh authenticated host presence in one atomic store operation. A listing advertised as full remains joinable because its player count is advisory and the game host owns the final capacity, identity, ban, and admission decision.
Each attempt derives independent host-punch, client-punch, and connection-ticket credentials plus opaque attempt and mediation IDs from a process-ephemeral HMAC key, the client subject, the complete canonical request fingerprint, a fresh salt, and a purpose/role label. Credentials are 32-byte base64url values (43 characters), below both the 192-character Rendezvous capability ceiling and LiteNetLib's 256-character NAT token ceiling. The connection ticket uses half of that payload for its attempt ID and half for an independently derived 128-bit authenticator, so the SDK can correlate concurrent introductions without increasing UDP response size. State retains keyed credential fingerprints, derivation inputs, and salt—not issued plaintext. All diagnostic string representations redact credentials and derivation material.
The client receives only its punch capability. A host polls its own listing with
the lease token in X-Rendezvous-Lease-Token and receives only host-role
capabilities through a signed, listing-bound, five-minute cursor. Replaying an
identical join request returns the same live attempt; changing the request under
the same owner/key conflicts. A client may cancel with its punch capability in
X-Rendezvous-Client-Punch-Capability; cancellation atomically marks the attempt
and retains a bounded tombstone until its original expiry. Host polling returns
that tombstone so a coordinator can revoke any local ticket authorization, while
endpoint binding, introduction, ticket issuance, and ticket consumption all
reject the cancelled attempt. Listing deletion, expiry, revocation, or process
restart removes every associated attempt and credential fingerprint.
Endpoint binding remains role- and capability-specific. The first endpoint observed for a role wins atomically; an exact UDP duplicate is idempotent, while endpoint or role substitution is rejected. An introduction is consumable once only after both roles bind, so concurrent attempts for the same listing cannot cross-wire.
The connection ticket is distinct from both punch capabilities and is reproduced
only after introduction succeeds. Its window begins at that moment and lasts at
most 20 seconds without outliving the 30-second attempt. The server has an atomic
fingerprint-consumption seam for mediator tests and revocation. On the game host,
the SDK's bounded ConnectionTicketValidator stores a process-keyed digest,
accepts an exact ticket once under a lock, rejects altered/cross-attempt/expired/
revoked/replayed tickets, and zeroes retained digests and key material on disposal.
Issue #11 carries the fixed-size ticket in the authenticated introduction. Issue
#12 extracts its embedded attempt ID, bounds the host's local authorization window
by both the host-polled attempt expiry and the configured ticket lifetime, then
wires one-time consumption into the caller-owned coordinator. Both peers receive
a digest of the exact expected ticket over HTTP and reject any syntactically valid
but unauthenticated introduction token. Embedding the ID prevents concurrent or
late introductions from cross-binding a valid ticket while preserving the
mediator's 2.0 response-byte amplification ceiling.
Consequences
- A join attempt is transport authorization, never proof of player identity or a game slot.
- Network-address-derived subjects are process-local abuse/idempotency scopes, not stable user identifiers; stronger authenticated player scopes require a future game-owned identity contract.
- Cancellation after a ticket has reached a host must also revoke that host's local validator entry; coordinator wiring owns that race in issue #12.
- Capability and ticket plaintext never enter browser results, state snapshots, logs, metrics, or generated string representations.