RakNet
Reliable UDP transport on port 19132 (IPv4) / 19133 (IPv6), RakNet protocol version 11. Still the transport for direct IP connections in 1.26.51.
MAGIC = 00 ff ff 00 fe fe fe fe fd fd fd fd 12 34 56 78 // 16 bytes, present in every unconnected/offline message
Basics
RakNet carries everything before login as plain UDP datagrams and everything after inside reliable frame sets. It provides no encryption of its own — game-layer encryption (AES-256-CTR after the handshake) is applied to the payload of each batch, not to RakNet headers. Multi-byte integers in the offline handshake are big-endian; the 3-byte counters inside frames and ACKs are little-endian. IPv4 addresses are written with each octet bitwise-NOT'd.
IPv4: 04 <NOT(ip[0])> <NOT(ip[1])> <NOT(ip[2])> <NOT(ip[3])> <port uint16 BE> // 7 bytes IPv6: 06 <AF_INET6=23 uint16 LE> <port uint16 BE> 00 00 00 00 <16-byte ip> 00 00 00 00 // 29 bytes
Offline handshake
C → S 0x05 OpenConnectionRequest1 // MTU discovery via padding S → C 0x06 OpenConnectionReply1 // server GUID, security cookie, MTU C → S 0x07 OpenConnectionRequest2 // cookie, server address, MTU, client GUID S → C 0x08 OpenConnectionReply2 --- everything below now rides inside frame sets --- C → S 0x09 ConnectionRequest S → C 0x10 ConnectionRequestAccepted C → S 0x13 NewIncomingConnection C ⇄ S 0x00/0x03 ConnectedPing/Pong keepalives C ⇄ S 0xFE Game packet batches
MTU discovery probes 1492 first, then 1200, then 576 — each request is retried 4 times, 500 ms apart. OpenConnectionRequest1 pads itself with zeros to the probed MTU minus 28 bytes (IP + UDP header), and the server derives the MTU from the datagram size. Modern implementations send a security cookie in OpenConnectionReply1: a CRC32 of (salt ‖ port ‖ IP) that must be echoed in OpenConnectionRequest2, with the salt rotating every 2 seconds. Vanilla clients always provide a negative client GUID.
uint8 id (= 0x05) bytes magic (16) uint8 protocol version (= 11) bytes zero padding ... total datagram length = probed MTU - 28
uint8 id (= 0x06) bytes magic (16) int64BE server GUID uint8 security (0 or 1) uint32BE cookie if security uint16BE mtu
uint8 id (= 0x07) bytes magic (16) uint32BE cookie + 1 skipped byte if secure address server address (see encoding above) uint16BE mtu int64BE client GUID
uint8 id (= 0x08) bytes magic (16) int64BE server GUID address client address uint16BE mtu uint8 doSecurity
0x09 ConnectionRequest: int64BE clientGUID · int64BE requestTime · uint8 secure
0x10 ConnectionRequestAccepted:
address clientAddress · uint16BE systemIndex
address[20] systemAddresses // NOT-encoded IPv4 zeroes for unused slots
int64BE pingTime · int64BE pongTimeA client answering a ping with a different protocol version receives 0x19 IncompatibleProtocolVersion (26 bytes: server protocol byte, magic, server GUID).
Message IDs
| ID | Name | Fields |
|---|---|---|
| 0x00 | ConnectedPing | Time (int64 BE) — keepalive, answered with ConnectedPong |
| 0x01 | UnconnectedPing | Time (int64 BE), Magic, Client GUID — server discovery |
| 0x02 | UnconnectedPingOpenConnections | Handled like UnconnectedPing |
| 0x03 | ConnectedPong | Ping time, pong time (int64 BE) |
| 0x04 | DetectLostConnections | Keepalive variant; answered with ConnectedPing |
| 0x05 | OpenConnectionRequest1 | Magic, protocol version (11), MTU padding |
| 0x06 | OpenConnectionReply1 | Magic, server GUID, security flag (+cookie), MTU |
| 0x07 | OpenConnectionRequest2 | Magic, cookie (+1 byte) if secure, server address, MTU, client GUID |
| 0x08 | OpenConnectionReply2 | Magic, server GUID, client address, MTU, security flag |
| 0x09 | ConnectionRequest | Client GUID, request time, security byte (reliable) |
| 0x10 | ConnectionRequestAccepted | Client address, system index, 20 system addresses, ping/pong times |
| 0x13 | NewIncomingConnection | Server address, 20 system addresses, ping/pong times |
| 0x15 | DisconnectNotification | Sent on graceful close |
| 0x19 | IncompatibleProtocolVersion | Server protocol byte, Magic, server GUID |
| 0x1C | UnconnectedPong | Time, server GUID, Magic, MOTD string (uint16 BE length) |
| 0x80–0xFE | Datagram (frame set) | Flag byte 0x84, sequence number (uint24 LE), frames |
| 0xA0 | NACK | Record count (uint16 BE) + ranges (3-byte LE pairs) |
| 0xC0 | ACK | Record count (uint16 BE) + singles/ranges (3-byte LE) |
| 0xFE | Game Packet batch | Bedrock batch wrapper: compression marker + packets |
Frames & reliability
Connected data is sent in datagrams (frame sets): a flag byte 0x84, a 3-byte little-endian sequence number, then one or more frames. Each frame declares a reliability and may be split into fragments. go-raknet uses five reliability classes; every game packet travels asReliableOrdered.
uint8 flags = 0x84 // datagram | needsBAndAS uint24LE sequence number frame[] frames // up to MTU - 28 bytes of content each
uint8 flags // reliability << 5 | split << 4 uint16BE bitLength // content length in bits uint24LE messageIndex if reliable (2,3,4) uint24LE sequenceIndex if sequenced (1,4) uint24LE orderIndex if ordered/sequenced (1,3,4) uint8 orderChannel always 0 in bedrock uint32BE splitCount if split uint16BE splitId if split uint32BE splitIndex if split bytes content
| Value | Reliability | Behaviour |
|---|---|---|
| 0 | Unreliable | Fire and forget, no extras |
| 1 | UnreliableSequenced | Drops older packets, sequenceIndex present |
| 2 | Reliable | Resent until ACKed, messageIndex present |
| 3 | ReliableOrdered | Reliable + orderIndex/orderChannel (Bedrock's choice) |
| 4 | ReliableSequenced | Reliable + sequenceIndex + ordering |
Split frames reassemble by splitId; servers cap splits at 512 pieces with 16 concurrent split messages. Frame overhead is 14 bytes (plus 10 when split), so the maximum frame content is MTU − 28 − 14.
ACK / NACK
ACK (0xC0) and NACK (0xA0) share one layout: a uint16 BE record count, then records — a 0x01 type byte with a single 3-byte LE sequence number, or a 0x00 type byte with min and max 3-byte LE numbers. ACKs confirm received datagrams, NACKs request retransmission of gaps. Records are capped so a reply fits one MTU; servers expand at most 8192 records.
uint8 flags = 0xC0 // 0xA0 for NACK uint16BE recordCount record[] records 0x01 uint24LE seq // single 0x00 uint24LE min · uint24LE max // inclusive range
0xFE game packets
Minecraft rides inside frames whose content starts with the batch header 0xFE. Everything after that byte is optional ciphertext, then the compression marker, then a stream of length-prefixed packets. RakNet itself never sees individual game packets.
0xFE // batch header
[8-byte checksum] // only when encrypted (see Encryption page)
uint8 compression marker // 0x00 zlib, 0x01 snappy, 0xFF uncompressed
batch: [varuint32 length · packet]* // packet = varuint32 header + body
// header = id | sender<<10 | target<<12The inner header packs the packet ID (10 bits) with sender and target sub-client indices (2 bits each at bits 10–11 and 12–13) for split-screen support. A batch holds at most 812 packets and decompresses to at most 16 MiB.
Timing & limits
| Constant | Value |
|---|---|
| RakNet protocol version | 11 (rejected with IncompatibleProtocolVersion otherwise) |
| MTU probe sequence | 1492 → 1200 → 576, 4 attempts each, 500 ms apart |
| Minimum supported MTU | 576 (probes below 400 clamped) |
| Keepalive | ConnectedPing every 500 ms |
| Connection timeout | 5 s + 2 × RTT without activity |
| Retransmit threshold | RTT + RTT/2 (RTT smoothed over 5 s, default 50 ms) |
| Tick rate | 100 ms (ACK flush every tick, resend check every 300 ms) |
| Receive window / ordered queue | 2048 entries (enforced server-side) |
| Handshake timeouts | 10 s dial, 5 s ping, 10 s half-open grace |
| Close | DisconnectNotification (0x15) after pending resends ACKed |
