Skip to content

About

Cloudflare Worker providing Atrinik server discovery and QUIC rendezvous

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

139 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Atrinik metaserver Worker

Check

This repository owns Atrinik's Cloudflare metaserver services for QUIC-only game transport. The services publish discovery metadata and exchange bounded connection candidates; they never proxy game traffic.

Supported services

The current contracts require coordinated access-token-capable consumers. Historical publisher and directory versions are not accepted by this runtime. The public API is canonical-only:

  • static Classic snapshots at classic.meta.atrinik.org/index.{html,json,xml};
  • signed Classic v3 publication at publish.meta.atrinik.org/v3/classic/servers/{server-id}/publish;
  • Classic rendezvous at rendezvous.meta.atrinik.org/v1/classic/servers/{server-id}; and
  • the independently versioned Game Protocol 1 publisher, rendezvous, and static snapshot contracts.

The retired CGI and legacy generic /v2 targets have no application dispatcher, redirect, fallback, or rollback path. Edge policy blocks them before Worker invocation. meta.atrinik.org remains unattached until it is enabled only as a static Game R2 origin. The exact route contract is documented in docs/routes.md.

The dynamic split is implemented as three deployable Workers. The existing atrinik-metaserver core remains the sole owner of D1, R2, schedules, and the RendezvousRoom and DirectoryBuilder Durable Objects. Domainless atrinik-metaserver-publisher and atrinik-metaserver-rendezvous edge Workers own only their route-specific breakers and native burst bindings, then call one named core entrypoint through a Service Binding. The edges reconstruct a fixed allowlisted request, so the raw request address and browser state never cross into the state owner; the core independently validates the complete route and protocol again. The checked-in edge configurations have no public route. Classic publishing and Classic rendezvous are enabled; Game publishing remains disabled. The core exports scheduled handlers, Durable Objects, and named Service Binding entrypoints only; it has no default fetch handler. The private DirectoryReconciliation entrypoint accepts one fixed empty POST from the deployment-owned Service Binding to reconcile the two current directory profiles and return verified publication receipts. It has no public route, caller binding or client-selected profile/object authority; see the capability contract.

The operator-safe rendezvous health source contract is the private RendezvousHealth entrypoint documented in docs/rendezvous-health.md. It exports only bounded aggregate evidence and never enumerates rooms or publishes a public health route.

There is deliberately no TCP directory, DNS ownership proof, game-port probe, or game relay. A server is owned by the SHA-256 identity derived from its persistent QUIC certificate. Both publishers fold freshness and identity proof into one replay-safe signed request. Rendezvous server peers authenticate separately.

Rendezvous is one short, bounded signaling attempt. Every client first obtains a blind stateless challenge from POST /v1/rendezvous/challenge/classic/{server-id} and solves fixed 24-bit SHA-256 work. The core verifies the proof and binary-ticket hash before any presence or room lookup. Open upgrades send the proof and fresh ticket headers; protected upgrades send the proof, bearer grant and client nonce. Only a live authenticated server control can admit the client. The room redeems protected grants and claims ticket replay tags before allocating a socket. Protected access_init confirms that same preauthorized grant and nonce; it never provides another authentication path. No access code, route capability or grant appears in a URL or WebSocket subprotocol. See admission details.

The grant expires within 15 seconds and is redeemed once through the current D1 route authority before room admission. Revocation and redemption serialize there; revocation also closes pending room routing. Independent encrypted game admission verifies the access code. Directory presence stores the authenticated certificate, name, optional endpoint and configured accessRequired policy privately for resolution. Public builders consume only current public entries, never private presence or token metadata. Open/private policy is independent of the number or validity of tokens.

Existing candidate, frame, byte, replay, expiry and terminal-teardown bounds remain enforced. Candidate endpoints are not persisted. The authenticated control channel uses atrinik-access-rendezvous-v1; retired invitation and password protocols are not fallback paths. See the normative access-token contract in the protocol repository and the separately gated deployment plan.

Development

Use the Node and npm versions pinned by .nvmrc and packageManager:

npm ci
npm run check
npm run deploy:development:dry-run
npm run deploy:production:dry-run

npm run check generates and verifies isolated core/publisher/rendezvous Wrangler declarations, runs every TypeScript project, the local Workers runtime tests, the Python administrative tests, and one distinct Wrangler dry run per deployable. Generated declarations and dist/ output are untracked and must not be edited.

