A packet flood becomes expensive before a listener decides to cancel it. The server allocates buffers, decompresses a frame, constructs a packet object, and dispatches it through every registered handler. By the time a semantic check says “bad”, most of the cost has already been paid. ExploitFixer’s packet path is arranged around that order, and the order is the design.
Two inspection stages with different costs
ExploitFixer exposes two packet stages. The raw stage runs in the PacketDecodeEvent handler at LOWEST priority. It reads frame bytes before the server turns them into a semantic packet and can cancel there. The decoded stage then runs through PacketReceiveEvent, where module listeners evaluate packet contents and add violations.
The raw stage is deliberately narrow. It can judge size, framing, and byte patterns. It cannot know whether a player is building a farm, using a modded client, or sending a legal but unusual inventory action. That is why the second stage exists. The ordering says cheap rejection happens before expensive interpretation; it does not replace interpretation.
raw-packet-guard.mode defaults to enforce. The alternative, observe, logs without cancelling or adding VLS. The semantic fallback under raw-packet-guard.receive-semantic.enabled is off by default. Operators who suspect false positives can observe first, but observe mode is not a silent version of enforce: it changes what the guard is allowed to do.
The watchdog measures handler time, not server health
PacketWatchdog is often read as a performance feature. It is a time bound. It measures processing time per logical packet and also keeps cumulative windows per player. It loads packet and period budgets, a TPS allowance, duplicate leniency, and a UUID whitelist. When a logical packet completes, elapsed time is compared against the effective budget.
The shipped packet-watchdog.budgets.packet is 1000 milliseconds, packet-watchdog.vls is 25.0, and packet-watchdog.cancel-packets is false. That default matters: cancelling movement packets can look like an immediate disconnect, so the plugin records the overrun without cutting the packet unless an operator opts in.
The watchdog is not a profiler for the rest of the server. A 1000 ms budget is a configured tolerance, not a measured safe threshold for every host. It contains one plugin’s packet work. It does not explain why a tick was slow, and it does not make another plugin’s listener cheap.
Punitive commands from the watchdog are hard-blocked below 100 current VLS, even when Violations.commands defines a lower bucket. That guardrail prevents a processing hiccup from turning directly into a punishment.
Modules detect; violations decide
The module set is large: roughly two dozen modules are constructed and registered at startup, each with its own YAML. Modules do detection. They do not each carry a private punishment system. Detection adds VLS, and the shared Violations policy runs threshold buckets in ascending order.
The shipped policy cancels at 25, reduces 20 VLS per second, and defines buckets at 25, 50, 75, and 100. Notifications, kicks, and console commands are deduplicated. A module with kick: false still cancels, notifies, logs, and sends webhooks, but skips its kick and custom console punishments.
That separation makes tuning possible. An operator can change the punishment ladder without editing every module, and a noisy module can be observed at a low threshold before it reaches a kick. It also means a violation count is not a conviction: it is a shared score fed by several independent detectors with different confidence levels.
The default ladder is not neutral. The 100 bucket runs a console command, and a console command executes with the server’s authority. Changing that line is a security decision, not a cosmetic config edit.
Violations:
cancel: 25
reduce: 20
commands:
25:
- "notify"
100:
- "notify"
- "kick"
Dependency gates run before module construction
ExploitFixer requires HamsterAPI 0.3.5 or newer. The check happens in onEnable before config, modules, listeners, and the packet hook manager are built. A missing or old dependency aborts startup instead of leaving a half-initialized packet layer.
PacketEvents and ProtocolLib are different. They are optional adapters, declared provided rather than shaded, and resolved only after the plugin checks that the dependency is present and enabled. Adapter construction uses the class name, and a failure cleans up rather than leaving a broken hook registered. The plugin can run with neither adapter; the available packet surface changes, but startup does not pretend the optional layer is guaranteed.
This gate ordering is a packaging decision with security consequences. A required dependency failure is loud and early. An optional dependency failure is contained to the hook layer. The cost is that the plugin has more than one supported packet configuration, and operators have to know which one they are running.
What this does not prove
The source is built against Spigot API 1.20.1 with Java 8 bytecode and api-version: 1.13. That is build metadata, not a tested Minecraft range. folia-supported: true is a declaration, and no Folia runtime test is recorded here.
The absence of a tested matrix matters for the packet path. Decode event behavior, packet classes, and cancellation semantics can differ across server implementations. The watchdog defaults are configuration values; they were not benchmarked on a live server for this article. A raw guard that catches malformed frames does not prove that every exploit family is covered by the decoded modules.
What the code does show is a consistent priority: reject early when the evidence is cheap, interpret later when it is not, and keep detection separate from punishment. That order is defensible before anyone quotes a performance number.
For installation and the full key set, see the ExploitFixer overview and the configuration reference. The ExploitFixer case study covers the wider product context; the official product page is arkflame.com/plugins/exploitfixer.