An independent C++23 service for CNA, the C++ reimplementation of the XNA 4.0 programming model. It provides what XNA games reached through their online service: accounts and Guide sign-in, gamer profiles and pictures, friends and presence, achievements, leaderboards with Ranked arbitration, messages and player reviews, parties, avatars, the online session directory with invitations and host migration, an event channel that pushes change hints, and the relay that carries online NetworkSession traffic.
It is not Xbox LIVE compatible. Protocol, accounts, assets and binaries are CNA's own; no Xbox LIVE wire format, service binary or proprietary asset is used or reproduced. Original code is MIT (LICENCE); dependencies keep their own licenses (THIRD_PARTY.md).
Specifications: control protocol v1 (every operation, argument, limit and error)
and relay protocol v1. CNA vendors their headers, parser and golden vectors
and checks them for drift (tools/net/check_service_protocol.py in CNA).
The hello operation advertises these capabilities; CNA refuses to use a feature whose capability
is missing.
| Capability | What it covers |
|---|---|
identity, authentication, session-refresh, heartbeat |
Title-scoped sign-in (scrypt verifiers), one-hour access tokens, rotating 30-day refresh families with replay revocation, auth.ping |
friends, friend-requests, presence, presence-status |
Mutual friend requests, per-title rich presence, online/away/busy, joinable and invitation flags |
game-defaults, gamer-zone |
Account-wide XNA GameDefaults; gamer zone; reputation from player reviews |
achievements, assets, files |
Title achievement catalogs with pictures; immutable hash-addressed PNG/GLB assets, read in chunks or as raw bytes (GET /cna/v1/files/<sha256>) |
leaderboard-reads, local-leaderboard-commit, leaderboard-epoch-abort, ranked-arbitration |
Paged, centered and restricted reads; commits at XNA gameplay boundaries; Ranked rounds reconciled from every machine's report |
messages, player-reviews |
Guide messages and the player review pane |
avatars, avatar-catalog-packs |
One validated avatar description and revision per account, negotiated per client: the stored avatar when the client has (or will install) its catalog, else a marked projection; each catalog version described as one installable pack |
session-directory, session-removal, host-migration, session-add-members, session-invitations, join-friend |
PlayerMatch and Ranked directory, RemoveFromSession, host migration, AddLocalGamer, persistent invitations, joining a friend's or party member's game |
parties |
Account-level parties of up to 8 friends: invitations, members with presence and joinable games (XNA PartySize, ShowParty, ShowPartySessions, SendPartyInvites) |
leaderboard-list, title-version |
The title's boards for the Guide's leaderboard page; a title's oldest accepted game version |
events |
The account event channel (WSS /cna/v1/events): hints invitations, messages, friends, party, sent after the request that changed something succeeded; clients re-read and keep polling as the fallback |
relay-tickets, relay |
One-use tickets and the WSS relay for ENet datagrams (game data and NetworkSession voice alike) |
Push is hints only: the session directory, presence, leaderboards and achievements are read, not pushed, and a lost hint costs one poll interval. Voice media is not a control-plane feature: the control protocol has no voice operation, and CNA's NetworkSession voice travels through the relay as ordinary ENet datagrams the server never inspects.
Not provided: Xbox LIVE compatibility of any kind; account self-registration or password change
over the wire (the administrator provisions accounts; gamer pictures are set by the administrator,
mottos and regions not at all); TrueSkill computation; time windows for
the ...Recent leaderboard keys; direct peer-to-peer connections (every online datagram goes
through the relay); a store (Marketplace), PartnerToken and title update delivery. Linux is the
tested server host; Windows and macOS builds are unvalidated. CNA's
docs/gamer-services-known-limitations.md lists every limitation, client and server.
Dependencies: OpenSSL >=3, Boost >=1.74 (headers, Beast), SQLite >=3.38, nlohmann/json >=3.11, all from the system.
cmake -S . -B build -G Ninja && cmake --build build --parallel
build/cna-gamer-services-server --database service.sqlite3 --listen 0.0.0.0 --port 47831 \
--cert certificate.pem --key private-key.pem
# Development only, plain HTTP on a numeric loopback address:
build/cna-gamer-services-server --database service.sqlite3 --insecure-loopbackCNA finds the service through CNA_GAMER_SERVICES_ENDPOINT (for example
https://games.example.org:47831/cna/v1), CNA_GAME_ID (a title ID registered below) and, for a
private CA, CNA_GAMER_SERVICES_CA_BUNDLE; see CNA's docs/gamer-services-server.md.
build/cna-gamer-services-admin <database> <command> ... works on the same database as a running
server and never prints a credential. Passwords and JSON documents come from stdin.
| Command | Effect |
|---|---|
title <id> <name> |
Registers a title; clients name it in every request |
title-minimum-version <id> <version> |
The oldest game version the title accepts (1.2.0; empty accepts every version): older clients, and clients that state none, get UPDATE_REQUIRED (XNA GameUpdateRequiredException) |
user <username> <gamertag> |
Creates an account; password on stdin (use a secret input, shell tracing off) |
achievement <title> |
Adds an achievement from JSON on stdin (key, name, description, howToEarn, score, optional picture hash) |
leaderboard <title> / seed-leaderboard <title> |
Defines a board / inserts a fixture row, JSON on stdin |
asset <title> <mime> <file> |
Imports a PNG (<=512x512, <=512 KiB) or GLB (<=16 MiB); prints its hash |
privilege <username> <name> <value> |
Sets an XNA GamerPrivileges value for an account, the operator's stand-in for parental controls: communication, profileViewing or userContent to everyone, friends or blocked; trade, purchase or premium to allowed or blocked. It reaches the player at their next sign-in, and the service enforces it at once (protocol/v1.md, Privacy) |
picture <username> <hash> |
Sets a gamer picture |
avatar-catalog <directory> |
Imports and fully validates a CNA avatar catalog (assets/avatars/v1, v2, v3) |
avatar <username> random [male|female], clear or set |
Gives an account an avatar (set reads the description hex from stdin) |
game-defaults <username> |
Sets the account's XNA GameDefaults, JSON on stdin |
inspect / inspect-online <title> |
Counts only: users, titles, sessions, earned; directory, invitations, relay records |
revoke-user <username> |
Revokes every credential of an account |
expire-access <username> |
Expires access tokens but keeps refresh authority (maintenance tests) |
reset-earned <title> / reset-online <title> |
Clears a title's earned achievements / its directory sessions, memberships and invitations (accounts, scores and abuse counters stay) |
Files. One SQLite database in WAL mode (service.sqlite3, -wal, -shm) plus the TLS key.
Restrict both to the unprivileged service user. Back up with SQLite's online backup
(sqlite3 service.sqlite3 ".backup backup.sqlite3"), never by copying the file while it runs.
Opening a database migrates it transactionally to the current schema (20); a newer schema is
refused and nothing is deleted to resolve it. Back up before upgrading the server.
TLS. TLS 1.2 or newer with the given chain and key; clients verify chain and hostname. A new
certificate needs a restart. Clients reconnect by themselves, and live online sessions resume their
relay connections (tested by service_cna_session_restart).
Live communication policy. Capability policy-refresh adds current privileges and the
account's complete blocked list to authenticated auth.ping responses. Current CNA clients
publish these snapshots into existing signed-in gamers on Dispatcher.Update (normally a 30-second
heartbeat); local mute remains independent. Policy responses can contain up to 1024 block tags,
while incoming request arrays retain their 256-item bound. Voice payloads remain opaque ENet relay
traffic: the relay authorizes current session membership/credentials, and cooperative CNA clients
apply communication policy before capture/playback. This is not server inspection of voice content
or immediate packet-by-packet privacy revocation. During service outages clients retain their last
known policy; a deployment requiring centrally enforced instant voice revocation needs a separate,
explicit authority model.
Durability. Commits use synchronous=NORMAL: crash-safe, but a power loss may roll back the
last moments of leases, presence and request bookkeeping. Everything a player would notice losing
(credentials and revocations, awards, scores, friends, messages, profile and avatar edits) commits
with synchronous=FULL.
Limits. 256 control connections, at most 32 from one address; 1024 authenticated relay machines, counted apart. A connection carries up to 1000 requests, idling at most 5 s between them; a CNA client holds one only around its bursts, so an address's 32 connections are shared by the players behind one NAT address rather than owned by 32 of them. Sign-in and refresh: 10 per minute per address. Messages up to 64 KiB. Request IDs that guard mutations against replay: 1,000,000 per title and 20,000 per account per day (reads, heartbeats, lease and presence updates record none). Past 32 live sign-ins an account's oldest refresh family is signed out. The server raises its descriptor limit to what the host allows (up to 65536) and keeps serving when descriptors run out.
Monitoring. One line a minute on stdout, never with a credential, address or request body:
stats AUTHENTICATION_FAILED=1 OK=5412 RATE_LIMITED=3 refused=0 control=41 relays=12 events=37
It gives response counts per error code since the previous line, connections refused by admission,
open control connections, attached relay machines and open event channels. INTERNAL_ERROR means storage failed (disk
full, a locked database); the response to the client never carries details. SIGINT and SIGTERM
stop the server.
One process. The service is one process with one SQLite writer and in-memory hubs for relay
channels and event channels. It does not scale across machines: a second node would need shared
relay routing, event delivery between nodes and distributed session ownership, not only a shared
database. A second server process on the same database refuses to start
(DATABASE_IN_USE): the first holds an operating-system lock on <database>.lock -- flock on
Linux and macOS, the file opened without sharing on Windows -- which the system releases when the
process ends, crash included, so no stale lock is ever left behind (tests/DatabaseLockTests.cpp;
measured on Linux, and the Windows code under Wine, not on Windows itself). The admin tool
deliberately takes no lock and works beside a running server. Back up a running server with
SQLite's online backup, which is consistent under WAL: sqlite3 service.sqlite3 ".backup service-backup.sqlite3".
| Limit | Applied | Counted |
|---|---|---|
| 256 connections in the shared pool (control requests, and relay upgrades not yet authorized; an authorized relay leaves the pool) | at accept, before the TLS handshake | server-wide |
| 32 connections held in that pool | at accept, before the handshake | per source address |
| 600 new connections a minute | at accept, before the handshake | per source address |
| TLS handshake must finish within 10 s | during the handshake | per connection |
| 10 sign-ins (and refreshes) a minute | after the handshake, per request | per source address (at most 4,096 addresses tracked; beyond that, unknown addresses are refused) |
| 20,000 recorded requests a day per account and title; 1,024 relays; 4,096 event channels | after authentication | per account / server |
A refused connection is closed at once and costs no handshake, so one address can cost the server at most 32 concurrent handshakes and 600 a minute, and can hold at most 32 of the 256 pool slots. Many addresses are not limited together: there is no server-wide handshake rate. Eight or more addresses that open connections and stall their handshakes can keep the pool full (each slot is freed after 10 s), and a large number of addresses completing handshakes costs CPU without any bound in the server. That is a distributed flood; the server does not defend against it and CNA does not claim DDoS protection. Put a firewall, L4 proxy or DDoS filter in front (see below).
The server is built to face untrusted clients, but a public deployment still needs these, in order:
- An account of its own. Run it as an unprivileged user that owns only the database directory
and the TLS key (mode 0600); under systemd,
NoNewPrivileges=yes,ProtectSystem=strict,ReadWritePaths=the database directory,Restart=on-failure. - A public certificate for the DNS name the games use, with its intermediates in the chain file. Clients verify the chain and the hostname against the system trust store (or the CA bundle a title ships). A renewed certificate needs a restart: schedule it; clients and relays reconnect.
- One open port. Control requests, the relay and the event channel share the TLS port. The server terminates TLS itself: put an L4 (TCP) filter in front, not a TLS-terminating proxy, which would also make every player appear to come from the proxy's address and share its per-address limits.
- Flood protection in front. The server's own limits are per address and stop one address (32 connections, 600 new ones a minute, both before the TLS handshake; 10 sign-ins a minute). Nothing in the server limits many addresses together -- not their connections, not their TLS handshakes -- so a distributed flood needs a firewall, connection-rate limiting or a DDoS filter upstream (see "Connection and rate limits, precisely").
- Titles and accounts. Create each title (
title), import its achievements, leaderboards and avatar catalogs, and settitle-minimum-versionwhen old game builds must update. Accounts are created by the operator (user); there is no self-registration and no password reset by mail. - Backups with SQLite's online backup on a schedule, a restore tried once, and a backup taken before every upgrade (migrations only go forward).
- Monitoring of the per-minute statistics line: alert on
INTERNAL_ERROR, onrefusedrising and onRATE_LIMITEDbursts. - Capacity. One process, one machine (see below): about 1,200 requests a second in the committed
measurement (
benchmarks/), while an idle player costs about two a minute.
tests/service_benchmark.py provisions a scratch title, starts the server and drives it from one
process per player, each from its own loopback address. Scenarios: steady (ordinary
authenticated mix, no think time), logins (the same plus eight clients signing in continuously),
idle (one address holding 160 idle connections), replay (the title's request-ID table filled to
200,000) and descriptors (a server limited to 48 descriptors facing 80 idle connections).
cmake -S . -B build-probe -G Ninja -DCMAKE_BUILD_TYPE=Release && cmake --build build-probe
python3 tests/service_benchmark.py build-probe --json result.jsonThe evidence is committed in benchmarks/: each run's commit, build, compiler,
CPU, OS, load, arguments and every scenario's numbers. The current one
(benchmarks/2026-09-30-824decd.json: Release, GCC 14.2, AMD Ryzen 7 PRO 7840U with 16 threads,
Debian 13, loopback, 64 players, 15 s windows, load average about 5-9 from other work):
| Scenario | Result |
|---|---|
| steady | 1,210 req/s, p50 48 ms, p99 118 ms |
| sign-in storm (8 clients signing in continuously) | 1,013 req/s, p99 411 ms |
| one address, 160 idle connections | 1,161 req/s, no failures |
| request-ID table at 90,000 | 1,335 req/s |
| descriptors exhausted | keeps serving |
No errors in any scenario. These are one shared machine's figures at one moment: the same run minutes earlier, under a load average of 13-17, measured half as much.
The audit fixes of 2026-09-29 were measured before and after on the same machine; that output was
not kept, so these figures are history rather than evidence: steady 73 -> 1,602 req/s, sign-in
storm 42 -> 1,193, 160 idle connections from one address locked 60 of 64 players out -> no
failures, a full request-ID table 188 -> 1,964 req/s at 200,000 IDs (--keep-alive --replay-fill 200000), and a server out of descriptors exited -> keeps serving.
These are closed-loop saturation figures. A player idling in a menu costs about two requests a minute (the heartbeat), so the real ceiling depends on what games do between heartbeats. Password sign-in is expensive by design; returning players renew with refresh tokens, which cost no derivation.
| Finding | Status |
|---|---|
| Every request ran on one of two network threads under one global lock, and sign-in held that lock through its scrypt derivation: one sign-in stalled every request, TLS handshake and relay datagram | Fixed 171b8cf: worker pools; derivation outside the lock |
An accept error (EMFILE) ended the whole server |
Fixed 171b8cf |
| Two or three fsynced commits per request (5.4 ms each on the reference disk) capped the server near 73 req/s | Fixed 5b71f19: durability classes, one bookkeeping transaction |
| The 30 s heartbeat spent a title's 100,000 daily request IDs after about 35 always-on players; one account could lock a title for a day; the ID count scanned the whole table on every request | Fixed 5b71f19: IDs only for mutations, per-account budget, cached count |
| A sign-in burst queued ordinary requests behind scrypt | Fixed 512e0b9: separate sign-in pool |
| 128 connections shared by HTTP and relays: one host with idle connections locked everyone out, and the server relayed at most 96 machines | Fixed fb43d9a: per-address admission, relays counted apart, 1024 relays |
| A TCP and TLS handshake per request | Fixed dcca1b4 (server) and CNA's reused curl handle |
| A 33rd sign-in within 30 days was refused, locking out players on clients without credential storage | Fixed 541679c: the oldest family is signed out |
| No operational output at all | Fixed cb1d6e4: per-minute statistics line |
| One process, one SQLite writer, in-memory relay and event hubs: no horizontal scaling | Retained |
| Floods from many addresses can still fill the 256 control connections; no handshake rate limit | Retained: needs a firewall or proxy in front |
| The per-account request budget lives in memory and starts over when the server restarts | Retained |
| A new TLS certificate needs a restart (clients and relays reconnect) | Retained |
| Clients poll; there is no push | Fixed e3e78a8: an account event channel sends hints, clients still poll as the fallback |
| Players behind one NAT address share its 32 control connections and its 10 sign-ins a minute (a LAN party signing in at once waits) | Retained: per-address limits are the flood protection; keep-alive idles only 5 s |
ctest --test-dir build --output-on-failure runs the unit suites (service, directory,
invitations, arbitration, avatars, social, relay protocol/authority/flow), the TLS end-to-end test
with separate Python clients and restart persistence, the WSS relay test (Python websockets,
pinned in tests/requirements.txt) and the benchmark smoke pass.
The tests that drive real CNA processes need CNA's harnesses from a CNA build; each one skips (exit 77, never a pass) when its harness is not configured:
CNA=/absolute/path/to/CNA; B=$CNA/cmake-build-debug
CNA_SERVICE_CLIENT_HARNESS=$B/cna_service_client_harness \
CNA_SERVICE_C_API_HARNESS=$B/cna_c_api_service_client \
CNA_SERVICE_DIRECTORY_CLIENT_HARNESS=$B/cna_service_directory_client_harness \
CNA_SERVICE_RELAY_CLIENT_HARNESS=$B/cna_service_relay_client_harness \
CNA_SERVICE_SESSION_CLIENT_HARNESS=$B/cna_service_session_client_harness \
CNA_SERVICE_AVATAR_CLIENT_HARNESS=$B/cna_service_avatar_client_harness \
CNA_AVATAR_CATALOGS=$CNA/modules/gamer-services/assets/avatars \
ctest --test-dir build --output-on-failureThey cover Guide sign-in, social flows, pictures and rich presence through the standard XNA API
(service_tls_e2e); the directory and public BeginFind (service_cna_directory); raw and owned
ENet over the relay (service_cna_relay, service_cna_owned_enet); the public NetworkSession
lifecycle for PlayerMatch and Ranked (service_cna_session), invitations (service_cna_invite),
service restart mid-session (service_cna_session_restart), host migration and host crash
(service_cna_session_migration, service_cna_session_host_crash), AddLocalGamer
(service_cna_session_add_gamer) and avatars with an installed catalog pack and a projection (service_cna_avatars).
The _nat variants and the host-crash and AddLocalGamer tests run each CNA process in its own
rootless user and network namespace behind an outbound-only NAT helper, with identical private
addresses and no inbound ports, so game data can only travel through the relay. They need
unprivileged user namespaces, unshare, ip and a slirp4netns binary, which is executed, never
linked:
CNA_SERVICE_SLIRP4NETNS=/absolute/slirp4netns \
CNA_SERVICE_SLIRP_LIBRARY_PATH=/optional/unpacked/library/path \
ctest --test-dir build -R '_nat$|host_crash|add_gamer' --output-on-failureTested with Debian slirp4netns 1.2.1 and libslirp 4.8.0 unpacked outside the repository; their licenses are in THIRD_PARTY.md. This is NAT-isolation evidence on one Linux host, not a measurement of public Internet latency or failover: public deployment is documented (above) but has not been independently qualified between genuinely remote networks.
Import each catalog CNA ships, then give accounts avatars:
build/cna-gamer-services-admin service.sqlite3 avatar-catalog $CNA/modules/gamer-services/assets/avatars/v1
build/cna-gamer-services-admin service.sqlite3 avatar-catalog $CNA/modules/gamer-services/assets/avatars/v2
build/cna-gamer-services-admin service.sqlite3 avatar-catalog $CNA/modules/gamer-services/assets/avatars/v3
build/cna-gamer-services-admin service.sqlite3 avatar alice random maleThe service stores one description and revision per account, never avatar geometry per account.
A CNA client draws the catalogs of its own release locally. When an avatar names a catalog the
client lacks, the client installs that catalog as one pack (avatars.catalogPack, then the
manifest and the files it lacks through the file route), validates it whole and activates it at
once; a client that declines updates is given a projection onto a catalog it has.
service_cna_avatars proves both with the standard XNA API, and service_avatar_validation uses
CNA's catalogs as golden fixtures while refusing malformed models and manifests.