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 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 = [