forked from quic-go/quic-go
http3: implement client-side GOAWAY handling (#5143)
When receiving a GOAWAY frame, the client: * immediately closes the connection if there are no active requests * refuses to open streams with stream IDs larger than the stream ID in the GOAWAY frame * closes the connection once the stream count drops to zero
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -149,35 +150,96 @@ func TestConnResetUnknownUniStream(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConnControlStreamFailures(t *testing.T) {
|
||||
t.Run("missing settings", func(t *testing.T) {
|
||||
testConnControlStreamFailures(t, (&dataFrame{}).Append(nil), ErrCodeMissingSettings)
|
||||
t.Run("missing SETTINGS", func(t *testing.T) {
|
||||
testConnControlStreamFailures(t, (&dataFrame{}).Append(nil), nil, ErrCodeMissingSettings)
|
||||
})
|
||||
t.Run("frame error", func(t *testing.T) {
|
||||
testConnControlStreamFailures(t,
|
||||
// 1337 is invalid value for the Extended CONNECT setting
|
||||
(&settingsFrame{Other: map[uint64]uint64{settingExtendedConnect: 1337}}).Append(nil),
|
||||
nil,
|
||||
ErrCodeFrameError,
|
||||
)
|
||||
})
|
||||
t.Run("control stream closed before SETTINGS", func(t *testing.T) {
|
||||
testConnControlStreamFailures(t, nil, io.EOF, ErrCodeClosedCriticalStream)
|
||||
})
|
||||
t.Run("control stream reset before SETTINGS", func(t *testing.T) {
|
||||
testConnControlStreamFailures(t,
|
||||
nil,
|
||||
&quic.StreamError{Remote: true, ErrorCode: 42},
|
||||
ErrCodeClosedCriticalStream,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func testConnControlStreamFailures(t *testing.T, data []byte, expectedErr ErrCode) {
|
||||
func TestConnGoAwayFailures(t *testing.T) {
|
||||
t.Run("invalid frame", func(t *testing.T) {
|
||||
b := (&settingsFrame{}).Append(nil)
|
||||
// 1337 is invalid value for the Extended CONNECT setting
|
||||
b = (&settingsFrame{Other: map[uint64]uint64{settingExtendedConnect: 1337}}).Append(b)
|
||||
testConnControlStreamFailures(t, b, nil, ErrCodeFrameError)
|
||||
})
|
||||
t.Run("not a GOAWAY", func(t *testing.T) {
|
||||
b := (&settingsFrame{}).Append(nil)
|
||||
// GOAWAY is the only allowed frame type after SETTINGS
|
||||
b = (&headersFrame{}).Append(b)
|
||||
testConnControlStreamFailures(t, b, nil, ErrCodeFrameUnexpected)
|
||||
})
|
||||
t.Run("stream closed before GOAWAY", func(t *testing.T) {
|
||||
testConnControlStreamFailures(t, (&settingsFrame{}).Append(nil), io.EOF, ErrCodeClosedCriticalStream)
|
||||
})
|
||||
t.Run("stream reset before GOAWAY", func(t *testing.T) {
|
||||
testConnControlStreamFailures(t,
|
||||
(&settingsFrame{}).Append(nil),
|
||||
&quic.StreamError{Remote: true, ErrorCode: 42},
|
||||
ErrCodeClosedCriticalStream,
|
||||
)
|
||||
})
|
||||
t.Run("invalid stream ID", func(t *testing.T) {
|
||||
data := (&settingsFrame{}).Append(nil)
|
||||
data = (&goAwayFrame{StreamID: 1}).Append(data)
|
||||
testConnControlStreamFailures(t, data, nil, ErrCodeIDError)
|
||||
})
|
||||
t.Run("increased stream ID", func(t *testing.T) {
|
||||
data := (&settingsFrame{}).Append(nil)
|
||||
data = (&goAwayFrame{StreamID: 4}).Append(data)
|
||||
data = (&goAwayFrame{StreamID: 8}).Append(data)
|
||||
testConnControlStreamFailures(t, data, nil, ErrCodeIDError)
|
||||
})
|
||||
}
|
||||
|
||||
func testConnControlStreamFailures(t *testing.T, data []byte, readErr error, expectedErr ErrCode) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
qconn := mockquic.NewMockEarlyConnection(mockCtrl)
|
||||
conn := newConnection(
|
||||
context.Background(),
|
||||
qconn,
|
||||
false,
|
||||
protocol.PerspectiveServer,
|
||||
protocol.PerspectiveClient,
|
||||
nil,
|
||||
0,
|
||||
)
|
||||
b := quicvarint.Append(nil, streamTypeControlStream)
|
||||
b = append(b, data...)
|
||||
r := bytes.NewReader(b)
|
||||
controlStr := mockquic.NewMockStream(mockCtrl)
|
||||
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(bytes.NewReader(b).Read).AnyTimes()
|
||||
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(func(b []byte) (int, error) {
|
||||
if r.Len() == 0 {
|
||||
return 0, readErr
|
||||
}
|
||||
return r.Read(b)
|
||||
}).AnyTimes()
|
||||
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr, nil)
|
||||
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done"))
|
||||
closed := make(chan struct{})
|
||||
|
||||
str := mockquic.NewMockStream(mockCtrl)
|
||||
str.EXPECT().StreamID().Return(4).AnyTimes()
|
||||
str.EXPECT().Context().Return(context.Background()).AnyTimes()
|
||||
qconn.EXPECT().OpenStreamSync(gomock.Any()).Return(str, nil)
|
||||
conn.openRequestStream(context.Background(), nil, nil, true, 1000)
|
||||
|
||||
qconn.EXPECT().CloseWithError(quic.ApplicationErrorCode(expectedErr), gomock.Any()).Do(func(quic.ApplicationErrorCode, string) error {
|
||||
close(closed)
|
||||
return nil
|
||||
@@ -199,6 +261,88 @@ func testConnControlStreamFailures(t *testing.T, data []byte, expectedErr ErrCod
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnGoAway(t *testing.T) {
|
||||
t.Run("no active streams", func(t *testing.T) {
|
||||
testConnGoAway(t, false)
|
||||
})
|
||||
t.Run("active stream", func(t *testing.T) {
|
||||
testConnGoAway(t, true)
|
||||
})
|
||||
}
|
||||
|
||||
func testConnGoAway(t *testing.T, withStream bool) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
qconn := mockquic.NewMockEarlyConnection(mockCtrl)
|
||||
conn := newConnection(
|
||||
context.Background(),
|
||||
qconn,
|
||||
false,
|
||||
protocol.PerspectiveClient,
|
||||
nil,
|
||||
0,
|
||||
)
|
||||
b := quicvarint.Append(nil, streamTypeControlStream)
|
||||
b = (&settingsFrame{}).Append(b)
|
||||
b = (&goAwayFrame{StreamID: 4}).Append(b)
|
||||
|
||||
var mockStr *mockquic.MockStream
|
||||
var str quic.Stream
|
||||
if withStream {
|
||||
mockStr = mockquic.NewMockStream(mockCtrl)
|
||||
mockStr.EXPECT().StreamID().Return(4).AnyTimes()
|
||||
mockStr.EXPECT().Context().Return(context.Background()).AnyTimes()
|
||||
qconn.EXPECT().OpenStreamSync(gomock.Any()).Return(mockStr, nil)
|
||||
s, err := conn.openRequestStream(context.Background(), nil, nil, true, 1000)
|
||||
require.NoError(t, err)
|
||||
str = s
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
r := bytes.NewReader(b)
|
||||
controlStr := mockquic.NewMockStream(mockCtrl)
|
||||
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(func(b []byte) (int, error) {
|
||||
if r.Len() == 0 {
|
||||
<-done
|
||||
return 0, errors.New("test done")
|
||||
}
|
||||
return r.Read(b)
|
||||
}).AnyTimes()
|
||||
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr, nil)
|
||||
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done"))
|
||||
closed := make(chan struct{})
|
||||
qconn.EXPECT().CloseWithError(quic.ApplicationErrorCode(ErrCodeNoError), gomock.Any()).Do(func(quic.ApplicationErrorCode, string) error {
|
||||
close(closed)
|
||||
return nil
|
||||
})
|
||||
// duplicate calls to CloseWithError are a no-op
|
||||
qconn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
go conn.handleUnidirectionalStreams(nil)
|
||||
|
||||
// the connection should be closed after the stream is closed
|
||||
if withStream {
|
||||
select {
|
||||
case <-closed:
|
||||
t.Fatal("connection closed")
|
||||
case <-time.After(scaleDuration(10 * time.Millisecond)):
|
||||
}
|
||||
|
||||
_, err := conn.openRequestStream(context.Background(), nil, nil, true, 1000)
|
||||
require.ErrorIs(t, err, errGoAway)
|
||||
|
||||
mockStr.EXPECT().Close()
|
||||
str.Close()
|
||||
mockStr.EXPECT().CancelRead(gomock.Any())
|
||||
str.CancelRead(1337)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-closed:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timeout waiting for close")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnRejectPushStream(t *testing.T) {
|
||||
t.Run("client", func(t *testing.T) {
|
||||
testConnRejectPushStream(t, protocol.PerspectiveClient, ErrCodeStreamCreationError)
|
||||
@@ -299,8 +443,16 @@ func TestConnSendAndReceiveDatagram(t *testing.T) {
|
||||
)
|
||||
b := quicvarint.Append(nil, streamTypeControlStream)
|
||||
b = (&settingsFrame{Datagram: true}).Append(b)
|
||||
r := bytes.NewReader(b)
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
controlStr := mockquic.NewMockStream(mockCtrl)
|
||||
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(bytes.NewReader(b).Read).AnyTimes()
|
||||
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(func(b []byte) (int, error) {
|
||||
if r.Len() == 0 {
|
||||
<-done
|
||||
}
|
||||
return r.Read(b)
|
||||
}).AnyTimes()
|
||||
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr, nil).MaxTimes(1)
|
||||
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done")).MaxTimes(1)
|
||||
qconn.EXPECT().ConnectionState().Return(quic.ConnectionState{SupportsDatagrams: true}).MaxTimes(1)
|
||||
@@ -351,6 +503,8 @@ func TestConnSendAndReceiveDatagram(t *testing.T) {
|
||||
expected = append(expected, []byte("foobaz")...)
|
||||
qconn.EXPECT().SendDatagram(expected).Return(assert.AnError)
|
||||
require.ErrorIs(t, conn.sendDatagram(strID2, []byte("foobaz")), assert.AnError)
|
||||
|
||||
qconn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
}
|
||||
|
||||
func TestConnDatagramFailures(t *testing.T) {
|
||||
@@ -376,18 +530,24 @@ func testConnDatagramFailures(t *testing.T, datagram []byte) {
|
||||
b := quicvarint.Append(nil, streamTypeControlStream)
|
||||
b = (&settingsFrame{Datagram: true}).Append(b)
|
||||
r := bytes.NewReader(b)
|
||||
done := make(chan struct{})
|
||||
controlStr := mockquic.NewMockStream(mockCtrl)
|
||||
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(r.Read).AnyTimes()
|
||||
controlStr.EXPECT().Read(gomock.Any()).DoAndReturn(func(b []byte) (int, error) {
|
||||
if r.Len() == 0 {
|
||||
<-done
|
||||
}
|
||||
return r.Read(b)
|
||||
}).AnyTimes()
|
||||
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(controlStr, nil).MaxTimes(1)
|
||||
qconn.EXPECT().AcceptUniStream(gomock.Any()).Return(nil, errors.New("test done")).MaxTimes(1)
|
||||
qconn.EXPECT().ConnectionState().Return(quic.ConnectionState{SupportsDatagrams: true}).MaxTimes(1)
|
||||
|
||||
qconn.EXPECT().ReceiveDatagram(gomock.Any()).Return(datagram, nil)
|
||||
done := make(chan struct{})
|
||||
qconn.EXPECT().CloseWithError(qerr.ApplicationErrorCode(ErrCodeDatagramError), gomock.Any()).Do(func(qerr.ApplicationErrorCode, string) error {
|
||||
close(done)
|
||||
return nil
|
||||
})
|
||||
qconn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).AnyTimes() // further calls to CloseWithError are a no-op
|
||||
go func() { conn.handleUnidirectionalStreams(nil) }()
|
||||
select {
|
||||
case <-done:
|
||||
|
||||
Reference in New Issue
Block a user