From d562d8947d3175db86b4fa849742433a9876cb63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=96=D0=BE=D1=80=D0=B0=20=D0=97=D0=BC=D0=B5=D0=B9=D0=BA?= =?UTF-8?q?=D0=B8=D0=BD?= <48821354+Katze-942@users.noreply.github.com> Date: Mon, 21 Sep 2026 01:51:03 +0300 Subject: [PATCH] Hysteria outbound: Fix UDP DATAGRAM truncation with ChromeParrot (#6788) https://github.com/XTLS/Xray-core/pull/6788#issuecomment-5751428127 --------- Co-authored-by: LjhAUMEM --- proxy/hysteria/client.go | 6 +- transport/internet/hysteria/hysteria_test.go | 165 +++++++++++++++++++ 2 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 transport/internet/hysteria/hysteria_test.go diff --git a/proxy/hysteria/client.go b/proxy/hysteria/client.go index 5b602c342..7021ab5a5 100644 --- a/proxy/hysteria/client.go +++ b/proxy/hysteria/client.go @@ -236,14 +236,14 @@ type UDPReader struct { func (r *UDPReader) ReadFrom(p []byte) (n int, addr *net.Destination, err error) { for { - var buf [hysteria.MaxDatagramFrameSize]byte + var packet [1500]byte - n, err := r.reader.Read(buf[:]) + n, err := r.reader.Read(packet[:]) if err != nil { return 0, nil, err } - msg, err := ParseUDPMessage(buf[:n]) + msg, err := ParseUDPMessage(packet[:n]) if err != nil { continue } diff --git a/transport/internet/hysteria/hysteria_test.go b/transport/internet/hysteria/hysteria_test.go new file mode 100644 index 000000000..23e09bc96 --- /dev/null +++ b/transport/internet/hysteria/hysteria_test.go @@ -0,0 +1,165 @@ +package hysteria + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "net" + "runtime" + "testing" + "time" + + "github.com/apernet/quic-go" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/protocol/tls/cert" +) + +func TestDatagram(t *testing.T) { + run := func() (addr net.Addr, recv chan int64, cancel func()) { + cert, _ := cert.MustGenerate(nil) + Certificate := [][]byte{cert.Certificate} + PrivateKey := common.Must2(x509.ParsePKCS8PrivateKey(cert.PrivateKey)) + + tlsConf := &tls.Config{ + Certificates: []tls.Certificate{ + { + Certificate: Certificate, + PrivateKey: PrivateKey, + }, + }, + NextProtos: []string{"h3"}, + } + + quicConf := &quic.Config{ + InitialStreamReceiveWindow: 8388608, + MaxStreamReceiveWindow: 8388608, + InitialConnectionReceiveWindow: 8388608 * 5 / 2, + MaxConnectionReceiveWindow: 8388608 * 5 / 2, + MaxIdleTimeout: 30 * time.Second, + MaxIncomingStreams: 1024, + DisablePathMTUDiscovery: runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin", + EnableDatagrams: true, + MaxDatagramFrameSize: MaxDatagramFrameSize, + AssumePeerMaxDatagramFrameSize: MaxDatagramFrameSize, + DisablePathManager: true, + } + + pktConn := common.Must2(net.ListenPacket("udp", "127.0.0.1:0")) + tr := &quic.Transport{Conn: pktConn} + l := common.Must2(tr.Listen(tlsConf, quicConf)) + + recv = make(chan int64) + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + defer pktConn.Close() + defer tr.Close() + defer l.Close() + defer close(recv) + + var buf [1500]byte + for { + conn, err := l.Accept(ctx) + if err != nil { + if !errors.Is(err, context.Canceled) { + t.Error(err) + } + break + } + err = conn.SendDatagram(buf[:]) + var qErr *quic.DatagramTooLargeError + if !errors.As(err, &qErr) { + t.Error(err) + } + recv <- qErr.MaxDatagramPayloadSize + defer conn.CloseWithError(0, "") + } + }() + + return l.Addr(), recv, cancel + } + + addr, recv, cancel := run() + + t.Run("With ChromeParrot", func(t *testing.T) { + tlsConf := &tls.Config{ + InsecureSkipVerify: true, + } + + quicConf := &quic.Config{ + InitialStreamReceiveWindow: 8388608, + MaxStreamReceiveWindow: 8388608, + InitialConnectionReceiveWindow: 8388608 * 5 / 2, + MaxConnectionReceiveWindow: 8388608 * 5 / 2, + MaxIdleTimeout: 30 * time.Second, + KeepAlivePeriod: 10 * time.Second, + DisablePathMTUDiscovery: runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin", + ChromeParrot: true, + EnableDatagrams: true, + MaxDatagramFrameSize: MaxDatagramFrameSize, + OmitMaxDatagramFrameSize: true, + DisablePathManager: true, + } + + pktConn := common.Must2(net.ListenPacket("udp", "127.0.0.1:0")) + tr := &quic.Transport{Conn: pktConn, ConnectionIDGenerator: quic.ZeroLengthConnectionIDGenerator{}} + conn := common.Must2(tr.DialEarly(context.Background(), addr, tlsConf, quicConf)) + + defer pktConn.Close() + defer tr.Close() + defer conn.CloseWithError(0, "") + + var buf [1500]byte + err := conn.SendDatagram(buf[:]) + var qErr *quic.DatagramTooLargeError + if !errors.As(err, &qErr) || qErr.MaxDatagramPayloadSize != 1197 { + t.Error(err) + } + if server := <-recv; server != 1243 { + t.Error(server) + } + }) + + t.Run("Without ChromeParrot", func(t *testing.T) { + tlsConf := &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"h3"}, + } + + quicConf := &quic.Config{ + InitialStreamReceiveWindow: 8388608, + MaxStreamReceiveWindow: 8388608, + InitialConnectionReceiveWindow: 8388608 * 5 / 2, + MaxConnectionReceiveWindow: 8388608 * 5 / 2, + MaxIdleTimeout: 30 * time.Second, + KeepAlivePeriod: 10 * time.Second, + DisablePathMTUDiscovery: runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin", + ChromeParrot: false, + EnableDatagrams: true, + MaxDatagramFrameSize: MaxDatagramFrameSize, + OmitMaxDatagramFrameSize: true, + DisablePathManager: true, + } + + pktConn := common.Must2(net.ListenPacket("udp", "127.0.0.1:0")) + tr := &quic.Transport{Conn: pktConn} + conn := common.Must2(tr.DialEarly(context.Background(), addr, tlsConf, quicConf)) + + defer pktConn.Close() + defer tr.Close() + defer conn.CloseWithError(0, "") + + var buf [1500]byte + err := conn.SendDatagram(buf[:]) + var qErr *quic.DatagramTooLargeError + if !errors.As(err, &qErr) || qErr.MaxDatagramPayloadSize != 1197 { + t.Error(err) + } + if server := <-recv; server != 1197 { + t.Error(server) + } + }) + + cancel() +}