Files
Rendezvous/docs/architecture/0001-v1-control-plane-boundaries.md
2026-07-16 04:11:24 +02:00

114 lines
5.8 KiB
Markdown

# 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
```mermaid
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
```mermaid
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.