// 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" "crypto/tls" "fmt" "net/netip" "strings" "testing" ) func TestConnIDClientHandshake(t *testing.T) { tc := newTestConn(t, clientSide) // On initialization, the client chooses local and remote IDs. // // The order in which we allocate the two isn't actually important, // but test is a lot simpler if we assume. if got, want := tc.conn.connIDState.srcConnID(), testLocalConnID(0); !bytes.Equal(got, want) { t.Errorf("after initialization: srcConnID = %x, want %x", got, want) } dstConnID, _ := tc.conn.connIDState.dstConnID() if got, want := dstConnID, testLocalConnID(-1); !bytes.Equal(got, want) { t.Errorf("after initialization: dstConnID = %x, want %x", got, want) } // The server's first Initial packet provides the client with a // non-transient remote connection ID. tc.writeFrames(packetTypeInitial, debugFrameCrypto{ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], }) dstConnID, _ = tc.conn.connIDState.dstConnID() if got, want := dstConnID, testPeerConnID(0); !bytes.Equal(got, want) { t.Errorf("after receiving Initial: dstConnID = %x, want %x", got, want) } wantLocal := []connID{{ cid: testLocalConnID(0), seq: 0, }} if got := tc.conn.connIDState.local; !connIDListEqual(got, wantLocal) { t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } wantRemote := []remoteConnID{{ connID: connID{ cid: testPeerConnID(0), seq: 0, }, }} if got := tc.conn.connIDState.remote; !remoteConnIDListEqual(got, wantRemote) { t.Errorf("remote ids: %v, want %v", fmtRemoteConnIDList(got), fmtRemoteConnIDList(wantRemote)) } } func TestConnIDServerHandshake(t *testing.T) { tc := newTestConn(t, serverSide) // On initialization, the server is provided with the client-chosen // transient connection ID, and allocates an ID of its own. // The Initial packet sets the remote connection ID. tc.writeFrames(packetTypeInitial, debugFrameCrypto{ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial][:1], }) if got, want := tc.conn.connIDState.srcConnID(), testLocalConnID(0); !bytes.Equal(got, want) { t.Errorf("after initClient: srcConnID = %q, want %q", got, want) } dstConnID, _ := tc.conn.connIDState.dstConnID() if got, want := dstConnID, testPeerConnID(0); !bytes.Equal(got, want) { t.Errorf("after initClient: dstConnID = %q, want %q", got, want) } // The Initial flight of CRYPTO data includes transport parameters, // which cause us to allocate another local connection ID. tc.writeFrames(packetTypeInitial, debugFrameCrypto{ off: 1, data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial][1:], }) wantLocal := []connID{{ cid: testPeerConnID(-1), seq: -1, }, { cid: testLocalConnID(0), seq: 0, }, { cid: testLocalConnID(1), seq: 1, }} if got := tc.conn.connIDState.local; !connIDListEqual(got, wantLocal) { t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } wantRemote := []remoteConnID{{ connID: connID{ cid: testPeerConnID(0), seq: 0, }, }} if got := tc.conn.connIDState.remote; !remoteConnIDListEqual(got, wantRemote) { t.Errorf("remote ids: %v, want %v", fmtRemoteConnIDList(got), fmtRemoteConnIDList(wantRemote)) } // The client's first Handshake packet permits the server to discard the // transient connection ID. tc.writeFrames(packetTypeHandshake, debugFrameCrypto{ data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], }) wantLocal = []connID{{ cid: testLocalConnID(0), seq: 0, }, { cid: testLocalConnID(1), seq: 1, }} if got := tc.conn.connIDState.local; !connIDListEqual(got, wantLocal) { t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } } func connIDListEqual(a, b []connID) bool { if len(a) != len(b) { return false } for i := range a { if a[i].seq != b[i].seq { return false } if !bytes.Equal(a[i].cid, b[i].cid) { return false } } return true } func remoteConnIDListEqual(a, b []remoteConnID) bool { if len(a) != len(b) { return false } for i := range a { if a[i].seq != b[i].seq { return false } if !bytes.Equal(a[i].cid, b[i].cid) { return false } if a[i].resetToken != b[i].resetToken { return false } } return true } func fmtConnIDList(s []connID) string { var strs []string for _, cid := range s { strs = append(strs, fmt.Sprintf("[seq:%v cid:{%x}]", cid.seq, cid.cid)) } return "{" + strings.Join(strs, " ") + "}" } func fmtRemoteConnIDList(s []remoteConnID) string { var strs []string for _, cid := range s { strs = append(strs, fmt.Sprintf("[seq:%v cid:{%x} token:{%x}]", cid.seq, cid.cid, cid.resetToken)) } return "{" + strings.Join(strs, " ") + "}" } func TestNewRandomConnID(t *testing.T) { cid, err := newRandomConnID(0) if len(cid) != connIDLen || err != nil { t.Fatalf("newConnID() = %x, %v; want %v bytes", cid, connIDLen, err) } } func TestConnIDPeerRequestsManyIDs(t *testing.T) { // "An endpoint SHOULD ensure that its peer has a sufficient number // of available and unused connection IDs." // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4 // // "An endpoint MAY limit the total number of connection IDs // issued for each connection [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-6 // // Peer requests 100 connection IDs. // We give them 4 in total. tc := newTestConn(t, serverSide, func(p *transportParameters) { p.activeConnIDLimit = 100 }) tc.ignoreFrame(frameTypeAck) tc.ignoreFrame(frameTypeCrypto) tc.writeFrames(packetTypeInitial, debugFrameCrypto{ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], }) tc.wantFrame("provide additional connection ID 1", packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testLocalConnID(1), token: testLocalStatelessResetToken(1), }) tc.wantFrame("provide additional connection ID 2", packetType1RTT, debugFrameNewConnectionID{ seq: 2, connID: testLocalConnID(2), token: testLocalStatelessResetToken(2), }) tc.wantFrame("provide additional connection ID 3", packetType1RTT, debugFrameNewConnectionID{ seq: 3, connID: testLocalConnID(3), token: testLocalStatelessResetToken(3), }) tc.wantIdle("connection ID limit reached, no more to provide") } func TestConnIDPeerProvidesTooManyIDs(t *testing.T) { // "An endpoint MUST NOT provide more connection IDs than the peer's limit." // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4 tc := newTestConn(t, serverSide) tc.handshake() tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: 2, connID: testLocalConnID(2), }) tc.wantFrame("peer provided 3 connection IDs, our limit is 2", packetType1RTT, debugFrameConnectionCloseTransport{ code: errConnectionIDLimit, }) } func TestConnIDPeerTemporarilyExceedsActiveConnIDLimit(t *testing.T) { // "An endpoint MAY send connection IDs that temporarily exceed a peer's limit // if the NEW_CONNECTION_ID frame also requires the retirement of any excess [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4 tc := newTestConn(t, serverSide) tc.handshake() tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ retirePriorTo: 2, seq: 2, connID: testPeerConnID(2), }, debugFrameNewConnectionID{ retirePriorTo: 2, seq: 3, connID: testPeerConnID(3), }) tc.wantFrame("peer requested we retire conn id 0", packetType1RTT, debugFrameRetireConnectionID{ seq: 0, }) tc.wantFrame("peer requested we retire conn id 1", packetType1RTT, debugFrameRetireConnectionID{ seq: 1, }) } func TestConnIDPeerRetiresConnID(t *testing.T) { // "An endpoint SHOULD supply a new connection ID when the peer retires a connection ID." // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-6 for _, side := range []connSide{ clientSide, serverSide, } { t.Run(side.String(), func(t *testing.T) { tc := newTestConn(t, side) tc.handshake() tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetType1RTT, debugFrameRetireConnectionID{ seq: 0, }) tc.wantFrame("provide replacement connection ID", packetType1RTT, debugFrameNewConnectionID{ seq: 2, retirePriorTo: 1, connID: testLocalConnID(2), token: testLocalStatelessResetToken(2), }) }) } } func TestConnIDPeerWithZeroLengthConnIDSendsNewConnectionID(t *testing.T) { // "An endpoint that selects a zero-length connection ID during the handshake // cannot issue a new connection ID." // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-8 tc := newTestConn(t, clientSide, func(p *transportParameters) { p.initialSrcConnID = []byte{} }) tc.peerConnID = []byte{} tc.ignoreFrame(frameTypeAck) tc.uncheckedHandshake() tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testPeerConnID(1), }) tc.wantFrame("invalid NEW_CONNECTION_ID: previous conn id is zero-length", packetType1RTT, debugFrameConnectionCloseTransport{ code: errProtocolViolation, }) } func TestConnIDPeerRequestsRetirement(t *testing.T) { // "Upon receipt of an increased Retire Prior To field, the peer MUST // stop using the corresponding connection IDs and retire them with // RETIRE_CONNECTION_ID frames [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.2-5 tc := newTestConn(t, clientSide) tc.handshake() tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: 2, retirePriorTo: 1, connID: testPeerConnID(2), }) tc.wantFrame("peer asked for conn id 0 to be retired", packetType1RTT, debugFrameRetireConnectionID{ seq: 0, }) if got, want := tc.lastPacket.dstConnID, testPeerConnID(1); !bytes.Equal(got, want) { t.Fatalf("used destination conn id {%x}, want {%x}", got, want) } } func TestConnIDPeerDoesNotAcknowledgeRetirement(t *testing.T) { // "An endpoint SHOULD limit the number of connection IDs it has retired locally // for which RETIRE_CONNECTION_ID frames have not yet been acknowledged." // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.2-6 tc := newTestConn(t, clientSide) tc.handshake() tc.ignoreFrame(frameTypeAck) tc.ignoreFrame(frameTypeRetireConnectionID) // Send a number of NEW_CONNECTION_ID frames, each retiring an old one. for seq := int64(0); seq < 7; seq++ { tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: seq + 2, retirePriorTo: seq + 1, connID: testPeerConnID(seq + 2), }) // We're ignoring the RETIRE_CONNECTION_ID frames. } tc.wantFrame("number of retired, unacked conn ids is too large", packetType1RTT, debugFrameConnectionCloseTransport{ code: errConnectionIDLimit, }) } func TestConnIDRepeatedNewConnectionIDFrame(t *testing.T) { // "Receipt of the same [NEW_CONNECTION_ID] frame multiple times // MUST NOT be treated as a connection error. // https://www.rfc-editor.org/rfc/rfc9000#section-19.15-7 tc := newTestConn(t, clientSide) tc.handshake() tc.ignoreFrame(frameTypeAck) for i := 0; i < 4; i++ { tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: 2, retirePriorTo: 1, connID: testPeerConnID(2), }) } tc.wantFrame("peer asked for conn id to be retired", packetType1RTT, debugFrameRetireConnectionID{ seq: 0, }) tc.wantIdle("repeated NEW_CONNECTION_ID frames are not an error") } func TestConnIDForSequenceNumberChanges(t *testing.T) { // "[...] if a sequence number is used for different connection IDs, // the endpoint MAY treat that receipt as a connection error // of type PROTOCOL_VIOLATION." // https://www.rfc-editor.org/rfc/rfc9000#section-19.15-8 tc := newTestConn(t, clientSide) tc.handshake() tc.ignoreFrame(frameTypeAck) tc.ignoreFrame(frameTypeRetireConnectionID) tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: 2, retirePriorTo: 1, connID: testPeerConnID(2), }) tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: 2, retirePriorTo: 1, connID: testPeerConnID(3), }) tc.wantFrame("connection ID for sequence 0 has changed", packetType1RTT, debugFrameConnectionCloseTransport{ code: errProtocolViolation, }) } func TestConnIDRetirePriorToAfterNewConnID(t *testing.T) { // "Receiving a value in the Retire Prior To field that is greater than // that in the Sequence Number field MUST be treated as a connection error // of type FRAME_ENCODING_ERROR. // https://www.rfc-editor.org/rfc/rfc9000#section-19.15-9 tc := newTestConn(t, serverSide) tc.handshake() tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ retirePriorTo: 3, seq: 2, connID: testPeerConnID(2), }) tc.wantFrame("invalid NEW_CONNECTION_ID: retired the new conn id", packetType1RTT, debugFrameConnectionCloseTransport{ code: errFrameEncoding, }) } func TestConnIDAlreadyRetired(t *testing.T) { // "An endpoint that receives a NEW_CONNECTION_ID frame with a // sequence number smaller than the Retire Prior To field of a // previously received NEW_CONNECTION_ID frame MUST send a // corresponding RETIRE_CONNECTION_ID frame [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-19.15-11 tc := newTestConn(t, clientSide) tc.handshake() tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: 4, retirePriorTo: 3, connID: testPeerConnID(4), }) tc.wantFrame("peer asked for conn id to be retired", packetType1RTT, debugFrameRetireConnectionID{ seq: 0, }) tc.wantFrame("peer asked for conn id to be retired", packetType1RTT, debugFrameRetireConnectionID{ seq: 1, }) tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: 2, retirePriorTo: 0, connID: testPeerConnID(2), }) tc.wantFrame("NEW_CONNECTION_ID was for an already-retired ID", packetType1RTT, debugFrameRetireConnectionID{ seq: 2, }) } func TestConnIDRepeatedRetireConnectionIDFrame(t *testing.T) { tc := newTestConn(t, clientSide) tc.handshake() tc.ignoreFrame(frameTypeAck) for i := 0; i < 4; i++ { tc.writeFrames(packetType1RTT, debugFrameRetireConnectionID{ seq: 0, }) } tc.wantFrame("issue new conn id after peer retires one", packetType1RTT, debugFrameNewConnectionID{ retirePriorTo: 1, seq: 2, connID: testLocalConnID(2), token: testLocalStatelessResetToken(2), }) tc.wantIdle("repeated RETIRE_CONNECTION_ID frames are not an error") } func TestConnIDRetiredUnsent(t *testing.T) { // "Receipt of a RETIRE_CONNECTION_ID frame containing a sequence number // greater than any previously sent to the peer MUST be treated as a // connection error of type PROTOCOL_VIOLATION." // https://www.rfc-editor.org/rfc/rfc9000#section-19.16-7 tc := newTestConn(t, clientSide) tc.handshake() tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetType1RTT, debugFrameRetireConnectionID{ seq: 2, }) tc.wantFrame("invalid NEW_CONNECTION_ID: previous conn id is zero-length", packetType1RTT, debugFrameConnectionCloseTransport{ code: errProtocolViolation, }) } func TestConnIDUsePreferredAddressConnID(t *testing.T) { // Peer gives us a connection ID in the preferred address transport parameter. // We don't use the preferred address at this time, but we should use the // connection ID. (It isn't tied to any specific address.) // // This test will probably need updating if/when we start using the preferred address. cid := testPeerConnID(10) tc := newTestConn(t, serverSide, func(p *transportParameters) { p.preferredAddrV4 = netip.MustParseAddrPort("0.0.0.0:0") p.preferredAddrV6 = netip.MustParseAddrPort("[::0]:0") p.preferredAddrConnID = cid p.preferredAddrResetToken = make([]byte, 16) }) tc.uncheckedHandshake() tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: 2, retirePriorTo: 1, connID: []byte{0xff}, }) tc.wantFrame("peer asked for conn id 0 to be retired", packetType1RTT, debugFrameRetireConnectionID{ seq: 0, }) if got, want := tc.lastPacket.dstConnID, cid; !bytes.Equal(got, want) { t.Fatalf("used destination conn id {%x}, want {%x} from preferred address transport parameter", got, want) } } func TestConnIDPeerProvidesPreferredAddrAndTooManyConnIDs(t *testing.T) { // Peer gives us more conn ids than our advertised limit, // including a conn id in the preferred address transport parameter. cid := testPeerConnID(10) tc := newTestConn(t, serverSide, func(p *transportParameters) { p.preferredAddrV4 = netip.MustParseAddrPort("0.0.0.0:0") p.preferredAddrV6 = netip.MustParseAddrPort("[::0]:0") p.preferredAddrConnID = cid p.preferredAddrResetToken = make([]byte, 16) }) tc.uncheckedHandshake() tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: 2, retirePriorTo: 0, connID: testPeerConnID(2), }) tc.wantFrame("peer provided 3 connection IDs, our limit is 2", packetType1RTT, debugFrameConnectionCloseTransport{ code: errConnectionIDLimit, }) } func TestConnIDPeerWithZeroLengthIDProvidesPreferredAddr(t *testing.T) { // Peer gives us more conn ids than our advertised limit, // including a conn id in the preferred address transport parameter. tc := newTestConn(t, serverSide, func(p *transportParameters) { p.initialSrcConnID = []byte{} p.preferredAddrV4 = netip.MustParseAddrPort("0.0.0.0:0") p.preferredAddrV6 = netip.MustParseAddrPort("[::0]:0") p.preferredAddrConnID = testPeerConnID(1) p.preferredAddrResetToken = make([]byte, 16) }, func(cids *newServerConnIDs) { cids.srcConnID = []byte{} }, func(tc *testConn) { tc.peerConnID = []byte{} }) tc.writeFrames(packetTypeInitial, debugFrameCrypto{ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], }) tc.wantFrame("peer with zero-length connection ID tried to provide another in transport parameters", packetTypeInitial, debugFrameConnectionCloseTransport{ code: errProtocolViolation, }) } func TestConnIDInitialSrcConnIDMismatch(t *testing.T) { // "Endpoints MUST validate that received [initial_source_connection_id] // parameters match received connection ID values." // https://www.rfc-editor.org/rfc/rfc9000#section-7.3-3 testSides(t, "", func(t *testing.T, side connSide) { tc := newTestConn(t, side, func(p *transportParameters) { p.initialSrcConnID = []byte("invalid") }) tc.ignoreFrame(frameTypeAck) tc.ignoreFrame(frameTypeCrypto) tc.writeFrames(packetTypeInitial, debugFrameCrypto{ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], }) if side == clientSide { // Server transport parameters are carried in the Handshake packet. tc.writeFrames(packetTypeHandshake, debugFrameCrypto{ data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], }) } tc.wantFrame("initial_source_connection_id transport parameter mismatch", packetTypeInitial, debugFrameConnectionCloseTransport{ code: errTransportParameter, }) }) } func TestConnIDsCleanedUpAfterClose(t *testing.T) { testSides(t, "", func(t *testing.T, side connSide) { tc := newTestConn(t, side, func(p *transportParameters) { if side == clientSide { token := testPeerStatelessResetToken(0) p.statelessResetToken = token[:] } }) tc.handshake() tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetType1RTT, debugFrameNewConnectionID{ seq: 2, retirePriorTo: 1, connID: testPeerConnID(2), token: testPeerStatelessResetToken(0), }) tc.wantFrame("peer asked for conn id 0 to be retired", packetType1RTT, debugFrameRetireConnectionID{ seq: 0, }) tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{}) tc.conn.Abort(nil) tc.wantFrame("CONN_CLOSE sent after user closes connection", packetType1RTT, debugFrameConnectionCloseTransport{}) // Wait for the conn to drain. // Then wait for the conn loop to exit, // and force an immediate sync of the connsMap updates // (normally only done by the endpoint read loop). tc.advanceToTimer() <-tc.conn.donec tc.endpoint.e.connsMap.applyUpdates() if got := len(tc.endpoint.e.connsMap.byConnID); got != 0 { t.Errorf("%v conn ids in endpoint map after closing, want 0", got) } if got := len(tc.endpoint.e.connsMap.byResetToken); got != 0 { t.Errorf("%v reset tokens in endpoint map after closing, want 0", got) } }) }