Reviewed pushes to protected main automatically deploy to the persistent DEV cohort through the automatic deploy-development.yml GitHub Actions workflow and npm run deploy:development. The machine contract is deployment/workers-builds-development.json. DEV uses the existing atrinik-metaserver-development core and its -publisher and -rendezvous callers; production resources are disjoint. Delivery checks the exact D1 ledger without applying migrations, fences stale workflow runs, stages all three Workers with circuits disabled, restores callers before core, and reads back the coherent 100% cohort before bounded canaries. Protected ATRINIK_DELIVERY_RESOURCE_POLICY and ATRINIK_DELIVERY_RESOURCE_READ_TOKEN pin and freshly verify resource ownership and configuration across PROD, DEV and any live-review cohort at every phase. The approved deployable digest includes the inventory policy digest. Target public callers must have no provider secrets; readback verifies secret names and cannot establish uniqueness of secret values.

Production changes require an explicit GitHub Actions workflow dispatch for an owner-approved immutable tag and exact SHA. Main pushes and release events cannot promote. Release provenance, artifact/configuration/migration digests, expected predecessor, fixed concurrency, and one-use attempts fence the write; workflow reruns are rejected. See deployment environments for prerequisites, DEV schema preparation, promotion phases and recovery. This source change proves no deployment or provider activation.

Private resource identifiers and credentials remain in owner-only protected configuration and provider secrets. Routine builds inherit encrypted core runtime secrets by name; public callers receive no signing keys or key IDs. GitHub Actions provides PR/source validation. Cloudflare Workers Builds is retired and takes no part in validation or delivery.

After an operator has separately provisioned an isolated R2 custom domain and its reviewed edge rules, validate the public static contract with the credential-free verifier:

python3 scripts/static_origin_canary.py \
  --profile game-v2 \
  --base-url https://game-directory-canary.example.org \
  --alias-prefix canary-v2 \
  --json
python3 scripts/static_origin_canary.py \
  --profile classic-v3 \
  --base-url https://classic-v6-directory-canary.example.org \
  --alias-prefix canary-v6 \
  --json

It performs only bounded public HTTPS requests. It verifies the three formats, shared generation and complete normalized server-model parity, freshness, native ETag and conditional retrieval, HEAD parity, security/cache/CORS headers, canonical HTTPS root absence, path/query/method denial, and bounded monotonic convergence. One convergence window covers the complete GET, HEAD, and conditional proof. If a same-path HEAD or conditional GET proves a strictly newer valid publication, the verifier resnapshots all three formats; persistent mismatches, validator reuse, and same-generation body changes still fail. The separate ingress verifier proves plaintext same-path redirects. Production hostnames require the additional --allow-production acknowledgement. The verifier cannot create, alter, or delete Cloudflare resources and accepts no API token.

The checked-in core Wrangler file has a placeholder D1 ID. All three checked-in Wrangler files have no production route. Supply reviewed production bindings during the provider-first deployment procedure. Never run remote migrations, deployments, or identity resets merely to validate a change.

Only the core requires current and previous replay/admission HMAC secrets. Their names are declared in its configuration; public callers have no secret or key-ID binding. Values belong only in Cloudflare encrypted secrets or ignored .dev.vars files. See docs/privacy.md for rotation and retention rules. Consecutive key pairs must overlap for strictly more than the 24-hour rendezvous replay window. Do not substitute plaintext Wrangler variables.

Storage

Production state exists. migrations/0001_initial.sql is immutable applied history, and every transition is an appended ordered migration. Tests apply the complete series and exercise upgrades from populated production-shaped state. Never edit, reorder, or reuse an applied migration number.

The SQLite-backed RendezvousRoom Durable Object is declared through Wrangler's exports configuration. Its application SQLite replay ledger has a high emergency storage ceiling and a 24-hour security horizon, but it is not an ordinary client quota. A row records its acceptance time and, when the client's first candidate or protected auth_init atomically claims it, exactly two purpose-separated HMAC-SHA-256 replay aliases derived with the current and previous source-tag keys. Raw tickets, SHA-256 routing digests, connection IDs, and candidate addresses never enter SQLite. Invite IDs, secrets, challenges, proofs, and serialized authorization frames do not enter SQLite, Durable Object key-value storage, hibernation attachments, logs, or metrics. Profile-scoped D1 presence stores the bearer-token hash, last-seen time, and random non-secret token generation; a separate public-only directory row stores only renderable metadata and an optional operator-published DNS hostname/UDP port pair. A private publication retains minimal authentication presence but deletes the public directory row and cannot admit either rendezvous role. The room's fixed key-value marker and both attachment roles retain only the generation needed to invalidate a previous control. Outside the transient signaling frame, retained raw-ticket state exists only in its client WebSocket attachment for at most 15 seconds; the server attachment keeps random connection IDs, opening/expiry times, and bounded routing digest/counter state until that same session expires. A terminal client attachment retains only bounded counters, the closed outcome enum, and teardown bookkeeping after clearing the ticket and digest. If attachment persistence and transport close fail together, Durable Object key-value storage retains only a fixed boolean recovery-quarantine flag: it contains no ticket, digest, connection/control ID, outcome, address, or counter. Reconstruction uses that flag to scrub and close every room socket, suppresses guessed terminal telemetry, and deletes the flag once cleanup is durable or the transports are closed. The client protocol requires 32 random ticket bytes encoded as 64 lowercase hex characters; the Worker enforces shape and single use but cannot prove client entropy. Classic v1 and v2 share one sequence/nonce lineage per certificate identity. The first accepted v2 publish atomically removes that identity's v1 presence, listing, and rendezvous generation, then durably marks it v2-only. A private v2 publish retains authenticated presence and server control while exposing no directory, endpoint, display, or policy field and admitting no client rendezvous. Sequence 18446744073709551615 may succeed once; every later publish returns fixed publish_sequence_exhausted without a minimum or state mutation.

