Whiteout

A packet-level anticheat for Paper and Folia, built on a vanilla-exact movement prediction engine.

kotlinpaperfoliaminecraft
On this page 8 sections
  1. Simulate, don’t threshold
  2. Two clocks and a lie detector
  3. Never tell the cheater
  4. No main thread, no locks
  5. Reading Bedrock before Geyser translates it
  6. Hiding what you shouldn’t see
  7. Tested against a cheat client built to beat it
  8. Under the hood

Whiteout is a server-side anticheat for Paper and Folia, written from scratch in Kotlin. It reads the client’s packets as they arrive, simulates what an unmodified client could legally have done, and flags the difference. The target isn’t the obvious fly hack, which everything catches. It’s the tuned cheat: reach set to 3.0, killaura clicking at a Gaussian 5 to 8 CPS, speed duty-cycled to 1.02x, each individual sample sitting politely under any threshold you could publish.

The stance is written into the code as rules: catch those, never false-flag an honest player, and never tell the cheater anything.

A quick tour:

  • 48 checks across nine families: bad packets, timer, latency, reach, aim, movement, inventory, combat and autoclicker. /whiteout checks shows every check’s live state, whether it’s off, alert-only or punishable.
  • A full movement prediction engine in the style of Grim, rather than a pile of speed limits.
  • Two latency clocks that make ping spoofing work against the spoofer.
  • Bedrock players judged on Bedrock input, read straight out of Geyser before it translates anything.
  • An optional anti-ESP layer that stops sending clients what they have no way to see.
  • Rich evidence for staff: in-game alerts, a Discord webhook, and an incident file per serious violation holding both clocks, server health, the client’s state and the last ten seconds of the player’s movement, tick by tick.

Simulate, don’t threshold

The movement check doesn’t ask “was that too fast?” It asks “what is the closest thing a vanilla client could have done here?” Every tick it simulates 144 candidate inputs per starting velocity (forward, strafe, jump, four sprint modes, and whether an item is in use), plus any knockback or explosion the server sent, and reports the gap between the best one and what the client claimed.

The physics is golden-tested against vanilla’s own numbers: 5.612 m/s sprinting, 4.317 walking, a jump apex of about 1.2522 blocks, terminal velocity of −3.92, ice friction, slab and stair step-up. Where the engine can’t be exact (fluids, ladders, riding, gliding, an unconfirmed teleport, a chunk the client hasn’t been sent) it abstains rather than guesses.

Packet gaps are the classic hole. A cheat that withholds movement packets for a few ticks used to buy abstention, and under a purpose-built test module, 1.5x speed went completely silent. So gaps of up to ten ticks are now crossed with a beam search instead of skipped, and withholding packets no longer hides anything.

For the cheats built to live under any single threshold, the answer is patience. A player whose mean signed prediction offset is +0.004 over 2,000 samples is cheating even if no single sample ever crossed a line, so several checks judge long-window, per-player distributions rather than individual moments.

Two clocks and a lie detector

Lag compensation is where anticheats get bypassed. Whiteout keeps two independent measures of each player’s latency: vanilla keep-alives, and its own transaction pings with unpredictable ids. Everything is compensated against the smaller of the two, so inflating your ping only makes you easier to judge, and the two disagreeing is itself the ping-spoof signal.

A transaction that’s withheld, late, reordered or answered for an id that was never sent makes the player’s state stricter, never looser. That one rule closes the family of bypasses built on holding acknowledgements back, and every other timeout in the plugin fails closed the same way.

Never tell the cheater

There are no setbacks. That’s deliberate, and it’s the most counterintuitive decision in the project. Cheat clients watch for corrections: fly modules auto-disable on a rubber-band, velocity modules pause when they get flagged, fake-lag modules flush on a correction. Sending none denies them the trigger, so the cheat keeps running while the evidence piles up.

