Intercepting Vanilla Minecraft Network Packets

In this tutorial we will discuss how to intercept vanilla Minecraft network packets.

Approach

While Forge allows us to register and handle custom network packets, it does not provide a way to intercept or modify vanilla Minecraft packets. Fortunately, Forge does provide a way for us to access Minecraft's Netty pipeline directly.

Netty is a library that Minecraft uses to handle its networking. The core component of Netty is the ChannelPipeline. The pipeline consists of a sequence of packet handlers that are each invoked until a packet is handled successfully. We will insert our own handler into this pipeline.

Implementing a packet handler

For this tutorial, we will be extending the SimpleChannelInboundHandler as we will only be handling inbound packets. If you wish to capture outbound packets as well, you can implement a ChannelDuplexHandler. We will also assume that we are implementing a handler for a client.

Our class will start off like this:

// ...
import io.netty.channel.SimpleChannelInboundHandler;
import net.minecraft.network.Packet;

public class CustomHandler extends SimpleChannelInboundHandler<Packet> {
	// ...
}

Let's add the method that will actually handle packets:

@Override
protected void channelRead0(ChannelHandlerContext ctx, Packet msg) throws Exception {
	// ...
}

The Packet class is subclassed by all vanilla Minecraft packets. To intercept a specific packet, first find the packet you want from the protocol specification and then check if the packet is of the correct type using the instanceof operator. We will use the ScoreboardObjective packet as an example:

if (msg instanceof S3BPacketScoreboardObjective) {
	System.out.println("Scoreboard objective changed!");
}

Finally, we need to pass the packet on to further handlers in the pipeline as we are only inspecting it:

ctx.fireChannelRead(msg);

Registering our packet handler

Before we add the code for registering our handler, let's first override the default constructor of our class:

public CustomHandler() {
	super(false);
}

This is necessary as, by default, the SimpleChannelInboundHandler automatically removes packets from memory after they are handled by our method. This is obviously undesirable as handlers later in the pipeline would no longer be able to read the packet.

We will also need to add an annotation to our class:

@ChannelHandler.Sharable
public class CustomHandler extends ...

Packet handlers can only be added once to a pipeline. This annotation ensures that if our handler is already registered, it will reuse the existing one instead of throwing an exception. This also means that your class must not rely on any unsynchronized variables. Furthermore, Minecraft's networking is on a separate thread so to access objects such as the game world you must use scheduled tasks. If your class does not access any state outside of the handler method then that's fine.

Finally, let's add our handler to the pipeline using Minecraft Forge:

@SubscribeEvent
public void connect(FMLNetworkEvent.ClientConnectedToServerEvent event) {
	ChannelPipeline pipeline = event.manager.channel().pipeline();
	pipeline.addBefore("packet_handler", this.getClass().getName(), this);
}

As evidenced by the event name, this method will be invoked whenever the client connects to a server. The first argument to the addBefore method is the name of the handler we want to precede. Here, "packet_handler" refers to Minecraft's packet handler. We cannot append our handler onto the end of the pipeline as Minecraft's handler does not pass on packets it receives. If you'd like to intercept Forge packets as well, you can use "fml:packet_handler" instead.

The second argument is a unique name for our handler. Any descriptive string will do so I chose the name of our packet handler class. The last argument is an instance of our handler.

Make sure to register our class with Minecraft Forge:

@Mod(...)
public class ... {
	@EventHandler
	public void init(FMLInitializationEvent event) {
		MinecraftForge.EVENT_BUS.register(new CustomHandler());
		// ...
	}
}

Now you can test the mod.

Modifying packet fields

Often, a packet's fields will be private or exposed only through a getter method. This makes it impossible to modify packet data. To get around this, we can use reflection.

Let's take the ScoreboardObjective packet as an example. We want to modify any scoreboard objective names so that they display as "Foo". Let's take a look at the class's member fields:

public class S3BPacketScoreboardObjective implements ...
{
	private String objectiveName;
	private String objectiveValue;
	private IScoreboardObjectiveCriteria.EnumRenderType type;
	private int field_149342_c;
	
	// ...
}

By examining Minecraft's protocol specification we can see that we need to modify the objectiveValue field. We will use the ReflectionHelper class from Forge:

S3BPacketScoreboardObjective packet = (S3BPacketScoreboardObjective) msg;
ReflectionHelper.setPrivateValue(S3BPacketScoreboardObjective.class, packet, "Foo", 1);

The first argument takes the class definition of the object we want to modify. The second argument is the object itself. The third argument is the value we want to set the field to. Finally, the last argument is the index of the field we want to modify. Note that the index may change across Minecraft versions.

Handling exceptions

Your packet handler must handle exceptions or else they may not be logged correctly and can cause the game to crash. The easiest way to do this is to catch any exceptions in the method itself:

try {
	// ...
} catch (Exception e) {
	// ...
}

ctx.fireChannelRead(msg);

Alternatively, if you're confident that your code will not throw any unhandled exceptions and you just want a complete stack trace in the event of an unexpected error you can override the exceptionCaught method:

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
	cause.printStackTrace();
	super.exceptionCaught(ctx, cause);
}