Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

check the server's HTTP/3 SETTINGS before initiating a session #120

Merged
merged 1 commit into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/quic-go/quic-go/quicvarint"
)

var errNoWebTransport = errors.New("server didn't enable WebTransport")

type Dialer struct {
// If not set, reasonable defaults will be used.
// In order for WebTransport to function, this implementation will:
Expand Down Expand Up @@ -110,7 +112,25 @@ func (d *Dialer) Dial(ctx context.Context, urlStr string, reqHdr http.Header) (*
}
req = req.WithContext(ctx)

rsp, err := d.RoundTripper.RoundTripOpt(req, http3.RoundTripOpt{DontCloseRequestStream: true})
rsp, err := d.RoundTripper.RoundTripOpt(req, http3.RoundTripOpt{
DontCloseRequestStream: true,
CheckSettings: func(settings http3.Settings) error {
if !settings.EnableExtendedConnect {
return errors.New("server didn't enable Extended CONNECT")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this check will break compatibility with older and the current version of webtransport-go, since quic-go servers only start enabling Extended CONNECT starting with the next release.

cc @sukunrt @MarcoPolo

}
if !settings.EnableDatagram {
return errors.New("server didn't enable HTTP/3 datagram support")
}
if settings.Other == nil {
return errNoWebTransport
}
s, ok := settings.Other[settingsEnableWebtransport]
if !ok || s != 1 {
return errNoWebTransport
}
return nil
},
})
if err != nil {
return nil, nil, err
}
Expand Down
111 changes: 107 additions & 4 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,53 @@ func (c *requestStreamDelayingConn) OpenStreamSync(ctx context.Context) (quic.St
return str, nil
}

const (
// Extended CONNECT, RFC 9220
settingExtendedConnect = 0x8
// HTTP Datagrams, RFC 9297
settingDatagram = 0x33
// WebTransport
settingsEnableWebtransport = 0x2b603742
)

// appendSettingsFrame serializes an HTTP/3 SETTINGS frame
// It reimplements the function in the http3 package, in a slightly simplified way.
func appendSettingsFrame(b []byte, values map[uint64]uint64) []byte {
b = quicvarint.Append(b, 0x4)
var l uint64
for k, val := range values {
l += uint64(quicvarint.Len(k)) + uint64(quicvarint.Len(val))
}
b = quicvarint.Append(b, l)
for id, val := range values {
b = quicvarint.Append(b, id)
b = quicvarint.Append(b, val)
}
return b
}

func TestClientInvalidResponseHandling(t *testing.T) {
tlsConf := tlsConf.Clone()
tlsConf.NextProtos = []string{"h3"}
s, err := quic.ListenAddr("localhost:0", tlsConf, nil)
s, err := quic.ListenAddr("localhost:0", tlsConf, &quic.Config{EnableDatagrams: true})
require.NoError(t, err)
errChan := make(chan error)
go func() {
conn, err := s.Accept(context.Background())
require.NoError(t, err)
// send the SETTINGS frame
settingsStr, err := conn.OpenUniStream()
require.NoError(t, err)
_, err = settingsStr.Write(appendSettingsFrame([]byte{0} /* stream type */, map[uint64]uint64{
settingDatagram: 1,
settingExtendedConnect: 1,
settingsEnableWebtransport: 1,
}))
require.NoError(t, err)

str, err := conn.AcceptStream(context.Background())
require.NoError(t, err)
// write a HTTP3 data frame. This will cause an error, since a HEADERS frame is expected
// write an HTTP/3 data frame. This will cause an error, since a HEADERS frame is expected
var b []byte
b = quicvarint.Append(b, 0x0)
b = quicvarint.Append(b, 1337)
Expand All @@ -79,11 +114,79 @@ func TestClientInvalidResponseHandling(t *testing.T) {
}
_, _, err = d.Dial(context.Background(), fmt.Sprintf("https://localhost:%d", s.Addr().(*net.UDPAddr).Port), nil)
require.Error(t, err)
sErr := <-errChan
var sErr error
select {
case sErr = <-errChan:
case <-time.After(5 * time.Second):
t.Fatal("timeout")
}
require.Error(t, sErr)
var appErr *quic.ApplicationError
require.True(t, errors.As(sErr, &appErr))
require.Equal(t, quic.ApplicationErrorCode(0x105), appErr.ErrorCode) // H3_FRAME_UNEXPECTED
require.Equal(t, http3.ErrCodeFrameUnexpected, http3.ErrCode(appErr.ErrorCode))
}

func TestClientInvalidSettingsHandling(t *testing.T) {
for _, tc := range []struct {
name string
settings map[uint64]uint64
errorStr string
}{
{
name: "Extended CONNECT disabled",
settings: map[uint64]uint64{
settingDatagram: 1,
settingExtendedConnect: 0,
settingsEnableWebtransport: 1,
},
errorStr: "server didn't enable Extended CONNECT",
},
{
name: "HTTP/3 DATAGRAMs disabled",
settings: map[uint64]uint64{
settingDatagram: 0,
settingExtendedConnect: 1,
settingsEnableWebtransport: 1,
},
errorStr: "server didn't enable HTTP/3 datagram support",
},
{
name: "WebTransport disabled",
settings: map[uint64]uint64{
settingDatagram: 1,
settingExtendedConnect: 1,
settingsEnableWebtransport: 0,
},
errorStr: "server didn't enable WebTransport",
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tlsConf := tlsConf.Clone()
tlsConf.NextProtos = []string{"h3"}
s, err := quic.ListenAddr("localhost:0", tlsConf, &quic.Config{EnableDatagrams: true})
require.NoError(t, err)
go func() {
conn, err := s.Accept(context.Background())
require.NoError(t, err)
// send the SETTINGS frame
settingsStr, err := conn.OpenUniStream()
require.NoError(t, err)
_, err = settingsStr.Write(appendSettingsFrame([]byte{0} /* stream type */, tc.settings))
require.NoError(t, err)
}()

d := webtransport.Dialer{
RoundTripper: &http3.RoundTripper{
TLSClientConfig: &tls.Config{RootCAs: certPool},
},
}
_, _, err = d.Dial(context.Background(), fmt.Sprintf("https://localhost:%d", s.Addr().(*net.UDPAddr).Port), nil)
require.Error(t, err)
require.ErrorContains(t, err, tc.errorStr)
})

}
}

func TestClientReorderedUpgrade(t *testing.T) {
Expand Down
Loading