mirror of
https://github.com/PaperMC/Velocity.git
synced 2026-06-21 09:47:44 +02:00
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:
@@ -31,6 +31,7 @@ import com.velocitypowered.proxy.config.migration.ForwardingMigration;
|
||||
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
|
||||
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
|
||||
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.util.AddressUtil;
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
@@ -511,7 +512,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
new KeyAuthenticationMigration(),
|
||||
new MotdMigration(),
|
||||
new MiniMessageTranslationsMigration(),
|
||||
new TransferIntegrationMigration()
|
||||
new TransferIntegrationMigration(),
|
||||
new PacketLimiterMigration()
|
||||
};
|
||||
|
||||
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 pps the maximum number of packets 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 static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, 500, -1);
|
||||
public record PacketLimiterConfig(int interval, int pps, int bytes, int bytesAfterDecompression) {
|
||||
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.
|
||||
@@ -1022,7 +1025,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
return new PacketLimiterConfig(
|
||||
config.getIntOrElse("interval", DEFAULT.interval()),
|
||||
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 {
|
||||
return DEFAULT;
|
||||
|
||||
@@ -29,7 +29,8 @@ public sealed interface ConfigurationMigration
|
||||
KeyAuthenticationMigration,
|
||||
MotdMigration,
|
||||
MiniMessageTranslationsMigration,
|
||||
TransferIntegrationMigration {
|
||||
TransferIntegrationMigration,
|
||||
PacketLimiterMigration {
|
||||
boolean shouldMigrate(CommentedFileConfig config);
|
||||
|
||||
void migrate(CommentedFileConfig config, Logger logger) throws IOException;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,9 @@ import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
|
||||
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
|
||||
import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
|
||||
import com.velocitypowered.proxy.network.Connections;
|
||||
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.VelocityConnectionEvent;
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,14 @@ import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
|
||||
import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame;
|
||||
|
||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||
import com.velocitypowered.proxy.network.limiter.PacketLimiter;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import java.util.List;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Decompresses a Minecraft packet.
|
||||
@@ -44,11 +47,12 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
||||
Boolean.getBoolean("velocity.increased-compression-cap")
|
||||
? 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 double MAX_COMPRESSION_RATIO = Double.parseDouble(System.getProperty("velocity.max-compression-ratio", "64"));
|
||||
private final ProtocolUtils.Direction direction;
|
||||
|
||||
private int threshold;
|
||||
private final VelocityCompressor compressor;
|
||||
@Nullable
|
||||
private PacketLimiter packetLimiter;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
// 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());
|
||||
return;
|
||||
}
|
||||
int length = in.readableBytes();
|
||||
|
||||
checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than"
|
||||
+ " threshold %s", claimedUncompressedSize, threshold);
|
||||
@@ -88,10 +95,6 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
||||
checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP,
|
||||
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
|
||||
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 uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
|
||||
@@ -99,6 +102,10 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
||||
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
|
||||
checkFrame(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);
|
||||
} catch (Exception e) {
|
||||
uncompressed.release();
|
||||
@@ -116,4 +123,8 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
||||
public void setThreshold(int threshold) {
|
||||
this.threshold = threshold;
|
||||
}
|
||||
|
||||
public void setPacketLimiter(@Nullable PacketLimiter packetLimiter) {
|
||||
this.packetLimiter = packetLimiter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 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.
|
||||
bind = "0.0.0.0:25565"
|
||||
@@ -75,9 +75,17 @@ sample-players-in-ping = false
|
||||
enable-player-address-logging = true
|
||||
|
||||
[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
|
||||
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
|
||||
# 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]
|
||||
# Configure your servers here. Each key represents the server's name, and the value
|
||||
|
||||
Reference in New Issue
Block a user