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"