/* * Copyright (c) 2019-2022 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.geyser.network.netty; import com.github.steveice10.packetlib.helper.TransportHelper; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollDatagramChannel; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.kqueue.KQueue; import io.netty.channel.kqueue.KQueueDatagramChannel; import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.util.concurrent.Future; import lombok.Getter; import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; import org.cloudburstmc.netty.channel.raknet.RakChannelFactory; import org.cloudburstmc.netty.channel.raknet.config.RakChannelOption; import org.cloudburstmc.netty.handler.codec.raknet.server.RakServerOfflineHandler; import org.cloudburstmc.netty.handler.codec.raknet.server.RakServerRateLimiter; import org.cloudburstmc.protocol.bedrock.BedrockPong; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.connection.ConnectionRequestEvent; import org.geysermc.geyser.command.defaults.ConnectionTestCommand; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.event.type.GeyserBedrockPingEventImpl; import org.geysermc.geyser.network.CIDRMatcher; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.network.GeyserServerInitializer; import org.geysermc.geyser.network.netty.handler.RakConnectionRequestHandler; import org.geysermc.geyser.network.netty.handler.RakPingHandler; import org.geysermc.geyser.network.netty.proxy.ProxyServerHandler; import org.geysermc.geyser.ping.GeyserPingInfo; import org.geysermc.geyser.ping.IGeyserPingPassthrough; import org.geysermc.geyser.skin.SkinProvider; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.translator.text.MessageTranslator; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.IntFunction; import java.util.function.Supplier; import static org.cloudburstmc.netty.channel.raknet.RakConstants.DEFAULT_GLOBAL_PACKET_LIMIT; import static org.cloudburstmc.netty.channel.raknet.RakConstants.DEFAULT_OFFLINE_PACKET_LIMIT; import static org.cloudburstmc.netty.channel.raknet.RakConstants.DEFAULT_PACKET_LIMIT; public final class GeyserServer { private static final boolean PRINT_DEBUG_PINGS = Boolean.parseBoolean(System.getProperty("Geyser.PrintPingsInDebugMode", "true")); /* The following constants are all used to ensure the ping does not reach a length where it is unparsable by the Bedrock client */ private static final int MINECRAFT_VERSION_BYTES_LENGTH = GameProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion().getBytes(StandardCharsets.UTF_8).length; private static final int BRAND_BYTES_LENGTH = GeyserImpl.NAME.getBytes(StandardCharsets.UTF_8).length; /** * The MOTD, sub-MOTD and Minecraft version ({@link #MINECRAFT_VERSION_BYTES_LENGTH}) combined cannot reach this length. */ private static final int MAGIC_RAKNET_LENGTH = 338; private static final Transport TRANSPORT = compatibleTransport(); /** * See {@link EventLoopGroup#shutdownGracefully(long, long, TimeUnit)} */ private static final int SHUTDOWN_QUIET_PERIOD_MS = 100; private static final int SHUTDOWN_TIMEOUT_MS = 500; private final GeyserImpl geyser; private EventLoopGroup group; // Split childGroup may improve IO private EventLoopGroup childGroup; private final ServerBootstrap bootstrap; private EventLoopGroup playerGroup; @Getter private final ExpiringMap proxiedAddresses; private final int listenCount; private ChannelFuture[] bootstrapFutures; /** * The port to broadcast in the pong. This can be different from the port the server is bound to, e.g. due to port forwarding. */ private final int broadcastPort; public GeyserServer(GeyserImpl geyser, int threadCount) { this.geyser = geyser; this.listenCount = Bootstraps.isReusePortAvailable() ? Integer.getInteger("Geyser.ListenCount", 2) : 1; GeyserImpl.getInstance().getLogger().debug("Listen thread count: " + listenCount); this.group = TRANSPORT.eventLoopGroupFactory().apply(listenCount); this.childGroup = TRANSPORT.eventLoopGroupFactory().apply(threadCount); this.bootstrap = this.createBootstrap(); // setup SO_REUSEPORT if exists Bootstraps.setupBootstrap(this.bootstrap); if (this.geyser.getConfig().getBedrock().isEnableProxyProtocol()) { this.proxiedAddresses = ExpiringMap.builder() .expiration(30 + 1, TimeUnit.MINUTES) .expirationPolicy(ExpirationPolicy.ACCESSED).build(); } else { this.proxiedAddresses = null; } // It's set to 0 only if no system property or manual config value was set if (geyser.getConfig().getBedrock().broadcastPort() == 0) { geyser.getConfig().getBedrock().setBroadcastPort(geyser.getConfig().getBedrock().port()); } this.broadcastPort = geyser.getConfig().getBedrock().broadcastPort(); } public CompletableFuture bind(InetSocketAddress address) { bootstrapFutures = new ChannelFuture[listenCount]; for (int i = 0; i < listenCount; i++) { ChannelFuture future = bootstrap.bind(address); modifyHandlers(future); bootstrapFutures[i] = future; } return Bootstraps.allOf(bootstrapFutures); } private void modifyHandlers(ChannelFuture future) { Channel channel = future.channel(); // Add our ping handler channel.pipeline() .addFirst(RakConnectionRequestHandler.NAME, new RakConnectionRequestHandler(this)) .addAfter(RakServerOfflineHandler.NAME, RakPingHandler.NAME, new RakPingHandler(this)); // Add proxy handler boolean isProxyProtocol = this.geyser.getConfig().getBedrock().isEnableProxyProtocol(); if (isProxyProtocol) { channel.pipeline().addFirst("proxy-protocol-decoder", new ProxyServerHandler()); } boolean isWhitelistedProxyProtocol = isProxyProtocol && !this.geyser.getConfig().getBedrock().getProxyProtocolWhitelistedIPs().isEmpty(); if (Boolean.parseBoolean(System.getProperty("Geyser.RakRateLimitingDisabled", "false")) || isWhitelistedProxyProtocol) { // We would already block any non-whitelisted IP addresses in onConnectionRequest so we can remove the rate limiter channel.pipeline().remove(RakServerRateLimiter.NAME); } } public void shutdown() { try { Future futureChildGroup = this.childGroup.shutdownGracefully(SHUTDOWN_QUIET_PERIOD_MS, SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS); this.childGroup = null; Future futureGroup = this.group.shutdownGracefully(SHUTDOWN_QUIET_PERIOD_MS, SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS); this.group = null; Future futurePlayerGroup = this.playerGroup.shutdownGracefully(SHUTDOWN_QUIET_PERIOD_MS, SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS); this.playerGroup = null; futureChildGroup.sync(); futureGroup.sync(); futurePlayerGroup.sync(); SkinProvider.shutdown(); } catch (InterruptedException e) { GeyserImpl.getInstance().getLogger().severe("Exception in shutdown process", e); } for (ChannelFuture f : bootstrapFutures) { f.channel().closeFuture().syncUninterruptibly(); } } private ServerBootstrap createBootstrap() { if (this.geyser.getConfig().isDebugMode()) { this.geyser.getLogger().debug("EventLoop type: " + TRANSPORT.datagramChannel()); if (TRANSPORT.datagramChannel() == NioDatagramChannel.class) { if (System.getProperties().contains("disableNativeEventLoop")) { this.geyser.getLogger().debug("EventLoop type is NIO because native event loops are disabled."); } else { // Use lambda here, not method reference, or else NoClassDefFoundError for Epoll/KQueue will not be caught this.geyser.getLogger().debug("Reason for no Epoll: " + throwableOrCaught(() -> Epoll.unavailabilityCause())); this.geyser.getLogger().debug("Reason for no KQueue: " + throwableOrCaught(() -> KQueue.unavailabilityCause())); } } } GeyserServerInitializer serverInitializer = new GeyserServerInitializer(this.geyser); playerGroup = serverInitializer.getEventLoopGroup(); this.geyser.getLogger().debug("Setting MTU to " + this.geyser.getConfig().getMtu()); int rakPacketLimit = positivePropOrDefault("Geyser.RakPacketLimit", DEFAULT_PACKET_LIMIT); this.geyser.getLogger().debug("Setting RakNet packet limit to " + rakPacketLimit); boolean isWhitelistedProxyProtocol = this.geyser.getConfig().getBedrock().isEnableProxyProtocol() && !this.geyser.getConfig().getBedrock().getProxyProtocolWhitelistedIPs().isEmpty(); int rakOfflinePacketLimit = positivePropOrDefault("Geyser.RakOfflinePacketLimit", isWhitelistedProxyProtocol ? Integer.MAX_VALUE : DEFAULT_OFFLINE_PACKET_LIMIT); this.geyser.getLogger().debug("Setting RakNet offline packet limit to " + rakOfflinePacketLimit); int rakGlobalPacketLimit = positivePropOrDefault("Geyser.RakGlobalPacketLimit", DEFAULT_GLOBAL_PACKET_LIMIT); this.geyser.getLogger().debug("Setting RakNet global packet limit to " + rakGlobalPacketLimit); return new ServerBootstrap() .channelFactory(RakChannelFactory.server(TRANSPORT.datagramChannel())) .group(group, childGroup) .option(RakChannelOption.RAK_HANDLE_PING, true) .option(RakChannelOption.RAK_MAX_MTU, this.geyser.getConfig().getMtu()) .option(RakChannelOption.RAK_PACKET_LIMIT, rakPacketLimit) .option(RakChannelOption.RAK_OFFLINE_PACKET_LIMIT, rakOfflinePacketLimit) .option(RakChannelOption.RAK_GLOBAL_PACKET_LIMIT, rakGlobalPacketLimit) .childHandler(serverInitializer); } public boolean onConnectionRequest(InetSocketAddress inetSocketAddress) { List allowedProxyIPs = geyser.getConfig().getBedrock().getProxyProtocolWhitelistedIPs(); if (geyser.getConfig().getBedrock().isEnableProxyProtocol() && !allowedProxyIPs.isEmpty()) { boolean isWhitelistedIP = false; for (CIDRMatcher matcher : geyser.getConfig().getBedrock().getWhitelistedIPsMatchers()) { if (matcher.matches(inetSocketAddress.getAddress())) { isWhitelistedIP = true; break; } } if (!isWhitelistedIP) { return false; } } String ip; if (geyser.getConfig().isLogPlayerIpAddresses()) { if (geyser.getConfig().getBedrock().isEnableProxyProtocol()) { ip = this.proxiedAddresses.getOrDefault(inetSocketAddress, inetSocketAddress).toString(); } else { ip = inetSocketAddress.toString(); } } else { ip = ""; } ConnectionRequestEvent requestEvent = new ConnectionRequestEvent(inetSocketAddress, this.proxiedAddresses.get(inetSocketAddress)); geyser.eventBus().fire(requestEvent); if (requestEvent.isCancelled()) { geyser.getLogger().debug("Connection request from " + ip + " was cancelled using the API!"); return false; } geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.attempt_connect", ip)); return true; } public BedrockPong onQuery(Channel channel, InetSocketAddress inetSocketAddress) { if (geyser.getConfig().isDebugMode() && PRINT_DEBUG_PINGS) { String ip; if (geyser.getConfig().isLogPlayerIpAddresses()) { if (geyser.getConfig().getBedrock().isEnableProxyProtocol()) { ip = this.proxiedAddresses.getOrDefault(inetSocketAddress, inetSocketAddress).toString(); } else { ip = inetSocketAddress.toString(); } } else { ip = ""; } geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.network.pinged", ip)); } GeyserConfiguration config = geyser.getConfig(); GeyserPingInfo pingInfo = null; if (config.isPassthroughMotd() || config.isPassthroughPlayerCounts()) { IGeyserPingPassthrough pingPassthrough = geyser.getBootstrap().getGeyserPingPassthrough(); if (pingPassthrough != null) { pingInfo = pingPassthrough.getPingInformation(inetSocketAddress); } } BedrockPong pong = new BedrockPong() .edition("MCPE") .gameType("Survival") // Can only be Survival or Creative as of 1.16.210.59 .nintendoLimited(false) .protocolVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()) .version(GameProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion()) // Required to not be empty as of 1.16.210.59. Can only contain . and numbers. .ipv4Port(this.broadcastPort) .ipv6Port(this.broadcastPort) .serverId(channel.config().getOption(RakChannelOption.RAK_GUID)); if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) { String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n"); String mainMotd = (motd.length > 0) ? motd[0] : config.getBedrock().primaryMotd(); // First line of the motd. String subMotd = (motd.length > 1) ? motd[1] : config.getBedrock().secondaryMotd(); // Second line of the motd if present, otherwise default. pong.motd(mainMotd.trim()); pong.subMotd(subMotd.trim()); // Trimmed to shift it to the left, prevents the universe from collapsing on us just because we went 2 characters over the text box's limit. } else { pong.motd(config.getBedrock().primaryMotd()); pong.subMotd(config.getBedrock().secondaryMotd()); } // Placed here to prevent overriding values set in the ping event. if (config.isPassthroughPlayerCounts() && pingInfo != null) { pong.playerCount(pingInfo.getPlayers().getOnline()); pong.maximumPlayerCount(pingInfo.getPlayers().getMax()); } else { pong.playerCount(geyser.getSessionManager().getSessions().size()); pong.maximumPlayerCount(config.getMaxPlayers()); } this.geyser.eventBus().fire(new GeyserBedrockPingEventImpl(pong, inetSocketAddress)); // https://github.com/GeyserMC/Geyser/issues/3388 pong.motd(pong.motd().replace(';', ':')); pong.subMotd(pong.subMotd().replace(';', ':')); // Fallbacks to prevent errors and allow Bedrock to see the server if (pong.motd() == null || pong.motd().isBlank()) { pong.motd(GeyserImpl.NAME); } if (pong.subMotd() == null || pong.subMotd().isBlank()) { // Sub-MOTD cannot be empty as of 1.16.210.59 pong.subMotd(GeyserImpl.NAME); } if (ConnectionTestCommand.CONNECTION_TEST_MOTD != null) { // Force-override as we are testing the connection and want to verify we are connecting to the right server through the MOTD pong.motd(ConnectionTestCommand.CONNECTION_TEST_MOTD); pong.subMotd(GeyserImpl.NAME); } // The ping will not appear if the MOTD + sub-MOTD is of a certain length. // We don't know why, though byte[] motdArray = pong.motd().getBytes(StandardCharsets.UTF_8); int subMotdLength = pong.subMotd().getBytes(StandardCharsets.UTF_8).length; if (motdArray.length + subMotdLength > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH)) { // Shorten the sub-MOTD first since that only appears locally if (subMotdLength > BRAND_BYTES_LENGTH) { pong.subMotd(GeyserImpl.NAME); subMotdLength = BRAND_BYTES_LENGTH; } if (motdArray.length > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength)) { // If the top MOTD is still too long, we chop it down byte[] newMotdArray = new byte[MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength]; System.arraycopy(motdArray, 0, newMotdArray, 0, newMotdArray.length); pong.motd(new String(newMotdArray, StandardCharsets.UTF_8)); } } //Bedrock will not even attempt a connection if the client thinks the server is full //so we have to fake it not being full if (pong.playerCount() >= pong.maximumPlayerCount()) { pong.maximumPlayerCount(pong.playerCount() + 1); } return pong; } /** * @return the throwable from the given supplier, or the throwable caught while calling the supplier. */ private static Throwable throwableOrCaught(Supplier supplier) { try { return supplier.get(); } catch (Throwable throwable) { return throwable; } } private static int positivePropOrDefault(String property, int defaultValue) { String value = System.getProperty(property); try { int parsed = value != null ? Integer.parseInt(value) : defaultValue; if (parsed < 1) { GeyserImpl.getInstance().getLogger().warning( "Non-postive integer value for " + property + ": " + value + ". Using default value: " + defaultValue ); return defaultValue; } return parsed; } catch (NumberFormatException e) { GeyserImpl.getInstance().getLogger().warning( "Invalid integer value for " + property + ": " + value + ". Using default value: " + defaultValue ); return defaultValue; } } private static Transport compatibleTransport() { TransportHelper.TransportMethod transportMethod = TransportHelper.determineTransportMethod(); if (transportMethod == TransportHelper.TransportMethod.EPOLL) { return new Transport(EpollDatagramChannel.class, EpollEventLoopGroup::new); } if (transportMethod == TransportHelper.TransportMethod.KQUEUE) { return new Transport(KQueueDatagramChannel.class, KQueueEventLoopGroup::new); } // if (transportMethod == TransportHelper.TransportMethod.IO_URING) { // return new Transport(IOUringDatagramChannel.class, IOUringEventLoopGroup::new); // } return new Transport(NioDatagramChannel.class, NioEventLoopGroup::new); } private record Transport(Class datagramChannel, IntFunction eventLoopGroupFactory) { } }