Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
import com.velocitypowered.proxy.tablist.KeyedVelocityTabList;
import com.velocitypowered.proxy.tablist.VelocityTabList;
import com.velocitypowered.proxy.tablist.VelocityTabListLegacy;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
import com.velocitypowered.proxy.util.DurationUtils;
import com.velocitypowered.proxy.util.TranslatableMapper;
Expand All @@ -111,6 +112,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -890,8 +892,17 @@ private Optional<RegisteredServer> getNextServerToTry(@Nullable RegisteredServer
String virtualHostStr = getVirtualHost().map(InetSocketAddress::getHostString)
.orElse("")
.toLowerCase(Locale.ROOT);
serversToTry = server.getConfiguration().getForcedHosts().getOrDefault(virtualHostStr,
Collections.emptyList());
serversToTry = server.getConfiguration().getForcedHosts().getOrDefault(
virtualHostStr,
server.getConfiguration()
.getForcedHosts()
.entrySet()
.stream()
.filter(entry -> AddressUtil.isHostMatchingPattern(entry.getKey(), virtualHostStr))
.map(Map.Entry::getValue)
.findFirst()
.orElse(Collections.emptyList())
Comment thread
lllincoln marked this conversation as resolved.
Outdated
);
}

if (serversToTry.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
import com.velocitypowered.proxy.config.PingPassthroughMode;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
import com.velocitypowered.proxy.util.AddressUtil;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -175,7 +177,16 @@ public CompletableFuture<ServerPing> getInitialPing(VelocityInboundConnection co
.map(str -> str.toLowerCase(Locale.ROOT))
.orElse("");
List<String> serversToTry = server.getConfiguration().getForcedHosts().getOrDefault(
virtualHostStr, server.getConfiguration().getAttemptConnectionOrder());
virtualHostStr,
server.getConfiguration()
.getForcedHosts()
.entrySet()
.stream()
.filter(entry -> AddressUtil.isHostMatchingPattern(entry.getKey(), virtualHostStr))
.map(Map.Entry::getValue)
.findFirst()
.orElse(server.getConfiguration().getAttemptConnectionOrder())
);
return attemptPingPassthrough(connection, passthroughMode, serversToTry, shownVersion, virtualHostStr);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,37 @@ public static InetSocketAddress parseAndResolveAddress(String ip) {
int port = uri.getPort() == -1 ? DEFAULT_MINECRAFT_PORT : uri.getPort();
return new InetSocketAddress(uri.getHost(), port);
}

/**
* Tests whether a host matches a pattern whose labels may be the
* wildcard {@code "*"}. Each {@code "*"} matches exactly one label; all other
* labels match case-insensitively.
*
* @param pattern the pattern to match against, for example, {@code *.soulrealms.net}
* @param host the virtual host to test, for example, {@code play.soulrealms.net}
Comment thread
lllincoln marked this conversation as resolved.
Outdated
* @return true if the host matches the pattern, false otherwise
*/
public static boolean isHostMatchingPattern(String pattern, String host) {
Comment thread
lllincoln marked this conversation as resolved.
Outdated
if (host == null || pattern == null) {
return false;
}

String[] patternDomains = pattern.split("\\.");
String[] strDomains = host.split("\\.");

if (patternDomains.length != strDomains.length) {
return false;
}

for (int i = 0; patternDomains.length > i; i++) {
Comment thread
lllincoln marked this conversation as resolved.
Outdated
String patternDomain = patternDomains[i];
String strDomain = strDomains[i];

if (!patternDomain.equals("*") && !strDomain.equalsIgnoreCase(patternDomain)) {
return false;
}
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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 com.velocitypowered.proxy.util.AddressUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class ForcedHostsTest {
@Test
void testIsHostMatchingPattern() {
Comment thread
lllincoln marked this conversation as resolved.
Outdated
// One wildcard, should match
Assertions.assertTrue(AddressUtil.isHostMatchingPattern("*.example.com", "play.example.com"));
Assertions.assertTrue(AddressUtil.isHostMatchingPattern("*.example.com", "a.example.com"));
Assertions.assertTrue(AddressUtil.isHostMatchingPattern("b.*.example.com", "b.a.example.com"));

// Different number of labels should not match
Assertions.assertFalse(AddressUtil.isHostMatchingPattern("*.example.com", "a.b.example.com"));

// Wildcards should not match apex domains
Assertions.assertFalse(AddressUtil.isHostMatchingPattern("*.example.com", "example.com"));

// Exact matches should match
Assertions.assertTrue(AddressUtil.isHostMatchingPattern("example.com", "example.com"));
Assertions.assertTrue(AddressUtil.isHostMatchingPattern("play.example.com", "play.example.com"));

// Different domains shouldn't match
Assertions.assertFalse(AddressUtil.isHostMatchingPattern("otherdomain.com", "example.com"));
Assertions.assertFalse(AddressUtil.isHostMatchingPattern("a.otherdomain.com", "a.example.com"));

// Malformed patterns and hosts
Assertions.assertFalse(AddressUtil.isHostMatchingPattern(null, "example.com"));
Assertions.assertFalse(AddressUtil.isHostMatchingPattern("example.com", null));
Assertions.assertFalse(AddressUtil.isHostMatchingPattern(null, null));

// Case insensitivity
Assertions.assertTrue(AddressUtil.isHostMatchingPattern("Example.COM", "example.com"));
Assertions.assertTrue(AddressUtil.isHostMatchingPattern("*.Example.com", "play.EXAMPLE.com"));

// Multiple wildcards
Assertions.assertTrue(AddressUtil.isHostMatchingPattern("*.*.example.com", "a.b.example.com"));
}
}