From 301dbc56d0e4affb18aa795b79eab90b7ce3c557 Mon Sep 17 00:00:00 2001 From: BBQ <35603386+HoneyBBQ@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:55:36 +0800 Subject: [PATCH] fix(listener): keep *net.UDPConn visible to quic-go for GSO/GRO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The quic and wt (WebTransport) listeners wrap the raw UDP socket with net.PacketConn metrics/stats/admission/limiter wrappers before handing it to quic-go. WrapPacketConn returns a plain net.PacketConn, which hides the underlying *net.UDPConn. quic-go probes the conn for OOBCapablePacketConn (SyscallConn, SetReadBuffer, ReadMsgUDP, WriteMsgUDP); behind the wrapper that probe fails, so quic-go disables its kernel offloads and cannot size its receive buffer. Switch the non-cipher path of both listeners to WrapUDPConn variants, which forward SyscallConn/SetReadBuffer/ReadMsgUDP/WriteMsgUDP down to the underlying *net.UDPConn, so quic-go sees an OOBCapablePacketConn and keeps GSO/GRO + buffer sizing. The same metrics/stats/admission/ limiting still apply. The quic listener's cipher path keeps the net.PacketConn wrappers, since CipherPacketConn is not OOB-capable. +48% throughput (1.80 → 2.69 Gbit/s single-stream, loopback iperf3). The quic-go receive-buffer warning is gone. --- listener/http3/wt/listener.go | 10 ++++++---- listener/quic/listener.go | 37 +++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/listener/http3/wt/listener.go b/listener/http3/wt/listener.go index 91b4b059..3212736d 100644 --- a/listener/http3/wt/listener.go +++ b/listener/http3/wt/listener.go @@ -74,10 +74,12 @@ func (l *wtListener) Init(md md.Metadata) (err error) { return } - pc = metrics.WrapPacketConn(l.options.Service, pc) - pc = stats.WrapPacketConn(pc, l.options.Stats) - pc = admission.WrapPacketConn(l.options.Admission, pc) - pc = limiter_wrapper.WrapPacketConn( + pc = metrics.WrapUDPConn(l.options.Service, pc) + if l.options.Stats != nil { + pc = stats.WrapUDPConn(pc, l.options.Stats) + } + pc = admission.WrapUDPConn(l.options.Admission, pc) + pc = limiter_wrapper.WrapUDPConn( pc, l.options.TrafficLimiter, traffic_limiter.ServiceLimitKey, diff --git a/listener/quic/listener.go b/listener/quic/listener.go index 939c5701..455cc748 100644 --- a/listener/quic/listener.go +++ b/listener/quic/listener.go @@ -70,20 +70,33 @@ func (l *quicListener) Init(md md.Metadata) (err error) { } if l.md.cipherKey != nil { conn = quic_util.CipherPacketConn(conn, l.md.cipherKey) + conn = metrics.WrapPacketConn(l.options.Service, conn) + conn = stats.WrapPacketConn(conn, l.options.Stats) + conn = admission.WrapPacketConn(l.options.Admission, conn) + conn = limiter_wrapper.WrapPacketConn( + conn, + l.options.TrafficLimiter, + traffic_limiter.ServiceLimitKey, + limiter.ScopeOption(limiter.ScopeService), + limiter.ServiceOption(l.options.Service), + limiter.NetworkOption(conn.LocalAddr().Network()), + ) + } else { + conn = metrics.WrapUDPConn(l.options.Service, conn) + if l.options.Stats != nil { + conn = stats.WrapUDPConn(conn, l.options.Stats) + } + conn = admission.WrapUDPConn(l.options.Admission, conn) + conn = limiter_wrapper.WrapUDPConn( + conn, + l.options.TrafficLimiter, + traffic_limiter.ServiceLimitKey, + limiter.ScopeOption(limiter.ScopeService), + limiter.ServiceOption(l.options.Service), + limiter.NetworkOption(conn.LocalAddr().Network()), + ) } - conn = metrics.WrapPacketConn(l.options.Service, conn) - conn = stats.WrapPacketConn(conn, l.options.Stats) - conn = admission.WrapPacketConn(l.options.Admission, conn) - conn = limiter_wrapper.WrapPacketConn( - conn, - l.options.TrafficLimiter, - traffic_limiter.ServiceLimitKey, - limiter.ScopeOption(limiter.ScopeService), - limiter.ServiceOption(l.options.Service), - limiter.NetworkOption(conn.LocalAddr().Network()), - ) - config := &quic.Config{ KeepAlivePeriod: l.md.keepAlivePeriod, HandshakeIdleTimeout: l.md.handshakeTimeout,