Compare commits

...
58 Commits
Author SHA1 Message Date
yoyosource 7a39ebb058 Fix StateRegistry and update to dev/4.0.0
SteamWarCI Build successful
2026-08-24 17:14:49 +02:00
yoyosource 57e9cd7f24 Merge remote-tracking branch 'upstream/dev/4.0.0'
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
2026-08-24 17:03:57 +02:00
Wouter Gritter 4772ca3022 Bump lmbda to 3.0.0, defining generated handlers with the proxy's lookup instead of the plugin's
Reapplies the lmbda 3.0.0 bump reverted in f918d0d6.
2026-08-14 12:53:49 +02:00
YoyoNow b9463125d1 Merge pull request 'Update26' (#3) from update26 into master
SteamWarCI Build successful
Reviewed-on: #3
2026-08-13 09:23:04 +02:00
Shane Freeder f918d0d649 Downgrade lmbda back to 2.0.0
lmbda 3.x moved to using hidden classes for generation which does
not work for cross classloader operations which are somewhat typical
for plugins.
2026-08-12 23:39:19 +01:00
Wouter GritterandGitHub e6fbcc9196 Various dependency bumps (#1860)
* Bump fastutil to 8.5.19 and remove exclusions

* Various dependency bumps
2026-08-12 18:32:24 +01:00
Radmir NoirusovandGitHub 14a69904f9 fix: retain reference-counted packets forwarded via handleGeneric (#1856)
During configuration, a ServerboundCustomClickActionPacket arriving
when connectionInFlight is null falls through to handleGeneric, which
writes it to the connected backend without retaining. The encoder
releases the packet, then MinecraftConnection.channelRead's finally
block releases again - double-free.

Two fixes:
- handle() now uses getConnectionInFlightOrConnectedServer() so the
  packet is properly retained before being written
- handleGeneric() retains any ByteBufHolder packet before write, not
  just PluginMessagePacket

Closes #1841
2026-08-12 18:29:30 +01:00
Jason PenillaandGitHub 71c50a75eb Export JSpecify annotations at runtime (#1861)
JSpecify annotations have runtime retention, so expose them through the API variant as recommended by JSpecify.
2026-08-12 18:25:15 +01:00
Shane Freeder 00759e5279 Revert "Fix dimension reading for some mods that add extra dimensions in 1.7.10 (#1734)"
This reverts commit 2676520c6a.
2026-08-03 14:09:05 +01:00
Phillipp W.andGitHub 06ade4775e fix: forward player loaded packet to backend server (#1862)
Return false after handling ServerboundPlayerLoadedPacket so Velocity's normal forwarding path sends the packet to the backend server.
2026-08-03 12:22:58 +01:00
Wouter Gritterandxphorror 2676520c6a Fix dimension reading for some mods that add extra dimensions in 1.7.10 (#1734)
Rewrite the comment and ternary operation to be clearer

Co-authored-by: xphorror <87706197+xphorror@users.noreply.github.com>
2026-08-02 11:13:25 +02:00
e11584ba35 Player Loaded World API (#1541)
Co-authored-by: Emil <12966472+Emilxyz@users.noreply.github.com>
Co-authored-by: Wouter Gritter <wouter@gritter.nl>
2026-07-31 20:03:22 +01:00
Shane Freeder a08972749b Rebuild natives 2026-07-30 16:51:07 +01:00
c6e9ca989e Add provides API (#1853)
Add provides API

Co-authored-by: Shane Freeder <theboyetronic@gmail.com>
2026-07-29 12:51:34 +01:00
Shane Freeder d30f1d9a74 Compressor cleanups 2026-07-29 00:51:41 +01:00
EmilandGitHub b45716deff feat: Make version clickable in velocity info command (#1775) 2026-07-19 16:02:20 +02:00
SpigotRCEandGitHub e653647962 [ci skip] typo fix (#1778) 2026-07-19 16:02:12 +02:00
Shane Freeder 5aab0d1427 [ci skip] primative and functional
oh, my! This stuff is not ideal, but it's the only real protection here unless
we just hack the automation here
2026-07-15 04:28:22 +01:00
Shane Freeder 1cd8d51d02 [ci skip] primative failsafe for mismatched version family 2026-07-15 02:29:29 +01:00
Shane Freeder 9eb338bd1c Fix version family 2026-07-15 02:22:41 +01:00
Andrew Steinborn 60380211f8 Defer ByteBufUtil.getBytes() in config stages until after we check for channel registry 2026-07-14 18:04:07 -04:00
Shane Freeder 3b239daf4c [ci skip] back to snapshots
Hello, Darkness, my old friend
2026-07-14 15:26:19 +01:00
Shane Freeder 90f89053a7 Release 4.0.0 2026-07-14 15:10:04 +01:00
Chaoscaot 67e16a308e Merge remote-tracking branch 'upstream/dev/4.0.0'
SteamWarCI Build successful
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java
2026-07-13 21:48:01 +02:00
Shane Freeder 4bd30bac33 Bump netty to 4.2.16.Final 2026-07-11 22:06:09 +01:00
BeanesandGitHub 28c9f5a356 Small optimization to prevent blocking netty threads on UUID.randomUUID() (#1781)
* Small optimization to prevent blocking netty threads on UUID.randomUUID()

* Change FastRandomUuid to be a valid uuid v4

* Update javadoc for spotless

* Migrate method to VelocityTabListLegacy and add notice that it is insecure

* Bring back deleted override
2026-07-11 22:09:17 +02:00
Wouter GritterandGitHub da7427fb51 Set TitleActionbarPacket's default action to SET_ACTION_BAR (#1802) 2026-07-11 22:09:11 +02:00
Wouter GritterandGitHub b8d1f16b07 Setup encryption before possible disconnect (#1754) 2026-07-11 22:09:03 +02:00
Wouter GritterandGitHub 729a050662 Fix build-time GraalVmProcessor warning (#1801) 2026-07-11 22:08:55 +02:00
Wouter GritterandGitHub c690b4abfc Strip pre-java-9 version check in Metrics as Velocity targets 21+ (#1836)
* Strip pre-java-9 version check in `Metrics` as Velocity targets 21+

* Simplify Java version check by using `Runtime.version()` (entry still needs the system property to produce the same bStats metrics)

* Reconstruct `java.version` system property through `Runtime.Version`

* Update `javaVersion()` javadoc

* Newline while we're here
2026-07-11 22:08:47 +02:00
Wouter GritterandGitHub 5cc6b0b7e5 Use SecureRandom to generate verification/anti-MITM token (#1797) 2026-07-11 22:08:36 +02:00
Wouter GritterandGitHub 757711a5bc Use correct max tab complete lengths for < 1.13, = 1.13 and > 1.13 (#1796) 2026-07-11 22:08:30 +02:00
Shane Freeder 95694cba28 Update jline to 4.3.1 2026-07-11 20:17:21 +01:00
Wouter Gritter 7cb01b85f0 [ci skip] Bump API source & docs link to java 25 2026-07-11 20:54:54 +02:00
Shane Freeder 0942e162c0 bump version to 4.0.0-SNAPSHOT 2026-07-11 19:06:13 +01:00
Shane Freeder c1cd71a4bc Toolchain and gradle bump (Java 25+) 2026-07-11 19:05:05 +01:00
Shane Freeder 843a47e2a3 [ci skip] Merge branch 'rel/3.0.0' into dev/3.0.0 2026-07-11 19:00:03 +01:00
Shane Freeder 4498f1e03d Release 3.5.1
Bumps fill plugin to mitigate a deployment issue
2026-07-11 16:56:22 +01:00
Shane Freeder 1abb3ba56a Release 3.5.0 2026-07-11 11:55:53 +01:00
Shane Freeder a5680fc223 chore: Bump dependencies in alignment with paper 2026-07-11 02:21:15 +01:00
EmilandGitHub ffca982a9d chore: drop adventure-platform-facet (#1843) 2026-07-11 02:09:54 +01:00
Shane Freeder 2b5d964ab0 Reapply "Adventure 5 (#1774)"
This reverts commit 06eb052ab5.
2026-07-11 01:36:17 +01:00
Shane Freeder f11cc86e69 Welcome to the 3.6.0 series 2026-07-11 01:36:06 +01:00
Shane Freeder 06eb052ab5 Revert "Adventure 5 (#1774)"
This reverts commit 97b386d86f.
2026-07-11 01:33:41 +01:00
AdrianandGitHub 97b386d86f Adventure 5 (#1774)
* Adventure 5.2.0
2026-07-10 11:53:08 +01:00
Clement RaynaudandGitHub 81a5817a82 Resolve backend DNS on a bounded thread pool to avoid head-of-line blocking (#1834)
* Use a bounded thread pool for backend DNS resolution

* Queue DNS lookups instead of rejecting when the resolver pool is busy
2026-07-08 11:27:56 +02:00
Wouter GritterandGitHub 1edab1411d Store historical console commands in .console_history (enables command history between restarts) (#1835) 2026-07-05 16:10:22 +01:00
Shane Freeder a7581821fb Use a shared per-proxy session ID for 26.2 login metrics
The 26.2 login success session ID is purely a metrics identifier. Mint
one shared UUID per proxy, regenerated when the proxy empties, mirroring
the vanilla server, instead of a random UUID per connection.
2026-06-16 12:27:00 +01:00
AaronandGitHub d7ad0522e9 Minecraft 26.2 (#1807)
* 26.2-snapshot-2

* 26.2-snapshot-3

* 26.2-snapshot-4

* 26.2-pre-1

* 26.2-pre-3

* 26.2-pre-4

* chore: set online mode and session id

* fix: checkstyle

* 26.2-pre-5

* 26.2-pre-6

* 26.2-rc-1

* 26.2-rc-2

* unregister old color argument in 26.2

* 26.2
2026-06-16 12:20:59 +01:00
0cbe10e951 Bump netty from 4.2.10.Final to 4.2.15.Final (#1817)
* Bump netty from 4.2.10.Final to 4.2.14.Final

* Bump netty from 4.2.14.Final to 4.2.15.Final

Co-authored-by: Wouter Gritter <wouter@gritter.nl>

---------

Co-authored-by: Wouter Gritter <wouter@gritter.nl>
2026-06-15 15:49:05 +01:00
Wouter GritterandGitHub bcf1bba3a1 Add missing package-info.javas for event packages (#1822) 2026-06-15 15:48:08 +01:00
Wouter GritterandGitHub 3b892899db Require non-null reason (#1823) 2026-06-15 15:47:31 +01:00
Wouter GritterandGitHub 1b64a70a00 Fix underflow and overflow error messages (#1812) 2026-06-02 16:22:33 +02:00
GoldenEditandGitHub 3b142f3099 Fix inverted PluginMessageEvent source/target in InitialConnectSessionHandler (#1811) 2026-05-28 11:02:48 +01:00
Wouter GritterandGitHub 25fbd833cd Add decompressed-bytes-per-second rate limit, update packet limiter defaults (#1786)
* Add decompressed-bytes-per-second packet limiter, update defaults
* Revert "Add compression ratio limiter"
2026-05-24 16:59:34 +01:00
Wouter GritterandGitHub b72cf26802 Cap pre-join plugin-message queue size (prevents arbitrary growth/OOM) (#1800)
* Cap pre-join plugin-message queue size (prevents arbitrary growth/OOM)

* Clear counters once as the entire queue will have been processed
2026-05-24 16:55:07 +01:00
Wouter GritterandGitHub 7d68208e11 Always close HttpClient (#1798)
* Always close HttpClient (even on exception)

* Get rid of try/catch since we now use java 21
2026-05-22 10:25:48 -05:00
Wouter GritterandGitHub 38ff21afd7 Fix theoretical IOOBE race (#1799) 2026-05-22 15:50:48 +01:00
72 changed files with 1031 additions and 349 deletions
+4 -6
View File
@@ -18,7 +18,7 @@ java {
} }
dependencies { dependencies {
compileOnlyApi(libs.jspecify) api(libs.jspecify)
api(libs.gson) api(libs.gson)
api(libs.guava) api(libs.guava)
@@ -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}/",
) )
@@ -97,6 +97,16 @@ public class PluginAnnotationProcessor extends AbstractProcessor {
} }
} }
for (String provided : plugin.provides()) {
if (!SerializedPluginDescription.ID_PATTERN.matcher(provided).matches()) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Invalid provided ID '" + provided + "' for plugin " + qualifiedName
+ ". IDs must start alphabetically, have lowercase alphanumeric characters, and "
+ "can contain dashes or underscores.");
return false;
}
}
// All good, generate the velocity-plugin.json. // All good, generate the velocity-plugin.json.
SerializedPluginDescription description = SerializedPluginDescription SerializedPluginDescription description = SerializedPluginDescription
.from(plugin, qualifiedName.toString()); .from(plugin, qualifiedName.toString());
@@ -35,11 +35,12 @@ public final class SerializedPluginDescription {
private final @Nullable String url; private final @Nullable String url;
private final @Nullable List<String> authors; private final @Nullable List<String> authors;
private final @Nullable List<Dependency> dependencies; private final @Nullable List<Dependency> dependencies;
private final @Nullable List<String> provides;
private final String main; private final String main;
private SerializedPluginDescription(String id, String name, String version, String description, private SerializedPluginDescription(String id, String name, String version, String description,
String url, String url,
List<String> authors, List<Dependency> dependencies, String main) { List<String> authors, List<Dependency> dependencies, List<String> provides, String main) {
Preconditions.checkNotNull(id, "id"); Preconditions.checkNotNull(id, "id");
Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid"); Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid");
this.id = id; this.id = id;
@@ -50,6 +51,7 @@ public final class SerializedPluginDescription {
this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors; this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors;
this.dependencies = this.dependencies =
dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies; dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies;
this.provides = provides == null || provides.isEmpty() ? ImmutableList.of() : provides;
this.main = Preconditions.checkNotNull(main, "main"); this.main = Preconditions.checkNotNull(main, "main");
} }
@@ -61,7 +63,9 @@ public final class SerializedPluginDescription {
return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(), return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(),
plugin.description(), plugin.url(), plugin.description(), plugin.url(),
Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty()) Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty())
.collect(Collectors.toList()), dependencies, qualifiedName); .collect(Collectors.toList()), dependencies,
Arrays.stream(plugin.provides()).filter(provided -> !provided.isEmpty())
.collect(Collectors.toList()), qualifiedName);
} }
public String getId() { public String getId() {
@@ -92,6 +96,10 @@ public final class SerializedPluginDescription {
return dependencies == null ? ImmutableList.of() : dependencies; return dependencies == null ? ImmutableList.of() : dependencies;
} }
public List<String> getProvides() {
return provides == null ? ImmutableList.of() : provides;
}
public String getMain() { public String getMain() {
return main; return main;
} }
@@ -112,12 +120,13 @@ public final class SerializedPluginDescription {
&& Objects.equals(url, that.url) && Objects.equals(url, that.url)
&& Objects.equals(authors, that.authors) && Objects.equals(authors, that.authors)
&& Objects.equals(dependencies, that.dependencies) && Objects.equals(dependencies, that.dependencies)
&& Objects.equals(provides, that.provides)
&& Objects.equals(main, that.main); && Objects.equals(main, that.main);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(id, name, version, description, url, authors, dependencies); return Objects.hash(id, name, version, description, url, authors, dependencies, provides);
} }
@Override @Override
@@ -130,6 +139,7 @@ public final class SerializedPluginDescription {
+ ", url='" + url + '\'' + ", url='" + url + '\''
+ ", authors=" + authors + ", authors=" + authors
+ ", dependencies=" + dependencies + ", dependencies=" + dependencies
+ ", provides=" + provides
+ ", main='" + main + '\'' + ", main='" + main + '\''
+ '}'; + '}';
} }
@@ -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,45 @@
/*
* Copyright (C) 2018-2026 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.
*/
package com.velocitypowered.api.event.player;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;
/**
* Called when a player is marked as loaded by the client.
*
* <p>This event is fired once per {@link com.velocitypowered.api.proxy.ServerConnection}
* when the player explicitly notifies the server after loading the world (closing the downloading terrain screen)
*
* @implNote Unlike Paper this event will <u>not</u> fire due to a timeout nor respawning.
* Though plugins can implement a timeout by scheduling a task in {@link ServerPostConnectEvent}
* and checking {@link com.velocitypowered.api.proxy.ServerConnection#isClientLoaded()}.
* @sinceMinecraft 1.21.4
* @since 4.1.0
*/
@Beta
public final class PlayerClientLoadedWorldEvent {
private final Player player;
public PlayerClientLoadedWorldEvent(Player player) {
this.player = Preconditions.checkNotNull(player, "player");
}
public Player getPlayer() {
return player;
}
@Override
public String toString() {
return "PlayerClientLoadedWorldEvent{"
+ "player=" + player
+ '}';
}
}
@@ -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;
@@ -72,4 +72,12 @@ public @interface Plugin {
* @return the plugin dependencies * @return the plugin dependencies
*/ */
Dependency[] dependencies() default {}; Dependency[] dependencies() default {};
/**
* The plugin IDs this plugin "provides" for. Each ID must match
* {@link SerializedPluginDescription#ID_PATTERN_STRING}.
*
* @return the provided IDs
*/
String[] provides() default {};
} }
@@ -100,6 +100,16 @@ public interface PluginDescription {
return Optional.empty(); return Optional.empty();
} }
/**
* Gets a {@link Collection} of the provided IDs of the {@link Plugin} within this container.
*
* @return the provided plugins IDs, can be empty
* @see Plugin#provides()
*/
default Collection<String> getProvidedIds() {
return ImmutableSet.of();
}
/** /**
* Returns the source the plugin was loaded from. * Returns the source the plugin was loaded from.
* *
@@ -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.
@@ -7,6 +7,7 @@
package com.velocitypowered.api.proxy; package com.velocitypowered.api.proxy;
import com.google.common.annotations.Beta;
import com.velocitypowered.api.proxy.messages.ChannelMessageSink; import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
import com.velocitypowered.api.proxy.messages.ChannelMessageSource; import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
@@ -40,6 +41,17 @@ public interface ServerConnection extends ChannelMessageSource, ChannelMessageSi
*/ */
ServerInfo getServerInfo(); ServerInfo getServerInfo();
/**
* Returns whether the client notified this connection of having loaded the world.
*
* @return true if the client has loaded the world
* @implNote This is purely client-dependent; see {@link com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent}.
* @sinceMinecraft 1.21.4
* @since 4.1.0
*/
@Beta
boolean isClientLoaded();
/** /**
* Returns the player that this connection is associated with. * Returns the player that this connection is associated with.
* *
@@ -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.1.0-SNAPSHOT
+23 -24
View File
@@ -2,25 +2,24 @@
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.1"
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.6.1"
spotless = "com.diffplug.spotless:8.2.0" spotless = "com.diffplug.spotless:8.9.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.10.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"
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.1.0" bstats = "org.bstats:bstats-base:3.2.1"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.3" caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.4"
checker-qual = "org.checkerframework:checker-qual:3.53.0" checker-qual = "org.checkerframework:checker-qual:4.2.1"
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3" checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
completablefutures = "com.spotify:completable-futures:0.3.6" completablefutures = "com.spotify:completable-futures:0.3.6"
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" } configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
@@ -30,24 +29,24 @@ 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.19"
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.1.2"
jspecify = "org.jspecify:jspecify:1.0.0" jspecify = "org.jspecify:jspecify:1.0.1"
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:3.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" }
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
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.23.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" }
@@ -55,10 +54,10 @@ 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-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-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" } netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
nightconfig = "com.electronwill.night-config:toml:3.8.3" nightconfig = "com.electronwill.night-config:toml:3.9.0"
slf4j = "org.slf4j:slf4j-api:2.0.17" slf4j = "org.slf4j:slf4j-api:2.0.18"
snakeyaml = "org.yaml:snakeyaml:2.5" snakeyaml = "org.yaml:snakeyaml:2.6"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.9.8" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.10.3"
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
[bundles] [bundles]
+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
@@ -5,29 +5,44 @@ set -e
# make sure we're in the correct directory - the top-level `native` directory # make sure we're in the correct directory - the top-level `native` directory
cd "$(dirname "$0")/.." || exit 1 cd "$(dirname "$0")/.." || exit 1
# Use docker by default, falling back to podman. Set CONTAINER_ENGINE to pick one explicitly.
if [ -z "$CONTAINER_ENGINE" ]; then
if command -v docker > /dev/null 2>&1; then
CONTAINER_ENGINE=docker
elif command -v podman > /dev/null 2>&1; then
CONTAINER_ENGINE=podman
else
echo "Neither docker nor podman was found on PATH." >&2
exit 1
fi
fi
echo "Using container engine: $CONTAINER_ENGINE"
ARCHS=(x86_64 aarch64) ARCHS=(x86_64 aarch64)
BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy alpine) BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy alpine)
COMPRESSION_VARIANTS=(ubuntu-focal alpine) COMPRESSION_VARIANTS=(ubuntu-focal alpine)
# Build one image per (variant, arch). Passing several --platform flags to a single tagged build
# does not produce a multi-arch tag - only the last architecture keeps the tag, and the run below
# then sees a platform mismatch, treats the image as missing and tries to pull it from a registry.
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
docker_platforms=""
for arch in "${ARCHS[@]}"; do for arch in "${ARCHS[@]}"; do
docker_platforms="$docker_platforms --platform linux/${arch}" echo "Building base build image for $variant on $arch..."
$CONTAINER_ENGINE build -t velocity-native-build:$variant-$arch --platform linux/${arch} \
-f build-support/$variant.Dockerfile .
done done
echo "Building base build image for $variant..."
docker build -t velocity-native-build:$variant $docker_platforms -f build-support/$variant.Dockerfile .
done done
for arch in "${ARCHS[@]}"; do for arch in "${ARCHS[@]}"; do
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
echo "Building native crypto for $arch on $variant..." echo "Building native crypto for $arch on $variant..."
docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-crypto.sh" $CONTAINER_ENGINE run --rm --pull=never -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant-$arch /bin/bash -c "cd /app && ./build-support/compile-linux-crypto.sh"
done done
for variant in "${COMPRESSION_VARIANTS[@]}"; do for variant in "${COMPRESSION_VARIANTS[@]}"; do
echo "Building native compression for $arch on $variant..." echo "Building native compression for $arch on $variant..."
docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh" $CONTAINER_ENGINE run --rm --pull=never -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant-$arch /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh"
done done
done done
+1 -2
View File
@@ -34,8 +34,7 @@ Java_com_velocitypowered_natives_compression_NativeZlibInflate_process(JNIEnv *e
jlong sourceAddress, jlong sourceAddress,
jint sourceLength, jint sourceLength,
jlong destinationAddress, jlong destinationAddress,
jint destinationLength, jint destinationLength)
jlong maximumSize)
{ {
struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx; struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx;
enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress, enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress,
@@ -56,24 +56,45 @@ public class JavaVelocityCompressor implements VelocityCompressor {
final int origIdx = source.readerIndex(); final int origIdx = source.readerIndex();
inflater.setInput(source.nioBuffer()); inflater.setInput(source.nioBuffer());
int totalProduced = 0;
try { try {
final int readable = source.readableBytes(); final int readable = source.readableBytes();
while (!inflater.finished() && inflater.getBytesRead() < readable) { while (!inflater.finished() && inflater.getBytesRead() < readable) {
if (totalProduced >= uncompressedSize) {
throw new DataFormatException("Decompressed data exceeds the claimed uncompressed size "
+ "of " + uncompressedSize + " bytes");
}
final int remaining = uncompressedSize - totalProduced;
if (!destination.isWritable()) { if (!destination.isWritable()) {
destination.ensureWritable(ZLIB_BUFFER_SIZE); destination.ensureWritable(Math.min(ZLIB_BUFFER_SIZE, remaining));
} }
ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(), ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(),
destination.writableBytes()); destination.writableBytes());
// Never let a single inflate step write past the claimed size
if (destNioBuf.remaining() > remaining) {
destNioBuf.limit(destNioBuf.position() + remaining);
}
int produced = inflater.inflate(destNioBuf); int produced = inflater.inflate(destNioBuf);
if (produced == 0 && !inflater.finished()) {
// Output space was available yet the inflater made no progress: the stream is truncated
// or corrupt (this also covers a peer that over-reported the uncompressed size).
throw new DataFormatException("Received a truncated or malformed deflate stream, "
+ "expected " + uncompressedSize + " bytes");
}
totalProduced += produced;
destination.writerIndex(destination.writerIndex() + produced); destination.writerIndex(destination.writerIndex() + produced);
} }
if (!inflater.finished()) { if (!inflater.finished()) {
throw new DataFormatException("Received a deflate stream that was too large, wanted " throw new DataFormatException("Received a truncated or malformed deflate stream, expected "
+ uncompressedSize); + uncompressedSize + " bytes");
} }
source.readerIndex(origIdx + inflater.getTotalIn());
source.readerIndex(origIdx + (int) inflater.getBytesRead());
} finally { } finally {
inflater.reset(); inflater.reset();
} }
@@ -102,7 +123,7 @@ public class JavaVelocityCompressor implements VelocityCompressor {
destination.writerIndex(destination.writerIndex() + produced); destination.writerIndex(destination.writerIndex() + produced);
} }
source.readerIndex(origIdx + deflater.getTotalIn()); source.readerIndex(origIdx + (int) deflater.getBytesRead());
deflater.reset(); deflater.reset();
} }
@@ -17,6 +17,7 @@
package com.velocitypowered.natives.compression; package com.velocitypowered.natives.compression;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@@ -77,6 +78,83 @@ class VelocityCompressorTest {
check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32)); check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32));
} }
private static final int BOMB_ACTUAL_SIZE = 1 << 20;
private static final int BOMB_LYING_CLAIM = 1024;
@Test
void javaRejectsUnderReportedUncompressedSize() throws DataFormatException {
VelocityCompressor compressor = JavaVelocityCompressor.FACTORY
.create(Deflater.DEFAULT_COMPRESSION);
DataFormatException ex = assertRejectsDecompressionBomb(compressor);
// The Java compressor's size guard names the claimed size in its message, so operators can
// tell an over-size rejection apart from a genuinely corrupt stream.
assertTrue(ex.getMessage().contains(String.valueOf(BOMB_LYING_CLAIM)),
"rejection must originate from the uncompressed-size guard, got: " + ex.getMessage());
}
@Test
@EnabledOnOs({LINUX})
void nativeRejectsUnderReportedUncompressedSize() throws DataFormatException {
VelocityCompressor compressor = Natives.compress.get().create(Deflater.DEFAULT_COMPRESSION);
if (compressor.preferredBufferType() != BufferPreference.DIRECT_REQUIRED) {
compressor.close();
fail("Loaded regular compressor");
}
// libdeflate rejects with its own native-origin message ("uncompressed size is inaccurate"),
// so we only assert the behavioural guarantee here, not the message text.
assertRejectsDecompressionBomb(compressor);
}
/**
* Asserts that a compressor refuses a decompression bomb: a small, valid deflate stream whose
* claimed uncompressed size is far smaller than what it actually inflates to. Verifies the same
* stream round-trips when the claimed size is honest (proving the rejection is caused by the
* under-reported size, not corrupt input) and that no output is written past the claimed size.
* Closes the compressor before returning the exception thrown by the rejected inflate.
*/
private DataFormatException assertRejectsDecompressionBomb(VelocityCompressor compressor)
throws DataFormatException {
// Direct buffers so this works for the native compressor, which requires them.
ByteBuf source = Unpooled.directBuffer(BOMB_ACTUAL_SIZE);
ByteBuf compressed = Unpooled.directBuffer();
try {
source.writeZero(BOMB_ACTUAL_SIZE);
compressor.deflate(source, compressed);
final int compressedSize = compressed.readableBytes();
assertTrue(compressedSize < BOMB_ACTUAL_SIZE / 100,
"sanity: payload really is a decompression bomb (" + compressedSize + " -> "
+ BOMB_ACTUAL_SIZE + ")");
// Positive control: the compressed stream is perfectly valid and round-trips when the peer
// tells the truth about its uncompressed size.
ByteBuf honest = Unpooled.directBuffer();
try {
compressor.inflate(compressed.duplicate(), honest, BOMB_ACTUAL_SIZE);
assertEquals(BOMB_ACTUAL_SIZE, honest.readableBytes(),
"valid stream must fully decompress when the claimed size is honest");
} finally {
honest.release();
}
// Attack: same valid stream, but a tiny claimed size. inflate must refuse rather than grow
// the destination without bound.
ByteBuf decompressed = Unpooled.directBuffer();
try {
DataFormatException ex = assertThrows(DataFormatException.class,
() -> compressor.inflate(compressed.duplicate(), decompressed, BOMB_LYING_CLAIM));
assertTrue(decompressed.writerIndex() <= BOMB_LYING_CLAIM,
"inflate must not write past the claimed uncompressed size");
return ex;
} finally {
decompressed.release();
}
} finally {
source.release();
compressed.release();
compressor.close();
}
}
private void check(VelocityCompressor compressor, Supplier<ByteBuf> bufSupplier) private void check(VelocityCompressor compressor, Supplier<ByteBuf> bufSupplier)
throws DataFormatException { throws DataFormatException {
ByteBuf source = bufSupplier.get(); ByteBuf source = bufSupplier.get();
+14 -61
View File
@@ -33,65 +33,6 @@ tasks {
transform(Log4j2PluginsCacheFileTransformer::class.java) transform(Log4j2PluginsCacheFileTransformer::class.java)
// Exclude all the collection types we don"t intend to use
exclude("it/unimi/dsi/fastutil/booleans/**")
exclude("it/unimi/dsi/fastutil/bytes/**")
exclude("it/unimi/dsi/fastutil/chars/**")
exclude("it/unimi/dsi/fastutil/doubles/**")
exclude("it/unimi/dsi/fastutil/floats/**")
exclude("it/unimi/dsi/fastutil/longs/**")
exclude("it/unimi/dsi/fastutil/shorts/**")
// Exclude the fastutil IO utilities - we don"t use them.
exclude("it/unimi/dsi/fastutil/io/**")
// Exclude most of the int types - Object2IntMap have a values() method that returns an
// IntCollection, and we need Int2ObjectMap
exclude("it/unimi/dsi/fastutil/ints/*Int2Boolean*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Byte*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Char*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Double*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Float*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Int*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Long*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Short*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Reference*")
exclude("it/unimi/dsi/fastutil/ints/IntAVL*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayF*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayI*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayL*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayP*")
exclude("it/unimi/dsi/fastutil/ints/IntArraySet*")
exclude("it/unimi/dsi/fastutil/ints/*IntBi*")
exclude("it/unimi/dsi/fastutil/ints/Int*Pair")
exclude("it/unimi/dsi/fastutil/ints/IntLinked*")
exclude("it/unimi/dsi/fastutil/ints/IntList*")
exclude("it/unimi/dsi/fastutil/ints/IntHeap*")
exclude("it/unimi/dsi/fastutil/ints/IntOpen*")
exclude("it/unimi/dsi/fastutil/ints/IntRB*")
exclude("it/unimi/dsi/fastutil/ints/IntSorted*")
exclude("it/unimi/dsi/fastutil/ints/*Priority*")
exclude("it/unimi/dsi/fastutil/ints/*BigList*")
// Try to exclude everything BUT Object2Int{LinkedOpen,Open,CustomOpen}HashMap
exclude("it/unimi/dsi/fastutil/objects/*ObjectArray*")
exclude("it/unimi/dsi/fastutil/objects/*ObjectAVL*")
exclude("it/unimi/dsi/fastutil/objects/*Object*Big*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Boolean*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Byte*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Char*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Double*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Float*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntArray*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntAVL*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntRB*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Long*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Object*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Reference*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Short*")
exclude("it/unimi/dsi/fastutil/objects/*ObjectRB*")
exclude("it/unimi/dsi/fastutil/objects/*Reference*")
// Exclude Checker Framework annotations // Exclude Checker Framework annotations
exclude("org/checkerframework/checker/**") exclude("org/checkerframework/checker/**")
@@ -112,6 +53,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
@@ -120,9 +70,13 @@ fill {
build { build {
channel = BuildChannel.STABLE channel = BuildChannel.STABLE
versionFamily("3.0.0") versionFamily("4.0.0")
version(projectVersion) version(projectVersion)
if (versionFamily.get().split(".")[0] != projectVersion.split(".")[0]) {
throw IllegalArgumentException("Version family does not match project version")
}
downloads { downloads {
register("server:default") { register("server:default") {
file = tasks.shadowJar.flatMap { it.archiveFile } file = tasks.shadowJar.flatMap { it.archiveFile }
@@ -159,7 +113,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;
@@ -219,7 +221,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
PluginDescription description = new VelocityPluginDescription( PluginDescription description = new VelocityPluginDescription(
"velocity", version.getName(), version.getVersion(), "The Velocity proxy", "velocity", version.getName(), version.getVersion(), "The Velocity proxy",
version.getName().equals("Velocity") ? VELOCITY_URL : null, version.getName().equals("Velocity") ? VELOCITY_URL : null,
ImmutableList.of(version.getVendor()), Collections.emptyList(), null); ImmutableList.of(version.getVendor()), Collections.emptyList(),
Collections.emptyList(), null);
VelocityPluginContainer container = new VelocityPluginContainer(description); VelocityPluginContainer container = new VelocityPluginContainer(description);
container.setInstance(VelocityVirtualPlugin.INSTANCE); container.setInstance(VelocityVirtualPlugin.INSTANCE);
return container; return container;
@@ -743,6 +746,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
@@ -163,6 +163,9 @@ public final class VelocityCommand {
.append(Component.text() .append(Component.text()
.content(version.getVersion()) .content(version.getVersion())
.decoration(TextDecoration.BOLD, false)) .decoration(TextDecoration.BOLD, false))
.hoverEvent(Component.translatable("velocity.command.version-offer-copy-version"))
.clickEvent(ClickEvent.copyToClipboard(version.getName() + " "
+ version.getVersion()))
.build(); .build();
final Component copyright = Component final Component copyright = Component
.translatable("velocity.command.version-copyright", .translatable("velocity.command.version-copyright",
@@ -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) {
@@ -1007,9 +1009,10 @@ public class VelocityConfiguration implements ProxyConfig {
* @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);
} }
} }
@@ -53,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
@@ -200,6 +201,10 @@ public interface MinecraftSessionHandler {
return false; return false;
} }
default boolean handle(ServerboundPlayerLoadedPacket packet) {
return false;
}
default boolean handle(ServerLoginPacket packet) { default boolean handle(ServerLoginPacket packet) {
return false; return false;
} }
@@ -296,6 +296,19 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
// Register and unregister packets are simply forwarded to the client as-is.
if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) {
return false;
}
if (PluginMessageUtil.isMcBrand(packet)) {
PluginMessagePacket rewritten = PluginMessageUtil
.rewriteMinecraftBrand(packet,
server.getVersion(), playerConnection.getProtocolVersion());
playerConnection.write(rewritten);
return true;
}
if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) { if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) {
// Handled. // Handled.
return true; return true;
@@ -277,7 +277,6 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(),
serverConn.getPlayer().getProtocolVersion())); serverConn.getPlayer().getProtocolVersion()));
} else { } else {
byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) { if (id == null) {
@@ -287,6 +286,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
// Handling this stuff async means that we should probably pause // Handling this stuff async means that we should probably pause
// the connection while we toss this off into another pool // the connection while we toss this off into another pool
byte[] bytes = ByteBufUtil.getBytes(packet.content());
this.serverConn.getConnection().setAutoReading(false); this.serverConn.getConnection().setAutoReading(false);
this.server.getEventManager() this.server.getEventManager()
.fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes)) .fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes))
@@ -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()
@@ -68,6 +68,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private final VelocityServer server; private final VelocityServer server;
private @Nullable MinecraftConnection connection; private @Nullable MinecraftConnection connection;
private boolean hasCompletedJoin = false; private boolean hasCompletedJoin = false;
private boolean clientLoaded = false; // 1.21.4+
private boolean gracefulDisconnect = false; private boolean gracefulDisconnect = false;
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN; private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
private final Map<Long, Long> pendingPings = new HashMap<>(); private final Map<Long, Long> pendingPings = new HashMap<>();
@@ -317,6 +318,15 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
} }
} }
public void setClientLoaded(boolean clientLoaded) {
this.clientLoaded = clientLoaded;
}
@Override
public boolean isClientLoaded() {
return clientLoaded;
}
boolean isGracefulDisconnect() { boolean isGracefulDisconnect() {
return gracefulDisconnect; return gracefulDisconnect;
} }
@@ -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;
@@ -47,6 +47,7 @@ import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket; import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -135,7 +136,6 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
} else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) {
return true; return true;
} else if (serverConn != null) { } else if (serverConn != null) {
byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) { if (id == null) {
@@ -145,6 +145,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
// Handling this stuff async means that we should probably pause // Handling this stuff async means that we should probably pause
// the connection while we toss this off into another pool // the connection while we toss this off into another pool
byte[] bytes = ByteBufUtil.getBytes(packet.content());
serverConn.getPlayer().getConnection().setAutoReading(false); serverConn.getPlayer().getConnection().setAutoReading(false);
this.server.getEventManager() this.server.getEventManager()
.fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes)) .fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes))
@@ -212,8 +213,9 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(ServerboundCustomClickActionPacket packet) { public boolean handle(ServerboundCustomClickActionPacket packet) {
if (player.getConnectionInFlight() != null) { VelocityServerConnection serverConnection = player.getConnectionInFlightOrConnectedServer();
player.getConnectionInFlight().ensureConnected().write(packet.retain()); if (serverConnection != null) {
serverConnection.ensureConnected().write(packet.retain());
return true; return true;
} }
@@ -240,8 +242,8 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
MinecraftConnection smc = serverConnection.getConnection(); MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.getPhase().consideredComplete()) { if (smc != null && serverConnection.getPhase().consideredComplete()) {
if (packet instanceof PluginMessagePacket) { if (packet instanceof ByteBufHolder bufHolder) {
((PluginMessagePacket) packet).retain(); bufHolder.retain();
} }
smc.write(packet); smc.write(packet);
} }
@@ -26,6 +26,7 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent; import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent;
import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
import com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.event.player.TabCompleteEvent;
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
@@ -52,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.RespawnPacket; import com.velocitypowered.proxy.protocol.packet.RespawnPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
@@ -88,6 +90,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 +108,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 +193,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
@@ -201,6 +245,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return true; // will forward onto the server return true; // will forward onto the server
} }
@Override
public boolean handle(ServerboundPlayerLoadedPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning - shouldn't be possible with a vanilla client
return true;
}
if (!serverConnection.isClientLoaded()) {
serverConnection.setClientLoaded(true);
server.getEventManager().fireAndForget(new PlayerClientLoadedWorldEvent(player));
}
return false;
}
@Override @Override
public boolean handle(SessionPlayerCommandPacket packet) { public boolean handle(SessionPlayerCommandPacket packet) {
if (player.getCurrentServer().isEmpty()) { if (player.getCurrentServer().isEmpty()) {
@@ -349,9 +407,26 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
if (!player.getPhase().handle(player, packet, serverConn)) { if (!player.getPhase().handle(player, packet, serverConn)) {
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
// We don't have any plugins listening on this channel, process the packet now.
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
.consideredComplete()) {
// The client is trying to send messages too early. This is primarily caused by mods,
// but further aggravated by Velocity. To work around these issues, we will queue any
// non-FML handshake messages to be sent once the FML handshake has completed or the
// JoinGame packet has been received by the proxy, whichever comes first.
//
// We also need to make sure to retain these packets, so they can be flushed
// appropriately.
enqueueLoginPluginMessage(packet.retain());
} else {
// The connection is ready, send the packet now.
backendConn.write(packet.retain());
}
} else {
byte[] copy = ByteBufUtil.getBytes(packet.content()); byte[] copy = ByteBufUtil.getBytes(packet.content());
String channel = packet.getChannel(); PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy);
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy);
server.getEventManager().fire(event).thenAcceptAsync(pme -> { server.getEventManager().fire(event).thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed()) { if (pme.getResult().isAllowed()) {
PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(), PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(),
@@ -359,7 +434,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
if (!player.getPhase().consideredComplete() || !serverConn.getPhase() if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
.consideredComplete()) { .consideredComplete()) {
// We're still processing the connection (see above), enqueue the packet for now. // We're still processing the connection (see above), enqueue the packet for now.
loginPluginMessages.add(message.retain()); enqueueLoginPluginMessage(message.retain());
} else { } else {
backendConn.write(message); backendConn.write(message);
} }
@@ -371,6 +446,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
} }
} }
}
return true; return true;
} }
@@ -621,6 +697,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 +931,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());
@@ -66,7 +66,7 @@ public class ServerListPingHandler {
}) })
.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()
@@ -60,9 +60,7 @@ final class CustomHandlerAdapter<F> {
UntargetedEventHandler buildUntargetedHandler(final Method method) UntargetedEventHandler buildUntargetedHandler(final Method method)
throws IllegalAccessException { throws IllegalAccessException {
final MethodHandle methodHandle = methodHandlesLookup.unreflect(method); final MethodHandle methodHandle = methodHandlesLookup.unreflect(method);
final MethodHandles.Lookup defineLookup = MethodHandles.privateLookupIn( final LambdaType<F> lambdaType = functionType.defineClassesWith(methodHandlesLookup);
method.getDeclaringClass(), methodHandlesLookup);
final LambdaType<F> lambdaType = functionType.defineClassesWith(defineLookup);
final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle); final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle);
final BiFunction<Object, Object, EventTask> handlerFunction = final BiFunction<Object, Object, EventTask> handlerFunction =
handlerBuilder.apply(invokeFunction); handlerBuilder.apply(invokeFunction);
@@ -243,7 +243,7 @@ public class VelocityEventManager implements EventManager {
} else { } else {
type = untargetedVoidHandlerType; type = untargetedVoidHandlerType;
} }
return LambdaFactory.create(type.defineClassesWith(lookup), methodHandle); return LambdaFactory.create(type.defineClassesWith(methodHandlesLookup), methodHandle);
} }
static final class MethodHandlerInfo { static final class MethodHandlerInfo {
@@ -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)
@@ -46,10 +46,12 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -62,6 +64,7 @@ public class VelocityPluginManager implements PluginManager {
private final Map<String, PluginContainer> pluginsById = new LinkedHashMap<>(); private final Map<String, PluginContainer> pluginsById = new LinkedHashMap<>();
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>(); private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
private final Set<PluginContainer> plugins = new LinkedHashSet<>();
private final VelocityServer server; private final VelocityServer server;
public VelocityPluginManager(VelocityServer server) { public VelocityPluginManager(VelocityServer server) {
@@ -74,7 +77,9 @@ public class VelocityPluginManager implements PluginManager {
* @param plugin the plugin to register * @param plugin the plugin to register
*/ */
public void registerPlugin(PluginContainer plugin) { public void registerPlugin(PluginContainer plugin) {
plugins.add(plugin);
pluginsById.put(plugin.getDescription().getId(), plugin); pluginsById.put(plugin.getDescription().getId(), plugin);
plugin.getDescription().getProvidedIds().forEach(id -> pluginsById.put(id, plugin));
Optional<?> instance = plugin.getInstance(); Optional<?> instance = plugin.getInstance();
instance.ifPresent(o -> pluginInstances.put(o, plugin)); instance.ifPresent(o -> pluginInstances.put(o, plugin));
} }
@@ -100,16 +105,34 @@ public class VelocityPluginManager implements PluginManager {
try { try {
PluginDescription candidate = loader.loadCandidate(path); PluginDescription candidate = loader.loadCandidate(path);
// If we found a duplicate candidate (with the same ID), don't load it. // A plugin claims its own ID plus every ID it provides. If any of those are already
PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent( // claimed by another candidate, don't load this one.
candidate.getId(), candidate); List<String> claimedIds = new ArrayList<>(candidate.getProvidedIds().size() + 1);
claimedIds.add(candidate.getId());
claimedIds.addAll(candidate.getProvidedIds());
if (maybeExistingCandidate != null) { PluginDescription conflict = null;
logger.error("Refusing to load plugin at path {} since we already " String conflictingId = null;
+ "loaded a plugin with the same ID {} from {}", for (String id : claimedIds) {
PluginDescription existing = foundCandidates.get(id);
if (existing != null) {
conflict = existing;
conflictingId = id;
break;
}
}
if (conflict != null) {
logger.error("Refusing to load plugin at path {} since ID {} was already "
+ "claimed by a plugin loaded from {}",
candidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"), candidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"),
candidate.getId(), conflictingId,
maybeExistingCandidate.getSource().map(Objects::toString).orElse("<UNKNOWN>")); conflict.getSource().map(Objects::toString).orElse("<UNKNOWN>"));
continue;
}
for (String id : claimedIds) {
foundCandidates.put(id, candidate);
} }
} catch (Throwable e) { } catch (Throwable e) {
logger.error("Unable to load plugin {}", path, e); logger.error("Unable to load plugin {}", path, e);
@@ -122,8 +145,10 @@ public class VelocityPluginManager implements PluginManager {
return; return;
} }
// foundCandidates indexes each candidate under its ID and any provided IDs, so dedupe before
// sorting to avoid loading a plugin more than once.
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates( List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(
new ArrayList<>(foundCandidates.values())); new ArrayList<>(new LinkedHashSet<>(foundCandidates.values())));
Map<String, PluginDescription> loadedCandidates = new HashMap<>(); Map<String, PluginDescription> loadedCandidates = new HashMap<>();
Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>(); Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>();
@@ -144,6 +169,7 @@ public class VelocityPluginManager implements PluginManager {
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin); VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
pluginContainers.put(container, loader.createModule(container)); pluginContainers.put(container, loader.createModule(container));
loadedCandidates.put(realPlugin.getId(), realPlugin); loadedCandidates.put(realPlugin.getId(), realPlugin);
realPlugin.getProvidedIds().forEach(id -> loadedCandidates.putIfAbsent(id, realPlugin));
} catch (Throwable e) { } catch (Throwable e) {
logger.error("Can't create module for plugin {}", candidate.getId(), e); logger.error("Can't create module for plugin {}", candidate.getId(), e);
} }
@@ -201,7 +227,7 @@ public class VelocityPluginManager implements PluginManager {
@Override @Override
public Collection<PluginContainer> getPlugins() { public Collection<PluginContainer> getPlugins() {
return Collections.unmodifiableCollection(pluginsById.values()); return Collections.unmodifiableCollection(plugins);
} }
@Override @Override
@@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.meta.PluginDependency; import com.velocitypowered.api.plugin.meta.PluginDependency;
@@ -43,6 +44,7 @@ public class VelocityPluginDescription implements PluginDescription {
private final @Nullable String url; private final @Nullable String url;
private final List<String> authors; private final List<String> authors;
private final Map<String, PluginDependency> dependencies; private final Map<String, PluginDependency> dependencies;
private final Collection<String> providedIds;
private final Path source; private final Path source;
/** /**
@@ -55,11 +57,13 @@ public class VelocityPluginDescription implements PluginDescription {
* @param url the website for the plugin * @param url the website for the plugin
* @param authors the authors of this plugin * @param authors the authors of this plugin
* @param dependencies the dependencies for this plugin * @param dependencies the dependencies for this plugin
* @param providedIds the IDs this plugin provides for
* @param source the original source for the plugin * @param source the original source for the plugin
*/ */
public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version, public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
@Nullable String description, @Nullable String url, @Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source) { @Nullable List<String> authors, Collection<PluginDependency> dependencies,
@Nullable Collection<String> providedIds, Path source) {
this.id = checkNotNull(id, "id"); this.id = checkNotNull(id, "id");
this.name = Strings.emptyToNull(name); this.name = Strings.emptyToNull(name);
this.version = Strings.emptyToNull(version); this.version = Strings.emptyToNull(version);
@@ -67,6 +71,8 @@ public class VelocityPluginDescription implements PluginDescription {
this.url = Strings.emptyToNull(url); this.url = Strings.emptyToNull(url);
this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors); this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors);
this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId()); this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId());
this.providedIds =
providedIds == null ? ImmutableSet.of() : ImmutableSet.copyOf(providedIds);
this.source = source; this.source = source;
} }
@@ -110,6 +116,11 @@ public class VelocityPluginDescription implements PluginDescription {
return Optional.ofNullable(dependencies.get(id)); return Optional.ofNullable(dependencies.get(id));
} }
@Override
public Collection<String> getProvidedIds() {
return providedIds;
}
@Override @Override
public Optional<Path> getSource() { public Optional<Path> getSource() {
return Optional.ofNullable(source); return Optional.ofNullable(source);
@@ -125,6 +136,7 @@ public class VelocityPluginDescription implements PluginDescription {
+ ", url='" + url + '\'' + ", url='" + url + '\''
+ ", authors=" + authors + ", authors=" + authors
+ ", dependencies=" + dependencies + ", dependencies=" + dependencies
+ ", providedIds=" + providedIds
+ ", source=" + source + ", source=" + source
+ '}'; + '}';
} }
@@ -78,6 +78,14 @@ public class JavaPluginLoader implements PluginLoader {
} }
} }
for (String providedId : pd.getProvides()) {
if (!SerializedPluginDescription.ID_PATTERN.matcher(providedId).matches()) {
throw new InvalidPluginException(
"Provided ID '" + providedId + "' for plugin '" + pd.getId() + "' is invalid."
);
}
}
return createCandidateDescription(pd, source); return createCandidateDescription(pd, source);
} }
@@ -181,6 +189,7 @@ public class JavaPluginLoader implements PluginLoader {
description.getUrl(), description.getUrl(),
description.getAuthors(), description.getAuthors(),
dependencies, dependencies,
description.getProvides(),
source, source,
description.getMain() description.getMain()
); );
@@ -197,6 +206,7 @@ public class JavaPluginLoader implements PluginLoader {
description.getUrl().orElse(null), description.getUrl().orElse(null),
description.getAuthors(), description.getAuthors(),
description.getDependencies(), description.getDependencies(),
description.getProvidedIds(),
description.getSource().orElse(null), description.getSource().orElse(null),
mainClass mainClass
); );
@@ -32,9 +32,9 @@ class JavaVelocityPluginDescription extends VelocityPluginDescription {
JavaVelocityPluginDescription(String id, @Nullable String name, @Nullable String version, JavaVelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
@Nullable String description, @Nullable String url, @Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source, @Nullable List<String> authors, Collection<PluginDependency> dependencies,
Class<?> mainClass) { @Nullable Collection<String> providedIds, Path source, Class<?> mainClass) {
super(id, name, version, description, url, authors, dependencies, source); super(id, name, version, description, url, authors, dependencies, providedIds, source);
this.mainClass = checkNotNull(mainClass); this.mainClass = checkNotNull(mainClass);
} }
@@ -32,9 +32,9 @@ class JavaVelocityPluginDescriptionCandidate extends VelocityPluginDescription {
JavaVelocityPluginDescriptionCandidate(String id, @Nullable String name, @Nullable String version, JavaVelocityPluginDescriptionCandidate(String id, @Nullable String name, @Nullable String version,
@Nullable String description, @Nullable String url, @Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source, @Nullable List<String> authors, Collection<PluginDependency> dependencies,
String mainClass) { @Nullable Collection<String> providedIds, Path source, String mainClass) {
super(id, name, version, description, url, authors, dependencies, source); super(id, name, version, description, url, authors, dependencies, providedIds, source);
this.mainClass = checkNotNull(mainClass); this.mainClass = checkNotNull(mainClass);
} }
@@ -17,7 +17,6 @@
package com.velocitypowered.proxy.plugin.util; package com.velocitypowered.proxy.plugin.util;
import com.google.common.collect.Maps;
import com.google.common.graph.Graph; import com.google.common.graph.Graph;
import com.google.common.graph.GraphBuilder; import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph; import com.google.common.graph.MutableGraph;
@@ -60,8 +59,19 @@ public class PluginDependencyUtils {
.allowsSelfLoops(false) .allowsSelfLoops(false)
.expectedNodeCount(sortedCandidates.size()) .expectedNodeCount(sortedCandidates.size())
.build(); .build();
Map<String, PluginDescription> candidateMap = Maps.uniqueIndex(sortedCandidates,
PluginDescription::getId); // Index candidates by their own ID and any IDs they provide, so a dependency can be satisfied
// by a plugin that provides that ID. Real IDs take precedence over provided IDs, and the first
// provider of a given ID wins; upstream loading rejects such conflicts before we get here.
Map<String, PluginDescription> candidateMap = new HashMap<>();
for (PluginDescription description : sortedCandidates) {
candidateMap.putIfAbsent(description.getId(), description);
}
for (PluginDescription description : sortedCandidates) {
for (String provided : description.getProvidedIds()) {
candidateMap.putIfAbsent(provided, description);
}
}
for (PluginDescription description : sortedCandidates) { for (PluginDescription description : sortedCandidates) {
graph.addNode(description); graph.addNode(description);
@@ -69,7 +79,8 @@ public class PluginDependencyUtils {
for (PluginDependency dependency : description.getDependencies()) { for (PluginDependency dependency : description.getDependencies()) {
PluginDescription in = candidateMap.get(dependency.getId()); PluginDescription in = candidateMap.get(dependency.getId());
if (in != null) { // Guard against self-loops: a plugin may name an ID it itself provides.
if (in != null && !in.equals(description)) {
graph.putEdge(description, in); graph.putEdge(description, in);
} }
} }
@@ -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()
@@ -18,37 +18,7 @@
package com.velocitypowered.proxy.protocol; package com.velocitypowered.proxy.protocol;
import static com.google.common.collect.Iterables.getLast; import static com.google.common.collect.Iterables.getLast;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12; import static com.velocitypowered.api.network.ProtocolVersion.*;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_14;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_15;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_17;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_2;
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_21;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_4;
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_9;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION;
import static com.velocitypowered.api.network.ProtocolVersion.SUPPORTED_VERSIONS;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.CLIENTBOUND; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.CLIENTBOUND;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.SERVERBOUND; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.SERVERBOUND;
@@ -88,6 +58,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
@@ -345,6 +316,12 @@ public enum StateRegistry {
map(0x13, MINECRAFT_1_21_2, false), map(0x13, MINECRAFT_1_21_2, false),
map(0x14, MINECRAFT_1_21_6, false), map(0x14, MINECRAFT_1_21_6, false),
map(0x15, MINECRAFT_26_1, false)); map(0x15, MINECRAFT_26_1, false));
serverbound.register(
ServerboundPlayerLoadedPacket.class,
() -> ServerboundPlayerLoadedPacket.INSTANCE,
map(0x2A, MINECRAFT_1_21_4, false),
map(0x2B, MINECRAFT_1_21_6, false),
map(0x2C, MINECRAFT_26_1, false));
serverbound.register( serverbound.register(
PluginMessagePacket.class, PluginMessagePacket.class,
PluginMessagePacket::new, PluginMessagePacket::new,
@@ -835,24 +812,27 @@ public enum StateRegistry {
ClientboundServerLinksPacket::new, ClientboundServerLinksPacket::new,
map(0x7B, MINECRAFT_1_21, false), map(0x7B, MINECRAFT_1_21, false),
map(0x82, MINECRAFT_1_21_2, false), map(0x82, MINECRAFT_1_21_2, false),
map(0x87, MINECRAFT_1_21_9, false)); map(0x87, MINECRAFT_1_21_9, false),
clientbound.register(UpdateTeamsPacket.class, UpdateTeamsPacket::new,
map(0x41, ProtocolVersion.MINECRAFT_1_9, true),
map(0x43, ProtocolVersion.MINECRAFT_1_12, true),
map(0x44, ProtocolVersion.MINECRAFT_1_12_1, true),
map(0x47, ProtocolVersion.MINECRAFT_1_13, true),
map(0x4B, ProtocolVersion.MINECRAFT_1_14, true),
map(0x4C, ProtocolVersion.MINECRAFT_1_15, true),
map(0x55, ProtocolVersion.MINECRAFT_1_17, true),
map(0x58, ProtocolVersion.MINECRAFT_1_19_1, true),
map(0x56, ProtocolVersion.MINECRAFT_1_19_3, true),
map(0x5A, ProtocolVersion.MINECRAFT_1_19_4, true),
map(0x5C, ProtocolVersion.MINECRAFT_1_20_2, true),
map(0x5E, ProtocolVersion.MINECRAFT_1_20_3, true),
map(0x60, ProtocolVersion.MINECRAFT_1_20_5, true),
map(0x67, ProtocolVersion.MINECRAFT_1_21_2, true),
map(0x6B, MINECRAFT_1_21_9, false),
map(0x89, MINECRAFT_26_1, false)); map(0x89, MINECRAFT_26_1, false));
clientbound.register(UpdateTeamsPacket.class, UpdateTeamsPacket::new,
map(0x41, MINECRAFT_1_9, true),
map(0x43, MINECRAFT_1_12, true),
map(0x44, MINECRAFT_1_12_1, true),
map(0x47, MINECRAFT_1_13, true),
map(0x4B, MINECRAFT_1_14, true),
map(0x4C, MINECRAFT_1_15, true),
map(0x55, MINECRAFT_1_17, true),
map(0x58, MINECRAFT_1_19_1, true),
map(0x56, MINECRAFT_1_19_3, true),
map(0x5A, MINECRAFT_1_19_4, true),
map(0x5C, MINECRAFT_1_20_2, true),
map(0x5E, MINECRAFT_1_20_3, true),
map(0x60, MINECRAFT_1_20_5, true),
map(0x67, MINECRAFT_1_21_2, true),
map(0x6B, MINECRAFT_1_21_9, false),
map(0x63, MINECRAFT_1_21_11, false),
map(0x6D, MINECRAFT_26_1, false),
map(0x6D, MINECRAFT_26_2, false));
} }
}, },
LOGIN { LOGIN {
@@ -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
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2025 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class ServerboundPlayerLoadedPacket implements MinecraftPacket {
public static final ServerboundPlayerLoadedPacket INSTANCE = new ServerboundPlayerLoadedPacket();
private ServerboundPlayerLoadedPacket() {
}
@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
}
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
}
@Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return 0;
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}
@@ -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>
@@ -49,6 +50,7 @@ velocity.command.glist-view-all=To view all players on servers, use /glist all.
velocity.command.reload-success=Velocity configuration successfully reloaded. velocity.command.reload-success=Velocity configuration successfully reloaded.
velocity.command.reload-failure=Unable to reload your Velocity configuration. Check the console for more details. velocity.command.reload-failure=Unable to reload your Velocity configuration. Check the console for more details.
velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> is licensed under the terms of the GNU General Public License v3. velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> is licensed under the terms of the GNU General Public License v3.
velocity.command.version-offer-copy-version=Click to copy version to clipboard
velocity.command.no-plugins=There are no plugins currently installed. velocity.command.no-plugins=There are no plugins currently installed.
velocity.command.plugins-list=Plugins: <arg:0> velocity.command.plugins-list=Plugins: <arg:0>
velocity.command.plugin-tooltip-website=Website: <arg:0> velocity.command.plugin-tooltip-website=Website: <arg:0>
+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
@@ -0,0 +1,119 @@
/*
* Copyright (C) 2018-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.connection.client;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.backend.BackendConnectionPhase;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.ReferenceCountUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class ClientConfigSessionHandlerTest {
private VelocityServer server;
private ConnectedPlayer player;
private ClientConfigSessionHandler handler;
@BeforeEach
void setUp() {
server = mock(VelocityServer.class);
player = mock(ConnectedPlayer.class);
handler = new ClientConfigSessionHandler(server, player);
}
@AfterEach
void tearDown() {
// nothing to clean up; each test manages its own ByteBufs
}
private ServerboundCustomClickActionPacket makePacket() {
ByteBuf frame = Unpooled.buffer().writeByte(0);
ServerboundCustomClickActionPacket pkt = new ServerboundCustomClickActionPacket();
pkt.replace(frame.readRetainedSlice(frame.readableBytes()));
return pkt;
}
@Test
void handleForwardsToInFlightServer() {
VelocityServerConnection inFlight = mock(VelocityServerConnection.class);
MinecraftConnection backend = mock(MinecraftConnection.class);
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(inFlight);
when(inFlight.ensureConnected()).thenReturn(backend);
ServerboundCustomClickActionPacket pkt = makePacket();
assertTrue(handler.handle(pkt));
verify(backend).write(pkt);
ReferenceCountUtil.release(pkt);
}
@Test
void handleForwardsToConnectedServerWhenInFlightIsNull() {
VelocityServerConnection connected = mock(VelocityServerConnection.class);
MinecraftConnection backend = mock(MinecraftConnection.class);
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(connected);
when(connected.ensureConnected()).thenReturn(backend);
ServerboundCustomClickActionPacket pkt = makePacket();
assertTrue(handler.handle(pkt));
verify(backend).write(pkt);
ReferenceCountUtil.release(pkt);
}
@Test
void handleReturnsFalseWhenNoServer() {
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(null);
ServerboundCustomClickActionPacket pkt = makePacket();
assertFalse(handler.handle(pkt));
ReferenceCountUtil.release(pkt);
}
@Test
void handleGenericRetainsAndForwards() {
VelocityServerConnection connected = mock(VelocityServerConnection.class);
MinecraftConnection backend = mock(MinecraftConnection.class);
BackendConnectionPhase phase = mock(BackendConnectionPhase.class);
when(player.getConnectedServer()).thenReturn(connected);
when(connected.getConnection()).thenReturn(backend);
when(connected.getPhase()).thenReturn(phase);
when(phase.consideredComplete()).thenReturn(true);
ServerboundCustomClickActionPacket pkt = makePacket();
int refBefore = pkt.refCnt();
handler.handleGeneric(pkt);
// retain() was called (+1) before write
assertEquals(refBefore + 1, pkt.refCnt());
verify(backend).write(pkt);
ReferenceCountUtil.release(pkt);
}
}
@@ -44,6 +44,15 @@ class PluginDependencyUtilsTest {
private static final PluginDescription CIRCULAR_DEPENDENCY_2 = testDescription("oval", private static final PluginDescription CIRCULAR_DEPENDENCY_2 = testDescription("oval",
new PluginDependency("circle", "", false)); new PluginDependency("circle", "", false));
// "provider" is loaded from a real ID but provides the virtual ID "some-api"; "consumer" and
// "zdependent" depend on it only through that provided ID / a chain that reaches it.
private static final PluginDescription PROVIDES_API = providingDescription("provider",
ImmutableList.of("some-api"));
private static final PluginDescription DEPENDS_ON_PROVIDED = providingDescription("consumer",
ImmutableList.of(), new PluginDependency("some-api", null, false));
private static final PluginDescription DEPENDS_ON_CONSUMER = testDescription("zdependent",
new PluginDependency("consumer", null, false));
@Test @Test
void sortCandidatesTrivial() throws Exception { void sortCandidatesTrivial() throws Exception {
List<PluginDescription> descriptionList = new ArrayList<>(); List<PluginDescription> descriptionList = new ArrayList<>();
@@ -96,10 +105,31 @@ class PluginDependencyUtilsTest {
assertThrows(IllegalStateException.class, () -> PluginDependencyUtils.sortCandidates(descs)); assertThrows(IllegalStateException.class, () -> PluginDependencyUtils.sortCandidates(descs));
} }
@Test
void sortCandidatesResolvesProvidedDependency() throws Exception {
List<PluginDescription> plugins = ImmutableList.of(DEPENDS_ON_PROVIDED, PROVIDES_API);
List<PluginDescription> expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED);
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
}
@Test
void sortCandidatesResolvesTransitiveProvidedDependency() throws Exception {
List<PluginDescription> plugins = ImmutableList.of(DEPENDS_ON_CONSUMER, DEPENDS_ON_PROVIDED,
PROVIDES_API);
List<PluginDescription> expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED,
DEPENDS_ON_CONSUMER);
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
}
private static PluginDescription testDescription(String id, PluginDependency... dependencies) { private static PluginDescription testDescription(String id, PluginDependency... dependencies) {
return providingDescription(id, ImmutableList.of(), dependencies);
}
private static PluginDescription providingDescription(String id, List<String> provides,
PluginDependency... dependencies) {
return new VelocityPluginDescription( return new VelocityPluginDescription(
id, "tuxed", "0.1", null, null, ImmutableList.of(), id, "tuxed", "0.1", null, null, ImmutableList.of(),
ImmutableList.copyOf(dependencies), null ImmutableList.copyOf(dependencies), provides, null
); );
} }
} }