The player sees nothing at all. No flag message, no identifiable cadence, and punishments aren’t broadcast by default, because a public ban message is a free feedback channel for anyone testing a bypass. Thresholds live in code rather than the config, since a published threshold is a published bypass target. And one check is pure bait: an invisible fake player spawned behind someone, which only a killaura will turn around and hit.

No main thread, no locks

Every player’s check and movement state is written only on that player’s own network thread, which netty already serialises, so the hot path needs no locks at all. Packet timing is measured right there at the network layer, before any tick scheduling, because hopping threads first would smear away the jitter that tells real lag apart from a blink cheat.

The world a check reads is a lag-compensated copy: the world as this player was actually shown it, one round trip behind, built on the chunk storage PacketEvents had already decoded so there’s no conversion cost. Nothing on a packet thread calls the Bukkit API; kicks and punishments hop to the right Folia scheduler, and server health is read per region.

A profiling pass with one player lapping a test lane cut Whiteout’s own allocation from 40.1 MB/s to 3.7 MB/s (from 69% of everything the JVM allocated to 17%), mostly by reading blocks through a flat lookup table instead of a library call that boxed and cloned every block it returned.

Reading Bedrock before Geyser translates it

Java-edition checks never judge Bedrock players; widening thresholds to fit them would just be an exemption with extra steps. Instead, Whiteout wraps each Geyser session in-process and reads Bedrock’s own input packet (the client’s tick counter, raw analog movement, input flags) before Geyser turns it into Java packets, and only checks written for Bedrock run on it.

When Geyser runs standalone or on the proxy, the same jar loads as a Geyser extension and relays that input on the player’s own Java connection, signed with HMAC-SHA256 and sequenced. A Bedrock client can’t send a Java plugin message, a Java client can’t sign one, and the sequence has to keep rising, so it can’t be forged from either side. Bedrock status itself only ever comes from Geyser or Floodgate, never from the client, after a real client was caught forging a payload to pass as a Geyser connection.

Hiding what you shouldn’t see

The one part that changes what clients are sent is concealment, and it’s off by default. A background flood fill from the player’s eyes works out the space they could actually see into, and players, mobs and items standing anywhere else are simply never sent, because a packet-reading ESP doesn’t need to render a mob to know exactly where it is. For players on the surface, everything below a set depth is emptied from the chunks they’re sent, flat rather than shaped (a shaped cut would outline what it hides) and with light zeroed so a torch in a buried base doesn’t show up on a light-map overlay.

Tested against a cheat client built to beat it

No check ships enabled until it survives a zero-false-positive soak. Each one is proven end to end by setting its threshold absurdly tight, then made to pass adversarial gates: real lag must not trip timer or blink checks, drag-clicking must not trip the autoclicker check, godbridging must not trip scaffold, a crowd of mobs must not trip movement.

The red team is my own Fabric client, written specifically to attack Whiteout, with tunable speed, reach, timer and packet-withholding modules driven by scripts over RCON. The last full matrix flagged speed down to 1.02x, 1.05x timer, fly, no-fall, killaura, criticals, reach, teleport aura, omnisprint and zero-knockback, with all 273 flags reaching chat on Folia, while honest sprinting, jumping, strafing, falling and knockback stayed clean.

Under the hood

  • Built in Kotlin on Java 25, against Paper and Folia 26.2 with paperweight’s Mojang-mapped dev bundle.
  • PacketEvents 2.13.0 for the packet layer, and optional Geyser 2.11 for the Bedrock tap or signed relay.
  • Commands are Paper’s native Brigadier API with a small annotation layer on top, since the usual command frameworks didn’t yet work on 26.2.
  • A heavily commented, 779-line config with a per-check block for every check, and an observe-only mode on by default so a live server can collect data before anything is enforced.
  • Bypass permissions per check family, registered at runtime with a default of false, since an unregistered permission node quietly defaults to true for operators.
  • Profiled with JFR, spark and my own Windchill.