quic: send ECN feedback to peers

Track the total number of ECT(0), ECT(1), and ECN-CE state of packets we
process in each packet number space. Send it back to the peer in each
ACK frame (unless it's all zeros).

"Even if an endpoint does not set an ECT field in packets it sends, the
endpoint MUST provide feedback about ECN markings it receives, if these
are accessible."
https://www.rfc-editor.org/rfc/rfc9000#section-13.4.1-2

For golang/go#58547

Change-Id: I3ce5be6c536198eaa711f527402503b0567fc7a5
Reviewed-on: https://go-review.googlesource.com/c/net/+/712280
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Rhys Hiltner <rhys.hiltner@gmail.com>
Auto-Submit: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Rhys Hiltner
2025-10-15 15:39:35 -07:00
committed by Gopher Robot
parent c296fafc21
commit 98daa2e33a
10 changed files with 153 additions and 45 deletions

View File

@@ -95,6 +95,9 @@ func main() {
basicTest(ctx, config, urls)
return
}
case "ecn":
// TODO: We give ECN feedback to the sender, but we don't add our own
// ECN marks to outgoing packets.
case "transfer":
// "The client should use small initial flow control windows
// for both stream- and connection-level flow control

View File

@@ -25,6 +25,15 @@ type ackState struct {
// The number of ack-eliciting packets in seen that we have not yet acknowledged.
unackedAckEliciting int
// Total ECN counters for this packet number space.
ecn ecnCounts
}
type ecnCounts struct {
t0 int
t1 int
ce int
}
// shouldProcess reports whether a packet should be handled or discarded.
@@ -43,10 +52,10 @@ func (acks *ackState) shouldProcess(num packetNumber) bool {
}
// receive records receipt of a packet.
func (acks *ackState) receive(now time.Time, space numberSpace, num packetNumber, ackEliciting bool) {
func (acks *ackState) receive(now time.Time, space numberSpace, num packetNumber, ackEliciting bool, ecn ecnBits) {
if ackEliciting {
acks.unackedAckEliciting++
if acks.mustAckImmediately(space, num) {
if acks.mustAckImmediately(space, num, ecn) {
acks.nextAck = now
} else if acks.nextAck.IsZero() {
// This packet does not need to be acknowledged immediately,
@@ -70,6 +79,15 @@ func (acks *ackState) receive(now time.Time, space numberSpace, num packetNumber
acks.maxRecvTime = now
}
switch ecn {
case ecnECT0:
acks.ecn.t0++
case ecnECT1:
acks.ecn.t1++
case ecnCE:
acks.ecn.ce++
}
// Limit the total number of ACK ranges by dropping older ranges.
//
// Remembering more ranges results in larger ACK frames.
@@ -92,7 +110,7 @@ func (acks *ackState) receive(now time.Time, space numberSpace, num packetNumber
// 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 {
func (acks *ackState) mustAckImmediately(space numberSpace, num packetNumber, ecn ecnBits) bool {
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.1
if space != appDataSpace {
// "[...] all ack-eliciting Initial and Handshake packets [...]"
@@ -128,6 +146,12 @@ func (acks *ackState) mustAckImmediately(space numberSpace, num packetNumber) bo
// there are no gaps. If it does not, there must be a gap.
return true
}
// "[...] packets marked with the ECN Congestion Experienced (CE) codepoint
// in the IP header SHOULD be acknowledged immediately [...]"
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.1-9
if ecn == ecnCE {
return true
}
// "[...] SHOULD send an ACK frame after receiving at least two ack-eliciting packets."
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.2
//

View File

@@ -17,7 +17,7 @@ func TestAcksDisallowDuplicate(t *testing.T) {
receive := []packetNumber{0, 1, 2, 4, 7, 6, 9}
seen := map[packetNumber]bool{}
for i, pnum := range receive {
acks.receive(now, appDataSpace, pnum, true)
acks.receive(now, appDataSpace, pnum, true, ecnNotECT)
seen[pnum] = true
for ppnum := packetNumber(0); ppnum < 11; ppnum++ {
if got, want := acks.shouldProcess(ppnum), !seen[ppnum]; got != want {
@@ -32,7 +32,7 @@ func TestAcksDisallowDiscardedAckRanges(t *testing.T) {
acks := ackState{}
now := time.Now()
for pnum := packetNumber(0); ; pnum += 2 {
acks.receive(now, appDataSpace, pnum, true)
acks.receive(now, appDataSpace, pnum, true, ecnNotECT)
send, _ := acks.acksToSend(now)
for ppnum := packetNumber(0); ppnum < packetNumber(send.min()); ppnum++ {
if acks.shouldProcess(ppnum) {
@@ -158,13 +158,13 @@ func TestAcksSent(t *testing.T) {
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)
acks.receive(start, test.space, p.pnum, p.ackEliciting, ecnNotECT)
}
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)
acks.receive(start, test.space, p.pnum, p.ackEliciting, ecnNotECT)
}
switch {
case len(test.wantAcks) == 0:
@@ -208,13 +208,13 @@ func TestAcksSent(t *testing.T) {
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.receive(now, appDataSpace, 0, true, ecnNotECT)
acks.receive(now, appDataSpace, 2, true, ecnNotECT)
acks.receive(now, appDataSpace, 4, true, ecnNotECT)
acks.receive(now, appDataSpace, 5, true, ecnNotECT)
acks.receive(now, appDataSpace, 6, true, ecnNotECT)
acks.handleAck(6) // discards all ranges prior to the one containing packet 6
acks.receive(now, appDataSpace, 7, true)
acks.receive(now, appDataSpace, 7, true, ecnNotECT)
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)
@@ -224,9 +224,9 @@ func TestAcksDiscardAfterAck(t *testing.T) {
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)
acks.receive(now, appDataSpace, 0, true, ecnNotECT)
acks.receive(now, appDataSpace, 4, true, ecnNotECT)
acks.receive(now, appDataSpace, 1, true, ecnNotECT)
if got, want := acks.largestSeen(), packetNumber(4); got != want {
t.Errorf("acks.largestSeen() = %v, want %v", got, want)
}

View File

@@ -32,7 +32,7 @@ func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetF
switch f := sent.next(); f {
default:
panic(fmt.Sprintf("BUG: unhandled acked/lost frame type %x", f))
case frameTypeAck:
case frameTypeAck, frameTypeAckECN:
// Unlike most information, loss of an ACK frame does not trigger
// retransmission. ACKs are sent in response to ack-eliciting packets,
// and always contain the latest information available.

View File

@@ -124,7 +124,7 @@ func (c *Conn) handleLongHeader(now time.Time, dgram *datagram, ptype packetType
}
c.connIDState.handlePacket(c, p.ptype, p.srcConnID)
ackEliciting := c.handleFrames(now, dgram, ptype, space, p.payload)
c.acks[space].receive(now, space, p.num, ackEliciting)
c.acks[space].receive(now, space, p.num, ackEliciting, dgram.ecn)
if p.ptype == packetTypeHandshake && c.side == serverSide {
c.loss.validateClientAddress()
@@ -174,7 +174,7 @@ func (c *Conn) handle1RTT(now time.Time, dgram *datagram, buf []byte) int {
c.log1RTTPacketReceived(p, buf)
}
ackEliciting := c.handleFrames(now, dgram, packetType1RTT, appDataSpace, p.payload)
c.acks[appDataSpace].receive(now, appDataSpace, p.num, ackEliciting)
c.acks[appDataSpace].receive(now, appDataSpace, p.num, ackEliciting, dgram.ecn)
return len(buf)
}
@@ -420,12 +420,15 @@ func (c *Conn) handleFrames(now time.Time, dgram *datagram, ptype packetType, sp
func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) int {
c.loss.receiveAckStart()
largest, ackDelay, n := consumeAckFrame(payload, func(rangeIndex int, start, end packetNumber) {
largest, ackDelay, ecn, n := consumeAckFrame(payload, func(rangeIndex int, start, end packetNumber) {
if err := c.loss.receiveAckRange(now, space, rangeIndex, start, end, c.handleAckOrLoss); err != nil {
c.abort(now, err)
return
}
})
// TODO: Make use of ECN feedback.
// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.3.2
_ = ecn
// Prior to receiving the peer's transport parameters, we cannot
// interpret the ACK Delay field because we don't know the ack_delay_exponent
// to apply.

View File

@@ -374,7 +374,7 @@ func (c *Conn) appendAckFrame(now time.Time, space numberSpace) bool {
return false
}
d := unscaledAckDelayFromDuration(delay, ackDelayExponent)
return c.w.appendAckFrame(seen, d)
return c.w.appendAckFrame(seen, d, c.acks[space].ecn)
}
func (c *Conn) appendConnectionCloseFrame(now time.Time, space numberSpace, err error) {

View File

@@ -136,11 +136,12 @@ func (f debugFramePing) LogValue() slog.Value {
type debugFrameAck struct {
ackDelay unscaledAckDelay
ranges []i64range[packetNumber]
ecn ecnCounts
}
func parseDebugFrameAck(b []byte) (f debugFrameAck, n int) {
f.ranges = nil
_, f.ackDelay, n = consumeAckFrame(b, func(_ int, start, end packetNumber) {
_, f.ackDelay, f.ecn, n = consumeAckFrame(b, func(_ int, start, end packetNumber) {
f.ranges = append(f.ranges, i64range[packetNumber]{
start: start,
end: end,
@@ -159,11 +160,15 @@ func (f debugFrameAck) String() string {
for _, r := range f.ranges {
s += fmt.Sprintf(" [%v,%v)", r.start, r.end)
}
if (f.ecn != ecnCounts{}) {
s += fmt.Sprintf(" ECN=[%d,%d,%d]", f.ecn.t0, f.ecn.t1, f.ecn.ce)
}
return s
}
func (f debugFrameAck) write(w *packetWriter) bool {
return w.appendAckFrame(rangeset[packetNumber](f.ranges), f.ackDelay)
return w.appendAckFrame(rangeset[packetNumber](f.ranges), f.ackDelay, f.ecn)
}
func (f debugFrameAck) LogValue() slog.Value {

View File

@@ -263,6 +263,65 @@ func TestFrameEncodeDecode(t *testing.T) {
0x0f, // Gap (i)
0x0e, // ACK Range Length (i)
},
}, {
s: "ACK Delay=10 [0,16) [17,32) ECN=[1,2,3]",
j: `"error: debugFrameAck should not appear as a slog Value"`,
f: debugFrameAck{
ackDelay: 10,
ranges: []i64range[packetNumber]{
{0x00, 0x10},
{0x11, 0x20},
},
ecn: ecnCounts{1, 2, 3},
},
b: []byte{
0x03, // TYPE (i) = 0x3
0x1f, // Largest Acknowledged (i)
10, // ACK Delay (i)
0x01, // ACK Range Count (i)
0x0e, // First ACK Range (i)
0x00, // Gap (i)
0x0f, // ACK Range Length (i)
0x01, // ECT0 Count (i)
0x02, // ECT1 Count (i)
0x03, // ECN-CE Count (i)
},
truncated: []byte{
0x03, // TYPE (i) = 0x3
0x1f, // Largest Acknowledged (i)
10, // ACK Delay (i)
0x00, // ACK Range Count (i)
0x0e, // First ACK Range (i)
0x01, // ECT0 Count (i)
0x02, // ECT1 Count (i)
0x03, // ECN-CE Count (i)
},
}, {
s: "ACK Delay=10 [17,32) ECN=[1,2,3]",
j: `"error: debugFrameAck should not appear as a slog Value"`,
f: debugFrameAck{
ackDelay: 10,
ranges: []i64range[packetNumber]{
{0x11, 0x20},
},
ecn: ecnCounts{1, 2, 3},
},
b: []byte{
0x03, // TYPE (i) = 0x3
0x1f, // Largest Acknowledged (i)
10, // ACK Delay (i)
0x00, // ACK Range Count (i)
0x0e, // First ACK Range (i)
0x01, // ECT0 Count (i)
0x02, // ECT1 Count (i)
0x03, // ECN-CE Count (i)
},
// Downgrading to a type 0x2 ACK frame is not allowed: "Even if an
// endpoint does not set an ECT field in packets it sends, the endpoint
// MUST provide feedback about ECN markings it receives, if these are
// accessible."
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.4.1-2
truncated: nil,
}, {
s: "RESET_STREAM ID=1 Code=2 FinalSize=3",
j: `{"frame_type":"reset_stream","stream_id":1,"final_size":3}`,
@@ -675,6 +734,7 @@ func TestFrameDecode(t *testing.T) {
ranges: []i64range[packetNumber]{
{0, 1},
},
ecn: ecnCounts{1, 2, 3},
},
b: []byte{
0x03, // TYPE (i) = 0x02..0x03

View File

@@ -157,25 +157,25 @@ func parse1RTTPacket(pkt []byte, k *updatingKeyPair, dstConnIDLen int, pnumMax p
// which includes both general parse failures and specific violations of frame
// constraints.
func consumeAckFrame(frame []byte, f func(rangeIndex int, start, end packetNumber)) (largest packetNumber, ackDelay unscaledAckDelay, n int) {
func consumeAckFrame(frame []byte, f func(rangeIndex int, start, end packetNumber)) (largest packetNumber, ackDelay unscaledAckDelay, ecn ecnCounts, n int) {
b := frame[1:] // type
largestAck, n := quicwire.ConsumeVarint(b)
if n < 0 {
return 0, 0, -1
return 0, 0, ecnCounts{}, -1
}
b = b[n:]
v, n := quicwire.ConsumeVarintInt64(b)
if n < 0 {
return 0, 0, -1
return 0, 0, ecnCounts{}, -1
}
b = b[n:]
ackDelay = unscaledAckDelay(v)
ackRangeCount, n := quicwire.ConsumeVarint(b)
if n < 0 {
return 0, 0, -1
return 0, 0, ecnCounts{}, -1
}
b = b[n:]
@@ -183,12 +183,12 @@ func consumeAckFrame(frame []byte, f func(rangeIndex int, start, end packetNumbe
for i := uint64(0); ; i++ {
rangeLen, n := quicwire.ConsumeVarint(b)
if n < 0 {
return 0, 0, -1
return 0, 0, ecnCounts{}, -1
}
b = b[n:]
rangeMin := rangeMax - packetNumber(rangeLen)
if rangeMin < 0 || rangeMin > rangeMax {
return 0, 0, -1
return 0, 0, ecnCounts{}, -1
}
f(int(i), rangeMin, rangeMax+1)
@@ -198,7 +198,7 @@ func consumeAckFrame(frame []byte, f func(rangeIndex int, start, end packetNumbe
gap, n := quicwire.ConsumeVarint(b)
if n < 0 {
return 0, 0, -1
return 0, 0, ecnCounts{}, -1
}
b = b[n:]
@@ -206,32 +206,30 @@ func consumeAckFrame(frame []byte, f func(rangeIndex int, start, end packetNumbe
}
if frame[0] != frameTypeAckECN {
return packetNumber(largestAck), ackDelay, len(frame) - len(b)
return packetNumber(largestAck), ackDelay, ecnCounts{}, len(frame) - len(b)
}
ect0Count, n := quicwire.ConsumeVarint(b)
if n < 0 {
return 0, 0, -1
return 0, 0, ecnCounts{}, -1
}
b = b[n:]
ect1Count, n := quicwire.ConsumeVarint(b)
if n < 0 {
return 0, 0, -1
return 0, 0, ecnCounts{}, -1
}
b = b[n:]
ecnCECount, n := quicwire.ConsumeVarint(b)
if n < 0 {
return 0, 0, -1
return 0, 0, ecnCounts{}, -1
}
b = b[n:]
// TODO: Make use of ECN feedback.
// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.3.2
_ = ect0Count
_ = ect1Count
_ = ecnCECount
ecn.t0 = int(ect0Count)
ecn.t1 = int(ect1Count)
ecn.ce = int(ecnCECount)
return packetNumber(largestAck), ackDelay, len(frame) - len(b)
return packetNumber(largestAck), ackDelay, ecn, len(frame) - len(b)
}
func consumeResetStreamFrame(b []byte) (id streamID, code uint64, finalSize int64, n int) {

View File

@@ -262,7 +262,7 @@ func (w *packetWriter) appendPingFrame() (added bool) {
// to the peer potentially failing to receive an acknowledgement
// for an older packet during a period of high packet loss or
// reordering. This may result in unnecessary retransmissions.
func (w *packetWriter) appendAckFrame(seen rangeset[packetNumber], delay unscaledAckDelay) (added bool) {
func (w *packetWriter) appendAckFrame(seen rangeset[packetNumber], delay unscaledAckDelay, ecn ecnCounts) (added bool) {
if len(seen) == 0 {
return false
}
@@ -270,10 +270,20 @@ func (w *packetWriter) appendAckFrame(seen rangeset[packetNumber], delay unscale
largest = uint64(seen.max())
firstRange = uint64(seen[len(seen)-1].size() - 1)
)
if w.avail() < 1+quicwire.SizeVarint(largest)+quicwire.SizeVarint(uint64(delay))+1+quicwire.SizeVarint(firstRange) {
var ecnLen int
ackType := byte(frameTypeAck)
if (ecn != ecnCounts{}) {
// "Even if an endpoint does not set an ECT field in packets it sends,
// the endpoint MUST provide feedback about ECN markings it receives, if
// these are accessible."
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.4.1-2
ecnLen = quicwire.SizeVarint(uint64(ecn.ce)) + quicwire.SizeVarint(uint64(ecn.t0)) + quicwire.SizeVarint(uint64(ecn.t1))
ackType = frameTypeAckECN
}
if w.avail() < 1+quicwire.SizeVarint(largest)+quicwire.SizeVarint(uint64(delay))+1+quicwire.SizeVarint(firstRange)+ecnLen {
return false
}
w.b = append(w.b, frameTypeAck)
w.b = append(w.b, ackType)
w.b = quicwire.AppendVarint(w.b, largest)
w.b = quicwire.AppendVarint(w.b, uint64(delay))
// The range count is technically a varint, but we'll reserve a single byte for it
@@ -285,7 +295,7 @@ func (w *packetWriter) appendAckFrame(seen rangeset[packetNumber], delay unscale
for i := len(seen) - 2; i >= 0; i-- {
gap := uint64(seen[i+1].start - seen[i].end - 1)
size := uint64(seen[i].size() - 1)
if w.avail() < quicwire.SizeVarint(gap)+quicwire.SizeVarint(size) || rangeCount > 62 {
if w.avail() < quicwire.SizeVarint(gap)+quicwire.SizeVarint(size)+ecnLen || rangeCount > 62 {
break
}
w.b = quicwire.AppendVarint(w.b, gap)
@@ -293,7 +303,12 @@ func (w *packetWriter) appendAckFrame(seen rangeset[packetNumber], delay unscale
rangeCount++
}
w.b[rangeCountOff] = rangeCount
w.sent.appendNonAckElicitingFrame(frameTypeAck)
if ackType == frameTypeAckECN {
w.b = quicwire.AppendVarint(w.b, uint64(ecn.t0))
w.b = quicwire.AppendVarint(w.b, uint64(ecn.t1))
w.b = quicwire.AppendVarint(w.b, uint64(ecn.ce))
}
w.sent.appendNonAckElicitingFrame(ackType)
w.sent.appendInt(uint64(seen.max()))
return true
}