Closes #2
5.8 KiB
ADR 0001: v1 control-plane boundaries and domain
- Status: Accepted
- Date: 2026-07-16
- Tracking: #2
Context
Rendezvous must help two game peers discover and attempt an authenticated direct connection without becoming a game server, an identity provider, or a gameplay traffic service. The HTTP API and UDP mediator share short-lived state and must agree on authorization, endpoint freshness, and tenant scope.
Decision
V1 is one ASP.NET Core deployable with separable directory, join-authorization, endpoint-registry, NAT-mediator, and operations modules. Modules communicate through application interfaces, not through transport DTOs or one another's storage implementation. Contracts and the client SDK remain independently packageable.
The service is a connection control plane. A successful join authorization only grants permission to attempt a direct connection. The game host remains the final authority for player identity, capacity, bans, admission, and gameplay. Rendezvous success is reported only after the host accepts a valid connection ticket and LiteNetLib establishes the authenticated peer connection.
Domain glossary
| Term | Definition | Lifetime and exposure |
|---|---|---|
SessionListing |
Bounded public discovery data for one hosted game session. | Visible only while its lease and host presence are fresh. Never contains endpoints or credentials. |
Lease |
Renewable capability controlling the lifetime of a listing. | Secret, host-only, expires unless renewed. |
HostPresence |
Authenticated observation of the host's local and public UDP endpoints from its gameplay socket. | Internal, short-lived, never returned by browsing. |
JoinAttempt |
Authorization linking one client attempt to one compatible session. | Internal and short-lived; it is not authoritative game admission. |
PunchCapability |
Opaque, one-time credential scoped to attempt, role, tenant, and expiry. | Sent only to its intended peer; consumed at the UDP mediator. |
ConnectionTicket |
Compact signed credential presented to the host during the direct connection. | One-time, short-lived, and scoped to the attempt and host. |
IDs are opaque and tenant-scoped. They are never canonical player, entity, or world identities.
Trust boundaries
flowchart LR
Browser["Untrusted browser/client"] -->|"HTTPS: browse/join"| Proxy["Reverse proxy"]
Host["Game host"] -->|"HTTPS: register/renew"| Proxy
Operator["Privileged operator"] -->|"separate authenticated route"| Proxy
Proxy -->|"normalized HTTP + trusted forwarding metadata"| Service["Rendezvous service"]
Host -->|"host gameplay UDP socket"| Mediator["UDP mediator module"]
Browser -->|"client gameplay UDP socket"| Mediator
Mediator <--> Service
Service -->|"read keys; never list or log values"| Secrets["Secret provider"]
Service -.->|"future authenticated state protocol"| Store["Future shared store"]
Service -->|"redacted events and aggregate metrics"| Ops["Observability systems"]
- Public HTTP input is hostile even after TLS termination. The proxy may be trusted to terminate TLS and supply forwarding metadata only when its source address is allowlisted; forwarded headers from other sources are discarded.
- Public UDP input is hostile even when structurally valid. HTTP-supplied endpoints are claims, never proof. Public response targets come only from an authenticated UDP packet's observed source. A private local candidate may be carried inside that packet only under ADR 0002's bounded same-LAN rules.
- Operator routes use a separate authentication policy and network exposure. Operator access does not bypass tenant scoping, audit, or secret redaction.
- The client SDK is convenience code in an untrusted process. Server decisions never rely on client-side validation or secrecy.
- Game hosts are authoritative only for their own gameplay admission. A host cannot enumerate or mutate another game/environment tenant.
- The secret provider is trusted with long-lived key material. The application receives only the minimum named key version it needs.
- A future shared store is a distinct authenticated boundary. Moving state to it does not make stored input trusted and requires a new availability ADR.
Connection data flow
sequenceDiagram
participant H as Game host
participant R as Rendezvous HTTP
participant M as Rendezvous UDP mediator
participant C as Game client
H->>R: Register listing (publisher authorization)
R-->>H: Lease + host-presence capability
H->>M: Presence from gameplay UDP socket
M->>R: Store observed endpoint and freshness
H->>R: Renew lease
C->>R: Browse compatible visible listings
C->>R: Request join attempt
R-->>C: Client punch capability
R-->>H: Host attempt/capability via authenticated poll or stream
H->>M: Host capability from gameplay UDP socket
C->>M: Client capability from gameplay UDP socket
M->>M: Validate scope, freshness, expiry, and replay state
M-->>H: Introduce verified client endpoints + connection ticket
M-->>C: Introduce verified host endpoints + connection ticket
C->>H: Direct LiteNetLib connect + ticket
H->>H: Validate and consume ticket; apply game admission
H-->>C: Authenticated peer connection or rejection
The mediator does not forward normal gameplay packets. A connection attempt that times out or is rejected returns a typed outcome to the caller.
Consequences
- Directory and mediator can ship together without erasing their module boundary.
- Contracts cannot expose server storage or LiteNetLib implementation types.
- Tests must cover the three-party host/service/client flow; an HTTP-only test is insufficient evidence of a successful connection.
- Splitting modules into processes requires an explicit protocol, shared-state ownership, deterministic mediator routing, and a superseding ADR.