79 lines
4.7 KiB
Markdown
79 lines
4.7 KiB
Markdown
# 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.
|