diff --git a/go.mod b/go.mod index 7ad6254fd4..92c7b8d76c 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module golang.org/x/net go 1.23.0 require ( - golang.org/x/crypto v0.36.0 - golang.org/x/sys v0.31.0 - golang.org/x/term v0.30.0 - golang.org/x/text v0.23.0 + golang.org/x/crypto v0.39.0 + golang.org/x/sys v0.33.0 + golang.org/x/term v0.32.0 + golang.org/x/text v0.26.0 ) diff --git a/go.sum b/go.sum index 27aba5f819..6b81fa4744 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= diff --git a/http2/frame.go b/http2/frame.go index 97bd8b06f7..db3264da8c 100644 --- a/http2/frame.go +++ b/http2/frame.go @@ -39,7 +39,7 @@ const ( FrameContinuation FrameType = 0x9 ) -var frameName = map[FrameType]string{ +var frameNames = [...]string{ FrameData: "DATA", FrameHeaders: "HEADERS", FramePriority: "PRIORITY", @@ -53,10 +53,10 @@ var frameName = map[FrameType]string{ } func (t FrameType) String() string { - if s, ok := frameName[t]; ok { - return s + if int(t) < len(frameNames) { + return frameNames[t] } - return fmt.Sprintf("UNKNOWN_FRAME_TYPE_%d", uint8(t)) + return fmt.Sprintf("UNKNOWN_FRAME_TYPE_%d", t) } // Flags is a bitmask of HTTP/2 flags. @@ -124,7 +124,7 @@ var flagName = map[FrameType]map[Flags]string{ // might be 0). type frameParser func(fc *frameCache, fh FrameHeader, countError func(string), payload []byte) (Frame, error) -var frameParsers = map[FrameType]frameParser{ +var frameParsers = [...]frameParser{ FrameData: parseDataFrame, FrameHeaders: parseHeadersFrame, FramePriority: parsePriorityFrame, @@ -138,8 +138,8 @@ var frameParsers = map[FrameType]frameParser{ } func typeFrameParser(t FrameType) frameParser { - if f := frameParsers[t]; f != nil { - return f + if int(t) < len(frameParsers) { + return frameParsers[t] } return parseUnknownFrame } @@ -509,7 +509,7 @@ func (fr *Framer) ReadFrame() (Frame, error) { } if fh.Length > fr.maxReadSize { if fh == invalidHTTP1LookingFrameHeader() { - return nil, fmt.Errorf("http2: failed reading the frame payload: %w, note that the frame header looked like an HTTP/1.1 header", err) + return nil, fmt.Errorf("http2: failed reading the frame payload: %w, note that the frame header looked like an HTTP/1.1 header", ErrFrameTooLarge) } return nil, ErrFrameTooLarge } diff --git a/http2/frame_test.go b/http2/frame_test.go index 86e5d4f80d..68505317e1 100644 --- a/http2/frame_test.go +++ b/http2/frame_test.go @@ -1258,3 +1258,21 @@ func TestSettingsDuplicates(t *testing.T) { } } + +func TestTypeFrameParser(t *testing.T) { + if len(frameNames) != len(frameParsers) { + t.Errorf("expected len(frameNames)=%d to equal len(frameParsers)=%d", + len(frameNames), len(frameParsers)) + } + + // typeFrameParser() for an unknown type returns a function that returns UnknownFrame + unknownFrameType := FrameType(FrameContinuation + 1) + unknownParser := typeFrameParser(unknownFrameType) + frame, err := unknownParser(nil, FrameHeader{}, nil, nil) + if err != nil { + t.Errorf("unknownParser() must not return an error: %v", err) + } + if _, isUnknown := frame.(*UnknownFrame); !isUnknown { + t.Errorf("expected UnknownFrame, got %T", frame) + } +} diff --git a/internal/http3/http3_synctest_test.go b/internal/http3/http3_synctest_test.go index ad26c6de09..a9c0ac2906 100644 --- a/internal/http3/http3_synctest_test.go +++ b/internal/http3/http3_synctest_test.go @@ -7,6 +7,7 @@ package http3 import ( + "context" "slices" "testing" "testing/synctest" @@ -14,9 +15,16 @@ import ( // runSynctest runs f in a synctest.Run bubble. // It arranges for t.Cleanup functions to run within the bubble. +// TODO: Replace with synctest.Test, which handles all this properly. func runSynctest(t *testing.T, f func(t testing.TB)) { synctest.Run(func() { - ct := &cleanupT{T: t} + // Create a context within the bubble, rather than using t.Context. + ctx, cancel := context.WithCancel(context.Background()) + ct := &cleanupT{ + T: t, + ctx: ctx, + cancel: cancel, + } defer ct.done() f(ct) }) @@ -33,6 +41,8 @@ func runSynctestSubtest(t *testing.T, name string, f func(t testing.TB)) { // Used to execute cleanup functions within a synctest bubble. type cleanupT struct { *testing.T + ctx context.Context + cancel context.CancelFunc cleanups []func() } @@ -41,7 +51,13 @@ func (t *cleanupT) Cleanup(f func()) { t.cleanups = append(t.cleanups, f) } +// Context replaces T.Context. +func (t *cleanupT) Context() context.Context { + return t.ctx +} + func (t *cleanupT) done() { + t.cancel() for _, f := range slices.Backward(t.cleanups) { f() } diff --git a/internal/timeseries/timeseries_test.go b/internal/timeseries/timeseries_test.go index 66325a912a..1c8ffff7f8 100644 --- a/internal/timeseries/timeseries_test.go +++ b/internal/timeseries/timeseries_test.go @@ -161,10 +161,3 @@ func TestExpectedErrorRate(t *testing.T) { checkNear(t, ts.Latest(2, buckets), min(float64(i), 3600), 600) } } - -func min(a, b float64) float64 { - if a < b { - return a - } - return b -} diff --git a/quic/ack_delay.go b/quic/ack_delay.go index 66bdf3cbc0..029ce6faec 100644 --- a/quic/ack_delay.go +++ b/quic/ack_delay.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/ack_delay_test.go b/quic/ack_delay_test.go index 038964a6dd..48ce24977e 100644 --- a/quic/ack_delay_test.go +++ b/quic/ack_delay_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/acks.go b/quic/acks.go index 039b7b46e6..d4ac4496e1 100644 --- a/quic/acks.go +++ b/quic/acks.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/acks_test.go b/quic/acks_test.go index d10f917ad9..7fca5617ba 100644 --- a/quic/acks_test.go +++ b/quic/acks_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/atomic_bits.go b/quic/atomic_bits.go index e1e2594d15..5244e04201 100644 --- a/quic/atomic_bits.go +++ b/quic/atomic_bits.go @@ -2,8 +2,6 @@ // 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 "sync/atomic" diff --git a/quic/bench_test.go b/quic/bench_test.go index 636b71327e..9d8e5d2318 100644 --- a/quic/bench_test.go +++ b/quic/bench_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/config.go b/quic/config.go index d6aa87730f..a9ec4bc437 100644 --- a/quic/config.go +++ b/quic/config.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/config_test.go b/quic/config_test.go index d292854f54..3511cd4a54 100644 --- a/quic/config_test.go +++ b/quic/config_test.go @@ -2,8 +2,6 @@ // 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" diff --git a/quic/congestion_reno.go b/quic/congestion_reno.go index a539835247..028a2ed6c3 100644 --- a/quic/congestion_reno.go +++ b/quic/congestion_reno.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/congestion_reno_test.go b/quic/congestion_reno_test.go index cda7a90a80..7b2521f8fb 100644 --- a/quic/congestion_reno_test.go +++ b/quic/congestion_reno_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/conn.go b/quic/conn.go index 1f1cfa6d0a..b9ec0e4059 100644 --- a/quic/conn.go +++ b/quic/conn.go @@ -2,16 +2,16 @@ // 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" + cryptorand "crypto/rand" "crypto/tls" "errors" "fmt" "log/slog" + "math/rand/v2" "net/netip" "time" ) @@ -26,6 +26,7 @@ type Conn struct { testHooks connTestHooks peerAddr netip.AddrPort localAddr netip.AddrPort + prng *rand.Rand msgc chan any donec chan struct{} // closed when conn loop exits @@ -38,6 +39,7 @@ type Conn struct { loss lossState streams streamsState path pathState + skip skipState // Packet protection keys, CRYPTO streams, and TLS state. keysInitial fixedKeyPair @@ -138,6 +140,14 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerHostname s } } + // A per-conn ChaCha8 PRNG is probably more than we need, + // but at least it's fairly small. + var seed [32]byte + if _, err := cryptorand.Read(seed[:]); err != nil { + panic(err) + } + c.prng = rand.New(rand.NewChaCha8(seed)) + // TODO: PMTU discovery. c.logConnectionStarted(cids.originalDstConnID, peerAddr) c.keysAppData.init() @@ -145,6 +155,7 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerHostname s c.streamsInit() c.lifetimeInit() c.restartIdleTimer(now) + c.skip.init(c) if err := c.startTLS(now, initialConnID, peerHostname, transportParameters{ initialSrcConnID: c.connIDState.srcConnID(), diff --git a/quic/conn_async_test.go b/quic/conn_async_test.go index 4671f8340e..f261e90025 100644 --- a/quic/conn_async_test.go +++ b/quic/conn_async_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/conn_close.go b/quic/conn_close.go index cd8d7e3c5a..5001ab13f0 100644 --- a/quic/conn_close.go +++ b/quic/conn_close.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/conn_close_test.go b/quic/conn_close_test.go index 2139750119..0b37b3ecfc 100644 --- a/quic/conn_close_test.go +++ b/quic/conn_close_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/conn_flow.go b/quic/conn_flow.go index 8b69ef7dba..1d04f45545 100644 --- a/quic/conn_flow.go +++ b/quic/conn_flow.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/conn_flow_test.go b/quic/conn_flow_test.go index 260684bdbc..52ecf92254 100644 --- a/quic/conn_flow_test.go +++ b/quic/conn_flow_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/conn_id.go b/quic/conn_id.go index 2d50f14fa6..8749e52b79 100644 --- a/quic/conn_id.go +++ b/quic/conn_id.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/conn_id_test.go b/quic/conn_id_test.go index 2c3f170160..c9da0eb090 100644 --- a/quic/conn_id_test.go +++ b/quic/conn_id_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/conn_loss.go b/quic/conn_loss.go index 623ebdd7c6..06761e3f83 100644 --- a/quic/conn_loss.go +++ b/quic/conn_loss.go @@ -2,8 +2,6 @@ // 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 "fmt" diff --git a/quic/conn_loss_test.go b/quic/conn_loss_test.go index 81d537803d..f13ea13d48 100644 --- a/quic/conn_loss_test.go +++ b/quic/conn_loss_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/conn_recv.go b/quic/conn_recv.go index dbfe34a343..a24fc36916 100644 --- a/quic/conn_recv.go +++ b/quic/conn_recv.go @@ -2,8 +2,6 @@ // 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 ( @@ -423,15 +421,10 @@ 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) { - if end > c.loss.nextNumber(space) { - // Acknowledgement of a packet we never sent. - c.abort(now, localTransportError{ - code: errProtocolViolation, - reason: "acknowledgement for unsent packet", - }) + if err := c.loss.receiveAckRange(now, space, rangeIndex, start, end, c.handleAckOrLoss); err != nil { + c.abort(now, err) return } - c.loss.receiveAckRange(now, space, rangeIndex, start, end, c.handleAckOrLoss) }) // Prior to receiving the peer's transport parameters, we cannot // interpret the ACK Delay field because we don't know the ack_delay_exponent diff --git a/quic/conn_recv_test.go b/quic/conn_recv_test.go index 0e94731bf7..1a0eb3a105 100644 --- a/quic/conn_recv_test.go +++ b/quic/conn_recv_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/conn_send.go b/quic/conn_send.go index a87cac232e..d6fb149d9f 100644 --- a/quic/conn_send.go +++ b/quic/conn_send.go @@ -2,8 +2,6 @@ // 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 ( @@ -144,6 +142,10 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } if sent := c.w.finish1RTTPacket(pnum, pnumMaxAcked, dstConnID, &c.keysAppData); sent != nil { c.packetSent(now, appDataSpace, sent) + if c.skip.shouldSkip(pnum + 1) { + c.loss.skipNumber(now, appDataSpace) + c.skip.updateNumberSkip(c) + } } } diff --git a/quic/conn_send_test.go b/quic/conn_send_test.go index 2205ff2f79..c5cf93644c 100644 --- a/quic/conn_send_test.go +++ b/quic/conn_send_test.go @@ -2,8 +2,6 @@ // 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 ( @@ -68,7 +66,11 @@ func TestSendPacketNumberSize(t *testing.T) { // current packet and the max acked one is sufficiently large. for want := maxAcked + 1; want < maxAcked+0x100; want++ { p := recvPing() - if p.num != want { + if p.num == want+1 { + // The conn skipped a packet number + // (defense against optimistic ACK attacks). + want++ + } else if p.num != want { t.Fatalf("received packet number %v, want %v", p.num, want) } gotPnumLen := int(p.header&0x03) + 1 diff --git a/quic/conn_streams.go b/quic/conn_streams.go index 87cfd297ed..bfe80c6dcf 100644 --- a/quic/conn_streams.go +++ b/quic/conn_streams.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/conn_streams_test.go b/quic/conn_streams_test.go index dc81ad9913..af3c1dec8f 100644 --- a/quic/conn_streams_test.go +++ b/quic/conn_streams_test.go @@ -2,8 +2,6 @@ // 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 ( @@ -244,9 +242,7 @@ func TestStreamsWriteQueueFairness(t *testing.T) { if p == nil { break } - tc.writeFrames(packetType1RTT, debugFrameAck{ - ranges: []i64range[packetNumber]{{0, p.num}}, - }) + tc.writeAckForLatest() for _, f := range p.frames { sf, ok := f.(debugFrameStream) if !ok { diff --git a/quic/conn_test.go b/quic/conn_test.go index 51402630fc..4b0511fce6 100644 --- a/quic/conn_test.go +++ b/quic/conn_test.go @@ -2,8 +2,6 @@ // 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 ( @@ -163,6 +161,9 @@ type testConn struct { peerConnID []byte // source conn id of peer's packets peerNextPacketNum [numberSpaceCount]packetNumber // next packet number to use + // Maximum packet number received from the conn. + pnumMax [numberSpaceCount]packetNumber + // Datagrams, packets, and frames sent by the conn, // but not yet processed by the test. sentDatagrams [][]byte @@ -847,6 +848,7 @@ func parseTestDatagram(t *testing.T, te *testEndpoint, tc *testConn, buf []byte) } case packetTypeInitial, packetTypeHandshake: var k fixedKeys + var pnumMax packetNumber if tc == nil { if ptype == packetTypeInitial { p, _ := parseGenericLongHeaderPacket(buf) @@ -858,18 +860,27 @@ func parseTestDatagram(t *testing.T, te *testEndpoint, tc *testConn, buf []byte) switch ptype { case packetTypeInitial: k = tc.keysInitial.r + pnumMax = tc.pnumMax[initialSpace] case packetTypeHandshake: k = tc.keysHandshake.r + pnumMax = tc.pnumMax[handshakeSpace] } } if !k.isSet() { t.Fatalf("reading %v packet with no read key", ptype) } - var pnumMax packetNumber // TODO: Track packet numbers. p, n := parseLongHeaderPacket(buf, k, pnumMax) if n < 0 { t.Fatalf("packet parse error") } + if tc != nil { + switch ptype { + case packetTypeInitial: + tc.pnumMax[initialSpace] = max(pnumMax, p.num) + case packetTypeHandshake: + tc.pnumMax[handshakeSpace] = max(pnumMax, p.num) + } + } frames, err := parseTestFrames(t, p.payload) if err != nil { t.Fatal(err) @@ -893,7 +904,10 @@ func parseTestDatagram(t *testing.T, te *testEndpoint, tc *testConn, buf []byte) if tc == nil || !tc.rkeyAppData.hdr.isSet() { t.Fatalf("reading 1-RTT packet with no read key") } - var pnumMax packetNumber // TODO: Track packet numbers. + var pnumMax packetNumber + if tc != nil { + pnumMax = tc.pnumMax[appDataSpace] + } pnumOff := 1 + len(tc.peerConnID) // Try unprotecting the packet with the first maxTestKeyPhases keys. var phase int @@ -916,6 +930,9 @@ func parseTestDatagram(t *testing.T, te *testEndpoint, tc *testConn, buf []byte) if err != nil { t.Fatalf("1-RTT packet payload parse error") } + if tc != nil { + tc.pnumMax[appDataSpace] = max(pnumMax, pnum) + } frames, err := parseTestFrames(t, pay) if err != nil { t.Fatal(err) diff --git a/quic/crypto_stream.go b/quic/crypto_stream.go index 806c963943..ce73cb54ff 100644 --- a/quic/crypto_stream.go +++ b/quic/crypto_stream.go @@ -2,8 +2,6 @@ // 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 // "Implementations MUST support buffering at least 4096 bytes of data diff --git a/quic/crypto_stream_test.go b/quic/crypto_stream_test.go index 6bee8bb9f6..64b6e556d0 100644 --- a/quic/crypto_stream_test.go +++ b/quic/crypto_stream_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/dgram.go b/quic/dgram.go index 6155893732..cea03694ee 100644 --- a/quic/dgram.go +++ b/quic/dgram.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/endpoint.go b/quic/endpoint.go index b9ababe6b1..1bb901525e 100644 --- a/quic/endpoint.go +++ b/quic/endpoint.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/endpoint_test.go b/quic/endpoint_test.go index dc1c510971..98b8756d1c 100644 --- a/quic/endpoint_test.go +++ b/quic/endpoint_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/errors.go b/quic/errors.go index b805b93c1b..25b2f62864 100644 --- a/quic/errors.go +++ b/quic/errors.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/files_test.go b/quic/files_test.go deleted file mode 100644 index 8113109e75..0000000000 --- a/quic/files_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// 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" - "os" - "strings" - "testing" -) - -// TestFiles checks that every file in this package has a build constraint on Go 1.21. -// -// The QUIC implementation depends on crypto/tls features added in Go 1.21, -// so there's no point in trying to build on anything older. -// -// Drop this test when the x/net go.mod depends on 1.21 or newer. -func TestFiles(t *testing.T) { - f, err := os.Open(".") - if err != nil { - t.Fatal(err) - } - names, err := f.Readdirnames(-1) - if err != nil { - t.Fatal(err) - } - for _, name := range names { - if !strings.HasSuffix(name, ".go") { - continue - } - b, err := os.ReadFile(name) - if err != nil { - t.Fatal(err) - } - // Check for copyright header while we're in here. - if !bytes.Contains(b, []byte("The Go Authors.")) { - t.Errorf("%v: missing copyright", name) - } - // doc.go doesn't need a build constraint. - if name == "doc.go" { - continue - } - if !bytes.Contains(b, []byte("//go:build go1.21")) { - t.Errorf("%v: missing constraint on go1.21", name) - } - } -} diff --git a/quic/frame_debug.go b/quic/frame_debug.go index 17234dd7cd..7cf03faf5b 100644 --- a/quic/frame_debug.go +++ b/quic/frame_debug.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/gate.go b/quic/gate.go index 8f1db2be66..1f570bb906 100644 --- a/quic/gate.go +++ b/quic/gate.go @@ -2,8 +2,6 @@ // 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" diff --git a/quic/gate_test.go b/quic/gate_test.go index 9e84a84bd6..54f7a8a4ac 100644 --- a/quic/gate_test.go +++ b/quic/gate_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/gotraceback_test.go b/quic/gotraceback_test.go index c22702faa4..f1f49b6bdf 100644 --- a/quic/gotraceback_test.go +++ b/quic/gotraceback_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.21 && unix +//go:build unix package quic diff --git a/quic/idle.go b/quic/idle.go index f5b2422adb..6b1dfd1d25 100644 --- a/quic/idle.go +++ b/quic/idle.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/idle_test.go b/quic/idle_test.go index 18f6a690a4..29d3bd1418 100644 --- a/quic/idle_test.go +++ b/quic/idle_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/key_update_test.go b/quic/key_update_test.go index 4a4d677713..2daf7db97f 100644 --- a/quic/key_update_test.go +++ b/quic/key_update_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/log.go b/quic/log.go index d7248343b0..eee2b5fd61 100644 --- a/quic/log.go +++ b/quic/log.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/loss.go b/quic/loss.go index 796b5f7a34..ffbf69ddb7 100644 --- a/quic/loss.go +++ b/quic/loss.go @@ -2,8 +2,6 @@ // 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 ( @@ -180,6 +178,15 @@ func (c *lossState) nextNumber(space numberSpace) packetNumber { return c.spaces[space].nextNum } +// skipPacketNumber skips a packet number as a defense against optimistic ACK attacks. +func (c *lossState) skipNumber(now time.Time, space numberSpace) { + sent := newSentPacket() + sent.num = c.spaces[space].nextNum + sent.time = now + sent.state = sentPacketUnsent + c.spaces[space].add(sent) +} + // packetSent records a sent packet. func (c *lossState) packetSent(now time.Time, log *slog.Logger, space numberSpace, sent *sentPacket) { sent.time = now @@ -232,42 +239,52 @@ func (c *lossState) receiveAckStart() { // receiveAckRange processes a range within an ACK frame. // The ackf function is called for each newly-acknowledged packet. -func (c *lossState) receiveAckRange(now time.Time, space numberSpace, rangeIndex int, start, end packetNumber, ackf func(numberSpace, *sentPacket, packetFate)) { +func (c *lossState) receiveAckRange(now time.Time, space numberSpace, rangeIndex int, start, end packetNumber, ackf func(numberSpace, *sentPacket, packetFate)) error { // Limit our range to the intersection of the ACK range and // the in-flight packets we have state for. if s := c.spaces[space].start(); start < s { start = s } if e := c.spaces[space].end(); end > e { - end = e + return localTransportError{ + code: errProtocolViolation, + reason: "acknowledgement for unsent packet", + } } if start >= end { - return + return nil } if rangeIndex == 0 { // If the latest packet in the ACK frame is newly-acked, // record the RTT in c.ackFrameRTT. sent := c.spaces[space].num(end - 1) - if !sent.acked { + if sent.state == sentPacketSent { c.ackFrameRTT = max(0, now.Sub(sent.time)) } } for pnum := start; pnum < end; pnum++ { sent := c.spaces[space].num(pnum) - if sent.acked || sent.lost { + if sent.state == sentPacketUnsent { + return localTransportError{ + code: errProtocolViolation, + reason: "acknowledgement for unsent packet", + } + } + if sent.state != sentPacketSent { continue } // This is a newly-acknowledged packet. if pnum > c.spaces[space].maxAcked { c.spaces[space].maxAcked = pnum } - sent.acked = true + sent.state = sentPacketAcked c.cc.packetAcked(now, sent) ackf(space, sent, packetAcked) if sent.ackEliciting { c.ackFrameContainsAckEliciting = true } } + return nil } // receiveAckEnd finishes processing an ack frame. @@ -317,7 +334,12 @@ func (c *lossState) receiveAckEnd(now time.Time, log *slog.Logger, space numberS func (c *lossState) discardPackets(space numberSpace, log *slog.Logger, lossf func(numberSpace, *sentPacket, packetFate)) { for i := 0; i < c.spaces[space].size; i++ { sent := c.spaces[space].nth(i) - sent.lost = true + if sent.state != sentPacketSent { + // This should not be possible, since we only discard packets + // in spaces which have never received an ack, but check anyway. + continue + } + sent.state = sentPacketLost c.cc.packetDiscarded(sent) lossf(numberSpace(space), sent, packetLost) } @@ -332,6 +354,9 @@ func (c *lossState) discardKeys(now time.Time, log *slog.Logger, space numberSpa // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.4 for i := 0; i < c.spaces[space].size; i++ { sent := c.spaces[space].nth(i) + if sent.state != sentPacketSent { + continue + } c.cc.packetDiscarded(sent) } c.spaces[space].discard() @@ -356,7 +381,7 @@ func (c *lossState) detectLoss(now time.Time, lossf func(numberSpace, *sentPacke for space := numberSpace(0); space < numberSpaceCount; space++ { for i := 0; i < c.spaces[space].size; i++ { sent := c.spaces[space].nth(i) - if sent.lost || sent.acked { + if sent.state != sentPacketSent { continue } // RFC 9002 Section 6.1 states that a packet is only declared lost if it @@ -372,13 +397,13 @@ func (c *lossState) detectLoss(now time.Time, lossf func(numberSpace, *sentPacke case sent.num <= c.spaces[space].maxAcked && !sent.time.After(lossTime): // Time threshold // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.1.2 - sent.lost = true + sent.state = sentPacketLost lossf(space, sent, packetLost) if sent.inFlight { c.cc.packetLost(now, space, sent, &c.rtt) } } - if !sent.lost { + if sent.state != sentPacketLost { break } } diff --git a/quic/loss_test.go b/quic/loss_test.go index 1fb9662e4c..545f2c414e 100644 --- a/quic/loss_test.go +++ b/quic/loss_test.go @@ -2,8 +2,6 @@ // 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 ( @@ -797,16 +795,38 @@ func TestLossKeysDiscarded(t *testing.T) { // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.4-1 test := newLossTest(t, clientSide, lossTestOpts{}) test.send(initialSpace, 0, testSentPacketSize(1200)) + test.send(initialSpace, 1, testSentPacketSize(1200)) // will be acked + test.send(initialSpace, 2, testSentPacketSize(1200)) + test.ack(initialSpace, 0*time.Millisecond, i64range[packetNumber]{1, 2}) + test.wantAck(initialSpace, 1) test.send(handshakeSpace, 0, testSentPacketSize(600)) - test.wantVar("bytes_in_flight", 1800) + test.send(handshakeSpace, 1, testSentPacketSize(600)) // will be acked + test.send(handshakeSpace, 2, testSentPacketSize(600)) + test.ack(handshakeSpace, 0*time.Millisecond, i64range[packetNumber]{1, 2}) + test.wantAck(handshakeSpace, 1) + test.wantVar("bytes_in_flight", 1200+1200+600+600) // 3600 test.discardKeys(initialSpace) - test.wantVar("bytes_in_flight", 600) + test.wantVar("bytes_in_flight", 600+600) // 1200 test.discardKeys(handshakeSpace) test.wantVar("bytes_in_flight", 0) } +func TestLossDiscardPackets(t *testing.T) { + test := newLossTest(t, clientSide, lossTestOpts{}) + test.send(initialSpace, 0, testSentPacketSize(1200)) + test.send(initialSpace, 1, testSentPacketSize(1200)) // will be acked + test.send(initialSpace, 2, testSentPacketSize(1200)) + test.ack(initialSpace, 0*time.Millisecond, i64range[packetNumber]{1, 2}) + test.wantAck(initialSpace, 1) + + test.discardPackets(initialSpace) + test.wantLoss(initialSpace, 0) + test.wantLoss(initialSpace, 2) + test.wantVar("bytes_in_flight", 0) +} + func TestLossInitialCongestionWindow(t *testing.T) { // "Endpoints SHOULD use an initial congestion window of [...]" // https://www.rfc-editor.org/rfc/rfc9002.html#section-7.2-1 @@ -1494,6 +1514,13 @@ func (c *lossTest) discardKeys(spaceID numberSpace) { c.c.discardKeys(c.now, nil, spaceID) } +func (c *lossTest) discardPackets(spaceID numberSpace) { + c.t.Helper() + c.checkUnexpectedEvents() + c.t.Logf("discard packets in %s", spaceID) + c.c.discardPackets(spaceID, nil, c.onAckOrLoss) +} + func (c *lossTest) setMaxAckDelay(d time.Duration) { c.t.Helper() c.checkUnexpectedEvents() diff --git a/quic/main_test.go b/quic/main_test.go index 25e0096e43..e2c9f3a990 100644 --- a/quic/main_test.go +++ b/quic/main_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/math.go b/quic/math.go index f9dd7545a9..d1e8a80025 100644 --- a/quic/math.go +++ b/quic/math.go @@ -2,8 +2,6 @@ // 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 func abs[T ~int | ~int64](a T) T { diff --git a/quic/pacer.go b/quic/pacer.go index bcba769361..5891f42597 100644 --- a/quic/pacer.go +++ b/quic/pacer.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/pacer_test.go b/quic/pacer_test.go index 9c69da0381..e776369437 100644 --- a/quic/pacer_test.go +++ b/quic/pacer_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/packet.go b/quic/packet.go index 883754f021..b9fa333c54 100644 --- a/quic/packet.go +++ b/quic/packet.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/packet_codec_test.go b/quic/packet_codec_test.go index 2a2b08f4e3..be335d7fdf 100644 --- a/quic/packet_codec_test.go +++ b/quic/packet_codec_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/packet_number.go b/quic/packet_number.go index 206053e581..9e9f0ad003 100644 --- a/quic/packet_number.go +++ b/quic/packet_number.go @@ -2,8 +2,6 @@ // 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 // A packetNumber is a QUIC packet number. diff --git a/quic/packet_number_test.go b/quic/packet_number_test.go index 4d8516ae6c..7450e39881 100644 --- a/quic/packet_number_test.go +++ b/quic/packet_number_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/packet_parser.go b/quic/packet_parser.go index dca3018086..eadf14fd18 100644 --- a/quic/packet_parser.go +++ b/quic/packet_parser.go @@ -2,8 +2,6 @@ // 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 "golang.org/x/net/internal/quic/quicwire" diff --git a/quic/packet_protection.go b/quic/packet_protection.go index 9f1bbc6a4a..7856d6b5d8 100644 --- a/quic/packet_protection.go +++ b/quic/packet_protection.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/packet_protection_test.go b/quic/packet_protection_test.go index 1fe1307311..60eb2313c4 100644 --- a/quic/packet_protection_test.go +++ b/quic/packet_protection_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/packet_test.go b/quic/packet_test.go index 58c584e162..7f6007f033 100644 --- a/quic/packet_test.go +++ b/quic/packet_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/packet_writer.go b/quic/packet_writer.go index e75edcda5b..3560ebbe4d 100644 --- a/quic/packet_writer.go +++ b/quic/packet_writer.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/path.go b/quic/path.go index 8c237dd45f..5170562c74 100644 --- a/quic/path.go +++ b/quic/path.go @@ -2,8 +2,6 @@ // 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" diff --git a/quic/path_test.go b/quic/path_test.go index a309ed14ba..60ff51e35d 100644 --- a/quic/path_test.go +++ b/quic/path_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/ping.go b/quic/ping.go index 3e7d9c51bd..e604f014bf 100644 --- a/quic/ping.go +++ b/quic/ping.go @@ -2,8 +2,6 @@ // 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" diff --git a/quic/ping_test.go b/quic/ping_test.go index a8fdf2567e..a8e6b61ada 100644 --- a/quic/ping_test.go +++ b/quic/ping_test.go @@ -2,8 +2,6 @@ // 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" diff --git a/quic/pipe.go b/quic/pipe.go index 75cf76db21..2ae651ea3e 100644 --- a/quic/pipe.go +++ b/quic/pipe.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/pipe_test.go b/quic/pipe_test.go index bcb3a8bc05..e0aa8f33ea 100644 --- a/quic/pipe_test.go +++ b/quic/pipe_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/qlog.go b/quic/qlog.go index 36831252c6..5d2fd0fc1e 100644 --- a/quic/qlog.go +++ b/quic/qlog.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/qlog_test.go b/quic/qlog_test.go index c0b5cd170f..08c2a77a81 100644 --- a/quic/qlog_test.go +++ b/quic/qlog_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/queue.go b/quic/queue.go index 7085e578b6..8b90ae7773 100644 --- a/quic/queue.go +++ b/quic/queue.go @@ -2,8 +2,6 @@ // 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" diff --git a/quic/queue_test.go b/quic/queue_test.go index d78216b0ec..eee34e5ba7 100644 --- a/quic/queue_test.go +++ b/quic/queue_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/quic.go b/quic/quic.go index 3e62d7cd94..26256bf422 100644 --- a/quic/quic.go +++ b/quic/quic.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/quic_test.go b/quic/quic_test.go index 1281b54eec..071003e963 100644 --- a/quic/quic_test.go +++ b/quic/quic_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/rangeset.go b/quic/rangeset.go index 528d53df39..3d6f5f9799 100644 --- a/quic/rangeset.go +++ b/quic/rangeset.go @@ -2,8 +2,6 @@ // 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 // A rangeset is a set of int64s, stored as an ordered list of non-overlapping, diff --git a/quic/rangeset_test.go b/quic/rangeset_test.go index 2027f14b88..0204b7a78f 100644 --- a/quic/rangeset_test.go +++ b/quic/rangeset_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/retry.go b/quic/retry.go index 8c56ee1b10..d70b254ba2 100644 --- a/quic/retry.go +++ b/quic/retry.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/retry_test.go b/quic/retry_test.go index c898ad331d..d6f025472e 100644 --- a/quic/retry_test.go +++ b/quic/retry_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/rtt.go b/quic/rtt.go index 494060c67d..0dc9bf5bf2 100644 --- a/quic/rtt.go +++ b/quic/rtt.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/rtt_test.go b/quic/rtt_test.go index 2f20b36298..e3185e2f02 100644 --- a/quic/rtt_test.go +++ b/quic/rtt_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/sent_packet.go b/quic/sent_packet.go index eedd2f61b3..f67606b353 100644 --- a/quic/sent_packet.go +++ b/quic/sent_packet.go @@ -2,8 +2,6 @@ // 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 ( @@ -21,10 +19,9 @@ type sentPacket struct { time time.Time // time sent ptype packetType + state sentPacketState ackEliciting bool // https://www.rfc-editor.org/rfc/rfc9002.html#section-2-3.4.1 inFlight bool // https://www.rfc-editor.org/rfc/rfc9002.html#section-2-3.6.1 - acked bool // ack has been received - lost bool // packet is presumed lost // Frames sent in the packet. // @@ -38,6 +35,15 @@ type sentPacket struct { n int // read offset into b } +type sentPacketState uint8 + +const ( + sentPacketSent = sentPacketState(iota) // sent but neither acked nor lost + sentPacketAcked // acked + sentPacketLost // declared lost + sentPacketUnsent // never sent +) + var sentPool = sync.Pool{ New: func() any { return &sentPacket{} diff --git a/quic/sent_packet_list.go b/quic/sent_packet_list.go index 6fb712a7a2..04116f2109 100644 --- a/quic/sent_packet_list.go +++ b/quic/sent_packet_list.go @@ -2,8 +2,6 @@ // 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 // A sentPacketList is a ring buffer of sentPackets. @@ -69,7 +67,7 @@ func (s *sentPacketList) num(num packetNumber) *sentPacket { func (s *sentPacketList) clean() { for s.size > 0 { sent := s.p[s.off] - if !sent.acked && !sent.lost { + if sent.state == sentPacketSent { return } sent.recycle() diff --git a/quic/sent_packet_list_test.go b/quic/sent_packet_list_test.go index 2f7f4d2c64..44c99c536d 100644 --- a/quic/sent_packet_list_test.go +++ b/quic/sent_packet_list_test.go @@ -2,8 +2,6 @@ // 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" @@ -28,7 +26,7 @@ func TestSentPacketListSlidingWindow(t *testing.T) { if got := list.nth(0); got != sent { t.Fatalf("list.nth(0) != list.num(%v)", prev) } - sent.acked = true + sent.state = sentPacketAcked list.clean() if got := list.num(prev); got != nil { t.Fatalf("list.num(%v) = packet %v, expected it to be discarded", prev, got.num) @@ -84,7 +82,7 @@ func TestSentPacketListCleanAll(t *testing.T) { } // Mark all the packets as acked. for i := packetNumber(0); i < count; i++ { - list.num(i).acked = true + list.num(i).state = sentPacketAcked } list.clean() if got, want := list.size, 0; got != want { diff --git a/quic/sent_packet_test.go b/quic/sent_packet_test.go index c0b04e6769..f0962b35fe 100644 --- a/quic/sent_packet_test.go +++ b/quic/sent_packet_test.go @@ -2,8 +2,6 @@ // 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" diff --git a/quic/sent_val.go b/quic/sent_val.go index 920658919b..f1682dbd78 100644 --- a/quic/sent_val.go +++ b/quic/sent_val.go @@ -2,8 +2,6 @@ // 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 // A sentVal tracks sending some piece of information to the peer. diff --git a/quic/sent_val_test.go b/quic/sent_val_test.go index d253d3a8de..774a154e7e 100644 --- a/quic/sent_val_test.go +++ b/quic/sent_val_test.go @@ -2,8 +2,6 @@ // 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" diff --git a/quic/skip.go b/quic/skip.go new file mode 100644 index 0000000000..f5ba764f8a --- /dev/null +++ b/quic/skip.go @@ -0,0 +1,62 @@ +// Copyright 2025 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. + +package quic + +// skipState is state for optimistic ACK defenses. +// +// An endpoint performs an optimistic ACK attack by sending acknowledgements for packets +// which it has not received, potentially convincing the sender's congestion controller to +// send at rates beyond what the network supports. +// +// We defend against this by periodically skipping packet numbers. +// Receiving an ACK for an unsent packet number is a PROTOCOL_VIOLATION error. +// +// We only skip packet numbers in the Application Data number space. +// The total data sent in the Initial/Handshake spaces should generally fit into +// the initial congestion window. +// +// https://www.rfc-editor.org/rfc/rfc9000.html#section-21.4 +type skipState struct { + // skip is the next packet number (in the Application Data space) we should skip. + skip packetNumber + + // maxSkip is the maximum number of packets to send before skipping another number. + // Increases over time. + maxSkip int64 +} + +func (ss *skipState) init(c *Conn) { + ss.maxSkip = 256 // skip our first packet number within this range + ss.updateNumberSkip(c) +} + +// shouldSkipAfter returns whether we should skip the given packet number. +func (ss *skipState) shouldSkip(num packetNumber) bool { + return ss.skip == num +} + +// updateNumberSkip schedules a packet to be skipped after skipping lastSkipped. +func (ss *skipState) updateNumberSkip(c *Conn) { + // Send at least this many packets before skipping. + // Limits the impact of skipping a little, + // plus allows most tests to ignore skipping. + const minSkip = 64 + + skip := minSkip + c.prng.Int64N(ss.maxSkip-minSkip) + ss.skip += packetNumber(skip) + + // Double the size of the skip each time until we reach 128k. + // The idea here is that an attacker needs to correctly ack ~N packets in order + // to send an optimistic ack for another ~N packets. + // Skipping packet numbers comes with a small cost (it causes the receiver to + // send an immediate ACK rather than the usual delayed ACK), so we increase the + // time between skips as a connection's lifetime grows. + // + // The 128k cap is arbitrary, chosen so that we skip a packet number + // about once a second when sending full-size datagrams at 1Gbps. + if ss.maxSkip < 128*1024 { + ss.maxSkip *= 2 + } +} diff --git a/quic/skip_test.go b/quic/skip_test.go new file mode 100644 index 0000000000..1fcb735ff1 --- /dev/null +++ b/quic/skip_test.go @@ -0,0 +1,81 @@ +// Copyright 2025 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. + +package quic + +import "testing" + +func TestSkipPackets(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) + connWritesPacket := func() { + s.WriteByte(0) + s.Flush() + tc.wantFrameType("conn sends STREAM data", + packetType1RTT, debugFrameStream{}) + tc.writeAckForLatest() + tc.wantIdle("conn is idle") + } + connWritesPacket() + +expectSkip: + for maxUntilSkip := 256; maxUntilSkip <= 1024; maxUntilSkip *= 2 { + for range maxUntilSkip + 1 { + nextNum := tc.lastPacket.num + 1 + + connWritesPacket() + + if tc.lastPacket.num == nextNum+1 { + // A packet number was skipped, as expected. + continue expectSkip + } + if tc.lastPacket.num != nextNum { + t.Fatalf("got packet number %v, want %v or %v+1", tc.lastPacket.num, nextNum, nextNum) + } + + } + t.Fatalf("no numbers skipped after %v packets", maxUntilSkip) + } +} + +func TestSkipAckForSkippedPacket(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) + + // Cause the connection to send packets until it skips a packet number. + for { + // Cause the connection to send a packet. + last := tc.lastPacket + s.WriteByte(0) + s.Flush() + tc.wantFrameType("conn sends STREAM data", + packetType1RTT, debugFrameStream{}) + + if tc.lastPacket.num > 256 { + t.Fatalf("no numbers skipped after 256 packets") + } + + // Acknowledge everything up to the packet before the one we just received. + // We don't acknowledge the most-recently-received packet, because doing + // so will cause the connection to drop state for the skipped packet number. + // (We only retain state up to the oldest in-flight packet.) + // + // If the conn has skipped a packet number, then this ack will improperly + // acknowledge the unsent packet. + t.Log(tc.lastPacket.num) + tc.writeFrames(tc.lastPacket.ptype, debugFrameAck{ + ranges: []i64range[packetNumber]{{0, tc.lastPacket.num}}, + }) + + if last != nil && tc.lastPacket.num == last.num+2 { + // The connection has skipped a packet number. + break + } + } + + // We wrote an ACK for a skipped packet number. + // The connection should close. + tc.wantFrame("ACK for skipped packet causes CONNECTION_CLOSE", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errProtocolViolation, + }) +} diff --git a/quic/stateless_reset.go b/quic/stateless_reset.go index 53c3ba5399..8907e2e58b 100644 --- a/quic/stateless_reset.go +++ b/quic/stateless_reset.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/stateless_reset_test.go b/quic/stateless_reset_test.go index 9458d2ea9d..33d467a95b 100644 --- a/quic/stateless_reset_test.go +++ b/quic/stateless_reset_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/stream.go b/quic/stream.go index 8068b10acd..b20cfe7fe0 100644 --- a/quic/stream.go +++ b/quic/stream.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/stream_limits.go b/quic/stream_limits.go index 71cc291351..ed31c365d3 100644 --- a/quic/stream_limits.go +++ b/quic/stream_limits.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/stream_limits_test.go b/quic/stream_limits_test.go index 8fed825d74..ad634113b8 100644 --- a/quic/stream_limits_test.go +++ b/quic/stream_limits_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/stream_test.go b/quic/stream_test.go index 2643ae3dba..4119cc1e74 100644 --- a/quic/stream_test.go +++ b/quic/stream_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/tls.go b/quic/tls.go index 89b31842cd..171d5a3138 100644 --- a/quic/tls.go +++ b/quic/tls.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/tls_test.go b/quic/tls_test.go index f4abdda582..21f782eade 100644 --- a/quic/tls_test.go +++ b/quic/tls_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/tlsconfig_test.go b/quic/tlsconfig_test.go index e24cef08ae..b1305ec00f 100644 --- a/quic/tlsconfig_test.go +++ b/quic/tlsconfig_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/transport_params.go b/quic/transport_params.go index 13d1c7c7d5..2734c586de 100644 --- a/quic/transport_params.go +++ b/quic/transport_params.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/transport_params_test.go b/quic/transport_params_test.go index f1961178e8..6f350287ff 100644 --- a/quic/transport_params_test.go +++ b/quic/transport_params_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/udp.go b/quic/udp.go index 0a578286b2..cf23c5ce88 100644 --- a/quic/udp.go +++ b/quic/udp.go @@ -2,8 +2,6 @@ // 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 "net/netip" diff --git a/quic/udp_darwin.go b/quic/udp_darwin.go index 2eb2e9f9f0..a8677cfc30 100644 --- a/quic/udp_darwin.go +++ b/quic/udp_darwin.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.21 && darwin +//go:build darwin package quic diff --git a/quic/udp_linux.go b/quic/udp_linux.go index 6f191ed398..ad0ce9cfd6 100644 --- a/quic/udp_linux.go +++ b/quic/udp_linux.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.21 && linux +//go:build linux package quic diff --git a/quic/udp_msg.go b/quic/udp_msg.go index 0b600a2b46..018e281683 100644 --- a/quic/udp_msg.go +++ b/quic/udp_msg.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.21 && !quicbasicnet && (darwin || linux) +//go:build !quicbasicnet && (darwin || linux) package quic diff --git a/quic/udp_other.go b/quic/udp_other.go index 28be6d2006..62a82f724c 100644 --- a/quic/udp_other.go +++ b/quic/udp_other.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.21 && (quicbasicnet || !(darwin || linux)) +//go:build quicbasicnet || !(darwin || linux) package quic diff --git a/quic/udp_packetconn.go b/quic/udp_packetconn.go index 85ce349ff1..2c7e71cf61 100644 --- a/quic/udp_packetconn.go +++ b/quic/udp_packetconn.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/udp_test.go b/quic/udp_test.go index 5c4ba10fcc..a92aa15748 100644 --- a/quic/udp_test.go +++ b/quic/udp_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/quic/version_test.go b/quic/version_test.go index 0bd8bac14b..60d83078d3 100644 --- a/quic/version_test.go +++ b/quic/version_test.go @@ -2,8 +2,6 @@ // 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 ( diff --git a/trace/events.go b/trace/events.go index c646a6952e..3aaffdd1f7 100644 --- a/trace/events.go +++ b/trace/events.go @@ -508,7 +508,7 @@ const eventsHTML = `