quic: tracking of received packets and acks to send

RFC 9000, Section 13.2.

For golang/go#58547

Change-Id: I0aad4c03fabb9087964dc9030bb8777d5159360c
Reviewed-on: https://go-review.googlesource.com/c/net/+/506595
Run-TryBot: Damien Neil <dneil@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/quic/acks.go b/internal/quic/acks.go
new file mode 100644
index 0000000..ba860ef
--- /dev/null
+++ b/internal/quic/acks.go
@@ -0,0 +1,184 @@
+// 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 (
+	"time"
+)
+
+// ackState tracks packets received from a peer within a number space.
+// It handles packet deduplication (don't process the same packet twice) and
+// determines the timing and content of ACK frames.
+type ackState struct {
+	seen rangeset[packetNumber]
+
+	// The time at which we must send an ACK frame, even if we have no other data to send.
+	nextAck time.Time
+
+	// The time we received the largest-numbered packet in seen.
+	maxRecvTime time.Time
+
+	// The largest-numbered ack-eliciting packet in seen.
+	maxAckEliciting packetNumber
+
+	// The number of ack-eliciting packets in seen that we have not yet acknowledged.
+	unackedAckEliciting int
+}
+
+// shouldProcess reports whether a packet should be handled or discarded.
+func (acks *ackState) shouldProcess(num packetNumber) bool {
+	if packetNumber(acks.seen.min()) > num {
+		// We've discarded the state for this range of packet numbers.
+		// Discard the packet rather than potentially processing a duplicate.
+		// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.3-5
+		return false
+	}
+	if acks.seen.contains(num) {
+		// Discard duplicate packets.
+		return false
+	}
+	return true
+}
+
+// receive records receipt of a packet.
+func (acks *ackState) receive(now time.Time, space numberSpace, num packetNumber, ackEliciting bool) {
+	if ackEliciting {
+		acks.unackedAckEliciting++
+		if acks.mustAckImmediately(space, num) {
+			acks.nextAck = now
+		} else if acks.nextAck.IsZero() {
+			// This packet does not need to be acknowledged immediately,
+			// but the ack must not be intentionally delayed by more than
+			// the max_ack_delay transport parameter we sent to the peer.
+			//
+			// We always delay acks by the maximum allowed, less the timer
+			// granularity. ("[max_ack_delay] SHOULD include the receiver's
+			// expected delays in alarms firing.")
+			//
+			// https://www.rfc-editor.org/rfc/rfc9000#section-18.2-4.28.1
+			acks.nextAck = now.Add(maxAckDelay - timerGranularity)
+		}
+		if num > acks.maxAckEliciting {
+			acks.maxAckEliciting = num
+		}
+	}
+
+	acks.seen.add(num, num+1)
+	if num == acks.seen.max() {
+		acks.maxRecvTime = now
+	}
+
+	// Limit the total number of ACK ranges by dropping older ranges.
+	//
+	// Remembering more ranges results in larger ACK frames.
+	//
+	// Remembering a large number of ranges could result in ACK frames becoming
+	// too large to fit in a packet, in which case we will silently drop older
+	// ranges during packet construction.
+	//
+	// Remembering fewer ranges can result in unnecessary retransmissions,
+	// since we cannot accept packets older than the oldest remembered range.
+	//
+	// The limit here is completely arbitrary. If it seems wrong, it probably is.
+	//
+	// https://www.rfc-editor.org/rfc/rfc9000#section-13.2.3
+	const maxAckRanges = 8
+	if overflow := acks.seen.numRanges() - maxAckRanges; overflow > 0 {
+		acks.seen.removeranges(0, overflow)
+	}
+}
+
+// mustAckImmediately reports whether an ack-eliciting packet must be acknowledged immediately,
+// or whether the ack may be deferred.
+func (acks *ackState) mustAckImmediately(space numberSpace, num packetNumber) bool {
+	// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.1
+	if space != appDataSpace {
+		// "[...] all ack-eliciting Initial and Handshake packets [...]"
+		// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.1-2
+		return true
+	}
+	if num < acks.maxAckEliciting {
+		// "[...] when the received packet has a packet number less than another
+		// ack-eliciting packet that has been received [...]"
+		// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.1-8.1
+		return true
+	}
+	if acks.seen.rangeContaining(acks.maxAckEliciting).end != num {
+		// "[...] when the packet has a packet number larger than the highest-numbered
+		// ack-eliciting packet that has been received and there are missing packets
+		// between that packet and this packet."
+		// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.1-8.2
+		//
+		// This case is a bit tricky. Let's say we've received:
+		//   0, ack-eliciting
+		//   1, ack-eliciting
+		//   3, NOT ack eliciting
+		//
+		// We have sent ACKs for 0 and 1. If we receive ack-eliciting packet 2,
+		// we do not need to send an immediate ACK, because there are no missing
+		// packets between it and the highest-numbered ack-eliciting packet (1).
+		// If we receive ack-eliciting packet 4, we do need to send an immediate ACK,
+		// because there's a gap (the missing packet 2).
+		//
+		// We check for this by looking up the ACK range which contains the
+		// highest-numbered ack-eliciting packet: [0, 1) in the above example.
+		// If the range ends just before the packet we are now processing,
+		// there are no gaps. If it does not, there must be a gap.
+		return true
+	}
+	if acks.unackedAckEliciting >= 2 {
+		// "[...] after receiving at least two ack-eliciting packets."
+		// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.2
+		return true
+	}
+	return false
+}
+
+// shouldSendAck reports whether the connection should send an ACK frame at this time,
+// in an ACK-only packet if necessary.
+func (acks *ackState) shouldSendAck(now time.Time) bool {
+	return !acks.nextAck.IsZero() && !acks.nextAck.After(now)
+}
+
+// acksToSend returns the set of packet numbers to ACK at this time, and the current ack delay.
+// It may return acks even if shouldSendAck returns false, when there are unacked
+// ack-eliciting packets whose ack is being delayed.
+func (acks *ackState) acksToSend(now time.Time) (nums rangeset[packetNumber], ackDelay time.Duration) {
+	if acks.nextAck.IsZero() && acks.unackedAckEliciting == 0 {
+		return nil, 0
+	}
+	// "[...] the delays intentionally introduced between the time the packet with the
+	// largest packet number is received and the time an acknowledgement is sent."
+	// https://www.rfc-editor.org/rfc/rfc9000#section-13.2.5-1
+	delay := now.Sub(acks.maxRecvTime)
+	if delay < 0 {
+		delay = 0
+	}
+	return acks.seen, delay
+}
+
+// sentAck records that an ACK frame has been sent.
+func (acks *ackState) sentAck() {
+	acks.nextAck = time.Time{}
+	acks.unackedAckEliciting = 0
+}
+
+// handleAck records that an ack has been received for a ACK frame we sent
+// containing the given Largest Acknowledged field.
+func (acks *ackState) handleAck(largestAcked packetNumber) {
+	// We can stop acking packets less or equal to largestAcked.
+	// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.4-1
+	//
+	// We rely on acks.seen containing the largest packet number that has been successfully
+	// processed, so we retain the range containing largestAcked and discard previous ones.
+	acks.seen.sub(0, acks.seen.rangeContaining(largestAcked).start)
+}
+
+// largestSeen reports the largest seen packet.
+func (acks *ackState) largestSeen() packetNumber {
+	return acks.seen.max()
+}
diff --git a/internal/quic/acks_test.go b/internal/quic/acks_test.go
new file mode 100644
index 0000000..4f10329
--- /dev/null
+++ b/internal/quic/acks_test.go
@@ -0,0 +1,248 @@
+// 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 (
+	"testing"
+	"time"
+)
+
+func TestAcksDisallowDuplicate(t *testing.T) {
+	// Don't process a packet that we've seen before.
+	acks := ackState{}
+	now := time.Now()
+	receive := []packetNumber{0, 1, 2, 4, 7, 6, 9}
+	seen := map[packetNumber]bool{}
+	for i, pnum := range receive {
+		acks.receive(now, appDataSpace, pnum, true)
+		seen[pnum] = true
+		for ppnum := packetNumber(0); ppnum < 11; ppnum++ {
+			if got, want := acks.shouldProcess(ppnum), !seen[ppnum]; got != want {
+				t.Fatalf("after receiving %v: acks.shouldProcess(%v) = %v, want %v", receive[:i+1], ppnum, got, want)
+			}
+		}
+	}
+}
+
+func TestAcksDisallowDiscardedAckRanges(t *testing.T) {
+	// Don't process a packet with a number in a discarded range.
+	acks := ackState{}
+	now := time.Now()
+	for pnum := packetNumber(0); ; pnum += 2 {
+		acks.receive(now, appDataSpace, pnum, true)
+		send, _ := acks.acksToSend(now)
+		for ppnum := packetNumber(0); ppnum < packetNumber(send.min()); ppnum++ {
+			if acks.shouldProcess(ppnum) {
+				t.Fatalf("after limiting ack ranges to %v: acks.shouldProcess(%v) (in discarded range) = true, want false", send, ppnum)
+			}
+		}
+		if send.min() > 10 {
+			break
+		}
+	}
+}
+
+func TestAcksSent(t *testing.T) {
+	type packet struct {
+		pnum         packetNumber
+		ackEliciting bool
+	}
+	for _, test := range []struct {
+		name  string
+		space numberSpace
+
+		// ackedPackets and packets are packets that we receive.
+		// After receiving all packets in ackedPackets, we send an ack.
+		// Then we receive the subsequent packets in packets.
+		ackedPackets []packet
+		packets      []packet
+
+		wantDelay time.Duration
+		wantAcks  rangeset[packetNumber]
+	}{{
+		name:  "no packets to ack",
+		space: initialSpace,
+	}, {
+		name:  "non-ack-eliciting packets are not acked",
+		space: initialSpace,
+		packets: []packet{{
+			pnum:         0,
+			ackEliciting: false,
+		}},
+	}, {
+		name:  "ack-eliciting Initial packets are acked immediately",
+		space: initialSpace,
+		packets: []packet{{
+			pnum:         0,
+			ackEliciting: true,
+		}},
+		wantAcks:  rangeset[packetNumber]{{0, 1}},
+		wantDelay: 0,
+	}, {
+		name:  "ack-eliciting Handshake packets are acked immediately",
+		space: handshakeSpace,
+		packets: []packet{{
+			pnum:         0,
+			ackEliciting: true,
+		}},
+		wantAcks:  rangeset[packetNumber]{{0, 1}},
+		wantDelay: 0,
+	}, {
+		name:  "ack-eliciting AppData packets are acked after max_ack_delay",
+		space: appDataSpace,
+		packets: []packet{{
+			pnum:         0,
+			ackEliciting: true,
+		}},
+		wantAcks:  rangeset[packetNumber]{{0, 1}},
+		wantDelay: maxAckDelay - timerGranularity,
+	}, {
+		name:  "reordered ack-eliciting packets are acked immediately",
+		space: appDataSpace,
+		ackedPackets: []packet{{
+			pnum:         1,
+			ackEliciting: true,
+		}},
+		packets: []packet{{
+			pnum:         0,
+			ackEliciting: true,
+		}},
+		wantAcks:  rangeset[packetNumber]{{0, 2}},
+		wantDelay: 0,
+	}, {
+		name:  "gaps in ack-eliciting packets are acked immediately",
+		space: appDataSpace,
+		packets: []packet{{
+			pnum:         1,
+			ackEliciting: true,
+		}},
+		wantAcks:  rangeset[packetNumber]{{1, 2}},
+		wantDelay: 0,
+	}, {
+		name:  "reordered non-ack-eliciting packets are not acked immediately",
+		space: appDataSpace,
+		ackedPackets: []packet{{
+			pnum:         1,
+			ackEliciting: true,
+		}},
+		packets: []packet{{
+			pnum:         2,
+			ackEliciting: true,
+		}, {
+			pnum:         0,
+			ackEliciting: false,
+		}, {
+			pnum:         4,
+			ackEliciting: false,
+		}},
+		wantAcks:  rangeset[packetNumber]{{0, 3}, {4, 5}},
+		wantDelay: maxAckDelay - timerGranularity,
+	}, {
+		name:  "immediate ack after two ack-eliciting packets are received",
+		space: appDataSpace,
+		packets: []packet{{
+			pnum:         0,
+			ackEliciting: true,
+		}, {
+			pnum:         1,
+			ackEliciting: true,
+		}},
+		wantAcks:  rangeset[packetNumber]{{0, 2}},
+		wantDelay: 0,
+	}} {
+		t.Run(test.name, func(t *testing.T) {
+			acks := ackState{}
+			start := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
+			for _, p := range test.ackedPackets {
+				t.Logf("receive %v.%v, ack-eliciting=%v", test.space, p.pnum, p.ackEliciting)
+				acks.receive(start, test.space, p.pnum, p.ackEliciting)
+			}
+			t.Logf("send an ACK frame")
+			acks.sentAck()
+			for _, p := range test.packets {
+				t.Logf("receive %v.%v, ack-eliciting=%v", test.space, p.pnum, p.ackEliciting)
+				acks.receive(start, test.space, p.pnum, p.ackEliciting)
+			}
+			switch {
+			case len(test.wantAcks) == 0:
+				// No ACK should be sent, even well after max_ack_delay.
+				if acks.shouldSendAck(start.Add(10 * maxAckDelay)) {
+					t.Errorf("acks.shouldSendAck(T+10*max_ack_delay) = true, want false")
+				}
+			case test.wantDelay > 0:
+				// No ACK should be sent before a delay.
+				if acks.shouldSendAck(start.Add(test.wantDelay - 1)) {
+					t.Errorf("acks.shouldSendAck(T+%v-1ns) = true, want false", test.wantDelay)
+				}
+				fallthrough
+			default:
+				// ACK should be sent after a delay.
+				if !acks.shouldSendAck(start.Add(test.wantDelay)) {
+					t.Errorf("acks.shouldSendAck(T+%v) = false, want true", test.wantDelay)
+				}
+			}
+			// acksToSend always reports the available packets that can be acked,
+			// and the amount of time that has passed since the most recent acked
+			// packet was received.
+			for _, delay := range []time.Duration{
+				0,
+				test.wantDelay,
+				test.wantDelay + 1,
+			} {
+				gotNums, gotDelay := acks.acksToSend(start.Add(delay))
+				wantDelay := delay
+				if len(gotNums) == 0 {
+					wantDelay = 0
+				}
+				if !slicesEqual(gotNums, test.wantAcks) || gotDelay != wantDelay {
+					t.Errorf("acks.acksToSend(T+%v) = %v, %v; want %v, %v", delay, gotNums, gotDelay, test.wantAcks, wantDelay)
+				}
+			}
+		})
+	}
+}
+
+// slicesEqual reports whether two slices are equal.
+// Replace this with slices.Equal once the module go.mod is go1.17 or newer.
+func slicesEqual[E comparable](s1, s2 []E) bool {
+	if len(s1) != len(s2) {
+		return false
+	}
+	for i := range s1 {
+		if s1[i] != s2[i] {
+			return false
+		}
+	}
+	return true
+}
+
+func TestAcksDiscardAfterAck(t *testing.T) {
+	acks := ackState{}
+	now := time.Now()
+	acks.receive(now, appDataSpace, 0, true)
+	acks.receive(now, appDataSpace, 2, true)
+	acks.receive(now, appDataSpace, 4, true)
+	acks.receive(now, appDataSpace, 5, true)
+	acks.receive(now, appDataSpace, 6, true)
+	acks.handleAck(6) // discards all ranges prior to the one containing packet 6
+	acks.receive(now, appDataSpace, 7, true)
+	got, _ := acks.acksToSend(now)
+	if len(got) != 1 {
+		t.Errorf("acks.acksToSend contains ranges prior to last acknowledged ack; got %v, want 1 range", got)
+	}
+}
+
+func TestAcksLargestSeen(t *testing.T) {
+	acks := ackState{}
+	now := time.Now()
+	acks.receive(now, appDataSpace, 0, true)
+	acks.receive(now, appDataSpace, 4, true)
+	acks.receive(now, appDataSpace, 1, true)
+	if got, want := acks.largestSeen(), packetNumber(4); got != want {
+		t.Errorf("acks.largestSeen() = %v, want %v", got, want)
+	}
+}
diff --git a/internal/quic/rangeset.go b/internal/quic/rangeset.go
index 5339c5a..4966a99 100644
--- a/internal/quic/rangeset.go
+++ b/internal/quic/rangeset.go
@@ -154,6 +154,11 @@
 	return s[len(s)-1].end
 }
 
+// numRanges returns the number of ranges in the rangeset.
+func (s rangeset[T]) numRanges() int {
+	return len(s)
+}
+
 // isrange reports if the rangeset covers exactly the range [start, end).
 func (s rangeset[T]) isrange(start, end T) bool {
 	switch len(s) {
diff --git a/internal/quic/rangeset_test.go b/internal/quic/rangeset_test.go
index 3080469..2027f14 100644
--- a/internal/quic/rangeset_test.go
+++ b/internal/quic/rangeset_test.go
@@ -295,3 +295,23 @@
 		}
 	}
 }
+
+func TestRangesetNumRanges(t *testing.T) {
+	for _, test := range []struct {
+		s    rangeset[int64]
+		want int
+	}{{
+		s:    rangeset[int64]{},
+		want: 0,
+	}, {
+		s:    rangeset[int64]{{0, 100}},
+		want: 1,
+	}, {
+		s:    rangeset[int64]{{0, 100}, {200, 300}},
+		want: 2,
+	}} {
+		if got, want := test.s.numRanges(), test.want; got != want {
+			t.Errorf("%+v.numRanges() = %v, want %v", test.s, got, want)
+		}
+	}
+}