NetherNet
A WebRTC (ICE + DTLS + SCTP) transport used for LAN games, Realms, and friends' worlds — replacing RakNet there since 1.19.30 and carrying most console play.
1. Obtain TURN credentials + peer network ID from signaling 2. Exchange SDP offer/answer as CONNECTREQUEST / CONNECTRESPONSE 3. Trickle ICE candidates as CANDIDATEADD (or embed them in the SDP) 4. DTLS handshake binds the identity assertion (ES384 JWS over fingerprints) 5. SCTP establishes; the dialer opens both data channels 6. Game packets flow as messages on ReliableDataChannel
Basics
A NetherNet connection is a WebRTC data-channel session built from pion-style ICE, DTLS and SCTP transports. The SDP has a single application/UDP/DTLS/SCTP webrtc-datachannel media section with sctp-port: 5000 and max-message-size: 262144. Unlike RakNet there is no game-layer batch header (0xFE) and no game-layer encryption: the transport disables both, because DTLS already protects the channel.
Signaling
Peers are identified by a 64-bit network ID (LAN) or an address-like string. Signals are plain text: a type, one space, the decimal connection ID, one space, then the data. The SDP offer/answer travels raw (not base64), the connection ID is a random uint64.
CONNECTREQUEST <connectionId> <SDP offer> // client → server CONNECTRESPONSE <connectionId> <SDP answer> // server → client CANDIDATEADD <connectionId> <candidate> // both, trickle ICE CONNECTERROR <connectionId> <error code> // both, signed int32
candidate:<foundation> 1 <udp|tcp> <priority> <address> <port> typ <host|srflx|relay|prflx> [raddr <addr> rport <port>] generation 0 ufrag <ufrag> network-id <n> network-cost 0 // inbound signals tolerate an optional "a=" prefix
Error codes enumerate the whole failure space (0 None, 1 DestinationNotLoggedIn, 2 NegotiationTimeout, 5 ICE, 24 NoSignalingChannel, 37 IdentityNotAllowed, ... 37 values in total). A remote CONNECTERROR tears the connection down immediately; the initiator also gives up 2 s after sending its own error.
How signals travel depends on the scenario: over the Xbox Live signaling WebSocket online, via LAN UDP discovery locally, or via HTTPS endpoints for dedicated servers. The go-nethernet library defines the Signaling interface and ships the LAN and HTTP implementations; the Xbox WebSocket transport is client-side only.
GET /v1/join -> status ping (JSON or empty 200)
POST /v1/join/<networkId> -> body: SDP offer (application/sdp)
response: SDP answer, or a decimal error code
// client candidate order: https://host:port, https://host, http://host:port, http://host
// trickle ICE is force-disabled in this mode (candidates embedded in SDP)LAN discovery
Servers listen on UDP port 7551; clients broadcast a request every 2 seconds. Every packet is a 32-byte HMAC-SHA256 tag followed by AES-256-ECB ciphertext (PKCS7), encrypted with SHA-256(0xdeadbeef as uint64 LE). Decrypted, it reads: uint16 LE total length, uint16 LE packet id, uint64 LE sender network ID, then 8 zero bytes of padding.
0 RequestPacket -- (empty) "who is out there?"
1 ResponsePacket -- uint32 LE length + hex-encoded ServerData
2 MessagePacket -- uint64 LE recipientID + uint32 LE length + signal text
("Ping" / empty data = keepalive)
ServerData (version 7): uint8 version, string serverName, varint32 protocol,
string version, string levelName, varint32 playerCount, varint32 maxPlayerCount,
varint32 gameType (0 survival / 1 creative / 2 adventure), bool editorWorld,
bool hardcore, bool acceptsOnlineAuth, bool acceptsSelfSignedAuth,
string nonce, varint32 connectionType (4 = LAN/NetherNet)The random nonce must be echoed back by the joining client in the Login packet's ClientData — servers drop joins whose nonce is missing, unknown, or expired. Peer addresses expire after 15 seconds of silence.
Identity
NetherNet peers authenticate with ECDSA P-384 (secp384r1) keys carried in JWTs. The client's identity token is RS256-signed by Minecraft's authorization service (issuer domain like authorization.franchise.minecraft-services.net); the server's token is self-signed ES384 with a 1-minute expiry. Both carry the matching public key in a cpk claim (base64 DER PKIX, or a JWK object from 1.26.40 on).
base64( JSON{
"assertion": JSON{ "fingerprints": "<detached ES384 JWS>",
"token": "<identity JWT>" },
"idp": { "domain": "<auth service>", "protocol": "default" }
} )
// the JWS signs { "fingerprint": [ { "algorithm": "sha-256",
// "digest": "AA:BB:..." } ] }
// built from the DTLS certificate fingerprints — it binds the
// identity key to the peer's DTLS certificateData channels
ReliableDataChannel
Ordered & fully reliable (no maxRetransmits). Carries all game packets — the only mode the game uses.
UnreliableDataChannel
MaxRetransmits 0, unordered. Reserved for low-latency data; cannot carry segmented messages.
The dialer (client) opens both channels after SCTP starts; the listener waits for the remote to open both and validates the labels and parameters. A server-opened channel is an error on the client side. ICE roles: the client is controlling, the server controlled.
Segmentation
SCTP messages are capped at the negotiated max-message-size, so large payloads are split: every message is prefixed with a uint8 counter of remaining segments, starting at totalSegments − 1 and reaching 0 on the last slice. Vanilla caps messages at 255 segments and the SDP max-message-size at 262144, giving 262143 bytes of payload per segment. One invalid or out-of-order segment kills the whole connection.
uint8 remainingSegments // totalSegments-1 ... 0 on the last slice bytes data // up to max-message-size - 1 bytes
Game packet framing
Over NetherNet the 0xFE batch header is dropped entirely: each data-channel message after the segment prefix is just the packet body — a varuint32 packet header (id | sender ‹‹ 10 | target ‹‹ 12) followed by the payload, with no batch length, no compression marker and no checksum. Compression still applies where the game requests it, and the Minecraft session layer is transport-agnostic. Vanilla clients report NetherNet server addresses as https://<host>:<port>:<port> (port repeated) in the Login packet.
RakNet: ... 0xFE | checksum | compression | [len][header+payload]* (AES-CTR on top) NetherNet: ... segmentPrefix | [header varuint32 + payload] (DTLS below)
Constants
| Constant | Value |
|---|---|
| LAN discovery port | 7551/UDP, broadcast every 2 s |
| Discovery crypto | HMAC-SHA256 + AES-256-ECB, key = SHA-256(uint64 LE 0xdeadbeef) |
| SCTP port (SDP) | 5000 |
| max-message-size (SDP) | 262144 (262143 payload per segment) |
| Max segments per message | 255 (uint8 remaining counter) |
| Dial timeout | 15 s |
| Error-signal timeout | 2 s |
| Listener negotiation timeout | 15 s (transport start 5 s) |
| Negotiation caps | 64 concurrent, 32 deferred signals per negotiation |
| ServerData version | 7 |
| Connection type (LAN) | 4 |
