Open Asterune
Transport and packet delivery

Transport and packet delivery

Examine Asterune's UDP handshake, record protection, reliable lanes, fragmentation, acknowledgements, and backpressure.

Asterune carries game messages over a transport interface that separates socket work from world simulation. Direct connections use UDP. In-process sessions can use loopback through the same delivery interface. The game protocol serializes messages, protects complete records, and assigns delivery semantics before the UDP connection splits those records into datagrams.

Message delivery and replicated state have separate lifecycles. A transport acknowledgement confirms receipt of a fragment, while the game still has to reassemble, decode, and apply its message. State replication explains how the decoded updates become a client's world.

Connection establishment and session keys

The UDP server challenges an unknown address before it starts authorization. The client repeats its connection request with a 16-byte cookie tied to its socket address and a time bucket. The server can verify that cookie without allocating a live connection. This establishes return-path reachability before performing the more expensive join-authority request.

After cookie validation, the server checks protocol compatibility and required capabilities. Authorization then runs asynchronously. Each attempt has a generation number, so a result from an expired or superseded attempt cannot establish a different connection at the same address. The server limits pending attempts and concurrent authorization work separately.

The accepted connection exchanges X25519 public keys. The shared secret feeds HKDF-SHA-256 with separate labels for client-to-server records, server-to-client records, and each direction's packet authentication. A packet key therefore does not double as a record-encryption key.

A session-bound join also binds the client's public key to the session and join token with a keyed proof. Its accept proof covers the client key, server key, player ID, tick rate, capabilities, and protocol version. The client verifies that proof when it has a session-bound accept key. Unbound LAN or self-hosted connections establish keys without that session-bound server-identity proof. The client also rejects an accept with missing required capabilities or an unsupported simulation tick rate.

The transport assigns a peer ID to the established connection. That ID belongs to the connection, while the player ID belongs to the admitted player. They are separate identifiers even when one player occupies one connection.

Encrypted records and authenticated datagrams

Record protection happens before fragmentation. The secure codec serializes a game message with bincode, encrypts its bytes with ChaCha20-Poly1305, and prefixes an 8-byte nonce counter. A 16-byte authentication tag follows the ciphertext. The codec accepts at most 256 KiB of plaintext in one record.

The UDP packet layer independently authenticates the datagram header, packet number, delivery fields, and fragment bytes. It supplies those bytes as associated data to ChaCha20-Poly1305 with an empty plaintext. This outer layer adds integrity protection without encrypting the packet's routing fields. The inner record remains encrypted while its fragments travel separately.

Game protocolGame messageSerialized with bincode
Secure codecProtected record8-byte nonce counterCiphertext16-byte tag
UDP connectionFragmentsAt most 1,100 bytes each
Packet layerDatagramsHeader and packet numberFragment bytes16-byte packet tag
Record protectionEncrypts at most 256 KiB of plaintextChecked after every fragment arrives
Packet authenticationLeaves routing fields readableChecked before reassembly

The secure codec serializes and encrypts a game message into a protected record. The UDP connection splits the record into fragments of at most 1,100 bytes, and the packet layer sends each fragment in an authenticated datagram. The receiver checks the packet tag before reassembly and decrypts the record after every fragment arrives.

A retransmitted fragment travels in a new datagram with a fresh packet number.

The distinction protects reassembly state. The receiver checks the outer tag before interpreting a session packet, accepting a fragment, or acting on an acknowledgement. After all fragments arrive, it checks and decrypts the inner record before exposing plaintext to the game protocol. An authenticated fragment alone cannot be applied as a partial game message.

Both layers have counters and replay windows. Zero is invalid, repeated counters are rejected, and counters at least 4,096 behind the highest accepted counter fall outside the window. A retransmission receives a fresh outer packet number even though it carries the same record fragment. This lets the receiver accept a retry without accepting a replay of the original datagram.

Independent lanes and delivery modes

A connection has 8 lanes. Control, gameplay, state, chat, and bulk traffic use named lanes; the remaining lane numbers are also valid. Each lane owns its reliable message order and its unreliable sequence history.

The delivery modes have different receive rules:

ModeSender behaviorReceiver behavior
Reliable orderedRetains fragments until acknowledged and retries missing fragmentsReassembles messages and delivers them in the lane's message order
Unreliable sequencedSends a single datagram without retriesDelivers only a sequence newer than the lane's last accepted sequenced packet
UnreliableSends a single datagram without retriesDelivers each authenticated payload without a sequence freshness test

