From b3e218bd7d27b5f9e6c7b5fdda4792ea0d7b978c Mon Sep 17 00:00:00 2001 From: BBaoVanC Date: Tue, 1 Apr 2025 10:48:00 -0500 Subject: [PATCH 01/18] Show proxy-wide online players in server ping (#811) * Show proxy-wide online players in server ping * Reflow arguments in VelocityConfiguration constructor --- .../api/proxy/server/ServerPing.java | 4 ++++ .../proxy/config/VelocityConfiguration.java | 15 ++++++++++++-- .../util/ServerListPingHandler.java | 20 ++++++++++++++++++- .../src/main/resources/default-velocity.toml | 5 +++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java index 9a27dc05..4d906b7b 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java @@ -516,6 +516,10 @@ public final class ServerPing { */ public static final class SamplePlayer { + public static final SamplePlayer ANONYMOUS = new SamplePlayer( + "Anonymous Player", + new UUID(0L, 0L) + ); private final String name; private final UUID id; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index bbd49f55..309004aa 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -78,6 +78,8 @@ public class VelocityConfiguration implements ProxyConfig { private boolean onlineModeKickExistingPlayers = false; @Expose private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED; + @Expose + private boolean samplePlayersInPing = false; private final Servers servers; private final ForcedHosts forcedHosts; @Expose @@ -105,8 +107,9 @@ public class VelocityConfiguration implements ProxyConfig { boolean preventClientProxyConnections, boolean announceForge, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret, boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough, - boolean enablePlayerAddressLogging, Servers servers, ForcedHosts forcedHosts, - Advanced advanced, Query query, Metrics metrics, boolean forceKeyAuthentication) { + boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers, + ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics, + boolean forceKeyAuthentication) { this.bind = bind; this.motd = motd; this.showMaxPlayers = showMaxPlayers; @@ -117,6 +120,7 @@ public class VelocityConfiguration implements ProxyConfig { this.forwardingSecret = forwardingSecret; this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers; this.pingPassthrough = pingPassthrough; + this.samplePlayersInPing = samplePlayersInPing; this.enablePlayerAddressLogging = enablePlayerAddressLogging; this.servers = servers; this.forcedHosts = forcedHosts; @@ -371,6 +375,10 @@ public class VelocityConfiguration implements ProxyConfig { return pingPassthrough; } + public boolean getSamplePlayersInPing() { + return samplePlayersInPing; + } + public boolean isPlayerAddressLoggingEnabled() { return enablePlayerAddressLogging; } @@ -507,6 +515,8 @@ public class VelocityConfiguration implements ProxyConfig { final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough", PingPassthroughMode.DISABLED); + final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false); + final String bind = config.getOrElse("bind", "0.0.0.0:25565"); final int maxPlayers = config.getIntOrElse("show-max-players", 500); final boolean onlineMode = config.getOrElse("online-mode", true); @@ -537,6 +547,7 @@ public class VelocityConfiguration implements ProxyConfig { forwardingSecret, kickExisting, pingPassthroughMode, + samplePlayersInPing, enablePlayerAddressLogging, new Servers(serversConfig), new ForcedHosts(forcedHostsConfig), diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java index 97947eb6..d1f2b0db 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java @@ -30,10 +30,12 @@ import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.server.VelocityRegisteredServer; import java.net.InetSocketAddress; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; /** * Common utilities for handling server list ping results. @@ -51,11 +53,27 @@ public class ServerListPingHandler { version = ProtocolVersion.MAXIMUM_VERSION; } VelocityConfiguration configuration = server.getConfiguration(); + List samplePlayers; + if (configuration.getSamplePlayersInPing()) { + List unshuffledPlayers = server.getAllPlayers().stream() + .map(p -> { + if (p.getPlayerSettings().isClientListingAllowed()) { + return new ServerPing.SamplePlayer(p.getUsername(), p.getUniqueId()); + } else { + return ServerPing.SamplePlayer.ANONYMOUS; + } + }) + .collect(Collectors.toList()); + Collections.shuffle(unshuffledPlayers); + samplePlayers = unshuffledPlayers.subList(0, Math.min(12, server.getPlayerCount())); + } else { + samplePlayers = ImmutableList.of(); + } return new ServerPing( new ServerPing.Version(version.getProtocol(), "Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING), new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), - ImmutableList.of()), + samplePlayers), configuration.getMotd(), configuration.getFavicon().orElse(null), configuration.isAnnounceForge() ? ModInfo.DEFAULT : null diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml index 5b0a1e27..2bd48723 100644 --- a/proxy/src/main/resources/default-velocity.toml +++ b/proxy/src/main/resources/default-velocity.toml @@ -66,6 +66,11 @@ kick-existing-players = false # configuration is used if no servers could be contacted. ping-passthrough = "DISABLED" +# If enabled (default is false), then a sample of the online players on the proxy will be visible +# when hovering over the player count in the server list. +# This doesn't have any effect when ping passthrough is set to either "description" or "all". +sample-players-in-ping = false + # If not enabled (default is true) player IP addresses will be replaced by in logs enable-player-address-logging = true -- 2.39.5 From 7ffa43f0e2792ee91edfdd845ce5349829b00973 Mon Sep 17 00:00:00 2001 From: Bridge <29434554+bridgelol@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:26:00 +0200 Subject: [PATCH 02/18] feat: implement command rate limiter (#1524) --- .../api/proxy/config/ProxyConfig.java | 55 ++++++++++++++++ .../velocitypowered/proxy/VelocityServer.java | 17 ++++- .../proxy/config/VelocityConfiguration.java | 65 +++++++++++++++++++ .../client/ClientPlaySessionHandler.java | 13 ++++ .../chat/RateLimitedCommandHandler.java | 58 +++++++++++++++++ .../chat/keyed/KeyedCommandHandler.java | 5 +- .../chat/legacy/LegacyCommandHandler.java | 6 +- .../chat/session/SessionCommandHandler.java | 6 +- .../ratelimit/CaffeineCacheRatelimiter.java | 17 +++-- .../util/ratelimit/NoopCacheRatelimiter.java | 6 +- .../proxy/util/ratelimit/Ratelimiter.java | 18 ++--- .../proxy/util/ratelimit/Ratelimiters.java | 5 +- .../proxy/l10n/messages.properties | 4 +- .../src/main/resources/default-velocity.toml | 21 ++++++ 14 files changed, 264 insertions(+), 32 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RateLimitedCommandHandler.java diff --git a/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java b/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java index 831e55af..12d3dbd1 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java @@ -148,4 +148,59 @@ public interface ProxyConfig { * @return read timeout (in milliseconds) */ 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; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index abf2cf00..e616fb4d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -75,6 +75,7 @@ import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import java.io.IOException; import java.io.InputStream; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.http.HttpClient; import java.nio.file.Files; @@ -162,7 +163,9 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { private final Map connectionsByUuid = new ConcurrentHashMap<>(); private final Map connectionsByName = new ConcurrentHashMap<>(); private final VelocityConsole console; - private @MonotonicNonNull Ratelimiter ipAttemptLimiter; + private @MonotonicNonNull Ratelimiter ipAttemptLimiter; + private @MonotonicNonNull Ratelimiter commandRateLimiter; + private @MonotonicNonNull Ratelimiter tabCompleteRateLimiter; private final VelocityEventManager eventManager; private final VelocityScheduler scheduler; private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar(); @@ -295,6 +298,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { } ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit()); + commandRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getCommandRatelimit()); + tabCompleteRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getTabCompleteRatelimit()); loadPlugins(); // 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(); } - public Ratelimiter getIpAttemptLimiter() { + public @MonotonicNonNull Ratelimiter getIpAttemptLimiter() { return ipAttemptLimiter; } + public @MonotonicNonNull Ratelimiter getCommandRateLimiter() { + return commandRateLimiter; + } + + public @MonotonicNonNull Ratelimiter getTabCompleteRateLimiter() { + return tabCompleteRateLimiter; + } + /** * Checks if the {@code connection} can be registered with the proxy. * diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 309004aa..dfd007ac 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -234,6 +234,11 @@ public class VelocityConfiguration implements ProxyConfig { valid = false; } + if (advanced.commandRateLimit < 0) { + logger.error("Invalid command rate limit {}", advanced.commandRateLimit); + valid = false; + } + loadFavicon(); return valid; @@ -355,6 +360,31 @@ public class VelocityConfiguration implements ProxyConfig { 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() { return advanced.isProxyProtocol(); } @@ -733,6 +763,16 @@ public class VelocityConfiguration implements ProxyConfig { private boolean acceptTransfers = false; @Expose 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() { } @@ -759,6 +799,11 @@ public class VelocityConfiguration implements ProxyConfig { this.logPlayerConnections = config.getOrElse("log-player-connections", true); this.acceptTransfers = config.getOrElse("accepts-transfers", 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; } + 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 public String toString() { return "Advanced{" diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 8678431c..6dccb36b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -113,6 +113,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private CompletableFuture configSwitchFuture; + private int failedTabCompleteAttempts; + /** * Constructs a client play session handler. * @@ -671,6 +673,17 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { 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) .thenAcceptAsync(suggestions -> { if (suggestions.isEmpty()) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RateLimitedCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RateLimitedCommandHandler.java new file mode 100644 index 00000000..e29dcc32 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RateLimitedCommandHandler.java @@ -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 . + */ + +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 implements CommandHandler { + + 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; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java index 5baedfb4..b10332f0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java @@ -21,17 +21,18 @@ import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.proxy.VelocityServer; 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 java.util.concurrent.CompletableFuture; import net.kyori.adventure.text.Component; -public class KeyedCommandHandler implements CommandHandler { +public class KeyedCommandHandler extends RateLimitedCommandHandler { private final ConnectedPlayer player; private final VelocityServer server; public KeyedCommandHandler(ConnectedPlayer player, VelocityServer server) { + super(player, server); this.player = player; this.server = server; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java index 30ad2c99..4f88ee34 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java @@ -20,16 +20,18 @@ package com.velocitypowered.proxy.protocol.packet.chat.legacy; import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.proxy.VelocityServer; 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.util.concurrent.CompletableFuture; -public class LegacyCommandHandler implements CommandHandler { +public class LegacyCommandHandler extends RateLimitedCommandHandler { private final ConnectedPlayer player; private final VelocityServer server; public LegacyCommandHandler(ConnectedPlayer player, VelocityServer server) { + super(player, server); this.player = player; this.server = server; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index 8d1dc0f2..6978f8ee 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -22,17 +22,19 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket; -import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; import java.util.concurrent.CompletableFuture; + +import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; -public class SessionCommandHandler implements CommandHandler { +public class SessionCommandHandler extends RateLimitedCommandHandler { private final ConnectedPlayer player; private final VelocityServer server; public SessionCommandHandler(ConnectedPlayer player, VelocityServer server) { + super(player, server); this.player = player; this.server = server; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/CaffeineCacheRatelimiter.java b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/CaffeineCacheRatelimiter.java index b42b3ca8..fa745922 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/CaffeineCacheRatelimiter.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/CaffeineCacheRatelimiter.java @@ -22,15 +22,15 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Ticker; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import java.net.InetAddress; import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.NotNull; /** * A simple rate-limiter based on a Caffeine {@link Cache}. */ -public class CaffeineCacheRatelimiter implements Ratelimiter { +public class CaffeineCacheRatelimiter implements Ratelimiter { - private final Cache expiringCache; + private final Cache expiringCache; private final long timeoutNanos; 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 - * @return true if we should allow the client, false if we should rate-limit + * @param key the object to rate limit + * @return true if we should allow the object, false if we should rate-limit */ @Override - public boolean attempt(InetAddress address) { - Preconditions.checkNotNull(address, "address"); + public boolean attempt(@NotNull T key) { long expectedNewValue = System.nanoTime() + timeoutNanos; - long last = expiringCache.get(address, (address1) -> expectedNewValue); + long last = expiringCache.get(key, (key1) -> expectedNewValue); return expectedNewValue == last; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/NoopCacheRatelimiter.java b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/NoopCacheRatelimiter.java index f420986d..9f77072a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/NoopCacheRatelimiter.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/NoopCacheRatelimiter.java @@ -17,16 +17,16 @@ package com.velocitypowered.proxy.util.ratelimit; -import java.net.InetAddress; +import org.jetbrains.annotations.NotNull; /** * A {@link Ratelimiter} that does no rate-limiting. */ -enum NoopCacheRatelimiter implements Ratelimiter { +enum NoopCacheRatelimiter implements Ratelimiter { INSTANCE; @Override - public boolean attempt(InetAddress address) { + public boolean attempt(@NotNull Object key) { return true; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiter.java b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiter.java index 973276d4..840b21a1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiter.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiter.java @@ -17,18 +17,18 @@ 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 { /** - * Determines whether or not to allow the connection. - * - * @param address the address to rate limit - * @return true if allowed, false if not - */ - boolean attempt(InetAddress address); + * Attempts to rate-limit the object. + * + * @param key the object to rate limit + * @return true if we should allow the object, false if we should rate-limit + */ + boolean attempt(@NotNull T key); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiters.java b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiters.java index 4dafd30c..f3063449 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiters.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/ratelimit/Ratelimiters.java @@ -28,8 +28,9 @@ public final class Ratelimiters { throw new AssertionError(); } - public static Ratelimiter createWithMilliseconds(long ms) { - return ms <= 0 ? NoopCacheRatelimiter.INSTANCE : new CaffeineCacheRatelimiter(ms, + @SuppressWarnings("unchecked") + public static Ratelimiter createWithMilliseconds(long ms) { + return ms <= 0 ? (Ratelimiter) NoopCacheRatelimiter.INSTANCE : new CaffeineCacheRatelimiter(ms, TimeUnit.MILLISECONDS); } } diff --git a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties index 07aa348f..d56ca9c6 100644 --- a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties +++ b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties @@ -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.send-usage=/send # Kick -velocity.kick.shutdown=Proxy shutting down. \ No newline at end of file +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. \ No newline at end of file diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml index 2bd48723..86a1d1f1 100644 --- a/proxy/src/main/resources/default-velocity.toml +++ b/proxy/src/main/resources/default-velocity.toml @@ -156,6 +156,27 @@ accepts-transfers = false # threads. Disabled by default. Requires Linux or macOS. 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] # Whether to enable responding to GameSpy 4 query responses or not. enabled = false -- 2.39.5 From 86b88cf4b71fa2cab0f5c5d32aa3028d176673a4 Mon Sep 17 00:00:00 2001 From: SpigotRCE <128710385+SpigotRCE@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:34:20 +0530 Subject: [PATCH 03/18] fix: typo --- proxy/src/main/resources/default-velocity.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml index 86a1d1f1..4d71e589 100644 --- a/proxy/src/main/resources/default-velocity.toml +++ b/proxy/src/main/resources/default-velocity.toml @@ -158,7 +158,7 @@ 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 +command-rate-limit = 50 # 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. -- 2.39.5 From c72a3eefdeee26d39d5382c30435f9ce1819153e Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Thu, 3 Apr 2025 22:51:26 +0100 Subject: [PATCH 04/18] Check if kicking on command rate limit is enabled --- .../proxy/protocol/packet/chat/RateLimitedCommandHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RateLimitedCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RateLimitedCommandHandler.java index e29dcc32..e5823643 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RateLimitedCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/RateLimitedCommandHandler.java @@ -38,7 +38,7 @@ public abstract class RateLimitedCommandHandler imple public boolean handlePlayerCommand(MinecraftPacket packet) { if (packetClass().isInstance(packet)) { if (!velocityServer.getCommandRateLimiter().attempt(player.getUniqueId())) { - if (failedAttempts++ >= velocityServer.getConfiguration().getKickAfterRateLimitedCommands()) { + if (velocityServer.getConfiguration().isKickOnCommandRateLimit() && failedAttempts++ >= velocityServer.getConfiguration().getKickAfterRateLimitedCommands()) { player.disconnect(Component.translatable("velocity.kick.command-rate-limit")); } -- 2.39.5 From aae97dce3dd5607f0ca8d333596f6345b9236268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E8=8E=B9=C2=B7=E7=BA=A4=E7=BB=AB?= Date: Mon, 7 Apr 2025 02:40:17 +0800 Subject: [PATCH 05/18] Track plugin message channels that registered by clients (#1276) --- .../client/ClientPlaySessionHandler.java | 6 ++ .../connection/client/ConnectedPlayer.java | 13 ++++ .../proxy/util/collect/CappedSet.java | 70 ++++++++++++++++++ .../proxy/util/collect/CappedSetTest.java | 71 +++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java create mode 100644 proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 6dccb36b..a7e490ed 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -311,6 +311,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { + "ready. Channel: {}. Packet discarded.", packet.getChannel()); } else if (PluginMessageUtil.isRegister(packet)) { List channels = PluginMessageUtil.getChannels(packet); + player.getClientsideChannels().addAll(channels); List channelIdentifiers = new ArrayList<>(); for (String channel : channels) { try { @@ -324,6 +325,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channelIdentifiers))); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isUnregister(packet)) { + player.getClientsideChannels().removeAll(PluginMessageUtil.getChannels(packet)); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isMcBrand(packet)) { String brand = PluginMessageUtil.readBrandMessage(packet.content()); @@ -592,6 +594,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { if (!channels.isEmpty()) { serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels)); } + // Tell the server about this client's plugin message channels. + if (!player.getClientsideChannels().isEmpty()) { + serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getClientsideChannels())); + } // If we had plugin messages queued during login/FML handshake, send them now. PluginMessagePacket pm; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 7324544d..0f60b4dd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -99,6 +99,7 @@ import com.velocitypowered.proxy.tablist.VelocityTabListLegacy; import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.DurationUtils; import com.velocitypowered.proxy.util.TranslatableMapper; +import com.velocitypowered.proxy.util.collect.CappedSet; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.net.InetSocketAddress; @@ -144,6 +145,7 @@ import org.jetbrains.annotations.NotNull; public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable, VelocityInboundConnection { + private static final int MAX_CLIENTSIDE_PLUGIN_CHANNELS = 1024; private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build(); static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED; @@ -173,6 +175,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private final InternalTabList tabList; private final VelocityServer server; private ClientConnectionPhase connectionPhase; + private final Collection clientsideChannels; private final CompletableFuture teardownFuture = new CompletableFuture<>(); private @MonotonicNonNull List serversToTry = null; private final ResourcePackHandler resourcePackHandler; @@ -205,6 +208,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED; this.connectionPhase = connection.getType().getInitialClientPhase(); this.onlineMode = onlineMode; + this.clientsideChannels = CappedSet.create(MAX_CLIENTSIDE_PLUGIN_CHANNELS); if (connection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) { this.tabList = new VelocityTabList(this); @@ -1342,6 +1346,15 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.connectionPhase = connectionPhase; } + /** + * Return all the plugin message channels that registered by client. + * + * @return the channels + */ + public Collection getClientsideChannels() { + return clientsideChannels; + } + @Override public @Nullable IdentifiedKey getIdentifiedKey() { return playerKey; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java new file mode 100644 index 00000000..692910d5 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2019-2023 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 . + */ + +package com.velocitypowered.proxy.util.collect; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ForwardingSet; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * An unsynchronized collection that puts an upper bound on the size of the collection. + */ +public final class CappedSet extends ForwardingSet { + + private final Set delegate; + private final int upperSize; + + private CappedSet(Set delegate, int upperSize) { + this.delegate = delegate; + this.upperSize = upperSize; + } + + /** + * Creates a capped collection backed by a {@link HashSet}. + * + * @param maxSize the maximum size of the collection + * @param the type of elements in the collection + * @return the new collection + */ + public static Set create(int maxSize) { + return new CappedSet<>(new HashSet<>(), maxSize); + } + + @Override + protected Set delegate() { + return delegate; + } + + @Override + public boolean add(T element) { + if (this.delegate.size() >= upperSize) { + Preconditions.checkState(this.delegate.contains(element), + "collection is too large (%s >= %s)", + this.delegate.size(), this.upperSize); + return false; + } + return this.delegate.add(element); + } + + @Override + public boolean addAll(Collection collection) { + return this.standardAddAll(collection); + } +} diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java new file mode 100644 index 00000000..2e118b4a --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019-2021 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 . + */ + +package com.velocitypowered.proxy.util.collect; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class CappedSetTest { + + @Test + void basicVerification() { + Collection coll = CappedSet.create(1); + assertTrue(coll.add("coffee"), "did not add single item"); + assertThrows(IllegalStateException.class, () -> coll.add("tea"), + "item was added to collection although it is too full"); + assertEquals(1, coll.size(), "collection grew in size unexpectedly"); + } + + @Test + void testAddAll() { + Set doesFill1 = ImmutableSet.of("coffee", "tea"); + Set doesFill2 = ImmutableSet.of("chocolate"); + Set overfill = ImmutableSet.of("Coke", "Pepsi"); + + Collection coll = CappedSet.create(3); + assertTrue(coll.addAll(doesFill1), "did not add items"); + assertTrue(coll.addAll(doesFill2), "did not add items"); + assertThrows(IllegalStateException.class, () -> coll.addAll(overfill), + "items added to collection although it is too full"); + assertEquals(3, coll.size(), "collection grew in size unexpectedly"); + } + + @Test + void handlesSetBehaviorCorrectly() { + Set doesFill1 = ImmutableSet.of("coffee", "tea"); + Set doesFill2 = ImmutableSet.of("coffee", "chocolate"); + Set overfill = ImmutableSet.of("coffee", "Coke", "Pepsi"); + + Collection coll = CappedSet.create(3); + assertTrue(coll.addAll(doesFill1), "did not add items"); + assertTrue(coll.addAll(doesFill2), "did not add items"); + assertThrows(IllegalStateException.class, () -> coll.addAll(overfill), + "items added to collection although it is too full"); + + assertFalse(coll.addAll(doesFill1), "added items?!?"); + + assertEquals(3, coll.size(), "collection grew in size unexpectedly"); + } +} \ No newline at end of file -- 2.39.5 From 676ec9cb21d4372973b7f5443b76a5e0bba2f0f8 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Sun, 6 Apr 2025 20:17:28 +0100 Subject: [PATCH 06/18] preliminary cleanup of plugin message channel handling --- .../backend/BungeeCordMessageResponder.java | 11 ++-- .../client/ClientPlaySessionHandler.java | 18 ++----- .../connection/client/ConnectedPlayer.java | 4 +- .../protocol/util/PluginMessageUtil.java | 51 +++++++++++++++++-- .../proxy/util/VelocityChannelRegistrar.java | 16 +++--- 5 files changed, 67 insertions(+), 33 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java index 6161c4c2..a2ea94d6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java @@ -20,6 +20,7 @@ package com.velocitypowered.proxy.connection.backend; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.api.proxy.server.RegisteredServer; @@ -316,9 +317,9 @@ public class BungeeCordMessageResponder { }); } - static String getBungeeCordChannel(ProtocolVersion version) { - return version.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL.getId() - : LEGACY_CHANNEL.getId(); + static ChannelIdentifier getBungeeCordChannel(ProtocolVersion version) { + return version.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL + : LEGACY_CHANNEL; } // Note: this method will always release the buffer! @@ -329,8 +330,8 @@ public class BungeeCordMessageResponder { // Note: this method will always release the buffer! private static void sendServerResponse(ConnectedPlayer player, ByteBuf buf) { MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected(); - String chan = getBungeeCordChannel(serverConnection.getProtocolVersion()); - PluginMessagePacket msg = new PluginMessagePacket(chan, buf); + ChannelIdentifier chan = getBungeeCordChannel(serverConnection.getProtocolVersion()); + PluginMessagePacket msg = new PluginMessagePacket(chan.getId(), buf); serverConnection.write(msg); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index a7e490ed..7c3a4486 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -162,7 +162,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public void activated() { configSwitchFuture = new CompletableFuture<>(); - Collection channels = + Collection channels = server.getChannelRegistrar().getChannelsForProtocol(player.getProtocolVersion()); if (!channels.isEmpty()) { PluginMessagePacket register = constructChannelsPacket(player.getProtocolVersion(), channels); @@ -310,22 +310,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { logger.warn("A plugin message was received while the backend server was not " + "ready. Channel: {}. Packet discarded.", packet.getChannel()); } else if (PluginMessageUtil.isRegister(packet)) { - List channels = PluginMessageUtil.getChannels(packet); + List channels = PluginMessageUtil.getChannels(packet, this.player.getProtocolVersion()); player.getClientsideChannels().addAll(channels); - List channelIdentifiers = new ArrayList<>(); - for (String channel : channels) { - try { - channelIdentifiers.add(MinecraftChannelIdentifier.from(channel)); - } catch (IllegalArgumentException e) { - channelIdentifiers.add(new LegacyChannelIdentifier(channel)); - } - } server.getEventManager() .fireAndForget( - new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channelIdentifiers))); + new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels))); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isUnregister(packet)) { - player.getClientsideChannels().removeAll(PluginMessageUtil.getChannels(packet)); + player.getClientsideChannels().removeAll(PluginMessageUtil.getChannels(packet, this.player.getProtocolVersion())); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isMcBrand(packet)) { String brand = PluginMessageUtil.readBrandMessage(packet.content()); @@ -589,7 +581,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Tell the server about the proxy's plugin message channels. ProtocolVersion serverVersion = serverMc.getProtocolVersion(); - final Collection channels = server.getChannelRegistrar() + final Collection channels = server.getChannelRegistrar() .getChannelsForProtocol(serverMc.getProtocolVersion()); if (!channels.isEmpty()) { serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels)); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 0f60b4dd..8f89add5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -175,7 +175,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private final InternalTabList tabList; private final VelocityServer server; private ClientConnectionPhase connectionPhase; - private final Collection clientsideChannels; + private final Collection clientsideChannels; private final CompletableFuture teardownFuture = new CompletableFuture<>(); private @MonotonicNonNull List serversToTry = null; private final ResourcePackHandler resourcePackHandler; @@ -1351,7 +1351,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, * * @return the channels */ - public Collection getClientsideChannels() { + public Collection getClientsideChannels() { return clientsideChannels; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java index 1cfd4a6f..801a313a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java @@ -22,13 +22,20 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.api.util.ProxyVersion; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; +import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.regex.Pattern; @@ -85,13 +92,15 @@ public final class PluginMessageUtil { .equals(UNREGISTER_CHANNEL); } + private static final QuietDecoderException ILLEGAL_CHANNEL = new QuietDecoderException("Illegal channel"); /** * Fetches all the channels in a register or unregister plugin message. * * @param message the message to get the channels from * @return the channels, as an immutable list */ - public static List getChannels(PluginMessagePacket message) { + public static List getChannels(PluginMessagePacket message, + ProtocolVersion protocolVersion) { checkNotNull(message, "message"); checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s", message.getChannel()); @@ -100,8 +109,25 @@ public final class PluginMessageUtil { // has caused issues with 1.13+ compatibility. Just return an empty list. return ImmutableList.of(); } - String channels = message.content().toString(StandardCharsets.UTF_8); - return ImmutableList.copyOf(channels.split("\0")); + String payload = message.content().toString(StandardCharsets.UTF_8); + String[] channels = payload.split("\0"); + List channelIdentifiers = new ArrayList<>(); + try { + for (String channel : channels) { + if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)) { + channelIdentifiers.add(MinecraftChannelIdentifier.from(channel)); + } else { + channelIdentifiers.add(new LegacyChannelIdentifier(channel)); + } + } + } catch (IllegalArgumentException e) { + if (MinecraftDecoder.DEBUG) { + throw e; + } else { + throw ILLEGAL_CHANNEL; + } + } + return ImmutableList.copyOf(channelIdentifiers); } /** @@ -112,16 +138,31 @@ public final class PluginMessageUtil { * @return the plugin message to send */ public static PluginMessagePacket constructChannelsPacket(ProtocolVersion protocolVersion, - Collection channels) { + Collection channels) { checkNotNull(channels, "channels"); checkArgument(!channels.isEmpty(), "no channels specified"); String channelName = protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY; ByteBuf contents = Unpooled.buffer(); - contents.writeCharSequence(String.join("\0", channels), StandardCharsets.UTF_8); + contents.writeCharSequence(joinChannels(channels), StandardCharsets.UTF_8); return new PluginMessagePacket(channelName, contents); } + private static String joinChannels(Collection channels) { + checkNotNull(channels, "channels"); + checkArgument(!channels.isEmpty(), "no channels specified"); + StringBuilder sb = new StringBuilder(); + Iterator iterator = channels.iterator(); + while (iterator.hasNext()) { + ChannelIdentifier channel = iterator.next(); + sb.append(channel.getId()); + if (iterator.hasNext()) { + sb.append('\0'); + } + } + return sb.toString(); + } + /** * Rewrites the brand message to indicate the presence of Velocity. * diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java index d28ab9bf..e878cfd8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java @@ -79,10 +79,10 @@ public class VelocityChannelRegistrar implements ChannelRegistrar { * * @return all legacy channel IDs */ - public Collection getLegacyChannelIds() { - Collection ids = new HashSet<>(); + public Collection getLegacyChannelIds() { + Collection ids = new HashSet<>(); for (ChannelIdentifier value : identifierMap.values()) { - ids.add(value.getId()); + ids.add(new LegacyChannelIdentifier(value.getId())); } return ids; } @@ -92,13 +92,13 @@ public class VelocityChannelRegistrar implements ChannelRegistrar { * * @return the channel IDs for Minecraft 1.13 and above */ - public Collection getModernChannelIds() { - Collection ids = new HashSet<>(); + public Collection getModernChannelIds() { + Collection ids = new HashSet<>(); for (ChannelIdentifier value : identifierMap.values()) { if (value instanceof MinecraftChannelIdentifier) { - ids.add(value.getId()); + ids.add(value); } else { - ids.add(PluginMessageUtil.transformLegacyToModernChannel(value.getId())); + ids.add(MinecraftChannelIdentifier.from(PluginMessageUtil.transformLegacyToModernChannel(value.getId()))); } } return ids; @@ -114,7 +114,7 @@ public class VelocityChannelRegistrar implements ChannelRegistrar { * @param protocolVersion the protocol version in use * @return the list of channels to register */ - public Collection getChannelsForProtocol(ProtocolVersion protocolVersion) { + public Collection getChannelsForProtocol(ProtocolVersion protocolVersion) { if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)) { return getModernChannelIds(); } -- 2.39.5 From b482443e7915890acb789421c4465cbae1e2833c Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Sun, 6 Apr 2025 21:22:22 +0100 Subject: [PATCH 07/18] Fix spot --- .../proxy/connection/client/ClientPlaySessionHandler.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 7c3a4486..1fd72063 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -30,8 +30,6 @@ import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; -import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; -import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; -- 2.39.5 From 747f70d80ae104968ba3c255c0a460d8ddae785d Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Sun, 6 Apr 2025 21:25:03 +0100 Subject: [PATCH 08/18] Appease checkstyle --- .../velocitypowered/proxy/protocol/util/PluginMessageUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java index 801a313a..800ca5df 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java @@ -93,6 +93,7 @@ public final class PluginMessageUtil { } private static final QuietDecoderException ILLEGAL_CHANNEL = new QuietDecoderException("Illegal channel"); + /** * Fetches all the channels in a register or unregister plugin message. * -- 2.39.5 From 9c1be72db0475fdd5ddc31ea04910731ffe34ecc Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Sun, 6 Apr 2025 21:25:08 +0100 Subject: [PATCH 09/18] Fix tests --- .../util/VelocityChannelRegistrarTest.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/VelocityChannelRegistrarTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/VelocityChannelRegistrarTest.java index 504df681..b6fb9cbd 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/util/VelocityChannelRegistrarTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/VelocityChannelRegistrarTest.java @@ -20,8 +20,10 @@ package com.velocitypowered.proxy.util; import static org.junit.jupiter.api.Assertions.assertEquals; import com.google.common.collect.ImmutableSet; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; class VelocityChannelRegistrarTest { @@ -46,9 +48,9 @@ class VelocityChannelRegistrarTest { // Two channels cover the modern channel (velocity:test) and the legacy-mapped channel // (legacy:velocitytest). Make sure they're what we expect. assertEquals(ImmutableSet.of(MODERN.getId(), SIMPLE_LEGACY_REMAPPED), registrar - .getModernChannelIds()); + .getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet())); assertEquals(ImmutableSet.of(SIMPLE_LEGACY.getId(), MODERN.getId()), registrar - .getLegacyChannelIds()); + .getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet())); } @Test @@ -57,9 +59,10 @@ class VelocityChannelRegistrarTest { registrar.register(SPECIAL_REMAP_LEGACY, MODERN_SPECIAL_REMAP); // This one, just one channel for the modern case. - assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId()), registrar.getModernChannelIds()); + assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId()), + registrar.getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet())); assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId(), SPECIAL_REMAP_LEGACY.getId()), - registrar.getLegacyChannelIds()); + registrar.getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet())); } @Test @@ -68,7 +71,9 @@ class VelocityChannelRegistrarTest { registrar.register(MODERN, SIMPLE_LEGACY); registrar.unregister(SIMPLE_LEGACY); - assertEquals(ImmutableSet.of(MODERN.getId()), registrar.getModernChannelIds()); - assertEquals(ImmutableSet.of(MODERN.getId()), registrar.getLegacyChannelIds()); + assertEquals(ImmutableSet.of(MODERN.getId()), + registrar.getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));; + assertEquals(ImmutableSet.of(MODERN.getId()), + registrar.getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet())); } } \ No newline at end of file -- 2.39.5 From a549880df1a7454799d30be57b9b7df4cb5c43b7 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Wed, 9 Apr 2025 01:21:08 -0400 Subject: [PATCH 10/18] Bump to Netty 4.2.0 (#1380) --- gradle/libs.versions.toml | 3 +- proxy/build.gradle.kts | 3 ++ .../proxy/network/TransportType.java | 44 ++++++++++++++----- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3057e031..05731bb2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ configurate3 = "3.7.3" configurate4 = "4.1.2" flare = "2.0.1" log4j = "2.24.1" -netty = "4.1.119.Final" +netty = "4.2.0.Final" [plugins] indra-publishing = "net.kyori.indra.publishing:2.0.6" @@ -54,6 +54,7 @@ netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" } netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" } netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" } +netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" } nightconfig = "com.electronwill.night-config:toml:3.6.7" slf4j = "org.slf4j:slf4j-api:2.0.12" snakeyaml = "org.yaml:snakeyaml:1.33" diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index ab736e20..b3c5892f 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -121,6 +121,9 @@ dependencies { implementation(libs.netty.transport.native.epoll) implementation(variantOf(libs.netty.transport.native.epoll) { classifier("linux-x86_64") }) implementation(variantOf(libs.netty.transport.native.epoll) { classifier("linux-aarch_64") }) + implementation(libs.netty.transport.native.iouring) + implementation(variantOf(libs.netty.transport.native.iouring) { classifier("linux-x86_64") }) + implementation(variantOf(libs.netty.transport.native.iouring) { classifier("linux-aarch_64") }) implementation(libs.netty.transport.native.kqueue) implementation(variantOf(libs.netty.transport.native.kqueue) { classifier("osx-x86_64") }) implementation(variantOf(libs.netty.transport.native.kqueue) { classifier("osx-aarch_64") }) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java index 6cbdfd8c..a1232cce 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java @@ -20,25 +20,32 @@ package com.velocitypowered.proxy.network; import com.velocitypowered.proxy.util.concurrent.VelocityNettyThreadFactory; import io.netty.channel.ChannelFactory; import io.netty.channel.EventLoopGroup; +import io.netty.channel.IoHandlerFactory; +import io.netty.channel.MultiThreadIoEventLoopGroup; import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollDatagramChannel; -import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollIoHandler; import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.epoll.EpollSocketChannel; import io.netty.channel.kqueue.KQueue; import io.netty.channel.kqueue.KQueueDatagramChannel; -import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueIoHandler; import io.netty.channel.kqueue.KQueueServerSocketChannel; import io.netty.channel.kqueue.KQueueSocketChannel; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.channel.uring.IoUring; +import io.netty.channel.uring.IoUringDatagramChannel; +import io.netty.channel.uring.IoUringIoHandler; +import io.netty.channel.uring.IoUringServerSocketChannel; +import io.netty.channel.uring.IoUringSocketChannel; import java.util.concurrent.ThreadFactory; -import java.util.function.BiFunction; +import java.util.function.Supplier; /** * Enumerates the supported transports for Velocity. @@ -47,32 +54,36 @@ public enum TransportType { NIO("NIO", NioServerSocketChannel::new, NioSocketChannel::new, NioDatagramChannel::new, - (name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))), + NioIoHandler::newFactory), EPOLL("epoll", EpollServerSocketChannel::new, EpollSocketChannel::new, EpollDatagramChannel::new, - (name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))), + EpollIoHandler::newFactory), KQUEUE("kqueue", KQueueServerSocketChannel::new, KQueueSocketChannel::new, KQueueDatagramChannel::new, - (name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type))); + KQueueIoHandler::newFactory), + IO_URING("io_uring", IoUringServerSocketChannel::new, + IoUringSocketChannel::new, + IoUringDatagramChannel::new, + IoUringIoHandler::newFactory); final String name; final ChannelFactory serverSocketChannelFactory; final ChannelFactory socketChannelFactory; final ChannelFactory datagramChannelFactory; - final BiFunction eventLoopGroupFactory; + final Supplier ioHandlerFactorySupplier; TransportType(final String name, final ChannelFactory serverSocketChannelFactory, final ChannelFactory socketChannelFactory, final ChannelFactory datagramChannelFactory, - final BiFunction eventLoopGroupFactory) { + final Supplier ioHandlerFactorySupplier) { this.name = name; this.serverSocketChannelFactory = serverSocketChannelFactory; this.socketChannelFactory = socketChannelFactory; this.datagramChannelFactory = datagramChannelFactory; - this.eventLoopGroupFactory = eventLoopGroupFactory; + this.ioHandlerFactorySupplier = ioHandlerFactorySupplier; } @Override @@ -80,8 +91,15 @@ public enum TransportType { return this.name; } + /** + * Creates a new event loop group for the given type. + * + * @param type the type of event loop group to create + * @return the event loop group + */ public EventLoopGroup createEventLoopGroup(final Type type) { - return this.eventLoopGroupFactory.apply(this.name, type); + return new MultiThreadIoEventLoopGroup( + 0, createThreadFactory(this.name, type), this.ioHandlerFactorySupplier.get()); } private static ThreadFactory createThreadFactory(final String name, final Type type) { @@ -98,6 +116,10 @@ public enum TransportType { return NIO; } + if (IoUring.isAvailable() && !Boolean.getBoolean("velocity.disable-iouring-transport")) { + return IO_URING; + } + if (Epoll.isAvailable()) { return EPOLL; } -- 2.39.5 From ae312339a36d9c60232a275378cf264d41d4f7c9 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Fri, 11 Apr 2025 00:35:49 -0400 Subject: [PATCH 11/18] Disable io_uring transport by default --- .../java/com/velocitypowered/proxy/network/TransportType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java index a1232cce..e154e5d5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java @@ -116,7 +116,7 @@ public enum TransportType { return NIO; } - if (IoUring.isAvailable() && !Boolean.getBoolean("velocity.disable-iouring-transport")) { + if (IoUring.isAvailable() && Boolean.getBoolean("velocity.enable-iouring-transport")) { return IO_URING; } -- 2.39.5 From a51711e4bb59b4c59a9e3980cfba049a22c470f2 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Sat, 12 Apr 2025 16:20:07 +0100 Subject: [PATCH 12/18] Use an ImmutableList Builder --- .../proxy/protocol/util/PluginMessageUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java index 800ca5df..76d6f727 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java @@ -112,7 +112,7 @@ public final class PluginMessageUtil { } String payload = message.content().toString(StandardCharsets.UTF_8); String[] channels = payload.split("\0"); - List channelIdentifiers = new ArrayList<>(); + ImmutableList.Builder channelIdentifiers = ImmutableList.builderWithExpectedSize(channels.length); try { for (String channel : channels) { if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)) { @@ -128,7 +128,7 @@ public final class PluginMessageUtil { throw ILLEGAL_CHANNEL; } } - return ImmutableList.copyOf(channelIdentifiers); + return channelIdentifiers.build(); } /** -- 2.39.5 From 7ad06614fe1ec573e7405adf58a9c2bac703a97f Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Sat, 12 Apr 2025 16:22:06 +0100 Subject: [PATCH 13/18] Appease checkstyle gods --- .../velocitypowered/proxy/protocol/util/PluginMessageUtil.java | 1 - 1 file changed, 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java index 76d6f727..fefc64c6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java @@ -33,7 +33,6 @@ import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; -- 2.39.5 From 74d05211d685ae8aa2a221b0bb086bae51b43eb4 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Sat, 12 Apr 2025 16:52:31 +0100 Subject: [PATCH 14/18] Also validate length before caring to invest time into processing --- .../connection/client/ClientPlaySessionHandler.java | 10 +++++++--- .../proxy/connection/client/ConnectedPlayer.java | 2 +- .../proxy/protocol/util/PluginMessageUtil.java | 8 +++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 1fd72063..adaeb337 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -308,14 +308,17 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { logger.warn("A plugin message was received while the backend server was not " + "ready. Channel: {}. Packet discarded.", packet.getChannel()); } else if (PluginMessageUtil.isRegister(packet)) { - List channels = PluginMessageUtil.getChannels(packet, this.player.getProtocolVersion()); + List channels = + PluginMessageUtil.getChannels(this.player.getClientsideChannels().size(), packet, + this.player.getProtocolVersion()); player.getClientsideChannels().addAll(channels); server.getEventManager() .fireAndForget( new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels))); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isUnregister(packet)) { - player.getClientsideChannels().removeAll(PluginMessageUtil.getChannels(packet, this.player.getProtocolVersion())); + player.getClientsideChannels() + .removeAll(PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion())); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isMcBrand(packet)) { String brand = PluginMessageUtil.readBrandMessage(packet.content()); @@ -392,7 +395,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Complete client switch player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG); VelocityServerConnection serverConnection = player.getConnectedServer(); - server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection)); + server.getEventManager() + .fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection)); if (serverConnection != null) { MinecraftConnection smc = serverConnection.ensureConnected(); CompletableFuture.runAsync(() -> { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 8f89add5..982b9652 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -145,7 +145,7 @@ import org.jetbrains.annotations.NotNull; public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable, VelocityInboundConnection { - private static final int MAX_CLIENTSIDE_PLUGIN_CHANNELS = 1024; + public static final int MAX_CLIENTSIDE_PLUGIN_CHANNELS = 1024; private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build(); static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java index fefc64c6..1936eb3f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java @@ -26,6 +26,7 @@ import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.api.util.ProxyVersion; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; @@ -96,10 +97,12 @@ public final class PluginMessageUtil { /** * Fetches all the channels in a register or unregister plugin message. * + * @param existingChannels the number of channels already registered * @param message the message to get the channels from * @return the channels, as an immutable list */ - public static List getChannels(PluginMessagePacket message, + public static List getChannels(int existingChannels, + PluginMessagePacket message, ProtocolVersion protocolVersion) { checkNotNull(message, "message"); checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s", @@ -110,7 +113,10 @@ public final class PluginMessageUtil { return ImmutableList.of(); } String payload = message.content().toString(StandardCharsets.UTF_8); + checkArgument(payload.length() <= Short.MAX_VALUE, "payload too long: %s", payload.length()); String[] channels = payload.split("\0"); + checkArgument(existingChannels + channels.length <= ConnectedPlayer.MAX_CLIENTSIDE_PLUGIN_CHANNELS, + "too many channels: %s + %s > %s", existingChannels, channels.length, ConnectedPlayer.MAX_CLIENTSIDE_PLUGIN_CHANNELS); ImmutableList.Builder channelIdentifiers = ImmutableList.builderWithExpectedSize(channels.length); try { for (String channel : channels) { -- 2.39.5 From 3f0a85d794e3f401409a013fac6ea48b22a2e3ae Mon Sep 17 00:00:00 2001 From: booky Date: Mon, 14 Apr 2025 19:52:02 +0200 Subject: [PATCH 15/18] Fix MinecraftChannelIdentifier parsing to align with vanilla (#1552) --- .../messages/MinecraftChannelIdentifier.java | 24 +++++++------------ .../MinecraftChannelIdentifierTest.java | 22 +++++++++++------ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java index 98967cfd..a271a414 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java @@ -11,7 +11,6 @@ import static com.google.common.base.Preconditions.checkArgument; import com.google.common.base.Strings; import java.util.Objects; -import java.util.regex.Pattern; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; @@ -21,8 +20,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; */ public final class MinecraftChannelIdentifier implements ChannelIdentifier { - private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9/\\-_]*"); - private final String namespace; private final String name; @@ -39,7 +36,7 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier { * @return a new channel identifier */ public static MinecraftChannelIdentifier forDefaultNamespace(String name) { - return new MinecraftChannelIdentifier("minecraft", name); + return new MinecraftChannelIdentifier(Key.MINECRAFT_NAMESPACE, name); } /** @@ -52,14 +49,10 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier { public static MinecraftChannelIdentifier create(String namespace, String name) { checkArgument(!Strings.isNullOrEmpty(namespace), "namespace is null or empty"); checkArgument(name != null, "namespace is null or empty"); - checkArgument(VALID_IDENTIFIER_REGEX.matcher(namespace).matches(), - "namespace is not valid, must match: %s got %s", - VALID_IDENTIFIER_REGEX.toString(), - namespace); - checkArgument(VALID_IDENTIFIER_REGEX.matcher(name).matches(), - "name is not valid, must match: %s got %s", - VALID_IDENTIFIER_REGEX.toString(), - name); + checkArgument(Key.parseableNamespace(namespace), + "namespace is not valid, must match: [a-z0-9_.-] got %s", namespace); + checkArgument(Key.parseableValue(name), + "name is not valid, must match: [a-z0-9/._-] got %s", name); return new MinecraftChannelIdentifier(namespace, name); } @@ -72,10 +65,9 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier { public static MinecraftChannelIdentifier from(String identifier) { int colonPos = identifier.indexOf(':'); if (colonPos == -1) { - throw new IllegalArgumentException("Identifier does not contain a colon."); - } - if (colonPos + 1 == identifier.length()) { - throw new IllegalArgumentException("Identifier is empty."); + return create(Key.MINECRAFT_NAMESPACE, identifier); + } else if (colonPos == 0) { + return create(Key.MINECRAFT_NAMESPACE, identifier.substring(1)); } String namespace = identifier.substring(0, colonPos); String name = identifier.substring(colonPos + 1); diff --git a/api/src/test/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifierTest.java b/api/src/test/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifierTest.java index 4c8919f3..9be12fc5 100644 --- a/api/src/test/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifierTest.java +++ b/api/src/test/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifierTest.java @@ -47,17 +47,25 @@ class MinecraftChannelIdentifierTest { create("velocity", "test/test2"); } + @Test + void fromIdentifierDefaultNamespace() { + assertEquals("minecraft", from("test").getNamespace()); + assertEquals("minecraft", from(":test").getNamespace()); + } + + @Test + void fromIdentifierAllowsEmptyName() { + from("minecraft:"); + from(":"); + from(""); + } + @Test void fromIdentifierThrowsOnBadValues() { assertAll( - () -> assertThrows(IllegalArgumentException.class, () -> from("")), - () -> assertThrows(IllegalArgumentException.class, () -> from(":")), - () -> assertThrows(IllegalArgumentException.class, () -> from(":a")), - () -> assertThrows(IllegalArgumentException.class, () -> from("a:")), () -> assertThrows(IllegalArgumentException.class, () -> from("hello:$$$$$$")), + () -> assertThrows(IllegalArgumentException.class, () -> from("he/llo:wor/ld")), () -> assertThrows(IllegalArgumentException.class, () -> from("hello::")) ); } - - -} \ No newline at end of file +} -- 2.39.5 From bd2bb6325ef88ef06b4adc4a62eff9d23d25e96d Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Thu, 17 Apr 2025 18:59:49 +0100 Subject: [PATCH 16/18] Validate state transition --- .../velocitypowered/proxy/connection/MinecraftConnection.java | 1 + .../proxy/connection/client/ClientPlaySessionHandler.java | 4 ++++ .../proxy/connection/client/ConnectedPlayer.java | 1 + 3 files changed, 6 insertions(+) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java index c9173907..3f34a54c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -84,6 +84,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { private static final Logger logger = LogManager.getLogger(MinecraftConnection.class); private final Channel channel; + public boolean pendingConfigurationSwitch = false; private SocketAddress remoteAddress; private StateRegistry state; private Map sessionHandlers; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index adaeb337..10b837fa 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -72,6 +72,7 @@ import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.CharacterUtil; +import com.velocitypowered.proxy.util.except.QuietRuntimeException; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; @@ -392,6 +393,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(FinishedUpdatePacket packet) { + if (!player.getConnection().pendingConfigurationSwitch) { + throw new QuietRuntimeException("Not expecting reconfiguration"); + } // Complete client switch player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG); VelocityServerConnection serverConnection = player.getConnectedServer(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 982b9652..22e1dc9c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -1318,6 +1318,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, connection.write(BundleDelimiterPacket.INSTANCE); } connection.write(StartUpdatePacket.INSTANCE); + connection.pendingConfigurationSwitch = true; connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); // Make sure we don't send any play packets to the player after update start connection.addPlayPacketQueueHandler(); -- 2.39.5 From b6e05cb0b94001cb560439758f203895bc838379 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sun, 27 Apr 2025 20:09:05 +0200 Subject: [PATCH 17/18] Refactor TCP Fast Open checks and update message identifiers. Removed transport type conditions for TCP Fast Open to streamline configuration usage. Added imports for new message identifiers in `ClientPlaySessionHandler`. Cleaned up Netty library definitions in `libs.versions.toml`. --- gradle/libs.versions.toml | 1 - .../proxy/connection/client/ClientPlaySessionHandler.java | 2 ++ .../com/velocitypowered/proxy/network/ConnectionManager.java | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 54fb5efe..05731bb2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,6 @@ netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = " netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" } netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" } netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" } -netty-transport-native-iouring = { module = "io.netty.incubator:netty-incubator-transport-native-io_uring", version = "0.0.25.Final" } netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" } netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" } nightconfig = "com.electronwill.night-config:toml:3.6.7" diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 8e6b39cf..0b89e7ba 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -30,6 +30,8 @@ import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java index 2a4c27c8..7b724f61 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java @@ -104,7 +104,7 @@ public final class ConnectionManager { .childOption(ChannelOption.IP_TOS, 0x18) .localAddress(address); - if (transportType.supportsTcpFastOpenServer() && server.getConfiguration().useTcpFastOpen()) { + if (server.getConfiguration().useTcpFastOpen()) { bootstrap.option(ChannelOption.TCP_FASTOPEN, 3); } @@ -197,7 +197,7 @@ public final class ConnectionManager { this.server.getConfiguration().getConnectTimeout()) .group(group == null ? this.workerGroup : group) .resolver(this.resolver.asGroup()); - if (transportType.supportsTcpFastOpenClient() && server.getConfiguration().useTcpFastOpen()) { + if (server.getConfiguration().useTcpFastOpen()) { bootstrap.option(ChannelOption.TCP_FASTOPEN_CONNECT, true); } return bootstrap; -- 2.39.5 From 91a61643bda6dffa2325aa65bad683cc9a81817c Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sun, 27 Apr 2025 20:24:41 +0200 Subject: [PATCH 18/18] Revert "Disable io_uring transport by default" This reverts commit ae312339a36d9c60232a275378cf264d41d4f7c9. --- .../java/com/velocitypowered/proxy/network/TransportType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java index e154e5d5..a1232cce 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java @@ -116,7 +116,7 @@ public enum TransportType { return NIO; } - if (IoUring.isAvailable() && Boolean.getBoolean("velocity.enable-iouring-transport")) { + if (IoUring.isAvailable() && !Boolean.getBoolean("velocity.disable-iouring-transport")) { return IO_URING; } -- 2.39.5