diff --git a/common-proxy/src/main/java/de/maxhenkel/voicechat/VoiceProxy.java b/common-proxy/src/main/java/de/maxhenkel/voicechat/VoiceProxy.java index ba4980a91..ca7ebed6b 100644 --- a/common-proxy/src/main/java/de/maxhenkel/voicechat/VoiceProxy.java +++ b/common-proxy/src/main/java/de/maxhenkel/voicechat/VoiceProxy.java @@ -15,7 +15,10 @@ import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.UUID; public abstract class VoiceProxy { @@ -255,4 +258,109 @@ public VoiceProxySniffer getSniffer() { return voiceProxySniffer; } + /** + * Returns a configured voice host override for the backend server the given player is connected to. + * Empty string means no override. + */ + public String getPerServerVoiceHost(UUID playerUUID) { + Map map = parseServerVoiceHosts(); + if (map.isEmpty()) { + return ""; + } + String serverName = resolvePlayersBackendServerName(playerUUID); + if (serverName == null) { + return ""; + } + HostPort hp = map.get(serverName.toLowerCase(Locale.ROOT)); + if (hp == null) { + return ""; + } + return hp.host; + } + + /** + * Returns an optional port override for the backend server the given player is connected to. + * Null means no override; use the sniffed backend voice port. + */ + public Integer getPerServerVoicePortOverride(UUID playerUUID) { + Map map = parseServerVoiceHosts(); + if (map.isEmpty()) { + return null; + } + String serverName = resolvePlayersBackendServerName(playerUUID); + if (serverName == null) { + return null; + } + HostPort hp = map.get(serverName.toLowerCase(Locale.ROOT)); + if (hp == null) { + return null; + } + return hp.portOverride; + } + + private String resolvePlayersBackendServerName(UUID playerUUID) { + try { + InetSocketAddress backend = getDefaultBackendSocket(playerUUID); + if (backend == null) { + return null; + } + String host = backend.getHostString(); + int port = backend.getPort(); + for (BackendServer s : getBackendServers()) { + if (s.getAddress() instanceof InetSocketAddress) { + InetSocketAddress isa = (InetSocketAddress) s.getAddress(); + if (isa.getHostString().equalsIgnoreCase(host) && isa.getPort() == port) { + return s.getName(); + } + } + } + } catch (Exception ignored) { + } + return null; + } + + private Map parseServerVoiceHosts() { + String raw = voiceProxyConfig.serverVoiceHosts.get(); + Map out = new HashMap<>(); + if (raw == null || raw.trim().isEmpty()) { + return out; + } + String[] entries = raw.split(","); + for (String e : entries) { + String entry = e.trim(); + if (entry.isEmpty()) continue; + int eq = entry.indexOf('='); + if (eq <= 0 || eq >= entry.length() - 1) continue; + String name = entry.substring(0, eq).trim(); + String hostPort = entry.substring(eq + 1).trim(); + if (name.isEmpty() || hostPort.isEmpty()) continue; + String host = hostPort; + Integer port = null; + int lastColon = hostPort.lastIndexOf(':'); + if (lastColon > 0 && lastColon < hostPort.length() - 1 && hostPort.indexOf(']') == -1) { + // Simple host:port parsing; IPv6 with brackets not supported in this shorthand + String pstr = hostPort.substring(lastColon + 1); + try { + int p = Integer.parseInt(pstr); + if (p > 0 && p <= 65535) { + port = p; + host = hostPort.substring(0, lastColon); + } + } catch (NumberFormatException ignored) { + } + } + out.put(name.toLowerCase(Locale.ROOT), new HostPort(host, port)); + } + return out; + } + + private static class HostPort { + final String host; + final Integer portOverride; + HostPort(String host, Integer portOverride) { + this.host = host; + this.portOverride = portOverride; + } + } + } diff --git a/common-proxy/src/main/java/de/maxhenkel/voicechat/config/ProxyConfig.java b/common-proxy/src/main/java/de/maxhenkel/voicechat/config/ProxyConfig.java index 7497b8253..7c3b512f0 100644 --- a/common-proxy/src/main/java/de/maxhenkel/voicechat/config/ProxyConfig.java +++ b/common-proxy/src/main/java/de/maxhenkel/voicechat/config/ProxyConfig.java @@ -10,6 +10,7 @@ public class ProxyConfig { public ConfigEntry bindAddress; public ConfigEntry voiceHost; public ConfigEntry allowPings; + public ConfigEntry serverVoiceHosts; public ProxyConfig(ConfigBuilder builder) { builder.header(String.format("Simple Voice Chat proxy config v%s", BuildConstants.MOD_VERSION)); @@ -40,6 +41,13 @@ public ProxyConfig(ConfigBuilder builder) { .booleanEntry("allow_pings", true, "If the voice chat proxy server should reply to external pings" ); + + serverVoiceHosts = builder + .stringEntry("server_voice_hosts", "", + "Optional per-server voice host overrides (comma-separated)", + "Format: serverName=host[:port],serverName2=host2[:port2]", + "If port is omitted, the backend voice port sniffed from the server will be used" + ); } } diff --git a/common-proxy/src/main/java/de/maxhenkel/voicechat/sniffer/SniffedSecretPacket.java b/common-proxy/src/main/java/de/maxhenkel/voicechat/sniffer/SniffedSecretPacket.java index 8281d2569..37c779244 100644 --- a/common-proxy/src/main/java/de/maxhenkel/voicechat/sniffer/SniffedSecretPacket.java +++ b/common-proxy/src/main/java/de/maxhenkel/voicechat/sniffer/SniffedSecretPacket.java @@ -84,13 +84,25 @@ public UUID getPlayerUUID() { } /** - * Modifies the packet to use the proxy port and clears the voice host. - * If a voice host is present on the proxy, this value will be used. + * Modifies the packet based on proxy configuration. + * If a per-server override exists for the players backend server, use that host + * and optionally a custom port, otherwise force clients to connect to the proxy port/host. * * @param voiceProxy the proxy + * @param proxyPlayerUUID the UUID of the player on the proxy * @return the modified packet */ - public ByteBuffer patch(VoiceProxy voiceProxy) { + public ByteBuffer patch(VoiceProxy voiceProxy, UUID proxyPlayerUUID) { + String overrideHost = voiceProxy.getPerServerVoiceHost(proxyPlayerUUID); + if (overrideHost != null && !overrideHost.isEmpty()) { + voiceHost = overrideHost; + Integer overridePort = voiceProxy.getPerServerVoicePortOverride(proxyPlayerUUID); + if (overridePort != null) { + serverPort = overridePort; + } + return toBytes(); + } + serverPort = voiceProxy.getPort(); voiceHost = voiceProxy.getConfig().voiceHost.get(); return toBytes(); diff --git a/common-proxy/src/main/java/de/maxhenkel/voicechat/sniffer/VoiceProxySniffer.java b/common-proxy/src/main/java/de/maxhenkel/voicechat/sniffer/VoiceProxySniffer.java index 5bb0cbc86..84fdfec1b 100644 --- a/common-proxy/src/main/java/de/maxhenkel/voicechat/sniffer/VoiceProxySniffer.java +++ b/common-proxy/src/main/java/de/maxhenkel/voicechat/sniffer/VoiceProxySniffer.java @@ -108,7 +108,7 @@ private ByteBuffer handleSecretPacket(ByteBuffer message, UUID playerUUID) throw SniffedSecretPacket packet = SniffedSecretPacket.fromBytes(message, compatibilityVersion); playerUUIDMap.put(packet.getPlayerUUID(), playerUUID); serverUDPPortMap.put(playerUUID, packet.getServerPort()); - return packet.patch(voiceProxy); + return packet.patch(voiceProxy, playerUUID); } /** diff --git a/velocity/changelog.md b/velocity/changelog.md index c836f876f..25c4dff67 100644 --- a/velocity/changelog.md +++ b/velocity/changelog.md @@ -1 +1,4 @@ - Added ping command + +- Velocity: Added per-server voice host overrides via `voicechat-proxy.properties` key `server_voice_hosts`. + Example: `server_voice_hosts=main=voice.example.com:24454,nether=voice-nether.example.com:25500`