| // 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 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, |
| }, |
| }, |
| }}, |
| } |
| } |