MESH ONLINECODENAME: Purple Rain

Wire Format

This page is the byte-level reference for Net's packet wire format. You won't need it for application code — the SDK does the framing — but you'll want it for debugging packet captures, writing a custom adapter, or building a relay or proxy that needs to read the routing fields without decrypting payloads.

The header is 68 bytes on the wire, 8-byte aligned, and contains every field a forwarding node needs to make a routing decision without decrypting anything. In memory the struct is 72 bytes (aligned to 8); only the first 68 hit the wire.

Header layout

code
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         MAGIC (0x4E45)        |     VER       |     FLAGS     |  4
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   PRIORITY    |    HOP_TTL    |   HOP_COUNT   |  FRAG_FLAGS   |  8
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       SUBPROTOCOL_ID          |        CHANNEL_HASH           | 12
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                         NONCE (12 bytes)                      + 24
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       SESSION_ID (8 bytes)                    | 32
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       STREAM_ID (8 bytes)                     | 40
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       SEQUENCE (8 bytes)                      | 48
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                      ORIGIN_HASH (8 bytes)                    + 56
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       SUBNET_ID (4 bytes)                     | 60
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       FRAGMENT_ID             |        FRAGMENT_OFFSET        | 64
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       PAYLOAD_LEN             |        EVENT_COUNT            | 68
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

All multi-byte integers are little-endian. The total header is 68 bytes; the payload follows, capped at 8,108 bytes; the Poly1305 authentication tag follows the payload, 16 bytes. ORIGIN_HASH is placed before SUBNET_ID so the u64 sits at a naturally 8-aligned offset — putting SUBNET_ID first would force a 4-byte padding gap and grow the in-memory struct to 76 bytes.

Fields

FieldBytesTypePurpose
MAGIC2u160x4E45 (ASCII "NE"). Identifies Net packets.
VERSION1u8Wire version. Current: 1.
FLAGS1u8Packet flags (see below).
PRIORITY1u8Routing priority (0255). Higher is more urgent.
HOP_TTL1u8Time-to-live in hops. Forwarders decrement; zero = drop.
HOP_COUNT1u8Hops traversed. Forwarders increment.
FRAG_FLAGS1u8Fragmentation flags (more-fragments bit, etc.).
SUBPROTOCOL_ID2u16Identifies how the payload is interpreted. See subprotocol-ids.
CHANNEL_HASH2u16xxh3-truncated hash of the channel name. Used for wire-speed authz.
NONCE12bytesAEAD nonce (counter-based).
SESSION_ID8u64Identifies the encrypted session.
STREAM_ID8u64Identifies the stream within the session.
SEQUENCE8u64Per-stream sequence number.
ORIGIN_HASH8u64Full 64-bit BLAKE2s-MAC of sender's ed25519 pubkey (EntityKeypair::origin_hash()). Maps unambiguously to the publisher's NodeId via origin_hash_to_node — even under adversarial collision-grinding (~2^32 work per target).
SUBNET_ID4u32Packed 4-level subnet hierarchy. See subnets.
FRAGMENT_ID2u16Identifies a fragment group for reassembly.
FRAGMENT_OFFSET2u16Byte offset of this fragment in the original payload.
PAYLOAD_LEN2u16Length of the encrypted payload (excluding tag).
EVENT_COUNT2u16Number of events packed into the payload.

Flags

The FLAGS byte is a bitfield:

BitNameMeaning
0RELIABLESender expects acknowledgement; receiver must send back NACK or implicit ack.
1NACKThis packet is a negative acknowledgement.
2PRIORITYHigh-priority path; bypasses fair queueing.
3FINCloses the stream after this packet.
4HANDSHAKECarries Noise handshake material (not yet encrypted).
5HEARTBEATLiveness probe; no payload semantics.
6–7reservedFuture use.

Constants

ConstantValue
Magic0x4E45
Version1
Header wire size68 bytes
Header in-memory size72 bytes (aligned to 8)
Max packet8,192 bytes
Max payload (excl. tag)8,108 bytes
Nonce size12 bytes
AEAD tag size16 bytes (Poly1305)

Encryption

The header is sent in the clear; the payload is encrypted with ChaCha20-Poly1305 AEAD. The 12-byte NONCE field is a per-session counter (not random), keyed independently for transmit and receive directions, ruling out nonce reuse without depending on randomness.

The handshake is Noise NKpsk0:

  • Initiator is anonymous.
  • Responder's static public key is known in advance (out-of-band exchange, certificate, or capability advertisement).
  • Pre-shared key adds symmetric authentication on top of the asymmetric exchange.

When two peers can't talk directly, MeshNode::connect_via(relay_addr) carries the Noise messages inside subprotocol 0x0601 over an existing encrypted session through a relay. The relay sees authenticated Noise bytes but can't forge them or derive the post-handshake session keys.

Session keys

After a successful handshake, each direction has its own key:

code
pub struct SessionKeys {
    pub tx_key: [u8; 32],
    pub rx_key: [u8; 32],
    pub session_id: u64,
}

PacketCipher wraps the AEAD primitive with the per-session monotonic counter for nonce generation.

Fragmentation

The wire MTU is 8,192 bytes. Payloads larger than 8,192 − 68 − 16 = 8,108 bytes are fragmented into multiple packets sharing a FRAGMENT_ID, with each fragment's FRAGMENT_OFFSET indicating its position in the reassembled payload.

The receiving session reassembles fragments by (SESSION_ID, FRAGMENT_ID). Out-of-order fragments are buffered until the group is complete; incomplete groups time out after a configurable interval.

What a forwarder needs

A pure forwarding node (no subprotocol handlers, no application logic) needs to read exactly these fields:

  • MAGIC and VERSION — to confirm it's a Net packet.
  • HOP_TTL and HOP_COUNT — to decrement and drop if zero.
  • SUBPROTOCOL_ID — to apply opaque-forwarding fallback for unknown protocols.
  • CHANNEL_HASH and ORIGIN_HASH — to consult the AuthGuard for wire-speed authorization.
  • SUBNET_ID — to apply gateway visibility rules at subnet boundaries.

None of these require decrypting the payload. The forwarder's decision is a header-only read plus a small number of in-memory lookups (channel registry, auth guard, subnet routing table) — typically under 10 nanoseconds per packet on modern hardware.

Performance characteristics

  • Header read: one cache line. Modern CPUs prefetch and decode in a few cycles.
  • AuthGuard probe: 4 KB bloom filter fits in L1; two atomic reads. ~10 ns on x86-64.
  • AEAD verify (when the packet is for the local node): ChaCha20-Poly1305 of a 1 KB payload, ~250 ns on a modern core.
  • Forwarding latency: dominated by the network path, not by Net's per-packet work. Net contributes single-digit microseconds end-to-end on the same LAN.

The wire format is designed around these properties. If you're writing a packet sniffer, a relay, or a custom adapter, the rule is: do as little as the protocol allows. The header is enough for almost every routing decision.