blob: b915d79c858ba2c9e0b1349832c428086e779e6d [file] [log] [blame]
// Copyright 2024 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.
//go:build go1.24 && goexperiment.synctest
package http3
import (
"context"
"errors"
"testing"
"testing/synctest"
"golang.org/x/net/quic"
)
func TestTransportCreatesControlStream(t *testing.T) {
runSynctest(t, func(t testing.TB) {
tc := newTestClientConn(t)
controlStream := tc.wantStream(
"client creates control stream",
streamTypeControl)
controlStream.wantFrameHeader(
"client sends SETTINGS frame on control stream",
frameTypeSettings)
controlStream.discardFrame()
})
}
func TestTransportUnknownUnidirectionalStream(t *testing.T) {
// "Recipients of unknown stream types MUST either abort reading of the stream
// or discard incoming data without further processing."
// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2-7
runSynctest(t, func(t testing.TB) {
tc := newTestClientConn(t)
tc.greet()
st := tc.newStream(0x21) // reserved stream type
// The client should send a STOP_SENDING for this stream,
// but it should not close the connection.
synctest.Wait()
if _, err := st.Write([]byte("hello")); err == nil {
t.Fatalf("write to send-only stream with an unknown type succeeded; want error")
}
tc.wantNotClosed("after receiving unknown unidirectional stream type")
})
}
func TestTransportUnknownSettings(t *testing.T) {
// "An implementation MUST ignore any [settings] parameter with
// an identifier it does not understand."
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-9
runSynctest(t, func(t testing.TB) {
tc := newTestClientConn(t)
controlStream := tc.newStream(streamTypeControl)
controlStream.writeSettings(0x1f+0x21, 0) // reserved settings type
controlStream.Flush()
tc.wantNotClosed("after receiving unknown settings")
})
}
func TestTransportInvalidSettings(t *testing.T) {
// "These reserved settings MUST NOT be sent, and their receipt MUST
// be treated as a connection error of type H3_SETTINGS_ERROR."
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1-5
runSynctest(t, func(t testing.TB) {
tc := newTestClientConn(t)
controlStream := tc.newStream(streamTypeControl)
controlStream.writeSettings(0x02, 0) // HTTP/2 SETTINGS_ENABLE_PUSH
controlStream.Flush()
tc.wantClosed("invalid setting", errH3SettingsError)
})
}
func TestTransportDuplicateStream(t *testing.T) {
for _, stype := range []streamType{
streamTypeControl,
streamTypeEncoder,
streamTypeDecoder,
} {
runSynctestSubtest(t, stype.String(), func(t testing.TB) {
tc := newTestClientConn(t)
_ = tc.newStream(stype)
tc.wantNotClosed("after creating one " + stype.String() + " stream")
// Opening a second control, encoder, or decoder stream
// is a protocol violation.
_ = tc.newStream(stype)
tc.wantClosed("duplicate stream", errH3StreamCreationError)
})
}
}
func TestTransportUnknownFrames(t *testing.T) {
for _, stype := range []streamType{
streamTypeControl,
} {
runSynctestSubtest(t, stype.String(), func(t testing.TB) {
tc := newTestClientConn(t)
tc.greet()
st := tc.peerUnidirectionalStream(stype)
data := "frame content"
st.writeVarint(0x1f + 0x21) // reserved frame type
st.writeVarint(int64(len(data))) // size
st.Write([]byte(data))
st.Flush()
tc.wantNotClosed("after writing unknown frame")
})
}
}
func TestTransportInvalidFrames(t *testing.T) {
runSynctest(t, func(t testing.TB) {
tc := newTestClientConn(t)
tc.greet()
tc.control.writeVarint(int64(frameTypeData))
tc.control.writeVarint(0) // size
tc.control.Flush()
tc.wantClosed("after writing DATA frame to control stream", errH3FrameUnexpected)
})
}
func TestTransportServerCreatesBidirectionalStream(t *testing.T) {
// "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
runSynctest(t, func(t testing.TB) {
tc := newTestClientConn(t)
tc.greet()
st := tc.newStream(streamTypeRequest)
st.Flush()
tc.wantClosed("after server creates bidi stream", errH3StreamCreationError)
})
}
func TestTransportServerCreatesBadUnidirectionalStream(t *testing.T) {
runSynctest(t, func(t testing.TB) {
tc := newTestClientConn(t)
tc.greet()
// Create and close a stream without sending the unidirectional stream header.
qs, err := tc.qconn.NewSendOnlyStream(canceledCtx)
if err != nil {
t.Fatal(err)
}
st := newTestQUICStream(tc.t, newStream(qs))
st.stream.stream.Close()
tc.wantClosed("after server creates and closes uni stream", errH3StreamCreationError)
})
}
// A testQUICConn wraps a *quic.Conn and provides methods for inspecting it.
type testQUICConn struct {
t testing.TB
qconn *quic.Conn
streams map[streamType][]*testQUICStream
}
func newTestQUICConn(t testing.TB, qconn *quic.Conn) *testQUICConn {
tq := &testQUICConn{
t: t,
qconn: qconn,
streams: make(map[streamType][]*testQUICStream),
}
go tq.acceptStreams(t.Context())
t.Cleanup(func() {
tq.qconn.Close()
})
return tq
}
func (tq *testQUICConn) acceptStreams(ctx context.Context) {
for {
qst, err := tq.qconn.AcceptStream(ctx)
if err != nil {
return
}
st := newStream(qst)
stype := streamTypeRequest
if qst.IsReadOnly() {
v, err := st.readVarint()
if err != nil {
tq.t.Errorf("error reading stream type from unidirectional stream: %v", err)
continue
}
stype = streamType(v)
}
tq.streams[stype] = append(tq.streams[stype], newTestQUICStream(tq.t, st))
}
}
// wantNotClosed asserts that the peer has not closed the connectioln.
func (tq *testQUICConn) wantNotClosed(reason string) {
t := tq.t
t.Helper()
synctest.Wait()
err := tq.qconn.Wait(canceledCtx)
if !errors.Is(err, context.Canceled) {
t.Fatalf("%v: want QUIC connection to be alive; closed with error: %v", reason, err)
}
}
// wantClosed asserts that the peer has closed the connection
// with the provided error code.
func (tq *testQUICConn) wantClosed(reason string, want error) {
t := tq.t
t.Helper()
synctest.Wait()
if e, ok := want.(http3Error); ok {
want = &quic.ApplicationError{Code: uint64(e)}
}
got := tq.qconn.Wait(canceledCtx)
if errors.Is(got, context.Canceled) {
t.Fatalf("%v: want QUIC connection closed, but it is not", reason)
}
if !errors.Is(got, want) {
t.Fatalf("%v: connection closed with error: %v; want %v", reason, got, want)
}
}
// wantStream asserts that a stream of a given type has been created,
// and returns that stream.
func (tq *testQUICConn) wantStream(reason string, stype streamType) *testQUICStream {
tq.t.Helper()
if len(tq.streams[stype]) == 0 {
tq.t.Fatalf("%v: stream not created", reason)
}
ts := tq.streams[stype][0]
tq.streams[stype] = tq.streams[stype][1:]
return ts
}
// testQUICStream wraps a QUIC stream and provides methods for inspecting it.
type testQUICStream struct {
t testing.TB
*stream
}
func newTestQUICStream(t testing.TB, st *stream) *testQUICStream {
st.stream.SetReadContext(canceledCtx)
st.stream.SetWriteContext(canceledCtx)
return &testQUICStream{
t: t,
stream: st,
}
}
// wantFrameHeader calls readFrameHeader and asserts that the frame is of a given type.
func (ts *testQUICStream) wantFrameHeader(reason string, wantType frameType) {
ts.t.Helper()
gotType, err := ts.readFrameHeader()
if err != nil {
ts.t.Fatalf("%v: failed to read frame header: %v", reason, err)
}
if gotType != wantType {
ts.t.Fatalf("%v: got frame type %v, want %v", reason, gotType, wantType)
}
}
func (ts *testQUICStream) Flush() error {
err := ts.stream.Flush()
if err != nil {
ts.t.Errorf("unexpected error flushing stream: %v", err)
}
return err
}
// A testClientConn is a ClientConn on a test network.
type testClientConn struct {
tr *Transport
cc *ClientConn
// *testQUICConn is the server half of the connection.
*testQUICConn
control *testQUICStream
}
func newTestClientConn(t testing.TB) *testClientConn {
e1, e2 := newQUICEndpointPair(t)
tr := &Transport{
Endpoint: e1,
Config: &quic.Config{
TLSConfig: testTLSConfig,
},
}
cc, err := tr.Dial(t.Context(), e2.LocalAddr().String())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
cc.Close()
})
srvConn, err := e2.Accept(t.Context())
if err != nil {
t.Fatal(err)
}
tc := &testClientConn{
tr: tr,
cc: cc,
testQUICConn: newTestQUICConn(t, srvConn),
}
synctest.Wait()
return tc
}
// greet performs initial connection handshaking with the client.
func (tc *testClientConn) greet() {
// Client creates a control stream.
clientControlStream := tc.wantStream(
"client creates control stream",
streamTypeControl)
clientControlStream.wantFrameHeader(
"client sends SETTINGS frame on control stream",
frameTypeSettings)
clientControlStream.discardFrame()
// Server creates a control stream.
tc.control = tc.newStream(streamTypeControl)
tc.control.writeVarint(int64(frameTypeSettings))
tc.control.writeVarint(0) // size
tc.control.Flush()
synctest.Wait()
}
func (tc *testClientConn) newStream(stype streamType) *testQUICStream {
tc.t.Helper()
var qs *quic.Stream
var err error
if stype == streamTypeRequest {
qs, err = tc.qconn.NewStream(canceledCtx)
} else {
qs, err = tc.qconn.NewSendOnlyStream(canceledCtx)
}
if err != nil {
tc.t.Fatal(err)
}
st := newStream(qs)
if stype != streamTypeRequest {
st.writeVarint(int64(stype))
if err := st.Flush(); err != nil {
tc.t.Fatal(err)
}
}
return newTestQUICStream(tc.t, st)
}
// peerUnidirectionalStream returns the peer-created unidirectional stream with
// the given type. It produces a fatal error if the peer hasn't created this stream.
func (tc *testClientConn) peerUnidirectionalStream(stype streamType) *testQUICStream {
var st *testQUICStream
switch stype {
case streamTypeControl:
st = tc.control
}
if st == nil {
tc.t.Fatalf("want peer stream of type %v, but not created yet", stype)
}
return st
}
// canceledCtx is a canceled Context.
// Used for performing non-blocking QUIC operations.
var canceledCtx = func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return ctx
}()