3.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. 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 removes the
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 ticket in the authenticated introduction; issue #12 wires
authorization and consumption into the caller-owned LiteNetLib coordinator.
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.