Add decompressed-bytes-per-second rate limit, update packet limiter defaults (#1786)

* Add decompressed-bytes-per-second packet limiter, update defaults
* Revert "Add compression ratio limiter"
This commit is contained in:
Wouter Gritter
2026-05-24 17:59:34 +02:00
committed by GitHub
parent b72cf26802
commit 25fbd833cd
7 changed files with 113 additions and 17 deletions

View File

@@ -31,6 +31,7 @@ import com.velocitypowered.proxy.config.migration.ForwardingMigration;
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration; import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration; import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
import com.velocitypowered.proxy.config.migration.MotdMigration; import com.velocitypowered.proxy.config.migration.MotdMigration;
import com.velocitypowered.proxy.config.migration.PacketLimiterMigration;
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration; import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.proxy.util.AddressUtil;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -511,7 +512,8 @@ public class VelocityConfiguration implements ProxyConfig {
new KeyAuthenticationMigration(), new KeyAuthenticationMigration(),
new MotdMigration(), new MotdMigration(),
new MiniMessageTranslationsMigration(), new MiniMessageTranslationsMigration(),
new TransferIntegrationMigration() new TransferIntegrationMigration(),
new PacketLimiterMigration()
}; };
for (final ConfigurationMigration migration : migrations) { for (final ConfigurationMigration migration : migrations) {
@@ -1007,9 +1009,10 @@ public class VelocityConfiguration implements ProxyConfig {
* @param interval the interval in seconds to measure packets over * @param interval the interval in seconds to measure packets over
* @param pps the maximum number of packets per second allowed * @param pps the maximum number of packets per second allowed
* @param bytes the maximum number of bytes per second allowed * @param bytes the maximum number of bytes per second allowed
* @param bytesAfterDecompression the maximum number of decompressed bytes per second allowed
*/ */
public record PacketLimiterConfig(int interval, int pps, int bytes) { public record PacketLimiterConfig(int interval, int pps, int bytes, int bytesAfterDecompression) {
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, 500, -1); public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, -1, -1, 5242880);
/** /**
* returns a PacketLimiterConfig from a config section, or the default if the section is null. * returns a PacketLimiterConfig from a config section, or the default if the section is null.
@@ -1022,7 +1025,8 @@ public class VelocityConfiguration implements ProxyConfig {
return new PacketLimiterConfig( return new PacketLimiterConfig(
config.getIntOrElse("interval", DEFAULT.interval()), config.getIntOrElse("interval", DEFAULT.interval()),
config.getIntOrElse("packets-per-second", DEFAULT.pps()), config.getIntOrElse("packets-per-second", DEFAULT.pps()),
config.getIntOrElse("bytes-per-second", DEFAULT.bytes()) config.getIntOrElse("bytes-per-second", DEFAULT.bytes()),
config.getIntOrElse("decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression())
); );
} else { } else {
return DEFAULT; return DEFAULT;

View File

@@ -29,7 +29,8 @@ public sealed interface ConfigurationMigration
KeyAuthenticationMigration, KeyAuthenticationMigration,
MotdMigration, MotdMigration,
MiniMessageTranslationsMigration, MiniMessageTranslationsMigration,
TransferIntegrationMigration { TransferIntegrationMigration,
PacketLimiterMigration {
boolean shouldMigrate(CommentedFileConfig config); boolean shouldMigrate(CommentedFileConfig config);
void migrate(CommentedFileConfig config, Logger logger) throws IOException; void migrate(CommentedFileConfig config, Logger logger) throws IOException;

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2026 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.config.migration;
import static com.velocitypowered.proxy.config.VelocityConfiguration.PacketLimiterConfig.DEFAULT;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import org.apache.logging.log4j.Logger;
/**
* Configuration migration for the new [packet-limiter] section.
* Config version 2.7 may contain this section with only the `interval`, `packets-per-second`
* and `bytes-per-second` attributes. Config version 2.8 enforces these exist, adds the new
* `decompressed-bytes-per-second` attribute, adjusts the new default, and adds comments.
*/
public final class PacketLimiterMigration implements ConfigurationMigration {
@Override
public boolean shouldMigrate(CommentedFileConfig config) {
return configVersion(config) < 2.8;
}
@Override
public void migrate(CommentedFileConfig config, Logger logger) {
config.set("packet-limiter.interval", DEFAULT.interval());
config.set("packet-limiter.packets-per-second", DEFAULT.pps());
config.set("packet-limiter.bytes-per-second", DEFAULT.bytes());
config.set("packet-limiter.decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression());
config.setComment("packet-limiter.interval", """
Size of the moving time window in seconds used to calculate average rates.
A larger window tolerates short bursts while still enforcing the configured limits over time.""");
config.setComment("packet-limiter.packets-per-second", """
Maximum average number of packets per second a client may send. -1 disables this check.""");
config.setComment("packet-limiter.bytes-per-second", """
Maximum average number of compressed (on-wire) bytes per second a client may send. -1 disables this check.""");
config.setComment("packet-limiter.decompressed-bytes-per-second", """
Maximum average number of decompressed bytes per second a client may send.
Protects against compression bomb attacks where small packets expand to excessive sizes after decompression.
-1 disables this check.""");
config.set("config-version", "2.8");
}
}

View File

@@ -38,7 +38,9 @@ import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler; import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
import com.velocitypowered.proxy.connection.client.StatusSessionHandler; import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
import com.velocitypowered.proxy.network.Connections; import com.velocitypowered.proxy.network.Connections;
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.VelocityConnectionEvent; import com.velocitypowered.proxy.protocol.VelocityConnectionEvent;
import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder;
@@ -571,6 +573,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder); channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder); channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
var packetLimiterConfig = server.getConfiguration().getPacketLimiterConfig();
if (minecraftDecoder.getDirection() == ProtocolUtils.Direction.SERVERBOUND
&& packetLimiterConfig.interval() > 0
&& packetLimiterConfig.bytesAfterDecompression() > 0) {
decoder.setPacketLimiter(new SimpleBytesPerSecondLimiter(
-1, packetLimiterConfig.bytesAfterDecompression(), packetLimiterConfig.interval()));
}
channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED); channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED);
} }
} }

