Allow configuration of RakNet limits (#4532)

* Allow configuration of RakNet limits

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Validate packet limiter system properties

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

---------

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>
This commit is contained in:
Kas-tle 2024-03-31 21:42:31 -07:00 committed by GitHub
parent fbafdbb2a7
commit c9ca4c82f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 102 additions and 7 deletions

View File

@ -40,9 +40,11 @@ import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.network.CIDRMatcher;
import org.geysermc.geyser.text.AsteriskSerializer;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.WebUtils;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@ -233,7 +235,18 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
List<CIDRMatcher> matchers = this.whitelistedIPsMatchers;
if (matchers == null) {
synchronized (this) {
this.whitelistedIPsMatchers = matchers = proxyProtocolWhitelistedIPs.stream()
// Check if proxyProtocolWhitelistedIPs contains URLs we need to fetch and parse by line
List<String> whitelistedCIDRs = new ArrayList<>();
for (String ip: proxyProtocolWhitelistedIPs) {
if (!ip.startsWith("http")) {
whitelistedCIDRs.add(ip);
continue;
}
WebUtils.getLineStream(ip).forEach(whitelistedCIDRs::add);
}
this.whitelistedIPsMatchers = matchers = whitelistedCIDRs.stream()
.map(CIDRMatcher::new)
.collect(Collectors.toList());
}

View File

@ -46,6 +46,7 @@ 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.command.defaults.ConnectionTestCommand;
@ -71,6 +72,10 @@ 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"));
@ -141,23 +146,31 @@ public final class GeyserServer {
bootstrapFutures = new ChannelFuture[listenCount];
for (int i = 0; i < listenCount; i++) {
ChannelFuture future = bootstrap.bind(address);
addHandlers(future);
modifyHandlers(future);
bootstrapFutures[i] = future;
}
return Bootstraps.allOf(bootstrapFutures);
}
private void addHandlers(ChannelFuture future) {
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
if (this.geyser.getConfig().getBedrock().isEnableProxyProtocol()) {
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() {
@ -199,11 +212,26 @@ public final class GeyserServer {
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);
}
@ -352,6 +380,27 @@ public final class GeyserServer {
}
}
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) {

View File

@ -27,6 +27,7 @@ package org.geysermc.geyser.util;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.geysermc.geyser.GeyserBootstrap;
@ -56,6 +57,8 @@ public class FileUtils {
*/
public static <T> T loadConfig(File src, Class<T> valueType) throws IOException {
ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory())
// Allow inference of single values as arrays
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));
return objectMapper.readValue(src, valueType);
}

View File

@ -40,6 +40,7 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Map;
import java.util.stream.Stream;
public class WebUtils {
@ -176,6 +177,13 @@ public class WebUtils {
return connectionToString(con);
}
/**
* Find a SRV record for the given address
*
* @param geyser Geyser instance
* @param remoteAddress Address to find the SRV record for
* @return The SRV record or null if not found
*/
public static String @Nullable [] findSrvRecord(GeyserImpl geyser, String remoteAddress) {
try {
// Searches for a server address and a port from a SRV record of the specified host name
@ -193,4 +201,26 @@ public class WebUtils {
}
return null;
}
/**
* Get a stream of lines from the given URL
*
* @param reqURL URL to fetch
* @return Stream of lines from the URL or an empty stream if the request fails
*/
public static Stream<String> getLineStream(String reqURL) {
try {
URL url = new URL(reqURL);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().toString() + "/" + GeyserImpl.VERSION); // Otherwise Java 8 fails on checking updates
con.setConnectTimeout(10000);
con.setReadTimeout(10000);
return connectionToString(con).lines();
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error("Error while trying to get a stream from " + reqURL, e);
return Stream.empty();
}
}
}

View File

@ -39,8 +39,8 @@ bedrock:
# A list of allowed PROXY protocol speaking proxy IP addresses/subnets. Only effective when "enable-proxy-protocol" is enabled, and
# should really only be used when you are not able to use a proper firewall (usually true with shared hosting providers etc.).
# Keeping this list empty means there is no IP address whitelist.
# Both IP addresses and subnets are supported.
#proxy-protocol-whitelisted-ips: [ "127.0.0.1", "172.18.0.0/16" ]
# IP addresses, subnets, and links to plain text files are supported.
#proxy-protocol-whitelisted-ips: [ "127.0.0.1", "172.18.0.0/16", "https://example.com/whitelist.txt" ]
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,

View File

@ -11,7 +11,7 @@ gson = "2.3.1" # Provided by Spigot 1.8.8
websocket = "1.5.1"
protocol = "3.0.0.Beta1-20240313.120922-126"
protocol-connection = "3.0.0.Beta1-20240313.120922-125"
raknet = "1.0.0.CR1-20240330.101522-15"
raknet = "1.0.0.CR1-20240330.103819-16"
blockstateupdater="1.20.70-20240303.125052-2"
mcauthlib = "d9d773e"
mcprotocollib = "1.20.4-2-20240116.220521-7"