blob: d426aa6902ef3ec42f5ef904a6484fa85f5be25b [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 (
"context"
"crypto/tls"
"fmt"
"testing"
)
// Frames may be retransmitted either when the packet containing the frame is lost, or on PTO.
// lostFrameTest runs a test in both configurations.
func lostFrameTest(t *testing.T, f func(t *testing.T, pto bool)) {
t.Run("lost", func(t *testing.T) {
f(t, false)
})
t.Run("pto", func(t *testing.T) {
f(t, true)
})
}
// triggerLossOrPTO causes the conn to declare the last sent packet lost,
// or advances to the PTO timer.
func (tc *testConn) triggerLossOrPTO(ptype packetType, pto bool) {
tc.t.Helper()
if pto {
if !tc.conn.loss.ptoTimerArmed {
tc.t.Fatalf("PTO timer not armed, expected it to be")
}
if *testVV {
tc.t.Logf("advancing to PTO timer")
}
tc.advanceTo(tc.conn.loss.timer)
return
}
if *testVV {
*testVV = false
defer func() {
tc.t.Logf("cause conn to declare last packet lost")
*testVV = true
}()
}
defer func(ignoreFrames map[byte]bool) {
tc.ignoreFrames = ignoreFrames
}(tc.ignoreFrames)
tc.ignoreFrames = map[byte]bool{
frameTypeAck: true,
frameTypePadding: true,
}
// Send three packets containing PINGs, and then respond with an ACK for the
// last one. This puts the last packet before the PINGs outside the packet
// reordering threshold, and it will be declared lost.
const lossThreshold = 3
var num packetNumber
for i := 0; i < lossThreshold; i++ {
tc.conn.ping(spaceForPacketType(ptype))
d := tc.readDatagram()
if d == nil {
tc.t.Fatalf("conn is idle; want PING frame")
}
if d.packets[0].ptype != ptype {
tc.t.Fatalf("conn sent %v packet; want %v", d.packets[0].ptype, ptype)
}
num = d.packets[0].num
}
tc.writeFrames(ptype, debugFrameAck{
ranges: []i64range[packetNumber]{
{num, num + 1},
},
})
}
func TestLostResetStreamFrame(t *testing.T) {
// "Cancellation of stream transmission, as carried in a RESET_STREAM frame,
// is sent until acknowledged or until all stream data is acknowledged by the peer [...]"
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.4
lostFrameTest(t, func(t *testing.T, pto bool) {
tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters)
tc.ignoreFrame(frameTypeAck)
s.Reset(1)
tc.wantFrame("reset stream",
packetType1RTT, debugFrameResetStream{
id: s.id,
code: 1,
})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("resent RESET_STREAM frame",
packetType1RTT, debugFrameResetStream{
id: s.id,
code: 1,
})
})
}
func TestLostStopSendingFrame(t *testing.T) {
// "[...] a request to cancel stream transmission, as encoded in a STOP_SENDING frame,
// is sent until the receiving part of the stream enters either a "Data Recvd" or
// "Reset Recvd" state [...]"
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.5
//
// Technically, we can stop sending a STOP_SENDING frame if the peer sends
// us all the data for the stream or resets it. We don't bother tracking this,
// however, so we'll keep sending the frame until it is acked. This is harmless.
lostFrameTest(t, func(t *testing.T, pto bool) {
tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, permissiveTransportParameters)
tc.ignoreFrame(frameTypeAck)
s.CloseRead()
tc.wantFrame("stream is read-closed",
packetType1RTT, debugFrameStopSending{
id: s.id,
})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("resent STOP_SENDING frame",
packetType1RTT, debugFrameStopSending{
id: s.id,
})
})
}
func TestLostCryptoFrame(t *testing.T) {
// "Data sent in CRYPTO frames is retransmitted [...] until all data has been acknowledged."
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.1
lostFrameTest(t, func(t *testing.T, pto bool) {
tc := newTestConn(t, clientSide)
tc.ignoreFrame(frameTypeAck)
tc.wantFrame("client sends Initial CRYPTO frame",
packetTypeInitial, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
})
tc.triggerLossOrPTO(packetTypeInitial, pto)
tc.wantFrame("client resends Initial CRYPTO frame",
packetTypeInitial, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
})
tc.wantFrame("client sends Handshake CRYPTO frame",
packetTypeHandshake, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake],
})
tc.wantFrame("client provides server with an additional connection ID",
packetType1RTT, debugFrameNewConnectionID{
seq: 1,
connID: testLocalConnID(1),
})
tc.triggerLossOrPTO(packetTypeHandshake, pto)
tc.wantFrame("client resends Handshake CRYPTO frame",
packetTypeHandshake, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake],
})
})
}
func TestLostStreamFrameEmpty(t *testing.T) {
// A STREAM frame opening a stream, but containing no stream data, should
// be retransmitted if lost.
lostFrameTest(t, func(t *testing.T, pto bool) {
ctx := canceledContext()
tc := newTestConn(t, clientSide, permissiveTransportParameters)
tc.handshake()
tc.ignoreFrame(frameTypeAck)
c, err := tc.conn.NewStream(ctx)
if err != nil {
t.Fatalf("NewStream: %v", err)
}
c.Write(nil) // open the stream
tc.wantFrame("created bidirectional stream 0",
packetType1RTT, debugFrameStream{
id: newStreamID(clientSide, bidiStream, 0),
data: []byte{},
})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("resent stream frame",
packetType1RTT, debugFrameStream{
id: newStreamID(clientSide, bidiStream, 0),
data: []byte{},
})
})
}
func TestLostStreamWithData(t *testing.T) {
// "Application data sent in STREAM frames is retransmitted in new STREAM
// frames unless the endpoint has sent a RESET_STREAM for that stream."
// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.2
//
// TODO: Lost stream frame after RESET_STREAM
lostFrameTest(t, func(t *testing.T, pto bool) {
data := []byte{0, 1, 2, 3, 4, 5, 6, 7}
tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) {
p.initialMaxStreamsUni = 1
p.initialMaxData = 1 << 20
p.initialMaxStreamDataUni = 1 << 20
})
s.Write(data[:4])
tc.wantFrame("send [0,4)",
packetType1RTT, debugFrameStream{
id: s.id,
off: 0,
data: data[:4],
})
s.Write(data[4:8])
tc.wantFrame("send [4,8)",
packetType1RTT, debugFrameStream{
id: s.id,
off: 4,
data: data[4:8],
})
s.CloseWrite()
tc.wantFrame("send FIN",
packetType1RTT, debugFrameStream{
id: s.id,
off: 8,
fin: true,
data: []byte{},
})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("resend data",
packetType1RTT, debugFrameStream{
id: s.id,
off: 0,
fin: true,
data: data[:8],
})
})
}
func TestLostStreamPartialLoss(t *testing.T) {
// Conn sends four STREAM packets.
// ACKs are received for the packets containing bytes 0 and 2.
// The remaining packets are declared lost.
// The Conn resends only the lost data.
//
// This test doesn't have a PTO mode, because the ACK for the packet containing byte 2
// starts the loss timer for the packet containing byte 1, and the PTO timer is not
// armed when the loss timer is.
data := []byte{0, 1, 2, 3}
tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) {
p.initialMaxStreamsUni = 1
p.initialMaxData = 1 << 20
p.initialMaxStreamDataUni = 1 << 20
})
for i := range data {
s.Write(data[i : i+1])
tc.wantFrame(fmt.Sprintf("send STREAM frame with byte %v", i),
packetType1RTT, debugFrameStream{
id: s.id,
off: int64(i),
data: data[i : i+1],
})
if i%2 == 0 {
tc.writeAckForLatest()
}
}
const pto = false
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("resend byte 1",
packetType1RTT, debugFrameStream{
id: s.id,
off: 1,
data: data[1:2],
})
tc.wantFrame("resend byte 3",
packetType1RTT, debugFrameStream{
id: s.id,
off: 3,
data: data[3:4],
})
tc.wantIdle("no more frames sent after packet loss")
}
func TestLostMaxStreamDataFrame(t *testing.T) {
// "[...] an updated value is sent when the packet containing
// the most recent MAX_STREAM_DATA frame for a stream is lost"
// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.8
lostFrameTest(t, func(t *testing.T, pto bool) {
const maxWindowSize = 10
buf := make([]byte, maxWindowSize)
tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) {
c.StreamReadBufferSize = maxWindowSize
})
// We send MAX_STREAM_DATA = 19.
tc.writeFrames(packetType1RTT, debugFrameStream{
id: s.id,
off: 0,
data: make([]byte, maxWindowSize),
})
if n, err := s.Read(buf[:maxWindowSize-1]); err != nil || n != maxWindowSize-1 {
t.Fatalf("Read() = %v, %v; want %v, nil", n, err, maxWindowSize-1)
}
tc.wantFrame("stream window is extended after reading data",
packetType1RTT, debugFrameMaxStreamData{
id: s.id,
max: (maxWindowSize * 2) - 1,
})
// MAX_STREAM_DATA = 20, which is only one more byte, so we don't send the frame.
if n, err := s.Read(buf); err != nil || n != 1 {
t.Fatalf("Read() = %v, %v; want %v, nil", n, err, 1)
}
tc.wantIdle("read doesn't extend window enough to send another MAX_STREAM_DATA")
// The MAX_STREAM_DATA = 19 packet was lost, so we send 20.
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("resent MAX_STREAM_DATA includes most current value",
packetType1RTT, debugFrameMaxStreamData{
id: s.id,
max: maxWindowSize * 2,
})
})
}
func TestLostMaxStreamDataFrameAfterStreamFinReceived(t *testing.T) {
// "An endpoint SHOULD stop sending MAX_STREAM_DATA frames when
// the receiving part of the stream enters a "Size Known" or "Reset Recvd" state."
// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.8
lostFrameTest(t, func(t *testing.T, pto bool) {
const maxWindowSize = 10
buf := make([]byte, maxWindowSize)
tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) {
c.StreamReadBufferSize = maxWindowSize
})
tc.writeFrames(packetType1RTT, debugFrameStream{
id: s.id,
off: 0,
data: make([]byte, maxWindowSize),
})
if n, err := s.Read(buf); err != nil || n != maxWindowSize {
t.Fatalf("Read() = %v, %v; want %v, nil", n, err, maxWindowSize)
}
tc.wantFrame("stream window is extended after reading data",
packetType1RTT, debugFrameMaxStreamData{
id: s.id,
max: 2 * maxWindowSize,
})
tc.writeFrames(packetType1RTT, debugFrameStream{
id: s.id,
off: maxWindowSize,
fin: true,
})
tc.ignoreFrame(frameTypePing)
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantIdle("lost MAX_STREAM_DATA not resent for stream in 'size known'")
})
}
func TestLostMaxStreamsFrameMostRecent(t *testing.T) {
// "[...] an updated value is sent when a packet containing the
// most recent MAX_STREAMS for a stream type frame is declared lost [...]"
// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.9
lostFrameTest(t, func(t *testing.T, pto bool) {
ctx := canceledContext()
tc := newTestConn(t, serverSide, func(c *Config) {
c.MaxUniRemoteStreams = 1
})
tc.handshake()
tc.ignoreFrame(frameTypeAck)
tc.writeFrames(packetType1RTT, debugFrameStream{
id: newStreamID(clientSide, uniStream, 0),
fin: true,
})
s, err := tc.conn.AcceptStream(ctx)
if err != nil {
t.Fatalf("AcceptStream() = %v", err)
}
if err := s.CloseContext(ctx); err != nil {
t.Fatalf("stream.Close() = %v", err)
}
tc.wantFrame("closing stream updates peer's MAX_STREAMS",
packetType1RTT, debugFrameMaxStreams{
streamType: uniStream,
max: 2,
})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("lost MAX_STREAMS is resent",
packetType1RTT, debugFrameMaxStreams{
streamType: uniStream,
max: 2,
})
})
}
func TestLostMaxStreamsFrameNotMostRecent(t *testing.T) {
// Send two MAX_STREAMS frames, lose the first one.
//
// No PTO mode for this test: The ack that causes the first frame
// to be lost arms the loss timer for the second, so the PTO timer is not armed.
const pto = false
ctx := canceledContext()
tc := newTestConn(t, serverSide, func(c *Config) {
c.MaxUniRemoteStreams = 2
})
tc.handshake()
tc.ignoreFrame(frameTypeAck)
for i := int64(0); i < 2; i++ {
tc.writeFrames(packetType1RTT, debugFrameStream{
id: newStreamID(clientSide, uniStream, i),
fin: true,
})
s, err := tc.conn.AcceptStream(ctx)
if err != nil {
t.Fatalf("AcceptStream() = %v", err)
}
if err := s.CloseContext(ctx); err != nil {
t.Fatalf("stream.Close() = %v", err)
}
tc.wantFrame("closing stream updates peer's MAX_STREAMS",
packetType1RTT, debugFrameMaxStreams{
streamType: uniStream,
max: 3 + i,
})
}
// The second MAX_STREAMS frame is acked.
tc.writeAckForLatest()
// The first MAX_STREAMS frame is lost.
tc.conn.ping(appDataSpace)
tc.wantFrame("connection should send a PING frame",
packetType1RTT, debugFramePing{})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantIdle("superseded MAX_DATA is not resent on loss")
}
func TestLostStreamDataBlockedFrame(t *testing.T) {
// "A new [STREAM_DATA_BLOCKED] frame is sent if a packet containing
// the most recent frame for a scope is lost [...]"
// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.10
lostFrameTest(t, func(t *testing.T, pto bool) {
tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) {
p.initialMaxStreamsUni = 1
p.initialMaxData = 1 << 20
})
w := runAsync(tc, func(ctx context.Context) (int, error) {
return s.WriteContext(ctx, []byte{0, 1, 2, 3})
})
defer w.cancel()
tc.wantFrame("write is blocked by flow control",
packetType1RTT, debugFrameStreamDataBlocked{
id: s.id,
max: 0,
})
tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{
id: s.id,
max: 1,
})
tc.wantFrame("write makes some progress, but is still blocked by flow control",
packetType1RTT, debugFrameStreamDataBlocked{
id: s.id,
max: 1,
})
tc.wantFrame("write consuming available window",
packetType1RTT, debugFrameStream{
id: s.id,
off: 0,
data: []byte{0},
})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("STREAM_DATA_BLOCKED is resent",
packetType1RTT, debugFrameStreamDataBlocked{
id: s.id,
max: 1,
})
tc.wantFrame("STREAM is resent as well",
packetType1RTT, debugFrameStream{
id: s.id,
off: 0,
data: []byte{0},
})
})
}
func TestLostStreamDataBlockedFrameAfterStreamUnblocked(t *testing.T) {
// "A new [STREAM_DATA_BLOCKED] frame is sent [...] only while
// the endpoint is blocked on the corresponding limit."
// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.10
lostFrameTest(t, func(t *testing.T, pto bool) {
tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) {
p.initialMaxStreamsUni = 1
p.initialMaxData = 1 << 20
})
data := []byte{0, 1, 2, 3}
w := runAsync(tc, func(ctx context.Context) (int, error) {
return s.WriteContext(ctx, data)
})
defer w.cancel()
tc.wantFrame("write is blocked by flow control",
packetType1RTT, debugFrameStreamDataBlocked{
id: s.id,
max: 0,
})
tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{
id: s.id,
max: 10,
})
tc.wantFrame("write completes after flow control available",
packetType1RTT, debugFrameStream{
id: s.id,
off: 0,
data: data,
})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("STREAM data is resent",
packetType1RTT, debugFrameStream{
id: s.id,
off: 0,
data: data,
})
tc.wantIdle("STREAM_DATA_BLOCKED is not resent, since the stream is not blocked")
})
}
func TestLostNewConnectionIDFrame(t *testing.T) {
// "New connection IDs are [...] retransmitted if the packet containing them is lost."
// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.13
lostFrameTest(t, func(t *testing.T, pto bool) {
tc := newTestConn(t, serverSide)
tc.handshake()
tc.ignoreFrame(frameTypeAck)
tc.writeFrames(packetType1RTT,
debugFrameRetireConnectionID{
seq: 1,
})
tc.wantFrame("provide a new connection ID after peer retires old one",
packetType1RTT, debugFrameNewConnectionID{
seq: 2,
connID: testLocalConnID(2),
})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("resend new connection ID",
packetType1RTT, debugFrameNewConnectionID{
seq: 2,
connID: testLocalConnID(2),
})
})
}
func TestLostRetireConnectionIDFrame(t *testing.T) {
// "[...] retired connection IDs are [...] retransmitted
// if the packet containing them is lost."
// https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.13
lostFrameTest(t, func(t *testing.T, pto bool) {
tc := newTestConn(t, clientSide)
tc.handshake()
tc.ignoreFrame(frameTypeAck)
tc.writeFrames(packetType1RTT,
debugFrameNewConnectionID{
seq: 2,
retirePriorTo: 1,
connID: testPeerConnID(2),
})
tc.wantFrame("peer requested connection id be retired",
packetType1RTT, debugFrameRetireConnectionID{
seq: 0,
})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("resend RETIRE_CONNECTION_ID",
packetType1RTT, debugFrameRetireConnectionID{
seq: 0,
})
})
}
func TestLostHandshakeDoneFrame(t *testing.T) {
// "The HANDSHAKE_DONE frame MUST be retransmitted until it is acknowledged."
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.16
lostFrameTest(t, func(t *testing.T, pto bool) {
tc := newTestConn(t, serverSide)
tc.ignoreFrame(frameTypeAck)
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
})
tc.wantFrame("server sends Initial CRYPTO frame",
packetTypeInitial, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
})
tc.wantFrame("server sends Handshake CRYPTO frame",
packetTypeHandshake, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake],
})
tc.wantFrame("server provides an additional connection ID",
packetType1RTT, debugFrameNewConnectionID{
seq: 1,
connID: testLocalConnID(1),
})
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
})
tc.wantFrame("server sends HANDSHAKE_DONE after handshake completes",
packetType1RTT, debugFrameHandshakeDone{})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantFrame("server resends HANDSHAKE_DONE",
packetType1RTT, debugFrameHandshakeDone{})
})
}