Datagrams, no ordering, no retransmission, no head-of-line blocking
A lost position is replaced by the next snapshot 50 ms later; waiting for it would freeze every newer update
TCP
Ordered reliable stream
A single lost segment stalls all later data until retransmitted: fatal for real-time positions
TCP for the login only (chosen)
One short exchange before the game
Login needs reliability and is not time-critical. TCP gives it for free and lets the server issue a session token before any UDP traffic is accepted; it carries no in-game data.
QUIC
Reliable and unreliable streams over UDP
Excellent, but heavy libraries and a lot of protocol we would not use
Binary and explicit (planned, specified in the RFC): fixed-size little-endian
fields written field by field by a BinaryWriter, never by copying structs, so padding
and compiler layout never reach the wire and Windows, Linux and macOS clients agree.
Header on every datagram: magic number, protocol version, message type, sequence
number, acknowledgement and ack bitfield, session id. A wrong magic or version is
dropped immediately.
Inputs, not positions, from clients: a compact bitmask per tick. The server cannot
be told "I am here", only "I pressed up", which removes a whole class of cheating.
Snapshots from the server at 20 Hz; clients interpolate remote entities between
the two latest snapshots to hide the low rate and jitter.
Reliable events over UDP: spawns, deaths and disconnections must arrive. They carry
sequence numbers; receivers acknowledge them with the last sequence plus a 32-bit
bitfield of the previous ones, and the sender resends unacknowledged events after a
timeout. This is the approach popularised by Glenn Fiedler's articles: no extra
connection, and acknowledgements ride on traffic that is sent anyway.
Sequence numbers on 16 bits with wrap-around comparison (RFC 1982 serial number
arithmetic), so the counter can wrap during long games without reordering bugs.
Datagrams of at most 1200 bytes, below common path MTUs, to avoid IP fragmentation
(a lost fragment loses the whole datagram).
Network thread + game thread (planned): Asio runs on its own thread; the
simulation runs at a fixed 60 Hz on another. They exchange messages through
single-producer single-consumer lock-free queues, so the simulation never waits on
a socket and a slow client cannot stall the tick.
Alternatives: one thread per client (does not scale, needs locks around the world
state); everything on one thread with non-blocking sockets (simple but couples the tick
rate to network activity).
Malformed or truncated packets crashing the parser
Every read is bounds-checked against the remaining bytes and returns an error instead of reading past the buffer; lengths are validated before allocation; unknown types are dropped
Oversized packets exhausting memory
Fixed maximum datagram size; no allocation proportional to an untrusted length beyond that limit
Spoofed or foreign UDP traffic
UDP is accepted only from the endpoint bound to a session token obtained through the TCP login; other datagrams are dropped
Flooding a server
Per-IP packet rate limit; excess is dropped and logged, so the tick rate is unaffected
Cheating clients
Server authority: clients send inputs only; movement, firing cooldowns, collisions and scores are computed on the server
One faulty client taking the server down
Per-client error isolation: an exception while handling one client is caught and that client is disconnected
Vulnerable or tampered dependencies
Dependencies pinned by version, commit or SHA-256 (CPM); GitHub Actions pinned by commit SHA
Detection and long-term monitoring
Fuzzing the packet parser with libFuzzer (planned) and dedicated malformed-packet
unit tests.
AddressSanitizer and UndefinedBehaviorSanitizer on every pull request (in place),
catching memory errors before they ship.
Static analysis with clang-tidy (bugprone, cert, cppcoreguidelines...) on every
pull request (in place).
Server logs (planned): rejected packets, rate-limit hits and disconnections are
logged with the source endpoint, so abuse is visible after the fact.
Degraded-network tests with netem (Linux) or clumsy (Windows) (planned): loss,
duplication, 150 ms latency and 5 KB/s cap, as the subject requires.