Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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;
Expand Down Expand Up @@ -511,7 +512,8 @@ public static VelocityConfiguration read(Path path) throws IOException {
new KeyAuthenticationMigration(),
new MotdMigration(),
new MiniMessageTranslationsMigration(),
new TransferIntegrationMigration()
new TransferIntegrationMigration(),
new PacketLimiterMigration()
};

for (final ConfigurationMigration migration : migrations) {
Expand Down Expand Up @@ -1004,12 +1006,13 @@ public boolean isEnabled() {
/**
* Configuration for packet limiting.
*
* @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 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.
Expand All @@ -1022,7 +1025,8 @@ public static PacketLimiterConfig fromConfig(CommentedConfig config) {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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, 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) {
// Check whether the first three config attributes are present before setting them.
// The packet-limiter section was added before this migration was written, so
// freshly generated configs may exist with version 2.7 (which we would migrate) that
// do contain a configured packet-limiter. Version 2.8 adds decompressed-bytes-per-second
// and switches the defaults to only use this by default.
boolean previouslyConfigured = false;
if (config.get("packet-limiter.interval") == null) {
config.set("packet-limiter.interval", DEFAULT.interval());
} else {
previouslyConfigured = true;
}

if (config.get("packet-limiter.packets-per-second") == null) {
config.set("packet-limiter.packets-per-second", DEFAULT.pps());
} else {
previouslyConfigured = true;
}

if (config.get("packet-limiter.bytes-per-second") == null) {
config.set("packet-limiter.bytes-per-second", DEFAULT.bytes());
} else {
previouslyConfigured = true;
}

// Only enable decompressed-bytes-per-second if the packet limiter was not configured previously.
config.set("packet-limiter.decompressed-bytes-per-second",
previouslyConfigured
? -1
: 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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
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;
Expand Down Expand Up @@ -571,6 +573,14 @@ public void setCompressionThreshold(int threshold) {
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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ protected void initChannel(final Channel ch) {
int configuredPacketsPerSecond = packetLimiterConfig.pps();
int configuredBytes = packetLimiterConfig.bytes();

if (configuredInterval > 0 && (configuredBytes > 0 || configuredPacketsPerSecond > 0)) {
if (configuredInterval > 0 && (configuredBytes > 0 || configuredPacketsPerSecond > 0)) {
ch.pipeline().get(MinecraftVarintFrameDecoder.class).setPacketLimiter(
new SimpleBytesPerSecondLimiter(configuredPacketsPerSecond, configuredBytes, configuredInterval)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@
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.
Expand All @@ -49,6 +52,8 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {

private int threshold;
private final VelocityCompressor compressor;
@Nullable
private PacketLimiter packetLimiter;

/**
* Creates a new {@code MinecraftCompressDecoder} with the specified compression {@code threshold}.
Expand All @@ -73,6 +78,10 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) t
+ " 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;
}
Expand All @@ -99,6 +108,10 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) t
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();
Expand All @@ -116,4 +129,8 @@ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
public void setThreshold(int threshold) {
this.threshold = threshold;
}

public void setPacketLimiter(@Nullable PacketLimiter packetLimiter) {
this.packetLimiter = packetLimiter;
}
}
12 changes: 10 additions & 2 deletions proxy/src/main/resources/default-velocity.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
Expand Down