View File

@@ -22,11 +22,14 @@ import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame; import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame;
import com.velocitypowered.natives.compression.VelocityCompressor; import com.velocitypowered.natives.compression.VelocityCompressor;
import com.velocitypowered.proxy.network.limiter.PacketLimiter;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.util.except.QuietDecoderException;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder; import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List; import java.util.List;
import org.jspecify.annotations.Nullable;
/** /**
* Decompresses a Minecraft packet. * Decompresses a Minecraft packet.
@@ -44,11 +47,12 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
Boolean.getBoolean("velocity.increased-compression-cap") Boolean.getBoolean("velocity.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : SERVERBOUND_MAXIMUM_UNCOMPRESSED_SIZE; ? HARD_MAXIMUM_UNCOMPRESSED_SIZE : SERVERBOUND_MAXIMUM_UNCOMPRESSED_SIZE;
private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation"); private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation");
private static final double MAX_COMPRESSION_RATIO = Double.parseDouble(System.getProperty("velocity.max-compression-ratio", "64"));
private final ProtocolUtils.Direction direction; private final ProtocolUtils.Direction direction;
private int threshold; private int threshold;
private final VelocityCompressor compressor; private final VelocityCompressor compressor;
@Nullable
private PacketLimiter packetLimiter;
/** /**
* Creates a new {@code MinecraftCompressDecoder} with the specified compression {@code threshold}. * Creates a new {@code MinecraftCompressDecoder} with the specified compression {@code threshold}.
@@ -73,10 +77,13 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
+ " threshold %s", actualUncompressedSize, threshold); + " threshold %s", actualUncompressedSize, threshold);
} }
// This message is not compressed. // This message is not compressed.
if (packetLimiter != null && !packetLimiter.account(in.readableBytes())) {
throw new QuietDecoderException("Rate limit exceeded while processing packets for %s"
.formatted(ctx.channel().remoteAddress()));
}
out.add(in.retain()); out.add(in.retain());
return; return;
} }
int length = in.readableBytes();
checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than" checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than"
+ " threshold %s", claimedUncompressedSize, threshold); + " threshold %s", claimedUncompressedSize, threshold);
@@ -88,10 +95,6 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP, checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP,
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize, "Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
SERVERBOUND_UNCOMPRESSED_CAP); SERVERBOUND_UNCOMPRESSED_CAP);
double maxCompressedAllowed = length * MAX_COMPRESSION_RATIO;
checkFrame(claimedUncompressedSize <= maxCompressedAllowed,
"Uncompressed size %s exceeds ratio threshold of %s for compressed sized %s", claimedUncompressedSize,
maxCompressedAllowed, length);
} }
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in); ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize); ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
@@ -99,6 +102,10 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize); compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
checkFrame(uncompressed.writerIndex() == claimedUncompressedSize, checkFrame(uncompressed.writerIndex() == claimedUncompressedSize,
"Decompressed size %s does not match claimed uncompressed size %s", uncompressed.writerIndex(), claimedUncompressedSize); "Decompressed size %s does not match claimed uncompressed size %s", uncompressed.writerIndex(), claimedUncompressedSize);
if (packetLimiter != null && !packetLimiter.account(claimedUncompressedSize)) {
throw new QuietDecoderException("Rate limit exceeded while processing packets for %s"
.formatted(ctx.channel().remoteAddress()));
}
out.add(uncompressed); out.add(uncompressed);
} catch (Exception e) { } catch (Exception e) {
uncompressed.release(); uncompressed.release();
@@ -116,4 +123,8 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
public void setThreshold(int threshold) { public void setThreshold(int threshold) {
this.threshold = threshold; this.threshold = threshold;
} }
public void setPacketLimiter(@Nullable PacketLimiter packetLimiter) {
this.packetLimiter = packetLimiter;
}
} }

View File

@@ -1,5 +1,5 @@
# Config version. Do not change this # Config version. Do not change this
config-version = "2.7" config-version = "2.8"
# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565. # What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565.
bind = "0.0.0.0:25565" bind = "0.0.0.0:25565"
@@ -75,9 +75,17 @@ sample-players-in-ping = false
enable-player-address-logging = true enable-player-address-logging = true
[packet-limiter] [packet-limiter]
# Size of the moving time window in seconds used to calculate average rates.
# A larger window tolerates short bursts while still enforcing the configured limits over time.
interval = 7 interval = 7
packets-per-second = 500 # Maximum average number of packets per second a client may send. -1 disables this check.
packets-per-second = -1
# Maximum average number of compressed (on-wire) bytes per second a client may send. -1 disables this check.
bytes-per-second = -1 bytes-per-second = -1
# Maximum average number of decompressed bytes per second a client may send.
# Protects against compression bomb attacks where small packets expand to excessive sizes after decompression.
# -1 disables this check.
decompressed-bytes-per-second = 5242880
[servers] [servers]
# Configure your servers here. Each key represents the server's name, and the value # Configure your servers here. Each key represents the server's name, and the value