Canonical identity resets and server-ID denial changes should be generated with scripts/admin_sql.py, reviewed, and only then applied by an authorized operator.

See DEPLOYMENT.md for the release checklist.

Request controls and privacy

The application does not track request IP addresses or derive identifiers from them. Authenticated request budgets use the server identity; private access-grant and ticket replay tags derive only from the corresponding random capabilities. Only a signed, canonical DNS hostname is eligible for persistence. Canonical xn-- labels are checked with strict, non-transitional UTS #46 processing, including STD3, hyphen, joiner, bidirectional, and DNS-length checks. Migration 0009_remove_legacy_storage.sql physically removes retired ownership, OTP, source-rate, shadow-directory, and wildcard-denial storage. Migration 0015_remove_ip_derived_pair_tracking.sql removes the populated source/server-pair attempt and cooldown tables, including their indexes. No publish infers a QUIC endpoint from the HTTPS source. The request path emits only the closed, redacted diagnostic events described in docs/privacy.md.

Identity rate bindings cap authenticated bursts. D1 retains authenticated publisher/server budgets and private access-request budgets. Each per-server Durable Object preserves replay rejection for 24 hours without imposing a daily player quota. Rooms admit at most 16 active client attempts and retain 64 client sockets only as an absolute implementation ceiling. A pre-Worker WAF rule is still required to prevent a blocked loop from consuming Worker invocations. The ceilings, 429 contract, shared-NAT policy, and circuit breakers are in docs/rate-limits.md; the reviewed edge-policy specification and deployment gate are in docs/edge-policy.md.

Static directory publication

Visible directory mutations and expiry advance one profile-scoped D1 revision and coalesce its durable outbox to the newest unpublished revision. Accepted heartbeats refresh presence and bounded private activity aggregates without changing the revision. The profile-scoped sustained-activity ranking and operator pin procedure are documented in docs/directory-ranking.md. A private, profile-named DirectoryBuilder Durable Object receives an O(1), alarm-only nudge after a visible commit and reconciles durable truth every five minutes and by alarm; a Queue is deliberately unnecessary. It serializes R2 work, persists bounded retry intent, coalesces revisions, and publishes all immutable generation objects before compare-and-swap replacement of the four public aliases (index.html, index.xml, index.json, and manifest.json). D1 acknowledges an outbox revision only after all aliases are read back with the exact generation, checksum, size, content type, cache metadata, opaque native R2 strong ETag, and privacy-safe custom metadata. The application SHA-256 remains independent body-integrity metadata; it is not the public HTTP validator.

After that checkpoint, the builder globally purges exactly the profile's three public index.* URLs through Cloudflare's single-file purge API. The long absolute cache lifetime remains unchanged: readers retain efficient CDN hits between updates, while a visible publish or expiry invalidates the old cohort. The pending build is also the durable purge journal. A timeout, rejection, or restart leaves it intact and schedules a one-minute retry; a duplicate purge is safe, and a later visible revision remains in the outbox for reconciliation. Unchanged heartbeats and already-current generations issue no purge. The core alone receives the dedicated Cache Purge token and exact zone/origin settings; the public edge Workers receive none of that authority. A bounded purge-pending directory-build metric exposes retry backlog without recording a generation, URL, token, zone, or account identifier.

manifest.json is private coordination metadata even though it lives in the alias bucket; the later static-host allowlist must deny public access to it. R2 cannot replace the four objects atomically. Readers can therefore observe cross-format skew while aliases converge, even though D1 never acknowledges a partial cohort. The static-host rollout must resolve that mismatch with the Game Protocol 1 atomic-alias contract before DNS attachment.

