diff --git a/internal/quic/cmd/interop/main.go b/internal/quic/cmd/interop/main.go
index 65142ad..682cc77 100644
--- a/internal/quic/cmd/interop/main.go
+++ b/internal/quic/cmd/interop/main.go
@@ -95,6 +95,9 @@
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
diff --git a/quic/acks.go b/quic/acks.go
index d4ac449..90f82be 100644
--- a/quic/acks.go
+++ b/quic/acks.go
@@ -25,6 +25,15 @@
// 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 @@
}
// 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 @@
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 @@
// 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 @@
// 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
//
diff --git a/quic/acks_test.go b/quic/acks_test.go
index 7fca561..2abdc31 100644
--- a/quic/acks_test.go
+++ b/quic/acks_test.go
@@ -17,7 +17,7 @@
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 @@
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 @@
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 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 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)
}
diff --git a/quic/conn_loss.go b/quic/conn_loss.go
index 06761e3..bc6d106 100644
--- a/quic/conn_loss.go
+++ b/quic/conn_loss.go
@@ -32,7 +32,7 @@
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.
diff --git a/quic/conn_recv.go b/quic/conn_recv.go
index e4ef23b..2a7204c 100644
--- a/quic/conn_recv.go
+++ b/quic/conn_recv.go
@@ -124,7 +124,7 @@
}
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 @@
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) 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.
diff --git a/quic/conn_send.go b/quic/conn_send.go
index d6fb149..3e8cf52 100644
--- a/quic/conn_send.go
+++ b/quic/conn_send.go
@@ -374,7 +374,7 @@
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) {
diff --git a/quic/frame_debug.go b/quic/frame_debug.go
index 7cf03fa..8d8fd54 100644
--- a/quic/frame_debug.go
+++ b/quic/frame_debug.go
@@ -136,11 +136,12 @@
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 @@
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 {
diff --git a/quic/packet_codec_test.go b/quic/packet_codec_test.go
index be335d7..4ae22b3 100644
--- a/quic/packet_codec_test.go
+++ b/quic/packet_codec_test.go
@@ -264,6 +264,65 @@
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}`,
f: debugFrameResetStream{
@@ -675,6 +734,7 @@
ranges: []i64range[packetNumber]{
{0, 1},
},
+ ecn: ecnCounts{1, 2, 3},
},
b: []byte{
0x03, // TYPE (i) = 0x02..0x03
diff --git a/quic/packet_parser.go b/quic/packet_parser.go
index eadf14f..265c4ae 100644
--- a/quic/packet_parser.go
+++ b/quic/packet_parser.go
@@ -157,25 +157,25 @@
// 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 @@
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 @@
gap, n := quicwire.ConsumeVarint(b)
if n < 0 {
- return 0, 0, -1
+ return 0, 0, ecnCounts{}, -1
}
b = b[n:]
@@ -206,32 +206,30 @@
}
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) {
diff --git a/quic/packet_writer.go b/quic/packet_writer.go
index 3560ebb..f446521 100644
--- a/quic/packet_writer.go
+++ b/quic/packet_writer.go
@@ -262,7 +262,7 @@
// 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 @@
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 @@
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 @@
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
}