From 002be32bb37a58a6465776f3df9f84a984ce0688 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 31 Jul 2021 12:52:49 -0400 Subject: [PATCH] Connect Geyser players directly to the server for plugin versions (#2413) - Faster loading times and improved latency; Geyser no longer creates a physical TCP connection to join the server - Less configuration: remote address and port are now irrelevant - Accurate IP addresses without needing Floodgate. Co-authored-by: Redned --- bootstrap/bungeecord/pom.xml | 7 +- .../bungeecord/GeyserBungeeInjector.java | 134 ++++++++++++++ .../bungeecord/GeyserBungeePlugin.java | 19 +- .../platform/spigot/GeyserSpigotInjector.java | 166 ++++++++++++++++++ .../platform/spigot/GeyserSpigotPlugin.java | 15 ++ .../velocity/GeyserVelocityInjector.java | 82 +++++++++ .../velocity/GeyserVelocityPlugin.java | 26 ++- connector/pom.xml | 4 +- .../connector/bootstrap/GeyserBootstrap.java | 6 + .../connector/common/GeyserInjector.java | 91 ++++++++++ .../configuration/GeyserConfiguration.java | 2 + .../GeyserJacksonConfiguration.java | 26 +++ .../network/session/GeyserSession.java | 40 ++++- connector/src/main/resources/config.yml | 10 +- 14 files changed, 617 insertions(+), 11 deletions(-) create mode 100644 bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeInjector.java create mode 100644 bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotInjector.java create mode 100644 bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityInjector.java create mode 100644 connector/src/main/java/org/geysermc/connector/common/GeyserInjector.java diff --git a/bootstrap/bungeecord/pom.xml b/bootstrap/bungeecord/pom.xml index 839b8982c..f49f5f408 100644 --- a/bootstrap/bungeecord/pom.xml +++ b/bootstrap/bungeecord/pom.xml @@ -17,10 +17,11 @@ 1.4.1-SNAPSHOT compile + - net.md-5 - bungeecord-api - 1.16-R0.5-SNAPSHOT + com.github.SpigotMC.BungeeCord + bungeecord-proxy + a7c6ede provided diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeInjector.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeInjector.java new file mode 100644 index 000000000..88fbe105a --- /dev/null +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeInjector.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.platform.bungeecord; + +import com.github.steveice10.packetlib.io.local.LocalServerChannelWrapper; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.local.LocalAddress; +import io.netty.util.AttributeKey; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.config.ListenerInfo; +import net.md_5.bungee.netty.PipelineUtils; +import org.geysermc.connector.bootstrap.GeyserBootstrap; +import org.geysermc.connector.common.GeyserInjector; + +import java.lang.reflect.Method; + +public class GeyserBungeeInjector extends GeyserInjector { + private final ProxyServer proxy; + /** + * Set as a variable so it is only set after the proxy has finished initializing + */ + private ChannelInitializer channelInitializer = null; + + public GeyserBungeeInjector(ProxyServer proxy) { + this.proxy = proxy; + } + + @Override + protected void initializeLocalChannel0(GeyserBootstrap bootstrap) throws Exception { + // TODO - allow Geyser to specify its own listener info properties + if (proxy.getConfig().getListeners().size() != 1) { + throw new UnsupportedOperationException("Geyser does not currently support multiple listeners with injection! " + + "Please reach out to us on our Discord at https://discord.gg/GeyserMC so we can hear feedback on your setup."); + } + ListenerInfo listenerInfo = proxy.getConfig().getListeners().stream().findFirst().orElseThrow(IllegalStateException::new); + + Class proxyClass = proxy.getClass(); + // Using the specified EventLoop is required, or else an error will be thrown + EventLoopGroup bossGroup; + EventLoopGroup workerGroup; + try { + EventLoopGroup eventLoops = (EventLoopGroup) proxyClass.getField("eventLoops").get(proxy); + // Netty redirects ServerBootstrap#group(EventLoopGroup) to #group(EventLoopGroup, EventLoopGroup) and uses the same event loop for both. + bossGroup = eventLoops; + workerGroup = eventLoops; + bootstrap.getGeyserLogger().debug("BungeeCord event loop style detected."); + } catch (NoSuchFieldException e) { + // Waterfall uses two separate event loops + // https://github.com/PaperMC/Waterfall/blob/fea7ec356dba6c6ac28819ff11be604af6eb484e/BungeeCord-Patches/0022-Use-a-worker-and-a-boss-event-loop-group.patch + bossGroup = (EventLoopGroup) proxyClass.getField("bossEventLoopGroup").get(proxy); + workerGroup = (EventLoopGroup) proxyClass.getField("workerEventLoopGroup").get(proxy); + bootstrap.getGeyserLogger().debug("Waterfall event loop style detected."); + } + + // Is currently just AttributeKey.valueOf("ListerInfo") but we might as well copy the value itself. + AttributeKey listener = PipelineUtils.LISTENER; + listenerInfo = new ListenerInfo( + listenerInfo.getSocketAddress(), + listenerInfo.getMotd(), + listenerInfo.getMaxPlayers(), + listenerInfo.getTabListSize(), + listenerInfo.getServerPriority(), + listenerInfo.isForceDefault(), + listenerInfo.getForcedHosts(), + listenerInfo.getTabListType(), + listenerInfo.isSetLocalAddress(), + listenerInfo.isPingPassthrough(), + listenerInfo.getQueryPort(), + listenerInfo.isQueryEnabled(), + bootstrap.getGeyserConfig().getRemote().isUseProxyProtocol() // If Geyser is expecting HAProxy, so should the Bungee end + ); + + // This method is what initializes the connection in Java Edition, after Netty is all set. + Method initChannel = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class); + initChannel.setAccessible(true); + + ChannelFuture channelFuture = (new ServerBootstrap() + .channel(LocalServerChannelWrapper.class) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + if (proxy.getConfig().getServers() == null) { + // Proxy hasn't finished loading all plugins - it loads the config after all plugins + // Probably doesn't need to be translatable? + bootstrap.getGeyserLogger().info("Disconnecting player as Bungee has not finished loading"); + ch.close(); + return; + } + + if (channelInitializer == null) { + // Proxy has finished initializing; we can safely grab this variable without fear of plugins modifying it + // (ViaVersion replaces this to inject) + channelInitializer = PipelineUtils.SERVER_CHILD; + } + initChannel.invoke(channelInitializer, ch); + } + }) + .childAttr(listener, listenerInfo) + .group(bossGroup, workerGroup) + .localAddress(LocalAddress.ANY)) + .bind() + .syncUninterruptibly(); + + this.localChannel = channelFuture; + this.serverSocketAddress = channelFuture.channel().localAddress(); + } +} diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java index adfb00e13..d97446052 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java @@ -40,10 +40,12 @@ import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.platform.bungeecord.command.GeyserBungeeCommandExecutor; import org.geysermc.platform.bungeecord.command.GeyserBungeeCommandManager; +import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.file.Path; import java.util.UUID; import java.util.logging.Level; @@ -52,6 +54,7 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { private GeyserBungeeCommandManager geyserCommandManager; private GeyserBungeeConfiguration geyserConfig; + private GeyserBungeeInjector geyserInjector; private GeyserBungeeLogger geyserLogger; private IGeyserPingPassthrough geyserBungeePingPassthrough; @@ -114,6 +117,9 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { this.connector = GeyserConnector.start(PlatformType.BUNGEECORD, this); + this.geyserInjector = new GeyserBungeeInjector(getProxy()); + this.geyserInjector.initializeLocalChannel(this); + this.geyserCommandManager = new GeyserBungeeCommandManager(connector); if (geyserConfig.isLegacyPingPassthrough()) { @@ -127,7 +133,12 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { @Override public void onDisable() { - connector.shutdown(); + if (connector != null) { + connector.shutdown(); + } + if (geyserInjector != null) { + geyserInjector.shutdown(); + } } @Override @@ -159,4 +170,10 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { public BootstrapDumpInfo getDumpInfo() { return new GeyserBungeeDumpInfo(getProxy()); } + + @Nullable + @Override + public SocketAddress getSocketAddress() { + return this.geyserInjector.getServerSocketAddress(); + } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotInjector.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotInjector.java new file mode 100644 index 000000000..d54414306 --- /dev/null +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotInjector.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.platform.spigot; + +import com.github.steveice10.packetlib.io.local.LocalServerChannelWrapper; +import com.viaversion.viaversion.bukkit.handlers.BukkitChannelInitializer; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.local.LocalAddress; +import org.bukkit.Bukkit; +import org.geysermc.connector.bootstrap.GeyserBootstrap; +import org.geysermc.connector.common.GeyserInjector; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.util.List; + +public class GeyserSpigotInjector extends GeyserInjector { + /** + * Used to determine if ViaVersion is setup to a state where Geyser players will fail at joining if injection is enabled + */ + private final boolean isViaVersion; + /** + * Used to uninject ourselves on shutdown. + */ + private List allServerChannels; + + public GeyserSpigotInjector(boolean isViaVersion) { + this.isViaVersion = isViaVersion; + } + + @Override + @SuppressWarnings("unchecked") + protected void initializeLocalChannel0(GeyserBootstrap bootstrap) throws Exception { + Class serverClazz; + try { + serverClazz = Class.forName("net.minecraft.server.MinecraftServer"); + // We're using 1.17+ + } catch (ClassNotFoundException e) { + // We're using pre-1.17 + String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server"); + serverClazz = Class.forName(prefix + ".MinecraftServer"); + } + Method getServer = serverClazz.getDeclaredMethod("getServer"); + Object server = getServer.invoke(null); + Object connection = null; + // Find the class that manages network IO + for (Method m : serverClazz.getDeclaredMethods()) { + if (m.getReturnType() != null) { + // First is Spigot-mapped name, second is Mojang-mapped name which is implemented as future-proofing + if (m.getReturnType().getSimpleName().equals("ServerConnection") || m.getReturnType().getSimpleName().equals("ServerConnectionListener")) { + if (m.getParameterTypes().length == 0) { + connection = m.invoke(server); + } + } + } + } + if (connection == null) { + throw new RuntimeException("Unable to find ServerConnection class!"); + } + + // Find the channel that Minecraft uses to listen to connections + ChannelFuture listeningChannel = null; + for (Field field : connection.getClass().getDeclaredFields()) { + if (field.getType() != List.class) { + continue; + } + field.setAccessible(true); + boolean rightList = ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0] == ChannelFuture.class; + if (!rightList) continue; + + allServerChannels = (List) field.get(connection); + for (ChannelFuture o : allServerChannels) { + listeningChannel = o; + break; + } + } + if (listeningChannel == null) { + throw new RuntimeException("Unable to find listening channel!"); + } + + // Making this a function prevents childHandler from being treated as a non-final variable + ChannelInitializer childHandler = getChildHandler(bootstrap, listeningChannel); + // This method is what initializes the connection in Java Edition, after Netty is all set. + Method initChannel = childHandler.getClass().getDeclaredMethod("initChannel", Channel.class); + initChannel.setAccessible(true); + + ChannelFuture channelFuture = (new ServerBootstrap() + .channel(LocalServerChannelWrapper.class) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + initChannel.invoke(childHandler, ch); + } + }) + .group(new DefaultEventLoopGroup()) + .localAddress(LocalAddress.ANY)) + .bind() + .syncUninterruptibly(); + // We don't need to add to the list, but plugins like ProtocolSupport and ProtocolLib that add to the main pipeline + // will work when we add to the list. + allServerChannels.add(channelFuture); + this.localChannel = channelFuture; + this.serverSocketAddress = channelFuture.channel().localAddress(); + } + + @SuppressWarnings("unchecked") + private ChannelInitializer getChildHandler(GeyserBootstrap bootstrap, ChannelFuture listeningChannel) { + List names = listeningChannel.channel().pipeline().names(); + ChannelInitializer childHandler = null; + for (String name : names) { + ChannelHandler handler = listeningChannel.channel().pipeline().get(name); + try { + Field childHandlerField = handler.getClass().getDeclaredField("childHandler"); + childHandlerField.setAccessible(true); + childHandler = (ChannelInitializer) childHandlerField.get(handler); + // ViaVersion non-Paper-injector workaround so we aren't double-injecting + if (isViaVersion && childHandler instanceof BukkitChannelInitializer) { + childHandler = ((BukkitChannelInitializer) childHandler).getOriginal(); + } + break; + } catch (Exception e) { + if (bootstrap.getGeyserConfig().isDebugMode()) { + e.printStackTrace(); + } + } + } + if (childHandler == null) { + throw new RuntimeException(); + } + return childHandler; + } + + @Override + public void shutdown() { + if (this.allServerChannels != null) { + this.allServerChannels.remove(this.localChannel); + this.allServerChannels = null; + } + super.shutdown(); + } +} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java index 4c67a931b..e6b2ee0cb 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java @@ -54,6 +54,7 @@ import org.geysermc.platform.spigot.world.manager.*; import java.io.File; import java.io.IOException; +import java.net.SocketAddress; import java.nio.file.Path; import java.util.List; import java.util.UUID; @@ -62,6 +63,7 @@ import java.util.logging.Level; public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { private GeyserSpigotCommandManager geyserCommandManager; private GeyserSpigotConfiguration geyserConfig; + private GeyserSpigotInjector geyserInjector; private GeyserSpigotLogger geyserLogger; private IGeyserPingPassthrough geyserSpigotPingPassthrough; private GeyserSpigotWorldManager geyserWorldManager; @@ -176,6 +178,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { // Set if we need to use a different method for getting a player's locale SpigotCommandSender.setUseLegacyLocaleMethod(isPre1_12); + // We want to do this late in the server startup process to allow plugins such as ViaVersion and ProtocolLib + // To do their job injecting, then connect into *that* + this.geyserInjector = new GeyserSpigotInjector(isViaVersion); + this.geyserInjector.initializeLocalChannel(this); + if (connector.getConfig().isUseAdapters()) { try { String name = Bukkit.getServer().getClass().getPackage().getName(); @@ -233,6 +240,9 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { if (connector != null) { connector.shutdown(); } + if (geyserInjector != null) { + geyserInjector.shutdown(); + } } @Override @@ -275,6 +285,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { return this.minecraftVersion; } + @Override + public SocketAddress getSocketAddress() { + return this.geyserInjector.getServerSocketAddress(); + } + public boolean isCompatible(String version, String whichVersion) { int[] currentVersion = parseVersion(version); int[] otherVersion = parseVersion(whichVersion); diff --git a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityInjector.java b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityInjector.java new file mode 100644 index 000000000..d13427faa --- /dev/null +++ b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityInjector.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.platform.velocity; + +import com.github.steveice10.packetlib.io.local.LocalServerChannelWrapper; +import com.velocitypowered.api.proxy.ProxyServer; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.local.LocalAddress; +import org.geysermc.connector.bootstrap.GeyserBootstrap; +import org.geysermc.connector.common.GeyserInjector; + +import java.lang.reflect.Field; +import java.util.function.Supplier; + +public class GeyserVelocityInjector extends GeyserInjector { + private final ProxyServer proxy; + + public GeyserVelocityInjector(ProxyServer proxy) { + this.proxy = proxy; + } + + @Override + @SuppressWarnings("unchecked") + protected void initializeLocalChannel0(GeyserBootstrap bootstrap) throws Exception { + Field cm = proxy.getClass().getDeclaredField("cm"); + cm.setAccessible(true); + Object connectionManager = cm.get(proxy); + Class connectionManagerClass = connectionManager.getClass(); + + Supplier> serverChannelInitializerHolder = (Supplier>) connectionManagerClass + .getMethod("getServerChannelInitializer") + .invoke(connectionManager); + ChannelInitializer channelInitializer = serverChannelInitializerHolder.get(); + + // Is set on Velocity's end for listening to Java connections - required on ours or else the initial world load process won't finish sometimes + Field serverWriteMarkField = connectionManagerClass.getDeclaredField("SERVER_WRITE_MARK"); + serverWriteMarkField.setAccessible(true); + WriteBufferWaterMark serverWriteMark = (WriteBufferWaterMark) serverWriteMarkField.get(null); + + EventLoopGroup bossGroup = (EventLoopGroup) connectionManagerClass.getMethod("getBossGroup").invoke(connectionManager); + + Field workerGroupField = connectionManagerClass.getDeclaredField("workerGroup"); + workerGroupField.setAccessible(true); + EventLoopGroup workerGroup = (EventLoopGroup) workerGroupField.get(connectionManager); + + ChannelFuture channelFuture = (new ServerBootstrap() + .channel(LocalServerChannelWrapper.class) + .childHandler(channelInitializer) + .group(bossGroup, workerGroup) // Cannot be DefaultEventLoopGroup + .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverWriteMark) // Required or else rare network freezes can occur + .localAddress(LocalAddress.ANY)) + .bind() + .syncUninterruptibly(); + + this.localChannel = channelFuture; + this.serverSocketAddress = channelFuture.channel().localAddress(); + } +} diff --git a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java index df25167e6..0802d07c2 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java @@ -28,6 +28,7 @@ package org.geysermc.platform.velocity; import com.google.inject.Inject; import com.velocitypowered.api.command.CommandManager; import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ListenerBoundEvent; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.Plugin; @@ -45,11 +46,13 @@ import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.platform.velocity.command.GeyserVelocityCommandExecutor; import org.geysermc.platform.velocity.command.GeyserVelocityCommandManager; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.file.Path; import java.nio.file.Paths; import java.util.UUID; @@ -68,6 +71,7 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { private GeyserVelocityCommandManager geyserCommandManager; private GeyserVelocityConfiguration geyserConfig; + private GeyserVelocityInjector geyserInjector; private GeyserVelocityLogger geyserLogger; private IGeyserPingPassthrough geyserPingPassthrough; @@ -130,6 +134,9 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { this.connector = GeyserConnector.start(PlatformType.VELOCITY, this); + this.geyserInjector = new GeyserVelocityInjector(proxyServer); + // Will be initialized after the proxy has been bound + this.geyserCommandManager = new GeyserVelocityCommandManager(connector); this.commandManager.register("geyser", new GeyserVelocityCommandExecutor(connector)); if (geyserConfig.isLegacyPingPassthrough()) { @@ -141,7 +148,12 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { @Override public void onDisable() { - connector.shutdown(); + if (connector != null) { + connector.shutdown(); + } + if (geyserInjector != null) { + geyserInjector.shutdown(); + } } @Override @@ -174,8 +186,20 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { onDisable(); } + @Subscribe + public void onProxyBound(ListenerBoundEvent event) { + // After this bound, we know that the channel initializer cannot change without it being ineffective for Velocity, too + geyserInjector.initializeLocalChannel(this); + } + @Override public BootstrapDumpInfo getDumpInfo() { return new GeyserVelocityDumpInfo(proxyServer); } + + @Nullable + @Override + public SocketAddress getSocketAddress() { + return this.geyserInjector.getServerSocketAddress(); + } } diff --git a/connector/pom.xml b/connector/pom.xml index 04cc2c117..34a31e89f 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -156,10 +156,10 @@ com.github.GeyserMC PacketLib - 0b75570 + 25eb4c4 compile - + io.netty netty-all diff --git a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java index 59dc58c2b..ef0e0068c 100644 --- a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java +++ b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java @@ -34,6 +34,7 @@ import org.geysermc.connector.network.translators.world.GeyserWorldManager; import org.geysermc.connector.network.translators.world.WorldManager; import javax.annotation.Nullable; +import java.net.SocketAddress; import java.nio.file.Path; public interface GeyserBootstrap { @@ -114,4 +115,9 @@ public interface GeyserBootstrap { default String getMinecraftServerVersion() { return null; } + + @Nullable + default SocketAddress getSocketAddress() { + return null; + } } diff --git a/connector/src/main/java/org/geysermc/connector/common/GeyserInjector.java b/connector/src/main/java/org/geysermc/connector/common/GeyserInjector.java new file mode 100644 index 000000000..f7da49727 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/common/GeyserInjector.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.common; + +import io.netty.channel.ChannelFuture; +import lombok.Getter; +import org.geysermc.connector.bootstrap.GeyserBootstrap; + +import java.net.SocketAddress; + +/** + * Used to inject Geyser clients directly into the server, bypassing the need to implement a complete TCP connection, + * by creating a local channel. + */ +public abstract class GeyserInjector { + /** + * The local channel we can use to inject ourselves into the server without creating a TCP connection. + */ + protected ChannelFuture localChannel; + /** + * The LocalAddress to use to connect to the server without connecting over TCP. + */ + @Getter + protected SocketAddress serverSocketAddress; + + /** + * + * @param bootstrap the bootstrap of the Geyser instance. + */ + public void initializeLocalChannel(GeyserBootstrap bootstrap) { + if (!bootstrap.getGeyserConfig().isUseDirectConnection()) { + bootstrap.getGeyserLogger().debug("Disabling direct injection!"); + return; + } + + if (this.localChannel != null) { + bootstrap.getGeyserLogger().warning("Geyser attempted to inject into the server connection handler twice! Please ensure you aren't using /reload or any plugin that (re)loads Geyser after the server has started."); + return; + } + + try { + initializeLocalChannel0(bootstrap); + bootstrap.getGeyserLogger().debug("Local injection succeeded!"); + } catch (Exception e) { + e.printStackTrace(); + // If the injector partially worked, undo it + shutdown(); + } + } + + /** + * The method to implement that is called by {@link #initializeLocalChannel(GeyserBootstrap)} wrapped around a try/catch. + */ + protected abstract void initializeLocalChannel0(GeyserBootstrap bootstrap) throws Exception; + + public void shutdown() { + if (localChannel != null && localChannel.channel().isOpen()) { + try { + localChannel.channel().close().sync(); + localChannel = null; + } catch (Exception e) { + e.printStackTrace(); + } + } else if (localChannel != null) { + localChannel = null; + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java index d0195df70..a48ce030b 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -173,6 +173,8 @@ public interface GeyserConfiguration { boolean isUseAdapters(); + boolean isUseDirectConnection(); + int getConfigVersion(); static void checkGeyserConfiguration(GeyserConfiguration geyserConfig, GeyserLogger geyserLogger) { diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java index 05dfdb51a..b502d81ca 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -28,6 +28,9 @@ package org.geysermc.connector.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import lombok.Getter; import lombok.Setter; @@ -35,7 +38,9 @@ import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.common.AuthType; import org.geysermc.connector.common.serializer.AsteriskSerializer; import org.geysermc.connector.network.CIDRMatcher; +import org.geysermc.connector.utils.LanguageUtils; +import java.io.IOException; import java.nio.file.Path; import java.util.Collections; import java.util.List; @@ -45,6 +50,7 @@ import java.util.stream.Collectors; @Getter @JsonIgnoreProperties(ignoreUnknown = true) +@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final public abstract class GeyserJacksonConfiguration implements GeyserConfiguration { /** @@ -191,6 +197,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @AsteriskSerializer.Asterisk(isIp = true) private String address = "auto"; + @JsonDeserialize(using = PortDeserializer.class) @Setter private int port = 25565; @@ -243,6 +250,25 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("use-adapters") private boolean useAdapters = true; + @JsonProperty("use-direct-connection") + private boolean useDirectConnection = true; + @JsonProperty("config-version") private int configVersion = 0; + + /** + * Ensure that the port deserializes in the config as a number no matter what. + */ + protected static class PortDeserializer extends JsonDeserializer { + @Override + public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String value = p.getValueAsString(); + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + System.err.println(LanguageUtils.getLocaleStringLog("geyser.bootstrap.config.invalid_port")); + return 25565; + } + } + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index cea9b3052..c8d60c1ea 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -70,6 +70,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NonNull; import lombok.Setter; +import org.geysermc.common.PlatformType; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.common.AuthType; @@ -744,7 +745,17 @@ public class GeyserSession implements CommandSender { disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); return; } - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteAddress)); + + if (downstream.isInternallyConnecting()) { + // Connected directly to the server + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect_internal", + authData.getName(), protocol.getProfile().getName())); + } else { + // Connected to an IP address + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", + authData.getName(), protocol.getProfile().getName(), remoteAddress)); + } + UUID uuid = protocol.getProfile().getId(); if (uuid == null) { // Set what our UUID *probably* is going to be @@ -774,7 +785,11 @@ public class GeyserSession implements CommandSender { public void disconnected(DisconnectedEvent event) { loggingIn = false; loggedIn = false; - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteAddress, event.getReason())); + if (downstream != null && downstream.isInternallyConnecting()) { + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect_internal", authData.getName(), event.getReason())); + } else { + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteAddress, event.getReason())); + } if (event.getCause() != null) { event.getCause().printStackTrace(); } @@ -819,7 +834,26 @@ public class GeyserSession implements CommandSender { if (!daylightCycle) { setDaylightCycle(true); } - downstream.connect(); + boolean internalConnect = false; + if (connector.getBootstrap().getSocketAddress() != null) { + try { + // Only affects Waterfall, but there is no sure way to differentiate between a proxy with this patch and a proxy without this patch + // Patch causing the issue: https://github.com/PaperMC/Waterfall/blob/7e6af4cef64d5d377a6ffd00a534379e6efa94cf/BungeeCord-Patches/0045-Don-t-use-a-bytebuf-for-packet-decoding.patch + // If native compression is enabled, then this line is tripped up if a heap buffer is sent over in such a situation + // as a new direct buffer is not created with that patch (HeapByteBufs throw an UnsupportedOperationException here): + // https://github.com/SpigotMC/BungeeCord/blob/a283aaf724d4c9a815540cd32f3aafaa72df9e05/native/src/main/java/net/md_5/bungee/jni/zlib/NativeZlib.java#L43 + // This issue could be mitigated down the line by preventing Bungee from setting compression + downstream.setFlag(BuiltinFlags.USE_ONLY_DIRECT_BUFFERS, connector.getPlatformType() == PlatformType.BUNGEECORD); + + downstream.connectInternal(connector.getBootstrap().getSocketAddress(), upstream.getAddress().getAddress().getHostAddress(), true); + internalConnect = true; + } catch (Exception e) { + e.printStackTrace(); + } + } + if (!internalConnect) { + downstream.connect(); + } connector.addPlayer(this); } diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index 3cbdb2e55..0d87c0c02 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -37,12 +37,13 @@ bedrock: remote: # The IP address of the remote (Java Edition) server # If it is "auto", for standalone version the remote address will be set to 127.0.0.1, - # for plugin versions, Geyser will attempt to find the best address to connect to. + # for plugin versions, it is recommended to keep this as "auto" so Geyser will automatically configure address, port, and auth-type. address: auto # The port of the remote (Java Edition) server # For plugin versions, if address has been set to "auto", the port will also follow the server's listening port. port: 25565 # Authentication type. Can be offline, online, or floodgate (see https://github.com/GeyserMC/Geyser/wiki/Floodgate). + # For plugin versions, it's recommended to keep the `address` field to "auto" so Floodgate support is automatically configured. auth-type: online # Allow for password-based authentication methods through Geyser. Only useful in online mode. # If this is false, users must authenticate to Microsoft using a code provided by Geyser on their desktop. @@ -65,6 +66,7 @@ extended-world-height: false # Floodgate uses encryption to ensure use from authorised sources. # This should point to the public key generated by Floodgate (BungeeCord, Spigot or Velocity) # You can ignore this when not using Floodgate. +# If you're using a plugin version of Floodgate on the same server, the key will automatically be picked up from Floodgate. floodgate-key-file: key.pem # The Xbox/Minecraft Bedrock username is the key for the Java server auth-info. @@ -197,4 +199,10 @@ enable-proxy-connections: false # Turning this off for Spigot will stop NMS from being used but will have a performance impact. use-adapters: true +# Whether to connect directly into the Java server without creating a TCP connection. +# This should only be disabled if a plugin that interfaces with packets or the network does not work correctly with Geyser. +# If enabled on plugin versions, the remote address and port sections are ignored +# If disabled on plugin versions, expect performance decrease and latency increase +use-direct-connection: true + config-version: 4