Skip to main content

Packet interception

Every active connection has a PacketPipeline. Register a handler with a packet name or ANY_PACKET, which is "*".

Handlers receive relayed PLAY and CONFIGURATION packets in both directions:

  • "c2s" travels from the Minecraft client to the upstream server.
  • "s2c" travels from the upstream server to the Minecraft client.
const { ANY_PACKET } = require("JNbot");

proxy.on("connection", (connection) => {
connection.pipeline.on(ANY_PACKET, (packet) => {
console.log(packet.direction, packet.state, packet.name);
});
});

Read and modify a decoded packet​

packet.data is available when the protocol schema can decode the packet. Assign a replacement object, or call markModified() after changing a nested value.

connection.pipeline.on("chat", (packet) => {
if (packet.direction !== "c2s" || packet.unparsed) return;

packet.data = {
...packet.data,
message: "[proxy] " + packet.data.message,
};
});
connection.pipeline.on("system_chat", (packet) => {
if (packet.unparsed) return;

packet.data.content = "Changed by JNbot";
packet.markModified();
});

JNbot forwards untouched packets as their original bytes. It only serializes a packet again after a decoded change.

Remove or add packets​

drop() prevents the current packet from reaching its target. inject() queues a decoded packet after the current packet. Both operations use the current packet direction.

connection.pipeline.on("chat", (packet) => {
if (packet.direction === "c2s" && packet.data?.message === "/blocked") {
packet.drop();
}
});

connection.pipeline.on("keep_alive", (packet) => {
if (packet.direction === "s2c") {
packet.inject("system_chat", {
content: "The server sent a keep-alive packet.",
overlay: false,
});
}
});

Use Connection.write(direction, name, data) when there is no current packet. It returns false when that side is disconnected or not in PLAY or CONFIGURATION.

connection.write("s2c", "system_chat", {
content: "Sent by the proxy",
overlay: false,
});

Raw packets​

When JNbot cannot decode a packet, packet.unparsed is true and packet.data is null. You can still drop it, inspect packet.raw, replace it with replaceRaw(), or queue raw data with injectRaw().

Raw bytes are decompressed packet bodies that include the packet id. They are specific to the current protocol version.

connection.pipeline.on(ANY_PACKET, (packet) => {
if (!packet.unparsed) return;

console.log("Unknown packet bytes:", packet.raw.length);

if (packet.direction === "s2c") {
packet.replaceRaw(Uint8Array.from([0x00]));
}
});

connection.writeRaw("c2s", Uint8Array.from([0x12, 0x00]));

Do not reuse raw packet bytes across Minecraft versions. JNbot does not translate packet ids or field layouts.