Skip to content

Commit

Permalink
implement HTTP/3 unistream hijacking
Browse files Browse the repository at this point in the history
  • Loading branch information
hareku committed Apr 20, 2022
1 parent 6d4a694 commit 027248f
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 42 deletions.
72 changes: 50 additions & 22 deletions http3/client.go
Expand Up @@ -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
Expand Down Expand Up @@ -167,22 +168,61 @@ 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 {
c.logger.Debugf("accepting unidirectional stream failed: %s", err)
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.
Expand All @@ -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)
}
}

Expand Down
4 changes: 4 additions & 0 deletions http3/roundtrip.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
72 changes: 52 additions & 20 deletions http3/server.go
Expand Up @@ -33,6 +33,8 @@ const (
nextProtoH3 = "h3"
)

type StreamType uint64

const (
streamTypeControlStream = 0
streamTypePushStream = 1
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand All @@ -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)
}
}
Expand Down

0 comments on commit 027248f

Please sign in to comment.