Repository navigation
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Dependency
This PR depends on the
quic-goPR SagerNet/quic-go#1, which exposes(*quic.Conn).MaxDatagramPayloadSize() int64. It must be merged and thequic-godependency bumped before this change builds against upstream.The Existing Problem
Currently,
sing-quicemploys a hardcoded maximum datagram payload size of1197bytes (udpMTU = 1197) in thehysteria2packet handler.When a large packet (e.g., a standard 1350-byte MTU packet from a TUN interface) enters the proxy,
sing-quicforcibly fragments it at the application layer into two smaller chunks before passing them toquic-go. Becausequic-go'sSendDatagramis never fed large enough packets, its internal DPLPMTUD (Datagram Packetization Layer Path MTU Discovery) mechanism is starved and fails to probe for larger path MTUs. This causes an artificial MTU bottleneck, breaking nested VPN protocols (like WireGuard) that rely on large packets and introducing unnecessary fragmentation overhead.What This Optimizes
This PR refactors
WriteToand packet handling in Hysteria2 to utilize the newly exposedMaxDatagramPayloadSize()API fromquic-go.By dynamically querying the actual PMTU estimate of the current QUIC connection,
sing-quicwill now only fragment packets when strictly necessary. If the path supports a standard MTU (e.g., 1500), large packets (like a 1350-byte IP packet) are now encapsulated and transmitted as a single, contiguous QUIC datagram frame.This change:
Proof of Optimization
I tested sending 1350-byte UDP payloads (1378-byte IP packets) through a TUN interface using both the old and new implementations, plotting the QUIC datagram packet lengths observed on the wire.
Packet loss amplification (10% random loss)
Because Hysteria2 datagrams are unreliable, splitting one inner packet into two QUIC datagrams means losing either fragment drops the whole inner packet, so the effective loss rate is amplified (
1 - 0.9^2 = 19%at 10% link loss). Without fragmentation the effective loss equals the link loss.Setup: NixOS VM, two network namespaces (client/server) joined by a veth pair,
tc qdisc add dev <veth> root netem loss 10%on both ends, sing-box TUN client → Hysteria2 server, theniperf3 -u -b 20M -l 1350 -t 10through the tunnel (1350-byte payloads). The only difference between runs is the sing-box binary (old =main, new = this PR).Raw iperf3 output, unmodified:
Note on TUIC
tuic/packet.goinsing-quichas the same hardcoded1200 - 3limit and could adopt the sameMaxDatagramPayloadSize()approach. This PR intentionally leaves TUIC untouched for now and only changes Hysteria2; TUIC can be handled in a follow-up.