From fcbb9c9215ae78a232893e9b87208eb0cbfd9a88 Mon Sep 17 00:00:00 2001 From: Alexandre Paez Sequeira Date: Tue, 14 Jul 2026 16:59:46 -0300 Subject: [PATCH] fix(MHPL-615): cap QUIC UDP payload size for Cloudflare Spectrum Players intermittently disconnect when the Hytale proxy runs behind Cloudflare Spectrum. Spectrum does not fragment UDP and drops any datagram too large to forward. The client-facing QUIC codec advertised the quiche default max_udp_payload_size (65527) and ran DPLPMTUD (discoverPmtu=true), so datagrams grew past the Spectrum-forwardable size and were blackholed, stalling sessions until the 60s idle timeout dropped the player. Cap the datagram size in both directions and disable PMTU probing, both driven by new config keys so the value can be tuned per deployment without a rebuild: - max-udp-payload-size (default 1200, QUIC's universal floor) sets both maxRecvUdpPayloadSize (advertised to the client, caps client->proxy, the direction Spectrum blackholes) and maxSendUdpPayloadSize (caps proxy->client) - discover-pmtu (default false) drives discoverPmtu(...) Behind Spectrum start at 1200 and raise toward 1350 once stability is confirmed. For direct (non-Spectrum) UDP exposure set discover-pmtu=true. --- .../hyproxy/config/HyProxyConfiguration.java | 14 +++++++++++++- .../io/QuicChannelInboundHandlerAdapter.java | 14 +++++++++++++- proxy/src/main/resources/default-config.toml | 17 +++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/proxy/src/main/java/ac/eva/hyproxy/config/HyProxyConfiguration.java b/proxy/src/main/java/ac/eva/hyproxy/config/HyProxyConfiguration.java index 2631e79..d6c3485 100644 --- a/proxy/src/main/java/ac/eva/hyproxy/config/HyProxyConfiguration.java +++ b/proxy/src/main/java/ac/eva/hyproxy/config/HyProxyConfiguration.java @@ -43,6 +43,13 @@ public class HyProxyConfiguration { private Map backends; private Map> permissions; + // QUIC UDP datagram sizing. maxUdpPayloadSize caps datagrams in both directions so + // they survive the path MTU; discoverPmtu toggles DPLPMTUD probing. See MHPL-615: + // both are tuned down when the proxy sits behind Cloudflare Spectrum, which drops + // oversized/fragmented UDP instead of forwarding it. + private int maxUdpPayloadSize; + private boolean discoverPmtu; + public InetSocketAddress getBind() { return AddressUtil.parseAndResolveAddress(bind); } @@ -184,6 +191,9 @@ public static HyProxyConfiguration load(HyProxy proxy, Path configFilePath) thro String initialBackend = config.getOrElse("initial-backend", "main"); boolean proxyCommunicationEnabled = config.getOrElse("proxy-communication", true); + int maxUdpPayloadSize = config.getIntOrElse("max-udp-payload-size", 1200); + boolean discoverPmtu = config.getOrElse("discover-pmtu", false); + CommentedConfig backendConfig = config.get("backends"); Map backends = backendConfig.valueMap() .entrySet() @@ -208,7 +218,9 @@ public static HyProxyConfiguration load(HyProxy proxy, Path configFilePath) thro initialBackend, proxyCommunicationEnabled, backends, - permissions + permissions, + maxUdpPayloadSize, + discoverPmtu ); } } diff --git a/proxy/src/main/java/ac/eva/hyproxy/io/QuicChannelInboundHandlerAdapter.java b/proxy/src/main/java/ac/eva/hyproxy/io/QuicChannelInboundHandlerAdapter.java index 4df89d7..480374c 100644 --- a/proxy/src/main/java/ac/eva/hyproxy/io/QuicChannelInboundHandlerAdapter.java +++ b/proxy/src/main/java/ac/eva/hyproxy/io/QuicChannelInboundHandlerAdapter.java @@ -12,6 +12,7 @@ import org.jspecify.annotations.Nullable; import ac.eva.hyproxy.HyProxy; import ac.eva.hyproxy.common.util.ProtocolUtil; +import ac.eva.hyproxy.config.HyProxyConfiguration; import ac.eva.hyproxy.io.channel.InboundChannelInitializer; import ac.eva.hyproxy.io.proto.DisconnectType; @@ -33,6 +34,9 @@ public boolean isSharable() { @Override public void channelActive(ChannelHandlerContext ctx) { + HyProxyConfiguration config = this.proxy.getConfiguration(); + int maxUdpPayloadSize = config.getMaxUdpPayloadSize(); + ChannelHandler handler = new QuicServerCodecBuilder() .sslContext(this.sslContext) .tokenHandler(InsecureQuicTokenHandler.INSTANCE) @@ -45,7 +49,15 @@ public void channelActive(ChannelHandlerContext ctx) { .initialMaxStreamDataBidirectionalLocal(128 * 1024) .initialMaxStreamDataBidirectionalRemote(128 * 1024) .initialMaxStreamsBidirectional(8) - .discoverPmtu(true) + // MHPL-615: cap the QUIC datagram size so packets survive the path MTU. + // maxRecvUdpPayloadSize is advertised to the client as our + // max_udp_payload_size, forcing it to never send us datagrams larger than + // this (the inbound direction that Cloudflare Spectrum blackholes); the + // send cap bounds our outbound datagrams. discoverPmtu is off behind + // Spectrum so DPLPMTUD does not grow datagrams past the forwardable size. + .maxRecvUdpPayloadSize(maxUdpPayloadSize) + .maxSendUdpPayloadSize(maxUdpPayloadSize) + .discoverPmtu(config.isDiscoverPmtu()) .congestionControlAlgorithm(QuicCongestionControlAlgorithm.BBR) .handler(new ChannelInboundHandlerAdapter() { @Override diff --git a/proxy/src/main/resources/default-config.toml b/proxy/src/main/resources/default-config.toml index b8ad967..165b971 100644 --- a/proxy/src/main/resources/default-config.toml +++ b/proxy/src/main/resources/default-config.toml @@ -9,6 +9,23 @@ proxy-secret-file = "proxy.secret" # if we should bind on ipv6 too, heavily recommended ipv6-support = true +# maximum QUIC UDP payload size (bytes) used with clients. this caps the datagram +# size in BOTH directions: it is advertised to the client as our max_udp_payload_size +# (so the client never sends us anything larger) and it bounds our own outgoing +# datagrams. keep it small enough to survive the whole path MTU. +# IMPORTANT (MHPL-615): Cloudflare Spectrum does NOT fragment UDP and silently drops +# any datagram too large to forward, so when the proxy is behind Spectrum this must +# stay under the Spectrum edge->origin forwardable size. 1200 is QUIC's universal +# floor (always deliverable); raise toward 1350 to recover throughput once stability +# is confirmed. must be >= 1200. +max-udp-payload-size = 1200 + +# whether QUIC should probe for a larger path MTU (DPLPMTUD). keep this off behind +# Cloudflare Spectrum: probing grows datagrams past the Spectrum-forwardable size and, +# with ICMP "packet too big" filtered on anycast paths, those packets get blackholed. +# only enable for direct (non-Spectrum) UDP exposure where the path MTU is trustworthy. +discover-pmtu = false + # initial backend or backend set we should in order try to connect the user to. initial-backend = "main"