Skip to content

A specification for the NetherNet protocol for Minecraft: Bedrock Edition.

Notifications You must be signed in to change notification settings

lactyy/nethernet-spec

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 

Repository files navigation

nethernet

NetherNet is a new WebRTC-based transport layer protocol that aims to be a replacement for RakNet on Minecraft: Bedrock Edition. It has been in development for the past few years, but is now starting to be rolled out to the public, starting on LAN and Xbox Live games. It cannot currently be used over direct connections.

The protocol is currently not very well documented, so this covers everything needed to implement it. Keep in mind that since this is a new protocol, it is subject to change at any time and thus this document may become outdated. All information here is from reverse engineering v1.21.20 of the game.

LAN discovery

LAN discovery is done on the 7551 port. Clients send a request packet to the broadcast address of the network. Servers broadcast back a response packet with their name, game mode, and other information.

Discovery packets are encrypted and are prefixed with a checksum. The encryption algorithm itself is AES-ECB with the key being the SHA-256 hash of 0xdeadbeef. The checksum is an HMAC with SHA-256 and the same key.

Each discovery packet starts with the packet length (uint16), packet type (uint16), and sender ID (uint64). After that, there is an 8-byte padding, followed by the actual packet data.

The sender ID is just a sort of session ID, a random number generated by the client to identify itself.

func encodeDiscoveryPacket(senderID uint64, pk discovery.Packet) ([]byte, error) {
    buf := new(bytes.Buffer)
    _ = binary.Write(buf, binary.LittleEndian, pk.ID())
    _ = binary.Write(buf, binary.LittleEndian, senderID)
    _ = binary.Write(buf, binary.LittleEndian, make([]byte, 8))
    pk.Write(buf)

    length := len(buf.Bytes())
    payload := append([]byte{byte(length), byte(length >> 8)}, buf.Bytes()...)
    data, err := encryptECB(payload)
    if err != nil {
        return nil, fmt.Errorf("error encrypting: %w", err)
    }

    hm := hmac.New(sha256.New, key[:])
    hm.Write(payload)
    data = append(hm.Sum(nil), data...)
    return data, nil
}

There are three discovery packets that are currently used:

  • DiscoveryRequestPacket (ID 0x00)
  • DiscoveryResponsePacket (ID 0x01)
  • DiscoveryMessagePacket (ID 0x02)

DiscoveryRequestPacket does not have any additional data. It is broadcasted by clients to look for servers on LAN.

DiscoveryResponsePacket sends a hex-encoded ServerData payload. The structure of it is as follows:

  • Version (uint8)
  • Server name (string)
  • Level name (string)
  • Game type (int32)
  • Player count (int32)
  • Max player count (int32)
  • Editor world (bool)
  • Transport layer (int32)

It is used by servers to respond to discovery requests.

DiscoveryMessagePacket is used for negotiating the ICE candidates and everything needed for the connection. It is sent by both the client and the server until the WebRTC connection has been established. It is structured as follows:

  • Recipient ID (uint64)
  • Data (string)

Xbox Live

This section expects you to have some knowledge of how Xbox Live session directory works. This will not cover how to connect and authenticate to it properly.

The Xbox Live session directory on latest versions will return a WebRTCNetworkId in SupportedConnections, like so:

"SupportedConnections": [
  {
    "ConnectionType": 3,
    "HostIpAddress": "",
    "HostPort": 0,
    "WebRTCNetworkId": XXXXXXXXXXXXXXXXXXX,
    "NetherNetId": XXXXXXXXXXXXXXXXXXX
  }
]

This WebRTC network ID is effectively the friend's session ID, used to connect to them.

The connecting client takes this sender ID and establishes a WebSocket connection to wss://signal.franchise.minecraft-services.net/ws/v1.0/signaling/XXXXXXXXXXXXXXXXXXX, where the X's are the client's own session ID. This WebSocket is authenticated with the user's MCToken, which can be obtained from PlayFab. Keep in mind that this token must be valid, and also have the treatment overrides to use this signaling server, otherwise an error with "Player not found." is returned when signaling a message into specific network ID.

Once connected, the server immediately sends back credentials for STUN and TURN servers that can be used if needed. The client is then expected to set up their WebRTC client with these servers and then the WebRTC connection can be negotiated.

The JSON messages sent in this WebSocket connection structures as follows:

  • Type (int)
    • This is one of:
      • RequestType::Ping (0x0)
      • RequestType::WebRTC (0x1)
      • RequestType::TurnAuth (0x2)
  • Message (string)
    • If Type is RequestType::Ping, This will be omitted.
    • If Type is RequestType::WebRTC, This is a signaling message which is described in WebRTC negotiation.
    • If Type is RequestType::TurnAuth, This is another JSON data, which is structured as follows:
      • ExpirationInSeconds (int)
      • TurnAuthServers ([]TurnAuthServer)
        • A list of TURN and STUN servers that can be used, which is structured as follows:
          • Username (string)
          • Password (string)
          • Urls ([]string)
  • To (uint64)
    • The network ID of remote connection. This is only present if sent from client.
    • If Type is RequestType::Ping, This will be omitted.
  • From (string)
    • The network ID of remote connection. This is only present if sent from server.
    • If Type is RequestType::TurnAuth, This is always Server.

WebRTC negotiation

Before the WebRTC connection is made, the details are negotiated with the messages sent either over the LAN DiscoveryMessagePacket or over the signaling server.

Each message is structured as follows: MESSAGETYPE CONNECTIONID DATA

There are four message types used for WebRTC negotiation:

  • CONNECTREQUEST
  • CONNECTRESPONSE
  • CANDIDATEADD
  • CONNECTERROR

The connection ID is a unique ID for each connection.

