Files
Rendezvous/docs/architecture/0008-scoped-join-attempts-and-tickets.md
KyuubiYoru b4b6072fe1
quality-gate / quality (push) Successful in 56s
feat(client): add rendezvous traversal coordinators (#12)
2026-07-16 08:39:05 +02:00

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.