Skip to content

Commit dbdb758

Browse files
authored
Merge pull request #20 from PaperMC/dev/3.0.0
[pull] main from PaperMC:dev/3.0.0
2 parents 4329771 + 7ffa43f commit dbdb758

File tree

14 files changed

+264
-32
lines changed

14 files changed

+264
-32
lines changed

api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,59 @@ public interface ProxyConfig {
148148
* @return read timeout (in milliseconds)
149149
*/
150150
int getReadTimeout();
151+
152+
/**
153+
* Get the rate limit for how fast a player can execute commands.
154+
*
155+
* @return the command rate limit (in milliseconds)
156+
*/
157+
int getCommandRatelimit();
158+
159+
/**
160+
* Get whether we should forward commands to the backend if the player is rate limited.
161+
*
162+
* @return whether to forward commands if rate limited
163+
*/
164+
boolean isForwardCommandsIfRateLimited();
165+
166+
/**
167+
* Get the kick limit for commands that are rate limited.
168+
* If this limit is 0 or less, the player will be not be kicked.
169+
*
170+
* @return the rate limited command rate limit
171+
*/
172+
int getKickAfterRateLimitedCommands();
173+
174+
/**
175+
* Get whether the proxy should kick players who are command rate limited.
176+
*
177+
* @return whether to kick players who are rate limited
178+
*/
179+
default boolean isKickOnCommandRateLimit() {
180+
return getKickAfterRateLimitedCommands() > 0;
181+
}
182+
183+
/**
184+
* Get the rate limit for how fast a player can tab complete.
185+
*
186+
* @return the tab complete rate limit (in milliseconds)
187+
*/
188+
int getTabCompleteRatelimit();
189+
190+
/**
191+
* Get the kick limit for tab completes that are rate limited.
192+
* If this limit is 0 or less, the player will be not be kicked.
193+
*
194+
* @return the rate limited command rate limit
195+
*/
196+
int getKickAfterRateLimitedTabCompletes();
197+
198+
/**
199+
* Get whether the proxy should kick players who are tab complete rate limited.
200+
*
201+
* @return whether to kick players who are rate limited
202+
*/
203+
default boolean isKickOnTabCompleteRateLimit() {
204+
return getKickAfterRateLimitedTabCompletes() > 0;
205+
}
151206
}

proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
import io.netty.channel.EventLoopGroup;
7676
import java.io.IOException;
7777
import java.io.InputStream;
78+
import java.net.InetAddress;
7879
import java.net.InetSocketAddress;
7980
import java.net.http.HttpClient;
8081
import java.nio.file.Files;
@@ -162,7 +163,9 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
162163
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
163164
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
164165
private final VelocityConsole console;
165-
private @MonotonicNonNull Ratelimiter ipAttemptLimiter;
166+
private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter;
167+
private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter;
168+
private @MonotonicNonNull Ratelimiter<UUID> tabCompleteRateLimiter;
166169
private final VelocityEventManager eventManager;
167170
private final VelocityScheduler scheduler;
168171
private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar();
@@ -295,6 +298,8 @@ void start() {
295298
}
296299

297300
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
301+
commandRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getCommandRatelimit());
302+
tabCompleteRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getTabCompleteRatelimit());
298303
loadPlugins();
299304

300305
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
@@ -654,10 +659,18 @@ public HttpClient createHttpClient() {
654659
return cm.createHttpClient();
655660
}
656661

