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 extends ProxyServer> 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