blob: 42f2bdd4a550e558f22c41dddae2476d1d2d10b9 [file] [log] [blame]
// Copyright 2023 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.21
package quic
import (
"bytes"
"context"
"crypto/tls"
"net/netip"
"testing"
"time"
)
type retryServerTest struct {
te *testEndpoint
originalSrcConnID []byte
originalDstConnID []byte
retry retryPacket
initialCrypto []byte
}
// newRetryServerTest creates a test server connection,
// sends the connection an Initial packet,
// and expects a Retry in response.
func newRetryServerTest(t *testing.T) *retryServerTest {
t.Helper()
config := &Config{
TLSConfig: newTestTLSConfig(serverSide),
RequireAddressValidation: true,
}
te := newTestEndpoint(t, config)
srcID := testPeerConnID(0)
dstID := testLocalConnID(-1)
params := defaultTransportParameters()
params.initialSrcConnID = srcID
initialCrypto := initialClientCrypto(t, te, params)
// Initial packet with no Token.
// Server responds with a Retry containing a token.
te.writeDatagram(&testDatagram{
packets: []*testPacket{{
ptype: packetTypeInitial,
num: 0,
version: quicVersion1,
srcConnID: srcID,
dstConnID: dstID,
frames: []debugFrame{
debugFrameCrypto{
data: initialCrypto,
},
},
}},
paddedSize: 1200,
})
got := te.readDatagram()
if len(got.packets) != 1 || got.packets[0].ptype != packetTypeRetry {
t.Fatalf("got datagram: %v\nwant Retry", got)
}
p := got.packets[0]
if got, want := p.dstConnID, srcID; !bytes.Equal(got, want) {
t.Fatalf("Retry destination = {%x}, want {%x}", got, want)
}
return &retryServerTest{
te: te,
originalSrcConnID: srcID,
originalDstConnID: dstID,
retry: retryPacket{
dstConnID: p.dstConnID,
srcConnID: p.srcConnID,
token: p.token,
},
initialCrypto: initialCrypto,
}
}
func TestRetryServerSucceeds(t *testing.T) {
rt := newRetryServerTest(t)
te := rt.te
te.advance(retryTokenValidityPeriod)
te.writeDatagram(&testDatagram{
packets: []*testPacket{{
ptype: packetTypeInitial,
num: 1,
version: quicVersion1,
srcConnID: rt.originalSrcConnID,
dstConnID: rt.retry.srcConnID,
token: rt.retry.token,
frames: []debugFrame{
debugFrameCrypto{
data: rt.initialCrypto,
},
},
}},
paddedSize: 1200,
})
tc := te.accept()
initial := tc.readPacket()
if initial == nil || initial.ptype != packetTypeInitial {
t.Fatalf("got packet:\n%v\nwant: Initial", initial)
}
handshake := tc.readPacket()
if handshake == nil || handshake.ptype != packetTypeHandshake {
t.Fatalf("got packet:\n%v\nwant: Handshake", initial)
}
if got, want := tc.sentTransportParameters.retrySrcConnID, rt.retry.srcConnID; !bytes.Equal(got, want) {
t.Errorf("retry_source_connection_id = {%x}, want {%x}", got, want)
}
if got, want := tc.sentTransportParameters.initialSrcConnID, initial.srcConnID; !bytes.Equal(got, want) {
t.Errorf("initial_source_connection_id = {%x}, want {%x}", got, want)
}
if got, want := tc.sentTransportParameters.originalDstConnID, rt.originalDstConnID; !bytes.Equal(got, want) {
t.Errorf("original_destination_connection_id = {%x}, want {%x}", got, want)
}
}
func TestRetryServerTokenInvalid(t *testing.T) {
// "If a server receives a client Initial that contains an invalid Retry token [...]
// the server SHOULD immediately close [...] the connection with an
// INVALID_TOKEN error."
// https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5
rt := newRetryServerTest(t)
te := rt.te
te.writeDatagram(&testDatagram{
packets: []*testPacket{{
ptype: packetTypeInitial,
num: 1,
version: quicVersion1,
srcConnID: rt.originalSrcConnID,
dstConnID: rt.retry.srcConnID,
token: append(rt.retry.token, 0),
frames: []debugFrame{
debugFrameCrypto{
data: rt.initialCrypto,
},
},
}},
paddedSize: 1200,
})
te.wantDatagram("server closes connection after Initial with invalid Retry token",
initialConnectionCloseDatagram(
rt.retry.srcConnID,
rt.originalSrcConnID,
errInvalidToken))
}
func TestRetryServerTokenTooOld(t *testing.T) {
// "[...] a token SHOULD have an expiration time [...]"
// https://www.rfc-editor.org/rfc/rfc9000#section-8.1.3-3
rt := newRetryServerTest(t)
te := rt.te
te.advance(retryTokenValidityPeriod + time.Second)
te.writeDatagram(&testDatagram{
packets: []*testPacket{{
ptype: packetTypeInitial,
num: 1,
version: quicVersion1,
srcConnID: rt.originalSrcConnID,
dstConnID: rt.retry.srcConnID,
token: rt.retry.token,
frames: []debugFrame{
debugFrameCrypto{
data: rt.initialCrypto,
},
},
}},
paddedSize: 1200,
})
te.wantDatagram("server closes connection after Initial with expired token",
initialConnectionCloseDatagram(
rt.retry.srcConnID,
rt.originalSrcConnID,
errInvalidToken))
}
func TestRetryServerTokenWrongIP(t *testing.T) {
// "Tokens sent in Retry packets SHOULD include information that allows the server
// to verify that the source IP address and port in client packets remain constant."
// https://www.rfc-editor.org/rfc/rfc9000#section-8.1.4-3
rt := newRetryServerTest(t)
te := rt.te
te.writeDatagram(&testDatagram{
packets: []*testPacket{{
ptype: packetTypeInitial,
num: 1,
version: quicVersion1,
srcConnID: rt.originalSrcConnID,
dstConnID: rt.retry.srcConnID,
token: rt.retry.token,
frames: []debugFrame{
debugFrameCrypto{
data: rt.initialCrypto,
},
},
}},
paddedSize: 1200,
addr: netip.MustParseAddrPort("10.0.0.2:8000"),
})
te.wantDatagram("server closes connection after Initial from wrong address",
initialConnectionCloseDatagram(
rt.retry.srcConnID,
rt.originalSrcConnID,
errInvalidToken))
}
func TestRetryServerIgnoresRetry(t *testing.T) {
tc := newTestConn(t, serverSide)
tc.handshake()
tc.write(&testDatagram{
packets: []*testPacket{{
ptype: packetTypeRetry,
originalDstConnID: testLocalConnID(-1),
srcConnID: testPeerConnID(0),
dstConnID: testLocalConnID(0),
token: []byte{1, 2, 3, 4},
}},
})
// Send two packets, to trigger an immediate ACK.
tc.writeFrames(packetType1RTT, debugFramePing{})
tc.writeFrames(packetType1RTT, debugFramePing{})
tc.wantFrameType("server connection ignores spurious Retry packet",
packetType1RTT, debugFrameAck{})
}
func TestRetryClientSuccess(t *testing.T) {
// "This token MUST be repeated by the client in all Initial packets it sends
// for that connection after it receives the Retry packet."
// https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-1
tc := newTestConn(t, clientSide)
tc.wantFrame("client Initial CRYPTO data",
packetTypeInitial, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
})
newServerConnID := []byte("new_conn_id")
token := []byte("token")
tc.write(&testDatagram{
packets: []*testPacket{{
ptype: packetTypeRetry,
originalDstConnID: testLocalConnID(-1),
srcConnID: newServerConnID,
dstConnID: testLocalConnID(0),
token: token,
}},
})
tc.wantPacket("client sends a new Initial packet with a token",
&testPacket{
ptype: packetTypeInitial,
num: 1,
version: quicVersion1,
srcConnID: testLocalConnID(0),
dstConnID: newServerConnID,
token: token,
frames: []debugFrame{
debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
},
},
},
)
tc.advanceToTimer()
tc.wantPacket("after PTO client sends another Initial packet with a token",
&testPacket{
ptype: packetTypeInitial,
num: 2,
version: quicVersion1,
srcConnID: testLocalConnID(0),
dstConnID: newServerConnID,
token: token,
frames: []debugFrame{
debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
},
},
},
)
}
func TestRetryClientInvalidServerTransportParameters(t *testing.T) {
// Various permutations of missing or invalid values for transport parameters
// after a Retry.
// https://www.rfc-editor.org/rfc/rfc9000#section-7.3
initialSrcConnID := testPeerConnID(0)
originalDstConnID := testLocalConnID(-1)
retrySrcConnID := testPeerConnID(100)
for _, test := range []struct {
name string
f func(*transportParameters)
ok bool
}{{
name: "valid",
f: func(p *transportParameters) {},
ok: true,
}, {
name: "missing initial_source_connection_id",
f: func(p *transportParameters) {
p.initialSrcConnID = nil
},
}, {
name: "invalid initial_source_connection_id",
f: func(p *transportParameters) {
p.initialSrcConnID = []byte("invalid")
},
}, {
name: "missing original_destination_connection_id",
f: func(p *transportParameters) {
p.originalDstConnID = nil
},
}, {
name: "invalid original_destination_connection_id",
f: func(p *transportParameters) {
p.originalDstConnID = []byte("invalid")
},
}, {
name: "missing retry_source_connection_id",
f: func(p *transportParameters) {
p.retrySrcConnID = nil
},
}, {
name: "invalid retry_source_connection_id",
f: func(p *transportParameters) {
p.retrySrcConnID = []byte("invalid")
},
}} {
t.Run(test.name, func(t *testing.T) {
tc := newTestConn(t, clientSide,
func(p *transportParameters) {
p.initialSrcConnID = initialSrcConnID
p.originalDstConnID = originalDstConnID
p.retrySrcConnID = retrySrcConnID
},
test.f)
tc.ignoreFrame(frameTypeAck)
tc.wantFrameType("client Initial CRYPTO data",
packetTypeInitial, debugFrameCrypto{})
tc.write(&testDatagram{
packets: []*testPacket{{
ptype: packetTypeRetry,
originalDstConnID: originalDstConnID,
srcConnID: retrySrcConnID,
dstConnID: testLocalConnID(0),
token: []byte{1, 2, 3, 4},
}},
})
tc.wantFrameType("client resends Initial CRYPTO data",
packetTypeInitial, debugFrameCrypto{})
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
})
if test.ok {
tc.wantFrameType("valid params, client sends Handshake",
packetTypeHandshake, debugFrameCrypto{})
} else {
tc.wantFrame("invalid transport parameters",
packetTypeInitial, debugFrameConnectionCloseTransport{
code: errTransportParameter,
})
}
})
}
}
func TestRetryClientIgnoresRetryAfterReceivingPacket(t *testing.T) {
// "After the client has received and processed an Initial or Retry packet
// from the server, it MUST discard any subsequent Retry packets that it receives."
// https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1
tc := newTestConn(t, clientSide)
tc.ignoreFrame(frameTypeAck)
tc.ignoreFrame(frameTypeNewConnectionID)
tc.wantFrameType("client Initial CRYPTO data",
packetTypeInitial, debugFrameCrypto{})
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
})
retry := &testDatagram{
packets: []*testPacket{{
ptype: packetTypeRetry,
originalDstConnID: testLocalConnID(-1),
srcConnID: testPeerConnID(100),
dstConnID: testLocalConnID(0),
token: []byte{1, 2, 3, 4},
}},
}
tc.write(retry)
tc.wantIdle("client ignores Retry after receiving Initial packet")
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
})
tc.wantFrameType("client Handshake CRYPTO data",
packetTypeHandshake, debugFrameCrypto{})
tc.write(retry)
tc.wantIdle("client ignores Retry after discarding Initial keys")
}
func TestRetryClientIgnoresRetryAfterReceivingRetry(t *testing.T) {
// "After the client has received and processed an Initial or Retry packet
// from the server, it MUST discard any subsequent Retry packets that it receives."
// https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1
tc := newTestConn(t, clientSide)
tc.wantFrameType("client Initial CRYPTO data",
packetTypeInitial, debugFrameCrypto{})
retry := &testDatagram{
packets: []*testPacket{{
ptype: packetTypeRetry,
originalDstConnID: testLocalConnID(-1),
srcConnID: testPeerConnID(100),
dstConnID: testLocalConnID(0),
token: []byte{1, 2, 3, 4},
}},
}
tc.write(retry)
tc.wantFrameType("client resends Initial CRYPTO data",
packetTypeInitial, debugFrameCrypto{})
tc.write(retry)
tc.wantIdle("client ignores second Retry")
}
func TestRetryClientIgnoresRetryWithInvalidIntegrityTag(t *testing.T) {
tc := newTestConn(t, clientSide)
tc.wantFrameType("client Initial CRYPTO data",
packetTypeInitial, debugFrameCrypto{})
pkt := encodeRetryPacket(testLocalConnID(-1), retryPacket{
srcConnID: testPeerConnID(100),
dstConnID: testLocalConnID(0),
token: []byte{1, 2, 3, 4},
})
pkt[len(pkt)-1] ^= 1 // invalidate the integrity tag
tc.endpoint.write(&datagram{
b: pkt,
peerAddr: testClientAddr,
})
tc.wantIdle("client ignores Retry with invalid integrity tag")
}
func TestRetryClientIgnoresRetryWithZeroLengthToken(t *testing.T) {
// "A client MUST discard a Retry packet with a zero-length Retry Token field."
// https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2
tc := newTestConn(t, clientSide)
tc.wantFrameType("client Initial CRYPTO data",
packetTypeInitial, debugFrameCrypto{})
tc.write(&testDatagram{
packets: []*testPacket{{
ptype: packetTypeRetry,
originalDstConnID: testLocalConnID(-1),
srcConnID: testPeerConnID(100),
dstConnID: testLocalConnID(0),
token: []byte{},
}},
})
tc.wantIdle("client ignores Retry with zero-length token")
}
func TestRetryStateValidateInvalidToken(t *testing.T) {
// Test handling of tokens that may have a valid signature,
// but unexpected contents.
var rs retryState
if err := rs.init(); err != nil {
t.Fatal(err)
}
nonce := make([]byte, rs.aead.NonceSize())
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
srcConnID := []byte{1, 2, 3, 4}
dstConnID := nonce[:20]
addr := testClientAddr
for _, test := range []struct {
name string
token []byte
}{{
name: "token too short",
token: []byte{1, 2, 3},
}, {
name: "token plaintext too short",
token: func() []byte {
plaintext := make([]byte, 7) // not enough bytes of content
token := append([]byte{}, nonce[20:]...)
return rs.aead.Seal(token, nonce, plaintext, rs.additionalData(srcConnID, addr))
}(),
}} {
t.Run(test.name, func(t *testing.T) {
if _, ok := rs.validateToken(now, test.token, srcConnID, dstConnID, addr); ok {
t.Errorf("validateToken succeeded, want failure")
}
})
}
}
func TestParseInvalidRetryPackets(t *testing.T) {
originalDstConnID := []byte{1, 2, 3, 4}
goodPkt := encodeRetryPacket(originalDstConnID, retryPacket{
dstConnID: []byte{1},
srcConnID: []byte{2},
token: []byte{3},
})
for _, test := range []struct {
name string
pkt []byte
}{{
name: "packet too short",
pkt: goodPkt[:len(goodPkt)-4],
}, {
name: "packet header invalid",
pkt: goodPkt[:5],
}, {
name: "integrity tag invalid",
pkt: func() []byte {
pkt := cloneBytes(goodPkt)
pkt[len(pkt)-1] ^= 1
return pkt
}(),
}} {
t.Run(test.name, func(t *testing.T) {
if _, ok := parseRetryPacket(test.pkt, originalDstConnID); ok {
t.Errorf("parseRetryPacket succeded, want failure")
}
})
}
}
func initialClientCrypto(t *testing.T, e *testEndpoint, p transportParameters) []byte {
t.Helper()
config := &tls.QUICConfig{TLSConfig: newTestTLSConfig(clientSide)}
tlsClient := tls.QUICClient(config)
tlsClient.SetTransportParameters(marshalTransportParameters(p))
tlsClient.Start(context.Background())
t.Cleanup(func() {
tlsClient.Close()
})
e.peerTLSConn = tlsClient
var data []byte
for {
e := tlsClient.NextEvent()
switch e.Kind {
case tls.QUICNoEvent:
return data
case tls.QUICWriteData:
if e.Level != tls.QUICEncryptionLevelInitial {
t.Fatal("initial data at unexpected level")
}
data = append(data, e.Data...)
}
}
}
func initialConnectionCloseDatagram(srcConnID, dstConnID []byte, code transportError) *testDatagram {
return &testDatagram{
packets: []*testPacket{{
ptype: packetTypeInitial,
num: 0,
version: quicVersion1,
srcConnID: srcConnID,
dstConnID: dstConnID,
frames: []debugFrame{
debugFrameConnectionCloseTransport{
code: code,
},
},
}},
}
}