Skip to content

Commit

Permalink
Merge pull request #2951 from lucas-clemente/h3-datagram-negotiation
Browse files Browse the repository at this point in the history
implement the HTTP/3 Datagram negotiation
  • Loading branch information
marten-seemann authored Dec 23, 2020
2 parents f68dfd5 + b753005 commit 4ad144c
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 18 deletions.
19 changes: 16 additions & 3 deletions http3/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var dialAddr = quic.DialAddrEarly

type roundTripperOpts struct {
DisableCompression bool
EnableDatagram bool
MaxHeaderBytes int64
}

Expand Down Expand Up @@ -68,7 +69,7 @@ func newClient(
dialer func(network, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error),
) (*client, error) {
if quicConfig == nil {
quicConfig = defaultQuicConfig
quicConfig = defaultQuicConfig.Clone()
} else if len(quicConfig.Versions) == 0 {
quicConfig = quicConfig.Clone()
quicConfig.Versions = []quic.VersionNumber{defaultQuicConfig.Versions[0]}
Expand All @@ -77,6 +78,7 @@ func newClient(
return nil, errors.New("can only use a single QUIC version for dialing a HTTP/3 connection")
}
quicConfig.MaxIncomingStreams = -1 // don't allow any bidirectional streams
quicConfig.EnableDatagrams = opts.EnableDatagram
logger := utils.DefaultLogger.WithPrefix("h3 client")

if tlsConf == nil {
Expand Down Expand Up @@ -131,7 +133,7 @@ func (c *client) setupSession() error {
buf := &bytes.Buffer{}
utils.WriteVarInt(buf, streamTypeControlStream)
// send the SETTINGS frame
(&settingsFrame{}).Write(buf)
(&settingsFrame{Datagram: c.opts.EnableDatagram}).Write(buf)
_, err = str.Write(buf.Bytes())
return err
}
Expand Down Expand Up @@ -165,8 +167,19 @@ func (c *client) handleUnidirectionalStreams() {
c.session.CloseWithError(quic.ErrorCode(errorFrameError), "")
return
}
if _, ok := f.(*settingsFrame); !ok {
sf, ok := f.(*settingsFrame)
if !ok {
c.session.CloseWithError(quic.ErrorCode(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.session.ConnectionState().SupportsDatagrams {
c.session.CloseWithError(quic.ErrorCode(errorSettingsError), "missing QUIC Datagram support")
}
}()
}
Expand Down
39 changes: 39 additions & 0 deletions http3/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ var _ = Describe("Client", func() {
Expect(dialerCalled).To(BeTrue())
})

It("enables HTTP/3 Datagrams", func() {
testErr := errors.New("handshake error")
client, err := newClient("localhost:1337", nil, &roundTripperOpts{EnableDatagram: true}, nil, nil)
Expect(err).ToNot(HaveOccurred())
dialAddr = func(hostname string, _ *tls.Config, quicConf *quic.Config) (quic.EarlySession, error) {
Expect(quicConf.EnableDatagrams).To(BeTrue())
return nil, testErr
}
_, err = client.RoundTrip(req)
Expect(err).To(MatchError(testErr))
})

It("errors when dialing fails", func() {
testErr := errors.New("handshake error")
client, err := newClient("localhost:1337", nil, &roundTripperOpts{}, nil, nil)
Expand Down Expand Up @@ -316,6 +328,33 @@ var _ = Describe("Client", func() {
Expect(err).To(MatchError("done"))
Eventually(done).Should(BeClosed())
})

It("errors when the server advertises datagram support (and we enabled support for it)", func() {
client.opts.EnableDatagram = true
buf := &bytes.Buffer{}
utils.WriteVarInt(buf, streamTypeControlStream)
(&settingsFrame{Datagram: true}).Write(buf)
controlStr := mockquic.NewMockStream(mockCtrl)
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(buf.Read).AnyTimes()
sess.EXPECT().AcceptUniStream(gomock.Any()).DoAndReturn(func(context.Context) (quic.ReceiveStream, error) {
return controlStr, nil
})
sess.EXPECT().AcceptUniStream(gomock.Any()).DoAndReturn(func(context.Context) (quic.ReceiveStream, error) {
<-testDone
return nil, errors.New("test done")
})
sess.EXPECT().ConnectionState().Return(quic.ConnectionState{SupportsDatagrams: false})
done := make(chan struct{})
sess.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Do(func(code quic.ErrorCode, reason string) {
defer GinkgoRecover()
Expect(code).To(BeEquivalentTo(errorSettingsError))
Expect(reason).To(Equal("missing QUIC Datagram support"))
close(done)
})
_, err := client.RoundTrip(request)
Expect(err).To(MatchError("done"))
Eventually(done).Should(BeClosed())
})
})

Context("Doing requests", func() {
Expand Down
41 changes: 34 additions & 7 deletions http3/frames.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@ func (f *headersFrame) Write(b *bytes.Buffer) {
utils.WriteVarInt(b, f.Length)
}

const settingDatagram = 0x276

type settingsFrame struct {
settings map[uint64]uint64
Datagram bool
other map[uint64]uint64 // all settings that we don't explicitly recognize
}

func parseSettingsFrame(r io.Reader, l uint64) (*settingsFrame, error) {
Expand All @@ -100,8 +103,9 @@ func parseSettingsFrame(r io.Reader, l uint64) (*settingsFrame, error) {
}
return nil, err
}
frame := &settingsFrame{settings: make(map[uint64]uint64)}
frame := &settingsFrame{}
b := bytes.NewReader(buf)
var readDatagram bool
for b.Len() > 0 {
id, err := utils.ReadVarInt(b)
if err != nil { // should not happen. We allocated the whole frame already.
Expand All @@ -111,22 +115,45 @@ func parseSettingsFrame(r io.Reader, l uint64) (*settingsFrame, error) {
if err != nil { // should not happen. We allocated the whole frame already.
return nil, err
}
if _, ok := frame.settings[id]; ok {
return nil, fmt.Errorf("duplicate setting: %d", id)

switch id {
case settingDatagram:
if readDatagram {
return nil, fmt.Errorf("duplicate setting: %d", id)
}
readDatagram = true
if val != 0 && val != 1 {
return nil, fmt.Errorf("invalid value for H3_DATAGRAM: %d", val)
}
frame.Datagram = val == 1
default:
if _, ok := frame.other[id]; ok {
return nil, fmt.Errorf("duplicate setting: %d", id)
}
if frame.other == nil {
frame.other = make(map[uint64]uint64)
}
frame.other[id] = val
}
frame.settings[id] = val
}
return frame, nil
}

func (f *settingsFrame) Write(b *bytes.Buffer) {
utils.WriteVarInt(b, 0x4)
var l protocol.ByteCount
for id, val := range f.settings {
for id, val := range f.other {
l += utils.VarIntLen(id) + utils.VarIntLen(val)
}
if f.Datagram {
l += utils.VarIntLen(settingDatagram) + utils.VarIntLen(1)
}
utils.WriteVarInt(b, uint64(l))
for id, val := range f.settings {
if f.Datagram {
utils.WriteVarInt(b, settingDatagram)
utils.WriteVarInt(b, 1)
}
for id, val := range f.other {
utils.WriteVarInt(b, id)
utils.WriteVarInt(b, val)
}
Expand Down
55 changes: 51 additions & 4 deletions http3/frames_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package http3

import (
"bytes"
"fmt"
"io"

"github.com/lucas-clemente/quic-go/internal/utils"
Expand Down Expand Up @@ -84,8 +85,8 @@ var _ = Describe("Frames", func() {
Expect(err).ToNot(HaveOccurred())
Expect(frame).To(BeAssignableToTypeOf(&settingsFrame{}))
sf := frame.(*settingsFrame)
Expect(sf.settings).To(HaveKeyWithValue(uint64(13), uint64(37)))
Expect(sf.settings).To(HaveKeyWithValue(uint64(0xdead), uint64(0xbeef)))
Expect(sf.other).To(HaveKeyWithValue(uint64(13), uint64(37)))
Expect(sf.other).To(HaveKeyWithValue(uint64(0xdead), uint64(0xbeef)))
})

It("rejects duplicate settings", func() {
Expand All @@ -101,7 +102,7 @@ var _ = Describe("Frames", func() {
})

It("writes", func() {
sf := &settingsFrame{settings: map[uint64]uint64{
sf := &settingsFrame{other: map[uint64]uint64{
1: 2,
99: 999,
13: 37,
Expand All @@ -114,7 +115,7 @@ var _ = Describe("Frames", func() {
})

It("errors on EOF", func() {
sf := &settingsFrame{settings: map[uint64]uint64{
sf := &settingsFrame{other: map[uint64]uint64{
13: 37,
0xdeadbeef: 0xdecafbad,
}}
Expand All @@ -132,5 +133,51 @@ var _ = Describe("Frames", func() {
Expect(err).To(MatchError(io.EOF))
}
})

Context("H3_DATAGRAM", func() {
It("reads the H3_DATAGRAM value", func() {
settings := appendVarInt(nil, settingDatagram)
settings = appendVarInt(settings, 1)
data := appendVarInt(nil, 4) // type byte
data = appendVarInt(data, uint64(len(settings)))
data = append(data, settings...)
f, err := parseNextFrame(bytes.NewReader(data))
Expect(err).ToNot(HaveOccurred())
Expect(f).To(BeAssignableToTypeOf(&settingsFrame{}))
sf := f.(*settingsFrame)
Expect(sf.Datagram).To(BeTrue())
})

It("rejects duplicate H3_DATAGRAM entries", func() {
settings := appendVarInt(nil, settingDatagram)
settings = appendVarInt(settings, 1)
settings = appendVarInt(settings, settingDatagram)
settings = appendVarInt(settings, 1)
data := appendVarInt(nil, 4) // type byte
data = appendVarInt(data, uint64(len(settings)))
data = append(data, settings...)
_, err := parseNextFrame(bytes.NewReader(data))
Expect(err).To(MatchError(fmt.Sprintf("duplicate setting: %d", settingDatagram)))
})

It("rejects invalid values for the H3_DATAGRAM entry", func() {
settings := appendVarInt(nil, settingDatagram)
settings = appendVarInt(settings, 1337)
data := appendVarInt(nil, 4) // type byte
data = appendVarInt(data, uint64(len(settings)))
data = append(data, settings...)
_, err := parseNextFrame(bytes.NewReader(data))
Expect(err).To(MatchError("invalid value for H3_DATAGRAM: 1337"))
})

It("writes the H3_DATAGRAM setting", func() {
sf := &settingsFrame{Datagram: true}
buf := &bytes.Buffer{}
sf.Write(buf)
frame, err := parseNextFrame(buf)
Expect(err).ToNot(HaveOccurred())
Expect(frame).To(Equal(sf))
})
})
})
})
6 changes: 6 additions & 0 deletions http3/roundtrip.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ type RoundTripper struct {
// If nil, reasonable default values will be used.
QuicConfig *quic.Config

// Enable support for HTTP/3 datagrams.
// If set to true, QuicConfig.EnableDatagram will be set.
// See https://www.ietf.org/archive/id/draft-schinazi-masque-h3-datagram-02.html.
EnableDatagrams bool

// Dial specifies an optional dial function for creating QUIC
// connections for requests.
// If Dial is nil, quic.DialAddr will be used.
Expand Down Expand Up @@ -135,6 +140,7 @@ func (r *RoundTripper) getClient(hostname string, onlyCached bool) (http.RoundTr
hostname,
r.TLSClientConfig,
&roundTripperOpts{
EnableDatagram: r.EnableDatagrams,
DisableCompression: r.DisableCompression,
MaxHeaderBytes: r.MaxResponseHeaderBytes,
},
Expand Down
33 changes: 29 additions & 4 deletions http3/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ type Server struct {
// If nil, it uses reasonable default values.
QuicConfig *quic.Config

// Enable support for HTTP/3 datagrams.
// If set to true, QuicConfig.EnableDatagram will be set.
// See https://www.ietf.org/archive/id/draft-schinazi-masque-h3-datagram-02.html.
EnableDatagrams bool

port uint32 // used atomically

mutex sync.Mutex
Expand Down Expand Up @@ -173,10 +178,19 @@ func (s *Server) serveImpl(tlsConf *tls.Config, conn net.PacketConn) error {

var ln quic.EarlyListener
var err error
quicConf := s.QuicConfig
if quicConf == nil {
quicConf = &quic.Config{}
} else {
quicConf = s.QuicConfig.Clone()
}
if s.EnableDatagrams {
quicConf.EnableDatagrams = true
}
if conn == nil {
ln, err = quicListenAddr(s.Addr, baseConf, s.QuicConfig)
ln, err = quicListenAddr(s.Addr, baseConf, quicConf)
} else {
ln, err = quicListen(conn, baseConf, s.QuicConfig)
ln, err = quicListen(conn, baseConf, quicConf)
}
if err != nil {
return err
Expand Down Expand Up @@ -223,7 +237,7 @@ func (s *Server) handleConn(sess quic.EarlySession) {
}
buf := &bytes.Buffer{}
utils.WriteVarInt(buf, streamTypeControlStream) // stream type
(&settingsFrame{}).Write(buf)
(&settingsFrame{Datagram: s.EnableDatagrams}).Write(buf)
str.Write(buf.Bytes())

go s.handleUnidirectionalStreams(sess)
Expand Down Expand Up @@ -287,8 +301,19 @@ func (s *Server) handleUnidirectionalStreams(sess quic.EarlySession) {
sess.CloseWithError(quic.ErrorCode(errorFrameError), "")
return
}
if _, ok := f.(*settingsFrame); !ok {
sf, ok := f.(*settingsFrame)
if !ok {
sess.CloseWithError(quic.ErrorCode(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 && !sess.ConnectionState().SupportsDatagrams {
sess.CloseWithError(quic.ErrorCode(errorSettingsError), "missing QUIC Datagram support")
}
}(str)
}
Expand Down
Loading

0 comments on commit 4ad144c

Please sign in to comment.