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
This commit is contained in:
ginuerzh
2026-06-20 15:01:14 +08:00
parent 296d87a597
commit 1d34d4543a
2 changed files with 43 additions and 38 deletions
+38 -37
View File
@@ -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).
+4
View File
@@ -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)
if mdutil.IsExists(md, "enableDatagrams") {
l.md.enableDatagrams = mdutil.GetBool(md, "enableDatagrams")
} else {
l.md.enableDatagrams = true
}
return
}