657-
public Ratelimiter getIpAttemptLimiter() {
662+
public @MonotonicNonNull Ratelimiter<InetAddress> getIpAttemptLimiter() {
658663
return ipAttemptLimiter;
659664
}
660665

666+
public @MonotonicNonNull Ratelimiter<UUID> getCommandRateLimiter() {
667+
return commandRateLimiter;
668+
}
669+
670+
public @MonotonicNonNull Ratelimiter<UUID> getTabCompleteRateLimiter() {
671+
return tabCompleteRateLimiter;
672+
}
673+
661674
/**
662675
* Checks if the {@code connection} can be registered with the proxy.
663676
*

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,11 @@ public boolean validate() {
234234
valid = false;
235235
}
236236

237+
if (advanced.commandRateLimit < 0) {
238+
logger.error("Invalid command rate limit {}", advanced.commandRateLimit);
239+
valid = false;
240+
}
241+
237242
loadFavicon();
238243

239244
return valid;
@@ -355,6 +360,31 @@ public int getReadTimeout() {
355360
return advanced.getReadTimeout();
356361
}
357362

363+
@Override
364+
public int getCommandRatelimit() {
365+
return advanced.getCommandRateLimit();
366+
}
367+
368+
@Override
369+
public int getTabCompleteRatelimit() {
370+
return advanced.getTabCompleteRateLimit();
371+
}
372+
373+
@Override
374+
public int getKickAfterRateLimitedTabCompletes() {
375+
return advanced.getKickAfterRateLimitedTabCompletes();
376+
}
377+
378+
@Override
379+
public boolean isForwardCommandsIfRateLimited() {
380+
return advanced.isForwardCommandsIfRateLimited();
381+
}
382+
383+
@Override
384+
public int getKickAfterRateLimitedCommands() {
385+
return advanced.getKickAfterRateLimitedCommands();
386+
}
387+
358388
public boolean isProxyProtocol() {
359389
return advanced.isProxyProtocol();
360390
}
@@ -733,6 +763,16 @@ private static class Advanced {
733763
private boolean acceptTransfers = false;
734764
@Expose
735765
private boolean enableReusePort = false;
766+
@Expose
767+
private int commandRateLimit = 50;
768+
@Expose
769+
private boolean forwardCommandsIfRateLimited = true;
770+
@Expose
771+
private int kickAfterRateLimitedCommands = 5;
772+
@Expose
773+
private int tabCompleteRateLimit = 50;
774+
@Expose
775+
private int kickAfterRateLimitedTabCompletes = 10;
736776

737777
private Advanced() {
738778
}
@@ -759,6 +799,11 @@ private Advanced(CommentedConfig config) {
759799
this.logPlayerConnections = config.getOrElse("log-player-connections", true);
760800
this.acceptTransfers = config.getOrElse("accepts-transfers", false);
761801
this.enableReusePort = config.getOrElse("enable-reuse-port", false);
802+
this.commandRateLimit = config.getIntOrElse("command-rate-limit", 25);
803+
this.forwardCommandsIfRateLimited = config.getOrElse("forward-commands-if-rate-limited", true);
804+
this.kickAfterRateLimitedCommands = config.getIntOrElse("kick-after-rate-limited-commands", 0);
805+
this.tabCompleteRateLimit = config.getIntOrElse("tab-complete-rate-limit", 10); // very lenient
806+
this.kickAfterRateLimitedTabCompletes = config.getIntOrElse("kick-after-rate-limited-tab-completes", 0);
762807
}
763808
}
764809

@@ -826,6 +871,26 @@ public boolean isEnableReusePort() {
826871
return enableReusePort;
827872
}
828873

874+
public int getCommandRateLimit() {
875+
return commandRateLimit;
876+
}
877+
878+
public boolean isForwardCommandsIfRateLimited() {
879+
return forwardCommandsIfRateLimited;
880+
}
881+
882+
public int getKickAfterRateLimitedCommands() {
883+
return kickAfterRateLimitedCommands;
884+
}
885+
886+
public int getTabCompleteRateLimit() {
887+
return tabCompleteRateLimit;
888+
}
889+
890+
public int getKickAfterRateLimitedTabCompletes() {
891+
return kickAfterRateLimitedTabCompletes;
892+
}
893+
829894
@Override
830895
public String toString() {
831896
return "Advanced{"

proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
113113

114114
private CompletableFuture<Void> configSwitchFuture;
115115

116+
private int failedTabCompleteAttempts;
117+
116118
/**
117119
* Constructs a client play session handler.
118120
*
@@ -671,6 +673,17 @@ private boolean handleCommandTabComplete(TabCompleteRequestPacket packet) {
671673
return false;
672674
}
673675

676+
if (!server.getTabCompleteRateLimiter().attempt(player.getUniqueId())) {
677+
if (server.getConfiguration().isKickOnTabCompleteRateLimit()
678+
&& failedTabCompleteAttempts++ >= server.getConfiguration().getKickAfterRateLimitedTabCompletes()) {
679+
player.disconnect(Component.translatable("velocity.kick.tab-complete-rate-limit"));
680+
}
681+
682+
return true;
683+
}
684+
685+
failedTabCompleteAttempts = 0;
686+
674687
server.getCommandManager().offerBrigadierSuggestions(player, command)
675688
.thenAcceptAsync(suggestions -> {
676689
if (suggestions.isEmpty()) {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright (C) 2025 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.protocol.packet.chat;
19+
20+
import com.velocitypowered.api.proxy.Player;
21+
import com.velocitypowered.proxy.VelocityServer;
22+
import com.velocitypowered.proxy.protocol.MinecraftPacket;
23+
import net.kyori.adventure.text.Component;
24+
25+
public abstract class RateLimitedCommandHandler<T extends MinecraftPacket> implements CommandHandler<T> {
26+
27+
private final Player player;
28+
private final VelocityServer velocityServer;
29+
30+
private int failedAttempts;
31+
32+
protected RateLimitedCommandHandler(Player player, VelocityServer velocityServer) {
33+
this.player = player;
34+
this.velocityServer = velocityServer;
35+
}
36+
37+
@Override
38+
public boolean handlePlayerCommand(MinecraftPacket packet) {
39+
if (packetClass().isInstance(packet)) {
40+
if (!velocityServer.getCommandRateLimiter().attempt(player.getUniqueId())) {
41+
if (failedAttempts++ >= velocityServer.getConfiguration().getKickAfterRateLimitedCommands()) {
42+
player.disconnect(Component.translatable("velocity.kick.command-rate-limit"));
43+
}
44+
45+
if (velocityServer.getConfiguration().isForwardCommandsIfRateLimited()) {
46+
return false; // Send the packet to the server
47+
}
48+
} else {
49+
failedAttempts = 0;
50+
}
51+
52+
handlePlayerCommandInternal(packetClass().cast(packet));
53+
return true;
54+
}
55+
56+
return false;
57+
}
58+
}

proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@
2121
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
2222
import com.velocitypowered.proxy.VelocityServer;
2323
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
24-
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
24+
import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
2525
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2;
2626
import java.util.concurrent.CompletableFuture;
2727
import net.kyori.adventure.text.Component;
2828

29-
public class KeyedCommandHandler implements CommandHandler<KeyedPlayerCommandPacket> {
29+
public class KeyedCommandHandler extends RateLimitedCommandHandler<KeyedPlayerCommandPacket> {
3030

3131
private final ConnectedPlayer player;
3232
private final VelocityServer server;
3333

3434
public KeyedCommandHandler(ConnectedPlayer player, VelocityServer server) {
35+
super(player, server);
3536
this.player = player;
3637
this.server = server;
3738
}

proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@
2020
import com.velocitypowered.api.event.command.CommandExecuteEvent;
2121
import com.velocitypowered.proxy.VelocityServer;
2222
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
23-
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
23+
import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
24+
2425
import java.time.Instant;
2526
import java.util.concurrent.CompletableFuture;
2627

27-
public class LegacyCommandHandler implements CommandHandler<LegacyChatPacket> {
28+
public class LegacyCommandHandler extends RateLimitedCommandHandler<LegacyChatPacket> {
2829

2930
private final ConnectedPlayer player;
3031
private final VelocityServer server;
3132

3233
public LegacyCommandHandler(ConnectedPlayer player, VelocityServer server) {
34+
super(player, server);
3335
this.player = player;
3436
this.server = server;
3537
}

proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,19 @@
2222
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
2323
import com.velocitypowered.proxy.protocol.MinecraftPacket;
2424
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
25-
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
2625
import java.util.concurrent.CompletableFuture;
26+
27+
import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
2728
import net.kyori.adventure.text.Component;
2829
import org.checkerframework.checker.nullness.qual.Nullable;
2930

30-
public class SessionCommandHandler implements CommandHandler<SessionPlayerCommandPacket> {
31+
public class SessionCommandHandler extends RateLimitedCommandHandler<SessionPlayerCommandPacket> {
3132

3233
private final ConnectedPlayer player;
3334
private final VelocityServer server;
3435

3536
public SessionCommandHandler(ConnectedPlayer player, VelocityServer server) {
37+
super(player, server);
3638
this.player = player;
3739
this.server = server;
3840
}

proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/CaffeineCacheRatelimiter.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@
2222
import com.github.benmanes.caffeine.cache.Ticker;
2323
import com.google.common.annotations.VisibleForTesting;
2424
import com.google.common.base.Preconditions;
25-
import java.net.InetAddress;
2625
import java.util.concurrent.TimeUnit;
26+
import org.jetbrains.annotations.NotNull;
2727

2828
/**
2929
* A simple rate-limiter based on a Caffeine {@link Cache}.
3030
*/
31-
public class CaffeineCacheRatelimiter implements Ratelimiter {
31+
public class CaffeineCacheRatelimiter<T> implements Ratelimiter<T> {
3232

33-
private final Cache<InetAddress, Long> expiringCache;
33+
private final Cache<T, Long> expiringCache;
3434
private final long timeoutNanos;
3535

3636
CaffeineCacheRatelimiter(long time, TimeUnit unit) {
@@ -49,16 +49,15 @@ public class CaffeineCacheRatelimiter implements Ratelimiter {
4949
}
5050

5151
/**
52-
* Attempts to rate-limit the client.
52+
* Attempts to rate-limit the object.
5353
*
54-
* @param address the address to rate limit
55-
* @return true if we should allow the client, false if we should rate-limit
54+
* @param key the object to rate limit
55+
* @return true if we should allow the object, false if we should rate-limit
5656
*/
5757
@Override
58-
public boolean attempt(InetAddress address) {
59-
Preconditions.checkNotNull(address, "address");
58+
public boolean attempt(@NotNull T key) {
6059
long expectedNewValue = System.nanoTime() + timeoutNanos;
61-
long last = expiringCache.get(address, (address1) -> expectedNewValue);
60+
long last = expiringCache.get(key, (key1) -> expectedNewValue);
6261
return expectedNewValue == last;
6362
}
6463
}

0 commit comments

Comments
 (0)