A missing reliable fragment holds later reliable messages on that lane. Another lane can continue delivering its own complete messages. There is no single message order across control, bulk, and state lanes, so a state update can arrive before content sent earlier on the bulk lane.

The game protocol assigns modes by meaning. Join data uses reliable bulk traffic; place content and session control use reliable control traffic. Ordinary server physics batches use unreliable sequenced gameplay delivery. Character reports that carry replayable input frames instead use reliable ordered state delivery: dropping one would remove simulation inputs that the server needs. Prediction and reconciliation describes that report path.

Fragmentation and ordered reassembly

A reliable record is divided into fragments containing at most 1,100 bytes. Each fragment identifies its lane, 16-bit message ID, fragment index, fragment count, and transmission sequence. Message IDs advance independently per lane; transmission sequences advance across the connection.

The reliable datagram spends its bytes as follows:

FieldSizeRole
Magic, transport version, and kind3 bytesPacket classification
Packet number8 bytesAuthentication nonce and replay detection
Reliable delivery header7 bytesTransmission sequence, lane, message ID, and fragment position
Fragment payloadUp to 1,100 bytesA slice of the protected record
Packet tag16 bytesOuter integrity authentication

The receiver stores fragments by lane and message ID. Duplicate fragments do not add another copy. A message becomes deliverable only after every fragment arrives and every earlier message in that lane has been delivered. The receiver concatenates the fragments in index order, releases their reassembly bytes, and advances the lane's next-delivery ID.

The receive window accepts messages fewer than 1,024 IDs ahead of the next expected message. Reassembly also has a connection-wide 4 MiB byte budget. A fragment refused for capacity receives no acknowledgement, allowing the sender to retry after the receiver drains earlier work. Invalid fragment counts, inconsistent counts, invalid lane numbers, or oversized fragments count as protocol violations. At 64 violations, the connection marks the peer closed.

Sequence comparisons use wrapping arithmetic over a half-range. The sender bounds outstanding message IDs before they can make old and new IDs ambiguous. An already delivered fragment can still be acknowledged again without delivering its message twice.

Acknowledgements and retransmission timing

An acknowledgement names a transmission sequence plus a 32-bit mask for the preceding sequences. A receive burst wider than that mask can produce multiple acknowledgement packets. The sender removes a fragment when the acknowledgement matches that fragment's most recent transmission sequence.

Each retry assigns a fresh transmission sequence. An acknowledgement for an earlier transmission no longer matches the retried fragment's stored sequence; the new transmission must be acknowledged. The message and fragment IDs stay the same, allowing duplicate detection at reassembly.

The initial retransmission timeout is 200 ms. After a fragment succeeds on its first transmission, the sender takes a round-trip-time (RTT) sample. It does not use retried fragments for RTT estimation because their acknowledgement would make the measured transmission ambiguous.

For subsequent samples, smoothed RTT uses a weight of 1 / 8 for the sample. RTT deviation uses a weight of 1 / 4 for the absolute difference from the previous smoothed value. The base timeout is smoothed_RTT + 4 × deviation, clamped between 200 ms and 2 s. Repeated retries exponentially increase a fragment's waiting time, with the final wait capped at 2 s.

Backpressure and latest-state replacement

Reliable state remains allocated until it is acknowledged. The connection becomes backlogged when unacknowledged payload exceeds 8 MiB or a lane reaches 16,384 outstanding message IDs. The UDP worker treats that condition as a lost connection rather than accumulating an unbounded reliable queue.

The client also has a bounded outbound channel. A full channel returns Backlogged before the socket worker accepts another ordinary send. For replaceable state, a separate latest-value map retains one pending payload per tag. Writing that tag again replaces its unsent value. Ordinary character-state reports without input frames and owned-physics reports use this path. Reports with replayable frames retain every report instead.

Unreliable traffic has a separate queue of at most 512 datagrams. When that queue fills, adding a packet removes its oldest queued packet. This bounds pending lossy traffic without making it reliable. The receiver's sequenced mode can also discard an authenticated packet that arrives after a newer one.

A silent connection sends keepalives at a default interval of 1 s and times out after a default 10 s without valid session traffic. Server transmit flushing and the client's socket poll use 5 ms intervals. These worker intervals do not set the world's simulation or replication frequency.