Skip to content

Commit 25fbd83

Browse files
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"
1 parent b72cf26 commit 25fbd83

7 files changed

Lines changed: 113 additions & 17 deletions

File tree

proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
3232
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
3333
import com.velocitypowered.proxy.config.migration.MotdMigration;
34+
import com.velocitypowered.proxy.config.migration.PacketLimiterMigration;
3435
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
3536
import com.velocitypowered.proxy.util.AddressUtil;
3637
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -511,7 +512,8 @@ public static VelocityConfiguration read(Path path) throws IOException {
511512
new KeyAuthenticationMigration(),
512513
new MotdMigration(),
513514
new MiniMessageTranslationsMigration(),
514-
new TransferIntegrationMigration()
515+
new TransferIntegrationMigration(),
516+
new PacketLimiterMigration()
515517
};
516518

517519
for (final ConfigurationMigration migration : migrations) {
@@ -1004,12 +1006,13 @@ public boolean isEnabled() {
10041006
/**
10051007
* Configuration for packet limiting.
10061008
*
1007-
* @param interval the interval in seconds to measure packets over
1008-
* @param pps the maximum number of packets per second allowed
1009-
* @param bytes the maximum number of bytes per second allowed
1009+
* @param interval the interval in seconds to measure packets over
1010+
* @param pps the maximum number of packets per second allowed
1011+
* @param bytes the maximum number of bytes per second allowed
1012+
* @param bytesAfterDecompression the maximum number of decompressed bytes per second allowed
10101013
*/
1011-
public record PacketLimiterConfig(int interval, int pps, int bytes) {
1012-
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, 500, -1);
1014+
public record PacketLimiterConfig(int interval, int pps, int bytes, int bytesAfterDecompression) {
1015+
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, -1, -1, 5242880);
10131016

10141017
/**
10151018
* returns a PacketLimiterConfig from a config section, or the default if the section is null.
@@ -1022,7 +1025,8 @@ public static PacketLimiterConfig fromConfig(CommentedConfig config) {
10221025
return new PacketLimiterConfig(
10231026
config.getIntOrElse("interval", DEFAULT.interval()),
10241027
config.getIntOrElse("packets-per-second", DEFAULT.pps()),
1025-
config.getIntOrElse("bytes-per-second", DEFAULT.bytes())
1028+
config.getIntOrElse("bytes-per-second", DEFAULT.bytes()),
1029+
config.getIntOrElse("decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression())
10261030
);
10271031
} else {
10281032
return DEFAULT;

proxy/src/main/java/com/velocitypowered/proxy/config/migration/ConfigurationMigration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ public sealed interface ConfigurationMigration
2929
KeyAuthenticationMigration,
3030
MotdMigration,
3131
MiniMessageTranslationsMigration,
32-
TransferIntegrationMigration {
32+
TransferIntegrationMigration,
33+
PacketLimiterMigration {
3334
boolean shouldMigrate(CommentedFileConfig config);
3435

3536
void migrate(CommentedFileConfig config, Logger logger) throws IOException;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright (C) 2026 Velocity Contributors
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.velocitypowered.proxy.config.migration;
19+
20+
import static com.velocitypowered.proxy.config.VelocityConfiguration.PacketLimiterConfig.DEFAULT;
21+
22+
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
23+
import org.apache.logging.log4j.Logger;
24+
25+
/**
26+
* Configuration migration for the new [packet-limiter] section.
27+
* Config version 2.7 may contain this section with only the `interval`, `packets-per-second`
28+
* and `bytes-per-second` attributes. Config version 2.8 enforces these exist, adds the new
29+
* `decompressed-bytes-per-second` attribute, adjusts the new default, and adds comments.
30+
*/
31+
public final class PacketLimiterMigration implements ConfigurationMigration {
32+
33+
@Override
34+
public boolean shouldMigrate(CommentedFileConfig config) {
35+
return configVersion(config) < 2.8;
36+
}
37+
38+
@Override
39+
public void migrate(CommentedFileConfig config, Logger logger) {
40+
config.set("packet-limiter.interval", DEFAULT.interval());
41+
config.set("packet-limiter.packets-per-second", DEFAULT.pps());
42+
config.set("packet-limiter.bytes-per-second", DEFAULT.bytes());
43+
config.set("packet-limiter.decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression());
44+
45+
config.setComment("packet-limiter.interval", """
46+
Size of the moving time window in seconds used to calculate average rates.
47+
A larger window tolerates short bursts while still enforcing the configured limits over time.""");
48+
49+
config.setComment("packet-limiter.packets-per-second", """
50+
Maximum average number of packets per second a client may send. -1 disables this check.""");
51+
52+
config.setComment("packet-limiter.bytes-per-second", """
53+
Maximum average number of compressed (on-wire) bytes per second a client may send. -1 disables this check.""");
54+
55+
config.setComment("packet-limiter.decompressed-bytes-per-second", """
56+
Maximum average number of decompressed bytes per second a client may send.
57+
Protects against compression bomb attacks where small packets expand to excessive sizes after decompression.
58+
-1 disables this check.""");
59+
60+
config.set("config-version", "2.8");
61+
}
62+
}

proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
3939
import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
4040
import com.velocitypowered.proxy.network.Connections;
41+
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
4142
import com.velocitypowered.proxy.protocol.MinecraftPacket;
43+
import com.velocitypowered.proxy.protocol.ProtocolUtils;
4244
import com.velocitypowered.proxy.protocol.StateRegistry;
4345
import com.velocitypowered.proxy.protocol.VelocityConnectionEvent;
4446
import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder;
@@ -571,6 +573,14 @@ public void setCompressionThreshold(int threshold) {
571573
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
572574
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
573575

576+
var packetLimiterConfig = server.getConfiguration().getPacketLimiterConfig();
577+
if (minecraftDecoder.getDirection() == ProtocolUtils.Direction.SERVERBOUND
578+
&& packetLimiterConfig.interval() > 0
579+
&& packetLimiterConfig.bytesAfterDecompression() > 0) {
580+
decoder.setPacketLimiter(new SimpleBytesPerSecondLimiter(
581+
-1, packetLimiterConfig.bytesAfterDecompression(), packetLimiterConfig.interval()));
582+
}
583+
574584
channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED);
575585
}
576586
}

proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ protected void initChannel(final Channel ch) {
8080
int configuredPacketsPerSecond = packetLimiterConfig.pps();
8181
int configuredBytes = packetLimiterConfig.bytes();
8282

83-
if (configuredInterval > 0 && (configuredBytes > 0 || configuredPacketsPerSecond > 0)) {
83+
if (configuredInterval > 0 && (configuredBytes > 0 || configuredPacketsPerSecond > 0)) {
8484
ch.pipeline().get(MinecraftVarintFrameDecoder.class).setPacketLimiter(
8585
new SimpleBytesPerSecondLimiter(configuredPacketsPerSecond, configuredBytes, configuredInterval)
8686
);

proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@
2222
import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame;
2323

2424
import com.velocitypowered.natives.compression.VelocityCompressor;
25+
import com.velocitypowered.proxy.network.limiter.PacketLimiter;
2526
import com.velocitypowered.proxy.protocol.ProtocolUtils;
27+
import com.velocitypowered.proxy.util.except.QuietDecoderException;
2628
import io.netty.buffer.ByteBuf;
2729
import io.netty.channel.ChannelHandlerContext;
2830
import io.netty.handler.codec.MessageToMessageDecoder;
2931
import java.util.List;
32+
import org.jspecify.annotations.Nullable;
3033

3134
/**
3235
* Decompresses a Minecraft packet.
@@ -44,11 +47,12 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
4447
Boolean.getBoolean("velocity.increased-compression-cap")
4548
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : SERVERBOUND_MAXIMUM_UNCOMPRESSED_SIZE;
4649
private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation");
47-
private static final double MAX_COMPRESSION_RATIO = Double.parseDouble(System.getProperty("velocity.max-compression-ratio", "64"));
4850
private final ProtocolUtils.Direction direction;
4951

5052
private int threshold;
5153
private final VelocityCompressor compressor;
54+
@Nullable
55+
private PacketLimiter packetLimiter;
5256

5357
/**
5458
* Creates a new {@code MinecraftCompressDecoder} with the specified compression {@code threshold}.
@@ -73,10 +77,13 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) t
7377
+ " threshold %s", actualUncompressedSize, threshold);
7478
}
7579
// This message is not compressed.
80+
if (packetLimiter != null && !packetLimiter.account(in.readableBytes())) {
81+
throw new QuietDecoderException("Rate limit exceeded while processing packets for %s"
82+
.formatted(ctx.channel().remoteAddress()));
83+
}
7684
out.add(in.retain());
7785
return;
7886
}
79-
int length = in.readableBytes();
8087

8188
checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than"
8289
+ " threshold %s", claimedUncompressedSize, threshold);
@@ -88,17 +95,17 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) t
8895
checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP,
8996
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
9097
SERVERBOUND_UNCOMPRESSED_CAP);
91-
double maxCompressedAllowed = length * MAX_COMPRESSION_RATIO;
92-
checkFrame(claimedUncompressedSize <= maxCompressedAllowed,
93-
"Uncompressed size %s exceeds ratio threshold of %s for compressed sized %s", claimedUncompressedSize,
94-
maxCompressedAllowed, length);
9598
}
9699
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
97100
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
98101
try {
99102
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
100103
checkFrame(uncompressed.writerIndex() == claimedUncompressedSize,
101104
"Decompressed size %s does not match claimed uncompressed size %s", uncompressed.writerIndex(), claimedUncompressedSize);
105+
if (packetLimiter != null && !packetLimiter.account(claimedUncompressedSize)) {
106+
throw new QuietDecoderException("Rate limit exceeded while processing packets for %s"
107+
.formatted(ctx.channel().remoteAddress()));
108+
}
102109
out.add(uncompressed);
103110
} catch (Exception e) {
104111
uncompressed.release();
@@ -116,4 +123,8 @@ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
116123
public void setThreshold(int threshold) {
117124
this.threshold = threshold;
118125
}
126+
127+
public void setPacketLimiter(@Nullable PacketLimiter packetLimiter) {
128+
this.packetLimiter = packetLimiter;
129+
}
119130
}

proxy/src/main/resources/default-velocity.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Config version. Do not change this
2-
config-version = "2.7"
2+
config-version = "2.8"
33

44
# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565.
55
bind = "0.0.0.0:25565"
@@ -75,9 +75,17 @@ sample-players-in-ping = false
7575
enable-player-address-logging = true
7676

7777
[packet-limiter]
78+
# Size of the moving time window in seconds used to calculate average rates.
79+
# A larger window tolerates short bursts while still enforcing the configured limits over time.
7880
interval = 7
79-
packets-per-second = 500
81+
# Maximum average number of packets per second a client may send. -1 disables this check.
82+
packets-per-second = -1
83+
# Maximum average number of compressed (on-wire) bytes per second a client may send. -1 disables this check.
8084
bytes-per-second = -1
85+
# Maximum average number of decompressed bytes per second a client may send.
86+
# Protects against compression bomb attacks where small packets expand to excessive sizes after decompression.
87+
# -1 disables this check.
88+
decompressed-bytes-per-second = 5242880
8189

8290
[servers]
8391
# Configure your servers here. Each key represents the server's name, and the value

0 commit comments

Comments
 (0)