# ExploitFixer: packet validation order before semantics

> How ExploitFixer checks raw frames before decoded packets, and what its watchdog budgets, violation buckets, and dependency gates do and do not cover.

- Author: LinsaFTW / Juan Cruz Linsalata
- Published: 2026-10-30
- Updated: 2026-10-30
- Slug: exploitfixer-packet-validation-order
- Tags: exploitfixer, packets, security, minecraft
- Canonical URL: https://linsaftw.arkflame.com/posts/exploitfixer-packet-validation-order.html

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.

```yaml
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](/docs/exploitfixer/overview.html) and the [configuration reference](/docs/exploitfixer/configuration.html). The [ExploitFixer case study](/work/exploitfixer.html) covers the wider product context; the official product page is [arkflame.com/plugins/exploitfixer](https://arkflame.com/plugins/exploitfixer/).
