| // 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/rand" |
| "crypto/tls" |
| "errors" |
| "net/netip" |
| "testing" |
| "time" |
| ) |
| |
| func TestStatelessResetClientSendsStatelessResetTokenTransportParameter(t *testing.T) { |
| // "[The stateless_reset_token] transport parameter MUST NOT be sent by a client [...]" |
| // https://www.rfc-editor.org/rfc/rfc9000#section-18.2-4.6.1 |
| resetToken := testPeerStatelessResetToken(0) |
| tc := newTestConn(t, serverSide, func(p *transportParameters) { |
| p.statelessResetToken = resetToken[:] |
| }) |
| tc.writeFrames(packetTypeInitial, |
| debugFrameCrypto{ |
| data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], |
| }) |
| tc.wantFrame("client provided stateless_reset_token transport parameter", |
| packetTypeInitial, debugFrameConnectionCloseTransport{ |
| code: errTransportParameter, |
| }) |
| } |
| |
| var testStatelessResetKey = func() (key [32]byte) { |
| if _, err := rand.Read(key[:]); err != nil { |
| panic(err) |
| } |
| return key |
| }() |
| |
| func testStatelessResetToken(cid []byte) statelessResetToken { |
| var gen statelessResetTokenGenerator |
| gen.init(testStatelessResetKey) |
| return gen.tokenForConnID(cid) |
| } |
| |
| func testLocalStatelessResetToken(seq int64) statelessResetToken { |
| return testStatelessResetToken(testLocalConnID(seq)) |
| } |
| |
| func newDatagramForReset(cid []byte, size int, addr netip.AddrPort) *datagram { |
| dgram := append([]byte{headerFormShort | fixedBit}, cid...) |
| for len(dgram) < size { |
| dgram = append(dgram, byte(len(dgram))) // semi-random junk |
| } |
| return &datagram{ |
| b: dgram, |
| peerAddr: addr, |
| } |
| } |
| |
| func TestStatelessResetSentSizes(t *testing.T) { |
| config := &Config{ |
| TLSConfig: newTestTLSConfig(serverSide), |
| StatelessResetKey: testStatelessResetKey, |
| } |
| addr := netip.MustParseAddr("127.0.0.1") |
| te := newTestEndpoint(t, config) |
| for i, test := range []struct { |
| reqSize int |
| wantSize int |
| }{{ |
| // Datagrams larger than 42 bytes result in a 42-byte stateless reset. |
| // This isn't specifically mandated by RFC 9000, but is implied. |
| // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-11 |
| reqSize: 1200, |
| wantSize: 42, |
| }, { |
| // "An endpoint that sends a Stateless Reset in response to a packet |
| // that is 43 bytes or shorter SHOULD send a Stateless Reset that is |
| // one byte shorter than the packet it responds to." |
| // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-11 |
| reqSize: 43, |
| wantSize: 42, |
| }, { |
| reqSize: 42, |
| wantSize: 41, |
| }, { |
| // We should send a stateless reset in response to the smallest possible |
| // valid datagram the peer can send us. |
| // The smallest packet is 1-RTT: |
| // header byte, conn id, packet num, payload, AEAD. |
| reqSize: 1 + connIDLen + 1 + 1 + 16, |
| wantSize: 1 + connIDLen + 1 + 1 + 16 - 1, |
| }, { |
| // The smallest possible stateless reset datagram is 21 bytes. |
| // Since our response must be smaller than the incoming datagram, |
| // we must not respond to a 21 byte or smaller packet. |
| reqSize: 21, |
| wantSize: 0, |
| }} { |
| cid := testLocalConnID(int64(i)) |
| token := testStatelessResetToken(cid) |
| addrport := netip.AddrPortFrom(addr, uint16(8000+i)) |
| te.write(newDatagramForReset(cid, test.reqSize, addrport)) |
| |
| got := te.read() |
| if len(got) != test.wantSize { |
| t.Errorf("got %v-byte response to %v-byte req, want %v", |
| len(got), test.reqSize, test.wantSize) |
| } |
| if len(got) == 0 { |
| continue |
| } |
| // "Endpoints MUST send Stateless Resets formatted as |
| // a packet with a short header." |
| // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-15 |
| if isLongHeader(got[0]) { |
| t.Errorf("response to %v-byte request is not a short-header packet\ngot: %x", test.reqSize, got) |
| } |
| if !bytes.HasSuffix(got, token[:]) { |
| t.Errorf("response to %v-byte request does not end in stateless reset token\ngot: %x\nwant suffix: %x", test.reqSize, got, token) |
| } |
| } |
| } |
| |
| func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) { |
| // "[...] Stateless Reset Token field values from [...] NEW_CONNECTION_ID frames [...]" |
| // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-1 |
| qr := &qlogRecord{} |
| tc := newTestConn(t, clientSide, qr.config) |
| tc.handshake() |
| tc.ignoreFrame(frameTypeAck) |
| |
| // Retire connection ID 0. |
| tc.writeFrames(packetType1RTT, |
| debugFrameNewConnectionID{ |
| retirePriorTo: 1, |
| seq: 2, |
| connID: testPeerConnID(2), |
| }) |
| tc.wantFrame("peer requested we retire conn id 0", |
| packetType1RTT, debugFrameRetireConnectionID{ |
| seq: 0, |
| }) |
| |
| resetToken := testPeerStatelessResetToken(1) // provided during handshake |
| dgram := append(make([]byte, 100), resetToken[:]...) |
| tc.endpoint.write(&datagram{ |
| b: dgram, |
| }) |
| |
| if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { |
| t.Errorf("conn.Wait() = %v, want errStatelessReset", err) |
| } |
| tc.wantIdle("closed connection is idle in draining") |
| tc.advance(1 * time.Second) // long enough to exit the draining state |
| tc.wantIdle("closed connection is idle after draining") |
| |
| qr.wantEvents(t, jsonEvent{ |
| "name": "connectivity:connection_closed", |
| "data": map[string]any{ |
| "trigger": "stateless_reset", |
| }, |
| }) |
| } |
| |
| func TestStatelessResetSuccessfulTransportParameter(t *testing.T) { |
| // "[...] Stateless Reset Token field values from [...] |
| // the server's transport parameters [...]" |
| // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-1 |
| resetToken := testPeerStatelessResetToken(0) |
| tc := newTestConn(t, clientSide, func(p *transportParameters) { |
| p.statelessResetToken = resetToken[:] |
| }) |
| tc.handshake() |
| |
| dgram := append(make([]byte, 100), resetToken[:]...) |
| tc.endpoint.write(&datagram{ |
| b: dgram, |
| }) |
| |
| if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { |
| t.Errorf("conn.Wait() = %v, want errStatelessReset", err) |
| } |
| tc.wantIdle("closed connection is idle") |
| } |
| |
| func TestStatelessResetSuccessfulPrefix(t *testing.T) { |
| for _, test := range []struct { |
| name string |
| prefix []byte |
| size int |
| }{{ |
| name: "short header and fixed bit", |
| prefix: []byte{ |
| headerFormShort | fixedBit, |
| }, |
| size: 100, |
| }, { |
| // "[...] endpoints MUST treat [long header packets] ending in a |
| // valid stateless reset token as a Stateless Reset [...]" |
| // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-15 |
| name: "long header no fixed bit", |
| prefix: []byte{ |
| headerFormLong, |
| }, |
| size: 100, |
| }, { |
| // "[...] the comparison MUST be performed when the first packet |
| // in an incoming datagram [...] cannot be decrypted." |
| // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-2 |
| name: "short header valid DCID", |
| prefix: append([]byte{ |
| headerFormShort | fixedBit, |
| }, testLocalConnID(0)...), |
| size: 100, |
| }, { |
| name: "handshake valid DCID", |
| prefix: append([]byte{ |
| headerFormLong | fixedBit | longPacketTypeHandshake, |
| }, testLocalConnID(0)...), |
| size: 100, |
| }, { |
| name: "no fixed bit valid DCID", |
| prefix: append([]byte{ |
| 0, |
| }, testLocalConnID(0)...), |
| size: 100, |
| }} { |
| t.Run(test.name, func(t *testing.T) { |
| resetToken := testPeerStatelessResetToken(0) |
| tc := newTestConn(t, clientSide, func(p *transportParameters) { |
| p.statelessResetToken = resetToken[:] |
| }) |
| tc.handshake() |
| |
| dgram := test.prefix |
| for len(dgram) < test.size-len(resetToken) { |
| dgram = append(dgram, byte(len(dgram))) // semi-random junk |
| } |
| dgram = append(dgram, resetToken[:]...) |
| tc.endpoint.write(&datagram{ |
| b: dgram, |
| }) |
| if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { |
| t.Errorf("conn.Wait() = %v, want errStatelessReset", err) |
| } |
| }) |
| } |
| } |
| |
| func TestStatelessResetRetiredConnID(t *testing.T) { |
| // "An endpoint MUST NOT check for any stateless reset tokens [...] |
| // for connection IDs that have been retired." |
| // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-3 |
| resetToken := testPeerStatelessResetToken(0) |
| tc := newTestConn(t, clientSide, func(p *transportParameters) { |
| p.statelessResetToken = resetToken[:] |
| }) |
| tc.handshake() |
| tc.ignoreFrame(frameTypeAck) |
| |
| // We retire connection ID 0. |
| 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, |
| }) |
| |
| // Receive a stateless reset for connection ID 0. |
| dgram := append(make([]byte, 100), resetToken[:]...) |
| tc.endpoint.write(&datagram{ |
| b: dgram, |
| }) |
| |
| if err := tc.conn.Wait(canceledContext()); !errors.Is(err, context.Canceled) { |
| t.Errorf("conn.Wait() = %v, want connection to be alive", err) |
| } |
| } |