Freshness rollover may create a newer artifact generation for an unchanged D1 revision. A five-minute reconciliation may also publish a newer generation at that revision when aggregate activity changes the deterministic order; the heartbeat itself still creates no build or R2 write. This is required so empty and heartbeat-only directories do not expire. Each body is valid for at most four hours and no later than the earliest backing listing expiry. The latest eight D1-acknowledged immutable four-object cohorts are retained as a bounded rollback window; a durable paginated sweep deletes at most 64 unacknowledged, older, or partial private objects per reconciliation, so a pre-existing backlog converges without an unbounded scan. The outbox holds at most one row per profile during an R2 outage.

Public expiry is rounded down to a conservative 15-minute boundary, and backing presence expires on the same boundary. An artifact therefore never reveals the exact server heartbeat timestamp and never outlives its backing row.

The Game Protocol 1 publisher and renderer consume the frozen protocol schema and fixtures. Signed Game requests use a profile-specific replay/budget ledger, a profile-qualified publication room, and a public row whose constrained shape cannot be confused with classic metadata. Private Game requests retain only minimal presence and delete that row. D1 also accounts for the exact canonical JSON bytes of every public Game row and rejects an aggregate that cannot fit the 262,144-byte protocol artifact. The independent GAME_PUBLISH_ENABLED breaker still ships disabled, while Classic publishing and rendezvous are enabled for supported Classic clients. The publisher remains domainless in checked-in Wrangler configuration until the protected production custom-domain attachment is read back. The Go producer and Rust consumer foundations are released, including opaque origin validator handling. Current Classic directories use schema atrinik-classic-directory-v6 / protocol 6; Game uses atrinik-game-directory-v2. Both publish only accessRequired policy. Historical Classic v4 and v5 documents remain migration evidence. CLASSIC_DIRECTORY_CUTOVER_MODE=v4-production or v5-production stages the new Classic output under canary-v6/; only v6-production selects root aliases. GAME_DIRECTORY_CUTOVER_MODE=v1-production stages Game under canary-v2/; only v2-production selects root aliases. These staging defaults do not serve historical formats. Any actual production cutover requires a separately reviewed operator action. Static authority attachment, cache rules, headers, CORS, CSP, custom-domain isolation, and consumer cutover remain explicit service-split gates. R2's opaque strong ETag and alias upload time satisfy the released validator/Last-Modified model only after the live custom-domain canary confirms the public response.

Observability

Automatic invocation logs are explicitly disabled. Deliberate custom logs are limited to redacted request_rejected, blacklist_match, and unexpected_error objects with closed, low-cardinality fields. Routine success, expected 404, rate-limit, and open-circuit traffic remains silent, so a throttled loop does not replace automatic invocation noise with one custom event per request.

For the rendezvous handler, rendezvous_control_disconnected identifies an unclean close of the active server control, rendezvous_control_error a transport error callback, and rendezvous_handler_failure a caught close/error handler failure. Deliberate retirement, superseded controls, and clean closes remain silent. These fixed codes omit peer close codes, reasons, exception text, and identities; they cannot establish which network participant caused a reset. The real Service Binding regression exercises edge-to-coordinator-to-room signaling after an idle interval and control replacement. Room eviction tests separately verify hibernation behavior; neither test reproduces a production network interruption.

All three deployable configurations also explicitly disable tracing, Workers Logpush, Tail/streaming-tail consumers, and OTLP destinations. Custom logs remain persisted at full sampling for the bounded diagnostics above. Audit account and zone Logpush jobs plus notification policies independently because those account resources are not implied by a Worker script setting.

This setting changes stored log-event volume, not Worker invocation usage. Cloudflare Worker Metrics and zone security analytics remain the sources for aggregate request/status counts, errors, CPU time, wall time, duration, and WAF mitigations. Durable Object metrics supply aggregate WebSocket connection and message activity. Each accepted client session attempts at most one best-effort write of an anonymous, bounded terminal summary to the atrinik_metaserver_rendezvous Analytics Engine dataset; no room-admission rejection or individual frame creates a custom point or room log. The schema and sampling-correct query are documented in docs/privacy.md. Each private builder reconciliation or alarm also emits one best-effort fixed-schema point to atrinik_metaserver_directory; the O(1) nudge emits no point. The schema contains only profile, closed build/retention outcomes, count, bounded duration, and a bounded cleanup count. Static client reads are direct R2/cache traffic and create neither a Worker invocation nor a custom builder point.

License

This project is licensed under the MIT License.

About

Cloudflare Worker providing Atrinik server discovery and QUIC rendezvous

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages