mirror of
https://github.com/golang/net.git
synced 2026-04-01 02:47:08 +09:00
The x/net go.mod now depends on go1.23. Change-Id: I24960949d84753f84f75fbdc97b7dfb92191a4d2 Reviewed-on: https://go-review.googlesource.com/c/net/+/664295 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com> Auto-Submit: Damien Neil <dneil@google.com>
569 lines
16 KiB
Go
569 lines
16 KiB
Go
// 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.
|
|
|
|
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 succeeded, 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,
|
|
},
|
|
},
|
|
}},
|
|
}
|
|
}
|