From 1d34d4543adea2932c00283db3e500e013019da6 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sat, 20 Jun 2026 15:01:14 +0800 Subject: [PATCH] fix(masque): default enableDatagrams to true on HTTP/3 listener and fix CONNECT-UDP error handling - listener/http3: default enableDatagrams to true when not explicitly set, so CONNECT-UDP (RFC 9298) works out of the box without requiring ?enableDatagrams=true in the config URL - handler/masque: dial target before sending 200 OK in handleConnectUDP, matching the handleConnectTCP order; if target dial fails, the handler can still return an error status instead of having already committed 200 - handler/masque: add missing WriteHeader(BadRequest) when ResolveUDPAddr fails, so the client gets a proper HTTP error instead of an opaque H3_REQUEST_CANCELLED stream reset Fixes #103 --- handler/masque/handler.go | 75 +++++++++++++++++++------------------- listener/http3/metadata.go | 6 ++- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/handler/masque/handler.go b/handler/masque/handler.go index 783ab257..b843fd36 100644 --- a/handler/masque/handler.go +++ b/handler/masque/handler.go @@ -355,6 +355,7 @@ func (h *masqueHandler) handleConnectUDP(ctx context.Context, w http.ResponseWri // Resolve target address raddr, err := net.ResolveUDPAddr("udp", targetAddr) if err != nil { + w.WriteHeader(http.StatusBadRequest) log.Error("masque: failed to resolve target address: ", err) return err } @@ -370,43 +371,6 @@ func (h *masqueHandler) handleConnectUDP(ctx context.Context, w http.ResponseWri // Get the underlying HTTP/3 stream stream := streamer.HTTPStream() - // Send success response with capsule-protocol header - w.Header().Set("Capsule-Protocol", "?1") - w.WriteHeader(http.StatusOK) - - // Flush the response headers - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } - - // Create datagram connection wrapping the HTTP/3 stream (client side) - datagramConn := masque_util.NewDatagramConn(stream, laddr, raddr) - defer datagramConn.Close() - - // Wrap with recorder stats and traffic limiter - var clientPC net.PacketConn = datagramConn - clientPC = stats_wrapper.WrapPacketConn(clientPC, pStats) - clientPC = traffic_wrapper.WrapPacketConn( - clientPC, - h.limiter, - clientID, - limiter.ServiceOption(h.options.Service), - limiter.ScopeOption(limiter.ScopeClient), - limiter.NetworkOption("udp"), - limiter.AddrOption(targetAddr), - limiter.ClientOption(clientID), - limiter.SrcOption(ro.RemoteAddr), - ) - - // Track per-client connection stats - if h.options.Observer != nil { - pstats := h.stats.Stats(clientID) - pstats.Add(stats.KindTotalConns, 1) - pstats.Add(stats.KindCurrentConns, 1) - defer pstats.Add(stats.KindCurrentConns, -1) - clientPC = stats_wrapper.WrapPacketConn(clientPC, pstats) - } - // Get target connection - either through router/chain or direct var targetPC net.PacketConn @@ -460,6 +424,43 @@ func (h *masqueHandler) handleConnectUDP(ctx context.Context, w http.ResponseWri // Wrap target with metrics targetPC = metrics.WrapPacketConn(h.options.Service, targetPC) + // Send success response with capsule-protocol header + w.Header().Set("Capsule-Protocol", "?1") + w.WriteHeader(http.StatusOK) + + // Flush the response headers + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + + // Create datagram connection wrapping the HTTP/3 stream (client side) + datagramConn := masque_util.NewDatagramConn(stream, laddr, raddr) + defer datagramConn.Close() + + // Wrap with recorder stats and traffic limiter + var clientPC net.PacketConn = datagramConn + clientPC = stats_wrapper.WrapPacketConn(clientPC, pStats) + clientPC = traffic_wrapper.WrapPacketConn( + clientPC, + h.limiter, + clientID, + limiter.ServiceOption(h.options.Service), + limiter.ScopeOption(limiter.ScopeClient), + limiter.NetworkOption("udp"), + limiter.AddrOption(targetAddr), + limiter.ClientOption(clientID), + limiter.SrcOption(ro.RemoteAddr), + ) + + // Track per-client connection stats + if h.options.Observer != nil { + pstats := h.stats.Stats(clientID) + pstats.Add(stats.KindTotalConns, 1) + pstats.Add(stats.KindCurrentConns, 1) + defer pstats.Add(stats.KindCurrentConns, -1) + clientPC = stats_wrapper.WrapPacketConn(clientPC, pstats) + } + // Relay UDP packets between client and target relay := udp.NewRelay(clientPC, targetPC). WithService(h.options.Service). diff --git a/listener/http3/metadata.go b/listener/http3/metadata.go index c8de9825..b9897385 100644 --- a/listener/http3/metadata.go +++ b/listener/http3/metadata.go @@ -47,7 +47,11 @@ func (l *http3Listener) parseMetadata(md mdata.Metadata) (err error) { l.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout) l.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout) l.md.maxStreams = mdutil.GetInt(md, maxStreams) - l.md.enableDatagrams = mdutil.GetBool(md, "enableDatagrams") + if mdutil.IsExists(md, "enableDatagrams") { + l.md.enableDatagrams = mdutil.GetBool(md, "enableDatagrams") + } else { + l.md.enableDatagrams = true + } return }