// Copyright 2025 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http3 import ( "context" "fmt" "net/http" "net/url" "sync" "golang.org/x/net/quic" ) // A transport is an HTTP/3 transport. // // It does not manage a pool of connections, // and therefore does not implement net/http.RoundTripper. // // TODO: Provide a way to register an HTTP/3 transport with a net/http.transport's // connection pool. type transport struct { // config is the QUIC configuration used for client connections. config *quic.Config listenQUIC func(addr string, config *quic.Config) (*quic.Endpoint, error) mu sync.Mutex // Guards fields below. // endpoint is the QUIC endpoint used by connections created by the // transport. If CloseIdleConnections is called when activeConns is empty, // endpoint will be unset. If unset, endpoint will be initialized by any // call to dial. endpoint *quic.Endpoint activeConns map[*clientConn]struct{} inFlightDials int } // netHTTPTransport implements the net/http.dialClientConner interface, // allowing our HTTP/3 transport to integrate with net/http. type netHTTPTransport struct { *transport } // RoundTrip is defined since Transport.RegisterProtocol takes in a // RoundTripper. However, this method will never be used as net/http's // dialClientConner interface does not have a RoundTrip method and will only // use DialClientConn to create a new RoundTripper. func (t netHTTPTransport) RoundTrip(*http.Request) (*http.Response, error) { panic("netHTTPTransport.RoundTrip should never be called") } func (t netHTTPTransport) DialClientConn(ctx context.Context, addr string, _ *url.URL, _ func()) (http.RoundTripper, error) { return t.transport.dial(ctx, addr) } type TransportOpts struct { // ListenQUIC determines how the transport will open a QUIC endpoint. // By default, quic.Listen("udp", addr, config) is used. // ListenQUIC might be called multiple times. ListenQUIC func(addr string, config *quic.Config) (*quic.Endpoint, error) // QUICConfig is the QUIC configuration used by the transport. // QUICConfig may be nil and should not be modified after calling // RegisterTransport. // If QUICConfig.TLSConfig is nil, the TLSConfig of the net/http Transport // given to RegisterTransport will be used. QUICConfig *quic.Config } // RegisterTransport configures a net/http HTTP/1 Transport to use HTTP/3. func RegisterTransport(tr *http.Transport, opts TransportOpts) { if opts.QUICConfig == nil { opts.QUICConfig = &quic.Config{} } if opts.QUICConfig.TLSConfig == nil { opts.QUICConfig.TLSConfig = tr.TLSClientConfig } if opts.ListenQUIC == nil { opts.ListenQUIC = func(addr string, config *quic.Config) (*quic.Endpoint, error) { return quic.Listen("udp", addr, config) } } tr3 := &transport{ // initConfig will clone the tr.TLSClientConfig. config: initConfig(opts.QUICConfig), listenQUIC: opts.ListenQUIC, activeConns: make(map[*clientConn]struct{}), } tr.RegisterProtocol("http/3", netHTTPTransport{tr3}) } func (tr *transport) incInFlightDials() { tr.mu.Lock() defer tr.mu.Unlock() tr.inFlightDials++ } func (tr *transport) decInFlightDials() { tr.mu.Lock() defer tr.mu.Unlock() tr.inFlightDials-- } func (tr *transport) initEndpoint() (err error) { tr.mu.Lock() defer tr.mu.Unlock() // This might cause rare issues on Darwin. Unlike Linux, Darwin kernel // seems to have the following behaviors: // - After closing a UDP socket, the port that was bound to the socket // might not be immediately usable again. // - When doing IPv6 dual-stack binding (e.g., bind to ":0"), it will // happily bind the IPv6 port, even when the IPv4 port is unavailable. // // When both of these are combined, in practice, it is possible for the // following to happen: // 1. Transport binds ":0", creating a dual-stack IPv6 UDP socket. // Everything works as expected. // 2. At some point, CloseIdleConnections is called and the socket is // closed. // 3. Soon after, a new dial is started, and a new dual-stack IPv6 socket // is coincidentally assigned the same port as the previous socket. // 4. If the IPv4 port is still unavailable, Darwin's permissive binding // behavior will cause us to have a socket that silently is unable to // receive packets on its IPv4 address. // 5. If the dial target is an IPv4 address, transport will be able to send // packets to the target, but will be unable to receive its reply. // // TransportOpts.ListenQUIC can technically be configured to avoid // dual-stack binding to avoid this issue, and high socket churn is // probably uncommon for regular use cases. However, finding a workaround // for this eventually would be ideal. if tr.endpoint == nil { tr.endpoint, err = tr.listenQUIC(":0", tr.config) } return err } // dial creates a new HTTP/3 client connection. func (tr *transport) dial(ctx context.Context, target string) (*clientConn, error) { tr.incInFlightDials() defer tr.decInFlightDials() if err := tr.initEndpoint(); err != nil { return nil, err } qconn, err := tr.endpoint.Dial(ctx, "udp", target, tr.config) if err != nil { return nil, err } return tr.newClientConn(ctx, qconn) } // CloseIdleConnections is called by net/http.Transport.CloseIdleConnections // after all existing idle connections are closed using http3.clientConn.Close. // // When the transport has no active connections anymore, calling this method // will make the transport clean up any shared resources that are no longer // required, such as its QUIC endpoint. func (tr *transport) CloseIdleConnections() { tr.mu.Lock() defer tr.mu.Unlock() if tr.endpoint == nil || len(tr.activeConns) > 0 || tr.inFlightDials > 0 { return } tr.endpoint.Close(canceledCtx) tr.endpoint = nil } // A clientConn is a client HTTP/3 connection. // // Multiple goroutines may invoke methods on a clientConn simultaneously. type clientConn struct { qconn *quic.Conn genericConn enc qpackEncoder dec qpackDecoder } func (tr *transport) registerConn(cc *clientConn) { tr.mu.Lock() defer tr.mu.Unlock() tr.activeConns[cc] = struct{}{} } func (tr *transport) unregisterConn(cc *clientConn) { tr.mu.Lock() defer tr.mu.Unlock() delete(tr.activeConns, cc) } func (tr *transport) newClientConn(ctx context.Context, qconn *quic.Conn) (*clientConn, error) { cc := &clientConn{ qconn: qconn, } tr.registerConn(cc) cc.enc.init() // Create control stream and send SETTINGS frame. controlStream, err := newConnStream(ctx, cc.qconn, streamTypeControl) if err != nil { tr.unregisterConn(cc) return nil, fmt.Errorf("http3: cannot create control stream: %v", err) } controlStream.writeSettings() controlStream.Flush() go func() { cc.acceptStreams(qconn, cc) tr.unregisterConn(cc) }() return cc, nil } // TODO: implement the rest of net/http.ClientConn methods beyond Close. func (cc *clientConn) Close() error { // We need to use Close rather than Abort on the QUIC connection. // Otherwise, when a net/http.Transport.CloseIdleConnections is called, it // might call the http3.transport.CloseIdleConnections prior to all idle // connections being fully closed; this would make it unable to close its // QUIC endpoint, making http3.transport.CloseIdleConnections a no-op // unintentionally. return cc.qconn.Close() } func (cc *clientConn) Err() error { return nil } func (cc *clientConn) Reserve() error { return nil } func (cc *clientConn) Release() { } func (cc *clientConn) Available() int { return 0 } func (cc *clientConn) InFlight() int { return 0 } func (cc *clientConn) handleControlStream(st *stream) error { // "A SETTINGS frame MUST be sent as the first frame of each control stream [...]" // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-2 if err := st.readSettings(func(settingsType, settingsValue int64) error { switch settingsType { case settingsMaxFieldSectionSize: _ = settingsValue // TODO case settingsQPACKMaxTableCapacity: _ = settingsValue // TODO case settingsQPACKBlockedStreams: _ = settingsValue // TODO default: // Unknown settings types are ignored. } return nil }); err != nil { return err } for { ftype, err := st.readFrameHeader() if err != nil { return err } switch ftype { case frameTypeCancelPush: // "If a CANCEL_PUSH frame is received that references a push ID // greater than currently allowed on the connection, // this MUST be treated as a connection error of type H3_ID_ERROR." // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3-7 return &connectionError{ code: errH3IDError, message: "CANCEL_PUSH received when no MAX_PUSH_ID has been sent", } case frameTypeGoaway: // TODO: Wait for requests to complete before closing connection. return errH3NoError default: // Unknown frames are ignored. if err := st.discardUnknownFrame(ftype); err != nil { return err } } } } func (cc *clientConn) handleEncoderStream(*stream) error { // TODO return nil } func (cc *clientConn) handleDecoderStream(*stream) error { // TODO return nil } func (cc *clientConn) handlePushStream(*stream) error { // "A client MUST treat receipt of a push stream as a connection error // of type H3_ID_ERROR when no MAX_PUSH_ID frame has been sent [...]" // https://www.rfc-editor.org/rfc/rfc9114.html#section-4.6-3 return &connectionError{ code: errH3IDError, message: "push stream created when no MAX_PUSH_ID has been sent", } } func (cc *clientConn) handleRequestStream(st *stream) error { // "Clients MUST treat receipt of a server-initiated bidirectional // stream as a connection error of type H3_STREAM_CREATION_ERROR [...]" // https://www.rfc-editor.org/rfc/rfc9114.html#section-6.1-3 return &connectionError{ code: errH3StreamCreationError, message: "server created bidirectional stream", } } // abort closes the connection with an error. func (cc *clientConn) abort(err error) { if e, ok := err.(*connectionError); ok { cc.qconn.Abort(&quic.ApplicationError{ Code: uint64(e.code), Reason: e.message, }) } else { cc.qconn.Abort(err) } }