Skip to content
mc-protocol
Esc
↑↓navigate↵open⌘Jpreview
On this page

Building a proxy

Forward packets between players and an upstream server, with optional packet handlers.

Proxy accepts connections from players and opens a separate connection to the upstream server for each player. It forwards packets in both directions by name and re-encodes them for the receiving connection.

import { Proxy } from "mc-protocol";

const proxy = new Proxy({
  listen: { host: "0.0.0.0", port: 25566 },
  upstream: { host: "play.example.net", port: 25565 },
  versions: [776], // optional; protocol numbers accepted from players
});

proxy.Client.on("ChatCommand", async (player, data, next) => {
  console.log(`${player.session.username} ran /${data.command}`);
  await next(data);
});

proxy.Server.on("SetPlayerInventory", (_server, data, next) => {
  if (shouldHideInventory(data)) return next(null);
  return next(data);
});

proxy.Client handles packets sent by players toward the upstream server. Its first argument is the player connection. proxy.Server handles packets sent by the upstream server toward a player. Its first argument is the upstream connection. Both connections expose sendPacket(name, data) for sending additional packets to that endpoint.

Call next(data) to forward a packet, optionally with changed data. Call next(null) to cancel it. If a handler does not call next, the packet is also stopped. Multiple handlers for the same packet run in registration order, so later handlers receive the data passed by the previous handler. For asynchronous work, await or return next(...).

Use proxy.Client.once(name, handler) or proxy.Server.once(name, handler) when a handler should process only the next matching packet.

The proxy forwards login, configuration, compression, and keep-alive packets to the real endpoints. proxy.server exposes the underlying Server, proxy.connections is a Map from player to upstream, and proxy.close() closes the listener and its connections.

Events

Event Arguments When
Proxy:Ready – The player-facing listener is open.
Proxy:Error error The player-facing listener failed.
Proxy:Close – The player-facing listener closed.
connection player, upstream A player connected and an upstream Client was created. The upstream socket may not be ready yet.
disconnect player, upstream The player socket closed. The upstream connection is destroyed.
Upstream:Error player, error The upstream socket or packet parse failed. A socket error also destroys the player.
Client:Error player, error The player connection failed to parse a packet.

Packet interception is proxy.Client.on / proxy.Server.on, not these events. The same table lives on Events.

The player-facing Server accepts the protocols in versions (default: the built-in default). The upstream Client adopts the player’s negotiated protocol when the forwarded Intension is sent, so both sides of a connection use the same packet tables.

Compression is negotiated independently on each connection. By default, forwarding LoginCompression gives the player the upstream server’s threshold. The proxy decodes each incoming packet and re-encodes it with the destination connection’s compression settings, so handlers continue to receive ordinary packet data.

You can rewrite the player-facing threshold without changing the upstream connection:

proxy.Server.on("LoginCompression", (_server, _data, next) => {
  return next({ threshold: 64 });
});

Dropping the announcement with next(null) leaves the player’s compression unchanged (disabled initially); the upstream connection still adopts the original threshold. The proxy’s internal server has autoInit: false, so it does not send its own announcement.

Was this page helpful?