Protocol-aware filtering: a deep dive

2026-01-20 · 8 min read

What is protocol-aware filtering?

Most DDoS protection works at the network level — counting packets, tracking connection rates, and blocking IPs that generate too much traffic. This is effective against volumetric floods but blind to application-layer attacks.

Protocol-aware filtering adds a layer of understanding. Instead of treating every TCP connection to port 25565 the same, we parse the Minecraft protocol and make decisions based on what the client is actually doing. Is it a valid handshake? Does the protocol version match? Is the username in a known bot pattern?

This is the difference between a bouncer who counts how many people are walking in the door and a bouncer who checks IDs.

Minecraft Java Edition

Java's protocol starts with a handshake packet that contains: the protocol version (which Minecraft release the client claims to be), the server address (which hostname the client is trying to reach), the server port, and the next state (status query or login).

We validate all four fields. A client sending protocol version 0 is obviously not legitimate. A client claiming to connect to a hostname that doesn't match any configured domain is either misconfigured or probing. A client that sends a handshake but never follows up with a login start is likely a scanner.

After the handshake, legitimate clients send a Login Start packet with their username. We evaluate this against your firewall rules before forwarding. If you've blocked the username pattern ^Bot_\d+$, the connection is dropped here — your server never even sees the login attempt.

For status queries (the server list ping), we answer them at the edge using cached MOTD data from your backend. This means your server doesn't process any ping traffic at all, which is significant during attacks when bots often hammer the status endpoint.

Minecraft Bedrock and RakNet

Bedrock's protocol is fundamentally different. It runs on UDP using the RakNet networking library, which provides reliable delivery, ordering, and fragmentation on top of UDP's unreliable, unordered packets.

The RakNet handshake has three stages: Unconnected Ping, Open Connection Request 1, and Open Connection Request 2. Each stage must complete correctly before the next one starts. We track this state for every source IP.

A legitimate Bedrock client follows this sequence exactly. An attacker sending flood traffic — random UDP packets, repeated pings, or connection requests without completing the handshake — deviates from the expected state machine and gets dropped.

We also assign internal session IDs to every established connection. Once a client completes the RakNet handshake, all subsequent packets must carry the correct session ID. Packets with invalid or missing session IDs are dropped, which prevents session hijacking attacks.

Geyser cross-play

Geyser presents a unique challenge: both Java TCP and Bedrock UDP traffic arrive at the same domain. We run both protocol parsers simultaneously — the TCP listener handles Java clients, the UDP listener handles Bedrock clients.

This is more complex than just running two separate proxies. The firewall rules need to apply across both protocols, health checks need to verify both TCP and UDP connectivity to the backend, and the load balancer needs to distribute both traffic types to the same backend pool.

We handle this with a unified configuration model: one domain, one set of rules, dual protocol support. The dashboard shows both connection types in the same analytics view.

Performance considerations

Protocol parsing adds CPU overhead compared to raw packet forwarding. To keep latency under 1ms, we use several optimizations:

The XDP layer handles the highest-volume, lowest-complexity filtering. By the time a packet reaches the userspace proxy, it's already passed IP reputation checks and rate limiting. The protocol parsers only process traffic that's likely legitimate.

The parsers themselves are zero-copy where possible — we read protocol fields directly from the packet buffer without allocating intermediate structures. For Java's VarInt encoding, we use a branchless decoder that processes the variable-length format in constant time.

Config lookups use ArcSwap for lock-free reads. The firewall rule evaluator uses a compiled representation of your rules that avoids hash map lookups in the hot path.

The result: on a single-core edge node, the proxy can handle tens of thousands of concurrent connections with sub-millisecond per-packet overhead.