
Список изменений
Allium v0.2.15a
Wiki: https://github.com/castledking/Allium/wiki
Highlights
-
LiquidBounce's plugin-enumeration probe now times out — Running
.serverinfo Pluginsagainst an Allium server producesPlugin detection timed out. It may be due to the server blocking the request.instead of a plugin list. This was verified black-box against a live LiquidBounce client. -
The command-suggestion protocol is now filtered at the packet level — A new/rewritten
CommandSuggestionsListenerintercepts the rawTAB_COMPLETErequest/response pair via PacketEvents and enforces Allium's visibility rules on the wire, where Bukkit events cannot reach. -
Every namespaced suggestion is stripped unless explicitly allowlisted — Unknown namespaces (
jbt:*,someplugin:*) can never leak regardless of any blocklist configuration, becausepluginname:commandentries are removed wholesale. -
The packet-level protection is independent of the existing command lists — With both
remove-unsafe-commandsdisabled andcommand-suggestion-filtersemptied, the black-box test still passes. The active protection is the packet-level bare-/response cancellation plus the namespaced-suggestion stripping rule.
Background: how the leak worked
LiquidBounce's primary stealth plugin-detection mechanism (verified in its source at ServerObserver.captureCommandSuggestions()) does not rely on /plugins. It sends:
ServerboundCommandSuggestionPacket(completionId, "/")
→ server-side Brigadier completion for the root node
→ ClientboundCommandSuggestionsPacket(match[])
→ extract every suggestion containing ":"
→ "pluginname:command" ⇒ pluginname
This bypasses DECLARE_COMMANDS filtering entirely: even with a curated command tree, a separate root completion request can expose registered namespaced commands that reveal plugin identities.
Why Bukkit events could not cover this
Paper's own maintainers document the limitations of event-level tab-completion handling:
-
PaperMC/Paper#12050 — "TabCompleteEvent doesnt Work": electronicboy explains that "tab completion is mostly handled on the client these days, with the client only asking the server to complete stuff it's told to ask the server about (i.e. the legacy bukkit command system); stuff like the root command is sent to the client entirely." Malfrador adds: "That event is only called for bukkit commands. It is not called for commands using the new brigadier command API."
-
PaperMC/Paper#10833 — "TabCompleteEvent not being called": the same gap is reported following Paper's command API changes.
-
PaperMC/Velocity#1168: documents the distinction between Brigadier command data and the older Bukkit completion mechanism.
The consequence is that TabCompleteEvent/AsyncTabCompleteEvent handlers such as Allium's CommandManager.onTabComplete can rewrite legacy Bukkit-command completions, but they do not provide reliable control over the modern Brigadier suggestion round-trip or the final suggestion packet.
Packet-level enforcement via PacketEvents closes that gap.
What changed
CommandSuggestionsListener (packet level, rewritten)
The listener enforces the same visibility policy at the protocol boundary:
| Behavior | Detail |
|---|---|
| Request tracking | Incoming Play.Client.TAB_COMPLETE texts are cached by transaction id (bounded map, TTL pruned). |
| Untracked responses cancelled | A Play.Server.TAB_COMPLETE whose transaction id has no cached request is dropped (cancel-unknown-suggestion-responses). |
Bare / probe cancelled | Any tracked request shorter than two characters — i.e. exactly LiquidBounce's / probe — gets its response cancelled outright. The client never receives ClientboundCommandSuggestionsPacket and the enumeration probe times out. |
| Namespaced stripping | Every match containing : is removed unless listed in allow-namespaced-suggestions. This prevents jbt:mob_bag and similar identifiers from leaking through suggestions. |
| Prefix blocklist kept | command-suggestion-filters still applies as an additional layer on plain suggestions. |
| Group rules enforced | Plain suggestions additionally pass through CommandManager.shouldAllowTabComplete(player, cmd) — the same hide.yml group policy that governs execution and the command tree. Policy stays in one place; the listener is purely an enforcement point. |
| Empty responses cancelled | If filtering removes every suggestion, the response is cancelled instead of sending an empty list. |
| Legit completion preserved | Requests that do not start with / (chat/player-name contexts) pass untouched; /he, /msg <partial>, etc. continue working because their input length is ≥ 2. |
hide.yml
New settings under hide.settings:
cancel-bare-slash-requestcancel-unknown-suggestion-responsesallow-namespaced-suggestions
Comments were updated to document the new packet-level protection. Existing keys remain unchanged and supported.
Architecture note
The command visibility policy remains in CommandManager; the PacketEvents listeners remain thin enforcement points.
DeclareCommandsListener still reads unsafe-commands-list rather than deriving its behavior from group rules. Unifying those policies is deferred future work — the existing static lists are independent of the packet-level suggestion-probe defense.
Verification
-
Maven suite: 170 tests, 0 failures, 0 errors —
mvn installgreen. -
Black-box: LiquidBounce
.serverinfo Pluginsagainst the test server returns the timeout message instead of a plugin list. Confirmed withremove-unsafe-commands: falseand an emptiedcommand-suggestion-filterslist, isolating the packet-level listener as the effective protection. -
Legitimate completion:
/he,/help, and/msg <partial>continue to provide normal completion for unprivileged players. -
Namespaced suggestions: Hidden namespaced entries such as
jbt:*are removed from suggestion responses unless explicitly allowlisted.
