Skip to content

Socket Protocol

MEMOxiiii edited this page Sep 17, 2026 · 3 revisions

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.

Wire format

  • 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, in socket/packet/id.go). The proxy rejects a client whose AuthRequest.Protocol doesn't match this exactly — bump it on both sides if you ever fork the wire format. Version 3 added RegisterServer.Transport; a proxy on 3 rejects any client still sending 2 with AuthResponseUnsupportedProtocol, 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 / AuthResponse

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

Packet types

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:

RegisterServer

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 / TransferResponse

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).

PlayerInfoRequest / PlayerInfoResponse

Request carries a PlayerUUID. Response carries Status (PlayerInfoResponseSuccess or PlayerInfoResponsePlayerNotFound) plus, on success, XUID and Address.

ServerListRequest / ServerListResponse

Request has no fields. Response carries Servers, a list of {Name string; PlayerCount int64} entries — every registered server, including ones currently failing health checks.

DisconnectPlayer

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.

Server groups and weight

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 (see routing.default_group / routing.fallback_groups in Configuration).
  • Within a pool, both SplitLoadBalancer and GroupedLoadBalancer balance new players in proportion to Weight — a server with twice the weight of another gets roughly twice the players before being considered equally loaded.
  • Leaving Weight at 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.

Draining and health

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.

Clustering and FindPlayerRequest

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).

Building a client from scratch

If you're writing a new client library rather than using an existing one:

  1. Open a TCP connection to network.communication.address (add TLS if network.communication.tls.enabled).
  2. Send AuthRequest{Protocol: 3, Secret: <configured secret>, Name: <your server's unique name>} and wait for AuthResponse.
  3. Send RegisterServer with your server's Address, Transport, LegacyAuth, Group, and Weight.
  4. Handle TransferRequest... actually — TransferRequest is sent by servers, not to them; the packets your client needs to be ready to receive from the proxy are AuthResponse, TransferResponse, PlayerInfoResponse, ServerListResponse, FindPlayerResponse, UpdatePlayerLatency, and DisconnectPlayer.
  5. Reuse the framing/marshal logic in socket/packet/ (via packet.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.

Clone this wiki locally