Merge remote-tracking branch 'upstream/dev/4.0.0'
SteamWarCI Build successful

# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java
This commit is contained in:
2026-07-13 21:48:01 +02:00
40 changed files with 416 additions and 174 deletions
+3 -5
View File
@@ -59,17 +59,15 @@ tasks {
val o = options as StandardJavadocDocletOptions val o = options as StandardJavadocDocletOptions
o.encoding = "UTF-8" o.encoding = "UTF-8"
o.source = "21" o.source = "25"
o.use() o.use()
o.links( o.links(
"https://www.javadocs.dev/org.slf4j/slf4j-api/${libs.slf4j.get().version}/", "https://www.javadocs.dev/org.slf4j/slf4j-api/${libs.slf4j.get().version}/",
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/", "https://guava.dev/releases/${libs.guava.get().version}/api/docs/",
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/", "https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
"https://docs.oracle.com/en/java/javase/17/docs/api/", "https://docs.oracle.com/en/java/javase/25/docs/api/",
"https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/", "https://jd.papermc.io/adventure/${libs.adventure.bom.get().version}/",
"https://jd.advntr.dev/text-minimessage/${libs.adventure.bom.get().version}/",
"https://jd.advntr.dev/key/${libs.adventure.bom.get().version}/",
"https://www.javadocs.dev/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/", "https://www.javadocs.dev/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/",
) )
@@ -0,0 +1,11 @@
/*
* Copyright (C) 2018 Velocity Contributors
*
* The Velocity API is licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in the api top-level directory.
*/
/**
* Provides events for handling command execution.
*/
package com.velocitypowered.api.event.command;
@@ -0,0 +1,11 @@
/*
* Copyright (C) 2018 Velocity Contributors
*
* The Velocity API is licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in the api top-level directory.
*/
/**
* Provides events for handling the player configuration phase.
*/
package com.velocitypowered.api.event.player.configuration;
@@ -0,0 +1,11 @@
/*
* Copyright (C) 2018 Velocity Contributors
*
* The Velocity API is licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in the api top-level directory.
*/
/**
* Provides events for handling registration of servers on the proxy.
*/
package com.velocitypowered.api.event.proxy.server;
@@ -95,7 +95,8 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
MINECRAFT_1_21_7(772, "1.21.7", "1.21.8"), MINECRAFT_1_21_7(772, "1.21.7", "1.21.8"),
MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"), MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"),
MINECRAFT_1_21_11(774, "1.21.11"), MINECRAFT_1_21_11(774, "1.21.11"),
MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2"); MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2"),
MINECRAFT_26_2(776, "26.2");
private static final int SNAPSHOT_BIT = 30; private static final int SNAPSHOT_BIT = 30;
@@ -197,7 +197,7 @@ public interface Player extends
* *
* @param reason component with the reason * @param reason component with the reason
*/ */
void disconnect(Component reason); void disconnect(@NotNull Component reason);
/** /**
* Sends chat input onto the players current server as if they typed it into the client chat box. * Sends chat input onto the players current server as if they typed it into the client chat box.
@@ -32,5 +32,6 @@ tasks.withType<Jar> {
archiveVersion.get() archiveVersion.get()
} }
attributes["Implementation-Version"] = velocityHumanVersion attributes["Implementation-Version"] = velocityHumanVersion
attributes["Enable-Native-Access"] = "ALL-UNNAMED"
} }
} }
+1 -1
View File
@@ -12,7 +12,7 @@ subprojects {
java { java {
toolchain { toolchain {
languageVersion.set(JavaLanguageVersion.of(21)) languageVersion.set(JavaLanguageVersion.of(25))
} }
} }
+1 -1
View File
@@ -1,2 +1,2 @@
group=com.velocitypowered group=com.velocitypowered
version=3.5.0-SNAPSHOT version=4.0.0-SNAPSHOT
+12 -13
View File
@@ -2,18 +2,17 @@
configurate3 = "3.7.3" configurate3 = "3.7.3"
configurate4 = "4.2.0" configurate4 = "4.2.0"
flare = "2.0.1" flare = "2.0.1"
log4j = "2.25.3" log4j = "2.26.0"
netty = "4.2.10.Final" netty = "4.2.16.Final"
[plugins] [plugins]
fill = "io.papermc.fill.gradle:1.0.10" fill = "io.papermc.fill.gradle:1.0.12"
shadow = "com.gradleup.shadow:9.3.1" shadow = "com.gradleup.shadow:9.5.1"
spotless = "com.diffplug.spotless:8.2.0" spotless = "com.diffplug.spotless:8.2.0"
[libraries] [libraries]
adventure-bom = "net.kyori:adventure-bom:4.26.1" adventure-bom = "net.kyori:adventure-bom:5.2.0"
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.26.1" adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:5.2.0"
adventure-facet = "net.kyori:adventure-platform-facet:4.4.1"
asm = "org.ow2.asm:asm:9.9.1" asm = "org.ow2.asm:asm:9.9.1"
auto-service = "com.google.auto.service:auto-service:1.1.1" auto-service = "com.google.auto.service:auto-service:1.1.1"
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1" auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1"
@@ -30,16 +29,16 @@ configurate4-hocon = { module = "org.spongepowered:configurate-hocon", version.r
configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" } configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" }
configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" } configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" }
disruptor = "com.lmax:disruptor:4.0.0" disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.15" fastutil = "it.unimi.dsi:fastutil:8.5.18"
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" } flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" } flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
jline = "org.jline:jline-terminal-jansi:3.30.6" jline = "org.jline:jline-terminal-ffm:4.3.1"
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
junit = "org.junit.jupiter:junit-jupiter:5.14.2" junit = "org.junit.jupiter:junit-jupiter:6.0.3"
jspecify = "org.jspecify:jspecify:1.0.0" jspecify = "org.jspecify:jspecify:1.0.0"
kyori-ansi = "net.kyori:ansi:1.1.1" kyori-ansi = "net.kyori:ansi:1.1.1"
guava = "com.google.guava:guava:33.5.0-jre" guava = "com.google.guava:guava:33.6.0-jre"
gson = "com.google.code.gson:gson:2.13.2" gson = "com.google.code.gson:gson:2.14.0"
guice = "com.google.inject:guice:7.0.0" guice = "com.google.inject:guice:7.0.0"
lmbda = "org.lanternpowered:lmbda:2.0.0" lmbda = "org.lanternpowered:lmbda:2.0.0"
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
@@ -47,7 +46,7 @@ log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "lo
log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" } log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" } log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" }
log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" } log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" }
mockito = "org.mockito:mockito-core:5.21.0" mockito = "org.mockito:mockito-core:5.22.0"
netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" } netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" }
netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" } netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" }
netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" } netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
+9 -1
View File
@@ -112,6 +112,15 @@ tasks {
workingDir = file("run").also(File::mkdirs) workingDir = file("run").also(File::mkdirs)
standardInput = System.`in` // Doesn't work? standardInput = System.`in` // Doesn't work?
} }
withType<JavaCompile>().configureEach {
options.compilerArgs.addAll(
listOf(
"-Alog4j.graalvm.groupId=${project.group}",
"-Alog4j.graalvm.artifactId=${project.name}"
)
)
}
} }
val projectVersion = version as String val projectVersion = version as String
@@ -159,7 +168,6 @@ dependencies {
implementation(libs.fastutil) implementation(libs.fastutil)
implementation(platform(libs.adventure.bom)) implementation(platform(libs.adventure.bom))
implementation(libs.adventure.text.serializer.json.legacy.impl) implementation(libs.adventure.text.serializer.json.legacy.impl)
implementation(libs.adventure.facet)
implementation(libs.completablefutures) implementation(libs.completablefutures)
implementation(libs.nightconfig) implementation(libs.nightconfig)
implementation(libs.bstats) implementation(libs.bstats)
@@ -21,10 +21,8 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.stream.Collectors;
import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.bstats.MetricsBase; import org.bstats.MetricsBase;
@@ -120,38 +118,28 @@ public class Metrics {
() -> server.getVersion().getVersion())); () -> server.getVersion().getVersion()));
metrics.addCustomChart(new DrilldownPie("java_version", () -> { metrics.addCustomChart(new DrilldownPie("java_version", () -> {
Map<String, Map<String, Integer>> map = new HashMap<>(); Runtime.Version version = Runtime.version();
String javaVersion = System.getProperty("java.version");
Map<String, Integer> entry = new HashMap<>();
entry.put(javaVersion, 1);
// http://openjdk.java.net/jeps/223 return Map.of(
// Java decided to change their versioning scheme and in doing so modified the "Java " + version.feature(),
// java.version system property to return $major[.$minor][.$security][-ea], as opposed to Map.of(javaVersion(version), 1));
// 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1",
// otherwise, 9+
String majorVersion = javaVersion.split("\\.")[0];
String release;
int indexOf = javaVersion.lastIndexOf('.');
if (majorVersion.equals("1")) {
release = "Java " + javaVersion.substring(0, indexOf);
} else {
// of course, it really wouldn't be all that simple if they didn't add a quirk, now
// would it valid strings for the major may potentially include values such as -ea to
// denote a pre release
Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion);
if (versionMatcher.find()) {
majorVersion = versionMatcher.group(0);
}
release = "Java " + majorVersion;
}
map.put(release, entry);
return map;
})); }));
} }
} }
} /**
* Recreates the exact {@code java.version} system property value from a {@link Runtime.Version}.
*
* <p>Per <a href="https://openjdk.org/jeps/223">JEP 223</a>, {@code java.version} is
* {@code $VNUM(-$PRE)?}; the build and optional segments only appear in {@code java.runtime.version}.
*
* @param v the runtime version
* @return the value {@code java.version} would hold on this JVM
*/
private static String javaVersion(Runtime.Version v) {
return v.version().stream()
.map(Object::toString)
.collect(Collectors.joining("."))
+ v.pre().map(p -> "-" + p).orElse("");
}
}
@@ -165,6 +165,8 @@ 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 Object sessionIdLock = new Object();
private volatile @Nullable UUID sessionId;
private final VelocityConsole console; private final VelocityConsole console;
private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter; private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter;
private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter; private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter;
@@ -743,6 +745,36 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection); connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
connectionsByUuid.remove(connection.getUniqueId(), connection); connectionsByUuid.remove(connection.getUniqueId(), connection);
connection.disconnected(); connection.disconnected();
if (this.sessionId != null && connectionsByUuid.isEmpty()) {
synchronized (this.sessionIdLock) {
if (connectionsByUuid.isEmpty()) {
this.sessionId = null;
}
}
}
}
/**
* Returns the metrics session ID for this proxy, generating one if none is currently active. The
* ID is shared by every player connected during a populated period and is regenerated once the
* proxy empties.
*
* @return the current session ID
*/
public UUID getSessionId() {
UUID uuid = this.sessionId;
if (uuid != null) {
return uuid;
}
synchronized (this.sessionIdLock) {
uuid = this.sessionId;
if (uuid == null) {
uuid = UUID.randomUUID();
this.sessionId = uuid;
}
return uuid;
}
} }
@Override @Override
@@ -31,6 +31,7 @@ import com.velocitypowered.proxy.config.migration.ForwardingMigration;
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration; import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration; import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
import com.velocitypowered.proxy.config.migration.MotdMigration; import com.velocitypowered.proxy.config.migration.MotdMigration;
import com.velocitypowered.proxy.config.migration.PacketLimiterMigration;
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration; import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.proxy.util.AddressUtil;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -511,7 +512,8 @@ public class VelocityConfiguration implements ProxyConfig {
new KeyAuthenticationMigration(), new KeyAuthenticationMigration(),
new MotdMigration(), new MotdMigration(),
new MiniMessageTranslationsMigration(), new MiniMessageTranslationsMigration(),
new TransferIntegrationMigration() new TransferIntegrationMigration(),
new PacketLimiterMigration()
}; };
for (final ConfigurationMigration migration : migrations) { for (final ConfigurationMigration migration : migrations) {
@@ -1004,12 +1006,13 @@ public class VelocityConfiguration implements ProxyConfig {
/** /**
* Configuration for packet limiting. * Configuration for packet limiting.
* *
* @param interval the interval in seconds to measure packets over * @param interval the interval in seconds to measure packets over
* @param pps the maximum number of packets per second allowed * @param pps the maximum number of packets per second allowed
* @param bytes the maximum number of bytes per second allowed * @param bytes the maximum number of bytes per second allowed
* @param bytesAfterDecompression the maximum number of decompressed bytes per second allowed
*/ */
public record PacketLimiterConfig(int interval, int pps, int bytes) { public record PacketLimiterConfig(int interval, int pps, int bytes, int bytesAfterDecompression) {
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, 500, -1); public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, -1, -1, 5242880);
/** /**
* returns a PacketLimiterConfig from a config section, or the default if the section is null. * returns a PacketLimiterConfig from a config section, or the default if the section is null.
@@ -1022,7 +1025,8 @@ public class VelocityConfiguration implements ProxyConfig {
return new PacketLimiterConfig( return new PacketLimiterConfig(
config.getIntOrElse("interval", DEFAULT.interval()), config.getIntOrElse("interval", DEFAULT.interval()),
config.getIntOrElse("packets-per-second", DEFAULT.pps()), config.getIntOrElse("packets-per-second", DEFAULT.pps()),
config.getIntOrElse("bytes-per-second", DEFAULT.bytes()) config.getIntOrElse("bytes-per-second", DEFAULT.bytes()),
config.getIntOrElse("decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression())
); );
} else { } else {
return DEFAULT; return DEFAULT;
@@ -29,7 +29,8 @@ public sealed interface ConfigurationMigration
KeyAuthenticationMigration, KeyAuthenticationMigration,
MotdMigration, MotdMigration,
MiniMessageTranslationsMigration, MiniMessageTranslationsMigration,
TransferIntegrationMigration { TransferIntegrationMigration,
PacketLimiterMigration {
boolean shouldMigrate(CommentedFileConfig config); boolean shouldMigrate(CommentedFileConfig config);
void migrate(CommentedFileConfig config, Logger logger) throws IOException; void migrate(CommentedFileConfig config, Logger logger) throws IOException;
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2026 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.config.migration;
import static com.velocitypowered.proxy.config.VelocityConfiguration.PacketLimiterConfig.DEFAULT;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import org.apache.logging.log4j.Logger;
/**
* Configuration migration for the new [packet-limiter] section.
* Config version 2.7 may contain this section with only the `interval`, `packets-per-second`
* and `bytes-per-second` attributes. Config version 2.8 enforces these exist, adds the new
* `decompressed-bytes-per-second` attribute, adjusts the new default, and adds comments.
*/
public final class PacketLimiterMigration implements ConfigurationMigration {
@Override
public boolean shouldMigrate(CommentedFileConfig config) {
return configVersion(config) < 2.8;
}
@Override
public void migrate(CommentedFileConfig config, Logger logger) {
config.set("packet-limiter.interval", DEFAULT.interval());
config.set("packet-limiter.packets-per-second", DEFAULT.pps());
config.set("packet-limiter.bytes-per-second", DEFAULT.bytes());
config.set("packet-limiter.decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression());
config.setComment("packet-limiter.interval", """
Size of the moving time window in seconds used to calculate average rates.
A larger window tolerates short bursts while still enforcing the configured limits over time.""");
config.setComment("packet-limiter.packets-per-second", """
Maximum average number of packets per second a client may send. -1 disables this check.""");
config.setComment("packet-limiter.bytes-per-second", """
Maximum average number of compressed (on-wire) bytes per second a client may send. -1 disables this check.""");
config.setComment("packet-limiter.decompressed-bytes-per-second", """
Maximum average number of decompressed bytes per second a client may send.
Protects against compression bomb attacks where small packets expand to excessive sizes after decompression.
-1 disables this check.""");
config.set("config-version", "2.8");
}
}
@@ -38,7 +38,9 @@ import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler; import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
import com.velocitypowered.proxy.connection.client.StatusSessionHandler; import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
import com.velocitypowered.proxy.network.Connections; import com.velocitypowered.proxy.network.Connections;
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.VelocityConnectionEvent; import com.velocitypowered.proxy.protocol.VelocityConnectionEvent;
import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder;
@@ -571,6 +573,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder); channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder); channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
var packetLimiterConfig = server.getConfiguration().getPacketLimiterConfig();
if (minecraftDecoder.getDirection() == ProtocolUtils.Direction.SERVERBOUND
&& packetLimiterConfig.interval() > 0
&& packetLimiterConfig.bytesAfterDecompression() > 0) {
decoder.setPacketLimiter(new SimpleBytesPerSecondLimiter(
-1, packetLimiterConfig.bytesAfterDecompression(), packetLimiterConfig.interval()));
}
channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED); channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED);
} }
} }
@@ -89,7 +89,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(JoinGamePacket packet) { public boolean handle(JoinGamePacket packet) {
MinecraftConnection smc = serverConn.ensureConnected(); final MinecraftConnection smc = serverConn.ensureConnected();
final RegisteredServer previousServer = serverConn.getPreviousServer().orElse(null); final RegisteredServer previousServer = serverConn.getPreviousServer().orElse(null);
final ConnectedPlayer player = serverConn.getPlayer(); final ConnectedPlayer player = serverConn.getPlayer();
final VelocityServerConnection existingConnection = player.getConnectedServer(); final VelocityServerConnection existingConnection = player.getConnectedServer();
@@ -106,6 +106,9 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
// Reset Tablist header and footer to prevent desync // Reset Tablist header and footer to prevent desync
player.clearPlayerListHeaderAndFooter(); player.clearPlayerListHeaderAndFooter();
// Override online mode
packet.setOnlineMode(player.isOnlineMode());
// The goods are in hand! We got JoinGame. Let's transition completely to the new state. // The goods are in hand! We got JoinGame. Let's transition completely to the new state.
smc.setAutoReading(false); smc.setAutoReading(false);
server.getEventManager() server.getEventManager()
@@ -236,6 +236,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
success.setUsername(player.getUsername()); success.setUsername(player.getUsername());
success.setProperties(player.getGameProfileProperties()); success.setProperties(player.getGameProfileProperties());
success.setUuid(player.getUniqueId()); success.setUuid(player.getUniqueId());
if (inbound.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
success.setSessionId(server.getSessionId());
}
mcConnection.write(success); mcConnection.write(success);
loginState = State.SUCCESS_SENT; loginState = State.SUCCESS_SENT;
@@ -88,6 +88,8 @@ import java.util.Queue;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.ComponentLike;
@@ -104,12 +106,23 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private static final boolean BACKPRESSURE_LOG = private static final boolean BACKPRESSURE_LOG =
Boolean.getBoolean("velocity.log-server-backpressure"); Boolean.getBoolean("velocity.log-server-backpressure");
// Caps the per-connection queue used while the FML/login phases are not yet "complete". Without
// these caps, a client that never completes its handshake phase can spam plugin messages (each up
// to ~32 KiB serverbound) and grow the queue without bound.
private static final long MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES =
Long.getLong("velocity.max-queued-login-plugin-message-bytes", 4L * 1024 * 1024);
private static final int MAX_QUEUED_LOGIN_PLUGIN_MESSAGES =
Integer.getInteger("velocity.max-queued-login-plugin-messages", 1024);
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class); private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
private final ConnectedPlayer player; private final ConnectedPlayer player;
private boolean spawned = false; private boolean spawned = false;
private final List<UUID> serverBossBars = new ArrayList<>(); private final List<UUID> serverBossBars = new ArrayList<>();
private final Queue<PluginMessagePacket> loginPluginMessages = new ConcurrentLinkedQueue<>(); private final Queue<PluginMessagePacket> loginPluginMessages = new ConcurrentLinkedQueue<>();
private final AtomicLong loginPluginMessagesBytes = new AtomicLong();
private final AtomicInteger loginPluginMessagesCount = new AtomicInteger();
private volatile boolean loginPluginMessagesOverflowed;
private final VelocityServer server; private final VelocityServer server;
private @Nullable TabCompleteRequestPacket outstandingTabComplete; private @Nullable TabCompleteRequestPacket outstandingTabComplete;
private final ChatHandler<? extends MinecraftPacket> chatHandler; private final ChatHandler<? extends MinecraftPacket> chatHandler;
@@ -178,9 +191,38 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void deactivated() { public void deactivated() {
player.discardChatQueue(); player.discardChatQueue();
for (PluginMessagePacket message : loginPluginMessages) { PluginMessagePacket message;
while ((message = loginPluginMessages.poll()) != null) {
ReferenceCountUtil.release(message); ReferenceCountUtil.release(message);
} }
loginPluginMessagesBytes.set(0);
loginPluginMessagesCount.set(0);
}
/**
* Adds a retained plugin message to the queue used while the FML/login phases are still in
* progress, enforcing the per-connection byte and count caps. Returns {@code true} if queued,
* {@code false} if the packet was released (and the player disconnected on overflow).
*/
private boolean enqueueLoginPluginMessage(PluginMessagePacket packet) {
if (loginPluginMessagesOverflowed) {
ReferenceCountUtil.release(packet);
return false;
}
int packetSize = packet.content().readableBytes();
long newBytes = loginPluginMessagesBytes.addAndGet(packetSize);
int newCount = loginPluginMessagesCount.incrementAndGet();
if (newBytes > MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES
|| newCount > MAX_QUEUED_LOGIN_PLUGIN_MESSAGES) {
loginPluginMessagesOverflowed = true;
ReferenceCountUtil.release(packet);
logger.warn("Disconnecting {}: pre-join plugin-message queue exceeded its limits "
+ "({} messages, {} bytes).", player, newCount, newBytes);
player.disconnect(Component.translatable("velocity.error.plugin-message-overflow"));
return false;
}
loginPluginMessages.add(packet);
return true;
} }
@Override @Override
@@ -349,25 +391,43 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
if (!player.getPhase().handle(player, packet, serverConn)) { if (!player.getPhase().handle(player, packet, serverConn)) {
byte[] copy = ByteBufUtil.getBytes(packet.content()); ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
String channel = packet.getChannel(); if (id == null) {
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy); // We don't have any plugins listening on this channel, process the packet now.
server.getEventManager().fire(event).thenAcceptAsync(pme -> { if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
if (pme.getResult().isAllowed()) { .consideredComplete()) {
PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(), // The client is trying to send messages too early. This is primarily caused by mods,
Unpooled.wrappedBuffer(copy)); // but further aggravated by Velocity. To work around these issues, we will queue any
if (!player.getPhase().consideredComplete() || !serverConn.getPhase() // non-FML handshake messages to be sent once the FML handshake has completed or the
.consideredComplete()) { // JoinGame packet has been received by the proxy, whichever comes first.
// We're still processing the connection (see above), enqueue the packet for now. //
loginPluginMessages.add(message.retain()); // We also need to make sure to retain these packets, so they can be flushed
} else { // appropriately.
backendConn.write(message); enqueueLoginPluginMessage(packet.retain());
} } else {
// The connection is ready, send the packet now.
backendConn.write(packet.retain());
} }
}, backendConn.eventLoop()).exceptionally((ex) -> { } else {
logger.error("Exception while handling plugin message packet for {}", player, ex); byte[] copy = ByteBufUtil.getBytes(packet.content());
return null; PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy);
}); server.getEventManager().fire(event).thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed()) {
PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(),
Unpooled.wrappedBuffer(copy));
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
.consideredComplete()) {
// We're still processing the connection (see above), enqueue the packet for now.
enqueueLoginPluginMessage(message.retain());
} else {
backendConn.write(message);
}
}
}, backendConn.eventLoop()).exceptionally((ex) -> {
logger.error("Exception while handling plugin message packet for {}", player, ex);
return null;
});
}
} }
} }
} }
@@ -621,6 +681,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
while ((pm = loginPluginMessages.poll()) != null) { while ((pm = loginPluginMessages.poll()) != null) {
serverMc.delayedWrite(pm); serverMc.delayedWrite(pm);
} }
loginPluginMessagesBytes.set(0);
loginPluginMessagesCount.set(0);
// Clear any title from the previous server. // Clear any title from the previous server.
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) { if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
@@ -853,6 +915,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
while ((pm = loginPluginMessages.poll()) != null) { while ((pm = loginPluginMessages.poll()) != null) {
connection.write(pm); connection.write(pm);
} }
loginPluginMessagesBytes.set(0);
loginPluginMessagesCount.set(0);
} }
} }
} }
@@ -84,7 +84,6 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket; import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket;
import com.velocitypowered.proxy.protocol.packet.TransferPacket; import com.velocitypowered.proxy.protocol.packet.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue;
import com.velocitypowered.proxy.protocol.packet.chat.ChatType;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory; import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory;
@@ -111,6 +110,7 @@ import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@@ -118,13 +118,10 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.permission.PermissionChecker; import net.kyori.adventure.permission.PermissionChecker;
import net.kyori.adventure.platform.facet.FacetPointers;
import net.kyori.adventure.platform.facet.FacetPointers.Type;
import net.kyori.adventure.pointer.Pointers; import net.kyori.adventure.pointer.Pointers;
import net.kyori.adventure.pointer.PointersSupplier; import net.kyori.adventure.pointer.PointersSupplier;
import net.kyori.adventure.resource.ResourcePackInfoLike; import net.kyori.adventure.resource.ResourcePackInfoLike;
@@ -166,7 +163,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
.resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername())) .resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername()))
.resolving(Identity.LOCALE, Player::getEffectiveLocale) .resolving(Identity.LOCALE, Player::getEffectiveLocale)
.resolving(PermissionChecker.POINTER, Player::getPermissionChecker) .resolving(PermissionChecker.POINTER, Player::getPermissionChecker)
.resolving(FacetPointers.TYPE, player -> Type.PLAYER)
.build(); .build();
/** /**
@@ -424,29 +420,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
@Override @Override
public void sendMessage(@NonNull Identity identity, @NonNull Component message) { public void sendMessage(final @NonNull Component message) {
Preconditions.checkNotNull(message, "message");
final Component translated = translateMessage(message); final Component translated = translateMessage(message);
connection.write(getChatBuilderFactory().builder() connection.write(getChatBuilderFactory().builder()
.component(translated).forIdentity(identity).toClient()); .component(translated).toClient());
} }
@Override @Override
public void sendMessage(@NonNull Identity identity, @NonNull Component message, public void sendActionBar(@NonNull Component message) {
@NonNull MessageType type) {
Preconditions.checkNotNull(message, "message");
Preconditions.checkNotNull(type, "type");
Component translated = translateMessage(message);
connection.write(getChatBuilderFactory().builder()
.component(translated).forIdentity(identity)
.setType(type == MessageType.CHAT ? ChatType.CHAT : ChatType.SYSTEM)
.toClient());
}
@Override
public void sendActionBar(net.kyori.adventure.text.@NonNull Component message) {
Component translated = translateMessage(message); Component translated = translateMessage(message);
ProtocolVersion playerVersion = getProtocolVersion(); ProtocolVersion playerVersion = getProtocolVersion();
@@ -645,7 +628,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
@Override @Override
public void disconnect(Component reason) { public void disconnect(@NotNull Component reason) {
Objects.requireNonNull(reason, "reason");
if (connection.eventLoop().inEventLoop()) { if (connection.eventLoop().inEventLoop()) {
disconnect0(reason, false); disconnect0(reason, false);
} else { } else {
@@ -65,8 +65,7 @@ public class InitialConnectSessionHandler implements MinecraftSessionHandler {
} }
byte[] copy = ByteBufUtil.getBytes(packet.content()); byte[] copy = ByteBufUtil.getBytes(packet.content());
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy);
copy);
server.getEventManager().fire(event) server.getEventManager().fire(event)
.thenAcceptAsync(pme -> { .thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed() && serverConn.isActive()) { if (pme.getResult().isAllowed() && serverConn.isActive()) {
@@ -50,9 +50,9 @@ import java.net.http.HttpResponse;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@@ -65,6 +65,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public class InitialLoginSessionHandler implements MinecraftSessionHandler { public class InitialLoginSessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(InitialLoginSessionHandler.class); private static final Logger logger = LogManager.getLogger(InitialLoginSessionHandler.class);
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final String MOJANG_HASJOINED_URL = private static final String MOJANG_HASJOINED_URL =
System.getProperty("mojang.sessionserver", System.getProperty("mojang.sessionserver",
"https://sessionserver.mojang.com/session/minecraft/hasJoined") "https://sessionserver.mojang.com/session/minecraft/hasJoined")
@@ -199,8 +200,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} }
byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret()); byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
// is enabled.
mcConnection.enableEncryption(decryptedSharedSecret);
String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString(); String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString();
String url = String.format(MOJANG_HASJOINED_URL, String url = String.format(MOJANG_HASJOINED_URL,
urlFormParameterEscaper().escape(login.getUsername()), serverId); urlFormParameterEscaper().escape(login.getUsername()), serverId);
@@ -229,18 +234,6 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
return; return;
} }
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
// is enabled.
try {
mcConnection.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption for connection", e);
// At this point, the connection is encrypted, but something's wrong on our side and
// we can't do anything about it.
mcConnection.close(true);
return;
}
if (response.statusCode() == 200) { if (response.statusCode() == 200) {
final GameProfile profile = GENERAL_GSON.fromJson(response.body(), final GameProfile profile = GENERAL_GSON.fromJson(response.body(),
GameProfile.class); GameProfile.class);
@@ -268,14 +261,8 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down")); inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
} }
}, mcConnection.eventLoop()) }, mcConnection.eventLoop())
.thenRun(() -> { .whenComplete((ignored, throwable) -> {
try { httpClient.close();
httpClient.close();
} catch (Exception e) {
// In Java 21, the HttpClient does not throw any Exception
// when trying to clean its resources, so this should not happen
logger.error("An unknown error occurred while trying to close an HttpClient", e);
}
}); });
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption", e); logger.error("Unable to enable encryption", e);
@@ -286,7 +273,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
private EncryptionRequestPacket generateEncryptionRequest() { private EncryptionRequestPacket generateEncryptionRequest() {
byte[] verify = new byte[4]; byte[] verify = new byte[4];
ThreadLocalRandom.current().nextBytes(verify); SECURE_RANDOM.nextBytes(verify);
EncryptionRequestPacket request = new EncryptionRequestPacket(); EncryptionRequestPacket request = new EncryptionRequestPacket();
request.setPublicKey(server.getServerKeyPair().getPublic().getEncoded()); request.setPublicKey(server.getServerKeyPair().getPublic().getEncoded());
@@ -57,16 +57,16 @@ public class ServerListPingHandler {
List<ServerPing.SamplePlayer> samplePlayers; List<ServerPing.SamplePlayer> samplePlayers;
if (configuration.getSamplePlayersInPing()) { if (configuration.getSamplePlayersInPing()) {
List<ServerPing.SamplePlayer> unshuffledPlayers = server.getAllPlayers().stream() List<ServerPing.SamplePlayer> unshuffledPlayers = server.getAllPlayers().stream()
.map(p -> { .map(p -> {
if (p.getPlayerSettings().isClientListingAllowed()) { if (p.getPlayerSettings().isClientListingAllowed()) {
return new ServerPing.SamplePlayer(p.getUsername(), p.getUniqueId()); return new ServerPing.SamplePlayer(p.getUsername(), p.getUniqueId());
} else { } else {
return ServerPing.SamplePlayer.ANONYMOUS; return ServerPing.SamplePlayer.ANONYMOUS;
} }
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
Collections.shuffle(unshuffledPlayers); Collections.shuffle(unshuffledPlayers);
samplePlayers = unshuffledPlayers.subList(0, Math.min(12, server.getPlayerCount())); samplePlayers = unshuffledPlayers.subList(0, Math.min(12, unshuffledPlayers.size()));
} else { } else {
samplePlayers = ImmutableList.of(); samplePlayers = ImmutableList.of();
} }
@@ -25,13 +25,11 @@ import com.velocitypowered.api.permission.Tristate;
import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.ConsoleCommandSource;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.permission.PermissionChecker; import net.kyori.adventure.permission.PermissionChecker;
import net.kyori.adventure.platform.facet.FacetPointers;
import net.kyori.adventure.platform.facet.FacetPointers.Type;
import net.kyori.adventure.pointer.Pointers; import net.kyori.adventure.pointer.Pointers;
import net.kyori.adventure.pointer.PointersSupplier; import net.kyori.adventure.pointer.PointersSupplier;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@@ -64,7 +62,6 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
.resolving(PermissionChecker.POINTER, VelocityConsole::getPermissionChecker) .resolving(PermissionChecker.POINTER, VelocityConsole::getPermissionChecker)
.resolving(Identity.LOCALE, (console) -> ClosestLocaleMatcher.INSTANCE .resolving(Identity.LOCALE, (console) -> ClosestLocaleMatcher.INSTANCE
.lookupClosest(Locale.getDefault())) .lookupClosest(Locale.getDefault()))
.resolving(FacetPointers.TYPE, (console) -> Type.CONSOLE)
.build(); .build();
public VelocityConsole(VelocityServer server) { public VelocityConsole(VelocityServer server) {
@@ -72,8 +69,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
} }
@Override @Override
public void sendMessage(@NonNull Identity identity, @NonNull Component message, public void sendMessage(@NonNull Component message) {
@NonNull MessageType messageType) {
componentLogger.info(message); componentLogger.info(message);
} }
@@ -111,6 +107,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
protected LineReader buildReader(LineReaderBuilder builder) { protected LineReader buildReader(LineReaderBuilder builder) {
return super.buildReader(builder return super.buildReader(builder
.appName("Velocity") .appName("Velocity")
.variable(LineReader.HISTORY_FILE, Path.of(".console_history"))
.completer((reader, parsedLine, list) -> { .completer((reader, parsedLine, list) -> {
try { try {
List<String> offers = this.server.getCommandManager() List<String> offers = this.server.getCommandManager()
@@ -80,7 +80,7 @@ public class ServerChannelInitializer extends ChannelInitializer<Channel> {
int configuredPacketsPerSecond = packetLimiterConfig.pps(); int configuredPacketsPerSecond = packetLimiterConfig.pps();
int configuredBytes = packetLimiterConfig.bytes(); int configuredBytes = packetLimiterConfig.bytes();
if (configuredInterval > 0 && (configuredBytes > 0 || configuredPacketsPerSecond > 0)) { if (configuredInterval > 0 && (configuredBytes > 0 || configuredPacketsPerSecond > 0)) {
ch.pipeline().get(MinecraftVarintFrameDecoder.class).setPacketLimiter( ch.pipeline().get(MinecraftVarintFrameDecoder.class).setPacketLimiter(
new SimpleBytesPerSecondLimiter(configuredPacketsPerSecond, configuredBytes, configuredInterval) new SimpleBytesPerSecondLimiter(configuredPacketsPerSecond, configuredBytes, configuredInterval)
); );
@@ -32,17 +32,20 @@ import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* An implementation of {@code InetNameResolver} that performs blocking DNS name lookups * An implementation of {@code InetNameResolver} that performs blocking DNS name lookups
* in a separate thread, avoiding blocking the Netty threads for an extended period of time * on a small bounded pool of separate threads, avoiding blocking the Netty threads for an
* and without the downsides of Netty's native DNS resolver. * extended period of time and without the downsides of Netty's native DNS resolver.
*/ */
public final class SeparatePoolInetNameResolver extends InetNameResolver { public final class SeparatePoolInetNameResolver extends InetNameResolver {
private static final int MAX_RESOLVE_THREADS = 8;
private final ExecutorService resolveExecutor; private final ExecutorService resolveExecutor;
private final InetNameResolver delegate; private final InetNameResolver delegate;
private final Cache<String, List<InetAddress>> cache; private final Cache<String, List<InetAddress>> cache;
@@ -56,11 +59,15 @@ public final class SeparatePoolInetNameResolver extends InetNameResolver {
*/ */
public SeparatePoolInetNameResolver(EventExecutor executor) { public SeparatePoolInetNameResolver(EventExecutor executor) {
super(executor); super(executor);
this.resolveExecutor = Executors.newSingleThreadExecutor( ThreadPoolExecutor resolveExecutor = new ThreadPoolExecutor(
MAX_RESOLVE_THREADS, MAX_RESOLVE_THREADS,
60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder() new ThreadFactoryBuilder()
.setNameFormat("Velocity DNS Resolver") .setNameFormat("Velocity DNS Resolver #%d")
.setDaemon(true) .setDaemon(true)
.build()); .build());
resolveExecutor.allowCoreThreadTimeOut(true);
this.resolveExecutor = resolveExecutor;
this.delegate = new DefaultNameResolver(executor); this.delegate = new DefaultNameResolver(executor);
this.cache = Caffeine.newBuilder() this.cache = Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS) .expireAfterWrite(30, TimeUnit.SECONDS)
@@ -60,7 +60,6 @@ public enum ProtocolUtils {
private static final GsonComponentSerializer PRE_1_16_SERIALIZER = private static final GsonComponentSerializer PRE_1_16_SERIALIZER =
GsonComponentSerializer.builder() GsonComponentSerializer.builder()
.downsampleColors()
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options( .options(
OptionSchema.globalSchema().stateBuilder() OptionSchema.globalSchema().stateBuilder()
@@ -22,11 +22,14 @@ import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame; import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame;
import com.velocitypowered.natives.compression.VelocityCompressor; import com.velocitypowered.natives.compression.VelocityCompressor;
import com.velocitypowered.proxy.network.limiter.PacketLimiter;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.util.except.QuietDecoderException;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder; import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List; import java.util.List;
import org.jspecify.annotations.Nullable;
/** /**
* Decompresses a Minecraft packet. * Decompresses a Minecraft packet.
@@ -44,11 +47,12 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
Boolean.getBoolean("velocity.increased-compression-cap") Boolean.getBoolean("velocity.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : SERVERBOUND_MAXIMUM_UNCOMPRESSED_SIZE; ? HARD_MAXIMUM_UNCOMPRESSED_SIZE : SERVERBOUND_MAXIMUM_UNCOMPRESSED_SIZE;
private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation"); private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation");
private static final double MAX_COMPRESSION_RATIO = Double.parseDouble(System.getProperty("velocity.max-compression-ratio", "64"));
private final ProtocolUtils.Direction direction; private final ProtocolUtils.Direction direction;
private int threshold; private int threshold;
private final VelocityCompressor compressor; private final VelocityCompressor compressor;
@Nullable
private PacketLimiter packetLimiter;
/** /**
* Creates a new {@code MinecraftCompressDecoder} with the specified compression {@code threshold}. * Creates a new {@code MinecraftCompressDecoder} with the specified compression {@code threshold}.
@@ -73,10 +77,13 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
+ " threshold %s", actualUncompressedSize, threshold); + " threshold %s", actualUncompressedSize, threshold);
} }
// This message is not compressed. // This message is not compressed.
if (packetLimiter != null && !packetLimiter.account(in.readableBytes())) {
throw new QuietDecoderException("Rate limit exceeded while processing packets for %s"
.formatted(ctx.channel().remoteAddress()));
}
out.add(in.retain()); out.add(in.retain());
return; return;
} }
int length = in.readableBytes();
checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than" checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than"
+ " threshold %s", claimedUncompressedSize, threshold); + " threshold %s", claimedUncompressedSize, threshold);
@@ -88,10 +95,6 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP, checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP,
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize, "Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
SERVERBOUND_UNCOMPRESSED_CAP); SERVERBOUND_UNCOMPRESSED_CAP);
double maxCompressedAllowed = length * MAX_COMPRESSION_RATIO;
checkFrame(claimedUncompressedSize <= maxCompressedAllowed,
"Uncompressed size %s exceeds ratio threshold of %s for compressed sized %s", claimedUncompressedSize,
maxCompressedAllowed, length);
} }
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in); ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize); ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
@@ -99,6 +102,10 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize); compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
checkFrame(uncompressed.writerIndex() == claimedUncompressedSize, checkFrame(uncompressed.writerIndex() == claimedUncompressedSize,
"Decompressed size %s does not match claimed uncompressed size %s", uncompressed.writerIndex(), claimedUncompressedSize); "Decompressed size %s does not match claimed uncompressed size %s", uncompressed.writerIndex(), claimedUncompressedSize);
if (packetLimiter != null && !packetLimiter.account(claimedUncompressedSize)) {
throw new QuietDecoderException("Rate limit exceeded while processing packets for %s"
.formatted(ctx.channel().remoteAddress()));
}
out.add(uncompressed); out.add(uncompressed);
} catch (Exception e) { } catch (Exception e) {
uncompressed.release(); uncompressed.release();
@@ -116,4 +123,8 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
public void setThreshold(int threshold) { public void setThreshold(int threshold) {
this.threshold = threshold; this.threshold = threshold;
} }
public void setPacketLimiter(@Nullable PacketLimiter packetLimiter) {
this.packetLimiter = packetLimiter;
}
} }
@@ -104,7 +104,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
throw handleOverflow(packet, expectedMaxLen, buf.readableBytes()); throw handleOverflow(packet, expectedMaxLen, buf.readableBytes());
} }
if (buf.readableBytes() < expectedMinLen) { if (buf.readableBytes() < expectedMinLen) {
throw handleUnderflow(packet, expectedMaxLen, buf.readableBytes()); throw handleUnderflow(packet, expectedMinLen, buf.readableBytes());
} }
} }
@@ -159,10 +159,10 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
int expectedMinLen = packet.decodeExpectedMinLength(in, direction, registry.version); int expectedMinLen = packet.decodeExpectedMinLength(in, direction, registry.version);
int expectedMaxLen = packet.decodeExpectedMaxLength(in, direction, registry.version); int expectedMaxLen = packet.decodeExpectedMaxLength(in, direction, registry.version);
if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) { if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) {
throw handleOverflow(packet, expectedMaxLen, in.readableBytes()); throw handleOverflow(packet, expectedMaxLen, payloadLength);
} }
if (payloadLength < expectedMinLen) { if (payloadLength < expectedMinLen) {
throw handleUnderflow(packet, expectedMaxLen, in.readableBytes()); throw handleUnderflow(packet, expectedMinLen, payloadLength);
} }
in.readerIndex(index); in.readerIndex(index);
@@ -52,6 +52,7 @@ public class JoinGamePacket implements MinecraftPacket {
private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+ private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+
private int portalCooldown; // 1.20+ private int portalCooldown; // 1.20+
private int seaLevel; // 1.21.2+ private int seaLevel; // 1.21.2+
private boolean onlineMode; // 26.2+
private boolean enforcesSecureChat; // 1.20.5+ private boolean enforcesSecureChat; // 1.20.5+
public int getEntityId() { public int getEntityId() {
@@ -190,6 +191,10 @@ public class JoinGamePacket implements MinecraftPacket {
this.seaLevel = seaLevel; this.seaLevel = seaLevel;
} }
public void setOnlineMode(boolean onlineMode) {
this.onlineMode = onlineMode;
}
public boolean getEnforcesSecureChat() { public boolean getEnforcesSecureChat() {
return this.enforcesSecureChat; return this.enforcesSecureChat;
} }
@@ -213,7 +218,7 @@ public class JoinGamePacket implements MinecraftPacket {
dimensionInfo + '\'' + ", currentDimensionData='" + currentDimensionData + '\'' + dimensionInfo + '\'' + ", currentDimensionData='" + currentDimensionData + '\'' +
", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance + ", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance +
", lastDeathPosition='" + lastDeathPosition + '\'' + ", portalCooldown=" + portalCooldown + ", lastDeathPosition='" + lastDeathPosition + '\'' + ", portalCooldown=" + portalCooldown +
", seaLevel=" + seaLevel + ", seaLevel=" + seaLevel + ", onlineMode=" + this.onlineMode +
'}'; '}';
} }
@@ -358,6 +363,10 @@ public class JoinGamePacket implements MinecraftPacket {
this.seaLevel = ProtocolUtils.readVarInt(buf); this.seaLevel = ProtocolUtils.readVarInt(buf);
} }
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
this.onlineMode = buf.readBoolean();
}
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) {
this.enforcesSecureChat = buf.readBoolean(); this.enforcesSecureChat = buf.readBoolean();
} }
@@ -510,6 +519,10 @@ public class JoinGamePacket implements MinecraftPacket {
ProtocolUtils.writeVarInt(buf, seaLevel); ProtocolUtils.writeVarInt(buf, seaLevel);
} }
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
buf.writeBoolean(this.onlineMode);
}
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) {
buf.writeBoolean(this.enforcesSecureChat); buf.writeBoolean(this.enforcesSecureChat);
} }
@@ -35,6 +35,7 @@ public class ServerLoginSuccessPacket implements MinecraftPacket {
private @Nullable UUID uuid; private @Nullable UUID uuid;
private @Nullable String username; private @Nullable String username;
private @Nullable List<GameProfile.Property> properties; private @Nullable List<GameProfile.Property> properties;
private @Nullable UUID sessionId;
private static final boolean strictErrorHandling = VelocityProperties private static final boolean strictErrorHandling = VelocityProperties
.readBoolean("velocity.strictErrorHandling", true); .readBoolean("velocity.strictErrorHandling", true);
@@ -68,6 +69,10 @@ public class ServerLoginSuccessPacket implements MinecraftPacket {
this.properties = properties; this.properties = properties;
} }
public void setSessionId(@Nullable UUID sessionId) {
this.sessionId = sessionId;
}
@Override @Override
public String toString() { public String toString() {
return "ServerLoginSuccess{" return "ServerLoginSuccess{"
@@ -96,6 +101,10 @@ public class ServerLoginSuccessPacket implements MinecraftPacket {
if (version == ProtocolVersion.MINECRAFT_1_20_5 || version == ProtocolVersion.MINECRAFT_1_21) { if (version == ProtocolVersion.MINECRAFT_1_20_5 || version == ProtocolVersion.MINECRAFT_1_21) {
buf.readBoolean(); buf.readBoolean();
} }
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
this.sessionId = ProtocolUtils.readUuid(buf);
}
} }
@Override @Override
@@ -127,6 +136,10 @@ public class ServerLoginSuccessPacket implements MinecraftPacket {
if (version == ProtocolVersion.MINECRAFT_1_20_5 || version == ProtocolVersion.MINECRAFT_1_21) { if (version == ProtocolVersion.MINECRAFT_1_20_5 || version == ProtocolVersion.MINECRAFT_1_21) {
buf.writeBoolean(strictErrorHandling); buf.writeBoolean(strictErrorHandling);
} }
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
ProtocolUtils.writeUuid(buf, this.sessionId);
}
} }
@Override @Override
@@ -31,8 +31,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
public class TabCompleteRequestPacket implements MinecraftPacket { public class TabCompleteRequestPacket implements MinecraftPacket {
private static final int VANILLA_MAX_TAB_COMPLETE_LEN = 2048;
private @Nullable String command; private @Nullable String command;
private int transactionId; private int transactionId;
private boolean assumeCommand; private boolean assumeCommand;
@@ -97,9 +95,11 @@ public class TabCompleteRequestPacket implements MinecraftPacket {
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
if (version.noLessThan(MINECRAFT_1_13)) { if (version.noLessThan(MINECRAFT_1_13)) {
this.transactionId = ProtocolUtils.readVarInt(buf); this.transactionId = ProtocolUtils.readVarInt(buf);
this.command = ProtocolUtils.readString(buf, VANILLA_MAX_TAB_COMPLETE_LEN);
// 1.13 only supports a max length of 256: https://bugs.mojang.com/browse/MC/issues/MC-132663
this.command = ProtocolUtils.readString(buf, version.equals(MINECRAFT_1_13) ? 256 : 32500);
} else { } else {
this.command = ProtocolUtils.readString(buf, VANILLA_MAX_TAB_COMPLETE_LEN); this.command = ProtocolUtils.readString(buf, 32767);
if (version.noLessThan(MINECRAFT_1_9)) { if (version.noLessThan(MINECRAFT_1_9)) {
this.assumeCommand = buf.readBoolean(); this.assumeCommand = buf.readBoolean();
} }
@@ -24,6 +24,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_3;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_2;
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id; import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id;
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet; import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet;
import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE; import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE;
@@ -207,7 +208,7 @@ public class ArgumentPropertyRegistry {
empty(id("minecraft:block_predicate", mapSet(MINECRAFT_1_19, 13))); empty(id("minecraft:block_predicate", mapSet(MINECRAFT_1_19, 13)));
empty(id("minecraft:item_stack", mapSet(MINECRAFT_1_19, 14))); empty(id("minecraft:item_stack", mapSet(MINECRAFT_1_19, 14)));
empty(id("minecraft:item_predicate", mapSet(MINECRAFT_1_19, 15))); empty(id("minecraft:item_predicate", mapSet(MINECRAFT_1_19, 15)));
empty(id("minecraft:color", mapSet(MINECRAFT_1_19, 16))); empty(id("minecraft:color", mapSet(MINECRAFT_26_2, -1), mapSet(MINECRAFT_1_19, 16))); // renamed to team_color in 26.2
empty(id("minecraft:component", mapSet(MINECRAFT_1_21_6, 18), mapSet(MINECRAFT_1_19, 17))); empty(id("minecraft:component", mapSet(MINECRAFT_1_21_6, 18), mapSet(MINECRAFT_1_19, 17)));
empty(id("minecraft:style", mapSet(MINECRAFT_1_21_6, 19), mapSet(MINECRAFT_1_20_3, 18))); // added 1.20.3 empty(id("minecraft:style", mapSet(MINECRAFT_1_21_6, 19), mapSet(MINECRAFT_1_20_3, 18))); // added 1.20.3
empty(id("minecraft:message", mapSet(MINECRAFT_1_21_6, 20), mapSet(MINECRAFT_1_20_3, 19), mapSet(MINECRAFT_1_19, 18))); empty(id("minecraft:message", mapSet(MINECRAFT_1_21_6, 20), mapSet(MINECRAFT_1_20_3, 19), mapSet(MINECRAFT_1_19, 18)));
@@ -281,6 +282,7 @@ public class ArgumentPropertyRegistry {
empty(id("minecraft:hex_color", mapSet(MINECRAFT_1_21_6, 17))); // added in 1.21.6 empty(id("minecraft:hex_color", mapSet(MINECRAFT_1_21_6, 17))); // added in 1.21.6
empty(id("minecraft:dialog", mapSet(MINECRAFT_1_21_6, 55))); // added in 1.21.6 empty(id("minecraft:dialog", mapSet(MINECRAFT_1_21_6, 55))); // added in 1.21.6
empty(id("minecraft:team_color", mapSet(MINECRAFT_26_2, 16))); // renamed from color in 26.2
// Crossstitch support // Crossstitch support
register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD); register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD);
@@ -28,7 +28,7 @@ public class TitleActionbarPacket extends GenericTitlePacket {
private ComponentHolder component; private ComponentHolder component;
public TitleActionbarPacket() { public TitleActionbarPacket() {
setAction(ActionType.SET_TITLE); setAction(ActionType.SET_ACTION_BAR);
} }
@Override @Override
@@ -31,6 +31,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
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;
@@ -95,7 +96,7 @@ public class VelocityTabListLegacy extends KeyedVelocityTabList {
entry.setLatencyInternal(item.getLatency()); entry.setLatencyInternal(item.getLatency());
} }
} else { } else {
UUID uuid = UUID.randomUUID(); // Use a fake uuid to preserve function of custom entries UUID uuid = generateInsecureRandomUuid(); // Use a fake uuid to preserve function of custom entries
nameMapping.put(item.getName(), uuid); nameMapping.put(item.getName(), uuid);
entries.put(uuid, (KeyedVelocityTabListEntry) TabListEntry.builder() entries.put(uuid, (KeyedVelocityTabListEntry) TabListEntry.builder()
.tabList(this) .tabList(this)
@@ -153,4 +154,17 @@ public class VelocityTabListLegacy extends KeyedVelocityTabList {
int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat) { int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat) {
return new VelocityTabListEntryLegacy(this, profile, displayName, latency, gameMode); return new VelocityTabListEntryLegacy(this, profile, displayName, latency, gameMode);
} }
/**
* Generates a random UUID v4 using {@link ThreadLocalRandom}. The result is a structurally valid
* UUID v4 but is not cryptographically secure
*
* @return a new random {@link UUID}
*/
private static UUID generateInsecureRandomUuid() {
ThreadLocalRandom random = ThreadLocalRandom.current();
long msb = (random.nextLong() & 0xffffffffffff0fffL) | 0x0000000000004000L; // version 4
long lsb = (random.nextLong() & 0x3fffffffffffffffL) | 0x8000000000000000L; // IETF variant
return new UUID(msb, lsb);
}
} }
@@ -25,6 +25,7 @@ velocity.error.internal-server-connection-error=An internal server connection er
velocity.error.logging-in-too-fast=You are logging in too fast, try again later. velocity.error.logging-in-too-fast=You are logging in too fast, try again later.
velocity.error.online-mode-only=You are not logged into your Minecraft account. If you are logged into your Minecraft account, try restarting your Minecraft client. velocity.error.online-mode-only=You are not logged into your Minecraft account. If you are logged into your Minecraft account, try restarting your Minecraft client.
velocity.error.player-connection-error=An internal error occurred in your connection. velocity.error.player-connection-error=An internal error occurred in your connection.
velocity.error.plugin-message-overflow=You sent too many plugin messages before completing the connection.
velocity.error.modern-forwarding-needs-new-client=This server is only compatible with Minecraft 1.13 and above. velocity.error.modern-forwarding-needs-new-client=This server is only compatible with Minecraft 1.13 and above.
velocity.error.modern-forwarding-failed=Your server did not send a forwarding request to the proxy. Make sure the server is configured for Velocity forwarding. velocity.error.modern-forwarding-failed=Your server did not send a forwarding request to the proxy. Make sure the server is configured for Velocity forwarding.
velocity.error.moved-to-new-server=You were kicked from <arg:0>: <arg:1> velocity.error.moved-to-new-server=You were kicked from <arg:0>: <arg:1>
+10 -2
View File
@@ -1,5 +1,5 @@
# Config version. Do not change this # Config version. Do not change this
config-version = "2.7" config-version = "2.8"
# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565. # What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565.
bind = "0.0.0.0:25565" bind = "0.0.0.0:25565"
@@ -75,9 +75,17 @@ sample-players-in-ping = false
enable-player-address-logging = true enable-player-address-logging = true
[packet-limiter] [packet-limiter]
# Size of the moving time window in seconds used to calculate average rates.
# A larger window tolerates short bursts while still enforcing the configured limits over time.
interval = 7 interval = 7
packets-per-second = 500 # Maximum average number of packets per second a client may send. -1 disables this check.
packets-per-second = -1
# Maximum average number of compressed (on-wire) bytes per second a client may send. -1 disables this check.
bytes-per-second = -1 bytes-per-second = -1
# Maximum average number of decompressed bytes per second a client may send.
# Protects against compression bomb attacks where small packets expand to excessive sizes after decompression.
# -1 disables this check.
decompressed-bytes-per-second = 5242880
[servers] [servers]
# Configure your servers here. Each key represents the server's name, and the value # Configure your servers here. Each key represents the server's name, and the value