diff --git a/api/src/main/java/com/velocitypowered/api/proxy/config/BackendServerConfig.java b/api/src/main/java/com/velocitypowered/api/proxy/config/BackendServerConfig.java
new file mode 100644
index 0000000000..802a487230
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/proxy/config/BackendServerConfig.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018-2025 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.proxy.config;
+
+import static java.util.Objects.requireNonNull;
+
+import com.velocitypowered.api.proxy.server.ServerInfoForwardingMode;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Exposes server configuration information that plugins may use.
+ *
+ * What's the forwarding mode?
+ * The server can use a different mode to obtain and forward player info.
+ * For instance, if you are running a 1.12 (or lower version) server on a velocity proxy with MODERN player info forwarding
+ * the server doesn't support MODERN forwarding. So you need to set LEGACY forwarding mode for that server
+ * and velocity will use ONLY FOR THAT SERVER the legacy forwarding mode.
+ * If the forwarding mode is null it means that the server is using the "player-info-forwarding-mode", set in the config.
+ *
+ * @param address The address of the backend server.
+ * @param forwardingMode The forwarding mode of the backend server.
+ * @since 3.4.0
+ * @see ServerInfoForwardingMode
+ * @see com.velocitypowered.api.proxy.server.ServerInfo#ServerInfo(String, java.net.InetSocketAddress, ServerInfoForwardingMode)
+ * @apiNote TIP: If you need to set this value when creating dynamic servers in your plugins
+ * you can do that by adding the {@link ServerInfoForwardingMode} value as the last parameter
+ * while creating a new {@link com.velocitypowered.api.proxy.server.ServerInfo}.
+ */
+@NullMarked
+public record BackendServerConfig(
+ String address,
+ @Nullable ServerInfoForwardingMode forwardingMode
+) {
+ public BackendServerConfig {
+ requireNonNull(address);
+ }
+
+ public BackendServerConfig(final String address) {
+ this(address, null);
+ }
+}
diff --git a/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java b/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java
index 12d3dbd106..de9ea1b125 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java
@@ -83,9 +83,23 @@ public interface ProxyConfig {
* does. For a view of all registered servers, see {@link ProxyServer#getAllServers()}.
*
* @return registered servers map
+ * @deprecated use {@link #getBackendServers()} instead.
*/
+ @Deprecated(forRemoval = true, since = "3.4.0")
Map getServers();
+ /**
+ * Get a Map of all servers registered in velocity.toml. This method does
+ * not return all the servers currently in memory, although in most cases it
+ * does. For a view of all registered servers, see {@link ProxyServer#getAllServers()}.
+ *
+ * @return registered servers map with, instead of the only address, the Backend Server Object for each
+ * of them which contains the address of the server and its info forwarding mode.
+ * @since 3.4.0
+ * @see com.velocitypowered.api.proxy.server.ServerInfoForwardingMode
+ */
+ Map getBackendServers();
+
/**
* Get the order of servers that players will be connected to.
*
diff --git a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerInfo.java b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerInfo.java
index fd686297ee..4be3d5e1d6 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerInfo.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerInfo.java
@@ -21,6 +21,23 @@ public final class ServerInfo implements Comparable {
private final String name;
private final InetSocketAddress address;
+ @Nullable
+ private final ServerInfoForwardingMode forwardingMode;
+
+ /**
+ * Creates a new ServerInfo object.
+ *
+ * @param name the name for the server
+ * @param address the address of the server to connect to
+ * @param forwardingMode the server info forwarding mode, or {@code null} if the mode from the config should be used
+ * @since 3.4.0
+ */
+ public ServerInfo(String name, InetSocketAddress address, @Nullable ServerInfoForwardingMode forwardingMode) {
+ this.name = Preconditions.checkNotNull(name, "name");
+ this.address = Preconditions.checkNotNull(address, "address");
+ this.forwardingMode = forwardingMode;
+ }
+
/**
* Creates a new ServerInfo object.
*
@@ -30,6 +47,7 @@ public final class ServerInfo implements Comparable {
public ServerInfo(String name, InetSocketAddress address) {
this.name = Preconditions.checkNotNull(name, "name");
this.address = Preconditions.checkNotNull(address, "address");
+ this.forwardingMode = null;
}
public final String getName() {
@@ -40,11 +58,23 @@ public final InetSocketAddress getAddress() {
return address;
}
+ /**
+ * Returns the forwarding mode used by the backend server to communicate with Velocity.
+ *
+ * @return the configured forwarding mode for the server, or {@code null}
+ * if the mode is inherited from the "player-info-forwarding-mode" set in the config
+ */
+ @Nullable
+ public final ServerInfoForwardingMode getServerInfoForwardingMode() {
+ return forwardingMode;
+ }
+
@Override
public String toString() {
return "ServerInfo{"
+ "name='" + name + '\''
+ ", address=" + address
+ + ", forwarding=" + forwardingMode
+ '}';
}
@@ -53,17 +83,17 @@ public final boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
- if (o == null || getClass() != o.getClass()) {
+ if (!(o instanceof final ServerInfo that)) {
return false;
}
- ServerInfo that = (ServerInfo) o;
return Objects.equals(name, that.name)
- && Objects.equals(address, that.address);
+ && Objects.equals(address, that.address)
+ && Objects.equals(forwardingMode, that.forwardingMode);
}
@Override
public final int hashCode() {
- return Objects.hash(name, address);
+ return Objects.hash(name, address, forwardingMode);
}
@Override
diff --git a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerInfoForwardingMode.java b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerInfoForwardingMode.java
new file mode 100644
index 0000000000..0f27cf97e2
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerInfoForwardingMode.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2018-2025 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.proxy.server;
+
+/**
+ * Supported per-server player info forwarding methods.
+ *
+ * @since 3.4.0
+ */
+public enum ServerInfoForwardingMode {
+ MODERN,
+ BUNGEEGUARD,
+ LEGACY,
+ NONE
+}
+
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java b/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java
index d0b7f34f24..50c4cbf799 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java
@@ -18,6 +18,7 @@
package com.velocitypowered.proxy;
import com.velocitypowered.api.proxy.server.ServerInfo;
+import com.velocitypowered.api.proxy.server.ServerInfoForwardingMode;
import com.velocitypowered.proxy.util.AddressUtil;
import java.io.IOException;
import java.net.InetSocketAddress;
@@ -105,17 +106,29 @@ private static class ServerInfoConverter implements ValueConverter {
@Override
public ServerInfo convert(String s) {
- String[] split = s.split(":", 2);
+ String[] split = s.split(":", 4);
if (split.length < 2) {
- throw new ValueConversionException("Invalid server format. Use :");
+ throw new ValueConversionException("Invalid server format. Use ::[port]:[forwardingmode]");
}
InetSocketAddress address;
+ ServerInfoForwardingMode mode = null;
try {
- address = AddressUtil.parseAddress(split[1]);
+ if (split.length >= 3) {
+ address = AddressUtil.parseAddress(split[1] + ":" + split[2]);
+ } else {
+ address = AddressUtil.parseAddress(split[1]);
+ }
} catch (IllegalStateException e) {
throw new ValueConversionException("Invalid hostname for server flag with name: " + split[0]);
}
- return new ServerInfo(split[0], address);
+ if (split.length == 4) {
+ try {
+ mode = ServerInfoForwardingMode.valueOf(split[3].toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new ValueConversionException("Invalid forwarding mode for server flag with name: " + split[0]);
+ }
+ }
+ return new ServerInfo(split[0], address, mode);
}
@Override
@@ -125,7 +138,7 @@ public Class extends ServerInfo> valueType() {
@Override
public String valuePattern() {
- return "name>:::[port]:[forwardingmode]";
}
}
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
index ccfc5f1409..1d681b299e 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
@@ -33,6 +33,7 @@
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
+import com.velocitypowered.api.proxy.config.BackendServerConfig;
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
@@ -297,8 +298,12 @@ void start() {
}
if (!options.isIgnoreConfigServers()) {
- for (Map.Entry entry : configuration.getServers().entrySet()) {
- servers.register(new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue())));
+ for (Map.Entry entry : configuration.getBackendServers().entrySet()) {
+ servers.register(new ServerInfo(
+ entry.getKey(),
+ AddressUtil.parseAddress(entry.getValue().address()),
+ entry.getValue().forwardingMode())
+ );
}
}
@@ -489,8 +494,12 @@ public boolean reloadConfiguration() throws IOException {
// Re-register servers. If a server is being replaced, make sure to note what players need to
// move back to a fallback server.
Collection evacuate = new ArrayList<>();
- for (Map.Entry entry : newConfiguration.getServers().entrySet()) {
- ServerInfo newInfo = new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue()));
+ for (Map.Entry entry : newConfiguration.getBackendServers().entrySet()) {
+ ServerInfo newInfo = new ServerInfo(
+ entry.getKey(),
+ AddressUtil.parseAddress(entry.getValue().address()),
+ entry.getValue().forwardingMode()
+ );
Optional rs = servers.getServer(entry.getKey());
if (rs.isEmpty()) {
servers.register(newInfo);
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 6f0fb61cc5..1c424b004b 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
@@ -24,7 +24,9 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gson.annotations.Expose;
+import com.velocitypowered.api.proxy.config.BackendServerConfig;
import com.velocitypowered.api.proxy.config.ProxyConfig;
+import com.velocitypowered.api.proxy.server.ServerInfoForwardingMode;
import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.proxy.config.migration.ConfigurationMigration;
import com.velocitypowered.proxy.config.migration.ForwardingMigration;
@@ -171,21 +173,31 @@ public boolean validate() {
break;
}
- if (servers.getServers().isEmpty()) {
+ if (servers.getBackendServers().isEmpty()) {
logger.warn("You don't have any servers configured.");
}
- for (Map.Entry entry : servers.getServers().entrySet()) {
+ for (Map.Entry entry : servers.getBackendServers().entrySet()) {
try {
- AddressUtil.parseAddress(entry.getValue());
+ AddressUtil.parseAddress(entry.getValue().address());
} catch (IllegalArgumentException e) {
logger.error("Server {} does not have a valid IP address.", entry.getKey(), e);
valid = false;
}
+
+ ServerInfoForwardingMode mode = entry.getValue().forwardingMode();
+ if (mode == ServerInfoForwardingMode.MODERN
+ || mode == ServerInfoForwardingMode.BUNGEEGUARD) {
+ if (forwardingSecret == null || forwardingSecret.length == 0) {
+ logger.error("You don't have a forwarding secret set. This is required if "
+ + "you are using MODERN or BUNGEEGUARD forwarding modes.");
+ valid = false;
+ }
+ }
}
for (String s : servers.getAttemptConnectionOrder()) {
- if (!servers.getServers().containsKey(s)) {
+ if (!servers.getBackendServers().containsKey(s)) {
logger.error("Fallback server " + s + " is not registered in your configuration!");
valid = false;
}
@@ -199,7 +211,7 @@ public boolean validate() {
}
for (String server : entry.getValue()) {
- if (!servers.getServers().containsKey(server)) {
+ if (!servers.getBackendServers().containsKey(server)) {
logger.error("Server '{}' for forced host '{}' does not exist", server, entry.getKey());
valid = false;
}
@@ -312,7 +324,14 @@ public byte[] getForwardingSecret() {
@Override
public Map getServers() {
- return servers.getServers();
+ Map serverAddresses = new HashMap<>();
+ getBackendServers().forEach((k, v) -> serverAddresses.put(k, v.address()));
+ return serverAddresses;
+ }
+
+ @Override
+ public Map getBackendServers() {
+ return servers.getBackendServers();
}
@Override
@@ -615,10 +634,10 @@ public boolean isOnlineModeKickExistingPlayers() {
private static class Servers {
- private Map servers = ImmutableMap.of(
- "lobby", "127.0.0.1:30066",
- "factions", "127.0.0.1:30067",
- "minigames", "127.0.0.1:30068"
+ private Map servers = ImmutableMap.of(
+ "lobby", new BackendServerConfig("127.0.0.1:30066"),
+ "factions", new BackendServerConfig("127.0.0.1:30067", ServerInfoForwardingMode.MODERN),
+ "minigames", new BackendServerConfig("127.0.0.1:30068", ServerInfoForwardingMode.LEGACY)
);
private List attemptConnectionOrder = ImmutableList.of("lobby");
@@ -627,14 +646,31 @@ private Servers() {
private Servers(CommentedConfig config) {
if (config != null) {
- Map servers = new HashMap<>();
+ Map servers = new HashMap<>();
for (UnmodifiableConfig.Entry entry : config.entrySet()) {
- if (entry.getValue() instanceof String) {
- servers.put(cleanServerName(entry.getKey()), entry.getValue());
+ if (entry.getValue() instanceof CommentedConfig c) {
+ String address = null;
+ ServerInfoForwardingMode forwardingMode = null;
+ for (UnmodifiableConfig.Entry entry2 : c.entrySet()) {
+ if (entry2.getKey().equalsIgnoreCase("address")) {
+ address = entry2.getValue();
+ }
+ if (entry2.getKey().equalsIgnoreCase("forwarding-mode")) {
+ forwardingMode = ServerInfoForwardingMode.valueOf(ServerInfoForwardingMode.class, entry2.getValue());
+ }
+ }
+ if (address == null) {
+ throw new IllegalArgumentException(
+ "Server entry " + entry.getKey() + " is missing address!");
+ }
+ servers.put(cleanServerName(entry.getKey()), new BackendServerConfig(address, forwardingMode));
+ //support for old server config system (forwarding mode will be null)
+ } else if (entry.getValue() instanceof String v) {
+ servers.put(cleanServerName(entry.getKey()), new BackendServerConfig(v));
} else {
if (!entry.getKey().equalsIgnoreCase("try")) {
throw new IllegalArgumentException(
- "Server entry " + entry.getKey() + " is not a string!");
+ "Server entry " + entry.getKey() + " is not a server!");
}
}
}
@@ -643,16 +679,16 @@ private Servers(CommentedConfig config) {
}
}
- private Servers(Map servers, List attemptConnectionOrder) {
+ private Servers(Map servers, List attemptConnectionOrder) {
this.servers = servers;
this.attemptConnectionOrder = attemptConnectionOrder;
}
- private Map getServers() {
+ private Map getBackendServers() {
return servers;
}
- public void setServers(Map servers) {
+ public void setServers(Map servers) {
this.servers = servers;
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java
index 14884af467..85a1b7a008 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java
@@ -83,7 +83,11 @@ public boolean handle(EncryptionRequestPacket packet) {
public boolean handle(LoginPluginMessagePacket packet) {
MinecraftConnection mc = serverConn.ensureConnected();
VelocityConfiguration configuration = server.getConfiguration();
- if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN
+
+ PlayerInfoForwarding forwardingMode =
+ serverConn.getServer().getConfiguredPlayerInfoForwarding();
+
+ if (forwardingMode == PlayerInfoForwarding.MODERN
&& packet.getChannel().equals(PlayerDataForwarding.CHANNEL)) {
int requestedForwardingVersion = PlayerDataForwarding.MODERN_DEFAULT;
@@ -143,7 +147,9 @@ public boolean handle(SetCompressionPacket packet) {
@Override
public boolean handle(ServerLoginSuccessPacket packet) {
- if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && !informationForwarded) {
+ PlayerInfoForwarding forwardingMode = serverConn.getServer().getConfiguredPlayerInfoForwarding();
+
+ if (forwardingMode == PlayerInfoForwarding.MODERN && !informationForwarded) {
resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE, serverConn.getServer()));
serverConn.disconnect();
return true;
@@ -202,7 +208,9 @@ public void exception(Throwable throwable) {
@Override
public void disconnected() {
- if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.LEGACY) {
+ PlayerInfoForwarding forwardingMode = serverConn.getServer().getConfiguredPlayerInfoForwarding();
+
+ if (forwardingMode == PlayerInfoForwarding.LEGACY) {
resultFuture.completeExceptionally(new QuietRuntimeException(
"""
The connection to the remote server was unexpectedly closed.
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java
index 71ebd7bc78..f652d9856d 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java
@@ -162,7 +162,8 @@ private String createBungeeGuardForwardingAddress(byte[] forwardingSecret) {
private void startHandshake() {
final MinecraftConnection mc = ensureConnected();
- PlayerInfoForwarding forwardingMode = server.getConfiguration().getPlayerInfoForwardingMode();
+
+ PlayerInfoForwarding forwardingMode = registeredServer.getConfiguredPlayerInfoForwarding();
// Initiate the handshake.
ProtocolVersion protocolVersion = proxyPlayer.getConnection().getProtocolVersion();
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java
index e48881f399..ba042aec46 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java
@@ -35,6 +35,7 @@
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.proxy.VelocityServer;
+import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
@@ -73,6 +74,13 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud
private final ServerInfo serverInfo;
private final Map players = new ConcurrentHashMap<>();
+ /**
+ * Register a backend server.
+ *
+ * @param server velocity instance
+ *
+ * @param serverInfo info of the backend server
+ */
public VelocityRegisteredServer(@Nullable VelocityServer server, ServerInfo serverInfo) {
this.server = server;
this.serverInfo = Preconditions.checkNotNull(serverInfo, "serverInfo");
@@ -83,6 +91,23 @@ public ServerInfo getServerInfo() {
return serverInfo;
}
+ /**
+ * Converts server info forward mode to Player info forwarding.
+ *
+ * @return player info forwarding
+ */
+ public PlayerInfoForwarding getConfiguredPlayerInfoForwarding() {
+ if (serverInfo.getServerInfoForwardingMode() == null) {
+ return server.getConfiguration().getPlayerInfoForwardingMode();
+ }
+ return switch (serverInfo.getServerInfoForwardingMode()) {
+ case LEGACY -> PlayerInfoForwarding.LEGACY;
+ case MODERN -> PlayerInfoForwarding.MODERN;
+ case BUNGEEGUARD -> PlayerInfoForwarding.BUNGEEGUARD;
+ case NONE -> PlayerInfoForwarding.NONE;
+ };
+ }
+
@Override
public Collection getPlayersConnected() {
return ImmutableList.copyOf(players.values());
diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml
index 4d71e589b9..f63d00af56 100644
--- a/proxy/src/main/resources/default-velocity.toml
+++ b/proxy/src/main/resources/default-velocity.toml
@@ -78,8 +78,8 @@ enable-player-address-logging = true
# Configure your servers here. Each key represents the server's name, and the value
# represents the IP address of the server to connect to.
lobby = "127.0.0.1:30066"
-factions = "127.0.0.1:30067"
-minigames = "127.0.0.1:30068"
+factions = {address = "127.0.0.1:30067", forwarding-mode = "MODERN"}
+minigames = {address = "127.0.0.1:30068", forwarding-mode = "LEGACY"}
# In what order we should try servers when a player logs in or is kicked from a server.
try = [