CONNECTREQUEST just contains the SDP offer from the client. The server responds with a CONNECTRESPONSE containing the SDP answer. After that, the client sends CANDIDATEADD messages with its ICE candidates. Once it has sent around three, the server will send it's own candidates and the connection will attempt to be established.

CONNECTERROR contains the code of the error occurred in the connection. This is one of:

  • None (0x00)
  • DestinationNotLoggedIn (0x01)
  • NegotiationTimeout (0x02)
  • WrongTransportVersion (0x03)
  • FailedToCreatePeerConnection (0x04)
  • ICE (0x05)
  • ConnectRequest (0x06)
  • ConnectResponse (0x07)
  • CandidateAdd (0x8)
  • InactivityTimeout (0x9)
  • FailedToCreateOffer (0x0a)
  • FailedToCreateAnswer (0x0b)
  • FailedToSetLocalDescription (0x0c)
  • FailedToSetRemoteDescription (0x0d)
  • NegotiationTimeoutWaitingForResponse (0x0e)
  • NegotiationTimeoutWaitingForAccept (0x0f)
  • IncomingConnectionIgnored (0x10)
  • SignalingParsingFailure (0x11)
  • SignalingUnknownError (0x12)
  • SignalingUnicastMessageDeliveryFailed (0x13)
  • SignalingBroadcastDeliveryFailed (0x15)
  • SignalingMessageDeliveryFailed (0x16)
  • SignalingTurnAuthFailed (0x16)
  • SignalingFallbackToBestEffortDelivery (0x17)
  • NoSignalingChannel (0x18)
  • NotLoggedIn (0x19)
  • SignalingFailedToSend (0x1a)

In NetherNet, the client connecting will act as the ICE controller, and the server will act as the ICE agent.

Below is an example of a client's SDP offer:

	sdp.SessionDescription{
		Origin:      sdp.Origin{Username: "-", SessionID: rand.Uint64(), SessionVersion: 0x2, NetworkType: "IN", AddressType: "IP4", UnicastAddress: "127.0.0.1"},
		SessionName: "-",
		TimeDescriptions: []sdp.TimeDescription{
			{},
		},
		Attributes: []sdp.Attribute{
			{Key: "group", Value: "BUNDLE 0"},
			{Key: "extmap-allow-mixed", Value: ""},
			{Key: "msid-semantic", Value: " WMS"},
		},
		MediaDescriptions: []*sdp.MediaDescription{
			{
				MediaName: sdp.MediaName{
					Media: "application",
					Port: sdp.RangedPort{
						Value: 9,
					},
					Protos:  []string{"UDP", "DTLS", "SCTP"},
					Formats: []string{"webrtc-datachannel"},
				},
				ConnectionInformation: &sdp.ConnectionInformation{
					NetworkType: "IN",
					AddressType: "IP4",
					Address: &sdp.Address{
						Address: "0.0.0.0",
					},
				},
				Attributes: []sdp.Attribute{
					{Key: "ice-ufrag", Value: iceParams.UsernameFragment},
					{Key: "ice-pwd", Value: iceParams.Password},
					{Key: "ice-options", Value: "trickle"},
					{Key: "fingerprint", Value: fmt.Sprintf("%s %s", fingerprint.Algorithm, fingerprint.Value)},
					{Key: "setup", Value: "actpass"},
					{Key: "mid", Value: "0"},
					{Key: "sctp-port", Value: "5000"},
					{Key: "max-message-size", Value: strconv.Itoa(int(sctpCapabilities.MaxMessageSize))},
				},
			},
		},
	}

Effectively the same thing is done for the SDP answer, except the setup attribute is set to active instead of actpass.

CANDIDATEADD's data follows the standard ICE candidate string format. An example of one is below:

candidate:XXXXXXXXXX 1 udp XXXXXXXXXX 127.0.0.1 12345 typ host generation 0 ufrag +2gl network-id 1 network-cost 10

WebRTC connection

Once the ICE connection is made, the client will also attempt to set up DTLS and then SCTP. Once SCTP is set up, the client will create two data channels:

  • ReliableDataChannel
  • UnreliableDataChannel

Packets themselves are encoded the same as they would be over RakNet, except:

  • Packets are left unencrypted even after a handshake packet.
  • If a packet exceeds 10,000 bytes, it is split into multiple packets.

To expand on the second point, each SCTP message is structured in the following way:

  • Segment count (uint8)
  • Packet (this is what encryption and compression is applied on!)
    • Packet length (varuint32)
    • Encoded packet data ([]byte)

Given that packets are usually under this 10,000 byte limit, you can usually expect zero segments. On the off chance that a packet does exceed this limit, it is split into multiple segments.

This should be treated a sort of promise. If sent a segment count of 3 for example, you should expect the next SCTP message to have a remaining segment count of 2, and so on.

It is unclear how this system works with the UnreliableDataChannel, given that packet drops could leave the packet in an unreconstructed state. As such, it is recommended to only use the ReliableDataChannel for now.

role := webrtc.ICERoleControlling
if err = ice.Start(nil, peerIceParams, &role); err != nil {
    panic(err)
}

if err = dtls.Start(peerDTLSParams); err != nil {
    panic(err)
}

if err = sctp.Start(peerSCTPParams); err != nil {
    panic(err)
}

reliableDataChannel, err := api.NewDataChannel(sctp, &webrtc.DataChannelParameters{Label: "ReliableDataChannel"})
if err != nil {
    panic(err)
}

unreliableDataChannel, err := api.NewDataChannel(sctp, &webrtc.DataChannelParameters{Label: "UnreliableDataChannel", Ordered: false})
if err != nil {
    panic(err)
}

Credits

Special thanks to the following people for help reverse engineering the protocol:

About

A specification for the NetherNet protocol for Minecraft: Bedrock Edition.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published