feat: implement command rate limiter (#1524)
This commit is contained in:
@@ -148,4 +148,59 @@ public interface ProxyConfig {
|
|||||||
* @return read timeout (in milliseconds)
|
* @return read timeout (in milliseconds)
|
||||||
*/
|
*/
|
||||||
int getReadTimeout();
|
int getReadTimeout();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rate limit for how fast a player can execute commands.
|
||||||
|
*
|
||||||
|
* @return the command rate limit (in milliseconds)
|
||||||
|
*/
|
||||||
|
int getCommandRatelimit();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether we should forward commands to the backend if the player is rate limited.
|
||||||
|
*
|
||||||
|
* @return whether to forward commands if rate limited
|
||||||
|
*/
|
||||||
|
boolean isForwardCommandsIfRateLimited();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the kick limit for commands that are rate limited.
|
||||||
|
* If this limit is 0 or less, the player will be not be kicked.
|
||||||
|
*
|
||||||
|
* @return the rate limited command rate limit
|
||||||
|
*/
|
||||||
|
int getKickAfterRateLimitedCommands();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether the proxy should kick players who are command rate limited.
|
||||||
|
*
|
||||||
|
* @return whether to kick players who are rate limited
|
||||||
|
*/
|
||||||
|
default boolean isKickOnCommandRateLimit() {
|
||||||
|
return getKickAfterRateLimitedCommands() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rate limit for how fast a player can tab complete.
|
||||||
|
*
|
||||||
|
* @return the tab complete rate limit (in milliseconds)
|
||||||
|
*/
|
||||||
|
int getTabCompleteRatelimit();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the kick limit for tab completes that are rate limited.
|
||||||
|
* If this limit is 0 or less, the player will be not be kicked.
|
||||||
|
*
|
||||||
|
* @return the rate limited command rate limit
|
||||||
|
*/
|
||||||
|
int getKickAfterRateLimitedTabCompletes();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether the proxy should kick players who are tab complete rate limited.
|
||||||
|
*
|
||||||
|
* @return whether to kick players who are rate limited
|
||||||
|
*/
|
||||||
|
default boolean isKickOnTabCompleteRateLimit() {
|
||||||
|
return getKickAfterRateLimitedTabCompletes() > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -75,6 +75,7 @@ import io.netty.channel.ChannelInitializer;
|
|||||||
import io.netty.channel.EventLoopGroup;
|
import io.netty.channel.EventLoopGroup;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -162,7 +163,9 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
||||||
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
||||||
private final VelocityConsole console;
|
private final VelocityConsole console;
|
||||||
private @MonotonicNonNull Ratelimiter ipAttemptLimiter;
|
private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter;
|
||||||
|
private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter;
|
||||||
|
private @MonotonicNonNull Ratelimiter<UUID> tabCompleteRateLimiter;
|
||||||
private final VelocityEventManager eventManager;
|
private final VelocityEventManager eventManager;
|
||||||
private final VelocityScheduler scheduler;
|
private final VelocityScheduler scheduler;
|
||||||
private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar();
|
private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar();
|
||||||
@@ -295,6 +298,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
|
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
|
||||||
|
commandRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getCommandRatelimit());
|
||||||
|
tabCompleteRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getTabCompleteRatelimit());
|
||||||
loadPlugins();
|
loadPlugins();
|
||||||
|
|
||||||
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
|
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
|
||||||
@@ -654,10 +659,18 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
return cm.createHttpClient();
|
return cm.createHttpClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Ratelimiter getIpAttemptLimiter() {
|
public @MonotonicNonNull Ratelimiter<InetAddress> getIpAttemptLimiter() {
|
||||||
return ipAttemptLimiter;
|
return ipAttemptLimiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @MonotonicNonNull Ratelimiter<UUID> getCommandRateLimiter() {
|
||||||
|
return commandRateLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @MonotonicNonNull Ratelimiter<UUID> getTabCompleteRateLimiter() {
|
||||||
|
return tabCompleteRateLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the {@code connection} can be registered with the proxy.
|
* Checks if the {@code connection} can be registered with the proxy.
|
||||||
*
|
*
|
||||||
|
@@ -234,6 +234,11 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (advanced.commandRateLimit < 0) {
|
||||||
|
logger.error("Invalid command rate limit {}", advanced.commandRateLimit);
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
loadFavicon();
|
loadFavicon();
|
||||||
|
|
||||||
return valid;
|
return valid;
|
||||||
@@ -355,6 +360,31 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
return advanced.getReadTimeout();
|
return advanced.getReadTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCommandRatelimit() {
|
||||||
|
return advanced.getCommandRateLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTabCompleteRatelimit() {
|
||||||
|
return advanced.getTabCompleteRateLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getKickAfterRateLimitedTabCompletes() {
|
||||||
|
return advanced.getKickAfterRateLimitedTabCompletes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isForwardCommandsIfRateLimited() {
|
||||||
|
return advanced.isForwardCommandsIfRateLimited();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getKickAfterRateLimitedCommands() {
|
||||||
|
return advanced.getKickAfterRateLimitedCommands();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isProxyProtocol() {
|
public boolean isProxyProtocol() {
|
||||||
return advanced.isProxyProtocol();
|
return advanced.isProxyProtocol();
|
||||||
}
|
}
|
||||||
@@ -733,6 +763,16 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
private boolean acceptTransfers = false;
|
private boolean acceptTransfers = false;
|
||||||
@Expose
|
@Expose
|
||||||
private boolean enableReusePort = false;
|
private boolean enableReusePort = false;
|
||||||
|
@Expose
|
||||||
|
private int commandRateLimit = 50;
|
||||||
|
@Expose
|
||||||
|
private boolean forwardCommandsIfRateLimited = true;
|
||||||
|
@Expose
|
||||||
|
private int kickAfterRateLimitedCommands = 5;
|
||||||
|
@Expose
|
||||||
|
private int tabCompleteRateLimit = 50;
|
||||||
|
@Expose
|
||||||
|
private int kickAfterRateLimitedTabCompletes = 10;
|
||||||
|
|
||||||
private Advanced() {
|
private Advanced() {
|
||||||
}
|
}
|
||||||
@@ -759,6 +799,11 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
this.logPlayerConnections = config.getOrElse("log-player-connections", true);
|
this.logPlayerConnections = config.getOrElse("log-player-connections", true);
|
||||||
this.acceptTransfers = config.getOrElse("accepts-transfers", false);
|
this.acceptTransfers = config.getOrElse("accepts-transfers", false);
|
||||||
this.enableReusePort = config.getOrElse("enable-reuse-port", false);
|
this.enableReusePort = config.getOrElse("enable-reuse-port", false);
|
||||||
|
this.commandRateLimit = config.getIntOrElse("command-rate-limit", 25);
|
||||||
|
this.forwardCommandsIfRateLimited = config.getOrElse("forward-commands-if-rate-limited", true);
|
||||||
|
this.kickAfterRateLimitedCommands = config.getIntOrElse("kick-after-rate-limited-commands", 0);
|
||||||
|
this.tabCompleteRateLimit = config.getIntOrElse("tab-complete-rate-limit", 10); // very lenient
|
||||||
|
this.kickAfterRateLimitedTabCompletes = config.getIntOrElse("kick-after-rate-limited-tab-completes", 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,6 +871,26 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
return enableReusePort;
|
return enableReusePort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCommandRateLimit() {
|
||||||
|
return commandRateLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isForwardCommandsIfRateLimited() {
|
||||||
|
return forwardCommandsIfRateLimited;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getKickAfterRateLimitedCommands() {
|
||||||
|
return kickAfterRateLimitedCommands;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTabCompleteRateLimit() {
|
||||||
|
return tabCompleteRateLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getKickAfterRateLimitedTabCompletes() {
|
||||||
|
return kickAfterRateLimitedTabCompletes;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Advanced{"
|
return "Advanced{"
|
||||||
|
@@ -113,6 +113,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
private CompletableFuture<Void> configSwitchFuture;
|
private CompletableFuture<Void> configSwitchFuture;
|
||||||
|
|
||||||
|
private int failedTabCompleteAttempts;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a client play session handler.
|
* Constructs a client play session handler.
|
||||||
*
|
*
|
||||||
@@ -671,6 +673,17 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!server.getTabCompleteRateLimiter().attempt(player.getUniqueId())) {
|
||||||
|
if (server.getConfiguration().isKickOnTabCompleteRateLimit()
|
||||||
|
&& failedTabCompleteAttempts++ >= server.getConfiguration().getKickAfterRateLimitedTabCompletes()) {
|
||||||
|
player.disconnect(Component.translatable("velocity.kick.tab-complete-rate-limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
failedTabCompleteAttempts = 0;
|
||||||
|
|
||||||
server.getCommandManager().offerBrigadierSuggestions(player, command)
|
server.getCommandManager().offerBrigadierSuggestions(player, command)
|
||||||
.thenAcceptAsync(suggestions -> {
|
.thenAcceptAsync(suggestions -> {
|
||||||
if (suggestions.isEmpty()) {
|
if (suggestions.isEmpty()) {
|
||||||
|
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.protocol.packet.chat;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
|
||||||
|
public abstract class RateLimitedCommandHandler<T extends MinecraftPacket> implements CommandHandler<T> {
|
||||||
|
|
||||||
|
private final Player player;
|
||||||
|
private final VelocityServer velocityServer;
|
||||||
|
|
||||||
|
private int failedAttempts;
|
||||||
|
|
||||||
|
protected RateLimitedCommandHandler(Player player, VelocityServer velocityServer) {
|
||||||
|
this.player = player;
|
||||||
|
this.velocityServer = velocityServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handlePlayerCommand(MinecraftPacket packet) {
|
||||||
|
if (packetClass().isInstance(packet)) {
|
||||||
|
if (!velocityServer.getCommandRateLimiter().attempt(player.getUniqueId())) {
|
||||||
|
if (failedAttempts++ >= velocityServer.getConfiguration().getKickAfterRateLimitedCommands()) {
|
||||||
|
player.disconnect(Component.translatable("velocity.kick.command-rate-limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (velocityServer.getConfiguration().isForwardCommandsIfRateLimited()) {
|
||||||
|
return false; // Send the packet to the server
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failedAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePlayerCommandInternal(packetClass().cast(packet));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@@ -21,17 +21,18 @@ import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
|||||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
|
import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2;
|
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
|
|
||||||
public class KeyedCommandHandler implements CommandHandler<KeyedPlayerCommandPacket> {
|
public class KeyedCommandHandler extends RateLimitedCommandHandler<KeyedPlayerCommandPacket> {
|
||||||
|
|
||||||
private final ConnectedPlayer player;
|
private final ConnectedPlayer player;
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
|
|
||||||
public KeyedCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
public KeyedCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
||||||
|
super(player, server);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
}
|
}
|
||||||
|
@@ -20,16 +20,18 @@ package com.velocitypowered.proxy.protocol.packet.chat.legacy;
|
|||||||
import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
|
import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
public class LegacyCommandHandler implements CommandHandler<LegacyChatPacket> {
|
public class LegacyCommandHandler extends RateLimitedCommandHandler<LegacyChatPacket> {
|
||||||
|
|
||||||
private final ConnectedPlayer player;
|
private final ConnectedPlayer player;
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
|
|
||||||
public LegacyCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
public LegacyCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
||||||
|
super(player, server);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
}
|
}
|
||||||
|
@@ -22,17 +22,19 @@ import com.velocitypowered.proxy.VelocityServer;
|
|||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
|
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
public class SessionCommandHandler implements CommandHandler<SessionPlayerCommandPacket> {
|
public class SessionCommandHandler extends RateLimitedCommandHandler<SessionPlayerCommandPacket> {
|
||||||
|
|
||||||
private final ConnectedPlayer player;
|
private final ConnectedPlayer player;
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
|
|
||||||
public SessionCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
public SessionCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
||||||
|
super(player, server);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
}
|
}
|
||||||
|
@@ -22,15 +22,15 @@ import com.github.benmanes.caffeine.cache.Caffeine;
|
|||||||
import com.github.benmanes.caffeine.cache.Ticker;
|
import com.github.benmanes.caffeine.cache.Ticker;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple rate-limiter based on a Caffeine {@link Cache}.
|
* A simple rate-limiter based on a Caffeine {@link Cache}.
|
||||||
*/
|
*/
|
||||||
public class CaffeineCacheRatelimiter implements Ratelimiter {
|
public class CaffeineCacheRatelimiter<T> implements Ratelimiter<T> {
|
||||||
|
|
||||||
private final Cache<InetAddress, Long> expiringCache;
|
private final Cache<T, Long> expiringCache;
|
||||||
private final long timeoutNanos;
|
private final long timeoutNanos;
|
||||||
|
|
||||||
CaffeineCacheRatelimiter(long time, TimeUnit unit) {
|
CaffeineCacheRatelimiter(long time, TimeUnit unit) {
|
||||||
@@ -49,16 +49,15 @@ public class CaffeineCacheRatelimiter implements Ratelimiter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to rate-limit the client.
|
* Attempts to rate-limit the object.
|
||||||
*
|
*
|
||||||
* @param address the address to rate limit
|
* @param key the object to rate limit
|
||||||
* @return true if we should allow the client, false if we should rate-limit
|
* @return true if we should allow the object, false if we should rate-limit
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean attempt(InetAddress address) {
|
public boolean attempt(@NotNull T key) {
|
||||||
Preconditions.checkNotNull(address, "address");
|
|
||||||
long expectedNewValue = System.nanoTime() + timeoutNanos;
|
long expectedNewValue = System.nanoTime() + timeoutNanos;
|
||||||
long last = expiringCache.get(address, (address1) -> expectedNewValue);
|
long last = expiringCache.get(key, (key1) -> expectedNewValue);
|
||||||
return expectedNewValue == last;
|
return expectedNewValue == last;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,16 +17,16 @@
|
|||||||
|
|
||||||
package com.velocitypowered.proxy.util.ratelimit;
|
package com.velocitypowered.proxy.util.ratelimit;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link Ratelimiter} that does no rate-limiting.
|
* A {@link Ratelimiter} that does no rate-limiting.
|
||||||
*/
|
*/
|
||||||
enum NoopCacheRatelimiter implements Ratelimiter {
|
enum NoopCacheRatelimiter implements Ratelimiter<Object> {
|
||||||
INSTANCE;
|
INSTANCE;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean attempt(InetAddress address) {
|
public boolean attempt(@NotNull Object key) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,18 +17,18 @@
|
|||||||
|
|
||||||
package com.velocitypowered.proxy.util.ratelimit;
|
package com.velocitypowered.proxy.util.ratelimit;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows rate limiting of clients.
|
* Allows rate limiting of objects.
|
||||||
*/
|
*/
|
||||||
public interface Ratelimiter {
|
public interface Ratelimiter<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether or not to allow the connection.
|
* Attempts to rate-limit the object.
|
||||||
*
|
*
|
||||||
* @param address the address to rate limit
|
* @param key the object to rate limit
|
||||||
* @return true if allowed, false if not
|
* @return true if we should allow the object, false if we should rate-limit
|
||||||
*/
|
*/
|
||||||
boolean attempt(InetAddress address);
|
boolean attempt(@NotNull T key);
|
||||||
}
|
}
|
||||||
|
@@ -28,8 +28,9 @@ public final class Ratelimiters {
|
|||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Ratelimiter createWithMilliseconds(long ms) {
|
@SuppressWarnings("unchecked")
|
||||||
return ms <= 0 ? NoopCacheRatelimiter.INSTANCE : new CaffeineCacheRatelimiter(ms,
|
public static <T> Ratelimiter<T> createWithMilliseconds(long ms) {
|
||||||
|
return ms <= 0 ? (Ratelimiter<T>) NoopCacheRatelimiter.INSTANCE : new CaffeineCacheRatelimiter(ms,
|
||||||
TimeUnit.MILLISECONDS);
|
TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -62,4 +62,6 @@ velocity.command.dump-server-error=An error occurred on the Velocity servers and
|
|||||||
velocity.command.dump-offline=Likely cause: Invalid system DNS settings or no internet connection
|
velocity.command.dump-offline=Likely cause: Invalid system DNS settings or no internet connection
|
||||||
velocity.command.send-usage=/send <player> <server>
|
velocity.command.send-usage=/send <player> <server>
|
||||||
# Kick
|
# Kick
|
||||||
velocity.kick.shutdown=Proxy shutting down.
|
velocity.kick.shutdown=Proxy shutting down.
|
||||||
|
velocity.kick.command-rate-limit=You are sending too many commands too quickly.
|
||||||
|
velocity.kick.tab-complete-rate-limit=You are sending too many tab complete requests too quickly.
|
@@ -156,6 +156,27 @@ accepts-transfers = false
|
|||||||
# threads. Disabled by default. Requires Linux or macOS.
|
# threads. Disabled by default. Requires Linux or macOS.
|
||||||
enable-reuse-port = false
|
enable-reuse-port = false
|
||||||
|
|
||||||
|
# How fast (in milliseconds) are clients allowed to send commands after the last command
|
||||||
|
# By default this is 50ms (20 commands per second)
|
||||||
|
command-rate-limit = 25
|
||||||
|
|
||||||
|
# Should we forward commands to the backend upon being rate limited?
|
||||||
|
# This will forward the command to the server instead of processing it on the proxy.
|
||||||
|
# Since most server implementations have a rate limit, this will prevent the player
|
||||||
|
# from being able to send excessive commands to the server.
|
||||||
|
forward-commands-if-rate-limited = true
|
||||||
|
|
||||||
|
# How many commands are allowed to be sent after the rate limit is hit before the player is kicked?
|
||||||
|
# Setting this to 0 or lower will disable this feature.
|
||||||
|
kick-after-rate-limited-commands = 0
|
||||||
|
|
||||||
|
# How fast (in milliseconds) are clients allowed to send tab completions after the last tab completion
|
||||||
|
tab-complete-rate-limit = 10
|
||||||
|
|
||||||
|
# How many tab completions are allowed to be sent after the rate limit is hit before the player is kicked?
|
||||||
|
# Setting this to 0 or lower will disable this feature.
|
||||||
|
kick-after-rate-limited-tab-completes = 0
|
||||||
|
|
||||||
[query]
|
[query]
|
||||||
# Whether to enable responding to GameSpy 4 query responses or not.
|
# Whether to enable responding to GameSpy 4 query responses or not.
|
||||||
enabled = false
|
enabled = false
|
||||||
|
Reference in New Issue
Block a user