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

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.