mirror of
https://github.com/PaperMC/Velocity.git
synced 2026-06-21 17:57:47 +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.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) {
|
||||||
@@ -1004,12 +1006,13 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
/**
|
/**
|
||||||
* Configuration for packet limiting.
|
* Configuration for packet limiting.
|
||||||
*
|
*
|
||||||
* @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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class ServerChannelInitializer extends ChannelInitializer<Channel> {
|
|||||||
int configuredPacketsPerSecond = packetLimiterConfig.pps();
|
int configuredPacketsPerSecond = packetLimiterConfig.pps();
|
||||||
int configuredBytes = packetLimiterConfig.bytes();
|
int configuredBytes = packetLimiterConfig.bytes();
|
||||||
|
|
||||||
if (configuredInterval > 0 && (configuredBytes > 0 || configuredPacketsPerSecond > 0)) {
|
if (configuredInterval > 0 && (configuredBytes > 0 || configuredPacketsPerSecond > 0)) {
|
||||||
ch.pipeline().get(MinecraftVarintFrameDecoder.class).setPacketLimiter(
|
ch.pipeline().get(MinecraftVarintFrameDecoder.class).setPacketLimiter(
|
||||||
new SimpleBytesPerSecondLimiter(configuredPacketsPerSecond, configuredBytes, configuredInterval)
|
new SimpleBytesPerSecondLimiter(configuredPacketsPerSecond, configuredBytes, configuredInterval)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user