#velocity

2458 messages · Page 3 of 3 (latest)

hexed flickerBOT
#

A client that stalls its FML/login handshake could enqueue plugin messages (each up to ~32 KiB serverbound) without bound in ClientPlaySessionHandler#loginPluginMessages, growing per-connection heap until an OOM kill. This PR caps the queue by both bytes (4 MiB) and count (1024), configurable via velocity.max-queued-login-plugin-message-bytes and velocity.max-queued-login-plugin-messages, and disconnects on overflow.

#

Building the project shows the following warning:

> Task :velocity-proxy:compileJava
warning: The `GraalVmProcessor` annotation processor is missing the recommended `log4j.graalvm.groupId` and `log4j.graalvm.artifactId` options.
  To follow the GraalVM recommendations, please add the following options to your build tool:
    -Alog4j.graalvm.groupId=<groupId>
    -Alog4j.graalvm.artifactId=<artifactId>

This PR fixes this by adding the required options.

#

Depending on how long a client is actually able to stall the login phase for, this may be a scary OOM once again.

Without this fix, and at the default rate limit of 500 pps, a client would be able to fill this queue with 160MB of plugin messages every second (at 32kb/PM). That's 10GB within a minute, currently exploitable.

Of course this PR fixes that, and I'm sorry to keep bumping this but, #1786 would have greatly crippled this attack. At a limit of 5MB/s of decompressed packets per second, it would take around half an hour to fill the same 10GB in this queue. Still an OOM, if the client is actually able to stall the login phase for 30 minutes, but way less instant.

hexed flickerBOT
#

I think this just hack the allowed range from -128 ~ 127 to -1~254 (255 is -1).
If modded servers register a dimension in -2 ~ -128, it may still broken.

