-
Notifications
You must be signed in to change notification settings - Fork 0
Socket Protocol
Backend servers talk to Portal over a binary TCP protocol on network.communication.address (default :19131, see Configuration). This is what Client Libraries implement for you — read this page if you're integrating custom server software that doesn't have a library yet, or if you're building your own client from scratch.
- Frame: 4-byte little-endian length prefix + payload
- Packet header: 2-byte little-endian packet ID
- Strings: varuint32 length prefix + UTF-8 bytes
- UUIDs: 16 bytes (big-endian)
- Integers: little-endian
-
Protocol version:
3(packet.ProtocolVersion, insocket/packet/id.go). The proxy rejects a client whoseAuthRequest.Protocoldoesn't match this exactly — bump it on both sides if you ever fork the wire format. Version3addedRegisterServer.Transport; a proxy on3rejects any client still sending2withAuthResponseUnsupportedProtocol, so upgrade the proxy and every backend library together.
A connection must send AuthRequest before any other packet is accepted; packets that require auth get an AuthResponse{Status: AuthResponseUnauthenticated} back instead of being processed until then.
AuthRequest carries Protocol (uint32), Secret (string, must match network.communication.secret), and Name (string) — the name this connection registers under. It must not already be in use by another connected client.
AuthResponse.Status is one of:
| Value | Meaning |
|---|---|
AuthResponseSuccess (0) |
Authenticated |
AuthResponseUnsupportedProtocol (1) |
Protocol didn't match packet.ProtocolVersion
|
AuthResponseIncorrectSecret (2) |
Secret didn't match network.communication.secret
|
AuthResponseAlreadyConnected (3) |
Another connection is already authenticated with this Name
|
AuthResponseUnauthenticated (4) |
Sent instead of handling any packet that required auth before AuthRequest succeeded |
| ID | Packet | Direction | Description |
|---|---|---|---|
0x00 |
AuthRequest | Server → Proxy | Authenticate with secret and register a connection name |
0x01 |
AuthResponse | Proxy → Server | Authentication result |
0x02 |
RegisterServer | Server → Proxy | Register server address, transport, legacy-auth flag, group and weight |
0x03 |
TransferRequest | Server → Proxy | Request player transfer |
0x04 |
TransferResponse | Proxy → Server | Transfer result |
0x05 |
PlayerInfoRequest | Server → Proxy | Query player XUID/IP |
0x06 |
PlayerInfoResponse | Proxy → Server | Player info result |
0x07 |
ServerListRequest | Server → Proxy | Request all servers (including unreachable ones) |
0x08 |
ServerListResponse | Proxy → Server | List of servers + player counts |
0x09 |
FindPlayerRequest | Server → Proxy | Find player on network |
0x0A |
FindPlayerResponse | Proxy → Server | Player location result |
0x0B |
UpdatePlayerLatency | Proxy → Server | Player latency update |
0x0C |
DisconnectPlayer | Proxy → Server | Ask a server to drop any stale session it holds for a player, sent right before a transfer |
0x0D |
SetServerDraining | Server → Proxy | Mark/unmark the sending server as draining for load balancing |
Each packet's exact fields live under socket/packet/ in the source, one file per packet. The ones worth knowing beyond their name:
Fields: Address (string), Transport (string, "raknet" or "nethernet"; empty is treated as "raknet"), LegacyAuth (bool — PocketMine-based servers need true, GeyserMC/Dragonfly need false), Group (string, may be empty), Weight (uint32, 0 treated as 1).
Address's format depends on Transport: a "host:port" pair for "raknet", or the full URL of the server's own NetherNet signaling endpoint (e.g. "http://host:port") for "nethernet". The proxy validates this at registration time and rejects a mismatch (e.g. a bare "host:port" with Transport: "nethernet") immediately with a clear error, rather than accepting it and failing later inside a health check or a transfer. See Backend Transports for the full picture, including the Dragonfly-side config this must be kept in sync with.
The server's registered name is not part of this packet — it's the Name the connection already authenticated with in AuthRequest.
TransferRequest carries PlayerUUID and the target Server name. TransferResponse.Status is one of TransferResponseSuccess, TransferResponseServerNotFound, TransferResponseAlreadyOnServer, TransferResponsePlayerNotFound, or TransferResponseError (in which case an Error string is also sent, describing what went wrong).
Request carries a PlayerUUID. Response carries Status (PlayerInfoResponseSuccess or PlayerInfoResponsePlayerNotFound) plus, on success, XUID and Address.
Request has no fields. Response carries Servers, a list of {Name string; PlayerCount int64} entries — every registered server, including ones currently failing health checks.
Carries PlayerName. Sent by the proxy to the target server right before completing a transfer, so that server can clean up a stale session for that player if one already exists (e.g. from a previous, interrupted connection) before the fresh one arrives.
RegisterServer carries Group (string) and Weight (uint32, 0 treated as 1).
- Servers registering with the same group name are treated as a pool by a
GroupedLoadBalancer(seerouting.default_group/routing.fallback_groupsin Configuration). - Within a pool, both
SplitLoadBalancerandGroupedLoadBalancerbalance new players in proportion toWeight— a server with twice the weight of another gets roughly twice the players before being considered equally loaded. - Leaving
Weightat its default keeps a plain even split, so weighting is opt-in whether you run one server or a large fleet of mixed-capacity machines.
A draining server (set via SetServerDraining, or the Admin Console's drain command) is skipped by both load balancers when picking a server for a newly joining player — players already connected to it are unaffected. This is the standard way to take a server out of rotation ahead of a restart.
The same skip logic applies to a server currently failing health checks (see health_check.* in Configuration) — it's automatically pulled out of, and later back into, load balancing without any action from the backend.
FindPlayerResponse also carries a Proxy field, populated when cluster.enabled is set (see Clustering) and the player was found on a different proxy instance via the shared Redis backend rather than this proxy's local session store.
Clustering only answers "is this player online, and where" across proxies — it does not implement proxy-to-proxy transfer routing. A backend server still needs its own way to act on a remote result (e.g. relaying a message rather than attempting to transfer the player directly).
If you're writing a new client library rather than using an existing one:
- Open a TCP connection to
network.communication.address(add TLS ifnetwork.communication.tls.enabled). - Send
AuthRequest{Protocol: 3, Secret: <configured secret>, Name: <your server's unique name>}and wait forAuthResponse. - Send
RegisterServerwith your server'sAddress,Transport,LegacyAuth,Group, andWeight. - Handle
TransferRequest... actually —TransferRequestis sent by servers, not to them; the packets your client needs to be ready to receive from the proxy areAuthResponse,TransferResponse,PlayerInfoResponse,ServerListResponse,FindPlayerResponse,UpdatePlayerLatency, andDisconnectPlayer. - Reuse the framing/marshal logic in
socket/packet/(viapacket.NewPool(),Header.Read/Write) as a reference implementation rather than re-deriving the wire format by hand — see Client Libraries for existing implementations to read alongside this page.
Getting started
Integrating a backend
Embedding Portal