diff --git a/http3/client.go b/http3/client.go index 27c4947b1d3..591ab41422b 100644 --- a/http3/client.go +++ b/http3/client.go @@ -44,6 +44,7 @@ type roundTripperOpts struct { MaxHeaderBytes int64 AdditionalSettings map[uint64]uint64 StreamHijacker func(FrameType, quic.Connection, quic.Stream) (hijacked bool, err error) + UniStreamHijacker func(StreamType, quic.Connection, quic.ReceiveStream) (hijacked bool, err error) } // client is a HTTP3 client doing requests @@ -167,6 +168,44 @@ func (c *client) handleBidirectionalStreams() { } func (c *client) handleUnidirectionalStreams() { + // handle first unistream for settings frame + str, err := c.conn.AcceptUniStream(context.Background()) + if err != nil { + c.logger.Debugf("accepting unidirectional stream failed: %s", err) + return + } + go func() { + streamType, err := quicvarint.Read(quicvarint.NewReader(str)) + if err != nil { + c.logger.Debugf("reading stream type on stream %d failed: %s", str.StreamID(), err) + return + } + + // first stream type must be control stream + if streamType != streamTypeControlStream { + c.conn.CloseWithError(quic.ApplicationErrorCode(errorStreamCreationError), "") + return + } + + f, err := parseNextFrame(str, nil) + if err != nil { + c.conn.CloseWithError(quic.ApplicationErrorCode(errorFrameError), "") + return + } + sf, ok := f.(*settingsFrame) + if !ok { + c.conn.CloseWithError(quic.ApplicationErrorCode(errorMissingSettings), "") + return + } + + // If datagram support was enabled on our side as well as on the server side, + // we can expect it to have been negotiated both on the transport and on the HTTP/3 layer. + // Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT). + if sf.Datagram && c.opts.EnableDatagram && !c.conn.ConnectionState().SupportsDatagrams { + c.conn.CloseWithError(quic.ApplicationErrorCode(errorSettingsError), "missing QUIC Datagram support") + } + }() + for { str, err := c.conn.AcceptUniStream(context.Background()) if err != nil { @@ -174,15 +213,16 @@ func (c *client) handleUnidirectionalStreams() { return } - go func() { + go func(str quic.ReceiveStream) { streamType, err := quicvarint.Read(quicvarint.NewReader(str)) if err != nil { c.logger.Debugf("reading stream type on stream %d failed: %s", str.StreamID(), err) return } - // We're only interested in the control stream here. switch streamType { case streamTypeControlStream: + c.conn.CloseWithError(quic.ApplicationErrorCode(errorStreamCreationError), "") + return case streamTypeQPACKEncoderStream, streamTypeQPACKDecoderStream: // Our QPACK implementation doesn't use the dynamic table yet. // TODO: check that only one stream of each type is opened. @@ -192,29 +232,17 @@ func (c *client) handleUnidirectionalStreams() { c.conn.CloseWithError(quic.ApplicationErrorCode(errorIDError), "") return default: + if c.opts.UniStreamHijacker != nil { + hijacked, err := c.opts.UniStreamHijacker(StreamType(streamType), c.conn, str) + if err == nil && hijacked { + return + } + } + str.CancelRead(quic.StreamErrorCode(errorStreamCreationError)) return } - f, err := parseNextFrame(str, nil) - if err != nil { - c.conn.CloseWithError(quic.ApplicationErrorCode(errorFrameError), "") - return - } - sf, ok := f.(*settingsFrame) - if !ok { - c.conn.CloseWithError(quic.ApplicationErrorCode(errorMissingSettings), "") - return - } - if !sf.Datagram { - return - } - // If datagram support was enabled on our side as well as on the server side, - // we can expect it to have been negotiated both on the transport and on the HTTP/3 layer. - // Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT). - if c.opts.EnableDatagram && !c.conn.ConnectionState().SupportsDatagrams { - c.conn.CloseWithError(quic.ApplicationErrorCode(errorSettingsError), "missing QUIC Datagram support") - } - }() + }(str) } } diff --git a/http3/roundtrip.go b/http3/roundtrip.go index 743ff0341af..c0c4b319f0d 100644 --- a/http3/roundtrip.go +++ b/http3/roundtrip.go @@ -58,6 +58,9 @@ type RoundTripper struct { // Alternatively, callers can take over the QUIC stream (by returning hijacked true). StreamHijacker func(FrameType, quic.Connection, quic.Stream) (hijacked bool, err error) + // When set, this callback is called for the first unknown stream type parsed on a unidirectional receive stream. + UniStreamHijacker func(StreamType, quic.Connection, quic.ReceiveStream) (hijacked bool, err error) + // Dial specifies an optional dial function for creating QUIC // connections for requests. // If Dial is nil, quic.DialAddrEarlyContext will be used. @@ -154,6 +157,7 @@ func (r *RoundTripper) getClient(hostname string, onlyCached bool) (http.RoundTr DisableCompression: r.DisableCompression, MaxHeaderBytes: r.MaxResponseHeaderBytes, StreamHijacker: r.StreamHijacker, + UniStreamHijacker: r.UniStreamHijacker, }, r.QuicConfig, r.Dial, diff --git a/http3/server.go b/http3/server.go index e1d818acc67..4b7ff4e1d5d 100644 --- a/http3/server.go +++ b/http3/server.go @@ -33,6 +33,8 @@ const ( nextProtoH3 = "h3" ) +type StreamType uint64 + const ( streamTypeControlStream = 0 streamTypePushStream = 1 @@ -151,6 +153,9 @@ type Server struct { // Alternatively, callers can take over the QUIC stream (by returning hijacked true). StreamHijacker func(FrameType, quic.Connection, quic.Stream) (hijacked bool, err error) + // When set, this callback is called for the first unknown stream type parsed on a unidirectional receive stream. + UniStreamHijacker func(StreamType, quic.Connection, quic.ReceiveStream) (hijacked bool, err error) + mutex sync.RWMutex listeners map[*quic.EarlyListener]listenerInfo @@ -397,6 +402,44 @@ func (s *Server) handleConn(conn quic.EarlyConnection) { } func (s *Server) handleUnidirectionalStreams(conn quic.EarlyConnection) { + // handle first unistream + str, err := conn.AcceptUniStream(context.Background()) + if err != nil { + s.logger.Debugf("accepting unidirectional stream failed: %s", err) + return + } + go func() { + streamType, err := quicvarint.Read(quicvarint.NewReader(str)) + if err != nil { + s.logger.Debugf("reading stream type on stream %d failed: %s", str.StreamID(), err) + return + } + + // first stream type must be control stream + if streamType != streamTypeControlStream { + conn.CloseWithError(quic.ApplicationErrorCode(errorStreamCreationError), "") + return + } + + f, err := parseNextFrame(str, nil) + if err != nil { + conn.CloseWithError(quic.ApplicationErrorCode(errorFrameError), "") + return + } + sf, ok := f.(*settingsFrame) + if !ok { + conn.CloseWithError(quic.ApplicationErrorCode(errorMissingSettings), "") + return + } + + // If datagram support was enabled on our side as well as on the client side, + // we can expect it to have been negotiated both on the transport and on the HTTP/3 layer. + // Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT). + if sf.Datagram && s.EnableDatagrams && !conn.ConnectionState().SupportsDatagrams { + conn.CloseWithError(quic.ApplicationErrorCode(errorSettingsError), "missing QUIC Datagram support") + } + }() + for { str, err := conn.AcceptUniStream(context.Background()) if err != nil { @@ -410,9 +453,10 @@ func (s *Server) handleUnidirectionalStreams(conn quic.EarlyConnection) { s.logger.Debugf("reading stream type on stream %d failed: %s", str.StreamID(), err) return } - // We're only interested in the control stream here. switch streamType { case streamTypeControlStream: + conn.CloseWithError(quic.ApplicationErrorCode(errorStreamCreationError), "") + return case streamTypeQPACKEncoderStream, streamTypeQPACKDecoderStream: // Our QPACK implementation doesn't use the dynamic table yet. // TODO: check that only one stream of each type is opened. @@ -421,28 +465,16 @@ func (s *Server) handleUnidirectionalStreams(conn quic.EarlyConnection) { conn.CloseWithError(quic.ApplicationErrorCode(errorStreamCreationError), "") return default: + if s.UniStreamHijacker != nil { + hijacked, err := s.UniStreamHijacker(StreamType(streamType), conn, str) + if err == nil && hijacked { + return + } + } + str.CancelRead(quic.StreamErrorCode(errorStreamCreationError)) return } - f, err := parseNextFrame(str, nil) - if err != nil { - conn.CloseWithError(quic.ApplicationErrorCode(errorFrameError), "") - return - } - sf, ok := f.(*settingsFrame) - if !ok { - conn.CloseWithError(quic.ApplicationErrorCode(errorMissingSettings), "") - return - } - if !sf.Datagram { - return - } - // If datagram support was enabled on our side as well as on the client side, - // we can expect it to have been negotiated both on the transport and on the HTTP/3 layer. - // Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT). - if s.EnableDatagrams && !conn.ConnectionState().SupportsDatagrams { - conn.CloseWithError(quic.ApplicationErrorCode(errorSettingsError), "missing QUIC Datagram support") - } }(str) } }