And the second things, GTNH is not "hack this to an unsigned byte to allow dim IDs up to 255".
It hack to an int to completely fix this vanilla bug.
(https://github.com/GTNewHorizons/Hodgepodge/blob/master/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/packets/MixinS01PacketJoinGame_FixDimensionID.java)

hexed flickerBOT
#

If modded servers register a dimension in -2 ~ -128, it may still broken.

Not sure if this is possible, in older versions anyway. JoinGamePacket handles the dimension as a byte, regardless of its sign, it will always encode/decode as the same hex value. Though in vanilla minecraft this is definitely expected to be signed, because the nether is -1. RespawnPacket just encodes this as an integer instead, which is also expected to be signed, as the vanilla protocol expects the values -1, 0 and 1.

GTNH is not "hack this to an unsigned byte to allow dim IDs up to 255".

IMO it is a hack. The nether being -1 suggests that this value should be signed. Mods using a value over 127 would have needed to patch this bug also; RespawnPacket needs to return a signed -1 only ever for the nether, otherwise it should treat the byte as unsigned (and have a range from 0 - 255). This patch makes the range for the dimension value essentially be -1 - 254 (inclusive), as 255 is effectively mapped t...

hexed flickerBOT
hexed flickerBOT
#

Correct me if I'm wrong, but it seems like SET_ACTION_BAR is the right action for the TitleActionbarPacket packet.

This bug doesn't surface when creating this class from the factory:

// GenericTitlePacket
public static GenericTitlePacket constructTitlePacket(ActionType type, ProtocolVersion version) {
  GenericTitlePacket packet = null;
  if (version.noLessThan(ProtocolVersion.MINECRAFT_1_17)) {
    packet = switch (type) {
      case SET_ACTION_BAR -> new TitleActionbarPacket();
      case SET_SUBTITLE -> new TitleSubtitlePacket();
      case SET_TIMES -> new TitleTimesPacket();
      case SET_TITLE -> new TitleTextPacket();
      case HIDE, RESET -> new TitleClearPacket();
      default -> throw new IllegalArgumentException("Invalid ActionType");
    };
  } else {
    packet = new LegacyTitlePacket();
  }
  packet.setAction(type);
  return packet;
}

Namely through:

case SET_ACTION_BAR -> new TitleActionbarPacket();
packet.setAction(type); // overrides```...
#

We may even enforce this contract by adding this below the switch statement in the factory method:

if (packet.getAction() != type) {
  throw new AssertionError("Title packet action type mismatch!");
}

But I don't think it's worth it to assert this at runtime (and this won't even work, TitleClearPacket can be created with both HIDE and RESET).

The enforcing of this is also quite inconsistent. TitleClearPacket throws when setting its action type to anything but HIDE and RESET, but the other packets let you set the action type to anything you want. Maybe it's worth expanding this PR to enforcing this for all other packet's setAction methods as well?

hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

I actually got curious and wrote a small benchmark for that and ran on my (admittedly ancient) desktop PC on both Windows (everyday usage) and Linux (installation ISO without background services):

> Windows
Benchmark                    Mode  Cnt       Score      Error   Units
UuidBenchmark.uuidInsecure  thrpt    6  151409.421 ± 1504.844  ops/ms
UuidBenchmark.uuidSecure    thrpt    6     698.752 ±   10.978  ops/ms

> Linux
Benchmark                    Mode  Cnt       Score     Error   Units
UuidBenchmark.uuidInsecure  thrpt    6  156989.602 ± 593.248  ops/ms
UuidBenchmark.uuidSecure    thrpt    6    2846.491 ±  35.525  ops/ms

So yeah, there are good magnitude-order speedups (less good on Linux, probably due to better reseeding from the kernel CSRNG). However, the baseline of 700 (or 2800) UUIDs/ms is not something that needs extreme urgent optimization.

hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

Expected Behavior

Hey, i'm trying to use Velocity for Modded Fabric on 26.1.2.

I had tried to join the Server without Velo, and it work right, but i had used now, the Automodpack plugin with the Mod from Automodpack. and this works fine :)

Without Velocity:
Client
https://mclo.gs/ojlvXCv

Server Log: https://mclo.gs/HIjrh9B

Actual Behavior

But when i try to join the server i got an issue and we had used -Dvelocity.max-known-packs= for more packets.

But, on joining it does not work and it throws this error:

[15:46:18] [Netty epoll Worker #4/ERROR] [com.velocitypowered.proxy.connection.MinecraftConnection]: [server connection] Suerion -> survival: exception encountered in com.velocitypowered.proxy.connection.backend.BackendPlaySessionHandler@2367b702
io.netty.handler.codec.CorruptedFrameException: Error decoding class com.velocitypowered.proxy.protocol.packet.AvailableCommandsPacket Direction CLIENTBOUND Protocol 26.1 State PLAY ID 0x10
	at com.velocitypowered.proxy.p```...
hexed flickerBOT
#

@electronicboy Then the labels are not Correct on Modrinth and Curseforge...

Had added crossstich and it work, but i got this issues on Velo:

-> Caused by: java.lang.IllegalArgumentException: Argument type identifier 58 unknown.

Velocity log
https://mclo.gs/SmmCICQ

Server Log
https://mclo.gs/50YzUOq

Client Log:
https://mclo.gs/AzeEUNv

Network Protokol
https://mclo.gs/ppm8Y5j

Should i try to disable BadPackets and NetProdis?

hexed flickerBOT
#
[17:22:04] [main/WARN]: Error loading class: net/minecraft/class_2641$class_7232 (java.lang.ClassNotFoundException: net/minecraft/class_2641$class_7232)
[17:22:04] [main/WARN]: @Mixin target net/minecraft/class_2641$class_7232 was not found crossstitch.mixins.json:command.CommandTreeSerializationMixin from mod crossstitch

I don't really know anything about fabric/modded platforms, but this makes me guess that crossstitch is not yet compatible with 26.1, probably thanks to the removal of obfuscation. There is https://github.com/PaperMC/CrossStitch/pull/23 which looks to update it, but I'm not sure what the state of that is.

If you can, give that pr a try and let us know if that resolves your problem.

hexed flickerBOT
#
[17:22:04] [main/WARN]: Error loading class: net/minecraft/class_2641$class_7232 (java.lang.ClassNotFoundException: net/minecraft/class_2641$class_7232)
[17:22:04] [main/WARN]: @Mixin target net/minecraft/class_2641$class_7232 was not found crossstitch.mixins.json:command.CommandTreeSerializationMixin from mod crossstitch

I don't really know anything about fabric/modded platforms, but this makes me guess that crossstitch is not yet compatible with 26.1, probably thanks to the removal of obfuscation. There is PaperMC/CrossStitch#23 which looks to update it, but I'm not sure what the state of that is.

If you can, give that pr a try and let us know if that resolves your problem.

@Emilxyz
With the pull, it workes! :) had build it and added it, now it work :), i could join :) it works fine, now i could test more

<img width="1920" height="1080" alt="Image" src="https://github.com/user-attachments/assets/1de33d97-50d7-4fcf-8230-5a5634fc8243" />

@electronicboy i had said, 26....

hexed flickerBOT
#

Filter the ServerboundChatSessionUpdatePacket when player info forwarding is disabled, since the client's profile key signature will not be valid for the player's offline UUID.

See comment in ClientPlaySessionHandler.java. This is marked as draft since it's in a "works for me" state, though I'm happy to address feedback and get this merged. I'm unsure whether this behavior belongs in a separate option or whether player-info-forwarding-mode = "NONE" is a good enough heuristic for when it's needed.

My use case is a public, Mojang-authenticated gateway to an online-mode=false Vanilla server (no Paper/Fabric) that unauthenticated bots can connect to over localhost.

hexed flickerBOT
#
[17:22:04] [main/WARN]: Error loading class: net/minecraft/class_2641$class_7232 (java.lang.ClassNotFoundException: net/minecraft/class_2641$class_7232)
[17:22:04] [main/WARN]: @Mixin target net/minecraft/class_2641$class_7232 was not found crossstitch.mixins.json:command.CommandTreeSerializationMixin from mod crossstitch

I don't really know anything about fabric/modded platforms, but this makes me guess that crossstitch is not yet compatible with 26.1, probably thanks to the removal of obfuscation. There is PaperMC/CrossStitch#23 which looks to update it, but I'm not sure what the state of that is.

If you can, give that pr a try and let us know if that resolves your problem.

It was ready but at the time I couldn't find any real mod with a custom argument for 26.1, so I wanted to wait for that.

hexed flickerBOT
hexed flickerBOT
#

Inspired by:

With this PR, plugins may dynamically decide to add commands to the client's command tree by triggering Player#sendAvailableCommands and listening for the PlayerAvailableCommandsEvent that gets fired as a result of it.

How much memory does this PR consume per player?

Let's assume 1.000 commands per player. A conservative lower bound, and a complete but educated-ish guess, would be about ~64 bytes per command, giving 64kb per player (for 500 players, this is 32MB)

Take this with a giant grain of salt, but I had Claude Opus 4.7 have a go at estimating the memory footprint this PR brings with it.
Prompt: Investigate exactly how much memory 'RootCommandNode<CommandSource> backendCommandsNode' in 'BackendPlaySessionHandler' consumes per player for 1000 commands with typical str...

#

I'm not too sure if we need to return a CompletableFuture here. This doesn't get returned anywhere else in the Player interface, but at the same time, when this is completed it does guarantee that all PlayerAvailableCommandsEvents have been fired and the packet is on its way to the player.

Could this be useful for plugins? Or should we just return void and rely on plugins listening for this even to figure out that the packet is (about to be) on it's way?

#

Velocity didn't have these type pointers before so the deprecation of adventure platform has absolutely nothing to do with this.

What do you mean by this exactly? Velocity does currently resolve FacetPointers.TYPE to FacetPointers.Type.PLAYER and CONSOLE through pointers().

It seems strange that this was ever the case, as net.kyori.adventure.platform.facet is an unsupported API that really should never have been used in the first place.

This makes the deprecation of platform-facet and the likes put Velocity in quite a predicament... Removing the dependency could break plugins relying on it, but continuing to use a deprecated library/API also doesn't seem like the right solution.

I'd rather this was included/PR'd into adventure-api.

Getting net.kyori.adventure.platform.facet into adventure-api does seem like the proper solution, but I don't know if they'll want that. Another solution would be to provide these classes in proxy/deprecated/adventure-facet, similar to con...

#

My initial thought was to put this behind add a feature flag system, which would need to be enabled by plugins on init but...

I mean we could do this intuitively.

  • get rid of Player#sendAvailableCommands
  • introduce Proxy#getAvailableCommandsService
interface AvailableCommandsService {
  void sendAvailableCommands(Player player);
}

Plugins would need to obtain the AvailableCommandsService which would trigger the command tree to now be saved.

The issue is however that plugins really need to do this on startup, or they'll end up missing the backend command trees from players that are already connected.

Though I don't really like this. Seems complex and unnecessary to avoid a couple megabytes of ram, realistically.
Maybe an opt-out system flag would be better. Having memory issues? Know that no plugins make use of this feature? Disable it.

hexed flickerBOT
#

As you've already mentioned, these pointers are marked as internal by adventure-platform. They're also not actually provided as a dependency by velocity-api and only required by velocity-proxy, which is not even published. So plugins using these is extremely unlikely and also unsupported anyways.

I'm also assuming that this pr will come with a velocity version bump, due to the other breaking changes in adventure 5. So I think just doing a clean cut and dropping adventure-platform-facet should be fine. Maybe mention it in an update announcement.

hexed flickerBOT
#

My initial thought was to put this behind add a feature flag system, which would need to be enabled by plugins on init but...

I mean we could do this intuitively.

  • get rid of Player#sendAvailableCommands
  • introduce ProxyServer#getAvailableCommandsService
interface AvailableCommandsService {
  void sendAvailableCommands(Player player);
}

Plugins would need to obtain the AvailableCommandsService which would trigger the command tree to now be saved.

The issue is however that plugins really need to do this on startup, or they'll end up missing the backend command trees from players that are already connected.

Though I don't really like this. Seems complex and unnecessary to avoid a couple megabytes of ram, realistically. Maybe an opt-out system flag would be better. Having memory issues? Know that no plugins make use of this feature? Disable it.

We could also just expose the AvailableCommandsService in the proxy init e...

#

Alternatively we could keep the method as is and throw if it was not enabled by some feature system on init.

I don't think we have any feature that works like this right? If we are going to set a precedent for "resource intensive feature that must be opt-in in an elegant way" we need to think of a proper solution, one that would also work for future features like this.

An alternative for this specific PR could be to store the raw packet instead of the decoded command tree. Decode the packet only when sendAvailableCommands is called. Trade a bit of CPU time (only when this feature is used) for a lower memory footprint. I'm kinda leaning towards this; I might do some testing to see how much this would reduce memory usage in comparison.

#

Alternatively we could keep the method as is and throw if it was not enabled by some feature system on init.

I don't think we have any feature that works like this right? If we are going to set a precedent for "resource intensive feature that must be opt-in in an elegant way" we need to think of a proper solution, one that would also work for future features like this.

Sure, though we also should think about the users of this API and if it is hidden behind some 'obscure' mechanism they might not find it

#

Yea I'm not sure, one of the maintainers might have an idea

Are clients able to trigger this flow, of adding a resource pack & thus registering a callback? I don't think so but I might be wrong.

If we can only trigger a callback being registered through a plugin, I don't think this is really a cause for concern. The lifetime of the map (and its entries if the client never responds) is only as long as the client is connected, worst case we're storing a couple stale entries but that buys us having compatibility with slow clients.

#

Sure, though we also should think about the users of this API and if it is hidden behind some 'obscure' mechanism they might not find it

I mean, if we don't want to hide this API, we could keep Player#sendAvailableCommands and throw with an exception saying You must call ProxyInitializeEvent#enableFeature(FeatureFlags.SEND_AVAILABLE_COMMANDS) to make use of this feature!

This should never really produce an exception at runtime, because plugin developers will (should) catch this during development.

Alternatively we could log an error or warning and have it behave like a NOP instead of throwing.

hexed flickerBOT
#

An alternative for this specific PR could be to store the raw packet instead of the decoded command tree. Decode the packet only when sendAvailableCommands is called. Trade a bit of CPU time (only when this feature is used) for a lower memory footprint. I'm kinda leaning towards this; I might do some testing to see how much this would reduce memory usage in comparison.

This is really cursed. AvailableCommandsPacket needs to store a byte[] encoded and perform encoding during the constructor, decoding during the getRootNode getter. Reading/writing the bytes into this encoded field during encode() and decode(). IMO this isn't worth it, it deviates from how other packets do things so much, but it will reduce the memory footprint by a lot. Curious what other maintainers think of this approach.

hexed flickerBOT
#

Sure, though we also should think about the users of this API and if it is hidden behind some 'obscure' mechanism they might not find it

I mean, if we don't want to hide this API, we could keep Player#sendAvailableCommands and throw with an exception saying You must call ProxyInitializeEvent#enableFeature(FeatureFlags.SEND_AVAILABLE_COMMANDS) to make use of this feature!

This should never really produce an exception at runtime, because plugin developers will (should) catch this during development.

Well this is what I meant.

Alternatively we could log an error or warning and have it behave like a NOP instead of throwing.

Well I think this could be annoying, and idk but I see no benefit with this option

hexed flickerBOT
#
  • Fix PluginMessageEvent in InitialConnectSessionHandler so client -> backend plugin messages use source = player and target = serverConn, matching ClientPlaySessionHandler and ClientConfigSessionHandler.
  • The handler previously used the backend -> client argument order from BackendPlaySessionHandler (introduced in #774), so client messages were exposed to plugins with ServerConnection as the source and Player as the target.
hexed flickerBOT
hexed flickerBOT
#

Expected Behavior

Expected Behavior

Pretty simple: instant respawn should successfully respawn the player without desync issues.

Actual Behavior

Bugged Behavior

Players who die and respawn from instant respawn become desynced at some point related to velocity (as there is no Minecraft bug related to the same issue).
Desynced players are invisible to all other players, cannot be hit or interacted with, however they can still interact with other players and the environment.
Desynced players ARE able to relog to become visible again.

Steps to Reproduce

Steps to Reproduce

  1. Set up velocity proxy and backend server ss per details below.
  2. Enable the instant respawn gamerule on the backend server
  3. Log in (at least) two clients
  4. Have one client die and be respawned
    ~ You should see the issue now

Plugin List

Velocity Plugins

  • Velocity
  • Luck Perms
  • MCKotlin-Velocity
  • VLobby
  • Simple Voice Chat

Fabric Mods

  • Fabric API
  • Ferrite Core
  • VMP Fabric
    -...
hexed flickerBOT
hexed flickerBOT
#

also experiencing this what hosting service do you use? I use hetzner

We were using Hetzner at the time, we swapped but also ended up finding a different issue from another plugin which was causing disconnects too. Now that we removed that plugin and swapped away from Hetzner we have barely experienced any issues.

hexed flickerBOT
#

As pointed out in https://github.com/PaperMC/Velocity/pull/1775#issuecomment-4273723975, when adding new translation keys to messages.properties, these keys are not populated into the translation files of existing setups. I think a full migration system, as suggested by the comment, for this would be overkill, we're rarely adding translations anyways, so I went with a different approach.

This pr loads the default messages.properties translations that are bundled with velocity as a "patch" on top of any existing translation files. This will prevent any missing translations from showing up as their key since the default english translations will always be available.

#

For a fork of Velocity, we have added a basic migration system for messages.properties.

See: https://github.com/GemstoneGG/Velocity-CTD/blob/libdeflate/proxy/src/main/java/com/velocitypowered/proxy/TranslationRegistryManager.java

This would take it one step further than this PR.

The tail of a messages.properties file that has been migrated by the above class looks like:

# Messages below have been added by a migration of this file at 2026-05-09 12:27:44 UTC.
velocity.command.heapdump-created=<green>Heap dump saved to <arg:0>
velocity.command.heapdump-failed=<red>Failed to write heap dump, see server log for details.
# Messages below have been added by a migration of this file at 2026-05-22 14:26:58 UTC.
velocity.command.version-offer-copy-version=<white>Click to copy version to clipboard
velocity.error.plugin-message-overflow=<red>You sent too many plugin messages before completing the connection.

Feel free to take inspiration or copy (it's licensed under...

hexed flickerBOT
#

I understand the concern that this might cause confusion for people, but I'm also not entirely happy with your approach. In the past, if users wanted to force velocity to only use a specific language, we would recommend them to delete all other translation files and rename the target translation file to messages.properties. This is something that's lost with your linked approach. So I think at most those migrations should only apply to the default messages.properties and ignore any missing keys in other translation files (or even entirely missing translation files). Curious what maintainers think about this though, before I make any changes.

#

IMO the default locale should try to use the system default similar to how I did it here: https://github.com/Timongcraft/TgcTranslations/blob/master/src/main/java/de/timongcraft/tgctranslations/TranslationManager.java#L240-L256
Then I don't really see the need to force a specific language as the default can be overridden via system property and thus we could migrate translations.

#

I agree on both comments, just for clarification: the referenced fork did away with all language .properties files and only kept the one messages.properties with english messages (in favor of maintainability of the fork itself). Ideally, if we go for an actual migration approach, it would take all of this into account of course.

For the migration we can try to keep the ordering of the properties keys (which is hard because java's properties implementation is backed by an unordered map) or just add the missing keys to the bottom (possibly with a comment like my example)

hexed flickerBOT
#

override online mode?

https://mcsrc.dev/1/diff/26.2-snapshot-2/26.2-pre-2/net/minecraft/client/multiplayer/ClientPacketListener

Looking at the src the client now uses the onlineMode boolean where it before set the Connection#isEncrypted to true when receiving the EncryptionRequestPacket (velocity) packet.

I haven't done any testing, but the old 26.1 client should only return true for Connection#isEncrypted when InitialLoginSessionHandler#generateEncryptionRequest() was called in velocity. If this indeed a drop-in-refactor on the client we should probably set this value to true iff InitialLoginSessionHandler#generateEncryptionRequest() is called

hexed flickerBOT
hexed flickerBOT
#

Hi! This PR adds two cli options which allow loading additional plugin jars and directories in addition to the default plugins folder.

I decided to keep the data directory fixed rather than tying it to the plugin jar location. Paper takes the same approach, and
since this option is likely to be used together with container volumes, which are often mounted readonly, keeping mutable data separate from the plugin binaries seems like the safer choice.

https://github.com/PaperMC/Paper/blob/216388dfdf25b365e0d3ec24748def4dcaa30a76/paper-server/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java#L176

For this reason, loadPlugins still keeps the directory argument so it can be used for the data directory.

https://github.com/vjh0107/Velocity/blob/feat/cli-extra-plugins/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java#L91~L94

hexed flickerBOT
#

HeaderAndFooterPacket has a similar situation, where it's never expected to be decoded:

public HeaderAndFooterPacket() {
  throw new UnsupportedOperationException("Decode is not implemented");
}

@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
  throw new UnsupportedOperationException("Decode is not implemented");
}

I'll update this PR to match this convention.

hexed flickerBOT
#

Right now this kinda a half-implemented feature. We can send player list header/footer Components to the player from proxy plugins, and we can also get the ones last sent by the proxy through plugins, but this is misleading. It's not the current header/footer that the client is displaying, as a backend may have overridden these.

Would it be worth it to introduce an event for this as well? One that would fire both when the backend sends this packet, and also when a plugin sets these manually with ConnectedPlayer#sendPlayerListHeaderAndFooter, allowing the Components to be manipulated?

hexed flickerBOT
#

IMO it is ok as is but I think this could be nice especially with an event to block backend changes.
But currently it is not expected to carry over the backends header and footer on switch which could be confusing for existing plugins. Holding the backends state only for the server connection could be an ugly solution to that.
Seems like 1.8 also already encoded it as component, but I didn't test it.

Small nitpicks:
I believe we don't need the JD for internal silent methods as they should be self explanatory, same with the // forward comment and well the comment above that too.

hexed flickerBOT
#

The readonly thing here is only about the extra plugin paths from the new CLI options (immutable jars, e.g. mounted from a container image). #1736's update folder lives under the normal plugins/ dir, which stays writable like it is now.
So you'd just keep updatable plugins in plugins/ and put the immutable ones behind the readonly mounts. The readonly mount never touches the update folder, so nothing breaks!

hexed flickerBOT
#

~This has an added bonus (or regression?) of keeping a client's player list header/footer when they switch servers now. ConfigSessionHandler line 247 currently sends these values right after switching to PLAY.~

So I was wrong: currently, the playerlist header & footer do not survive a server switch. Looks like this is by design, as we do re-send the header/footer after server switch (as per the PR description), but before that, we clear the header/footer (see ClientPlaySessionHandler#doSwitch, line 606.

This means that right now (before and after this PR), eventhough playerListHeader and playerListFooter are stored in ConnectedPlayer, they are reset on server switch. This PR doesn't change this, it only ensures the stored values are now up to date with what the client actually sees (if the backend changes it)

Apart from an event (should we add this?), this PR should be good to go.

hexed flickerBOT
#

I don't this Velocity should care how the backend server processes the dimension ID. In that sense, I think it's better to just forward whatever GTNH attaches to/expects from a login packet (see GTNewHorizons/Hodgepodge#341). This is the patch I use:

--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java
@@ -53,6 +53,8 @@ public class JoinGamePacket implements MinecraftPacket {
   private int portalCooldown; // 1.20+
   private int seaLevel; // 1.21.2+
   private boolean enforcesSecureChat; // 1.20.5+
+  private int dimensionId; // GTNH
+  private boolean extraDimensionId;

   public int getEntityId() {
     return entityId;
@@ -259,6 +261,10 @@ public class JoinGamePacket implements MinecraftPacket {
     if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) {
       this.showRespawnScreen = buf.readBoolean();
     }
+    if (buf.readableBytes() >=```...
#

I don't this Velocity should care how the backend server processes the dimension ID. In that sense, I think it's better to just forward whatever GTNH attaches to/expects from a login packet (see GTNewHorizons/Hodgepodge#341). This is the patch I use:

--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java
@@ -53,6 +53,8 @@ public class JoinGamePacket implements MinecraftPacket {
   private int portalCooldown; // 1.20+
   private int seaLevel; // 1.21.2+
   private boolean enforcesSecureChat; // 1.20.5+
+  private int dimensionId; // GTNH
+  private boolean extraDimensionId;

   public int getEntityId() {
     return entityId;
@@ -259,6 +261,10 @@ public class JoinGamePacket implements MinecraftPacket {
     if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) {
       this.showRespawnScreen = buf.readBoolean();
     }
+    if (buf.readableBytes() >=```...
hexed flickerBOT
#

its more workaround then fix. Root cause is still unknown.

Allocating and instanly releasing bytebuf should not "leak" memory.

Also it seems these attacks are not affecting bungeecord instances at all, while it does not have any limits put here and there and still has 8MB limit for decompression buffer.

So i suspect velocity is doing something what cause it. How velocity handles compression/decompression and/or by using adaptive allocator (bungee still use pooled)

#

Nobody has provided any useful information on this, hence why a lot of it is all speculation and targetting specific identified issues. The pooled allocator is to my understanding more conservative about its allocations and so is potentially more risk-free here, might be worth switching back, ideally somebody would be able to test this (and either find a mitigation or report upstream)

#

The pooled allocator is to my understanding more conservative about its allocations and so is potentially more risk-free here

VeloFlame (fork of velocity ctd) is using pooled allocated and seems also was affected.

Pooled can also have fragmentation issues when allocating large buffers https://github.com/netty/netty/issues/16827, it seems similar to what these attacks doing, but not exactly. His fragmentation issue is caused by differrent lifetime of allocated bytebuf (store, proccess, release at some point later at random order, if i understand correctly) while velocity is releasing buffers immediately after allocating so it should not suffer from fragmentation, unless allocated buffer for compression somehow cause fragmentation.

#

That could explain it. OOM != memory leak by default. We could just be holding onto too many buffers/decompressed PMs.

In that case we may consider only reading the incoming packets when there are not too many PMs in limbo (due to events). This could explain why I was only able to recreate the issue with a small amount of memory - the network that's experiencing this has many plugins that might be doing work on plugin message events. An attacker could target specific channels that a specific plugin would need to do significant work for in their event handler.

#

If it is a root cause, then i think i wrote all exploits in the first post after looking at code for 10 minutes.
https://github.com/PaperMC/Velocity/issues/1742#issuecomment-4079209228

probably some plugin message packets + plugin what process PluginMessageEvent async

probably a root cause of this "decompression" attack, if async events theory is right

or abusing loginPluginMessages queue.

fixed by #1800

do not have back pressure handling

potential attack vector for some setup configurations

hexed flickerBOT
#

I was able to recreate-ish this attack from backend -> proxy by sending 50 1MB plugin messages per tick (1GB of plugin messages per second). Assuming this compresses well enough for a client to perform the same before the various commits attempting to fix this, this might have been possible by a client.

Same behavior with @Subscribe(async = true) (default) and @Subscribe(async = false).

Interestingly it didn't OOM the proxy, rather it killed the client connection with an OOM exception (differs from what we were seeing earlier):

[20:53:05 ERROR]: [server connection] wouterg_ -> paperqueue: exception encountered in com.velocitypowered.proxy.connection.backend.BackendPlaySessionHandler@27e08e88
java.lang.OutOfMemoryError: Java heap space

Other than sending the PM packets from the backend instead of the client this time, the largest difference is that I now have a plugin that's actually listening to this specific channel. This was not the case with my previous vibecoded mod tests...

hexed flickerBOT
hexed flickerBOT
#

My threory is that netty can decode multiple packets in on read() so i think potential issue is here.

https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java#L410

Due to thenAcceptAsync it always do context switch even if it same thread ( i think). So in theory it could allocate too much these arrays if it will read too much packets in single call, but with default PM size limit(292kb) it need to decode 34k packets in single read() call to crash 10gb instance and https://github.com/PaperMC/Velocity/commit/0219993c8a9adc4bbfe97628a7c0dd40174927f0 is probably effective fix of it.

But initial report of 8MB of decompressed buffers does not allign with that threory unless you disabled PM size limits.

hexed flickerBOT
#

Expected Behavior

n/a

Actual Behavior

# Specify a read timeout for connections here. The default is 30 seconds.
read-timeout = 30000

The default read-timeout of 30 seconds matches the (modern) client value exactly. When a backend server dies/stalls completely (simulate with kill -STOP <pid>), the client disconnects itself after 30 seconds. Instead of Velocity considering this a read timeout (and sending them to a fallback server, or gracefully disconnecting the client), the client hard disconnects with "Timed out".

Setting the default read-timeout to some delta below the client's timeout would be the solution here, e.g. 25000 (25 seconds). I'd argue this could be even lower, but 25 seconds seems like a reasonable value, keeping the client's timeout in mind.

One issue here is that a PR updating the default to 25 seconds won't fix existing setups. Could this warrant a migration?

Steps to Reproduce

n/a

Plugin List

n/a

Velocity Version

latest

Addi...

#

Note that Velocity itself sending any kind of packet to the client after the backend being killed, makes it respect Velocity's read-timeout of 30 seconds again. The client sees this packet from Velocity and doesn't timeout anymore, and Velocity will now see this 30 second timeout and handle the fallback or graceful disconnect itself.

To recreate (failing state, client times out before proxy can do something):

  • kill -9 <backend pid>
  • ensure Velocity doesn't send any packets (no commands), observe vanilla gameplay breaking (blocks not updating, obviously because the backend is dead)
  • client kills connection with "Timed out"

To recreate currently working state:

  • kill -9 <backend pid>
  • make Velocity send you a packet, e.g. execute /velocity info
  • observe Velocity handling read-timeout correctly
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

When a player joins via a forced host whose first backend accepts the TCP connection but then never completes login (e.g. a firewall/anti-DDoS front that answers pings but black-holes logins), they get disconnected with "An internal error occurred in your connection" instead of being moved to the next server in the fallback chain. This issue surfaced on an issue on Velocity-CTD: GemstoneGG#938. I was able to recreate this failure state (simulating a "firewall" that answers pings but black-holes logins) with a Python script that does precisely this.

While the player idles in config waiting for the backend, two ReadTimeoutHandlers with the same duration race:

  • backend connection timeout -> drives the fallback chain (correct)
  • player connection timeout -> hard-disconnects (wrong)

The player-side handler is created first, so it fires first and wins -> disconnect instead of failover.

This is also why just lowering read-timeout didn't help (see referenced i...

hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

This currently crashes Fabric clients when sendAvailableCommands() is sent during non-PLAY phases: https://github.com/FabricMC/fabric-api/issues/5423 / https://github.com/GemstoneGG/Velocity-CTD/issues/959

This needs to be fixed with a simple check in sendAvailableCommands(), possibly returning a CompletableFuture<Boolean> instead?

I'd love to hear what else needs to be done to get this PR in a mergeable state!

hexed flickerBOT
#

A plugin providing a null reason to Player#disconnect will get a vague NullPointerException pointing towards Velocity's/Adventure's internals, even though the problem is a proxy plugin providing a null reason.

This PR makes this non-null contract explicit and throws with a clear NPE.

Exception:

[07:17:53 INFO]: [connected player] xDevanger (/REDACTED): kicked from server lobby: ᴍᴄꜰᴜɴɴʏ.ᴘʟ » Serwer ʟᴏʙʙʏ jest w trakcie restartu...
[07:17:53 INFO]: [server connection] xDevanger -> lobby has disconnected
[07:17:53 WARN] [io.netty.util.concurrent.AbstractEventExecutor]: A task raised an exception. Task: com.velocitypowered.proxy.connection.client.ConnectedPlayer$$Lambda/0x0000000069e10a30@61031780
java.lang.NullPointerException: like
        at java.base/java.util.Objects.requireNonNull(Objects.java:246) ~[?:?]
        at net.kyori.adventure.text.Component.append(Component.java:2112) ~[velocity-ctd-fatjar-3.5.0-SNAPSHOT-301.jar:3.5.0-SNAPSHOT-git-2878e6c7-```...
hexed flickerBOT
#

team_color registration collides with color for 26.2 clients

In ArgumentPropertyRegistry, minecraft:team_color is registered at index 16, but minecraft:color is still mapped to 16 as well (its mapSet(MINECRAFT_1_19, 16) has no later override). Because ArgumentIdentifier applies each mapping to every protocol >= its version, both identifiers resolve to wire ID 16 for a 26.2 client:

  • writeIdentifier serializes both color and team_color as 16
  • readIdentifier iterates an unordered HashMap and returns whichever of the two matches 16 first — nondeterministic

If team_color is just color renamed in 26.2 (keeping index 16), the fix is to retire color at 26.2 using the existing -1 shadow idiom (same as mob_effect/item_enchantment/entity_summon):

// minecraft:color renamed to minecraft:team_color in 26.2
empty(id("minecraft:color", mapSet(MINECRAFT_26_2, -1), mapSet(MINECRAFT_1_19, 16)));
...
empty(id("minecraft:team_color", ma_```...
#

Interestingly, mojang seems to be a "use the same session ID for everybody but regenerate it when the server is empty"

https://mcsrc.dev/1/26.2-rc-2/net/minecraft/server/network/ServerConnectionListener#L225

https://mcsrc.dev/1/26.2-rc-2/net/minecraft/server/network/ServerConnectionListener#L241

This is used purely for metrics on mojangs side, I guess the "good behavior" here would probably be to reproduce that logic

#

@electronicboy beat me by 11 minutes, but I had also dug into the duplicate registration --

tl;dr it works as-is but it's a bit fragile, and there's a cleaner way to express it.

It's not actually problematic on the wire. minecraft:color is mapSet(MINECRAFT_1_19, 16), and ArgumentIdentifier back-fills that id to every version >= 1.19, so id 16 already covers 26.2. The new team_color entry also resolves to 16 on 26.2, so both keys end up in byIdentifier and readIdentifier returns whichever the HashMap iteration happens to hit first (can differ from JVM to JVM, which isn't great...). Since both use EMPTY (no payload) and both map to 16 at 26.2, the re-encoded output is identical either way, so it's correct, but the lookup is non-deterministic, which I don't love.

It looks like this is a rename rather than an insertion too, so Mojang just renamed slot 16 color -> team_color in 26.2.

For 1.19+ the string identifier is purely cosmetic. writeIdentifier/...

#

Yea, I mean, on the wire it doesn't matter but can be an odd moment when debugging it, I'd rather just keep it as two seperate enteries for the sake of not having to worry about that, being able to read back the data when debugging is nice rather than tripping myself up because the id is unexpected

I agree! Retiring the 1.19 - 26.1 registration in favor of this rename for 26.2+ is the canonical solution, ignore my comment :)

#

I'm wondering what Mojang sees as a "session" in this case. Does a new session occur the moment the last player online disconnects (or the moment the first player connects, going from 0 -> 1 players, effectively the same), or does a new session only occur when the server was "paused" due to it being empty (correct me if I'm wrong, but a vanilla server should pause processing ticks after no players have been online for x seconds (30?))

If it's the latter that's harder to match in Velocity... Though regardless, if the client expects the session ID to be the same for everyone connected at the same time, it's most likely better to match this behavior. Easiest is to just generate a session ID on boot.

#

session ID is sent from the proxy long before we have a backend, so that's not an option;

And I guess their modal is basically single player servers where people join in the eveing, have a "play session" together, and leave, and come back in a day or a few hours or whatever. For a larger server which always had players on that would always be shared

#

I have a hunch the session ID will be used in the new friends feature that got dropped from the 26.2 release. I'd guess you would be able to easily add players that are on the same server as you. As this will be done through Mojang's servers, the client can just report the session UUID it got from the connected server, and Mojang can match players against the same session UUID and consider them in the same server.

This would break with the current random UUID per connection approach. Though, it might also be useful for plugins to be able to control this. I could imagine a feature where certain players want to turn this off (staff? content people?). A plugin would be able to check a permission, and override the session UUID with a random one. Though that's all assuming it will be used for the dropped social/friends feature.

hexed flickerBOT
#

Summary

Finishes the Configurate migration: the proxy's configuration moves from velocity.toml (night-config) to velocity.yml (Configurate 4 / YAML). This is a deliberately near-1:1 "yaml-ified" port — same keys, sections and documentation — with richer configuration left as a follow-up. Existing velocity.toml installs are migrated automatically on first start.

Draft / not for immediate merge. Opening this for documentation and review. It builds on the rebased dev/3-configurate work.

What it does

  • ConfigurationLoader.loadConfiguration() is the new entry point, wired into VelocityServer startup and reload. It resolves velocity.yml, migrating a legacy velocity.toml or writing the documented default on first start.
  • Model mapping via Configurate ObjectMapper over @ConfigSerializable VelocityConfiguration, with NamingSchemes.LOWER_CASE_DASHED so camelCase fields map to lower-case-dashed keys. @Setting covers the handful the sche...
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

I added wildcard support to [forced-hosts], resolving #1587. Unit tests pass and checkstyle passes. BUT I have not tested this against a running proxy, so I'd appreciate feedback and help testing this out, especially from those on #1587 who submitted the issue.

What this does

Consider the example from #1587

[forced-hosts]
# Soul Realms
"play.soulrealms.net" = [ "soulrealms-smp-1" ]
"yt.soulrealms.net"   = [ "soulrealms-smp-1" ]
"ip.soulrealms.net"   = [ "soulrealms-smp-1" ]
"soulrealms.net"      = [ "soulrealms-smp-1" ]

Now this can be changed to:

[forced-hosts]
# Soul Realms
"*.soulrealms.net" = [ "soulrealms-smp-1" ]
"soulrealms.net"   = [ "soulrealms-smp-1" ]

How matching works

I added AddressUtil.isHostMatchingPattern(pattern, host). Patterns are matched on each label where * matches exactly one label. To be matched, patterns must have the same number of labels as the host. Matching is case insensiti...

hexed flickerBOT
#

Hi everyone, I'm drafting a PR to get this done: https://github.com/PaperMC/Velocity/pull/1826. If you could give it a test, that would be a huge help. To try it out:

  1. Build the proxy yourself from my repo and branch feature/wildcard-forced-hosts OR you can use my development build velocity-proxy-3.5.0-UNOFFICIAL-wildcard-forced-host-testing-build.zip (Java 21) from commit 415d127 if it's easier.
  2. Add a wildcard entry to [forced-hosts] in your velocity.toml like @JosTheDude described.
  3. Connect using a few different subdomains (play.example.com, yt.example.com, etc.) and make sure you land on the right server.

Let me know if the matching is working correctly (espe...

hexed flickerBOT
#
3z

Expected Behavior

Usernames accepted during offline-mode login should be validated against the vanilla character set (alphanumerics and underscore, length 1–16) to prevent log injection and identity confusion.

Actual Behavior

ServerLoginPacket.java (line 84–88) only checks for emptiness:

username = ProtocolUtils.readString(buf, 16);
if (username.isEmpty()) {
    throw EMPTY_USERNAME;
}

readString(buf, 16) permits any UTF-8 content up to 16 code units. In offline mode, this name becomes the profile name and the offline UUID seed, and is forwarded to the backend as-is. Spaces, control characters (\n, \r), section signs (§), and Unicode homoglyphs are all accepted.

This only affects offline-mode or forwarding=none/legacy setups. Online-mode names come from Mojang and are already safe, and modern forwarding wraps the name in an HMAC.

Steps to Reproduce

  1. Run Velocity with online-mode=false (or forwarding-mode=none).
  2. Connect with a...
#
3z

Expected Behavior

Malformed or too-short UDP datagrams sent to the GS4 query port should be silently dropped.

Actual Behavior

GameSpyQueryHandler.channelRead0 (around line 107–119) reads magic bytes, type, and sessionId via readUnsignedByte()/readByte()/readInt() without first checking readableBytes(). A datagram shorter than 7 bytes throws IndexOutOfBoundsException on every packet, caught and logged at WARNING level:

Error whilst handling query packet from <sender>

The challenge-token validation for 0x00 stat responses (line 139–143) correctly prevents reflection of the larger response — a spoofed source can't obtain a valid token. So this is just log noise and minor CPU overhead, not a reflection amplification issue.

Steps to Reproduce

  1. Enable enable-query=true in velocity.toml.
  2. Send a short UDP datagram to the query port:
    echo -n "x" | nc -u <proxy-ip> <query-port>
    
  3. Each datagram produces a WARNING-level...
hexed flickerBOT
#

The code references are accurate: ServerLoginPacket.decode() only checks for emptiness, and in offline mode the raw name flows into GameProfile.forOfflinePlayer() and the offline UUID seed with no charset validation. So the underlying observation is correct.

That said, I don't think the proposed fix is the right approach, and most of the listed harms don't really hold up:

  • Log injection is the one genuinely valid concern. A username with \r\n gets embedded unescaped in ConnectedPlayer.toString() and logged at INFO ("{} has connected"), so it can forge log lines. This is worth fixing.
  • Identity confusion via homoglyphs isn't a real vulnerability here. The offline UUID is a deterministic hash of the exact name bytes, so distinct strings produce distinct UUIDs with no collision. Look-alike names are a social issue, not a proxy one.
  • Forwarding the name as-is is by design. Modern forwarding HMAC-wraps the name, and for none/legacy the backend does its own validatio...
hexed flickerBOT
#

Three nits:

1 - This is a pretty large method chain that's duplicated twice, it would probably make sense to extract this out into its own method.

2 - the "or default" value is always computed, even in the case where the exact virtualHostStr mapping is present. And the .orElse(Collections.emptyList()) also always returns an empty list. Fortunately this doesn't comput or allocate anything, but this means that all three cases - exact match, pattern match, empty list - are always computed, even when an exact match could abort the latter two, which is a smell

3 - Essentially this is a giant double if-else statement:

if (/* exact forced host defined */) {
  serversToTry = /* exact forced host */;
} else if (/* isHostMatchingPattern matches */) {
  serversToTry = /* isHostMatchingPattern result */;
} else {
  serversToTry = Collections.emptyList();
}

However this is disguised in getOrDefault (first two if and if else) and .orElse(Collections.emptyList...

hexed flickerBOT
#

Requested Feature

In the proxy module and all other modules that are licensed under the GNU GPL, I'd like to switch from the full license header (as seen in HEADER.txt) to a modern SPDX declaration.

In every file, the header would then change to:

/**
 * SPDX-FileCopyrightText: Copyright (C) $YEAR Velocity Contributors
 * SPDX-License-Identifier: GPL-3.0-only
 */

For more context, see https://spdx.dev/learn/handling-license-info/

The api module is so small (and licensed differently), it might not even need it, but we may consider also using the SPDX declaration there.

This could also be a good oppertunity to bump the copyright year to 2018-2026 on all files, if a substantial change has been made to the file recently (this can probably be automated).

Why is this needed?

n/a

Alternative Solutions

n/a

Additional Information

No response

#

Another nit: try to match the test class name with the class it's actually testing. In this case it would be AddressUtil. If this test class already exists and/or these test methods would seem out of place, consider adding a nested class, e.g.

class AddressUtilTest {

  class HostMatchingPatternTest {
    /* isHostMatchingPattern test methods */
  }
}

Some people are against this, but when doing this, I would also consider statically importing the AddressUtil.isHostMatchingPattern method.

hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

Expected Behavior

If I join a server that rejects my connection during the configuration phase, I expect to get kicked from that backend server as expected, velocity then would choose a new server and I get moved to a new server without problems.

Actual Behavior

If you get kicked during configuration phase, the client state gets bugged and the client thinks its receiving registry entries a second time, which Mojang doesn't allow, so the player gets kicked for an invalid network state.

I have also confirmed this on a vanilla client!

The following network crash log is generated by the client:

disconnect-2026-06-26_02.56.46-client.txt_

Steps to Reproduce

Option 1:
Run my example repo: https://github.com/davidmayr/PaperMC-Velocity-Issue-Protocol-Error-During-Config

  • Contains a plugin that kicks during configuration
  • Contains a docker compose file for running two...
hexed flickerBOT
hexed flickerBOT
#

Here are some screenshots of the packets that get sent to the client using a proxy in front of velocity to capture packet flow. The packet names are from minestom as this proxy layer that I just added in front of Velocity was made by someone in the minestom community.

<img width="680" height="692" alt="Image" src="https://github.com/user-attachments/assets/1bec138a-f787-404c-adbe-db5731f29db9" />

client isn't garaunteed to be configured, which looks like what probably happened with your client?

The issue isn't that the client wasn't configured; it's that the client was configured twice, and the registry data collided. I can understand that this might be a difficult fix.

I'm unaware if the client actually needs all registries to be configured, but couldn't Velocity track somehow if the client would be ready to go and do the following:

  • If the client is ready to leave, kick them out of configuration and immediately kick them back in, then send them the stuff from the b...
#

I don't really want to tie proxy state expectations too much towards tracking the clients exact current state, etc.

Sending a transfer packet was considered for dealing with modded support but it would involve a lot of risky changes, and breaks the reliability aspect, we cannot know if your ingress address is stable enough to allow us to reuse it to connect, especially as what the client reports isn't garaunteed, i.e. if you're using SRV records to RR the DNS it will break as we get the resolved address

#

I also don't think it's a good idea to keep track of the full client state. But tbh, you don't even need to keep track of everything. Once a packet for e.g. registry X flows through, count that registry as sent. Not optimal, but fine enough I guess

If the server has sent all at least one packet for each registry that the client demands, count the configuration phase as being able to finish early. If it can't be finished early and there are no registry or tag packets, allow the player to continue. If there are registry packets but not all of them, abort mission and kick player. Obviously that doesn't really work on modded, but in the worst case It's probably not any worse than what we have right now.

The only state that would require is a list of Keys of sent registries and a static hardcoded list of registries the client requires for its version to compare against.

The other option is really just to kick the player for the kick reason of the server. In any case, that's b...

hexed flickerBOT
#

Thinking about it, any registry packet being sent would be enough to try to terminate early. Once that happened, conflict likely occurs anyway, so it doesn't really matter if the client errors out because we leave the config phase with invalid state or if we let the client error out with duplicates. But it would at least improve the situation, whilst keeping it just as bad as it was before in any other case

hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

Expected Behavior

A slow DNS lookup for one backend shouldn't delay connecting to other backends. Sending a player to a server whose hostname is fine (cached or fast to resolve) should be quick regardless of other lookups happening at the same time.

Actual Behavior

SeparatePoolInetNameResolver runs every backend hostname lookup on a single thread, with a 30s cache:

https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/network/netty/SeparatePoolInetNameResolver.java#L57-L90

If a lookup is slow, every other uncached resolution queues behind it on that one thread until it returns. Cached names are fine (they skip the executor), but anything not currently cached has to wait. There's also no timeout on the lookup, so one hung resolve pins the resolver thread for the full JDK resolver duration.

We hit this on a fallback. A player gets kicked and is redirected to the fallback server (defined by hostname). The connect can't sta...

hexed flickerBOT
#

Fixes #1833.

SeparatePoolInetNameResolver runs every backend hostname lookup on a single thread (Executors.newSingleThreadExecutor). Because the lookups are blocking, one slow resolve serializes all the others behind it: an unrelated stalled DNS query delays connecting players to otherwise-healthy servers until it returns or the JDK resolver times out. We saw fallback connections to a hostname-defined server hang for 10–18s while the same name resolved instantly on another thread.

This swaps the single-thread executor for a small bounded pool (ThreadPoolExecutor, 0–8 threads, 60s keep-alive, SynchronousQueue), so a slow lookup only ties up one thread instead of blocking all resolution. Threads are created on demand and reaped when idle, so it costs nothing at rest.

hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

Runtime.version() contains the feature version and extras

Runtime.version().toString() will produce an output like 21.0.1+12-LTS, not the 21.0.1 that System.getProperty("java.version") provides.

I'd propose the following refactor:

metrics.addCustomChart(new DrilldownPie("java_version", () -> {
  Runtime.Version version = Runtime.version();

  return Map.of(
      "Java " + version.feature(),
      Map.of(System.getProperty("java.version"), 1));
}));

but it seems a bit odd to mix the old "api" (system property) and the new Runtime.Version API here. Thoughts?

hexed flickerBOT
#

Expected Behavior

no warn?

Actual Behavior

[16:09:49 WARN] [io.netty.util.ReferenceCountUtil]: Failed to release a message: AdaptivePoolingAllocator$AdaptiveByteBuf(freed)
io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1

Steps to Reproduce

idk

Plugin List

....[16:55:03 INFO]: Plugins: velocity, alert, cleanstaffchat, floodgate, kaurivpn, luckperms, mckotlin-velocity, nantibot, nlogin, nuvotifier, packetevents, pl-hide-pro, skinsrestorer, vlobby

Velocity Version

....[16:54:48 INFO]: Velocity 3.5.0-SNAPSHOT (git-a7581821-b605)

Additional Information

No response

hexed flickerBOT
#

Sorry.

io.netty.channel.socket.ChannelOutputShutdownException: Channel output shutdown
at io.netty.channel.AbstractChannel$AbstractUnsafe.shutdownOutput(AbstractChannel.java:520) ~[server.jar:3.5.0-SNAPSHOT (git-a7581821-b605)]
at io.netty.channel.AbstractChannel$AbstractUnsafe.handleWriteError(AbstractChannel.java:816) ~[server.jar:3.5.0-SNAPSHOT (git-a7581821-b605)]
at io.netty.channel.AbstractChannel$AbstractUnsafe.flush0(AbstractChannel.java:796) ~[server.jar:3.5.0-SNAPSHOT (git-a7581821-b605)]
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.epollOutReady(AbstractEpollChannel.java:619) ~[server.jar:3.5.0-SNAPSHOT (git-a7581821-b605)]
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.handle(AbstractEpollChannel.java:477) ~[server.jar:3.5.0-SNAPSHOT (git-a7581821-b605)]
at io.netty.channel.epoll.EpollIoHandler$DefaultEpollIoRegistration.handle(EpollIoHandler.java:349) ~[server.jar:3.5.0-SNA...

#

Expected Behavior

No warn?

Actual Behavior

io.netty.channel.socket.ChannelOutputShutdownException: Channel output shutdown
at io.netty.channel.AbstractChannel$AbstractUnsafe.shutdownOutput(AbstractChannel.java:520) ~[server.jar:3.5.0-SNAPSHOT (git-a7581821-b605)]
at io.netty.channel.AbstractChannel$AbstractUnsafe.handleWriteError(AbstractChannel.java:816) ~[server.jar:3.5.0-SNAPSHOT (git-a7581821-b605)]
at io.netty.channel.AbstractChannel$AbstractUnsafe.flush0(AbstractChannel.java:796) ~[server.jar:3.5.0-SNAPSHOT (git-a7581821-b605)]
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.epollOutReady(AbstractEpollChannel.java:619) ~[server.jar:3.5.0-SNAPSHOT (git-a7581821-b605)]
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.handle(AbstractEpollChannel.java:477) ~[server.jar:3.5.0-SNAPSHOT (git-a7581821-b605)]
at io.netty.channel.epoll.EpollIoHandler$DefaultEpollIoRegistration.handle(E...

#

Your error is occuring in the networking stack, in which you have plugins which are fairly well established to hijack into, many of which have consistently broken on version updates. If you can reproduce the issue without plugins we can look into it, but given the highly invasive nature of those plugins, especially to support offline mode, a setup we do not support, you are on your own

hexed flickerBOT
#

Requested Feature

Add a unique, stable connection identifier to the InboundConnection interface so plugins can reliably correlate the same TCP connection across PreLoginEvent, GameProfileRequestEvent, and LoginEvent.

Something like:

public interface InboundConnection {
    // existing methods...

    /**
     * Returns a unique identifier for this connection, stable for the
     * entire lifetime of the connection.
     */
    UUID getConnectionId();  // or long
}

The identifier should be:

  • Generated once per TCP connection (in HandshakeSessionHandler / InitialInboundConnection constructor)
  • Available across all login-phase events: PreLoginEvent, GameProfileRequestEvent, and ideally also from LoginEvent (via ConnectedPlayer or a similar accessor)
  • Consistent even when the InboundConnection wrapper object changes internally

Why is this needed?

event.getConnection() returns a LoginInboundConnection that—while the same instanc...

hexed flickerBOT
hexed flickerBOT
#

MinecraftConnection would be the right class to hold the UUID here, as all [Velocity]InboundConnection implementations hold a reference to a shared reference to this class. MinecraftConnection 正是用来存储 UUID 的合适类,因为所有的 [Velocity]InboundConnection 实现都包含了对这个类的引用。

Thanks for the guidance! I'm working on this in my fork and should have a PR ready soon.

hexed flickerBOT
#

Add a stable connection identifier to MinecraftConnection

A unique connectionId (AtomicLong) is held in MinecraftConnection,
the shared underlying instance across all InboundConnection wrappers,
surfaced via InboundConnection.getConnectionId().

This allows plugins to reliably track the same TCP connection across
PreLoginEvent, GameProfileRequestEvent, and LoginEvent, even when
the InboundConnection wrapper object differs between events.

Tested with both online-mode and offline-mode connections:

  • [x] Same connection across all login-phase events: same connId
  • [x] Different players: different connId
  • [x] Premium (Mojang-authenticated) connections: connId stable
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

I think we should shift from a per-connection id to a session id: one UUID that's stable for the player's entire session on the proxy, and shared across the client<->proxy connection and every proxy<->backend connection they open. So I'd name it sessionId rather than connectionId.

Right now that shared/stable property doesn't actually hold. Since every MinecraftConnection generates its own UUID.randomUUID() in the constructor:

  • each serverbound connection gets a different id, regenerated on every server switch, so it's neither stable nor shared with the client side
  • outgoing server-list pings (VelocityRegisteredServer) allocate an id they never use

It happens to work for the login-phase events only because the InboundConnection wrappers all delegate to the one clientbound MinecraftConnection, but that's incidental, not a contract we could promise to plugins.

What I'd propose:

  • @Nullable UUID sessionId on MinecraftConnection, passed in via th...
hexed flickerBOT
#

Interestingly, we did use Netty's async DNS resolver here in the past: 6e7c0298de14c1af2ad5aa4161f6bde054f6bb74

But this seems to come with some downsides (as the javadoc comment on SeparatePoolInetNameResolver also suggests). Wanted to suggest this as an alternative but that seems like a no-go.

One nit:
When 8 concurrent DNS requests occur, we will now throw RejectedExecutionException on the next request (SynchronousQueue has no size).
Something like

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    MAX_RESOLVE_THREADS, MAX_RESOLVE_THREADS,
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(), // unbounded -> never rejects
    new ThreadFactoryBuilder()
        .setNameFormat("Velocity DNS Resolver #%d")
        .setDaemon(true)
        .build());
executor.allowCoreThreadTimeOut(true);

Wouldn't throw and has the same queueing behavior as before, just with multiple threads (corePoolSize must also be MAX_RESOLVE_THREADS, ot...

hexed flickerBOT
hexed flickerBOT
#

Expected Behavior

Without this error, the player would have continued playing on the server without disconnecting from Velocity.

Actual Behavior

If the server opens a Dialog screen during the Configuration stage, sending the client's ServerboundCustomClickActionPacket packet causes the proxy to return an error:

[16:18:57 INFO]: [connected player] krytickYT (/0.0.0.0:0) has disconnected: An internal error occurred in your connection.
[16:18:57 ERROR]: [connected player] krytickYT (/0.0.0.0:0): exception encountered in com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler@34c000a7
io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
        at io.netty.util.internal.RefCnt.throwIllegalRefCountOnRelease(RefCnt.java:239) ~[velocity-3.5.0-SNAPSHOT-6```...
hexed flickerBOT
#

I also wrote a plugin for test

[13:42:36 INFO]: Booting up Velocity 3.5.0-SNAPSHOT (git-48e7519e)...
[13:42:36 INFO]: Connections will use NIO channels, Java compression, Java ciphers
[13:42:36 INFO]: Loading localizations...
[13:42:36 INFO]: Loading plugins...
[13:42:36 INFO]: Loaded plugin cidtest 3.5.0-SNAPSHOT by xfy2412
[13:42:36 INFO]: Loaded 2 plugins
[13:42:36 INFO] [cidtest]: CIDTest Loaded — Testing the stability of getSessionId()
[13:42:36 INFO]: Listening on /[0:0:0:0:0:0:0:0]:25565
[13:42:36 INFO]: Done (0.62s)!
[13:42:54 INFO] [cidtest]: [CIDTEST] PreLogin: xfy2102 connId=e1e309f8-e7c3-424c-8b02-56153af8633f hash=1834913037
[13:42:56 INFO] [cidtest]: [CIDTEST] GameProfileRequest: xfy2102 connId=e1e309f8-e7c3-424c-8b02-56153af8633f hash=1834913037
[13:42:56 INFO]: [connected player] xfy2102 (/192.168.110.1:4564) has connected
[13:42:56 INFO] [cidtest]: [CIDTEST] Login: xfy2102 connId=e1e309f8-e7c3-424c-8b02-56153af8633f uuid=2439bef3-b313-424b-a22c```...
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

I agree, though it'll be best-effort and we probably dont want to match every case. Splitting on those 3 delimiters seems reasonable (case insensitive) but afaik minimessage also supports self-closing tags (?), possibly spaces between the tag name and the self-closing /. Emulating the minimessage parsing engine seems out of scope, and deserializing+serializing would be too intrusive imo.

hexed flickerBOT
#
[PaperMC/Velocity] branch deleted: update/adventure/5.0.0
hexed flickerBOT
hexed flickerBOT
#

Expected Behavior

Usage of the Velocity API is backward compatible for plugins compiled against previous Velocity minor versions, like 3.2 or 3.0.

Actual Behavior

Velocity 3.5.0 switched to Adventure 5 with https://github.com/PaperMC/Velocity/pull/1774. I have no idea why no one mentioned this, but this is a breaking change. Adventure 4 was a public API dependency that integrated with Velocity's APIs, and now plugins are broken because of it.

Steps to Reproduce

N/A

Plugin List

N/A

Velocity Version

Build 608, commit 97b386d86ff99f787117cb30e8a4b50c83dba6c2

Additional Information

No response

hexed flickerBOT
#

I kind of have to agree here:

As a velocity user, I should be able to update my proxy to a new build within my same version without my setup breaking, ever. Bumping a provided dependency to a new major version with a ton of breaking changes should not be done without any sort of version bump, in my opinion at least. In the past, such dependency bumps seem to have come with at least a minor version bump (e.g., 3.2.0 or 3.5.0), so I'm also a bit surprised this didn't happen here.

Well technically speaking, this is a snapshot version of a future 3.5.0, which is already a version bump from the 3.4.0 released in January, with decent compatibility with plugins made for 3.4.0 version if they didn't ignore a 5 year old deprecation

Saying "this is a snapshot of a future 3.5.0" is kind of wrong considering velocity is doing rolling releases and 3.5.0 is the only version we currently support (so it isn't really a future version).

hexed flickerBOT
#

Being honest versioning has been a sore point for a while as part of a desire to have certain things nailed down before we start going for all of the next lineup of breaking stuff; I could revert and bump out a new version and repull it back in, but it does create a weird contention point around stuff; versioning gets weird because we don't maintain two versions at once, and doing a release right now feels icky

hexed flickerBOT
#

adventure-platform is no longer maintained, and there has been some discussion in the original adventure 5 PR on what to do with adventure-platform-facet (see https://github.com/PaperMC/Velocity/pull/1774#issuecomment-4500943734 and replies).

We were only providing some internal pointers that, to my understanding, didn't do anything anyways since there is no adventure-platform for velocity. This PR drops adventure-platform-facet entirely.

hexed flickerBOT
#

@mbax Bluntness is not incivility - you just have a different cultural background.

I don't think that users' opinions about what feels "nice" in an API should have a bearing on the discussion. Fundamentally, semver violations are about a lack of communication, and the norms of versioning that exist for a project influence its users' expectations and behavior toward each other. @Emilxyz wrote this in plain terms. It's silly to use handwaving about deprecation and vague notions of version development to dismiss these concerns.

If the Velocity maintainers want to formally abandon semver and deliver breaking changes in minor releases, that is their prerogative. If so, it should probably be clearly communicated: "Warning: This Velocity version may not be compatible with plugins built for Velocity 3.0-3.4." However, I doubt such a disclaimer is what the maintainers want, because as I think I'm driving at, there's an aesthetic want to keep versions low while also pursuing API rene...

hexed flickerBOT
#

To be honest, I'm not really too intune with Semver, especially when it comes to updating libraries;

Major releases tend to be aligned for our own set of chunky breakages which we have aligned up, this was basically my strategy of being able to update a library for alignment with the rest of the ecosystem given the demand for it, without said library bump being inaccessible bethind a large swath of internal breakages

hexed flickerBOT
#

I think that the big issue is that semver requires releases, and we haven't had releases in so long because theres always something we wanted to address before cutting a release. I'm not exactly sure what to do going forward, my understanding is that the nature of semver applying to releases is that we could to a "technically" and do a 4.x release right now and remain compliant with it, but the entire signaling around semver requires releases, not an ever-evolving roll of snapshots, which is where the issue lies.

I'm not really sure on what I should do right this second in regards to versioning here, the semver promise was always a weird one once we stopped cutting releases

hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

@A248 wrote:

@mbax Bluntness is not incivility - you just have a different cultural background.

Incorrectly declaring a statement to be opinion, so that you can dismiss it as opinion, is uncivil. If you continue to rudely dismiss statements as invalid just because you don't like the reality they contain and then hide behind "I'm just blunt" as an excuse (along with some other rude behavior seen here and in other issues), you won't be commenting here much longer.

hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

Velocity already parses and validates signed Minecraft player chat internally, but PlayerChatEvent currently exposes only the player and plaintext message to plugins.

Carry the original signed-message metadata across the public API boundary so ecosystem plugins can inspect modern Minecraft signed chat without depending on internal packet classes.

This change is backward compatible and does not alter validation, forwarding, cancellation or rewrite behavior.

hexed flickerBOT
hexed flickerBOT
#

Summary

Velocity parses and forwards modern Minecraft signed player chat internally,
but the public PlayerChatEvent API currently exposes only the player and
the plaintext message.

This pull request exposes the original protocol-level player chat metadata
through the public API and adds a general Velocity-managed path for delivering
server-decorated player chat to selected proxy players.

The implementation is not tied to a specific chat plugin. It provides a
general API foundation that plugins can use without accessing internal packet
classes or implementing version-specific Minecraft protocol handling.

Motivation

Modern Minecraft player chat is no longer just a plaintext string. Depending
on the protocol generation, it includes:

  • sender identity
  • original message body
  • signed state
  • signature
  • timestamp
  • salt
  • keyed or session identity
  • last-seen and message-chain metadata

Velocity already processes much of this information in...

hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

To be honest, I'm not really too intune with Semver, especially when it comes to updating libraries;

Major releases tend to be aligned for our own set of chunky breakages which we have aligned up, this was basically my strategy of being able to update a library for alignment with the rest of the ecosystem given the demand for it, without said library bump being inaccessible bethind a large swath of internal breakages

I know this issue is now closed and a few days old now, but I wanted to comment on something interesting here.

While yes technically Adventure is a library, it is so heavily baked into both Paper and Velocity API that I feel it is more appropriate to treat it as a component in each software's API, not just as a library. IMO this is the most sensical solution as normal library version bumps (even if major) would only affect the software internals, not its API. This would then mean semver would beg a major version bump on any breaking adventure changes, wh...

hexed flickerBOT
#

Summary

Two small defensive fixes for the proxy's network layer.

1. Guard GameSpyQueryHandler against short datagrams

GameSpyQueryHandler.channelRead0() reads magic bytes, type, and sessionId
without first checking readableBytes(). A malformed UDP datagram shorter
than 7 bytes causes IndexOutOfBoundsException on every packet, spamming
the log at WARNING level.

This adds a readableBytes() >= 7 guard at the start so short datagrams
are silently dropped instead.

2. Validate usernames in ServerLoginPacket

In offline mode, ServerLoginPacket.decode() accepts any UTF-8 content
as a username. This username becomes the offline UUID seed and is
forwarded to the backend as-is, enabling log injection and identity
confusion.

This adds a [A-Za-z0-9_]+ regex check (the vanilla Minecraft character
set), rejecting non-conforming usernames with a QuietDecoderException.

Testing

./gradlew :velocity-proxy:check passes all checks (compile, Checkstyl...

hexed flickerBOT
#

Please update the PR description accordingly. And throwing the second part of this issue away could be a shame, if you think it's still relevant, I'd encourage you to open it under a second PR.

Other than "testing" it by compiling & running unit tests, were you able to verify that this indeed fixes the referenced issue? The issue contains a MRE in the form of a command to run.

One issue still:
"7" is a weird magic number here. Is there any way we could calculate this value? Most likely not, but at the very least I'd like this to be a private static field with a short javadoc explaining this is the shorted expected message and which branch reads this shortest possible message (QUERY_TYPE_HANDSHAKE / QUERY_TYPE_STAT).

hexed flickerBOT
#

What

ClientPlaySessionHandler#handle(ClientSettingsPacket) fetches
player.getConnectedServer(), null-checks it, then discards the local
variable and calls player.getConnectedServer() a second time before
calling .ensureConnected() on the result.

Between these two calls the player's connected server can change on
another thread (e.g. mid server-switch), so the second call can return
null (NPE on .ensureConnected()) or a connection whose
ensureConnected() throws IllegalStateException, crashing/kicking
the session — the same class of bug fixed for the chat/command packet
handlers in f6fbd25 ("Downgrade severity of handling several incoming
user input packet states"), which however did not touch this handler.

Fix

Reuse the already null-checked local serverConnection reference
instead of re-querying player.getConnectedServer().

Testing

Built locally with ./gradlew build; no behavioral change for the
non-racy path, since the secon...

hexed flickerBOT
hexed flickerBOT
hexed flickerBOT
#

This just fixes an annoying bug in the Finnish language, and almost all other languages where ! would appear in a specific language. The messages.properties for localization does not even have ! for the English language, which was weird.
My IDE was also complaining about some unused variables in other languages, so I just removed them.

I just also realized there's a crowdin for velocitypowered, my bad, but this seems faster.

before:
<img width="410" height="104" alt="image" src="https://github.com/user-attachments/assets/f0d3f4c3-4ad4-451f-96d5-d132e5e5d925" />
after:
<img width="416" height="104" alt="image" src="https://github.com/user-attachments/assets/c2662d20-b1f0-463d-99a1-d091964482bd" />

hexed flickerBOT
#

Fixes #1841.

During configuration, a custom click packet could reach an already connected backend after the in-flight connection was cleared. That path fell through to generic forwarding without retaining the reference-counted packet, so the backend encoder and inbound decoder could over-release the same frame.

Resolve either the in-flight or connected server before forwarding and retain the packet for the backend write. The regression test recreates the shared-frame lifecycle and verifies that forwarding completes without an IllegalReferenceCountException.

Validation:

  • Focused ClientConfigSessionHandlerTest
  • Full :velocity-proxy:test
  • Main and test Checkstyle
  • Full Gradle build

All checks completed successfully.

hexed flickerBOT
hexed flickerBOT
#

Fixes #1841.

During configuration, a ServerboundCustomClickActionPacket arriving from a dialog screen could reach an already connected backend after the in-flight connection was cleared. The old code checked only getConnectionInFlight(), so the packet fell through to handleGeneric(). That path only retained PluginMessagePacket before writing — other reference-counted packets were forwarded without retain, causing the backend encoder and MinecraftConnection.channelRead's finally block to over-release the same frame (IllegalReferenceCountException).

Two-layer fix:

  • handle() now uses getConnectionInFlightOrConnectedServer() to forward the packet with retain() regardless of whether the server is still in-flight or already connected
  • handleGeneric() retains any ByteBufHolder packet before write instead of special-casing only PluginMessagePacket — defense in depth against the same bug for future packets

Added ClientConfigSessionHandlerTest with 4 tests cove...

#

Root cause: ClientConfigSessionHandler.handle(ServerboundCustomClickActionPacket) used getConnectionInFlight(), so when the in-flight connection was already cleared (server connected), the packet fell through to handleGeneric(). That path only retained PluginMessagePacket before forwarding — other ByteBufHolder packets were not retained.

The backend encoder releases the packet after encoding, then MinecraftConnection.channelRead() finally block calls ReferenceCountUtil.release(msg) again → double-free on the same underlying ByteBuf.

Fix in #1856:

  1. handle() now uses getConnectionInFlightOrConnectedServer() so the packet is retained before write regardless of connection state
  2. handleGeneric() retains any ByteBufHolder (not just PluginMessagePacket), preventing the same class of bug for future refcounted packets

All 205 proxy tests pass, Checkstyle and Spotless clean.

hexed flickerBOT
#

Fixes #1828.

GameSpyQueryHandler.channelRead0 reads magic bytes, type, and sessionId without first checking readableBytes(). A malformed UDP datagram shorter than 7 bytes throws IndexOutOfBoundsException on every packet, caught and logged at WARNING:

Error whilst handling query packet from

Fix:

  • Added MINIMUM_MESSAGE_LENGTH (7) — a named constant with javadoc explaining the 2+1+4 byte layout and which handler branch reads the shortest valid message (QUERY_TYPE_HANDSHAKE).
  • Silently drop any datagram shorter than MINIMUM_MESSAGE_LENGTH before touching any byte.
  • Also guard the challenge-token read in the QUERY_TYPE_STAT branch (4 bytes after the 7-byte header) for defence-in-depth — a 7-byte datagram with type 0x00 would pass the header check but fail on the readInt() for the challenge token.

Test coverage:

  • Short/empty datagrams are silently dropped (no exception)
  • 7-byte STAT datagram without challenge token is silently dropped (no...
hexed flickerBOT
#

Expected Behavior

A native 26.2 client should complete the login handshake through Velocity and connect to the proxy/backend normally, the same way it does when translated through ViaVersion from 26.1. Velocity should be able to send a valid login_finished packet to a 26.2 client without the client's own decoder rejecting it.

Actual Behavior

The 26.2 client is kicked immediately during login with DecoderException: Failed to decode packet 'clientbound/minecraft:login_finished', even with no backends running and no other plugins involved.

Steps to Reproduce

Run Velocity 4.1.0-SNAPSHOT (build 8 or 9)
Set online-mode = false in velocity.toml
Start Velocity (backend servers can be stopped/offline — not required to reproduce)
Connect using a vanilla, unmodified 26.2 Java client with an offline account
Client fails at login with the DecoderException / login_finished decode error every time

To confirm it's isolated to native 26.2 handling:

Remove ViaVersion and Via...

hexed flickerBOT