diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java b/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java
index 718e57f1..aa73cefd 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java
@@ -16,13 +16,49 @@
*/
package com.velocitypowered.proxy.config;
+import com.electronwill.nightconfig.core.Config;
/**
- * Supported passthrough modes for ping passthrough.
+ * Object to contain all the things that can be toggled for ping passthrough.
+ *
+ * @param version Whether the version should be passed through.
+ * @param players Whether the player count should be passed through.
+ * @param description Whether the description should be passed through.
+ * @param favicon Whether the favicon should be passed through.
+ * @param modinfo Whether the modinfo should be passed through.
*/
-public enum PingPassthroughMode {
- DISABLED,
- MODS,
- DESCRIPTION,
- ALL
+public record PingPassthroughMode(boolean version, boolean players,
+ boolean description, boolean favicon, boolean modinfo) {
+ public static final PingPassthroughMode DEFAULT = new PingPassthroughMode();
+
+ /**
+ * Creates a default PingPassthroughMode.
+ */
+ private PingPassthroughMode() {
+ this(false, false, false, false, false);
+ }
+
+ /**
+ * Returns a PingPassthroughMode from a config section, or the default if the section is null.
+ * Based on the code for PacketLimiterConfig.
+ *
+ * @param config The configuration object to parse.
+ * @return The PingPassthroughMode, or the default if {@code config} is null.
+ */
+ public static PingPassthroughMode fromConfig(Config config) {
+ if (config == null) {
+ return DEFAULT;
+ }
+ return new PingPassthroughMode(
+ config.getOrElse("version", DEFAULT.version()),
+ config.getOrElse("players", DEFAULT.players()),
+ config.getOrElse("description", DEFAULT.description()),
+ config.getOrElse("favicon", DEFAULT.favicon()),
+ config.getOrElse("modinfo", DEFAULT.modinfo()));
+ }
+
+ public boolean enabled() {
+ return this.version || this.players || this.description || this.favicon
+ || this.modinfo;
+ }
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
index 2c7826e0..fc9ba583 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
@@ -32,6 +32,7 @@ import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
import com.velocitypowered.proxy.config.migration.MotdMigration;
import com.velocitypowered.proxy.config.migration.PacketLimiterMigration;
+import com.velocitypowered.proxy.config.migration.PingPassthroughMigration;
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
import com.velocitypowered.proxy.util.AddressUtil;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -79,7 +80,7 @@ public class VelocityConfiguration implements ProxyConfig {
@Expose
private boolean onlineModeKickExistingPlayers = false;
@Expose
- private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED;
+ private PingPassthroughMode pingPassthrough = PingPassthroughMode.DEFAULT;
@Expose
private boolean samplePlayersInPing = false;
private final Servers servers;
@@ -513,7 +514,8 @@ public class VelocityConfiguration implements ProxyConfig {
new MotdMigration(),
new MiniMessageTranslationsMigration(),
new TransferIntegrationMigration(),
- new PacketLimiterMigration()
+ new PacketLimiterMigration(),
+ new PingPassthroughMigration(),
};
for (final ConfigurationMigration migration : migrations) {
@@ -555,9 +557,7 @@ public class VelocityConfiguration implements ProxyConfig {
final CommentedConfig metricsConfig = config.get("metrics");
final PlayerInfoForwarding forwardingMode = config.getEnumOrElse(
"player-info-forwarding-mode", PlayerInfoForwarding.NONE);
- final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
- PingPassthroughMode.DISABLED);
-
+ final PingPassthroughMode pingPassthrough = PingPassthroughMode.fromConfig(config.get("ping-passthrough"));
final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false);
final String bind = config.getOrElse("bind", "0.0.0.0:25565");
@@ -590,7 +590,7 @@ public class VelocityConfiguration implements ProxyConfig {
forwardingMode,
forwardingSecret,
kickExisting,
- pingPassthroughMode,
+ pingPassthrough,
samplePlayersInPing,
enablePlayerAddressLogging,
new Servers(serversConfig),
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/migration/ConfigurationMigration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/migration/ConfigurationMigration.java
index 7c00b7bb..27891369 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/config/migration/ConfigurationMigration.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/migration/ConfigurationMigration.java
@@ -30,7 +30,8 @@ public sealed interface ConfigurationMigration
MotdMigration,
MiniMessageTranslationsMigration,
TransferIntegrationMigration,
- PacketLimiterMigration {
+ PacketLimiterMigration,
+ PingPassthroughMigration {
boolean shouldMigrate(CommentedFileConfig config);
void migrate(CommentedFileConfig config, Logger logger) throws IOException;
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/migration/PingPassthroughMigration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/migration/PingPassthroughMigration.java
new file mode 100644
index 00000000..702a06cf
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/migration/PingPassthroughMigration.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2024-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 .
+ */
+
+package com.velocitypowered.proxy.config.migration;
+
+import com.electronwill.nightconfig.core.file.CommentedFileConfig;
+import java.util.Locale;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Migrate the old ping passthrough entry to separate config entries.
+ */
+public final class PingPassthroughMigration implements ConfigurationMigration {
+ @Override
+ public boolean shouldMigrate(final CommentedFileConfig config) {
+ return configVersion(config) < 2.9;
+ }
+
+ @Override
+ public void migrate(final CommentedFileConfig config, final Logger logger) {
+ // Get legacy ping passthrough value
+ final String legacyMode = config.getOrElse("ping-passthrough", "DISABLED")
+ .toUpperCase(Locale.ROOT);
+ boolean version = false;
+ boolean players = false;
+ boolean description = false;
+ boolean favicon = false;
+ boolean modinfo = false;
+
+ switch (legacyMode) {
+ case "ALL":
+ version = true;
+ players = true;
+ description = true;
+ favicon = true;
+ modinfo = true;
+ break;
+ case "DESCRIPTION":
+ description = true;
+ modinfo = true;
+ break;
+ case "MODS":
+ modinfo = true;
+ break;
+ default: // DISABLED
+ break;
+ }
+
+ config.removeComment("ping-passthrough");
+ config.remove("ping-passthrough");
+
+ config.setComment("announce-forge",
+ " Announce whether or not your server supports Forge. If you run a modded server, we\n"
+ + " suggest turning this on.\n"
+ + "\n"
+ + " If your network runs one modpack consistently, consider using ping-passthrough.modinfo = true\n"
+ + " instead for a nicer display in the server list.");
+
+ config.set("ping-passthrough.version", version);
+ config.setComment(
+ "ping-passthrough.version",
+ " Should Velocity pass the version number from the backend server when responding to server list ping requests?"
+ );
+
+ config.set("ping-passthrough.players", players);
+ config.setComment(
+ "ping-passthrough.players",
+ " Should Velocity pass the player count from the backend server when responding to server list ping requests?"
+ );
+
+ config.set("ping-passthrough.description", description);
+ config.setComment(
+ "ping-passthrough.description",
+ " Should Velocity pass the description from the backend server when responding to server list ping requests?"
+ );
+
+ config.set("ping-passthrough.favicon", favicon);
+ config.setComment(
+ "ping-passthrough.favicon",
+ " Should Velocity pass the favicon (also known as the server icon) from the backend server when responding to server list ping requests?"
+ );
+
+ config.set("ping-passthrough.modinfo", modinfo);
+ config.setComment(
+ "ping-passthrough.modinfo",
+ " Should Velocity pass the mod list from the backend server when responding to server list ping requests?"
+ );
+
+ config.set("config-version", "2.9");
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java
index 5ccb2b11..97582ca2 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java
@@ -23,6 +23,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.server.PingOptions;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerPing;
+import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.api.util.ModInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.PingPassthroughMode;
@@ -100,60 +101,68 @@ public class ServerListPingHandler {
CompletableFuture> pingResponses = CompletableFutures.successfulAsList(pings,
(ex) -> fallback);
- return switch (mode) {
- case ALL -> pingResponses.thenApply(responses -> {
- // Find the first non-fallback
- for (ServerPing response : responses) {
- if (response == fallback) {
- continue;
- }
- if (response.getDescriptionComponent() == null) {
- return response.asBuilder()
- .description(Component.empty())
- .build();
- }
+ // Return early if ping passthrough is not enabled
+ if (!mode.enabled()) {
+ return CompletableFuture.completedFuture(fallback);
+ }
- return response;
+ return pingResponses.thenApply(responses -> {
+ // Find the first non-fallback
+ for (ServerPing response : responses) {
+ if (response == fallback) {
+ continue;
}
- return fallback;
- });
- case MODS -> pingResponses.thenApply(responses -> {
- // Find the first non-fallback that contains a mod list
- for (ServerPing response : responses) {
- if (response == fallback) {
- continue;
- }
- Optional modInfo = response.getModinfo();
- if (modInfo.isPresent()) {
- return fallback.asBuilder().mods(modInfo.get()).build();
- }
- }
- return fallback;
- });
- case DESCRIPTION -> pingResponses.thenApply(responses -> {
- // Find the first non-fallback. If it includes a modlist, add it too.
- for (ServerPing response : responses) {
- if (response == fallback) {
- continue;
- }
- if (response.getDescriptionComponent() == null) {
- continue;
- }
- return new ServerPing(
- fallback.getVersion(),
- fallback.getPlayers().orElse(null),
- response.getDescriptionComponent(),
- fallback.getFavicon().orElse(null),
- response.getModinfo().orElse(null)
- );
+ ServerPing.Version version;
+ if (mode.version()) {
+ version = response.getVersion();
+ } else {
+ version = fallback.getVersion();
}
- return fallback;
- });
- // Not possible, but covered for completeness.
- default -> CompletableFuture.completedFuture(fallback);
- };
+
+ ServerPing.Players players;
+ if (mode.players()) {
+ players = response.getPlayers().orElse(null);
+ } else {
+ players = fallback.getPlayers().orElse(null);
+ }
+
+ Component description;
+ if (mode.description()) {
+ if (response.getDescriptionComponent() != null) {
+ description = response.getDescriptionComponent();
+ } else {
+ description = Component.empty();
+ }
+ } else {
+ description = fallback.getDescriptionComponent();
+ }
+
+ Favicon favicon;
+ if (mode.favicon()) {
+ favicon = response.getFavicon().orElse(null);
+ } else {
+ favicon = fallback.getFavicon().orElse(null);
+ }
+
+ ModInfo modinfo;
+ if (mode.modinfo()) {
+ modinfo = response.getModinfo().orElse(null);
+ } else {
+ modinfo = fallback.getModinfo().orElse(null);
+ }
+
+ return new ServerPing(
+ version,
+ players,
+ description,
+ favicon,
+ modinfo
+ );
+ }
+ return fallback;
+ });
}
/**
@@ -168,7 +177,7 @@ public class ServerListPingHandler {
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
PingPassthroughMode passthroughMode = configuration.getPingPassthrough();
- if (passthroughMode == PingPassthroughMode.DISABLED) {
+ if (!passthroughMode.enabled()) {
return CompletableFuture.completedFuture(constructLocalPing(shownVersion));
} else {
String virtualHostStr = connection.getVirtualHost().map(InetSocketAddress::getHostString)
diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml
index 0eae2734..34a17f6c 100644
--- a/proxy/src/main/resources/default-velocity.toml
+++ b/proxy/src/main/resources/default-velocity.toml
@@ -1,5 +1,5 @@
# Config version. Do not change this
-config-version = "2.8"
+config-version = "2.9"
# 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"
@@ -43,7 +43,7 @@ forwarding-secret-file = "forwarding.secret"
# Announce whether or not your server supports Forge. If you run a modded server, we
# suggest turning this on.
#
-# If your network runs one modpack consistently, consider using ping-passthrough = "mods"
+# If your network runs one modpack consistently, consider using ping-passthrough.modinfo = true
# instead for a nicer display in the server list.
announce-forge = false
@@ -51,21 +51,6 @@ announce-forge = false
# any existing player who is online if a duplicate connection attempt is made.
kick-existing-players = false
-# Should Velocity pass server list ping requests to a backend server?
-# Available options:
-# - "disabled": No pass-through will be done. The velocity.toml and server-icon.png
-# will determine the initial server list ping response.
-# - "mods": Passes only the mod list from your backend server into the response.
-# The first server in your try list (or forced host) with a mod list will be
-# used. If no backend servers can be contacted, Velocity won't display any
-# mod information.
-# - "description": Uses the description and mod list from the backend server. The first
-# server in the try (or forced host) list that responds is used for the
-# description and mod list.
-# - "all": Uses the backend server's response as the proxy response. The Velocity
-# configuration is used if no servers could be contacted.
-ping-passthrough = "DISABLED"
-
# If enabled (default is false), then a sample of the online players on the proxy will be visible
# when hovering over the player count in the server list.
# This doesn't have any effect when ping passthrough is set to either "description" or "all".
@@ -74,6 +59,18 @@ sample-players-in-ping = false
# If not enabled (default is true) player IP addresses will be replaced by in logs
enable-player-address-logging = true
+[ping-passthrough]
+# Should Velocity pass the version number from the backend server when responding to server list ping requests?
+version = false
+# Should Velocity pass the player count from the backend server when responding to server list ping requests?
+players = false
+# Should Velocity pass the description from the backend server when responding to server list ping requests?
+description = false
+# Should Velocity pass the favicon (also known as the server icon) from the backend server when responding to server list ping requests?
+favicon = false
+# Should Velocity pass the mod list from the backend server when responding to server list ping requests?
+modinfo = false
+
[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.