From a1613c223097a1c9817dd7112dc1cbe7f8245dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Bl=C3=B6cher?= Date: Sun, 23 Apr 2023 16:55:56 +0000 Subject: [PATCH 001/168] http2: handle trailing colon in authorityAddr This change modifies the authorityAddr result for authorities with empty port information, such as "example.com:". Previously, such authorities passed through the function unchanged. This conflicts with the result from net/http's canonicalAddr, which returns "example.com:443" (for HTTPS). net/http's canonicalAddr result is passed to http2's upgradeFn (defined inside http2.configureTransports) from net/http's (*Transport).dialConn. The connection is then added to http2's cache under the canonicalAddr key. However, cache lookups are performed in (*Transport).RoundTripOpt using the result from authorityAddr applied directly to the input URL. The lookup thus fails if authorityAddr and canonicalAddr don't agree. http2's lookup error propagates upwards to net/http's (*Transport).roundTrip, where the request is retried. The end result is an infinite loop of the request being repeated, each time with a freshly dialed connection, that can only be stopped by a timeout. Aligning the results of http2's authorityAddr and net/http's canonicalAddr fixes the bug. While an authority with a trailing colon is invalid per URL specifications, I have personally come across misconfigured web servers emitting such URLs as redirects. This is how I discovered this issue in http2. Change-Id: If47aa61b8d256d76a3451090076e6eb5ff596c9e GitHub-Last-Rev: cb0470115705139cfc60a3d27ec432363fd54a1c GitHub-Pull-Request: golang/net#170 Reviewed-on: https://go-review.googlesource.com/c/net/+/487915 Run-TryBot: Damien Neil Reviewed-by: Dmitri Shuralyov TryBot-Result: Gopher Robot Reviewed-by: Damien Neil --- http2/transport.go | 5 ++++- http2/transport_test.go | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/http2/transport.go b/http2/transport.go index b9632380e7..b20c749171 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -518,11 +518,14 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { func authorityAddr(scheme string, authority string) (addr string) { host, port, err := net.SplitHostPort(authority) if err != nil { // authority didn't have a port + host = authority + port = "" + } + if port == "" { // authority's port was empty port = "443" if scheme == "http" { port = "80" } - host = authority } if a, err := idna.ToASCII(host); err == nil { host = a diff --git a/http2/transport_test.go b/http2/transport_test.go index d3156208cf..99848485b9 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -4456,11 +4456,14 @@ func TestAuthorityAddr(t *testing.T) { }{ {"http", "foo.com", "foo.com:80"}, {"https", "foo.com", "foo.com:443"}, + {"https", "foo.com:", "foo.com:443"}, {"https", "foo.com:1234", "foo.com:1234"}, {"https", "1.2.3.4:1234", "1.2.3.4:1234"}, {"https", "1.2.3.4", "1.2.3.4:443"}, + {"https", "1.2.3.4:", "1.2.3.4:443"}, {"https", "[::1]:1234", "[::1]:1234"}, {"https", "[::1]", "[::1]:443"}, + {"https", "[::1]:", "[::1]:443"}, } for _, tt := range tests { got := authorityAddr(tt.scheme, tt.authority) From 81261084d015c2928e689ffda4e7c246f562fbf9 Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Wed, 5 Jul 2023 08:45:45 +0000 Subject: [PATCH 002/168] dns/dnsmessage: update Parser docs The current API returns ErrSectionDone, not (nil,nil). Change-Id: I95c721c6c198e7302b9154bc39617b502e3d62f9 GitHub-Last-Rev: c66bcff3b11bac48439712a2a6867857d26fb865 GitHub-Pull-Request: golang/net#181 Reviewed-on: https://go-review.googlesource.com/c/net/+/507955 Run-TryBot: Ian Lance Taylor Auto-Submit: Ian Lance Taylor Reviewed-by: Ian Lance Taylor TryBot-Result: Gopher Robot Reviewed-by: Damien Neil --- dns/dnsmessage/message.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index 1577d4a19d..37da3de4d3 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -527,12 +527,14 @@ func (r *Resource) pack(msg []byte, compression map[string]int, compressionOff i // When parsing is started, the Header is parsed. Next, each Question can be // either parsed or skipped. Alternatively, all Questions can be skipped at // once. When all Questions have been parsed, attempting to parse Questions -// will return (nil, nil) and attempting to skip Questions will return -// (true, nil). After all Questions have been either parsed or skipped, all +// will return the [ErrSectionDone] error. +// After all Questions have been either parsed or skipped, all // Answers, Authorities and Additionals can be either parsed or skipped in the // same way, and each type of Resource must be fully parsed or skipped before // proceeding to the next type of Resource. // +// Parser is safe to copy to preserve the parsing state. +// // Note that there is no requirement to fully skip or parse the message. type Parser struct { msg []byte From d8f9c0143e94e55c0e871e302e81cf982732df30 Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Fri, 2 Jun 2023 11:59:55 +0000 Subject: [PATCH 003/168] dns/dnsmessage: add fuzz test After CL 443215 pack(unpack(msg)) should never fail, so we can add a fuzz test to prove that. Change-Id: Ia2abfc30e2b2a492b4dd5de6ca6f29d2324bd737 GitHub-Last-Rev: 1d9812a34c3295730951535bd79917f5bb2c187e GitHub-Pull-Request: golang/net#177 Reviewed-on: https://go-review.googlesource.com/c/net/+/500296 Auto-Submit: Ian Lance Taylor Reviewed-by: Joedian Reid TryBot-Result: Gopher Robot Reviewed-by: Damien Neil Reviewed-by: Ian Lance Taylor Run-TryBot: Ian Lance Taylor Run-TryBot: Mateusz Poliwczak --- dns/dnsmessage/message_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index ce2716e42d..64c6db86d1 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -1643,3 +1643,31 @@ func TestNoFmt(t *testing.T) { } } } + +func FuzzUnpackPack(f *testing.F) { + for _, msg := range []Message{smallTestMsg(), largeTestMsg()} { + bytes, _ := msg.Pack() + f.Add(bytes) + } + + f.Fuzz(func(t *testing.T, msg []byte) { + var m Message + if err := m.Unpack(msg); err != nil { + return + } + + msgPacked, err := m.Pack() + if err != nil { + t.Fatalf("failed to pack message that was succesfully unpacked: %v", err) + } + + var m2 Message + if err := m2.Unpack(msgPacked); err != nil { + t.Fatalf("failed to unpack message that was succesfully packed: %v", err) + } + + if !reflect.DeepEqual(m, m2) { + t.Fatal("unpack(msg) is not deep equal to unpack(pack(unpack(msg)))") + } + }) +} From 9475ce144dec10136752baa1aa72dae6b96b4ece Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 23 Jun 2023 10:42:34 -0700 Subject: [PATCH 004/168] quic: fix typos in comments For golang/go#58547 Change-Id: I79f06d22fc010bf2e339df47abed3df170d18339 Reviewed-on: https://go-review.googlesource.com/c/net/+/506075 Reviewed-by: Ian Lance Taylor TryBot-Result: Gopher Robot Run-TryBot: Damien Neil --- internal/quic/errors.go | 2 +- internal/quic/sent_val.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/quic/errors.go b/internal/quic/errors.go index a9ebbe4b74..55d32f3106 100644 --- a/internal/quic/errors.go +++ b/internal/quic/errors.go @@ -10,7 +10,7 @@ import ( "fmt" ) -// A transportError is an transport error code from RFC 9000 Section 20.1. +// A transportError is a transport error code from RFC 9000 Section 20.1. // // The transportError type doesn't implement the error interface to ensure we always // distinguish between errors sent to and received from the peer. diff --git a/internal/quic/sent_val.go b/internal/quic/sent_val.go index b33d8b00f2..31f69e47d0 100644 --- a/internal/quic/sent_val.go +++ b/internal/quic/sent_val.go @@ -37,7 +37,7 @@ func (s sentVal) isSet() bool { return s != 0 } // shouldSend reports whether the value is set and has not been sent to the peer. func (s sentVal) shouldSend() bool { return s.state() == sentValUnsent } -// shouldSend reports whether the the value needs to be sent to the peer. +// shouldSend reports whether the value needs to be sent to the peer. // The value needs to be sent if it is set and has not been sent. // If pto is true, indicating that we are sending a PTO probe, the value // should also be sent if it is set and has not been acknowledged. From 304cc91b19ae873219f3d0807c8533267629cf2e Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 14 Oct 2022 10:19:03 -0700 Subject: [PATCH 005/168] 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 Reviewed-by: Jonathan Amsterdam TryBot-Result: Gopher Robot --- internal/quic/acks.go | 184 ++++++++++++++++++++++++ internal/quic/acks_test.go | 248 +++++++++++++++++++++++++++++++++ internal/quic/rangeset.go | 5 + internal/quic/rangeset_test.go | 20 +++ 4 files changed, 457 insertions(+) create mode 100644 internal/quic/acks.go create mode 100644 internal/quic/acks_test.go diff --git a/internal/quic/acks.go b/internal/quic/acks.go new file mode 100644 index 0000000000..ba860efb2b --- /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 0000000000..4f1032910f --- /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 5339c5ac51..4966a99d2c 100644 --- a/internal/quic/rangeset.go +++ b/internal/quic/rangeset.go @@ -154,6 +154,11 @@ func (s rangeset[T]) end() T { 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 308046905a..2027f14b88 100644 --- a/internal/quic/rangeset_test.go +++ b/internal/quic/rangeset_test.go @@ -295,3 +295,23 @@ func TestRangesetIsRange(t *testing.T) { } } } + +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) + } + } +} From 57553cbff16307d5178b250ad301e7b466f9d969 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 13 Oct 2022 12:09:20 -0700 Subject: [PATCH 006/168] quic: connection ids Each side of a QUIC connection chooses the connection IDs used by its peer. In our case, we use 8-byte random IDs. A connection has a list of connection IDs that it may receive packets on, and a list that it may send packets to. Add a minimal data structure for tracking these lists, and handling of the connection IDs tracked across Initial and Handshake packets. This does not yet handle post-handshake connection ID changes made in NEW_CONNECTION_ID and RETIRE_CONNECTION_ID frames. RFC 9000, Section 5.1. For golang/go#58547 Change-Id: I3e059393cacafbcea04a1b4131c0c7dc28acad5e Reviewed-on: https://go-review.googlesource.com/c/net/+/506675 Run-TryBot: Damien Neil Reviewed-by: Jonathan Amsterdam TryBot-Result: Gopher Robot --- internal/quic/conn_id.go | 147 ++++++++++++++++++++++++++++++++++ internal/quic/conn_id_test.go | 109 +++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 internal/quic/conn_id.go create mode 100644 internal/quic/conn_id_test.go diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go new file mode 100644 index 0000000000..deea70d326 --- /dev/null +++ b/internal/quic/conn_id.go @@ -0,0 +1,147 @@ +// 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 ( + "crypto/rand" +) + +// connIDState is a conn's connection IDs. +type connIDState struct { + // The destination connection IDs of packets we receive are local. + // The destination connection IDs of packets we send are remote. + // + // Local IDs are usually issued by us, and remote IDs by the peer. + // The exception is the transient destination connection ID sent in + // a client's Initial packets, which is chosen by the client. + local []connID + remote []connID +} + +// A connID is a connection ID and associated metadata. +type connID struct { + // cid is the connection ID itself. + cid []byte + + // seq is the connection ID's sequence number: + // https://www.rfc-editor.org/rfc/rfc9000.html#section-5.1.1-1 + // + // For the transient destination ID in a client's Initial packet, this is -1. + seq int64 +} + +func (s *connIDState) initClient(newID newConnIDFunc) error { + // Client chooses its initial connection ID, and sends it + // in the Source Connection ID field of the first Initial packet. + locid, err := newID() + if err != nil { + return err + } + s.local = append(s.local, connID{ + seq: 0, + cid: locid, + }) + + // Client chooses an initial, transient connection ID for the server, + // and sends it in the Destination Connection ID field of the first Initial packet. + remid, err := newID() + if err != nil { + return err + } + s.remote = append(s.remote, connID{ + seq: -1, + cid: remid, + }) + return nil +} + +func (s *connIDState) initServer(newID newConnIDFunc, dstConnID []byte) error { + // Client-chosen, transient connection ID received in the first Initial packet. + // The server will not use this as the Source Connection ID of packets it sends, + // but remembers it because it may receive packets sent to this destination. + s.local = append(s.local, connID{ + seq: -1, + cid: cloneBytes(dstConnID), + }) + + // Server chooses a connection ID, and sends it in the Source Connection ID of + // the response to the clent. + locid, err := newID() + if err != nil { + return err + } + s.local = append(s.local, connID{ + seq: 0, + cid: locid, + }) + return nil +} + +// srcConnID is the Source Connection ID to use in a sent packet. +func (s *connIDState) srcConnID() []byte { + if s.local[0].seq == -1 && len(s.local) > 1 { + // Don't use the transient connection ID if another is available. + return s.local[1].cid + } + return s.local[0].cid +} + +// dstConnID is the Destination Connection ID to use in a sent packet. +func (s *connIDState) dstConnID() []byte { + return s.remote[0].cid +} + +// handlePacket updates the connection ID state during the handshake +// (Initial and Handshake packets). +func (s *connIDState) handlePacket(side connSide, ptype packetType, srcConnID []byte) { + switch { + case ptype == packetTypeInitial && side == clientSide: + if len(s.remote) == 1 && s.remote[0].seq == -1 { + // We're a client connection processing the first Initial packet + // from the server. Replace the transient remote connection ID + // with the Source Connection ID from the packet. + s.remote[0] = connID{ + seq: 0, + cid: cloneBytes(srcConnID), + } + } + case ptype == packetTypeInitial && side == serverSide: + if len(s.remote) == 0 { + // We're a server connection processing the first Initial packet + // from the client. Set the client's connection ID. + s.remote = append(s.remote, connID{ + seq: 0, + cid: cloneBytes(srcConnID), + }) + } + case ptype == packetTypeHandshake && side == serverSide: + if len(s.local) > 0 && s.local[0].seq == -1 { + // We're a server connection processing the first Handshake packet from + // the client. Discard the transient, client-chosen connection ID used + // for Initial packets; the client will never send it again. + s.local = append(s.local[:0], s.local[1:]...) + } + } +} + +func cloneBytes(b []byte) []byte { + n := make([]byte, len(b)) + copy(n, b) + return n +} + +type newConnIDFunc func() ([]byte, error) + +func newRandomConnID() ([]byte, error) { + // It is not necessary for connection IDs to be cryptographically secure, + // but it doesn't hurt. + id := make([]byte, connIDLen) + if _, err := rand.Read(id); err != nil { + return nil, err + } + return id, nil +} diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go new file mode 100644 index 0000000000..7c31e9d560 --- /dev/null +++ b/internal/quic/conn_id_test.go @@ -0,0 +1,109 @@ +// 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 ( + "fmt" + "reflect" + "testing" +) + +func TestConnIDClientHandshake(t *testing.T) { + // On initialization, the client chooses local and remote IDs. + // + // The order in which we allocate the two isn't actually important, + // but test is a lot simpler if we assume. + var s connIDState + s.initClient(newConnIDSequence()) + if got, want := string(s.srcConnID()), "local-1"; got != want { + t.Errorf("after initClient: srcConnID = %q, want %q", got, want) + } + if got, want := string(s.dstConnID()), "local-2"; got != want { + t.Errorf("after initClient: dstConnID = %q, want %q", got, want) + } + + // The server's first Initial packet provides the client with a + // non-transient remote connection ID. + s.handlePacket(clientSide, packetTypeInitial, []byte("remote-1")) + if got, want := string(s.dstConnID()), "remote-1"; got != want { + t.Errorf("after receiving Initial: dstConnID = %q, want %q", got, want) + } + + wantLocal := []connID{{ + cid: []byte("local-1"), + seq: 0, + }} + if !reflect.DeepEqual(s.local, wantLocal) { + t.Errorf("local ids: %v, want %v", s.local, wantLocal) + } + wantRemote := []connID{{ + cid: []byte("remote-1"), + seq: 0, + }} + if !reflect.DeepEqual(s.remote, wantRemote) { + t.Errorf("remote ids: %v, want %v", s.remote, wantRemote) + } +} + +func TestConnIDServerHandshake(t *testing.T) { + // On initialization, the server is provided with the client-chosen + // transient connection ID, and allocates an ID of its own. + // The Initial packet sets the remote connection ID. + var s connIDState + s.initServer(newConnIDSequence(), []byte("transient")) + s.handlePacket(serverSide, packetTypeInitial, []byte("remote-1")) + if got, want := string(s.srcConnID()), "local-1"; got != want { + t.Errorf("after initClient: srcConnID = %q, want %q", got, want) + } + if got, want := string(s.dstConnID()), "remote-1"; got != want { + t.Errorf("after initClient: dstConnID = %q, want %q", got, want) + } + + wantLocal := []connID{{ + cid: []byte("transient"), + seq: -1, + }, { + cid: []byte("local-1"), + seq: 0, + }} + if !reflect.DeepEqual(s.local, wantLocal) { + t.Errorf("local ids: %v, want %v", s.local, wantLocal) + } + wantRemote := []connID{{ + cid: []byte("remote-1"), + seq: 0, + }} + if !reflect.DeepEqual(s.remote, wantRemote) { + t.Errorf("remote ids: %v, want %v", s.remote, wantRemote) + } + + // The client's first Handshake packet permits the server to discard the + // transient connection ID. + s.handlePacket(serverSide, packetTypeHandshake, []byte("remote-1")) + wantLocal = []connID{{ + cid: []byte("local-1"), + seq: 0, + }} + if !reflect.DeepEqual(s.local, wantLocal) { + t.Errorf("after handshake local ids: %v, want %v", s.local, wantLocal) + } +} + +func newConnIDSequence() newConnIDFunc { + var n uint64 + return func() ([]byte, error) { + n++ + return []byte(fmt.Sprintf("local-%v", n)), nil + } +} + +func TestNewRandomConnID(t *testing.T) { + cid, err := newRandomConnID() + if len(cid) != connIDLen || err != nil { + t.Fatalf("newConnID() = %x, %v; want %v bytes", cid, connIDLen, err) + } +} From 4a3f925950ab4f8466e4582f84f3a4a8444f0271 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 27 Jun 2023 15:17:02 -0700 Subject: [PATCH 007/168] quic: basic connection event loop Add the Conn type, representing a QUIC connection. A Conn's behavior is driven by an event loop goroutine. This goroutine owns most Conn state. External events (datagrams received, user operations such as writing to streams) send events to the loop goroutine on a message channel. The testConn type, used in tests, wraps a Conn and takes control of its event loop. The testConn permits tests to interact with a Conn synchronously, sending it events, observing the result, and controlling the Conn's view of time passing. Add a very minimal implementation of connection idle timeouts (RFC 9000, Section 10.1) to test the implementation of synthetic time. For golang/go#58547 Change-Id: Ic517e5e7bb019f4a677f892a807ca0417d6e19b1 Reviewed-on: https://go-review.googlesource.com/c/net/+/506678 TryBot-Result: Gopher Robot Run-TryBot: Damien Neil Reviewed-by: Jonathan Amsterdam --- internal/quic/conn.go | 155 ++++++++++++++++++++++++++++++ internal/quic/conn_test.go | 188 +++++++++++++++++++++++++++++++++++++ internal/quic/quic.go | 2 + 3 files changed, 345 insertions(+) create mode 100644 internal/quic/conn.go create mode 100644 internal/quic/conn_test.go diff --git a/internal/quic/conn.go b/internal/quic/conn.go new file mode 100644 index 0000000000..d6dbac1a93 --- /dev/null +++ b/internal/quic/conn.go @@ -0,0 +1,155 @@ +// 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 ( + "errors" + "fmt" + "time" +) + +// A Conn is a QUIC connection. +// +// Multiple goroutines may invoke methods on a Conn simultaneously. +type Conn struct { + msgc chan any + donec chan struct{} // closed when conn loop exits + exited bool // set to make the conn loop exit immediately + + testHooks connTestHooks + + // idleTimeout is the time at which the connection will be closed due to inactivity. + // https://www.rfc-editor.org/rfc/rfc9000#section-10.1 + maxIdleTimeout time.Duration + idleTimeout time.Time +} + +// connTestHooks override conn behavior in tests. +type connTestHooks interface { + nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any) +} + +func newConn(now time.Time, hooks connTestHooks) (*Conn, error) { + c := &Conn{ + donec: make(chan struct{}), + testHooks: hooks, + maxIdleTimeout: defaultMaxIdleTimeout, + idleTimeout: now.Add(defaultMaxIdleTimeout), + } + + // A one-element buffer allows us to wake a Conn's event loop as a + // non-blocking operation. + c.msgc = make(chan any, 1) + + go c.loop(now) + return c, nil +} + +type timerEvent struct{} + +// loop is the connection main loop. +// +// Except where otherwise noted, all connection state is owned by the loop goroutine. +// +// The loop processes messages from c.msgc and timer events. +// Other goroutines may examine or modify conn state by sending the loop funcs to execute. +func (c *Conn) loop(now time.Time) { + defer close(c.donec) + + // The connection timer sends a message to the connection loop on expiry. + // We need to give it an expiry when creating it, so set the initial timeout to + // an arbitrary large value. The timer will be reset before this expires (and it + // isn't a problem if it does anyway). Skip creating the timer in tests which + // take control of the connection message loop. + var timer *time.Timer + var lastTimeout time.Time + hooks := c.testHooks + if hooks == nil { + timer = time.AfterFunc(1*time.Hour, func() { + c.sendMsg(timerEvent{}) + }) + defer timer.Stop() + } + + for !c.exited { + nextTimeout := c.idleTimeout + + var m any + if hooks != nil { + // Tests only: Wait for the test to tell us to continue. + now, m = hooks.nextMessage(c.msgc, nextTimeout) + } else if !nextTimeout.IsZero() && nextTimeout.Before(now) { + // A connection timer has expired. + now = time.Now() + m = timerEvent{} + } else { + // Reschedule the connection timer if necessary + // and wait for the next event. + if !nextTimeout.Equal(lastTimeout) && !nextTimeout.IsZero() { + // Resetting a timer created with time.AfterFunc guarantees + // that the timer will run again. We might generate a spurious + // timer event under some circumstances, but that's okay. + timer.Reset(nextTimeout.Sub(now)) + lastTimeout = nextTimeout + } + m = <-c.msgc + now = time.Now() + } + switch m := m.(type) { + case timerEvent: + // A connection timer has expired. + if !now.Before(c.idleTimeout) { + // "[...] the connection is silently closed and + // its state is discarded [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-10.1-1 + c.exited = true + return + } + case func(time.Time, *Conn): + // Send a func to msgc to run it on the main Conn goroutine + m(now, c) + default: + panic(fmt.Sprintf("quic: unrecognized conn message %T", m)) + } + } +} + +// sendMsg sends a message to the conn's loop. +// It does not wait for the message to be processed. +func (c *Conn) sendMsg(m any) error { + select { + case c.msgc <- m: + case <-c.donec: + return errors.New("quic: connection closed") + } + return nil +} + +// runOnLoop executes a function within the conn's loop goroutine. +func (c *Conn) runOnLoop(f func(now time.Time, c *Conn)) error { + donec := make(chan struct{}) + if err := c.sendMsg(func(now time.Time, c *Conn) { + defer close(donec) + f(now, c) + }); err != nil { + return err + } + select { + case <-donec: + case <-c.donec: + return errors.New("quic: connection closed") + } + return nil +} + +// exit fully terminates a connection immediately. +func (c *Conn) exit() { + c.runOnLoop(func(now time.Time, c *Conn) { + c.exited = true + }) + <-c.donec +} diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go new file mode 100644 index 0000000000..a1709958e1 --- /dev/null +++ b/internal/quic/conn_test.go @@ -0,0 +1,188 @@ +// 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 ( + "math" + "testing" + "time" +) + +func TestConnTestConn(t *testing.T) { + tc := newTestConn(t, serverSide) + if got, want := tc.timeUntilEvent(), defaultMaxIdleTimeout; got != want { + t.Errorf("new conn timeout=%v, want %v (max_idle_timeout)", got, want) + } + + var ranAt time.Time + tc.conn.runOnLoop(func(now time.Time, c *Conn) { + ranAt = now + }) + if !ranAt.Equal(tc.now) { + t.Errorf("func ran on loop at %v, want %v", ranAt, tc.now) + } + tc.wait() + + nextTime := tc.now.Add(defaultMaxIdleTimeout / 2) + tc.advanceTo(nextTime) + tc.conn.runOnLoop(func(now time.Time, c *Conn) { + ranAt = now + }) + if !ranAt.Equal(nextTime) { + t.Errorf("func ran on loop at %v, want %v", ranAt, nextTime) + } + tc.wait() + + tc.advanceToTimer() + if err := tc.conn.sendMsg(nil); err == nil { + t.Errorf("after advancing to idle timeout, sendMsg = nil, want error") + } + if !tc.conn.exited { + t.Errorf("after advancing to idle timeout, exited = false, want true") + } +} + +// A testConn is a Conn whose external interactions (sending and receiving packets, +// setting timers) can be manipulated in tests. +type testConn struct { + t *testing.T + conn *Conn + now time.Time + timer time.Time + timerLastFired time.Time + idlec chan struct{} // only accessed on the conn's loop +} + +// newTestConn creates a Conn for testing. +// +// The Conn's event loop is controlled by the test, +// allowing test code to access Conn state directly +// by first ensuring the loop goroutine is idle. +func newTestConn(t *testing.T, side connSide) *testConn { + t.Helper() + tc := &testConn{ + t: t, + now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + } + t.Cleanup(tc.cleanup) + + conn, err := newConn(tc.now, (*testConnHooks)(tc)) + if err != nil { + tc.t.Fatal(err) + } + tc.conn = conn + + tc.wait() + return tc +} + +// advance causes time to pass. +func (tc *testConn) advance(d time.Duration) { + tc.t.Helper() + tc.advanceTo(tc.now.Add(d)) +} + +// advanceTo sets the current time. +func (tc *testConn) advanceTo(now time.Time) { + tc.t.Helper() + if tc.now.After(now) { + tc.t.Fatalf("time moved backwards: %v -> %v", tc.now, now) + } + tc.now = now + if tc.timer.After(tc.now) { + return + } + tc.conn.sendMsg(timerEvent{}) + tc.wait() +} + +// advanceToTimer sets the current time to the time of the Conn's next timer event. +func (tc *testConn) advanceToTimer() { + if tc.timer.IsZero() { + tc.t.Fatalf("advancing to timer, but timer is not set") + } + tc.advanceTo(tc.timer) +} + +const infiniteDuration = time.Duration(math.MaxInt64) + +// timeUntilEvent returns the amount of time until the next connection event. +func (tc *testConn) timeUntilEvent() time.Duration { + if tc.timer.IsZero() { + return infiniteDuration + } + if tc.timer.Before(tc.now) { + return 0 + } + return tc.timer.Sub(tc.now) +} + +// wait blocks until the conn becomes idle. +// The conn is idle when it is blocked waiting for a packet to arrive or a timer to expire. +// Tests shouldn't need to call wait directly. +// testConn methods that wake the Conn event loop will call wait for them. +func (tc *testConn) wait() { + tc.t.Helper() + idlec := make(chan struct{}) + fail := false + tc.conn.sendMsg(func(now time.Time, c *Conn) { + if tc.idlec != nil { + tc.t.Errorf("testConn.wait called concurrently") + fail = true + close(idlec) + } else { + // nextMessage will close idlec. + tc.idlec = idlec + } + }) + select { + case <-idlec: + case <-tc.conn.donec: + } + if fail { + panic(fail) + } +} + +func (tc *testConn) cleanup() { + if tc.conn == nil { + return + } + tc.conn.exit() +} + +// testConnHooks implements connTestHooks. +type testConnHooks testConn + +// nextMessage is called by the Conn's event loop to request its next event. +func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.Time, m any) { + tc.timer = timer + if !timer.IsZero() && !timer.After(tc.now) { + if timer.Equal(tc.timerLastFired) { + // If the connection timer fires at time T, the Conn should take some + // action to advance the timer into the future. If the Conn reschedules + // the timer for the same time, it isn't making progress and we have a bug. + tc.t.Errorf("connection timer spinning; now=%v timer=%v", tc.now, timer) + } else { + tc.timerLastFired = timer + return tc.now, timerEvent{} + } + } + select { + case m := <-msgc: + return tc.now, m + default: + } + // If the message queue is empty, then the conn is idle. + if tc.idlec != nil { + idlec := tc.idlec + tc.idlec = nil + close(idlec) + } + m = <-msgc + return tc.now, m +} diff --git a/internal/quic/quic.go b/internal/quic/quic.go index 982c6751b7..c69c0b9840 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -19,6 +19,8 @@ const connIDLen = 8 // Local values of various transport parameters. // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2 const ( + defaultMaxIdleTimeout = 30 * time.Second // max_idle_timeout + // The max_udp_payload_size transport parameter is the size of our // network receive buffer. // From 16cc77a3d1797230d5e8bfd5a27fb0979d24faaf Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 31 Jan 2023 09:00:25 -0800 Subject: [PATCH 008/168] quic: print better stacks on SIGQUIT When handling an uncaught SIGQUIT (C-\), the runtime prints stacks with GOTRACEBACK=all. This is more detail than we need or want when debugging a hung test by killing it with C-\. Add a signal handler in tests to print stacks with GOTRACEBACK=all instead. For golang/go#58547 Change-Id: I8b381cec41a645568aa2eb675ca7f936f35e145a Reviewed-on: https://go-review.googlesource.com/c/net/+/509016 Run-TryBot: Damien Neil TryBot-Result: Gopher Robot Reviewed-by: Jonathan Amsterdam --- internal/quic/gotraceback_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 internal/quic/gotraceback_test.go diff --git a/internal/quic/gotraceback_test.go b/internal/quic/gotraceback_test.go new file mode 100644 index 0000000000..c22702faa4 --- /dev/null +++ b/internal/quic/gotraceback_test.go @@ -0,0 +1,26 @@ +// 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 && unix + +package quic + +import ( + "os" + "os/signal" + "runtime/debug" + "syscall" +) + +// When killed with SIGQUIT (C-\), print stacks with GOTRACEBACK=all rather than system, +// to reduce irrelevant noise when debugging hung tests. +func init() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGQUIT) + go func() { + <-ch + debug.SetTraceback("all") + panic("SIGQUIT") + }() +} From 0adcadfb6b87451705307d07906d4cfdc7677584 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 13 Jun 2023 14:02:18 -0700 Subject: [PATCH 009/168] quic: send and receive datagrams Add the ability for Conns to send and receive datagrams. No socket handling yet; this only functions in tests for now. Extend testConn to permit tests to send packets to Conns and observe the packets Conns send. There's a circular dependency here: We can't test Handshake and 1-RTT packets until we have the handshake implemented, but we can't implement the handshake without the ability to send and receive Handshake and 1-RTT packets. This CL adds the ability to send and receive those packets; tests for those paths will follow with the handshake implementation. For golang/go#58547 Change-Id: I4e7f88f5f039baf7e01f68a53639022866786af9 Reviewed-on: https://go-review.googlesource.com/c/net/+/509017 Run-TryBot: Damien Neil Reviewed-by: Jonathan Amsterdam TryBot-Result: Gopher Robot --- internal/quic/conn.go | 90 ++++++- internal/quic/conn_loss.go | 46 ++++ internal/quic/conn_recv.go | 264 +++++++++++++++++++ internal/quic/conn_send.go | 255 +++++++++++++++++++ internal/quic/conn_test.go | 394 ++++++++++++++++++++++++++++- internal/quic/dgram.go | 38 +++ internal/quic/frame_debug.go | 2 +- internal/quic/packet_parser.go | 6 +- internal/quic/packet_protection.go | 4 +- internal/quic/packet_test.go | 17 ++ internal/quic/packet_writer.go | 5 +- internal/quic/ping.go | 16 ++ internal/quic/ping_test.go | 35 +++ internal/quic/quic.go | 4 + internal/quic/sent_packet.go | 2 + internal/quic/tls.go | 23 ++ 16 files changed, 1184 insertions(+), 17 deletions(-) create mode 100644 internal/quic/conn_loss.go create mode 100644 internal/quic/conn_recv.go create mode 100644 internal/quic/conn_send.go create mode 100644 internal/quic/dgram.go create mode 100644 internal/quic/ping.go create mode 100644 internal/quic/ping_test.go create mode 100644 internal/quic/tls.go diff --git a/internal/quic/conn.go b/internal/quic/conn.go index d6dbac1a93..cdf79d607c 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -9,6 +9,7 @@ package quic import ( "errors" "fmt" + "net/netip" "time" ) @@ -16,16 +17,37 @@ import ( // // Multiple goroutines may invoke methods on a Conn simultaneously. type Conn struct { + side connSide + listener connListener + testHooks connTestHooks + peerAddr netip.AddrPort + msgc chan any donec chan struct{} // closed when conn loop exits exited bool // set to make the conn loop exit immediately - testHooks connTestHooks + w packetWriter + acks [numberSpaceCount]ackState // indexed by number space + connIDState connIDState + tlsState tlsState + loss lossState // idleTimeout is the time at which the connection will be closed due to inactivity. // https://www.rfc-editor.org/rfc/rfc9000#section-10.1 maxIdleTimeout time.Duration idleTimeout time.Time + + peerAckDelayExponent int8 // -1 when unknown + + // Tests only: Send a PING in a specific number space. + testSendPingSpace numberSpace + testSendPing sentVal +} + +// The connListener is the Conn's Listener. +// Defined as an interface so we can swap it out in tests. +type connListener interface { + sendDatagram(p []byte, addr netip.AddrPort) error } // connTestHooks override conn behavior in tests. @@ -33,18 +55,41 @@ type connTestHooks interface { nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any) } -func newConn(now time.Time, hooks connTestHooks) (*Conn, error) { +func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, l connListener, hooks connTestHooks) (*Conn, error) { c := &Conn{ - donec: make(chan struct{}), - testHooks: hooks, - maxIdleTimeout: defaultMaxIdleTimeout, - idleTimeout: now.Add(defaultMaxIdleTimeout), + side: side, + listener: l, + peerAddr: peerAddr, + msgc: make(chan any, 1), + donec: make(chan struct{}), + testHooks: hooks, + maxIdleTimeout: defaultMaxIdleTimeout, + idleTimeout: now.Add(defaultMaxIdleTimeout), + peerAckDelayExponent: -1, } // A one-element buffer allows us to wake a Conn's event loop as a // non-blocking operation. c.msgc = make(chan any, 1) + if c.side == clientSide { + if err := c.connIDState.initClient(newRandomConnID); err != nil { + return nil, err + } + initialConnID = c.connIDState.dstConnID() + } else { + if err := c.connIDState.initServer(newRandomConnID, initialConnID); err != nil { + return nil, err + } + } + + // The smallest allowed maximum QUIC datagram size is 1200 bytes. + // TODO: PMTU discovery. + const maxDatagramSize = 1200 + c.loss.init(c.side, maxDatagramSize, now) + + c.tlsState.init(c.side, initialConnID) + go c.loop(now) return c, nil } @@ -76,7 +121,14 @@ func (c *Conn) loop(now time.Time) { } for !c.exited { - nextTimeout := c.idleTimeout + sendTimeout := c.maybeSend(now) // try sending + + // Note that we only need to consider the ack timer for the App Data space, + // since the Initial and Handshake spaces always ack immediately. + nextTimeout := sendTimeout + nextTimeout = firstTime(nextTimeout, c.idleTimeout) + nextTimeout = firstTime(nextTimeout, c.loss.timer) + nextTimeout = firstTime(nextTimeout, c.acks[appDataSpace].nextAck) var m any if hooks != nil { @@ -100,6 +152,9 @@ func (c *Conn) loop(now time.Time) { now = time.Now() } switch m := m.(type) { + case *datagram: + c.handleDatagram(now, m) + m.recycle() case timerEvent: // A connection timer has expired. if !now.Before(c.idleTimeout) { @@ -109,6 +164,7 @@ func (c *Conn) loop(now time.Time) { c.exited = true return } + c.loss.advance(now, c.handleAckOrLoss) case func(time.Time, *Conn): // Send a func to msgc to run it on the main Conn goroutine m(now, c) @@ -146,6 +202,12 @@ func (c *Conn) runOnLoop(f func(now time.Time, c *Conn)) error { return nil } +// abort terminates a connection with an error. +func (c *Conn) abort(now time.Time, err error) { + // TODO: Send CONNECTION_CLOSE frames. + c.exit() +} + // exit fully terminates a connection immediately. func (c *Conn) exit() { c.runOnLoop(func(now time.Time, c *Conn) { @@ -153,3 +215,17 @@ func (c *Conn) exit() { }) <-c.donec } + +// firstTime returns the earliest non-zero time, or zero if both times are zero. +func firstTime(a, b time.Time) time.Time { + switch { + case a.IsZero(): + return b + case b.IsZero(): + return a + case a.Before(b): + return a + default: + return b + } +} diff --git a/internal/quic/conn_loss.go b/internal/quic/conn_loss.go new file mode 100644 index 0000000000..11ed42dbb9 --- /dev/null +++ b/internal/quic/conn_loss.go @@ -0,0 +1,46 @@ +// 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 "fmt" + +// handleAckOrLoss deals with the final fate of a packet we sent: +// Either the peer acknowledges it, or we declare it lost. +// +// In order to handle packet loss, we must retain any information sent to the peer +// until the peer has acknowledged it. +// +// When information is acknowledged, we can discard it. +// +// When information is lost, we mark it for retransmission. +// See RFC 9000, Section 13.3 for a complete list of information which is retransmitted on loss. +// https://www.rfc-editor.org/rfc/rfc9000#section-13.3 +func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetFate) { + // The list of frames in a sent packet is marshaled into a buffer in the sentPacket + // by the packetWriter. Unmarshal that buffer here. This code must be kept in sync with + // packetWriter.append*. + // + // A sent packet meets its fate (acked or lost) only once, so it's okay to consume + // the sentPacket's buffer here. + for !sent.done() { + switch f := sent.next(); f { + default: + panic(fmt.Sprintf("BUG: unhandled lost frame type %x", f)) + case frameTypeAck: + // 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. + // + // Acknowledgement of an ACK frame may allow us to discard information + // about older packets. + largest := packetNumber(sent.nextInt()) + if fate == packetAcked { + c.acks[space].handleAck(largest) + } + } + } +} diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go new file mode 100644 index 0000000000..d5a3b8cb0c --- /dev/null +++ b/internal/quic/conn_recv.go @@ -0,0 +1,264 @@ +// 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" +) + +func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { + buf := dgram.b + c.loss.datagramReceived(now, len(buf)) + for len(buf) > 0 { + var n int + ptype := getPacketType(buf) + switch ptype { + case packetTypeInitial: + if c.side == serverSide && len(dgram.b) < minimumClientInitialDatagramSize { + // Discard client-sent Initial packets in too-short datagrams. + // https://www.rfc-editor.org/rfc/rfc9000#section-14.1-4 + return + } + n = c.handleLongHeader(now, ptype, initialSpace, buf) + case packetTypeHandshake: + n = c.handleLongHeader(now, ptype, handshakeSpace, buf) + case packetType1RTT: + n = c.handle1RTT(now, buf) + default: + return + } + if n <= 0 { + // Invalid data at the end of a datagram is ignored. + break + } + c.idleTimeout = now.Add(c.maxIdleTimeout) + buf = buf[n:] + } +} + +func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpace, buf []byte) int { + if !c.tlsState.rkeys[space].isSet() { + return skipLongHeaderPacket(buf) + } + + pnumMax := c.acks[space].largestSeen() + p, n := parseLongHeaderPacket(buf, c.tlsState.rkeys[space], pnumMax) + if n < 0 { + return -1 + } + if p.reservedBits != 0 { + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2-8.2.1 + c.abort(now, localTransportError(errProtocolViolation)) + return -1 + } + + if !c.acks[space].shouldProcess(p.num) { + return n + } + + c.connIDState.handlePacket(c.side, p.ptype, p.srcConnID) + ackEliciting := c.handleFrames(now, ptype, space, p.payload) + c.acks[space].receive(now, space, p.num, ackEliciting) + if p.ptype == packetTypeHandshake && c.side == serverSide { + c.loss.validateClientAddress() + + // TODO: Discard Initial keys. + // https://www.rfc-editor.org/rfc/rfc9001#section-4.9.1-2 + } + return n +} + +func (c *Conn) handle1RTT(now time.Time, buf []byte) int { + if !c.tlsState.rkeys[appDataSpace].isSet() { + // 1-RTT packets extend to the end of the datagram, + // so skip the remainder of the datagram if we can't parse this. + return len(buf) + } + + pnumMax := c.acks[appDataSpace].largestSeen() + p, n := parse1RTTPacket(buf, c.tlsState.rkeys[appDataSpace], connIDLen, pnumMax) + if n < 0 { + return -1 + } + if p.reservedBits != 0 { + // https://www.rfc-editor.org/rfc/rfc9000#section-17.3.1-4.8.1 + c.abort(now, localTransportError(errProtocolViolation)) + return -1 + } + + if !c.acks[appDataSpace].shouldProcess(p.num) { + return len(buf) + } + + ackEliciting := c.handleFrames(now, packetType1RTT, appDataSpace, p.payload) + c.acks[appDataSpace].receive(now, appDataSpace, p.num, ackEliciting) + return len(buf) +} + +func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, payload []byte) (ackEliciting bool) { + if len(payload) == 0 { + // "An endpoint MUST treat receipt of a packet containing no frames + // as a connection error of type PROTOCOL_VIOLATION." + // https://www.rfc-editor.org/rfc/rfc9000#section-12.4-3 + c.abort(now, localTransportError(errProtocolViolation)) + return false + } + // frameOK verifies that ptype is one of the packets in mask. + frameOK := func(c *Conn, ptype, mask packetType) (ok bool) { + if ptype&mask == 0 { + // "An endpoint MUST treat receipt of a frame in a packet type + // that is not permitted as a connection error of type + // PROTOCOL_VIOLATION." + // https://www.rfc-editor.org/rfc/rfc9000#section-12.4-3 + c.abort(now, localTransportError(errProtocolViolation)) + return false + } + return true + } + // Packet masks from RFC 9000 Table 3. + // https://www.rfc-editor.org/rfc/rfc9000#table-3 + const ( + IH_1 = packetTypeInitial | packetTypeHandshake | packetType1RTT + __01 = packetType0RTT | packetType1RTT + ___1 = packetType1RTT + ) + for len(payload) > 0 { + switch payload[0] { + case frameTypePadding, frameTypeAck, frameTypeAckECN, + frameTypeConnectionCloseTransport, frameTypeConnectionCloseApplication: + default: + ackEliciting = true + } + n := -1 + switch payload[0] { + case frameTypePadding: + // PADDING is OK in all spaces. + n = 1 + case frameTypePing: + // PING is OK in all spaces. + // + // A PING frame causes us to respond with an ACK by virtue of being + // an ack-eliciting frame, but requires no other action. + n = 1 + case frameTypeAck, frameTypeAckECN: + if !frameOK(c, ptype, IH_1) { + return + } + n = c.handleAckFrame(now, space, payload) + case frameTypeResetStream: + if !frameOK(c, ptype, __01) { + return + } + _, _, _, n = consumeResetStreamFrame(payload) + case frameTypeStopSending: + if !frameOK(c, ptype, __01) { + return + } + _, _, n = consumeStopSendingFrame(payload) + case frameTypeCrypto: + if !frameOK(c, ptype, IH_1) { + return + } + _, _, n = consumeCryptoFrame(payload) + case frameTypeNewToken: + if !frameOK(c, ptype, ___1) { + return + } + _, n = consumeNewTokenFrame(payload) + case 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f: // STREAM + if !frameOK(c, ptype, __01) { + return + } + _, _, _, _, n = consumeStreamFrame(payload) + case frameTypeMaxData: + if !frameOK(c, ptype, __01) { + return + } + _, n = consumeMaxDataFrame(payload) + case frameTypeMaxStreamData: + if !frameOK(c, ptype, __01) { + return + } + _, _, n = consumeMaxStreamDataFrame(payload) + case frameTypeMaxStreamsBidi, frameTypeMaxStreamsUni: + if !frameOK(c, ptype, __01) { + return + } + _, _, n = consumeMaxStreamsFrame(payload) + case frameTypeStreamsBlockedBidi, frameTypeStreamsBlockedUni: + if !frameOK(c, ptype, __01) { + return + } + _, _, n = consumeStreamsBlockedFrame(payload) + case frameTypeStreamDataBlocked: + if !frameOK(c, ptype, __01) { + return + } + _, _, n = consumeStreamDataBlockedFrame(payload) + case frameTypeNewConnectionID: + if !frameOK(c, ptype, __01) { + return + } + _, _, _, _, n = consumeNewConnectionIDFrame(payload) + case frameTypeConnectionCloseTransport: + // CONNECTION_CLOSE is OK in all spaces. + _, _, _, n = consumeConnectionCloseTransportFrame(payload) + case frameTypeConnectionCloseApplication: + // CONNECTION_CLOSE is OK in all spaces. + _, _, n = consumeConnectionCloseApplicationFrame(payload) + case frameTypeHandshakeDone: + if !frameOK(c, ptype, ___1) { + return + } + n = 1 + } + if n < 0 { + c.abort(now, localTransportError(errFrameEncoding)) + return false + } + payload = payload[n:] + } + return ackEliciting +} + +func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) int { + c.loss.receiveAckStart() + _, 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(errProtocolViolation)) + 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 + // to apply. + // + // For servers, we should always know the ack_delay_exponent because the + // client's transport parameters are carried in its Initial packets and we + // won't send an ack-eliciting Initial packet until after receiving the last + // client Initial packet. + // + // For clients, we won't receive the server's transport parameters until handling + // its Handshake flight, which will probably happen after reading its ACK for our + // Initial packet(s). However, the peer's acknowledgement delay cannot reduce our + // adjusted RTT sample below min_rtt, and min_rtt is generally going to be set + // by the packet containing the ACK for our Initial flight. Therefore, the + // ACK Delay for an ACK in the Initial space is likely to be ignored anyway. + // + // Long story short, setting the delay to 0 prior to reading transport parameters + // is usually going to have no effect, will have only a minor effect in the rare + // cases when it happens, and there aren't any good alternatives anyway since we + // can't interpret the ACK Delay field without knowing the exponent. + var delay time.Duration + if c.peerAckDelayExponent >= 0 { + delay = ackDelay.Duration(uint8(c.peerAckDelayExponent)) + } + c.loss.receiveAckEnd(now, space, delay, c.handleAckOrLoss) + return n +} diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go new file mode 100644 index 0000000000..3a51ceb285 --- /dev/null +++ b/internal/quic/conn_send.go @@ -0,0 +1,255 @@ +// 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" +) + +// maybeSend sends datagrams, if possible. +// +// If sending is blocked by pacing, it returns the next time +// a datagram may be sent. +func (c *Conn) maybeSend(now time.Time) (next time.Time) { + // Assumption: The congestion window is not underutilized. + // If congestion control, pacing, and anti-amplification all permit sending, + // but we have no packet to send, then we will declare the window underutilized. + c.loss.cc.setUnderutilized(false) + + // Send one datagram on each iteration of this loop, + // until we hit a limit or run out of data to send. + // + // For each number space where we have write keys, + // attempt to construct a packet in that space. + // If the packet contains no frames (we have no data in need of sending), + // abandon the packet. + // + // Speculatively constructing packets means we don't need + // separate code paths for "do we have data to send?" and + // "send the data" that need to be kept in sync. + for { + limit, next := c.loss.sendLimit(now) + if limit == ccBlocked { + // If anti-amplification blocks sending, then no packet can be sent. + return next + } + // We may still send ACKs, even if congestion control or pacing limit sending. + + // Prepare to write a datagram of at most maxSendSize bytes. + c.w.reset(c.loss.maxSendSize()) + + // Initial packet. + pad := false + var sentInitial *sentPacket + if k := c.tlsState.wkeys[initialSpace]; k.isSet() { + pnumMaxAcked := c.acks[initialSpace].largestSeen() + pnum := c.loss.nextNumber(initialSpace) + p := longPacket{ + ptype: packetTypeInitial, + version: 1, + num: pnum, + dstConnID: c.connIDState.dstConnID(), + srcConnID: c.connIDState.srcConnID(), + } + c.w.startProtectedLongHeaderPacket(pnumMaxAcked, p) + c.appendFrames(now, initialSpace, pnum, limit) + sentInitial = c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, k, p) + if sentInitial != nil { + // Client initial packets need to be sent in a datagram padded to + // at least 1200 bytes. We can't add the padding yet, however, + // since we may want to coalesce additional packets with this one. + if c.side == clientSide || sentInitial.ackEliciting { + pad = true + } + } + } + + // Handshake packet. + if k := c.tlsState.wkeys[handshakeSpace]; k.isSet() { + pnumMaxAcked := c.acks[handshakeSpace].largestSeen() + pnum := c.loss.nextNumber(handshakeSpace) + p := longPacket{ + ptype: packetTypeHandshake, + version: 1, + num: pnum, + dstConnID: c.connIDState.dstConnID(), + srcConnID: c.connIDState.srcConnID(), + } + c.w.startProtectedLongHeaderPacket(pnumMaxAcked, p) + c.appendFrames(now, handshakeSpace, pnum, limit) + if sent := c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, k, p); sent != nil { + c.loss.packetSent(now, handshakeSpace, sent) + if c.side == clientSide { + // TODO: Discard the Initial keys. + // https://www.rfc-editor.org/rfc/rfc9001.html#section-4.9.1 + } + } + } + + // 1-RTT packet. + if k := c.tlsState.wkeys[appDataSpace]; k.isSet() { + pnumMaxAcked := c.acks[appDataSpace].largestSeen() + pnum := c.loss.nextNumber(appDataSpace) + dstConnID := c.connIDState.dstConnID() + c.w.start1RTTPacket(pnum, pnumMaxAcked, dstConnID) + c.appendFrames(now, appDataSpace, pnum, limit) + if pad && len(c.w.payload()) > 0 { + // 1-RTT packets have no length field and extend to the end + // of the datagram, so if we're sending a datagram that needs + // padding we need to add it inside the 1-RTT packet. + c.w.appendPaddingTo(minimumClientInitialDatagramSize) + pad = false + } + if sent := c.w.finish1RTTPacket(pnum, pnumMaxAcked, dstConnID, k); sent != nil { + c.loss.packetSent(now, appDataSpace, sent) + } + } + + buf := c.w.datagram() + if len(buf) == 0 { + if limit == ccOK { + // We have nothing to send, and congestion control does not + // block sending. The congestion window is underutilized. + c.loss.cc.setUnderutilized(true) + } + return next + } + + if sentInitial != nil { + if pad { + // Pad out the datagram with zeros, coalescing the Initial + // packet with invalid packets that will be ignored by the peer. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-14.1-1 + for len(buf) < minimumClientInitialDatagramSize { + buf = append(buf, 0) + // Technically this padding isn't in any packet, but + // account it to the Initial packet in this datagram + // for purposes of flow control and loss recovery. + sentInitial.size++ + sentInitial.inFlight = true + } + } + if k := c.tlsState.wkeys[initialSpace]; k.isSet() { + c.loss.packetSent(now, initialSpace, sentInitial) + } + } + + c.listener.sendDatagram(buf, c.peerAddr) + } +} + +func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, limit ccLimit) { + shouldSendAck := c.acks[space].shouldSendAck(now) + if limit != ccOK { + // ACKs are not limited by congestion control. + if shouldSendAck && c.appendAckFrame(now, space) { + c.acks[space].sentAck() + } + return + } + // We want to send an ACK frame if the ack controller wants to send a frame now, + // OR if we are sending a packet anyway and have ack-eliciting packets which we + // have not yet acked. + // + // We speculatively add ACK frames here, to put them at the front of the packet + // to avoid truncation. + // + // After adding all frames, if we don't need to send an ACK frame and have not + // added any other frames, we abandon the packet. + if c.appendAckFrame(now, space) { + defer func() { + // All frames other than ACK and PADDING are ack-eliciting, + // so if the packet is ack-eliciting we've added additional + // frames to it. + if shouldSendAck || c.w.sent.ackEliciting { + // Either we are willing to send an ACK-only packet, + // or we've added additional frames. + c.acks[space].sentAck() + } else { + // There's nothing in this packet but ACK frames, and + // we don't want to send an ACK-only packet at this time. + // Abandoning the packet means we wrote an ACK frame for + // nothing, but constructing the frame is cheap. + c.w.abandonPacket() + } + }() + } + if limit != ccOK { + return + } + pto := c.loss.ptoExpired + + // TODO: Add all the other frames we can send. + + // Test-only PING frames. + if space == c.testSendPingSpace && c.testSendPing.shouldSendPTO(pto) { + if !c.w.appendPingFrame() { + return + } + c.testSendPing.setSent(pnum) + } + + // If this is a PTO probe and we haven't added an ack-eliciting frame yet, + // add a PING to make this an ack-eliciting probe. + // + // Technically, there are separate PTO timers for each number space. + // When a PTO timer expires, we MUST send an ack-eliciting packet in the + // timer's space. We SHOULD send ack-eliciting packets in every other space + // with in-flight data. (RFC 9002, section 6.2.4) + // + // What we actually do is send a single datagram containing an ack-eliciting packet + // for every space for which we have keys. + // + // We fill the PTO probe packets with new or unacknowledged data. For example, + // a PTO probe sent for the Initial space will generally retransmit previously + // sent but unacknowledged CRYPTO data. + // + // When sending a PTO probe datagram containing multiple packets, it is + // possible that an earlier packet will fill up the datagram, leaving no + // space for the remaining probe packet(s). This is not a problem in practice. + // + // A client discards Initial keys when it first sends a Handshake packet + // (RFC 9001 Section 4.9.1). Handshake keys are discarded when the handshake + // is confirmed (RFC 9001 Section 4.9.2). The PTO timer is not set for the + // Application Data packet number space until the handshake is confirmed + // (RFC 9002 Section 6.2.1). Therefore, the only times a PTO probe can fire + // while data for multiple spaces is in flight are: + // + // - a server's Initial or Handshake timers can fire while Initial and Handshake + // data is in flight; and + // + // - a client's Handshake timer can fire while Handshake and Application Data + // data is in flight. + // + // It is theoretically possible for a server's Initial CRYPTO data to overflow + // the maximum datagram size, but unlikely in practice; this space contains + // only the ServerHello TLS message, which is small. It's also unlikely that + // the Handshake PTO probe will fire while Initial data is in flight (this + // requires not just that the Initial CRYPTO data completely fill a datagram, + // but a quite specific arrangement of lost and retransmitted packets.) + // We don't bother worrying about this case here, since the worst case is + // that we send a PTO probe for the in-flight Initial data and drop the + // Handshake probe. + // + // If a client's Handshake PTO timer fires while Application Data data is in + // flight, it is possible that the resent Handshake CRYPTO data will crowd + // out the probe for the Application Data space. However, since this probe is + // optional (recall that the Application Data PTO timer is never set until + // after Handshake keys have been discarded), dropping it is acceptable. + if pto && !c.w.sent.ackEliciting { + c.w.appendPingFrame() + } +} + +func (c *Conn) appendAckFrame(now time.Time, space numberSpace) bool { + seen, delay := c.acks[space].acksToSend(now) + if len(seen) == 0 { + return false + } + d := unscaledAckDelayFromDuration(delay, ackDelayExponent) + return c.w.appendAckFrame(seen, d) +} diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index a1709958e1..6bb12e210e 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -7,7 +7,12 @@ package quic import ( + "errors" + "fmt" "math" + "net/netip" + "reflect" + "strings" "testing" "time" ) @@ -46,6 +51,52 @@ func TestConnTestConn(t *testing.T) { } } +type testDatagram struct { + packets []*testPacket + paddedSize int +} + +func (d testDatagram) String() string { + var b strings.Builder + fmt.Fprintf(&b, "datagram with %v packets", len(d.packets)) + if d.paddedSize > 0 { + fmt.Fprintf(&b, " (padded to %v bytes)", d.paddedSize) + } + b.WriteString(":") + for _, p := range d.packets { + b.WriteString("\n") + b.WriteString(p.String()) + } + return b.String() +} + +type testPacket struct { + ptype packetType + version uint32 + num packetNumber + dstConnID []byte + srcConnID []byte + frames []debugFrame +} + +func (p testPacket) String() string { + var b strings.Builder + fmt.Fprintf(&b, " %v %v", p.ptype, p.num) + if p.version != 0 { + fmt.Fprintf(&b, " version=%v", p.version) + } + if p.srcConnID != nil { + fmt.Fprintf(&b, " src={%x}", p.srcConnID) + } + if p.dstConnID != nil { + fmt.Fprintf(&b, " dst={%x}", p.dstConnID) + } + for _, f := range p.frames { + fmt.Fprintf(&b, "\n %v", f) + } + return b.String() +} + // A testConn is a Conn whose external interactions (sending and receiving packets, // setting timers) can be manipulated in tests. type testConn struct { @@ -55,6 +106,30 @@ type testConn struct { timer time.Time timerLastFired time.Time idlec chan struct{} // only accessed on the conn's loop + + // Read and write keys are distinct from the conn's keys, + // because the test may know about keys before the conn does. + // For example, when sending a datagram with coalesced + // Initial and Handshake packets to a client conn, + // we use Handshake keys to encrypt the packet. + // The client only acquires those keys when it processes + // the Initial packet. + rkeys [numberSpaceCount]keys // for packets sent to the conn + wkeys [numberSpaceCount]keys // for packets sent by the conn + + // Information about the conn's (fake) peer. + peerConnID []byte // source conn id of peer's packets + peerNextPacketNum [numberSpaceCount]packetNumber // next packet number to use + + // Datagrams, packets, and frames sent by the conn, + // but not yet processed by the test. + sentDatagrams [][]byte + sentPackets []*testPacket + sentFrames []debugFrame + sentFramePacketType packetType + + // Frame types to ignore in tests. + ignoreFrames map[byte]bool } // newTestConn creates a Conn for testing. @@ -65,17 +140,41 @@ type testConn struct { func newTestConn(t *testing.T, side connSide) *testConn { t.Helper() tc := &testConn{ - t: t, - now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + t: t, + now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + peerConnID: []byte{0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5}, + ignoreFrames: map[byte]bool{ + frameTypePadding: true, // ignore PADDING by default + }, } t.Cleanup(tc.cleanup) - conn, err := newConn(tc.now, (*testConnHooks)(tc)) + var initialConnID []byte + if side == serverSide { + // The initial connection ID for the server is chosen by the client. + // When creating a server-side connection, pick a random connection ID here. + var err error + initialConnID, err = newRandomConnID() + if err != nil { + tc.t.Fatal(err) + } + } + + conn, err := newConn( + tc.now, + side, + initialConnID, + netip.MustParseAddrPort("127.0.0.1:443"), + (*testConnListener)(tc), + (*testConnHooks)(tc)) if err != nil { tc.t.Fatal(err) } tc.conn = conn + tc.wkeys[initialSpace] = conn.tlsState.wkeys[initialSpace] + tc.rkeys[initialSpace] = conn.tlsState.rkeys[initialSpace] + tc.wait() return tc } @@ -108,6 +207,16 @@ func (tc *testConn) advanceToTimer() { tc.advanceTo(tc.timer) } +func (tc *testConn) timerDelay() time.Duration { + if tc.timer.IsZero() { + return math.MaxInt64 // infinite + } + if tc.timer.Before(tc.now) { + return 0 + } + return tc.timer.Sub(tc.now) +} + const infiniteDuration = time.Duration(math.MaxInt64) // timeUntilEvent returns the amount of time until the next connection event. @@ -155,6 +264,277 @@ func (tc *testConn) cleanup() { tc.conn.exit() } +// write sends the Conn a datagram. +func (tc *testConn) write(d *testDatagram) { + tc.t.Helper() + var buf []byte + for _, p := range d.packets { + space := spaceForPacketType(p.ptype) + if p.num >= tc.peerNextPacketNum[space] { + tc.peerNextPacketNum[space] = p.num + 1 + } + buf = append(buf, tc.encodeTestPacket(p)...) + } + for len(buf) < d.paddedSize { + buf = append(buf, 0) + } + tc.conn.sendMsg(&datagram{ + b: buf, + }) + tc.wait() +} + +// writeFrame sends the Conn a datagram containing the given frames. +func (tc *testConn) writeFrames(ptype packetType, frames ...debugFrame) { + tc.t.Helper() + space := spaceForPacketType(ptype) + dstConnID := tc.conn.connIDState.local[0].cid + if tc.conn.connIDState.local[0].seq == -1 && ptype != packetTypeInitial { + // Only use the transient connection ID in Initial packets. + dstConnID = tc.conn.connIDState.local[1].cid + } + d := &testDatagram{ + packets: []*testPacket{{ + ptype: ptype, + num: tc.peerNextPacketNum[space], + frames: frames, + version: 1, + dstConnID: dstConnID, + srcConnID: tc.peerConnID, + }}, + } + if ptype == packetTypeInitial && tc.conn.side == serverSide { + d.paddedSize = 1200 + } + tc.write(d) +} + +// ignoreFrame hides frames of the given type sent by the Conn. +func (tc *testConn) ignoreFrame(frameType byte) { + tc.ignoreFrames[frameType] = true +} + +// readDatagram reads the next datagram sent by the Conn. +// It returns nil if the Conn has no more datagrams to send at this time. +func (tc *testConn) readDatagram() *testDatagram { + tc.t.Helper() + tc.wait() + tc.sentPackets = nil + tc.sentFrames = nil + if len(tc.sentDatagrams) == 0 { + return nil + } + buf := tc.sentDatagrams[0] + tc.sentDatagrams = tc.sentDatagrams[1:] + return tc.parseTestDatagram(buf) +} + +// readPacket reads the next packet sent by the Conn. +// It returns nil if the Conn has no more packets to send at this time. +func (tc *testConn) readPacket() *testPacket { + tc.t.Helper() + for len(tc.sentPackets) == 0 { + d := tc.readDatagram() + if d == nil { + return nil + } + tc.sentPackets = d.packets + } + p := tc.sentPackets[0] + tc.sentPackets = tc.sentPackets[1:] + return p +} + +// readFrame reads the next frame sent by the Conn. +// It returns nil if the Conn has no more frames to send at this time. +func (tc *testConn) readFrame() (debugFrame, packetType) { + tc.t.Helper() + for len(tc.sentFrames) == 0 { + p := tc.readPacket() + if p == nil { + return nil, packetTypeInvalid + } + tc.sentFramePacketType = p.ptype + tc.sentFrames = p.frames + } + f := tc.sentFrames[0] + tc.sentFrames = tc.sentFrames[1:] + return f, tc.sentFramePacketType +} + +// wantDatagram indicates that we expect the Conn to send a datagram. +func (tc *testConn) wantDatagram(expectation string, want *testDatagram) { + tc.t.Helper() + got := tc.readDatagram() + if !reflect.DeepEqual(got, want) { + tc.t.Fatalf("%v:\ngot datagram: %v\nwant datagram: %v", expectation, got, want) + } +} + +// wantPacket indicates that we expect the Conn to send a packet. +func (tc *testConn) wantPacket(expectation string, want *testPacket) { + tc.t.Helper() + got := tc.readPacket() + if !reflect.DeepEqual(got, want) { + tc.t.Fatalf("%v:\ngot packet: %v\nwant packet: %v", expectation, got, want) + } +} + +// wantFrame indicates that we expect the Conn to send a frame. +func (tc *testConn) wantFrame(expectation string, wantType packetType, want debugFrame) { + tc.t.Helper() + got, gotType := tc.readFrame() + if got == nil { + tc.t.Fatalf("%v:\nconnection is idle\nwant %v frame: %v", expectation, wantType, want) + } + if gotType != wantType { + tc.t.Fatalf("%v:\ngot %v packet, want %v", expectation, wantType, want) + } + if !reflect.DeepEqual(got, want) { + tc.t.Fatalf("%v:\ngot frame: %v\nwant frame: %v", expectation, got, want) + } +} + +// wantIdle indicates that we expect the Conn to not send any more frames. +func (tc *testConn) wantIdle(expectation string) { + tc.t.Helper() + switch { + case len(tc.sentFrames) > 0: + tc.t.Fatalf("expect: %v\nunexpectedly got: %v", expectation, tc.sentFrames[0]) + case len(tc.sentPackets) > 0: + tc.t.Fatalf("expect: %v\nunexpectedly got: %v", expectation, tc.sentPackets[0]) + } + if f, _ := tc.readFrame(); f != nil { + tc.t.Fatalf("expect: %v\nunexpectedly got: %v", expectation, f) + } +} + +func (tc *testConn) encodeTestPacket(p *testPacket) []byte { + tc.t.Helper() + var w packetWriter + w.reset(1200) + var pnumMaxAcked packetNumber + if p.ptype != packetType1RTT { + w.startProtectedLongHeaderPacket(pnumMaxAcked, longPacket{ + ptype: p.ptype, + version: p.version, + num: p.num, + dstConnID: p.dstConnID, + srcConnID: p.srcConnID, + }) + } else { + w.start1RTTPacket(p.num, pnumMaxAcked, p.dstConnID) + } + for _, f := range p.frames { + f.write(&w) + } + space := spaceForPacketType(p.ptype) + if !tc.rkeys[space].isSet() { + tc.t.Fatalf("sending packet with no %v keys available", space) + return nil + } + if p.ptype != packetType1RTT { + w.finishProtectedLongHeaderPacket(pnumMaxAcked, tc.rkeys[space], longPacket{ + ptype: p.ptype, + version: p.version, + num: p.num, + dstConnID: p.dstConnID, + srcConnID: p.srcConnID, + }) + } else { + w.finish1RTTPacket(p.num, pnumMaxAcked, p.dstConnID, tc.rkeys[space]) + } + return w.datagram() +} + +func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { + tc.t.Helper() + bufSize := len(buf) + d := &testDatagram{} + for len(buf) > 0 { + if buf[0] == 0 { + d.paddedSize = bufSize + break + } + ptype := getPacketType(buf) + space := spaceForPacketType(ptype) + if !tc.wkeys[space].isSet() { + tc.t.Fatalf("no keys for space %v, packet type %v", space, ptype) + } + if isLongHeader(buf[0]) { + var pnumMax packetNumber // TODO: Track packet numbers. + p, n := parseLongHeaderPacket(buf, tc.wkeys[space], pnumMax) + if n < 0 { + tc.t.Fatalf("packet parse error") + } + frames, err := tc.parseTestFrames(p.payload) + if err != nil { + tc.t.Fatal(err) + } + d.packets = append(d.packets, &testPacket{ + ptype: p.ptype, + version: p.version, + num: p.num, + dstConnID: p.dstConnID, + srcConnID: p.srcConnID, + frames: frames, + }) + buf = buf[n:] + } else { + var pnumMax packetNumber // TODO: Track packet numbers. + p, n := parse1RTTPacket(buf, tc.wkeys[space], len(tc.peerConnID), pnumMax) + if n < 0 { + tc.t.Fatalf("packet parse error") + } + dstConnID, _ := dstConnIDForDatagram(buf) + frames, err := tc.parseTestFrames(p.payload) + if err != nil { + tc.t.Fatal(err) + } + d.packets = append(d.packets, &testPacket{ + ptype: packetType1RTT, + num: p.num, + dstConnID: dstConnID, + frames: frames, + }) + buf = buf[n:] + } + } + return d +} + +func (tc *testConn) parseTestFrames(payload []byte) ([]debugFrame, error) { + tc.t.Helper() + var frames []debugFrame + for len(payload) > 0 { + f, n := parseDebugFrame(payload) + if n < 0 { + return nil, errors.New("error parsing frames") + } + if !tc.ignoreFrames[payload[0]] { + frames = append(frames, f) + } + payload = payload[n:] + } + return frames, nil +} + +func spaceForPacketType(ptype packetType) numberSpace { + switch ptype { + case packetTypeInitial: + return initialSpace + case packetType0RTT: + panic("TODO: packetType0RTT") + case packetTypeHandshake: + return handshakeSpace + case packetTypeRetry: + panic("TODO: packetTypeRetry") + case packetType1RTT: + return appDataSpace + } + panic("unknown packet type") +} + // testConnHooks implements connTestHooks. type testConnHooks testConn @@ -186,3 +566,11 @@ func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.T m = <-msgc return tc.now, m } + +// testConnListener implements connListener. +type testConnListener testConn + +func (tc *testConnListener) sendDatagram(p []byte, addr netip.AddrPort) error { + tc.sentDatagrams = append(tc.sentDatagrams, append([]byte(nil), p...)) + return nil +} diff --git a/internal/quic/dgram.go b/internal/quic/dgram.go new file mode 100644 index 0000000000..79e6650fa4 --- /dev/null +++ b/internal/quic/dgram.go @@ -0,0 +1,38 @@ +// 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 ( + "net/netip" + "sync" +) + +type datagram struct { + b []byte + addr netip.AddrPort +} + +var datagramPool = sync.Pool{ + New: func() any { + return &datagram{ + b: make([]byte, maxUDPPayloadSize), + } + }, +} + +func newDatagram() *datagram { + m := datagramPool.Get().(*datagram) + m.b = m.b[:cap(m.b)] + return m +} + +func (m *datagram) recycle() { + if cap(m.b) != maxUDPPayloadSize { + return + } + datagramPool.Put(m) +} diff --git a/internal/quic/frame_debug.go b/internal/quic/frame_debug.go index 945bb9d1f7..3009a04507 100644 --- a/internal/quic/frame_debug.go +++ b/internal/quic/frame_debug.go @@ -120,7 +120,7 @@ type debugFrameAck struct { func parseDebugFrameAck(b []byte) (f debugFrameAck, n int) { f.ranges = nil - _, f.ackDelay, n = consumeAckFrame(b, func(start, end packetNumber) { + _, f.ackDelay, n = consumeAckFrame(b, func(_ int, start, end packetNumber) { f.ranges = append(f.ranges, i64range[packetNumber]{ start: start, end: end, diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index 908a82ed90..c22f031038 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -91,7 +91,7 @@ func parseLongHeaderPacket(pkt []byte, k keys, pnumMax packetNumber) (p longPack pnumOff := len(pkt) - len(b) pkt = pkt[:pnumOff+int(payLen)] - if k.initialized() { + if k.isSet() { var err error p.payload, p.num, err = k.unprotect(pkt, pnumOff, pnumMax) if err != nil { @@ -162,7 +162,7 @@ func parse1RTTPacket(pkt []byte, k keys, dstConnIDLen int, pnumMax packetNumber) // which includes both general parse failures and specific violations of frame // constraints. -func consumeAckFrame(frame []byte, f func(start, end packetNumber)) (largest packetNumber, ackDelay unscaledAckDelay, n int) { +func consumeAckFrame(frame []byte, f func(rangeIndex int, start, end packetNumber)) (largest packetNumber, ackDelay unscaledAckDelay, n int) { b := frame[1:] // type largestAck, n := consumeVarint(b) @@ -195,7 +195,7 @@ func consumeAckFrame(frame []byte, f func(start, end packetNumber)) (largest pac if rangeMin < 0 || rangeMin > rangeMax { return 0, 0, -1 } - f(rangeMin, rangeMax+1) + f(int(i), rangeMin, rangeMax+1) if i == ackRangeCount { break diff --git a/internal/quic/packet_protection.go b/internal/quic/packet_protection.go index 1f0a735e8e..18470536ff 100644 --- a/internal/quic/packet_protection.go +++ b/internal/quic/packet_protection.go @@ -163,8 +163,8 @@ func (k keys) xorIV(pnum packetNumber) { k.iv[len(k.iv)-1] ^= uint8(pnum) } -// initialized returns true if valid keys are available. -func (k keys) initialized() bool { +// isSet returns true if valid keys are available. +func (k keys) isSet() bool { return k.aead != nil } diff --git a/internal/quic/packet_test.go b/internal/quic/packet_test.go index b13a587e54..f3a8b7d570 100644 --- a/internal/quic/packet_test.go +++ b/internal/quic/packet_test.go @@ -9,10 +9,27 @@ package quic import ( "bytes" "encoding/hex" + "fmt" "strings" "testing" ) +func (p packetType) String() string { + switch p { + case packetTypeInitial: + return "Initial" + case packetType0RTT: + return "0-RTT" + case packetTypeHandshake: + return "Handshake" + case packetTypeRetry: + return "Retry" + case packetType1RTT: + return "1-RTT" + } + return fmt.Sprintf("unknown packet type %v", byte(p)) +} + func TestPacketHeader(t *testing.T) { for _, test := range []struct { name string diff --git a/internal/quic/packet_writer.go b/internal/quic/packet_writer.go index 97987e0c2f..6c4c452cdd 100644 --- a/internal/quic/packet_writer.go +++ b/internal/quic/packet_writer.go @@ -237,7 +237,10 @@ func (w *packetWriter) appendPingFrame() (added bool) { return false } w.b = append(w.b, frameTypePing) - w.sent.appendAckElicitingFrame(frameTypePing) + // Mark this packet as ack-eliciting and in-flight, + // but there's no need to record the presence of a PING frame in it. + w.sent.ackEliciting = true + w.sent.inFlight = true return true } diff --git a/internal/quic/ping.go b/internal/quic/ping.go new file mode 100644 index 0000000000..3e7d9c51bd --- /dev/null +++ b/internal/quic/ping.go @@ -0,0 +1,16 @@ +// 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" + +func (c *Conn) ping(space numberSpace) { + c.sendMsg(func(now time.Time, c *Conn) { + c.testSendPing.setUnsent() + c.testSendPingSpace = space + }) +} diff --git a/internal/quic/ping_test.go b/internal/quic/ping_test.go new file mode 100644 index 0000000000..4a732ed543 --- /dev/null +++ b/internal/quic/ping_test.go @@ -0,0 +1,35 @@ +// 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" + +func TestPing(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.conn.ping(initialSpace) + tc.wantFrame("connection should send a PING frame", + packetTypeInitial, debugFramePing{}) + + tc.advanceToTimer() + tc.wantFrame("on PTO, connection should send another PING frame", + packetTypeInitial, debugFramePing{}) + + tc.wantIdle("after sending PTO probe, no additional frames to send") +} + +func TestAck(t *testing.T) { + tc := newTestConn(t, serverSide) + tc.writeFrames(packetTypeInitial, + debugFramePing{}, + ) + tc.wantFrame("connection should respond to ack-eliciting packet with an ACK frame", + packetTypeInitial, + debugFrameAck{ + ranges: []i64range[packetNumber]{{0, 1}}, + }, + ) +} diff --git a/internal/quic/quic.go b/internal/quic/quic.go index c69c0b9840..9df7f7e2b1 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -41,6 +41,10 @@ const ( // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.1.2-6 const timerGranularity = 1 * time.Millisecond +// Minimum size of a UDP datagram sent by a client carrying an Initial packet. +// https://www.rfc-editor.org/rfc/rfc9000#section-14.1 +const minimumClientInitialDatagramSize = 1200 + // A connSide distinguishes between the client and server sides of a connection. type connSide int8 diff --git a/internal/quic/sent_packet.go b/internal/quic/sent_packet.go index e5a80be3bb..4f11aa1368 100644 --- a/internal/quic/sent_packet.go +++ b/internal/quic/sent_packet.go @@ -29,6 +29,8 @@ type sentPacket struct { // we need to process an ack for or loss of this packet. // For example, a CRYPTO frame is recorded as the frame type (0x06), offset, and length, // but does not include the sent data. + // + // This buffer is written by packetWriter.append* and read by Conn.handleAckOrLoss. b []byte n int // read offset into b } diff --git a/internal/quic/tls.go b/internal/quic/tls.go new file mode 100644 index 0000000000..1cdb727e24 --- /dev/null +++ b/internal/quic/tls.go @@ -0,0 +1,23 @@ +// 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 + +// tlsState encapsulates interactions with TLS. +type tlsState struct { + // Encryption keys indexed by number space. + rkeys [numberSpaceCount]keys + wkeys [numberSpaceCount]keys +} + +func (s *tlsState) init(side connSide, initialConnID []byte) { + clientKeys, serverKeys := initialKeys(initialConnID) + if side == clientSide { + s.wkeys[initialSpace], s.rkeys[initialSpace] = clientKeys, serverKeys + } else { + s.wkeys[initialSpace], s.rkeys[initialSpace] = serverKeys, clientKeys + } +} From 8db2eadc7c3bda7baabb79fe1fadb8a093d5d391 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 18 Jan 2023 09:47:28 -0800 Subject: [PATCH 010/168] quic: transport parameter encoding and decoding Transport parameters are passed in the extension_data field of the quic_transport_parameters TLS extension. RFC 9000, Section 18. RFC 9001, Section 8.2. For golang/go#58547 Change-Id: I294ab6cdef19256f5db02dc269e8b417b1d5e54b Reviewed-on: https://go-review.googlesource.com/c/net/+/510575 Auto-Submit: Damien Neil Reviewed-by: Jonathan Amsterdam Run-TryBot: Damien Neil TryBot-Result: Gopher Robot --- internal/quic/transport_params.go | 277 ++++++++++++++++++ internal/quic/transport_params_test.go | 374 +++++++++++++++++++++++++ 2 files changed, 651 insertions(+) create mode 100644 internal/quic/transport_params.go create mode 100644 internal/quic/transport_params_test.go diff --git a/internal/quic/transport_params.go b/internal/quic/transport_params.go new file mode 100644 index 0000000000..416bfb8679 --- /dev/null +++ b/internal/quic/transport_params.go @@ -0,0 +1,277 @@ +// 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 ( + "encoding/binary" + "net/netip" + "time" +) + +// transportParameters transferred in the quic_transport_parameters TLS extension. +// https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2 +type transportParameters struct { + originalDstConnID []byte + maxIdleTimeout time.Duration + statelessResetToken []byte + maxUDPPayloadSize int64 + initialMaxData int64 + initialMaxStreamDataBidiLocal int64 + initialMaxStreamDataBidiRemote int64 + initialMaxStreamDataUni int64 + initialMaxStreamsBidi int64 + initialMaxStreamsUni int64 + ackDelayExponent uint8 + maxAckDelay time.Duration + disableActiveMigration bool + preferredAddrV4 netip.AddrPort + preferredAddrV6 netip.AddrPort + preferredAddrConnID []byte + preferredAddrResetToken []byte + activeConnIDLimit int64 + initialSrcConnID []byte + retrySrcConnID []byte +} + +const ( + defaultParamMaxUDPPayloadSize = 65527 + defaultParamAckDelayExponent = 3 + defaultParamMaxAckDelayMilliseconds = 25 + defaultParamActiveConnIDLimit = 2 +) + +// defaultTransportParameters is initialized to the RFC 9000 default values. +func defaultTransportParameters() transportParameters { + return transportParameters{ + maxUDPPayloadSize: defaultParamMaxUDPPayloadSize, + ackDelayExponent: defaultParamAckDelayExponent, + maxAckDelay: defaultParamMaxAckDelayMilliseconds * time.Millisecond, + activeConnIDLimit: defaultParamActiveConnIDLimit, + } +} + +const ( + paramOriginalDestinationConnectionID = 0x00 + paramMaxIdleTimeout = 0x01 + paramStatelessResetToken = 0x02 + paramMaxUDPPayloadSize = 0x03 + paramInitialMaxData = 0x04 + paramInitialMaxStreamDataBidiLocal = 0x05 + paramInitialMaxStreamDataBidiRemote = 0x06 + paramInitialMaxStreamDataUni = 0x07 + paramInitialMaxStreamsBidi = 0x08 + paramInitialMaxStreamsUni = 0x09 + paramAckDelayExponent = 0x0a + paramMaxAckDelay = 0x0b + paramDisableActiveMigration = 0x0c + paramPreferredAddress = 0x0d + paramActiveConnectionIDLimit = 0x0e + paramInitialSourceConnectionID = 0x0f + paramRetrySourceConnectionID = 0x10 +) + +func marshalTransportParameters(p transportParameters) []byte { + var b []byte + if v := p.originalDstConnID; v != nil { + b = appendVarint(b, paramOriginalDestinationConnectionID) + b = appendVarintBytes(b, v) + } + if v := uint64(p.maxIdleTimeout / time.Millisecond); v != 0 { + b = appendVarint(b, paramMaxIdleTimeout) + b = appendVarint(b, uint64(sizeVarint(v))) + b = appendVarint(b, uint64(v)) + } + if v := p.statelessResetToken; v != nil { + b = appendVarint(b, paramStatelessResetToken) + b = appendVarintBytes(b, v) + } + if v := p.maxUDPPayloadSize; v != defaultParamMaxUDPPayloadSize { + b = appendVarint(b, paramMaxUDPPayloadSize) + b = appendVarint(b, uint64(sizeVarint(uint64(v)))) + b = appendVarint(b, uint64(v)) + } + if v := p.initialMaxData; v != 0 { + b = appendVarint(b, paramInitialMaxData) + b = appendVarint(b, uint64(sizeVarint(uint64(v)))) + b = appendVarint(b, uint64(v)) + } + if v := p.initialMaxStreamDataBidiLocal; v != 0 { + b = appendVarint(b, paramInitialMaxStreamDataBidiLocal) + b = appendVarint(b, uint64(sizeVarint(uint64(v)))) + b = appendVarint(b, uint64(v)) + } + if v := p.initialMaxStreamDataBidiRemote; v != 0 { + b = appendVarint(b, paramInitialMaxStreamDataBidiRemote) + b = appendVarint(b, uint64(sizeVarint(uint64(v)))) + b = appendVarint(b, uint64(v)) + } + if v := p.initialMaxStreamDataUni; v != 0 { + b = appendVarint(b, paramInitialMaxStreamDataUni) + b = appendVarint(b, uint64(sizeVarint(uint64(v)))) + b = appendVarint(b, uint64(v)) + } + if v := p.initialMaxStreamsBidi; v != 0 { + b = appendVarint(b, paramInitialMaxStreamsBidi) + b = appendVarint(b, uint64(sizeVarint(uint64(v)))) + b = appendVarint(b, uint64(v)) + } + if v := p.initialMaxStreamsUni; v != 0 { + b = appendVarint(b, paramInitialMaxStreamsUni) + b = appendVarint(b, uint64(sizeVarint(uint64(v)))) + b = appendVarint(b, uint64(v)) + } + if v := p.ackDelayExponent; v != defaultParamAckDelayExponent { + b = appendVarint(b, paramAckDelayExponent) + b = appendVarint(b, uint64(sizeVarint(uint64(v)))) + b = appendVarint(b, uint64(v)) + } + if v := uint64(p.maxAckDelay / time.Millisecond); v != defaultParamMaxAckDelayMilliseconds { + b = appendVarint(b, paramMaxAckDelay) + b = appendVarint(b, uint64(sizeVarint(v))) + b = appendVarint(b, v) + } + if p.disableActiveMigration { + b = appendVarint(b, paramDisableActiveMigration) + b = append(b, 0) // 0-length value + } + if p.preferredAddrConnID != nil { + b = append(b, paramPreferredAddress) + b = appendVarint(b, uint64(4+2+16+2+1+len(p.preferredAddrConnID)+16)) + b = append(b, p.preferredAddrV4.Addr().AsSlice()...) // 4 bytes + b = binary.BigEndian.AppendUint16(b, p.preferredAddrV4.Port()) // 2 bytes + b = append(b, p.preferredAddrV6.Addr().AsSlice()...) // 16 bytes + b = binary.BigEndian.AppendUint16(b, p.preferredAddrV6.Port()) // 2 bytes + b = appendUint8Bytes(b, p.preferredAddrConnID) // 1 byte + len(conn_id) + b = append(b, p.preferredAddrResetToken...) // 16 bytes + } + if v := p.activeConnIDLimit; v != defaultParamActiveConnIDLimit { + b = appendVarint(b, paramActiveConnectionIDLimit) + b = appendVarint(b, uint64(sizeVarint(uint64(v)))) + b = appendVarint(b, uint64(v)) + } + if v := p.initialSrcConnID; v != nil { + b = appendVarint(b, paramInitialSourceConnectionID) + b = appendVarintBytes(b, v) + } + if v := p.retrySrcConnID; v != nil { + b = appendVarint(b, paramRetrySourceConnectionID) + b = appendVarintBytes(b, v) + } + return b +} + +func unmarshalTransportParams(params []byte) (transportParameters, error) { + p := defaultTransportParameters() + for len(params) > 0 { + id, n := consumeVarint(params) + if n < 0 { + return p, localTransportError(errTransportParameter) + } + params = params[n:] + val, n := consumeVarintBytes(params) + if n < 0 { + return p, localTransportError(errTransportParameter) + } + params = params[n:] + n = 0 + switch id { + case paramOriginalDestinationConnectionID: + p.originalDstConnID = val + n = len(val) + case paramMaxIdleTimeout: + var v uint64 + v, n = consumeVarint(val) + // If this is unreasonably large, consider it as no timeout to avoid + // time.Duration overflows. + if v > 1<<32 { + v = 0 + } + p.maxIdleTimeout = time.Duration(v) * time.Millisecond + case paramStatelessResetToken: + if len(val) != 16 { + return p, localTransportError(errTransportParameter) + } + p.statelessResetToken = val + n = 16 + case paramMaxUDPPayloadSize: + p.maxUDPPayloadSize, n = consumeVarintInt64(val) + if p.maxUDPPayloadSize < 1200 { + return p, localTransportError(errTransportParameter) + } + case paramInitialMaxData: + p.initialMaxData, n = consumeVarintInt64(val) + case paramInitialMaxStreamDataBidiLocal: + p.initialMaxStreamDataBidiLocal, n = consumeVarintInt64(val) + case paramInitialMaxStreamDataBidiRemote: + p.initialMaxStreamDataBidiRemote, n = consumeVarintInt64(val) + case paramInitialMaxStreamDataUni: + p.initialMaxStreamDataUni, n = consumeVarintInt64(val) + case paramInitialMaxStreamsBidi: + p.initialMaxStreamsBidi, n = consumeVarintInt64(val) + case paramInitialMaxStreamsUni: + p.initialMaxStreamsUni, n = consumeVarintInt64(val) + case paramAckDelayExponent: + var v uint64 + v, n = consumeVarint(val) + if v > 20 { + return p, localTransportError(errTransportParameter) + } + p.ackDelayExponent = uint8(v) + case paramMaxAckDelay: + var v uint64 + v, n = consumeVarint(val) + if v >= 1<<14 { + return p, localTransportError(errTransportParameter) + } + p.maxAckDelay = time.Duration(v) * time.Millisecond + case paramDisableActiveMigration: + p.disableActiveMigration = true + case paramPreferredAddress: + if len(val) < 4+2+16+2+1 { + return p, localTransportError(errTransportParameter) + } + p.preferredAddrV4 = netip.AddrPortFrom( + netip.AddrFrom4(*(*[4]byte)(val[:4])), + binary.BigEndian.Uint16(val[4:][:2]), + ) + val = val[4+2:] + p.preferredAddrV6 = netip.AddrPortFrom( + netip.AddrFrom16(*(*[16]byte)(val[:16])), + binary.BigEndian.Uint16(val[16:][:2]), + ) + val = val[16+2:] + var nn int + p.preferredAddrConnID, nn = consumeUint8Bytes(val) + if nn < 0 { + return p, localTransportError(errTransportParameter) + } + val = val[nn:] + if len(val) != 16 { + return p, localTransportError(errTransportParameter) + } + p.preferredAddrResetToken = val + val = nil + case paramActiveConnectionIDLimit: + p.activeConnIDLimit, n = consumeVarintInt64(val) + if p.activeConnIDLimit < 2 { + return p, localTransportError(errTransportParameter) + } + case paramInitialSourceConnectionID: + p.initialSrcConnID = val + n = len(val) + case paramRetrySourceConnectionID: + p.retrySrcConnID = val + n = len(val) + default: + n = len(val) + } + if n != len(val) { + return p, localTransportError(errTransportParameter) + } + } + return p, nil +} diff --git a/internal/quic/transport_params_test.go b/internal/quic/transport_params_test.go new file mode 100644 index 0000000000..e1c45ca0e6 --- /dev/null +++ b/internal/quic/transport_params_test.go @@ -0,0 +1,374 @@ +// 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" + "math" + "net/netip" + "reflect" + "testing" + "time" +) + +func TestTransportParametersMarshalUnmarshal(t *testing.T) { + for _, test := range []struct { + params func(p *transportParameters) + enc []byte + }{{ + params: func(p *transportParameters) { + p.originalDstConnID = []byte("connid") + }, + enc: []byte{ + 0x00, // original_destination_connection_id + byte(len("connid")), + 'c', 'o', 'n', 'n', 'i', 'd', + }, + }, { + params: func(p *transportParameters) { + p.maxIdleTimeout = 10 * time.Millisecond + }, + enc: []byte{ + 0x01, // max_idle_timeout + 1, // length + 10, // varint msecs + }, + }, { + params: func(p *transportParameters) { + p.statelessResetToken = []byte("0123456789abcdef") + }, + enc: []byte{ + 0x02, // stateless_reset_token + 16, // length + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', // reset token + }, + }, { + params: func(p *transportParameters) { + p.maxUDPPayloadSize = 1200 + }, + enc: []byte{ + 0x03, // max_udp_payload_size + 2, // length + 0x44, 0xb0, // varint value + }, + }, { + params: func(p *transportParameters) { + p.initialMaxData = 10 + }, + enc: []byte{ + 0x04, // initial_max_data + 1, // length + 10, // varint value + }, + }, { + params: func(p *transportParameters) { + p.initialMaxStreamDataBidiLocal = 10 + }, + enc: []byte{ + 0x05, // initial_max_stream_data_bidi_local + 1, // length + 10, // varint value + }, + }, { + params: func(p *transportParameters) { + p.initialMaxStreamDataBidiRemote = 10 + }, + enc: []byte{ + 0x06, // initial_max_stream_data_bidi_remote + 1, // length + 10, // varint value + }, + }, { + params: func(p *transportParameters) { + p.initialMaxStreamDataUni = 10 + }, + enc: []byte{ + 0x07, // initial_max_stream_data_uni + 1, // length + 10, // varint value + }, + }, { + params: func(p *transportParameters) { + p.initialMaxStreamsBidi = 10 + }, + enc: []byte{ + 0x08, // initial_max_streams_bidi + 1, // length + 10, // varint value + }, + }, { + params: func(p *transportParameters) { + p.initialMaxStreamsUni = 10 + }, + enc: []byte{ + 0x09, // initial_max_streams_uni + 1, // length + 10, // varint value + }, + }, { + params: func(p *transportParameters) { + p.ackDelayExponent = 4 + }, + enc: []byte{ + 0x0a, // ack_delay_exponent + 1, // length + 4, // varint value + }, + }, { + params: func(p *transportParameters) { + p.maxAckDelay = 10 * time.Millisecond + }, + enc: []byte{ + 0x0b, // max_ack_delay + 1, // length + 10, // varint value + }, + }, { + params: func(p *transportParameters) { + p.disableActiveMigration = true + }, + enc: []byte{ + 0x0c, // disable_active_migration + 0, // length + }, + }, { + params: func(p *transportParameters) { + p.preferredAddrV4 = netip.MustParseAddrPort("127.0.0.1:80") + p.preferredAddrV6 = netip.MustParseAddrPort("[fe80::1]:1024") + p.preferredAddrConnID = []byte("connid") + p.preferredAddrResetToken = []byte("0123456789abcdef") + }, + enc: []byte{ + 0x0d, // preferred_address + byte(4 + 2 + 16 + 2 + 1 + len("connid") + 16), // length + 127, 0, 0, 1, // v4 address + 0, 80, // v4 port + 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // v6 address + 0x04, 0x00, // v6 port, + 6, // connection id length + 'c', 'o', 'n', 'n', 'i', 'd', // connection id + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', // reset token + }, + }, { + params: func(p *transportParameters) { + p.activeConnIDLimit = 10 + }, + enc: []byte{ + 0x0e, // active_connection_id_limit + 1, // length + 10, // varint value + }, + }, { + params: func(p *transportParameters) { + p.initialSrcConnID = []byte("connid") + }, + enc: []byte{ + 0x0f, // initial_source_connection_id + byte(len("connid")), + 'c', 'o', 'n', 'n', 'i', 'd', + }, + }, { + params: func(p *transportParameters) { + p.retrySrcConnID = []byte("connid") + }, + enc: []byte{ + 0x10, // retry_source_connection_id + byte(len("connid")), + 'c', 'o', 'n', 'n', 'i', 'd', + }, + }} { + wantParams := defaultTransportParameters() + test.params(&wantParams) + gotBytes := marshalTransportParameters(wantParams) + if !bytes.Equal(gotBytes, test.enc) { + t.Errorf("marshalTransportParameters(%#v):\n got: %x\nwant: %x", wantParams, gotBytes, test.enc) + } + gotParams, err := unmarshalTransportParams(test.enc) + if err != nil { + t.Errorf("unmarshalTransportParams(%x): unexpected error: %v", test.enc, err) + } else if !reflect.DeepEqual(gotParams, wantParams) { + t.Errorf("unmarshalTransportParams(%x):\n got: %#v\nwant: %#v", test.enc, gotParams, wantParams) + } + } +} + +func TestTransportParametersErrors(t *testing.T) { + for _, test := range []struct { + desc string + enc []byte + }{{ + desc: "invalid id", + enc: []byte{ + 0x40, // too short + }, + }, { + desc: "parameter too short", + enc: []byte{ + 0x00, // original_destination_connection_id + 0x04, // length + 1, 2, 3, // not enough data + }, + }, { + desc: "extra data in parameter", + enc: []byte{ + 0x01, // max_idle_timeout + 2, // length + 10, // varint msecs + 0, // extra junk + }, + }, { + desc: "invalid varint in parameter", + enc: []byte{ + 0x01, // max_idle_timeout + 1, // length + 0x40, // incomplete varint + }, + }, { + desc: "stateless_reset_token not 16 bytes", + enc: []byte{ + 0x02, // stateless_reset_token, + 15, // length + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + }, + }, { + desc: "preferred_address is too short", + enc: []byte{ + 0x0d, // preferred_address + byte(3), + 127, 0, 0, + }, + }, { + desc: "preferred_address reset token too short", + enc: []byte{ + 0x0d, // preferred_address + byte(4 + 2 + 16 + 2 + 1 + len("connid") + 15), // length + 127, 0, 0, 1, // v4 address + 0, 80, // v4 port + 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // v6 address + 0x04, 0x00, // v6 port, + 6, // connection id length + 'c', 'o', 'n', 'n', 'i', 'd', // connection id + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', // reset token, one byte too short + + }, + }, { + desc: "preferred_address conn id too long", + enc: []byte{ + 0x0d, // preferred_address + byte(4 + 2 + 16 + 2 + 1 + len("connid") + 16), // length + 127, 0, 0, 1, // v4 address + 0, 80, // v4 port + 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // v6 address + 0x04, 0x00, // v6 port, + byte(len("connid")) + 16 + 1, // connection id length, too long + 'c', 'o', 'n', 'n', 'i', 'd', // connection id + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', // reset token + + }, + }} { + _, err := unmarshalTransportParams(test.enc) + if err == nil { + t.Errorf("%v:\nunmarshalTransportParams(%x): unexpectedly succeeded", test.desc, test.enc) + } + } +} + +func TestTransportParametersRangeErrors(t *testing.T) { + for _, test := range []struct { + desc string + params func(p *transportParameters) + }{{ + desc: "max_udp_payload_size < 1200", + params: func(p *transportParameters) { + p.maxUDPPayloadSize = 1199 + }, + }, { + desc: "ack_delay_exponent > 20", + params: func(p *transportParameters) { + p.ackDelayExponent = 21 + }, + }, { + desc: "max_ack_delay > 1^14 ms", + params: func(p *transportParameters) { + p.maxAckDelay = (1 << 14) * time.Millisecond + }, + }, { + desc: "active_connection_id_limit < 2", + params: func(p *transportParameters) { + p.activeConnIDLimit = 1 + }, + }} { + p := defaultTransportParameters() + test.params(&p) + enc := marshalTransportParameters(p) + _, err := unmarshalTransportParams(enc) + if err == nil { + t.Errorf("%v: unmarshalTransportParams unexpectedly succeeded", test.desc) + } + } +} + +func TestTransportParameterMaxIdleTimeoutOverflowsDuration(t *testing.T) { + tooManyMS := 1 + (math.MaxInt64 / uint64(time.Millisecond)) + + var enc []byte + enc = appendVarint(enc, paramMaxIdleTimeout) + enc = appendVarint(enc, uint64(sizeVarint(tooManyMS))) + enc = appendVarint(enc, uint64(tooManyMS)) + + dec, err := unmarshalTransportParams(enc) + if err != nil { + t.Fatalf("unmarshalTransportParameters(enc) = %v", err) + } + if got, want := dec.maxIdleTimeout, time.Duration(0); got != want { + t.Errorf("max_idle_timeout=%v, got maxIdleTimeout=%v; want %v", tooManyMS, got, want) + } +} + +func TestTransportParametersSkipUnknownParameters(t *testing.T) { + enc := []byte{ + 0x20, // unknown transport parameter + 1, // length + 0, // varint value + + 0x04, // initial_max_data + 1, // length + 10, // varint value + + 0x21, // unknown transport parameter + 1, // length + 0, // varint value + } + dec, err := unmarshalTransportParams(enc) + if err != nil { + t.Fatalf("unmarshalTransportParameters(enc) = %v", err) + } + if got, want := dec.initialMaxData, int64(10); got != want { + t.Errorf("got initial_max_data=%v; want %v", got, want) + } +} + +func FuzzTransportParametersMarshalUnmarshal(f *testing.F) { + f.Fuzz(func(t *testing.T, in []byte) { + p1, err := unmarshalTransportParams(in) + if err != nil { + return + } + out := marshalTransportParameters(p1) + p2, err := unmarshalTransportParams(out) + if err != nil { + t.Fatalf("round trip unmarshal/remarshal: unmarshal error: %v\n%x", err, in) + } + if !reflect.DeepEqual(p1, p2) { + t.Fatalf("round trip unmarshal/remarshal: parameters differ:\n%x\n%#v\n%#v", in, p1, p2) + } + }) +} From d0912d407c27bec63dfa5df0ef9012910774f8f4 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 14 Jul 2023 12:52:21 -0700 Subject: [PATCH 011/168] quic: add pipe type Streams (including CRYPTO streams) are an ordered byte sequence. Both outgoing and incoming streams require random access to a portion of that sequence. Outbound packets may be lost, requiring us to resend the data in the lost packet. Inbound packets may arrive out of order. Add a "pipe" type as a building block for both inbound and outbound streams. A pipe is a window into a portion of a stream, permitting random read and write access within that window (unlike bufio.Reader or bufio.Writer). Pipes are implemented as a linked list of blocks. Block sizes are uniform and allocations are pooled, avoiding non-pool allocations in the steady state. Pipe memory consumption is proportional to the current window, and goes to zero when the window has been fully consumed (unlike bytes.Buffer). For golang/go#58547 Change-Id: I0c16707552c9c46f31055daea2396590a924fc60 Reviewed-on: https://go-review.googlesource.com/c/net/+/510615 Run-TryBot: Damien Neil Reviewed-by: Jonathan Amsterdam TryBot-Result: Gopher Robot --- internal/quic/pipe.go | 149 +++++++++++++++++++++++++++++++++++++ internal/quic/pipe_test.go | 95 +++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 internal/quic/pipe.go create mode 100644 internal/quic/pipe_test.go diff --git a/internal/quic/pipe.go b/internal/quic/pipe.go new file mode 100644 index 0000000000..978a4f3d8b --- /dev/null +++ b/internal/quic/pipe.go @@ -0,0 +1,149 @@ +// 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 ( + "sync" +) + +// A pipe is a byte buffer used in implementing streams. +// +// A pipe contains a window of stream data. +// Random access reads and writes are supported within the window. +// Writing past the end of the window extends it. +// Data may be discarded from the start of the pipe, advancing the window. +type pipe struct { + start int64 + end int64 + head *pipebuf + tail *pipebuf +} + +type pipebuf struct { + off int64 + b []byte + next *pipebuf +} + +func (pb *pipebuf) end() int64 { + return pb.off + int64(len(pb.b)) +} + +var pipebufPool = sync.Pool{ + New: func() any { + return &pipebuf{ + b: make([]byte, 4096), + } + }, +} + +func newPipebuf() *pipebuf { + return pipebufPool.Get().(*pipebuf) +} + +func (b *pipebuf) recycle() { + b.off = 0 + b.next = nil + pipebufPool.Put(b) +} + +// writeAt writes len(b) bytes to the pipe at offset off. +// +// Writes to offsets before p.start are discarded. +// Writes to offsets after p.end extend the pipe window. +func (p *pipe) writeAt(b []byte, off int64) { + end := off + int64(len(b)) + if end > p.end { + p.end = end + } else if end <= p.start { + return + } + + if off < p.start { + // Discard the portion of b which falls before p.start. + trim := p.start - off + b = b[trim:] + off = p.start + } + + if p.head == nil { + p.head = newPipebuf() + p.head.off = p.start + p.tail = p.head + } + pb := p.head + if off >= p.tail.off { + // Common case: Writing past the end of the pipe. + pb = p.tail + } + for { + pboff := off - pb.off + if pboff < int64(len(pb.b)) { + n := copy(pb.b[pboff:], b) + if n == len(b) { + return + } + off += int64(n) + b = b[n:] + } + if pb.next == nil { + pb.next = newPipebuf() + pb.next.off = pb.off + int64(len(pb.b)) + p.tail = pb.next + } + pb = pb.next + } +} + +// copy copies len(b) bytes into b starting from off. +// The pipe must contain [off, off+len(b)). +func (p *pipe) copy(off int64, b []byte) { + dst := b[:0] + p.read(off, len(b), func(c []byte) error { + dst = append(dst, c...) + return nil + }) +} + +// read calls f with the data in [off, off+n) +// The data may be provided sequentially across multiple calls to f. +func (p *pipe) read(off int64, n int, f func([]byte) error) error { + if off < p.start { + panic("invalid read range") + } + for pb := p.head; pb != nil && n > 0; pb = pb.next { + if off >= pb.end() { + continue + } + b := pb.b[off-pb.off:] + if len(b) > n { + b = b[:n] + } + off += int64(len(b)) + n -= len(b) + if err := f(b); err != nil { + return err + } + } + if n > 0 { + panic("invalid read range") + } + return nil +} + +// discardBefore discards all data prior to off. +func (p *pipe) discardBefore(off int64) { + for p.head != nil && p.head.end() < off { + head := p.head + p.head = p.head.next + head.recycle() + } + if p.head == nil { + p.tail = nil + } + p.start = off +} diff --git a/internal/quic/pipe_test.go b/internal/quic/pipe_test.go new file mode 100644 index 0000000000..7a05ff4d47 --- /dev/null +++ b/internal/quic/pipe_test.go @@ -0,0 +1,95 @@ +// 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" + "math/rand" + "testing" +) + +func TestPipeWrites(t *testing.T) { + type writeOp struct { + start, end int64 + } + type discardBeforeOp struct { + off int64 + } + type op any + src := make([]byte, 65536) + rand.New(rand.NewSource(0)).Read(src) + for _, test := range []struct { + desc string + ops []op + }{{ + desc: "sequential writes", + ops: []op{ + writeOp{0, 1024}, + writeOp{1024, 4096}, + writeOp{4096, 65536}, + }, + }, { + desc: "disordered overlapping writes", + ops: []op{ + writeOp{2000, 8000}, + writeOp{0, 3000}, + writeOp{7000, 12000}, + }, + }, { + desc: "write to discarded region", + ops: []op{ + writeOp{0, 65536}, + discardBeforeOp{32768}, + writeOp{0, 1000}, + writeOp{3000, 5000}, + writeOp{0, 32768}, + }, + }, { + desc: "write overlaps discarded region", + ops: []op{ + discardBeforeOp{10000}, + writeOp{0, 20000}, + }, + }, { + desc: "discard everything", + ops: []op{ + writeOp{0, 10000}, + discardBeforeOp{10000}, + writeOp{10000, 20000}, + }, + }} { + var p pipe + var wantset rangeset[int64] + var wantStart, wantEnd int64 + for i, o := range test.ops { + switch o := o.(type) { + case writeOp: + p.writeAt(src[o.start:o.end], o.start) + wantset.add(o.start, o.end) + wantset.sub(0, wantStart) + if o.end > wantEnd { + wantEnd = o.end + } + case discardBeforeOp: + p.discardBefore(o.off) + wantset.sub(0, o.off) + wantStart = o.off + } + if p.start != wantStart || p.end != wantEnd { + t.Errorf("%v: after %#v p contains [%v,%v), want [%v,%v)", test.desc, test.ops[:i+1], p.start, p.end, wantStart, wantEnd) + } + for _, r := range wantset { + want := src[r.start:][:r.size()] + got := make([]byte, r.size()) + p.copy(r.start, got) + if !bytes.Equal(got, want) { + t.Errorf("%v after %#v, mismatch in data in %v", test.desc, test.ops[:i+1], r) + } + } + } + } +} From dd5bc96b138a4a27c151b47f09059ce31d13ecfc Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 20 Jul 2023 16:50:08 -0700 Subject: [PATCH 012/168] internal/quic: deflake TestConnTestConn Sending a message to a connection returns an error when the connection event loop had exited. This is unreliable, since a sent to the conn's message channel can succeed after the event loop exits, writing the message to the channel buffer. Drop the error return from Conn.sendMsg; it isn't useful, since it's always possible for the connection to exit with messages still in the channel buffer. Fixes golang/go#61485 Change-Id: Ic8351f984df827af881cf7b6d93d97031d2e615c Reviewed-on: https://go-review.googlesource.com/c/net/+/511658 TryBot-Result: Gopher Robot Reviewed-by: Jonathan Amsterdam Auto-Submit: Damien Neil Run-TryBot: Damien Neil --- internal/quic/conn.go | 11 ++++------- internal/quic/conn_test.go | 3 --- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index cdf79d607c..e6375e86a8 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -176,24 +176,21 @@ func (c *Conn) loop(now time.Time) { // sendMsg sends a message to the conn's loop. // It does not wait for the message to be processed. -func (c *Conn) sendMsg(m any) error { +// The conn may close before processing the message, in which case it is lost. +func (c *Conn) sendMsg(m any) { select { case c.msgc <- m: case <-c.donec: - return errors.New("quic: connection closed") } - return nil } // runOnLoop executes a function within the conn's loop goroutine. func (c *Conn) runOnLoop(f func(now time.Time, c *Conn)) error { donec := make(chan struct{}) - if err := c.sendMsg(func(now time.Time, c *Conn) { + c.sendMsg(func(now time.Time, c *Conn) { defer close(donec) f(now, c) - }); err != nil { - return err - } + }) select { case <-donec: case <-c.donec: diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 6bb12e210e..fda1d4b869 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -43,9 +43,6 @@ func TestConnTestConn(t *testing.T) { tc.wait() tc.advanceToTimer() - if err := tc.conn.sendMsg(nil); err == nil { - t.Errorf("after advancing to idle timeout, sendMsg = nil, want error") - } if !tc.conn.exited { t.Errorf("after advancing to idle timeout, exited = false, want true") } From 5e678bb28c36ba4aef595a4e468e51eda5d71c12 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 31 Jan 2023 08:58:13 -0800 Subject: [PATCH 013/168] quic: CRYPTO stream handling CRYPTO frames carry TLS handshake messages. Add a cryptoStream type which manages the TLS handshake stream, including retransmission of lost data, processing out-of-order received data, etc. For golang/go#58547 Change-Id: I8defa38e22d9c1bb8753f3a44d5ae0853fa56de8 Reviewed-on: https://go-review.googlesource.com/c/net/+/510616 Reviewed-by: Jonathan Amsterdam Run-TryBot: Damien Neil TryBot-Result: Gopher Robot --- internal/quic/crypto_stream.go | 159 +++++++++++++++++ internal/quic/crypto_stream_test.go | 265 ++++++++++++++++++++++++++++ 2 files changed, 424 insertions(+) create mode 100644 internal/quic/crypto_stream.go create mode 100644 internal/quic/crypto_stream_test.go diff --git a/internal/quic/crypto_stream.go b/internal/quic/crypto_stream.go new file mode 100644 index 0000000000..6cda6578c1 --- /dev/null +++ b/internal/quic/crypto_stream.go @@ -0,0 +1,159 @@ +// 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 + +// "Implementations MUST support buffering at least 4096 bytes of data +// received in out-of-order CRYPTO frames." +// https://www.rfc-editor.org/rfc/rfc9000.html#section-7.5-2 +// +// 4096 is too small for real-world cases, however, so we allow more. +const cryptoBufferSize = 1 << 20 + +// A cryptoStream is the stream of data passed in CRYPTO frames. +// There is one cryptoStream per packet number space. +type cryptoStream struct { + // CRYPTO data received from the peer. + in pipe + inset rangeset[int64] // bytes received + + // CRYPTO data queued for transmission to the peer. + out pipe + outunsent rangeset[int64] // bytes in need of sending + outacked rangeset[int64] // bytes acked by peer +} + +// handleCrypto processes data received in a CRYPTO frame. +func (s *cryptoStream) handleCrypto(off int64, b []byte, f func([]byte) error) error { + end := off + int64(len(b)) + if end-s.inset.min() > cryptoBufferSize { + return localTransportError(errCryptoBufferExceeded) + } + s.inset.add(off, end) + if off == s.in.start { + // Fast path: This is the next chunk of data in the stream, + // so just handle it immediately. + if err := f(b); err != nil { + return err + } + s.in.discardBefore(end) + } else { + // This is either data we've already processed, + // data we can't process yet, or a mix of both. + s.in.writeAt(b, off) + } + // s.in.start is the next byte in sequence. + // If it's in s.inset, we have bytes to provide. + // If it isn't, we don't--we're either out of data, + // or only have data that comes after the next byte. + if !s.inset.contains(s.in.start) { + return nil + } + // size is the size of the first contiguous chunk of bytes + // that have not been processed yet. + size := int(s.inset[0].end - s.in.start) + if size <= 0 { + return nil + } + err := s.in.read(s.in.start, size, f) + s.in.discardBefore(s.inset[0].end) + return err +} + +// write queues data for sending to the peer. +// It does not block or limit the amount of buffered data. +// QUIC connections don't communicate the amount of CRYPTO data they are willing to buffer, +// so we send what we have and the peer can close the connection if it is too much. +func (s *cryptoStream) write(b []byte) { + start := s.out.end + s.out.writeAt(b, start) + s.outunsent.add(start, s.out.end) +} + +// ackOrLoss reports that an CRYPTO frame sent by us has been acknowledged by the peer, or lost. +func (s *cryptoStream) ackOrLoss(start, end int64, fate packetFate) { + switch fate { + case packetAcked: + s.outacked.add(start, end) + s.outunsent.sub(start, end) + // If this ack is for data at the start of the send buffer, we can now discard it. + if s.outacked.contains(s.out.start) { + s.out.discardBefore(s.outacked[0].end) + } + case packetLost: + // Mark everything lost, but not previously acked, as needing retransmission. + // We do this by adding all the lost bytes to outunsent, and then + // removing everything already acked. + s.outunsent.add(start, end) + for _, a := range s.outacked { + s.outunsent.sub(a.start, a.end) + } + } +} + +// dataToSend reports what data should be sent in CRYPTO frames to the peer. +// It calls f with each range of data to send. +// f uses sendData to get the bytes to send, and returns the number of bytes sent. +// dataToSend calls f until no data is left, or f returns 0. +// +// This function is unusually indirect (why not just return a []byte, +// or implement io.Reader?). +// +// Returning a []byte to the caller either requires that we store the +// data to send contiguously (which we don't), allocate a temporary buffer +// and copy into it (inefficient), or return less data than we have available +// (requires complexity to avoid unnecessarily breaking data across frames). +// +// Accepting a []byte from the caller (io.Reader) makes packet construction +// difficult. Since CRYPTO data is encoded with a varint length prefix, the +// location of the data depends on the length of the data. (We could hardcode +// a 2-byte length, of course.) +// +// Instead, we tell the caller how much data is, the caller figures out where +// to put it (and possibly decides that it doesn't have space for this data +// in the packet after all), and the caller then makes a separate call to +// copy the data it wants into position. +func (s *cryptoStream) dataToSend(pto bool, f func(off, size int64) (sent int64)) { + for { + var off, size int64 + if pto { + // On PTO, resend unacked data that fits in the probe packet. + // For simplicity, we send the range starting at s.out.start + // (which is definitely unacked, or else we would have discarded it) + // up to the next acked byte (if any). + // + // This may miss unacked data starting after that acked byte, + // but avoids resending data the peer has acked. + off = s.out.start + end := s.out.end + for _, r := range s.outacked { + if r.start > off { + end = r.start + break + } + } + size = end - s.out.start + } else if s.outunsent.numRanges() > 0 { + off = s.outunsent.min() + size = s.outunsent[0].size() + } + if size == 0 { + return + } + n := f(off, size) + if n == 0 || pto { + return + } + } +} + +// sendData fills b with data to send to the peer, starting at off, +// and marks the data as sent. The caller must have already ascertained +// that there is data to send in this region using dataToSend. +func (s *cryptoStream) sendData(off int64, b []byte) { + s.out.copy(off, b) + s.outunsent.sub(off, off+int64(len(b))) +} diff --git a/internal/quic/crypto_stream_test.go b/internal/quic/crypto_stream_test.go new file mode 100644 index 0000000000..a6c1e1b521 --- /dev/null +++ b/internal/quic/crypto_stream_test.go @@ -0,0 +1,265 @@ +// 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 ( + "crypto/rand" + "reflect" + "testing" +) + +func TestCryptoStreamReceive(t *testing.T) { + data := make([]byte, 1<<20) + rand.Read(data) // doesn't need to be crypto/rand, but non-deprecated and harmless + type frame struct { + start int64 + end int64 + want int + } + for _, test := range []struct { + name string + frames []frame + }{{ + name: "linear", + frames: []frame{{ + start: 0, + end: 1000, + want: 1000, + }, { + start: 1000, + end: 2000, + want: 2000, + }, { + // larger than any realistic packet can hold + start: 2000, + end: 1 << 20, + want: 1 << 20, + }}, + }, { + name: "out of order", + frames: []frame{{ + start: 1000, + end: 2000, + }, { + start: 2000, + end: 3000, + }, { + start: 0, + end: 1000, + want: 3000, + }}, + }, { + name: "resent", + frames: []frame{{ + start: 0, + end: 1000, + want: 1000, + }, { + start: 0, + end: 1000, + want: 1000, + }, { + start: 1000, + end: 2000, + want: 2000, + }, { + start: 0, + end: 1000, + want: 2000, + }, { + start: 1000, + end: 2000, + want: 2000, + }}, + }, { + name: "overlapping", + frames: []frame{{ + start: 0, + end: 1000, + want: 1000, + }, { + start: 3000, + end: 4000, + want: 1000, + }, { + start: 2000, + end: 3000, + want: 1000, + }, { + start: 1000, + end: 3000, + want: 4000, + }}, + }} { + t.Run(test.name, func(t *testing.T) { + var s cryptoStream + var got []byte + for _, f := range test.frames { + t.Logf("receive [%v,%v)", f.start, f.end) + s.handleCrypto( + f.start, + data[f.start:f.end], + func(b []byte) error { + t.Logf("got new bytes [%v,%v)", len(got), len(got)+len(b)) + got = append(got, b...) + return nil + }, + ) + if len(got) != f.want { + t.Fatalf("have bytes [0,%v), want [0,%v)", len(got), f.want) + } + for i := range got { + if got[i] != data[i] { + t.Fatalf("byte %v of received data = %v, want %v", i, got[i], data[i]) + } + } + } + }) + } +} + +func TestCryptoStreamSends(t *testing.T) { + data := make([]byte, 1<<20) + rand.Read(data) // doesn't need to be crypto/rand, but non-deprecated and harmless + type ( + sendOp i64range[int64] + ackOp i64range[int64] + lossOp i64range[int64] + ) + for _, test := range []struct { + name string + size int64 + ops []any + wantSend []i64range[int64] + wantPTOSend []i64range[int64] + }{{ + name: "writes with data remaining", + size: 4000, + ops: []any{ + sendOp{0, 1000}, + sendOp{1000, 2000}, + sendOp{2000, 3000}, + }, + wantSend: []i64range[int64]{ + {3000, 4000}, + }, + wantPTOSend: []i64range[int64]{ + {0, 4000}, + }, + }, { + name: "lost data is resent", + size: 4000, + ops: []any{ + sendOp{0, 1000}, + sendOp{1000, 2000}, + sendOp{2000, 3000}, + sendOp{3000, 4000}, + lossOp{1000, 2000}, + lossOp{3000, 4000}, + }, + wantSend: []i64range[int64]{ + {1000, 2000}, + {3000, 4000}, + }, + wantPTOSend: []i64range[int64]{ + {0, 4000}, + }, + }, { + name: "acked data at start of range", + size: 4000, + ops: []any{ + sendOp{0, 4000}, + ackOp{0, 1000}, + ackOp{1000, 2000}, + ackOp{2000, 3000}, + }, + wantSend: nil, + wantPTOSend: []i64range[int64]{ + {3000, 4000}, + }, + }, { + name: "acked data is not resent on pto", + size: 4000, + ops: []any{ + sendOp{0, 4000}, + ackOp{1000, 2000}, + }, + wantSend: nil, + wantPTOSend: []i64range[int64]{ + {0, 1000}, + }, + }, { + // This is an unusual, but possible scenario: + // Data is sent, resent, one of the two sends is acked, and the other is lost. + name: "acked and then lost data is not resent", + size: 4000, + ops: []any{ + sendOp{0, 4000}, + sendOp{1000, 2000}, // resent, no-op + ackOp{1000, 2000}, + lossOp{1000, 2000}, + }, + wantSend: nil, + wantPTOSend: []i64range[int64]{ + {0, 1000}, + }, + }, { + // The opposite of the above scenario: data is marked lost, and then acked + // before being resent. + name: "lost and then acked data is not resent", + size: 4000, + ops: []any{ + sendOp{0, 4000}, + sendOp{1000, 2000}, // resent, no-op + lossOp{1000, 2000}, + ackOp{1000, 2000}, + }, + wantSend: nil, + wantPTOSend: []i64range[int64]{ + {0, 1000}, + }, + }} { + t.Run(test.name, func(t *testing.T) { + var s cryptoStream + s.write(data[:test.size]) + for _, op := range test.ops { + switch op := op.(type) { + case sendOp: + t.Logf("send [%v,%v)", op.start, op.end) + b := make([]byte, op.end-op.start) + s.sendData(op.start, b) + case ackOp: + t.Logf("ack [%v,%v)", op.start, op.end) + s.ackOrLoss(op.start, op.end, packetAcked) + case lossOp: + t.Logf("loss [%v,%v)", op.start, op.end) + s.ackOrLoss(op.start, op.end, packetLost) + default: + t.Fatalf("unhandled type %T", op) + } + } + var gotSend []i64range[int64] + s.dataToSend(true, func(off, size int64) (wrote int64) { + gotSend = append(gotSend, i64range[int64]{off, off + size}) + return 0 + }) + if !reflect.DeepEqual(gotSend, test.wantPTOSend) { + t.Fatalf("got data to send on PTO: %v, want %v", gotSend, test.wantPTOSend) + } + gotSend = nil + s.dataToSend(false, func(off, size int64) (wrote int64) { + gotSend = append(gotSend, i64range[int64]{off, off + size}) + b := make([]byte, size) + s.sendData(off, b) + return int64(len(b)) + }) + if !reflect.DeepEqual(gotSend, test.wantSend) { + t.Fatalf("got data to send: %v, want %v", gotSend, test.wantSend) + } + }) + } +} From dd0aa3399ca64473636cc7074182c590dc9b4e31 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 17 Jul 2023 09:48:38 -0700 Subject: [PATCH 014/168] quic: tls handshake Exchange TLS handshake data in CRYPTO frames. Receive packet protection keys from the TLS layer. Discard packet protection keys as the handshake progresses. Send and receive HANDSHAKE_DONE frames (used by the server to inform the client of the handshake completing). Add a very minimal implementation of CONNECTION_CLOSE, just enough to let us write tests that trigger immediate close of connections. For golang/go#58547 Change-Id: I77496ca65bd72977565733739d563eaa2bb7d8d3 Reviewed-on: https://go-review.googlesource.com/c/net/+/510915 Reviewed-by: Jonathan Amsterdam TryBot-Result: Gopher Robot Run-TryBot: Damien Neil Auto-Submit: Damien Neil --- internal/quic/config.go | 20 ++ internal/quic/conn.go | 75 +++++- internal/quic/conn_loss.go | 7 +- internal/quic/conn_loss_test.go | 143 ++++++++++ internal/quic/conn_recv.go | 41 ++- internal/quic/conn_send.go | 72 ++++- internal/quic/conn_test.go | 150 ++++++++++- internal/quic/ping_test.go | 20 +- internal/quic/quic.go | 8 + internal/quic/tls.go | 134 +++++++++- internal/quic/tls_test.go | 421 ++++++++++++++++++++++++++++++ internal/quic/tlsconfig_test.go | 62 +++++ internal/quic/transport_params.go | 4 +- 13 files changed, 1105 insertions(+), 52 deletions(-) create mode 100644 internal/quic/config.go create mode 100644 internal/quic/conn_loss_test.go create mode 100644 internal/quic/tls_test.go create mode 100644 internal/quic/tlsconfig_test.go diff --git a/internal/quic/config.go b/internal/quic/config.go new file mode 100644 index 0000000000..7d1b7433af --- /dev/null +++ b/internal/quic/config.go @@ -0,0 +1,20 @@ +// 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 ( + "crypto/tls" +) + +// A Config structure configures a QUIC endpoint. +// A Config must not be modified after it has been passed to a QUIC function. +// A Config may be reused; the quic package will also not modify it. +type Config struct { + // TLSConfig is the endpoint's TLS configuration. + // It must be non-nil and include at least one certificate or else set GetCertificate. + TLSConfig *tls.Config +} diff --git a/internal/quic/conn.go b/internal/quic/conn.go index e6375e86a8..8130c549b9 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -7,6 +7,7 @@ package quic import ( + "crypto/tls" "errors" "fmt" "net/netip" @@ -19,6 +20,7 @@ import ( type Conn struct { side connSide listener connListener + config *Config testHooks connTestHooks peerAddr netip.AddrPort @@ -29,14 +31,27 @@ type Conn struct { w packetWriter acks [numberSpaceCount]ackState // indexed by number space connIDState connIDState - tlsState tlsState loss lossState + // errForPeer is set when the connection is being closed. + errForPeer error + connCloseSent [numberSpaceCount]bool + // idleTimeout is the time at which the connection will be closed due to inactivity. // https://www.rfc-editor.org/rfc/rfc9000#section-10.1 maxIdleTimeout time.Duration idleTimeout time.Time + // Packet protection keys, CRYPTO streams, and TLS state. + rkeys [numberSpaceCount]keys + wkeys [numberSpaceCount]keys + crypto [numberSpaceCount]cryptoStream + tls *tls.QUICConn + + // handshakeConfirmed is set when the handshake is confirmed. + // For server connections, it tracks sending HANDSHAKE_DONE. + handshakeConfirmed sentVal + peerAckDelayExponent int8 // -1 when unknown // Tests only: Send a PING in a specific number space. @@ -53,12 +68,14 @@ type connListener interface { // connTestHooks override conn behavior in tests. type connTestHooks interface { nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any) + handleTLSEvent(tls.QUICEvent) } -func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, l connListener, hooks connTestHooks) (*Conn, error) { +func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l connListener, hooks connTestHooks) (*Conn, error) { c := &Conn{ side: side, listener: l, + config: config, peerAddr: peerAddr, msgc: make(chan any, 1), donec: make(chan struct{}), @@ -88,12 +105,58 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. const maxDatagramSize = 1200 c.loss.init(c.side, maxDatagramSize, now) - c.tlsState.init(c.side, initialConnID) + c.startTLS(now, initialConnID, transportParameters{ + initialSrcConnID: c.connIDState.srcConnID(), + ackDelayExponent: ackDelayExponent, + maxUDPPayloadSize: maxUDPPayloadSize, + maxAckDelay: maxAckDelay, + }) go c.loop(now) return c, nil } +// confirmHandshake is called when the handshake is confirmed. +// https://www.rfc-editor.org/rfc/rfc9001#section-4.1.2 +func (c *Conn) confirmHandshake(now time.Time) { + // If handshakeConfirmed is unset, the handshake is not confirmed. + // If it is unsent, the handshake is confirmed and we need to send a HANDSHAKE_DONE. + // If it is sent, we have sent a HANDSHAKE_DONE. + // If it is received, the handshake is confirmed and we do not need to send anything. + if c.handshakeConfirmed.isSet() { + return // already confirmed + } + if c.side == serverSide { + // When the server confirms the handshake, it sends a HANDSHAKE_DONE. + c.handshakeConfirmed.setUnsent() + } else { + // The client never sends a HANDSHAKE_DONE, so we set handshakeConfirmed + // to the received state, indicating that the handshake is confirmed and we + // don't need to send anything. + c.handshakeConfirmed.setReceived() + } + c.loss.confirmHandshake() + // "An endpoint MUST discard its Handshake keys when the TLS handshake is confirmed" + // https://www.rfc-editor.org/rfc/rfc9001#section-4.9.2-1 + c.discardKeys(now, handshakeSpace) +} + +// discardKeys discards unused packet protection keys. +// https://www.rfc-editor.org/rfc/rfc9001#section-4.9 +func (c *Conn) discardKeys(now time.Time, space numberSpace) { + c.rkeys[space].discard() + c.wkeys[space].discard() + c.loss.discardKeys(now, space) +} + +// receiveTransportParameters applies transport parameters sent by the peer. +func (c *Conn) receiveTransportParameters(p transportParameters) { + c.peerAckDelayExponent = p.ackDelayExponent + c.loss.setMaxAckDelay(p.maxAckDelay) + + // TODO: Many more transport parameters to come. +} + type timerEvent struct{} // loop is the connection main loop. @@ -104,6 +167,7 @@ type timerEvent struct{} // Other goroutines may examine or modify conn state by sending the loop funcs to execute. func (c *Conn) loop(now time.Time) { defer close(c.donec) + defer c.tls.Close() // The connection timer sends a message to the connection loop on expiry. // We need to give it an expiry when creating it, so set the initial timeout to @@ -201,8 +265,9 @@ func (c *Conn) runOnLoop(f func(now time.Time, c *Conn)) error { // abort terminates a connection with an error. func (c *Conn) abort(now time.Time, err error) { - // TODO: Send CONNECTION_CLOSE frames. - c.exit() + if c.errForPeer == nil { + c.errForPeer = err + } } // exit fully terminates a connection immediately. diff --git a/internal/quic/conn_loss.go b/internal/quic/conn_loss.go index 11ed42dbb9..6cb459c33f 100644 --- a/internal/quic/conn_loss.go +++ b/internal/quic/conn_loss.go @@ -29,7 +29,7 @@ func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetF for !sent.done() { switch f := sent.next(); f { default: - panic(fmt.Sprintf("BUG: unhandled lost frame type %x", f)) + panic(fmt.Sprintf("BUG: unhandled acked/lost frame type %x", f)) case frameTypeAck: // Unlike most information, loss of an ACK frame does not trigger // retransmission. ACKs are sent in response to ack-eliciting packets, @@ -41,6 +41,11 @@ func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetF if fate == packetAcked { c.acks[space].handleAck(largest) } + case frameTypeCrypto: + start, end := sent.nextRange() + c.crypto[space].ackOrLoss(start, end, fate) + case frameTypeHandshakeDone: + c.handshakeConfirmed.ackOrLoss(sent.num, fate) } } } diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go new file mode 100644 index 0000000000..be4f5fb2c8 --- /dev/null +++ b/internal/quic/conn_loss_test.go @@ -0,0 +1,143 @@ +// 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 ( + "crypto/tls" + "testing" +) + +// Frames may be retransmitted either when the packet containing the frame is lost, or on PTO. +// lostFrameTest runs a test in both configurations. +func lostFrameTest(t *testing.T, f func(t *testing.T, pto bool)) { + t.Run("lost", func(t *testing.T) { + f(t, false) + }) + t.Run("pto", func(t *testing.T) { + f(t, true) + }) +} + +// triggerLossOrPTO causes the conn to declare the last sent packet lost, +// or advances to the PTO timer. +func (tc *testConn) triggerLossOrPTO(ptype packetType, pto bool) { + tc.t.Helper() + if pto { + if !tc.conn.loss.ptoTimerArmed { + tc.t.Fatalf("PTO timer not armed, expected it to be") + } + tc.advanceTo(tc.conn.loss.timer) + return + } + defer func(ignoreFrames map[byte]bool) { + tc.ignoreFrames = ignoreFrames + }(tc.ignoreFrames) + tc.ignoreFrames = map[byte]bool{ + frameTypeAck: true, + frameTypePadding: true, + } + // Send three packets containing PINGs, and then respond with an ACK for the + // last one. This puts the last packet before the PINGs outside the packet + // reordering threshold, and it will be declared lost. + const lossThreshold = 3 + var num packetNumber + for i := 0; i < lossThreshold; i++ { + tc.conn.ping(spaceForPacketType(ptype)) + d := tc.readDatagram() + if d == nil { + tc.t.Fatalf("conn is idle; want PING frame") + } + if d.packets[0].ptype != ptype { + tc.t.Fatalf("conn sent %v packet; want %v", d.packets[0].ptype, ptype) + } + num = d.packets[0].num + } + tc.writeFrames(ptype, debugFrameAck{ + ranges: []i64range[packetNumber]{ + {num, num + 1}, + }, + }) +} + +func TestLostCRYPTOFrame(t *testing.T) { + // "Data sent in CRYPTO frames is retransmitted [...] until all data has been acknowledged." + // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.1 + lostFrameTest(t, func(t *testing.T, pto bool) { + tc := newTestConn(t, clientSide) + tc.ignoreFrame(frameTypeAck) + + tc.wantFrame("client sends Initial CRYPTO frame", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + tc.triggerLossOrPTO(packetTypeInitial, pto) + tc.wantFrame("client resends Initial CRYPTO frame", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + + tc.wantFrame("client sends Handshake CRYPTO frame", + packetTypeHandshake, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake], + }) + tc.triggerLossOrPTO(packetTypeHandshake, pto) + tc.wantFrame("client resends Handshake CRYPTO frame", + packetTypeHandshake, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake], + }) + }) +} + +func TestLostHandshakeDoneFrame(t *testing.T) { + // "The HANDSHAKE_DONE frame MUST be retransmitted until it is acknowledged." + // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.16 + lostFrameTest(t, func(t *testing.T, pto bool) { + tc := newTestConn(t, serverSide) + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.wantFrame("server sends Initial CRYPTO frame", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + tc.wantFrame("server sends Handshake CRYPTO frame", + packetTypeHandshake, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake], + }) + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + + tc.wantFrame("server sends HANDSHAKE_DONE after handshake completes", + packetType1RTT, debugFrameHandshakeDone{}) + tc.wantFrame("server sends session ticket in CRYPTO frame", + packetType1RTT, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelApplication], + }) + + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("server resends HANDSHAKE_DONE", + packetType1RTT, debugFrameHandshakeDone{}) + tc.wantFrame("server resends session ticket", + packetType1RTT, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelApplication], + }) + }) +} diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index d5a3b8cb0c..7eb03e7279 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -41,12 +41,12 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { } func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpace, buf []byte) int { - if !c.tlsState.rkeys[space].isSet() { + if !c.rkeys[space].isSet() { return skipLongHeaderPacket(buf) } pnumMax := c.acks[space].largestSeen() - p, n := parseLongHeaderPacket(buf, c.tlsState.rkeys[space], pnumMax) + p, n := parseLongHeaderPacket(buf, c.rkeys[space], pnumMax) if n < 0 { return -1 } @@ -66,21 +66,23 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa if p.ptype == packetTypeHandshake && c.side == serverSide { c.loss.validateClientAddress() - // TODO: Discard Initial keys. + // "[...] a server MUST discard Initial keys when it first successfully + // processes a Handshake packet [...]" // https://www.rfc-editor.org/rfc/rfc9001#section-4.9.1-2 + c.discardKeys(now, initialSpace) } return n } func (c *Conn) handle1RTT(now time.Time, buf []byte) int { - if !c.tlsState.rkeys[appDataSpace].isSet() { + if !c.rkeys[appDataSpace].isSet() { // 1-RTT packets extend to the end of the datagram, // so skip the remainder of the datagram if we can't parse this. return len(buf) } pnumMax := c.acks[appDataSpace].largestSeen() - p, n := parse1RTTPacket(buf, c.tlsState.rkeys[appDataSpace], connIDLen, pnumMax) + p, n := parse1RTTPacket(buf, c.rkeys[appDataSpace], connIDLen, pnumMax) if n < 0 { return -1 } @@ -163,7 +165,7 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, if !frameOK(c, ptype, IH_1) { return } - _, _, n = consumeCryptoFrame(payload) + n = c.handleCryptoFrame(now, space, payload) case frameTypeNewToken: if !frameOK(c, ptype, ___1) { return @@ -207,14 +209,18 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, case frameTypeConnectionCloseTransport: // CONNECTION_CLOSE is OK in all spaces. _, _, _, n = consumeConnectionCloseTransportFrame(payload) + // TODO: https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2 + c.abort(now, localTransportError(errNo)) case frameTypeConnectionCloseApplication: // CONNECTION_CLOSE is OK in all spaces. _, _, n = consumeConnectionCloseApplicationFrame(payload) + // TODO: https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2 + c.abort(now, localTransportError(errNo)) case frameTypeHandshakeDone: if !frameOK(c, ptype, ___1) { return } - n = 1 + n = c.handleHandshakeDoneFrame(now, space, payload) } if n < 0 { c.abort(now, localTransportError(errFrameEncoding)) @@ -262,3 +268,24 @@ func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) c.loss.receiveAckEnd(now, space, delay, c.handleAckOrLoss) return n } + +func (c *Conn) handleCryptoFrame(now time.Time, space numberSpace, payload []byte) int { + off, data, n := consumeCryptoFrame(payload) + err := c.handleCrypto(now, space, off, data) + if err != nil { + c.abort(now, err) + return -1 + } + return n +} + +func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payload []byte) int { + if c.side == serverSide { + // Clients should never send HANDSHAKE_DONE. + // https://www.rfc-editor.org/rfc/rfc9000#section-19.20-4 + c.abort(now, localTransportError(errProtocolViolation)) + return -1 + } + c.confirmHandshake(now) + return 1 +} diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 3a51ceb285..71d24e6f0f 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -7,6 +7,8 @@ package quic import ( + "crypto/tls" + "errors" "time" ) @@ -45,7 +47,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // Initial packet. pad := false var sentInitial *sentPacket - if k := c.tlsState.wkeys[initialSpace]; k.isSet() { + if k := c.wkeys[initialSpace]; k.isSet() { pnumMaxAcked := c.acks[initialSpace].largestSeen() pnum := c.loss.nextNumber(initialSpace) p := longPacket{ @@ -62,14 +64,14 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // Client initial packets need to be sent in a datagram padded to // at least 1200 bytes. We can't add the padding yet, however, // since we may want to coalesce additional packets with this one. - if c.side == clientSide || sentInitial.ackEliciting { + if c.side == clientSide { pad = true } } } // Handshake packet. - if k := c.tlsState.wkeys[handshakeSpace]; k.isSet() { + if k := c.wkeys[handshakeSpace]; k.isSet() { pnumMaxAcked := c.acks[handshakeSpace].largestSeen() pnum := c.loss.nextNumber(handshakeSpace) p := longPacket{ @@ -84,14 +86,16 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { if sent := c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, k, p); sent != nil { c.loss.packetSent(now, handshakeSpace, sent) if c.side == clientSide { - // TODO: Discard the Initial keys. - // https://www.rfc-editor.org/rfc/rfc9001.html#section-4.9.1 + // "[...] a client MUST discard Initial keys when it first + // sends a Handshake packet [...]" + // https://www.rfc-editor.org/rfc/rfc9001.html#section-4.9.1-2 + c.discardKeys(now, initialSpace) } } } // 1-RTT packet. - if k := c.tlsState.wkeys[appDataSpace]; k.isSet() { + if k := c.wkeys[appDataSpace]; k.isSet() { pnumMaxAcked := c.acks[appDataSpace].largestSeen() pnum := c.loss.nextNumber(appDataSpace) dstConnID := c.connIDState.dstConnID() @@ -133,7 +137,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { sentInitial.inFlight = true } } - if k := c.tlsState.wkeys[initialSpace]; k.isSet() { + if k := c.wkeys[initialSpace]; k.isSet() { c.loss.packetSent(now, initialSpace, sentInitial) } } @@ -143,6 +147,26 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, limit ccLimit) { + if c.errForPeer != nil { + // This is the bare minimum required to send a CONNECTION_CLOSE frame + // when closing a connection immediately, for example in response to a + // protocol error. + // + // This does not handle the closing and draining states + // (https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2), + // but it's enough to let us write tests that result in a CONNECTION_CLOSE, + // and have those tests still pass when we finish implementing + // connection shutdown. + // + // TODO: Finish implementing connection shutdown. + if !c.connCloseSent[space] { + c.exited = true + c.appendConnectionCloseFrame(c.errForPeer) + c.connCloseSent[space] = true + } + return + } + shouldSendAck := c.acks[space].shouldSendAck(now) if limit != ccOK { // ACKs are not limited by congestion control. @@ -185,6 +209,21 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, // TODO: Add all the other frames we can send. + // HANDSHAKE_DONE + if c.handshakeConfirmed.shouldSendPTO(pto) { + if !c.w.appendHandshakeDoneFrame() { + return + } + c.handshakeConfirmed.setSent(pnum) + } + + // CRYPTO + c.crypto[space].dataToSend(pto, func(off, size int64) int64 { + b, _ := c.w.appendCryptoFrame(off, int(size)) + c.crypto[space].sendData(off, b) + return int64(len(b)) + }) + // Test-only PING frames. if space == c.testSendPingSpace && c.testSendPing.shouldSendPTO(pto) { if !c.w.appendPingFrame() { @@ -253,3 +292,22 @@ func (c *Conn) appendAckFrame(now time.Time, space numberSpace) bool { d := unscaledAckDelayFromDuration(delay, ackDelayExponent) return c.w.appendAckFrame(seen, d) } + +func (c *Conn) appendConnectionCloseFrame(err error) { + // TODO: Send application errors. + switch e := err.(type) { + case localTransportError: + c.w.appendConnectionCloseTransportFrame(transportError(e), 0, "") + default: + // TLS alerts are sent using error codes [0x0100,0x01ff). + // https://www.rfc-editor.org/rfc/rfc9000#section-20.1-2.36.1 + var alert tls.AlertError + if errors.As(err, &alert) { + // tls.AlertError is a uint8, so this can't exceed 0x01ff. + code := errTLSBase + transportError(alert) + c.w.appendConnectionCloseTransportFrame(code, 0, "") + return + } + c.w.appendConnectionCloseTransportFrame(errInternal, 0, "") + } +} diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index fda1d4b869..511fb97a0a 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -7,6 +7,9 @@ package quic import ( + "bytes" + "context" + "crypto/tls" "errors" "fmt" "math" @@ -111,8 +114,22 @@ type testConn struct { // we use Handshake keys to encrypt the packet. // The client only acquires those keys when it processes // the Initial packet. - rkeys [numberSpaceCount]keys // for packets sent to the conn - wkeys [numberSpaceCount]keys // for packets sent by the conn + rkeys [numberSpaceCount]keyData // for packets sent to the conn + wkeys [numberSpaceCount]keyData // for packets sent by the conn + + // testConn uses a test hook to snoop on the conn's TLS events. + // CRYPTO data produced by the conn's QUICConn is placed in + // cryptoDataOut. + // + // The peerTLSConn is is a QUICConn representing the peer. + // CRYPTO data produced by the conn is written to peerTLSConn, + // and data produced by peerTLSConn is placed in cryptoDataIn. + cryptoDataOut map[tls.QUICEncryptionLevel][]byte + cryptoDataIn map[tls.QUICEncryptionLevel][]byte + peerTLSConn *tls.QUICConn + + localConnID []byte + transientConnID []byte // Information about the conn's (fake) peer. peerConnID []byte // source conn id of peer's packets @@ -129,12 +146,18 @@ type testConn struct { ignoreFrames map[byte]bool } +type keyData struct { + suite uint16 + secret []byte + k keys +} + // newTestConn creates a Conn for testing. // // The Conn's event loop is controlled by the test, // allowing test code to access Conn state directly // by first ensuring the loop goroutine is idle. -func newTestConn(t *testing.T, side connSide) *testConn { +func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { t.Helper() tc := &testConn{ t: t, @@ -143,9 +166,24 @@ func newTestConn(t *testing.T, side connSide) *testConn { ignoreFrames: map[byte]bool{ frameTypePadding: true, // ignore PADDING by default }, + cryptoDataOut: make(map[tls.QUICEncryptionLevel][]byte), + cryptoDataIn: make(map[tls.QUICEncryptionLevel][]byte), } t.Cleanup(tc.cleanup) + config := &Config{ + TLSConfig: newTestTLSConfig(side), + } + peerProvidedParams := defaultTransportParameters() + for _, o := range opts { + switch o := o.(type) { + case func(*tls.Config): + o(config.TLSConfig) + default: + t.Fatalf("unknown newTestConn option %T", o) + } + } + var initialConnID []byte if side == serverSide { // The initial connection ID for the server is chosen by the client. @@ -157,11 +195,21 @@ func newTestConn(t *testing.T, side connSide) *testConn { } } + peerQUICConfig := &tls.QUICConfig{TLSConfig: newTestTLSConfig(side.peer())} + if side == clientSide { + tc.peerTLSConn = tls.QUICServer(peerQUICConfig) + } else { + tc.peerTLSConn = tls.QUICClient(peerQUICConfig) + } + tc.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) + tc.peerTLSConn.Start(context.Background()) + conn, err := newConn( tc.now, side, initialConnID, netip.MustParseAddrPort("127.0.0.1:443"), + config, (*testConnListener)(tc), (*testConnHooks)(tc)) if err != nil { @@ -169,8 +217,16 @@ func newTestConn(t *testing.T, side connSide) *testConn { } tc.conn = conn - tc.wkeys[initialSpace] = conn.tlsState.wkeys[initialSpace] - tc.rkeys[initialSpace] = conn.tlsState.rkeys[initialSpace] + if side == serverSide { + tc.transientConnID = tc.conn.connIDState.local[0].cid + tc.localConnID = tc.conn.connIDState.local[1].cid + } else if side == clientSide { + tc.transientConnID = tc.conn.connIDState.remote[0].cid + tc.localConnID = tc.conn.connIDState.local[0].cid + } + + tc.wkeys[initialSpace].k = conn.wkeys[initialSpace] + tc.rkeys[initialSpace].k = conn.rkeys[initialSpace] tc.wait() return tc @@ -385,7 +441,7 @@ func (tc *testConn) wantFrame(expectation string, wantType packetType, want debu tc.t.Fatalf("%v:\nconnection is idle\nwant %v frame: %v", expectation, wantType, want) } if gotType != wantType { - tc.t.Fatalf("%v:\ngot %v packet, want %v", expectation, wantType, want) + tc.t.Fatalf("%v:\ngot %v packet, want %v\ngot frame: %v", expectation, gotType, wantType, got) } if !reflect.DeepEqual(got, want) { tc.t.Fatalf("%v:\ngot frame: %v\nwant frame: %v", expectation, got, want) @@ -426,12 +482,12 @@ func (tc *testConn) encodeTestPacket(p *testPacket) []byte { f.write(&w) } space := spaceForPacketType(p.ptype) - if !tc.rkeys[space].isSet() { + if !tc.rkeys[space].k.isSet() { tc.t.Fatalf("sending packet with no %v keys available", space) return nil } if p.ptype != packetType1RTT { - w.finishProtectedLongHeaderPacket(pnumMaxAcked, tc.rkeys[space], longPacket{ + w.finishProtectedLongHeaderPacket(pnumMaxAcked, tc.rkeys[space].k, longPacket{ ptype: p.ptype, version: p.version, num: p.num, @@ -439,7 +495,7 @@ func (tc *testConn) encodeTestPacket(p *testPacket) []byte { srcConnID: p.srcConnID, }) } else { - w.finish1RTTPacket(p.num, pnumMaxAcked, p.dstConnID, tc.rkeys[space]) + w.finish1RTTPacket(p.num, pnumMaxAcked, p.dstConnID, tc.rkeys[space].k) } return w.datagram() } @@ -455,12 +511,12 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { } ptype := getPacketType(buf) space := spaceForPacketType(ptype) - if !tc.wkeys[space].isSet() { + if !tc.wkeys[space].k.isSet() { tc.t.Fatalf("no keys for space %v, packet type %v", space, ptype) } if isLongHeader(buf[0]) { var pnumMax packetNumber // TODO: Track packet numbers. - p, n := parseLongHeaderPacket(buf, tc.wkeys[space], pnumMax) + p, n := parseLongHeaderPacket(buf, tc.wkeys[space].k, pnumMax) if n < 0 { tc.t.Fatalf("packet parse error") } @@ -479,11 +535,10 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { buf = buf[n:] } else { var pnumMax packetNumber // TODO: Track packet numbers. - p, n := parse1RTTPacket(buf, tc.wkeys[space], len(tc.peerConnID), pnumMax) + p, n := parse1RTTPacket(buf, tc.wkeys[space].k, len(tc.peerConnID), pnumMax) if n < 0 { tc.t.Fatalf("packet parse error") } - dstConnID, _ := dstConnIDForDatagram(buf) frames, err := tc.parseTestFrames(p.payload) if err != nil { tc.t.Fatal(err) @@ -491,7 +546,7 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { d.packets = append(d.packets, &testPacket{ ptype: packetType1RTT, num: p.num, - dstConnID: dstConnID, + dstConnID: buf[1:][:len(tc.peerConnID)], frames: frames, }) buf = buf[n:] @@ -535,6 +590,73 @@ func spaceForPacketType(ptype packetType) numberSpace { // testConnHooks implements connTestHooks. type testConnHooks testConn +// handleTLSEvent processes TLS events generated by +// the connection under test's tls.QUICConn. +// +// We maintain a second tls.QUICConn representing the peer, +// and feed the TLS handshake data into it. +// +// We stash TLS handshake data from both sides in the testConn, +// where it can be used by tests. +// +// We snoop packet protection keys out of the tls.QUICConns, +// and verify that both sides of the connection are getting +// matching keys. +func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { + setKey := func(keys *[numberSpaceCount]keyData, e tls.QUICEvent) { + k, err := newKeys(e.Suite, e.Data) + if err != nil { + tc.t.Errorf("newKeys: %v", err) + return + } + var space numberSpace + switch { + case e.Level == tls.QUICEncryptionLevelHandshake: + space = handshakeSpace + case e.Level == tls.QUICEncryptionLevelApplication: + space = appDataSpace + default: + tc.t.Errorf("unexpected encryption level %v", e.Level) + return + } + s := "read" + if keys == &tc.wkeys { + s = "write" + } + if keys[space].k.isSet() { + if keys[space].suite != e.Suite || !bytes.Equal(keys[space].secret, e.Data) { + tc.t.Errorf("%v key mismatch for level for level %v", s, e.Level) + } + return + } + keys[space].suite = e.Suite + keys[space].secret = append([]byte{}, e.Data...) + keys[space].k = k + } + switch e.Kind { + case tls.QUICSetReadSecret: + setKey(&tc.rkeys, e) + case tls.QUICSetWriteSecret: + setKey(&tc.wkeys, e) + case tls.QUICWriteData: + tc.cryptoDataOut[e.Level] = append(tc.cryptoDataOut[e.Level], e.Data...) + tc.peerTLSConn.HandleData(e.Level, e.Data) + } + for { + e := tc.peerTLSConn.NextEvent() + switch e.Kind { + case tls.QUICNoEvent: + return + case tls.QUICSetReadSecret: + setKey(&tc.wkeys, e) + case tls.QUICSetWriteSecret: + setKey(&tc.rkeys, e) + case tls.QUICWriteData: + tc.cryptoDataIn[e.Level] = append(tc.cryptoDataIn[e.Level], e.Data...) + } + } +} + // nextMessage is called by the Conn's event loop to request its next event. func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.Time, m any) { tc.timer = timer diff --git a/internal/quic/ping_test.go b/internal/quic/ping_test.go index 4a732ed543..c370aaf1d8 100644 --- a/internal/quic/ping_test.go +++ b/internal/quic/ping_test.go @@ -10,26 +10,34 @@ import "testing" func TestPing(t *testing.T) { tc := newTestConn(t, clientSide) - tc.conn.ping(initialSpace) + tc.handshake() + + tc.conn.ping(appDataSpace) tc.wantFrame("connection should send a PING frame", - packetTypeInitial, debugFramePing{}) + packetType1RTT, debugFramePing{}) tc.advanceToTimer() tc.wantFrame("on PTO, connection should send another PING frame", - packetTypeInitial, debugFramePing{}) + packetType1RTT, debugFramePing{}) tc.wantIdle("after sending PTO probe, no additional frames to send") } func TestAck(t *testing.T) { tc := newTestConn(t, serverSide) - tc.writeFrames(packetTypeInitial, + tc.handshake() + + // Send two packets, to trigger an immediate ACK. + tc.writeFrames(packetType1RTT, + debugFramePing{}, + ) + tc.writeFrames(packetType1RTT, debugFramePing{}, ) tc.wantFrame("connection should respond to ack-eliciting packet with an ACK frame", - packetTypeInitial, + packetType1RTT, debugFrameAck{ - ranges: []i64range[packetNumber]{{0, 1}}, + ranges: []i64range[packetNumber]{{0, 3}}, }, ) } diff --git a/internal/quic/quic.go b/internal/quic/quic.go index 9df7f7e2b1..a61c91f16b 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -64,6 +64,14 @@ func (s connSide) String() string { } } +func (s connSide) peer() connSide { + if s == clientSide { + return serverSide + } else { + return clientSide + } +} + // A numberSpace is the context in which a packet number applies. // https://www.rfc-editor.org/rfc/rfc9000.html#section-12.3-7 type numberSpace byte diff --git a/internal/quic/tls.go b/internal/quic/tls.go index 1cdb727e24..4306a3e46d 100644 --- a/internal/quic/tls.go +++ b/internal/quic/tls.go @@ -6,18 +6,132 @@ package quic -// tlsState encapsulates interactions with TLS. -type tlsState struct { - // Encryption keys indexed by number space. - rkeys [numberSpaceCount]keys - wkeys [numberSpaceCount]keys -} +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "time" +) -func (s *tlsState) init(side connSide, initialConnID []byte) { +// startTLS starts the TLS handshake. +func (c *Conn) startTLS(now time.Time, initialConnID []byte, params transportParameters) error { clientKeys, serverKeys := initialKeys(initialConnID) - if side == clientSide { - s.wkeys[initialSpace], s.rkeys[initialSpace] = clientKeys, serverKeys + if c.side == clientSide { + c.wkeys[initialSpace], c.rkeys[initialSpace] = clientKeys, serverKeys } else { - s.wkeys[initialSpace], s.rkeys[initialSpace] = serverKeys, clientKeys + c.wkeys[initialSpace], c.rkeys[initialSpace] = serverKeys, clientKeys + } + + qconfig := &tls.QUICConfig{TLSConfig: c.config.TLSConfig} + if c.side == clientSide { + c.tls = tls.QUICClient(qconfig) + } else { + c.tls = tls.QUICServer(qconfig) + } + c.tls.SetTransportParameters(marshalTransportParameters(params)) + // TODO: We don't need or want a context for cancelation here, + // but users can use a context to plumb values through to hooks defined + // in the tls.Config. Pass through a context. + if err := c.tls.Start(context.TODO()); err != nil { + return err + } + return c.handleTLSEvents(now) +} + +func (c *Conn) handleTLSEvents(now time.Time) error { + for { + e := c.tls.NextEvent() + if c.testHooks != nil { + c.testHooks.handleTLSEvent(e) + } + switch e.Kind { + case tls.QUICNoEvent: + return nil + case tls.QUICSetReadSecret: + space, k, err := tlsKey(e) + if err != nil { + return err + } + c.rkeys[space] = k + case tls.QUICSetWriteSecret: + space, k, err := tlsKey(e) + if err != nil { + return err + } + c.wkeys[space] = k + case tls.QUICWriteData: + space, err := spaceForLevel(e.Level) + if err != nil { + return err + } + c.crypto[space].write(e.Data) + case tls.QUICHandshakeDone: + if c.side == serverSide { + // "[...] the TLS handshake is considered confirmed + // at the server when the handshake completes." + // https://www.rfc-editor.org/rfc/rfc9001#section-4.1.2-1 + c.confirmHandshake(now) + if !c.config.TLSConfig.SessionTicketsDisabled { + if err := c.tls.SendSessionTicket(false); err != nil { + return err + } + } + } + case tls.QUICTransportParameters: + params, err := unmarshalTransportParams(e.Data) + if err != nil { + return err + } + c.receiveTransportParameters(params) + } + } +} + +// tlsKey returns the keys in a QUICSetReadSecret or QUICSetWriteSecret event. +func tlsKey(e tls.QUICEvent) (numberSpace, keys, error) { + space, err := spaceForLevel(e.Level) + if err != nil { + return 0, keys{}, err + } + k, err := newKeys(e.Suite, e.Data) + if err != nil { + return 0, keys{}, err + } + return space, k, nil +} + +func spaceForLevel(level tls.QUICEncryptionLevel) (numberSpace, error) { + switch level { + case tls.QUICEncryptionLevelInitial: + return initialSpace, nil + case tls.QUICEncryptionLevelHandshake: + return handshakeSpace, nil + case tls.QUICEncryptionLevelApplication: + return appDataSpace, nil + default: + return 0, fmt.Errorf("quic: internal error: write handshake data at level %v", level) + } +} + +// handleCrypto processes data received in a CRYPTO frame. +func (c *Conn) handleCrypto(now time.Time, space numberSpace, off int64, data []byte) error { + var level tls.QUICEncryptionLevel + switch space { + case initialSpace: + level = tls.QUICEncryptionLevelInitial + case handshakeSpace: + level = tls.QUICEncryptionLevelHandshake + case appDataSpace: + level = tls.QUICEncryptionLevelApplication + default: + return errors.New("quic: internal error: received CRYPTO frame in unexpected number space") + } + err := c.crypto[space].handleCrypto(off, data, func(b []byte) error { + return c.tls.HandleData(level, b) + }) + if err != nil { + return err } + return c.handleTLSEvents(now) } diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go new file mode 100644 index 0000000000..df07820086 --- /dev/null +++ b/internal/quic/tls_test.go @@ -0,0 +1,421 @@ +// 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 ( + "crypto/tls" + "crypto/x509" + "errors" + "reflect" + "testing" + "time" +) + +// handshake executes the handshake. +func (tc *testConn) handshake() { + tc.t.Helper() + defer func(saved map[byte]bool) { + tc.ignoreFrames = saved + }(tc.ignoreFrames) + tc.ignoreFrames = nil + t := tc.t + dgrams := handshakeDatagrams(tc) + i := 0 + for { + if i == len(dgrams)-1 { + if tc.conn.side == clientSide { + want := tc.now.Add(maxAckDelay - timerGranularity) + if !tc.timer.Equal(want) { + t.Fatalf("want timer = %v (max_ack_delay), got %v", want, tc.timer) + } + if got := tc.readDatagram(); got != nil { + t.Fatalf("client unexpectedly sent: %v", got) + } + } + tc.advance(maxAckDelay) + } + + // Check that we're sending exactly the data we expect. + // Any variation from the norm here should be intentional. + got := tc.readDatagram() + var want *testDatagram + if !(tc.conn.side == serverSide && i == 0) && i < len(dgrams) { + want = dgrams[i] + fillCryptoFrames(want, tc.cryptoDataOut) + i++ + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("dgram %v:\ngot %v\n\nwant %v", i, got, want) + } + if i >= len(dgrams) { + break + } + + fillCryptoFrames(dgrams[i], tc.cryptoDataIn) + tc.write(dgrams[i]) + i++ + } +} + +func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { + var ( + clientConnID []byte + serverConnID []byte + ) + if tc.conn.side == clientSide { + clientConnID = tc.localConnID + serverConnID = tc.peerConnID + } else { + clientConnID = tc.peerConnID + serverConnID = tc.localConnID + } + return []*testDatagram{{ + // Client Initial + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 0, + version: 1, + srcConnID: clientConnID, + dstConnID: tc.transientConnID, + frames: []debugFrame{ + debugFrameCrypto{}, + }, + }}, + paddedSize: 1200, + }, { + // Server Initial + Handshake + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 0, + version: 1, + srcConnID: serverConnID, + dstConnID: clientConnID, + frames: []debugFrame{ + debugFrameAck{ + ranges: []i64range[packetNumber]{{0, 1}}, + }, + debugFrameCrypto{}, + }, + }, { + ptype: packetTypeHandshake, + num: 0, + version: 1, + srcConnID: serverConnID, + dstConnID: clientConnID, + frames: []debugFrame{ + debugFrameCrypto{}, + }, + }}, + }, { + // Client Handshake + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 1, + version: 1, + srcConnID: clientConnID, + dstConnID: serverConnID, + frames: []debugFrame{ + debugFrameAck{ + ranges: []i64range[packetNumber]{{0, 1}}, + }, + }, + }, { + ptype: packetTypeHandshake, + num: 0, + version: 1, + srcConnID: clientConnID, + dstConnID: serverConnID, + frames: []debugFrame{ + debugFrameAck{ + ranges: []i64range[packetNumber]{{0, 1}}, + }, + debugFrameCrypto{}, + }, + }}, + paddedSize: 1200, + }, { + // Server HANDSHAKE_DONE and session ticket + packets: []*testPacket{{ + ptype: packetType1RTT, + num: 0, + dstConnID: clientConnID, + frames: []debugFrame{ + debugFrameHandshakeDone{}, + debugFrameCrypto{}, + }, + }}, + }, { + // Client ack (after max_ack_delay) + packets: []*testPacket{{ + ptype: packetType1RTT, + num: 0, + dstConnID: serverConnID, + frames: []debugFrame{ + debugFrameAck{ + ackDelay: unscaledAckDelayFromDuration( + maxAckDelay, ackDelayExponent), + ranges: []i64range[packetNumber]{{0, 1}}, + }, + }, + }}, + }} +} + +func fillCryptoFrames(d *testDatagram, data map[tls.QUICEncryptionLevel][]byte) { + for _, p := range d.packets { + var level tls.QUICEncryptionLevel + switch p.ptype { + case packetTypeInitial: + level = tls.QUICEncryptionLevelInitial + case packetTypeHandshake: + level = tls.QUICEncryptionLevelHandshake + case packetType1RTT: + level = tls.QUICEncryptionLevelApplication + default: + continue + } + for i := range p.frames { + c, ok := p.frames[i].(debugFrameCrypto) + if !ok { + continue + } + c.data = data[level] + data[level] = nil + p.frames[i] = c + } + } +} + +func TestConnClientHandshake(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.handshake() + tc.advance(1 * time.Second) + tc.wantIdle("no packets should be sent by an idle conn after the handshake") +} + +func TestConnServerHandshake(t *testing.T) { + tc := newTestConn(t, serverSide) + tc.handshake() + tc.advance(1 * time.Second) + tc.wantIdle("no packets should be sent by an idle conn after the handshake") +} + +func TestConnKeysDiscardedClient(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.ignoreFrame(frameTypeAck) + + tc.wantFrame("client sends Initial CRYPTO frame", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + tc.wantFrame("client sends Handshake CRYPTO frame", + packetTypeHandshake, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake], + }) + + // The client discards Initial keys after sending a Handshake packet. + tc.writeFrames(packetTypeInitial, + debugFrameConnectionCloseTransport{code: errInternal}) + tc.wantIdle("client has discarded Initial keys, cannot read CONNECTION_CLOSE") + + // The client discards Handshake keys after receiving a HANDSHAKE_DONE frame. + tc.writeFrames(packetType1RTT, + debugFrameHandshakeDone{}) + tc.writeFrames(packetTypeHandshake, + debugFrameConnectionCloseTransport{code: errInternal}) + tc.wantIdle("client has discarded Handshake keys, cannot read CONNECTION_CLOSE") + + tc.writeFrames(packetType1RTT, + debugFrameConnectionCloseTransport{code: errInternal}) + tc.wantFrame("client closes connection after 1-RTT CONNECTION_CLOSE", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errNo, + }) +} + +func TestConnKeysDiscardedServer(t *testing.T) { + tc := newTestConn(t, serverSide, func(c *tls.Config) { + c.SessionTicketsDisabled = true + }) + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.wantFrame("server sends Initial CRYPTO frame", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + tc.wantFrame("server sends Handshake CRYPTO frame", + packetTypeHandshake, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake], + }) + + // The server discards Initial keys after receiving a Handshake packet. + // The Handshake packet contains only the start of the client's CRYPTO flight here, + // to avoids completing the handshake yet. + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][:1], + }) + tc.writeFrames(packetTypeInitial, + debugFrameConnectionCloseTransport{code: errInternal}) + tc.wantIdle("server has discarded Initial keys, cannot read CONNECTION_CLOSE") + + // The server discards Handshake keys after sending a HANDSHAKE_DONE frame. + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + off: 1, + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][1:], + }) + tc.wantFrame("server sends HANDSHAKE_DONE after handshake completes", + packetType1RTT, debugFrameHandshakeDone{}) + tc.writeFrames(packetTypeHandshake, + debugFrameConnectionCloseTransport{code: errInternal}) + tc.wantIdle("server has discarded Handshake keys, cannot read CONNECTION_CLOSE") + + tc.writeFrames(packetType1RTT, + debugFrameConnectionCloseTransport{code: errInternal}) + tc.wantFrame("server closes connection after 1-RTT CONNECTION_CLOSE", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errNo, + }) +} + +func TestConnInvalidCryptoData(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.ignoreFrame(frameTypeAck) + + tc.wantFrame("client sends Initial CRYPTO frame", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + + // Render the server's response invalid. + // + // The client closes the connection with CRYPTO_ERROR. + // + // Changing the first byte will change the TLS message type, + // so we can reasonably assume that this is an unexpected_message alert (10). + tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][0] ^= 0x1 + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + tc.wantFrame("client closes connection due to TLS handshake error", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errTLSBase + 10, + }) +} + +func TestConnInvalidPeerCertificate(t *testing.T) { + tc := newTestConn(t, clientSide, func(c *tls.Config) { + c.VerifyPeerCertificate = func([][]byte, [][]*x509.Certificate) error { + return errors.New("I will not buy this certificate. It is scratched.") + } + }) + tc.ignoreFrame(frameTypeAck) + + tc.wantFrame("client sends Initial CRYPTO frame", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + tc.wantFrame("client closes connection due to rejecting server certificate", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errTLSBase + 42, // 42: bad_certificate + }) +} + +func TestConnHandshakeDoneSentToServer(t *testing.T) { + tc := newTestConn(t, serverSide) + tc.handshake() + + tc.writeFrames(packetType1RTT, + debugFrameHandshakeDone{}) + tc.wantFrame("server closes connection when client sends a HANDSHAKE_DONE frame", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errProtocolViolation, + }) +} + +func TestConnCryptoDataOutOfOrder(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.ignoreFrame(frameTypeAck) + + tc.wantFrame("client sends Initial CRYPTO frame", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.wantIdle("client is idle, server Handshake flight has not arrived") + + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + off: 15, + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][15:], + }) + tc.wantIdle("client is idle, server Handshake flight is not complete") + + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + off: 1, + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][1:20], + }) + tc.wantIdle("client is idle, server Handshake flight is still not complete") + + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][0:1], + }) + tc.wantFrame("client sends Handshake CRYPTO frame", + packetTypeHandshake, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake], + }) +} + +func TestConnCryptoBufferSizeExceeded(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.ignoreFrame(frameTypeAck) + + tc.wantFrame("client sends Initial CRYPTO frame", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + off: cryptoBufferSize, + data: []byte{0}, + }) + tc.wantFrame("client closes connection after server exceeds CRYPTO buffer", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errCryptoBufferExceeded, + }) +} diff --git a/internal/quic/tlsconfig_test.go b/internal/quic/tlsconfig_test.go new file mode 100644 index 0000000000..47bfb05983 --- /dev/null +++ b/internal/quic/tlsconfig_test.go @@ -0,0 +1,62 @@ +// 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 ( + "crypto/tls" + "strings" +) + +func newTestTLSConfig(side connSide) *tls.Config { + config := &tls.Config{ + InsecureSkipVerify: true, + CipherSuites: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + }, + MinVersion: tls.VersionTLS13, + } + if side == serverSide { + config.Certificates = []tls.Certificate{testCert} + } + return config +} + +var testCert = func() tls.Certificate { + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + panic(err) + } + return cert +}() + +// localhostCert is a PEM-encoded TLS cert with SAN IPs +// "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. +// generated from src/crypto/tls: +// go run generate_cert.go --ecdsa-curve P256 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var localhostCert = []byte(`-----BEGIN CERTIFICATE----- +MIIBrDCCAVKgAwIBAgIPCvPhO+Hfv+NW76kWxULUMAoGCCqGSM49BAMCMBIxEDAO +BgNVBAoTB0FjbWUgQ28wIBcNNzAwMTAxMDAwMDAwWhgPMjA4NDAxMjkxNjAwMDBa +MBIxEDAOBgNVBAoTB0FjbWUgQ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARh +WRF8p8X9scgW7JjqAwI9nYV8jtkdhqAXG9gyEgnaFNN5Ze9l3Tp1R9yCDBMNsGms +PyfMPe5Jrha/LmjgR1G9o4GIMIGFMA4GA1UdDwEB/wQEAwIChDATBgNVHSUEDDAK +BggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSOJri/wLQxq6oC +Y6ZImms/STbTljAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAA +AAAAAAAAAAAAATAKBggqhkjOPQQDAgNIADBFAiBUguxsW6TGhixBAdORmVNnkx40 +HjkKwncMSDbUaeL9jQIhAJwQ8zV9JpQvYpsiDuMmqCuW35XXil3cQ6Drz82c+fvE +-----END CERTIFICATE-----`) + +// localhostKey is the private key for localhostCert. +var localhostKey = []byte(testingKey(`-----BEGIN TESTING KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgY1B1eL/Bbwf/MDcs +rnvvWhFNr1aGmJJR59PdCN9lVVqhRANCAARhWRF8p8X9scgW7JjqAwI9nYV8jtkd +hqAXG9gyEgnaFNN5Ze9l3Tp1R9yCDBMNsGmsPyfMPe5Jrha/LmjgR1G9 +-----END TESTING KEY-----`)) + +// testingKey helps keep security scanners from getting excited about a private key in this file. +func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } diff --git a/internal/quic/transport_params.go b/internal/quic/transport_params.go index 416bfb8679..89ea69fb97 100644 --- a/internal/quic/transport_params.go +++ b/internal/quic/transport_params.go @@ -25,7 +25,7 @@ type transportParameters struct { initialMaxStreamDataUni int64 initialMaxStreamsBidi int64 initialMaxStreamsUni int64 - ackDelayExponent uint8 + ackDelayExponent int8 maxAckDelay time.Duration disableActiveMigration bool preferredAddrV4 netip.AddrPort @@ -220,7 +220,7 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { if v > 20 { return p, localTransportError(errTransportParameter) } - p.ackDelayExponent = uint8(v) + p.ackDelayExponent = int8(v) case paramMaxAckDelay: var v uint64 v, n = consumeVarint(val) From 08001ccbedb025e618cc203ed6d33973d5839a2f Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 21 Jul 2023 13:21:18 -0700 Subject: [PATCH 015/168] quic: debug logging of packets Log every packet sent and received to stdout when GODEBUG=quiclogpackets=1. For golang/go#58547 Change-Id: I904336017ea646ad6459557b44702bfe4c732ba9 Reviewed-on: https://go-review.googlesource.com/c/net/+/513439 Reviewed-by: Jonathan Amsterdam TryBot-Result: Gopher Robot Run-TryBot: Damien Neil --- internal/quic/conn.go | 4 +++ internal/quic/conn_recv.go | 6 ++++ internal/quic/conn_send.go | 9 +++++ internal/quic/log.go | 69 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 internal/quic/log.go diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 8130c549b9..ff03bd7f87 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -116,6 +116,10 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. return c, nil } +func (c *Conn) String() string { + return fmt.Sprintf("quic.Conn(%v,->%v)", c.side, c.peerAddr) +} + // confirmHandshake is called when the handshake is confirmed. // https://www.rfc-editor.org/rfc/rfc9001#section-4.1.2 func (c *Conn) confirmHandshake(now time.Time) { diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 7eb03e7279..3baa79a0cc 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -60,6 +60,9 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa return n } + if logPackets { + logInboundLongPacket(c, p) + } c.connIDState.handlePacket(c.side, p.ptype, p.srcConnID) ackEliciting := c.handleFrames(now, ptype, space, p.payload) c.acks[space].receive(now, space, p.num, ackEliciting) @@ -96,6 +99,9 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int { return len(buf) } + if logPackets { + logInboundShortPacket(c, p) + } ackEliciting := c.handleFrames(now, packetType1RTT, appDataSpace, p.payload) c.acks[appDataSpace].receive(now, appDataSpace, p.num, ackEliciting) return len(buf) diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 71d24e6f0f..62c9b62ec7 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -59,6 +59,9 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } c.w.startProtectedLongHeaderPacket(pnumMaxAcked, p) c.appendFrames(now, initialSpace, pnum, limit) + if logPackets { + logSentPacket(c, packetTypeInitial, pnum, p.srcConnID, p.dstConnID, c.w.payload()) + } sentInitial = c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, k, p) if sentInitial != nil { // Client initial packets need to be sent in a datagram padded to @@ -83,6 +86,9 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } c.w.startProtectedLongHeaderPacket(pnumMaxAcked, p) c.appendFrames(now, handshakeSpace, pnum, limit) + if logPackets { + logSentPacket(c, packetTypeHandshake, pnum, p.srcConnID, p.dstConnID, c.w.payload()) + } if sent := c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, k, p); sent != nil { c.loss.packetSent(now, handshakeSpace, sent) if c.side == clientSide { @@ -108,6 +114,9 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { c.w.appendPaddingTo(minimumClientInitialDatagramSize) pad = false } + if logPackets { + logSentPacket(c, packetType1RTT, pnum, nil, dstConnID, c.w.payload()) + } if sent := c.w.finish1RTTPacket(pnum, pnumMaxAcked, dstConnID, k); sent != nil { c.loss.packetSent(now, appDataSpace, sent) } diff --git a/internal/quic/log.go b/internal/quic/log.go new file mode 100644 index 0000000000..d7248343b0 --- /dev/null +++ b/internal/quic/log.go @@ -0,0 +1,69 @@ +// 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 ( + "fmt" + "os" + "strings" +) + +var logPackets bool + +// Parse GODEBUG settings. +// +// GODEBUG=quiclogpackets=1 -- log every packet sent and received. +func init() { + s := os.Getenv("GODEBUG") + for len(s) > 0 { + var opt string + opt, s, _ = strings.Cut(s, ",") + switch opt { + case "quiclogpackets=1": + logPackets = true + } + } +} + +func logInboundLongPacket(c *Conn, p longPacket) { + if !logPackets { + return + } + prefix := c.String() + fmt.Printf("%v recv %v %v\n", prefix, p.ptype, p.num) + logFrames(prefix+" <- ", p.payload) +} + +func logInboundShortPacket(c *Conn, p shortPacket) { + if !logPackets { + return + } + prefix := c.String() + fmt.Printf("%v recv 1-RTT %v\n", prefix, p.num) + logFrames(prefix+" <- ", p.payload) +} + +func logSentPacket(c *Conn, ptype packetType, pnum packetNumber, src, dst, payload []byte) { + if !logPackets || len(payload) == 0 { + return + } + prefix := c.String() + fmt.Printf("%v send %v %v\n", prefix, ptype, pnum) + logFrames(prefix+" -> ", payload) +} + +func logFrames(prefix string, payload []byte) { + for len(payload) > 0 { + f, n := parseDebugFrame(payload) + if n < 0 { + fmt.Printf("%vBAD DATA\n", prefix) + break + } + payload = payload[n:] + fmt.Printf("%v%v\n", prefix, f) + } +} From bd8ac9ecf8d3c89c10b91a3b40cb5f536a99635b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 20 Jul 2023 16:45:15 -0700 Subject: [PATCH 016/168] quic: fill out connection id handling Add support for sending and receiving NEW_CONNECTION_ID and RETIRE_CONNECTION_ID frames. Keep the peer supplied with up to 4 connection IDs. Retire connection IDs as required by the peer. Support connection IDs provided in the preferred_address transport parameter. RFC 9000, Section 5.1. For golang/go#58547 Change-Id: I015a69b94c40a6396e9f117a92c88acaf83c594e Reviewed-on: https://go-review.googlesource.com/c/net/+/513440 TryBot-Result: Gopher Robot Run-TryBot: Damien Neil Reviewed-by: Jonathan Amsterdam --- internal/quic/conn.go | 32 ++- internal/quic/conn_id.go | 238 +++++++++++++++++- internal/quic/conn_id_test.go | 422 +++++++++++++++++++++++++++++++- internal/quic/conn_loss.go | 6 + internal/quic/conn_loss_test.go | 65 +++++ internal/quic/conn_recv.go | 29 ++- internal/quic/conn_send.go | 19 +- internal/quic/conn_test.go | 74 ++++-- internal/quic/frame_debug.go | 5 +- internal/quic/packet_parser.go | 4 +- internal/quic/packet_writer.go | 7 +- internal/quic/ping_test.go | 2 +- internal/quic/quic.go | 10 + internal/quic/tls.go | 4 +- internal/quic/tls_test.go | 156 ++++++++++-- 15 files changed, 998 insertions(+), 75 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index ff03bd7f87..77ecea0d62 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -69,6 +69,7 @@ type connListener interface { type connTestHooks interface { nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any) handleTLSEvent(tls.QUICEvent) + newConnID(seq int64) ([]byte, error) } func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l connListener, hooks connTestHooks) (*Conn, error) { @@ -90,12 +91,12 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. c.msgc = make(chan any, 1) if c.side == clientSide { - if err := c.connIDState.initClient(newRandomConnID); err != nil { + if err := c.connIDState.initClient(c.newConnIDFunc()); err != nil { return nil, err } - initialConnID = c.connIDState.dstConnID() + initialConnID, _ = c.connIDState.dstConnID() } else { - if err := c.connIDState.initServer(newRandomConnID, initialConnID); err != nil { + if err := c.connIDState.initServer(c.newConnIDFunc(), initialConnID); err != nil { return nil, err } } @@ -154,11 +155,27 @@ func (c *Conn) discardKeys(now time.Time, space numberSpace) { } // receiveTransportParameters applies transport parameters sent by the peer. -func (c *Conn) receiveTransportParameters(p transportParameters) { +func (c *Conn) receiveTransportParameters(p transportParameters) error { c.peerAckDelayExponent = p.ackDelayExponent c.loss.setMaxAckDelay(p.maxAckDelay) + if err := c.connIDState.setPeerActiveConnIDLimit(p.activeConnIDLimit, c.newConnIDFunc()); err != nil { + return err + } + if p.preferredAddrConnID != nil { + var ( + seq int64 = 1 // sequence number of this conn id is 1 + retirePriorTo int64 = 0 // retire nothing + resetToken [16]byte + ) + copy(resetToken[:], p.preferredAddrResetToken) + if err := c.connIDState.handleNewConnID(seq, retirePriorTo, p.preferredAddrConnID, resetToken); err != nil { + return err + } + } // TODO: Many more transport parameters to come. + + return nil } type timerEvent struct{} @@ -295,3 +312,10 @@ func firstTime(a, b time.Time) time.Time { return b } } + +func (c *Conn) newConnIDFunc() newConnIDFunc { + if c.testHooks != nil { + return c.testHooks.newConnID + } + return newRandomConnID +} diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index deea70d326..561dea2c14 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -7,6 +7,7 @@ package quic import ( + "bytes" "crypto/rand" ) @@ -18,8 +19,16 @@ type connIDState struct { // Local IDs are usually issued by us, and remote IDs by the peer. // The exception is the transient destination connection ID sent in // a client's Initial packets, which is chosen by the client. + // + // These are []connID rather than []*connID to minimize allocations. local []connID remote []connID + + nextLocalSeq int64 + retireRemotePriorTo int64 // largest Retire Prior To value sent by the peer + peerActiveConnIDLimit int64 // peer's active_connection_id_limit transport parameter + + needSend bool } // A connID is a connection ID and associated metadata. @@ -32,12 +41,24 @@ type connID struct { // // For the transient destination ID in a client's Initial packet, this is -1. seq int64 + + // retired is set when the connection ID is retired. + retired bool + + // send is set when the connection ID's state needs to be sent to the peer. + // + // For local IDs, this indicates a new ID that should be sent + // in a NEW_CONNECTION_ID frame. + // + // For remote IDs, this indicates a retired ID that should be sent + // in a RETIRE_CONNECTION_ID frame. + send sentVal } func (s *connIDState) initClient(newID newConnIDFunc) error { // Client chooses its initial connection ID, and sends it // in the Source Connection ID field of the first Initial packet. - locid, err := newID() + locid, err := newID(0) if err != nil { return err } @@ -45,10 +66,11 @@ func (s *connIDState) initClient(newID newConnIDFunc) error { seq: 0, cid: locid, }) + s.nextLocalSeq = 1 // Client chooses an initial, transient connection ID for the server, // and sends it in the Destination Connection ID field of the first Initial packet. - remid, err := newID() + remid, err := newID(-1) if err != nil { return err } @@ -70,7 +92,7 @@ func (s *connIDState) initServer(newID newConnIDFunc, dstConnID []byte) error { // Server chooses a connection ID, and sends it in the Source Connection ID of // the response to the clent. - locid, err := newID() + locid, err := newID(0) if err != nil { return err } @@ -78,6 +100,7 @@ func (s *connIDState) initServer(newID newConnIDFunc, dstConnID []byte) error { seq: 0, cid: locid, }) + s.nextLocalSeq = 1 return nil } @@ -91,8 +114,44 @@ func (s *connIDState) srcConnID() []byte { } // dstConnID is the Destination Connection ID to use in a sent packet. -func (s *connIDState) dstConnID() []byte { - return s.remote[0].cid +func (s *connIDState) dstConnID() (cid []byte, ok bool) { + for i := range s.remote { + if !s.remote[i].retired { + return s.remote[i].cid, true + } + } + return nil, false +} + +// setPeerActiveConnIDLimit sets the active_connection_id_limit +// transport parameter received from the peer. +func (s *connIDState) setPeerActiveConnIDLimit(lim int64, newID newConnIDFunc) error { + s.peerActiveConnIDLimit = lim + return s.issueLocalIDs(newID) +} + +func (s *connIDState) issueLocalIDs(newID newConnIDFunc) error { + toIssue := min(int(s.peerActiveConnIDLimit), maxPeerActiveConnIDLimit) + for i := range s.local { + if s.local[i].seq != -1 && !s.local[i].retired { + toIssue-- + } + } + for toIssue > 0 { + cid, err := newID(s.nextLocalSeq) + if err != nil { + return err + } + s.local = append(s.local, connID{ + seq: s.nextLocalSeq, + cid: cid, + }) + s.local[len(s.local)-1].send.setUnsent() + s.nextLocalSeq++ + s.needSend = true + toIssue-- + } + return nil } // handlePacket updates the connection ID state during the handshake @@ -128,19 +187,184 @@ func (s *connIDState) handlePacket(side connSide, ptype packetType, srcConnID [] } } +func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken [16]byte) error { + if len(s.remote[0].cid) == 0 { + // "An endpoint that is sending packets with a zero-length + // Destination Connection ID MUST treat receipt of a NEW_CONNECTION_ID + // frame as a connection error of type PROTOCOL_VIOLATION." + // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.15-6 + return localTransportError(errProtocolViolation) + } + + if retire > s.retireRemotePriorTo { + s.retireRemotePriorTo = retire + } + + have := false // do we already have this connection ID? + active := 0 + for i := range s.remote { + rcid := &s.remote[i] + if !rcid.retired && rcid.seq < s.retireRemotePriorTo { + s.retireRemote(rcid) + } + if !rcid.retired { + active++ + } + if rcid.seq == seq { + if !bytes.Equal(rcid.cid, cid) { + return localTransportError(errProtocolViolation) + } + have = true // yes, we've seen this sequence number + } + } + + if !have { + // This is a new connection ID that we have not seen before. + // + // We could take steps to keep the list of remote connection IDs + // sorted by sequence number, but there's no particular need + // so we don't bother. + s.remote = append(s.remote, connID{ + seq: seq, + cid: cloneBytes(cid), + }) + if seq < s.retireRemotePriorTo { + // This ID was already retired by a previous NEW_CONNECTION_ID frame. + s.retireRemote(&s.remote[len(s.remote)-1]) + } else { + active++ + } + } + + if active > activeConnIDLimit { + // Retired connection IDs (including newly-retired ones) do not count + // against the limit. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-5.1.1-5 + return localTransportError(errConnectionIDLimit) + } + + // "An endpoint SHOULD limit the number of connection IDs it has retired locally + // for which RETIRE_CONNECTION_ID frames have not yet been acknowledged." + // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.2-6 + // + // Set a limit of four times the active_connection_id_limit for + // the total number of remote connection IDs we keep state for locally. + if len(s.remote) > 4*activeConnIDLimit { + return localTransportError(errConnectionIDLimit) + } + + return nil +} + +// retireRemote marks a remote connection ID as retired. +func (s *connIDState) retireRemote(rcid *connID) { + rcid.retired = true + rcid.send.setUnsent() + s.needSend = true +} + +func (s *connIDState) handleRetireConnID(seq int64, newID newConnIDFunc) error { + if seq >= s.nextLocalSeq { + return localTransportError(errProtocolViolation) + } + for i := range s.local { + if s.local[i].seq == seq { + s.local = append(s.local[:i], s.local[i+1:]...) + break + } + } + s.issueLocalIDs(newID) + return nil +} + +func (s *connIDState) ackOrLossNewConnectionID(pnum packetNumber, seq int64, fate packetFate) { + for i := range s.local { + if s.local[i].seq != seq { + continue + } + s.local[i].send.ackOrLoss(pnum, fate) + if fate != packetAcked { + s.needSend = true + } + return + } +} + +func (s *connIDState) ackOrLossRetireConnectionID(pnum packetNumber, seq int64, fate packetFate) { + for i := 0; i < len(s.remote); i++ { + if s.remote[i].seq != seq { + continue + } + if fate == packetAcked { + // We have retired this connection ID, and the peer has acked. + // Discard its state completely. + s.remote = append(s.remote[:i], s.remote[i+1:]...) + } else { + // RETIRE_CONNECTION_ID frame was lost, mark for retransmission. + s.needSend = true + s.remote[i].send.ackOrLoss(pnum, fate) + } + return + } +} + +// appendFrames appends NEW_CONNECTION_ID and RETIRE_CONNECTION_ID frames +// to the current packet. +// +// It returns true if no more frames need appending, +// false if not everything fit in the current packet. +func (s *connIDState) appendFrames(w *packetWriter, pnum packetNumber, pto bool) bool { + if !s.needSend && !pto { + // Fast path: We don't need to send anything. + return true + } + retireBefore := int64(0) + if s.local[0].seq != -1 { + retireBefore = s.local[0].seq + } + for i := range s.local { + if !s.local[i].send.shouldSendPTO(pto) { + continue + } + if !w.appendNewConnectionIDFrame( + s.local[i].seq, + retireBefore, + s.local[i].cid, + [16]byte{}, // TODO: stateless reset token + ) { + return false + } + s.local[i].send.setSent(pnum) + } + for i := range s.remote { + if !s.remote[i].send.shouldSendPTO(pto) { + continue + } + if !w.appendRetireConnectionIDFrame(s.remote[i].seq) { + return false + } + s.remote[i].send.setSent(pnum) + } + s.needSend = false + return true +} + func cloneBytes(b []byte) []byte { n := make([]byte, len(b)) copy(n, b) return n } -type newConnIDFunc func() ([]byte, error) +type newConnIDFunc func(seq int64) ([]byte, error) -func newRandomConnID() ([]byte, error) { +func newRandomConnID(_ int64) ([]byte, error) { // It is not necessary for connection IDs to be cryptographically secure, // but it doesn't hurt. id := make([]byte, connIDLen) if _, err := rand.Read(id); err != nil { + // TODO: Surface this error as a metric or log event or something. + // rand.Read really shouldn't ever fail, but if it does, we should + // have a way to inform the user. return nil, err } return id, nil diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index 7c31e9d560..74905578dc 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -7,7 +7,10 @@ package quic import ( + "bytes" + "crypto/tls" "fmt" + "net/netip" "reflect" "testing" ) @@ -22,14 +25,16 @@ func TestConnIDClientHandshake(t *testing.T) { if got, want := string(s.srcConnID()), "local-1"; got != want { t.Errorf("after initClient: srcConnID = %q, want %q", got, want) } - if got, want := string(s.dstConnID()), "local-2"; got != want { + dstConnID, _ := s.dstConnID() + if got, want := string(dstConnID), "local-2"; got != want { t.Errorf("after initClient: dstConnID = %q, want %q", got, want) } // The server's first Initial packet provides the client with a // non-transient remote connection ID. s.handlePacket(clientSide, packetTypeInitial, []byte("remote-1")) - if got, want := string(s.dstConnID()), "remote-1"; got != want { + dstConnID, _ = s.dstConnID() + if got, want := string(dstConnID), "remote-1"; got != want { t.Errorf("after receiving Initial: dstConnID = %q, want %q", got, want) } @@ -59,7 +64,8 @@ func TestConnIDServerHandshake(t *testing.T) { if got, want := string(s.srcConnID()), "local-1"; got != want { t.Errorf("after initClient: srcConnID = %q, want %q", got, want) } - if got, want := string(s.dstConnID()), "remote-1"; got != want { + dstConnID, _ := s.dstConnID() + if got, want := string(dstConnID), "remote-1"; got != want { t.Errorf("after initClient: dstConnID = %q, want %q", got, want) } @@ -95,15 +101,421 @@ func TestConnIDServerHandshake(t *testing.T) { func newConnIDSequence() newConnIDFunc { var n uint64 - return func() ([]byte, error) { + return func(_ int64) ([]byte, error) { n++ return []byte(fmt.Sprintf("local-%v", n)), nil } } func TestNewRandomConnID(t *testing.T) { - cid, err := newRandomConnID() + cid, err := newRandomConnID(0) if len(cid) != connIDLen || err != nil { t.Fatalf("newConnID() = %x, %v; want %v bytes", cid, connIDLen, err) } } + +func TestConnIDPeerRequestsManyIDs(t *testing.T) { + // "An endpoint SHOULD ensure that its peer has a sufficient number + // of available and unused connection IDs." + // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4 + // + // "An endpoint MAY limit the total number of connection IDs + // issued for each connection [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-6 + // + // Peer requests 100 connection IDs. + // We give them 4 in total. + tc := newTestConn(t, serverSide, func(p *transportParameters) { + p.activeConnIDLimit = 100 + }) + tc.ignoreFrame(frameTypeAck) + tc.ignoreFrame(frameTypeCrypto) + + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.wantFrame("provide additional connection ID 1", + packetType1RTT, debugFrameNewConnectionID{ + seq: 1, + connID: testLocalConnID(1), + }) + tc.wantFrame("provide additional connection ID 2", + packetType1RTT, debugFrameNewConnectionID{ + seq: 2, + connID: testLocalConnID(2), + }) + tc.wantFrame("provide additional connection ID 3", + packetType1RTT, debugFrameNewConnectionID{ + seq: 3, + connID: testLocalConnID(3), + }) + tc.wantIdle("connection ID limit reached, no more to provide") +} + +func TestConnIDPeerProvidesTooManyIDs(t *testing.T) { + // "An endpoint MUST NOT provide more connection IDs than the peer's limit." + // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4 + tc := newTestConn(t, serverSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + connID: testLocalConnID(2), + }) + tc.wantFrame("peer provided 3 connection IDs, our limit is 2", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errConnectionIDLimit, + }) +} + +func TestConnIDPeerTemporarilyExceedsActiveConnIDLimit(t *testing.T) { + // "An endpoint MAY send connection IDs that temporarily exceed a peer's limit + // if the NEW_CONNECTION_ID frame also requires the retirement of any excess [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4 + tc := newTestConn(t, serverSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + retirePriorTo: 2, + seq: 2, + connID: testPeerConnID(2), + }, debugFrameNewConnectionID{ + retirePriorTo: 2, + seq: 3, + connID: testPeerConnID(3), + }) + tc.wantFrame("peer requested we retire conn id 0", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + tc.wantFrame("peer requested we retire conn id 1", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 1, + }) +} + +func TestConnIDPeerRetiresConnID(t *testing.T) { + // "An endpoint SHOULD supply a new connection ID when the peer retires a connection ID." + // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-6 + for _, side := range []connSide{ + clientSide, + serverSide, + } { + t.Run(side.String(), func(t *testing.T) { + tc := newTestConn(t, side) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetType1RTT, + debugFrameRetireConnectionID{ + seq: 0, + }) + tc.wantFrame("provide replacement connection ID", + packetType1RTT, debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 1, + connID: testLocalConnID(2), + }) + }) + } +} + +func TestConnIDPeerWithZeroLengthConnIDSendsNewConnectionID(t *testing.T) { + // An endpoint that selects a zero-length connection ID during the handshake + // cannot issue a new connection ID." + // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-8 + tc := newTestConn(t, clientSide, func(c *tls.Config) { + c.SessionTicketsDisabled = true + }) + tc.peerConnID = []byte{} + tc.ignoreFrame(frameTypeAck) + tc.uncheckedHandshake() + + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 1, + connID: testPeerConnID(1), + }) + tc.wantFrame("invalid NEW_CONNECTION_ID: previous conn id is zero-length", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errProtocolViolation, + }) +} + +func TestConnIDPeerRequestsRetirement(t *testing.T) { + // "Upon receipt of an increased Retire Prior To field, the peer MUST + // stop using the corresponding connection IDs and retire them with + // RETIRE_CONNECTION_ID frames [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.2-5 + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 1, + connID: testPeerConnID(2), + }) + tc.wantFrame("peer asked for conn id 0 to be retired", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + if got, want := tc.sentFramePacket.dstConnID, testPeerConnID(1); !bytes.Equal(got, want) { + t.Fatalf("used destination conn id {%x}, want {%x}", got, want) + } +} + +func TestConnIDPeerDoesNotAcknowledgeRetirement(t *testing.T) { + // "An endpoint SHOULD limit the number of connection IDs it has retired locally + // for which RETIRE_CONNECTION_ID frames have not yet been acknowledged." + // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.2-6 + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.ignoreFrame(frameTypeRetireConnectionID) + + // Send a number of NEW_CONNECTION_ID frames, each retiring an old one. + for seq := int64(0); seq < 7; seq++ { + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: seq + 2, + retirePriorTo: seq + 1, + connID: testPeerConnID(seq + 2), + }) + // We're ignoring the RETIRE_CONNECTION_ID frames. + } + tc.wantFrame("number of retired, unacked conn ids is too large", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errConnectionIDLimit, + }) +} + +func TestConnIDRepeatedNewConnectionIDFrame(t *testing.T) { + // "Receipt of the same [NEW_CONNECTION_ID] frame multiple times + // MUST NOT be treated as a connection error. + // https://www.rfc-editor.org/rfc/rfc9000#section-19.15-7 + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + for i := 0; i < 4; i++ { + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 1, + connID: testPeerConnID(2), + }) + } + tc.wantFrame("peer asked for conn id to be retired", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + tc.wantIdle("repeated NEW_CONNECTION_ID frames are not an error") +} + +func TestConnIDForSequenceNumberChanges(t *testing.T) { + // "[...] if a sequence number is used for different connection IDs, + // the endpoint MAY treat that receipt as a connection error + // of type PROTOCOL_VIOLATION." + // https://www.rfc-editor.org/rfc/rfc9000#section-19.15-8 + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.ignoreFrame(frameTypeRetireConnectionID) + + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 1, + connID: testPeerConnID(2), + }) + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 1, + connID: testPeerConnID(3), + }) + tc.wantFrame("connection ID for sequence 0 has changed", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errProtocolViolation, + }) +} + +func TestConnIDRetirePriorToAfterNewConnID(t *testing.T) { + // "Receiving a value in the Retire Prior To field that is greater than + // that in the Sequence Number field MUST be treated as a connection error + // of type FRAME_ENCODING_ERROR. + // https://www.rfc-editor.org/rfc/rfc9000#section-19.15-9 + tc := newTestConn(t, serverSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + retirePriorTo: 3, + seq: 2, + connID: testPeerConnID(2), + }) + tc.wantFrame("invalid NEW_CONNECTION_ID: retired the new conn id", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errFrameEncoding, + }) +} + +func TestConnIDAlreadyRetired(t *testing.T) { + // "An endpoint that receives a NEW_CONNECTION_ID frame with a + // sequence number smaller than the Retire Prior To field of a + // previously received NEW_CONNECTION_ID frame MUST send a + // corresponding RETIRE_CONNECTION_ID frame [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-19.15-11 + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 4, + retirePriorTo: 3, + connID: testPeerConnID(4), + }) + tc.wantFrame("peer asked for conn id to be retired", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + tc.wantFrame("peer asked for conn id to be retired", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 1, + }) + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 0, + connID: testPeerConnID(2), + }) + tc.wantFrame("NEW_CONNECTION_ID was for an already-retired ID", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 2, + }) +} + +func TestConnIDRepeatedRetireConnectionIDFrame(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + for i := 0; i < 4; i++ { + tc.writeFrames(packetType1RTT, + debugFrameRetireConnectionID{ + seq: 0, + }) + } + tc.wantFrame("issue new conn id after peer retires one", + packetType1RTT, debugFrameNewConnectionID{ + retirePriorTo: 1, + seq: 2, + connID: testLocalConnID(2), + }) + tc.wantIdle("repeated RETIRE_CONNECTION_ID frames are not an error") +} + +func TestConnIDRetiredUnsent(t *testing.T) { + // "Receipt of a RETIRE_CONNECTION_ID frame containing a sequence number + // greater than any previously sent to the peer MUST be treated as a + // connection error of type PROTOCOL_VIOLATION." + // https://www.rfc-editor.org/rfc/rfc9000#section-19.16-7 + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetType1RTT, + debugFrameRetireConnectionID{ + seq: 2, + }) + tc.wantFrame("invalid NEW_CONNECTION_ID: previous conn id is zero-length", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errProtocolViolation, + }) +} + +func TestConnIDUsePreferredAddressConnID(t *testing.T) { + // Peer gives us a connection ID in the preferred address transport parameter. + // We don't use the preferred address at this time, but we should use the + // connection ID. (It isn't tied to any specific address.) + // + // This test will probably need updating if/when we start using the preferred address. + cid := testPeerConnID(10) + tc := newTestConn(t, serverSide, func(p *transportParameters) { + p.preferredAddrV4 = netip.MustParseAddrPort("0.0.0.0:0") + p.preferredAddrV6 = netip.MustParseAddrPort("[::0]:0") + p.preferredAddrConnID = cid + p.preferredAddrResetToken = make([]byte, 16) + }) + tc.uncheckedHandshake() + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 1, + connID: []byte{0xff}, + }) + tc.wantFrame("peer asked for conn id 0 to be retired", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + if got, want := tc.sentFramePacket.dstConnID, cid; !bytes.Equal(got, want) { + t.Fatalf("used destination conn id {%x}, want {%x} from preferred address transport parameter", got, want) + } +} + +func TestConnIDPeerProvidesPreferredAddrAndTooManyConnIDs(t *testing.T) { + // Peer gives us more conn ids than our advertised limit, + // including a conn id in the preferred address transport parameter. + cid := testPeerConnID(10) + tc := newTestConn(t, serverSide, func(p *transportParameters) { + p.preferredAddrV4 = netip.MustParseAddrPort("0.0.0.0:0") + p.preferredAddrV6 = netip.MustParseAddrPort("[::0]:0") + p.preferredAddrConnID = cid + p.preferredAddrResetToken = make([]byte, 16) + }) + tc.uncheckedHandshake() + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 0, + connID: testPeerConnID(2), + }) + tc.wantFrame("peer provided 3 connection IDs, our limit is 2", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errConnectionIDLimit, + }) +} + +func TestConnIDPeerWithZeroLengthIDProvidesPreferredAddr(t *testing.T) { + // Peer gives us more conn ids than our advertised limit, + // including a conn id in the preferred address transport parameter. + tc := newTestConn(t, serverSide, func(p *transportParameters) { + p.preferredAddrV4 = netip.MustParseAddrPort("0.0.0.0:0") + p.preferredAddrV6 = netip.MustParseAddrPort("[::0]:0") + p.preferredAddrConnID = testPeerConnID(1) + p.preferredAddrResetToken = make([]byte, 16) + }) + tc.peerConnID = []byte{} + + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.wantFrame("peer with zero-length connection ID tried to provide another in transport parameters", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errProtocolViolation, + }) +} diff --git a/internal/quic/conn_loss.go b/internal/quic/conn_loss.go index 6cb459c33f..57570d0861 100644 --- a/internal/quic/conn_loss.go +++ b/internal/quic/conn_loss.go @@ -44,6 +44,12 @@ func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetF case frameTypeCrypto: start, end := sent.nextRange() c.crypto[space].ackOrLoss(start, end, fate) + case frameTypeNewConnectionID: + seq := int64(sent.nextInt()) + c.connIDState.ackOrLossNewConnectionID(sent.num, seq, fate) + case frameTypeRetireConnectionID: + seq := int64(sent.nextInt()) + c.connIDState.ackOrLossRetireConnectionID(sent.num, seq, fate) case frameTypeHandshakeDone: c.handshakeConfirmed.ackOrLoss(sent.num, fate) } diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index be4f5fb2c8..021c86c876 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -93,6 +93,11 @@ func TestLostCRYPTOFrame(t *testing.T) { packetTypeHandshake, debugFrameCrypto{ data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake], }) + tc.wantFrame("client provides server with an additional connection ID", + packetType1RTT, debugFrameNewConnectionID{ + seq: 1, + connID: testLocalConnID(1), + }) tc.triggerLossOrPTO(packetTypeHandshake, pto) tc.wantFrame("client resends Handshake CRYPTO frame", packetTypeHandshake, debugFrameCrypto{ @@ -101,6 +106,61 @@ func TestLostCRYPTOFrame(t *testing.T) { }) } +func TestLostNewConnectionIDFrame(t *testing.T) { + // "New connection IDs are [...] retransmitted if the packet containing them is lost." + // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.13 + lostFrameTest(t, func(t *testing.T, pto bool) { + tc := newTestConn(t, serverSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetType1RTT, + debugFrameRetireConnectionID{ + seq: 1, + }) + tc.wantFrame("provide a new connection ID after peer retires old one", + packetType1RTT, debugFrameNewConnectionID{ + seq: 2, + connID: testLocalConnID(2), + }) + + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("resend new connection ID", + packetType1RTT, debugFrameNewConnectionID{ + seq: 2, + connID: testLocalConnID(2), + }) + }) +} + +func TestLostRetireConnectionIDFrame(t *testing.T) { + // "[...] retired connection IDs are [...] retransmitted + // if the packet containing them is lost." + // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.13 + lostFrameTest(t, func(t *testing.T, pto bool) { + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 1, + connID: testPeerConnID(2), + }) + tc.wantFrame("peer requested connection id be retired", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("resend RETIRE_CONNECTION_ID", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + }) +} + func TestLostHandshakeDoneFrame(t *testing.T) { // "The HANDSHAKE_DONE frame MUST be retransmitted until it is acknowledged." // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.16 @@ -120,6 +180,11 @@ func TestLostHandshakeDoneFrame(t *testing.T) { packetTypeHandshake, debugFrameCrypto{ data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake], }) + tc.wantFrame("server provides an additional connection ID", + packetType1RTT, debugFrameNewConnectionID{ + seq: 1, + connID: testLocalConnID(1), + }) tc.writeFrames(packetTypeHandshake, debugFrameCrypto{ data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 3baa79a0cc..7992a619f4 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -211,7 +211,12 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, if !frameOK(c, ptype, __01) { return } - _, _, _, _, n = consumeNewConnectionIDFrame(payload) + n = c.handleNewConnectionIDFrame(now, space, payload) + case frameTypeRetireConnectionID: + if !frameOK(c, ptype, __01) { + return + } + n = c.handleRetireConnectionIDFrame(now, space, payload) case frameTypeConnectionCloseTransport: // CONNECTION_CLOSE is OK in all spaces. _, _, _, n = consumeConnectionCloseTransportFrame(payload) @@ -285,6 +290,28 @@ func (c *Conn) handleCryptoFrame(now time.Time, space numberSpace, payload []byt return n } +func (c *Conn) handleNewConnectionIDFrame(now time.Time, space numberSpace, payload []byte) int { + seq, retire, connID, resetToken, n := consumeNewConnectionIDFrame(payload) + if n < 0 { + return -1 + } + if err := c.connIDState.handleNewConnID(seq, retire, connID, resetToken); err != nil { + c.abort(now, err) + } + return n +} + +func (c *Conn) handleRetireConnectionIDFrame(now time.Time, space numberSpace, payload []byte) int { + seq, n := consumeRetireConnectionIDFrame(payload) + if n < 0 { + return -1 + } + if err := c.connIDState.handleRetireConnID(seq, c.newConnIDFunc()); err != nil { + c.abort(now, err) + } + return n +} + func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payload []byte) int { if c.side == serverSide { // Clients should never send HANDSHAKE_DONE. diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 62c9b62ec7..d410548a98 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -44,6 +44,13 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // Prepare to write a datagram of at most maxSendSize bytes. c.w.reset(c.loss.maxSendSize()) + dstConnID, ok := c.connIDState.dstConnID() + if !ok { + // It is currently not possible for us to end up without a connection ID, + // but handle the case anyway. + return time.Time{} + } + // Initial packet. pad := false var sentInitial *sentPacket @@ -54,7 +61,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { ptype: packetTypeInitial, version: 1, num: pnum, - dstConnID: c.connIDState.dstConnID(), + dstConnID: dstConnID, srcConnID: c.connIDState.srcConnID(), } c.w.startProtectedLongHeaderPacket(pnumMaxAcked, p) @@ -81,7 +88,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { ptype: packetTypeHandshake, version: 1, num: pnum, - dstConnID: c.connIDState.dstConnID(), + dstConnID: dstConnID, srcConnID: c.connIDState.srcConnID(), } c.w.startProtectedLongHeaderPacket(pnumMaxAcked, p) @@ -104,7 +111,6 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { if k := c.wkeys[appDataSpace]; k.isSet() { pnumMaxAcked := c.acks[appDataSpace].largestSeen() pnum := c.loss.nextNumber(appDataSpace) - dstConnID := c.connIDState.dstConnID() c.w.start1RTTPacket(pnum, pnumMaxAcked, dstConnID) c.appendFrames(now, appDataSpace, pnum, limit) if pad && len(c.w.payload()) > 0 { @@ -233,6 +239,13 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, return int64(len(b)) }) + // NEW_CONNECTION_ID, RETIRE_CONNECTION_ID + if space == appDataSpace { + if !c.connIDState.appendFrames(&c.w, pnum, pto) { + return + } + } + // Test-only PING frames. if space == c.testSendPingSpace && c.testSendPing.shouldSendPTO(pto) { if !c.w.appendPingFrame() { diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 511fb97a0a..317ca8f81f 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -128,19 +128,16 @@ type testConn struct { cryptoDataIn map[tls.QUICEncryptionLevel][]byte peerTLSConn *tls.QUICConn - localConnID []byte - transientConnID []byte - // Information about the conn's (fake) peer. peerConnID []byte // source conn id of peer's packets peerNextPacketNum [numberSpaceCount]packetNumber // next packet number to use // Datagrams, packets, and frames sent by the conn, // but not yet processed by the test. - sentDatagrams [][]byte - sentPackets []*testPacket - sentFrames []debugFrame - sentFramePacketType packetType + sentDatagrams [][]byte + sentPackets []*testPacket + sentFrames []debugFrame + sentFramePacket *testPacket // Frame types to ignore in tests. ignoreFrames map[byte]bool @@ -162,7 +159,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { tc := &testConn{ t: t, now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - peerConnID: []byte{0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5}, + peerConnID: testPeerConnID(0), ignoreFrames: map[byte]bool{ frameTypePadding: true, // ignore PADDING by default }, @@ -179,6 +176,8 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { switch o := o.(type) { case func(*tls.Config): o(config.TLSConfig) + case func(p *transportParameters): + o(&peerProvidedParams) default: t.Fatalf("unknown newTestConn option %T", o) } @@ -189,7 +188,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { // The initial connection ID for the server is chosen by the client. // When creating a server-side connection, pick a random connection ID here. var err error - initialConnID, err = newRandomConnID() + initialConnID, err = newRandomConnID(0) if err != nil { tc.t.Fatal(err) } @@ -217,14 +216,6 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { } tc.conn = conn - if side == serverSide { - tc.transientConnID = tc.conn.connIDState.local[0].cid - tc.localConnID = tc.conn.connIDState.local[1].cid - } else if side == clientSide { - tc.transientConnID = tc.conn.connIDState.remote[0].cid - tc.localConnID = tc.conn.connIDState.local[0].cid - } - tc.wkeys[initialSpace].k = conn.wkeys[initialSpace] tc.rkeys[initialSpace].k = conn.rkeys[initialSpace] @@ -326,7 +317,11 @@ func (tc *testConn) write(d *testDatagram) { if p.num >= tc.peerNextPacketNum[space] { tc.peerNextPacketNum[space] = p.num + 1 } - buf = append(buf, tc.encodeTestPacket(p)...) + pad := 0 + if p.ptype == packetType1RTT { + pad = d.paddedSize + } + buf = append(buf, tc.encodeTestPacket(p, pad)...) } for len(buf) < d.paddedSize { buf = append(buf, 0) @@ -407,12 +402,12 @@ func (tc *testConn) readFrame() (debugFrame, packetType) { if p == nil { return nil, packetTypeInvalid } - tc.sentFramePacketType = p.ptype + tc.sentFramePacket = p tc.sentFrames = p.frames } f := tc.sentFrames[0] tc.sentFrames = tc.sentFrames[1:] - return f, tc.sentFramePacketType + return f, tc.sentFramePacket.ptype } // wantDatagram indicates that we expect the Conn to send a datagram. @@ -462,7 +457,7 @@ func (tc *testConn) wantIdle(expectation string) { } } -func (tc *testConn) encodeTestPacket(p *testPacket) []byte { +func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { tc.t.Helper() var w packetWriter w.reset(1200) @@ -486,6 +481,7 @@ func (tc *testConn) encodeTestPacket(p *testPacket) []byte { tc.t.Fatalf("sending packet with no %v keys available", space) return nil } + w.appendPaddingTo(pad) if p.ptype != packetType1RTT { w.finishProtectedLongHeaderPacket(pnumMaxAcked, tc.rkeys[space].k, longPacket{ ptype: p.ptype, @@ -504,6 +500,7 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { tc.t.Helper() bufSize := len(buf) d := &testDatagram{} + size := len(buf) for len(buf) > 0 { if buf[0] == 0 { d.paddedSize = bufSize @@ -552,6 +549,20 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { buf = buf[n:] } } + // This is rather hackish: If the last frame in the last packet + // in the datagram is PADDING, then remove it and record + // the padded size in the testDatagram.paddedSize. + // + // This makes it easier to write a test that expects a datagram + // padded to 1200 bytes. + if len(d.packets) > 0 && len(d.packets[len(d.packets)-1].frames) > 0 { + p := d.packets[len(d.packets)-1] + f := p.frames[len(p.frames)-1] + if _, ok := f.(debugFramePadding); ok { + p.frames = p.frames[:len(p.frames)-1] + d.paddedSize = size + } + } return d } @@ -686,6 +697,27 @@ func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.T return tc.now, m } +func (tc *testConnHooks) newConnID(seq int64) ([]byte, error) { + return testLocalConnID(seq), nil +} + +// testLocalConnID returns the connection ID with a given sequence number +// used by a Conn under test. +func testLocalConnID(seq int64) []byte { + cid := make([]byte, connIDLen) + copy(cid, []byte{0xc0, 0xff, 0xee}) + cid[len(cid)-1] = byte(seq) + return cid +} + +// testPeerConnID returns the connection ID with a given sequence number +// used by the fake peer of a Conn under test. +func testPeerConnID(seq int64) []byte { + // Use a different length than we choose for our own conn ids, + // to help catch any bad assumptions. + return []byte{0xbe, 0xee, 0xff, byte(seq)} +} + // testConnListener implements connListener. type testConnListener testConn diff --git a/internal/quic/frame_debug.go b/internal/quic/frame_debug.go index 3009a04507..7a5aee57b1 100644 --- a/internal/quic/frame_debug.go +++ b/internal/quic/frame_debug.go @@ -386,10 +386,7 @@ func (f debugFrameNewConnectionID) write(w *packetWriter) bool { // debugFrameRetireConnectionID is a NEW_CONNECTION_ID frame. type debugFrameRetireConnectionID struct { - seq uint64 - retirePriorTo uint64 - connID []byte - token [16]byte + seq int64 } func parseDebugFrameRetireConnectionID(b []byte) (f debugFrameRetireConnectionID, n int) { diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index c22f031038..0520078974 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -454,10 +454,10 @@ func consumeNewConnectionIDFrame(b []byte) (seq, retire int64, connID []byte, re return seq, retire, connID, resetToken, n } -func consumeRetireConnectionIDFrame(b []byte) (seq uint64, n int) { +func consumeRetireConnectionIDFrame(b []byte) (seq int64, n int) { n = 1 var nn int - seq, nn = consumeVarint(b[n:]) + seq, nn = consumeVarintInt64(b[n:]) if nn < 0 { return 0, -1 } diff --git a/internal/quic/packet_writer.go b/internal/quic/packet_writer.go index 6c4c452cdd..a80b4711ee 100644 --- a/internal/quic/packet_writer.go +++ b/internal/quic/packet_writer.go @@ -482,13 +482,14 @@ func (w *packetWriter) appendNewConnectionIDFrame(seq, retirePriorTo int64, conn return true } -func (w *packetWriter) appendRetireConnectionIDFrame(seq uint64) (added bool) { - if w.avail() < 1+sizeVarint(seq) { +func (w *packetWriter) appendRetireConnectionIDFrame(seq int64) (added bool) { + if w.avail() < 1+sizeVarint(uint64(seq)) { return false } w.b = append(w.b, frameTypeRetireConnectionID) - w.b = appendVarint(w.b, seq) + w.b = appendVarint(w.b, uint64(seq)) w.sent.appendAckElicitingFrame(frameTypeRetireConnectionID) + w.sent.appendInt(uint64(seq)) return true } diff --git a/internal/quic/ping_test.go b/internal/quic/ping_test.go index c370aaf1d8..a8fdf2567e 100644 --- a/internal/quic/ping_test.go +++ b/internal/quic/ping_test.go @@ -37,7 +37,7 @@ func TestAck(t *testing.T) { tc.wantFrame("connection should respond to ack-eliciting packet with an ACK frame", packetType1RTT, debugFrameAck{ - ranges: []i64range[packetNumber]{{0, 3}}, + ranges: []i64range[packetNumber]{{0, 4}}, }, ) } diff --git a/internal/quic/quic.go b/internal/quic/quic.go index a61c91f16b..84ce2bda13 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -35,6 +35,16 @@ const ( ackDelayExponent = 3 // ack_delay_exponent maxAckDelay = 25 * time.Millisecond // max_ack_delay + + // The active_conn_id_limit transport parameter is the maximum + // number of connection IDs from the peer we're willing to store. + // + // maxPeerActiveConnIDLimit is the maximum number of connection IDs + // we're willing to send to the peer. + // + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-6.2.1 + activeConnIDLimit = 2 + maxPeerActiveConnIDLimit = 4 ) // Local timer granularity. diff --git a/internal/quic/tls.go b/internal/quic/tls.go index 4306a3e46d..ed848c6a13 100644 --- a/internal/quic/tls.go +++ b/internal/quic/tls.go @@ -83,7 +83,9 @@ func (c *Conn) handleTLSEvents(now time.Time) error { if err != nil { return err } - c.receiveTransportParameters(params) + if err := c.receiveTransportParameters(params); err != nil { + return err + } } } } diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index df07820086..3768dc0c07 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -63,15 +63,26 @@ func (tc *testConn) handshake() { func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { var ( - clientConnID []byte - serverConnID []byte + clientConnIDs [][]byte + serverConnIDs [][]byte + transientConnID []byte ) + localConnIDs := [][]byte{ + testLocalConnID(0), + testLocalConnID(1), + } + peerConnIDs := [][]byte{ + testPeerConnID(0), + testPeerConnID(1), + } if tc.conn.side == clientSide { - clientConnID = tc.localConnID - serverConnID = tc.peerConnID + clientConnIDs = localConnIDs + serverConnIDs = peerConnIDs + transientConnID = testLocalConnID(-1) } else { - clientConnID = tc.peerConnID - serverConnID = tc.localConnID + clientConnIDs = peerConnIDs + serverConnIDs = localConnIDs + transientConnID = []byte{0xde, 0xad, 0xbe, 0xef} } return []*testDatagram{{ // Client Initial @@ -79,21 +90,21 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { ptype: packetTypeInitial, num: 0, version: 1, - srcConnID: clientConnID, - dstConnID: tc.transientConnID, + srcConnID: clientConnIDs[0], + dstConnID: transientConnID, frames: []debugFrame{ debugFrameCrypto{}, }, }}, paddedSize: 1200, }, { - // Server Initial + Handshake + // Server Initial + Handshake + 1-RTT packets: []*testPacket{{ ptype: packetTypeInitial, num: 0, version: 1, - srcConnID: serverConnID, - dstConnID: clientConnID, + srcConnID: serverConnIDs[0], + dstConnID: clientConnIDs[0], frames: []debugFrame{ debugFrameAck{ ranges: []i64range[packetNumber]{{0, 1}}, @@ -104,20 +115,30 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { ptype: packetTypeHandshake, num: 0, version: 1, - srcConnID: serverConnID, - dstConnID: clientConnID, + srcConnID: serverConnIDs[0], + dstConnID: clientConnIDs[0], frames: []debugFrame{ debugFrameCrypto{}, }, + }, { + ptype: packetType1RTT, + num: 0, + dstConnID: clientConnIDs[0], + frames: []debugFrame{ + debugFrameNewConnectionID{ + seq: 1, + connID: serverConnIDs[1], + }, + }, }}, }, { - // Client Handshake + // Client Initial + Handshake + 1-RTT packets: []*testPacket{{ ptype: packetTypeInitial, num: 1, version: 1, - srcConnID: clientConnID, - dstConnID: serverConnID, + srcConnID: clientConnIDs[0], + dstConnID: serverConnIDs[0], frames: []debugFrame{ debugFrameAck{ ranges: []i64range[packetNumber]{{0, 1}}, @@ -127,23 +148,39 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { ptype: packetTypeHandshake, num: 0, version: 1, - srcConnID: clientConnID, - dstConnID: serverConnID, + srcConnID: clientConnIDs[0], + dstConnID: serverConnIDs[0], frames: []debugFrame{ debugFrameAck{ ranges: []i64range[packetNumber]{{0, 1}}, }, debugFrameCrypto{}, }, + }, { + ptype: packetType1RTT, + num: 0, + dstConnID: serverConnIDs[0], + frames: []debugFrame{ + debugFrameAck{ + ranges: []i64range[packetNumber]{{0, 1}}, + }, + debugFrameNewConnectionID{ + seq: 1, + connID: clientConnIDs[1], + }, + }, }}, paddedSize: 1200, }, { // Server HANDSHAKE_DONE and session ticket packets: []*testPacket{{ ptype: packetType1RTT, - num: 0, - dstConnID: clientConnID, + num: 1, + dstConnID: clientConnIDs[0], frames: []debugFrame{ + debugFrameAck{ + ranges: []i64range[packetNumber]{{0, 1}}, + }, debugFrameHandshakeDone{}, debugFrameCrypto{}, }, @@ -152,13 +189,13 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { // Client ack (after max_ack_delay) packets: []*testPacket{{ ptype: packetType1RTT, - num: 0, - dstConnID: serverConnID, + num: 1, + dstConnID: serverConnIDs[0], frames: []debugFrame{ debugFrameAck{ ackDelay: unscaledAckDelayFromDuration( maxAckDelay, ackDelayExponent), - ranges: []i64range[packetNumber]{{0, 1}}, + ranges: []i64range[packetNumber]{{0, 2}}, }, }, }}, @@ -190,6 +227,69 @@ func fillCryptoFrames(d *testDatagram, data map[tls.QUICEncryptionLevel][]byte) } } +// uncheckedHandshake executes the handshake. +// +// Unlike testConn.handshake, it sends nothing unnecessary +// (in particular, no NEW_CONNECTION_ID frames), +// and does not validate the conn's responses. +// +// Useful for testing scenarios where configuration has +// changed the handshake responses in some way. +func (tc *testConn) uncheckedHandshake() { + defer func(saved map[byte]bool) { + tc.ignoreFrames = saved + }(tc.ignoreFrames) + tc.ignoreFrames = map[byte]bool{ + frameTypeAck: true, + frameTypeCrypto: true, + frameTypeNewConnectionID: true, + } + if tc.conn.side == serverSide { + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + tc.wantFrame("send HANDSHAKE_DONE after handshake completes", + packetType1RTT, debugFrameHandshakeDone{}) + tc.writeFrames(packetType1RTT, + debugFrameAck{ + ackDelay: unscaledAckDelayFromDuration( + maxAckDelay, ackDelayExponent), + ranges: []i64range[packetNumber]{{0, tc.sentFramePacket.num + 1}}, + }) + } else { + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + tc.wantIdle("don't expect any frames we aren't ignoring") + // Send the next two frames in separate packets, so the client sends an + // ack immediately without delay. We want to consume that ack here, rather + // than returning with a delayed ack waiting to be sent. + tc.ignoreFrames = nil + tc.writeFrames(packetType1RTT, + debugFrameHandshakeDone{}) + tc.writeFrames(packetType1RTT, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + tc.wantFrame("client ACKs server's first 1-RTT packet", + packetType1RTT, debugFrameAck{ + ranges: []i64range[packetNumber]{{0, 2}}, + }) + + } + tc.wantIdle("handshake is done") +} + func TestConnClientHandshake(t *testing.T) { tc := newTestConn(t, clientSide) tc.handshake() @@ -224,6 +324,11 @@ func TestConnKeysDiscardedClient(t *testing.T) { packetTypeHandshake, debugFrameCrypto{ data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake], }) + tc.wantFrame("client provides an additional connection ID", + packetType1RTT, debugFrameNewConnectionID{ + seq: 1, + connID: testLocalConnID(1), + }) // The client discards Initial keys after sending a Handshake packet. tc.writeFrames(packetTypeInitial, @@ -273,6 +378,11 @@ func TestConnKeysDiscardedServer(t *testing.T) { }) tc.writeFrames(packetTypeInitial, debugFrameConnectionCloseTransport{code: errInternal}) + tc.wantFrame("server provides an additional connection ID", + packetType1RTT, debugFrameNewConnectionID{ + seq: 1, + connID: testLocalConnID(1), + }) tc.wantIdle("server has discarded Initial keys, cannot read CONNECTION_CLOSE") // The server discards Handshake keys after sending a HANDSHAKE_DONE frame. From 63fe334ad57133b911a1422472a28b11de828c89 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 31 Jan 2023 08:58:02 -0800 Subject: [PATCH 017/168] quic: gate and queue synchronization primitives Add a form of monitor (in the sense of the synchronization primitive) for controlling access to queues and streams. We call this a "gate". A gate acts as a mutex and condition variable with one bit of state. A gate may be locked and unlocked. Lock operations may optionally block on the gate condition being set. Unlock operations always record the new value of the condition. Gates play nicely with contexts. Unlike traditional condition variables, gates do not suffer from spurious wakeups: A goroutine waiting for a gate condition is not woken before the condition is set. Gates are inspired by the queue design from Bryan Mills's talk, Rethinking Classical Concurrency Patterns. Add a queue implemented with a gate. For golang/go#58547 Change-Id: Ibec6d1f29a2c03a7184fca7392ed5639f96b6485 Reviewed-on: https://go-review.googlesource.com/c/net/+/513955 TryBot-Result: Gopher Robot Reviewed-by: Jonathan Amsterdam Run-TryBot: Damien Neil --- internal/quic/gate.go | 104 ++++++++++++++++++++++++++ internal/quic/gate_test.go | 142 ++++++++++++++++++++++++++++++++++++ internal/quic/queue.go | 65 +++++++++++++++++ internal/quic/queue_test.go | 59 +++++++++++++++ 4 files changed, 370 insertions(+) create mode 100644 internal/quic/gate.go create mode 100644 internal/quic/gate_test.go create mode 100644 internal/quic/queue.go create mode 100644 internal/quic/queue_test.go diff --git a/internal/quic/gate.go b/internal/quic/gate.go new file mode 100644 index 0000000000..efb28daf8f --- /dev/null +++ b/internal/quic/gate.go @@ -0,0 +1,104 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import "context" + +// An gate is a monitor (mutex + condition variable) with one bit of state. +// +// The condition may be either set or unset. +// Lock operations may be unconditional, or wait for the condition to be set. +// Unlock operations record the new state of the condition. +type gate struct { + // When unlocked, exactly one of set or unset contains a value. + // When locked, neither chan contains a value. + set chan struct{} + unset chan struct{} +} + +func newGate() gate { + g := gate{ + set: make(chan struct{}, 1), + unset: make(chan struct{}, 1), + } + g.unset <- struct{}{} + return g +} + +// lock acquires the gate unconditionally. +// It reports whether the condition is set. +func (g *gate) lock() (set bool) { + select { + case <-g.set: + return true + case <-g.unset: + return false + } +} + +// waitAndLock waits until the condition is set before acquiring the gate. +func (g *gate) waitAndLock() { + <-g.set +} + +// waitAndLockContext waits until the condition is set before acquiring the gate. +// If the context expires, waitAndLockContext returns an error and does not acquire the gate. +func (g *gate) waitAndLockContext(ctx context.Context) error { + select { + case <-g.set: + return nil + default: + } + select { + case <-g.set: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// waitWithLock releases an acquired gate until the condition is set. +// The caller must have previously acquired the gate. +// Upon return from waitWithLock, the gate will still be held. +// If waitWithLock returns nil, the condition is set. +func (g *gate) waitWithLock(ctx context.Context) error { + g.unlock(false) + err := g.waitAndLockContext(ctx) + if err != nil { + if g.lock() { + // The condition was set in between the context expiring + // and us reacquiring the gate. + err = nil + } + } + return err +} + +// lockIfSet acquires the gate if and only if the condition is set. +func (g *gate) lockIfSet() (acquired bool) { + select { + case <-g.set: + return true + default: + return false + } +} + +// unlock sets the condition and releases the gate. +func (g *gate) unlock(set bool) { + if set { + g.set <- struct{}{} + } else { + g.unset <- struct{}{} + } +} + +// unlock sets the condition to the result of f and releases the gate. +// Useful in defers. +func (g *gate) unlockFunc(f func() bool) { + g.unlock(f()) +} diff --git a/internal/quic/gate_test.go b/internal/quic/gate_test.go new file mode 100644 index 0000000000..0122e39865 --- /dev/null +++ b/internal/quic/gate_test.go @@ -0,0 +1,142 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "testing" + "time" +) + +func TestGateLockAndUnlock(t *testing.T) { + g := newGate() + if set := g.lock(); set { + t.Errorf("g.lock() of never-locked gate: true, want false") + } + unlockedc := make(chan struct{}) + donec := make(chan struct{}) + go func() { + defer close(donec) + set := g.lock() + select { + case <-unlockedc: + default: + t.Errorf("g.lock() succeeded while gate was held") + } + if !set { + t.Errorf("g.lock() of set gate: false, want true") + } + g.unlock(false) + }() + time.Sleep(1 * time.Millisecond) + close(unlockedc) + g.unlock(true) + <-donec + if set := g.lock(); set { + t.Errorf("g.lock() of unset gate: true, want false") + } +} + +func TestGateWaitAndLock(t *testing.T) { + g := newGate() + set := false + go func() { + for i := 0; i < 3; i++ { + g.lock() + g.unlock(false) + time.Sleep(1 * time.Millisecond) + } + g.lock() + set = true + g.unlock(true) + }() + g.waitAndLock() + if !set { + t.Errorf("g.waitAndLock() returned before gate was set") + } +} + +func TestGateWaitAndLockContext(t *testing.T) { + g := newGate() + // waitAndLockContext is canceled + ctx, cancel := context.WithCancel(context.Background()) + go func() { + time.Sleep(1 * time.Millisecond) + cancel() + }() + if err := g.waitAndLockContext(ctx); err != context.Canceled { + t.Errorf("g.waitAndLockContext() = %v, want context.Canceled", err) + } + // waitAndLockContext succeeds + set := false + go func() { + time.Sleep(1 * time.Millisecond) + g.lock() + set = true + g.unlock(true) + }() + if err := g.waitAndLockContext(context.Background()); err != nil { + t.Errorf("g.waitAndLockContext() = %v, want nil", err) + } + if !set { + t.Errorf("g.waitAndLockContext() returned before gate was set") + } + g.unlock(true) + // waitAndLockContext succeeds when the gate is set and the context is canceled + if err := g.waitAndLockContext(ctx); err != nil { + t.Errorf("g.waitAndLockContext() = %v, want nil", err) + } +} + +func TestGateWaitWithLock(t *testing.T) { + g := newGate() + // waitWithLock is canceled + ctx, cancel := context.WithCancel(context.Background()) + go func() { + time.Sleep(1 * time.Millisecond) + cancel() + }() + g.lock() + if err := g.waitWithLock(ctx); err != context.Canceled { + t.Errorf("g.waitWithLock() = %v, want context.Canceled", err) + } + // waitWithLock succeeds + set := false + go func() { + g.lock() + set = true + g.unlock(true) + }() + time.Sleep(1 * time.Millisecond) + if err := g.waitWithLock(context.Background()); err != nil { + t.Errorf("g.waitWithLock() = %v, want nil", err) + } + if !set { + t.Errorf("g.waitWithLock() returned before gate was set") + } +} + +func TestGateLockIfSet(t *testing.T) { + g := newGate() + if locked := g.lockIfSet(); locked { + t.Errorf("g.lockIfSet() of unset gate = %v, want false", locked) + } + g.lock() + g.unlock(true) + if locked := g.lockIfSet(); !locked { + t.Errorf("g.lockIfSet() of set gate = %v, want true", locked) + } +} + +func TestGateUnlockFunc(t *testing.T) { + g := newGate() + go func() { + g.lock() + defer g.unlockFunc(func() bool { return true }) + }() + g.waitAndLock() +} diff --git a/internal/quic/queue.go b/internal/quic/queue.go new file mode 100644 index 0000000000..9bb71ca3f4 --- /dev/null +++ b/internal/quic/queue.go @@ -0,0 +1,65 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import "context" + +// A queue is an unbounded queue of some item (new connections and streams). +type queue[T any] struct { + // The gate condition is set if the queue is non-empty or closed. + gate gate + err error + q []T +} + +func newQueue[T any]() queue[T] { + return queue[T]{gate: newGate()} +} + +// close closes the queue, causing pending and future pop operations +// to return immediately with err. +func (q *queue[T]) close(err error) { + q.gate.lock() + defer q.unlock() + if q.err == nil { + q.err = err + } +} + +// put appends an item to the queue. +// It returns true if the item was added, false if the queue is closed. +func (q *queue[T]) put(v T) bool { + q.gate.lock() + defer q.unlock() + if q.err != nil { + return false + } + q.q = append(q.q, v) + return true +} + +// get removes the first item from the queue, blocking until ctx is done, an item is available, +// or the queue is closed. +func (q *queue[T]) get(ctx context.Context) (T, error) { + var zero T + if err := q.gate.waitAndLockContext(ctx); err != nil { + return zero, err + } + defer q.unlock() + if q.err != nil { + return zero, q.err + } + v := q.q[0] + copy(q.q[:], q.q[1:]) + q.q[len(q.q)-1] = zero + q.q = q.q[:len(q.q)-1] + return v, nil +} + +func (q *queue[T]) unlock() { + q.gate.unlock(q.err != nil || len(q.q) > 0) +} diff --git a/internal/quic/queue_test.go b/internal/quic/queue_test.go new file mode 100644 index 0000000000..8debeff110 --- /dev/null +++ b/internal/quic/queue_test.go @@ -0,0 +1,59 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "io" + "testing" + "time" +) + +func TestQueue(t *testing.T) { + nonblocking, cancel := context.WithCancel(context.Background()) + cancel() + + q := newQueue[int]() + if got, err := q.get(nonblocking); err != context.Canceled { + t.Fatalf("q.get() = %v, %v, want nil, contex.Canceled", got, err) + } + + if !q.put(1) { + t.Fatalf("q.put(1) = false, want true") + } + if !q.put(2) { + t.Fatalf("q.put(2) = false, want true") + } + if got, err := q.get(nonblocking); got != 1 || err != nil { + t.Fatalf("q.get() = %v, %v, want 1, nil", got, err) + } + if got, err := q.get(nonblocking); got != 2 || err != nil { + t.Fatalf("q.get() = %v, %v, want 2, nil", got, err) + } + if got, err := q.get(nonblocking); err != context.Canceled { + t.Fatalf("q.get() = %v, %v, want nil, contex.Canceled", got, err) + } + + go func() { + time.Sleep(1 * time.Millisecond) + q.put(3) + }() + if got, err := q.get(context.Background()); got != 3 || err != nil { + t.Fatalf("q.get() = %v, %v, want 3, nil", got, err) + } + + if !q.put(4) { + t.Fatalf("q.put(2) = false, want true") + } + q.close(io.EOF) + if got, err := q.get(context.Background()); got != 0 || err != io.EOF { + t.Fatalf("q.get() = %v, %v, want 0, io.EOF", got, err) + } + if q.put(5) { + t.Fatalf("q.put(5) = true, want false") + } +} From 8ffa475fbdb33da97e8bf79cc5791ee8751fca5e Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Thu, 6 Jul 2023 10:25:47 -0700 Subject: [PATCH 018/168] html: only render content literally in the HTML namespace Per the WHATWG HTML specification, section 13.3, only append the literal content of a text node if we are in the HTML namespace. Thanks to Mohammad Thoriq Aziz for reporting this issue. Fixes golang/go#61615 Fixes CVE-2023-3978 Change-Id: I332152904d4e7646bd2441602bcbe591fc655fa4 Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1942896 Reviewed-by: Tatiana Bradley Run-TryBot: Roland Shoemaker Reviewed-by: Damien Neil TryBot-Result: Security TryBots Reviewed-on: https://go-review.googlesource.com/c/net/+/514896 Reviewed-by: Roland Shoemaker TryBot-Result: Gopher Robot Run-TryBot: Damien Neil --- html/render.go | 28 +++++++++++++++++++---- html/render_test.go | 56 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/html/render.go b/html/render.go index 8b28031905..e8c1233455 100644 --- a/html/render.go +++ b/html/render.go @@ -194,9 +194,8 @@ func render1(w writer, n *Node) error { } } - // Render any child nodes. - switch n.Data { - case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp": + // Render any child nodes + if childTextNodesAreLiteral(n) { for c := n.FirstChild; c != nil; c = c.NextSibling { if c.Type == TextNode { if _, err := w.WriteString(c.Data); err != nil { @@ -213,7 +212,7 @@ func render1(w writer, n *Node) error { // last element in the file, with no closing tag. return plaintextAbort } - default: + } else { for c := n.FirstChild; c != nil; c = c.NextSibling { if err := render1(w, c); err != nil { return err @@ -231,6 +230,27 @@ func render1(w writer, n *Node) error { return w.WriteByte('>') } +func childTextNodesAreLiteral(n *Node) bool { + // Per WHATWG HTML 13.3, if the parent of the current node is a style, + // script, xmp, iframe, noembed, noframes, or plaintext element, and the + // current node is a text node, append the value of the node's data + // literally. The specification is not explicit about it, but we only + // enforce this if we are in the HTML namespace (i.e. when the namespace is + // ""). + // NOTE: we also always include noscript elements, although the + // specification states that they should only be rendered as such if + // scripting is enabled for the node (which is not something we track). + if n.Namespace != "" { + return false + } + switch n.Data { + case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp": + return true + default: + return false + } +} + // writeQuoted writes s to w surrounded by quotes. Normally it will use double // quotes, but if s contains a double quote, it will use single quotes. // It is used for writing the identifiers in a doctype declaration. diff --git a/html/render_test.go b/html/render_test.go index 08e592be27..22d08641a0 100644 --- a/html/render_test.go +++ b/html/render_test.go @@ -6,6 +6,8 @@ package html import ( "bytes" + "fmt" + "strings" "testing" ) @@ -108,16 +110,16 @@ func TestRenderer(t *testing.T) { // just commentary. The "0:" prefixes are for easy cross-reference with // the nodes array. treeAsText := [...]string{ - 0: ``, - 1: `. `, - 2: `. `, - 3: `. . "0<1"`, - 4: `. .

`, - 5: `. . . "2"`, - 6: `. . . `, - 7: `. . . . "3"`, - 8: `. . . `, - 9: `. . . . "&4"`, + 0: ``, + 1: `. `, + 2: `. `, + 3: `. . "0<1"`, + 4: `. .

`, + 5: `. . . "2"`, + 6: `. . . `, + 7: `. . . . "3"`, + 8: `. . . `, + 9: `. . . . "&4"`, 10: `. . "5"`, 11: `. .

`, 12: `. .
`, @@ -169,3 +171,37 @@ func TestRenderer(t *testing.T) { t.Errorf("got vs want:\n%s\n%s\n", got, want) } } + +func TestRenderTextNodes(t *testing.T) { + elements := []string{"style", "script", "xmp", "iframe", "noembed", "noframes", "plaintext", "noscript"} + for _, namespace := range []string{ + "", // html + "svg", + "math", + } { + for _, e := range elements { + var namespaceOpen, namespaceClose string + if namespace != "" { + namespaceOpen, namespaceClose = fmt.Sprintf("<%s>", namespace), fmt.Sprintf("", namespace) + } + doc := fmt.Sprintf(`%s<%s>&%s`, namespaceOpen, e, e, namespaceClose) + n, err := Parse(strings.NewReader(doc)) + if err != nil { + t.Fatal(err) + } + b := bytes.NewBuffer(nil) + if err := Render(b, n); err != nil { + t.Fatal(err) + } + + expected := doc + if namespace != "" { + expected = strings.Replace(expected, "&", "&", 1) + } + + if b.String() != expected { + t.Errorf("unexpected output: got %q, want %q", b.String(), expected) + } + } + } +} From 167593b38cf631be267ebcd8d612b7c58138d8c4 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 26 Jul 2023 11:46:09 -0400 Subject: [PATCH 019/168] quic: create and accept streams Add minimal API surface for creating streams, basic loop for sending stream-related frames. No limits, data, or lifetime management yet. RFC 9000, Sections 2 and 3. For golang/go#58547 Change-Id: I2c167b9363d0121b8a8776309d165b0f47f8f090 Reviewed-on: https://go-review.googlesource.com/c/net/+/514115 Reviewed-by: Jonathan Amsterdam TryBot-Result: Gopher Robot Run-TryBot: Damien Neil --- internal/quic/conn.go | 17 ++- internal/quic/conn_loss.go | 10 ++ internal/quic/conn_loss_test.go | 32 +++++ internal/quic/conn_recv.go | 15 +- internal/quic/conn_send.go | 7 + internal/quic/conn_streams.go | 215 +++++++++++++++++++++++++++++ internal/quic/conn_streams_test.go | 144 +++++++++++++++++++ internal/quic/conn_test.go | 11 ++ internal/quic/packet_parser.go | 3 + internal/quic/quic.go | 1 + internal/quic/stream.go | 151 ++++++++++++++++++++ internal/quic/stream_test.go | 33 +++++ 12 files changed, 637 insertions(+), 2 deletions(-) create mode 100644 internal/quic/conn_streams.go create mode 100644 internal/quic/conn_streams_test.go create mode 100644 internal/quic/stream.go create mode 100644 internal/quic/stream_test.go diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 77ecea0d62..5601b989e3 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -32,6 +32,7 @@ type Conn struct { acks [numberSpaceCount]ackState // indexed by number space connIDState connIDState loss lossState + streams streamsState // errForPeer is set when the connection is being closed. errForPeer error @@ -105,6 +106,7 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. // TODO: PMTU discovery. const maxDatagramSize = 1200 c.loss.init(c.side, maxDatagramSize, now) + c.streamsInit() c.startTLS(now, initialConnID, transportParameters{ initialSrcConnID: c.connIDState.srcConnID(), @@ -178,7 +180,10 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error { return nil } -type timerEvent struct{} +type ( + timerEvent struct{} + wakeEvent struct{} +) // loop is the connection main loop. // @@ -250,6 +255,8 @@ func (c *Conn) loop(now time.Time) { return } c.loss.advance(now, c.handleAckOrLoss) + case wakeEvent: + // We're being woken up to try sending some frames. case func(time.Time, *Conn): // Send a func to msgc to run it on the main Conn goroutine m(now, c) @@ -269,6 +276,14 @@ func (c *Conn) sendMsg(m any) { } } +// wake wakes up the conn's loop. +func (c *Conn) wake() { + select { + case c.msgc <- wakeEvent{}: + default: + } +} + // runOnLoop executes a function within the conn's loop goroutine. func (c *Conn) runOnLoop(f func(now time.Time, c *Conn)) error { donec := make(chan struct{}) diff --git a/internal/quic/conn_loss.go b/internal/quic/conn_loss.go index 57570d0861..ca178089d2 100644 --- a/internal/quic/conn_loss.go +++ b/internal/quic/conn_loss.go @@ -44,6 +44,16 @@ func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetF case frameTypeCrypto: start, end := sent.nextRange() c.crypto[space].ackOrLoss(start, end, fate) + case frameTypeStreamBase, + frameTypeStreamBase | streamFinBit: + id := streamID(sent.nextInt()) + start, end := sent.nextRange() + s := c.streamForID(id) + if s == nil { + continue + } + fin := f&streamFinBit != 0 + s.ackOrLossData(sent.num, start, end, fin, fate) case frameTypeNewConnectionID: seq := int64(sent.nextInt()) c.connIDState.ackOrLossNewConnectionID(sent.num, seq, fate) diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index 021c86c876..3c9e6149ab 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -106,6 +106,38 @@ func TestLostCRYPTOFrame(t *testing.T) { }) } +func TestLostStreamFrameEmpty(t *testing.T) { + // A STREAM frame opening a stream, but containing no stream data, should + // be retransmitted if lost. + lostFrameTest(t, func(t *testing.T, pto bool) { + ctx := canceledContext() + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.initialMaxStreamDataBidiRemote = 100 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + c, err := tc.conn.NewStream(ctx) + if err != nil { + t.Fatalf("NewStream: %v", err) + } + c.Write(nil) // open the stream + tc.wantFrame("created bidirectional stream 0", + packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, bidiStream, 0), + data: []byte{}, + }) + + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("resent stream frame", + packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, bidiStream, 0), + data: []byte{}, + }) + }) + +} + func TestLostNewConnectionIDFrame(t *testing.T) { // "New connection IDs are [...] retransmitted if the packet containing them is lost." // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.13 diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 7992a619f4..45ef3844e8 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -181,7 +181,7 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, if !frameOK(c, ptype, __01) { return } - _, _, _, _, n = consumeStreamFrame(payload) + n = c.handleStreamFrame(now, space, payload) case frameTypeMaxData: if !frameOK(c, ptype, __01) { return @@ -290,6 +290,19 @@ func (c *Conn) handleCryptoFrame(now time.Time, space numberSpace, payload []byt return n } +func (c *Conn) handleStreamFrame(now time.Time, space numberSpace, payload []byte) int { + id, off, fin, b, n := consumeStreamFrame(payload) + if n < 0 { + return -1 + } + if s := c.streamForFrame(now, id, recvStream); s != nil { + if err := s.handleData(off, b, fin); err != nil { + c.abort(now, err) + } + } + return n +} + func (c *Conn) handleNewConnectionIDFrame(now time.Time, space numberSpace, payload []byte) int { seq, retire, connID, resetToken, n := consumeNewConnectionIDFrame(payload) if n < 0 { diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index d410548a98..6e6fbc5857 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -254,6 +254,13 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, c.testSendPing.setSent(pnum) } + // All stream-related frames. This should come last in the packet, + // so large amounts of STREAM data don't crowd out other frames + // we may need to send. + if !c.appendStreamFrames(&c.w, pnum, pto) { + return + } + // If this is a PTO probe and we haven't added an ack-eliciting frame yet, // add a PING to make this an ack-eliciting probe. // diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go new file mode 100644 index 0000000000..82e9028609 --- /dev/null +++ b/internal/quic/conn_streams.go @@ -0,0 +1,215 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "sync" + "sync/atomic" + "time" +) + +type streamsState struct { + queue queue[*Stream] // new, peer-created streams + + streamsMu sync.Mutex + streams map[streamID]*Stream + opened [streamTypeCount]int64 // number of streams opened by us + + // Streams with frames to send are stored in a circular linked list. + // sendHead is the next stream to write, or nil if there are no streams + // with data to send. sendTail is the last stream to write. + needSend atomic.Bool + sendMu sync.Mutex + sendHead *Stream + sendTail *Stream +} + +func (c *Conn) streamsInit() { + c.streams.streams = make(map[streamID]*Stream) + c.streams.queue = newQueue[*Stream]() +} + +// AcceptStream waits for and returns the next stream created by the peer. +func (c *Conn) AcceptStream(ctx context.Context) (*Stream, error) { + return c.streams.queue.get(ctx) +} + +// NewStream creates a stream. +// +// If the peer's maximum stream limit for the connection has been reached, +// NewStream blocks until the limit is increased or the context expires. +func (c *Conn) NewStream(ctx context.Context) (*Stream, error) { + return c.newLocalStream(ctx, bidiStream) +} + +// NewSendOnlyStream creates a unidirectional, send-only stream. +// +// If the peer's maximum stream limit for the connection has been reached, +// NewSendOnlyStream blocks until the limit is increased or the context expires. +func (c *Conn) NewSendOnlyStream(ctx context.Context) (*Stream, error) { + return c.newLocalStream(ctx, uniStream) +} + +func (c *Conn) newLocalStream(ctx context.Context, typ streamType) (*Stream, error) { + // TODO: Stream limits. + c.streams.streamsMu.Lock() + defer c.streams.streamsMu.Unlock() + + num := c.streams.opened[typ] + c.streams.opened[typ]++ + + s := newStream(c, newStreamID(c.side, typ, num)) + c.streams.streams[s.id] = s + return s, nil +} + +// streamFrameType identifies which direction of a stream, +// from the local perspective, a frame is associated with. +// +// For example, STREAM is a recvStream frame, +// because it carries data from the peer to us. +type streamFrameType uint8 + +const ( + sendStream = streamFrameType(iota) // for example, MAX_DATA + recvStream // for example, STREAM_DATA_BLOCKED +) + +// streamForID returns the stream with the given id. +// If the stream does not exist, it returns nil. +func (c *Conn) streamForID(id streamID) *Stream { + c.streams.streamsMu.Lock() + defer c.streams.streamsMu.Unlock() + return c.streams.streams[id] +} + +// streamForFrame returns the stream with the given id. +// If the stream does not exist, it may be created. +// +// streamForFrame aborts the connection if the stream id, state, and frame type don't align. +// For example, it aborts the connection with a STREAM_STATE error if a MAX_DATA frame +// is received for a receive-only stream, or if the peer attempts to create a stream that +// should be originated locally. +// +// streamForFrame returns nil if the stream no longer exists or if an error occurred. +func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) *Stream { + if id.streamType() == uniStream { + if (id.initiator() == c.side) != (ftype == sendStream) { + // Received an invalid frame for unidirectional stream. + // For example, a RESET_STREAM frame for a send-only stream. + c.abort(now, localTransportError(errStreamState)) + return nil + } + } + + c.streams.streamsMu.Lock() + defer c.streams.streamsMu.Unlock() + if s := c.streams.streams[id]; s != nil { + return s + } + // TODO: Check for closed streams, once we support closing streams. + if id.initiator() == c.side { + c.abort(now, localTransportError(errStreamState)) + return nil + } + s := newStream(c, id) + c.streams.streams[id] = s + c.streams.queue.put(s) + return s +} + +// queueStreamForSend marks a stream as containing frames that need sending. +func (c *Conn) queueStreamForSend(s *Stream) { + c.streams.sendMu.Lock() + defer c.streams.sendMu.Unlock() + if s.next != nil { + // Already in the queue. + return + } + if c.streams.sendHead == nil { + // The queue was empty. + c.streams.sendHead = s + c.streams.sendTail = s + s.next = s + } else { + // Insert this stream at the end of the queue. + c.streams.sendTail.next = s + c.streams.sendTail = s + } + c.streams.needSend.Store(true) + c.wake() +} + +// appendStreamFrames writes stream-related frames to the current packet. +// +// It returns true if no more frames need appending, +// false if not everything fit in the current packet. +func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) bool { + if pto { + return c.appendStreamFramesPTO(w, pnum) + } + if !c.streams.needSend.Load() { + return true + } + c.streams.sendMu.Lock() + defer c.streams.sendMu.Unlock() + for { + s := c.streams.sendHead + const pto = false + if !s.appendInFrames(w, pnum, pto) { + return false + } + avail := w.avail() + if !s.appendOutFrames(w, pnum, pto) { + // We've sent some data for this stream, but it still has more to send. + // If the stream got a reasonable chance to put data in a packet, + // advance sendHead to the next stream in line, to avoid starvation. + // We'll come back to this stream after going through the others. + // + // If the packet was already mostly out of space, leave sendHead alone + // and come back to this stream again on the next packet. + if avail > 512 { + c.streams.sendHead = s.next + c.streams.sendTail = s + } + return false + } + s.next = nil + if s == c.streams.sendTail { + // This was the last stream. + c.streams.sendHead = nil + c.streams.sendTail = nil + c.streams.needSend.Store(false) + return true + } + // We've sent all data for this stream, so remove it from the list. + c.streams.sendTail.next = s.next + c.streams.sendHead = s.next + s.next = nil + } +} + +// appendStreamFramesPTO writes stream-related frames to the current packet +// for a PTO probe. +// +// It returns true if no more frames need appending, +// false if not everything fit in the current packet. +func (c *Conn) appendStreamFramesPTO(w *packetWriter, pnum packetNumber) bool { + c.streams.sendMu.Lock() + defer c.streams.sendMu.Unlock() + for _, s := range c.streams.streams { + const pto = true + if !s.appendInFrames(w, pnum, pto) { + return false + } + if !s.appendOutFrames(w, pnum, pto) { + return false + } + } + return true +} diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go new file mode 100644 index 0000000000..8481a604c5 --- /dev/null +++ b/internal/quic/conn_streams_test.go @@ -0,0 +1,144 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "testing" +) + +func TestStreamsCreate(t *testing.T) { + ctx := canceledContext() + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.initialMaxStreamDataBidiLocal = 100 + p.initialMaxStreamDataBidiRemote = 100 + }) + tc.handshake() + + c, err := tc.conn.NewStream(ctx) + if err != nil { + t.Fatalf("NewStream: %v", err) + } + c.Write(nil) // open the stream + tc.wantFrame("created bidirectional stream 0", + packetType1RTT, debugFrameStream{ + id: 0, // client-initiated, bidi, number 0 + data: []byte{}, + }) + + c, err = tc.conn.NewSendOnlyStream(ctx) + if err != nil { + t.Fatalf("NewStream: %v", err) + } + c.Write(nil) // open the stream + tc.wantFrame("created unidirectional stream 0", + packetType1RTT, debugFrameStream{ + id: 2, // client-initiated, uni, number 0 + data: []byte{}, + }) + + c, err = tc.conn.NewStream(ctx) + if err != nil { + t.Fatalf("NewStream: %v", err) + } + c.Write(nil) // open the stream + tc.wantFrame("created bidirectional stream 1", + packetType1RTT, debugFrameStream{ + id: 4, // client-initiated, uni, number 4 + data: []byte{}, + }) +} + +func TestStreamsAccept(t *testing.T) { + ctx := canceledContext() + tc := newTestConn(t, serverSide) + tc.handshake() + + tc.writeFrames(packetType1RTT, + debugFrameStream{ + id: 0, // client-initiated, bidi, number 0 + }, + debugFrameStream{ + id: 2, // client-initiated, uni, number 0 + }, + debugFrameStream{ + id: 4, // client-initiated, bidi, number 1 + }) + + for _, accept := range []struct { + id streamID + readOnly bool + }{ + {0, false}, + {2, true}, + {4, false}, + } { + s, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("conn.AcceptStream() = %v, want stream %v", err, accept.id) + } + if got, want := s.id, accept.id; got != want { + t.Fatalf("conn.AcceptStream() = stream %v, want %v", got, want) + } + if got, want := s.IsReadOnly(), accept.readOnly; got != want { + t.Fatalf("stream %v: s.IsReadOnly() = %v, want %v", accept.id, got, want) + } + } + + _, err := tc.conn.AcceptStream(ctx) + if err != context.Canceled { + t.Fatalf("conn.AcceptStream() = %v, want context.Canceled", err) + } +} + +func TestStreamsStreamNotCreated(t *testing.T) { + // "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR + // if it receives a STREAM frame for a locally initiated stream that has + // not yet been created [...]" + // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.8-3 + tc := newTestConn(t, serverSide) + tc.handshake() + + tc.writeFrames(packetType1RTT, + debugFrameStream{ + id: 1, // server-initiated, bidi, number 0 + }) + tc.wantFrame("peer sent STREAM frame for an uncreated local stream", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errStreamState, + }) +} + +func TestStreamsStreamSendOnly(t *testing.T) { + // "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR + // if it receives a STREAM frame for a locally initiated stream that has + // not yet been created [...]" + // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.8-3 + ctx := canceledContext() + tc := newTestConn(t, serverSide) + tc.handshake() + + c, err := tc.conn.NewSendOnlyStream(ctx) + if err != nil { + t.Fatalf("NewStream: %v", err) + } + c.Write(nil) // open the stream + tc.wantFrame("created unidirectional stream 0", + packetType1RTT, debugFrameStream{ + id: 3, // server-initiated, uni, number 0 + data: []byte{}, + }) + + tc.writeFrames(packetType1RTT, + debugFrameStream{ + id: 3, // server-initiated, bidi, number 0 + }) + tc.wantFrame("peer sent STREAM frame for a send-only stream", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errStreamState, + }) +} diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 317ca8f81f..1fe1e7b84a 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -725,3 +725,14 @@ func (tc *testConnListener) sendDatagram(p []byte, addr netip.AddrPort) error { tc.sentDatagrams = append(tc.sentDatagrams, append([]byte(nil), p...)) return nil } + +// canceledContext returns a canceled Context. +// +// Functions which take a context preference progress over cancelation. +// For example, a read with a canceled context will return data if any is available. +// Tests use canceled contexts to perform non-blocking operations. +func canceledContext() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + return ctx +} diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index 0520078974..9a00da7560 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -330,6 +330,9 @@ func consumeStreamFrame(b []byte) (id streamID, off int64, fin bool, data []byte data = b[n:] n += len(data) } + if off+int64(len(data)) >= 1<<62 { + return 0, 0, false, nil, -1 + } return streamID(idInt), off, fin, data, n } diff --git a/internal/quic/quic.go b/internal/quic/quic.go index 84ce2bda13..8cd61aed08 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -112,6 +112,7 @@ type streamType uint8 const ( bidiStream = streamType(iota) uniStream + streamTypeCount ) func (s streamType) String() string { diff --git a/internal/quic/stream.go b/internal/quic/stream.go new file mode 100644 index 0000000000..b55f927e02 --- /dev/null +++ b/internal/quic/stream.go @@ -0,0 +1,151 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "errors" +) + +type Stream struct { + id streamID + conn *Conn + + // outgate's lock guards all send-related state. + // + // The gate condition is set if a write to the stream will not block, + // either because the stream has available flow control or because + // the write will fail. + outgate gate + outopened sentVal // set if we should open the stream + + prev, next *Stream // guarded by streamsState.sendMu +} + +func newStream(c *Conn, id streamID) *Stream { + s := &Stream{ + conn: c, + id: id, + outgate: newGate(), + } + + // Lock and unlock outgate to update the stream writability state. + s.outgate.lock() + s.outUnlock() + + return s +} + +// IsReadOnly reports whether the stream is read-only +// (a unidirectional stream created by the peer). +func (s *Stream) IsReadOnly() bool { + return s.id.streamType() == uniStream && s.id.initiator() != s.conn.side +} + +// IsWriteOnly reports whether the stream is write-only +// (a unidirectional stream created locally). +func (s *Stream) IsWriteOnly() bool { + return s.id.streamType() == uniStream && s.id.initiator() == s.conn.side +} + +// Read reads data from the stream. +// See ReadContext for more details. +func (s *Stream) Read(b []byte) (n int, err error) { + return s.ReadContext(context.Background(), b) +} + +// ReadContext reads data from the stream. +// +// ReadContext returns as soon as at least one byte of data is available. +// +// If the peer closes the stream cleanly, ReadContext returns io.EOF after +// returning all data sent by the peer. +// If the peer terminates reads abruptly, ReadContext returns StreamResetError. +func (s *Stream) ReadContext(ctx context.Context, b []byte) (n int, err error) { + // TODO: implement + return 0, errors.New("unimplemented") +} + +// Write writes data to the stream. +// See WriteContext for more details. +func (s *Stream) Write(b []byte) (n int, err error) { + return s.WriteContext(context.Background(), b) +} + +// WriteContext writes data to the stream. +// +// WriteContext writes data to the stream write buffer. +// Buffered data is only sent when the buffer is sufficiently full. +// Call the Flush method to ensure buffered data is sent. +// +// If the peer aborts reads on the stream, ReadContext returns StreamResetError. +func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) { + if s.IsReadOnly() { + return 0, errors.New("write to read-only stream") + } + if len(b) > 0 { + // TODO: implement + return 0, errors.New("unimplemented") + } + if err := s.outgate.waitAndLockContext(ctx); err != nil { + return 0, err + } + defer s.outUnlock() + + // Set outopened to send a STREAM frame with no data, + // opening the stream on the peer. + s.outopened.set() + + return n, nil +} + +// outUnlock unlocks s.outgate. +// It sets the gate condition if writes to s will not block. +// If s has frames to write, it notifies the Conn. +func (s *Stream) outUnlock() { + if s.outopened.shouldSend() { + s.conn.queueStreamForSend(s) + } + canSend := true // TODO: set sendability status based on flow control + s.outgate.unlock(canSend) +} + +// handleData handles data received in a STREAM frame. +func (s *Stream) handleData(off int64, b []byte, fin bool) error { + // TODO + return nil +} + +// ackOrLossData handles the fate of a STREAM frame. +func (s *Stream) ackOrLossData(pnum packetNumber, start, end int64, fin bool, fate packetFate) { + s.outgate.lock() + defer s.outUnlock() + s.outopened.ackOrLoss(pnum, fate) +} + +func (s *Stream) appendInFrames(w *packetWriter, pnum packetNumber, pto bool) bool { + // TODO: STOP_SENDING + // TODO: MAX_STREAM_DATA + return true +} + +func (s *Stream) appendOutFrames(w *packetWriter, pnum packetNumber, pto bool) bool { + // TODO: RESET_STREAM + // TODO: STREAM_DATA_BLOCKED + // TODO: STREAM frames with data + if s.outopened.shouldSendPTO(pto) { + off := int64(0) + size := 0 + fin := false + _, added := w.appendStreamFrame(s.id, off, size, fin) + if !added { + return false + } + s.outopened.setSent(pnum) + } + return true +} diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go new file mode 100644 index 0000000000..8ae9dbc825 --- /dev/null +++ b/internal/quic/stream_test.go @@ -0,0 +1,33 @@ +// 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 ( + "reflect" + "testing" +) + +func TestStreamOffsetTooLarge(t *testing.T) { + // "Receipt of a frame that exceeds [2^62-1] MUST be treated as a + // connection error of type FRAME_ENCODING_ERROR or FLOW_CONTROL_ERROR." + // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.8-9 + tc := newTestConn(t, serverSide) + tc.handshake() + + tc.writeFrames(packetType1RTT, + debugFrameStream{ + id: newStreamID(clientSide, bidiStream, 0), + off: 1<<62 - 1, + data: []byte{0}, + }) + got, _ := tc.readFrame() + want1 := debugFrameConnectionCloseTransport{code: errFrameEncoding} + want2 := debugFrameConnectionCloseTransport{code: errFlowControl} + if !reflect.DeepEqual(got, want1) && !reflect.DeepEqual(got, want2) { + t.Fatalf("STREAM offset exceeds 2^62-1\ngot: %v\nwant: %v\n or: %v", got, want1, want2) + } +} From a7da556f067cc43c881288fc0577d0000a6ad619 Mon Sep 17 00:00:00 2001 From: David Fu Date: Mon, 10 Jul 2023 07:36:53 +0000 Subject: [PATCH 020/168] http2: optimize buffer allocation in transport We have identified a high memory usage problem in our production service, which utilizes Traefik as a gRPC proxy. This service handles a substantial volume of gRPC bi-directional streaming requests that can persist for extended periods, spanning many days. Currently, there exists only a single global buffer pool in the http2 package. The allocated buffers, regardless of their sizes, are shared among requests with vastly different characteristics. For instance, gRPC streaming requests typically require smaller buffer sizes and occupy buffers for significant durations. Conversely, general HTTP requests may necessitate larger buffer sizes but only retain them temporarily. Unfortunately, the large buffers allocated by HTTP requests are can be chosen for subsequent gRPC streaming requests, resulting in numerous large buffers being unable to be recycled. In our production environment, which processes approximately 1 million gRPC streaming requests, memory usage can soar to an excessive 800 GiB. This is a substantial waste of resources. To address this challenge, we propose implementing a multi-layered buffer pool mechanism. This mechanism allows requests with varying characteristics to select buffers of appropriate sizes, optimizing resource allocation and recycling. Change-Id: I834f7c08d90fd298aac7971ad45dc1a36251788b GitHub-Last-Rev: 477197698f27f55a1cffe6864fcb84582f80c7a7 GitHub-Pull-Request: golang/net#182 Reviewed-on: https://go-review.googlesource.com/c/net/+/508415 Run-TryBot: Damien Neil Reviewed-by: David Chase Reviewed-by: Brad Fitzpatrick TryBot-Result: Gopher Robot Auto-Submit: Damien Neil Reviewed-by: Damien Neil --- http2/transport.go | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/http2/transport.go b/http2/transport.go index b20c749171..b0d482f9f4 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -19,6 +19,7 @@ import ( "io/fs" "log" "math" + "math/bits" mathrand "math/rand" "net" "net/http" @@ -1680,7 +1681,27 @@ func (cs *clientStream) frameScratchBufferLen(maxFrameSize int) int { return int(n) // doesn't truncate; max is 512K } -var bufPool sync.Pool // of *[]byte +// Seven bufPools manage different frame sizes. This helps to avoid scenarios where long-running +// streaming requests using small frame sizes occupy large buffers initially allocated for prior +// requests needing big buffers. The size ranges are as follows: +// {0 KB, 16 KB], {16 KB, 32 KB], {32 KB, 64 KB], {64 KB, 128 KB], {128 KB, 256 KB], +// {256 KB, 512 KB], {512 KB, infinity} +// In practice, the maximum scratch buffer size should not exceed 512 KB due to +// frameScratchBufferLen(maxFrameSize), thus the "infinity pool" should never be used. +// It exists mainly as a safety measure, for potential future increases in max buffer size. +var bufPools [7]sync.Pool // of *[]byte +func bufPoolIndex(size int) int { + if size <= 16384 { + return 0 + } + size -= 1 + bits := bits.Len(uint(size)) + index := bits - 14 + if index >= len(bufPools) { + return len(bufPools) - 1 + } + return index +} func (cs *clientStream) writeRequestBody(req *http.Request) (err error) { cc := cs.cc @@ -1698,12 +1719,13 @@ func (cs *clientStream) writeRequestBody(req *http.Request) (err error) { // Scratch buffer for reading into & writing from. scratchLen := cs.frameScratchBufferLen(maxFrameSize) var buf []byte - if bp, ok := bufPool.Get().(*[]byte); ok && len(*bp) >= scratchLen { - defer bufPool.Put(bp) + index := bufPoolIndex(scratchLen) + if bp, ok := bufPools[index].Get().(*[]byte); ok && len(*bp) >= scratchLen { + defer bufPools[index].Put(bp) buf = *bp } else { buf = make([]byte, scratchLen) - defer bufPool.Put(&buf) + defer bufPools[index].Put(&buf) } var sawEOF bool From 60ae793a0dde26dc7ddd0a789e7b53e263e9ef33 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 1 Aug 2023 16:13:12 -0700 Subject: [PATCH 021/168] quic: don't send session tickets The crypto/tls QUIC session ticket API may change prior to the go1.21 release (see golang/go#60107). Drop session tickets entirely for now. We can revisit this when adding 0-RTT support later, which will also need to interact with session tickets. For golang/go#58547 Change-Id: Ib24c456508e39ed11fa284ca3832ba61dc5121f3 Reviewed-on: https://go-review.googlesource.com/c/net/+/514999 Run-TryBot: Damien Neil TryBot-Result: Gopher Robot Reviewed-by: Roland Shoemaker --- internal/quic/conn_id_test.go | 4 +--- internal/quic/conn_loss_test.go | 8 -------- internal/quic/tls.go | 5 ----- internal/quic/tls_test.go | 7 ++----- 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index 74905578dc..04baf0edaf 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -229,9 +229,7 @@ func TestConnIDPeerWithZeroLengthConnIDSendsNewConnectionID(t *testing.T) { // An endpoint that selects a zero-length connection ID during the handshake // cannot issue a new connection ID." // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-8 - tc := newTestConn(t, clientSide, func(c *tls.Config) { - c.SessionTicketsDisabled = true - }) + tc := newTestConn(t, clientSide) tc.peerConnID = []byte{} tc.ignoreFrame(frameTypeAck) tc.uncheckedHandshake() diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index 3c9e6149ab..2e30b5af6c 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -224,17 +224,9 @@ func TestLostHandshakeDoneFrame(t *testing.T) { tc.wantFrame("server sends HANDSHAKE_DONE after handshake completes", packetType1RTT, debugFrameHandshakeDone{}) - tc.wantFrame("server sends session ticket in CRYPTO frame", - packetType1RTT, debugFrameCrypto{ - data: tc.cryptoDataOut[tls.QUICEncryptionLevelApplication], - }) tc.triggerLossOrPTO(packetType1RTT, pto) tc.wantFrame("server resends HANDSHAKE_DONE", packetType1RTT, debugFrameHandshakeDone{}) - tc.wantFrame("server resends session ticket", - packetType1RTT, debugFrameCrypto{ - data: tc.cryptoDataOut[tls.QUICEncryptionLevelApplication], - }) }) } diff --git a/internal/quic/tls.go b/internal/quic/tls.go index ed848c6a13..584316f0e4 100644 --- a/internal/quic/tls.go +++ b/internal/quic/tls.go @@ -72,11 +72,6 @@ func (c *Conn) handleTLSEvents(now time.Time) error { // at the server when the handshake completes." // https://www.rfc-editor.org/rfc/rfc9001#section-4.1.2-1 c.confirmHandshake(now) - if !c.config.TLSConfig.SessionTicketsDisabled { - if err := c.tls.SendSessionTicket(false); err != nil { - return err - } - } } case tls.QUICTransportParameters: params, err := unmarshalTransportParams(e.Data) diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 3768dc0c07..45ed2517e9 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -172,7 +172,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { }}, paddedSize: 1200, }, { - // Server HANDSHAKE_DONE and session ticket + // Server HANDSHAKE_DONE packets: []*testPacket{{ ptype: packetType1RTT, num: 1, @@ -182,7 +182,6 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { ranges: []i64range[packetNumber]{{0, 1}}, }, debugFrameHandshakeDone{}, - debugFrameCrypto{}, }, }}, }, { @@ -351,9 +350,7 @@ func TestConnKeysDiscardedClient(t *testing.T) { } func TestConnKeysDiscardedServer(t *testing.T) { - tc := newTestConn(t, serverSide, func(c *tls.Config) { - c.SessionTicketsDisabled = true - }) + tc := newTestConn(t, serverSide) tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetTypeInitial, From 464865166c04e207ce296d9f3534c7bf5a224d0e Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 2 Aug 2023 12:43:07 -0700 Subject: [PATCH 022/168] quic: add -vv flag for more verbose tests Add a -vv flag to make tests log each packet sent/received. Disable logging of packets generally not relevant to the test, namely the handshake and the series of pings and acks used to trigger loss detection in loss tests. For golang/go#58547 Change-Id: I69c7f6743436648c2c2f202e38c3f6fb2c73c802 Reviewed-on: https://go-review.googlesource.com/c/net/+/515339 Run-TryBot: Damien Neil TryBot-Result: Gopher Robot Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_loss_test.go | 10 ++++++++++ internal/quic/conn_test.go | 31 ++++++++++++++++++++++++++++++- internal/quic/tls_test.go | 7 +++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index 2e30b5af6c..e3d16a7baa 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -30,9 +30,19 @@ func (tc *testConn) triggerLossOrPTO(ptype packetType, pto bool) { if !tc.conn.loss.ptoTimerArmed { tc.t.Fatalf("PTO timer not armed, expected it to be") } + if *testVV { + tc.t.Logf("advancing to PTO timer") + } tc.advanceTo(tc.conn.loss.timer) return } + if *testVV { + *testVV = false + defer func() { + tc.t.Logf("cause conn to declare last packet lost") + *testVV = true + }() + } defer func(ignoreFrames map[byte]bool) { tc.ignoreFrames = ignoreFrames }(tc.ignoreFrames) diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 1fe1e7b84a..110b0a9f90 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -11,6 +11,7 @@ import ( "context" "crypto/tls" "errors" + "flag" "fmt" "math" "net/netip" @@ -20,6 +21,8 @@ import ( "time" ) +var testVV = flag.Bool("vv", false, "even more verbose test output") + func TestConnTestConn(t *testing.T) { tc := newTestConn(t, serverSide) if got, want := tc.timeUntilEvent(), defaultMaxIdleTimeout; got != want { @@ -308,10 +311,34 @@ func (tc *testConn) cleanup() { tc.conn.exit() } +func (tc *testConn) logDatagram(text string, d *testDatagram) { + tc.t.Helper() + if !*testVV { + return + } + pad := "" + if d.paddedSize > 0 { + pad = fmt.Sprintf(" (padded to %v)", d.paddedSize) + } + tc.t.Logf("%v datagram%v", text, pad) + for _, p := range d.packets { + switch p.ptype { + case packetType1RTT: + tc.t.Logf(" %v pnum=%v", p.ptype, p.num) + default: + tc.t.Logf(" %v pnum=%v ver=%v dst={%x} src={%x}", p.ptype, p.num, p.version, p.dstConnID, p.srcConnID) + } + for _, f := range p.frames { + tc.t.Logf(" %v", f) + } + } +} + // write sends the Conn a datagram. func (tc *testConn) write(d *testDatagram) { tc.t.Helper() var buf []byte + tc.logDatagram("<- conn under test receives", d) for _, p := range d.packets { space := spaceForPacketType(p.ptype) if p.num >= tc.peerNextPacketNum[space] { @@ -374,7 +401,9 @@ func (tc *testConn) readDatagram() *testDatagram { } buf := tc.sentDatagrams[0] tc.sentDatagrams = tc.sentDatagrams[1:] - return tc.parseTestDatagram(buf) + d := tc.parseTestDatagram(buf) + tc.logDatagram("-> conn under test sends", d) + return d } // readPacket reads the next packet sent by the Conn. diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 45ed2517e9..1e3d6b6223 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -18,6 +18,13 @@ import ( // handshake executes the handshake. func (tc *testConn) handshake() { tc.t.Helper() + if *testVV { + *testVV = false + defer func() { + *testVV = true + tc.t.Logf("performed connection handshake") + }() + } defer func(saved map[byte]bool) { tc.ignoreFrames = saved }(tc.ignoreFrames) From 0b21d06592a511ec037411df9c245e8c15f31b22 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 2 Aug 2023 17:51:42 -0700 Subject: [PATCH 023/168] quic: framework for testing blocking operations For some tests, we want to start a blocking operation and then subsequently control the progress of that operation. For example, we might write to a stream, and then feed the connection MAX_STREAM_DATA frames to permit it to gradually send the written data. This is difficult to coordinate: We can start the write in a goroutine, but we have no way to synchronize with it. Add support for testing this sort of operation, instrumenting locations where operations can block and tracking when operations are in progress and when they are blocked. This is all rather terribly complicated, but it mostly puts the complexity in one place rather than in every test. For golang/go#58547 Change-Id: I09d8f0e359f3c9fd0d444bc0320e9d53391d4877 Reviewed-on: https://go-review.googlesource.com/c/net/+/515340 TryBot-Result: Gopher Robot Reviewed-by: Olif Oftimis Run-TryBot: Damien Neil Reviewed-by: Jonathan Amsterdam --- internal/quic/conn.go | 9 ++ internal/quic/conn_async_test.go | 185 +++++++++++++++++++++++++++++ internal/quic/conn_streams.go | 2 +- internal/quic/conn_streams_test.go | 29 +++++ internal/quic/conn_test.go | 35 +++--- internal/quic/queue.go | 14 ++- 6 files changed, 258 insertions(+), 16 deletions(-) create mode 100644 internal/quic/conn_async_test.go diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 5601b989e3..90e6739630 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -7,6 +7,7 @@ package quic import ( + "context" "crypto/tls" "errors" "fmt" @@ -71,6 +72,7 @@ type connTestHooks interface { nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any) handleTLSEvent(tls.QUICEvent) newConnID(seq int64) ([]byte, error) + waitAndLockGate(ctx context.Context, g *gate) error } func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l connListener, hooks connTestHooks) (*Conn, error) { @@ -299,6 +301,13 @@ func (c *Conn) runOnLoop(f func(now time.Time, c *Conn)) error { return nil } +func (c *Conn) waitAndLockGate(ctx context.Context, g *gate) error { + if c.testHooks != nil { + return c.testHooks.waitAndLockGate(ctx, g) + } + return g.waitAndLockContext(ctx) +} + // abort terminates a connection with an error. func (c *Conn) abort(now time.Time, err error) { if c.errForPeer == nil { diff --git a/internal/quic/conn_async_test.go b/internal/quic/conn_async_test.go new file mode 100644 index 0000000000..2078325a53 --- /dev/null +++ b/internal/quic/conn_async_test.go @@ -0,0 +1,185 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "errors" + "fmt" + "path/filepath" + "runtime" + "sync" +) + +// asyncTestState permits handling asynchronous operations in a synchronous test. +// +// For example, a test may want to write to a stream and observe that +// STREAM frames are sent with the contents of the write in response +// to MAX_STREAM_DATA frames received from the peer. +// The Stream.Write is an asynchronous operation, but the test is simpler +// if we can start the write, observe the first STREAM frame sent, +// send a MAX_STREAM_DATA frame, observe the next STREAM frame sent, etc. +// +// We do this by instrumenting points where operations can block. +// We start async operations like Write in a goroutine, +// and wait for the operation to either finish or hit a blocking point. +// When the connection event loop is idle, we check a list of +// blocked operations to see if any can be woken. +type asyncTestState struct { + mu sync.Mutex + notify chan struct{} + blocked map[*blockedAsync]struct{} +} + +// An asyncOp is an asynchronous operation that results in (T, error). +type asyncOp[T any] struct { + v T + err error + + caller string + state *asyncTestState + donec chan struct{} + cancelFunc context.CancelFunc +} + +// cancel cancels the async operation's context, and waits for +// the operation to complete. +func (a *asyncOp[T]) cancel() { + select { + case <-a.donec: + return // already done + default: + } + a.cancelFunc() + <-a.state.notify + select { + case <-a.donec: + default: + panic(fmt.Errorf("%v: async op failed to finish after being canceled", a.caller)) + } +} + +var errNotDone = errors.New("async op is not done") + +// result returns the result of the async operation. +// It returns errNotDone if the operation is still in progress. +// +// Note that unlike a traditional async/await, this doesn't block +// waiting for the operation to complete. Since tests have full +// control over the progress of operations, an asyncOp can only +// become done in reaction to the test taking some action. +func (a *asyncOp[T]) result() (v T, err error) { + select { + case <-a.donec: + return a.v, a.err + default: + return v, errNotDone + } +} + +// A blockedAsync is a blocked async operation. +// +// Currently, the only type of blocked operation is one waiting on a gate. +type blockedAsync struct { + g *gate + donec chan struct{} // closed when the operation is unblocked +} + +type asyncContextKey struct{} + +// runAsync starts an asynchronous operation. +// +// The function f should call a blocking function such as +// Stream.Write or Conn.AcceptStream and return its result. +// It must use the provided context. +func runAsync[T any](ts *testConn, f func(context.Context) (T, error)) *asyncOp[T] { + as := &ts.asyncTestState + if as.notify == nil { + as.notify = make(chan struct{}) + as.blocked = make(map[*blockedAsync]struct{}) + } + _, file, line, _ := runtime.Caller(1) + ctx := context.WithValue(context.Background(), asyncContextKey{}, true) + ctx, cancel := context.WithCancel(ctx) + a := &asyncOp[T]{ + state: as, + caller: fmt.Sprintf("%v:%v", filepath.Base(file), line), + donec: make(chan struct{}), + cancelFunc: cancel, + } + go func() { + a.v, a.err = f(ctx) + close(a.donec) + as.notify <- struct{}{} + }() + ts.t.Cleanup(func() { + if _, err := a.result(); err == errNotDone { + ts.t.Errorf("%v: async operation is still executing at end of test", a.caller) + a.cancel() + } + }) + // Wait for the operation to either finish or block. + <-as.notify + return a +} + +// waitAndLockGate replaces gate.waitAndLock in tests. +func (as *asyncTestState) waitAndLockGate(ctx context.Context, g *gate) error { + if g.lockIfSet() { + // Gate can be acquired without blocking. + return nil + } + if err := ctx.Err(); err != nil { + // Context has already expired. + return err + } + if ctx.Value(asyncContextKey{}) == nil { + // Context is not one that we've created, and hasn't expired. + // This probably indicates that we've tried to perform a + // blocking operation without using the async test harness here, + // which may have unpredictable results. + panic("blocking async point with unexpected Context") + } + // Record this as a pending blocking operation. + as.mu.Lock() + b := &blockedAsync{ + g: g, + donec: make(chan struct{}), + } + as.blocked[b] = struct{}{} + as.mu.Unlock() + // Notify the creator of the operation that we're blocked, + // and wait to be woken up. + as.notify <- struct{}{} + select { + case <-b.donec: + case <-ctx.Done(): + return ctx.Err() + } + return nil +} + +// wakeAsync tries to wake up a blocked async operation. +// It returns true if one was woken, false otherwise. +func (as *asyncTestState) wakeAsync() bool { + as.mu.Lock() + var woken *blockedAsync + for w := range as.blocked { + if w.g.lockIfSet() { + woken = w + delete(as.blocked, woken) + break + } + } + as.mu.Unlock() + if woken == nil { + return false + } + close(woken.donec) + <-as.notify // must not hold as.mu while blocked here + return true +} diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index 82e9028609..f626323b5a 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -36,7 +36,7 @@ func (c *Conn) streamsInit() { // AcceptStream waits for and returns the next stream created by the peer. func (c *Conn) AcceptStream(ctx context.Context) (*Stream, error) { - return c.streams.queue.get(ctx) + return c.streams.queue.getWithHooks(ctx, c.testHooks) } // NewStream creates a stream. diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go index 8481a604c5..bcbbe81ce3 100644 --- a/internal/quic/conn_streams_test.go +++ b/internal/quic/conn_streams_test.go @@ -95,6 +95,35 @@ func TestStreamsAccept(t *testing.T) { } } +func TestStreamsBlockingAccept(t *testing.T) { + tc := newTestConn(t, serverSide) + tc.handshake() + + a := runAsync(tc, func(ctx context.Context) (*Stream, error) { + return tc.conn.AcceptStream(ctx) + }) + if _, err := a.result(); err != errNotDone { + tc.t.Fatalf("AcceptStream() = _, %v; want errNotDone", err) + } + + sid := newStreamID(clientSide, bidiStream, 0) + tc.writeFrames(packetType1RTT, + debugFrameStream{ + id: sid, + }) + + s, err := a.result() + if err != nil { + t.Fatalf("conn.AcceptStream() = _, %v, want stream", err) + } + if got, want := s.id, sid; got != want { + t.Fatalf("conn.AcceptStream() = stream %v, want %v", got, want) + } + if got, want := s.IsReadOnly(), false; got != want { + t.Fatalf("s.IsReadOnly() = %v, want %v", got, want) + } +} + func TestStreamsStreamNotCreated(t *testing.T) { // "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR // if it receives a STREAM frame for a locally initiated stream that has diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 110b0a9f90..5aad69f4d1 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -144,6 +144,8 @@ type testConn struct { // Frame types to ignore in tests. ignoreFrames map[byte]bool + + asyncTestState } type keyData struct { @@ -700,21 +702,26 @@ func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { // nextMessage is called by the Conn's event loop to request its next event. func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.Time, m any) { tc.timer = timer - if !timer.IsZero() && !timer.After(tc.now) { - if timer.Equal(tc.timerLastFired) { - // If the connection timer fires at time T, the Conn should take some - // action to advance the timer into the future. If the Conn reschedules - // the timer for the same time, it isn't making progress and we have a bug. - tc.t.Errorf("connection timer spinning; now=%v timer=%v", tc.now, timer) - } else { - tc.timerLastFired = timer - return tc.now, timerEvent{} + for { + if !timer.IsZero() && !timer.After(tc.now) { + if timer.Equal(tc.timerLastFired) { + // If the connection timer fires at time T, the Conn should take some + // action to advance the timer into the future. If the Conn reschedules + // the timer for the same time, it isn't making progress and we have a bug. + tc.t.Errorf("connection timer spinning; now=%v timer=%v", tc.now, timer) + } else { + tc.timerLastFired = timer + return tc.now, timerEvent{} + } + } + select { + case m := <-msgc: + return tc.now, m + default: + } + if !tc.wakeAsync() { + break } - } - select { - case m := <-msgc: - return tc.now, m - default: } // If the message queue is empty, then the conn is idle. if tc.idlec != nil { diff --git a/internal/quic/queue.go b/internal/quic/queue.go index 9bb71ca3f4..489721a8af 100644 --- a/internal/quic/queue.go +++ b/internal/quic/queue.go @@ -45,8 +45,20 @@ func (q *queue[T]) put(v T) bool { // get removes the first item from the queue, blocking until ctx is done, an item is available, // or the queue is closed. func (q *queue[T]) get(ctx context.Context) (T, error) { + return q.getWithHooks(ctx, nil) +} + +// getWithHooks is get, but uses testHooks for locking when non-nil. +// This is a bit of an layer violation, but a simplification overall. +func (q *queue[T]) getWithHooks(ctx context.Context, testHooks connTestHooks) (T, error) { var zero T - if err := q.gate.waitAndLockContext(ctx); err != nil { + var err error + if testHooks != nil { + err = testHooks.waitAndLockGate(ctx, &q.gate) + } else { + err = q.gate.waitAndLockContext(ctx) + } + if err != nil { return zero, err } defer q.unlock() From c8c0290b421c479315f66c7b68b617ef6e73c668 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Fri, 4 Aug 2023 20:36:23 +0000 Subject: [PATCH 024/168] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Once this CL is submitted, and post-submit testing succeeds on all first-class ports across all supported Go versions, this repository will be tagged with its next minor version. Change-Id: I0e70dd95a267e08181e5ee4d7c3239a032aebdb3 Reviewed-on: https://go-review.googlesource.com/c/net/+/516036 Run-TryBot: Gopher Robot Reviewed-by: Dmitri Shuralyov TryBot-Result: Gopher Robot Reviewed-by: Dmitri Shuralyov Auto-Submit: Gopher Robot Reviewed-by: Carlos Amedee --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 018af6f4e1..90f428f40d 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module golang.org/x/net go 1.17 require ( - golang.org/x/crypto v0.11.0 - golang.org/x/sys v0.10.0 - golang.org/x/term v0.10.0 - golang.org/x/text v0.11.0 + golang.org/x/crypto v0.12.0 + golang.org/x/sys v0.11.0 + golang.org/x/term v0.11.0 + golang.org/x/text v0.12.0 ) diff --git a/go.sum b/go.sum index a9f84de711..c39d831315 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -20,21 +20,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 1e23797619c957fb2d0a7ed9ae1083fb31f592b8 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Mon, 7 Aug 2023 18:39:43 +0000 Subject: [PATCH 025/168] publicsuffix: update table to version 20230804 using version 63cbc63 last update was done in 2022 Change-Id: Ic4634caf5c9dfd97211a5dff966a3ea2ed6a461e GitHub-Last-Rev: 5b94982f4d7ad7032c80df6a20d7ac09f0e3fc96 GitHub-Pull-Request: golang/net#187 Reviewed-on: https://go-review.googlesource.com/c/net/+/515895 Auto-Submit: Damien Neil Run-TryBot: Damien Neil TryBot-Result: Gopher Robot Reviewed-by: Damien Neil Reviewed-by: Michael Knyszek --- publicsuffix/data/children | Bin 2876 -> 2976 bytes publicsuffix/data/nodes | Bin 48280 -> 46610 bytes publicsuffix/data/text | 2 +- publicsuffix/example_test.go | 2 +- publicsuffix/table.go | 14 +- publicsuffix/table_test.go | 1667 ++++++++++------------------------ 6 files changed, 497 insertions(+), 1188 deletions(-) diff --git a/publicsuffix/data/children b/publicsuffix/data/children index 1038c561ade4683e91b37e8298fc5ead7c306779..08261bffd196fd6942b4cebb5ff06e0ffe53808d 100644 GIT binary patch literal 2976 zcmWO8`9qWS9{_Nl=dkqI0;nk~GuU>uM@qcI1JG0(VzeBB> z_6N0UM-8>A@-p>RrAY0X)8av$LqKtrG2viXZot8+JnGl50;&X;1(bx9 zQ%BN&qmERar^@gJsx0ghbv&$+IuT~4D#F^SiYg~{Cd@^h3%eh1F8yi1xvFQ>dHkHJ zPJcmJt9_}U-)3UpLH7T2?FgI zK8yCum`(SGIrM;xx%2?5iuTHwPs^+t+85Rm-|#frKVu#7w{E2aU>h9}zMT%t*r5ru z=8&N9JtQb&Fa3dapJp%|q(2NlM2A2L2?@87kc=7aMum5g(HTy9wDkr(25you8MnyTj63vL>jN?_;~_oH`iP7Ve@st+ zr}RX4LC>_lrUmJTL@E7b25I61 z(kv)Rvu-wN;pUJQVJ>OaMUz%hMXm_*$rW8ZY2y}>HbG6=#01hVXi2*+iF9zu><~6d&y01AGs;)CpX1Ba*Hbu z9UymvL*$Nln7+&XLf_T>O701zn)_TCxvwjy{}E4+$6_^k!qt%{LOppRHjt-66M3p@ zAy36t@=Ulwp6S}jGqHm_=dRMvg}=y4u9LhJZjqO|+vKHqhknJ~C9i~g;#MTEfneQRU9Dt;5gqJD< zQEyui&nZ9Txy&KFo7+$%vkm8c-PR)C%rxX{`-b;(ThIF`zvcZhHy}UTMkIIJ#LJa> zB+oP;xy^`zZMkT$n+1K8c?b=2JBEfS&uWKdoad2nc3q#N4q;f~Th`l9bVywFaML1<@oAlmC8N5;sx$e6tZ{hGa$ zFY!3WmqeEHCE366CABB`Qje2-X=DXontci#jXcdC&Ay0^*8YLYJpSa%A}{gBA}jf0 zwKe?lNRdCDZRg89YWecWdcHin0iEz@L=};3=yar0Lb7iMlKNeNd|zE4KPxkk-<>51 z+?_3v@6M4V)*B@&8}>-j8mc85XS*bs4gX4V=7dW2^_w9%&=@N@mh+S3Y`@Er%6@-K z#3)b6gQg*pmr-XJ_PyH-*F1>voI8y1@|?}cJk?C#k6$GPHs>S-dG1LHn)@R&#B(nb z(p=7r`B7v(^Sr~%><`SWmS9G^cM7wle|FN6=ptrS%K>Ihv>3EzZws@re=D=K?ry4Ps35K4*$sw=fk0a)T=NIf5!$yMs;*a3!6Z*OPQ=-*x7~ycO@D^)@N#<(zb5=4Qt}I;Y7nuoNUO!OWTcjS==x3Wrh;@vi4G( z5_bgeZ7-AW^NEq~Q^jJvDo(C9CSZMs1{+jbxzQ&{p6jz%o@-2z?^i9y`;9B*`#V<2 z^HrrvygYVMXTD-_UIDwfvyfdP zvnZCt7qLt74k(t&irJ;{huEcghZW26erA_-ma-|bBWz0iQ8p#7Op(%gj7^mtXH(*woGwiskVq+2whs*yWw4*%h)giWTu^*%f)`6kp2DE53~Xon0ln$gY-I*|jno zyEgtWc3oa4`%U}}_M5!h?E3gS?E1X>tUmsMLZ6?YF!*W|hJ}d=Lw*u#^i5_Db}d#M z$}dnH>MB$m_O&PuFDz0V&Og8&?kZ+~%|FPNb{%7n_?}_Qe9y9FU6?1ydxU|)!##)=?9RTav(%)3=T2XK#1cqjBtoB($o|@ z%G3p;)!i`0|2j-i-+~FIe_^8IB}{g_g(+qhrZ_!ds<{t@nft*Erw`0DD0OK)my7PzS69t#dOZ27C|60lQ&| zSq~}B;^5SP4XjDo3O$83^qHT!#;vQ zuZe|xk^rXev|zJlTd=usN3gj&9SXa@gCdO%P89wCCp8^VK{}zL+X1HvU2s}+1J01! zaHjhnoGp9^=QNMtJb4;?Uh@JjkbmJq_iOmQ@GV^AX}IW;z#kSC{?zt{OT0T=a`lBu z%X?6z9RSt5H&nX@fz{#%HCh5TJ_u~C;b6CfLap{wsN+XNoogJ_YbQbjKN%WaQ=!o^ z9h$T=p_z|>X4foeu|z?ub{<^elYnJuk&~5~WWeYg9TcO*v16;fgdiYGZ!S99}T0Pv=8sM&_1n#*?rM)c2 zrF0Q3rF(iyQ6eWrMeb6j$3u#H21+6EeW|o3K*}X5r0zXKq6WiqNQGm^Q7L12~wY;r2hjALVp1O literal 2876 zcmWO8`CAib0svrUzRb+`nSk6zK|{VtNTRiJd7yyG8JU16mRliLD^_b=t;%||>na8l zP!yF%fdB@t+Ui#Avu#}!)CeZrS5%ZMplz*Iv9&5~*B{>h;dOCwadCeq;GIS9q`Z^& z4zVS!huE^`0kP%QL!#hTKe0dV5pkd}k|?!C6Nl&oqRgryj?$?_d0`G=rZt2)emhZX z-9en7jfADpL|Ci`i8}faai*}0IAc9YoTX2R&&HotpS7M5e;NNJao+kBaiQ><_=2^8 zxJch1F2>u5ONGtEC2I%qt+kW*&U&Bt!TN}}690_2YJE;zx4sqEGeBIQz$5DSQiP46 z346kOMDyNYqMeyTbTF|*XM&RGx}8MyGpQt*u$=@DVW1RXU~nZTDBVa8s31KJv7}dH zBIym6lHSS`(z|gP>8ng7eGQqUP?<$eHK@s{jhpc_xP=T*Zp8tHe~|%=yGSwoHz`r> z)<_JcSPBnfsj`ez7!GR`jVH+&@Dv%`cn*ia+c-qoh(iobI27K&p-MXrH8hiA?+&y|}@M@D2V1e1j9<8#Y&blbeWd+C1_t-LV zFPDvbjVnn9e-(BZ^RUCF!FO$1e2@DG-!tapd$u+BKKC)cZ(N7__@9t{+^1xpZ3BK_ z+^BiTZN?961>`V)8y?}C@Ca9iM~sK@DE|l^HJ0O1+cErze;hwDR^UgrD*Tvl#*evb z^0Bc7|IF3mpN(JPpKV{`C;ao|6Yc_jV*C$&V*3XF!oP@r;V$8){LA<$_h0g<@jLPv z|9kS8?F#uTca40(uP4WhjpT3q7V>v~7x}x*LB6)#C*N?7@EhZgCI~j&0$~Cx8>VVw!|d%~wxAQtHFbe- z!9%dvKG>A@uAl4OuxMFt@*X#=tTqgl#u|G&m!hma509ElUken0(Qe4A9O41^* zym>KL(aeFg;#82@KJFwSYKQPHo9H~8wqhMdMH`rIA02L+E*-E#6u$9T1*vgX6*vgj8Y?a#< zwkmlmTUAoPR<-;SnBBGkbMki9T(X0$F4@V}xb0$VN_Mj~Ero1t@?N&Kq=>C;*#|7i zMsTvE6r3(O#&d6}m3X+vNIX(vB_0RjBpz+?JkPcSo_8C^Qyoait;Ej8= z@c!=nmEv{&O$k=uMdO=r+-qkyl^6me1$6X8Kq1|gj8iuh`!2qi@qvt zD`oL5pw9FhpuXujMSXw7MqTasiE8$JOSPqkQ9bF4sRu_hsnJQBsh=j5Q_m-z);~|b zNsS%7MUC~gP`~%KQhyx1PmT8uQGfQ1QGchuqUqj0X(q#uM#8D|1fhf$2<3r-j3C<0 z5ll}ME}$otN6_w$DB80;hV~LB(q0Y~?JZnNPjaNtLSZgFIU|qubLeURjP$^7w=>?-ckWN6n~%?+Tm9zH?b#7@ zXLcOjdpwDD_^k?bWakAsj;rarej55OKV9Ho*?$E7b^JBslKn>JQb8~-eI!HV02%2| z$$&qUfeL|)m*d9pDm-MoK2I5)<0Yf}Cd-%{KN(XoRR;a1$zVk!2=9l1)T!@NY+X-;H1`;(b2(Nd->H-+gk zFOzlkFK4<%sZ4k73Z~oq0n^=|ChN&fXL`(;OizCn(<{oB_2%X<3z;Ev5i^{-j~O->Ll;qr{M`oRE(3&|2mo>-j|YhX z3QnwM$KsKCQt%ZKoA40!@ zPoRImdMK*?6sq!e!VGaR%uwgSj6pTb5^G_WdNo{GlMmJ6&2qJRH`I#vKz)q~>IaX& zj|Pvz{2DX-c<>}#J9r+h6JLbu)R*7}@nyJS@Fv`(zAfKW(++pmbjWuOzJZ^M-@-4% zT<`gC~Y|LJQsx>of=8Da~Pc23Nu}8Vfv&>)<(j8lJ}&;Q65|@XF9N z_&+=buWDYxtF^D;b^Hci*Sf%ZmVoucJlMc8u;B!R4Z{=Q4VDjYX$7!}^?^-V3AAaY zuw{5AY#&}A?_d*PhgJzYhL=ExmV=HHYUmi&z`Lv#KG5pmL+xj9h%JCaS_2%>7Qr{{ zVK}aRj7au5;yIy$(s?N;i;seG`Xbsn2|=A7nqm5d_}q!P)U)ktAE zfu`$Dq8XAiXom9~de3qm&D4E^f+Uwwkn=hUw%kA=Ix7m5G@($Z9fj#y(QHXOn(gdD zb1c1RuI?V1CwYM8IR{X2h9V^|P^9xEiqgG83nj17LgzSI zWceE{){`h&N}=cxh+^vaC|2)=V(UCmoZb_~kNBVjsUK2G{ZXPc043_>XsKR-mexh0 zl#wX3LK=h8q$wy(pMlawGEurT8)fQqP?lbgvPbgKO8t7YYUDGd)^9_ax;;oI-G_Ag Z1B%rnr6^x|0)0GUL2F0Oqfew4{}2BS2s{7) diff --git a/publicsuffix/data/nodes b/publicsuffix/data/nodes index 34751cd5b9d1b14c3a0977e4fa4b04db058b241d..1dae6ede8f292889cb4252aa473312fea1bac46a 100644 GIT binary patch literal 46610 zcmaf*b)1#e_W$?kNoIy-2m?_O6)Zwjp7Wf}IcLsvASMv;X%`~Cg1UVHCVpS5;A`+3e$DplxF$`ke~RpAr%S2WEosVT%r}*7N30Q%5BeZw8-;9A%2-sZly|o-Wj9haN+o7c4W@j` z-ufohs(Wn85j+Z0{XXB$VuUk1=XP8Qbe-L~3gT;Gb z8m6wI~KO$n+ege9N{d z6^I~Gb+1FI)NNf##n+%~)I!Qz14<#tsr!tHj%$xJD7h5YX$HOgM*r_tzZxN+265WEQ>?*T$B65Kzy(%kwJB(nkMP;>S zV&3;6NsL?-R5`8FFlszSM82JNrRF+y343LKP%7U5Dx=0L#D~ix8`aR($!Pem^{8RS zv6%O+v#DXN`&nuGtzHc`3`(&XXcX^{Mj+xsUE>>&K{WTX0!I-so}%A#2|W!;f(RU=x5 z;U~=1qvtU1{!-+=$AD??5Yox`TLn@x3oK?n z=*`L9WOrdLr4+d`H+877?Xxgt&gvz$Ye=H`0r@k(_d|XlYo6Vk)cCeJRyD!=)ucMI zOHJq)*{gQ*{2>}MaanMZV1CA`cI%klu68$0u2FkBQL3D-XxIsa^e@y{l< z*$ZLg zThxlbN2Em7gOdHyAlEjjeOu?&t3%p;K`j(;sYBgsO?7D7L+xrt>!Nm0mDj6T{XM7^ zXJDoLHoO^&deq@Y2N4xqEY*%)wa7CEq@G|S?Cdk+?Jbby(1op9@MpEml6?1o;&apQ|2{ zQ47h!kuS@o&TL&`rIZ_1XQfi6I;-;yt6J>cSfdtqePjO`!%SGjEIdMd;k(%fN;f&b?RdG_e2!?#7|-` zXV#Wo3#0lTA%9@h^dsy(#Z*ho4y#(y<~3>i!=^4VexT%;ib&{{2HKuOE_M#ystYKU zj0(c-153%64n+EhvM`7b-@Z=54^s-b4Ysx=<2QFZP@{DzkxY?}BDkHS=PK5&!s=^lT31V^uQ;kUC{SzCu9TzoWK z<50V*H!L;Go_QYC5Pr5>HMFu=s@RiFs?k`7T;S#m)nxqUQO(Ai-PqfLT0tv5%C?cA zvYEtI@LmSwIf5=}@-iXb2&v}RWk2#svJF`*!)B*y32%TK-Vng(bV$KAGz#7Yb8%)9 z!ZKKWr@K^3`>7!C?MGXr0ukQ>LLLRZ@7aJ_5k9$@((`qY_vsnmmjLp8E7flsA^)m{ z{Nhq8T1T16+U;m3{KJ3|eFm<74s!m4l&X6mCwkOM^H8ER?-#jqK^i>OPBzX8Qk7lJ zUsn&@v1mj#fHdiAq3W$wD_cKC&GQW+ftP?0%;@oxv#Qm`9#o6(2TSq& z4&=VWNBJM!YIUmv%SAu<$W=F{{5Jry`e#JU3K+$|bx8>|PX;hj(`@Q;<4jD2pA2CB zI~cz2NJQA(Nv@u-Vdb+Xb$Ndm)o7QXZ|MNgD-bi@p{_`LZIVu=&=sxyl!}f@V`)y& zmsPm{uYLtog=M&`DhsG9ecP$JeeLSXu2*Z+RYr9U+y~L{#$hCGs-q;r-m8c_TZf;EBNSM1tyS|14bkXVJBhB-}_bmi>N8=8pgm zTn*Rv1HBrJW4@Br;_9AlSdPhd>~3pU*LXgl8a~X4-wy>9<4;xekd5ekA+>;vUM2=^ zlG5EEjXn`n*R(!Yr>^zS$EfEtOnFYjTXX}}!c%=z%?@>Kdp4mKulB0zjOjJ%y0!)F z_&vi9$ScLl+Xx$Bf+=4PTw_fw<7K-|U2h!Pu5M@zfU5KXiQ`ZkNDpLz23 ziJYtv#vCub@KL9bjp}B@jyKP>B76&LyV=yMZr&sl+puHVeOm|4&m|(?=gn{#led@) zX)~XQF<6H+kk}=e>Xz1g{Kl)B)ZOhxR&|e|@D}!Vse4*8FdrD-gS}U=SN2tty4P^g zW=ykV1z9yM@{vDcN>np z+q%>X-Dg|Xi>+^Bs)T8vN?9Q;h+l=BAO(L=zg7*$Df-25Dd%Fha{=B*%Ky%ZBV~@p*L^ z{n?Juxtpa#?|VySj4yyuxCJ%$-r{JHm)Nf9Q7`wOgRtjEhkC`mFCyii`PC~O-w}~J z2WUZgaPsX1!?#yG_BP|(xVT=uYIr&rJ|ztpt?Z(EGO=t7$EY`eMx-!Hy}CKCM(s2V zNb4}58N;w=4D+hj+_F){yiNFcu@lP&`_*f08%*`OQCFj0Zt-0&rjr) zI5K{=sW-w!VS3f=fP9D}Pa0fLS_r?F6P9sYFV(d&+Lt-07NXJFjlI<4LG`9Fr$)Wm znul}mYaQzCg5SVgcrZT7S7V@XCRmID3>e3F)Z2X#7^c~)-tl~o0pEdEtlWl1#l}9W zbp%r&_0Ee9Q@!i{5{<}GM9SyzJk#2VOF!%ByKRYf^`4P1)qCB4 zpyvMrknRVu5^6*^qK_$yI>09f|I|97jLYdi{sVFawjjg-!+TL3!Y?qm3J$Reaj5r= zB7Bron(F*Q!2B zO*7R;t+Q&>$C2kv^>J&U27GU$W;FTLCjrSuC8sY{HI%H98PZ-Ox#fhDt|eLp*-M_gv>!Kbuag`nmNsJ-(Z$`U{fw2w10ap> z*+uSGv|BRWo7&;NLNIO~wN&J6k0rD9+#2%o&kjqLN49A9>2S-xPNQq1_=xMH_)x4A z4`q?5I*?v4N7PxejXZ>71zncxwvCwbthN#KS9Fx==&%fFcYq)whedO39az3;u;c`9 za9VP@?xd}}B>=HwD)JE;$q5J>C)g|_Ua;3%Mj9j9EhAe$BIwYtbW;2ZVHJ#=ad}F&vbJ|xEu-825JoP+c;u|?`2oM) zS=jR$`1Kl@mNAKuMCmo1n3{->@M^zh%(mT{DE(Fo<4WLquk57yI^5`40ZV?o%wfsj zT3urq>ps?M8D~BNme>lf5JqA$1<2s;otAN}$5<`ngEGs4c_1y_6OqL6^;9RhEaSIb zi_zjTyJdn=1jw?(n5zl7J}I~ak;I)2%Y^obIQQb$>&>L?NlJ;QH$gTaXGYlR5`Dy$ z*v3Fgyoqq)&5f4bTKzSa-91xG%kG^wc3AdErei*tUTfK-+iSJ#Sv0Ag>L{F>MMR$1 ziMR5L8VK-)ddr@zV-a?H>Zm@0c|VVH%DlH5xw-U~_Z=HizKP4gtqg|phtLhQ`h;Mr z{1Goxl(5lP;Sfyf7T%OqnczU!+bste6ET%6!CQHgmpDFe zK)9|6dk<4h%t9?Z%3(P$xCd>;yAx{p2z-?1YXY?Lr5%<7yY6qdOf@nvHCKgz_PPV| zW+%x$qMq)lMJ{q40fug_w@foGw-ejkUfRyUTjVJ(WJj-MTI(1LL{CFF+181-A6=G% zj8ou-=9!j*T1Ue*J|Ift18KX+2HXSdEeE^hTv8y9!=@~Z@h)5j_JPrUo|W)7c3KWG zEQnMeN|fG3ewL+vGG6*S5mhY5QEZOGa>(XmSpbTUu><#R%$pZr-u+XDT$V5Jv0gnx=NZ3Y&vPf^D*PPwQU+;C88}L0)0TRp z(Q;VNOiU#cL|*bOr1U#<&0idr>BjDWH14P;gv-F^KFe>J-uEb3b4M^5>~2J@vB5IK zeU}xDSBazK^DfJbZ4*t)OwW(#dPg)^W^M}hSY~;WR6`5NRpK%y_Uh_zKAn=!fK+;! z#QuA_0RAXd<5)s9jgsDPha1Bv#r|Ql9NuOSbo_d+Wp?Vf46t0%f$)`t zn!LhK>6A{(?Dj>X7BekJ7$;FOAHtr0DR4_>3FezI5H!#T?v1y&>@Mc91Z|jImLpno z(J1L|!17h7nTNFt>9!ncPPJN&Y###G`*N4%DEG%!%TcXM+AVWlt98)b(=iYj*<~>T z7uKRXl3)^}#K#?Q{rA-)Qs>3g$$%_8#A`8H(-|EpIUyGw11e*BFWLAMuJ6;3kV0ns z30Nt5(_=APhgh+<7^Ee`K~=T@wd5j)#bbU9qa@#E@wEP7TD5qynUF)oJ0SY$%2V@%759ru`)`EB2sFmf=ON-;pCtVC2vE{d>kLa z72V9E4{`24t^-CJZAnRSa%UY@Zh;Z}jZ*S60!)6r(Q-=rg}_MUW1x_WC{>t4Fok37 zkZ}kXjz_rs0H@_tqZg#rcOzF2CW(oa-E@zf^b1x7aQ-T4=BqH={Z!rkabkPYPidON za%$TW7*!D=Mi&gH*K(RiKfC;^2lFHCmeab<2ar!LNBvCz4^MGH4g^N@WYh|0)-prQ zm6UjJQogMS`$vPN`giOVUQlaU;Bk4hg51~wc^~1jNifRhMIpxtIT?+zMQn>@i=}kIHpr5#kQ%8j7m-yYr|fbLq-D3| zK^}-h9*qc*(s~hodKF|tE~I@Jq1?0^uAfJfHcT)XP$gfx_&)NdX z17!KAOvv6aD$;>lksXEP3K<6Q3OB+P-ZV(0S;(c36E;~ERxHY()Q|^}vV=S-xvyi=B^J9Zi;OzB(YHG+i(03e z#32uqad~P>9)_^-nay(M)(orVEceDb=FFxZ%UK<>YhZMAC^amB%fPLavV4BP_Om^cF6j4tHldBN+2Tlj1X zr5;QfMRZEC1mFb=FrW{(#w(cDCq{j^3tdE2AGhMQAWdaBF(8|8>^iF6FLCbM+P`Zt z*F#{i_Xj&(ce3W{Ha!INh!|}1Md^=$Q8FA<17kym)Sx|mi7;q%aQJ8|)iS&-_tlXc zd9t>J$gZ}RaRZSLATSwqzKJqeT|yiXFWaJZmE6f#?c`w8a)d`MCqr2soyxj2Lil5@ zAh9EC%Mn>ePdh%u(b!{1M5Z0$H2Prt)C8l8l#HB#sWBI0U|6(HSsM43L*>nFMysSm zzsAyeDf^O8_3Y6%l>^r-HGX}IgzJt$ zw7r}g8xcN|F=VMFXxlNOJH1|I>#6Hl<5Ai2+HOdAk90MpWnoCEjPsd785d{oN^$0M z_}~-_`srCX8r80HQui=ybMM7o;q78i7JxA$?aEqPXp(Gu&92g_>D@$Wt7S}bX3f?8 z>uIl3c@p$cjZdzCRYZK~D9u-G_^?}d^|||OlN=0{HhGA1--#*r`5vm1sfyp+7Z#Ij z4b5>7a@w1`bIoi2)6{=8;GIqC{wr6*Ra`46rxm|SUO*|i!?Vb%3VXLy$573(T7gR@ zmDU&>=mTNN)wAwUeVH`am&Tr0G3B9uU@rA(RMXoa|DMyoQ5hoV{*`5Y7N0?eDaNr(%*h0KOnZEL$R!ldJhJ8)%O!>v;mhzGYBvx`>MARKt?ti^5S7& zFvLIUrcG|`-PtyTO&{K5y)t4w%G$ObVN+ixIBD~4f#|ztJxa=JG3t+DzHEOOresYo zpq}5U8va^F&d-#R`7pTPr0p^Gz z;b+Z|()<3uAN_X0eZNOds_%5Ez5Ue;Lgzob@g~Q- ztaEsP(ddWIYgE=Nifh&0`ukILb+c0G=h(p~W5bwK2l;c0F-+R@qGfFcsUDrlVubaH zR^CAlWYQ{$7~F8nW`a4@#pOWTyxRZi1u;5!B$iENvUi)~oT(0}l)D1wMI<(NTsQU1~SgEyG9)1PumHiDO%DEkAIp;Y* zDu;6WjUDQsW&cSIk%Rxn&}wR&iP4C4*Pzw|PICH}{?$WjV5Qc*e<2YWvmw^2GSp#u z;&?Y=U|`=GWwMrJ*6bRI@=c8LS3ul)jE{p6{?^36yOfMfD{9*y#`tztvkxc@e};$A z*svOA#W5yoS3x}2!O$lPd6m3tUN%vRd4wT16Ui)7%}562IYrhI_tthw@=nA&50S(~ z+Kk*9$RCtSdTG;_h5C-H;=&dVG5gSFWI)Qe_&sdr!e%wSXjYgY?Tu=B>scm)P>u!z z9RNd(4zFVQlTYyG${p>n$v!nxXTvVfACirkm%hv$kMGOQUD+5sBMqP1%sBm_dskSw zyU}^hyJ~mE_UGfZwdNEUvX{-u+3BUH<)brw_8C}s&)Ep$yiD3g!f^3&?fB8_5{$J9)GY6ih}_p8_8c^V&cr$${hf<-}G%TYeMC&O9CSV~-Kc%QArW4H3C+0CU(<0PH%*q6M$SSrDalTazUDVAeTyYwN!5vvR0}O_duSI>T`{dR%vUOYOj>! z@+@tOkQasQ=z+Z42zf)uyF&iqVpUcFh)qbYRP%ZuyGv;wDaq%u=`$K3vjULWQZ-03D?s43fkH8P7wiXv?~k7n`aANyd3=I9aPe?yq8;CQ7@>b9(YqrvFr(nft0iWwoEW1ttnrjg~ z?ds&dJw2WAl0I3;%MpB>RLu5d!QS|n!&GZ1W!%XC$@p>;e)F-Hbrd0F$$fj)&IF_6 zHZW%&j)4(%ILe>aLUnEyPj{)%>-L z#C*B?&-wws)@i+jaHtr460UO9P`b5G&6g9v{LVFWRQ8dqog)t+iPjuQ_Vr!J@%@_0 zc3^7EzYuYJL^kXTTGUB_KEJXy_Qljmo5yZfC%Y4N@U8We=a(`taAHiI{NhUD$hit# z>jTTxDYhZ#x}q%WV{az0IhW-@uAwbw9l_+R2Vc&!bgk<~G%VIlM7}rK$elv?xq?cc zs&bDCs8hDNV^n2L%q?cDsUOjGI(Gf1AD7vWNUB-uTPVKL?l0$jFN5cHi6a*&U1-b6&QS0ou54+b#x zeFk!Bg*q)E&)8#kW~tK}yhCs~sRKr(lU&J;ohNHkUYvb!^(pk$_!BYe&x3?&B?Yv(s@5~RlUs^w{Xr? zmRZ{&m5dzA1NvVj&3%ze-g&8)n$;2I zyu~|W=b7}IMP4S3u3ByZhjw=7(45ezft2v@JN7RhMFunTkx;kFBh`2?>)FOjN z?EfI_5n<1NfZ-JwK7rwTK^*;`R{SCy5aECp{tu3VA`%jj(0>yNi%3L7BL9DvQ4x-b zaP0pt92emN5&m1k66G)wJO4LBCp#%>g`!sY-!P{{q)0@H{{JwFMYu$SOaA}Dr6OD= z!hgeDUJj#tCya{1|1tzL`bv?j6uGMZQ?6R%szq+Dmis?@`e+e8T7>_GO1qr56YeqP z|C?)FD!1LX@_J^)ba}lpk~^~65j@Ra7LKqK?Cnw3>o(WYHjQbJo>9j`Xc}hDpcN?PKb0%x2ay^VBeA zp32JOX=3ymcFD2wa546Cyx9+;lrA4|<$p;j?FBog9-@?)Mai`qE3VZ{`|Q0Mgn&Rm zTCZ>SAeR^5IYrhDwZ;mOOIfX^AFp${d*B9$Wi#z@Nd5whx|aH>R-x3Fn^2(Ik%q^a~7 zZW}Ih-V_9K%_r9cyB-y|cGz|M!Se^7PoZI+YNh0ms=P+fFRK1_kEE|t2k(*e1JPeZ z7UCmf1)6?_{R`vIn`zcJR=q~?$s z^5RQVXKz28tw_l$mw_ip31-;W?dqJ4%;oA_k9==tDey7EB%O+1<((@(`cuwh$<_E4 z58VI#M)5E2d-Q{ylIu3xoR~VVb3U1LuIg6jTlVu|Z!`yC`$@D}Z`=sEp$YOSRom5! z({XZC8-5uP4NbAnIM0NUwi_KPk1f{b3n^tD03+u>#^lhoo$CC$ljG`w;HwO3*W(Ph z{B4Mgdxz4<6P@VhQq6him`S@KXb?kv2i*XYo^eWM`w1uR(~(WY;~^hq#l zD#B?`)0T4V&MoGB3TA^F)@1r40c}Ke2(Q@&3cQEQi%%Sx%F>XzN`- z(=WaCZRp_pZf#zl6LU^uSPW5_+@LXMAiYG%Yi;YLN4Qv8?qUAcoY<)@zI;6)j4vT2 zmg8|GFO~l?t{h*2MW@O6UM+njuO$C6#^pYCaIoqZGK1WIzXbe?Tx=(5`U+$~L*GTp zfYaAcdM*qa^_^uycyEZzfTfo|;b6s_P1Vs#!UnyWa&bB6Qs2`UUA5dn{rUPr8`a;x z=(#G>s>U}1wY+JhQ(plmF7v3`)<3Z%r^3~MJonoE$CTVvc?%%M8TNr()eUMyudCgq zp=a{nUmL%I6+LU6&!nmPK{X9r=lRczBQ2-Tf9k)!pZ=S9eJiAIw)G|8sCSmD#*IJ9 zXukoZGQ0;~7F&Ah>CrrSHMPpu`BuikK=>n$=my_+#Fldl4AsS?D}SDnU|t@)aiv<` zEnh!bG8tTaeu#*l5ut&Pe4B}FmGxBbWdpl`UTEdYaWDj(PTf8KDMNa#`%AC>xr)jc zBznXu%YjV4)Lo2ceT!Z33Yfk5{sEBm3@Hvg$e@l-wFJ z8ji-8G4+r?uTKg#$lA1uNvAKr-8U0k#R|^Ap&)Z+uzM08;C$Fm7%=~6A-3bmZelHd z^&;(=Le^Z30^`<>U4voFWbeNXR+%_D{K0{qbfKZIz8Nl)y-8~*`le`eP@oemv%Gw|o&|Jy{=yR*X#NiPtf+Ormo zf1kSkKS;!%m-{1~%_0!$B4?@y!sA)NUrKh3k387zS{w+=a>BUl&oE`>lX8(*x zuP^!okiQ50?~B%mZ-_%r$-f~D+(i;?*V+hmnCGuy*_L$gx_T~p2=pZtto$XXGAr~= zbeS{4oY{?GG4O$-TufIiAz@V|2v_|~wc3J4_0!1B9Ys~muh|v;VB~)b+E%rkaVnoc zjoh69?0TAABcr90eNgWChssZhb7beV$j4Aa&SbC-y&gI1+UCE;X8Q)=;6CKa_>}I} zi--I}$-tZR!M+^)v`OE4>$#w_tC!Nj9t{1#?9uD9p0$D7$d&$ag8OJ3mCN0=POSf9 zT?NvF^itj@#@oI;(V;=Pa^-kzqhWwhoE(odb;Q}Ld@T&E$g;I*}U{tq|au9 z`O|80qonNL(p$MlZA1>;o3ySTj)TJbb*nMN3WARzdUhxG#d1o1^-%8}&69m7jwc*TU6f(im!DMa}|-jXdkH zye|P<&(MLHCkJ73Azfz3Z*z@2(IAPt@;4Wx1G{b~Tz*UigZ+2l^GE$oME{szAU3&} zai0Y-_Ax_WqEm9OU`FYamh(JNWp1Et6E#DyKg8+^-0j)i8plv*L*F@(aoOSq;9`@2A0*Kwd#&gJNHOSIdoXB{*16{ z@~}7VQx2}VFZ8NAYOZmpwf2qVIgh`}P__d}XKrDpTKmTD+(Fg!(oxy(lPft-Dw}+U zlXWr(a^(eW!w&Gtr<3bSu7i*=*iFDuthG~q5frmmnCC8mss&Z4QukS3mKAex? z0k!_MTFmovsdcSfx7}$R=2Ulf^3%M#Dg!Qrmw-xNdj736SRE@rtLH2}&}Dzw#Rtdp zz=kUa8$EmUgwS6@4o+CTPz-)`ucx1$Wsbvup{JEr8~AzqpZk{jka4ACU2{H9S6I&R zGE=(ffa-Z@!RQh5QNW_!+k z5w1KD7_z(?r!f~oNzpxttm&^*^dCEwR?(OGkDV$PceAM0GI5Ms5H4E>7DG;bMsp45 zdxMk=ycvcQxsSF=iCbdd%fKyJ!@;ZaA||Y_m98kAwvml*1L!N30la)W;ipE5DTRTm z8!_PaQPLT5Hwxo6NR_)0;U`cle}dafbG1VVv(dbTQ<^Dn9?dO?nC~)>+;cFX|7IxV zZvgJs0805UBl4MIPZqr38G`vHGU@+$}gq4p?N;`=>@Fuwm zyn89+Lk3OYQx@95S7amb&moW>gz+;70>AAbLsmvp(6y4`a~g>VFZE!`hRfo+7)_=7 z`>8tGN$1G~I50K(apYOu#4^&0NOJvZ$kSxkx{S#fSWozc|A|)oYTMQZf{q5+Gq;Hqz#xEik2y zl6*^NB%Aq^>Czx2Zs?W%VZSM_Jxw`c$5MbaC-%TSu7eQvb<+FuVvysQ2Eox(^%p|k z?iwj!ApA=KM=U}GKWBRLpb&+2I5I{3rx5L(Ok%*Igoy; ziNh0gR0Sa<|-Wb!OBnL3o^+IJ-5AovlZJ#-?ain~Bxb}!Yk*QmxP zlF4LoH&)JU2jm*Cn5)RRF`TOLmW_^o$_b2*KpOM{Bfgfw;5`jeGO>XW4nZS%1EVf^ zOBM*^bA;sk#Npm%LcX;^?v&E&kkkhwnFb!_bkPDS-FOA$_CkE@pj7-WIjvkpimQ$v z4LP+GaxQPvs;=fYXH~ZddEgaD*C@!260$aio~hmwd)52B0GUcPeO`^auYGNux?g`m zrytD+4kB`nu%2E+O5~bKpEm}N8v6Y5w>u+!Oi}NnHt_h6s!JS)u+qOJHH(I{2|C&KpA zdpYlpV+f?lryH63cBzLpABdWj6R67c3z-f#F9TuA0@SQqnXo#n>~)P8*gFW>+6`%h z;RsN5K8l>>3QB1UJIPBB#?TH#^zFWS7_Mu)Q$1YrxDUA~R$SAg=$6~iJrd_mk6j3H zIM4OM-Lg?V-1m}CJrerVr5@>--=`i8tb*YPhMCjPf=|tCz`X2lqZ=_`$ksNVQ|_Z( zQdRR2$!H;%jJ8Yw$xn4Hn?0a9A4ggH_|#*Oy+i7;{wauLok&~WqHgtg^p!64_$$-F zm$u!h)*Jg0k+sF4*1yKjeN_H!AQ->6Sv?V1>r_v4oe@$`7H1=(E^ARwZdsa!c>{ZC zXCpV});jf6pzGUFzvATpFt(7j>y;>FK@x||Z&1%h`O*Ee?RUdX>p(c`^g22;Bb`u}cc|xL zw|A@O-q@>2ZIFLoQ6ts`)Q0ZY>(ujxg$%i(eX7-)=z-Bj{xah@8uzpvQ+Ich`)}>4 zwf|PTYAZjzQMGM3*~PO}6a=z)wd;Q#vAlra(GLd+bt>kEmSe^8Il*Mf$FH_KC^_Ch zEmI!#bH65Qx!+M*`=sg4(4 zXK9%*!LPakao`TU11q-Q8Oru+ovLf&hz8YdPN-Min_7BQPvlsPX6#Rf(idVXZvpu1 zX;{v3!N|UZ>}EbvM|RJ|l>K&x>Urtj9@QIsJEVHIPWOT8>?YM$Fs4!UZJG#h`}tkC z{I(u)hM=#aWR>f-{AS`E`4ZKfeTX;jQ@`36kO$fHy}H%L?q~7q_(uS_0ByF}_*KV( zYHk*bmZv41)odeH{9_}KN(N@=h~=y$Us5fUPv#2WMx*eTO_1MVqZR>6+KOs3SjhHS1)0H?P%Q7Gii=YQ=Bd1}VPQ1>p&o4_zBUzXPtZ1EZds{A>iW{}#HvY%G%-sOn?h z&@Og^$7qZEf=J|h)J%qt@;*<3 zTyk;)2)@R7Y>SAzhlcw>xJHO56Tf1r$PPaDu?Tx_O=k?rc49t`T4)Z{a4x#hBVCxf z1V?_^<3nHcG7d7)C}{Fw>iaMn>rsnb4+-aE-p{|DD)V8~Qe|*`|AbN80V6raPj+Rc z37>#pb3+%xS74=(d9Dh6?*^5;?sCh&i-j$M;CQOxhs4LxO^A$y=#KZ?if-gPA!CCi zVj(Vr`52A8=qIc_sHQwr{lCJET`5*r|5R{zH^CeTqr8)n9s;Gmw!oN8wV;W3Jr5vU z_&RO=*NHb#3nRv2sfzDr^$ES$NUpx3ExrWju~ffA0Xm; z2zIwe@JEqzkwB6;wie#v?7%Sdb z1Hj)RB}osZ$la(JZt@a0=#Rv?-2`(MjOY|Ekq_@d_#;2MpNU)XU*ah86l$?Wb=Z>?v;1aE8M1Cf zr$b8Dpb?a#S~L@4HV{$KWOSo%QZ-gkG6E!9AMTPpm%GyKf?(*X3 zbDn41Dcba44L!>N(%6A;XiXDomoLeyR$#PPR$I>oG(z(b~DzRSZ&=Q z2aKmtGxx_)d^b8k2VFno_a{ zd&ar=Ejmgf!+g{M+2{k(x;b|j_n*$kx6T`(f+RzmKTM7)oIiUS()u@y$e zWaPZEBUaxL+%^9EYw#A5za&mx2zvKD$ocnY(pBu;#4>X3CM@4CmKzWW^*HH;7h#01 z#;ETCG$NB?ke^XAzdJjH&qe8?K61L!OR1%c-oLsYIekKToNDNibmH(K z7xrN}u?%R*dkE0|C@vFnMHsjr;D-EW-tPic=?SnMq6;Q;1EU*P{P&FopI3WcUXkVXehS zG>&kh1!&Ru3P=g&OQ+LjoYjHwexOP|gb!ac8s_;J4c`x-k^>;-R{=1`7#4azFE8+d z`S?ySe-1Rgs>HT~)O&#FJ_m`&YF2R|e)aJq8DX7tKR`9mA4ctzJ`&rBfyn;248Pk; z+e?_&Cv*33Cor5K2;GDEVz~?{?*VCQCUVhm190Uqrq}k$S$=?@3!p%052ltmATkGo zhhZSL1p`I%)3AIWy87RKOY}i=1b*G?5s8n3Q8I~=k^sC{#3+Qe*;h?j+vyiMHGZg4jZYi%)YA(W1?8cZwAcgUfdja{j_JtXRER znN3w6Sju1SLF9IpD)*scU@;=T76;t@dlBx%TTm`4JbR;2EyJSpaQY)@QVPpsQ0497 zqrM5uzxs%1Bf9Ygl+0-il|UYGAQ!8}ig5thh)ae_PHG|T z<#65d9ej8gA;dnwO3{HqIb@*`>H|<&KBROqM!gS$#W>3cLz11$Y5=4YA5jxVX;%-> z4nf#B00TuA_A;m)0la<7lW=?!_S|=)Tety+u>oPDfE%0m;yRqmAPhM%>Z`zBq!$r= z#Bq<3(wi_U`iLziyJ_1@`(KGMwU)@2gJodb`7MfNIlKoCqs`b4wZyqWI*L1^fekmF zi@kycIP$LO##>by2HF5rl9f)|=CFhcDt)3&4in(7Ix%$_Bs#GH-F+b?*%&%z+Zs?}-R`gPO zpZQj0Qk|PaM+HB?=)mKMtV5CUXc%YPAT>m3lp&{cRdG!Qj^v~j>c>DdeG{lo#IGUW z`@{~gQ`#Swp@vRMQz6NDQYyt|>@Y~xbA(?Y2hGAa0i+)W;;sTf_T#sp1mT!mOed@0 zMjpm*WFq*go&{gQACRi&X!Cx9H}5x?GA2_kz8aCr1(fvq7ke5x_c%lfd(yzNF+hN` z>KHyV@LMV;(8T=>xZDE`&xersQz2Z+;cnoWvy*E4IX~v*5vAf6C;40IW1OB&sXC4+ zy^Wds05?3Iwpbx;#_qLL55h`f_a@XP^kC%xpoNd38hM@<0OdEgAa@Ro$Zp+~WaPw` z0+=o^AaHI~cm5c4@gh%Lr)j9V~{Zw`$5y`*G+;eOGG%Ti{1M1DpV znUA;7I{X%1=@)MRH(Pk5iAFSvmB^1_ za@vI>J-qcZi7^!f?)%fx^`(LIqyVWq4z)I;R7S-l-% z&a1<+k5X7xbmLe`CF>z`cW*#Mo|B^y5a^xG9MwnGhT}JIBIW}plI(~aY>H*CO^!#S zR4xGrmapR-aDO5PzCSYP^9NYNf)Dv|z6uiG;Us9eiIE#zPB#!w2@HLZE0RN7lutoa zWDW@Qwpe~T!2O#Wh<801_k4(OB%j#yZH3zpS6^9#uR%8%0blZML;?pjqt>5=503yp z7)I$gPEcJ88JMf`1mxcY7JW!4ewvamiLQAUDJhu(alhsTxI7S*JV8XI0l1}d=JLpK zHF+=2gSR6W{sczsY%IIwWkkVHxKWw9#iz7@^vfV)NcK>D*$_w&TZ}gnD)}I4#RoRP z-Iq46oF{{8R=|~qM}46du5;{KR6PN(6l8VcqYy{Yk*wYcW=$dupx%376nqSc7 znGmA$cE_IHLv`or<5tBagyT!n7`u}DxC2vuxeE%*&K031*`a8jeUzUZr?1q>}^78ezoIFb>8*G!tlrz2v@ncK}642h{UjV<=hj(9QcC zdn4lL4pnI6^`blUND$;j05trce(vutgrPqaHsm)Ku{+Qx$m#+{e-mUCSc<+yPOq4u zuTU$pFlbUaQF`!492F$c2(L{eLpDmWLtP+Ez$kS3$e#?P5;=^S=V3Ikg0{d4>;=YP zUat&g<1y;n-ART{YrxU_eUNK05RxaQR4&{?c@l}rm&;YtXseV9m&p1cmR(>8f6zcC zuOv_fG2tgjh-FL$% zKDiewU*R%x1UV@91ebAnDPYPG!hKp3)hU2X)Wh{Pp;1)t2Sy17N}~uTucTAtt%nNT z1o2#shVD?$2c1N8a~;sq(T%*@4WNSpr0#w@rmmuDRAV44-58alvwsS5#dBR4sKHd! zjU(Sx4lKWpQGHL7nnzpF*R(}$LBpsZ?Gc%8(LIKMbe}YYzX_7gxe*vgiAWPjy(_>H z-IH-;PQiR=8wT`-8$YoNwUcR!RDv||2Yv&?F%X&2jiU>&9QTXbewd2f?ZnDj*} z1kg*QKGJy}=|iNf8B^b5G;&S=k%*7HQyBO=>-T$TcF8r9MeKC0wE zuRAI=11piM;p&ULz!*E#j~Gq*i6HW^pLpBQ@cr6=#uqRmN8+Q>gMsn|WJ5m-`BQKc zzXx$N3U1^CLP%C(v~U8tr6cGCk4;2mX$if6TO=!icU4VDwm)Irqa@)+@C~Zoosl z#-%ul)HWd}J5#6*m!WPfhhK3(-hdnaC)LO(L?U|%_bxxBWpqWPYa3SNHNwCi^_Gu_ z-V4GgC5ie;GbGiD&7vm9nb0yA;WFTst57()dI;a8t1 z+)p5)?}Fk#!A;6Tbu2B744sH`{}c6Cc@F0VS(HL{Ou5&0VNWj8t7^JIFsTQSvMNWT zPUK`b>b`WJN-2DAFMWAGfZ}pWtEdHr`OhHL4IAmG$LKsyfrGXU=tky4+>09s^#d

kM-i?FIu zi!BC$zP>B?7~t{G!56u;iD2sRQ6`tI;X-=Q{VH;W)36uLM$P=P4`CT_{+kgo3(znQ zsv}JoBck7kxnJqR)GytT6)R}lmr~Uff(g#XK=|4q&Se}#_VDk@y}1OrkOeDoxy}zB zj!2{hRLOG?so0Om6StzP?=4F2#Z-9|uJ-^mQa=2KmUyx3r!BM;K%o^_34MWD!AwZ$ zbQcEfV2-S4A^WBbe96%b1*1%KH(C zJ&u)v>VERqjTOJbo_;XVGbOMRzxqfQ{FmrH0G9CQ6Bzq2o8s#FPzJ*IM++F`7F@%HEuZKpmZE^g$@!Hz60}-r&mI@a4{WC=VF!^Aq5^STTZPxxE<#&r4V`_+#?MI6eoYrR(7OFT|8y zseKnw3dk0nYT@y#;2cEsnKd+}n_+Q0R*D<}jON*}x6}z1xz8@$w~4T3z=+DNLC{UL zY%GAhFQ67HP6z#5KpI~FEjiLfY;s!Had;m?&O87{?0gKA7Gtz9(FF`SuayQi0fQI% z1MjHf{m2=O7}bve1NYvt?Syke7&yf%1k2tq-U7!XS1wl6Z;&JgH8BQg<@xqE=Ba1)mGMM^=i3!_1zEc+6<QN<{SVi5w4CA5yFKcha^75wo7KQkMwQ%j<-IFHr%b_?dKE{(?)rDi`fWPwVTz z@Z$eh+<8aWRUM7~oZHo9$-NM;1DN(`rnhtZy^^l3bk&;!#>Di11EGZ`S=kmFmZls(*z!j2&!{uHnJb5Z|p3>4|Hn-4?s$eS2V$vZ14_l^OS?S#|#=a4Vm zn{vzhV54#g^wh$lV=#fzN1)fx8KI1R9|>pi6c-=X^cp~$P*B=*-G_rDs3*LJ<`RDv zKb|b^Ldp%~NXjpTet z)9DypDl3ukeelsRT>TN0()ZD+?374P63OPTbv+mMl2^7i>5&fI-l}K1bPJDeB-PMd z=`CMjbda&ZjCd04Ls~eKB^?D+ zb^>N@0{mCsB!?XFf1y-n{4?sUI6#r6Oc4?(YR}TNpgj)$*p$oKj2GLUi;#oU7l!Xh z*tMBVpLnp5)!=rEkdbPf1dj}1*%2iXio3vhH#$Lb2ba5ZK!jBE8&??9ayWdy$@ zRnL;8Ll_oAKNDQlWo`XE{`CxTf;z2zAof*I?OEu|)Uv z!Cjl>rikf4Q$LCF}jpTLdF{_PZox9f`H@zQ0u>1#D)yb7qg%22UZfPDblKGLhY%0Sk*5E+|kZ4}!}**2ukNOh$;@NGwHnJrXW zqn#sN6{2VJ?AGBbOWVtj+l=QEvK0)Wh~0VLwRze7EnVm7eZIR}o^3DcbCe;ysNQzc zw)0hFyV&sZ7M@J)Di_$|l=HLSKEbZ@o(6ezcq24tKLw?uI&R^q=H7TZo$H_2N*(Hqv2AZa_+nRWefc;d!2fsK)x!KzheA2~ZgMzh zMbW8jZE_ytB~_v^8FJ&m*h^4^`JImn7#RbM79gWE?=#?KzuRVfCdWlTPrNGRUy_CIz+;Sea6?eD|LSgDP#8`Z_7Yo8vj5& zraD2}$2xvImP+@M|HA33-%Uq!*zHf%^V@FN?sd~=>#wBVlwQ%6AEYe4ryYfftM|;J z?dQ!KKoPWn;g}{wui0rWq!CA!4Kc7@PL-7}1|>YE)7aT_MX&K~OBV^L8%jX=jH=KR zwEi=(;+Cb9(Rd{}QlIe}lu7qczK4H0*_IOebqJ?_0134oz6GObGWIPXc(#}SvhN4! zA^Ol--GLB@k0+rj&<~K=#@Lro4qnYud0w1vmy=rZH1zzFIsw%*9RDv!Xn409hGYN~ zIWGqHhvF21_hTw8=kTp!EL0j#YzIYVfD+3mAvJnPcL@P`P z_&0k1N01QPNw!Flm(*u)s^P2Ps^P?bBr^Hda2>2Q+ztr!LA3f;9c24x5WFvtqvj(x zsQHK@>nliYTHlSH%PA;$eLFe!4MAQ^Ad1&`m-vF$^2IU0{}yl;0+uf!Z~4+V;66~wAC%We zAR6$Y&e+coCVDqSW8d2l>for>9?PQk#=tc7(5d8_+qT)pZzk*7llBeiF;OQaiRGe%rUkQrrRZE1R z9J-IZfv3VyIcor2jf|48lBZDBJUG3d*Vr;pN@OB0cs2>8ecd3JqrXJF9m;w5gR+?& z)YjpGH(O9%hF--1{?5FLfvHDb6d@|n) zC_qig$;g)c>40eli*Q=WCdlhwhn}uZJPXm(Phq3>)CfeQXuRabRj~FRg$JLB!QWyM zs>DPin|z@B77c~RQd;mDQY{JY95Ub_yc3>7`wa1PQI~1<%6$p`eL!wJISVQ>>0Uk) zb_2aJ(-Dn9uX+IGlpKK^f$M0~wfpj|((N-HJudRb&Y5F6^@@wytr64N6Kq8r*RF=1 zEXV|IrJyR=_7TeSz}ox{Qa|IV`6*Zn&75Pp0&A(6z-0rd?#xcp_4*+&StqyBHAv+J zk3{Hr0+i^zF?4Vw6;N^OD!?5+z@2cVf7^lM(O>*4w2=xROzVEi0~o>ReA-Kz*^&6K-hh1DpVrY72!`%T*ZFRm?b4&ya+( z3gO@gUTaRFjG8dyYvS^BIY<0!=0U1v0HQT(;jd;rRB9yjea**ZfX@K6C%*s)vm>B3 zE}()OwY9l`j&8tx^7YkQ_K|x+Euj>WnWkJ~l8+Bh5qGAVS z)Mpg{a#3CVKI;K;r$zmt0*=fBoCJY-OI|~gBZk82%Mqr&3U=!o1T>SaVFHvJjzaAX z$ISzroDc9*WJ6q1E7zFW4YdRXXgDwLgNl>kv}<2C^@|I+vs5_LAl$*|mwWbTO*rsb<0L+tYHwgHxQ2CvtKDYt!XCd&I zsp&<;Z~8S1eDRdH5c$j9^jt|T1s{~U^I9w$5K$s3Fy8>E_G;`j_zB|mxK!iP+iM=6yCCRBuy>cI}9vSTAsY_@pK+dGQ5z6*zz0b?SU)_`kFYC)ac%T zJyP$6iY8ROfnsa_mb+)Fji;3DSNlmLAZcFvn?BhE>621c*6i6@dzRDw{nuIDQHv2e z`&l0|6Mx3JB|JjwOzYSNa z)1M7tq)g8N2t9jFK_`uMI?%1aj0@-*+ouu^2#HF`;F3|^W z3o$40LKc6On_KzmUyyAUHh$bcjHgqWljh@D&1csmU=#LN{CiBm`C^AzxWNdUBT^s1 z+N7f(uVL@T-JQ+6%IuBgIFNeNB@KH*Qg4Z|FNo0z$x(;EmAMgT&eWt?q~XcDlcA?m zj*jy-nnlJ1KX3ybB$?4I8=dtOa}kO0G21+|TDTuW}f8dIg6@($<+u4U{*2ODUXQ z6G3&SkauMH8lGOp>FeE@>1oIogfZrIz|a$@Q>P)lMbJx6InJQ)q9>3oKM9r5pHZP^ z5TKKazyf$-)V*LsTQc z>7q~Z*Ey87REi9HNnoz2F(#K91PESI>X0YyAVu@B}$QprbK%OMZmtJJ#9MQ_tn*d!%bM!e7Mi#@2N93Cc(1^H(1lrPD zXB>HaOHUt19ei6Vz{NMmFC?49zcRdl^qQOm!7?ODl_|z${uSVoz*B|tjkUPZI6O7%M=+4A~3y zM3ym&d(Ly1Bg-XPqcOS79LcLb0q$v~qBfxHz&KLV3rjya ztWy?p$3I8~c;`@;`H%86=^=P7{Gc7q9(SXS00yUVi8EH>bW+jwNH%%f`>2<)U)32| zGOcN6v)6=mebAim!cLFr2SoxC*?GflaGEaiaS{TLwE^bx&3}%(%G6|r%hkzDy{9!& zK&mm?3EqiqP`-ioVYMQv_1S8=a+#4I zVXb*H2`wq|T33-?a&e zG=6N2{EP;&0f`j@;LQm#E|Do#oeij&2?x3wX}ySVQj@a2flBZlD5tKXjLNsjTUFwr zpcBa!nCC?=^23(4wZ9C)>GLQowKq}*x5I|URSUJwfrBilRK5aAYT*#S4`+#Dp9Qbt zf==4?*es|#5Bb8oB7D2g1p)tRp58*#icbV@I|+%4DJ_`fTglU;YUtgn+hM5ULZ}2j zMg&KQ)Y_NBh(Ep!4sL+d#K{oQz{_Pfkx+5~$nkOg(3?r#{EBvRoJMumiuu$OQorRV z0+O$FQN|JQT=N)2NA-}c=GD32-5}=pDBr3|eK3Aa2lep*^nwdYFzOd6LpL0x?>qhw z#_P7Ysmr|}UwR`?M#~WY8uH87ytW&n`2!3(Zh}!{w@4?Q2q8s4&TFT}Sd;<+@&U}6i%sTV0c zvDA%}%P6gU8QGF8i0>Z)FV@ipa*}L;Eq&m_Hab5@f`rj4Squk_N9${VZ`G|r zuZ^dNSHi}&K3Ee|)_8M)UA$T`Nj1r9Y0S9qVBEieY~fvyvg#ekmLNy?d0yj>y7;E+ z$y-P$dCN({fdX!vgNC}mOWZEtU0y5RAt53`Ga@^KARin;!23u@t#JW1pvk~4s04O- zhFylYlvKBmY`W;JjzWEyy%ayO1Lde&&+vrS63Vb0p5K6!y)LB9KnHgfPf&PO3gohm zK3eU;;Ay~iOTOtaPe)|Y*?s0u%D$mK^seN{N2L12@o3j!9fjIQPUF?~#TlIXXOJ4a zst?&7&&4x@v7t<4>0wni`@W9Q9V>^~D9>#l9v!MSG=7+F<+8V9d7ecWC7Q8|!R|>^ z@_$vhqGW0)Pb8A@hMhy*jrMe-4umyb&r>xiitXy{<-N3nlF7of_c=;*AdNkJf4t1A z_)l%4jH9u&f(NL5hePx%>vRw`_g&XVbIvdU%SE=dFn{Q!C-LyAM2(MWZ-(TZDC^T% zGa$;qVpxRYM4!3eOXXfZ$uPioF16s54#=^YZw~2GoNv>db+D7$2pjobpyVA(g0W-3 z++VlzN7T4Ngl$m?c&<>=mq(C(#tOYcI`Yd!Zz!1Mq}f2NM0G&Ru-+sLpHS=yzo@7#!_YGb*lksf24$NEtDf~|Dg#;j?u;UJ(~9R2oVg`S-DweImAg92 zFP37#zto}448GSYItI>39;%rkBZF(P+ zGw$Qg;nXa)yE|@s9A0F;fy=OUhp??ZnY@85g@@l}L^H?g`L>3_{9J21x!i2i-O zjITyCu=QZkdCuW7GVH)M8ohueLlL@lP0cYK;QzLj!{XcKXvzPFO~rvDbKJNJE74mB z3NNOPir$8yj9x?amytJZ?Yk!og6n8oT2S)1EU5TVldbHDhd^0bt5}ML!iQo=>gL?b z!JF{QF5JzrWE&@oKI)*19tI*s2bW@h=b~bdc%(_Qu=XM+PwKv$vbyBGp44I&1k`D{ z4f(S!L(k)$Awiv%@4G>=<$D-GiRa3_8v%_A_~!ZpMNarIil;dv4v?9}{010O=QdW> zaGmeN#t)7RHJwG_>b@qu)y~sgrXtjbnVpn2{%yV$odV_w-fN z??f+!uf`*dJp|hr@k;@_om){hRMgh&ickNxh>Hx#I3isYKXx*j#;zs#ZH#no%) zpx^Y@SN}c@4+&~HdWVN@Zqe>ZjA4&#=|0re$~XD@RHJc-9J+%}`xGrBZ7RF%KutjN zDyQ~t5xWnyrc;n|6%*gCzglUreoRHb*|KH)BU+0^8{NSlRMNz&`pMw_5KaD?YP2pA z6#2dO-HuAMNNpNAtYz07DF05AzNIUNa~R!D^Ax?_x=_ya`}U>=<&Mbo{nO(X&{65k ziJ4FBr=zU@w2i)d6v!yY^lKS=+{v(ela8{h7fd7`Acu;InoR$SfMb|(4vt{yE?2Z3Q3_`427bvke6 z+kXxwFRQ+KytwFenuc=V-{~?#DrNO&pr{E%WpPqji~b3o?P^r=#@9|dDw)&Li&dtN z#6=e9C~~*istwtfim3NdD*x(WgQ~o{J{k;JLZ!2Xvr(nMTu_`Fz!KX);$6K zV!rGJ`PG#??GzL-INi@x_Iv0BCxsC~rj=sfh2U9cl#)BTpeNRDy^hbZ9Aryg3SMKF zYE-rRXYj4!oK8qdC*08APmaq4$jq2Eo^PeMqoLRobQLi9mJ|n-4u5?uMEzgzt>PgH zG79LJE0@7brToMNt{@>*45{Fq6xp;zs`feL4Ibx%zb66u7ZZ}B0qQr>o!4$VO}iT@ zQvHjvtA{5P8J}oIdIWhy9EM!Mb~Dy1qte{vypDS9X8i8U#k9+PSP7Z%ChGNe#JumIetn{*tV=2kN3S?RV=Z#18`ewJ;na+V76I1nT0K0@ z@peY(l%5EivCwZ3UoYlzz6tk51|Ypp@G2J;aaa&umZQ4dYf-EY?uu`rk8-f@0MI62 z4mEpxDR~FiU!h9cC%_IhdtAQvzn0b)ds6>ydd(e!9#AZrx1uJQXm@{cb={ zsY6T@Qrt>nCM_8cg;RZIvS%JSn8h|LtTsD0sn)QUuH|u+14zqzs_+|s%%xtup0%l{688DlyNv&qnilYK%bGS>m+Jqv) z`v%SC-kVy@mSDWsY*|s+YfiTg0`!dOHGgKwKl_ONP-4zSo#xMas$0!di5qX8+S3*= zPxEgel?i9_v=#3=%o<_L?(#j_);3Cla}N`idi4&6x!GHfQ!6&}*I%c&ODpW7W?C z=Fj8jv>I5VfPPe`_r(4Ge|6sH#IDBo{okWu*W84k0--tlFi;;tp=CgV=c=`HhEQv&2D;#E!TF1kE2Ts zAB%&k$)yF974n)e=^?@|)2TPL)6XQFc;%W)u>0s$m~v%noKzXGq_y#+n}TFtU%70G zQytVE>|rqR!XR^k%XzAmAE)UKUQ-uRPb z*~1d3Cx@;H*T|-yvUQy#OhYzXFM9b`&Ba3$xdPUzQh|wyq@tm6-#O zpTReK#At2jTf#xPl_!f>OJFGU4+w;_06LVcyfzE*?-3LUc2ao~ua#C8Ovd?^kS%G} z8J!Tll5g>qbd@35iItl99A2d_7b;EN6n;A8R$fgf966oT%HPKrmYq%EdK00p?5XPX z1fy!m`0)>~E)(fEQU7-2)iL*+LHgCNp{#jzsGHWVD7J7!2ZhV1O2uj%MTMqht+J{O zCTpLQr%U0bYB^$6Uj?X{(#4qTOa<_ql3|`+Gicu{n7&Q$DZ2c?F?oR53n6eA+aPNH z2>JTjwGg;;4TX<{tKteqD8)A+bg}FoA1P&w?iMf)%46gl^Oz|D{ulIgIo&P$7|l;t zF&@1M*>oB8N6iXhe%vJ8Qe|q-(TA}dtJrDIR=c+q5u6jn7n`Dp-FbEWO@HUC>>#O+ zw+*W)eXL5RFw>_3bXHe}_GGniRXJ*q+b+?A`CTPk2UM?|qB4_Bv&k|K4Yxa?iaJlqxv8RVnAhWTmPq>`ImF zN)ke;Jk+67WgWf#E9&VT(xg;@k)o1}309S~=N5-jWrG7s#s9Tf$P%SoyXurOzi3d& z&dgd$%Mr_KpsoB-+7jo`R#MffZ0^%UpSwZXT8e4&-eoFP(_>e5BdtZL;zFNNZg+>W zC+5183OeXD9`FJ(+N@M~JE+WfvT_(F&|9z}@18xvrhIXFN((+_ea?XCXxs@vUeMvff|nREqPdT1qMS z;lD4aQWBrFf_@KT@vrEpXirtCiE&_Y9)w#3Uc`ch4Pd#wUZriGhz(;LF1T)Yt8^n5 z7{%vM+IvZxQogf$REA--!jgj1CHHly{>Euk12<4D8il^1QBB0}Hygy+Np(U8FfK)D zX&IQkA)m@NhNIMc*;Lt^Z*wVCz06btjp?=U9ow!3Cd$y~DM3|W8<<@^=yOl%MwSWOzy zs5uBgmG9KiTVyKho`_ZD7^i|D_#S~UP9-FjO<1eC9tHkYR70cGQ@ZG>dF@I6n- zcm=!0D{kU)l!MZb@Hvwh0OvR_&z52%2?QHz64m3k%a&$dNJtuS#Y*&*IW~g}?Hfpwj zCH9m}s>pixg3r4bmhk3eHFj6Ho`AXOaHqRrE)jjBlE8Njs@y?H{ALWfZZH{??_eo@ zqE#K3nBYtNdSSb6tW&`AhIs79|UzS~V9y zRdfC75F_AHha?U`!e5D@!tDSr{?J3#UE0W^HJ@tv%oKH~aY(zG;iAND=Gy2boDTul^^#8=zxgJdDSVr%?~`VAy6cy2oH>cA=>$+w z0+8lefHW_{xOvT1$UQOu>xJcWa>H$<3G|v z&h+%JRcE%uq~nqX$enHKY}eCNi_dANn$b!~uB2p~0U-Yz07YB9jODWs;}ZO>sc_@l zg>~v2!vIV8&3eNAJ-y*K(H9=jNMhcbg2ZC@%AO(yl|43A$D=4kciYLFH}TCs!L82k zn&4FzxK^6naywF5;zTY8)m{xamVLYE+qpgkDdti3eyVN}6Gf3jG zT9Vyycxh;umR|={|c^IwLrYwS#iX+1JoEIVzMeC3? z4mZ_BovUiq#pbu|bc9;fd?Tj?%{L)de6mf=-;A|OTDLAzmpXH;>N4XPdV^QtrLzt# z!HMWAm0T;`&hQj367mmvW9i+>nm7>V_>yOrToQ)vB)Wmv*Qt5~(ga&h1kF zFzj^r9_>+A#>!jORb{!2czLFu%(kJvygy@JFdpBWx#U4!7Qrr&kts_~Rad<^0pJlG zgZt`mbzrhu;9{he>&{M$cOx766lQm318q+d7te0mJg;=B1)GaNP%x_=eZ!^WYxulJ zBI})kQtvSiRU67=O1eK!rg~N?mALX|O!s1^L&MGVfwkZ%~qeaHE*TSTdR9Zvh^ zn5r%@A2H87VR@BQ@gJBZ1oi%Z0kZf8KOM5(2cE;4^KO&jIRf=X(*Yhl)sF1KI;>p? zNY793MP|U~`N@Xt*pYp*y9Q7)E(f!BT)nDy52#i3Z3F65gOP5f zBONWqKx~9h0fuu4HhfRFsD{K?dfij%Rb$V>TGeC>K|$H^053ZpYi2)eRBgAbro3uVe?951p^l!%fHT1dybcCk3k?`Sy_i_f$ zcQH094l~u##1vfclSs-~)uNV#c7Uq#8Q~j@AK~Hfx%V!jG?KW4CD>tqH>9!wa;}j2 zF14)nuw=E|dmf6H6fxNzo;dbPqCw5k<}<@i=`J@`uQ z^T7NJV#UKyP*n(U=ZT`~HB((}>k?M9c+h{DzRD&P1t2>N-lq%+c*?87eYQ|{4x+9U}A_eYA zChW(d%9GiFA5Tj4h9tGdC~m<)c@Bv9Y>>((a_wG$ab_!%{e1LA&aZ=9RHxP?R!}Wi zfoyRA1{PcY6<`!+RLx8(Gwb@ni73>_g)0`;1cQ>B%aQu)|`y(@}r^au8gz z0?b=m)#mMFyR|i1t6Gc$T#U^3C@or!4RfxEjWKqRx*)O39mIZKn`+tIU{$ToVK5hY zz#O}hj`BfX)!Na9jj|g+5P6|awHYO~s_oUc&=PsWP3~4NQti=>TGier0V`hEt~#PU zF4bvxQB^VpEoDPxl{%=MF!>k&qtmWBTb@CmnMH=>kH#Z!Im9?W2^X>&X&Z`|zd~$S zI#gHU?poDt?y!P6mo{%j1JEvQqH5xT=SQj)vJ-n6D8*TeyAyk0E?x;f@B5lWh)^Xd zP-tT!y01;f>2F~!lEt!cS3SLwlde;2swX@iiEzxLdRh;lBd^A*ws_>R!2LDGi|(kS zR0WH>$^$vYiwzH@{OjFnOY8Bi`0+~zs=jE)nS>3})5ZW~AQsA4s@9EqIqI1!}Cgt zdNFahRqZsc#E|=pI<@P?ATCtMEQ^1(2rUseyp%N-wjlq^(kt1_v|KRsTMW`}ZZmx5`wnIm1@<+74M{BCptpp@?6{bGXsvrT>Kx)@or z)Ks4)4xr?E9c%tCP4!tK$;wjj2__33N1|%Ahm^k)%+9Nbf%Enb^||qCt@=DM*o8^i zor9}a5|`!ln#Y>t+FD)mC~M0NE$VmUKJ-;S06ybBY?Q1>QonCL8(H(@B#ScMYN2BQ zy;XORNa1OX*!Y#$*YxWU!W1vxiyzLH>n#?evKB07wOcHSwHR`5MxSAAhrDW9tch2> zV3C^>W1X8U-fp)fRbAd_NlM6iUsZro{|7#ht^{-53t}i?T2fy;tr z){@b=fvW#5Jc`SfAC6)?e6x+wzd)08OJ?3IJJop*&%R``WQy04`Qq6~__C02|D=f^ zD0V}3qagpA21~!vsa8wBgx6~6pYV#RS@cF`gLI1r zh|9L^T*+n_pFv#WGg%qpXC(bL5(c5#_#kx$CyIU;v9om-h=cOK8lv;gXF#UEco)~_E-idT(lKl)Jd?1 z!xzd$B9t3o8Z7gmsss0ptR%}2=XywDY%K`J+bu(5-_jeEqlWixH0S^9whT?YPH*)Y z$(CW8FKMBa(*U45lP$Tf*{zn`#CnZmoj8@<%y!0I%hD;S}KQuD%H#;n&Hec3c8C`yMn`MkqWwne+9D;1PgWf8h zk5tr8$y@2itz(mk>8%|YnrO2e&=pKU%Vc2q=e0wwuVZ9bhaiag?yj?p^=xuk#wN0B zLC{WM{oN}*%gQ2#CtmbBU|2X4NFs{2PgFIns*1+2-W%BLxogN%MH0BUuk z<*RynZ4S#pi5z-e6;{hQV;^vheI`rz3G}+<2Cd}zcFVX{Ih^Dl2sHC$6a?l|ihkZ; zIoLImj=+n_mV*-+lxoJ|pmUlXYe`+O%m+cib*PHG=dp};zbjMYsWzNmipkKVR;;zu zS;i+?a3L@QrNJILoB=D=2AGyZjKi#!LlPD=mp=7a_5pFcn|z zr{nNcWcPMj4vlZIu_Ic+QZ~B-@&K9xRW{3^i3plYex{nY5`6LM4$B0;JlVv%>KX2H znjp*UmI<3*hRV;onEsNS@RK$n~!)cN489IS&lM|76xjz$8uE5beH95V?eEC zvVSh3SS|FvU`_^TJw7Wul;k@s%~({7TX`36l3(2C8P&S*rZ0 zwU(+xKNLh|t;=_|(>ugu`Yp3ll>=^IQ5~g1+VV@K?VMz+_4A_OQA+ujVJ&}Q&{7=< zqs4obhw3ehAvdEc{{iWJuo1EzX6ItNrMhLbmGM;_tQPGyMm3vo27Iu@-hn0djswlr z4MeTQ2j*L;nzwQTUL_lS)u?t$jj@o9s*jo=9q<`uo291Z&RUG;(B^%+-ZH}*bEEXF zEtvca^pVFpr39A9MoO+*TUiP|1lcME?E{MEsDR?&x^1ihFGo4jn0_qEo*$H`-R4IHNvBD3e z<8z`>_$$>SM=@kL){4rJEvlBbO|TSg>xO)f(qbfO96Ds7iimq=^@OW7bWmpRc=wghWsD>EQ# z5G#9x{j%(FOqM;H2T8;r9jGeXvJ|qNu$S%H1=&Ze%0B7``S%tf!t$*!|0r$0T+Ilo zYazBcBo*26jOCC47%CrH067x%`(~4nLb@In|gBUo6KCIXD?r zi`__k*lIZ~+EZ&eEwPri(1<3>>8?9nD3CMtaGp(-lgnUzm)84as2_t2Iqr{e5M5)D z37KG3Dy^Fc=@$xe@%DFS<44*G?-@*re#+2BrV|qPX&@~=6X5x)K~+-yBJ0(lfw=Gx z9f5~@0D1*4J+(dLWMwx~=(bccC!c`n_k#t$(BbkD67y_e#9ksU`V~&>7e=AR!yvnE z>m*F0GAY^W;ad@e+<{o{tBN70cRzw5Pd2^&30^>|HgOu?ToFL#zebzB?e>Hi54q#+ zy{Fc<(*>t3Cc4Wzuq%tX@dc{fho+&=AeHrfVsII7-B+SYUt$I1NL+O@EEOG;0vDjq zWhHc#Pi(<+d2laGrZ@gz0KU_E(6W>cL*A#9#jaybJdCAOKS;Ra@D-g5i|KEI<(WFL zbf7AhpiQ5Az4EB#m!qUU@CBMts*hmNT_7!dnb7GyIl2%9o)qF|j3MQFS9D2%KaIXT z|LTF)gRB#zHyLyaNJjLvxi(VO%%iPI`*TD0{z;$QGxTLsU;1c1*N+6pD(~(zi{)^M zuGp|lZJ<;@iW-kW@*W}e47t)XhEOs&CtB+tfJMC$R4V(|b|FL~2^}~iw^lkXRVTSQ$z10kOa7Sz^9KNTK7^{k#jt37{U!BsmZXF4)nrPO>nNSo zt}+rbIqlgQanZ;v3Lazt;jFBle z^nkjnlPC?6anqUe93bO*rIF`1*C><3bO_4`I>c^P@E+oe8wa|O zXsp{GKW(i~R;NH8vyx}sq=T`c-ujQVzWLTC))z?~KmB;)lEZOM1IhZwf}GR#&Pp{n zdV;?qGZwf zpS=Z}Fkba*GI72gyZXb9sJv&7Z9u&ibB|_rSL_V3RXzzm^HV=1d1%$Xd1k<@*B9qp zR5{OQiuWa^_XU`HmzyPwzFu?krghRLv8sLykK(r?rn9Ny+(s6Kw<5GTQXoYvmhqoq zDZ3vmMk|3ZatN6FFa|zQ(*tHnJww(%{rFJW1GE05D%h38F8&@-^RY6hs+kbO#wZkA zL`=gc0n+%amA$7QM1Mcz+$SZ;tN2DdsyG?GYIhO}TI=YLS86&q#lxu17=>G<$5Sdf zCzFHHD_x)pgFat2pbB{nShK!Wjo7SLr01n~G$D3BNry)knZhj~^%Uc!?+B`{G0XB~X!O7%`iMVpNn$hD?-3+VN$U+=GU z_}rlKonk^3K+0NatCYKI=MqTKiFJhOxF$7H&qniUdP^Rs%{UoiPLt|RNcBNPqs)^8 z=FbAySU_9;#5y&yU1H#zM29h$Hs@gC5AZO@`oEbQp5M z9(e`XXgBC}Mil3EDr-XT>`$8@7=nb;Y(-Yz2kV5|GfnkSZlc!I{wv`Orh#2jr>gCC$tm$v^WW zdP5JP%5^;?{xxEe#dW0eEF{Vvrpj#!TI4px$f3hu)=FExiOGLKf<09AmfD-FZFO|a zg5+PB1XFu$j!E~SBTsayXB72h*L7g(&n6Nr6jvS?`@-8 z8yYQA9RN|XsjCxcQ!<&9z)%8135*nh(YuWa3?(p91%?tBDf^^NU?d3)B`}o0Py!=a zU?_p11cpUm=uJr-CLIDp2@EAL{^x3@%$EPBH(4!0fh>o5Ym+tS0If;e8v#YC3UZoYkE^EJoZ1b+L}9AOkM`j=LnZvm+O}_ z^0uXSfj3%&Mb0Hod4XbBTObl4Pk*WIgM_}XRR>mi+??{Rr9i059xMrqP?_EjW^B#M($PERaG$S6xBPEj&5&Y9|O2v!z@JvR%d7LZnY+f z>fu(1Yy?UIAxFR7ji6vWb_+h`*?I3g#CrF58MnT3tPIqlZ!PmF>yppyIG6?*adNks z;45xY6ZU@QCmK8&9Tqt@NQiA+>hL=bXJPCoN0N;3eom1CL(017#>Hx4io8YcKfhZ| zyvEl>b|(4M5&lX@$JqGTc?h^4Q8blU_&in<+2SVYC}I| zWCkw>sXj?3AC;tz7NvjZ6PC$;>(F9qSqsbtAazpfgp-)|&Yr%B(>oVrWiY0=Abc-6 zb+c@I?x#?oN8c;Zy{_*ddcqpzv}qX5nY6iYwNjP6BK}0JI(C=ZN>!E--h@E1VbO03xJ;)s zf-1MVAl*n+-{I-2tBT`^o+kt9n7u`dm38@34t1S!wqVhjz;q90Fr1Nmvu0AP~HoZ(JWK4@3b`XC9)|;PI%g_ ztg~SmnT=R)i1oIJTh<17G?HEIxT_C^CG$p-L7#&3Oi}X4BgA^k4xo*^kXM(tzS2f% z56Lj3qlcV)z^YCdIh1u{G~b1*)KvgaT_wv1pEasM9Rt(}?YOG?JxMk913!Mq1OA{~ zR)O5awW?&vmrMvX4ns+4T%QaYF0=6SQp+-WE%$pNZ#O|cCD_SV0VAiIxn;?7kmHviJ6J}4@JIbo|HcYr2g4^TYw6Y* zxIgsr6jhe}cm@l=-D#@q^;41P|6>mxWl~DLy%~H#rley{7Y-`?$T^-;(il2)dMCZs zUs+dv5@Atzf%o0RUP}k_SY#~^QnK9R2bBZ2a@Ge?HJvuw*UZMW6KEUS%xDb?plZ~q zOJP}#AN}PHASr;2n@bGz(emfLtwYE1Mv9NcABpJtpKSAEZ-CJyP20fb0R>6!%cj&(osh9oJyNPun;`OBk$Sw8B0fl=kZK__h0Jz9 z&TfLp7fe}}G=}z2oX%atxFtVGTkdwUZs@oHbc|?Z**uR?$f#aSaAY6sKbokiv6Awa z!ZJucMe4T%l6DITtYp1%$PI4B#6^(f8J-M-fzK#PQ5Abe@t&#w(Os(2x1p1^rIN+V z$>Q9-sj6~wvZ!)v!l0z(Z>M)p7?9VaW#mJWK6~g$k(>O~19w6`Xa?W&C>?PmNt4z< z9;A0M$m#FoKz@ZUBfST*SW4^2oQ#d2?>}z`38LW39FjqT45TA-Jq~8Rv<(DlQOH2V z`i)!)xv~(_%-wLmmB{vcAclf(Ff{Z!@b%k?f_}e%s{i@)X0-!yz(-`*pyeUR@5`9` z(~uZ?1geH^L-VlDu3>e&7KsrT5Q7nqqblPCv}Ame2l+P=8Q;;7nM5@+)ep%)bLIpv z=dMkGJPk`$8kmRvt6No7edbqH+b$#anTuHFl4}=X@*mw)A7vO7ADyX`FBxCEL0j^* zX{tKjYG=7_&E&)HcjKxi?^70!?5|N^9g@bFf4_|j@LSXI=%0w$7A;XThDA_nU5)yo zD`DyXF0t>Aan=_fSOjENza|F$5hRjcM>gwH&}UtVTUiU?lLOz37Z>Ad*6k^fwM@T} zlbcy6t|IJ76VSo|P@NK)zeJs~dqoj0d`U-6S1QAE9O?(2f)>licrPIb*q` z1sBdE*tTn4#Kv0M?3E0>zO6{Vf`QLC4;YRkftK622+ftGZT1{OGGcfa3)wRPB;KX! z7=x>h>LrkQFgxzWuHz|K9D87KybSu3abd)MB0?FPJ0V}BF)}_)_0Q^YEG5Kz}0W2lk+T*xF?>S}WA- zx4N3~P0nj+{b13Tah6YTHM@}DTz^bhomzEd4=aB(s7~!Z)sNHX3W66Uew%#Q-GRRJ zWHAo<)3RSBm9yWJs%&@JUoKat-ElNSoAZbmn%<(UwJS|^Vo`ICIw{b#NKG$$E2>W3 zd+v7S3_jLRxr*-UQRYjv%ay0-$f)wZG;eF~L4?ytYVSeBU$t|o3M9VkQNhiYpbEWp zctAy}uSr$WSK7K&-i#x`r_YHaQ<;q;&mDmAyNFfJLNYsN5^Xv2NUo7Df^<{~NJm|b z3q#s6h=T!N<`!NB^xs5vU>zNUJ|=28JzI&|3A7D+<0hOA4kN@#uOWNrD#&rXdm1H2 zgHd}}P}G7|DjwU^t@3YsW|1l^s83ZzFKt<*iW7AKb>g0GMr)W{xTK!8T%GjS>U61| z92iEH{K2A~F$(E0x}~G<4E$FX=U-L1q-tta|4YIxO5IxNUsZWT*85*z_(a0@|HAN# zY~cU38x$>l4u<|(4NEorUrbqm4T;4Zuz z+yxc^X}x^`n~3FoZnqY_!c0*kWZC?Tbytfij&I5)YiIW3DXdMOLy~W)hh=CZ_~d-m zU)~gEtwE`!nI%ZcO|<1r?CKl!%rD>@9V8@LmDZf%LcNktsx0yrz*-MccO?;s-=x~e z9*`mrQ#muNOxT}Ug0d#EXsHWfk%wTV56ALQz5n`2p1}3nFK3aIjzz5ZnO&BD{XD22 z_Vz#2w9+fj_WD6gAN>1R^h*n0j(dM^Uq_#WN-^n`=cA-)c&Tp}inz{Perv1IW zpF_*FO`oiOP1$=VXta_W?$pdab`~Q|GiTW5{wIYE zznK~!@7@P&@RNUOoh)-tr!gM#y2~mDX6wEcn7rA?$P_Z5a%8tIYNC{}nvRs+3`O?W zZR*_m<=fSHW^S0aYiS#tLRvTu3@{C(G<=3gTuKt!zGv$lWMISgElP*~TdYlmFY9G` zt&_lFlRZ@5@#v$F?K`T22hou>wg;BU!aSJV=>N8t(#a7BXBsukw;jIU*y;Lpder%O zx6&q8LuxR;*H!%$&Sd1V@{drkSk|D=aSkMdjnukw7cAN3NMxT*?+`a%EDVWqaJ4=p z@e`uoj!5aeL=FR`lT}ZR!5fpnTt-G%jAS8bk(aE>{v{8_ix0%^pm}Umw(l9sl`~l= zZp&*{7wq^MllmpAzUI^V^zo?QbxT-UxEp~m3i3kLdfnPils?2g*TNs%>cS;oEMf$I z>0pw4!}1{SH2;)Cc6pB1AxV-O>jCn?-=FeX61Xohd*3ujmgt%C*Xyi5@kN)xqMzus zguYe#-%59jzG+F^B#`YFFZr^874tz8%yS4+pO{mw;QCDJYlmX_(zEYgUL)=Oz(TH1 zWC~qkd`rpNATf|R(Rfd*Ebz~*I60)A@z^e>zOY7 z4@Dvib=y0aEU|Cr8k4&|2 z@5yOMd}f7D1|`V>p9VhU{TLmxcIthrcjecMs$8D+z2WV<75c9clEu7lRQr-kK2gva zt8(rELBVDg8J#&*Elk~N`65{d*0~e(;a37&kG;mE8}Qob{rXbQCGT^thg8`(qYqov z!IB~F_gxcICG{s(`y9OAl9!Gyd1B5#j7J{#8BP1W>>Zw`F+T8&M(*o;OnbjvTgQ#o z-V{}N{~~I+4dRD>_b!tpcM=2eC%5#(`RlSCm(5FGE?Z+Ju|FA!KiqH3l^@Fu;j-($ zYti}qN80KT>#f82I>z-Lr6+dZ-E}Xt(f{d9+Q7K!UFh#1|6tKOaY}wbMj^!L%l7`a zsjGja@(1Sqn& z!YfnxNKt=5r|(%SP9h_!ekLcYKZLLPQ>r!hBQ|3oRW*Uw_l3Q84f~tZhrq1Au^FgH zV8;{WpZz_Y);HE^cXyGUqd=APhL^J>)#(E7|aq`y*|sVHGYRU<%{?#*)=M6(W{MDb-MAl1bq5e z_If;Aikl*=PO3+lZAm#gBk zp?1dOkpCFh-?tan!Q#0JyRvI2?;_$-IR^y#gW^AqTURG!fht}O^YG(gc6e=Q)<-`E z;2PItdV2?c3|iv3Xwl!?{c#;_UG~1Y-%CB|dY5n(yp>wBZY#PEXnHHs=div+(!oiV z4;|FJG&Cc2trpYC(8s67*3uidGGzLT07=KZmr+`f2dc8)>Che-b6u3=)urA8YHr0D zz3ys5zGIr{mA5kbU56pp%!atHt@Vz1K`m)3-;_It(A&F49N2;ueVsqB6$zO=6GQ5rj`oHX1^O3rf10%;L8?c~_cXoyf(HqmFY;-fzTMZ~t@rwt?1I^0g~-Q2 zYN5%RJ&SJ}bDmkX-z|-Pz@v-kJ)H3v_TZBJ2P#LpgHjQLocnL)WzxMpjLaO4bhZbV z((&&g2hs_|K!0;G;89Xa-{K@qLyP|K#IUzu=&58Btl++B`8Pb?jGW%cRBPR;t`B|A ztkr=XvMt0~{PYra{jOm?bwmEJpt|Ah6Y2rv$L^>`Y>b*pW?KunaTxptb|u6&23{sY zshf!IfE|>q&mxg)Fh=RmHc_nyc-ppfbz@>Rv$p@y@C}k*S>=>M(ppFmNB34Vy9tR* z`J)jAFn^%r#T2+-e)`Z0uL z!0aw{bCTgvH#hW0>41Fzvc&PtZVRbfLNytPh3LrKhevXhROxb^m1C`Aec6q^+QF!j z@AYnZeWuto9O|~%8b4U%sWR;v#v|j*9(CI*0ot->ap~0|%YHl= z^m2Xow^CnqgK@~^9H?_`px(c3F;ocd;?UI6+&*Z$M;Dp&G-TfUwZ zePX%$2yD)LLYy;&;Ea?t%ykfvtddi`kw)tILIFzh{efrI7T(E~Gv*Pw^EJ|IVNMWi z@4vXA@?6is#y5*^dOf#Xi^-?xbv{M>s*Vf8vLl_2%67yySXs2bqBnmOvWC3SieAzI z^Vgj8iw&5oJ|}916+UBX3(PGkl;o3#!bwOJUBZ#BXzJDIOSsYeNfCBuW73dqv9y@X zu9VkAm5%_j{B!(}zrv#og@WpH?UXL2sw1TTYL4-I3s~fSyz+Mu`#lbpXF=+Gmgg;J zE7tTM!g2l#i%agG<&WMd*OBNeOrUSZm=yq@$hDkn9c_9wb1fm8^k1~`K0(QIZzdgw z5m;j)&iMN8fDBv?IZNPHG9@cd$->nI#2{YBjH-5Ht+WJE)tV|UxR4!+UElZ&$OOV^ z$jjoGoW6`sO2#2V{#Zaw3uEcNSTIY(J*iEQ@jg(=dg@#av!{S8)_*dka5t`c7f0xj z)1mJz7MW-W%ri2RP&x<&W-EgdzuAREvB>r?w4R-!^wV0zisH!S9fwUn;3qPbrTl1-RA13PJ(i9TbKVA z8q0{U>FFT|0F`ipESS{vJtR1Q0l&3 zG#?0aVt#5(PfvWf+ zWJ&22$QoI+1?1OafxE02{{a$T!Jx!f;79xhsXhTQEG-y65s+q!h#gK<|AN;0xFqYF zL@iiMc18oBGA?FwSUl|zY5bokmR?K*p=&4=gX9G zONB2L9%U*dMW?dv17%wo`UfTFpcZ&ICqu25oT|{&^5TQHzuy z5xJb+${XR!zk#;+;%o+QH_Xuwt|j#{cR{AEfLu*GjEGQ#Q~`5bxPfxadb z^oJ$6l~J&ro5HmE7|o6|VM(6DoE}my!}A!iA2G+S*1r9-2hAs8Xh3%uiN|5Kufv-C z5j$dONH}&ZR`&+pZ&3Gkf8VL@3+66T_jUY$KHGV0MJj5;%cDimkqUsp_F!pEqOe$27G*Dh~*18mh9Qg6~1M zTEAsLntIrqSFaxKxv^b661u`W72?O>FI>X zaX)i;z1rYCx=wA_n$)Ns^>egWS+~G4^bE+5c>#{jN3^O(ce^{(WBKz^)MKrwjXYrt z^r^=^yhV9DF}g)Pv3WNkwx)OBbSkA3!>u-kXSb=1-S;-BC-vXANP54X(6RSCxn*So z#wTLPvc4G`FQCu*nUDexfDZA3;3gaA@exEQYYR#TTuR3Pkg5S!BawTgiI<1^3FqtL z)(WqB%6)2^dTMWJliK8-2LcB{RQ7xAYE#c)jp}K4qfI@%#q_CXO0Gvib{F~v%Ae~> zI;W0D_zzOhcPp-rtVA~Hy9V{lYtuT_v%&iosb_bLY=>oJmwGOCo=-j3Gao=1Z_}m@ zwBdr>Y4tk*^!<)R>CkKG&A!yGo-aQXLn#}n+IhTDIX{EcbsDRgTMT*nZ(%V%0a@Qx zO5sUMSRFH|7G$MEhAe^{C2dorP527pK}azpSWw9tR&dJ}$o*-oC2#hyjlIlTT=cGx z52zLggp|NiT)qS{i&E(=luGYh3|Sw7Y)FGVCe@9U$}gg~Tz+U=k+mH%2$l-@)o;ah zQhFJ_iq}#h@>p6S&&1V9^j3eq1@cV~>yZ2aw&ov8*vm4vsONjqo7D@!if;A77G5SM z^zR@?9z-;XUnES1t(9f?Ei%lRLJ+;**$KqObOa-`g=Brn-%d5;V9ZM#os=HNWb|PS zc_#TF6N#yBC#7JBQvSj1sCpIk?&c=cPiw=O_fZv_H(zFMaGO?-DCdt{-U2k2|ub%gsoJQdEG) z_S)e4kf8{tQLXk#TRlp>uftMeN3%IUjp}?ahMuM){%=Y(x1%q3o`c?Z+JW1FmVjK4 zgiU(OFSoOOJV~&F*;osnOIvXfHljti8k_5%e3*8&!p; zN_DOe&7U@7{2{air@$95Iw&2FaX0g_Hw8*xS`4!lmKdL!smKYiRA*z-lS8%0Y^3e= zWMKTjgblpgN$6M|RkQ))#d8|4cCa0#d%6L1F$e;WdvWGgX^YTS6d)~%+@wWiJE#Kp z&=J}Jmgp?1)%T#U`WE=g{{gDV0#!vJEoz25 z)CgI>h=4^pKp*#^S+djE*hRGgeFb)yi(hO9%V#Ladx9xhTniTSV>g=RU|k?Hs-mKY zNKQt=Jdmnvf&95!#C+{Q(=)|;6q)FYR3hcp#@J+a-N4D!B7X`JO>i0qQhAYsorjfd1E7DyU|zvF8X5YQ}E~#s@_Bg zN>2ozaUp!+C-9^EIgEQo(&qdX2R%b5xi=@HYKMnXA(1Tls0l+dc+MRt2);r{0uOqz zn-7b#jgUmg6R`X}^t$B>$&wWSuei!V=?dENWFhlhZW7(6(Hz)qr@D=j^CFa1cvEp& z-ou8r1qsuQsIU0J2fl5{>c5F9M-mluH$%$3*vM(zAGNTkH=oE6L?*u^ndfMO1k{r5fa}RQc;+32aP8>;@iI%yG2o zWDWe7ferb?7r|$-8$1rp5f6r385j?&L?W<~j_AkObyk8UREo*q+02Tfn=xc;qt|(e zn`*pgzccP>Fjw5%Ot9aEC3=#mf5uLj?uI$Y)}{*QHX`Ar!}kID%H`QFuoe=Ux`Ws+ zp;Ue^AWP*mTs0OF+C`YEWCR17ZD=`^-snD5g^r-A*QRJ8>I>Y!a80Kpcr9Au$D+P? zlpT<7kwZok#D7u%@*?_z_crd2ee6;sH0H>vF3E^3^uE?fV*UVgNREyEq zP1fR)!U*bbLX{!uUHqM!7~BoWz??4Jk{KWUm{Kqs5_l56NIUof8zJGB92n|IWz1(n z+!5T0H((<;!A_iCZyO&;<5g5+5loNFPm>1Zjo5{=3w$hx0wK%LW>n4(N6!&ikAl4!1?`ADo$^r z^d<^ineAw9MtxMyj`3+-$R3YuaV;!`mte>_HWkd*!xEei+`^|Q`Dgjb*lzUI+=iI@ z7?f5`LL#(*-U<)#^T<&=(gvSSW|OvGC7CO`%qR0EG;L;qP^pZuwtuw^O4 zLEh`m|ok@^7P+yzp5D@9C>bZxRyY|k^BmBZJch~OAbNuVv;^(+1~wq;GMdr1 zbrBM>pPTck7UeNoC6Btw>pUv#aSNXb2H8=*S`7~%B{MWx&Wd*$#Hy#dVL7OMKd;u?UK zko?URvl+3-(iE9i?S!NgYtb)+&qbSgRUCw0!ldA*c29~to zp`glJMZ#4{U~4uZk$*7?j3>OL&$lRb{|XlORL}lk=bdgxeGeq`5gooIKD2xab7>d_ z#Rp?IuK*WfE}o~OU)gYLCrYd3q~Q4#W|!RaN9QM_FQo^~Ly#yQD*DcBhFOj{RR_Qv z-qaRWuFd?Cre!Y=+#$E(G=SJT^(q-9V~fAY(cXF zLp5^7${5Qi7NUvrPw_%SJz^r@Co2n zUQ1i>Edo~lvklMPkn&$>3oLF0^RILmr%?4wq*PJS0bE)CjPu(ehXBYt5<~g(F;wzU zJs}QIEsbHLbSh%{jO2a>F=Gn~^x;lFOoht=0NN{>+mfuTTCo0N+omjWntAfNQ7pQ z7J18M>P~D#Hf(3e-lt@~jSHcjXpXcZTNJdR^q~k)!Wm=Mi*F~?*@TPFyjhJ<^xTHZa(U( z$h#2Zq%HtSW}Cm%Ltc>TBuqMgh3EkdIWSp$2dZ5CQDx3=NBuQ+lHn2Tnr9}1^y^02 zUP6KY9&A+KMs-HAozar#WdE%w&^uj8CctB7`7u5M=HQ5B$QDyA+>|1~z1Wark6ykC zPeeAfh8V>DEhBRvr<=Tk&>Rr;(H~)s%cDX8H?BM*LWUz zAWKWhOYj+sh`9S1FTJ~bsFK@?q8n`}m;+1oS9T(P4*2rVL5u$zY?R3F>4NV!Q~kCL zlkY9b2@CE=?Uy7kn8>@HN4WGHsgA3KfIeej1Pw_#{ z=mtv|kMtg@2Tvcj3T}frD%l=B0OQdO5V`2)&s_jA_yL)19Ezbl*?P-YqN?b(c1SE0 z%r7uq0`q`tj6@=ujYq}2=Hg9jCw|OEvvDY66nYY9=27@jGQmN$!=auiy0VG;=K)yL zpVb;q!B^nHMtB42L+4;Tn;xdA2R6h8V zcLXYshpLL38j*Oyhf?`SJ0woWFG~i&F0pYK>Z|8@Q1BgqoCjgDSRW?z%5{q$eVM2) zzYmkapqFY7F#Nl*Rv`DvHO)jQaBmaj0c^N`K|&8yNbVU*CtxTz6wM(?QC|lv{*!4d z{RJ(B{je6RLpF9ZRp0aQ#bqbaS688bBH_QG0ezpOfWD@Ssc{Ew)$?d8`34E&0P*PT zZpb~{tw$XgFU^CL-Ik7B*`3W)V3@6t&{`G+=fPMDy@g%9kw(u&m41t2?sO9?AHC6; zSPS3nWq5X@IeHvv<~$g?p*Z@&_aj>|98!D$NIgx>_>qZ8x10ox3q17753u!(Txf1P z=oc>{R!)=#S9YM~>aCF5xN8X|*hoV=QJ|0Zdg;?gwUP};lnlYR??WWaNe&cTgl1Ew zY3VH53d~IOB~jJec2t4DOo-UQ^p?MlT|H1WMk;OVFs?7?{Cn{-w35u$Cw+6BovQ4d z{uzk5U)h4v?*sUjfr9d9#X)(-@tgrEigcm$e0oE*Ot8YosphY5q$JNIt`8{{nc}Zp zy%=YE8Px_@yptPHa4H>^OmIq$C*|ycGn&yXXxj zFcjQ@$>3Ykt8XSwh#_`5w=??nfZltmq8=Rlz8M6AyHS5TC37MY?w&3>21d=2daYGP+wI_hkrHB#2!OJkEL-eeBsfU%$M)TqI)}Fk;8HH zq-L^M{$QeK78BNU1$^c^01DMZsxC%L{=-NVji)2fhWgN*_z`nD{(P`5co9RfWw;eu zj7NbLh((@3Ouq;!ksGe;zuK^?;c7ILbmiLFq;wW^vV-+$skI3`GhItp~INhO2-B5=&K@aBe&3DK8RRpLlf5SplZl03wTqA z^G7z2HU&ZOA)ZIRh3H%2s&C<&kpxSHTm0CEf*Hd*QCf+$ScMO}*Egf@JmC6o6p(tf z!nBo5b2GsTlfnE6eBtLHk?*llVPd!DtHs#3n6|tp9B4idKL4YTSZyPKR@4h2H%flR z1>-~jh2Cg`$T_y?cVHMcO8Wgwi99L<#E?~20XeB1@4{F`wi95 zHdyo#F|rUt#baqR=Gw6A_kEqy0l{EZ)v zF2`DEHxj{lpfa9-#9y}4A+Ja(d)iU;0l?$$!x!I;m)ehl-6?>SQ&r>|AE>sYG;lH= z>EnLMI5q-#_ul)SDf+c|o{Ca^jMo7R$a8#fTmUVVXmM8JrLhs>k~qiC23qvYMyg|2 zfg^H#67AQH#9g$N$c2GB7v|8n*yw#@cNba;laLM0?_k`Lfm>k1ZtvTh1k4`U#%7G9 zE&g6A9kM=0SKx=XTd>wcNzx~@5LEi0S^l~gEiPF6n;|8e>cR2=h9VDu&q!`Y%Nm&V z)1qD(3gy#heWe$Y*Rs{Cv8x|d^~VK;%*dT-oBwMZ@{ z^x-Ld1rqvYSW%d^@FfE1^bXpdZzgo<5OWDB@i8y=5pdT<|e_n72}mFZIFBE2tG-2g;GR)p*SO17e5$7HL}|q^5@M%9bTOF zL-b3oV7b7Ug-O4-75cy8&O1t~Dr@xjR(Dl(qG=jvkSG`t6N8E(>a85QsybJ7ZgfUW z7{=^i0y>e=0dp9Y?=y`f>gbFZrfHfE&<1ow5FK^U5i^L2+4tMKu#2gu^VVDIt?&J_ z*E#p>boSZzoOAa*VMQ45Bv#YNj;sM+n_PM&3x<-LizHmMcaKviOj!hzJb5X(UP`_% z(-vm2M4ojclL7nehmZwaHrdEwWD1ad4yoO7srBRO`coDsOD!cWwNO2+3d%rF7PL%Q>DqX$uO%OB{;x4z*}WpD zg346aVa}#c@B=-sFLsNQl-RH4)*c$_6M9IdCf1tpCwYgcu>s2+G#!m2FRh5Im~2shtaw{HWuWOY$;mGUW~_;^aw7tj6N zGuhxG+uOVQ9Nb*HT*+inWKUH6|f6+!B@sxx~CK$Zqji>Ldp98Z>X8nLb((5G2B*f?no&ob(f zVqld+k!JgJy9i3!jx^mBm0J0zswG`uprv*9yiNKheVO!KFk}OSDx~$$7gqNfsyTJf zkGV68C2p^yt}xwn^A+7JFxwwH%O+bRa|l^I_%_$Tv$YDkPp6-j$ zzC;CC>(Ea+54BZVdw7ngG;Kq23MJJ`rMz7`qg0z`NE|)=4HbV$QK+VBXCw2cCR6*R z1Zmdsgom|$eZhTbx2#RAnH_M3q)&<~r>eh?zwA#{RiVmTv1DWmO=dj^=fHNFwqjj? zHy}<6dYWk?Jxbp@n*#6b1p@fGtC>kl>&@U+?tJhha_o`0isx=-zLw0^n_hsT{xu3B zJ$%R4zYIlGcBSan_~43d!|&m(vU46I#IQkUW{dX9hTus(*i zGuogyRP>YCVbukUZx!;bRyLyxF2mth`v{=o3EZdTbezGe$);fK0#aw8T3J7?;u%fd ztU2iH?GMp4pM%J{A4TNvVBaA8^h$2e&0g%X4&<2 zp>D%(22qbOlqO^L-aQ|(x&qzk;qn7X(h!Y-8~;Ou@0LGB%FyK(P=v-@BKx%Xs&yal@iw>(ewI|S*C z&{Yz<@$|yp(~Dg^yM(hR3`pQF-qVogy$Pnl1d2EIppf?k6!OY+rpgN93?bP2Lg*aW z=ENEottWdqt>wz6W&AZzSZBf|a06D0#t@i@=Qo^v4WRTQtx1<&ilR`tCo9*8Ui zuZRX{EaGgXngXFX)Q-sNZulJpD{sCZ85=jCUmwr(YT}X{Y^C=T*G}xCMn6K~&&&c0 zMq2U);%Pmkru715kP7O5ji;!BtHg)g02&0UK~CNdETIxlE!n`5JitR#Kk#%rK)V0n z0W@RyNLcAjzdnyIqh0w;v z7xQj)p`WGSA5*AS^sY8%zDZni5{&mo2eh(TT`vr+ z_)RZLQNZ^?_L$avLG&39*^n)IU9Zh-e3%vpLoo>O`} z`~5201_KsCRzI9=JoR&A-^o^(Hca!O&<;}Wj20Ye6|HY=r9s`Yv^^-lT&l0hCcBwb z`61{WSWC*<1G3(U>5&mT}M4m;q z>Rd|WB?t;htL4|zv7QYIY(C&q>dZnH=axDgU+bNacqe~VAd#Au9=CYH$ z35u9K$m@48`su|l)>F{NuZApe(*kOA2f}sZS=ITFg-bV}Y14XW^FVR!hcwqU0QaTn z+;lC^+z>QOg-;{;%}nT0Tb=@j>For01RYst?I&@HRG?7EE|M; z>v&Uq>u6P<%oHlykx|+H68=1!ROD^g``dD0dKFo{tgv<%s=0G|C|!rIS#R^2)=LC3 zaT4sKp8>4A7`8fY5TG8zTAi4rY#4Y685RS#kxi_CU+r$H;F(1&gEe?lq#{6ez8wvQ zg=x3jK-OykYnJ8VDtCma`5G))e3)c6QW}4v9q@8Dc(=Bq#)?d;^$vnc?}tH9E@^6P zNBhKIeN^xS*z2`>B{w$X=r>}V#t>@g20HI|sG+yvg__W+em5wd5k1(d8rqs_Xyb>Y znp^(3N8W@?udCDlQPv-k7T%0%!T2%?UR!`6V)#|##vtlu!>_oo4XqlnefYO1*_04X zKSAfFeh2!!AnGn56&-`1^6D%=trIX?vU4ddpOXu}^=RO_6ycs8)CkCVRo+HKdV6Tb zutd~dv$9cg6{%1iT-+aEt11b_dcSJNJ71@ztMYh%tA@OT+8~ zpZsJ9jiEGl1+2WYn(>2!5UEq*)KMNft=;DXRi^eOFJg%FDZ4~=^t&hZb@y|u^fOW! zq^9R-p9RU2Oa>;~$}bHiIoTFn($r85^oon*%zWU}GVP;Kw}4xRxzSl-0Lkxw5jL_D z=3)EeyWlJ#R=KHouZ)3Ny`Dm$FI{rqqC{fcRc_;r4)%Wi!*f4|aE|NV3-oJIPyf-A zrHwPNxUC02826{e#+#kXXw~eG`;E75dj`+Rnnu=fKXP+iw8!ATd5IUa|pnVG`W*cueXYv}1$iM{*+2o!3 zDactna@ef&!SlvH^Cm51)77jfT5&|1ZRri5=sD5)mFk#4J@nX#)Pc`K9&JIc7eRJ; zyY+DAs z=yQA7P^RZF?_3P+aczK@fQJJZM`r8B95*$;i>!GJD7o_eJL*;Jl0TJac@xPd4;Ze> z0$Gj_Hl!K#+_${`T##p?!I(EOY>@|kdSWBNNzhzVNDd>`aq?qK5cH`Z; z+n~*?q?Q?1Ib#kfc&IR9(UVJYmImfGCeqC`1eqZ zJOvxr)@B?3DxHOw*e7A8;b)>+!N-&iDPhZ}9xS{6kzq_C$H|(PcN+g%zc|-;FBIN@ zZ~RDs;{>=EhhmcaPH6QslKkN)o+sPB^h{aj1=$#O6wjP@sLc75rPkXzY{abNv&_mxf=K%?@EZ90>AM8)xU82WL6U%otzY53-zrXv*3YU49{_Y$ngw6O<1c z1kbi?9iH)K6~hU-UGbREWzR z#OxC0GxRDJy){ZBXeKf~PWDLBeK{^d7h@=?WUptq$Z8x;`#Z0MS zRG=cQSJv=JvZEd7@RAWejooPHI}pH8ty0mqk!z&pQp=&wVqHUGEskAuUxQBTrWx`3 zuGdLv+loMeZ`4>*u zxSr;5oeaOo8@RH!tr^-XvUM4x!k1x^fCXOS0{FRq?SsKQbn&>`7=+uPjjqNn@h=?= za=(>=+3P`;-|p25(B4z*gY0t{R4gP_sXrLEI#I2X*Tr)TBIAcHKympZ@a3U><5hHw zQ8_53Y8y>0Og9Bbi47{z&x#}5YJ@>@;PXJ?mG42%C^%PsNvUTX>Uze(+1-en zxo>STc6J0inN|6Ej9tYiw^LdGO6}$Ap{)z=ZL*Mjnuq=_6vd*8g}=DYrYmsE@#gLm zpMz`@GNMQNF!2omY*5n&mjlr+$R=f@+Q1sFS3DR(J}$~005lwoAlECTyt0O`Y!2DT z-vNzJ@XR+J?S13hU~owmW;)vsa`s@dUk5PBE(dZSLz+8|Bl%=vT|0C>Rtr$zzY4j2 z`3DsK9F*Wis9XC~JAP1(R{k4l6#s27^~*cx-@4w|_1v%Up+F~Ucus(Q;A}hK6qp7Y z{D2n8-U3xCT!M%9ckLXwFgsHU*4cz4v_%FwP@lG_TxeDa^ zJf79xolU9CJH7IQF}TzV-f!0fZl_W7I*`~ItyuR)K)eC1>L+)j)h-{xE5Hj>^f24( z!A0w4hf#c$gUQ{86ja>{Mbm9Jd7kSh7K~w*-yduS~`ek#j?U z%Mck^A*^nof{}aufcw@XXjeC>$9Pt=9yR>4z>Du6K-~+;)=h3UKCfCyY0LxDXmKwU z97{n-26&AxQsBRqY*RgkXxd3>BtW*zPHFH8o&{cN$M&bRP!I)_Ue-qGD9Fmfw0^}& zP*g1JgUdY>MBl{uqwfmXu@LZK9&#TACH7|lPjms+gaA)306e<^uvW5NJ?L@)$kyw~ z4TL}r&h18_TtLm4aIS6TS>m%rRBJrQ{)=G{m<|Z-4B$hDW4b^Wsj@YGxO@wjguH&P zHIVrq%oO0j8t3GKcQ_)0E5UQ0nhou!Zl1Z@AzRH`5sy(z*9o9l6046Lj?NJY?KV9^ z*8e%F@>OWi_&5y83|h;-8M%SwWJC9L!_Px1x*FA@_ff&(qagGCD+-q)2V7PI8aIIA z+O{5Z(uEsddBH-zqQ}(Pbx8)o>%T^|+D{P_ls~dAS>sjtvfcQ+qqc;`YiI*B1p%j_ zU&)o-#uvUTY@{B8y?z$x0XK;+o<~WUDa4}Pa5+B*yJYp@G#{c>br9nOFQXtjH4DSK z0U_DHP<0ih9&r`x=m>S2hBSYx=-da2zdC4q(XnM7S^2#Z7k8|mv4HHCXli}bZhYw$ zv%9xp?D{7ut-N9hmA*8HpdW(9mmS0CT3mCRjXin~R(YV?*wax2zp?`{n0F-#S?`FT z`R!QkUD2b2LMAb*cSorN}g^vzP4gitMcJ43~?;9Rdc99)yu-= zMbwSI1YW2b4eEQ~5?stP_fj1w9k?VcC~9XQBkF3!E(w5-Il2+}zz&5( zw&S~`pOk2Y^|lXlRzg<#UN5OqvMvdHxaUJwlkLO?V|i93qf6~t7&MNj1*3oU<6N&o z7P*dR(Pv;9-R_l_iGo-!6qNytUG~>L-j8>vk?T1WFDVj`pcg1P70xARN_JZoVCQ-? zcsd7)VIkui%cL^prD))K64SZF!`v4F%BLXG6Gxh_A%w_#ppD+c06B9q$R!^l(mxhf z{;{BxhlFf81+^cMbzP07o|Vm%R(4Y$Tepg91h2E*_-1niR&k~VM*L1EH6PLk%0y6n z2L+99%R-{0gi1;u4#H&;Dfj3Fc*Mg#GxZ{ zu<=X=Yil`uY}r%rs~;4mzJEdk?_|tS$u96G^Q+l^476j6s02mjW(`0j`9$QbK=~&G4(diJrCcE}{5i9^ffrv{i4w zs_KIXU>CGipOcC=5$lUzL^gg8Oyd$6i$B;8=#bLq@&K=pN(@GBV(fCj1Obzu155!g zQA0x|Zh=AKc0$yNw}t?A45bBa=v?i9LAA`+s`H?zE_?w{1cT}c=vO@##j6)d_6~$s z-}^k^L8MjxO&Dxi3fNi-c!N+~wVdQ${Vp=9cP;_!7FK%%}JgjC8a%--pMm5a>+R&x8meSg5;ZobY z2+%Ii7RLZLNQ%7_Kh)mAu}ZZM!L;@X;%2oHFs*%$1DomwEdh)e44AMCP$j8q$m$wF zuDcvF)!mDrI@!8a_Yh51w|X()X~Fv&4OO>M7`!2++Xeg!&UGJ1>Bn%X&&dM}=2`vF zT)=+N)=z|U{Xt6slW@ZN<6Z{1_0Zbwk#o*1u71~Qrsp`huo0>C+&02fffri)?FbeW(m zMT4g6p9kCkS<|buOw*f4YuZ5rxNqRKS76Cnob=Q%x+ULdyogPU5VQ}n6lk{iXx_`f z5rBP4pQTg_D&@m2A&RF;Qz_+~%BE6!2&K}do@pv2NK?vLRnt%I)OUO;lA4Nq8V6%U z+ud|Q&O7PD^0)WW(;P-vqeDKg^-|p8ADVfV{QRe<(cgQn6W1|DT}-y@86TyoZ+7|e zckf3Bko+mItbgwp7=tN*soZ8t2U;@DbQxW;RIgt#}%ww9C&^LmD~fbl!!bPvZi{LkQvIPXWHtTj$jy?BIPXO??F7D^sNjr7(s`*QS4 z#u$$PMQ$xKbjgi#B4nv+C5D`YQ)h8GkKyQffz+N5LCWs|BNpM=BU+ci<)R2cW04QQ zCGWd7RFmI!gN7CZMq!OX;{;3;a1csrXj1PY(a@xxL7cxCopXQHjn$SyHtaS)foz1< zTd|XI#|J6RsAhOd#U1l!wu2%u#sUU#BUffiMH%1~`k~F~S@=^Faf%q{Xpr~0g=!=< zmpcHA!l9ztA-QID)e=}4^0%+B2#+Y7Dyd^zIbF!DM&5%rG zDa?`RJmezUt!S<^UU?X4+mMm=S0?=k`T_PSHGX!TmP9mDeOPK`VRDt34-L|~Qcp9& zRa)4&jy`b*AR<+@rey#mAeoA@CZnNqJq%voQ!W3Ulo|(-^}e4$c4#lN(kr~oullK0 zV1n{G-al+*aAlS%oA9&@l( zrs^a3`A4F2UIbmrDxYx><{CcS6zEaHGwr5je(oq&KHE`cmi%H>~v@Dxz2 z5Uj#C0wTxeFb>KBi})WXXc|Oz(BmjH=*1q?_%9!`py}A&V@InoQ{n8J4q5CDCj;p? zm|Cmt)VUuDn;j#@olw|j`mxJ5>yh@I&`M;-w+*ZLu7;w{3#&}=KV21(5Mwu92u0$n zR`|&_-@sU6_d)Z$nYKJxDgM53;wiNfq&I$WBt06V--HhrNDs zyXEIgnf#k5yF~CVqaeDZg}PnYiyvfBs&^SwJSG$);F8Egm+a#xb;);B>au}x1q=8w z@<|R=5Ou?+plL|Hn4{$mL@uyn=NM*+#lQ<)(+~SEBWOAqO@m+b!1)~181Wsc8V6iz z#%{#y6Djp)Bh8!LiXU7A=f-c)$~}yfF2Ib*7@)`I*Ni|~`9ahpaWAQ1`S6QexE_jA z5m_l;ZIvT==Gj7{G_3bidI_LhzJSV~f-F=6@QO1Oe$mA<(aL(0XYNUG8F>}74P8Ef zyav^m_fh%?J(A}_Y;TRF)6&D8>sNqWUWCrk4v=HrQt56eN*>Pu zyyWp_DB|E*PB)$?>)TzEVOqA7XEnD9`wJj*wUBj97d$zgSI^`PB|>DI;F9<=OkJ;m z=bIxGPO`z%0G3Iq_i2Fp5ojxZok78k6ojoVgvY%I+K+7c9uF3}jMC`iWGn2=07o~a z7k2LL3TzuGuxVdh;YQlP#49!bGtagoQhMsN9?p^;F4d8RYGIZ7%aiOyTjwLKO61O7 zM?L-@RlGipYo^KXzm=VOff*f5$X|N8Uqk$`g?Z-4{pya@Sp~6#| zR`i2x9SRsbhVCY<=U3$9|D*Bi=yQg9LB4%H?5Ci(s!{trGLpK8HS~~HUE`lxr$1c& zx~T?f>km?cgEIX&;fj)GA^ioWKj2do+KuSoHGC_!(!X=6pQ5C?@O^&P=`bdtzogki z*ucEC3)^p`@$|K7xLE8m8|NSVV=GhLv#_(aDooA&8vo2olAM62TtH?S%FnollzAuY zmFd_7?~x%MG47T&+~;OI){wST{cI~e=BTQ#10YL76_)Up=@e&d6d8sLe0C2`sPmWH zFDcExE{kV18O){L>o!Ntmv!MqLjZ>Kwt5B3;1f`{;Kljo=mIy|7p$bA#;_3F$dDr&ZVlbiptY1b&%>OBExCY&y>NcElN5pJ=n3PF9jzSl; zsajGUB%~vb&bdv zS#M$SUAMOi^3S?5N+Ty7^CKL|l8qtSsu{AU!LkdoV=Z7Dz_)829rsX7?AtY;XL1{b zl_jMzc^dVDw7HL_urlY;OG)6eG{KNx`Z)!b!LzEZl5&`P2UjbDRNY(5ODx-3fEdSe zy37gDn~>pse?3`gX=alhlQN`QdZ}EpZUc5M zI*A5NHkr6oQl$L;Hbw)~l3JScY!;;ht(q~-qsZm~rJWS)KOAJ;k}zyqC$z&d01in> z$E|O&-taa)=-+GIylY4edlC21(s4y>f4PNU0lE=}`@pf4GZTyFZKq$;i;>KuXb-Kz z_)P&^&jxbFtqw#?%fX8~Y0JXwW?cCWgs0Uqy@E2)OhZZTX%LI+O-O;05T52pdXvCD zuJ<*Uo~$U~z0~WP1`VQ(hVDnT(nk$(%rx5MwiX?Lq$xZr`j965kY@?~Rm#qzwxNbL zN?EiW7pTBs+Qa31L(L0XvHg&ZP>jD2YiQq8@a_ie661{@F8?s>xu6XHJv~Fg>7=yh zvdzPOj&CW=`J^4INq=RZSOT~P4>RSzf%aFKkAhcpS2y3L(rM+LLEVPQJZ$)(sB8QP zUalPQHE1gu*=|s> zTAd{M7px_3nUL4Y*n;V- z{b8WPiONtfNe(~*Z)0-k6@N(%+oIn-!{^xfaFH4Xq+7S->nZ8k1CF%MbS71|v@gDC zFwhGYleDLh)h;qzhD|Hqq#3Q<830jy?3_iUM5MkII#V6<9@$^CqBwVrX46`fUGy~)hulk5-RCD#Os!L$XHl@1-LC6=kCNSo)VNNtabWzuXw7FWIC;L5b zqhEd?r`*xCimbjB8M3v^JArY~6N4;r7Jd-vaxz5a0vdKjNL2vhi_tzL2dukVgjNDQ z(a*g!z!tJGxjrE(^K3m&UvG)g^;eNIdf+@$x&ny>c^`pGSi(T1UTUchQZrgpJc}=F zVE~t|H0d-F0V*4e8akm$$cm4KI{_{S1=bEI8iN=oSiAuln>*n=5ry=|kg^|yX$dH? zdr2AjEuSDz6!0pZ2)UE4O(|SwRAra#FDS|B3C|9f=vKz+N7Tjyx3`z z@}#=56H}4t`poI-428B#T(7jnoS`n2*G%=tmvY6$_|y3FGyXgNTdKufpyP7#ApP9^wcD|VBFE?x*($^J9R(snKcTo>CHK+GDg64@a z*>DZQiA?xJpm%K^HsPp|UQi=W4>ZfA6vhGBL3Hgxqe2C9`P zZj%j6wi~F?(B5KFpG3`*Ivj5EWR|3vCwCm=Hczp}begAhoIKC8bn!5sGN#p0;Wk~| z)MUCkk93=E%d&|Np*Xj+neNRp<8eRcF+J9|*pH~Q>FKy=9u+L4;m4G=nO@h6ou;>= zu+#KeSvJ$xQSLVV7RO-uJLCgl)a6uPPe^1Bj2XZk%|O?I^UR>-hhOfNjb^aJVKYM= zhuh3B)-}T&r*&ecRlR00Hw~G^9kp&V!sS6`r1x!`8KvhkqaC@OW{h{$jIBM!ZI)OW zoo30}(~zcHLqjjDGfO?6xy{nGhtD(1+?Lxc>v+*-mNTZ9;X zW=-f^K^-=Z_xx_E@SnA> z-x}JB;(p#k=kYoeiM1^}lW{P`H2SKrlDW6%?+gAZl( zD8*h4w$BtyW+*z#)f)Yq;)uM0Q8y)bpINThK>v8^Rl@ocNon=S1=Ze=`FKi@{*g$| zX=HektZmWuHu^+AKxuU{_LJ2psAXtNF{m#mlwBd`q*mO_2$t9&DOs6RAzwC?TL4v? z7&Yn+%@ROXZw%IZf>Rd*>jX`20Dn-{-8we*i&noaj< zJTamnK<5XMEz)_^c+Aro8~3_EaytEtb8pBoMVtK9GgfG4WB>;IK>uEAXiZx`>8AsY robrQ|owa>aoRhN_X(Mf!48lrA?l?kCzkiF4Q8iUAs#=ol_WAz{i*386 diff --git a/publicsuffix/data/text b/publicsuffix/data/text index 124dcd61f4..7e516413f6 100644 --- a/publicsuffix/data/text +++ b/publicsuffix/data/text @@ -1 +1 @@ -billustrationionjukudoyamakeupowiathletajimageandsoundandvision-riopretobishimagentositecnologiabiocelotenkawabipanasonicatfoodnetworkinggroupperbirdartcenterprisecloudaccesscamdvrcampaniabirkenesoddtangenovarahkkeravjuegoshikikiraraholtalenishikatakazakindependent-revieweirbirthplaceu-1bitbucketrzynishikatsuragirlyuzawabitternidiscoverybjarkoybjerkreimdbaltimore-og-romsdalp1bjugnishikawazukamishihoronobeautydalwaysdatabaseballangenkainanaejrietisalatinabenogatabitorderblackfridaybloombergbauernishimerabloxcms3-website-us-west-2blushakotanishinomiyashironocparachutingjovikarateu-2bmoattachmentsalangenishinoomotegovtattoolforgerockartuzybmsalon-1bmwellbeingzoneu-3bnrwesteuropenairbusantiquesaltdalomzaporizhzhedmarkaratsuginamikatagamilanotairesistanceu-4bondigitaloceanspacesaludishangrilanciabonnishinoshimatsusakahoginankokubunjindianapolis-a-bloggerbookonlinewjerseyboomlahppiacenzachpomorskienishiokoppegardiskussionsbereichattanooganordkapparaglidinglassassinationalheritageu-north-1boschaefflerdalondonetskarelianceu-south-1bostik-serveronagasukevje-og-hornnesalvadordalibabalatinord-aurdalipaywhirlondrinaplesknsalzburgleezextraspace-to-rentalstomakomaibarabostonakijinsekikogentappssejnyaarparalleluxembourglitcheltenham-radio-opensocialorenskogliwicebotanicalgardeno-staginglobodoes-itcouldbeworldisrechtranakamurataiwanairforcechireadthedocsxeroxfinitybotanicgardenishitosashimizunaminamiawajikindianmarketinglogowestfalenishiwakindielddanuorrindigenamsskoganeindustriabotanyanagawallonieruchomoscienceandindustrynissandiegoddabouncemerckmsdnipropetrovskjervoyageorgeorgiabounty-fullensakerrypropertiesamegawaboutiquebecommerce-shopselectaxihuanissayokkaichintaifun-dnsaliasamnangerboutireservditchyouriparasiteboyfriendoftheinternetflixjavaldaostathellevangerbozen-sudtirolottokorozawabozen-suedtirolouvreisenissedalovepoparisor-fronisshingucciprianiigataipeidsvollovesickariyakumodumeloyalistoragebplaceducatorprojectcmembersampalermomahaccapooguybrandywinevalleybrasiliadboxosascoli-picenorddalpusercontentcp4bresciaokinawashirosatobamagazineuesamsclubartowestus2brindisibenikitagataikikuchikumagayagawalmartgorybristoloseyouriparliamentjeldsundivtasvuodnakaniikawatanagurabritishcolumbialowiezaganiyodogawabroadcastlebtimnetzlgloomy-routerbroadwaybroke-itvedestrandivttasvuotnakanojohanamakindlefrakkestadiybrokerbrothermesaverdeatnulmemergencyachtsamsungloppennebrowsersafetymarketsandnessjoenl-ams-1brumunddalublindesnesandoybrunelastxn--0trq7p7nnbrusselsandvikcoromantovalle-daostavangerbruxellesanfranciscofreakunekobayashikaoirmemorialucaniabryanskodjedugit-pagespeedmobilizeroticagliaricoharuovatlassian-dev-builderscbglugsjcbnpparibashkiriabrynewmexicoacharterbuzzwfarmerseinebwhalingmbhartiffany-2bzhitomirbzzcodyn-vpndnsantacruzsantafedjeffersoncoffeedbackdropocznordlandrudupontariobranconavstackasaokamikoaniikappudownloadurbanamexhibitioncogretakamatsukawacollectioncolognewyorkshirebungoonordre-landurhamburgrimstadynamisches-dnsantamariakecolonialwilliamsburgripeeweeklylotterycoloradoplateaudnedalncolumbusheycommunexus-3community-prochowicecomobaravendbambleborkapsicilyonagoyauthgear-stagingivestbyglandroverhallair-traffic-controlleyombomloabaths-heilbronnoysunddnslivegarsheiheijibigawaustraliaustinnfshostrolekamisatokaizukameyamatotakadaustevollivornowtv-infolldalolipopmcdircompanychipstmncomparemarkerryhotelsantoandrepbodynaliasnesoddenmarkhangelskjakdnepropetrovskiervaapsteigenflfannefrankfurtjxn--12cfi8ixb8lutskashibatakashimarshallstatebankashiharacomsecaaskimitsubatamibuildingriwatarailwaycondoshichinohealth-carereformemsettlersanukindustriesteamfamberlevagangaviikanonjinfinitigotembaixadaconferenceconstructionconsuladogadollsaobernardomniweatherchanneluxuryconsultanthropologyconsultingroks-thisayamanobeokakegawacontactkmaxxn--12co0c3b4evalled-aostamayukinsuregruhostingrondarcontagematsubaravennaharimalborkashiwaracontemporaryarteducationalchikugodonnakaiwamizawashtenawsmppl-wawdev-myqnapcloudcontrolledogawarabikomaezakirunoopschlesischesaogoncartoonartdecologiacontractorskenconventureshinodearthickashiwazakiyosatokamachilloutsystemscloudsitecookingchannelsdvrdnsdojogaszkolancashirecifedexetercoolblogdnsfor-better-thanawassamukawatarikuzentakatairavpagecooperativano-frankivskygearapparochernigovernmentksatxn--1ck2e1bananarepublic-inquiryggeebinatsukigatajimidsundevelopmentatarantours3-external-1copenhagencyclopedichiropracticatholicaxiashorokanaiecoproductionsaotomeinforumzcorporationcorsicahcesuoloanswatch-and-clockercorvettenrissagaeroclubmedecincinnativeamericanantiquest-le-patron-k3sapporomuracosenzamamidorittoeigersundynathomebuiltwithdarkasserverrankoshigayaltakasugaintelligencecosidnshome-webservercellikescandypoppdaluzerncostumedicallynxn--1ctwolominamatargets-itlon-2couchpotatofriesardegnarutomobegetmyiparsardiniacouncilvivanovoldacouponsarlcozoracq-acranbrookuwanalyticsarpsborgrongausdalcrankyowariasahikawatchandclockasukabeauxartsandcraftsarufutsunomiyawakasaikaitabashijonawatecrdyndns-at-homedepotaruinterhostsolutionsasayamatta-varjjatmpartinternationalfirearmsaseboknowsitallcreditcardyndns-at-workshoppingrossetouchigasakitahiroshimansionsaskatchewancreditunioncremonashgabadaddjaguarqcxn--1lqs03ncrewhmessinarashinomutashinaintuitoyosatoyokawacricketnedalcrimeast-kazakhstanangercrotonecrownipartsassarinuyamashinazawacrsaudacruisesauheradyndns-blogsitextilegnicapetownnews-stagingroundhandlingroznycuisinellancasterculturalcentertainmentoyotapartysvardocuneocupcakecuritibabymilk3curvallee-d-aosteinkjerusalempresashibetsurugashimaringatlantajirinvestmentsavannahgacutegirlfriendyndns-freeboxoslocalzonecymrulvikasumigaurawa-mazowszexnetlifyinzairtrafficplexus-1cyonabarumesswithdnsaveincloudyndns-homednsaves-the-whalessandria-trani-barletta-andriatranibarlettaandriacyouthruherecipescaracaltanissettaishinomakilovecollegefantasyleaguernseyfembetsukumiyamazonawsglobalacceleratorahimeshimabaridagawatchesciencecentersciencehistoryfermockasuyamegurownproviderferraraferraris-a-catererferrerotikagoshimalopolskanlandyndns-picsaxofetsundyndns-remotewdyndns-ipasadenaroyfgujoinvilleitungsenfhvalerfidontexistmein-iservschulegallocalhostrodawarafieldyndns-serverdalfigueresindevicenzaolkuszczytnoipirangalsaceofilateliafilegear-augustowhoswholdingsmall-webthingscientistordalfilegear-debianfilegear-gbizfilegear-iefilegear-jpmorganfilegear-sg-1filminamiechizenfinalfinancefineartscrapper-sitefinlandyndns-weblikes-piedmonticellocus-4finnoyfirebaseappaviancarrdyndns-wikinkobearalvahkijoetsuldalvdalaskanittedallasalleasecuritytacticschoenbrunnfirenetoystre-slidrettozawafirenzefirestonefirewebpaascrappingulenfirmdaleikangerfishingoldpoint2thisamitsukefitjarvodkafjordyndns-workangerfitnessettlementozsdellogliastradingunmanxn--1qqw23afjalerfldrvalleeaosteflekkefjordyndns1flesberguovdageaidnunjargaflickragerogerscrysecretrosnubar0flierneflirfloginlinefloppythonanywhereggio-calabriafloraflorencefloridatsunangojomedicinakamagayahabackplaneapplinzis-a-celticsfanfloripadoval-daostavalleyfloristanohatakahamalselvendrellflorokunohealthcareerscwienflowerservehalflifeinsurancefltrani-andria-barletta-trani-andriaflynnhosting-clusterfnchiryukyuragifuchungbukharanzanfndynnschokokekschokoladenfnwkaszubytemarkatowicefoolfor-ourfor-somedio-campidano-mediocampidanomediofor-theaterforexrothachijolsterforgotdnservehttpbin-butterforli-cesena-forlicesenaforlillesandefjordynservebbscholarshipschoolbusinessebyforsaleirfjordynuniversityforsandasuolodingenfortalfortefortmissoulangevagrigentomologyeonggiehtavuoatnagahamaroygardencowayfortworthachinoheavyfosneservehumourfotraniandriabarlettatraniandriafoxfordecampobassociatest-iserveblogsytemp-dnserveirchitachinakagawashingtondchernivtsiciliafozfr-par-1fr-par-2franamizuhobby-sitefrancaiseharafranziskanerimalvikatsushikabedzin-addrammenuorochesterfredrikstadtvserveminecraftranoyfreeddnsfreebox-oservemp3freedesktopfizerfreemasonryfreemyiphosteurovisionfreesitefreetlservep2pgfoggiafreiburgushikamifuranorfolkebibleksvikatsuyamarugame-hostyhostingxn--2m4a15efrenchkisshikirkeneservepicservequakefreseniuscultureggio-emilia-romagnakasatsunairguardiannakadomarinebraskaunicommbankaufentigerfribourgfriuli-v-giuliafriuli-ve-giuliafriuli-vegiuliafriuli-venezia-giuliafriuli-veneziagiuliafriuli-vgiuliafriuliv-giuliafriulive-giuliafriulivegiuliafriulivenezia-giuliafriuliveneziagiuliafriulivgiuliafrlfroganservesarcasmatartanddesignfrognfrolandynv6from-akrehamnfrom-alfrom-arfrom-azurewebsiteshikagamiishibukawakepnoorfrom-capitalonewportransipharmacienservicesevastopolefrom-coalfrom-ctranslatedynvpnpluscountryestateofdelawareclaimschoolsztynsettsupportoyotomiyazakis-a-candidatefrom-dchitosetodayfrom-dediboxafrom-flandersevenassisienarvikautokeinoticeablewismillerfrom-gaulardalfrom-hichisochikuzenfrom-iafrom-idyroyrvikingruenoharafrom-ilfrom-in-berlindasewiiheyaizuwakamatsubushikusakadogawafrom-ksharpharmacyshawaiijimarcheapartmentshellaspeziafrom-kyfrom-lanshimokawafrom-mamurogawatsonfrom-mdfrom-medizinhistorischeshimokitayamattelekommunikationfrom-mifunefrom-mnfrom-modalenfrom-mshimonitayanagit-reposts-and-telecommunicationshimonosekikawafrom-mtnfrom-nchofunatoriginstantcloudfrontdoorfrom-ndfrom-nefrom-nhktistoryfrom-njshimosuwalkis-a-chefarsundyndns-mailfrom-nminamifuranofrom-nvalleedaostefrom-nynysagamiharafrom-ohdattorelayfrom-oketogolffanshimotsukefrom-orfrom-padualstackazoologicalfrom-pratogurafrom-ris-a-conservativegashimotsumayfirstockholmestrandfrom-schmidtre-gauldalfrom-sdscloudfrom-tnfrom-txn--2scrj9chonanbunkyonanaoshimakanegasakikugawaltervistailscaleforcefrom-utsiracusaikirovogradoyfrom-vald-aostarostwodzislawildlifestylefrom-vtransportefrom-wafrom-wiardwebview-assetshinichinanfrom-wvanylvenneslaskerrylogisticshinjournalismartlabelingfrom-wyfrosinonefrostalowa-wolawafroyal-commissionfruskydivingfujiiderafujikawaguchikonefujiminokamoenairkitapps-auction-rancherkasydneyfujinomiyadattowebhoptogakushimotoganefujiokayamandalfujisatoshonairlinedre-eikerfujisawafujishiroishidakabiratoridedyn-berlincolnfujitsuruokazakiryuohkurafujiyoshidavvenjargap-east-1fukayabeardubaiduckdnsncfdfukuchiyamadavvesiidappnodebalancertmgrazimutheworkpccwilliamhillfukudomigawafukuis-a-cpalacefukumitsubishigakisarazure-mobileirvikazteleportlligatransurlfukuokakamigaharafukuroishikarikaturindalfukusakishiwadazaifudaigokaseljordfukuyamagatakaharunusualpersonfunabashiriuchinadafunagatakahashimamakisofukushimangonnakatombetsumy-gatewayfunahashikamiamakusatsumasendaisenergyfundaciofunkfeuerfuoiskujukuriyamangyshlakasamatsudoomdnstracefuosskoczowinbar1furubirafurudonostiaafurukawajimaniwakuratefusodegaurafussaintlouis-a-anarchistoireggiocalabriafutabayamaguchinomihachimanagementrapaniizafutboldlygoingnowhere-for-morenakatsugawafuttsurutaharafuturecmshinjukumamotoyamashikefuturehostingfuturemailingfvghamurakamigoris-a-designerhandcraftedhandsonyhangglidinghangoutwentehannanmokuizumodenaklodzkochikuseihidorahannorthwesternmutualhanyuzenhapmircloudletshintokushimahappounzenharvestcelebrationhasamap-northeast-3hasaminami-alpshintomikasaharahashbangryhasudahasura-apphiladelphiaareadmyblogspotrdhasvikfh-muensterhatogayahoooshikamaishimofusartshinyoshitomiokamisunagawahatoyamazakitakatakanabeatshiojirishirifujiedahatsukaichikaiseiyoichimkentrendhostinghattfjelldalhayashimamotobusellfylkesbiblackbaudcdn-edgestackhero-networkisboringhazuminobushistoryhelplfinancialhelsinkitakyushuaiahembygdsforbundhemneshioyanaizuerichardlimanowarudahemsedalhepforgeblockshirahamatonbetsurgeonshalloffameiwamasoyheroyhetemlbfanhgtvaohigashiagatsumagoianiahigashichichibuskerudhigashihiroshimanehigashiizumozakitamigrationhigashikagawahigashikagurasoedahigashikawakitaaikitamotosunndalhigashikurumeeresinstaginghigashimatsushimarburghigashimatsuyamakitaakitadaitoigawahigashimurayamamotorcycleshirakokonoehigashinarusells-for-lesshiranukamitondabayashiogamagoriziahigashinehigashiomitamanortonsberghigashiosakasayamanakakogawahigashishirakawamatakanezawahigashisumiyoshikawaminamiaikitanakagusukumodernhigashitsunosegawahigashiurausukitashiobarahigashiyamatokoriyamanashifteditorxn--30rr7yhigashiyodogawahigashiyoshinogaris-a-doctorhippyhiraizumisatohnoshoohirakatashinagawahiranairportland-4-salernogiessennanjobojis-a-financialadvisor-aurdalhirarahiratsukaerusrcfastlylbanzaicloudappspotagerhirayaitakaokalmykiahistorichouseshiraois-a-geekhakassiahitachiomiyagildeskaliszhitachiotagonohejis-a-greenhitraeumtgeradegreehjartdalhjelmelandholeckodairaholidayholyhomegoodshiraokamitsuehomeiphilatelyhomelinkyard-cloudjiffyresdalhomelinuxn--32vp30hachiojiyahikobierzycehomeofficehomesecuritymacaparecidahomesecuritypchoseikarugamvikarlsoyhomesenseeringhomesklepphilipsynology-diskstationhomeunixn--3bst00minamiiserniahondahongooglecodebergentinghonjyoitakarazukaluganskharkivaporcloudhornindalhorsells-for-ustkanmakiwielunnerhortendofinternet-dnshiratakahagitapphoenixn--3ds443ghospitalhoteleshishikuis-a-guruhotelwithflightshisognehotmailhoyangerhoylandetakasagophonefosshisuifuettertdasnetzhumanitieshitaramahungryhurdalhurumajis-a-hard-workershizukuishimogosenhyllestadhyogoris-a-hunterhyugawarahyundaiwafuneis-into-carsiiitesilkharkovaresearchaeologicalvinklein-the-bandairtelebitbridgestoneenebakkeshibechambagricultureadymadealstahaugesunderseaportsinfolionetworkdalaheadjudygarlandis-into-cartoonsimple-urlis-into-gamesserlillyis-leetrentin-suedtirolis-lostre-toteneis-a-lawyeris-not-certifiedis-savedis-slickhersonis-uberleetrentino-a-adigeis-very-badajozis-a-liberalis-very-evillageis-very-goodyearis-very-niceis-very-sweetpepperugiais-with-thebandovre-eikerisleofmanaustdaljellybeanjenv-arubahccavuotnagaragusabaerobaticketsirdaljeonnamerikawauejetztrentino-aadigejevnakershusdecorativeartslupskhmelnytskyivarggatrentino-alto-adigejewelryjewishartgalleryjfkhplaystation-cloudyclusterjgorajlljls-sto1jls-sto2jls-sto3jmphotographysiojnjaworznospamproxyjoyentrentino-altoadigejoyokaichibajddarchitecturealtorlandjpnjprslzjurkotohiradomainstitutekotourakouhokutamamurakounosupabasembokukizunokunimilitarykouyamarylhurstjordalshalsenkouzushimasfjordenkozagawakozakis-a-llamarnardalkozowindowskrakowinnersnoasakatakkokamiminersokndalkpnkppspbarcelonagawakkanaibetsubamericanfamilyds3-fips-us-gov-west-1krasnikahokutokashikis-a-musiciankrasnodarkredstonekrelliankristiansandcatsolarssonkristiansundkrodsheradkrokstadelvalle-aostatic-accessolognekryminamiizukaminokawanishiaizubangekumanotteroykumatorinovecoregontrailroadkumejimashikis-a-nascarfankumenantokonamegatakatoris-a-nursells-itrentin-sud-tirolkunisakis-a-painteractivelvetrentin-sudtirolkunitachiaraindropilotsolundbecknx-serversellsyourhomeftphxn--3e0b707ekunitomigusukuleuvenetokigawakunneppuboliviajessheimpertrixcdn77-secureggioemiliaromagnamsosnowiechristiansburgminakamichiharakunstsammlungkunstunddesignkuokgroupimientaketomisatoolsomakurehabmerkurgankurobeeldengeluidkurogimimatakatsukis-a-patsfankuroisoftwarezzoologykuromatsunais-a-personaltrainerkuronkurotakikawasakis-a-photographerokussldkushirogawakustanais-a-playershiftcryptonomichigangwonkusupersalezajskomakiyosemitekutchanelkutnowruzhgorodeokuzumakis-a-republicanonoichinomiyakekvafjordkvalsundkvamscompute-1kvanangenkvinesdalkvinnheradkviteseidatingkvitsoykwpspdnsomnatalkzmisakis-a-soxfanmisasaguris-a-studentalmisawamisconfusedmishimasudamissilemisugitokuyamatsumaebashikshacknetrentino-sued-tirolmitakeharamitourismilemitoyoakemiuramiyazurecontainerdpolicemiyotamatsukuris-a-teacherkassyno-dshowamjondalenmonstermontrealestatefarmequipmentrentino-suedtirolmonza-brianzapposor-odalmonza-e-della-brianzaptokyotangotpantheonsitemonzabrianzaramonzaebrianzamonzaedellabrianzamoonscalebookinghostedpictetrentinoa-adigemordoviamoriyamatsumotofukemoriyoshiminamiashigaramormonmouthachirogatakamoriokakudamatsuemoroyamatsunomortgagemoscowiosor-varangermoseushimodatemosjoenmoskenesorfoldmossorocabalena-devicesorreisahayakawakamiichikawamisatottoris-a-techietis-a-landscaperspectakasakitchenmosvikomatsushimarylandmoteginowaniihamatamakinoharamoviemovimientolgamozilla-iotrentinoaadigemtranbytomaritimekeepingmuginozawaonsensiositemuikaminoyamaxunispacemukoebenhavnmulhouseoullensvanguardmunakatanemuncienciamuosattemupinbarclaycards3-sa-east-1murmanskomforbar2murotorcraftrentinoalto-adigemusashinoharamuseetrentinoaltoadigemuseumverenigingmusicargodaddyn-o-saurlandesortlandmutsuzawamy-wanggoupilemyactivedirectorymyamazeplaymyasustor-elvdalmycdmycloudnsoruminamimakis-a-rockstarachowicemydattolocalcertificationmyddnsgeekgalaxymydissentrentinos-tirolmydobissmarterthanyoumydrobofageologymydsoundcastronomy-vigorlicemyeffectrentinostirolmyfastly-terrariuminamiminowamyfirewalledreplittlestargardmyforuminamioguni5myfritzmyftpaccessouthcarolinaturalhistorymuseumcentermyhome-servermyjinomykolaivencloud66mymailermymediapchristmasakillucernemyokohamamatsudamypepinkommunalforbundmypetsouthwest1-uslivinghistorymyphotoshibalashovhadanorth-kazakhstanmypicturestaurantrentinosud-tirolmypsxn--3pxu8kommunemysecuritycamerakermyshopblocksowamyshopifymyspreadshopwarendalenugmythic-beastspectruminamisanrikubetsuppliesoomytis-a-bookkeepermaritimodspeedpartnermytuleap-partnersphinxn--41amyvnchromediatechnologymywirepaircraftingvollohmusashimurayamashikokuchuoplantationplantspjelkavikomorotsukagawaplatformsharis-a-therapistoiaplatter-appinokofuefukihaboromskogplatterpioneerplazaplcube-serversicherungplumbingoplurinacionalpodhalepodlasiellaktyubinskiptveterinairealmpmnpodzonepohlpoivronpokerpokrovskomvuxn--3hcrj9choyodobashichikashukujitawaraumalatvuopmicrosoftbankarmoypoliticarrierpolitiendapolkowicepoltavalle-d-aostaticspydebergpomorzeszowitdkongsbergponpesaro-urbino-pesarourbinopesaromasvuotnarusawapordenonepornporsangerporsangugeporsgrunnanyokoshibahikariwanumatakinouepoznanpraxis-a-bruinsfanprdpreservationpresidioprgmrprimetelemarkongsvingerprincipeprivatizehealthinsuranceprofesionalprogressivestfoldpromombetsupplypropertyprotectionprotonetrentinosued-tirolprudentialpruszkowithgoogleapiszprvcyberprzeworskogpulawypunyufuelveruminamiuonumassa-carrara-massacarraramassabuyshousesopotrentino-sud-tirolpupugliapussycateringebuzentsujiiepvhadselfiphdfcbankazunoticiashinkamigototalpvtrentinosuedtirolpwchungnamdalseidsbergmodellingmxn--11b4c3dray-dnsupdaterpzqhaebaruericssongdalenviknakayamaoris-a-cubicle-slavellinodeobjectshinshinotsurfashionstorebaselburguidefinimamateramochizukimobetsumidatlantichirurgiens-dentistes-en-franceqldqotoyohashimotoshimatsuzakis-an-accountantshowtimelbourneqponiatowadaqslgbtrentinsud-tirolqualifioappippueblockbusternopilawaquickconnectrentinsudtirolquicksytesrhtrentinsued-tirolquipelementsrltunestuff-4-saletunkonsulatrobeebyteappigboatsmolaquilanxessmushcdn77-sslingturystykaniepcetuscanytushuissier-justicetuvalleaostaverntuxfamilytwmailvestvagoyvevelstadvibo-valentiavibovalentiavideovillastufftoread-booksnestorfjordvinnicasadelamonedagestangevinnytsiavipsinaappiwatevirginiavirtual-uservecounterstrikevirtualcloudvirtualservervirtualuserveexchangevirtuelvisakuhokksundviterbolognagasakikonaikawagoevivianvivolkenkundenvixn--42c2d9avlaanderennesoyvladikavkazimierz-dolnyvladimirvlogintoyonezawavminanovologdanskonyveloftrentino-stirolvolvolkswagentstuttgartrentinsuedtirolvolyngdalvoorlopervossevangenvotevotingvotoyonovps-hostrowiecircustomer-ocimmobilienwixsitewloclawekoobindalwmcloudwmflabsurnadalwoodsidelmenhorstabackyardsurreyworse-thandawowithyoutuberspacekitagawawpdevcloudwpenginepoweredwphostedmailwpmucdnpixolinodeusercontentrentinosudtirolwpmudevcdnaccessokanagawawritesthisblogoipizzawroclawiwatsukiyonoshiroomgwtcirclerkstagewtfastvps-serverisignwuozuwzmiuwajimaxn--4gbriminingxn--4it168dxn--4it797kooris-a-libertarianxn--4pvxs4allxn--54b7fta0ccivilaviationredumbrellajollamericanexpressexyxn--55qw42gxn--55qx5dxn--5dbhl8dxn--5js045dxn--5rtp49civilisationrenderxn--5rtq34koperviklabudhabikinokawachinaganoharamcocottempurlxn--5su34j936bgsgxn--5tzm5gxn--6btw5axn--6frz82gxn--6orx2rxn--6qq986b3xlxn--7t0a264civilizationthewifiatmallorcafederation-webspacexn--80aaa0cvacationsusonoxn--80adxhksuzakananiimiharuxn--80ao21axn--80aqecdr1axn--80asehdbarclays3-us-east-2xn--80aswgxn--80aukraanghkembuchikujobservableusercontentrevisohughestripperxn--8dbq2axn--8ltr62koryokamikawanehonbetsuwanouchijiwadeliveryxn--8pvr4uxn--8y0a063axn--90a1affinitylotterybnikeisenbahnxn--90a3academiamicable-modemoneyxn--90aeroportalabamagasakishimabaraffleentry-snowplowiczeladzxn--90aishobarakawaharaoxn--90amckinseyxn--90azhytomyrxn--9dbhblg6dietritonxn--9dbq2axn--9et52uxn--9krt00axn--andy-iraxn--aroport-byandexcloudxn--asky-iraxn--aurskog-hland-jnbarefootballooningjerstadgcapebretonamicrolightingjesdalombardiadembroideryonagunicloudiherokuappanamasteiermarkaracoldwarszawauthgearappspacehosted-by-previderxn--avery-yuasakuragawaxn--b-5gaxn--b4w605ferdxn--balsan-sdtirol-nsbsuzukanazawaxn--bck1b9a5dre4civilwarmiasadoesntexisteingeekarpaczest-a-la-maisondre-landrayddns5yxn--bdddj-mrabdxn--bearalvhki-y4axn--berlevg-jxaxn--bhcavuotna-s4axn--bhccavuotna-k7axn--bidr-5nachikatsuuraxn--bievt-0qa2xn--bjarky-fyaotsurgeryxn--bjddar-ptargithubpreviewsaitohmannore-og-uvdalxn--blt-elabourxn--bmlo-graingerxn--bod-2naturalsciencesnaturellesuzukis-an-actorxn--bozen-sdtirol-2obanazawaxn--brnny-wuacademy-firewall-gatewayxn--brnnysund-m8accident-investigation-acornxn--brum-voagatroandinosaureportrentoyonakagyokutoyakomaganexn--btsfjord-9zaxn--bulsan-sdtirol-nsbaremetalpha-myqnapcloud9guacuiababia-goracleaningitpagexlimoldell-ogliastraderxn--c1avgxn--c2br7gxn--c3s14mincomcastreserve-onlinexn--cck2b3bargainstances3-us-gov-west-1xn--cckwcxetdxn--cesena-forl-mcbremangerxn--cesenaforl-i8axn--cg4bkis-an-actresshwindmillxn--ciqpnxn--clchc0ea0b2g2a9gcdxn--comunicaes-v6a2oxn--correios-e-telecomunicaes-ghc29axn--czr694barreaudiblebesbydgoszczecinemagnethnologyoriikaragandauthordalandroiddnss3-ap-southeast-2ix4432-balsan-suedtirolimiteddnskinggfakefurniturecreationavuotnaritakoelnayorovigotsukisosakitahatakahatakaishimoichinosekigaharaurskog-holandingitlaborxn--czrs0trogstadxn--czru2dxn--czrw28barrel-of-knowledgeappgafanquanpachicappacificurussiautomotivelandds3-ca-central-16-balsan-sudtirollagdenesnaaseinet-freaks3-ap-southeast-123websiteleaf-south-123webseiteckidsmynasushiobarackmazerbaijan-mayen-rootaribeiraogakibichuobiramusementdllpages3-ap-south-123sitewebhareidfjordvagsoyerhcloudd-dnsiskinkyolasiteastcoastaldefenceastus2038xn--d1acj3barrell-of-knowledgecomputerhistoryofscience-fictionfabricafjs3-us-west-1xn--d1alfaromeoxn--d1atromsakegawaxn--d5qv7z876clanbibaidarmeniaxn--davvenjrga-y4axn--djrs72d6uyxn--djty4kosaigawaxn--dnna-grajewolterskluwerxn--drbak-wuaxn--dyry-iraxn--e1a4cldmailukowhitesnow-dnsangohtawaramotoineppubtlsanjotelulubin-brbambinagisobetsuitagajoburgjerdrumcprequalifymein-vigorgebetsukuibmdeveloperauniteroizumizakinderoyomitanobninskanzakiyokawaraustrheimatunduhrennebulsan-suedtirololitapunk123kotisivultrobjectselinogradimo-siemenscaledekaascolipiceno-ipifony-1337xn--eckvdtc9dxn--efvn9svalbardunloppaderbornxn--efvy88hagakhanamigawaxn--ehqz56nxn--elqq16hagebostadxn--eveni-0qa01gaxn--f6qx53axn--fct429kosakaerodromegallupaasdaburxn--fhbeiarnxn--finny-yuaxn--fiq228c5hsvchurchaseljeepsondriodejaneirockyotobetsuliguriaxn--fiq64barsycenterprisesakievennodesadistcgrouplidlugolekagaminord-frontierxn--fiqs8sveioxn--fiqz9svelvikoninjambylxn--fjord-lraxn--fjq720axn--fl-ziaxn--flor-jraxn--flw351exn--forl-cesena-fcbssvizzeraxn--forlcesena-c8axn--fpcrj9c3dxn--frde-grandrapidsvn-repostorjcloud-ver-jpchowderxn--frna-woaraisaijosoyroroswedenxn--frya-hraxn--fzc2c9e2cleverappsannanxn--fzys8d69uvgmailxn--g2xx48clicketcloudcontrolapparmatsuuraxn--gckr3f0fauskedsmokorsetagayaseralingenoamishirasatogliattipschulserverxn--gecrj9clickrisinglesannohekinannestadraydnsanokaruizawaxn--ggaviika-8ya47haibarakitakamiizumisanofidelitysfjordxn--gildeskl-g0axn--givuotna-8yasakaiminatoyookaneyamazoexn--gjvik-wuaxn--gk3at1exn--gls-elacaixaxn--gmq050is-an-anarchistoricalsocietysnesigdalxn--gmqw5axn--gnstigbestellen-zvbrplsbxn--45br5cylxn--gnstigliefern-wobihirosakikamijimatsushigexn--h-2failxn--h1aeghair-surveillancexn--h1ahnxn--h1alizxn--h2breg3eveneswidnicasacampinagrandebungotakadaemongolianxn--h2brj9c8clinichippubetsuikilatironporterxn--h3cuzk1digickoseis-a-linux-usershoujis-a-knightpointtohoboleslawieconomiastalbanshizuokamogawaxn--hbmer-xqaxn--hcesuolo-7ya35barsyonlinewhampshirealtychyattorneyagawakuyabukihokumakogeniwaizumiotsurugimbalsfjordeportexaskoyabeagleboardetroitskypecorivneatonoshoes3-eu-west-3utilitiesquare7xn--hebda8basicserversaillesjabbottateshinanomachildrensgardenhlfanhsbc66xn--hery-iraxn--hgebostad-g3axn--hkkinen-5waxn--hmmrfeasta-s4accident-prevention-aptibleangaviikadenaamesjevuemielnoboribetsuckswidnikkolobrzegersundxn--hnefoss-q1axn--hobl-iraxn--holtlen-hxaxn--hpmir-xqaxn--hxt814exn--hyanger-q1axn--hylandet-54axn--i1b6b1a6a2exn--imr513nxn--indery-fyasugithubusercontentromsojamisonxn--io0a7is-an-artistgstagexn--j1adpkomonotogawaxn--j1aefbsbxn--1lqs71dyndns-office-on-the-webhostingrpassagensavonarviikamiokameokamakurazakiwakunigamihamadaxn--j1ael8basilicataniautoscanadaeguambulancentralus-2xn--j1amhakatanorthflankddiamondshinshiroxn--j6w193gxn--jlq480n2rgxn--jlq61u9w7basketballfinanzgorzeleccodespotenzakopanewspaperxn--jlster-byasuokannamihokkaidopaaskvollxn--jrpeland-54axn--jvr189miniserversusakis-a-socialistg-builderxn--k7yn95exn--karmy-yuaxn--kbrq7oxn--kcrx77d1x4axn--kfjord-iuaxn--klbu-woaxn--klt787dxn--kltp7dxn--kltx9axn--klty5xn--45brj9cistrondheimperiaxn--koluokta-7ya57hakodatexn--kprw13dxn--kpry57dxn--kput3is-an-engineeringxn--krager-gyatominamibosogndalxn--kranghke-b0axn--krdsherad-m8axn--krehamn-dxaxn--krjohka-hwab49jdevcloudfunctionsimplesitexn--ksnes-uuaxn--kvfjord-nxaxn--kvitsy-fyatsukanoyakagexn--kvnangen-k0axn--l-1fairwindswiebodzin-dslattuminamiyamashirokawanabeepilepsykkylvenicexn--l1accentureklamborghinikolaeventswinoujscienceandhistoryxn--laheadju-7yatsushiroxn--langevg-jxaxn--lcvr32dxn--ldingen-q1axn--leagaviika-52batochigifts3-us-west-2xn--lesund-huaxn--lgbbat1ad8jdfaststackschulplattformetacentrumeteorappassenger-associationxn--lgrd-poacctrusteexn--lhppi-xqaxn--linds-pramericanartrvestnestudioxn--lns-qlavagiskexn--loabt-0qaxn--lrdal-sraxn--lrenskog-54axn--lt-liacliniquedapliexn--lten-granexn--lury-iraxn--m3ch0j3axn--mely-iraxn--merker-kuaxn--mgb2ddeswisstpetersburgxn--mgb9awbfbx-ostrowwlkpmguitarschwarzgwangjuifminamidaitomanchesterxn--mgba3a3ejtrycloudflarevistaplestudynamic-dnsrvaroyxn--mgba3a4f16axn--mgba3a4fra1-deloittevaksdalxn--mgba7c0bbn0axn--mgbaakc7dvfstdlibestadxn--mgbaam7a8hakonexn--mgbab2bdxn--mgbah1a3hjkrdxn--mgbai9a5eva00batsfjordiscordsays3-website-ap-northeast-1xn--mgbai9azgqp6jejuniperxn--mgbayh7gpalmaseratis-an-entertainerxn--mgbbh1a71exn--mgbc0a9azcgxn--mgbca7dzdoxn--mgbcpq6gpa1axn--mgberp4a5d4a87gxn--mgberp4a5d4arxn--mgbgu82axn--mgbi4ecexposedxn--mgbpl2fhskosherbrookegawaxn--mgbqly7c0a67fbclintonkotsukubankarumaifarmsteadrobaknoluoktachikawakayamadridvallee-aosteroyxn--mgbqly7cvafr-1xn--mgbt3dhdxn--mgbtf8flapymntrysiljanxn--mgbtx2bauhauspostman-echocolatemasekd1xn--mgbx4cd0abbvieeexn--mix082fbxoschweizxn--mix891fedorainfraclouderaxn--mjndalen-64axn--mk0axin-vpnclothingdustdatadetectjmaxxxn--12c1fe0bradescotlandrrxn--mk1bu44cn-northwest-1xn--mkru45is-bykleclerchoshibuyachiyodancexn--mlatvuopmi-s4axn--mli-tlavangenxn--mlselv-iuaxn--moreke-juaxn--mori-qsakurais-certifiedxn--mosjen-eyawaraxn--mot-tlazioxn--mre-og-romsdal-qqbuseranishiaritakurashikis-foundationxn--msy-ula0hakubaghdadultravelchannelxn--mtta-vrjjat-k7aflakstadaokagakicks-assnasaarlandxn--muost-0qaxn--mxtq1minisitexn--ngbc5azdxn--ngbe9e0axn--ngbrxn--45q11citadelhicampinashikiminohostfoldnavyxn--nit225koshimizumakiyosunnydayxn--nmesjevuemie-tcbalestrandabergamoarekeymachineustarnbergxn--nnx388axn--nodessakyotanabelaudiopsysynology-dstreamlitappittsburghofficialxn--nqv7fs00emaxn--nry-yla5gxn--ntso0iqx3axn--ntsq17gxn--nttery-byaeserveftplanetariuminamitanexn--nvuotna-hwaxn--nyqy26axn--o1achernihivgubsxn--o3cw4hakuis-a-democratravelersinsurancexn--o3cyx2axn--od0algxn--od0aq3belementorayoshiokanumazuryukuhashimojibxos3-website-ap-southeast-1xn--ogbpf8flatangerxn--oppegrd-ixaxn--ostery-fyawatahamaxn--osyro-wuaxn--otu796dxn--p1acfedorapeoplegoismailillehammerfeste-ipatriaxn--p1ais-gonexn--pgbs0dhlx3xn--porsgu-sta26fedoraprojectoyotsukaidoxn--pssu33lxn--pssy2uxn--q7ce6axn--q9jyb4cngreaterxn--qcka1pmcpenzaporizhzhiaxn--qqqt11minnesotaketakayamassivegridxn--qxa6axn--qxamsterdamnserverbaniaxn--rady-iraxn--rdal-poaxn--rde-ulaxn--rdy-0nabaris-into-animeetrentin-sued-tirolxn--rennesy-v1axn--rhkkervju-01afeiraquarelleasingujaratoyouraxn--rholt-mragowoltlab-democraciaxn--rhqv96gxn--rht27zxn--rht3dxn--rht61exn--risa-5naturbruksgymnxn--risr-iraxn--rland-uuaxn--rlingen-mxaxn--rmskog-byaxn--rny31hakusanagochihayaakasakawaiishopitsitexn--rovu88bellevuelosangeles3-website-ap-southeast-2xn--rros-granvindafjordxn--rskog-uuaxn--rst-0naturhistorischesxn--rsta-framercanvasxn--rvc1e0am3exn--ryken-vuaxn--ryrvik-byaxn--s-1faithaldenxn--s9brj9cnpyatigorskolecznagatorodoyxn--sandnessjen-ogbellunord-odalombardyn53xn--sandy-yuaxn--sdtirol-n2axn--seral-lraxn--ses554gxn--sgne-graphoxn--4dbgdty6citichernovtsyncloudrangedaluccarbonia-iglesias-carboniaiglesiascarboniaxn--skierv-utazasxn--skjervy-v1axn--skjk-soaxn--sknit-yqaxn--sknland-fxaxn--slat-5natuurwetenschappenginexn--slt-elabcieszynh-servebeero-stageiseiroumuenchencoreapigeelvinckoshunantankmpspawnextdirectrentino-s-tirolxn--smla-hraxn--smna-gratangentlentapisa-geekosugexn--snase-nraxn--sndre-land-0cbeneventochiokinoshimaintenancebinordreisa-hockeynutazurestaticappspaceusercontentateyamaveroykenglandeltaitogitsumitakagiizeasypanelblagrarchaeologyeongbuk0emmafann-arboretumbriamallamaceiobbcg123homepagefrontappchizip61123minsidaarborteaches-yogasawaracingroks-theatree123hjemmesidealerimo-i-rana4u2-localhistorybolzano-altoadigeometre-experts-comptables3-ap-northeast-123miwebcambridgehirn4t3l3p0rtarumizusawabogadobeaemcloud-fr123paginaweberkeleyokosukanrabruzzombieidskoguchikushinonsenasakuchinotsuchiurakawafaicloudineat-url-o-g-i-naval-d-aosta-valleyokote164-b-datacentermezproxyzgoraetnabudejjudaicadaquest-mon-blogueurodirumaceratabuseating-organicbcn-north-123saitamakawabartheshopencraftrainingdyniajuedischesapeakebayernavigationavoi234lima-cityeats3-ap-northeast-20001wwwedeployokozeastasiamunemurorangecloudplatform0xn--snes-poaxn--snsa-roaxn--sr-aurdal-l8axn--sr-fron-q1axn--sr-odal-q1axn--sr-varanger-ggbentleyurihonjournalistjohnikonanporovnobserverxn--srfold-byaxn--srreisa-q1axn--srum-gratis-a-bulls-fanxn--stfold-9xaxn--stjrdal-s1axn--stjrdalshalsen-sqbeppublishproxyusuharavocatanzarowegroweiboltashkentatamotorsitestingivingjemnes3-eu-central-1kappleadpages-12hpalmspringsakerxn--stre-toten-zcbeskidyn-ip24xn--t60b56axn--tckweddingxn--tiq49xqyjelasticbeanstalkhmelnitskiyamarumorimachidaxn--tjme-hraxn--tn0agrocerydxn--tnsberg-q1axn--tor131oxn--trany-yuaxn--trentin-sd-tirol-rzbestbuyshoparenagareyamaizurugbyenvironmentalconservationflashdrivefsnillfjordiscordsezjampaleoceanographics3-website-eu-west-1xn--trentin-sdtirol-7vbetainaboxfuseekloges3-website-sa-east-1xn--trentino-sd-tirol-c3bhzcasertainaioirasebastopologyeongnamegawafflecellclstagemologicaliforniavoues3-eu-west-1xn--trentino-sdtirol-szbielawalbrzycharitypedreamhostersvp4xn--trentinosd-tirol-rzbiellaakesvuemieleccebizenakanotoddeninoheguriitatebayashiibahcavuotnagaivuotnagaokakyotambabybluebitelevisioncilla-speziaxarnetbank8s3-eu-west-2xn--trentinosdtirol-7vbieszczadygeyachimataijiiyamanouchikuhokuryugasakitaurayasudaxn--trentinsd-tirol-6vbievat-band-campaignieznombrendlyngengerdalces3-website-us-east-1xn--trentinsdtirol-nsbifukagawalesundiscountypeformelhusgardeninomiyakonojorpelandiscourses3-website-us-west-1xn--trgstad-r1axn--trna-woaxn--troms-zuaxn--tysvr-vraxn--uc0atvestre-slidrexn--uc0ay4axn--uist22halsakakinokiaxn--uisz3gxn--unjrga-rtarnobrzegyptianxn--unup4yxn--uuwu58axn--vads-jraxn--valle-aoste-ebbtularvikonskowolayangroupiemontexn--valle-d-aoste-ehboehringerikexn--valleaoste-e7axn--valledaoste-ebbvadsoccerxn--vard-jraxn--vegrshei-c0axn--vermgensberater-ctb-hostingxn--vermgensberatung-pwbigvalledaostaobaomoriguchiharag-cloud-championshiphoplixboxenirasakincheonishiazaindependent-commissionishigouvicasinordeste-idclkarasjohkamikitayamatsurindependent-inquest-a-la-masionishiharaxn--vestvgy-ixa6oxn--vg-yiabkhaziaxn--vgan-qoaxn--vgsy-qoa0jelenia-goraxn--vgu402cnsantabarbaraxn--vhquvestre-totennishiawakuraxn--vler-qoaxn--vre-eiker-k8axn--vrggt-xqadxn--vry-yla5gxn--vuq861biharstadotsubetsugaruhrxn--w4r85el8fhu5dnraxn--w4rs40lxn--wcvs22dxn--wgbh1cntjomeldaluroyxn--wgbl6axn--xhq521bihorologyusuisservegame-serverxn--xkc2al3hye2axn--xkc2dl3a5ee0hammarfeastafricaravantaaxn--y9a3aquariumintereitrentino-sudtirolxn--yer-znaumburgxn--yfro4i67oxn--ygarden-p1axn--ygbi2ammxn--4dbrk0cexn--ystre-slidre-ujbikedaejeonbukarasjokarasuyamarriottatsunoceanographiquehimejindependent-inquiryuufcfanishiizunazukindependent-panelomoliseminemrxn--zbx025dxn--zf0ao64axn--zf0avxlxn--zfr164bilbaogashimadachicagoboavistanbulsan-sudtirolbia-tempio-olbiatempioolbialystokkeliwebredirectme-south-1xnbayxz \ No newline at end of file +birkenesoddtangentinglogoweirbitbucketrzynishikatakayamatta-varjjatjomembersaltdalovepopartysfjordiskussionsbereichatinhlfanishikatsuragitappassenger-associationishikawazukamiokameokamakurazakitaurayasudabitternidisrechtrainingloomy-routerbjarkoybjerkreimdbalsan-suedtirololitapunkapsienamsskoganeibmdeveloperauniteroirmemorialombardiadempresashibetsukumiyamagasakinderoyonagunicloudevelopmentaxiijimarriottayninhaccanthobby-siteval-d-aosta-valleyoriikaracolognebinatsukigataiwanumatajimidsundgcahcesuolocustomer-ocimperiautoscanalytics-gatewayonagoyaveroykenflfanpachihayaakasakawaiishopitsitemasekd1kappenginedre-eikerimo-siemenscaledekaascolipicenoboribetsucks3-eu-west-3utilities-16-balestrandabergentappsseekloges3-eu-west-123paginawebcamauction-acornfshostrodawaraktyubinskaunicommbank123kotisivultrobjectselinogradimo-i-rana4u2-localhostrolekanieruchomoscientistordal-o-g-i-nikolaevents3-ap-northeast-2-ddnsking123homepagefrontappchizip61123saitamakawababia-goracleaningheannakadomarineat-urlimanowarudakuneustarostwodzislawdev-myqnapcloudcontrolledgesuite-stagingdyniamusementdllclstagehirnikonantomobelementorayokosukanoyakumoliserniaurland-4-salernord-aurdalipaywhirlimiteddnslivelanddnss3-ap-south-123siteweberlevagangaviikanonji234lima-cityeats3-ap-southeast-123webseiteambulancechireadmyblogspotaribeiraogakicks-assurfakefurniturealmpmninoheguribigawaurskog-holandinggfarsundds3-ap-southeast-20001wwwedeployokote123hjemmesidealerdalaheadjuegoshikibichuobiraustevollimombetsupplyokoze164-balena-devices3-ca-central-123websiteleaf-south-12hparliamentatsunobninsk8s3-eu-central-1337bjugnishimerablackfridaynightjxn--11b4c3ditchyouripatriabloombergretaijindustriesteinkjerbloxcmsaludivtasvuodnakaiwanairlinekobayashimodatecnologiablushakotanishinomiyashironomniwebview-assetsalvadorbmoattachmentsamegawabmsamnangerbmwellbeingzonebnrweatherchannelsdvrdnsamparalleluxenishinoomotegotsukishiwadavvenjargamvikarpaczest-a-la-maisondre-landivttasvuotnakamai-stagingloppennebomlocalzonebonavstackartuzybondigitaloceanspacesamsclubartowest1-usamsunglugsmall-webspacebookonlineboomlaakesvuemielecceboschristmasakilatiron-riopretoeidsvollovesickaruizawabostik-serverrankoshigayachtsandvikcoromantovalle-d-aostakinouebostonakijinsekikogentlentapisa-geekarumaifmemsetkmaxxn--12c1fe0bradescotksatmpaviancapitalonebouncemerckmsdscloudiybounty-fullensakerrypropertiesangovtoyosatoyokawaboutiquebecologialaichaugiangmbhartiengiangminakamichiharaboutireservdrangedalpusercontentoyotapfizerboyfriendoftheinternetflixn--12cfi8ixb8lublindesnesanjosoyrovnoticiasannanishinoshimattelemarkasaokamikitayamatsurinfinitigopocznore-og-uvdalucaniabozen-sudtiroluccanva-appstmnishiokoppegardray-dnsupdaterbozen-suedtirolukowesteuropencraftoyotomiyazakinsurealtypeformesswithdnsannohekinanporovigonohejinternationaluroybplacedogawarabikomaezakirunordkappgfoggiabrandrayddns5ybrasiliadboxoslockerbresciaogashimadachicappadovaapstemp-dnswatchest-mon-blogueurodirumagazinebrindisiciliabroadwaybroke-itvedestrandraydnsanokashibatakashimashikiyosatokigawabrokerbrothermesserlifestylebtimnetzpisdnpharmaciensantamariakebrowsersafetymarketingmodumetacentrumeteorappharmacymruovatlassian-dev-builderschaefflerbrumunddalutskashiharabrusselsantoandreclaimsanukintlon-2bryanskiptveterinaireadthedocsaobernardovre-eikerbrynebwestus2bzhitomirbzzwhitesnowflakecommunity-prochowicecomodalenissandoycompanyaarphdfcbankasumigaurawa-mazowszexn--1ck2e1bambinagisobetsuldalpha-myqnapcloudaccess3-us-east-2ixboxeroxfinityolasiteastus2comparemarkerryhotelsaves-the-whalessandria-trani-barletta-andriatranibarlettaandriacomsecaasnesoddeno-stagingrondarcondoshifteditorxn--1ctwolominamatarnobrzegrongrossetouchijiwadedyn-berlincolnissayokoshibahikariyaltakazakinzais-a-bookkeepermarshallstatebankasuyalibabahccavuotnagaraholtaleniwaizumiotsurugashimaintenanceomutazasavonarviikaminoyamaxunispaceconferenceconstructionflashdrivefsncf-ipfsaxoconsuladobeio-static-accesscamdvrcampaniaconsultantranoyconsultingroundhandlingroznysaitohnoshookuwanakayamangyshlakdnepropetrovskanlandyndns-freeboxostrowwlkpmgrphilipsyno-dschokokekscholarshipschoolbusinessebycontactivetrailcontagematsubaravendbambleborkdalvdalcest-le-patron-rancherkasydneyukuhashimokawavoues3-sa-east-1contractorskenissedalcookingruecoolblogdnsfor-better-thanhhoarairforcentralus-1cooperativano-frankivskodjeephonefosschoolsztynsetransiphotographysiocoproductionschulplattforminamiechizenisshingucciprianiigatairaumalatvuopmicrolightinguidefinimaringatlancastercorsicafjschulservercosenzakopanecosidnshome-webservercellikescandypopensocialcouchpotatofrieschwarzgwangjuh-ohtawaramotoineppueblockbusternopilawacouncilcouponscrapper-sitecozoravennaharimalborkaszubytemarketscrappinguitarscrysecretrosnubananarepublic-inquiryurihonjoyenthickaragandaxarnetbankanzakiwielunnerepairbusanagochigasakishimabarakawaharaolbia-tempio-olbiatempioolbialowiezachpomorskiengiangjesdalolipopmcdirepbodyn53cqcxn--1lqs03niyodogawacrankyotobetsumidaknongujaratmallcrdyndns-homednscwhminamifuranocreditcardyndns-iphutholdingservehttpbincheonl-ams-1creditunionionjukujitawaravpagecremonashorokanaiecrewhoswholidaycricketnedalcrimeast-kazakhstanangercrotonecrowniphuyencrsvp4cruiseservehumourcuisinellair-traffic-controllagdenesnaaseinet-freakserveircasertainaircraftingvolloansnasaarlanduponthewifidelitypedreamhostersaotomeldaluxurycuneocupcakecuritibacgiangiangryggeecurvalled-aostargets-itranslatedyndns-mailcutegirlfriendyndns-office-on-the-webhoptogurafedoraprojectransurlfeirafembetsukuis-a-bruinsfanfermodenakasatsunairportrapaniizaferraraferraris-a-bulls-fanferrerotikagoshimalopolskanittedalfetsundyndns-wikimobetsumitakagildeskaliszkolamericanfamilydservemp3fgunmaniwamannorth-kazakhstanfhvalerfilegear-augustowiiheyakagefilegear-deatnuniversitysvardofilegear-gbizfilegear-iefilegear-jpmorgangwonporterfilegear-sg-1filminamiizukamiminefinalchikugokasellfyis-a-candidatefinancefinnoyfirebaseappiemontefirenetlifylkesbiblackbaudcdn-edgestackhero-networkinggroupowiathletajimabaria-vungtaudiopsysharpigboatshawilliamhillfirenzefirestonefireweblikes-piedmontravelersinsurancefirmdalegalleryfishingoldpoint2thisamitsukefitjarfitnessettsurugiminamimakis-a-catererfjalerfkatsushikabeebyteappilottonsberguovdageaidnunjargausdalflekkefjordyndns-workservep2phxn--1lqs71dyndns-remotewdyndns-picserveminecraftransporteflesbergushikamifuranorthflankatsuyamashikokuchuoflickragerokunohealthcareershellflierneflirfloginlinefloppythonanywherealtorfloraflorencefloripalmasfjordenfloristanohatajiris-a-celticsfanfloromskogxn--2m4a15eflowershimokitayamafltravinhlonganflynnhosting-clusterfncashgabadaddjabbottoyourafndyndns1fnwkzfolldalfoolfor-ourfor-somegurownproviderfor-theaterfordebianforexrotheworkpccwinbar0emmafann-arborlandd-dnsiskinkyowariasahikawarszawashtenawsmppl-wawsglobalacceleratorahimeshimakanegasakievennodebalancern4t3l3p0rtatarantours3-ap-northeast-123minsidaarborteaches-yogano-ipifony-123miwebaccelastx4432-b-datacenterprisesakijobservableusercontentateshinanomachintaifun-dnsdojournalistoloseyouriparisor-fronavuotnarashinoharaetnabudejjunipereggio-emilia-romagnaroyboltateyamajureggiocalabriakrehamnayoro0o0forgotdnshimonitayanagithubpreviewsaikisarazure-mobileirfjordynnservepicservequakeforli-cesena-forlicesenaforlillehammerfeste-ipimientaketomisatoolshimonosekikawaforsalegoismailillesandefjordynservebbservesarcasmileforsandasuolodingenfortalfortefosneshimosuwalkis-a-chefashionstorebaseljordyndns-serverisignfotrdynulvikatowicefoxn--2scrj9casinordlandurbanamexnetgamersapporomurafozfr-1fr-par-1fr-par-2franamizuhoboleslawiecommerce-shoppingyeongnamdinhachijohanamakisofukushimaoris-a-conservativegarsheiheijis-a-cparachutingfredrikstadynv6freedesktopazimuthaibinhphuocelotenkawakayamagnetcieszynh-servebeero-stageiseiroumugifuchungbukharag-cloud-championshiphoplixn--30rr7yfreemyiphosteurovisionredumbrellangevagrigentobishimadridvagsoygardenebakkeshibechambagricoharugbydgoszczecin-berlindasdaburfreesitefreetlshimotsukefreisennankokubunjis-a-cubicle-slavellinodeobjectshimotsumafrenchkisshikindleikangerfreseniushinichinanfriuli-v-giuliafriuli-ve-giuliafriuli-vegiuliafriuli-venezia-giuliafriuli-veneziagiuliafriuli-vgiuliafriuliv-giuliafriulive-giuliafriulivegiuliafriulivenezia-giuliafriuliveneziagiuliafriulivgiuliafrlfroganshinjotelulubin-vpncateringebunkyonanaoshimamateramockashiwarafrognfrolandynvpnpluservicesevastopolitiendafrom-akamaized-stagingfrom-alfrom-arfrom-azurewebsiteshikagamiishibuyabukihokuizumobaragusabaerobaticketshinjukuleuvenicefrom-campobassociatest-iserveblogsytenrissadistdlibestadultrentin-sudtirolfrom-coachaseljeducationcillahppiacenzaganfrom-ctrentin-sued-tirolfrom-dcatfooddagestangefrom-decagliarikuzentakataikillfrom-flapymntrentin-suedtirolfrom-gap-east-1from-higashiagatsumagoianiafrom-iafrom-idyroyrvikingulenfrom-ilfrom-in-the-bandairtelebitbridgestonemurorangecloudplatform0from-kshinkamigototalfrom-kyfrom-langsonyantakahamalselveruminamiminowafrom-malvikaufentigerfrom-mdfrom-mein-vigorlicefrom-mifunefrom-mnfrom-modshinshinotsurgeryfrom-mshinshirofrom-mtnfrom-ncatholicurus-4from-ndfrom-nefrom-nhs-heilbronnoysundfrom-njshintokushimafrom-nminamioguni5from-nvalledaostargithubusercontentrentino-a-adigefrom-nycaxiaskvollpagesardegnarutolgaulardalvivanovoldafrom-ohdancefrom-okegawassamukawataris-a-democratrentino-aadigefrom-orfrom-panasonichernovtsykkylvenneslaskerrylogisticsardiniafrom-pratohmamurogawatsonrenderfrom-ris-a-designerimarugame-hostyhostingfrom-schmidtre-gauldalfrom-sdfrom-tnfrom-txn--32vp30hachinoheavyfrom-utsiracusagaeroclubmedecin-addrammenuorodoyerfrom-val-daostavalleyfrom-vtrentino-alto-adigefrom-wafrom-wiardwebthingsjcbnpparibashkiriafrom-wvallee-aosteroyfrom-wyfrosinonefrostabackplaneapplebesbyengerdalp1froyal-commissionfruskydivingfujiiderafujikawaguchikonefujiminokamoenairtrafficplexus-2fujinomiyadapliefujiokazakinkobearalvahkikonaibetsubame-south-1fujisatoshoeshintomikasaharafujisawafujishiroishidakabiratoridediboxn--3bst00minamisanrikubetsupportrentino-altoadigefujitsuruokakamigaharafujiyoshidappnodearthainguyenfukayabeardubaikawagoefukuchiyamadatsunanjoburgfukudomigawafukuis-a-doctorfukumitsubishigakirkeneshinyoshitomiokamisatokamachippubetsuikitchenfukuokakegawafukuroishikariwakunigamigrationfukusakirovogradoyfukuyamagatakaharunusualpersonfunabashiriuchinadattorelayfunagatakahashimamakiryuohkurafunahashikamiamakusatsumasendaisenergyeongginowaniihamatamakinoharafundfunkfeuerfuoiskujukuriyamandalfuosskoczowindowskrakowinefurubirafurudonordreisa-hockeynutwentertainmentrentino-s-tirolfurukawajimangolffanshiojirishirifujiedafusoctrangfussagamiharafutabayamaguchinomihachimanagementrentino-stirolfutboldlygoingnowhere-for-more-og-romsdalfuttsurutashinais-a-financialadvisor-aurdalfuturecmshioyamelhushirahamatonbetsurnadalfuturehostingfuturemailingfvghakuis-a-gurunzenhakusandnessjoenhaldenhalfmoonscalebookinghostedpictetrentino-sud-tirolhalsakakinokiaham-radio-opinbar1hamburghammarfeastasiahamurakamigoris-a-hard-workershiraokamisunagawahanamigawahanawahandavvesiidanangodaddyn-o-saurealestatefarmerseinehandcrafteducatorprojectrentino-sudtirolhangglidinghangoutrentino-sued-tirolhannannestadhannosegawahanoipinkazohanyuzenhappouzshiratakahagianghasamap-northeast-3hasaminami-alpshishikuis-a-hunterhashbanghasudazaifudaigodogadobeioruntimedio-campidano-mediocampidanomediohasura-appinokokamikoaniikappudopaashisogndalhasvikazteleportrentino-suedtirolhatogayahoooshikamagayaitakamoriokakudamatsuehatoyamazakitahiroshimarcheapartmentshisuifuettertdasnetzhatsukaichikaiseiyoichipshitaramahattfjelldalhayashimamotobusells-for-lesshizukuishimoichilloutsystemscloudsitehazuminobushibukawahelplfinancialhelsinkitakamiizumisanofidonnakamurataitogliattinnhemneshizuokamitondabayashiogamagoriziahemsedalhepforgeblockshoujis-a-knightpointtokaizukamaishikshacknetrentinoa-adigehetemlbfanhigashichichibuzentsujiiehigashihiroshimanehigashiizumozakitakatakanabeautychyattorneyagawakkanaioirasebastopoleangaviikadenagahamaroyhigashikagawahigashikagurasoedahigashikawakitaaikitakyushunantankazunovecorebungoonow-dnshowahigashikurumeinforumzhigashimatsushimarnardalhigashimatsuyamakitaakitadaitoigawahigashimurayamamotorcycleshowtimeloyhigashinarusells-for-uhigashinehigashiomitamanoshiroomghigashiosakasayamanakakogawahigashishirakawamatakanezawahigashisumiyoshikawaminamiaikitamihamadahigashitsunospamproxyhigashiurausukitamotosunnydayhigashiyamatokoriyamanashiibaclieu-1higashiyodogawahigashiyoshinogaris-a-landscaperspectakasakitanakagusukumoldeliveryhippyhiraizumisatohokkaidontexistmein-iservschulecznakaniikawatanagurahirakatashinagawahiranais-a-lawyerhirarahiratsukaeruhirayaizuwakamatsubushikusakadogawahitachiomiyaginozawaonsensiositehitachiotaketakaokalmykiahitraeumtgeradegreehjartdalhjelmelandholyhomegoodshwinnersiiitesilkddiamondsimple-urlhomeipioneerhomelinkyard-cloudjiffyresdalhomelinuxn--3ds443ghomeofficehomesecuritymacaparecidahomesecuritypchiryukyuragiizehomesenseeringhomeskleppippugliahomeunixn--3e0b707ehondahonjyoitakarazukaluganskfh-muensterhornindalhorsells-itrentinoaadigehortendofinternet-dnsimplesitehospitalhotelwithflightsirdalhotmailhoyangerhoylandetakasagooglecodespotrentinoalto-adigehungyenhurdalhurumajis-a-liberalhyllestadhyogoris-a-libertarianhyugawarahyundaiwafuneis-very-evillasalleitungsenis-very-goodyearis-very-niceis-very-sweetpepperugiais-with-thebandoomdnstraceisk01isk02jenv-arubacninhbinhdinhktistoryjeonnamegawajetztrentinostiroljevnakerjewelryjgorajlljls-sto1jls-sto2jls-sto3jmpixolinodeusercontentrentinosud-tiroljnjcloud-ver-jpchitosetogitsuliguriajoyokaichibahcavuotnagaivuotnagaokakyotambabymilk3jozis-a-musicianjpnjprsolarvikhersonlanxessolundbeckhmelnitskiyamasoykosaigawakosakaerodromegalloabatobamaceratachikawafaicloudineencoreapigeekoseis-a-painterhostsolutionslupskhakassiakosheroykoshimizumakis-a-patsfankoshughesomakosugekotohiradomainstitutekotourakouhokumakogenkounosupersalevangerkouyamasudakouzushimatrixn--3pxu8khplaystation-cloudyclusterkozagawakozakis-a-personaltrainerkozowiosomnarviklabudhabikinokawachinaganoharamcocottekpnkppspbarcelonagawakepnord-odalwaysdatabaseballangenkainanaejrietisalatinabenogiehtavuoatnaamesjevuemielnombrendlyngen-rootaruibxos3-us-gov-west-1krasnikahokutokonamegatakatoris-a-photographerokussldkrasnodarkredstonekrelliankristiansandcatsoowitdkmpspawnextdirectrentinosudtirolkristiansundkrodsheradkrokstadelvaldaostavangerkropyvnytskyis-a-playershiftcryptonomichinomiyakekryminamiyamashirokawanabelaudnedalnkumamotoyamatsumaebashimofusakatakatsukis-a-republicanonoichinosekigaharakumanowtvaokumatorinokumejimatsumotofukekumenanyokkaichirurgiens-dentistes-en-francekundenkunisakis-a-rockstarachowicekunitachiaraisaijolsterkunitomigusukukis-a-socialistgstagekunneppubtlsopotrentinosued-tirolkuokgroupizzakurgankurobegetmyipirangalluplidlugolekagaminorddalkurogimimozaokinawashirosatochiokinoshimagentositempurlkuroisodegaurakuromatsunais-a-soxfankuronkurotakikawasakis-a-studentalkushirogawakustanais-a-teacherkassyncloudkusuppliesor-odalkutchanelkutnokuzumakis-a-techietipslzkvafjordkvalsundkvamsterdamnserverbaniakvanangenkvinesdalkvinnheradkviteseidatingkvitsoykwpspdnsor-varangermishimatsusakahogirlymisugitokorozawamitakeharamitourismartlabelingmitoyoakemiuramiyazurecontainerdpoliticaobangmiyotamatsukuris-an-actormjondalenmonzabrianzaramonzaebrianzamonzaedellabrianzamordoviamorenapolicemoriyamatsuuramoriyoshiminamiashigaramormonstermoroyamatsuzakis-an-actressmushcdn77-sslingmortgagemoscowithgoogleapiszmoseushimogosenmosjoenmoskenesorreisahayakawakamiichikawamisatottoris-an-anarchistjordalshalsenmossortlandmosviknx-serversusakiyosupabaseminemotegit-reposoruminanomoviemovimientokyotangotembaixadattowebhareidsbergmozilla-iotrentinosuedtirolmtranbytomaridagawalmartrentinsud-tirolmuikaminokawanishiaizubangemukoelnmunakatanemuosattemupkomatsushimassa-carrara-massacarraramassabuzzmurmanskomforbar2murotorcraftranakatombetsumy-gatewaymusashinodesakegawamuseumincomcastoripressorfoldmusicapetownnews-stagingmutsuzawamy-vigormy-wanggoupilemyactivedirectorymyamazeplaymyasustor-elvdalmycdmycloudnsoundcastorjdevcloudfunctionsokndalmydattolocalcertificationmyddnsgeekgalaxymydissentrentinsudtirolmydobissmarterthanyoumydrobofageometre-experts-comptablesowamydspectruminisitemyeffectrentinsued-tirolmyfastly-edgekey-stagingmyfirewalledreplittlestargardmyforuminterecifedextraspace-to-rentalstomakomaibaramyfritzmyftpaccesspeedpartnermyhome-servermyjinomykolaivencloud66mymailermymediapchoseikarugalsacemyokohamamatsudamypeplatformsharis-an-artistockholmestrandmypetsphinxn--41amyphotoshibajddarvodkafjordvaporcloudmypictureshinomypsxn--42c2d9amysecuritycamerakermyshopblockspjelkavikommunalforbundmyshopifymyspreadshopselectrentinsuedtirolmytabitordermythic-beastspydebergmytis-a-anarchistg-buildermytuleap-partnersquaresindevicenzamyvnchoshichikashukudoyamakeuppermywirecipescaracallypoivronpokerpokrovskommunepolkowicepoltavalle-aostavernpomorzeszowithyoutuberspacekitagawaponpesaro-urbino-pesarourbinopesaromasvuotnaritakurashikis-bykleclerchitachinakagawaltervistaipeigersundynamic-dnsarlpordenonepornporsangerporsangugeporsgrunnanpoznanpraxihuanprdprgmrprimetelprincipeprivatelinkomonowruzhgorodeoprivatizehealthinsuranceprofesionalprogressivegasrlpromonza-e-della-brianzaptokuyamatsushigepropertysnesrvarggatrevisogneprotectionprotonetroandindependent-inquest-a-la-masionprudentialpruszkowiwatsukiyonotaireserve-onlineprvcyonabarumbriaprzeworskogpunyufuelpupulawypussycatanzarowixsitepvhachirogatakahatakaishimojis-a-geekautokeinotteroypvtrogstadpwchowderpzqhadanorthwesternmutualqldqotoyohashimotoshimaqponiatowadaqslgbtroitskomorotsukagawaqualifioapplatter-applatterplcube-serverquangngais-certifiedugit-pagespeedmobilizeroticaltanissettailscaleforcequangninhthuanquangtritonoshonais-foundationquickconnectromsakuragawaquicksytestreamlitapplumbingouvaresearchitectesrhtrentoyonakagyokutoyakomakizunokunimimatakasugais-an-engineeringquipelementstrippertuscanytushungrytuvalle-daostamayukis-into-animeiwamizawatuxfamilytuyenquangbinhthuantwmailvestnesuzukis-gonevestre-slidreggio-calabriavestre-totennishiawakuravestvagoyvevelstadvibo-valentiaavibovalentiavideovinhphuchromedicinagatorogerssarufutsunomiyawakasaikaitakokonoevinnicarbonia-iglesias-carboniaiglesiascarboniavinnytsiavipsinaapplurinacionalvirginanmokurennebuvirtual-userveexchangevirtualservervirtualuserveftpodhalevisakurais-into-carsnoasakuholeckodairaviterboliviajessheimmobilienvivianvivoryvixn--45br5cylvlaanderennesoyvladikavkazimierz-dolnyvladimirvlogintoyonezawavmintsorocabalashovhachiojiyahikobierzycevologdanskoninjambylvolvolkswagencyouvolyngdalvoorlopervossevangenvotevotingvotoyonovps-hostrowiechungnamdalseidfjordynathomebuiltwithdarkhangelskypecorittogojomeetoystre-slidrettozawawmemergencyahabackdropalermochizukikirarahkkeravjuwmflabsvalbardunloppadualstackomvuxn--3hcrj9chonanbuskerudynamisches-dnsarpsborgripeeweeklylotterywoodsidellogliastradingworse-thanhphohochiminhadselbuyshouseshirakolobrzegersundongthapmircloudletshiranukamishihorowowloclawekonskowolawawpdevcloudwpenginepoweredwphostedmailwpmucdnipropetrovskygearappodlasiellaknoluoktagajobojis-an-entertainerwpmudevcdnaccessojamparaglidingwritesthisblogoipodzonewroclawmcloudwsseoullensvanguardianwtcp4wtfastlylbanzaicloudappspotagereporthruherecreationinomiyakonojorpelandigickarasjohkameyamatotakadawuozuerichardlillywzmiuwajimaxn--4it797konsulatrobeepsondriobranconagareyamaizuruhrxn--4pvxs4allxn--54b7fta0ccistrondheimpertrixcdn77-secureadymadealstahaugesunderxn--55qw42gxn--55qx5dxn--5dbhl8dxn--5js045dxn--5rtp49citadelhichisochimkentozsdell-ogliastraderxn--5rtq34kontuminamiuonumatsunoxn--5su34j936bgsgxn--5tzm5gxn--6btw5axn--6frz82gxn--6orx2rxn--6qq986b3xlxn--7t0a264citicarrdrobakamaiorigin-stagingmxn--12co0c3b4evalleaostaobaomoriguchiharaffleentrycloudflare-ipfstcgroupaaskimitsubatamibulsan-suedtirolkuszczytnoopscbgrimstadrrxn--80aaa0cvacationsvchoyodobashichinohealth-carereforminamidaitomanaustdalxn--80adxhksveioxn--80ao21axn--80aqecdr1axn--80asehdbarclaycards3-us-west-1xn--80aswgxn--80aukraanghkeliwebpaaskoyabeagleboardxn--8dbq2axn--8ltr62konyvelohmusashimurayamassivegridxn--8pvr4uxn--8y0a063axn--90a1affinitylotterybnikeisencowayxn--90a3academiamicable-modemoneyxn--90aeroportsinfolionetworkangerxn--90aishobaraxn--90amckinseyxn--90azhytomyrxn--9dbq2axn--9et52uxn--9krt00axn--andy-iraxn--aroport-byanagawaxn--asky-iraxn--aurskog-hland-jnbarclays3-us-west-2xn--avery-yuasakurastoragexn--b-5gaxn--b4w605ferdxn--balsan-sdtirol-nsbsvelvikongsbergxn--bck1b9a5dre4civilaviationfabricafederation-webredirectmediatechnologyeongbukashiwazakiyosembokutamamuraxn--bdddj-mrabdxn--bearalvhki-y4axn--berlevg-jxaxn--bhcavuotna-s4axn--bhccavuotna-k7axn--bidr-5nachikatsuuraxn--bievt-0qa2xn--bjarky-fyanaizuxn--bjddar-ptarumizusawaxn--blt-elabcienciamallamaceiobbcn-north-1xn--bmlo-graingerxn--bod-2natalxn--bozen-sdtirol-2obanazawaxn--brnny-wuacademy-firewall-gatewayxn--brnnysund-m8accident-investigation-aptibleadpagesquare7xn--brum-voagatrustkanazawaxn--btsfjord-9zaxn--bulsan-sdtirol-nsbarefootballooningjovikarasjoketokashikiyokawaraxn--c1avgxn--c2br7gxn--c3s14misakis-a-therapistoiaxn--cck2b3baremetalombardyn-vpndns3-website-ap-northeast-1xn--cckwcxetdxn--cesena-forl-mcbremangerxn--cesenaforl-i8axn--cg4bkis-into-cartoonsokamitsuexn--ciqpnxn--clchc0ea0b2g2a9gcdxn--czr694bargainstantcloudfrontdoorestauranthuathienhuebinordre-landiherokuapparochernigovernmentjeldsundiscordsays3-website-ap-southeast-1xn--czrs0trvaroyxn--czru2dxn--czrw28barrel-of-knowledgeapplinziitatebayashijonawatebizenakanojoetsumomodellinglassnillfjordiscordsezgoraxn--d1acj3barrell-of-knowledgecomputermezproxyzgorzeleccoffeedbackanagawarmiastalowa-wolayangroupars3-website-ap-southeast-2xn--d1alfaststacksevenassigdalxn--d1atrysiljanxn--d5qv7z876clanbibaiduckdnsaseboknowsitallxn--davvenjrga-y4axn--djrs72d6uyxn--djty4koobindalxn--dnna-grajewolterskluwerxn--drbak-wuaxn--dyry-iraxn--e1a4cldmail-boxaxn--eckvdtc9dxn--efvn9svn-repostuff-4-salexn--efvy88haebaruericssongdalenviknaklodzkochikushinonsenasakuchinotsuchiurakawaxn--ehqz56nxn--elqq16hagakhanhhoabinhduongxn--eveni-0qa01gaxn--f6qx53axn--fct429kooris-a-nascarfanxn--fhbeiarnxn--finny-yuaxn--fiq228c5hsbcleverappsassarinuyamashinazawaxn--fiq64barsycenterprisecloudcontrolappgafanquangnamasteigenoamishirasatochigifts3-website-eu-west-1xn--fiqs8swidnicaravanylvenetogakushimotoganexn--fiqz9swidnikitagatakkomaganexn--fjord-lraxn--fjq720axn--fl-ziaxn--flor-jraxn--flw351exn--forl-cesena-fcbsswiebodzindependent-commissionxn--forlcesena-c8axn--fpcrj9c3dxn--frde-granexn--frna-woaxn--frya-hraxn--fzc2c9e2clickrisinglesjaguarxn--fzys8d69uvgmailxn--g2xx48clinicasacampinagrandebungotakadaemongolianishitosashimizunaminamiawajikintuitoyotsukaidownloadrudtvsaogoncapooguyxn--gckr3f0fastvps-serveronakanotoddenxn--gecrj9cliniquedaklakasamatsudoesntexisteingeekasserversicherungroks-theatrentin-sud-tirolxn--ggaviika-8ya47hagebostadxn--gildeskl-g0axn--givuotna-8yandexcloudxn--gjvik-wuaxn--gk3at1exn--gls-elacaixaxn--gmq050is-into-gamessinamsosnowieconomiasadojin-dslattuminamitanexn--gmqw5axn--gnstigbestellen-zvbrplsbxn--45brj9churcharterxn--gnstigliefern-wobihirosakikamijimayfirstorfjordxn--h-2failxn--h1ahnxn--h1alizxn--h2breg3eveneswinoujsciencexn--h2brj9c8clothingdustdatadetectrani-andria-barletta-trani-andriaxn--h3cuzk1dienbienxn--hbmer-xqaxn--hcesuolo-7ya35barsyonlinehimejiiyamanouchikujoinvilleirvikarasuyamashikemrevistathellequipmentjmaxxxjavald-aostatics3-website-sa-east-1xn--hebda8basicserversejny-2xn--hery-iraxn--hgebostad-g3axn--hkkinen-5waxn--hmmrfeasta-s4accident-prevention-k3swisstufftoread-booksnestudioxn--hnefoss-q1axn--hobl-iraxn--holtlen-hxaxn--hpmir-xqaxn--hxt814exn--hyanger-q1axn--hylandet-54axn--i1b6b1a6a2exn--imr513nxn--indery-fyaotsusonoxn--io0a7is-leetrentinoaltoadigexn--j1adpohlxn--j1aefauskedsmokorsetagayaseralingenovaraxn--j1ael8basilicataniaxn--j1amhaibarakisosakitahatakamatsukawaxn--j6w193gxn--jlq480n2rgxn--jlster-byasakaiminatoyookananiimiharuxn--jrpeland-54axn--jvr189misasaguris-an-accountantsmolaquilaocais-a-linux-useranishiaritabashikaoizumizakitashiobaraxn--k7yn95exn--karmy-yuaxn--kbrq7oxn--kcrx77d1x4axn--kfjord-iuaxn--klbu-woaxn--klt787dxn--kltp7dxn--kltx9axn--klty5xn--45q11circlerkstagentsasayamaxn--koluokta-7ya57haiduongxn--kprw13dxn--kpry57dxn--kput3is-lostre-toteneis-a-llamarumorimachidaxn--krager-gyasugitlabbvieeexn--kranghke-b0axn--krdsherad-m8axn--krehamn-dxaxn--krjohka-hwab49jdfastly-terrariuminamiiseharaxn--ksnes-uuaxn--kvfjord-nxaxn--kvitsy-fyasuokanmakiwakuratexn--kvnangen-k0axn--l-1fairwindsynology-diskstationxn--l1accentureklamborghinikkofuefukihabororosynology-dsuzakadnsaliastudynaliastrynxn--laheadju-7yatominamibosoftwarendalenugxn--langevg-jxaxn--lcvr32dxn--ldingen-q1axn--leagaviika-52basketballfinanzjaworznoticeableksvikaratsuginamikatagamilanotogawaxn--lesund-huaxn--lgbbat1ad8jejuxn--lgrd-poacctulaspeziaxn--lhppi-xqaxn--linds-pramericanexpresservegame-serverxn--loabt-0qaxn--lrdal-sraxn--lrenskog-54axn--lt-liacn-northwest-1xn--lten-granvindafjordxn--lury-iraxn--m3ch0j3axn--mely-iraxn--merker-kuaxn--mgb2ddesxn--mgb9awbfbsbxn--1qqw23axn--mgba3a3ejtunesuzukamogawaxn--mgba3a4f16axn--mgba3a4fra1-deloittexn--mgba7c0bbn0axn--mgbaakc7dvfsxn--mgbaam7a8haiphongonnakatsugawaxn--mgbab2bdxn--mgbah1a3hjkrdxn--mgbai9a5eva00batsfjordiscountry-snowplowiczeladzlgleezeu-2xn--mgbai9azgqp6jelasticbeanstalkharkovalleeaostexn--mgbayh7gparasitexn--mgbbh1a71exn--mgbc0a9azcgxn--mgbca7dzdoxn--mgbcpq6gpa1axn--mgberp4a5d4a87gxn--mgberp4a5d4arxn--mgbgu82axn--mgbi4ecexposedxn--mgbpl2fhskopervikhmelnytskyivalleedaostexn--mgbqly7c0a67fbcngroks-thisayamanobeatsaudaxn--mgbqly7cvafricargoboavistanbulsan-sudtirolxn--mgbt3dhdxn--mgbtf8flatangerxn--mgbtx2bauhauspostman-echofunatoriginstances3-website-us-east-1xn--mgbx4cd0abkhaziaxn--mix082fbx-osewienxn--mix891fbxosexyxn--mjndalen-64axn--mk0axindependent-inquiryxn--mk1bu44cnpyatigorskjervoyagexn--mkru45is-not-certifiedxn--mlatvuopmi-s4axn--mli-tlavagiskexn--mlselv-iuaxn--moreke-juaxn--mori-qsakuratanxn--mosjen-eyatsukannamihokksundxn--mot-tlavangenxn--mre-og-romsdal-qqbuservecounterstrikexn--msy-ula0hair-surveillancexn--mtta-vrjjat-k7aflakstadaokayamazonaws-cloud9guacuiababybluebiteckidsmynasushiobaracingrok-freeddnsfreebox-osascoli-picenogatabuseating-organicbcgjerdrumcprequalifymelbourneasypanelblagrarq-authgear-stagingjerstadeltaishinomakilovecollegefantasyleaguenoharauthgearappspacehosted-by-previderehabmereitattoolforgerockyombolzano-altoadigeorgeorgiauthordalandroideporteatonamidorivnebetsukubankanumazuryomitanocparmautocodebergamoarekembuchikumagayagawafflecelloisirs3-external-180reggioemiliaromagnarusawaustrheimbalsan-sudtirolivingitpagexlivornobserveregruhostingivestbyglandroverhalladeskjakamaiedge-stagingivingjemnes3-eu-west-2038xn--muost-0qaxn--mxtq1misawaxn--ngbc5azdxn--ngbe9e0axn--ngbrxn--4dbgdty6ciscofreakamaihd-stagingriwataraindroppdalxn--nit225koryokamikawanehonbetsuwanouchikuhokuryugasakis-a-nursellsyourhomeftpiwatexn--nmesjevuemie-tcbalatinord-frontierxn--nnx388axn--nodessakurawebsozais-savedxn--nqv7fs00emaxn--nry-yla5gxn--ntso0iqx3axn--ntsq17gxn--nttery-byaeservehalflifeinsurancexn--nvuotna-hwaxn--nyqy26axn--o1achernivtsicilynxn--4dbrk0cexn--o3cw4hakatanortonkotsunndalxn--o3cyx2axn--od0algardxn--od0aq3beneventodayusuharaxn--ogbpf8fldrvelvetromsohuissier-justicexn--oppegrd-ixaxn--ostery-fyatsushiroxn--osyro-wuaxn--otu796dxn--p1acfedjeezxn--p1ais-slickharkivallee-d-aostexn--pgbs0dhlx3xn--porsgu-sta26fedorainfraclouderaxn--pssu33lxn--pssy2uxn--q7ce6axn--q9jyb4cnsauheradyndns-at-homedepotenzamamicrosoftbankasukabedzin-brbalsfjordietgoryoshiokanravocats3-fips-us-gov-west-1xn--qcka1pmcpenzapposxn--qqqt11misconfusedxn--qxa6axn--qxamunexus-3xn--rady-iraxn--rdal-poaxn--rde-ulazioxn--rdy-0nabaris-uberleetrentinos-tirolxn--rennesy-v1axn--rhkkervju-01afedorapeoplefrakkestadyndns-webhostingujogaszxn--rholt-mragowoltlab-democraciaxn--rhqv96gxn--rht27zxn--rht3dxn--rht61exn--risa-5naturalxn--risr-iraxn--rland-uuaxn--rlingen-mxaxn--rmskog-byawaraxn--rny31hakodatexn--rovu88bentleyusuitatamotorsitestinglitchernihivgubs3-website-us-west-1xn--rros-graphicsxn--rskog-uuaxn--rst-0naturbruksgymnxn--rsta-framercanvasxn--rvc1e0am3exn--ryken-vuaxn--ryrvik-byawatahamaxn--s-1faitheshopwarezzoxn--s9brj9cntraniandriabarlettatraniandriaxn--sandnessjen-ogbentrendhostingliwiceu-3xn--sandy-yuaxn--sdtirol-n2axn--seral-lraxn--ses554gxn--sgne-graphoxn--4gbriminiserverxn--skierv-utazurestaticappspaceusercontentunkongsvingerxn--skjervy-v1axn--skjk-soaxn--sknit-yqaxn--sknland-fxaxn--slat-5navigationxn--slt-elabogadobeaemcloud-fr1xn--smla-hraxn--smna-gratangenxn--snase-nraxn--sndre-land-0cbeppublishproxyuufcfanirasakindependent-panelomonza-brianzaporizhzhedmarkarelianceu-4xn--snes-poaxn--snsa-roaxn--sr-aurdal-l8axn--sr-fron-q1axn--sr-odal-q1axn--sr-varanger-ggbeskidyn-ip24xn--srfold-byaxn--srreisa-q1axn--srum-gratis-a-bloggerxn--stfold-9xaxn--stjrdal-s1axn--stjrdalshalsen-sqbestbuyshoparenagasakikuchikuseihicampinashikiminohostfoldnavyuzawaxn--stre-toten-zcbetainaboxfuselfipartindependent-reviewegroweibolognagasukeu-north-1xn--t60b56axn--tckweddingxn--tiq49xqyjelenia-goraxn--tjme-hraxn--tn0agrocerydxn--tnsberg-q1axn--tor131oxn--trany-yuaxn--trentin-sd-tirol-rzbhzc66xn--trentin-sdtirol-7vbialystokkeymachineu-south-1xn--trentino-sd-tirol-c3bielawakuyachimataharanzanishiazaindielddanuorrindigenamerikawauevje-og-hornnes3-website-us-west-2xn--trentino-sdtirol-szbiella-speziaxn--trentinosd-tirol-rzbieszczadygeyachiyodaeguamfamscompute-1xn--trentinosdtirol-7vbievat-band-campaignieznoorstaplesakyotanabellunordeste-idclkarlsoyxn--trentinsd-tirol-6vbifukagawalbrzycharitydalomzaporizhzhiaxn--trentinsdtirol-nsbigv-infolkebiblegnicalvinklein-butterhcloudiscoursesalangenishigotpantheonsitexn--trgstad-r1axn--trna-woaxn--troms-zuaxn--tysvr-vraxn--uc0atventuresinstagingxn--uc0ay4axn--uist22hakonexn--uisz3gxn--unjrga-rtashkenturindalxn--unup4yxn--uuwu58axn--vads-jraxn--valle-aoste-ebbturystykaneyamazoexn--valle-d-aoste-ehboehringerikexn--valleaoste-e7axn--valledaoste-ebbvadsoccertmgreaterxn--vard-jraxn--vegrshei-c0axn--vermgensberater-ctb-hostingxn--vermgensberatung-pwbiharstadotsubetsugarulezajskiervaksdalondonetskarmoyxn--vestvgy-ixa6oxn--vg-yiabruzzombieidskogasawarackmazerbaijan-mayenbaidarmeniaxn--vgan-qoaxn--vgsy-qoa0jellybeanxn--vgu402coguchikuzenishiwakinvestmentsaveincloudyndns-at-workisboringsakershusrcfdyndns-blogsitexn--vhquvestfoldxn--vler-qoaxn--vre-eiker-k8axn--vrggt-xqadxn--vry-yla5gxn--vuq861bihoronobeokagakikugawalesundiscoverdalondrinaplesknsalon-1xn--w4r85el8fhu5dnraxn--w4rs40lxn--wcvs22dxn--wgbh1communexn--wgbl6axn--xhq521bikedaejeonbuk0xn--xkc2al3hye2axn--xkc2dl3a5ee0hakubackyardshiraois-a-greenxn--y9a3aquarelleasingxn--yer-znavois-very-badxn--yfro4i67oxn--ygarden-p1axn--ygbi2ammxn--4it168dxn--ystre-slidre-ujbiofficialorenskoglobodoes-itcouldbeworldishangrilamdongnairkitapps-audibleasecuritytacticsxn--0trq7p7nnishiharaxn--zbx025dxn--zf0ao64axn--zf0avxlxn--zfr164bipartsaloonishiizunazukindustriaxnbayernxz \ No newline at end of file diff --git a/publicsuffix/example_test.go b/publicsuffix/example_test.go index 3f44dcfe75..c051dac8e0 100644 --- a/publicsuffix/example_test.go +++ b/publicsuffix/example_test.go @@ -77,7 +77,7 @@ func ExamplePublicSuffix_manager() { // > golang.dev dev is ICANN Managed // > golang.net net is ICANN Managed // > play.golang.org org is ICANN Managed - // > gophers.in.space.museum space.museum is ICANN Managed + // > gophers.in.space.museum museum is ICANN Managed // > // > 0emm.com com is ICANN Managed // > a.0emm.com a.0emm.com is Privately Managed diff --git a/publicsuffix/table.go b/publicsuffix/table.go index 6bdadcc448..78d400fa65 100644 --- a/publicsuffix/table.go +++ b/publicsuffix/table.go @@ -4,7 +4,7 @@ package publicsuffix import _ "embed" -const version = "publicsuffix.org's public_suffix_list.dat, git revision e248cbc92a527a166454afe9914c4c1b4253893f (2022-11-15T18:02:38Z)" +const version = "publicsuffix.org's public_suffix_list.dat, git revision 63cbc63d470d7b52c35266aa96c4c98c96ec499c (2023-08-03T10:01:25Z)" const ( nodesBits = 40 @@ -26,7 +26,7 @@ const ( ) // numTLD is the number of top level domains. -const numTLD = 1494 +const numTLD = 1474 // text is the combined text of all labels. // @@ -63,8 +63,8 @@ var nodes uint40String //go:embed data/children var children uint32String -// max children 718 (capacity 1023) -// max text offset 32976 (capacity 65535) -// max text length 36 (capacity 63) -// max hi 9656 (capacity 16383) -// max lo 9651 (capacity 16383) +// max children 743 (capacity 1023) +// max text offset 30876 (capacity 65535) +// max text length 31 (capacity 63) +// max hi 9322 (capacity 16383) +// max lo 9317 (capacity 16383) diff --git a/publicsuffix/table_test.go b/publicsuffix/table_test.go index 99698271a9..a297b3b0dd 100644 --- a/publicsuffix/table_test.go +++ b/publicsuffix/table_test.go @@ -2,7 +2,7 @@ package publicsuffix -const numICANNRules = 7367 +const numICANNRules = 6893 var rules = [...]string{ "ac", @@ -302,9 +302,26 @@ var rules = [...]string{ "org.bi", "biz", "bj", - "asso.bj", - "barreau.bj", - "gouv.bj", + "africa.bj", + "agro.bj", + "architectes.bj", + "assur.bj", + "avocats.bj", + "co.bj", + "com.bj", + "eco.bj", + "econo.bj", + "edu.bj", + "info.bj", + "loisirs.bj", + "money.bj", + "net.bj", + "org.bj", + "ote.bj", + "resto.bj", + "restaurant.bj", + "tourism.bj", + "univ.bj", "bm", "com.bm", "edu.bm", @@ -3596,552 +3613,6 @@ var rules = [...]string{ "co.mu", "or.mu", "museum", - "academy.museum", - "agriculture.museum", - "air.museum", - "airguard.museum", - "alabama.museum", - "alaska.museum", - "amber.museum", - "ambulance.museum", - "american.museum", - "americana.museum", - "americanantiques.museum", - "americanart.museum", - "amsterdam.museum", - "and.museum", - "annefrank.museum", - "anthro.museum", - "anthropology.museum", - "antiques.museum", - "aquarium.museum", - "arboretum.museum", - "archaeological.museum", - "archaeology.museum", - "architecture.museum", - "art.museum", - "artanddesign.museum", - "artcenter.museum", - "artdeco.museum", - "arteducation.museum", - "artgallery.museum", - "arts.museum", - "artsandcrafts.museum", - "asmatart.museum", - "assassination.museum", - "assisi.museum", - "association.museum", - "astronomy.museum", - "atlanta.museum", - "austin.museum", - "australia.museum", - "automotive.museum", - "aviation.museum", - "axis.museum", - "badajoz.museum", - "baghdad.museum", - "bahn.museum", - "bale.museum", - "baltimore.museum", - "barcelona.museum", - "baseball.museum", - "basel.museum", - "baths.museum", - "bauern.museum", - "beauxarts.museum", - "beeldengeluid.museum", - "bellevue.museum", - "bergbau.museum", - "berkeley.museum", - "berlin.museum", - "bern.museum", - "bible.museum", - "bilbao.museum", - "bill.museum", - "birdart.museum", - "birthplace.museum", - "bonn.museum", - "boston.museum", - "botanical.museum", - "botanicalgarden.museum", - "botanicgarden.museum", - "botany.museum", - "brandywinevalley.museum", - "brasil.museum", - "bristol.museum", - "british.museum", - "britishcolumbia.museum", - "broadcast.museum", - "brunel.museum", - "brussel.museum", - "brussels.museum", - "bruxelles.museum", - "building.museum", - "burghof.museum", - "bus.museum", - "bushey.museum", - "cadaques.museum", - "california.museum", - "cambridge.museum", - "can.museum", - "canada.museum", - "capebreton.museum", - "carrier.museum", - "cartoonart.museum", - "casadelamoneda.museum", - "castle.museum", - "castres.museum", - "celtic.museum", - "center.museum", - "chattanooga.museum", - "cheltenham.museum", - "chesapeakebay.museum", - "chicago.museum", - "children.museum", - "childrens.museum", - "childrensgarden.museum", - "chiropractic.museum", - "chocolate.museum", - "christiansburg.museum", - "cincinnati.museum", - "cinema.museum", - "circus.museum", - "civilisation.museum", - "civilization.museum", - "civilwar.museum", - "clinton.museum", - "clock.museum", - "coal.museum", - "coastaldefence.museum", - "cody.museum", - "coldwar.museum", - "collection.museum", - "colonialwilliamsburg.museum", - "coloradoplateau.museum", - "columbia.museum", - "columbus.museum", - "communication.museum", - "communications.museum", - "community.museum", - "computer.museum", - "computerhistory.museum", - "xn--comunicaes-v6a2o.museum", - "contemporary.museum", - "contemporaryart.museum", - "convent.museum", - "copenhagen.museum", - "corporation.museum", - "xn--correios-e-telecomunicaes-ghc29a.museum", - "corvette.museum", - "costume.museum", - "countryestate.museum", - "county.museum", - "crafts.museum", - "cranbrook.museum", - "creation.museum", - "cultural.museum", - "culturalcenter.museum", - "culture.museum", - "cyber.museum", - "cymru.museum", - "dali.museum", - "dallas.museum", - "database.museum", - "ddr.museum", - "decorativearts.museum", - "delaware.museum", - "delmenhorst.museum", - "denmark.museum", - "depot.museum", - "design.museum", - "detroit.museum", - "dinosaur.museum", - "discovery.museum", - "dolls.museum", - "donostia.museum", - "durham.museum", - "eastafrica.museum", - "eastcoast.museum", - "education.museum", - "educational.museum", - "egyptian.museum", - "eisenbahn.museum", - "elburg.museum", - "elvendrell.museum", - "embroidery.museum", - "encyclopedic.museum", - "england.museum", - "entomology.museum", - "environment.museum", - "environmentalconservation.museum", - "epilepsy.museum", - "essex.museum", - "estate.museum", - "ethnology.museum", - "exeter.museum", - "exhibition.museum", - "family.museum", - "farm.museum", - "farmequipment.museum", - "farmers.museum", - "farmstead.museum", - "field.museum", - "figueres.museum", - "filatelia.museum", - "film.museum", - "fineart.museum", - "finearts.museum", - "finland.museum", - "flanders.museum", - "florida.museum", - "force.museum", - "fortmissoula.museum", - "fortworth.museum", - "foundation.museum", - "francaise.museum", - "frankfurt.museum", - "franziskaner.museum", - "freemasonry.museum", - "freiburg.museum", - "fribourg.museum", - "frog.museum", - "fundacio.museum", - "furniture.museum", - "gallery.museum", - "garden.museum", - "gateway.museum", - "geelvinck.museum", - "gemological.museum", - "geology.museum", - "georgia.museum", - "giessen.museum", - "glas.museum", - "glass.museum", - "gorge.museum", - "grandrapids.museum", - "graz.museum", - "guernsey.museum", - "halloffame.museum", - "hamburg.museum", - "handson.museum", - "harvestcelebration.museum", - "hawaii.museum", - "health.museum", - "heimatunduhren.museum", - "hellas.museum", - "helsinki.museum", - "hembygdsforbund.museum", - "heritage.museum", - "histoire.museum", - "historical.museum", - "historicalsociety.museum", - "historichouses.museum", - "historisch.museum", - "historisches.museum", - "history.museum", - "historyofscience.museum", - "horology.museum", - "house.museum", - "humanities.museum", - "illustration.museum", - "imageandsound.museum", - "indian.museum", - "indiana.museum", - "indianapolis.museum", - "indianmarket.museum", - "intelligence.museum", - "interactive.museum", - "iraq.museum", - "iron.museum", - "isleofman.museum", - "jamison.museum", - "jefferson.museum", - "jerusalem.museum", - "jewelry.museum", - "jewish.museum", - "jewishart.museum", - "jfk.museum", - "journalism.museum", - "judaica.museum", - "judygarland.museum", - "juedisches.museum", - "juif.museum", - "karate.museum", - "karikatur.museum", - "kids.museum", - "koebenhavn.museum", - "koeln.museum", - "kunst.museum", - "kunstsammlung.museum", - "kunstunddesign.museum", - "labor.museum", - "labour.museum", - "lajolla.museum", - "lancashire.museum", - "landes.museum", - "lans.museum", - "xn--lns-qla.museum", - "larsson.museum", - "lewismiller.museum", - "lincoln.museum", - "linz.museum", - "living.museum", - "livinghistory.museum", - "localhistory.museum", - "london.museum", - "losangeles.museum", - "louvre.museum", - "loyalist.museum", - "lucerne.museum", - "luxembourg.museum", - "luzern.museum", - "mad.museum", - "madrid.museum", - "mallorca.museum", - "manchester.museum", - "mansion.museum", - "mansions.museum", - "manx.museum", - "marburg.museum", - "maritime.museum", - "maritimo.museum", - "maryland.museum", - "marylhurst.museum", - "media.museum", - "medical.museum", - "medizinhistorisches.museum", - "meeres.museum", - "memorial.museum", - "mesaverde.museum", - "michigan.museum", - "midatlantic.museum", - "military.museum", - "mill.museum", - "miners.museum", - "mining.museum", - "minnesota.museum", - "missile.museum", - "missoula.museum", - "modern.museum", - "moma.museum", - "money.museum", - "monmouth.museum", - "monticello.museum", - "montreal.museum", - "moscow.museum", - "motorcycle.museum", - "muenchen.museum", - "muenster.museum", - "mulhouse.museum", - "muncie.museum", - "museet.museum", - "museumcenter.museum", - "museumvereniging.museum", - "music.museum", - "national.museum", - "nationalfirearms.museum", - "nationalheritage.museum", - "nativeamerican.museum", - "naturalhistory.museum", - "naturalhistorymuseum.museum", - "naturalsciences.museum", - "nature.museum", - "naturhistorisches.museum", - "natuurwetenschappen.museum", - "naumburg.museum", - "naval.museum", - "nebraska.museum", - "neues.museum", - "newhampshire.museum", - "newjersey.museum", - "newmexico.museum", - "newport.museum", - "newspaper.museum", - "newyork.museum", - "niepce.museum", - "norfolk.museum", - "north.museum", - "nrw.museum", - "nyc.museum", - "nyny.museum", - "oceanographic.museum", - "oceanographique.museum", - "omaha.museum", - "online.museum", - "ontario.museum", - "openair.museum", - "oregon.museum", - "oregontrail.museum", - "otago.museum", - "oxford.museum", - "pacific.museum", - "paderborn.museum", - "palace.museum", - "paleo.museum", - "palmsprings.museum", - "panama.museum", - "paris.museum", - "pasadena.museum", - "pharmacy.museum", - "philadelphia.museum", - "philadelphiaarea.museum", - "philately.museum", - "phoenix.museum", - "photography.museum", - "pilots.museum", - "pittsburgh.museum", - "planetarium.museum", - "plantation.museum", - "plants.museum", - "plaza.museum", - "portal.museum", - "portland.museum", - "portlligat.museum", - "posts-and-telecommunications.museum", - "preservation.museum", - "presidio.museum", - "press.museum", - "project.museum", - "public.museum", - "pubol.museum", - "quebec.museum", - "railroad.museum", - "railway.museum", - "research.museum", - "resistance.museum", - "riodejaneiro.museum", - "rochester.museum", - "rockart.museum", - "roma.museum", - "russia.museum", - "saintlouis.museum", - "salem.museum", - "salvadordali.museum", - "salzburg.museum", - "sandiego.museum", - "sanfrancisco.museum", - "santabarbara.museum", - "santacruz.museum", - "santafe.museum", - "saskatchewan.museum", - "satx.museum", - "savannahga.museum", - "schlesisches.museum", - "schoenbrunn.museum", - "schokoladen.museum", - "school.museum", - "schweiz.museum", - "science.museum", - "scienceandhistory.museum", - "scienceandindustry.museum", - "sciencecenter.museum", - "sciencecenters.museum", - "science-fiction.museum", - "sciencehistory.museum", - "sciences.museum", - "sciencesnaturelles.museum", - "scotland.museum", - "seaport.museum", - "settlement.museum", - "settlers.museum", - "shell.museum", - "sherbrooke.museum", - "sibenik.museum", - "silk.museum", - "ski.museum", - "skole.museum", - "society.museum", - "sologne.museum", - "soundandvision.museum", - "southcarolina.museum", - "southwest.museum", - "space.museum", - "spy.museum", - "square.museum", - "stadt.museum", - "stalbans.museum", - "starnberg.museum", - "state.museum", - "stateofdelaware.museum", - "station.museum", - "steam.museum", - "steiermark.museum", - "stjohn.museum", - "stockholm.museum", - "stpetersburg.museum", - "stuttgart.museum", - "suisse.museum", - "surgeonshall.museum", - "surrey.museum", - "svizzera.museum", - "sweden.museum", - "sydney.museum", - "tank.museum", - "tcm.museum", - "technology.museum", - "telekommunikation.museum", - "television.museum", - "texas.museum", - "textile.museum", - "theater.museum", - "time.museum", - "timekeeping.museum", - "topology.museum", - "torino.museum", - "touch.museum", - "town.museum", - "transport.museum", - "tree.museum", - "trolley.museum", - "trust.museum", - "trustee.museum", - "uhren.museum", - "ulm.museum", - "undersea.museum", - "university.museum", - "usa.museum", - "usantiques.museum", - "usarts.museum", - "uscountryestate.museum", - "usculture.museum", - "usdecorativearts.museum", - "usgarden.museum", - "ushistory.museum", - "ushuaia.museum", - "uslivinghistory.museum", - "utah.museum", - "uvic.museum", - "valley.museum", - "vantaa.museum", - "versailles.museum", - "viking.museum", - "village.museum", - "virginia.museum", - "virtual.museum", - "virtuel.museum", - "vlaanderen.museum", - "volkenkunde.museum", - "wales.museum", - "wallonie.museum", - "war.museum", - "washingtondc.museum", - "watchandclock.museum", - "watch-and-clock.museum", - "western.museum", - "westfalen.museum", - "whaling.museum", - "wildlife.museum", - "williamsburg.museum", - "windmill.museum", - "workshop.museum", - "york.museum", - "yorkshire.museum", - "yosemite.museum", - "youth.museum", - "zoological.museum", - "zoology.museum", - "xn--9dbhblg6di.museum", - "xn--h1aegh.museum", "mv", "aero.mv", "biz.mv", @@ -5133,52 +4604,60 @@ var rules = [...]string{ "turystyka.pl", "gov.pl", "ap.gov.pl", + "griw.gov.pl", "ic.gov.pl", "is.gov.pl", - "us.gov.pl", "kmpsp.gov.pl", + "konsulat.gov.pl", "kppsp.gov.pl", - "kwpsp.gov.pl", - "psp.gov.pl", - "wskr.gov.pl", "kwp.gov.pl", + "kwpsp.gov.pl", + "mup.gov.pl", "mw.gov.pl", - "ug.gov.pl", - "um.gov.pl", - "umig.gov.pl", - "ugim.gov.pl", - "upow.gov.pl", - "uw.gov.pl", - "starostwo.gov.pl", + "oia.gov.pl", + "oirm.gov.pl", + "oke.gov.pl", + "oow.gov.pl", + "oschr.gov.pl", + "oum.gov.pl", "pa.gov.pl", + "pinb.gov.pl", + "piw.gov.pl", "po.gov.pl", + "pr.gov.pl", + "psp.gov.pl", "psse.gov.pl", "pup.gov.pl", "rzgw.gov.pl", "sa.gov.pl", + "sdn.gov.pl", + "sko.gov.pl", "so.gov.pl", "sr.gov.pl", - "wsa.gov.pl", - "sko.gov.pl", + "starostwo.gov.pl", + "ug.gov.pl", + "ugim.gov.pl", + "um.gov.pl", + "umig.gov.pl", + "upow.gov.pl", + "uppo.gov.pl", + "us.gov.pl", + "uw.gov.pl", "uzs.gov.pl", + "wif.gov.pl", "wiih.gov.pl", "winb.gov.pl", - "pinb.gov.pl", "wios.gov.pl", "witd.gov.pl", - "wzmiuw.gov.pl", - "piw.gov.pl", "wiw.gov.pl", - "griw.gov.pl", - "wif.gov.pl", - "oum.gov.pl", - "sdn.gov.pl", - "zp.gov.pl", - "uppo.gov.pl", - "mup.gov.pl", + "wkz.gov.pl", + "wsa.gov.pl", + "wskr.gov.pl", + "wsse.gov.pl", "wuoz.gov.pl", - "konsulat.gov.pl", - "oirm.gov.pl", + "wzmiuw.gov.pl", + "zp.gov.pl", + "zpisdn.gov.pl", "augustow.pl", "babia-gora.pl", "bedzin.pl", @@ -5722,6 +5201,7 @@ var rules = [...]string{ "kirovograd.ua", "km.ua", "kr.ua", + "kropyvnytskyi.ua", "krym.ua", "ks.ua", "kv.ua", @@ -6063,18 +5543,84 @@ var rules = [...]string{ "net.vi", "org.vi", "vn", + "ac.vn", + "ai.vn", + "biz.vn", "com.vn", - "net.vn", - "org.vn", "edu.vn", "gov.vn", - "int.vn", - "ac.vn", - "biz.vn", + "health.vn", + "id.vn", "info.vn", + "int.vn", + "io.vn", "name.vn", + "net.vn", + "org.vn", "pro.vn", - "health.vn", + "angiang.vn", + "bacgiang.vn", + "backan.vn", + "baclieu.vn", + "bacninh.vn", + "baria-vungtau.vn", + "bentre.vn", + "binhdinh.vn", + "binhduong.vn", + "binhphuoc.vn", + "binhthuan.vn", + "camau.vn", + "cantho.vn", + "caobang.vn", + "daklak.vn", + "daknong.vn", + "danang.vn", + "dienbien.vn", + "dongnai.vn", + "dongthap.vn", + "gialai.vn", + "hagiang.vn", + "haiduong.vn", + "haiphong.vn", + "hanam.vn", + "hanoi.vn", + "hatinh.vn", + "haugiang.vn", + "hoabinh.vn", + "hungyen.vn", + "khanhhoa.vn", + "kiengiang.vn", + "kontum.vn", + "laichau.vn", + "lamdong.vn", + "langson.vn", + "laocai.vn", + "longan.vn", + "namdinh.vn", + "nghean.vn", + "ninhbinh.vn", + "ninhthuan.vn", + "phutho.vn", + "phuyen.vn", + "quangbinh.vn", + "quangnam.vn", + "quangngai.vn", + "quangninh.vn", + "quangtri.vn", + "soctrang.vn", + "sonla.vn", + "tayninh.vn", + "thaibinh.vn", + "thainguyen.vn", + "thanhhoa.vn", + "thanhphohochiminh.vn", + "thuathienhue.vn", + "tiengiang.vn", + "travinh.vn", + "tuyenquang.vn", + "vinhlong.vn", + "vinhphuc.vn", + "yenbai.vn", "vu", "com.vu", "edu.vu", @@ -6221,7 +5767,6 @@ var rules = [...]string{ "org.zw", "aaa", "aarp", - "abarth", "abb", "abbott", "abbvie", @@ -6235,7 +5780,6 @@ var rules = [...]string{ "accountants", "aco", "actor", - "adac", "ads", "adult", "aeg", @@ -6249,7 +5793,6 @@ var rules = [...]string{ "airforce", "airtel", "akdn", - "alfaromeo", "alibaba", "alipay", "allfinanz", @@ -6445,7 +5988,6 @@ var rules = [...]string{ "contact", "contractors", "cooking", - "cookingchannel", "cool", "corsica", "country", @@ -6554,7 +6096,6 @@ var rules = [...]string{ "feedback", "ferrari", "ferrero", - "fiat", "fidelity", "fido", "film", @@ -6576,7 +6117,6 @@ var rules = [...]string{ "fly", "foo", "food", - "foodnetwork", "football", "ford", "forex", @@ -6661,7 +6201,6 @@ var rules = [...]string{ "helsinki", "here", "hermes", - "hgtv", "hiphop", "hisamitsu", "hitachi", @@ -6680,7 +6219,6 @@ var rules = [...]string{ "host", "hosting", "hot", - "hoteles", "hotels", "hotmail", "house", @@ -6761,7 +6299,6 @@ var rules = [...]string{ "lamborghini", "lamer", "lancaster", - "lancia", "land", "landrover", "lanxess", @@ -6789,7 +6326,6 @@ var rules = [...]string{ "limited", "limo", "lincoln", - "linde", "link", "lipsy", "live", @@ -6800,7 +6336,6 @@ var rules = [...]string{ "loans", "locker", "locus", - "loft", "lol", "london", "lotte", @@ -6813,7 +6348,6 @@ var rules = [...]string{ "lundbeck", "luxe", "luxury", - "macys", "madrid", "maif", "maison", @@ -6827,7 +6361,6 @@ var rules = [...]string{ "markets", "marriott", "marshalls", - "maserati", "mattel", "mba", "mckinsey", @@ -6868,7 +6401,6 @@ var rules = [...]string{ "mtn", "mtr", "music", - "mutual", "nab", "nagoya", "natura", @@ -6933,7 +6465,6 @@ var rules = [...]string{ "partners", "parts", "party", - "passagens", "pay", "pccw", "pet", @@ -7063,7 +6594,6 @@ var rules = [...]string{ "select", "sener", "services", - "ses", "seven", "sew", "sex", @@ -7157,7 +6687,6 @@ var rules = [...]string{ "tiaa", "tickets", "tienda", - "tiffany", "tips", "tires", "tirol", @@ -7180,7 +6709,6 @@ var rules = [...]string{ "trading", "training", "travel", - "travelchannel", "travelers", "travelersinsurance", "trust", @@ -7225,7 +6753,6 @@ var rules = [...]string{ "voting", "voto", "voyage", - "vuelos", "wales", "walmart", "walter", @@ -7316,7 +6843,6 @@ var rules = [...]string{ "xn--io0a7i", "xn--j1aef", "xn--jlq480n2rg", - "xn--jlq61u9w7b", "xn--jvr189m", "xn--kcrx77d1x4a", "xn--kput3i", @@ -7379,17 +6905,35 @@ var rules = [...]string{ "graphox.us", "*.devcdnaccesso.com", "*.on-acorn.io", + "activetrail.biz", "adobeaemcloud.com", "*.dev.adobeaemcloud.com", "hlx.live", "adobeaemcloud.net", "hlx.page", "hlx3.page", + "adobeio-static.net", + "adobeioruntime.net", "beep.pl", "airkitapps.com", "airkitapps-au.com", "airkitapps.eu", "aivencloud.com", + "akadns.net", + "akamai.net", + "akamai-staging.net", + "akamaiedge.net", + "akamaiedge-staging.net", + "akamaihd.net", + "akamaihd-staging.net", + "akamaiorigin.net", + "akamaiorigin-staging.net", + "akamaized.net", + "akamaized-staging.net", + "edgekey.net", + "edgekey-staging.net", + "edgesuite.net", + "edgesuite-staging.net", "barsy.ca", "*.compute.estate", "*.alces.network", @@ -7456,46 +7000,72 @@ var rules = [...]string{ "s3.dualstack.us-east-2.amazonaws.com", "s3.us-east-2.amazonaws.com", "s3-website.us-east-2.amazonaws.com", + "analytics-gateway.ap-northeast-1.amazonaws.com", + "analytics-gateway.eu-west-1.amazonaws.com", + "analytics-gateway.us-east-1.amazonaws.com", + "analytics-gateway.us-east-2.amazonaws.com", + "analytics-gateway.us-west-2.amazonaws.com", + "webview-assets.aws-cloud9.af-south-1.amazonaws.com", "vfs.cloud9.af-south-1.amazonaws.com", "webview-assets.cloud9.af-south-1.amazonaws.com", + "webview-assets.aws-cloud9.ap-east-1.amazonaws.com", "vfs.cloud9.ap-east-1.amazonaws.com", "webview-assets.cloud9.ap-east-1.amazonaws.com", + "webview-assets.aws-cloud9.ap-northeast-1.amazonaws.com", "vfs.cloud9.ap-northeast-1.amazonaws.com", "webview-assets.cloud9.ap-northeast-1.amazonaws.com", + "webview-assets.aws-cloud9.ap-northeast-2.amazonaws.com", "vfs.cloud9.ap-northeast-2.amazonaws.com", "webview-assets.cloud9.ap-northeast-2.amazonaws.com", + "webview-assets.aws-cloud9.ap-northeast-3.amazonaws.com", "vfs.cloud9.ap-northeast-3.amazonaws.com", "webview-assets.cloud9.ap-northeast-3.amazonaws.com", + "webview-assets.aws-cloud9.ap-south-1.amazonaws.com", "vfs.cloud9.ap-south-1.amazonaws.com", "webview-assets.cloud9.ap-south-1.amazonaws.com", + "webview-assets.aws-cloud9.ap-southeast-1.amazonaws.com", "vfs.cloud9.ap-southeast-1.amazonaws.com", "webview-assets.cloud9.ap-southeast-1.amazonaws.com", + "webview-assets.aws-cloud9.ap-southeast-2.amazonaws.com", "vfs.cloud9.ap-southeast-2.amazonaws.com", "webview-assets.cloud9.ap-southeast-2.amazonaws.com", + "webview-assets.aws-cloud9.ca-central-1.amazonaws.com", "vfs.cloud9.ca-central-1.amazonaws.com", "webview-assets.cloud9.ca-central-1.amazonaws.com", + "webview-assets.aws-cloud9.eu-central-1.amazonaws.com", "vfs.cloud9.eu-central-1.amazonaws.com", "webview-assets.cloud9.eu-central-1.amazonaws.com", + "webview-assets.aws-cloud9.eu-north-1.amazonaws.com", "vfs.cloud9.eu-north-1.amazonaws.com", "webview-assets.cloud9.eu-north-1.amazonaws.com", + "webview-assets.aws-cloud9.eu-south-1.amazonaws.com", "vfs.cloud9.eu-south-1.amazonaws.com", "webview-assets.cloud9.eu-south-1.amazonaws.com", + "webview-assets.aws-cloud9.eu-west-1.amazonaws.com", "vfs.cloud9.eu-west-1.amazonaws.com", "webview-assets.cloud9.eu-west-1.amazonaws.com", + "webview-assets.aws-cloud9.eu-west-2.amazonaws.com", "vfs.cloud9.eu-west-2.amazonaws.com", "webview-assets.cloud9.eu-west-2.amazonaws.com", + "webview-assets.aws-cloud9.eu-west-3.amazonaws.com", "vfs.cloud9.eu-west-3.amazonaws.com", "webview-assets.cloud9.eu-west-3.amazonaws.com", + "webview-assets.aws-cloud9.me-south-1.amazonaws.com", "vfs.cloud9.me-south-1.amazonaws.com", "webview-assets.cloud9.me-south-1.amazonaws.com", + "webview-assets.aws-cloud9.sa-east-1.amazonaws.com", "vfs.cloud9.sa-east-1.amazonaws.com", "webview-assets.cloud9.sa-east-1.amazonaws.com", + "webview-assets.aws-cloud9.us-east-1.amazonaws.com", "vfs.cloud9.us-east-1.amazonaws.com", "webview-assets.cloud9.us-east-1.amazonaws.com", + "webview-assets.aws-cloud9.us-east-2.amazonaws.com", "vfs.cloud9.us-east-2.amazonaws.com", "webview-assets.cloud9.us-east-2.amazonaws.com", + "webview-assets.aws-cloud9.us-west-1.amazonaws.com", "vfs.cloud9.us-west-1.amazonaws.com", "webview-assets.cloud9.us-west-1.amazonaws.com", + "webview-assets.aws-cloud9.us-west-2.amazonaws.com", "vfs.cloud9.us-west-2.amazonaws.com", "webview-assets.cloud9.us-west-2.amazonaws.com", "cn-north-1.eb.amazonaws.com.cn", @@ -7542,6 +7112,7 @@ var rules = [...]string{ "myasustor.com", "cdn.prod.atlassian-dev.net", "translated.page", + "autocode.dev", "myfritz.net", "onavstack.net", "*.awdev.ca", @@ -7588,6 +7159,8 @@ var rules = [...]string{ "vm.bytemark.co.uk", "cafjs.com", "mycd.eu", + "canva-apps.cn", + "canva-apps.com", "drr.ac", "uwu.ai", "carrd.co", @@ -7653,8 +7226,11 @@ var rules = [...]string{ "cloudcontrolled.com", "cloudcontrolapp.com", "*.cloudera.site", - "pages.dev", + "cf-ipfs.com", + "cloudflare-ipfs.com", "trycloudflare.com", + "pages.dev", + "r2.dev", "workers.dev", "wnext.app", "co.ca", @@ -8227,6 +7803,7 @@ var rules = [...]string{ "channelsdvr.net", "u.channelsdvr.net", "edgecompute.app", + "fastly-edge.com", "fastly-terrarium.com", "fastlylb.net", "map.fastlylb.net", @@ -8566,6 +8143,7 @@ var rules = [...]string{ "ngo.ng", "edu.scot", "sch.so", + "ie.ua", "hostyhosting.io", "xn--hkkinen-5wa.fi", "*.moonscale.io", @@ -8633,7 +8211,6 @@ var rules = [...]string{ "iobb.net", "mel.cloudlets.com.au", "cloud.interhostsolutions.be", - "users.scale.virtualcloud.com.br", "mycloud.by", "alp1.ae.flow.ch", "appengine.flow.ch", @@ -8657,9 +8234,7 @@ var rules = [...]string{ "de.trendhosting.cloud", "jele.club", "amscompute.com", - "clicketcloud.com", "dopaas.com", - "hidora.com", "paas.hosted-by-previder.com", "rag-cloud.hosteur.com", "rag-cloud-ch.hosteur.com", @@ -8834,6 +8409,7 @@ var rules = [...]string{ "azurestaticapps.net", "1.azurestaticapps.net", "2.azurestaticapps.net", + "3.azurestaticapps.net", "centralus.azurestaticapps.net", "eastasia.azurestaticapps.net", "eastus2.azurestaticapps.net", @@ -8864,7 +8440,19 @@ var rules = [...]string{ "cloud.nospamproxy.com", "netlify.app", "4u.com", + "ngrok.app", + "ngrok-free.app", + "ngrok.dev", + "ngrok-free.dev", "ngrok.io", + "ap.ngrok.io", + "au.ngrok.io", + "eu.ngrok.io", + "in.ngrok.io", + "jp.ngrok.io", + "sa.ngrok.io", + "us.ngrok.io", + "ngrok.pizza", "nh-serv.co.uk", "nfshost.com", "*.developer.app", @@ -9084,6 +8672,7 @@ var rules = [...]string{ "eu.pythonanywhere.com", "qoto.io", "qualifioapp.com", + "ladesk.com", "qbuser.com", "cloudsite.builders", "instances.spawn.cc", @@ -9132,6 +8721,53 @@ var rules = [...]string{ "xn--h1aliz.xn--p1acf", "xn--90a1af.xn--p1acf", "xn--41a.xn--p1acf", + "180r.com", + "dojin.com", + "sakuratan.com", + "sakuraweb.com", + "x0.com", + "2-d.jp", + "bona.jp", + "crap.jp", + "daynight.jp", + "eek.jp", + "flop.jp", + "halfmoon.jp", + "jeez.jp", + "matrix.jp", + "mimoza.jp", + "ivory.ne.jp", + "mail-box.ne.jp", + "mints.ne.jp", + "mokuren.ne.jp", + "opal.ne.jp", + "sakura.ne.jp", + "sumomo.ne.jp", + "topaz.ne.jp", + "netgamers.jp", + "nyanta.jp", + "o0o0.jp", + "rdy.jp", + "rgr.jp", + "rulez.jp", + "s3.isk01.sakurastorage.jp", + "s3.isk02.sakurastorage.jp", + "saloon.jp", + "sblo.jp", + "skr.jp", + "tank.jp", + "uh-oh.jp", + "undo.jp", + "rs.webaccel.jp", + "user.webaccel.jp", + "websozai.jp", + "xii.jp", + "squares.net", + "jpn.org", + "kirara.st", + "x0.to", + "from.tv", + "sakura.tv", "*.builder.code.com", "*.dev-builder.code.com", "*.stg-builder.code.com", @@ -9204,6 +8840,9 @@ var rules = [...]string{ "beta.bounty-full.com", "small-web.org", "vp4.me", + "snowflake.app", + "privatelink.snowflake.app", + "streamlit.app", "streamlitapp.com", "try-snowplow.com", "srht.site", @@ -9243,6 +8882,7 @@ var rules = [...]string{ "myspreadshop.se", "myspreadshop.co.uk", "api.stdlib.com", + "storipress.app", "storj.farm", "utwente.io", "soc.srcf.net", @@ -9272,6 +8912,8 @@ var rules = [...]string{ "vpnplus.to", "direct.quickconnect.to", "tabitorder.co.il", + "mytabit.co.il", + "mytabit.com", "taifun-dns.de", "beta.tailscale.net", "ts.net", @@ -9350,6 +8992,7 @@ var rules = [...]string{ "hk.org", "ltd.hk", "inc.hk", + "it.com", "name.pm", "sch.tf", "biz.wf", @@ -9472,7 +9115,6 @@ var rules = [...]string{ var nodeLabels = [...]string{ "aaa", "aarp", - "abarth", "abb", "abbott", "abbvie", @@ -9488,7 +9130,6 @@ var nodeLabels = [...]string{ "aco", "actor", "ad", - "adac", "ads", "adult", "ae", @@ -9508,7 +9149,6 @@ var nodeLabels = [...]string{ "airtel", "akdn", "al", - "alfaromeo", "alibaba", "alipay", "allfinanz", @@ -9750,7 +9390,6 @@ var nodeLabels = [...]string{ "contact", "contractors", "cooking", - "cookingchannel", "cool", "coop", "corsica", @@ -9882,7 +9521,6 @@ var nodeLabels = [...]string{ "ferrari", "ferrero", "fi", - "fiat", "fidelity", "fido", "film", @@ -9908,7 +9546,6 @@ var nodeLabels = [...]string{ "fo", "foo", "food", - "foodnetwork", "football", "ford", "forex", @@ -10014,7 +9651,6 @@ var nodeLabels = [...]string{ "helsinki", "here", "hermes", - "hgtv", "hiphop", "hisamitsu", "hitachi", @@ -10036,7 +9672,6 @@ var nodeLabels = [...]string{ "host", "hosting", "hot", - "hoteles", "hotels", "hotmail", "house", @@ -10149,7 +9784,6 @@ var nodeLabels = [...]string{ "lamborghini", "lamer", "lancaster", - "lancia", "land", "landrover", "lanxess", @@ -10180,7 +9814,6 @@ var nodeLabels = [...]string{ "limited", "limo", "lincoln", - "linde", "link", "lipsy", "live", @@ -10192,7 +9825,6 @@ var nodeLabels = [...]string{ "loans", "locker", "locus", - "loft", "lol", "london", "lotte", @@ -10212,7 +9844,6 @@ var nodeLabels = [...]string{ "lv", "ly", "ma", - "macys", "madrid", "maif", "maison", @@ -10226,7 +9857,6 @@ var nodeLabels = [...]string{ "markets", "marriott", "marshalls", - "maserati", "mattel", "mba", "mc", @@ -10286,7 +9916,6 @@ var nodeLabels = [...]string{ "mu", "museum", "music", - "mutual", "mv", "mw", "mx", @@ -10374,7 +10003,6 @@ var nodeLabels = [...]string{ "partners", "parts", "party", - "passagens", "pay", "pccw", "pe", @@ -10530,7 +10158,6 @@ var nodeLabels = [...]string{ "select", "sener", "services", - "ses", "seven", "sew", "sex", @@ -10647,7 +10274,6 @@ var nodeLabels = [...]string{ "tiaa", "tickets", "tienda", - "tiffany", "tips", "tires", "tirol", @@ -10677,7 +10303,6 @@ var nodeLabels = [...]string{ "trading", "training", "travel", - "travelchannel", "travelers", "travelersinsurance", "trust", @@ -10739,7 +10364,6 @@ var nodeLabels = [...]string{ "voto", "voyage", "vu", - "vuelos", "wales", "walmart", "walter", @@ -10856,7 +10480,6 @@ var nodeLabels = [...]string{ "xn--j1amh", "xn--j6w193g", "xn--jlq480n2rg", - "xn--jlq61u9w7b", "xn--jvr189m", "xn--kcrx77d1x4a", "xn--kprw13d", @@ -11119,18 +10742,24 @@ var nodeLabels = [...]string{ "loginline", "messerli", "netlify", + "ngrok", + "ngrok-free", "noop", "northflank", "ondigitalocean", "onflashdrive", "platform0", "run", + "snowflake", + "storipress", + "streamlit", "telebit", "typedream", "vercel", "web", "wnext", "a", + "privatelink", "bet", "com", "coop", @@ -11316,6 +10945,7 @@ var nodeLabels = [...]string{ "edu", "or", "org", + "activetrail", "cloudns", "dscloud", "dyndns", @@ -11330,10 +10960,27 @@ var nodeLabels = [...]string{ "orx", "selfip", "webhop", - "asso", - "barreau", + "africa", + "agro", + "architectes", + "assur", + "avocats", "blogspot", - "gouv", + "co", + "com", + "eco", + "econo", + "edu", + "info", + "loisirs", + "money", + "net", + "org", + "ote", + "restaurant", + "resto", + "tourism", + "univ", "com", "edu", "gov", @@ -11529,9 +11176,6 @@ var nodeLabels = [...]string{ "zlg", "blogspot", "simplesite", - "virtualcloud", - "scale", - "users", "ac", "al", "am", @@ -11772,6 +11416,7 @@ var nodeLabels = [...]string{ "ac", "ah", "bj", + "canva-apps", "com", "cq", "edu", @@ -11853,6 +11498,7 @@ var nodeLabels = [...]string{ "owo", "001www", "0emm", + "180r", "1kapp", "3utilities", "4u", @@ -11888,11 +11534,13 @@ var nodeLabels = [...]string{ "br", "builtwithdark", "cafjs", + "canva-apps", "cechire", + "cf-ipfs", "ciscofreak", - "clicketcloud", "cloudcontrolapp", "cloudcontrolled", + "cloudflare-ipfs", "cn", "co", "code", @@ -11919,6 +11567,7 @@ var nodeLabels = [...]string{ "dnsdojo", "dnsiskinky", "doesntexist", + "dojin", "dontexist", "doomdns", "dopaas", @@ -11951,6 +11600,7 @@ var nodeLabels = [...]string{ "eu", "evennode", "familyds", + "fastly-edge", "fastly-terrarium", "fastvps-server", "fbsbx", @@ -12024,7 +11674,6 @@ var nodeLabels = [...]string{ "health-carereform", "herokuapp", "herokussl", - "hidora", "hk", "hobby-site", "homelinux", @@ -12098,6 +11747,7 @@ var nodeLabels = [...]string{ "isa-geek", "isa-hockeynut", "issmarterthanyou", + "it", "jdevcloud", "jelastic", "joyent", @@ -12107,6 +11757,7 @@ var nodeLabels = [...]string{ "kozow", "kr", "ktistory", + "ladesk", "likes-pie", "likescandy", "linode", @@ -12133,6 +11784,7 @@ var nodeLabels = [...]string{ "myshopblocks", "myshopify", "myspreadshop", + "mytabit", "mythic-beasts", "mytuleap", "myvnc", @@ -12179,6 +11831,8 @@ var nodeLabels = [...]string{ "rhcloud", "ru", "sa", + "sakuratan", + "sakuraweb", "saves-the-whales", "scrysec", "securitytactics", @@ -12241,6 +11895,7 @@ var nodeLabels = [...]string{ "wphostedmail", "wpmucdn", "writesthisblog", + "x0", "xnbay", "yolasite", "za", @@ -12295,107 +11950,154 @@ var nodeLabels = [...]string{ "us-east-2", "us-west-1", "us-west-2", + "aws-cloud9", "cloud9", + "webview-assets", "vfs", "webview-assets", + "aws-cloud9", "cloud9", + "webview-assets", "vfs", "webview-assets", + "analytics-gateway", + "aws-cloud9", "cloud9", "dualstack", + "webview-assets", "vfs", "webview-assets", "s3", + "aws-cloud9", "cloud9", "dualstack", "s3", "s3-website", + "webview-assets", "vfs", "webview-assets", "s3", + "aws-cloud9", "cloud9", + "webview-assets", "vfs", "webview-assets", + "aws-cloud9", "cloud9", "dualstack", "s3", "s3-website", + "webview-assets", "vfs", "webview-assets", "s3", + "aws-cloud9", "cloud9", "dualstack", + "webview-assets", "vfs", "webview-assets", "s3", + "aws-cloud9", "cloud9", "dualstack", + "webview-assets", "vfs", "webview-assets", "s3", + "aws-cloud9", "cloud9", "dualstack", "s3", "s3-website", + "webview-assets", "vfs", "webview-assets", "s3", + "aws-cloud9", "cloud9", "dualstack", "s3", "s3-website", + "webview-assets", "vfs", "webview-assets", "s3", + "aws-cloud9", "cloud9", + "webview-assets", "vfs", "webview-assets", + "aws-cloud9", "cloud9", + "webview-assets", "vfs", "webview-assets", + "analytics-gateway", + "aws-cloud9", "cloud9", "dualstack", + "webview-assets", "vfs", "webview-assets", "s3", + "aws-cloud9", "cloud9", "dualstack", "s3", "s3-website", + "webview-assets", "vfs", "webview-assets", "s3", + "aws-cloud9", "cloud9", "dualstack", "s3", "s3-website", + "webview-assets", "vfs", "webview-assets", "s3", + "aws-cloud9", "cloud9", + "webview-assets", "vfs", "webview-assets", + "aws-cloud9", "cloud9", "dualstack", + "webview-assets", "vfs", "webview-assets", "s3", + "analytics-gateway", + "aws-cloud9", "cloud9", "dualstack", + "webview-assets", "vfs", "webview-assets", "s3", + "analytics-gateway", + "aws-cloud9", "cloud9", "dualstack", "s3", "s3-website", + "webview-assets", "vfs", "webview-assets", "s3", + "aws-cloud9", "cloud9", + "webview-assets", "vfs", "webview-assets", + "analytics-gateway", + "aws-cloud9", "cloud9", + "webview-assets", "vfs", "webview-assets", "r", @@ -12610,6 +12312,7 @@ var nodeLabels = [...]string{ "pages", "customer", "bss", + "autocode", "curv", "deno", "deno-staging", @@ -12623,8 +12326,11 @@ var nodeLabels = [...]string{ "localcert", "loginline", "mediatech", + "ngrok", + "ngrok-free", "pages", "platter-app", + "r2", "shiftcrypto", "stg", "stgstage", @@ -13016,6 +12722,7 @@ var nodeLabels = [...]string{ "net", "org", "blogspot", + "mytabit", "ravpage", "tabitorder", "ac", @@ -13176,6 +12883,13 @@ var nodeLabels = [...]string{ "dyndns", "id", "apps", + "ap", + "au", + "eu", + "in", + "jp", + "sa", + "us", "stage", "mock", "sys", @@ -13649,6 +13363,7 @@ var nodeLabels = [...]string{ "net", "org", "sch", + "2-d", "ac", "ad", "aichi", @@ -13662,6 +13377,7 @@ var nodeLabels = [...]string{ "bitter", "blogspot", "blush", + "bona", "boo", "boy", "boyfriend", @@ -13682,18 +13398,22 @@ var nodeLabels = [...]string{ "cocotte", "coolblog", "cranky", + "crap", "cutegirl", "daa", + "daynight", "deca", "deci", "digick", "ed", + "eek", "egoism", "ehime", "fakefur", "fashionstore", "fem", "flier", + "flop", "floppy", "fool", "frenchkiss", @@ -13710,6 +13430,7 @@ var nodeLabels = [...]string{ "greater", "gunma", "hacca", + "halfmoon", "handcrafted", "heavy", "her", @@ -13725,6 +13446,7 @@ var nodeLabels = [...]string{ "ishikawa", "itigo", "iwate", + "jeez", "jellybean", "kagawa", "kagoshima", @@ -13748,7 +13470,9 @@ var nodeLabels = [...]string{ "lovepop", "lovesick", "main", + "matrix", "mie", + "mimoza", "miyagi", "miyazaki", "mods", @@ -13761,10 +13485,13 @@ var nodeLabels = [...]string{ "namaste", "nara", "ne", + "netgamers", "niigata", "nikita", "nobushi", "noor", + "nyanta", + "o0o0", "oita", "okayama", "okinawa", @@ -13785,22 +13512,30 @@ var nodeLabels = [...]string{ "pussycat", "pya", "raindrop", + "rdy", "readymade", + "rgr", + "rulez", "sadist", "saga", "saitama", + "sakurastorage", + "saloon", "sapporo", + "sblo", "schoolbus", "secret", "sendai", "shiga", "shimane", "shizuoka", + "skr", "staba", "stripper", "sub", "sunnyday", "supersale", + "tank", "theshop", "thick", "tochigi", @@ -13809,7 +13544,9 @@ var nodeLabels = [...]string{ "tonkotsu", "tottori", "toyama", + "uh-oh", "under", + "undo", "upper", "usercontent", "velvet", @@ -13818,8 +13555,11 @@ var nodeLabels = [...]string{ "vivian", "wakayama", "watson", + "webaccel", "weblike", + "websozai", "whitesnow", + "xii", "xn--0trq7p7nn", "xn--1ctwo", "xn--1lqs03n", @@ -14954,6 +14694,14 @@ var nodeLabels = [...]string{ "yoshino", "aseinet", "gehirn", + "ivory", + "mail-box", + "mints", + "mokuren", + "opal", + "sakura", + "sumomo", + "topaz", "user", "aga", "agano", @@ -15221,6 +14969,10 @@ var nodeLabels = [...]string{ "yoshida", "yoshikawa", "yoshimi", + "isk01", + "isk02", + "s3", + "s3", "city", "city", "aisho", @@ -15476,6 +15228,8 @@ var nodeLabels = [...]string{ "wakayama", "yuasa", "yura", + "rs", + "user", "asahi", "funagata", "higashine", @@ -15865,552 +15619,6 @@ var nodeLabels = [...]string{ "net", "or", "org", - "academy", - "agriculture", - "air", - "airguard", - "alabama", - "alaska", - "amber", - "ambulance", - "american", - "americana", - "americanantiques", - "americanart", - "amsterdam", - "and", - "annefrank", - "anthro", - "anthropology", - "antiques", - "aquarium", - "arboretum", - "archaeological", - "archaeology", - "architecture", - "art", - "artanddesign", - "artcenter", - "artdeco", - "arteducation", - "artgallery", - "arts", - "artsandcrafts", - "asmatart", - "assassination", - "assisi", - "association", - "astronomy", - "atlanta", - "austin", - "australia", - "automotive", - "aviation", - "axis", - "badajoz", - "baghdad", - "bahn", - "bale", - "baltimore", - "barcelona", - "baseball", - "basel", - "baths", - "bauern", - "beauxarts", - "beeldengeluid", - "bellevue", - "bergbau", - "berkeley", - "berlin", - "bern", - "bible", - "bilbao", - "bill", - "birdart", - "birthplace", - "bonn", - "boston", - "botanical", - "botanicalgarden", - "botanicgarden", - "botany", - "brandywinevalley", - "brasil", - "bristol", - "british", - "britishcolumbia", - "broadcast", - "brunel", - "brussel", - "brussels", - "bruxelles", - "building", - "burghof", - "bus", - "bushey", - "cadaques", - "california", - "cambridge", - "can", - "canada", - "capebreton", - "carrier", - "cartoonart", - "casadelamoneda", - "castle", - "castres", - "celtic", - "center", - "chattanooga", - "cheltenham", - "chesapeakebay", - "chicago", - "children", - "childrens", - "childrensgarden", - "chiropractic", - "chocolate", - "christiansburg", - "cincinnati", - "cinema", - "circus", - "civilisation", - "civilization", - "civilwar", - "clinton", - "clock", - "coal", - "coastaldefence", - "cody", - "coldwar", - "collection", - "colonialwilliamsburg", - "coloradoplateau", - "columbia", - "columbus", - "communication", - "communications", - "community", - "computer", - "computerhistory", - "contemporary", - "contemporaryart", - "convent", - "copenhagen", - "corporation", - "corvette", - "costume", - "countryestate", - "county", - "crafts", - "cranbrook", - "creation", - "cultural", - "culturalcenter", - "culture", - "cyber", - "cymru", - "dali", - "dallas", - "database", - "ddr", - "decorativearts", - "delaware", - "delmenhorst", - "denmark", - "depot", - "design", - "detroit", - "dinosaur", - "discovery", - "dolls", - "donostia", - "durham", - "eastafrica", - "eastcoast", - "education", - "educational", - "egyptian", - "eisenbahn", - "elburg", - "elvendrell", - "embroidery", - "encyclopedic", - "england", - "entomology", - "environment", - "environmentalconservation", - "epilepsy", - "essex", - "estate", - "ethnology", - "exeter", - "exhibition", - "family", - "farm", - "farmequipment", - "farmers", - "farmstead", - "field", - "figueres", - "filatelia", - "film", - "fineart", - "finearts", - "finland", - "flanders", - "florida", - "force", - "fortmissoula", - "fortworth", - "foundation", - "francaise", - "frankfurt", - "franziskaner", - "freemasonry", - "freiburg", - "fribourg", - "frog", - "fundacio", - "furniture", - "gallery", - "garden", - "gateway", - "geelvinck", - "gemological", - "geology", - "georgia", - "giessen", - "glas", - "glass", - "gorge", - "grandrapids", - "graz", - "guernsey", - "halloffame", - "hamburg", - "handson", - "harvestcelebration", - "hawaii", - "health", - "heimatunduhren", - "hellas", - "helsinki", - "hembygdsforbund", - "heritage", - "histoire", - "historical", - "historicalsociety", - "historichouses", - "historisch", - "historisches", - "history", - "historyofscience", - "horology", - "house", - "humanities", - "illustration", - "imageandsound", - "indian", - "indiana", - "indianapolis", - "indianmarket", - "intelligence", - "interactive", - "iraq", - "iron", - "isleofman", - "jamison", - "jefferson", - "jerusalem", - "jewelry", - "jewish", - "jewishart", - "jfk", - "journalism", - "judaica", - "judygarland", - "juedisches", - "juif", - "karate", - "karikatur", - "kids", - "koebenhavn", - "koeln", - "kunst", - "kunstsammlung", - "kunstunddesign", - "labor", - "labour", - "lajolla", - "lancashire", - "landes", - "lans", - "larsson", - "lewismiller", - "lincoln", - "linz", - "living", - "livinghistory", - "localhistory", - "london", - "losangeles", - "louvre", - "loyalist", - "lucerne", - "luxembourg", - "luzern", - "mad", - "madrid", - "mallorca", - "manchester", - "mansion", - "mansions", - "manx", - "marburg", - "maritime", - "maritimo", - "maryland", - "marylhurst", - "media", - "medical", - "medizinhistorisches", - "meeres", - "memorial", - "mesaverde", - "michigan", - "midatlantic", - "military", - "mill", - "miners", - "mining", - "minnesota", - "missile", - "missoula", - "modern", - "moma", - "money", - "monmouth", - "monticello", - "montreal", - "moscow", - "motorcycle", - "muenchen", - "muenster", - "mulhouse", - "muncie", - "museet", - "museumcenter", - "museumvereniging", - "music", - "national", - "nationalfirearms", - "nationalheritage", - "nativeamerican", - "naturalhistory", - "naturalhistorymuseum", - "naturalsciences", - "nature", - "naturhistorisches", - "natuurwetenschappen", - "naumburg", - "naval", - "nebraska", - "neues", - "newhampshire", - "newjersey", - "newmexico", - "newport", - "newspaper", - "newyork", - "niepce", - "norfolk", - "north", - "nrw", - "nyc", - "nyny", - "oceanographic", - "oceanographique", - "omaha", - "online", - "ontario", - "openair", - "oregon", - "oregontrail", - "otago", - "oxford", - "pacific", - "paderborn", - "palace", - "paleo", - "palmsprings", - "panama", - "paris", - "pasadena", - "pharmacy", - "philadelphia", - "philadelphiaarea", - "philately", - "phoenix", - "photography", - "pilots", - "pittsburgh", - "planetarium", - "plantation", - "plants", - "plaza", - "portal", - "portland", - "portlligat", - "posts-and-telecommunications", - "preservation", - "presidio", - "press", - "project", - "public", - "pubol", - "quebec", - "railroad", - "railway", - "research", - "resistance", - "riodejaneiro", - "rochester", - "rockart", - "roma", - "russia", - "saintlouis", - "salem", - "salvadordali", - "salzburg", - "sandiego", - "sanfrancisco", - "santabarbara", - "santacruz", - "santafe", - "saskatchewan", - "satx", - "savannahga", - "schlesisches", - "schoenbrunn", - "schokoladen", - "school", - "schweiz", - "science", - "science-fiction", - "scienceandhistory", - "scienceandindustry", - "sciencecenter", - "sciencecenters", - "sciencehistory", - "sciences", - "sciencesnaturelles", - "scotland", - "seaport", - "settlement", - "settlers", - "shell", - "sherbrooke", - "sibenik", - "silk", - "ski", - "skole", - "society", - "sologne", - "soundandvision", - "southcarolina", - "southwest", - "space", - "spy", - "square", - "stadt", - "stalbans", - "starnberg", - "state", - "stateofdelaware", - "station", - "steam", - "steiermark", - "stjohn", - "stockholm", - "stpetersburg", - "stuttgart", - "suisse", - "surgeonshall", - "surrey", - "svizzera", - "sweden", - "sydney", - "tank", - "tcm", - "technology", - "telekommunikation", - "television", - "texas", - "textile", - "theater", - "time", - "timekeeping", - "topology", - "torino", - "touch", - "town", - "transport", - "tree", - "trolley", - "trust", - "trustee", - "uhren", - "ulm", - "undersea", - "university", - "usa", - "usantiques", - "usarts", - "uscountryestate", - "usculture", - "usdecorativearts", - "usgarden", - "ushistory", - "ushuaia", - "uslivinghistory", - "utah", - "uvic", - "valley", - "vantaa", - "versailles", - "viking", - "village", - "virginia", - "virtual", - "virtuel", - "vlaanderen", - "volkenkunde", - "wales", - "wallonie", - "war", - "washingtondc", - "watch-and-clock", - "watchandclock", - "western", - "westfalen", - "whaling", - "wildlife", - "williamsburg", - "windmill", - "workshop", - "xn--9dbhblg6di", - "xn--comunicaes-v6a2o", - "xn--correios-e-telecomunicaes-ghc29a", - "xn--h1aegh", - "xn--lns-qla", - "york", - "yorkshire", - "yosemite", - "youth", - "zoological", - "zoology", "aero", "biz", "com", @@ -16483,6 +15691,19 @@ var nodeLabels = [...]string{ "asso", "nom", "adobeaemcloud", + "adobeio-static", + "adobeioruntime", + "akadns", + "akamai", + "akamai-staging", + "akamaiedge", + "akamaiedge-staging", + "akamaihd", + "akamaihd-staging", + "akamaiorigin", + "akamaiorigin-staging", + "akamaized", + "akamaized-staging", "alwaysdata", "appudo", "at-band-camp", @@ -16532,6 +15753,10 @@ var nodeLabels = [...]string{ "dynv6", "eating-organic", "edgeapp", + "edgekey", + "edgekey-staging", + "edgesuite", + "edgesuite-staging", "elastx", "endofinternet", "familyds", @@ -16612,6 +15837,7 @@ var nodeLabels = [...]string{ "shopselect", "siteleaf", "square7", + "squares", "srcf", "static-access", "supabase", @@ -16634,6 +15860,7 @@ var nodeLabels = [...]string{ "cdn", "1", "2", + "3", "centralus", "eastasia", "eastus2", @@ -17619,6 +16846,7 @@ var nodeLabels = [...]string{ "is-very-nice", "is-very-sweet", "isa-geek", + "jpn", "js", "kicks-ass", "mayfirst", @@ -17774,6 +17002,7 @@ var nodeLabels = [...]string{ "org", "framer", "1337", + "ngrok", "biz", "com", "edu", @@ -17978,12 +17207,17 @@ var nodeLabels = [...]string{ "kwpsp", "mup", "mw", + "oia", "oirm", + "oke", + "oow", + "oschr", "oum", "pa", "pinb", "piw", "po", + "pr", "psp", "psse", "pup", @@ -18009,11 +17243,14 @@ var nodeLabels = [...]string{ "wios", "witd", "wiw", + "wkz", "wsa", "wskr", + "wsse", "wuoz", "wzmiuw", "zp", + "zpisdn", "co", "name", "own", @@ -18355,6 +17592,7 @@ var nodeLabels = [...]string{ "consulado", "edu", "embaixada", + "kirara", "mil", "net", "noho", @@ -18501,6 +17739,7 @@ var nodeLabels = [...]string{ "quickconnect", "rdv", "vpnplus", + "x0", "direct", "prequalifyme", "now-dns", @@ -18549,7 +17788,9 @@ var nodeLabels = [...]string{ "travel", "better-than", "dyndns", + "from", "on-the-web", + "sakura", "worse-than", "blogspot", "club", @@ -18602,6 +17843,7 @@ var nodeLabels = [...]string{ "dp", "edu", "gov", + "ie", "if", "in", "inf", @@ -18616,6 +17858,7 @@ var nodeLabels = [...]string{ "kirovograd", "km", "kr", + "kropyvnytskyi", "krym", "ks", "kv", @@ -19010,18 +18253,84 @@ var nodeLabels = [...]string{ "net", "org", "ac", + "ai", + "angiang", + "bacgiang", + "backan", + "baclieu", + "bacninh", + "baria-vungtau", + "bentre", + "binhdinh", + "binhduong", + "binhphuoc", + "binhthuan", "biz", "blogspot", + "camau", + "cantho", + "caobang", "com", + "daklak", + "daknong", + "danang", + "dienbien", + "dongnai", + "dongthap", "edu", + "gialai", "gov", + "hagiang", + "haiduong", + "haiphong", + "hanam", + "hanoi", + "hatinh", + "haugiang", "health", + "hoabinh", + "hungyen", + "id", "info", "int", + "io", + "khanhhoa", + "kiengiang", + "kontum", + "laichau", + "lamdong", + "langson", + "laocai", + "longan", + "namdinh", "name", "net", + "nghean", + "ninhbinh", + "ninhthuan", "org", + "phutho", + "phuyen", "pro", + "quangbinh", + "quangnam", + "quangngai", + "quangninh", + "quangtri", + "soctrang", + "sonla", + "tayninh", + "thaibinh", + "thainguyen", + "thanhhoa", + "thanhphohochiminh", + "thuathienhue", + "tiengiang", + "travinh", + "tuyenquang", + "vinhlong", + "vinhphuc", + "yenbai", "blog", "cn", "com", From f09e75378f8ee74a1ff2e883d590eac175d93fea Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 2 Aug 2023 12:41:31 -0700 Subject: [PATCH 026/168] quic: send and receive stream data Send and receive data in STREAM frames. Write-close streams and communicate the final size in a STREAM frame with the FIN bit. Return io.EOF on reads at the end of a stream. Handle stream-level flow control. Send window updates in MAX_STREAM_DATA frames, send STREAM_DATA_BLOCKED when flow control is not available. Does not include connection-level flow control, read-closing, aborting, or removing streams from a conn after both sides have closed the stream. For golang/go#58547 Change-Id: Ib2b449bf54eb6cf200c4f6e2dd2c33274dda3387 Reviewed-on: https://go-review.googlesource.com/c/net/+/515815 Run-TryBot: Damien Neil Reviewed-by: Jonathan Amsterdam TryBot-Result: Gopher Robot --- internal/quic/config.go | 25 + internal/quic/conn.go | 3 + internal/quic/conn_loss.go | 8 + internal/quic/conn_loss_test.go | 270 +++++++++++ internal/quic/conn_recv.go | 13 +- internal/quic/conn_streams.go | 31 +- internal/quic/conn_test.go | 2 + internal/quic/crypto_stream.go | 23 +- internal/quic/gate.go | 12 +- internal/quic/quic_test.go | 37 ++ internal/quic/stream.go | 394 ++++++++++++++-- internal/quic/stream_test.go | 794 ++++++++++++++++++++++++++++++++ 12 files changed, 1549 insertions(+), 63 deletions(-) create mode 100644 internal/quic/quic_test.go diff --git a/internal/quic/config.go b/internal/quic/config.go index 7d1b7433af..df493579f2 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -17,4 +17,29 @@ type Config struct { // TLSConfig is the endpoint's TLS configuration. // It must be non-nil and include at least one certificate or else set GetCertificate. TLSConfig *tls.Config + + // StreamReadBufferSize is the maximum amount of data sent by the peer that a + // stream will buffer for reading. + // If zero, the default value of 1MiB is used. + // If negative, the limit is zero. + StreamReadBufferSize int64 + + // StreamWriteBufferSize is the maximum amount of data a stream will buffer for + // sending to the peer. + // If zero, the default value of 1MiB is used. + // If negative, the limit is zero. + StreamWriteBufferSize int64 } + +func configDefault(v, def int64) int64 { + switch v { + case -1: + return 0 + case 0: + return def + } + return v +} + +func (c *Config) streamReadBufferSize() int64 { return configDefault(c.StreamReadBufferSize, 1<<20) } +func (c *Config) streamWriteBufferSize() int64 { return configDefault(c.StreamWriteBufferSize, 1<<20) } diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 90e6739630..0952a79e81 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -160,6 +160,9 @@ func (c *Conn) discardKeys(now time.Time, space numberSpace) { // receiveTransportParameters applies transport parameters sent by the peer. func (c *Conn) receiveTransportParameters(p transportParameters) error { + c.streams.peerInitialMaxStreamDataBidiLocal = p.initialMaxStreamDataBidiLocal + c.streams.peerInitialMaxStreamDataRemote[bidiStream] = p.initialMaxStreamDataBidiRemote + c.streams.peerInitialMaxStreamDataRemote[uniStream] = p.initialMaxStreamDataUni c.peerAckDelayExponent = p.ackDelayExponent c.loss.setMaxAckDelay(p.maxAckDelay) if err := c.connIDState.setPeerActiveConnIDLimit(p.activeConnIDLimit, c.newConnIDFunc()); err != nil { diff --git a/internal/quic/conn_loss.go b/internal/quic/conn_loss.go index ca178089d2..f42f7e5282 100644 --- a/internal/quic/conn_loss.go +++ b/internal/quic/conn_loss.go @@ -44,6 +44,14 @@ func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetF case frameTypeCrypto: start, end := sent.nextRange() c.crypto[space].ackOrLoss(start, end, fate) + case frameTypeMaxStreamData, + frameTypeStreamDataBlocked: + id := streamID(sent.nextInt()) + s := c.streamForID(id) + if s == nil { + continue + } + s.ackOrLoss(sent.num, f, fate) case frameTypeStreamBase, frameTypeStreamBase | streamFinBit: id := streamID(sent.nextInt()) diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index e3d16a7baa..d9445150a3 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -7,7 +7,9 @@ package quic import ( + "context" "crypto/tls" + "fmt" "testing" ) @@ -145,7 +147,275 @@ func TestLostStreamFrameEmpty(t *testing.T) { data: []byte{}, }) }) +} + +func TestLostStreamWithData(t *testing.T) { + // "Application data sent in STREAM frames is retransmitted in new STREAM + // frames unless the endpoint has sent a RESET_STREAM for that stream." + // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.2 + // + // TODO: Lost stream frame after RESET_STREAM + lostFrameTest(t, func(t *testing.T, pto bool) { + data := []byte{0, 1, 2, 3, 4, 5, 6, 7} + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) { + p.initialMaxStreamsUni = 1 + p.initialMaxData = 1 << 20 + p.initialMaxStreamDataUni = 1 << 20 + }) + s.Write(data[:4]) + tc.wantFrame("send [0,4)", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + data: data[:4], + }) + s.Write(data[4:8]) + tc.wantFrame("send [4,8)", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 4, + data: data[4:8], + }) + s.Close() + tc.wantFrame("send FIN", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 8, + fin: true, + data: []byte{}, + }) + + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("resend data", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + fin: true, + data: data[:8], + }) + }) +} + +func TestLostStreamPartialLoss(t *testing.T) { + // Conn sends four STREAM packets. + // ACKs are received for the packets containing bytes 0 and 2. + // The remaining packets are declared lost. + // The Conn resends only the lost data. + // + // This test doesn't have a PTO mode, because the ACK for the packet containing byte 2 + // starts the loss timer for the packet containing byte 1, and the PTO timer is not + // armed when the loss timer is. + data := []byte{0, 1, 2, 3} + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) { + p.initialMaxStreamsUni = 1 + p.initialMaxData = 1 << 20 + p.initialMaxStreamDataUni = 1 << 20 + }) + for i := range data { + s.Write(data[i : i+1]) + tc.wantFrame(fmt.Sprintf("send STREAM frame with byte %v", i), + packetType1RTT, debugFrameStream{ + id: s.id, + off: int64(i), + data: data[i : i+1], + }) + if i%2 == 0 { + num := tc.sentFramePacket.num + tc.writeFrames(packetType1RTT, debugFrameAck{ + ranges: []i64range[packetNumber]{ + {num, num + 1}, + }, + }) + } + } + const pto = false + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("resend byte 1", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 1, + data: data[1:2], + }) + tc.wantFrame("resend byte 3", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 3, + data: data[3:4], + }) + tc.wantIdle("no more frames sent after packet loss") +} + +func TestLostMaxStreamDataFrame(t *testing.T) { + // "[...] an updated value is sent when the packet containing + // the most recent MAX_STREAM_DATA frame for a stream is lost" + // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.8 + lostFrameTest(t, func(t *testing.T, pto bool) { + const maxWindowSize = 10 + buf := make([]byte, maxWindowSize) + tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { + c.StreamReadBufferSize = maxWindowSize + }) + + // We send MAX_STREAM_DATA = 19. + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + data: make([]byte, maxWindowSize), + }) + if n, err := s.Read(buf[:maxWindowSize-1]); err != nil || n != maxWindowSize-1 { + t.Fatalf("Read() = %v, %v; want %v, nil", n, err, maxWindowSize-1) + } + tc.wantFrame("stream window is extended after reading data", + packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: (maxWindowSize * 2) - 1, + }) + + // MAX_STREAM_DATA = 20, which is only one more byte, so we don't send the frame. + if n, err := s.Read(buf); err != nil || n != 1 { + t.Fatalf("Read() = %v, %v; want %v, nil", n, err, 1) + } + tc.wantIdle("read doesn't extend window enough to send another MAX_STREAM_DATA") + + // The MAX_STREAM_DATA = 19 packet was lost, so we send 20. + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("resent MAX_STREAM_DATA includes most current value", + packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: maxWindowSize * 2, + }) + }) +} + +func TestLostMaxStreamDataFrameAfterStreamFinReceived(t *testing.T) { + // "An endpoint SHOULD stop sending MAX_STREAM_DATA frames when + // the receiving part of the stream enters a "Size Known" or "Reset Recvd" state." + // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.8 + lostFrameTest(t, func(t *testing.T, pto bool) { + const maxWindowSize = 10 + buf := make([]byte, maxWindowSize) + tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { + c.StreamReadBufferSize = maxWindowSize + }) + + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + data: make([]byte, maxWindowSize), + }) + if n, err := s.Read(buf); err != nil || n != maxWindowSize { + t.Fatalf("Read() = %v, %v; want %v, nil", n, err, maxWindowSize) + } + tc.wantFrame("stream window is extended after reading data", + packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: 2 * maxWindowSize, + }) + + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + off: maxWindowSize, + fin: true, + }) + + tc.ignoreFrame(frameTypePing) + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantIdle("lost MAX_STREAM_DATA not resent for stream in 'size known'") + }) +} + +func TestLostStreamDataBlockedFrame(t *testing.T) { + // "A new [STREAM_DATA_BLOCKED] frame is sent if a packet containing + // the most recent frame for a scope is lost [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.10 + lostFrameTest(t, func(t *testing.T, pto bool) { + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) { + p.initialMaxStreamsUni = 1 + p.initialMaxData = 1 << 20 + }) + + w := runAsync(tc, func(ctx context.Context) (int, error) { + return s.WriteContext(ctx, []byte{0, 1, 2, 3}) + }) + defer w.cancel() + tc.wantFrame("write is blocked by flow control", + packetType1RTT, debugFrameStreamDataBlocked{ + id: s.id, + max: 0, + }) + + tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: 1, + }) + tc.wantFrame("write makes some progress, but is still blocked by flow control", + packetType1RTT, debugFrameStreamDataBlocked{ + id: s.id, + max: 1, + }) + tc.wantFrame("write consuming available window", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + data: []byte{0}, + }) + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("STREAM_DATA_BLOCKED is resent", + packetType1RTT, debugFrameStreamDataBlocked{ + id: s.id, + max: 1, + }) + tc.wantFrame("STREAM is resent as well", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + data: []byte{0}, + }) + }) +} + +func TestLostStreamDataBlockedFrameAfterStreamUnblocked(t *testing.T) { + // "A new [STREAM_DATA_BLOCKED] frame is sent [...] only while + // the endpoint is blocked on the corresponding limit." + // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.10 + lostFrameTest(t, func(t *testing.T, pto bool) { + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) { + p.initialMaxStreamsUni = 1 + p.initialMaxData = 1 << 20 + }) + + data := []byte{0, 1, 2, 3} + w := runAsync(tc, func(ctx context.Context) (int, error) { + return s.WriteContext(ctx, data) + }) + defer w.cancel() + tc.wantFrame("write is blocked by flow control", + packetType1RTT, debugFrameStreamDataBlocked{ + id: s.id, + max: 0, + }) + + tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: 10, + }) + tc.wantFrame("write completes after flow control available", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + data: data, + }) + + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("STREAM data is resent", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + data: data, + }) + tc.wantIdle("STREAM_DATA_BLOCKED is not resent, since the stream is not blocked") + }) } func TestLostNewConnectionIDFrame(t *testing.T) { diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 45ef3844e8..00985b6703 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -191,7 +191,7 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, if !frameOK(c, ptype, __01) { return } - _, _, n = consumeMaxStreamDataFrame(payload) + n = c.handleMaxStreamDataFrame(now, payload) case frameTypeMaxStreamsBidi, frameTypeMaxStreamsUni: if !frameOK(c, ptype, __01) { return @@ -280,6 +280,17 @@ func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) return n } +func (c *Conn) handleMaxStreamDataFrame(now time.Time, payload []byte) int { + id, maxStreamData, n := consumeMaxStreamDataFrame(payload) + if s := c.streamForFrame(now, id, sendStream); s != nil { + if err := s.handleMaxStreamData(maxStreamData); err != nil { + c.abort(now, err) + return -1 + } + } + return n +} + func (c *Conn) handleCryptoFrame(now time.Time, space numberSpace, payload []byte) int { off, data, n := consumeCryptoFrame(payload) err := c.handleCrypto(now, space, off, data) diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index f626323b5a..7a531f52b2 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -20,6 +20,10 @@ type streamsState struct { streams map[streamID]*Stream opened [streamTypeCount]int64 // number of streams opened by us + // Peer configuration provided in transport parameters. + peerInitialMaxStreamDataRemote [streamTypeCount]int64 // streams opened by us + peerInitialMaxStreamDataBidiLocal int64 // streams opened by them + // Streams with frames to send are stored in a circular linked list. // sendHead is the next stream to write, or nil if there are no streams // with data to send. sendTail is the last stream to write. @@ -55,15 +59,24 @@ func (c *Conn) NewSendOnlyStream(ctx context.Context) (*Stream, error) { return c.newLocalStream(ctx, uniStream) } -func (c *Conn) newLocalStream(ctx context.Context, typ streamType) (*Stream, error) { +func (c *Conn) newLocalStream(ctx context.Context, styp streamType) (*Stream, error) { // TODO: Stream limits. c.streams.streamsMu.Lock() defer c.streams.streamsMu.Unlock() - num := c.streams.opened[typ] - c.streams.opened[typ]++ + num := c.streams.opened[styp] + c.streams.opened[styp]++ + + s := newStream(c, newStreamID(c.side, styp, num)) + s.outmaxbuf = c.config.streamWriteBufferSize() + s.outwin = c.streams.peerInitialMaxStreamDataRemote[styp] + if styp == bidiStream { + s.inmaxbuf = c.config.streamReadBufferSize() + s.inwin = c.config.streamReadBufferSize() + } + s.inUnlock() + s.outUnlock() - s := newStream(c, newStreamID(c.side, typ, num)) c.streams.streams[s.id] = s return s, nil } @@ -117,7 +130,17 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) c.abort(now, localTransportError(errStreamState)) return nil } + s := newStream(c, id) + s.inmaxbuf = c.config.streamReadBufferSize() + s.inwin = c.config.streamReadBufferSize() + if id.streamType() == bidiStream { + s.outmaxbuf = c.config.streamWriteBufferSize() + s.outwin = c.streams.peerInitialMaxStreamDataBidiLocal + } + s.inUnlock() + s.outUnlock() + c.streams.streams[id] = s c.streams.queue.put(s) return s diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 5aad69f4d1..2480f9cb0a 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -179,6 +179,8 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { peerProvidedParams := defaultTransportParameters() for _, o := range opts { switch o := o.(type) { + case func(*Config): + o(config) case func(*tls.Config): o(config.TLSConfig) case func(p *transportParameters): diff --git a/internal/quic/crypto_stream.go b/internal/quic/crypto_stream.go index 6cda6578c1..75dea87d0d 100644 --- a/internal/quic/crypto_stream.go +++ b/internal/quic/crypto_stream.go @@ -118,28 +118,7 @@ func (s *cryptoStream) ackOrLoss(start, end int64, fate packetFate) { // copy the data it wants into position. func (s *cryptoStream) dataToSend(pto bool, f func(off, size int64) (sent int64)) { for { - var off, size int64 - if pto { - // On PTO, resend unacked data that fits in the probe packet. - // For simplicity, we send the range starting at s.out.start - // (which is definitely unacked, or else we would have discarded it) - // up to the next acked byte (if any). - // - // This may miss unacked data starting after that acked byte, - // but avoids resending data the peer has acked. - off = s.out.start - end := s.out.end - for _, r := range s.outacked { - if r.start > off { - end = r.start - break - } - } - size = end - s.out.start - } else if s.outunsent.numRanges() > 0 { - off = s.outunsent.min() - size = s.outunsent[0].size() - } + off, size := dataToSend(s.out, s.outunsent, s.outacked, pto) if size == 0 { return } diff --git a/internal/quic/gate.go b/internal/quic/gate.go index efb28daf8f..27ab07a6f9 100644 --- a/internal/quic/gate.go +++ b/internal/quic/gate.go @@ -20,13 +20,19 @@ type gate struct { unset chan struct{} } +// newGate returns a new, unlocked gate with the condition unset. func newGate() gate { - g := gate{ + g := newLockedGate() + g.unlock(false) + return g +} + +// newLocked gate returns a new, locked gate. +func newLockedGate() gate { + return gate{ set: make(chan struct{}, 1), unset: make(chan struct{}, 1), } - g.unset <- struct{}{} - return g } // lock acquires the gate unconditionally. diff --git a/internal/quic/quic_test.go b/internal/quic/quic_test.go new file mode 100644 index 0000000000..1281b54eec --- /dev/null +++ b/internal/quic/quic_test.go @@ -0,0 +1,37 @@ +// 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" +) + +func testSides(t *testing.T, name string, f func(*testing.T, connSide)) { + if name != "" { + name += "/" + } + t.Run(name+"server", func(t *testing.T) { f(t, serverSide) }) + t.Run(name+"client", func(t *testing.T) { f(t, clientSide) }) +} + +func testStreamTypes(t *testing.T, name string, f func(*testing.T, streamType)) { + if name != "" { + name += "/" + } + t.Run(name+"bidi", func(t *testing.T) { f(t, bidiStream) }) + t.Run(name+"uni", func(t *testing.T) { f(t, uniStream) }) +} + +func testSidesAndStreamTypes(t *testing.T, name string, f func(*testing.T, connSide, streamType)) { + if name != "" { + name += "/" + } + t.Run(name+"server/bidi", func(t *testing.T) { f(t, serverSide, bidiStream) }) + t.Run(name+"client/bidi", func(t *testing.T) { f(t, clientSide, bidiStream) }) + t.Run(name+"server/uni", func(t *testing.T) { f(t, serverSide, uniStream) }) + t.Run(name+"client/uni", func(t *testing.T) { f(t, clientSide, uniStream) }) +} diff --git a/internal/quic/stream.go b/internal/quic/stream.go index b55f927e02..83215dfd3e 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -9,34 +9,57 @@ package quic import ( "context" "errors" + "io" ) type Stream struct { id streamID conn *Conn + // ingate's lock guards all receive-related state. + // + // The gate condition is set if a read from the stream will not block, + // either because the stream has available data or because the read will fail. + ingate gate + in pipe // received data + inwin int64 // last MAX_STREAM_DATA sent to the peer + insendmax sentVal // set when we should send MAX_STREAM_DATA to the peer + inmaxbuf int64 // maximum amount of data we will buffer + insize int64 // stream final size; -1 before this is known + inset rangeset[int64] // received ranges + // outgate's lock guards all send-related state. // // The gate condition is set if a write to the stream will not block, // either because the stream has available flow control or because // the write will fail. - outgate gate - outopened sentVal // set if we should open the stream + outgate gate + out pipe // buffered data to send + outwin int64 // maximum MAX_STREAM_DATA received from the peer + outmaxbuf int64 // maximum amount of data we will buffer + outunsent rangeset[int64] // ranges buffered but not yet sent + outacked rangeset[int64] // ranges sent and acknowledged + outopened sentVal // set if we should open the stream + outclosed sentVal // set by CloseWrite + outblocked sentVal // set when a write to the stream is blocked by flow control prev, next *Stream // guarded by streamsState.sendMu } +// newStream returns a new stream. +// +// The stream's ingate and outgate are locked. +// (We create the stream with locked gates so after the caller +// initializes the flow control window, +// unlocking outgate will set the stream writability state.) func newStream(c *Conn, id streamID) *Stream { s := &Stream{ conn: c, id: id, - outgate: newGate(), + insize: -1, // -1 indicates the stream size is unknown + ingate: newLockedGate(), + outgate: newLockedGate(), } - - // Lock and unlock outgate to update the stream writability state. - s.outgate.lock() - s.outUnlock() - return s } @@ -66,8 +89,48 @@ func (s *Stream) Read(b []byte) (n int, err error) { // returning all data sent by the peer. // If the peer terminates reads abruptly, ReadContext returns StreamResetError. func (s *Stream) ReadContext(ctx context.Context, b []byte) (n int, err error) { - // TODO: implement - return 0, errors.New("unimplemented") + if s.IsWriteOnly() { + return 0, errors.New("read from write-only stream") + } + // Wait until data is available. + if err := s.conn.waitAndLockGate(ctx, &s.ingate); err != nil { + return 0, err + } + defer s.inUnlock() + if s.insize == s.in.start { + return 0, io.EOF + } + // Getting here indicates the stream contains data to be read. + if len(s.inset) < 1 || s.inset[0].start != 0 || s.inset[0].end <= s.in.start { + panic("BUG: inconsistent input stream state") + } + if size := int(s.inset[0].end - s.in.start); size < len(b) { + b = b[:size] + } + start := s.in.start + end := start + int64(len(b)) + s.in.copy(start, b) + s.in.discardBefore(end) + if s.insize == -1 || s.insize > s.inwin { + if shouldUpdateFlowControl(s.inwin-s.in.start, s.inmaxbuf) { + // Update stream flow control with a STREAM_MAX_DATA frame. + s.insendmax.setUnsent() + } + } + if end == s.insize { + return len(b), io.EOF + } + return len(b), nil +} + +// shouldUpdateFlowControl determines whether to send a flow control window update. +// +// We want to balance keeping the peer well-supplied with flow control with not sending +// many small updates. +func shouldUpdateFlowControl(curwin, maxwin int64) bool { + // Update flow control if doing so gives the peer at least 64k tokens, + // or if it will double the current window. + return maxwin-curwin >= 64<<10 || curwin*2 < maxwin } // Write writes data to the stream. @@ -87,65 +150,330 @@ func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) if s.IsReadOnly() { return 0, errors.New("write to read-only stream") } - if len(b) > 0 { - // TODO: implement - return 0, errors.New("unimplemented") + canWrite := s.outgate.lock() + if s.outclosed.isSet() { + s.outUnlock() + return 0, errors.New("write to closed stream") } - if err := s.outgate.waitAndLockContext(ctx); err != nil { - return 0, err + if len(b) == 0 { + // We aren't writing any data, but send a STREAM frame to open the stream + // if we haven't done so already. + s.outopened.set() + } + for len(b) > 0 { + // The first time through this loop, we may or may not be write blocked. + // We exit the loop after writing all data, so on subsequent passes through + // the loop we are always write blocked. + if !canWrite { + // We're blocked, either by flow control or by our own buffer limit. + // We either need the peer to extend our flow control window, + // or ack some of our outstanding packets. + if s.out.end == s.outwin { + // We're blocked by flow control. + // Send a STREAM_DATA_BLOCKED frame to let the peer know. + s.outblocked.setUnsent() + } + s.outUnlock() + if err := s.conn.waitAndLockGate(ctx, &s.outgate); err != nil { + return n, err + } + // Successfully returning from waitAndLockGate means we are no longer + // write blocked. (Unlike traditional condition variables, gates do not + // have spurious wakeups.) + } + s.outblocked.clear() + // Write limit is min(our own buffer limit, the peer-provided flow control window). + // This is a stream offset. + lim := min(s.out.start+s.outmaxbuf, s.outwin) + // Amount to write is min(the full buffer, data up to the write limit). + // This is a number of bytes. + nn := min(int64(len(b)), lim-s.out.end) + // Copy the data into the output buffer and mark it as unsent. + s.outunsent.add(s.out.end, s.out.end+nn) + s.out.writeAt(b[:nn], s.out.end) + s.outopened.set() + b = b[nn:] + n += int(nn) + // If we have bytes left to send, we're blocked. + canWrite = false } + s.outUnlock() + return n, nil +} + +// Close closes the stream. +// See CloseContext for more details. +func (s *Stream) Close() error { + return s.CloseContext(context.Background()) +} + +// CloseContext closes the stream. +// Any blocked stream operations will be unblocked and return errors. +// +// CloseContext flushes any data in the stream write buffer and waits for the peer to +// acknowledge receipt of the data. +// If the stream has been reset, it waits for the peer to acknowledge the reset. +// If the context expires before the peer receives the stream's data, +// CloseContext discards the buffer and returns the context error. +func (s *Stream) CloseContext(ctx context.Context) error { + s.CloseRead() + s.CloseWrite() + // TODO: wait for peer to acknowledge data + // TODO: Return code from peer's RESET_STREAM frame? + return nil +} + +// CloseRead aborts reads on the stream. +// Any blocked reads will be unblocked and return errors. +// +// CloseRead notifies the peer that the stream has been closed for reading. +// It does not wait for the peer to acknowledge the closure. +// Use CloseContext to wait for the peer's acknowledgement. +func (s *Stream) CloseRead() { + if s.IsWriteOnly() { + return + } + // TODO: support read-closing streams with a STOP_SENDING frame +} + +// CloseWrite aborts writes on the stream. +// Any blocked writes will be unblocked and return errors. +// +// CloseWrite sends any data in the stream write buffer to the peer. +// It does not wait for the peer to acknowledge receipt of the data. +// Use CloseContext to wait for the peer's acknowledgement. +func (s *Stream) CloseWrite() { + if s.IsReadOnly() { + return + } + s.outgate.lock() defer s.outUnlock() + s.outclosed.set() +} - // Set outopened to send a STREAM frame with no data, - // opening the stream on the peer. - s.outopened.set() +// inUnlock unlocks s.ingate. +// It sets the gate condition if reads from s will not block. +// If s has receive-related frames to write, it notifies the Conn. +func (s *Stream) inUnlock() { + if s.inUnlockNoQueue() { + s.conn.queueStreamForSend(s) + } +} - return n, nil +// inUnlockNoQueue is inUnlock, +// but reports whether s has frames to write rather than notifying the Conn. +func (s *Stream) inUnlockNoQueue() (shouldSend bool) { + // TODO: STOP_SENDING + canRead := s.inset.contains(s.in.start) || // data available to read + s.insize == s.in.start // at EOF + s.ingate.unlock(canRead) + return s.insendmax.shouldSend() // STREAM_MAX_DATA } // outUnlock unlocks s.outgate. // It sets the gate condition if writes to s will not block. -// If s has frames to write, it notifies the Conn. +// If s has send-related frames to write, it notifies the Conn. func (s *Stream) outUnlock() { - if s.outopened.shouldSend() { + if s.outUnlockNoQueue() { s.conn.queueStreamForSend(s) } - canSend := true // TODO: set sendability status based on flow control - s.outgate.unlock(canSend) +} + +// outUnlockNoQueue is outUnlock, +// but reports whether s has frames to write rather than notifying the Conn. +func (s *Stream) outUnlockNoQueue() (shouldSend bool) { + lim := min(s.out.start+s.outmaxbuf, s.outwin) + canWrite := lim > s.out.end || // available flow control + s.outclosed.isSet() // closed + s.outgate.unlock(canWrite) + return len(s.outunsent) > 0 || // STREAM frame with data + s.outclosed.shouldSend() || // STREAM frame with FIN bit + s.outopened.shouldSend() || // STREAM frame with no data + s.outblocked.shouldSend() // STREAM_DATA_BLOCKED } // handleData handles data received in a STREAM frame. func (s *Stream) handleData(off int64, b []byte, fin bool) error { - // TODO + s.ingate.lock() + defer s.inUnlock() + end := off + int64(len(b)) + if end > s.inwin { + // The peer sent us data past the maximum flow control window we gave them. + return localTransportError(errFlowControl) + } + if s.insize != -1 && end > s.insize { + // The peer sent us data past the final size of the stream they previously gave us. + return localTransportError(errFinalSize) + } + s.in.writeAt(b, off) + s.inset.add(off, end) + if fin { + if s.insize != -1 && s.insize != end { + // The peer changed the final size of the stream. + return localTransportError(errFinalSize) + } + s.insize = end + // The peer has enough flow control window to send the entire stream. + s.insendmax.clear() + } + return nil +} + +// handleMaxStreamData handles an update received in a MAX_STREAM_DATA frame. +func (s *Stream) handleMaxStreamData(maxStreamData int64) error { + s.outgate.lock() + defer s.outUnlock() + s.outwin = max(maxStreamData, s.outwin) return nil } +// ackOrLoss handles the fate of stream frames other than STREAM. +func (s *Stream) ackOrLoss(pnum packetNumber, ftype byte, fate packetFate) { + // Frames which carry new information each time they are sent + // (MAX_STREAM_DATA, STREAM_DATA_BLOCKED) must only be marked + // as received if the most recent packet carrying this frame is acked. + // + // Frames which are always the same (STOP_SENDING, RESET_STREAM) + // can be marked as received if any packet carrying this frame is acked. + switch ftype { + case frameTypeMaxStreamData: + s.ingate.lock() + s.insendmax.ackLatestOrLoss(pnum, fate) + s.inUnlock() + case frameTypeStreamDataBlocked: + s.outgate.lock() + s.outblocked.ackLatestOrLoss(pnum, fate) + s.outUnlock() + default: + // TODO: Handle STOP_SENDING, RESET_STREAM. + panic("unhandled frame type") + } +} + // ackOrLossData handles the fate of a STREAM frame. func (s *Stream) ackOrLossData(pnum packetNumber, start, end int64, fin bool, fate packetFate) { s.outgate.lock() defer s.outUnlock() s.outopened.ackOrLoss(pnum, fate) + if fin { + s.outclosed.ackOrLoss(pnum, fate) + } + switch fate { + case packetAcked: + s.outacked.add(start, end) + s.outunsent.sub(start, end) + // If this ack is for data at the start of the send buffer, we can now discard it. + if s.outacked.contains(s.out.start) { + s.out.discardBefore(s.outacked[0].end) + } + case packetLost: + // Mark everything lost, but not previously acked, as needing retransmission. + // We do this by adding all the lost bytes to outunsent, and then + // removing everything already acked. + s.outunsent.add(start, end) + for _, a := range s.outacked { + s.outunsent.sub(a.start, a.end) + } + } } +// appendInFrames appends STOP_SENDING and MAX_STREAM_DATA frames +// to the current packet. +// +// It returns true if no more frames need appending, +// false if not everything fit in the current packet. func (s *Stream) appendInFrames(w *packetWriter, pnum packetNumber, pto bool) bool { + s.ingate.lock() + defer s.inUnlockNoQueue() // TODO: STOP_SENDING - // TODO: MAX_STREAM_DATA + if s.insendmax.shouldSendPTO(pto) { + // MAX_STREAM_DATA + maxStreamData := s.in.start + s.inmaxbuf + if !w.appendMaxStreamDataFrame(s.id, maxStreamData) { + return false + } + s.inwin = maxStreamData + s.insendmax.setSent(pnum) + } return true } +// appendOutFrames appends RESET_STREAM, STREAM_DATA_BLOCKED, and STREAM frames +// to the current packet. +// +// It returns true if no more frames need appending, +// false if not everything fit in the current packet. func (s *Stream) appendOutFrames(w *packetWriter, pnum packetNumber, pto bool) bool { + s.outgate.lock() + defer s.outUnlockNoQueue() // TODO: RESET_STREAM - // TODO: STREAM_DATA_BLOCKED - // TODO: STREAM frames with data - if s.outopened.shouldSendPTO(pto) { - off := int64(0) - size := 0 - fin := false - _, added := w.appendStreamFrame(s.id, off, size, fin) + if s.outblocked.shouldSendPTO(pto) { + // STREAM_DATA_BLOCKED + if !w.appendStreamDataBlockedFrame(s.id, s.out.end) { + return false + } + s.outblocked.setSent(pnum) + s.frameOpensStream(pnum) + } + // STREAM + for { + off, size := dataToSend(s.out, s.outunsent, s.outacked, pto) + fin := s.outclosed.isSet() && off+size == s.out.end + shouldSend := size > 0 || // have data to send + s.outopened.shouldSendPTO(pto) || // should open the stream + (fin && s.outclosed.shouldSendPTO(pto)) // should close the stream + if !shouldSend { + return true + } + b, added := w.appendStreamFrame(s.id, off, int(size), fin) if !added { return false } + s.out.copy(off, b) + s.outunsent.sub(off, off+int64(len(b))) + s.frameOpensStream(pnum) + if fin { + s.outclosed.setSent(pnum) + } + if pto { + return true + } + if int64(len(b)) < size { + return false + } + } +} + +// frameOpensStream records that we're sending a frame that will open the stream. +// +// If we don't have an acknowledgement from the peer for a previous frame opening the stream, +// record this packet as being the latest one to open it. +func (s *Stream) frameOpensStream(pnum packetNumber) { + if !s.outopened.isReceived() { s.outopened.setSent(pnum) } - return true +} + +// dataToSend returns the next range of data to send in a STREAM or CRYPTO_STREAM. +func dataToSend(out pipe, outunsent, outacked rangeset[int64], pto bool) (start, size int64) { + switch { + case pto: + // On PTO, resend unacked data that fits in the probe packet. + // For simplicity, we send the range starting at s.out.start + // (which is definitely unacked, or else we would have discarded it) + // up to the next acked byte (if any). + // + // This may miss unacked data starting after that acked byte, + // but avoids resending data the peer has acked. + for _, r := range outacked { + if r.start > out.start { + return out.start, r.start - out.start + } + } + return out.start, out.end - out.start + case outunsent.numRanges() > 0: + return outunsent.min(), outunsent[0].size() + default: + return out.end, 0 + } } diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 8ae9dbc825..d158e72afb 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -7,10 +7,703 @@ package quic import ( + "bytes" + "context" + "crypto/rand" + "fmt" + "io" "reflect" + "strings" "testing" ) +func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + ctx := canceledContext() + want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.initialMaxStreamsBidi = 100 + p.initialMaxStreamsUni = 100 + p.initialMaxData = 1 << 20 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + // Non-blocking write with no flow control. + s, err := tc.conn.newLocalStream(ctx, styp) + if err != nil { + t.Fatal(err) + } + _, err = s.WriteContext(ctx, want) + if err != context.Canceled { + t.Fatalf("write to stream with no flow control: err = %v, want context.Canceled", err) + } + tc.wantFrame("write blocked by flow control triggers a STREAM_DATA_BLOCKED frame", + packetType1RTT, debugFrameStreamDataBlocked{ + id: s.id, + max: 0, + }) + + // Blocking write waiting for flow control. + w := runAsync(tc, func(ctx context.Context) (int, error) { + return s.WriteContext(ctx, want) + }) + tc.wantFrame("second blocked write triggers another STREAM_DATA_BLOCKED", + packetType1RTT, debugFrameStreamDataBlocked{ + id: s.id, + max: 0, + }) + tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: 4, + }) + tc.wantFrame("stream window extended, but still more data to write", + packetType1RTT, debugFrameStreamDataBlocked{ + id: s.id, + max: 4, + }) + tc.wantFrame("stream window extended to 4, expect blocked write to progress", + packetType1RTT, debugFrameStream{ + id: s.id, + data: want[:4], + }) + + tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: int64(len(want)), + }) + tc.wantFrame("stream window extended further, expect blocked write to finish", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 4, + data: want[4:], + }) + n, err := w.result() + if n != len(want) || err != nil { + t.Errorf("Write() = %v, %v; want %v, nil", n, err, len(want)) + } + }) +} + +func TestStreamIgnoresMaxStreamDataReduction(t *testing.T) { + // "A sender MUST ignore any MAX_STREAM_DATA [...] frames that + // do not increase flow control limits." + // https://www.rfc-editor.org/rfc/rfc9000#section-4.1-9 + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + ctx := canceledContext() + want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + tc := newTestConn(t, clientSide, func(p *transportParameters) { + if styp == uniStream { + p.initialMaxStreamsUni = 1 + p.initialMaxStreamDataUni = 4 + } else { + p.initialMaxStreamsBidi = 1 + p.initialMaxStreamDataBidiRemote = 4 + } + p.initialMaxData = 1 << 20 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.ignoreFrame(frameTypeStreamDataBlocked) + + // Write [0,1). + s, err := tc.conn.newLocalStream(ctx, styp) + if err != nil { + t.Fatal(err) + } + s.WriteContext(ctx, want[:1]) + tc.wantFrame("sent data (1 byte) fits within flow control limit", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + data: want[:1], + }) + + // MAX_STREAM_DATA tries to decrease limit, and is ignored. + tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: 2, + }) + + // Write [1,4). + s.WriteContext(ctx, want[1:]) + tc.wantFrame("stream limit is 4 bytes, ignoring decrease in MAX_STREAM_DATA", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 1, + data: want[1:4], + }) + + // MAX_STREAM_DATA increases limit. + // Second MAX_STREAM_DATA decreases it, and is ignored. + tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: 8, + }) + tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: 6, + }) + + // Write [1,4). + s.WriteContext(ctx, want[4:]) + tc.wantFrame("stream limit is 8 bytes, ignoring decrease in MAX_STREAM_DATA", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 4, + data: want[4:8], + }) + }) +} + +func TestStreamWriteBlockedByWriteBufferLimit(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + ctx := canceledContext() + want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + const maxWriteBuffer = 4 + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.initialMaxStreamsBidi = 100 + p.initialMaxStreamsUni = 100 + p.initialMaxData = 1 << 20 + p.initialMaxStreamDataBidiRemote = 1 << 20 + p.initialMaxStreamDataUni = 1 << 20 + }, func(c *Config) { + c.StreamWriteBufferSize = maxWriteBuffer + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + // Write more data than StreamWriteBufferSize. + // The peer has given us plenty of flow control, + // so we're just blocked by our local limit. + s, err := tc.conn.newLocalStream(ctx, styp) + if err != nil { + t.Fatal(err) + } + w := runAsync(tc, func(ctx context.Context) (int, error) { + return s.WriteContext(ctx, want) + }) + tc.wantFrame("stream write should send as much data as write buffer allows", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + data: want[:maxWriteBuffer], + }) + tc.wantIdle("no STREAM_DATA_BLOCKED, we're blocked locally not by flow control") + + // ACK for previously-sent data allows making more progress. + tc.writeFrames(packetType1RTT, debugFrameAck{ + ranges: []i64range[packetNumber]{{0, tc.sentFramePacket.num + 1}}, + }) + tc.wantFrame("ACK for previous data allows making progress", + packetType1RTT, debugFrameStream{ + id: s.id, + off: maxWriteBuffer, + data: want[maxWriteBuffer:][:maxWriteBuffer], + }) + + // Cancel the write with data left to send. + w.cancel() + n, err := w.result() + if n != 2*maxWriteBuffer || err == nil { + t.Fatalf("WriteContext() = %v, %v; want %v bytes, error", n, err, 2*maxWriteBuffer) + } + }) +} + +func TestStreamReceive(t *testing.T) { + // "Endpoints MUST be able to deliver stream data to an application as + // an ordered byte stream." + // https://www.rfc-editor.org/rfc/rfc9000#section-2.2-2 + want := make([]byte, 5000) + for i := range want { + want[i] = byte(i) + } + type frame struct { + start int64 + end int64 + fin bool + want int + wantEOF bool + } + for _, test := range []struct { + name string + frames []frame + }{{ + name: "linear", + frames: []frame{{ + start: 0, + end: 1000, + want: 1000, + }, { + start: 1000, + end: 2000, + want: 2000, + }, { + start: 2000, + end: 3000, + want: 3000, + fin: true, + wantEOF: true, + }}, + }, { + name: "out of order", + frames: []frame{{ + start: 1000, + end: 2000, + }, { + start: 2000, + end: 3000, + }, { + start: 0, + end: 1000, + want: 3000, + }}, + }, { + name: "resent", + frames: []frame{{ + start: 0, + end: 1000, + want: 1000, + }, { + start: 0, + end: 1000, + want: 1000, + }, { + start: 1000, + end: 2000, + want: 2000, + }, { + start: 0, + end: 1000, + want: 2000, + }, { + start: 1000, + end: 2000, + want: 2000, + }}, + }, { + name: "overlapping", + frames: []frame{{ + start: 0, + end: 1000, + want: 1000, + }, { + start: 3000, + end: 4000, + want: 1000, + }, { + start: 2000, + end: 3000, + want: 1000, + }, { + start: 1000, + end: 3000, + want: 4000, + }}, + }, { + name: "early eof", + frames: []frame{{ + start: 3000, + end: 3000, + fin: true, + want: 0, + }, { + start: 1000, + end: 2000, + want: 0, + }, { + start: 0, + end: 1000, + want: 2000, + }, { + start: 2000, + end: 3000, + want: 3000, + wantEOF: true, + }}, + }, { + name: "empty eof", + frames: []frame{{ + start: 0, + end: 1000, + want: 1000, + }, { + start: 1000, + end: 1000, + fin: true, + want: 1000, + wantEOF: true, + }}, + }} { + testStreamTypes(t, test.name, func(t *testing.T, styp streamType) { + ctx := canceledContext() + tc := newTestConn(t, serverSide) + tc.handshake() + sid := newStreamID(clientSide, styp, 0) + var s *Stream + got := make([]byte, len(want)) + var total int + for _, f := range test.frames { + t.Logf("receive [%v,%v)", f.start, f.end) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: sid, + off: f.start, + data: want[f.start:f.end], + fin: f.fin, + }) + if s == nil { + var err error + s, err = tc.conn.AcceptStream(ctx) + if err != nil { + tc.t.Fatalf("conn.AcceptStream() = %v", err) + } + } + for { + n, err := s.ReadContext(ctx, got[total:]) + t.Logf("s.ReadContext() = %v, %v", n, err) + total += n + if f.wantEOF && err != io.EOF { + t.Fatalf("ReadContext() error = %v; want io.EOF", err) + } + if !f.wantEOF && err == io.EOF { + t.Fatalf("ReadContext() error = io.EOF, want something else") + } + if err != nil { + break + } + } + if total != f.want { + t.Fatalf("total bytes read = %v, want %v", total, f.want) + } + for i := 0; i < total; i++ { + if got[i] != want[i] { + t.Fatalf("byte %v differs: got %v, want %v", i, got[i], want[i]) + } + } + } + }) + } + +} + +func TestStreamReceiveExtendsStreamWindow(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + const maxWindowSize = 20 + ctx := canceledContext() + tc := newTestConn(t, serverSide, func(c *Config) { + c.StreamReadBufferSize = maxWindowSize + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + sid := newStreamID(clientSide, styp, 0) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: sid, + off: 0, + data: make([]byte, maxWindowSize), + }) + s, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("AcceptStream: %v", err) + } + tc.wantIdle("stream window is not extended before data is read") + buf := make([]byte, maxWindowSize+1) + if n, err := s.ReadContext(ctx, buf); n != maxWindowSize || err != nil { + t.Fatalf("s.ReadContext() = %v, %v; want %v, nil", n, err, maxWindowSize) + } + tc.wantFrame("stream window is extended after reading data", + packetType1RTT, debugFrameMaxStreamData{ + id: sid, + max: maxWindowSize * 2, + }) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: sid, + off: maxWindowSize, + data: make([]byte, maxWindowSize), + fin: true, + }) + if n, err := s.ReadContext(ctx, buf); n != maxWindowSize || err != io.EOF { + t.Fatalf("s.ReadContext() = %v, %v; want %v, io.EOF", n, err, maxWindowSize) + } + tc.wantIdle("stream window is not extended after FIN") + }) +} + +func TestStreamReceiveViolatesStreamDataLimit(t *testing.T) { + // "A receiver MUST close the connection with an error of type FLOW_CONTROL_ERROR if + // the sender violates the advertised [...] stream data limits [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-4.1-8 + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + const maxStreamData = 10 + for _, test := range []struct { + off int64 + size int64 + }{{ + off: maxStreamData, + size: 1, + }, { + off: 0, + size: maxStreamData + 1, + }, { + off: maxStreamData - 1, + size: 2, + }} { + tc := newTestConn(t, serverSide, func(c *Config) { + c.StreamReadBufferSize = maxStreamData + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, styp, 0), + off: test.off, + data: make([]byte, test.size), + }) + tc.wantFrame( + fmt.Sprintf("data [%v,%v) violates stream data limit and closes connection", + test.off, test.off+test.size), + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errFlowControl, + }, + ) + } + }) +} + +func TestStreamReceiveDuplicateDataDoesNotViolateLimits(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + const maxData = 10 + tc := newTestConn(t, serverSide, func(c *Config) { + // TODO: Add connection-level maximum data here as well. + c.StreamReadBufferSize = maxData + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + for i := 0; i < 3; i++ { + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, styp, 0), + off: 0, + data: make([]byte, maxData), + }) + tc.wantIdle(fmt.Sprintf("conn sends no frames after receiving data frame %v", i)) + } + }) +} + +func TestStreamFinalSizeChangedByStreamFrame(t *testing.T) { + // "If a [...] STREAM frame is received indicating a change + // in the final size for the stream, an endpoint SHOULD + // respond with an error of type FINAL_SIZE_ERROR [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-4.5-5 + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc := newTestConn(t, serverSide) + tc.handshake() + sid := newStreamID(clientSide, styp, 0) + + const write1size = 4 + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: sid, + off: 10, + fin: true, + }) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: sid, + off: 9, + fin: true, + }) + tc.wantFrame("change in final size of stream is an error", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errFinalSize, + }, + ) + }) +} + +func TestStreamDataBeyondFinalSize(t *testing.T) { + // "A receiver SHOULD treat receipt of data at or beyond + // the final size as an error of type FINAL_SIZE_ERROR [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-4.5-5 + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc := newTestConn(t, serverSide) + tc.handshake() + sid := newStreamID(clientSide, styp, 0) + + const write1size = 4 + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: sid, + off: 0, + data: make([]byte, 16), + fin: true, + }) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: sid, + off: 16, + data: []byte{0}, + }) + tc.wantFrame("received data past final size of stream", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errFinalSize, + }, + ) + }) +} + +func TestStreamReceiveUnblocksReader(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc := newTestConn(t, serverSide) + tc.handshake() + want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + sid := newStreamID(clientSide, styp, 0) + + // AcceptStream blocks until a STREAM frame is received. + accept := runAsync(tc, func(ctx context.Context) (*Stream, error) { + return tc.conn.AcceptStream(ctx) + }) + const write1size = 4 + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: sid, + off: 0, + data: want[:write1size], + }) + s, err := accept.result() + if err != nil { + t.Fatalf("AcceptStream() = %v", err) + } + + // ReadContext succeeds immediately, since we already have data. + got := make([]byte, len(want)) + read := runAsync(tc, func(ctx context.Context) (int, error) { + return s.ReadContext(ctx, got) + }) + if n, err := read.result(); n != write1size || err != nil { + t.Fatalf("ReadContext = %v, %v; want %v, nil", n, err, write1size) + } + + // ReadContext blocks waiting for more data. + read = runAsync(tc, func(ctx context.Context) (int, error) { + return s.ReadContext(ctx, got[write1size:]) + }) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: sid, + off: write1size, + data: want[write1size:], + fin: true, + }) + if n, err := read.result(); n != len(want)-write1size || err != io.EOF { + t.Fatalf("ReadContext = %v, %v; want %v, io.EOF", n, err, len(want)-write1size) + } + if !bytes.Equal(got, want) { + t.Fatalf("read bytes %x, want %x", got, want) + } + }) +} + +// testStreamSendFrameInvalidState calls the test func with a stream ID for: +// +// - a remote bidirectional stream that the peer has not created +// - a remote unidirectional stream +// +// It then sends the returned frame (STREAM, STREAM_DATA_BLOCKED, etc.) +// to the conn and expects a STREAM_STATE_ERROR. +func testStreamSendFrameInvalidState(t *testing.T, f func(sid streamID) debugFrame) { + testSides(t, "stream_not_created", func(t *testing.T, side connSide) { + tc := newTestConn(t, side) + tc.handshake() + tc.writeFrames(packetType1RTT, f(newStreamID(side, bidiStream, 0))) + tc.wantFrame("frame for local stream which has not been created", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errStreamState, + }) + }) + testSides(t, "uni_stream", func(t *testing.T, side connSide) { + ctx := canceledContext() + tc := newTestConn(t, side) + tc.handshake() + sid := newStreamID(side, uniStream, 0) + s, err := tc.conn.NewSendOnlyStream(ctx) + if err != nil { + t.Fatal(err) + } + s.Write(nil) // open the stream + tc.wantFrame("new stream is opened", + packetType1RTT, debugFrameStream{ + id: sid, + data: []byte{}, + }) + tc.writeFrames(packetType1RTT, f(sid)) + tc.wantFrame("send-oriented frame for send-only stream", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errStreamState, + }) + }) +} + +func TestStreamStreamFrameInvalidState(t *testing.T) { + // "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR + // if it receives a STREAM frame for a locally initiated stream + // that has not yet been created, or for a send-only stream." + // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.8-3 + testStreamSendFrameInvalidState(t, func(sid streamID) debugFrame { + return debugFrameStream{ + id: sid, + } + }) +} + +func TestStreamDataBlockedInvalidState(t *testing.T) { + // "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR + // if it receives a STREAM frame for a locally initiated stream + // that has not yet been created, or for a send-only stream." + // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.8-3 + testStreamSendFrameInvalidState(t, func(sid streamID) debugFrame { + return debugFrameStream{ + id: sid, + } + }) +} + +// testStreamReceiveFrameInvalidState calls the test func with a stream ID for: +// +// - a remote bidirectional stream that the peer has not created +// - a local unidirectional stream +// +// It then sends the returned frame (MAX_STREAM_DATA, STOP_SENDING, etc.) +// to the conn and expects a STREAM_STATE_ERROR. +func testStreamReceiveFrameInvalidState(t *testing.T, f func(sid streamID) debugFrame) { + testSides(t, "stream_not_created", func(t *testing.T, side connSide) { + tc := newTestConn(t, side) + tc.handshake() + tc.writeFrames(packetType1RTT, f(newStreamID(side, bidiStream, 0))) + tc.wantFrame("frame for local stream which has not been created", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errStreamState, + }) + }) + testSides(t, "uni_stream", func(t *testing.T, side connSide) { + tc := newTestConn(t, side) + tc.handshake() + tc.writeFrames(packetType1RTT, f(newStreamID(side.peer(), uniStream, 0))) + tc.wantFrame("receive-oriented frame for receive-only stream", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errStreamState, + }) + }) +} + +func TestStreamMaxStreamDataInvalidState(t *testing.T) { + // "Receiving a MAX_STREAM_DATA frame for a locally initiated stream + // that has not yet been created MUST be treated as a connection error + // of type STREAM_STATE_ERROR. An endpoint that receives a MAX_STREAM_DATA + // frame for a receive-only stream MUST terminate the connection + // with error STREAM_STATE_ERROR." + // https://www.rfc-editor.org/rfc/rfc9000#section-19.10-2 + testStreamReceiveFrameInvalidState(t, func(sid streamID) debugFrame { + return debugFrameMaxStreamData{ + id: sid, + max: 1000, + } + }) +} + func TestStreamOffsetTooLarge(t *testing.T) { // "Receipt of a frame that exceeds [2^62-1] MUST be treated as a // connection error of type FRAME_ENCODING_ERROR or FLOW_CONTROL_ERROR." @@ -31,3 +724,104 @@ func TestStreamOffsetTooLarge(t *testing.T) { t.Fatalf("STREAM offset exceeds 2^62-1\ngot: %v\nwant: %v\n or: %v", got, want1, want2) } } + +func TestStreamReadFromWriteOnlyStream(t *testing.T) { + _, s := newTestConnAndLocalStream(t, serverSide, uniStream) + buf := make([]byte, 10) + wantErr := "read from write-only stream" + if n, err := s.Read(buf); err == nil || !strings.Contains(err.Error(), wantErr) { + t.Errorf("s.Read() = %v, %v; want error %q", n, err, wantErr) + } +} + +func TestStreamWriteToReadOnlyStream(t *testing.T) { + _, s := newTestConnAndRemoteStream(t, serverSide, uniStream) + buf := make([]byte, 10) + wantErr := "write to read-only stream" + if n, err := s.Write(buf); err == nil || !strings.Contains(err.Error(), wantErr) { + t.Errorf("s.Write() = %v, %v; want error %q", n, err, wantErr) + } +} + +func TestStreamWriteToClosedStream(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, serverSide, bidiStream, func(p *transportParameters) { + p.initialMaxStreamsBidi = 1 + p.initialMaxData = 1 << 20 + p.initialMaxStreamDataBidiRemote = 1 << 20 + }) + s.Close() + tc.wantFrame("stream is opened after being closed", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + fin: true, + data: []byte{}, + }) + wantErr := "write to closed stream" + if n, err := s.Write([]byte{}); err == nil || !strings.Contains(err.Error(), wantErr) { + t.Errorf("s.Write() = %v, %v; want error %q", n, err, wantErr) + } +} + +func TestStreamWriteMoreThanOnePacketOfData(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) { + p.initialMaxStreamsUni = 1 + p.initialMaxData = 1 << 20 + p.initialMaxStreamDataUni = 1 << 20 + }) + want := make([]byte, 4096) + rand.Read(want) // doesn't need to be crypto/rand, but non-deprecated and harmless + w := runAsync(tc, func(ctx context.Context) (int, error) { + return s.WriteContext(ctx, want) + }) + got := make([]byte, 0, len(want)) + for { + f, _ := tc.readFrame() + if f == nil { + break + } + sf, ok := f.(debugFrameStream) + if !ok { + t.Fatalf("unexpected frame: %v", sf) + } + if len(got) != int(sf.off) { + t.Fatalf("got frame: %v\nwant offset %v", sf, len(got)) + } + got = append(got, sf.data...) + } + if n, err := w.result(); n != len(want) || err != nil { + t.Fatalf("s.WriteContext() = %v, %v; want %v, nil", n, err, len(want)) + } + if !bytes.Equal(got, want) { + t.Fatalf("mismatch in received stream data") + } +} + +func newTestConnAndLocalStream(t *testing.T, side connSide, styp streamType, opts ...any) (*testConn, *Stream) { + t.Helper() + ctx := canceledContext() + tc := newTestConn(t, side, opts...) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + s, err := tc.conn.newLocalStream(ctx, styp) + if err != nil { + t.Fatalf("conn.newLocalStream(%v) = %v", styp, err) + } + return tc, s +} + +func newTestConnAndRemoteStream(t *testing.T, side connSide, styp streamType, opts ...any) (*testConn, *Stream) { + t.Helper() + ctx := canceledContext() + tc := newTestConn(t, side, opts...) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(side.peer(), styp, 0), + }) + s, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("conn.AcceptStream() = %v", err) + } + return tc, s +} From 126a5f3b343c940b1ce677f43b138556311b0999 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 9 Aug 2023 10:55:08 -0700 Subject: [PATCH 027/168] quic: fix some bugs in the sendable stream list Write a test for multiple streams simultaneously sending data. Exercise the stream send queue, verify that we fairly schedule sends among the available streams. Fix a couple bugs turned up by the test. For golang/go#58547 Change-Id: I6a56f121d5cb49e79c9e4ad043fb94d34a4dab40 Reviewed-on: https://go-review.googlesource.com/c/net/+/517859 Reviewed-by: Jonathan Amsterdam TryBot-Result: Gopher Robot Run-TryBot: Damien Neil --- internal/quic/conn_streams.go | 10 ++-- internal/quic/conn_streams_test.go | 82 ++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index 7a531f52b2..dd35e34cf6 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -163,6 +163,7 @@ func (c *Conn) queueStreamForSend(s *Stream) { // Insert this stream at the end of the queue. c.streams.sendTail.next = s c.streams.sendTail = s + s.next = c.streams.sendHead } c.streams.needSend.Store(true) c.wake() @@ -202,7 +203,11 @@ func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) } return false } + next := s.next s.next = nil + if (next == s) != (s == c.streams.sendTail) { + panic("BUG: sendable stream list state is inconsistent") + } if s == c.streams.sendTail { // This was the last stream. c.streams.sendHead = nil @@ -211,9 +216,8 @@ func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) return true } // We've sent all data for this stream, so remove it from the list. - c.streams.sendTail.next = s.next - c.streams.sendHead = s.next - s.next = nil + c.streams.sendTail.next = next + c.streams.sendHead = next } } diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go index bcbbe81ce3..877dbb94fc 100644 --- a/internal/quic/conn_streams_test.go +++ b/internal/quic/conn_streams_test.go @@ -171,3 +171,85 @@ func TestStreamsStreamSendOnly(t *testing.T) { code: errStreamState, }) } + +func TestStreamsWriteQueueFairness(t *testing.T) { + ctx := canceledContext() + const dataLen = 1 << 20 + const numStreams = 3 + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.initialMaxStreamsBidi = numStreams + p.initialMaxData = 1<<62 - 1 + p.initialMaxStreamDataBidiRemote = dataLen + }, func(c *Config) { + c.StreamWriteBufferSize = dataLen + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + // Create a number of streams, and write a bunch of data to them. + // The streams are not limited by flow control. + // + // The first stream we create is going to immediately consume all + // available congestion window. + // + // Once we've created all the remaining streams, + // we start sending acks back to open up the congestion window. + // We verify that all streams can make progress. + data := make([]byte, dataLen) + var streams []*Stream + for i := 0; i < numStreams; i++ { + s, err := tc.conn.NewStream(ctx) + if err != nil { + t.Fatal(err) + } + streams = append(streams, s) + if n, err := s.WriteContext(ctx, data); n != len(data) || err != nil { + t.Fatalf("s.WriteContext() = %v, %v; want %v, nil", n, err, len(data)) + } + // Wait for the stream to finish writing whatever frames it can before + // congestion control blocks it. + tc.wait() + } + + sent := make([]int64, len(streams)) + for { + p := tc.readPacket() + if p == nil { + break + } + tc.writeFrames(packetType1RTT, debugFrameAck{ + ranges: []i64range[packetNumber]{{0, p.num}}, + }) + for _, f := range p.frames { + sf, ok := f.(debugFrameStream) + if !ok { + t.Fatalf("got unexpected frame (want STREAM): %v", sf) + } + if got, want := sf.off, sent[sf.id.num()]; got != want { + t.Fatalf("got frame: %v\nwant offset: %v", sf, want) + } + sent[sf.id.num()] = sf.off + int64(len(sf.data)) + // Look at the amount of data sent by all streams, excluding the first one. + // (The first stream got a head start when it consumed the initial window.) + // + // We expect that difference between the streams making the most and least progress + // so far will be less than the maximum datagram size. + minSent := sent[1] + maxSent := sent[1] + for _, s := range sent[2:] { + minSent = min(minSent, s) + maxSent = max(maxSent, s) + } + const maxDelta = maxUDPPayloadSize + if d := maxSent - minSent; d > maxDelta { + t.Fatalf("stream data sent: %v; delta=%v, want delta <= %v", sent, d, maxDelta) + } + } + } + // Final check that every stream sent the full amount of data expected. + for num, s := range sent { + if s != dataLen { + t.Errorf("stream %v sent %v bytes, want %v", num, s, dataLen) + } + } +} From 95cb3bb9eb72a38ad7817552051746cf41999f5a Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Sat, 19 Aug 2023 08:25:09 +0000 Subject: [PATCH 028/168] dns/dnsmessage: show AD and CD bit in Header.GoString() Change-Id: I7b973d255ec4ab1e1c0f8539b811ddc0503c2f48 GitHub-Last-Rev: 954434b6211a6c24d281cda61547070b586ea818 GitHub-Pull-Request: golang/net#188 Reviewed-on: https://go-review.googlesource.com/c/net/+/521075 Run-TryBot: Mateusz Poliwczak TryBot-Result: Gopher Robot Auto-Submit: Ian Lance Taylor Reviewed-by: Dmitri Shuralyov Reviewed-by: Ian Lance Taylor Run-TryBot: Ian Lance Taylor --- dns/dnsmessage/message.go | 2 ++ dns/dnsmessage/message_test.go | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index 37da3de4d3..69938d54ff 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -361,6 +361,8 @@ func (m *Header) GoString() string { "Truncated: " + printBool(m.Truncated) + ", " + "RecursionDesired: " + printBool(m.RecursionDesired) + ", " + "RecursionAvailable: " + printBool(m.RecursionAvailable) + ", " + + "AuthenticData: " + printBool(m.AuthenticData) + ", " + + "CheckingDisabled: " + printBool(m.CheckingDisabled) + ", " + "RCode: " + m.RCode.GoString() + "}" } diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index 64c6db86d1..83fac78128 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -1185,8 +1185,7 @@ func TestGoString(t *testing.T) { t.Error("Message.GoString lost information or largeTestMsg changed: msg != largeTestMsg()") } got := msg.GoString() - - want := `dnsmessage.Message{Header: dnsmessage.Header{ID: 0, Response: true, OpCode: 0, Authoritative: true, Truncated: false, RecursionDesired: false, RecursionAvailable: false, RCode: dnsmessage.RCodeSuccess}, Questions: []dnsmessage.Question{dnsmessage.Question{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET}}, Answers: []dnsmessage.Resource{dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.AResource{A: [4]byte{127, 0, 0, 2}}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeAAAA, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.AAAAResource{AAAA: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeCNAME, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.CNAMEResource{CNAME: dnsmessage.MustNewName("alias.example.com.")}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeSOA, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.SOAResource{NS: dnsmessage.MustNewName("ns1.example.com."), MBox: dnsmessage.MustNewName("mb.example.com."), Serial: 1, Refresh: 2, Retry: 3, Expire: 4, MinTTL: 5}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypePTR, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.PTRResource{PTR: dnsmessage.MustNewName("ptr.example.com.")}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeMX, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.MXResource{Pref: 7, MX: dnsmessage.MustNewName("mx.example.com.")}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeSRV, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.SRVResource{Priority: 8, Weight: 9, Port: 11, Target: dnsmessage.MustNewName("srv.example.com.")}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: 65362, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.UnknownResource{Type: 65362, Data: []byte{42, 0, 43, 44}}}}, Authorities: []dnsmessage.Resource{dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeNS, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.NSResource{NS: dnsmessage.MustNewName("ns1.example.com.")}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeNS, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.NSResource{NS: dnsmessage.MustNewName("ns2.example.com.")}}}, Additionals: []dnsmessage.Resource{dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeTXT, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.TXTResource{TXT: []string{"So Long\x2c and Thanks for All the Fish"}}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeTXT, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.TXTResource{TXT: []string{"Hamster Huey and the Gooey Kablooie"}}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("."), Type: dnsmessage.TypeOPT, Class: 4096, TTL: 4261412864, Length: 0}, Body: &dnsmessage.OPTResource{Options: []dnsmessage.Option{dnsmessage.Option{Code: 10, Data: []byte{1, 35, 69, 103, 137, 171, 205, 239}}}}}}}` + want := `dnsmessage.Message{Header: dnsmessage.Header{ID: 0, Response: true, OpCode: 0, Authoritative: true, Truncated: false, RecursionDesired: false, RecursionAvailable: false, AuthenticData: false, CheckingDisabled: false, RCode: dnsmessage.RCodeSuccess}, Questions: []dnsmessage.Question{dnsmessage.Question{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET}}, Answers: []dnsmessage.Resource{dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.AResource{A: [4]byte{127, 0, 0, 2}}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeAAAA, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.AAAAResource{AAAA: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeCNAME, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.CNAMEResource{CNAME: dnsmessage.MustNewName("alias.example.com.")}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeSOA, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.SOAResource{NS: dnsmessage.MustNewName("ns1.example.com."), MBox: dnsmessage.MustNewName("mb.example.com."), Serial: 1, Refresh: 2, Retry: 3, Expire: 4, MinTTL: 5}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypePTR, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.PTRResource{PTR: dnsmessage.MustNewName("ptr.example.com.")}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeMX, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.MXResource{Pref: 7, MX: dnsmessage.MustNewName("mx.example.com.")}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeSRV, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.SRVResource{Priority: 8, Weight: 9, Port: 11, Target: dnsmessage.MustNewName("srv.example.com.")}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: 65362, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.UnknownResource{Type: 65362, Data: []byte{42, 0, 43, 44}}}}, Authorities: []dnsmessage.Resource{dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeNS, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.NSResource{NS: dnsmessage.MustNewName("ns1.example.com.")}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeNS, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.NSResource{NS: dnsmessage.MustNewName("ns2.example.com.")}}}, Additionals: []dnsmessage.Resource{dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeTXT, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.TXTResource{TXT: []string{"So Long\x2c and Thanks for All the Fish"}}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("foo.bar.example.com."), Type: dnsmessage.TypeTXT, Class: dnsmessage.ClassINET, TTL: 0, Length: 0}, Body: &dnsmessage.TXTResource{TXT: []string{"Hamster Huey and the Gooey Kablooie"}}}, dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName("."), Type: dnsmessage.TypeOPT, Class: 4096, TTL: 4261412864, Length: 0}, Body: &dnsmessage.OPTResource{Options: []dnsmessage.Option{dnsmessage.Option{Code: 10, Data: []byte{1, 35, 69, 103, 137, 171, 205, 239}}}}}}}` if got != want { t.Errorf("got msg1.GoString() = %s\nwant = %s", got, want) From 9cde5a081510f83ae10bc2bf88231babd81ef2d5 Mon Sep 17 00:00:00 2001 From: Alexander Yastrebov Date: Wed, 12 Jul 2023 19:16:51 +0000 Subject: [PATCH 029/168] net/http2: remove awaitGracefulShutdown It was added by https://golang.org/cl/43455 and its usage was removed by https://golang.org/cl/43230 Updates golang/go#20302 Change-Id: I5072c3d9cbf9a33d2ac613bc5a3c059dc54e9d29 GitHub-Last-Rev: 68a32fb702168992427174c41c5d4638f4e567ad GitHub-Pull-Request: golang/net#184 Reviewed-on: https://go-review.googlesource.com/c/net/+/509117 Run-TryBot: Damien Neil TryBot-Result: Gopher Robot Reviewed-by: Damien Neil Reviewed-by: Cherry Mui --- http2/server.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/http2/server.go b/http2/server.go index 033b6e6db6..6d5e008874 100644 --- a/http2/server.go +++ b/http2/server.go @@ -1012,14 +1012,6 @@ func (sc *serverConn) serve() { } } -func (sc *serverConn) awaitGracefulShutdown(sharedCh <-chan struct{}, privateCh chan struct{}) { - select { - case <-sc.doneServing: - case <-sharedCh: - close(privateCh) - } -} - type serverMessage int // Message values sent to serveMsgCh. From f89417cca1f18e39ab1db1bb80c42728f99d6143 Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Tue, 22 Aug 2023 07:54:22 +0000 Subject: [PATCH 030/168] dns/dnsmessage: reduce Parser size In the net package the Parser is copied a lot, the size of the Parser can be reduced easily by not storing the entire ResourceHeader in the Parser. It reduces the size from 328B to 80B. Also it makes sure that the resource header parsing methods don't return stale headers (from different sections). Change-Id: If05b03ba654ca5c03d536e86446c5a2a7dc79ec3 GitHub-Last-Rev: dacd25cc355269ff2a89d855d2094bb8f152c83c GitHub-Pull-Request: golang/net#186 Reviewed-on: https://go-review.googlesource.com/c/net/+/514855 Reviewed-by: Matthew Dempsky Auto-Submit: Matthew Dempsky TryBot-Result: Gopher Robot Run-TryBot: Mateusz Poliwczak Run-TryBot: Damien Neil Reviewed-by: Damien Neil --- dns/dnsmessage/message.go | 69 +++++++++++--------- dns/dnsmessage/message_test.go | 114 +++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 32 deletions(-) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index 69938d54ff..19ea8f17ce 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -542,11 +542,13 @@ type Parser struct { msg []byte header header - section section - off int - index int - resHeaderValid bool - resHeader ResourceHeader + section section + off int + index int + resHeaderValid bool + resHeaderOffset int + resHeaderType Type + resHeaderLength uint16 } // Start parses the header and enables the parsing of Questions. @@ -597,8 +599,9 @@ func (p *Parser) resource(sec section) (Resource, error) { func (p *Parser) resourceHeader(sec section) (ResourceHeader, error) { if p.resHeaderValid { - return p.resHeader, nil + p.off = p.resHeaderOffset } + if err := p.checkAdvance(sec); err != nil { return ResourceHeader{}, err } @@ -608,14 +611,16 @@ func (p *Parser) resourceHeader(sec section) (ResourceHeader, error) { return ResourceHeader{}, err } p.resHeaderValid = true - p.resHeader = hdr + p.resHeaderOffset = p.off + p.resHeaderType = hdr.Type + p.resHeaderLength = hdr.Length p.off = off return hdr, nil } func (p *Parser) skipResource(sec section) error { if p.resHeaderValid { - newOff := p.off + int(p.resHeader.Length) + newOff := p.off + int(p.resHeaderLength) if newOff > len(p.msg) { return errResourceLen } @@ -866,14 +871,14 @@ func (p *Parser) SkipAllAdditionals() error { // One of the XXXHeader methods must have been called before calling this // method. func (p *Parser) CNAMEResource() (CNAMEResource, error) { - if !p.resHeaderValid || p.resHeader.Type != TypeCNAME { + if !p.resHeaderValid || p.resHeaderType != TypeCNAME { return CNAMEResource{}, ErrNotStarted } r, err := unpackCNAMEResource(p.msg, p.off) if err != nil { return CNAMEResource{}, err } - p.off += int(p.resHeader.Length) + p.off += int(p.resHeaderLength) p.resHeaderValid = false p.index++ return r, nil @@ -884,14 +889,14 @@ func (p *Parser) CNAMEResource() (CNAMEResource, error) { // One of the XXXHeader methods must have been called before calling this // method. func (p *Parser) MXResource() (MXResource, error) { - if !p.resHeaderValid || p.resHeader.Type != TypeMX { + if !p.resHeaderValid || p.resHeaderType != TypeMX { return MXResource{}, ErrNotStarted } r, err := unpackMXResource(p.msg, p.off) if err != nil { return MXResource{}, err } - p.off += int(p.resHeader.Length) + p.off += int(p.resHeaderLength) p.resHeaderValid = false p.index++ return r, nil @@ -902,14 +907,14 @@ func (p *Parser) MXResource() (MXResource, error) { // One of the XXXHeader methods must have been called before calling this // method. func (p *Parser) NSResource() (NSResource, error) { - if !p.resHeaderValid || p.resHeader.Type != TypeNS { + if !p.resHeaderValid || p.resHeaderType != TypeNS { return NSResource{}, ErrNotStarted } r, err := unpackNSResource(p.msg, p.off) if err != nil { return NSResource{}, err } - p.off += int(p.resHeader.Length) + p.off += int(p.resHeaderLength) p.resHeaderValid = false p.index++ return r, nil @@ -920,14 +925,14 @@ func (p *Parser) NSResource() (NSResource, error) { // One of the XXXHeader methods must have been called before calling this // method. func (p *Parser) PTRResource() (PTRResource, error) { - if !p.resHeaderValid || p.resHeader.Type != TypePTR { + if !p.resHeaderValid || p.resHeaderType != TypePTR { return PTRResource{}, ErrNotStarted } r, err := unpackPTRResource(p.msg, p.off) if err != nil { return PTRResource{}, err } - p.off += int(p.resHeader.Length) + p.off += int(p.resHeaderLength) p.resHeaderValid = false p.index++ return r, nil @@ -938,14 +943,14 @@ func (p *Parser) PTRResource() (PTRResource, error) { // One of the XXXHeader methods must have been called before calling this // method. func (p *Parser) SOAResource() (SOAResource, error) { - if !p.resHeaderValid || p.resHeader.Type != TypeSOA { + if !p.resHeaderValid || p.resHeaderType != TypeSOA { return SOAResource{}, ErrNotStarted } r, err := unpackSOAResource(p.msg, p.off) if err != nil { return SOAResource{}, err } - p.off += int(p.resHeader.Length) + p.off += int(p.resHeaderLength) p.resHeaderValid = false p.index++ return r, nil @@ -956,14 +961,14 @@ func (p *Parser) SOAResource() (SOAResource, error) { // One of the XXXHeader methods must have been called before calling this // method. func (p *Parser) TXTResource() (TXTResource, error) { - if !p.resHeaderValid || p.resHeader.Type != TypeTXT { + if !p.resHeaderValid || p.resHeaderType != TypeTXT { return TXTResource{}, ErrNotStarted } - r, err := unpackTXTResource(p.msg, p.off, p.resHeader.Length) + r, err := unpackTXTResource(p.msg, p.off, p.resHeaderLength) if err != nil { return TXTResource{}, err } - p.off += int(p.resHeader.Length) + p.off += int(p.resHeaderLength) p.resHeaderValid = false p.index++ return r, nil @@ -974,14 +979,14 @@ func (p *Parser) TXTResource() (TXTResource, error) { // One of the XXXHeader methods must have been called before calling this // method. func (p *Parser) SRVResource() (SRVResource, error) { - if !p.resHeaderValid || p.resHeader.Type != TypeSRV { + if !p.resHeaderValid || p.resHeaderType != TypeSRV { return SRVResource{}, ErrNotStarted } r, err := unpackSRVResource(p.msg, p.off) if err != nil { return SRVResource{}, err } - p.off += int(p.resHeader.Length) + p.off += int(p.resHeaderLength) p.resHeaderValid = false p.index++ return r, nil @@ -992,14 +997,14 @@ func (p *Parser) SRVResource() (SRVResource, error) { // One of the XXXHeader methods must have been called before calling this // method. func (p *Parser) AResource() (AResource, error) { - if !p.resHeaderValid || p.resHeader.Type != TypeA { + if !p.resHeaderValid || p.resHeaderType != TypeA { return AResource{}, ErrNotStarted } r, err := unpackAResource(p.msg, p.off) if err != nil { return AResource{}, err } - p.off += int(p.resHeader.Length) + p.off += int(p.resHeaderLength) p.resHeaderValid = false p.index++ return r, nil @@ -1010,14 +1015,14 @@ func (p *Parser) AResource() (AResource, error) { // One of the XXXHeader methods must have been called before calling this // method. func (p *Parser) AAAAResource() (AAAAResource, error) { - if !p.resHeaderValid || p.resHeader.Type != TypeAAAA { + if !p.resHeaderValid || p.resHeaderType != TypeAAAA { return AAAAResource{}, ErrNotStarted } r, err := unpackAAAAResource(p.msg, p.off) if err != nil { return AAAAResource{}, err } - p.off += int(p.resHeader.Length) + p.off += int(p.resHeaderLength) p.resHeaderValid = false p.index++ return r, nil @@ -1028,14 +1033,14 @@ func (p *Parser) AAAAResource() (AAAAResource, error) { // One of the XXXHeader methods must have been called before calling this // method. func (p *Parser) OPTResource() (OPTResource, error) { - if !p.resHeaderValid || p.resHeader.Type != TypeOPT { + if !p.resHeaderValid || p.resHeaderType != TypeOPT { return OPTResource{}, ErrNotStarted } - r, err := unpackOPTResource(p.msg, p.off, p.resHeader.Length) + r, err := unpackOPTResource(p.msg, p.off, p.resHeaderLength) if err != nil { return OPTResource{}, err } - p.off += int(p.resHeader.Length) + p.off += int(p.resHeaderLength) p.resHeaderValid = false p.index++ return r, nil @@ -1049,11 +1054,11 @@ func (p *Parser) UnknownResource() (UnknownResource, error) { if !p.resHeaderValid { return UnknownResource{}, ErrNotStarted } - r, err := unpackUnknownResource(p.resHeader.Type, p.msg, p.off, p.resHeader.Length) + r, err := unpackUnknownResource(p.resHeaderType, p.msg, p.off, p.resHeaderLength) if err != nil { return UnknownResource{}, err } - p.off += int(p.resHeader.Length) + p.off += int(p.resHeaderLength) p.resHeaderValid = false p.index++ return r, nil diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index 83fac78128..ddb062b1e3 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -1670,3 +1670,117 @@ func FuzzUnpackPack(f *testing.F) { } }) } + +func TestParseResourceHeaderMultipleTimes(t *testing.T) { + msg := Message{ + Header: Header{Response: true, Authoritative: true}, + Answers: []Resource{ + { + ResourceHeader{ + Name: MustNewName("go.dev."), + Type: TypeA, + Class: ClassINET, + }, + &AResource{[4]byte{127, 0, 0, 1}}, + }, + }, + Authorities: []Resource{ + { + ResourceHeader{ + Name: MustNewName("go.dev."), + Type: TypeA, + Class: ClassINET, + }, + &AResource{[4]byte{127, 0, 0, 1}}, + }, + }, + } + + raw, err := msg.Pack() + if err != nil { + t.Fatal(err) + } + + var p Parser + + if _, err := p.Start(raw); err != nil { + t.Fatal(err) + } + + if err := p.SkipAllQuestions(); err != nil { + t.Fatal(err) + } + + hdr1, err := p.AnswerHeader() + if err != nil { + t.Fatal(err) + } + + hdr2, err := p.AnswerHeader() + if err != nil { + t.Fatal(err) + } + + if hdr1 != hdr2 { + t.Fatal("AnswerHeader called multiple times without parsing the RData returned different headers") + } + + if _, err := p.AResource(); err != nil { + t.Fatal(err) + } + + if _, err := p.AnswerHeader(); err != ErrSectionDone { + t.Fatalf("unexpected error: %v, want: %v", err, ErrSectionDone) + } + + hdr3, err := p.AuthorityHeader() + if err != nil { + t.Fatal(err) + } + + hdr4, err := p.AuthorityHeader() + if err != nil { + t.Fatal(err) + } + + if hdr3 != hdr4 { + t.Fatal("AuthorityHeader called multiple times without parsing the RData returned different headers") + } + + if _, err := p.AResource(); err != nil { + t.Fatal(err) + } + + if _, err := p.AuthorityHeader(); err != ErrSectionDone { + t.Fatalf("unexpected error: %v, want: %v", err, ErrSectionDone) + } +} + +func TestParseDifferentResourceHeadersWithoutParsingRData(t *testing.T) { + msg := smallTestMsg() + raw, err := msg.Pack() + if err != nil { + t.Fatal(err) + } + + var p Parser + if _, err := p.Start(raw); err != nil { + t.Fatal(err) + } + + if err := p.SkipAllQuestions(); err != nil { + t.Fatal(err) + } + + if _, err := p.AnswerHeader(); err != nil { + t.Fatal(err) + } + + if _, err := p.AdditionalHeader(); err == nil { + t.Errorf("p.AdditionalHeader() unexpected success") + } + + if _, err := p.AuthorityHeader(); err == nil { + t.Errorf("p.AuthorityHeader() unexpected success") + } +} From 0f7767ccf469d91c5c628723ad5971768b33b981 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 22 Aug 2023 14:20:27 -0700 Subject: [PATCH 031/168] dns/dnsmessage: validate cached section when skipping sections When skipping a section when p.resHeaderValid is set, verify that the cached resource header is for the right section. Fixes golang/go#62220 Change-Id: I8731dfdb5ad3cca94221b58f8be830bd2e16cff3 Reviewed-on: https://go-review.googlesource.com/c/net/+/521995 Reviewed-by: Mateusz Poliwczak Reviewed-by: Ian Lance Taylor Run-TryBot: Damien Neil TryBot-Result: Gopher Robot --- dns/dnsmessage/message.go | 2 +- dns/dnsmessage/message_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index 19ea8f17ce..cd997bab07 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -619,7 +619,7 @@ func (p *Parser) resourceHeader(sec section) (ResourceHeader, error) { } func (p *Parser) skipResource(sec section) error { - if p.resHeaderValid { + if p.resHeaderValid && p.section == sec { newOff := p.off + int(p.resHeaderLength) if newOff > len(p.msg) { return errResourceLen diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index ddb062b1e3..1b7f3cb35a 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -1784,3 +1784,32 @@ func TestParseDifferentResourceHeadersWithoutParsingRData(t *testing.T) { t.Errorf("p.AuthorityHeader() unexpected success") } } + +func TestParseWrongSection(t *testing.T) { + msg := smallTestMsg() + raw, err := msg.Pack() + if err != nil { + t.Fatal(err) + } + + var p Parser + if _, err := p.Start(raw); err != nil { + t.Fatal(err) + } + + if err := p.SkipAllQuestions(); err != nil { + t.Fatalf("p.SkipAllQuestions() = %v", err) + } + if _, err := p.AnswerHeader(); err != nil { + t.Fatalf("p.AnswerHeader() = %v", err) + } + if _, err := p.AuthorityHeader(); err == nil { + t.Fatalf("p.AuthorityHeader(): unexpected success in Answer section") + } + if err := p.SkipAuthority(); err == nil { + t.Fatalf("p.SkipAuthority(): unexpected success in Answer section") + } + if err := p.SkipAllAuthorities(); err == nil { + t.Fatalf("p.SkipAllAuthorities(): unexpected success in Answer section") + } +} From 3d2be970e8ac4df2e4e5f0dd892c668bafad41cc Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 25 Aug 2023 12:46:28 -0700 Subject: [PATCH 032/168] quic: fix testConn.uncheckedHandshake This test helper was sending the connection-under-test the wrong TLS 1-RTT data: It was resending the handshake-level data rather than the application-level data. This error was hidden by a crypto/tls bug, fixed in CL 522595. Change-Id: Ib672b174ddb1dfa5763f1eb3dd830932a0d26cad Reviewed-on: https://go-review.googlesource.com/c/net/+/522678 Run-TryBot: Damien Neil Reviewed-by: Bryan Mills Auto-Submit: Damien Neil TryBot-Result: Gopher Robot --- internal/quic/tls_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 1e3d6b6223..35cb8bf008 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -242,6 +242,7 @@ func fillCryptoFrames(d *testDatagram, data map[tls.QUICEncryptionLevel][]byte) // Useful for testing scenarios where configuration has // changed the handshake responses in some way. func (tc *testConn) uncheckedHandshake() { + tc.t.Helper() defer func(saved map[byte]bool) { tc.ignoreFrames = saved }(tc.ignoreFrames) @@ -268,6 +269,7 @@ func (tc *testConn) uncheckedHandshake() { ranges: []i64range[packetNumber]{{0, tc.sentFramePacket.num + 1}}, }) } else { + tc.wantIdle("initial frames are ignored") tc.writeFrames(packetTypeInitial, debugFrameCrypto{ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], @@ -285,7 +287,7 @@ func (tc *testConn) uncheckedHandshake() { debugFrameHandshakeDone{}) tc.writeFrames(packetType1RTT, debugFrameCrypto{ - data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + data: tc.cryptoDataIn[tls.QUICEncryptionLevelApplication], }) tc.wantFrame("client ACKs server's first 1-RTT packet", packetType1RTT, debugFrameAck{ From d8d84787ad6422cae430ca5e2455b1e0abf99225 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 9 Aug 2023 13:31:38 -0700 Subject: [PATCH 033/168] quic: read-closing and reset streams, wait on close s.Close waits for the peer to acknowledge receipt of sent data before returning. s.ReadClose closes the receive end of a stream, discarding buffered data and sending a STOP_SENDING frame to the peer. s.Reset(code) closes the send end of a stream with an error, which is sent to the peer in a RESET_STREAM frame. Receipt of a STOP_SENDING frame resets the stream locally and causes future writes to fail. Receipt of a RESET_STREAM frame causes future reads to fail. Stream state is currently retained even after a stream has been completely closed. A future CL will add cleanup. For golang/go#58547 Change-Id: I29088ae570db4079926ad426be6e85dace2122da Reviewed-on: https://go-review.googlesource.com/c/net/+/518435 Run-TryBot: Damien Neil Reviewed-by: Jonathan Amsterdam TryBot-Result: Gopher Robot --- internal/quic/conn.go | 21 ++ internal/quic/conn_async_test.go | 46 +++- internal/quic/conn_loss.go | 4 +- internal/quic/conn_loss_test.go | 55 ++++- internal/quic/conn_recv.go | 30 ++- internal/quic/errors.go | 8 + internal/quic/stream.go | 253 ++++++++++++++----- internal/quic/stream_test.go | 404 +++++++++++++++++++++++++++++-- internal/quic/wire.go | 5 +- 9 files changed, 732 insertions(+), 94 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 0952a79e81..ee8f011f8a 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -73,6 +73,7 @@ type connTestHooks interface { handleTLSEvent(tls.QUICEvent) newConnID(seq int64) ([]byte, error) waitAndLockGate(ctx context.Context, g *gate) error + waitOnDone(ctx context.Context, ch <-chan struct{}) error } func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l connListener, hooks connTestHooks) (*Conn, error) { @@ -311,6 +312,26 @@ func (c *Conn) waitAndLockGate(ctx context.Context, g *gate) error { return g.waitAndLockContext(ctx) } +func (c *Conn) waitOnDone(ctx context.Context, ch <-chan struct{}) error { + if c.testHooks != nil { + return c.testHooks.waitOnDone(ctx, ch) + } + // Check the channel before the context. + // We always prefer to return results when available, + // even when provided with an already-canceled context. + select { + case <-ch: + return nil + default: + } + select { + case <-ch: + case <-ctx.Done(): + return ctx.Err() + } + return nil +} + // abort terminates a connection with an error. func (c *Conn) abort(now time.Time, err error) { if c.errForPeer == nil { diff --git a/internal/quic/conn_async_test.go b/internal/quic/conn_async_test.go index 2078325a53..0da3ddb452 100644 --- a/internal/quic/conn_async_test.go +++ b/internal/quic/conn_async_test.go @@ -82,10 +82,11 @@ func (a *asyncOp[T]) result() (v T, err error) { } // A blockedAsync is a blocked async operation. -// -// Currently, the only type of blocked operation is one waiting on a gate. type blockedAsync struct { - g *gate + // Exactly one of these will be set, depending on the type of blocked operation. + g *gate + ch <-chan struct{} + donec chan struct{} // closed when the operation is unblocked } @@ -133,6 +134,25 @@ func (as *asyncTestState) waitAndLockGate(ctx context.Context, g *gate) error { // Gate can be acquired without blocking. return nil } + return as.block(ctx, &blockedAsync{ + g: g, + }) +} + +// waitOnDone replaces receiving from a chan struct{} in tests. +func (as *asyncTestState) waitOnDone(ctx context.Context, ch <-chan struct{}) error { + select { + case <-ch: + return nil // read without blocking + default: + } + return as.block(ctx, &blockedAsync{ + ch: ch, + }) +} + +// block waits for a blocked async operation to complete. +func (as *asyncTestState) block(ctx context.Context, b *blockedAsync) error { if err := ctx.Err(); err != nil { // Context has already expired. return err @@ -144,12 +164,9 @@ func (as *asyncTestState) waitAndLockGate(ctx context.Context, g *gate) error { // which may have unpredictable results. panic("blocking async point with unexpected Context") } + b.donec = make(chan struct{}) // Record this as a pending blocking operation. as.mu.Lock() - b := &blockedAsync{ - g: g, - donec: make(chan struct{}), - } as.blocked[b] = struct{}{} as.mu.Unlock() // Notify the creator of the operation that we're blocked, @@ -169,8 +186,19 @@ func (as *asyncTestState) wakeAsync() bool { as.mu.Lock() var woken *blockedAsync for w := range as.blocked { - if w.g.lockIfSet() { - woken = w + switch { + case w.g != nil: + if w.g.lockIfSet() { + woken = w + } + case w.ch != nil: + select { + case <-w.ch: + woken = w + default: + } + } + if woken != nil { delete(as.blocked, woken) break } diff --git a/internal/quic/conn_loss.go b/internal/quic/conn_loss.go index f42f7e5282..103db9fa41 100644 --- a/internal/quic/conn_loss.go +++ b/internal/quic/conn_loss.go @@ -44,7 +44,9 @@ func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetF case frameTypeCrypto: start, end := sent.nextRange() c.crypto[space].ackOrLoss(start, end, fate) - case frameTypeMaxStreamData, + case frameTypeResetStream, + frameTypeStopSending, + frameTypeMaxStreamData, frameTypeStreamDataBlocked: id := streamID(sent.nextInt()) s := c.streamForID(id) diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index d9445150a3..dc0dc6cd33 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -75,7 +75,58 @@ func (tc *testConn) triggerLossOrPTO(ptype packetType, pto bool) { }) } -func TestLostCRYPTOFrame(t *testing.T) { +func TestLostResetStreamFrame(t *testing.T) { + // "Cancellation of stream transmission, as carried in a RESET_STREAM frame, + // is sent until acknowledged or until all stream data is acknowledged by the peer [...]" + // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.4 + lostFrameTest(t, func(t *testing.T, pto bool) { + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) + tc.ignoreFrame(frameTypeAck) + + s.Reset(1) + tc.wantFrame("reset stream", + packetType1RTT, debugFrameResetStream{ + id: s.id, + code: 1, + }) + + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("resent RESET_STREAM frame", + packetType1RTT, debugFrameResetStream{ + id: s.id, + code: 1, + }) + }) +} + +func TestLostStopSendingFrame(t *testing.T) { + // "[...] a request to cancel stream transmission, as encoded in a STOP_SENDING frame, + // is sent until the receiving part of the stream enters either a "Data Recvd" or + // "Reset Recvd" state [...]" + // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.5 + // + // Technically, we can stop sending a STOP_SENDING frame if the peer sends + // us all the data for the stream or resets it. We don't bother tracking this, + // however, so we'll keep sending the frame until it is acked. This is harmless. + lostFrameTest(t, func(t *testing.T, pto bool) { + tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, permissiveTransportParameters) + tc.ignoreFrame(frameTypeAck) + + s.CloseRead() + tc.wantFrame("stream is read-closed", + packetType1RTT, debugFrameStopSending{ + id: s.id, + }) + + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("resent STOP_SENDING frame", + packetType1RTT, debugFrameStopSending{ + id: s.id, + }) + }) +} + +func TestLostCryptoFrame(t *testing.T) { // "Data sent in CRYPTO frames is retransmitted [...] until all data has been acknowledged." // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.1 lostFrameTest(t, func(t *testing.T, pto bool) { @@ -176,7 +227,7 @@ func TestLostStreamWithData(t *testing.T) { off: 4, data: data[4:8], }) - s.Close() + s.CloseWrite() tc.wantFrame("send FIN", packetType1RTT, debugFrameStream{ id: s.id, diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 00985b6703..e0a91ab00c 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -161,12 +161,12 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, if !frameOK(c, ptype, __01) { return } - _, _, _, n = consumeResetStreamFrame(payload) + n = c.handleResetStreamFrame(now, space, payload) case frameTypeStopSending: if !frameOK(c, ptype, __01) { return } - _, _, n = consumeStopSendingFrame(payload) + n = c.handleStopSendingFrame(now, space, payload) case frameTypeCrypto: if !frameOK(c, ptype, IH_1) { return @@ -291,6 +291,32 @@ func (c *Conn) handleMaxStreamDataFrame(now time.Time, payload []byte) int { return n } +func (c *Conn) handleResetStreamFrame(now time.Time, space numberSpace, payload []byte) int { + id, code, finalSize, n := consumeResetStreamFrame(payload) + if n < 0 { + return -1 + } + if s := c.streamForFrame(now, id, recvStream); s != nil { + if err := s.handleReset(code, finalSize); err != nil { + c.abort(now, err) + } + } + return n +} + +func (c *Conn) handleStopSendingFrame(now time.Time, space numberSpace, payload []byte) int { + id, code, n := consumeStopSendingFrame(payload) + if n < 0 { + return -1 + } + if s := c.streamForFrame(now, id, sendStream); s != nil { + if err := s.handleStopSending(code); err != nil { + c.abort(now, err) + } + } + return n +} + func (c *Conn) handleCryptoFrame(now time.Time, space numberSpace, payload []byte) int { off, data, n := consumeCryptoFrame(payload) err := c.handleCrypto(now, space, off, data) diff --git a/internal/quic/errors.go b/internal/quic/errors.go index 55d32f3106..f156859325 100644 --- a/internal/quic/errors.go +++ b/internal/quic/errors.go @@ -99,6 +99,14 @@ func (e peerTransportError) Error() string { return fmt.Sprintf("peer closed connection: %v: %q", e.code, e.reason) } +// A StreamErrorCode is an application protocol error code (RFC 9000, Section 20.2) +// indicating whay a stream is being closed. +type StreamErrorCode uint64 + +func (e StreamErrorCode) Error() string { + return fmt.Sprintf("stream error code %v", uint64(e)) +} + // An ApplicationError is an application protocol error code (RFC 9000, Section 20.2). // Application protocol errors may be sent when terminating a stream or connection. type ApplicationError struct { diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 83215dfd3e..12117dbd39 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -9,6 +9,7 @@ package quic import ( "context" "errors" + "fmt" "io" ) @@ -20,28 +21,33 @@ type Stream struct { // // The gate condition is set if a read from the stream will not block, // either because the stream has available data or because the read will fail. - ingate gate - in pipe // received data - inwin int64 // last MAX_STREAM_DATA sent to the peer - insendmax sentVal // set when we should send MAX_STREAM_DATA to the peer - inmaxbuf int64 // maximum amount of data we will buffer - insize int64 // stream final size; -1 before this is known - inset rangeset[int64] // received ranges + ingate gate + in pipe // received data + inwin int64 // last MAX_STREAM_DATA sent to the peer + insendmax sentVal // set when we should send MAX_STREAM_DATA to the peer + inmaxbuf int64 // maximum amount of data we will buffer + insize int64 // stream final size; -1 before this is known + inset rangeset[int64] // received ranges + inclosed sentVal // set by CloseRead + inresetcode int64 // RESET_STREAM code received from the peer; -1 if not reset // outgate's lock guards all send-related state. // // The gate condition is set if a write to the stream will not block, // either because the stream has available flow control or because // the write will fail. - outgate gate - out pipe // buffered data to send - outwin int64 // maximum MAX_STREAM_DATA received from the peer - outmaxbuf int64 // maximum amount of data we will buffer - outunsent rangeset[int64] // ranges buffered but not yet sent - outacked rangeset[int64] // ranges sent and acknowledged - outopened sentVal // set if we should open the stream - outclosed sentVal // set by CloseWrite - outblocked sentVal // set when a write to the stream is blocked by flow control + outgate gate + out pipe // buffered data to send + outwin int64 // maximum MAX_STREAM_DATA received from the peer + outmaxbuf int64 // maximum amount of data we will buffer + outunsent rangeset[int64] // ranges buffered but not yet sent + outacked rangeset[int64] // ranges sent and acknowledged + outopened sentVal // set if we should open the stream + outclosed sentVal // set by CloseWrite + outblocked sentVal // set when a write to the stream is blocked by flow control + outreset sentVal // set by Reset + outresetcode uint64 // reset code to send in RESET_STREAM + outdone chan struct{} // closed when all data sent prev, next *Stream // guarded by streamsState.sendMu } @@ -54,11 +60,13 @@ type Stream struct { // unlocking outgate will set the stream writability state.) func newStream(c *Conn, id streamID) *Stream { s := &Stream{ - conn: c, - id: id, - insize: -1, // -1 indicates the stream size is unknown - ingate: newLockedGate(), - outgate: newLockedGate(), + conn: c, + id: id, + insize: -1, // -1 indicates the stream size is unknown + inresetcode: -1, // -1 indicates no RESET_STREAM received + ingate: newLockedGate(), + outgate: newLockedGate(), + outdone: make(chan struct{}), } return s } @@ -87,7 +95,8 @@ func (s *Stream) Read(b []byte) (n int, err error) { // // If the peer closes the stream cleanly, ReadContext returns io.EOF after // returning all data sent by the peer. -// If the peer terminates reads abruptly, ReadContext returns StreamResetError. +// If the peer aborts reads on the stream, ReadContext returns +// an error wrapping StreamResetCode. func (s *Stream) ReadContext(ctx context.Context, b []byte) (n int, err error) { if s.IsWriteOnly() { return 0, errors.New("read from write-only stream") @@ -97,6 +106,12 @@ func (s *Stream) ReadContext(ctx context.Context, b []byte) (n int, err error) { return 0, err } defer s.inUnlock() + if s.inresetcode != -1 { + return 0, fmt.Errorf("stream reset by peer: %w", StreamErrorCode(s.inresetcode)) + } + if s.inclosed.isSet() { + return 0, errors.New("read from closed stream") + } if s.insize == s.in.start { return 0, io.EOF } @@ -145,26 +160,17 @@ func (s *Stream) Write(b []byte) (n int, err error) { // Buffered data is only sent when the buffer is sufficiently full. // Call the Flush method to ensure buffered data is sent. // -// If the peer aborts reads on the stream, ReadContext returns StreamResetError. +// TODO: Implement Flush. func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) { if s.IsReadOnly() { return 0, errors.New("write to read-only stream") } canWrite := s.outgate.lock() - if s.outclosed.isSet() { - s.outUnlock() - return 0, errors.New("write to closed stream") - } - if len(b) == 0 { - // We aren't writing any data, but send a STREAM frame to open the stream - // if we haven't done so already. - s.outopened.set() - } - for len(b) > 0 { + for { // The first time through this loop, we may or may not be write blocked. // We exit the loop after writing all data, so on subsequent passes through // the loop we are always write blocked. - if !canWrite { + if len(b) > 0 && !canWrite { // We're blocked, either by flow control or by our own buffer limit. // We either need the peer to extend our flow control window, // or ack some of our outstanding packets. @@ -181,6 +187,21 @@ func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) // write blocked. (Unlike traditional condition variables, gates do not // have spurious wakeups.) } + if s.outreset.isSet() { + s.outUnlock() + return n, errors.New("write to reset stream") + } + if s.outclosed.isSet() { + s.outUnlock() + return n, errors.New("write to closed stream") + } + // We set outopened here rather than below, + // so if this is a zero-length write we still + // open the stream despite not writing any data to it. + s.outopened.set() + if len(b) == 0 { + break + } s.outblocked.clear() // Write limit is min(our own buffer limit, the peer-provided flow control window). // This is a stream offset. @@ -191,7 +212,6 @@ func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) // Copy the data into the output buffer and mark it as unsent. s.outunsent.add(s.out.end, s.out.end+nn) s.out.writeAt(b[:nn], s.out.end) - s.outopened.set() b = b[nn:] n += int(nn) // If we have bytes left to send, we're blocked. @@ -218,9 +238,8 @@ func (s *Stream) Close() error { func (s *Stream) CloseContext(ctx context.Context) error { s.CloseRead() s.CloseWrite() - // TODO: wait for peer to acknowledge data // TODO: Return code from peer's RESET_STREAM frame? - return nil + return s.conn.waitOnDone(ctx, s.outdone) } // CloseRead aborts reads on the stream. @@ -233,7 +252,17 @@ func (s *Stream) CloseRead() { if s.IsWriteOnly() { return } - // TODO: support read-closing streams with a STOP_SENDING frame + s.ingate.lock() + defer s.inUnlock() + if s.inset.isrange(0, s.insize) || s.inresetcode != -1 { + // We've already received all data from the peer, + // so there's no need to send STOP_SENDING. + // This is the same as saying we sent one and they got it. + s.inclosed.setReceived() + } else { + s.inclosed.set() + } + s.in.discardBefore(s.in.end) } // CloseWrite aborts writes on the stream. @@ -251,6 +280,29 @@ func (s *Stream) CloseWrite() { s.outclosed.set() } +// Reset aborts writes on the stream and notifies the peer +// that the stream was terminated abruptly. +// Any blocked writes will be unblocked and return errors. +// +// Reset sends the application protocol error code to the peer. +// It does not wait for the peer to acknowledge receipt of the error. +// Use CloseContext to wait for the peer's acknowledgement. +func (s *Stream) Reset(code uint64) { + s.outgate.lock() + defer s.outUnlock() + if s.outreset.isSet() { + return + } + // We could check here to see if the stream is closed and the + // peer has acked all the data and the FIN, but sending an + // extra RESET_STREAM in this case is harmless. + s.outreset.set() + s.outresetcode = code + s.out.discardBefore(s.out.end) + s.outunsent = rangeset[int64]{} + s.outblocked.clear() +} + // inUnlock unlocks s.ingate. // It sets the gate condition if reads from s will not block. // If s has receive-related frames to write, it notifies the Conn. @@ -263,11 +315,13 @@ func (s *Stream) inUnlock() { // inUnlockNoQueue is inUnlock, // but reports whether s has frames to write rather than notifying the Conn. func (s *Stream) inUnlockNoQueue() (shouldSend bool) { - // TODO: STOP_SENDING canRead := s.inset.contains(s.in.start) || // data available to read - s.insize == s.in.start // at EOF - s.ingate.unlock(canRead) - return s.insendmax.shouldSend() // STREAM_MAX_DATA + s.insize == s.in.start || // at EOF + s.inresetcode != -1 || // reset by peer + s.inclosed.isSet() // closed locally + defer s.ingate.unlock(canRead) + return s.insendmax.shouldSend() || // STREAM_MAX_DATA + s.inclosed.shouldSend() // STOP_SENDING } // outUnlock unlocks s.outgate. @@ -282,10 +336,24 @@ func (s *Stream) outUnlock() { // outUnlockNoQueue is outUnlock, // but reports whether s has frames to write rather than notifying the Conn. func (s *Stream) outUnlockNoQueue() (shouldSend bool) { + isDone := s.outclosed.isReceived() && s.outacked.isrange(0, s.out.end) || // all data acked + s.outreset.isSet() // reset locally + if isDone { + select { + case <-s.outdone: + default: + close(s.outdone) + } + } lim := min(s.out.start+s.outmaxbuf, s.outwin) canWrite := lim > s.out.end || // available flow control - s.outclosed.isSet() // closed - s.outgate.unlock(canWrite) + s.outclosed.isSet() || // closed locally + s.outreset.isSet() // reset locally + defer s.outgate.unlock(canWrite) + if s.outreset.isSet() { + // If the stream is reset locally, the only frame we'll send is RESET_STREAM. + return s.outreset.shouldSend() + } return len(s.outunsent) > 0 || // STREAM frame with data s.outclosed.shouldSend() || // STREAM frame with FIN bit s.outopened.shouldSend() || // STREAM frame with no data @@ -297,21 +365,17 @@ func (s *Stream) handleData(off int64, b []byte, fin bool) error { s.ingate.lock() defer s.inUnlock() end := off + int64(len(b)) - if end > s.inwin { - // The peer sent us data past the maximum flow control window we gave them. - return localTransportError(errFlowControl) + if err := s.checkStreamBounds(end, fin); err != nil { + return err } - if s.insize != -1 && end > s.insize { - // The peer sent us data past the final size of the stream they previously gave us. - return localTransportError(errFinalSize) + if s.inclosed.isSet() || s.inresetcode != -1 { + // The user read-closed the stream, or the peer reset it. + // Either way, we can discard this frame. + return nil } s.in.writeAt(b, off) s.inset.add(off, end) if fin { - if s.insize != -1 && s.insize != end { - // The peer changed the final size of the stream. - return localTransportError(errFinalSize) - } s.insize = end // The peer has enough flow control window to send the entire stream. s.insendmax.clear() @@ -319,6 +383,53 @@ func (s *Stream) handleData(off int64, b []byte, fin bool) error { return nil } +// handleReset handles a RESET_STREAM frame. +func (s *Stream) handleReset(code uint64, finalSize int64) error { + s.ingate.lock() + defer s.inUnlock() + const fin = true + if err := s.checkStreamBounds(finalSize, fin); err != nil { + return err + } + if s.inresetcode != -1 { + // The stream was already reset. + return nil + } + s.in.discardBefore(s.in.end) + s.inresetcode = int64(code) + s.insize = finalSize + return nil +} + +// checkStreamBounds validates the stream offset in a STREAM or RESET_STREAM frame. +func (s *Stream) checkStreamBounds(end int64, fin bool) error { + if end > s.inwin { + // The peer sent us data past the maximum flow control window we gave them. + return localTransportError(errFlowControl) + } + if s.insize != -1 && end > s.insize { + // The peer sent us data past the final size of the stream they previously gave us. + return localTransportError(errFinalSize) + } + if fin && s.insize != -1 && end != s.insize { + // The peer changed the final size of the stream. + return localTransportError(errFinalSize) + } + if fin && end < s.in.end { + // The peer has previously sent us data past the final size. + return localTransportError(errFinalSize) + } + return nil +} + +// handleStopSending handles a STOP_SENDING frame. +func (s *Stream) handleStopSending(code uint64) error { + // Peer requests that we reset this stream. + // https://www.rfc-editor.org/rfc/rfc9000#section-3.5-4 + s.Reset(code) + return nil +} + // handleMaxStreamData handles an update received in a MAX_STREAM_DATA frame. func (s *Stream) handleMaxStreamData(maxStreamData int64) error { s.outgate.lock() @@ -336,6 +447,14 @@ func (s *Stream) ackOrLoss(pnum packetNumber, ftype byte, fate packetFate) { // Frames which are always the same (STOP_SENDING, RESET_STREAM) // can be marked as received if any packet carrying this frame is acked. switch ftype { + case frameTypeResetStream: + s.outgate.lock() + s.outreset.ackOrLoss(pnum, fate) + s.outUnlock() + case frameTypeStopSending: + s.ingate.lock() + s.inclosed.ackOrLoss(pnum, fate) + s.inUnlock() case frameTypeMaxStreamData: s.ingate.lock() s.insendmax.ackLatestOrLoss(pnum, fate) @@ -345,7 +464,6 @@ func (s *Stream) ackOrLoss(pnum packetNumber, ftype byte, fate packetFate) { s.outblocked.ackLatestOrLoss(pnum, fate) s.outUnlock() default: - // TODO: Handle STOP_SENDING, RESET_STREAM. panic("unhandled frame type") } } @@ -358,6 +476,10 @@ func (s *Stream) ackOrLossData(pnum packetNumber, start, end int64, fin bool, fa if fin { s.outclosed.ackOrLoss(pnum, fate) } + if s.outreset.isSet() { + // If the stream has been reset, we don't care any more. + return + } switch fate { case packetAcked: s.outacked.add(start, end) @@ -385,6 +507,15 @@ func (s *Stream) ackOrLossData(pnum packetNumber, start, end int64, fin bool, fa func (s *Stream) appendInFrames(w *packetWriter, pnum packetNumber, pto bool) bool { s.ingate.lock() defer s.inUnlockNoQueue() + if s.inclosed.shouldSendPTO(pto) { + // We don't currently have an API for setting the error code. + // Just send zero. + code := uint64(0) + if !w.appendStopSendingFrame(s.id, code) { + return false + } + s.inclosed.setSent(pnum) + } // TODO: STOP_SENDING if s.insendmax.shouldSendPTO(pto) { // MAX_STREAM_DATA @@ -406,7 +537,17 @@ func (s *Stream) appendInFrames(w *packetWriter, pnum packetNumber, pto bool) bo func (s *Stream) appendOutFrames(w *packetWriter, pnum packetNumber, pto bool) bool { s.outgate.lock() defer s.outUnlockNoQueue() - // TODO: RESET_STREAM + if s.outreset.isSet() { + // RESET_STREAM + if s.outreset.shouldSendPTO(pto) { + if !w.appendResetStreamFrame(s.id, s.outresetcode, s.out.end) { + return false + } + s.outreset.setSent(pnum) + s.frameOpensStream(pnum) + } + return true + } if s.outblocked.shouldSendPTO(pto) { // STREAM_DATA_BLOCKED if !w.appendStreamDataBlockedFrame(s.id, s.out.end) { diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index d158e72afb..5904a93428 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -10,6 +10,7 @@ import ( "bytes" "context" "crypto/rand" + "errors" "fmt" "io" "reflect" @@ -489,32 +490,76 @@ func TestStreamReceiveDuplicateDataDoesNotViolateLimits(t *testing.T) { }) } -func TestStreamFinalSizeChangedByStreamFrame(t *testing.T) { - // "If a [...] STREAM frame is received indicating a change - // in the final size for the stream, an endpoint SHOULD - // respond with an error of type FINAL_SIZE_ERROR [...]" - // https://www.rfc-editor.org/rfc/rfc9000#section-4.5-5 +func finalSizeTest(t *testing.T, wantErr transportError, f func(tc *testConn, sid streamID) (finalSize int64), opts ...any) { testStreamTypes(t, "", func(t *testing.T, styp streamType) { - tc := newTestConn(t, serverSide) - tc.handshake() - sid := newStreamID(clientSide, styp, 0) + for _, test := range []struct { + name string + finalFrame func(tc *testConn, sid streamID, finalSize int64) + }{{ + name: "FIN", + finalFrame: func(tc *testConn, sid streamID, finalSize int64) { + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: sid, + off: finalSize, + fin: true, + }) + }, + }, { + name: "RESET_STREAM", + finalFrame: func(tc *testConn, sid streamID, finalSize int64) { + tc.writeFrames(packetType1RTT, debugFrameResetStream{ + id: sid, + finalSize: finalSize, + }) + }, + }} { + t.Run(test.name, func(t *testing.T) { + tc := newTestConn(t, serverSide, opts...) + tc.handshake() + sid := newStreamID(clientSide, styp, 0) + finalSize := f(tc, sid) + test.finalFrame(tc, sid, finalSize) + tc.wantFrame("change in final size of stream is an error", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: wantErr, + }, + ) + }) + } + }) +} - const write1size = 4 +func TestStreamFinalSizeChangedAfterFin(t *testing.T) { + // "If a RESET_STREAM or STREAM frame is received indicating a change + // in the final size for the stream, an endpoint SHOULD respond with + // an error of type FINAL_SIZE_ERROR [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-4.5-5 + finalSizeTest(t, errFinalSize, func(tc *testConn, sid streamID) (finalSize int64) { tc.writeFrames(packetType1RTT, debugFrameStream{ id: sid, off: 10, fin: true, }) + return 9 + }) +} + +func TestStreamFinalSizeBeforePreviousData(t *testing.T) { + finalSizeTest(t, errFinalSize, func(tc *testConn, sid streamID) (finalSize int64) { tc.writeFrames(packetType1RTT, debugFrameStream{ - id: sid, - off: 9, - fin: true, + id: sid, + off: 10, + data: []byte{0}, }) - tc.wantFrame("change in final size of stream is an error", - packetType1RTT, debugFrameConnectionCloseTransport{ - code: errFinalSize, - }, - ) + return 9 + }) +} + +func TestStreamFinalSizePastMaxStreamData(t *testing.T) { + finalSizeTest(t, errFlowControl, func(tc *testConn, sid streamID) (finalSize int64) { + return 11 + }, func(c *Config) { + c.StreamReadBufferSize = 10 }) } @@ -637,6 +682,19 @@ func testStreamSendFrameInvalidState(t *testing.T, f func(sid streamID) debugFra }) } +func TestStreamResetStreamInvalidState(t *testing.T) { + // "An endpoint that receives a RESET_STREAM frame for a send-only + // stream MUST terminate the connection with error STREAM_STATE_ERROR." + // https://www.rfc-editor.org/rfc/rfc9000#section-19.4-3 + testStreamSendFrameInvalidState(t, func(sid streamID) debugFrame { + return debugFrameResetStream{ + id: sid, + code: 0, + finalSize: 0, + } + }) +} + func TestStreamStreamFrameInvalidState(t *testing.T) { // "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR // if it receives a STREAM frame for a locally initiated stream @@ -689,6 +747,20 @@ func testStreamReceiveFrameInvalidState(t *testing.T, f func(sid streamID) debug }) } +func TestStreamStopSendingInvalidState(t *testing.T) { + // "Receiving a STOP_SENDING frame for a locally initiated stream + // that has not yet been created MUST be treated as a connection error + // of type STREAM_STATE_ERROR. An endpoint that receives a STOP_SENDING + // frame for a receive-only stream MUST terminate the connection with + // error STREAM_STATE_ERROR." + // https://www.rfc-editor.org/rfc/rfc9000#section-19.5-2 + testStreamReceiveFrameInvalidState(t, func(sid streamID) debugFrame { + return debugFrameStopSending{ + id: sid, + } + }) +} + func TestStreamMaxStreamDataInvalidState(t *testing.T) { // "Receiving a MAX_STREAM_DATA frame for a locally initiated stream // that has not yet been created MUST be treated as a connection error @@ -743,13 +815,47 @@ func TestStreamWriteToReadOnlyStream(t *testing.T) { } } -func TestStreamWriteToClosedStream(t *testing.T) { - tc, s := newTestConnAndLocalStream(t, serverSide, bidiStream, func(p *transportParameters) { - p.initialMaxStreamsBidi = 1 - p.initialMaxData = 1 << 20 - p.initialMaxStreamDataBidiRemote = 1 << 20 +func TestStreamReadFromClosedStream(t *testing.T) { + tc, s := newTestConnAndRemoteStream(t, serverSide, bidiStream, permissiveTransportParameters) + s.CloseRead() + tc.wantFrame("CloseRead sends a STOP_SENDING frame", + packetType1RTT, debugFrameStopSending{ + id: s.id, + }) + wantErr := "read from closed stream" + if n, err := s.Read(make([]byte, 16)); err == nil || !strings.Contains(err.Error(), wantErr) { + t.Errorf("s.Read() = %v, %v; want error %q", n, err, wantErr) + } + // Data which shows up after STOP_SENDING is discarded. + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + data: []byte{1, 2, 3}, + fin: true, + }) + if n, err := s.Read(make([]byte, 16)); err == nil || !strings.Contains(err.Error(), wantErr) { + t.Errorf("s.Read() = %v, %v; want error %q", n, err, wantErr) + } +} + +func TestStreamCloseReadWithAllDataReceived(t *testing.T) { + tc, s := newTestConnAndRemoteStream(t, serverSide, bidiStream, permissiveTransportParameters) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + data: []byte{1, 2, 3}, + fin: true, }) - s.Close() + s.CloseRead() + tc.wantIdle("CloseRead in Data Recvd state doesn't need to send STOP_SENDING") + // We had all the data for the stream, but CloseRead discarded it. + wantErr := "read from closed stream" + if n, err := s.Read(make([]byte, 16)); err == nil || !strings.Contains(err.Error(), wantErr) { + t.Errorf("s.Read() = %v, %v; want error %q", n, err, wantErr) + } +} + +func TestStreamWriteToClosedStream(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, serverSide, bidiStream, permissiveTransportParameters) + s.CloseWrite() tc.wantFrame("stream is opened after being closed", packetType1RTT, debugFrameStream{ id: s.id, @@ -763,6 +869,45 @@ func TestStreamWriteToClosedStream(t *testing.T) { } } +func TestStreamResetBlockedStream(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, serverSide, bidiStream, func(p *transportParameters) { + p.initialMaxStreamsBidi = 1 + p.initialMaxData = 1 << 20 + p.initialMaxStreamDataBidiRemote = 4 + }) + tc.ignoreFrame(frameTypeStreamDataBlocked) + writing := runAsync(tc, func(ctx context.Context) (int, error) { + return s.WriteContext(ctx, []byte{0, 1, 2, 3, 4, 5, 6, 7}) + }) + tc.wantFrame("stream writes data until blocked by flow control", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + data: []byte{0, 1, 2, 3}, + }) + s.Reset(42) + tc.wantFrame("stream is reset", + packetType1RTT, debugFrameResetStream{ + id: s.id, + code: 42, + finalSize: 4, + }) + wantErr := "write to reset stream" + if n, err := writing.result(); n != 4 || !strings.Contains(err.Error(), wantErr) { + t.Errorf("s.Write() interrupted by Reset: %v, %q; want 4, %q", n, err, wantErr) + } + tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: 1 << 20, + }) + tc.wantIdle("flow control is available, but stream has been reset") + s.Reset(100) + tc.wantIdle("resetting stream a second time has no effect") + if n, err := s.Write([]byte{}); err == nil || !strings.Contains(err.Error(), wantErr) { + t.Errorf("s.Write() = %v, %v; want error %q", n, err, wantErr) + } +} + func TestStreamWriteMoreThanOnePacketOfData(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) { p.initialMaxStreamsUni = 1 @@ -797,6 +942,209 @@ func TestStreamWriteMoreThanOnePacketOfData(t *testing.T) { } } +func TestStreamCloseWaitsForAcks(t *testing.T) { + ctx := canceledContext() + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) + data := make([]byte, 100) + s.WriteContext(ctx, data) + tc.wantFrame("conn sends data for the stream", + packetType1RTT, debugFrameStream{ + id: s.id, + data: data, + }) + if err := s.CloseContext(ctx); err != context.Canceled { + t.Fatalf("s.Close() = %v, want context.Canceled (data not acked yet)", err) + } + tc.wantFrame("conn sends FIN for closed stream", + packetType1RTT, debugFrameStream{ + id: s.id, + off: int64(len(data)), + fin: true, + data: []byte{}, + }) + closing := runAsync(tc, func(ctx context.Context) (struct{}, error) { + return struct{}{}, s.CloseContext(ctx) + }) + if _, err := closing.result(); err != errNotDone { + t.Fatalf("s.CloseContext() = %v, want it to block waiting for acks", err) + } + tc.writeFrames(packetType1RTT, debugFrameAck{ + ranges: []i64range[packetNumber]{{0, tc.sentFramePacket.num + 1}}, + }) + if _, err := closing.result(); err != nil { + t.Fatalf("s.CloseContext() = %v, want nil (all data acked)", err) + } +} + +func TestStreamCloseUnblocked(t *testing.T) { + for _, test := range []struct { + name string + unblock func(tc *testConn, s *Stream) + }{{ + name: "data received", + unblock: func(tc *testConn, s *Stream) { + tc.writeFrames(packetType1RTT, debugFrameAck{ + ranges: []i64range[packetNumber]{{0, tc.sentFramePacket.num + 1}}, + }) + }, + }, { + name: "stop sending received", + unblock: func(tc *testConn, s *Stream) { + tc.writeFrames(packetType1RTT, debugFrameStopSending{ + id: s.id, + }) + }, + }, { + name: "stream reset", + unblock: func(tc *testConn, s *Stream) { + s.Reset(0) + tc.wait() // wait for test conn to process the Reset + }, + }} { + t.Run(test.name, func(t *testing.T) { + ctx := canceledContext() + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) + data := make([]byte, 100) + s.WriteContext(ctx, data) + tc.wantFrame("conn sends data for the stream", + packetType1RTT, debugFrameStream{ + id: s.id, + data: data, + }) + if err := s.CloseContext(ctx); err != context.Canceled { + t.Fatalf("s.Close() = %v, want context.Canceled (data not acked yet)", err) + } + tc.wantFrame("conn sends FIN for closed stream", + packetType1RTT, debugFrameStream{ + id: s.id, + off: int64(len(data)), + fin: true, + data: []byte{}, + }) + closing := runAsync(tc, func(ctx context.Context) (struct{}, error) { + return struct{}{}, s.CloseContext(ctx) + }) + if _, err := closing.result(); err != errNotDone { + t.Fatalf("s.CloseContext() = %v, want it to block waiting for acks", err) + } + test.unblock(tc, s) + if _, err := closing.result(); err != nil { + t.Fatalf("s.CloseContext() = %v, want nil (all data acked)", err) + } + }) + } +} + +func TestStreamPeerResetsWithUnreadAndUnsentData(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + ctx := canceledContext() + tc, s := newTestConnAndRemoteStream(t, serverSide, styp) + data := []byte{0, 1, 2, 3, 4, 5, 6, 7} + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + data: data, + }) + got := make([]byte, 4) + if n, err := s.ReadContext(ctx, got); n != len(got) || err != nil { + t.Fatalf("Read start of stream: got %v, %v; want %v, nil", n, err, len(got)) + } + const sentCode = 42 + tc.writeFrames(packetType1RTT, debugFrameResetStream{ + id: s.id, + finalSize: 20, + code: sentCode, + }) + wantErr := StreamErrorCode(sentCode) + if n, err := s.ReadContext(ctx, got); n != 0 || !errors.Is(err, wantErr) { + t.Fatalf("Read reset stream: got %v, %v; want 0, %v", n, err, wantErr) + } + }) +} + +func TestStreamPeerResetWakesBlockedRead(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc, s := newTestConnAndRemoteStream(t, serverSide, styp) + reader := runAsync(tc, func(ctx context.Context) (int, error) { + got := make([]byte, 4) + return s.ReadContext(ctx, got) + }) + const sentCode = 42 + tc.writeFrames(packetType1RTT, debugFrameResetStream{ + id: s.id, + finalSize: 20, + code: sentCode, + }) + wantErr := StreamErrorCode(sentCode) + if n, err := reader.result(); n != 0 || !errors.Is(err, wantErr) { + t.Fatalf("Read reset stream: got %v, %v; want 0, %v", n, err, wantErr) + } + }) +} + +func TestStreamPeerResetFollowedByData(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc, s := newTestConnAndRemoteStream(t, serverSide, styp) + tc.writeFrames(packetType1RTT, debugFrameResetStream{ + id: s.id, + finalSize: 4, + code: 1, + }) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + data: []byte{0, 1, 2, 3}, + }) + // Another reset with a different code, for good measure. + tc.writeFrames(packetType1RTT, debugFrameResetStream{ + id: s.id, + finalSize: 4, + code: 2, + }) + wantErr := StreamErrorCode(1) + if n, err := s.Read(make([]byte, 16)); n != 0 || !errors.Is(err, wantErr) { + t.Fatalf("Read from reset stream: got %v, %v; want 0, %v", n, err, wantErr) + } + }) +} + +func TestStreamPeerStopSendingForActiveStream(t *testing.T) { + // "An endpoint that receives a STOP_SENDING frame MUST send a RESET_STREAM frame if + // the stream is in the "Ready" or "Send" state." + // https://www.rfc-editor.org/rfc/rfc9000#section-3.5-4 + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc, s := newTestConnAndLocalStream(t, serverSide, styp, permissiveTransportParameters) + for i := 0; i < 4; i++ { + s.Write([]byte{byte(i)}) + tc.wantFrame("write sends a STREAM frame to peer", + packetType1RTT, debugFrameStream{ + id: s.id, + off: int64(i), + data: []byte{byte(i)}, + }) + } + tc.writeFrames(packetType1RTT, debugFrameStopSending{ + id: s.id, + code: 42, + }) + tc.wantFrame("receiving STOP_SENDING causes stream reset", + packetType1RTT, debugFrameResetStream{ + id: s.id, + code: 42, + finalSize: 4, + }) + if n, err := s.Write([]byte{0}); err == nil { + t.Errorf("s.Write() after STOP_SENDING = %v, %v; want error", n, err) + } + // This ack will result in some of the previous frames being marked as lost. + tc.writeFrames(packetType1RTT, debugFrameAck{ + ranges: []i64range[packetNumber]{{ + tc.sentFramePacket.num, + tc.sentFramePacket.num + 1, + }}, + }) + tc.wantIdle("lost STREAM frames for reset stream are not resent") + }) +} + func newTestConnAndLocalStream(t *testing.T, side connSide, styp streamType, opts ...any) (*testConn, *Stream) { t.Helper() ctx := canceledContext() @@ -825,3 +1173,13 @@ func newTestConnAndRemoteStream(t *testing.T, side connSide, styp streamType, op } return tc, s } + +// permissiveTransportParameters may be passed as an option to newTestConn. +func permissiveTransportParameters(p *transportParameters) { + p.initialMaxStreamsBidi = maxVarint + p.initialMaxStreamsUni = maxVarint + p.initialMaxData = maxVarint + p.initialMaxStreamDataBidiRemote = maxVarint + p.initialMaxStreamDataBidiLocal = maxVarint + p.initialMaxStreamDataUni = maxVarint +} diff --git a/internal/quic/wire.go b/internal/quic/wire.go index f0643c9229..8486029151 100644 --- a/internal/quic/wire.go +++ b/internal/quic/wire.go @@ -8,7 +8,10 @@ package quic import "encoding/binary" -const maxVarintSize = 8 +const ( + maxVarintSize = 8 // encoded size in bytes + maxVarint = (1 << 62) - 1 +) // consumeVarint parses a variable-length integer, reporting its length. // It returns a negative length upon an error. From efb8d7ab942d2b798abae11dfebfb9043cac78be Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Sat, 26 Aug 2023 16:55:17 +0000 Subject: [PATCH 034/168] dns/dnsmessage: don't include bytes after name.Length in the compression map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This improves the performance of name compression and makes the name.Data[name.Length:] not included in the compression map, it is unnecessary and might cause issues (i.e. reusing the Name struct, without using the NewName function). goos: linux goarch: amd64 pkg: golang.org/x/net/dns/dnsmessage cpu: Intel(R) Core(TM) i5-4200M CPU @ 2.50GHz │ before │ after │ │ sec/op │ sec/op vs base │ Pack-4 15.672µ ± 13% 5.470µ ± 14% -65.10% (p=0.000 n=10) AppendPack-4 15.144µ ± 12% 5.330µ ± 10% -64.80% (p=0.000 n=10) geomean 15.41µ 5.400µ -64.95% │ before │ after │ │ B/op │ B/op vs base │ Pack-4 6.051Ki ± 0% 1.285Ki ± 0% -78.76% (p=0.000 n=10) AppendPack-4 5684.0 ± 0% 804.0 ± 0% -85.86% (p=0.000 n=10) geomean 5.795Ki 1.005Ki -82.67% │ before │ after │ │ allocs/op │ allocs/op vs base │ Pack-4 21.00 ± 0% 11.00 ± 0% -47.62% (p=0.000 n=10) AppendPack-4 20.00 ± 0% 10.00 ± 0% -50.00% (p=0.000 n=10) geomean 20.49 10.49 -48.82% Change-Id: Idf40d5d4790d37eb7253214f089eff859a937c60 GitHub-Last-Rev: a3182830e27086a0e12e116c7f7916468eb1edf2 GitHub-Pull-Request: golang/net#190 Reviewed-on: https://go-review.googlesource.com/c/net/+/522817 Run-TryBot: Mateusz Poliwczak TryBot-Result: Gopher Robot Reviewed-by: Ian Lance Taylor Run-TryBot: Ian Lance Taylor Reviewed-by: Damien Neil Auto-Submit: Ian Lance Taylor --- dns/dnsmessage/message.go | 11 +++++++++-- dns/dnsmessage/message_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index cd997bab07..9ddf2c2292 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -1961,6 +1961,8 @@ func (n *Name) pack(msg []byte, compression map[string]int, compressionOff int) return append(msg, 0), nil } + var nameAsStr string + // Emit sequence of counted strings, chopping at dots. for i, begin := 0, 0; i < int(n.Length); i++ { // Check for the end of the segment. @@ -1991,7 +1993,7 @@ func (n *Name) pack(msg []byte, compression map[string]int, compressionOff int) // segment. A pointer is two bytes with the two most significant // bits set to 1 to indicate that it is a pointer. if (i == 0 || n.Data[i-1] == '.') && compression != nil { - if ptr, ok := compression[string(n.Data[i:])]; ok { + if ptr, ok := compression[string(n.Data[i:n.Length])]; ok { // Hit. Emit a pointer instead of the rest of // the domain. return append(msg, byte(ptr>>8|0xC0), byte(ptr)), nil @@ -2000,7 +2002,12 @@ func (n *Name) pack(msg []byte, compression map[string]int, compressionOff int) // Miss. Add the suffix to the compression table if the // offset can be stored in the available 14 bytes. if len(msg) <= int(^uint16(0)>>2) { - compression[string(n.Data[i:])] = len(msg) - compressionOff + if nameAsStr == "" { + // allocate n.Data on the heap once, to avoid allocating it + // multiple times (for next labels). + nameAsStr = string(n.Data[:n.Length]) + } + compression[nameAsStr[i:]] = len(msg) - compressionOff } } } diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index 1b7f3cb35a..ee42febbc2 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -1813,3 +1813,37 @@ func TestParseWrongSection(t *testing.T) { t.Fatalf("p.SkipAllAuthorities(): unexpected success in Answer section") } } + +func TestBuilderNameCompressionWithNonZeroedName(t *testing.T) { + b := NewBuilder(nil, Header{}) + b.EnableCompression() + if err := b.StartQuestions(); err != nil { + t.Fatalf("b.StartQuestions() unexpected error: %v", err) + } + + name := MustNewName("go.dev.") + if err := b.Question(Question{Name: name}); err != nil { + t.Fatalf("b.Question() unexpected error: %v", err) + } + + // Character that is not part of the name (name.Data[:name.Length]), + // shouldn't affect the compression algorithm. + name.Data[name.Length] = '1' + if err := b.Question(Question{Name: name}); err != nil { + t.Fatalf("b.Question() unexpected error: %v", err) + } + + msg, err := b.Finish() + if err != nil { + t.Fatalf("b.Finish() unexpected error: %v", err) + } + + expect := []byte{ + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, // header + 2, 'g', 'o', 3, 'd', 'e', 'v', 0, 0, 0, 0, 0, // question 1 + 0xC0, 12, 0, 0, 0, 0, // question 2 + } + if !bytes.Equal(msg, expect) { + t.Fatalf("b.Finish() = %v, want: %v", msg, expect) + } +} From 4a2d37ed365334ff00b166660d7c497fcfeaef1b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 31 Jul 2023 15:26:38 -0700 Subject: [PATCH 035/168] http2: remove Docker-requiring tests Remove two tests, one of which uses curl and the other which uses h2load. These tests don't seem worth the complexity of keeping around a Dockerfile and curl/h2load dependencies. Change-Id: I0370af061168e46d8110fa40eba8dabe68acecc3 Reviewed-on: https://go-review.googlesource.com/c/net/+/514597 Reviewed-by: Brad Fitzpatrick Run-TryBot: Damien Neil TryBot-Result: Gopher Robot Reviewed-by: Bryan Mills --- http2/Dockerfile | 51 ------------------------ http2/Makefile | 3 -- http2/http2_test.go | 62 ----------------------------- http2/server_test.go | 92 -------------------------------------------- 4 files changed, 208 deletions(-) delete mode 100644 http2/Dockerfile delete mode 100644 http2/Makefile diff --git a/http2/Dockerfile b/http2/Dockerfile deleted file mode 100644 index 8512245952..0000000000 --- a/http2/Dockerfile +++ /dev/null @@ -1,51 +0,0 @@ -# -# This Dockerfile builds a recent curl with HTTP/2 client support, using -# a recent nghttp2 build. -# -# See the Makefile for how to tag it. If Docker and that image is found, the -# Go tests use this curl binary for integration tests. -# - -FROM ubuntu:trusty - -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y git-core build-essential wget - -RUN apt-get install -y --no-install-recommends \ - autotools-dev libtool pkg-config zlib1g-dev \ - libcunit1-dev libssl-dev libxml2-dev libevent-dev \ - automake autoconf - -# The list of packages nghttp2 recommends for h2load: -RUN apt-get install -y --no-install-recommends make binutils \ - autoconf automake autotools-dev \ - libtool pkg-config zlib1g-dev libcunit1-dev libssl-dev libxml2-dev \ - libev-dev libevent-dev libjansson-dev libjemalloc-dev \ - cython python3.4-dev python-setuptools - -# Note: setting NGHTTP2_VER before the git clone, so an old git clone isn't cached: -ENV NGHTTP2_VER 895da9a -RUN cd /root && git clone https://github.com/tatsuhiro-t/nghttp2.git - -WORKDIR /root/nghttp2 -RUN git reset --hard $NGHTTP2_VER -RUN autoreconf -i -RUN automake -RUN autoconf -RUN ./configure -RUN make -RUN make install - -WORKDIR /root -RUN wget https://curl.se/download/curl-7.45.0.tar.gz -RUN tar -zxvf curl-7.45.0.tar.gz -WORKDIR /root/curl-7.45.0 -RUN ./configure --with-ssl --with-nghttp2=/usr/local -RUN make -RUN make install -RUN ldconfig - -CMD ["-h"] -ENTRYPOINT ["/usr/local/bin/curl"] - diff --git a/http2/Makefile b/http2/Makefile deleted file mode 100644 index 55fd826f77..0000000000 --- a/http2/Makefile +++ /dev/null @@ -1,3 +0,0 @@ -curlimage: - docker build -t gohttp2/curl . - diff --git a/http2/http2_test.go b/http2/http2_test.go index f77c08a107..a16774b7ff 100644 --- a/http2/http2_test.go +++ b/http2/http2_test.go @@ -6,16 +6,13 @@ package http2 import ( "bytes" - "errors" "flag" "fmt" "io/ioutil" "net/http" "os" - "os/exec" "path/filepath" "regexp" - "strconv" "strings" "testing" "time" @@ -85,44 +82,6 @@ func encodeHeaderNoImplicit(t *testing.T, headers ...string) []byte { return buf.Bytes() } -// Verify that curl has http2. -func requireCurl(t *testing.T) { - out, err := dockerLogs(curl(t, "--version")) - if err != nil { - t.Skipf("failed to determine curl features; skipping test") - } - if !strings.Contains(string(out), "HTTP2") { - t.Skip("curl doesn't support HTTP2; skipping test") - } -} - -func curl(t *testing.T, args ...string) (container string) { - out, err := exec.Command("docker", append([]string{"run", "-d", "--net=host", "gohttp2/curl"}, args...)...).Output() - if err != nil { - t.Skipf("Failed to run curl in docker: %v, %s", err, out) - } - return strings.TrimSpace(string(out)) -} - -// Verify that h2load exists. -func requireH2load(t *testing.T) { - out, err := dockerLogs(h2load(t, "--version")) - if err != nil { - t.Skipf("failed to probe h2load; skipping test: %s", out) - } - if !strings.Contains(string(out), "h2load nghttp2/") { - t.Skipf("h2load not present; skipping test. (Output=%q)", out) - } -} - -func h2load(t *testing.T, args ...string) (container string) { - out, err := exec.Command("docker", append([]string{"run", "-d", "--net=host", "--entrypoint=/usr/local/bin/h2load", "gohttp2/curl"}, args...)...).Output() - if err != nil { - t.Skipf("Failed to run h2load in docker: %v, %s", err, out) - } - return strings.TrimSpace(string(out)) -} - type puppetCommand struct { fn func(w http.ResponseWriter, r *http.Request) done chan<- bool @@ -151,27 +110,6 @@ func (p *handlerPuppet) do(fn func(http.ResponseWriter, *http.Request)) { p.ch <- puppetCommand{fn, done} <-done } -func dockerLogs(container string) ([]byte, error) { - out, err := exec.Command("docker", "wait", container).CombinedOutput() - if err != nil { - return out, err - } - exitStatus, err := strconv.Atoi(strings.TrimSpace(string(out))) - if err != nil { - return out, errors.New("unexpected exit status from docker wait") - } - out, err = exec.Command("docker", "logs", container).CombinedOutput() - exec.Command("docker", "rm", container).Run() - if err == nil && exitStatus != 0 { - err = fmt.Errorf("exit status %d: %s", exitStatus, out) - } - return out, err -} - -func kill(container string) { - exec.Command("docker", "kill", container).Run() - exec.Command("docker", "rm", container).Run() -} func cleanDate(res *http.Response) { if d := res.Header["Date"]; len(d) == 1 { diff --git a/http2/server_test.go b/http2/server_test.go index cd73291ea0..b99c5af549 100644 --- a/http2/server_test.go +++ b/http2/server_test.go @@ -20,13 +20,11 @@ import ( "net/http" "net/http/httptest" "os" - "os/exec" "reflect" "runtime" "strconv" "strings" "sync" - "sync/atomic" "testing" "time" @@ -2704,96 +2702,6 @@ func readBodyHandler(t *testing.T, want string) func(w http.ResponseWriter, r *h } } -// TestServerWithCurl currently fails, hence the LenientCipherSuites test. See: -// -// https://github.com/tatsuhiro-t/nghttp2/issues/140 & -// http://sourceforge.net/p/curl/bugs/1472/ -func TestServerWithCurl(t *testing.T) { testServerWithCurl(t, false) } -func TestServerWithCurl_LenientCipherSuites(t *testing.T) { testServerWithCurl(t, true) } - -func testServerWithCurl(t *testing.T, permitProhibitedCipherSuites bool) { - if runtime.GOOS != "linux" { - t.Skip("skipping Docker test when not on Linux; requires --net which won't work with boot2docker anyway") - } - if testing.Short() { - t.Skip("skipping curl test in short mode") - } - requireCurl(t) - var gotConn int32 - testHookOnConn = func() { atomic.StoreInt32(&gotConn, 1) } - - const msg = "Hello from curl!\n" - ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Foo", "Bar") - w.Header().Set("Client-Proto", r.Proto) - io.WriteString(w, msg) - })) - ConfigureServer(ts.Config, &Server{ - PermitProhibitedCipherSuites: permitProhibitedCipherSuites, - }) - ts.TLS = ts.Config.TLSConfig // the httptest.Server has its own copy of this TLS config - ts.StartTLS() - defer ts.Close() - - t.Logf("Running test server for curl to hit at: %s", ts.URL) - container := curl(t, "--silent", "--http2", "--insecure", "-v", ts.URL) - defer kill(container) - res, err := dockerLogs(container) - if err != nil { - t.Fatal(err) - } - - body := string(res) - // Search for both "key: value" and "key:value", since curl changed their format - // Our Dockerfile contains the latest version (no space), but just in case people - // didn't rebuild, check both. - if !strings.Contains(body, "foo: Bar") && !strings.Contains(body, "foo:Bar") { - t.Errorf("didn't see foo: Bar header") - t.Logf("Got: %s", body) - } - if !strings.Contains(body, "client-proto: HTTP/2") && !strings.Contains(body, "client-proto:HTTP/2") { - t.Errorf("didn't see client-proto: HTTP/2 header") - t.Logf("Got: %s", res) - } - if !strings.Contains(string(res), msg) { - t.Errorf("didn't see %q content", msg) - t.Logf("Got: %s", res) - } - - if atomic.LoadInt32(&gotConn) == 0 { - t.Error("never saw an http2 connection") - } -} - -var doh2load = flag.Bool("h2load", false, "Run h2load test") - -func TestServerWithH2Load(t *testing.T) { - if !*doh2load { - t.Skip("Skipping without --h2load flag.") - } - if runtime.GOOS != "linux" { - t.Skip("skipping Docker test when not on Linux; requires --net which won't work with boot2docker anyway") - } - requireH2load(t) - - msg := strings.Repeat("Hello, h2load!\n", 5000) - ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, msg) - w.(http.Flusher).Flush() - io.WriteString(w, msg) - })) - ts.StartTLS() - defer ts.Close() - - cmd := exec.Command("docker", "run", "--net=host", "--entrypoint=/usr/local/bin/h2load", "gohttp2/curl", - "-n100000", "-c100", "-m100", ts.URL) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - t.Fatal(err) - } -} - func TestServer_MaxDecoderHeaderTableSize(t *testing.T) { wantHeaderTableSize := uint32(initialHeaderTableSize * 2) st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {}, func(s *Server) { From 52fbe3731bc7b6873c58d80aae59dc20abbf89c9 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Sun, 13 Aug 2023 10:33:31 -0400 Subject: [PATCH 036/168] quic: add test helpers for acking packets Add connTest methods to send the conn-under-test an ACK for the latest packet it sent, or for all packets in the number space it last sent in. For golang/go#58547 Change-Id: Id35cad9bddf9dd32074dc121fd360a65b989fb4b Reviewed-on: https://go-review.googlesource.com/c/net/+/522055 Run-TryBot: Damien Neil TryBot-Result: Gopher Robot Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_id_test.go | 4 ++-- internal/quic/conn_loss_test.go | 7 +------ internal/quic/conn_test.go | 34 +++++++++++++++++++++++++++------ internal/quic/stream_test.go | 19 ++++-------------- internal/quic/tls_test.go | 2 +- 5 files changed, 36 insertions(+), 30 deletions(-) diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index 04baf0edaf..d479cd4a87 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -264,7 +264,7 @@ func TestConnIDPeerRequestsRetirement(t *testing.T) { packetType1RTT, debugFrameRetireConnectionID{ seq: 0, }) - if got, want := tc.sentFramePacket.dstConnID, testPeerConnID(1); !bytes.Equal(got, want) { + if got, want := tc.lastPacket.dstConnID, testPeerConnID(1); !bytes.Equal(got, want) { t.Fatalf("used destination conn id {%x}, want {%x}", got, want) } } @@ -467,7 +467,7 @@ func TestConnIDUsePreferredAddressConnID(t *testing.T) { packetType1RTT, debugFrameRetireConnectionID{ seq: 0, }) - if got, want := tc.sentFramePacket.dstConnID, cid; !bytes.Equal(got, want) { + if got, want := tc.lastPacket.dstConnID, cid; !bytes.Equal(got, want) { t.Fatalf("used destination conn id {%x}, want {%x} from preferred address transport parameter", got, want) } } diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index dc0dc6cd33..bb43030330 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -271,12 +271,7 @@ func TestLostStreamPartialLoss(t *testing.T) { data: data[i : i+1], }) if i%2 == 0 { - num := tc.sentFramePacket.num - tc.writeFrames(packetType1RTT, debugFrameAck{ - ranges: []i64range[packetNumber]{ - {num, num + 1}, - }, - }) + tc.writeAckForLatest() } } const pto = false diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 2480f9cb0a..2aa38fcf3d 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -137,10 +137,10 @@ type testConn struct { // Datagrams, packets, and frames sent by the conn, // but not yet processed by the test. - sentDatagrams [][]byte - sentPackets []*testPacket - sentFrames []debugFrame - sentFramePacket *testPacket + sentDatagrams [][]byte + sentPackets []*testPacket + sentFrames []debugFrame + lastPacket *testPacket // Frame types to ignore in tests. ignoreFrames map[byte]bool @@ -388,6 +388,28 @@ func (tc *testConn) writeFrames(ptype packetType, frames ...debugFrame) { tc.write(d) } +// writeAckForAll sends the Conn a datagram containing an ack for all packets up to the +// last one received. +func (tc *testConn) writeAckForAll() { + if tc.lastPacket == nil { + return + } + tc.writeFrames(tc.lastPacket.ptype, debugFrameAck{ + ranges: []i64range[packetNumber]{{0, tc.lastPacket.num + 1}}, + }) +} + +// writeAckForLatest sends the Conn a datagram containing an ack for the +// most recent packet received. +func (tc *testConn) writeAckForLatest() { + if tc.lastPacket == nil { + return + } + tc.writeFrames(tc.lastPacket.ptype, debugFrameAck{ + ranges: []i64range[packetNumber]{{tc.lastPacket.num, tc.lastPacket.num + 1}}, + }) +} + // ignoreFrame hides frames of the given type sent by the Conn. func (tc *testConn) ignoreFrame(frameType byte) { tc.ignoreFrames[frameType] = true @@ -423,6 +445,7 @@ func (tc *testConn) readPacket() *testPacket { } p := tc.sentPackets[0] tc.sentPackets = tc.sentPackets[1:] + tc.lastPacket = p return p } @@ -435,12 +458,11 @@ func (tc *testConn) readFrame() (debugFrame, packetType) { if p == nil { return nil, packetTypeInvalid } - tc.sentFramePacket = p tc.sentFrames = p.frames } f := tc.sentFrames[0] tc.sentFrames = tc.sentFrames[1:] - return f, tc.sentFramePacket.ptype + return f, tc.lastPacket.ptype } // wantDatagram indicates that we expect the Conn to send a datagram. diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 5904a93428..bafd236c9f 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -193,9 +193,7 @@ func TestStreamWriteBlockedByWriteBufferLimit(t *testing.T) { tc.wantIdle("no STREAM_DATA_BLOCKED, we're blocked locally not by flow control") // ACK for previously-sent data allows making more progress. - tc.writeFrames(packetType1RTT, debugFrameAck{ - ranges: []i64range[packetNumber]{{0, tc.sentFramePacket.num + 1}}, - }) + tc.writeAckForAll() tc.wantFrame("ACK for previous data allows making progress", packetType1RTT, debugFrameStream{ id: s.id, @@ -968,9 +966,7 @@ func TestStreamCloseWaitsForAcks(t *testing.T) { if _, err := closing.result(); err != errNotDone { t.Fatalf("s.CloseContext() = %v, want it to block waiting for acks", err) } - tc.writeFrames(packetType1RTT, debugFrameAck{ - ranges: []i64range[packetNumber]{{0, tc.sentFramePacket.num + 1}}, - }) + tc.writeAckForAll() if _, err := closing.result(); err != nil { t.Fatalf("s.CloseContext() = %v, want nil (all data acked)", err) } @@ -983,9 +979,7 @@ func TestStreamCloseUnblocked(t *testing.T) { }{{ name: "data received", unblock: func(tc *testConn, s *Stream) { - tc.writeFrames(packetType1RTT, debugFrameAck{ - ranges: []i64range[packetNumber]{{0, tc.sentFramePacket.num + 1}}, - }) + tc.writeAckForAll() }, }, { name: "stop sending received", @@ -1135,12 +1129,7 @@ func TestStreamPeerStopSendingForActiveStream(t *testing.T) { t.Errorf("s.Write() after STOP_SENDING = %v, %v; want error", n, err) } // This ack will result in some of the previous frames being marked as lost. - tc.writeFrames(packetType1RTT, debugFrameAck{ - ranges: []i64range[packetNumber]{{ - tc.sentFramePacket.num, - tc.sentFramePacket.num + 1, - }}, - }) + tc.writeAckForLatest() tc.wantIdle("lost STREAM frames for reset stream are not resent") }) } diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 35cb8bf008..180ea8beec 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -266,7 +266,7 @@ func (tc *testConn) uncheckedHandshake() { debugFrameAck{ ackDelay: unscaledAckDelayFromDuration( maxAckDelay, ackDelayExponent), - ranges: []i64range[packetNumber]{{0, tc.sentFramePacket.num + 1}}, + ranges: []i64range[packetNumber]{{0, tc.lastPacket.num + 1}}, }) } else { tc.wantIdle("initial frames are ignored") From 4332436fd1223b60e3127494d5ff771fba2c0adf Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 16 Aug 2023 08:57:37 -0400 Subject: [PATCH 037/168] quic: send more transport parameters Send various transport parameters that we weren't sending yet, but should have been. Add a test for transport parameters sent by us. For golang/go#58547 Change-Id: Id16c46ee39040b091633aca8d4cff4c60562a603 Reviewed-on: https://go-review.googlesource.com/c/net/+/523575 Run-TryBot: Damien Neil TryBot-Result: Gopher Robot Reviewed-by: Jonathan Amsterdam --- internal/quic/config_test.go | 32 ++++++++++++++++++++++++++++++++ internal/quic/conn.go | 14 ++++++++++---- internal/quic/conn_test.go | 10 ++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 internal/quic/config_test.go diff --git a/internal/quic/config_test.go b/internal/quic/config_test.go new file mode 100644 index 0000000000..cec57c5e36 --- /dev/null +++ b/internal/quic/config_test.go @@ -0,0 +1,32 @@ +// 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" + +func TestConfigTransportParameters(t *testing.T) { + const ( + wantInitialMaxStreamData = int64(2) + ) + tc := newTestConn(t, clientSide, func(c *Config) { + c.StreamReadBufferSize = wantInitialMaxStreamData + }) + tc.handshake() + if tc.sentTransportParameters == nil { + t.Fatalf("conn didn't send transport parameters during handshake") + } + p := tc.sentTransportParameters + if got, want := p.initialMaxStreamDataBidiLocal, wantInitialMaxStreamData; got != want { + t.Errorf("initial_max_stream_data_bidi_local = %v, want %v", got, want) + } + if got, want := p.initialMaxStreamDataBidiRemote, wantInitialMaxStreamData; got != want { + t.Errorf("initial_max_stream_data_bidi_remote = %v, want %v", got, want) + } + if got, want := p.initialMaxStreamDataUni, wantInitialMaxStreamData; got != want { + t.Errorf("initial_max_stream_data_uni = %v, want %v", got, want) + } +} diff --git a/internal/quic/conn.go b/internal/quic/conn.go index ee8f011f8a..04dcd7b6bb 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -111,11 +111,17 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. c.loss.init(c.side, maxDatagramSize, now) c.streamsInit() + // TODO: initial_source_connection_id, retry_source_connection_id c.startTLS(now, initialConnID, transportParameters{ - initialSrcConnID: c.connIDState.srcConnID(), - ackDelayExponent: ackDelayExponent, - maxUDPPayloadSize: maxUDPPayloadSize, - maxAckDelay: maxAckDelay, + initialSrcConnID: c.connIDState.srcConnID(), + ackDelayExponent: ackDelayExponent, + maxUDPPayloadSize: maxUDPPayloadSize, + maxAckDelay: maxAckDelay, + disableActiveMigration: true, + initialMaxStreamDataBidiLocal: config.streamReadBufferSize(), + initialMaxStreamDataBidiRemote: config.streamReadBufferSize(), + initialMaxStreamDataUni: config.streamReadBufferSize(), + activeConnIDLimit: activeConnIDLimit, }) go c.loop(now) diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 2aa38fcf3d..8ebe49e0e5 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -142,6 +142,9 @@ type testConn struct { sentFrames []debugFrame lastPacket *testPacket + // Transport parameters sent by the conn. + sentTransportParameters *transportParameters + // Frame types to ignore in tests. ignoreFrames map[byte]bool @@ -719,6 +722,13 @@ func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { setKey(&tc.rkeys, e) case tls.QUICWriteData: tc.cryptoDataIn[e.Level] = append(tc.cryptoDataIn[e.Level], e.Data...) + case tls.QUICTransportParameters: + p, err := unmarshalTransportParams(e.Data) + if err != nil { + tc.t.Logf("sent unparseable transport parameters %x %v", e.Data, err) + } else { + tc.sentTransportParameters = &p + } } } } From d1b0a97d84e0fa88851b5a065e23262afed10400 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 15 Aug 2023 11:11:36 -0400 Subject: [PATCH 038/168] quic: avoid sending 1-RTT frames in initial/handshake packets Restructure the send path a little to make it clear that 1-RTT frames go in 1-RTT packets. For golang/go#58547 Change-Id: Id4c2c86c8ccd350bf490f38a8bb01ad9bc2639ee Reviewed-on: https://go-review.googlesource.com/c/net/+/524035 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_send.go | 40 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 6e6fbc5857..9d315fb392 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -224,14 +224,6 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, // TODO: Add all the other frames we can send. - // HANDSHAKE_DONE - if c.handshakeConfirmed.shouldSendPTO(pto) { - if !c.w.appendHandshakeDoneFrame() { - return - } - c.handshakeConfirmed.setSent(pnum) - } - // CRYPTO c.crypto[space].dataToSend(pto, func(off, size int64) int64 { b, _ := c.w.appendCryptoFrame(off, int(size)) @@ -239,13 +231,6 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, return int64(len(b)) }) - // NEW_CONNECTION_ID, RETIRE_CONNECTION_ID - if space == appDataSpace { - if !c.connIDState.appendFrames(&c.w, pnum, pto) { - return - } - } - // Test-only PING frames. if space == c.testSendPingSpace && c.testSendPing.shouldSendPTO(pto) { if !c.w.appendPingFrame() { @@ -254,11 +239,26 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, c.testSendPing.setSent(pnum) } - // All stream-related frames. This should come last in the packet, - // so large amounts of STREAM data don't crowd out other frames - // we may need to send. - if !c.appendStreamFrames(&c.w, pnum, pto) { - return + if space == appDataSpace { + // HANDSHAKE_DONE + if c.handshakeConfirmed.shouldSendPTO(pto) { + if !c.w.appendHandshakeDoneFrame() { + return + } + c.handshakeConfirmed.setSent(pnum) + } + + // NEW_CONNECTION_ID, RETIRE_CONNECTION_ID + if !c.connIDState.appendFrames(&c.w, pnum, pto) { + return + } + + // All stream-related frames. This should come last in the packet, + // so large amounts of STREAM data don't crowd out other frames + // we may need to send. + if !c.appendStreamFrames(&c.w, pnum, pto) { + return + } } // If this is a PTO probe and we haven't added an ack-eliciting frame yet, From fe2abcb6e15f7a34d285220b437d421da4c76775 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 29 Aug 2023 08:21:51 -0700 Subject: [PATCH 039/168] quic: validate stream limits in transport params The maximum number of streams of a given type (bidi/uni) is capped to 2^60, since a larger number would overflow a varint. Validate limits received in transport parameters. RFC 9000, Section 4.6 For golang/go#58547 Change-Id: I7a4a15c569da91ad1b89a5dc71e1c5b213dbda9a Reviewed-on: https://go-review.googlesource.com/c/net/+/524037 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/packet_parser.go | 2 +- internal/quic/quic.go | 4 ++++ internal/quic/stream_test.go | 4 ++-- internal/quic/transport_params.go | 6 ++++++ internal/quic/transport_params_test.go | 14 ++++++++++++++ 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index 9a00da7560..ca5b37b2bd 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -378,7 +378,7 @@ func consumeMaxStreamsFrame(b []byte) (typ streamType, max int64, n int) { return 0, 0, -1 } n += nn - if v > 1<<60 { + if v > maxStreamsLimit { return 0, 0, -1 } return typ, int64(v), n diff --git a/internal/quic/quic.go b/internal/quic/quic.go index 8cd61aed08..71738e129d 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -55,6 +55,10 @@ const timerGranularity = 1 * time.Millisecond // https://www.rfc-editor.org/rfc/rfc9000#section-14.1 const minimumClientInitialDatagramSize = 1200 +// Maximum number of streams of a given type which may be created. +// https://www.rfc-editor.org/rfc/rfc9000.html#section-4.6-2 +const maxStreamsLimit = 1 << 60 + // A connSide distinguishes between the client and server sides of a connection. type connSide int8 diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index bafd236c9f..7b8ba2c54f 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -1165,8 +1165,8 @@ func newTestConnAndRemoteStream(t *testing.T, side connSide, styp streamType, op // permissiveTransportParameters may be passed as an option to newTestConn. func permissiveTransportParameters(p *transportParameters) { - p.initialMaxStreamsBidi = maxVarint - p.initialMaxStreamsUni = maxVarint + p.initialMaxStreamsBidi = maxStreamsLimit + p.initialMaxStreamsUni = maxStreamsLimit p.initialMaxData = maxVarint p.initialMaxStreamDataBidiRemote = maxVarint p.initialMaxStreamDataBidiLocal = maxVarint diff --git a/internal/quic/transport_params.go b/internal/quic/transport_params.go index 89ea69fb97..dc76d16509 100644 --- a/internal/quic/transport_params.go +++ b/internal/quic/transport_params.go @@ -212,8 +212,14 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { p.initialMaxStreamDataUni, n = consumeVarintInt64(val) case paramInitialMaxStreamsBidi: p.initialMaxStreamsBidi, n = consumeVarintInt64(val) + if p.initialMaxStreamsBidi > maxStreamsLimit { + return p, localTransportError(errTransportParameter) + } case paramInitialMaxStreamsUni: p.initialMaxStreamsUni, n = consumeVarintInt64(val) + if p.initialMaxStreamsUni > maxStreamsLimit { + return p, localTransportError(errTransportParameter) + } case paramAckDelayExponent: var v uint64 v, n = consumeVarint(val) diff --git a/internal/quic/transport_params_test.go b/internal/quic/transport_params_test.go index e1c45ca0e6..cc88e83fd6 100644 --- a/internal/quic/transport_params_test.go +++ b/internal/quic/transport_params_test.go @@ -236,6 +236,20 @@ func TestTransportParametersErrors(t *testing.T) { 15, // length 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, }, + }, { + desc: "initial_max_streams_bidi is too large", + enc: []byte{ + 0x08, // initial_max_streams_bidi, + 8, // length, + 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + }, + }, { + desc: "initial_max_streams_uni is too large", + enc: []byte{ + 0x08, // initial_max_streams_uni, + 9, // length, + 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + }, }, { desc: "preferred_address is too short", enc: []byte{ From 8b010a5243b670aca8d2277a3c989d1b6a198a08 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 29 Aug 2023 09:25:04 -0700 Subject: [PATCH 040/168] quic: fix race condition in runAsync test helper asyncTestState.wakeAsync runs on the conn's goroutine and accesses as.blocked, so we need to hold as.mu while initializing as.blocked in runAsync. For golang/go#58547 Change-Id: Idb5921895cee89dfceec2b2439c43f2e380b64ce Reviewed-on: https://go-review.googlesource.com/c/net/+/524095 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_async_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/quic/conn_async_test.go b/internal/quic/conn_async_test.go index 0da3ddb452..5b419c4e54 100644 --- a/internal/quic/conn_async_test.go +++ b/internal/quic/conn_async_test.go @@ -101,7 +101,9 @@ func runAsync[T any](ts *testConn, f func(context.Context) (T, error)) *asyncOp[ as := &ts.asyncTestState if as.notify == nil { as.notify = make(chan struct{}) + as.mu.Lock() as.blocked = make(map[*blockedAsync]struct{}) + as.mu.Unlock() } _, file, line, _ := runtime.Caller(1) ctx := context.WithValue(context.Background(), asyncContextKey{}, true) From b4d09be75101024ceed6b173b49a5630084174e6 Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Mon, 28 Aug 2023 16:40:48 +0000 Subject: [PATCH 041/168] dns/dnsmessage: compress all names while appending to a buffer Change-Id: Iedccbf3e47a63b2239def189ab41bab18a64c398 GitHub-Last-Rev: eb23195734794ab2b211677e5e3616de5f0eb7be GitHub-Pull-Request: golang/net#189 Reviewed-on: https://go-review.googlesource.com/c/net/+/522575 TryBot-Result: Gopher Robot Reviewed-by: Ian Lance Taylor Run-TryBot: Mateusz Poliwczak Reviewed-by: Joedian Reid Auto-Submit: Ian Lance Taylor --- dns/dnsmessage/message.go | 7 ++++--- dns/dnsmessage/message_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index 9ddf2c2292..0215a5dde3 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -2000,14 +2000,15 @@ func (n *Name) pack(msg []byte, compression map[string]int, compressionOff int) } // Miss. Add the suffix to the compression table if the - // offset can be stored in the available 14 bytes. - if len(msg) <= int(^uint16(0)>>2) { + // offset can be stored in the available 14 bits. + newPtr := len(msg) - compressionOff + if newPtr <= int(^uint16(0)>>2) { if nameAsStr == "" { // allocate n.Data on the heap once, to avoid allocating it // multiple times (for next labels). nameAsStr = string(n.Data[:n.Length]) } - compression[nameAsStr[i:]] = len(msg) - compressionOff + compression[nameAsStr[i:]] = newPtr } } } diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index ee42febbc2..23fb3d5748 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -1847,3 +1847,30 @@ func TestBuilderNameCompressionWithNonZeroedName(t *testing.T) { t.Fatalf("b.Finish() = %v, want: %v", msg, expect) } } + +func TestBuilderCompressionInAppendMode(t *testing.T) { + maxPtr := int(^uint16(0) >> 2) + b := NewBuilder(make([]byte, maxPtr, maxPtr+512), Header{}) + b.EnableCompression() + if err := b.StartQuestions(); err != nil { + t.Fatalf("b.StartQuestions() unexpected error: %v", err) + } + if err := b.Question(Question{Name: MustNewName("go.dev.")}); err != nil { + t.Fatalf("b.Question() unexpected error: %v", err) + } + if err := b.Question(Question{Name: MustNewName("go.dev.")}); err != nil { + t.Fatalf("b.Question() unexpected error: %v", err) + } + msg, err := b.Finish() + if err != nil { + t.Fatalf("b.Finish() unexpected error: %v", err) + } + expect := []byte{ + 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, // header + 2, 'g', 'o', 3, 'd', 'e', 'v', 0, 0, 0, 0, 0, // question 1 + 0xC0, 12, 0, 0, 0, 0, // question 2 + } + if !bytes.Equal(msg[maxPtr:], expect) { + t.Fatalf("msg[maxPtr:] = %v, want: %v", msg[maxPtr:], expect) + } +} From 7374d342a2c3de79d7b96f3aacdb13498c3fc3b5 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 29 Aug 2023 12:51:05 -0700 Subject: [PATCH 042/168] quic: don't block when closing read-only streams Stream.Close blocks until all data sent on a stream has been acked by the peer. Don't block indefinitely when closing a read-only stream, waiting for an ack of data we never sent. For golang/go#58547 Change-Id: I4087666f739d7388e460b613d211c043626f1c87 Reviewed-on: https://go-review.googlesource.com/c/net/+/524038 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/stream.go | 7 ++++++- internal/quic/stream_test.go | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 12117dbd39..1033cbb401 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -66,7 +66,9 @@ func newStream(c *Conn, id streamID) *Stream { inresetcode: -1, // -1 indicates no RESET_STREAM received ingate: newLockedGate(), outgate: newLockedGate(), - outdone: make(chan struct{}), + } + if !s.IsReadOnly() { + s.outdone = make(chan struct{}) } return s } @@ -237,6 +239,9 @@ func (s *Stream) Close() error { // CloseContext discards the buffer and returns the context error. func (s *Stream) CloseContext(ctx context.Context) error { s.CloseRead() + if s.IsReadOnly() { + return nil + } s.CloseWrite() // TODO: Return code from peer's RESET_STREAM frame? return s.conn.waitOnDone(ctx, s.outdone) diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 7b8ba2c54f..79377c6a4a 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -972,6 +972,17 @@ func TestStreamCloseWaitsForAcks(t *testing.T) { } } +func TestStreamCloseReadOnly(t *testing.T) { + tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, permissiveTransportParameters) + if err := s.CloseContext(canceledContext()); err != nil { + t.Errorf("s.CloseContext() = %v, want nil", err) + } + tc.wantFrame("closed stream sends STOP_SENDING", + packetType1RTT, debugFrameStopSending{ + id: s.id, + }) +} + func TestStreamCloseUnblocked(t *testing.T) { for _, test := range []struct { name string From b82f062c4bc1abcfe993e3750d64c4bdd4a14f87 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 29 Aug 2023 15:35:30 -0700 Subject: [PATCH 043/168] quic: include ignored frames in test log output When looking at a test log, it's a bit confusing to have some of the frames silently omitted. Print ignored frames. Unfortunately, this means we need to do the actual ignoring of frames after printing the packet. We specify frames to ignore by the frame number, but after parsing we don't have a simple way to map from the debugFrame type back to the number. Add a big, ugly mapping function to do this; it's clunky, but isolated to one function in tests. For golang/go#58547 Change-Id: I242f5511dc16be2350fa49030af38588fe92a988 Reviewed-on: https://go-review.googlesource.com/c/net/+/524295 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_test.go | 77 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 8ebe49e0e5..d8c44558dc 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -431,7 +431,80 @@ func (tc *testConn) readDatagram() *testDatagram { buf := tc.sentDatagrams[0] tc.sentDatagrams = tc.sentDatagrams[1:] d := tc.parseTestDatagram(buf) + // Log the datagram before removing ignored frames. + // When things go wrong, it's useful to see all the frames. tc.logDatagram("-> conn under test sends", d) + typeForFrame := func(f debugFrame) byte { + // This is very clunky, and points at a problem + // in how we specify what frames to ignore in tests. + // + // We mark frames to ignore using the frame type, + // but we've got a debugFrame data structure here. + // Perhaps we should be ignoring frames by debugFrame + // type instead: tc.ignoreFrame[debugFrameAck](). + switch f := f.(type) { + case debugFramePadding: + return frameTypePadding + case debugFramePing: + return frameTypePing + case debugFrameAck: + return frameTypeAck + case debugFrameResetStream: + return frameTypeResetStream + case debugFrameStopSending: + return frameTypeStopSending + case debugFrameCrypto: + return frameTypeCrypto + case debugFrameNewToken: + return frameTypeNewToken + case debugFrameStream: + return frameTypeStreamBase + case debugFrameMaxData: + return frameTypeMaxData + case debugFrameMaxStreamData: + return frameTypeMaxStreamData + case debugFrameMaxStreams: + if f.streamType == bidiStream { + return frameTypeMaxStreamsBidi + } else { + return frameTypeMaxStreamsUni + } + case debugFrameDataBlocked: + return frameTypeDataBlocked + case debugFrameStreamDataBlocked: + return frameTypeStreamDataBlocked + case debugFrameStreamsBlocked: + if f.streamType == bidiStream { + return frameTypeStreamsBlockedBidi + } else { + return frameTypeStreamsBlockedUni + } + case debugFrameNewConnectionID: + return frameTypeNewConnectionID + case debugFrameRetireConnectionID: + return frameTypeRetireConnectionID + case debugFramePathChallenge: + return frameTypePathChallenge + case debugFramePathResponse: + return frameTypePathResponse + case debugFrameConnectionCloseTransport: + return frameTypeConnectionCloseTransport + case debugFrameConnectionCloseApplication: + return frameTypeConnectionCloseApplication + case debugFrameHandshakeDone: + return frameTypeHandshakeDone + } + panic(fmt.Errorf("unhandled frame type %T", f)) + } + for _, p := range d.packets { + var frames []debugFrame + for _, f := range p.frames { + if !tc.ignoreFrames[typeForFrame(f)] { + frames = append(frames, f) + } + } + p.frames = frames + } return d } @@ -632,9 +705,7 @@ func (tc *testConn) parseTestFrames(payload []byte) ([]debugFrame, error) { if n < 0 { return nil, errors.New("error parsing frames") } - if !tc.ignoreFrames[payload[0]] { - frames = append(frames, f) - } + frames = append(frames, f) payload = payload[n:] } return frames, nil From 03d5e623398478fa929c8ba4b8f15de74017d82a Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Tue, 29 Aug 2023 15:48:27 +0200 Subject: [PATCH 044/168] http2: remove unused ClientConn.tconnClosed It was added in CL 429060 but was never used. Change-Id: Ie1bcd44559006082afed319c0db677ff2ca957d7 Reviewed-on: https://go-review.googlesource.com/c/net/+/523935 Auto-Submit: Ian Lance Taylor Reviewed-by: Ian Lance Taylor Run-TryBot: Tobias Klauser Reviewed-by: Damien Neil TryBot-Result: Gopher Robot LUCI-TryBot-Result: Go LUCI --- http2/transport.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/http2/transport.go b/http2/transport.go index b0d482f9f4..4515b22c4a 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -291,8 +291,7 @@ func (t *Transport) initConnPool() { // HTTP/2 server. type ClientConn struct { t *Transport - tconn net.Conn // usually *tls.Conn, except specialized impls - tconnClosed bool + tconn net.Conn // usually *tls.Conn, except specialized impls tlsState *tls.ConnectionState // nil only for specialized impls reused uint32 // whether conn is being reused; atomic singleUse bool // whether being used for a single http.Request From 97384c11dd0db63357820b2cfcb44c40fbc3116a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 29 Aug 2023 13:18:00 -0700 Subject: [PATCH 045/168] quic: remove streams from the conn when done When a stream has been fully shut down--the peer has closed its end and acked every frame we will send for it--remove it from the Conn's set of active streams. We do the actual removal on the conn's loop, so stream cleanup can access conn state without worrying about locking. For golang/go#58547 Change-Id: Id9715693649929b07d303f0c4b3a782d135f0326 Reviewed-on: https://go-review.googlesource.com/c/net/+/524296 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/atomic_bits.go | 33 +++++++ internal/quic/conn_streams.go | 62 +++++++++---- internal/quic/conn_streams_test.go | 89 ++++++++++++++++++ internal/quic/conn_test.go | 2 + internal/quic/stream.go | 140 +++++++++++++++++++++++------ internal/quic/stream_test.go | 33 +++++++ 6 files changed, 315 insertions(+), 44 deletions(-) create mode 100644 internal/quic/atomic_bits.go diff --git a/internal/quic/atomic_bits.go b/internal/quic/atomic_bits.go new file mode 100644 index 0000000000..e1e2594d15 --- /dev/null +++ b/internal/quic/atomic_bits.go @@ -0,0 +1,33 @@ +// 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 "sync/atomic" + +// atomicBits is an atomic uint32 that supports setting individual bits. +type atomicBits[T ~uint32] struct { + bits atomic.Uint32 +} + +// set sets the bits in mask to the corresponding bits in v. +// It returns the new value. +func (a *atomicBits[T]) set(v, mask T) T { + if v&^mask != 0 { + panic("BUG: bits in v are not in mask") + } + for { + o := a.bits.Load() + n := (o &^ uint32(mask)) | uint32(v) + if a.bits.CompareAndSwap(o, n) { + return T(n) + } + } +} + +func (a *atomicBits[T]) load() T { + return T(a.bits.Load()) +} diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index dd35e34cf6..0ede284e23 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -185,24 +185,46 @@ func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) for { s := c.streams.sendHead const pto = false - if !s.appendInFrames(w, pnum, pto) { - return false + + state := s.state.load() + if state&streamInSend != 0 { + s.ingate.lock() + ok := s.appendInFramesLocked(w, pnum, pto) + state = s.inUnlockNoQueue() + if !ok { + return false + } } - avail := w.avail() - if !s.appendOutFrames(w, pnum, pto) { - // We've sent some data for this stream, but it still has more to send. - // If the stream got a reasonable chance to put data in a packet, - // advance sendHead to the next stream in line, to avoid starvation. - // We'll come back to this stream after going through the others. - // - // If the packet was already mostly out of space, leave sendHead alone - // and come back to this stream again on the next packet. - if avail > 512 { - c.streams.sendHead = s.next - c.streams.sendTail = s + + if state&streamOutSend != 0 { + avail := w.avail() + s.outgate.lock() + ok := s.appendOutFramesLocked(w, pnum, pto) + state = s.outUnlockNoQueue() + if !ok { + // We've sent some data for this stream, but it still has more to send. + // If the stream got a reasonable chance to put data in a packet, + // advance sendHead to the next stream in line, to avoid starvation. + // We'll come back to this stream after going through the others. + // + // If the packet was already mostly out of space, leave sendHead alone + // and come back to this stream again on the next packet. + if avail > 512 { + c.streams.sendHead = s.next + c.streams.sendTail = s + } + return false } - return false } + + if state == streamInDone|streamOutDone { + // Stream is finished, remove it from the conn. + s.state.set(streamConnRemoved, streamConnRemoved) + delete(c.streams.streams, s.id) + + // TODO: Provide the peer with additional stream quota (MAX_STREAMS). + } + next := s.next s.next = nil if (next == s) != (s == c.streams.sendTail) { @@ -231,10 +253,16 @@ func (c *Conn) appendStreamFramesPTO(w *packetWriter, pnum packetNumber) bool { defer c.streams.sendMu.Unlock() for _, s := range c.streams.streams { const pto = true - if !s.appendInFrames(w, pnum, pto) { + s.ingate.lock() + inOK := s.appendInFramesLocked(w, pnum, pto) + s.inUnlockNoQueue() + if !inOK { return false } - if !s.appendOutFrames(w, pnum, pto) { + s.outgate.lock() + outOK := s.appendOutFramesLocked(w, pnum, pto) + s.outUnlockNoQueue() + if !outOK { return false } } diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go index 877dbb94fc..9bbc994b11 100644 --- a/internal/quic/conn_streams_test.go +++ b/internal/quic/conn_streams_test.go @@ -8,6 +8,8 @@ package quic import ( "context" + "fmt" + "io" "testing" ) @@ -253,3 +255,90 @@ func TestStreamsWriteQueueFairness(t *testing.T) { } } } + +func TestStreamsShutdown(t *testing.T) { + // These tests verify that a stream is removed from the Conn's map of live streams + // after it is fully shut down. + // + // Each case consists of a setup step, after which one stream should exist, + // and a shutdown step, after which no streams should remain in the Conn. + for _, test := range []struct { + name string + side streamSide + styp streamType + setup func(*testing.T, *testConn, *Stream) + shutdown func(*testing.T, *testConn, *Stream) + }{{ + name: "closed", + side: localStream, + styp: uniStream, + setup: func(t *testing.T, tc *testConn, s *Stream) { + s.CloseContext(canceledContext()) + }, + shutdown: func(t *testing.T, tc *testConn, s *Stream) { + tc.writeAckForAll() + }, + }, { + name: "local close", + side: localStream, + styp: bidiStream, + setup: func(t *testing.T, tc *testConn, s *Stream) { + tc.writeFrames(packetType1RTT, debugFrameResetStream{ + id: s.id, + }) + s.CloseContext(canceledContext()) + }, + shutdown: func(t *testing.T, tc *testConn, s *Stream) { + tc.writeAckForAll() + }, + }, { + name: "remote reset", + side: localStream, + styp: bidiStream, + setup: func(t *testing.T, tc *testConn, s *Stream) { + s.CloseContext(canceledContext()) + tc.wantIdle("all frames after CloseContext are ignored") + tc.writeAckForAll() + }, + shutdown: func(t *testing.T, tc *testConn, s *Stream) { + tc.writeFrames(packetType1RTT, debugFrameResetStream{ + id: s.id, + }) + }, + }, { + name: "local close", + side: remoteStream, + styp: uniStream, + setup: func(t *testing.T, tc *testConn, s *Stream) { + ctx := canceledContext() + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + fin: true, + }) + if n, err := s.ReadContext(ctx, make([]byte, 16)); n != 0 || err != io.EOF { + t.Errorf("ReadContext() = %v, %v; want 0, io.EOF", n, err) + } + }, + shutdown: func(t *testing.T, tc *testConn, s *Stream) { + s.CloseRead() + }, + }} { + name := fmt.Sprintf("%v/%v/%v", test.side, test.styp, test.name) + t.Run(name, func(t *testing.T) { + tc, s := newTestConnAndStream(t, serverSide, test.side, test.styp, + permissiveTransportParameters) + tc.ignoreFrame(frameTypeStreamBase) + tc.ignoreFrame(frameTypeStopSending) + test.setup(t, tc, s) + tc.wantIdle("conn should be idle after setup") + if got, want := len(tc.conn.streams.streams), 1; got != want { + t.Fatalf("after setup: %v streams in Conn's map; want %v", got, want) + } + test.shutdown(t, tc, s) + tc.wantIdle("conn should be idle after shutdown") + if got, want := len(tc.conn.streams.streams), 0; got != want { + t.Fatalf("after shutdown: %v streams in Conn's map; want %v", got, want) + } + }) + } +} diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index d8c44558dc..ea720d5754 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -394,6 +394,7 @@ func (tc *testConn) writeFrames(ptype packetType, frames ...debugFrame) { // writeAckForAll sends the Conn a datagram containing an ack for all packets up to the // last one received. func (tc *testConn) writeAckForAll() { + tc.t.Helper() if tc.lastPacket == nil { return } @@ -405,6 +406,7 @@ func (tc *testConn) writeAckForAll() { // writeAckForLatest sends the Conn a datagram containing an ack for the // most recent packet received. func (tc *testConn) writeAckForLatest() { + tc.t.Helper() if tc.lastPacket == nil { return } diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 1033cbb401..2dbf4461ba 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -49,9 +49,38 @@ type Stream struct { outresetcode uint64 // reset code to send in RESET_STREAM outdone chan struct{} // closed when all data sent + // Atomic stream state bits. + // + // These bits provide a fast way to coordinate between the + // send and receive sides of the stream, and the conn's loop. + // + // streamIn* bits must be set with ingate held. + // streamOut* bits must be set with outgate held. + // streamConn* bits are set by the conn's loop. + state atomicBits[streamState] + prev, next *Stream // guarded by streamsState.sendMu } +type streamState uint32 + +const ( + // streamInSend and streamOutSend are set when there are + // frames to send for the inbound or outbound sides of the stream. + // For example, MAX_STREAM_DATA or STREAM_DATA_BLOCKED. + streamInSend = streamState(1 << iota) + streamOutSend + + // streamInDone and streamOutDone are set when the inbound or outbound + // sides of the stream are finished. When both are set, the stream + // can be removed from the Conn and forgotten. + streamInDone + streamOutDone + + // streamConnRemoved is set when the stream has been removed from the conn. + streamConnRemoved +) + // newStream returns a new stream. // // The stream's ingate and outgate are locked. @@ -289,15 +318,34 @@ func (s *Stream) CloseWrite() { // that the stream was terminated abruptly. // Any blocked writes will be unblocked and return errors. // -// Reset sends the application protocol error code to the peer. +// Reset sends the application protocol error code, which must be +// less than 2^62, to the peer. // It does not wait for the peer to acknowledge receipt of the error. // Use CloseContext to wait for the peer's acknowledgement. +// +// Reset does not affect reads. +// Use CloseRead to abort reads on the stream. func (s *Stream) Reset(code uint64) { + const userClosed = true + s.resetInternal(code, userClosed) +} + +func (s *Stream) resetInternal(code uint64, userClosed bool) { s.outgate.lock() defer s.outUnlock() + if s.IsReadOnly() { + return + } + if userClosed { + // Mark that the user closed the stream. + s.outclosed.set() + } if s.outreset.isSet() { return } + if code > maxVarint { + code = maxVarint + } // We could check here to see if the stream is closed and the // peer has acked all the data and the FIN, but sending an // extra RESET_STREAM in this case is harmless. @@ -310,44 +358,67 @@ func (s *Stream) Reset(code uint64) { // inUnlock unlocks s.ingate. // It sets the gate condition if reads from s will not block. -// If s has receive-related frames to write, it notifies the Conn. +// If s has receive-related frames to write or if both directions +// are done and the stream should be removed, it notifies the Conn. func (s *Stream) inUnlock() { - if s.inUnlockNoQueue() { + state := s.inUnlockNoQueue() + if state&streamInSend != 0 || state == streamInDone|streamOutDone { s.conn.queueStreamForSend(s) } } // inUnlockNoQueue is inUnlock, // but reports whether s has frames to write rather than notifying the Conn. -func (s *Stream) inUnlockNoQueue() (shouldSend bool) { +func (s *Stream) inUnlockNoQueue() streamState { canRead := s.inset.contains(s.in.start) || // data available to read s.insize == s.in.start || // at EOF s.inresetcode != -1 || // reset by peer s.inclosed.isSet() // closed locally defer s.ingate.unlock(canRead) - return s.insendmax.shouldSend() || // STREAM_MAX_DATA - s.inclosed.shouldSend() // STOP_SENDING + var state streamState + switch { + case s.IsWriteOnly(): + state = streamInDone + case s.inresetcode != -1: // reset by peer + fallthrough + case s.in.start == s.insize: // all data received and read + // We don't increase MAX_STREAMS until the user calls ReadClose or Close, + // so the receive side is not finished until inclosed is set. + if s.inclosed.isSet() { + state = streamInDone + } + case s.insendmax.shouldSend(): // STREAM_MAX_DATA + state = streamInSend + case s.inclosed.shouldSend(): // STOP_SENDING + state = streamInSend + } + const mask = streamInDone | streamInSend + return s.state.set(state, mask) } // outUnlock unlocks s.outgate. // It sets the gate condition if writes to s will not block. -// If s has send-related frames to write, it notifies the Conn. +// If s has send-related frames to write or if both directions +// are done and the stream should be removed, it notifies the Conn. func (s *Stream) outUnlock() { - if s.outUnlockNoQueue() { + state := s.outUnlockNoQueue() + if state&streamOutSend != 0 || state == streamInDone|streamOutDone { s.conn.queueStreamForSend(s) } } // outUnlockNoQueue is outUnlock, // but reports whether s has frames to write rather than notifying the Conn. -func (s *Stream) outUnlockNoQueue() (shouldSend bool) { +func (s *Stream) outUnlockNoQueue() streamState { isDone := s.outclosed.isReceived() && s.outacked.isrange(0, s.out.end) || // all data acked s.outreset.isSet() // reset locally if isDone { select { case <-s.outdone: default: - close(s.outdone) + if !s.IsReadOnly() { + close(s.outdone) + } } } lim := min(s.out.start+s.outmaxbuf, s.outwin) @@ -355,14 +426,32 @@ func (s *Stream) outUnlockNoQueue() (shouldSend bool) { s.outclosed.isSet() || // closed locally s.outreset.isSet() // reset locally defer s.outgate.unlock(canWrite) - if s.outreset.isSet() { - // If the stream is reset locally, the only frame we'll send is RESET_STREAM. - return s.outreset.shouldSend() - } - return len(s.outunsent) > 0 || // STREAM frame with data - s.outclosed.shouldSend() || // STREAM frame with FIN bit - s.outopened.shouldSend() || // STREAM frame with no data - s.outblocked.shouldSend() // STREAM_DATA_BLOCKED + var state streamState + switch { + case s.IsReadOnly(): + state = streamOutDone + case s.outclosed.isReceived() && s.outacked.isrange(0, s.out.end): // all data sent and acked + fallthrough + case s.outreset.isReceived(): // RESET_STREAM sent and acked + // We don't increase MAX_STREAMS until the user calls WriteClose or Close, + // so the send side is not finished until outclosed is set. + if s.outclosed.isSet() { + state = streamOutDone + } + case s.outreset.shouldSend(): // RESET_STREAM + state = streamOutSend + case s.outreset.isSet(): // RESET_STREAM sent but not acknowledged + case len(s.outunsent) > 0: // STREAM frame with data + state = streamOutSend + case s.outclosed.shouldSend(): // STREAM frame with FIN bit + state = streamOutSend + case s.outopened.shouldSend(): // STREAM frame with no data + state = streamOutSend + case s.outblocked.shouldSend(): // STREAM_DATA_BLOCKED + state = streamOutSend + } + const mask = streamOutDone | streamOutSend + return s.state.set(state, mask) } // handleData handles data received in a STREAM frame. @@ -431,7 +520,8 @@ func (s *Stream) checkStreamBounds(end int64, fin bool) error { func (s *Stream) handleStopSending(code uint64) error { // Peer requests that we reset this stream. // https://www.rfc-editor.org/rfc/rfc9000#section-3.5-4 - s.Reset(code) + const userReset = false + s.resetInternal(code, userReset) return nil } @@ -504,14 +594,12 @@ func (s *Stream) ackOrLossData(pnum packetNumber, start, end int64, fin bool, fa } } -// appendInFrames appends STOP_SENDING and MAX_STREAM_DATA frames +// appendInFramesLocked appends STOP_SENDING and MAX_STREAM_DATA frames // to the current packet. // // It returns true if no more frames need appending, // false if not everything fit in the current packet. -func (s *Stream) appendInFrames(w *packetWriter, pnum packetNumber, pto bool) bool { - s.ingate.lock() - defer s.inUnlockNoQueue() +func (s *Stream) appendInFramesLocked(w *packetWriter, pnum packetNumber, pto bool) bool { if s.inclosed.shouldSendPTO(pto) { // We don't currently have an API for setting the error code. // Just send zero. @@ -534,14 +622,12 @@ func (s *Stream) appendInFrames(w *packetWriter, pnum packetNumber, pto bool) bo return true } -// appendOutFrames appends RESET_STREAM, STREAM_DATA_BLOCKED, and STREAM frames +// appendOutFramesLocked appends RESET_STREAM, STREAM_DATA_BLOCKED, and STREAM frames // to the current packet. // // It returns true if no more frames need appending, // false if not everything fit in the current packet. -func (s *Stream) appendOutFrames(w *packetWriter, pnum packetNumber, pto bool) bool { - s.outgate.lock() - defer s.outUnlockNoQueue() +func (s *Stream) appendOutFramesLocked(w *packetWriter, pnum packetNumber, pto bool) bool { if s.outreset.isSet() { // RESET_STREAM if s.outreset.shouldSendPTO(pto) { diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 79377c6a4a..e22e0432ef 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -1111,6 +1111,24 @@ func TestStreamPeerResetFollowedByData(t *testing.T) { }) } +func TestStreamResetInvalidCode(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream) + s.Reset(1 << 62) + tc.wantFrame("reset with invalid code sends a RESET_STREAM anyway", + packetType1RTT, debugFrameResetStream{ + id: s.id, + // The code we send here isn't specified, + // so this could really be any value. + code: (1 << 62) - 1, + }) +} + +func TestStreamResetReceiveOnly(t *testing.T) { + tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream) + s.Reset(0) + tc.wantIdle("resetting a receive-only stream has no effect") +} + func TestStreamPeerStopSendingForActiveStream(t *testing.T) { // "An endpoint that receives a STOP_SENDING frame MUST send a RESET_STREAM frame if // the stream is in the "Ready" or "Send" state." @@ -1145,6 +1163,21 @@ func TestStreamPeerStopSendingForActiveStream(t *testing.T) { }) } +type streamSide string + +const ( + localStream = streamSide("local") + remoteStream = streamSide("remote") +) + +func newTestConnAndStream(t *testing.T, side connSide, sside streamSide, styp streamType, opts ...any) (*testConn, *Stream) { + if sside == localStream { + return newTestConnAndLocalStream(t, side, styp, opts...) + } else { + return newTestConnAndRemoteStream(t, side, styp, opts...) + } +} + func newTestConnAndLocalStream(t *testing.T, side connSide, styp streamType, opts ...any) (*testConn, *Stream) { t.Helper() ctx := canceledContext() From 2a0da8be5a758b33fa896384d689071e832b4aa2 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Tue, 5 Sep 2023 15:01:14 +0000 Subject: [PATCH 046/168] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: I2011e2fc11608c371c3145c95a4cf98609010f99 Reviewed-on: https://go-review.googlesource.com/c/net/+/525635 Reviewed-by: Dmitri Shuralyov TryBot-Result: Gopher Robot Run-TryBot: Gopher Robot Auto-Submit: Gopher Robot Reviewed-by: Dmitri Shuralyov Reviewed-by: Carlos Amedee --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 90f428f40d..b16f4e5e68 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module golang.org/x/net go 1.17 require ( - golang.org/x/crypto v0.12.0 - golang.org/x/sys v0.11.0 - golang.org/x/term v0.11.0 - golang.org/x/text v0.12.0 + golang.org/x/crypto v0.13.0 + golang.org/x/sys v0.12.0 + golang.org/x/term v0.12.0 + golang.org/x/text v0.13.0 ) diff --git a/go.sum b/go.sum index c39d831315..0fd3311f48 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -20,21 +20,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 94087adbf6b27706b82037e0ab2736b0c1b41618 Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Fri, 1 Sep 2023 09:55:13 +0000 Subject: [PATCH 047/168] dns/dnsmessage: mention that Name in non-escaped Change-Id: I090dea04d6007dc985d89270d0138f822dc7a10b GitHub-Last-Rev: c604beebc1e15970d310d7379817c33113f19716 GitHub-Pull-Request: golang/net#176 Reviewed-on: https://go-review.googlesource.com/c/net/+/500295 Auto-Submit: Ian Lance Taylor Reviewed-by: Ian Lance Taylor Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI --- dns/dnsmessage/message.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index 0215a5dde3..dda888a90a 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -1901,7 +1901,7 @@ func unpackBytes(msg []byte, off int, field []byte) (int, error) { const nonEncodedNameMax = 254 -// A Name is a non-encoded domain name. It is used instead of strings to avoid +// A Name is a non-encoded and non-escaped domain name. It is used instead of strings to avoid // allocations. type Name struct { Data [255]byte @@ -1928,6 +1928,8 @@ func MustNewName(name string) Name { } // String implements fmt.Stringer.String. +// +// Note: characters inside the labels are not escaped in any way. func (n Name) String() string { return string(n.Data[:n.Length]) } From 717519db95f15e62a4b469aa350185dbeaf26804 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 29 Aug 2023 08:29:32 -0700 Subject: [PATCH 048/168] quic: limits on the number of open streams Honor the peer's max stream limit. New stream creation blocks until stream quota is available. Enforce the number of open streams created by the peer. Send updated stream quota as streams are closed locally. Remove streams from the conn's set when they are fully closed. For golang/go#58547 Change-Id: Iff969c5cb8e8e0c6ad91d217a92c38bceabef8ee Reviewed-on: https://go-review.googlesource.com/c/net/+/524036 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/config.go | 42 ++++- internal/quic/config_test.go | 12 +- internal/quic/conn.go | 4 + internal/quic/conn_loss.go | 4 + internal/quic/conn_loss_test.go | 83 ++++++++- internal/quic/conn_recv.go | 14 +- internal/quic/conn_streams.go | 62 ++++++- internal/quic/conn_streams_test.go | 148 ++++++++++++++- internal/quic/quic.go | 7 + internal/quic/stream.go | 4 + internal/quic/stream_limits.go | 109 +++++++++++ internal/quic/stream_limits_test.go | 269 ++++++++++++++++++++++++++++ internal/quic/stream_test.go | 8 +- 13 files changed, 735 insertions(+), 31 deletions(-) create mode 100644 internal/quic/stream_limits.go create mode 100644 internal/quic/stream_limits_test.go diff --git a/internal/quic/config.go b/internal/quic/config.go index df493579f2..f78e811926 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -18,6 +18,18 @@ type Config struct { // It must be non-nil and include at least one certificate or else set GetCertificate. TLSConfig *tls.Config + // MaxBidiRemoteStreams limits the number of simultaneous bidirectional streams + // a peer may open. + // If zero, the default value of 100 is used. + // If negative, the limit is zero. + MaxBidiRemoteStreams int64 + + // MaxUniRemoteStreams limits the number of simultaneous unidirectional streams + // a peer may open. + // If zero, the default value of 100 is used. + // If negative, the limit is zero. + MaxUniRemoteStreams int64 + // StreamReadBufferSize is the maximum amount of data sent by the peer that a // stream will buffer for reading. // If zero, the default value of 1MiB is used. @@ -31,15 +43,29 @@ type Config struct { StreamWriteBufferSize int64 } -func configDefault(v, def int64) int64 { - switch v { - case -1: - return 0 - case 0: +func configDefault(v, def, limit int64) int64 { + switch { + case v == 0: return def + case v < 0: + return 0 + default: + return min(v, limit) } - return v } -func (c *Config) streamReadBufferSize() int64 { return configDefault(c.StreamReadBufferSize, 1<<20) } -func (c *Config) streamWriteBufferSize() int64 { return configDefault(c.StreamWriteBufferSize, 1<<20) } +func (c *Config) maxBidiRemoteStreams() int64 { + return configDefault(c.MaxBidiRemoteStreams, 100, maxStreamsLimit) +} + +func (c *Config) maxUniRemoteStreams() int64 { + return configDefault(c.MaxUniRemoteStreams, 100, maxStreamsLimit) +} + +func (c *Config) streamReadBufferSize() int64 { + return configDefault(c.StreamReadBufferSize, 1<<20, maxVarint) +} + +func (c *Config) streamWriteBufferSize() int64 { + return configDefault(c.StreamWriteBufferSize, 1<<20, maxVarint) +} diff --git a/internal/quic/config_test.go b/internal/quic/config_test.go index cec57c5e36..8d67ef0d40 100644 --- a/internal/quic/config_test.go +++ b/internal/quic/config_test.go @@ -10,9 +10,13 @@ import "testing" func TestConfigTransportParameters(t *testing.T) { const ( - wantInitialMaxStreamData = int64(2) + wantInitialMaxStreamData = int64(2) + wantInitialMaxStreamsBidi = int64(3) + wantInitialMaxStreamsUni = int64(4) ) tc := newTestConn(t, clientSide, func(c *Config) { + c.MaxBidiRemoteStreams = wantInitialMaxStreamsBidi + c.MaxUniRemoteStreams = wantInitialMaxStreamsUni c.StreamReadBufferSize = wantInitialMaxStreamData }) tc.handshake() @@ -29,4 +33,10 @@ func TestConfigTransportParameters(t *testing.T) { if got, want := p.initialMaxStreamDataUni, wantInitialMaxStreamData; got != want { t.Errorf("initial_max_stream_data_uni = %v, want %v", got, want) } + if got, want := p.initialMaxStreamsBidi, wantInitialMaxStreamsBidi; got != want { + t.Errorf("initial_max_stream_data_uni = %v, want %v", got, want) + } + if got, want := p.initialMaxStreamsUni, wantInitialMaxStreamsUni; got != want { + t.Errorf("initial_max_stream_data_uni = %v, want %v", got, want) + } } diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 04dcd7b6bb..642c507618 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -121,6 +121,8 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. initialMaxStreamDataBidiLocal: config.streamReadBufferSize(), initialMaxStreamDataBidiRemote: config.streamReadBufferSize(), initialMaxStreamDataUni: config.streamReadBufferSize(), + initialMaxStreamsBidi: c.streams.remoteLimit[bidiStream].max, + initialMaxStreamsUni: c.streams.remoteLimit[uniStream].max, activeConnIDLimit: activeConnIDLimit, }) @@ -167,6 +169,8 @@ func (c *Conn) discardKeys(now time.Time, space numberSpace) { // receiveTransportParameters applies transport parameters sent by the peer. func (c *Conn) receiveTransportParameters(p transportParameters) error { + c.streams.localLimit[bidiStream].setMax(p.initialMaxStreamsBidi) + c.streams.localLimit[uniStream].setMax(p.initialMaxStreamsUni) c.streams.peerInitialMaxStreamDataBidiLocal = p.initialMaxStreamDataBidiLocal c.streams.peerInitialMaxStreamDataRemote[bidiStream] = p.initialMaxStreamDataBidiRemote c.streams.peerInitialMaxStreamDataRemote[uniStream] = p.initialMaxStreamDataUni diff --git a/internal/quic/conn_loss.go b/internal/quic/conn_loss.go index 103db9fa41..b8146a4255 100644 --- a/internal/quic/conn_loss.go +++ b/internal/quic/conn_loss.go @@ -64,6 +64,10 @@ func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetF } fin := f&streamFinBit != 0 s.ackOrLossData(sent.num, start, end, fin, fate) + case frameTypeMaxStreamsBidi: + c.streams.remoteLimit[bidiStream].sendMax.ackLatestOrLoss(sent.num, fate) + case frameTypeMaxStreamsUni: + c.streams.remoteLimit[uniStream].sendMax.ackLatestOrLoss(sent.num, fate) case frameTypeNewConnectionID: seq := int64(sent.nextInt()) c.connIDState.ackOrLossNewConnectionID(sent.num, seq, fate) diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index bb43030330..d426aa6902 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -174,9 +174,7 @@ func TestLostStreamFrameEmpty(t *testing.T) { // be retransmitted if lost. lostFrameTest(t, func(t *testing.T, pto bool) { ctx := canceledContext() - tc := newTestConn(t, clientSide, func(p *transportParameters) { - p.initialMaxStreamDataBidiRemote = 100 - }) + tc := newTestConn(t, clientSide, permissiveTransportParameters) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -370,6 +368,85 @@ func TestLostMaxStreamDataFrameAfterStreamFinReceived(t *testing.T) { }) } +func TestLostMaxStreamsFrameMostRecent(t *testing.T) { + // "[...] an updated value is sent when a packet containing the + // most recent MAX_STREAMS for a stream type frame is declared lost [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.9 + lostFrameTest(t, func(t *testing.T, pto bool) { + ctx := canceledContext() + tc := newTestConn(t, serverSide, func(c *Config) { + c.MaxUniRemoteStreams = 1 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, uniStream, 0), + fin: true, + }) + s, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("AcceptStream() = %v", err) + } + if err := s.CloseContext(ctx); err != nil { + t.Fatalf("stream.Close() = %v", err) + } + tc.wantFrame("closing stream updates peer's MAX_STREAMS", + packetType1RTT, debugFrameMaxStreams{ + streamType: uniStream, + max: 2, + }) + + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("lost MAX_STREAMS is resent", + packetType1RTT, debugFrameMaxStreams{ + streamType: uniStream, + max: 2, + }) + }) +} + +func TestLostMaxStreamsFrameNotMostRecent(t *testing.T) { + // Send two MAX_STREAMS frames, lose the first one. + // + // No PTO mode for this test: The ack that causes the first frame + // to be lost arms the loss timer for the second, so the PTO timer is not armed. + const pto = false + ctx := canceledContext() + tc := newTestConn(t, serverSide, func(c *Config) { + c.MaxUniRemoteStreams = 2 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + for i := int64(0); i < 2; i++ { + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, uniStream, i), + fin: true, + }) + s, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("AcceptStream() = %v", err) + } + if err := s.CloseContext(ctx); err != nil { + t.Fatalf("stream.Close() = %v", err) + } + tc.wantFrame("closing stream updates peer's MAX_STREAMS", + packetType1RTT, debugFrameMaxStreams{ + streamType: uniStream, + max: 3 + i, + }) + } + + // The second MAX_STREAMS frame is acked. + tc.writeAckForLatest() + + // The first MAX_STREAMS frame is lost. + tc.conn.ping(appDataSpace) + tc.wantFrame("connection should send a PING frame", + packetType1RTT, debugFramePing{}) + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantIdle("superseded MAX_DATA is not resent on loss") +} + func TestLostStreamDataBlockedFrame(t *testing.T) { // "A new [STREAM_DATA_BLOCKED] frame is sent if a packet containing // the most recent frame for a scope is lost [...]" diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index e0a91ab00c..faf3a37f1a 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -196,7 +196,7 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, if !frameOK(c, ptype, __01) { return } - _, _, n = consumeMaxStreamsFrame(payload) + n = c.handleMaxStreamsFrame(now, payload) case frameTypeStreamsBlockedBidi, frameTypeStreamsBlockedUni: if !frameOK(c, ptype, __01) { return @@ -282,6 +282,9 @@ func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) func (c *Conn) handleMaxStreamDataFrame(now time.Time, payload []byte) int { id, maxStreamData, n := consumeMaxStreamDataFrame(payload) + if n < 0 { + return -1 + } if s := c.streamForFrame(now, id, sendStream); s != nil { if err := s.handleMaxStreamData(maxStreamData); err != nil { c.abort(now, err) @@ -291,6 +294,15 @@ func (c *Conn) handleMaxStreamDataFrame(now time.Time, payload []byte) int { return n } +func (c *Conn) handleMaxStreamsFrame(now time.Time, payload []byte) int { + styp, max, n := consumeMaxStreamsFrame(payload) + if n < 0 { + return -1 + } + c.streams.localLimit[styp].setMax(max) + return n +} + func (c *Conn) handleResetStreamFrame(now time.Time, space numberSpace, payload []byte) int { id, code, finalSize, n := consumeResetStreamFrame(payload) if n < 0 { diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index 0ede284e23..716ed2d504 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -18,7 +18,10 @@ type streamsState struct { streamsMu sync.Mutex streams map[streamID]*Stream - opened [streamTypeCount]int64 // number of streams opened by us + + // Limits on the number of streams, indexed by streamType. + localLimit [streamTypeCount]localStreamLimits + remoteLimit [streamTypeCount]remoteStreamLimits // Peer configuration provided in transport parameters. peerInitialMaxStreamDataRemote [streamTypeCount]int64 // streams opened by us @@ -36,6 +39,10 @@ type streamsState struct { func (c *Conn) streamsInit() { c.streams.streams = make(map[streamID]*Stream) c.streams.queue = newQueue[*Stream]() + c.streams.localLimit[bidiStream].init() + c.streams.localLimit[uniStream].init() + c.streams.remoteLimit[bidiStream].init(c.config.maxBidiRemoteStreams()) + c.streams.remoteLimit[uniStream].init(c.config.maxUniRemoteStreams()) } // AcceptStream waits for and returns the next stream created by the peer. @@ -60,12 +67,13 @@ func (c *Conn) NewSendOnlyStream(ctx context.Context) (*Stream, error) { } func (c *Conn) newLocalStream(ctx context.Context, styp streamType) (*Stream, error) { - // TODO: Stream limits. c.streams.streamsMu.Lock() defer c.streams.streamsMu.Unlock() - num := c.streams.opened[styp] - c.streams.opened[styp]++ + num, err := c.streams.localLimit[styp].open(ctx, c) + if err != nil { + return nil, err + } s := newStream(c, newStreamID(c.side, styp, num)) s.outmaxbuf = c.config.streamWriteBufferSize() @@ -122,16 +130,46 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) c.streams.streamsMu.Lock() defer c.streams.streamsMu.Unlock() - if s := c.streams.streams[id]; s != nil { + s, isOpen := c.streams.streams[id] + if s != nil { return s } - // TODO: Check for closed streams, once we support closing streams. + + num := id.num() + styp := id.streamType() if id.initiator() == c.side { + if num < c.streams.localLimit[styp].opened { + // This stream was created by us, and has been closed. + return nil + } + // Received a frame for a stream that should be originated by us, + // but which we never created. c.abort(now, localTransportError(errStreamState)) return nil + } else { + // if isOpen, this is a stream that was implicitly opened by a + // previous frame for a larger-numbered stream, but we haven't + // actually created it yet. + if !isOpen && num < c.streams.remoteLimit[styp].opened { + // This stream was created by the peer, and has been closed. + return nil + } } - s := newStream(c, id) + prevOpened := c.streams.remoteLimit[styp].opened + if err := c.streams.remoteLimit[styp].open(id); err != nil { + c.abort(now, err) + return nil + } + + // Receiving a frame for a stream implicitly creates all streams + // with the same initiator and type and a lower number. + // Add a nil entry to the streams map for each implicitly created stream. + for n := newStreamID(id.initiator(), id.streamType(), prevOpened); n < id; n += 4 { + c.streams.streams[n] = nil + } + + s = newStream(c, id) s.inmaxbuf = c.config.streamReadBufferSize() s.inwin = c.config.streamReadBufferSize() if id.streamType() == bidiStream { @@ -174,6 +212,8 @@ func (c *Conn) queueStreamForSend(s *Stream) { // It returns true if no more frames need appending, // false if not everything fit in the current packet. func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) bool { + c.streams.remoteLimit[uniStream].appendFrame(w, uniStream, pnum, pto) + c.streams.remoteLimit[bidiStream].appendFrame(w, bidiStream, pnum, pto) if pto { return c.appendStreamFramesPTO(w, pnum) } @@ -222,7 +262,11 @@ func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) s.state.set(streamConnRemoved, streamConnRemoved) delete(c.streams.streams, s.id) - // TODO: Provide the peer with additional stream quota (MAX_STREAMS). + // Record finalization of remote streams, to know when + // to extend the peer's stream limit. + if s.id.initiator() != c.side { + c.streams.remoteLimit[s.id.streamType()].close() + } } next := s.next @@ -251,6 +295,7 @@ func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) func (c *Conn) appendStreamFramesPTO(w *packetWriter, pnum packetNumber) bool { c.streams.sendMu.Lock() defer c.streams.sendMu.Unlock() + const pto = true for _, s := range c.streams.streams { const pto = true s.ingate.lock() @@ -259,6 +304,7 @@ func (c *Conn) appendStreamFramesPTO(w *packetWriter, pnum packetNumber) bool { if !inOK { return false } + s.outgate.lock() outOK := s.appendOutFramesLocked(w, pnum, pto) s.outUnlockNoQueue() diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go index 9bbc994b11..ab1df1a248 100644 --- a/internal/quic/conn_streams_test.go +++ b/internal/quic/conn_streams_test.go @@ -10,15 +10,13 @@ import ( "context" "fmt" "io" + "math" "testing" ) func TestStreamsCreate(t *testing.T) { ctx := canceledContext() - tc := newTestConn(t, clientSide, func(p *transportParameters) { - p.initialMaxStreamDataBidiLocal = 100 - p.initialMaxStreamDataBidiRemote = 100 - }) + tc := newTestConn(t, clientSide, permissiveTransportParameters) tc.handshake() c, err := tc.conn.NewStream(ctx) @@ -126,7 +124,7 @@ func TestStreamsBlockingAccept(t *testing.T) { } } -func TestStreamsStreamNotCreated(t *testing.T) { +func TestStreamsLocalStreamNotCreated(t *testing.T) { // "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR // if it receives a STREAM frame for a locally initiated stream that has // not yet been created [...]" @@ -144,13 +142,39 @@ func TestStreamsStreamNotCreated(t *testing.T) { }) } +func TestStreamsLocalStreamClosed(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, clientSide, uniStream, permissiveTransportParameters) + s.CloseWrite() + tc.wantFrame("FIN for closed stream", + packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, uniStream, 0), + fin: true, + data: []byte{}, + }) + tc.writeAckForAll() + + tc.writeFrames(packetType1RTT, debugFrameStopSending{ + id: newStreamID(clientSide, uniStream, 0), + }) + tc.wantIdle("frame for finalized stream is ignored") + + // ACKing the last stream packet should have cleaned up the stream. + // Check that we don't have any state left. + if got := len(tc.conn.streams.streams); got != 0 { + t.Fatalf("after close, len(tc.conn.streams.streams) = %v, want 0", got) + } + if tc.conn.streams.sendHead != nil { + t.Fatalf("after close, stream send queue is not empty; should be") + } +} + func TestStreamsStreamSendOnly(t *testing.T) { // "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR // if it receives a STREAM frame for a locally initiated stream that has // not yet been created [...]" // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.8-3 ctx := canceledContext() - tc := newTestConn(t, serverSide) + tc := newTestConn(t, serverSide, permissiveTransportParameters) tc.handshake() c, err := tc.conn.NewSendOnlyStream(ctx) @@ -342,3 +366,115 @@ func TestStreamsShutdown(t *testing.T) { }) } } + +func TestStreamsCreateAndCloseRemote(t *testing.T) { + // This test exercises creating new streams in response to frames + // from the peer, and cleaning up after streams are fully closed. + // + // It's overfitted to the current implementation, but works through + // a number of corner cases in that implementation. + // + // Disable verbose logging in this test: It sends a lot of packets, + // and they're not especially interesting on their own. + defer func(vv bool) { + *testVV = vv + }(*testVV) + *testVV = false + ctx := canceledContext() + tc := newTestConn(t, serverSide, permissiveTransportParameters) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + type op struct { + id streamID + } + type streamOp op + type resetOp op + type acceptOp op + const noStream = math.MaxInt64 + stringID := func(id streamID) string { + return fmt.Sprintf("%v/%v", id.streamType(), id.num()) + } + for _, op := range []any{ + "opening bidi/5 implicitly opens bidi/0-4", + streamOp{newStreamID(clientSide, bidiStream, 5)}, + acceptOp{newStreamID(clientSide, bidiStream, 5)}, + "bidi/3 was implicitly opened", + streamOp{newStreamID(clientSide, bidiStream, 3)}, + acceptOp{newStreamID(clientSide, bidiStream, 3)}, + resetOp{newStreamID(clientSide, bidiStream, 3)}, + "bidi/3 is done, frames for it are discarded", + streamOp{newStreamID(clientSide, bidiStream, 3)}, + "open and close some uni streams as well", + streamOp{newStreamID(clientSide, uniStream, 0)}, + acceptOp{newStreamID(clientSide, uniStream, 0)}, + streamOp{newStreamID(clientSide, uniStream, 1)}, + acceptOp{newStreamID(clientSide, uniStream, 1)}, + streamOp{newStreamID(clientSide, uniStream, 2)}, + acceptOp{newStreamID(clientSide, uniStream, 2)}, + resetOp{newStreamID(clientSide, uniStream, 1)}, + resetOp{newStreamID(clientSide, uniStream, 0)}, + resetOp{newStreamID(clientSide, uniStream, 2)}, + "closing an implicitly opened stream causes us to accept it", + resetOp{newStreamID(clientSide, bidiStream, 0)}, + acceptOp{newStreamID(clientSide, bidiStream, 0)}, + resetOp{newStreamID(clientSide, bidiStream, 1)}, + acceptOp{newStreamID(clientSide, bidiStream, 1)}, + resetOp{newStreamID(clientSide, bidiStream, 2)}, + acceptOp{newStreamID(clientSide, bidiStream, 2)}, + "stream bidi/3 was reset previously", + resetOp{newStreamID(clientSide, bidiStream, 3)}, + resetOp{newStreamID(clientSide, bidiStream, 4)}, + acceptOp{newStreamID(clientSide, bidiStream, 4)}, + "stream bidi/5 was reset previously", + resetOp{newStreamID(clientSide, bidiStream, 5)}, + "stream bidi/6 was not implicitly opened", + resetOp{newStreamID(clientSide, bidiStream, 6)}, + acceptOp{newStreamID(clientSide, bidiStream, 6)}, + } { + if _, ok := op.(acceptOp); !ok { + if s, err := tc.conn.AcceptStream(ctx); err == nil { + t.Fatalf("accepted stream %v, want none", stringID(s.id)) + } + } + switch op := op.(type) { + case string: + t.Log("# " + op) + case streamOp: + t.Logf("open stream %v", stringID(op.id)) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: streamID(op.id), + }) + case resetOp: + t.Logf("reset stream %v", stringID(op.id)) + tc.writeFrames(packetType1RTT, debugFrameResetStream{ + id: op.id, + }) + case acceptOp: + s, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("AcceptStream() = %q; want stream %v", err, stringID(op.id)) + } + if s.id != op.id { + t.Fatalf("accepted stram %v; want stream %v", err, stringID(op.id)) + } + t.Logf("accepted stream %v", stringID(op.id)) + // Immediately close the stream, so the stream becomes done when the + // peer closes its end. + s.CloseContext(ctx) + } + p := tc.readPacket() + if p != nil { + tc.writeFrames(p.ptype, debugFrameAck{ + ranges: []i64range[packetNumber]{{0, p.num + 1}}, + }) + } + } + // Every stream should be fully closed now. + // Check that we don't have any state left. + if got := len(tc.conn.streams.streams); got != 0 { + t.Fatalf("after test, len(tc.conn.streams.streams) = %v, want 0", got) + } + if tc.conn.streams.sendHead != nil { + t.Fatalf("after test, stream send queue is not empty; should be") + } +} diff --git a/internal/quic/quic.go b/internal/quic/quic.go index 71738e129d..cf4137e810 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -59,6 +59,13 @@ const minimumClientInitialDatagramSize = 1200 // https://www.rfc-editor.org/rfc/rfc9000.html#section-4.6-2 const maxStreamsLimit = 1 << 60 +// Maximum number of streams we will allow the peer to create implicitly. +// A stream ID that is used out of order results in all streams of that type +// with lower-numbered IDs also being opened. To limit the amount of work we +// will do in response to a single frame, we cap the peer's stream limit to +// this value. +const implicitStreamLimit = 100 + // A connSide distinguishes between the client and server sides of a connection. type connSide int8 diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 2dbf4461ba..b759e406cc 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -330,6 +330,10 @@ func (s *Stream) Reset(code uint64) { s.resetInternal(code, userClosed) } +// resetInternal resets the send side of the stream. +// +// If userClosed is true, this is s.Reset. +// If userClosed is false, this is a reaction to a STOP_SENDING frame. func (s *Stream) resetInternal(code uint64, userClosed bool) { s.outgate.lock() defer s.outUnlock() diff --git a/internal/quic/stream_limits.go b/internal/quic/stream_limits.go new file mode 100644 index 0000000000..5ea7146b50 --- /dev/null +++ b/internal/quic/stream_limits.go @@ -0,0 +1,109 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" +) + +// Limits on the number of open streams. +// Every connection has separate limits for bidirectional and unidirectional streams. +// +// Note that the MAX_STREAMS limit includes closed as well as open streams. +// Closing a stream doesn't enable an endpoint to open a new one; +// only an increase in the MAX_STREAMS limit does. + +// localStreamLimits are limits on the number of open streams created by us. +type localStreamLimits struct { + gate gate + max int64 // peer-provided MAX_STREAMS + opened int64 // number of streams opened by us +} + +func (lim *localStreamLimits) init() { + lim.gate = newGate() +} + +// open creates a new local stream, blocking until MAX_STREAMS quota is available. +func (lim *localStreamLimits) open(ctx context.Context, c *Conn) (num int64, err error) { + // TODO: Send a STREAMS_BLOCKED when blocked. + if err := c.waitAndLockGate(ctx, &lim.gate); err != nil { + return 0, err + } + n := lim.opened + lim.opened++ + lim.gate.unlock(lim.opened < lim.max) + return n, nil +} + +// setMax sets the MAX_STREAMS provided by the peer. +func (lim *localStreamLimits) setMax(maxStreams int64) { + lim.gate.lock() + lim.max = max(lim.max, maxStreams) + lim.gate.unlock(lim.opened < lim.max) +} + +// remoteStreamLimits are limits on the number of open streams created by the peer. +type remoteStreamLimits struct { + max int64 // last MAX_STREAMS sent to the peer + opened int64 // number of streams opened by the peer (including subsequently closed ones) + closed int64 // number of peer streams in the "closed" state + maxOpen int64 // how many streams we want to let the peer simultaneously open + sendMax sentVal // set when we should send MAX_STREAMS +} + +func (lim *remoteStreamLimits) init(maxOpen int64) { + lim.maxOpen = maxOpen + lim.max = min(maxOpen, implicitStreamLimit) // initial limit sent in transport parameters + lim.opened = 0 +} + +// open handles the peer opening a new stream. +func (lim *remoteStreamLimits) open(id streamID) error { + num := id.num() + if num >= lim.max { + return localTransportError(errStreamLimit) + } + if num >= lim.opened { + lim.opened = num + 1 + lim.maybeUpdateMax() + } + return nil +} + +// close handles the peer closing an open stream. +func (lim *remoteStreamLimits) close() { + lim.closed++ + lim.maybeUpdateMax() +} + +// maybeUpdateMax updates the MAX_STREAMS value we will send to the peer. +func (lim *remoteStreamLimits) maybeUpdateMax() { + newMax := min( + // Max streams the peer can have open at once. + lim.closed+lim.maxOpen, + // Max streams the peer can open with a single frame. + lim.opened+implicitStreamLimit, + ) + avail := lim.max - lim.opened + if newMax > lim.max && (avail < 8 || newMax-lim.max >= 2*avail) { + // If the peer has less than 8 streams, or if increasing the peer's + // stream limit would double it, then send a MAX_STREAMS. + lim.max = newMax + lim.sendMax.setUnsent() + } +} + +// appendFrame appends a MAX_DATA frame if necessary. +func (lim *remoteStreamLimits) appendFrame(w *packetWriter, typ streamType, pnum packetNumber, pto bool) { + if !lim.sendMax.shouldSendPTO(pto) { + return + } + if w.appendMaxStreamsFrame(typ, lim.max) { + lim.sendMax.setSent(pnum) + } +} diff --git a/internal/quic/stream_limits_test.go b/internal/quic/stream_limits_test.go new file mode 100644 index 0000000000..3f291e9f4c --- /dev/null +++ b/internal/quic/stream_limits_test.go @@ -0,0 +1,269 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "crypto/tls" + "testing" +) + +func TestStreamLimitNewStreamBlocked(t *testing.T) { + // "An endpoint that receives a frame with a stream ID exceeding the limit + // it has sent MUST treat this as a connection error of type STREAM_LIMIT_ERROR [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-4.6-3 + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + ctx := canceledContext() + tc := newTestConn(t, clientSide, + permissiveTransportParameters, + func(p *transportParameters) { + p.initialMaxStreamsBidi = 0 + p.initialMaxStreamsUni = 0 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + opening := runAsync(tc, func(ctx context.Context) (*Stream, error) { + return tc.conn.newLocalStream(ctx, styp) + }) + if _, err := opening.result(); err != errNotDone { + t.Fatalf("new stream blocked by limit: %v, want errNotDone", err) + } + tc.writeFrames(packetType1RTT, debugFrameMaxStreams{ + streamType: styp, + max: 1, + }) + if _, err := opening.result(); err != nil { + t.Fatalf("new stream not created after limit raised: %v", err) + } + if _, err := tc.conn.newLocalStream(ctx, styp); err == nil { + t.Fatalf("new stream blocked by raised limit: %v, want error", err) + } + }) +} + +func TestStreamLimitMaxStreamsDecreases(t *testing.T) { + // "MAX_STREAMS frames that do not increase the stream limit MUST be ignored." + // https://www.rfc-editor.org/rfc/rfc9000#section-4.6-4 + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + ctx := canceledContext() + tc := newTestConn(t, clientSide, + permissiveTransportParameters, + func(p *transportParameters) { + p.initialMaxStreamsBidi = 0 + p.initialMaxStreamsUni = 0 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.writeFrames(packetType1RTT, debugFrameMaxStreams{ + streamType: styp, + max: 2, + }) + tc.writeFrames(packetType1RTT, debugFrameMaxStreams{ + streamType: styp, + max: 1, + }) + if _, err := tc.conn.newLocalStream(ctx, styp); err != nil { + t.Fatalf("open stream 1, limit 2, got error: %v", err) + } + if _, err := tc.conn.newLocalStream(ctx, styp); err != nil { + t.Fatalf("open stream 2, limit 2, got error: %v", err) + } + if _, err := tc.conn.newLocalStream(ctx, styp); err == nil { + t.Fatalf("open stream 3, limit 2, got error: %v", err) + } + }) +} + +func TestStreamLimitViolated(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc := newTestConn(t, serverSide, + func(c *Config) { + if styp == bidiStream { + c.MaxBidiRemoteStreams = 10 + } else { + c.MaxUniRemoteStreams = 10 + } + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, styp, 9), + }) + tc.wantIdle("stream number 9 is within the limit") + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, styp, 10), + }) + tc.wantFrame("stream number 10 is beyond the limit", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errStreamLimit, + }, + ) + }) +} + +func TestStreamLimitImplicitStreams(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc := newTestConn(t, serverSide, + func(c *Config) { + c.MaxBidiRemoteStreams = 1 << 60 + c.MaxUniRemoteStreams = 1 << 60 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + if got, want := tc.sentTransportParameters.initialMaxStreamsBidi, int64(implicitStreamLimit); got != want { + t.Errorf("sent initial_max_streams_bidi = %v, want %v", got, want) + } + if got, want := tc.sentTransportParameters.initialMaxStreamsUni, int64(implicitStreamLimit); got != want { + t.Errorf("sent initial_max_streams_uni = %v, want %v", got, want) + } + + // Create stream 0. + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, styp, 0), + }) + tc.wantIdle("max streams not increased enough to send a new frame") + + // Create streams [0, implicitStreamLimit). + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, styp, implicitStreamLimit-1), + }) + tc.wantFrame("max streams increases to implicit stream limit", + packetType1RTT, debugFrameMaxStreams{ + streamType: styp, + max: 2 * implicitStreamLimit, + }) + + // Create a stream past the limit. + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, styp, 2*implicitStreamLimit), + }) + tc.wantFrame("stream is past the limit", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errStreamLimit, + }, + ) + }) +} + +func TestStreamLimitMaxStreamsTransportParameterTooLarge(t *testing.T) { + // "If a max_streams transport parameter [...] is received with + // a value greater than 2^60 [...] the connection MUST be closed + // immediately with a connection error of type TRANSPORT_PARAMETER_ERROR [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-4.6-2 + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc := newTestConn(t, serverSide, + func(p *transportParameters) { + if styp == bidiStream { + p.initialMaxStreamsBidi = 1<<60 + 1 + } else { + p.initialMaxStreamsUni = 1<<60 + 1 + } + }) + tc.writeFrames(packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.wantFrame("max streams transport parameter is too large", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errTransportParameter, + }, + ) + }) +} + +func TestStreamLimitMaxStreamsFrameTooLarge(t *testing.T) { + // "If [...] a MAX_STREAMS frame is received with a value + // greater than 2^60 [...] the connection MUST be closed immediately + // with a connection error [...] of type FRAME_ENCODING_ERROR [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-4.6-2 + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc := newTestConn(t, serverSide) + tc.handshake() + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetType1RTT, debugFrameMaxStreams{ + streamType: styp, + max: 1<<60 + 1, + }) + tc.wantFrame("MAX_STREAMS value is too large", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errFrameEncoding, + }, + ) + }) +} + +func TestStreamLimitSendUpdatesMaxStreams(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + ctx := canceledContext() + tc := newTestConn(t, serverSide, func(c *Config) { + if styp == uniStream { + c.MaxUniRemoteStreams = 4 + c.MaxBidiRemoteStreams = 0 + } else { + c.MaxUniRemoteStreams = 0 + c.MaxBidiRemoteStreams = 4 + } + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + var streams []*Stream + for i := 0; i < 4; i++ { + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, styp, int64(i)), + fin: true, + }) + s, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("AcceptStream = %v", err) + } + streams = append(streams, s) + } + streams[3].CloseContext(ctx) + if styp == bidiStream { + tc.wantFrame("stream is closed", + packetType1RTT, debugFrameStream{ + id: streams[3].id, + fin: true, + data: []byte{}, + }) + tc.writeAckForAll() + } + tc.wantFrame("closing a stream when peer is at limit immediately extends the limit", + packetType1RTT, debugFrameMaxStreams{ + streamType: styp, + max: 5, + }) + }) +} + +func TestStreamLimitStopSendingDoesNotUpdateMaxStreams(t *testing.T) { + tc, s := newTestConnAndRemoteStream(t, serverSide, bidiStream, func(c *Config) { + c.MaxBidiRemoteStreams = 1 + }) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + fin: true, + }) + s.CloseRead() + tc.writeFrames(packetType1RTT, debugFrameStopSending{ + id: s.id, + }) + tc.wantFrame("recieved STOP_SENDING, send RESET_STREAM", + packetType1RTT, debugFrameResetStream{ + id: s.id, + }) + tc.writeAckForAll() + tc.wantIdle("MAX_STREAMS is not extended until the user fully closes the stream") + s.CloseWrite() + tc.wantFrame("user closing the stream triggers MAX_STREAMS update", + packetType1RTT, debugFrameMaxStreams{ + streamType: bidiStream, + max: 2, + }) +} diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index e22e0432ef..fb21255a4f 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -649,7 +649,7 @@ func TestStreamReceiveUnblocksReader(t *testing.T) { // to the conn and expects a STREAM_STATE_ERROR. func testStreamSendFrameInvalidState(t *testing.T, f func(sid streamID) debugFrame) { testSides(t, "stream_not_created", func(t *testing.T, side connSide) { - tc := newTestConn(t, side) + tc := newTestConn(t, side, permissiveTransportParameters) tc.handshake() tc.writeFrames(packetType1RTT, f(newStreamID(side, bidiStream, 0))) tc.wantFrame("frame for local stream which has not been created", @@ -659,7 +659,7 @@ func testStreamSendFrameInvalidState(t *testing.T, f func(sid streamID) debugFra }) testSides(t, "uni_stream", func(t *testing.T, side connSide) { ctx := canceledContext() - tc := newTestConn(t, side) + tc := newTestConn(t, side, permissiveTransportParameters) tc.handshake() sid := newStreamID(side, uniStream, 0) s, err := tc.conn.NewSendOnlyStream(ctx) @@ -796,7 +796,7 @@ func TestStreamOffsetTooLarge(t *testing.T) { } func TestStreamReadFromWriteOnlyStream(t *testing.T) { - _, s := newTestConnAndLocalStream(t, serverSide, uniStream) + _, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) buf := make([]byte, 10) wantErr := "read from write-only stream" if n, err := s.Read(buf); err == nil || !strings.Contains(err.Error(), wantErr) { @@ -1112,7 +1112,7 @@ func TestStreamPeerResetFollowedByData(t *testing.T) { } func TestStreamResetInvalidCode(t *testing.T) { - tc, s := newTestConnAndLocalStream(t, serverSide, uniStream) + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) s.Reset(1 << 62) tc.wantFrame("reset with invalid code sends a RESET_STREAM anyway", packetType1RTT, debugFrameResetStream{ From c3c626055bf2f7ed06e40a48a40a2a46cf32785d Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 22 Aug 2023 16:44:32 -0700 Subject: [PATCH 049/168] quic: simplify gate operations Unify the waitAndLockDate and waitOnDone test hooks into a single waitUntil, which takes a func param reporting when the operation is done. Make gate.waitAndLock take a Context, drop waitAndLockContext. Everything that locks a gate passes a Context; there's no need for the context-free variant. Drop gate.waitWithLock, nothing used it. Add a connTestHooks parameter to gate.waitAndLock and queue.get. This parameter is an abstraction layer violation, but pretending we're not always passing it through is just unnecessary confusion. For golang/go#58547 Change-Id: Ifefb73b5a4ae0bac9822a5334117f3b3989f019e Reviewed-on: https://go-review.googlesource.com/c/net/+/524957 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn.go | 19 +++++---- internal/quic/conn_async_test.go | 54 ++++++------------------- internal/quic/conn_streams.go | 2 +- internal/quic/gate.go | 29 +++----------- internal/quic/gate_test.go | 69 +++++--------------------------- internal/quic/queue.go | 16 +------- internal/quic/queue_test.go | 12 +++--- internal/quic/stream.go | 4 +- internal/quic/stream_limits.go | 2 +- 9 files changed, 49 insertions(+), 158 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 642c507618..707b335bea 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -72,8 +72,7 @@ type connTestHooks interface { nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any) handleTLSEvent(tls.QUICEvent) newConnID(seq int64) ([]byte, error) - waitAndLockGate(ctx context.Context, g *gate) error - waitOnDone(ctx context.Context, ch <-chan struct{}) error + waitUntil(ctx context.Context, until func() bool) error } func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l connListener, hooks connTestHooks) (*Conn, error) { @@ -315,16 +314,16 @@ func (c *Conn) runOnLoop(f func(now time.Time, c *Conn)) error { return nil } -func (c *Conn) waitAndLockGate(ctx context.Context, g *gate) error { - if c.testHooks != nil { - return c.testHooks.waitAndLockGate(ctx, g) - } - return g.waitAndLockContext(ctx) -} - func (c *Conn) waitOnDone(ctx context.Context, ch <-chan struct{}) error { if c.testHooks != nil { - return c.testHooks.waitOnDone(ctx, ch) + return c.testHooks.waitUntil(ctx, func() bool { + select { + case <-ch: + return true + default: + } + return false + }) } // Check the channel before the context. // We always prefer to return results when available, diff --git a/internal/quic/conn_async_test.go b/internal/quic/conn_async_test.go index 5b419c4e54..dc2a57f9dd 100644 --- a/internal/quic/conn_async_test.go +++ b/internal/quic/conn_async_test.go @@ -83,10 +83,7 @@ func (a *asyncOp[T]) result() (v T, err error) { // A blockedAsync is a blocked async operation. type blockedAsync struct { - // Exactly one of these will be set, depending on the type of blocked operation. - g *gate - ch <-chan struct{} - + until func() bool // when this returns true, the operation is unblocked donec chan struct{} // closed when the operation is unblocked } @@ -130,31 +127,12 @@ func runAsync[T any](ts *testConn, f func(context.Context) (T, error)) *asyncOp[ return a } -// waitAndLockGate replaces gate.waitAndLock in tests. -func (as *asyncTestState) waitAndLockGate(ctx context.Context, g *gate) error { - if g.lockIfSet() { - // Gate can be acquired without blocking. +// waitUntil waits for a blocked async operation to complete. +// The operation is complete when the until func returns true. +func (as *asyncTestState) waitUntil(ctx context.Context, until func() bool) error { + if until() { return nil } - return as.block(ctx, &blockedAsync{ - g: g, - }) -} - -// waitOnDone replaces receiving from a chan struct{} in tests. -func (as *asyncTestState) waitOnDone(ctx context.Context, ch <-chan struct{}) error { - select { - case <-ch: - return nil // read without blocking - default: - } - return as.block(ctx, &blockedAsync{ - ch: ch, - }) -} - -// block waits for a blocked async operation to complete. -func (as *asyncTestState) block(ctx context.Context, b *blockedAsync) error { if err := ctx.Err(); err != nil { // Context has already expired. return err @@ -166,7 +144,10 @@ func (as *asyncTestState) block(ctx context.Context, b *blockedAsync) error { // which may have unpredictable results. panic("blocking async point with unexpected Context") } - b.donec = make(chan struct{}) + b := &blockedAsync{ + until: until, + donec: make(chan struct{}), + } // Record this as a pending blocking operation. as.mu.Lock() as.blocked[b] = struct{}{} @@ -188,20 +169,9 @@ func (as *asyncTestState) wakeAsync() bool { as.mu.Lock() var woken *blockedAsync for w := range as.blocked { - switch { - case w.g != nil: - if w.g.lockIfSet() { - woken = w - } - case w.ch != nil: - select { - case <-w.ch: - woken = w - default: - } - } - if woken != nil { - delete(as.blocked, woken) + if w.until() { + woken = w + delete(as.blocked, w) break } } diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index 716ed2d504..9ec2fa0d69 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -47,7 +47,7 @@ func (c *Conn) streamsInit() { // AcceptStream waits for and returns the next stream created by the peer. func (c *Conn) AcceptStream(ctx context.Context) (*Stream, error) { - return c.streams.queue.getWithHooks(ctx, c.testHooks) + return c.streams.queue.get(ctx, c.testHooks) } // NewStream creates a stream. diff --git a/internal/quic/gate.go b/internal/quic/gate.go index 27ab07a6f9..a2fb537115 100644 --- a/internal/quic/gate.go +++ b/internal/quic/gate.go @@ -47,13 +47,11 @@ func (g *gate) lock() (set bool) { } // waitAndLock waits until the condition is set before acquiring the gate. -func (g *gate) waitAndLock() { - <-g.set -} - -// waitAndLockContext waits until the condition is set before acquiring the gate. -// If the context expires, waitAndLockContext returns an error and does not acquire the gate. -func (g *gate) waitAndLockContext(ctx context.Context) error { +// If the context expires, waitAndLock returns an error and does not acquire the gate. +func (g *gate) waitAndLock(ctx context.Context, testHooks connTestHooks) error { + if testHooks != nil { + return testHooks.waitUntil(ctx, g.lockIfSet) + } select { case <-g.set: return nil @@ -67,23 +65,6 @@ func (g *gate) waitAndLockContext(ctx context.Context) error { } } -// waitWithLock releases an acquired gate until the condition is set. -// The caller must have previously acquired the gate. -// Upon return from waitWithLock, the gate will still be held. -// If waitWithLock returns nil, the condition is set. -func (g *gate) waitWithLock(ctx context.Context) error { - g.unlock(false) - err := g.waitAndLockContext(ctx) - if err != nil { - if g.lock() { - // The condition was set in between the context expiring - // and us reacquiring the gate. - err = nil - } - } - return err -} - // lockIfSet acquires the gate if and only if the condition is set. func (g *gate) lockIfSet() (acquired bool) { select { diff --git a/internal/quic/gate_test.go b/internal/quic/gate_test.go index 0122e39865..9e84a84bd6 100644 --- a/internal/quic/gate_test.go +++ b/internal/quic/gate_test.go @@ -41,37 +41,18 @@ func TestGateLockAndUnlock(t *testing.T) { } } -func TestGateWaitAndLock(t *testing.T) { - g := newGate() - set := false - go func() { - for i := 0; i < 3; i++ { - g.lock() - g.unlock(false) - time.Sleep(1 * time.Millisecond) - } - g.lock() - set = true - g.unlock(true) - }() - g.waitAndLock() - if !set { - t.Errorf("g.waitAndLock() returned before gate was set") - } -} - func TestGateWaitAndLockContext(t *testing.T) { g := newGate() - // waitAndLockContext is canceled + // waitAndLock is canceled ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(1 * time.Millisecond) cancel() }() - if err := g.waitAndLockContext(ctx); err != context.Canceled { - t.Errorf("g.waitAndLockContext() = %v, want context.Canceled", err) + if err := g.waitAndLock(ctx, nil); err != context.Canceled { + t.Errorf("g.waitAndLock() = %v, want context.Canceled", err) } - // waitAndLockContext succeeds + // waitAndLock succeeds set := false go func() { time.Sleep(1 * time.Millisecond) @@ -79,44 +60,16 @@ func TestGateWaitAndLockContext(t *testing.T) { set = true g.unlock(true) }() - if err := g.waitAndLockContext(context.Background()); err != nil { - t.Errorf("g.waitAndLockContext() = %v, want nil", err) + if err := g.waitAndLock(context.Background(), nil); err != nil { + t.Errorf("g.waitAndLock() = %v, want nil", err) } if !set { - t.Errorf("g.waitAndLockContext() returned before gate was set") + t.Errorf("g.waitAndLock() returned before gate was set") } g.unlock(true) - // waitAndLockContext succeeds when the gate is set and the context is canceled - if err := g.waitAndLockContext(ctx); err != nil { - t.Errorf("g.waitAndLockContext() = %v, want nil", err) - } -} - -func TestGateWaitWithLock(t *testing.T) { - g := newGate() - // waitWithLock is canceled - ctx, cancel := context.WithCancel(context.Background()) - go func() { - time.Sleep(1 * time.Millisecond) - cancel() - }() - g.lock() - if err := g.waitWithLock(ctx); err != context.Canceled { - t.Errorf("g.waitWithLock() = %v, want context.Canceled", err) - } - // waitWithLock succeeds - set := false - go func() { - g.lock() - set = true - g.unlock(true) - }() - time.Sleep(1 * time.Millisecond) - if err := g.waitWithLock(context.Background()); err != nil { - t.Errorf("g.waitWithLock() = %v, want nil", err) - } - if !set { - t.Errorf("g.waitWithLock() returned before gate was set") + // waitAndLock succeeds when the gate is set and the context is canceled + if err := g.waitAndLock(ctx, nil); err != nil { + t.Errorf("g.waitAndLock() = %v, want nil", err) } } @@ -138,5 +91,5 @@ func TestGateUnlockFunc(t *testing.T) { g.lock() defer g.unlockFunc(func() bool { return true }) }() - g.waitAndLock() + g.waitAndLock(context.Background(), nil) } diff --git a/internal/quic/queue.go b/internal/quic/queue.go index 489721a8af..7085e578b6 100644 --- a/internal/quic/queue.go +++ b/internal/quic/queue.go @@ -44,21 +44,9 @@ func (q *queue[T]) put(v T) bool { // get removes the first item from the queue, blocking until ctx is done, an item is available, // or the queue is closed. -func (q *queue[T]) get(ctx context.Context) (T, error) { - return q.getWithHooks(ctx, nil) -} - -// getWithHooks is get, but uses testHooks for locking when non-nil. -// This is a bit of an layer violation, but a simplification overall. -func (q *queue[T]) getWithHooks(ctx context.Context, testHooks connTestHooks) (T, error) { +func (q *queue[T]) get(ctx context.Context, testHooks connTestHooks) (T, error) { var zero T - var err error - if testHooks != nil { - err = testHooks.waitAndLockGate(ctx, &q.gate) - } else { - err = q.gate.waitAndLockContext(ctx) - } - if err != nil { + if err := q.gate.waitAndLock(ctx, testHooks); err != nil { return zero, err } defer q.unlock() diff --git a/internal/quic/queue_test.go b/internal/quic/queue_test.go index 8debeff110..d78216b0ec 100644 --- a/internal/quic/queue_test.go +++ b/internal/quic/queue_test.go @@ -18,7 +18,7 @@ func TestQueue(t *testing.T) { cancel() q := newQueue[int]() - if got, err := q.get(nonblocking); err != context.Canceled { + if got, err := q.get(nonblocking, nil); err != context.Canceled { t.Fatalf("q.get() = %v, %v, want nil, contex.Canceled", got, err) } @@ -28,13 +28,13 @@ func TestQueue(t *testing.T) { if !q.put(2) { t.Fatalf("q.put(2) = false, want true") } - if got, err := q.get(nonblocking); got != 1 || err != nil { + if got, err := q.get(nonblocking, nil); got != 1 || err != nil { t.Fatalf("q.get() = %v, %v, want 1, nil", got, err) } - if got, err := q.get(nonblocking); got != 2 || err != nil { + if got, err := q.get(nonblocking, nil); got != 2 || err != nil { t.Fatalf("q.get() = %v, %v, want 2, nil", got, err) } - if got, err := q.get(nonblocking); err != context.Canceled { + if got, err := q.get(nonblocking, nil); err != context.Canceled { t.Fatalf("q.get() = %v, %v, want nil, contex.Canceled", got, err) } @@ -42,7 +42,7 @@ func TestQueue(t *testing.T) { time.Sleep(1 * time.Millisecond) q.put(3) }() - if got, err := q.get(context.Background()); got != 3 || err != nil { + if got, err := q.get(context.Background(), nil); got != 3 || err != nil { t.Fatalf("q.get() = %v, %v, want 3, nil", got, err) } @@ -50,7 +50,7 @@ func TestQueue(t *testing.T) { t.Fatalf("q.put(2) = false, want true") } q.close(io.EOF) - if got, err := q.get(context.Background()); got != 0 || err != io.EOF { + if got, err := q.get(context.Background(), nil); got != 0 || err != io.EOF { t.Fatalf("q.get() = %v, %v, want 0, io.EOF", got, err) } if q.put(5) { diff --git a/internal/quic/stream.go b/internal/quic/stream.go index b759e406cc..d2f2cd7a3c 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -133,7 +133,7 @@ func (s *Stream) ReadContext(ctx context.Context, b []byte) (n int, err error) { return 0, errors.New("read from write-only stream") } // Wait until data is available. - if err := s.conn.waitAndLockGate(ctx, &s.ingate); err != nil { + if err := s.ingate.waitAndLock(ctx, s.conn.testHooks); err != nil { return 0, err } defer s.inUnlock() @@ -211,7 +211,7 @@ func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) s.outblocked.setUnsent() } s.outUnlock() - if err := s.conn.waitAndLockGate(ctx, &s.outgate); err != nil { + if err := s.outgate.waitAndLock(ctx, s.conn.testHooks); err != nil { return n, err } // Successfully returning from waitAndLockGate means we are no longer diff --git a/internal/quic/stream_limits.go b/internal/quic/stream_limits.go index 5ea7146b50..db3ab22929 100644 --- a/internal/quic/stream_limits.go +++ b/internal/quic/stream_limits.go @@ -31,7 +31,7 @@ func (lim *localStreamLimits) init() { // open creates a new local stream, blocking until MAX_STREAMS quota is available. func (lim *localStreamLimits) open(ctx context.Context, c *Conn) (num int64, err error) { // TODO: Send a STREAMS_BLOCKED when blocked. - if err := c.waitAndLockGate(ctx, &lim.gate); err != nil { + if err := lim.gate.waitAndLock(ctx, c.testHooks); err != nil { return 0, err } n := lim.opened From da5f9f7960a1ba4dc992f6f92e71dbe4f29d30cf Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 24 Aug 2023 11:26:58 -0700 Subject: [PATCH 050/168] quic: don't block Writes on stream-level flow control Data written to a stream can be sent to the peer in a STREAM frame only when: - congestion control window is available - pacing does not block sending - stream-level flow control is available - connection-level flow control is available There must be a pushback mechanism to limit the amount of locally buffered stream data, but I no longer believe the stream-level flow control needs to be part of that pushback. Using connection-level flow control (not yet implemented) to block stream Write calls is problematic, because it makes it difficult to fairly divide available send capacity between multiple streams. If writes to a stream consume connection-level flow control before we commit that data to the wire, it becomes very easy for one stream to starve others. It's confusing to use stream-level flow control to block Write calls, but not connection-level flow control. This will especially produce unexpected behavior if the recipient chooses to provide unlimited stream-level quota but limited connection-level quota. Change Stream.Write to only block writes based on the configured maximum send buffer size. We may now buffer data which cannot be immediately sent, but that was the case already when transmission is blocked by congestion control. In the future, we may want to make the stream buffer sizes adaptive in response to the amount of in-flight data. Rename Config.Stream*BufferSize to MaxStream*BufferSize, to allow for possibly adding a minimum size later. For golang/go#58547 Change-Id: I528a611fefb16b323776965c5b2ab5644035ed7a Reviewed-on: https://go-review.googlesource.com/c/net/+/524958 LUCI-TryBot-Result: Go LUCI Commit-Queue: Damien Neil Auto-Submit: Damien Neil Reviewed-by: Jonathan Amsterdam --- internal/quic/config.go | 16 ++-- internal/quic/config_test.go | 2 +- internal/quic/conn.go | 6 +- internal/quic/conn_loss_test.go | 4 +- internal/quic/conn_streams.go | 12 +-- internal/quic/conn_streams_test.go | 2 +- internal/quic/crypto_stream.go | 2 +- internal/quic/stream.go | 59 ++++++++------ internal/quic/stream_test.go | 120 +++++++++++++++++++++-------- internal/quic/tls_test.go | 1 + 10 files changed, 145 insertions(+), 79 deletions(-) diff --git a/internal/quic/config.go b/internal/quic/config.go index f78e811926..d68e2c7ade 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -30,17 +30,17 @@ type Config struct { // If negative, the limit is zero. MaxUniRemoteStreams int64 - // StreamReadBufferSize is the maximum amount of data sent by the peer that a + // MaxStreamReadBufferSize is the maximum amount of data sent by the peer that a // stream will buffer for reading. // If zero, the default value of 1MiB is used. // If negative, the limit is zero. - StreamReadBufferSize int64 + MaxStreamReadBufferSize int64 - // StreamWriteBufferSize is the maximum amount of data a stream will buffer for + // MaxStreamWriteBufferSize is the maximum amount of data a stream will buffer for // sending to the peer. // If zero, the default value of 1MiB is used. // If negative, the limit is zero. - StreamWriteBufferSize int64 + MaxStreamWriteBufferSize int64 } func configDefault(v, def, limit int64) int64 { @@ -62,10 +62,10 @@ func (c *Config) maxUniRemoteStreams() int64 { return configDefault(c.MaxUniRemoteStreams, 100, maxStreamsLimit) } -func (c *Config) streamReadBufferSize() int64 { - return configDefault(c.StreamReadBufferSize, 1<<20, maxVarint) +func (c *Config) maxStreamReadBufferSize() int64 { + return configDefault(c.MaxStreamReadBufferSize, 1<<20, maxVarint) } -func (c *Config) streamWriteBufferSize() int64 { - return configDefault(c.StreamWriteBufferSize, 1<<20, maxVarint) +func (c *Config) maxStreamWriteBufferSize() int64 { + return configDefault(c.MaxStreamWriteBufferSize, 1<<20, maxVarint) } diff --git a/internal/quic/config_test.go b/internal/quic/config_test.go index 8d67ef0d40..b99ffef649 100644 --- a/internal/quic/config_test.go +++ b/internal/quic/config_test.go @@ -17,7 +17,7 @@ func TestConfigTransportParameters(t *testing.T) { tc := newTestConn(t, clientSide, func(c *Config) { c.MaxBidiRemoteStreams = wantInitialMaxStreamsBidi c.MaxUniRemoteStreams = wantInitialMaxStreamsUni - c.StreamReadBufferSize = wantInitialMaxStreamData + c.MaxStreamReadBufferSize = wantInitialMaxStreamData }) tc.handshake() if tc.sentTransportParameters == nil { diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 707b335bea..117364f55d 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -117,9 +117,9 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. maxUDPPayloadSize: maxUDPPayloadSize, maxAckDelay: maxAckDelay, disableActiveMigration: true, - initialMaxStreamDataBidiLocal: config.streamReadBufferSize(), - initialMaxStreamDataBidiRemote: config.streamReadBufferSize(), - initialMaxStreamDataUni: config.streamReadBufferSize(), + initialMaxStreamDataBidiLocal: config.maxStreamReadBufferSize(), + initialMaxStreamDataBidiRemote: config.maxStreamReadBufferSize(), + initialMaxStreamDataUni: config.maxStreamReadBufferSize(), initialMaxStreamsBidi: c.streams.remoteLimit[bidiStream].max, initialMaxStreamsUni: c.streams.remoteLimit[uniStream].max, activeConnIDLimit: activeConnIDLimit, diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index d426aa6902..d8368f0219 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -297,7 +297,7 @@ func TestLostMaxStreamDataFrame(t *testing.T) { const maxWindowSize = 10 buf := make([]byte, maxWindowSize) tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { - c.StreamReadBufferSize = maxWindowSize + c.MaxStreamReadBufferSize = maxWindowSize }) // We send MAX_STREAM_DATA = 19. @@ -339,7 +339,7 @@ func TestLostMaxStreamDataFrameAfterStreamFinReceived(t *testing.T) { const maxWindowSize = 10 buf := make([]byte, maxWindowSize) tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { - c.StreamReadBufferSize = maxWindowSize + c.MaxStreamReadBufferSize = maxWindowSize }) tc.writeFrames(packetType1RTT, debugFrameStream{ diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index 9ec2fa0d69..5816d49f32 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -76,11 +76,11 @@ func (c *Conn) newLocalStream(ctx context.Context, styp streamType) (*Stream, er } s := newStream(c, newStreamID(c.side, styp, num)) - s.outmaxbuf = c.config.streamWriteBufferSize() + s.outmaxbuf = c.config.maxStreamWriteBufferSize() s.outwin = c.streams.peerInitialMaxStreamDataRemote[styp] if styp == bidiStream { - s.inmaxbuf = c.config.streamReadBufferSize() - s.inwin = c.config.streamReadBufferSize() + s.inmaxbuf = c.config.maxStreamReadBufferSize() + s.inwin = c.config.maxStreamReadBufferSize() } s.inUnlock() s.outUnlock() @@ -170,10 +170,10 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) } s = newStream(c, id) - s.inmaxbuf = c.config.streamReadBufferSize() - s.inwin = c.config.streamReadBufferSize() + s.inmaxbuf = c.config.maxStreamReadBufferSize() + s.inwin = c.config.maxStreamReadBufferSize() if id.streamType() == bidiStream { - s.outmaxbuf = c.config.streamWriteBufferSize() + s.outmaxbuf = c.config.maxStreamWriteBufferSize() s.outwin = c.streams.peerInitialMaxStreamDataBidiLocal } s.inUnlock() diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go index ab1df1a248..8ae007ccc7 100644 --- a/internal/quic/conn_streams_test.go +++ b/internal/quic/conn_streams_test.go @@ -207,7 +207,7 @@ func TestStreamsWriteQueueFairness(t *testing.T) { p.initialMaxData = 1<<62 - 1 p.initialMaxStreamDataBidiRemote = dataLen }, func(c *Config) { - c.StreamWriteBufferSize = dataLen + c.MaxStreamWriteBufferSize = dataLen }) tc.handshake() tc.ignoreFrame(frameTypeAck) diff --git a/internal/quic/crypto_stream.go b/internal/quic/crypto_stream.go index 75dea87d0d..8aa8f7b828 100644 --- a/internal/quic/crypto_stream.go +++ b/internal/quic/crypto_stream.go @@ -118,7 +118,7 @@ func (s *cryptoStream) ackOrLoss(start, end int64, fate packetFate) { // copy the data it wants into position. func (s *cryptoStream) dataToSend(pto bool, f func(off, size int64) (sent int64)) { for { - off, size := dataToSend(s.out, s.outunsent, s.outacked, pto) + off, size := dataToSend(s.out.start, s.out.end, s.outunsent, s.outacked, pto) if size == 0 { return } diff --git a/internal/quic/stream.go b/internal/quic/stream.go index d2f2cd7a3c..fbc36334ba 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -202,14 +202,7 @@ func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) // We exit the loop after writing all data, so on subsequent passes through // the loop we are always write blocked. if len(b) > 0 && !canWrite { - // We're blocked, either by flow control or by our own buffer limit. - // We either need the peer to extend our flow control window, - // or ack some of our outstanding packets. - if s.out.end == s.outwin { - // We're blocked by flow control. - // Send a STREAM_DATA_BLOCKED frame to let the peer know. - s.outblocked.setUnsent() - } + // Our send buffer is full. Wait for the peer to ack some data. s.outUnlock() if err := s.outgate.waitAndLock(ctx, s.conn.testHooks); err != nil { return n, err @@ -233,18 +226,24 @@ func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) if len(b) == 0 { break } - s.outblocked.clear() - // Write limit is min(our own buffer limit, the peer-provided flow control window). + // Write limit is our send buffer limit. // This is a stream offset. - lim := min(s.out.start+s.outmaxbuf, s.outwin) + lim := s.out.start + s.outmaxbuf // Amount to write is min(the full buffer, data up to the write limit). // This is a number of bytes. nn := min(int64(len(b)), lim-s.out.end) // Copy the data into the output buffer and mark it as unsent. - s.outunsent.add(s.out.end, s.out.end+nn) + if s.out.end <= s.outwin { + s.outunsent.add(s.out.end, min(s.out.end+nn, s.outwin)) + } s.out.writeAt(b[:nn], s.out.end) b = b[nn:] n += int(nn) + if s.out.end > s.outwin { + // We're blocked by flow control. + // Send a STREAM_DATA_BLOCKED frame to let the peer know. + s.outblocked.set() + } // If we have bytes left to send, we're blocked. canWrite = false } @@ -425,8 +424,8 @@ func (s *Stream) outUnlockNoQueue() streamState { } } } - lim := min(s.out.start+s.outmaxbuf, s.outwin) - canWrite := lim > s.out.end || // available flow control + lim := s.out.start + s.outmaxbuf + canWrite := lim > s.out.end || // available send buffer s.outclosed.isSet() || // closed locally s.outreset.isSet() // reset locally defer s.outgate.unlock(canWrite) @@ -533,7 +532,19 @@ func (s *Stream) handleStopSending(code uint64) error { func (s *Stream) handleMaxStreamData(maxStreamData int64) error { s.outgate.lock() defer s.outUnlock() - s.outwin = max(maxStreamData, s.outwin) + if maxStreamData <= s.outwin { + return nil + } + if s.out.end > s.outwin { + s.outunsent.add(s.outwin, min(maxStreamData, s.out.end)) + } + s.outwin = maxStreamData + if s.out.end > s.outwin { + // We've still got more data than flow control window. + s.outblocked.setUnsent() + } else { + s.outblocked.clear() + } return nil } @@ -635,7 +646,7 @@ func (s *Stream) appendOutFramesLocked(w *packetWriter, pnum packetNumber, pto b if s.outreset.isSet() { // RESET_STREAM if s.outreset.shouldSendPTO(pto) { - if !w.appendResetStreamFrame(s.id, s.outresetcode, s.out.end) { + if !w.appendResetStreamFrame(s.id, s.outresetcode, min(s.outwin, s.out.end)) { return false } s.outreset.setSent(pnum) @@ -645,15 +656,15 @@ func (s *Stream) appendOutFramesLocked(w *packetWriter, pnum packetNumber, pto b } if s.outblocked.shouldSendPTO(pto) { // STREAM_DATA_BLOCKED - if !w.appendStreamDataBlockedFrame(s.id, s.out.end) { + if !w.appendStreamDataBlockedFrame(s.id, s.outwin) { return false } s.outblocked.setSent(pnum) s.frameOpensStream(pnum) } - // STREAM for { - off, size := dataToSend(s.out, s.outunsent, s.outacked, pto) + // STREAM + off, size := dataToSend(min(s.out.start, s.outwin), min(s.out.end, s.outwin), s.outunsent, s.outacked, pto) fin := s.outclosed.isSet() && off+size == s.out.end shouldSend := size > 0 || // have data to send s.outopened.shouldSendPTO(pto) || // should open the stream @@ -691,7 +702,7 @@ func (s *Stream) frameOpensStream(pnum packetNumber) { } // dataToSend returns the next range of data to send in a STREAM or CRYPTO_STREAM. -func dataToSend(out pipe, outunsent, outacked rangeset[int64], pto bool) (start, size int64) { +func dataToSend(start, end int64, outunsent, outacked rangeset[int64], pto bool) (sendStart, size int64) { switch { case pto: // On PTO, resend unacked data that fits in the probe packet. @@ -702,14 +713,14 @@ func dataToSend(out pipe, outunsent, outacked rangeset[int64], pto bool) (start, // This may miss unacked data starting after that acked byte, // but avoids resending data the peer has acked. for _, r := range outacked { - if r.start > out.start { - return out.start, r.start - out.start + if r.start > start { + return start, r.start - start } } - return out.start, out.end - out.start + return start, end - start case outunsent.numRanges() > 0: return outunsent.min(), outunsent[0].size() default: - return out.end, 0 + return end, 0 } } diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index fb21255a4f..b014852871 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -18,6 +18,67 @@ import ( "testing" ) +func TestStreamWriteBlockedByOutputBuffer(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + ctx := canceledContext() + want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + const writeBufferSize = 4 + tc := newTestConn(t, clientSide, permissiveTransportParameters, func(c *Config) { + c.MaxStreamWriteBufferSize = writeBufferSize + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + s, err := tc.conn.newLocalStream(ctx, styp) + if err != nil { + t.Fatal(err) + } + + // Non-blocking write. + n, err := s.WriteContext(ctx, want) + if n != writeBufferSize || err != context.Canceled { + t.Fatalf("s.WriteContext() = %v, %v; want %v, context.Canceled", n, err, writeBufferSize) + } + tc.wantFrame("first write buffer of data sent", + packetType1RTT, debugFrameStream{ + id: s.id, + data: want[:writeBufferSize], + }) + off := int64(writeBufferSize) + + // Blocking write, which must wait for buffer space. + w := runAsync(tc, func(ctx context.Context) (int, error) { + return s.WriteContext(ctx, want[writeBufferSize:]) + }) + tc.wantIdle("write buffer is full, no more data can be sent") + + // The peer's ack of the STREAM frame allows progress. + tc.writeAckForAll() + tc.wantFrame("second write buffer of data sent", + packetType1RTT, debugFrameStream{ + id: s.id, + off: off, + data: want[off:][:writeBufferSize], + }) + off += writeBufferSize + tc.wantIdle("write buffer is full, no more data can be sent") + + // The peer's ack of the second STREAM frame allows sending the remaining data. + tc.writeAckForAll() + tc.wantFrame("remaining data sent", + packetType1RTT, debugFrameStream{ + id: s.id, + off: off, + data: want[off:], + }) + + if n, err := w.result(); n != len(want)-writeBufferSize || err != nil { + t.Fatalf("s.WriteContext() = %v, %v; want %v, nil", + len(want)-writeBufferSize, err, writeBufferSize) + } + }) +} + func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) { testStreamTypes(t, "", func(t *testing.T, styp streamType) { ctx := canceledContext() @@ -30,14 +91,15 @@ func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) { tc.handshake() tc.ignoreFrame(frameTypeAck) - // Non-blocking write with no flow control. s, err := tc.conn.newLocalStream(ctx, styp) if err != nil { t.Fatal(err) } - _, err = s.WriteContext(ctx, want) - if err != context.Canceled { - t.Fatalf("write to stream with no flow control: err = %v, want context.Canceled", err) + + // Data is written to the stream output buffer, but we have no flow control. + _, err = s.WriteContext(ctx, want[:1]) + if err != nil { + t.Fatalf("write with available output buffer: unexpected error: %v", err) } tc.wantFrame("write blocked by flow control triggers a STREAM_DATA_BLOCKED frame", packetType1RTT, debugFrameStreamDataBlocked{ @@ -45,15 +107,14 @@ func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) { max: 0, }) - // Blocking write waiting for flow control. - w := runAsync(tc, func(ctx context.Context) (int, error) { - return s.WriteContext(ctx, want) - }) - tc.wantFrame("second blocked write triggers another STREAM_DATA_BLOCKED", - packetType1RTT, debugFrameStreamDataBlocked{ - id: s.id, - max: 0, - }) + // Write more data. + _, err = s.WriteContext(ctx, want[1:]) + if err != nil { + t.Fatalf("write with available output buffer: unexpected error: %v", err) + } + tc.wantIdle("adding more blocked data does not trigger another STREAM_DATA_BLOCKED") + + // Provide some flow control window. tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ id: s.id, max: 4, @@ -69,6 +130,7 @@ func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) { data: want[:4], }) + // Provide more flow control window. tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ id: s.id, max: int64(len(want)), @@ -79,10 +141,6 @@ func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) { off: 4, data: want[4:], }) - n, err := w.result() - if n != len(want) || err != nil { - t.Errorf("Write() = %v, %v; want %v, nil", n, err, len(want)) - } }) } @@ -169,7 +227,7 @@ func TestStreamWriteBlockedByWriteBufferLimit(t *testing.T) { p.initialMaxStreamDataBidiRemote = 1 << 20 p.initialMaxStreamDataUni = 1 << 20 }, func(c *Config) { - c.StreamWriteBufferSize = maxWriteBuffer + c.MaxStreamWriteBufferSize = maxWriteBuffer }) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -391,7 +449,7 @@ func TestStreamReceiveExtendsStreamWindow(t *testing.T) { const maxWindowSize = 20 ctx := canceledContext() tc := newTestConn(t, serverSide, func(c *Config) { - c.StreamReadBufferSize = maxWindowSize + c.MaxStreamReadBufferSize = maxWindowSize }) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -448,7 +506,7 @@ func TestStreamReceiveViolatesStreamDataLimit(t *testing.T) { size: 2, }} { tc := newTestConn(t, serverSide, func(c *Config) { - c.StreamReadBufferSize = maxStreamData + c.MaxStreamReadBufferSize = maxStreamData }) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -473,7 +531,7 @@ func TestStreamReceiveDuplicateDataDoesNotViolateLimits(t *testing.T) { const maxData = 10 tc := newTestConn(t, serverSide, func(c *Config) { // TODO: Add connection-level maximum data here as well. - c.StreamReadBufferSize = maxData + c.MaxStreamReadBufferSize = maxData }) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -557,7 +615,7 @@ func TestStreamFinalSizePastMaxStreamData(t *testing.T) { finalSizeTest(t, errFlowControl, func(tc *testConn, sid streamID) (finalSize int64) { return 11 }, func(c *Config) { - c.StreamReadBufferSize = 10 + c.MaxStreamReadBufferSize = 10 }) } @@ -868,16 +926,15 @@ func TestStreamWriteToClosedStream(t *testing.T) { } func TestStreamResetBlockedStream(t *testing.T) { - tc, s := newTestConnAndLocalStream(t, serverSide, bidiStream, func(p *transportParameters) { - p.initialMaxStreamsBidi = 1 - p.initialMaxData = 1 << 20 - p.initialMaxStreamDataBidiRemote = 4 - }) + tc, s := newTestConnAndLocalStream(t, serverSide, bidiStream, permissiveTransportParameters, + func(c *Config) { + c.MaxStreamWriteBufferSize = 4 + }) tc.ignoreFrame(frameTypeStreamDataBlocked) writing := runAsync(tc, func(ctx context.Context) (int, error) { return s.WriteContext(ctx, []byte{0, 1, 2, 3, 4, 5, 6, 7}) }) - tc.wantFrame("stream writes data until blocked by flow control", + tc.wantFrame("stream writes data until write buffer fills", packetType1RTT, debugFrameStream{ id: s.id, off: 0, @@ -894,11 +951,8 @@ func TestStreamResetBlockedStream(t *testing.T) { if n, err := writing.result(); n != 4 || !strings.Contains(err.Error(), wantErr) { t.Errorf("s.Write() interrupted by Reset: %v, %q; want 4, %q", n, err, wantErr) } - tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ - id: s.id, - max: 1 << 20, - }) - tc.wantIdle("flow control is available, but stream has been reset") + tc.writeAckForAll() + tc.wantIdle("buffer space is available, but stream has been reset") s.Reset(100) tc.wantIdle("resetting stream a second time has no effect") if n, err := s.Write([]byte{}); err == nil || !strings.Contains(err.Error(), wantErr) { diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 180ea8beec..0f22f4fb31 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -21,6 +21,7 @@ func (tc *testConn) handshake() { if *testVV { *testVV = false defer func() { + tc.t.Helper() *testVV = true tc.t.Logf("performed connection handshake") }() From 5401f7662e2c7ceb600750c75c0fd16dff605f68 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 6 Sep 2023 16:10:21 -0700 Subject: [PATCH 051/168] quic: test lost bidi MAX_STREAMS frame handling Test a previously untested path in lost frame handling. For golang/go#58547 Change-Id: I2a6fab795aa76db15b511bc48b9c14cd549626dd Reviewed-on: https://go-review.googlesource.com/c/net/+/526715 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_loss_test.go | 68 +++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index d8368f0219..f74ec7e64d 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -372,36 +372,46 @@ func TestLostMaxStreamsFrameMostRecent(t *testing.T) { // "[...] an updated value is sent when a packet containing the // most recent MAX_STREAMS for a stream type frame is declared lost [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.9 - lostFrameTest(t, func(t *testing.T, pto bool) { - ctx := canceledContext() - tc := newTestConn(t, serverSide, func(c *Config) { - c.MaxUniRemoteStreams = 1 - }) - tc.handshake() - tc.ignoreFrame(frameTypeAck) - tc.writeFrames(packetType1RTT, debugFrameStream{ - id: newStreamID(clientSide, uniStream, 0), - fin: true, + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + lostFrameTest(t, func(t *testing.T, pto bool) { + ctx := canceledContext() + tc := newTestConn(t, serverSide, func(c *Config) { + c.MaxUniRemoteStreams = 1 + c.MaxBidiRemoteStreams = 1 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, styp, 0), + fin: true, + }) + s, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("AcceptStream() = %v", err) + } + s.CloseContext(ctx) + if styp == bidiStream { + tc.wantFrame("stream is closed", + packetType1RTT, debugFrameStream{ + id: s.id, + data: []byte{}, + fin: true, + }) + tc.writeAckForAll() + } + tc.wantFrame("closing stream updates peer's MAX_STREAMS", + packetType1RTT, debugFrameMaxStreams{ + streamType: styp, + max: 2, + }) + + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("lost MAX_STREAMS is resent", + packetType1RTT, debugFrameMaxStreams{ + streamType: styp, + max: 2, + }) }) - s, err := tc.conn.AcceptStream(ctx) - if err != nil { - t.Fatalf("AcceptStream() = %v", err) - } - if err := s.CloseContext(ctx); err != nil { - t.Fatalf("stream.Close() = %v", err) - } - tc.wantFrame("closing stream updates peer's MAX_STREAMS", - packetType1RTT, debugFrameMaxStreams{ - streamType: uniStream, - max: 2, - }) - - tc.triggerLossOrPTO(packetType1RTT, pto) - tc.wantFrame("lost MAX_STREAMS is resent", - packetType1RTT, debugFrameMaxStreams{ - streamType: uniStream, - max: 2, - }) }) } From 044c3080420519f6e4588f90c82d1311a55688aa Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 7 Sep 2023 12:17:02 -0700 Subject: [PATCH 052/168] quic: check for packet overflow when writing MAX_STREAMS Return a bool from remoteStreamLimits.appendFrame indicating whether the packet had space for all appended frames, matching the pattern of other functions that write frames. For golang/go#58547 Change-Id: If21d1b192cea210b94a0c6ce996a73fe43b3babe Reviewed-on: https://go-review.googlesource.com/c/net/+/526755 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_streams.go | 10 ++++++++-- internal/quic/stream_limits.go | 16 ++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index 5816d49f32..76e9bf94c5 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -212,8 +212,14 @@ func (c *Conn) queueStreamForSend(s *Stream) { // It returns true if no more frames need appending, // false if not everything fit in the current packet. func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) bool { - c.streams.remoteLimit[uniStream].appendFrame(w, uniStream, pnum, pto) - c.streams.remoteLimit[bidiStream].appendFrame(w, bidiStream, pnum, pto) + // MAX_STREAM_DATA + if !c.streams.remoteLimit[uniStream].appendFrame(w, uniStream, pnum, pto) { + return false + } + if !c.streams.remoteLimit[bidiStream].appendFrame(w, bidiStream, pnum, pto) { + return false + } + if pto { return c.appendStreamFramesPTO(w, pnum) } diff --git a/internal/quic/stream_limits.go b/internal/quic/stream_limits.go index db3ab22929..6eda7883b9 100644 --- a/internal/quic/stream_limits.go +++ b/internal/quic/stream_limits.go @@ -98,12 +98,16 @@ func (lim *remoteStreamLimits) maybeUpdateMax() { } } -// appendFrame appends a MAX_DATA frame if necessary. -func (lim *remoteStreamLimits) appendFrame(w *packetWriter, typ streamType, pnum packetNumber, pto bool) { - if !lim.sendMax.shouldSendPTO(pto) { - return - } - if w.appendMaxStreamsFrame(typ, lim.max) { +// appendFrame appends a MAX_STREAMS frame to the current packet, if necessary. +// +// It returns true if no more frames need appending, +// false if not everything fit in the current packet. +func (lim *remoteStreamLimits) appendFrame(w *packetWriter, typ streamType, pnum packetNumber, pto bool) bool { + if lim.sendMax.shouldSendPTO(pto) { + if !w.appendMaxStreamsFrame(typ, lim.max) { + return false + } lim.sendMax.setSent(pnum) } + return true } From 217377b643f451a5d5ae764f65253e8c3e164ed2 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 15 Aug 2023 12:31:16 -0400 Subject: [PATCH 053/168] quic: inbound connection-level flow control Track the peer's connection level flow control window. Update the window with MAX_DATA frames as data is consumed by the user. Adjust shouldUpdateFlowControl so that we can use the same algorithm for both stream-level and connection-level flow control. The new algorithm is to send an update when doing so extends the peer's window by at least 1/8 of the maximum window size. For golang/go#58547 Change-Id: I2d8d82d06f0cb4b2ac25b3396c3cf4126a96e9cc Reviewed-on: https://go-review.googlesource.com/c/net/+/526716 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/config.go | 10 ++ internal/quic/config_test.go | 5 + internal/quic/conn.go | 1 + internal/quic/conn_flow.go | 111 +++++++++++++++++++ internal/quic/conn_flow_test.go | 186 ++++++++++++++++++++++++++++++++ internal/quic/conn_loss.go | 2 + internal/quic/conn_loss_test.go | 48 ++++++++- internal/quic/conn_streams.go | 9 ++ internal/quic/stream.go | 23 +++- 9 files changed, 386 insertions(+), 9 deletions(-) create mode 100644 internal/quic/conn_flow.go create mode 100644 internal/quic/conn_flow_test.go diff --git a/internal/quic/config.go b/internal/quic/config.go index d68e2c7ade..b390d6911e 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -41,6 +41,12 @@ type Config struct { // If zero, the default value of 1MiB is used. // If negative, the limit is zero. MaxStreamWriteBufferSize int64 + + // MaxConnReadBufferSize is the maximum amount of data sent by the peer that a + // connection will buffer for reading, across all streams. + // If zero, the default value of 1MiB is used. + // If negative, the limit is zero. + MaxConnReadBufferSize int64 } func configDefault(v, def, limit int64) int64 { @@ -69,3 +75,7 @@ func (c *Config) maxStreamReadBufferSize() int64 { func (c *Config) maxStreamWriteBufferSize() int64 { return configDefault(c.MaxStreamWriteBufferSize, 1<<20, maxVarint) } + +func (c *Config) maxConnReadBufferSize() int64 { + return configDefault(c.MaxConnReadBufferSize, 1<<20, maxVarint) +} diff --git a/internal/quic/config_test.go b/internal/quic/config_test.go index b99ffef649..d292854f54 100644 --- a/internal/quic/config_test.go +++ b/internal/quic/config_test.go @@ -10,6 +10,7 @@ import "testing" func TestConfigTransportParameters(t *testing.T) { const ( + wantInitialMaxData = int64(1) wantInitialMaxStreamData = int64(2) wantInitialMaxStreamsBidi = int64(3) wantInitialMaxStreamsUni = int64(4) @@ -18,12 +19,16 @@ func TestConfigTransportParameters(t *testing.T) { c.MaxBidiRemoteStreams = wantInitialMaxStreamsBidi c.MaxUniRemoteStreams = wantInitialMaxStreamsUni c.MaxStreamReadBufferSize = wantInitialMaxStreamData + c.MaxConnReadBufferSize = wantInitialMaxData }) tc.handshake() if tc.sentTransportParameters == nil { t.Fatalf("conn didn't send transport parameters during handshake") } p := tc.sentTransportParameters + if got, want := p.initialMaxData, wantInitialMaxData; got != want { + t.Errorf("initial_max_data = %v, want %v", got, want) + } if got, want := p.initialMaxStreamDataBidiLocal, wantInitialMaxStreamData; got != want { t.Errorf("initial_max_stream_data_bidi_local = %v, want %v", got, want) } diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 117364f55d..0ab6f69477 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -117,6 +117,7 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. maxUDPPayloadSize: maxUDPPayloadSize, maxAckDelay: maxAckDelay, disableActiveMigration: true, + initialMaxData: config.maxConnReadBufferSize(), initialMaxStreamDataBidiLocal: config.maxStreamReadBufferSize(), initialMaxStreamDataBidiRemote: config.maxStreamReadBufferSize(), initialMaxStreamDataUni: config.maxStreamReadBufferSize(), diff --git a/internal/quic/conn_flow.go b/internal/quic/conn_flow.go new file mode 100644 index 0000000000..790210b4a9 --- /dev/null +++ b/internal/quic/conn_flow.go @@ -0,0 +1,111 @@ +// 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 ( + "sync/atomic" + "time" +) + +// connInflow tracks connection-level flow control for data sent by the peer to us. +// +// There are four byte offsets of significance in the stream of data received from the peer, +// each >= to the previous: +// +// - bytes read by the user +// - bytes received from the peer +// - limit sent to the peer in a MAX_DATA frame +// - potential new limit to sent to the peer +// +// We maintain a flow control window, so as bytes are read by the user +// the potential limit is extended correspondingly. +// +// We keep an atomic counter of bytes read by the user and not yet applied to the +// potential limit (credit). When this count grows large enough, we update the +// new limit to send and mark that we need to send a new MAX_DATA frame. +type connInflow struct { + sent sentVal // set when we need to send a MAX_DATA update to the peer + usedLimit int64 // total bytes sent by the peer, must be less than sentLimit + sentLimit int64 // last MAX_DATA sent to the peer + newLimit int64 // new MAX_DATA to send + + credit atomic.Int64 // bytes read but not yet applied to extending the flow-control window +} + +func (c *Conn) inflowInit() { + // The initial MAX_DATA limit is sent as a transport parameter. + c.streams.inflow.sentLimit = c.config.maxConnReadBufferSize() + c.streams.inflow.newLimit = c.streams.inflow.sentLimit +} + +// handleStreamBytesReadOffLoop records that the user has consumed bytes from a stream. +// We may extend the peer's flow control window. +// +// This is called indirectly by the user, via Read or CloseRead. +func (c *Conn) handleStreamBytesReadOffLoop(n int64) { + if c.shouldUpdateFlowControl(c.streams.inflow.credit.Add(n)) { + // We should send a MAX_DATA update to the peer. + // Record this on the Conn's main loop. + c.sendMsg(func(now time.Time, c *Conn) { + c.sendMaxDataUpdate() + }) + } +} + +// handleStreamBytesReadOnLoop extends the peer's flow control window after +// data has been discarded due to a RESET_STREAM frame. +// +// This is called on the conn's loop. +func (c *Conn) handleStreamBytesReadOnLoop(n int64) { + if c.shouldUpdateFlowControl(c.streams.inflow.credit.Add(n)) { + c.sendMaxDataUpdate() + } +} + +func (c *Conn) sendMaxDataUpdate() { + c.streams.inflow.sent.setUnsent() + // Apply current credit to the limit. + // We don't strictly need to do this here + // since appendMaxDataFrame will do so as well, + // but this avoids redundant trips down this path + // if the MAX_DATA frame doesn't go out right away. + c.streams.inflow.newLimit += c.streams.inflow.credit.Swap(0) +} + +func (c *Conn) shouldUpdateFlowControl(credit int64) bool { + return shouldUpdateFlowControl(c.config.maxConnReadBufferSize(), credit) +} + +// handleStreamBytesReceived records that the peer has sent us stream data. +func (c *Conn) handleStreamBytesReceived(n int64) error { + c.streams.inflow.usedLimit += n + if c.streams.inflow.usedLimit > c.streams.inflow.sentLimit { + return localTransportError(errFlowControl) + } + return nil +} + +// appendMaxDataFrame appends a MAX_DATA frame to the current packet. +// +// It returns true if no more frames need appending, +// false if it could not fit a frame in the current packet. +func (c *Conn) appendMaxDataFrame(w *packetWriter, pnum packetNumber, pto bool) bool { + if c.streams.inflow.sent.shouldSendPTO(pto) { + // Add any unapplied credit to the new limit now. + c.streams.inflow.newLimit += c.streams.inflow.credit.Swap(0) + if !w.appendMaxDataFrame(c.streams.inflow.newLimit) { + return false + } + c.streams.inflow.sent.setSent(pnum) + } + return true +} + +// ackOrLossMaxData records the fate of a MAX_DATA frame. +func (c *Conn) ackOrLossMaxData(pnum packetNumber, fate packetFate) { + c.streams.inflow.sent.ackLatestOrLoss(pnum, fate) +} diff --git a/internal/quic/conn_flow_test.go b/internal/quic/conn_flow_test.go new file mode 100644 index 0000000000..f01a7389c4 --- /dev/null +++ b/internal/quic/conn_flow_test.go @@ -0,0 +1,186 @@ +// 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" + +func TestConnInflowReturnOnRead(t *testing.T) { + ctx := canceledContext() + tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { + c.MaxConnReadBufferSize = 64 + }) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + data: make([]byte, 64), + }) + const readSize = 8 + if n, err := s.ReadContext(ctx, make([]byte, readSize)); n != readSize || err != nil { + t.Fatalf("s.Read() = %v, %v; want %v, nil", n, err, readSize) + } + tc.wantFrame("available window increases, send a MAX_DATA", + packetType1RTT, debugFrameMaxData{ + max: 64 + readSize, + }) + if n, err := s.ReadContext(ctx, make([]byte, 64)); n != 64-readSize || err != nil { + t.Fatalf("s.Read() = %v, %v; want %v, nil", n, err, 64-readSize) + } + tc.wantFrame("available window increases, send a MAX_DATA", + packetType1RTT, debugFrameMaxData{ + max: 128, + }) +} + +func TestConnInflowReturnOnClose(t *testing.T) { + tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { + c.MaxConnReadBufferSize = 64 + }) + tc.ignoreFrame(frameTypeStopSending) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + data: make([]byte, 64), + }) + s.CloseRead() + tc.wantFrame("closing stream updates connection-level flow control", + packetType1RTT, debugFrameMaxData{ + max: 128, + }) +} + +func TestConnInflowReturnOnReset(t *testing.T) { + tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { + c.MaxConnReadBufferSize = 64 + }) + tc.ignoreFrame(frameTypeStopSending) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + data: make([]byte, 32), + }) + tc.writeFrames(packetType1RTT, debugFrameResetStream{ + id: s.id, + finalSize: 64, + }) + s.CloseRead() + tc.wantFrame("receiving stream reseet updates connection-level flow control", + packetType1RTT, debugFrameMaxData{ + max: 128, + }) +} + +func TestConnInflowStreamViolation(t *testing.T) { + tc := newTestConn(t, serverSide, func(c *Config) { + c.MaxConnReadBufferSize = 100 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + // Total MAX_DATA consumed: 50 + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, bidiStream, 0), + data: make([]byte, 50), + }) + // Total MAX_DATA consumed: 80 + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, uniStream, 0), + off: 20, + data: make([]byte, 10), + }) + // Total MAX_DATA consumed: 100 + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, bidiStream, 0), + off: 70, + fin: true, + }) + // This stream has already consumed quota for these bytes. + // Total MAX_DATA consumed: 100 + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, uniStream, 0), + data: make([]byte, 20), + }) + tc.wantIdle("peer has consumed all MAX_DATA quota") + + // Total MAX_DATA consumed: 101 + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, bidiStream, 2), + data: make([]byte, 1), + }) + tc.wantFrame("peer violates MAX_DATA limit", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errFlowControl, + }) +} + +func TestConnInflowResetViolation(t *testing.T) { + tc := newTestConn(t, serverSide, func(c *Config) { + c.MaxConnReadBufferSize = 100 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, bidiStream, 0), + data: make([]byte, 100), + }) + tc.wantIdle("peer has consumed all MAX_DATA quota") + + tc.writeFrames(packetType1RTT, debugFrameResetStream{ + id: newStreamID(clientSide, uniStream, 0), + finalSize: 0, + }) + tc.wantIdle("stream reset does not consume MAX_DATA quota, no error") + + tc.writeFrames(packetType1RTT, debugFrameResetStream{ + id: newStreamID(clientSide, uniStream, 1), + finalSize: 1, + }) + tc.wantFrame("RESET_STREAM final size violates MAX_DATA limit", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errFlowControl, + }) +} + +func TestConnInflowMultipleStreams(t *testing.T) { + ctx := canceledContext() + tc := newTestConn(t, serverSide, func(c *Config) { + c.MaxConnReadBufferSize = 128 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + var streams []*Stream + for _, id := range []streamID{ + newStreamID(clientSide, uniStream, 0), + newStreamID(clientSide, uniStream, 1), + newStreamID(clientSide, bidiStream, 0), + newStreamID(clientSide, bidiStream, 1), + } { + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: id, + data: make([]byte, 32), + }) + s, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("AcceptStream() = %v", err) + } + streams = append(streams, s) + if n, err := s.ReadContext(ctx, make([]byte, 1)); err != nil || n != 1 { + t.Fatalf("s.Read() = %v, %v; want 1, nil", n, err) + } + } + tc.wantIdle("streams have read data, but not enough to update MAX_DATA") + + if n, err := streams[0].ReadContext(ctx, make([]byte, 32)); err != nil || n != 31 { + t.Fatalf("s.Read() = %v, %v; want 31, nil", n, err) + } + tc.wantFrame("read enough data to trigger a MAX_DATA update", + packetType1RTT, debugFrameMaxData{ + max: 128 + 32 + 1 + 1 + 1, + }) + + streams[2].CloseRead() + tc.wantFrame("closed stream triggers another MAX_DATA update", + packetType1RTT, debugFrameMaxData{ + max: 128 + 32 + 1 + 32 + 1, + }) +} diff --git a/internal/quic/conn_loss.go b/internal/quic/conn_loss.go index b8146a4255..85bda314ec 100644 --- a/internal/quic/conn_loss.go +++ b/internal/quic/conn_loss.go @@ -44,6 +44,8 @@ func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetF case frameTypeCrypto: start, end := sent.nextRange() c.crypto[space].ackOrLoss(start, end, fate) + case frameTypeMaxData: + c.ackOrLossMaxData(sent.num, fate) case frameTypeResetStream, frameTypeStopSending, frameTypeMaxStreamData, diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index f74ec7e64d..9b88462518 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -289,18 +289,58 @@ func TestLostStreamPartialLoss(t *testing.T) { tc.wantIdle("no more frames sent after packet loss") } +func TestLostMaxDataFrame(t *testing.T) { + // "An updated value is sent in a MAX_DATA frame if the packet + // containing the most recently sent MAX_DATA frame is declared lost [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.7 + lostFrameTest(t, func(t *testing.T, pto bool) { + const maxWindowSize = 32 + buf := make([]byte, maxWindowSize) + tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { + c.MaxConnReadBufferSize = 32 + }) + + // We send MAX_DATA = 63. + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + off: 0, + data: make([]byte, maxWindowSize), + }) + if n, err := s.Read(buf[:maxWindowSize-1]); err != nil || n != maxWindowSize-1 { + t.Fatalf("Read() = %v, %v; want %v, nil", n, err, maxWindowSize-1) + } + tc.wantFrame("conn window is extended after reading data", + packetType1RTT, debugFrameMaxData{ + max: (maxWindowSize * 2) - 1, + }) + + // MAX_DATA = 64, which is only one more byte, so we don't send the frame. + if n, err := s.Read(buf); err != nil || n != 1 { + t.Fatalf("Read() = %v, %v; want %v, nil", n, err, 1) + } + tc.wantIdle("read doesn't extend window enough to send another MAX_DATA") + + // The MAX_DATA = 63 packet was lost, so we send 64. + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantFrame("resent MAX_DATA includes most current value", + packetType1RTT, debugFrameMaxData{ + max: maxWindowSize * 2, + }) + }) +} + func TestLostMaxStreamDataFrame(t *testing.T) { // "[...] an updated value is sent when the packet containing // the most recent MAX_STREAM_DATA frame for a stream is lost" // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.8 lostFrameTest(t, func(t *testing.T, pto bool) { - const maxWindowSize = 10 + const maxWindowSize = 32 buf := make([]byte, maxWindowSize) tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { c.MaxStreamReadBufferSize = maxWindowSize }) - // We send MAX_STREAM_DATA = 19. + // We send MAX_STREAM_DATA = 63. tc.writeFrames(packetType1RTT, debugFrameStream{ id: s.id, off: 0, @@ -315,13 +355,13 @@ func TestLostMaxStreamDataFrame(t *testing.T) { max: (maxWindowSize * 2) - 1, }) - // MAX_STREAM_DATA = 20, which is only one more byte, so we don't send the frame. + // MAX_STREAM_DATA = 64, which is only one more byte, so we don't send the frame. if n, err := s.Read(buf); err != nil || n != 1 { t.Fatalf("Read() = %v, %v; want %v, nil", n, err, 1) } tc.wantIdle("read doesn't extend window enough to send another MAX_STREAM_DATA") - // The MAX_STREAM_DATA = 19 packet was lost, so we send 20. + // The MAX_STREAM_DATA = 63 packet was lost, so we send 64. tc.triggerLossOrPTO(packetType1RTT, pto) tc.wantFrame("resent MAX_STREAM_DATA includes most current value", packetType1RTT, debugFrameMaxStreamData{ diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index 76e9bf94c5..0a72d26eba 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -27,6 +27,9 @@ type streamsState struct { peerInitialMaxStreamDataRemote [streamTypeCount]int64 // streams opened by us peerInitialMaxStreamDataBidiLocal int64 // streams opened by them + // Connection-level flow control. + inflow connInflow + // Streams with frames to send are stored in a circular linked list. // sendHead is the next stream to write, or nil if there are no streams // with data to send. sendTail is the last stream to write. @@ -43,6 +46,7 @@ func (c *Conn) streamsInit() { c.streams.localLimit[uniStream].init() c.streams.remoteLimit[bidiStream].init(c.config.maxBidiRemoteStreams()) c.streams.remoteLimit[uniStream].init(c.config.maxUniRemoteStreams()) + c.inflowInit() } // AcceptStream waits for and returns the next stream created by the peer. @@ -212,6 +216,11 @@ func (c *Conn) queueStreamForSend(s *Stream) { // It returns true if no more frames need appending, // false if not everything fit in the current packet. func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) bool { + // MAX_DATA + if !c.appendMaxDataFrame(w, pnum, pto) { + return false + } + // MAX_STREAM_DATA if !c.streams.remoteLimit[uniStream].appendFrame(w, uniStream, pnum, pto) { return false diff --git a/internal/quic/stream.go b/internal/quic/stream.go index fbc36334ba..84c437d890 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -156,9 +156,10 @@ func (s *Stream) ReadContext(ctx context.Context, b []byte) (n int, err error) { start := s.in.start end := start + int64(len(b)) s.in.copy(start, b) + s.conn.handleStreamBytesReadOffLoop(int64(len(b))) s.in.discardBefore(end) if s.insize == -1 || s.insize > s.inwin { - if shouldUpdateFlowControl(s.inwin-s.in.start, s.inmaxbuf) { + if shouldUpdateFlowControl(s.inmaxbuf, s.in.start+s.inmaxbuf-s.inwin) { // Update stream flow control with a STREAM_MAX_DATA frame. s.insendmax.setUnsent() } @@ -173,10 +174,8 @@ func (s *Stream) ReadContext(ctx context.Context, b []byte) (n int, err error) { // // We want to balance keeping the peer well-supplied with flow control with not sending // many small updates. -func shouldUpdateFlowControl(curwin, maxwin int64) bool { - // Update flow control if doing so gives the peer at least 64k tokens, - // or if it will double the current window. - return maxwin-curwin >= 64<<10 || curwin*2 < maxwin +func shouldUpdateFlowControl(maxWindow, addedWindow int64) bool { + return addedWindow >= maxWindow/8 } // Write writes data to the stream. @@ -295,6 +294,7 @@ func (s *Stream) CloseRead() { } else { s.inclosed.set() } + s.conn.handleStreamBytesReadOffLoop(s.in.end - s.in.start) s.in.discardBefore(s.in.end) } @@ -470,6 +470,12 @@ func (s *Stream) handleData(off int64, b []byte, fin bool) error { // Either way, we can discard this frame. return nil } + if s.insize == -1 && end > s.in.end { + added := end - s.in.end + if err := s.conn.handleStreamBytesReceived(added); err != nil { + return err + } + } s.in.writeAt(b, off) s.inset.add(off, end) if fin { @@ -492,6 +498,13 @@ func (s *Stream) handleReset(code uint64, finalSize int64) error { // The stream was already reset. return nil } + if s.insize == -1 { + added := finalSize - s.in.end + if err := s.conn.handleStreamBytesReceived(added); err != nil { + return err + } + } + s.conn.handleStreamBytesReadOnLoop(finalSize - s.in.start) s.in.discardBefore(s.in.end) s.inresetcode = int64(code) s.insize = finalSize From cae7dab4ad1c5416c5a4bf94b01c45f5874a7e6f Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Sat, 9 Sep 2023 08:23:44 -0700 Subject: [PATCH 054/168] quic: outbound connection-level flow control Track the peer-provided flow control window. Only send stream data when the window permits. For golang/go#58547 Change-Id: I30c054346623e389b3d1cff1de629f1bbf918635 Reviewed-on: https://go-review.googlesource.com/c/net/+/527376 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn.go | 1 + internal/quic/conn_flow.go | 23 +++ internal/quic/conn_flow_test.go | 150 ++++++++++++++++++- internal/quic/conn_recv.go | 11 +- internal/quic/conn_streams.go | 232 +++++++++++++++++++++-------- internal/quic/conn_streams_test.go | 4 +- internal/quic/stream.go | 91 ++++++++--- internal/quic/stream_test.go | 8 + 8 files changed, 432 insertions(+), 88 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 0ab6f69477..c24e790325 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -169,6 +169,7 @@ func (c *Conn) discardKeys(now time.Time, space numberSpace) { // receiveTransportParameters applies transport parameters sent by the peer. func (c *Conn) receiveTransportParameters(p transportParameters) error { + c.streams.outflow.setMaxData(p.initialMaxData) c.streams.localLimit[bidiStream].setMax(p.initialMaxStreamsBidi) c.streams.localLimit[uniStream].setMax(p.initialMaxStreamsUni) c.streams.peerInitialMaxStreamDataBidiLocal = p.initialMaxStreamDataBidiLocal diff --git a/internal/quic/conn_flow.go b/internal/quic/conn_flow.go index 790210b4a9..265fdaf855 100644 --- a/internal/quic/conn_flow.go +++ b/internal/quic/conn_flow.go @@ -109,3 +109,26 @@ func (c *Conn) appendMaxDataFrame(w *packetWriter, pnum packetNumber, pto bool) func (c *Conn) ackOrLossMaxData(pnum packetNumber, fate packetFate) { c.streams.inflow.sent.ackLatestOrLoss(pnum, fate) } + +// connOutflow tracks connection-level flow control for data sent by us to the peer. +type connOutflow struct { + max int64 // largest MAX_DATA received from peer + used int64 // total bytes of STREAM data sent to peer +} + +// setMaxData updates the connection-level flow control limit +// with the initial limit conveyed in transport parameters +// or an update from a MAX_DATA frame. +func (f *connOutflow) setMaxData(maxData int64) { + f.max = max(f.max, maxData) +} + +// avail returns the number of connection-level flow control bytes available. +func (f *connOutflow) avail() int64 { + return f.max - f.used +} + +// consume records consumption of n bytes of flow. +func (f *connOutflow) consume(n int64) { + f.used += n +} diff --git a/internal/quic/conn_flow_test.go b/internal/quic/conn_flow_test.go index f01a7389c4..28559b4695 100644 --- a/internal/quic/conn_flow_test.go +++ b/internal/quic/conn_flow_test.go @@ -6,7 +6,9 @@ package quic -import "testing" +import ( + "testing" +) func TestConnInflowReturnOnRead(t *testing.T) { ctx := canceledContext() @@ -184,3 +186,149 @@ func TestConnInflowMultipleStreams(t *testing.T) { max: 128 + 32 + 1 + 32 + 1, }) } + +func TestConnOutflowBlocked(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, clientSide, uniStream, + permissiveTransportParameters, + func(p *transportParameters) { + p.initialMaxData = 10 + }) + tc.ignoreFrame(frameTypeAck) + + data := makeTestData(32) + n, err := s.Write(data) + if n != len(data) || err != nil { + t.Fatalf("s.Write() = %v, %v; want %v, nil", n, err, len(data)) + } + + tc.wantFrame("stream writes data up to MAX_DATA limit", + packetType1RTT, debugFrameStream{ + id: s.id, + data: data[:10], + }) + tc.wantIdle("stream is blocked by MAX_DATA limit") + + tc.writeFrames(packetType1RTT, debugFrameMaxData{ + max: 20, + }) + tc.wantFrame("stream writes data up to new MAX_DATA limit", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 10, + data: data[10:20], + }) + tc.wantIdle("stream is blocked by new MAX_DATA limit") + + tc.writeFrames(packetType1RTT, debugFrameMaxData{ + max: 100, + }) + tc.wantFrame("stream writes remaining data", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 20, + data: data[20:], + }) +} + +func TestConnOutflowMaxDataDecreases(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, clientSide, uniStream, + permissiveTransportParameters, + func(p *transportParameters) { + p.initialMaxData = 10 + }) + tc.ignoreFrame(frameTypeAck) + + // Decrease in MAX_DATA is ignored. + tc.writeFrames(packetType1RTT, debugFrameMaxData{ + max: 5, + }) + + data := makeTestData(32) + n, err := s.Write(data) + if n != len(data) || err != nil { + t.Fatalf("s.Write() = %v, %v; want %v, nil", n, err, len(data)) + } + + tc.wantFrame("stream writes data up to MAX_DATA limit", + packetType1RTT, debugFrameStream{ + id: s.id, + data: data[:10], + }) +} + +func TestConnOutflowMaxDataRoundRobin(t *testing.T) { + ctx := canceledContext() + tc := newTestConn(t, clientSide, permissiveTransportParameters, + func(p *transportParameters) { + p.initialMaxData = 0 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + s1, err := tc.conn.newLocalStream(ctx, uniStream) + if err != nil { + t.Fatalf("conn.newLocalStream(%v) = %v", uniStream, err) + } + s2, err := tc.conn.newLocalStream(ctx, uniStream) + if err != nil { + t.Fatalf("conn.newLocalStream(%v) = %v", uniStream, err) + } + + s1.Write(make([]byte, 10)) + s2.Write(make([]byte, 10)) + + tc.writeFrames(packetType1RTT, debugFrameMaxData{ + max: 1, + }) + tc.wantFrame("stream 1 writes data up to MAX_DATA limit", + packetType1RTT, debugFrameStream{ + id: s1.id, + data: []byte{0}, + }) + + tc.writeFrames(packetType1RTT, debugFrameMaxData{ + max: 2, + }) + tc.wantFrame("stream 2 writes data up to MAX_DATA limit", + packetType1RTT, debugFrameStream{ + id: s2.id, + data: []byte{0}, + }) + + tc.writeFrames(packetType1RTT, debugFrameMaxData{ + max: 3, + }) + tc.wantFrame("stream 1 writes data up to MAX_DATA limit", + packetType1RTT, debugFrameStream{ + id: s1.id, + off: 1, + data: []byte{0}, + }) +} + +func TestConnOutflowMetaAndData(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, clientSide, bidiStream, + permissiveTransportParameters, + func(p *transportParameters) { + p.initialMaxData = 0 + }) + tc.ignoreFrame(frameTypeAck) + + data := makeTestData(32) + s.Write(data) + + s.CloseRead() + tc.wantFrame("CloseRead sends a STOP_SENDING, not flow controlled", + packetType1RTT, debugFrameStopSending{ + id: s.id, + }) + + tc.writeFrames(packetType1RTT, debugFrameMaxData{ + max: 100, + }) + tc.wantFrame("unblocked MAX_DATA", + packetType1RTT, debugFrameStream{ + id: s.id, + data: data, + }) +} diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index faf3a37f1a..07f17e3ccb 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -186,7 +186,7 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, if !frameOK(c, ptype, __01) { return } - _, n = consumeMaxDataFrame(payload) + n = c.handleMaxDataFrame(now, payload) case frameTypeMaxStreamData: if !frameOK(c, ptype, __01) { return @@ -280,6 +280,15 @@ func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) return n } +func (c *Conn) handleMaxDataFrame(now time.Time, payload []byte) int { + maxData, n := consumeMaxDataFrame(payload) + if n < 0 { + return -1 + } + c.streams.outflow.setMaxData(maxData) + return n +} + func (c *Conn) handleMaxStreamDataFrame(now time.Time, payload []byte) int { id, maxStreamData, n := consumeMaxStreamDataFrame(payload) if n < 0 { diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index 0a72d26eba..7c6c8be2c0 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -28,15 +28,15 @@ type streamsState struct { peerInitialMaxStreamDataBidiLocal int64 // streams opened by them // Connection-level flow control. - inflow connInflow - - // Streams with frames to send are stored in a circular linked list. - // sendHead is the next stream to write, or nil if there are no streams - // with data to send. sendTail is the last stream to write. - needSend atomic.Bool - sendMu sync.Mutex - sendHead *Stream - sendTail *Stream + inflow connInflow + outflow connOutflow + + // Streams with frames to send are stored in one of two circular linked lists, + // depending on whether they require connection-level flow control. + needSend atomic.Bool + sendMu sync.Mutex + queueMeta streamRing // streams with any non-flow-controlled frames + queueData streamRing // streams with only flow-controlled frames } func (c *Conn) streamsInit() { @@ -188,29 +188,67 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) return s } -// queueStreamForSend marks a stream as containing frames that need sending. -func (c *Conn) queueStreamForSend(s *Stream) { +// maybeQueueStreamForSend marks a stream as containing frames that need sending. +func (c *Conn) maybeQueueStreamForSend(s *Stream, state streamState) { + if state.wantQueue() == state.inQueue() { + return // already on the right queue + } c.streams.sendMu.Lock() defer c.streams.sendMu.Unlock() - if s.next != nil { - // Already in the queue. - return - } - if c.streams.sendHead == nil { - // The queue was empty. - c.streams.sendHead = s - c.streams.sendTail = s - s.next = s - } else { - // Insert this stream at the end of the queue. - c.streams.sendTail.next = s - c.streams.sendTail = s - s.next = c.streams.sendHead - } + state = s.state.load() // may have changed while waiting + c.queueStreamForSendLocked(s, state) + c.streams.needSend.Store(true) c.wake() } +// queueStreamForSendLocked moves a stream to the correct send queue, +// or removes it from all queues. +// +// state is the last known stream state. +func (c *Conn) queueStreamForSendLocked(s *Stream, state streamState) { + for { + wantQueue := state.wantQueue() + inQueue := state.inQueue() + if inQueue == wantQueue { + return // already on the right queue + } + + switch inQueue { + case metaQueue: + c.streams.queueMeta.remove(s) + case dataQueue: + c.streams.queueData.remove(s) + } + + switch wantQueue { + case metaQueue: + c.streams.queueMeta.append(s) + state = s.state.set(streamQueueMeta, streamQueueMeta|streamQueueData) + case dataQueue: + c.streams.queueData.append(s) + state = s.state.set(streamQueueData, streamQueueMeta|streamQueueData) + case noQueue: + state = s.state.set(0, streamQueueMeta|streamQueueData) + } + + // If the stream state changed while we were moving the stream, + // we might now be on the wrong queue. + // + // For example: + // - stream has data to send: streamOutSendData|streamQueueData + // - appendStreamFrames sends all the data: streamQueueData + // - concurrently, more data is written: streamOutSendData|streamQueueData + // - appendStreamFrames calls us with the last state it observed + // (streamQueueData). + // - We remove the stream from the queue and observe the updated state: + // streamOutSendData + // - We realize that the stream needs to go back on the data queue. + // + // Go back around the loop to confirm we're on the correct queue. + } +} + // appendStreamFrames writes stream-related frames to the current packet. // // It returns true if no more frames need appending, @@ -237,44 +275,45 @@ func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) } c.streams.sendMu.Lock() defer c.streams.sendMu.Unlock() - for { - s := c.streams.sendHead - const pto = false - + // queueMeta contains streams with non-flow-controlled frames to send. + for c.streams.queueMeta.head != nil { + s := c.streams.queueMeta.head state := s.state.load() - if state&streamInSend != 0 { + if state&(streamQueueMeta|streamConnRemoved) != streamQueueMeta { + panic("BUG: queueMeta stream is not streamQueueMeta") + } + if state&streamInSendMeta != 0 { s.ingate.lock() ok := s.appendInFramesLocked(w, pnum, pto) state = s.inUnlockNoQueue() if !ok { return false } + if state&streamInSendMeta != 0 { + panic("BUG: streamInSendMeta set after successfully appending frames") + } } - - if state&streamOutSend != 0 { - avail := w.avail() + if state&streamOutSendMeta != 0 { s.outgate.lock() + // This might also append flow-controlled frames if we have any + // and available conn-level quota. That's fine. ok := s.appendOutFramesLocked(w, pnum, pto) state = s.outUnlockNoQueue() - if !ok { - // We've sent some data for this stream, but it still has more to send. - // If the stream got a reasonable chance to put data in a packet, - // advance sendHead to the next stream in line, to avoid starvation. - // We'll come back to this stream after going through the others. - // - // If the packet was already mostly out of space, leave sendHead alone - // and come back to this stream again on the next packet. - if avail > 512 { - c.streams.sendHead = s.next - c.streams.sendTail = s - } + // We're checking both ok and state, because appendOutFramesLocked + // might have filled up the packet with flow-controlled data. + // If so, we want to move the stream to queueData for any remaining frames. + if !ok && state&streamOutSendMeta != 0 { return false } + if state&streamOutSendMeta != 0 { + panic("BUG: streamOutSendMeta set after successfully appending frames") + } } - - if state == streamInDone|streamOutDone { + // We've sent all frames for this stream, so remove it from the send queue. + c.streams.queueMeta.remove(s) + if state&(streamInDone|streamOutDone) == streamInDone|streamOutDone { // Stream is finished, remove it from the conn. - s.state.set(streamConnRemoved, streamConnRemoved) + state = s.state.set(streamConnRemoved, streamQueueMeta|streamConnRemoved) delete(c.streams.streams, s.id) // Record finalization of remote streams, to know when @@ -282,24 +321,59 @@ func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) if s.id.initiator() != c.side { c.streams.remoteLimit[s.id.streamType()].close() } + } else { + state = s.state.set(0, streamQueueMeta|streamConnRemoved) } - - next := s.next - s.next = nil - if (next == s) != (s == c.streams.sendTail) { - panic("BUG: sendable stream list state is inconsistent") + // The stream may have flow-controlled data to send, + // or something might have added non-flow-controlled frames after we + // unlocked the stream. + // If so, put the stream back on a queue. + c.queueStreamForSendLocked(s, state) + } + // queueData contains streams with flow-controlled frames. + for c.streams.queueData.head != nil { + avail := c.streams.outflow.avail() + if avail == 0 { + break // no flow control quota available + } + s := c.streams.queueData.head + s.outgate.lock() + ok := s.appendOutFramesLocked(w, pnum, pto) + state := s.outUnlockNoQueue() + if !ok { + // We've sent some data for this stream, but it still has more to send. + // If the stream got a reasonable chance to put data in a packet, + // advance sendHead to the next stream in line, to avoid starvation. + // We'll come back to this stream after going through the others. + // + // If the packet was already mostly out of space, leave sendHead alone + // and come back to this stream again on the next packet. + if avail > 512 { + c.streams.queueData.head = s.next + } + return false + } + if state&streamQueueData == 0 { + panic("BUG: queueData stream is not streamQueueData") } - if s == c.streams.sendTail { - // This was the last stream. - c.streams.sendHead = nil - c.streams.sendTail = nil - c.streams.needSend.Store(false) + if state&streamOutSendData != 0 { + // We must have run out of connection-level flow control: + // appendOutFramesLocked says it wrote all it can, but there's + // still data to send. + // + // Advance sendHead to the next stream in line to avoid starvation. + if c.streams.outflow.avail() != 0 { + panic("BUG: streamOutSendData set and flow control available after send") + } + c.streams.queueData.head = s.next return true } - // We've sent all data for this stream, so remove it from the list. - c.streams.sendTail.next = next - c.streams.sendHead = next + c.streams.queueData.remove(s) + state = s.state.set(0, streamQueueData) + c.queueStreamForSendLocked(s, state) } + c.streams.needSend.Store(c.streams.queueData.head != nil) + return true } // appendStreamFramesPTO writes stream-related frames to the current packet @@ -329,3 +403,37 @@ func (c *Conn) appendStreamFramesPTO(w *packetWriter, pnum packetNumber) bool { } return true } + +// A streamRing is a circular linked list of streams. +type streamRing struct { + head *Stream +} + +// remove removes s from the ring. +// s must be on the ring. +func (r *streamRing) remove(s *Stream) { + if s.next == s { + r.head = nil // s was the last stream in the ring + } else { + s.prev.next = s.next + s.next.prev = s.prev + if r.head == s { + r.head = s.next + } + } +} + +// append places s at the last position in the ring. +// s must not be attached to any ring. +func (r *streamRing) append(s *Stream) { + if r.head == nil { + r.head = s + s.next = s + s.prev = s + } else { + s.prev = r.head.prev + s.next = r.head + s.prev.next = s + s.next.prev = s + } +} diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go index 8ae007ccc7..69f982c3a6 100644 --- a/internal/quic/conn_streams_test.go +++ b/internal/quic/conn_streams_test.go @@ -163,7 +163,7 @@ func TestStreamsLocalStreamClosed(t *testing.T) { if got := len(tc.conn.streams.streams); got != 0 { t.Fatalf("after close, len(tc.conn.streams.streams) = %v, want 0", got) } - if tc.conn.streams.sendHead != nil { + if tc.conn.streams.queueMeta.head != nil { t.Fatalf("after close, stream send queue is not empty; should be") } } @@ -474,7 +474,7 @@ func TestStreamsCreateAndCloseRemote(t *testing.T) { if got := len(tc.conn.streams.streams); got != 0 { t.Fatalf("after test, len(tc.conn.streams.streams) = %v, want 0", got) } - if tc.conn.streams.sendHead != nil { + if tc.conn.streams.queueMeta.head != nil { t.Fatalf("after test, stream send queue is not empty; should be") } } diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 84c437d890..923ff232ef 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -57,6 +57,7 @@ type Stream struct { // streamIn* bits must be set with ingate held. // streamOut* bits must be set with outgate held. // streamConn* bits are set by the conn's loop. + // streamQueue* bits must be set with streamsState.sendMu held. state atomicBits[streamState] prev, next *Stream // guarded by streamsState.sendMu @@ -65,11 +66,19 @@ type Stream struct { type streamState uint32 const ( - // streamInSend and streamOutSend are set when there are - // frames to send for the inbound or outbound sides of the stream. - // For example, MAX_STREAM_DATA or STREAM_DATA_BLOCKED. - streamInSend = streamState(1 << iota) - streamOutSend + // streamInSendMeta is set when there are frames to send for the + // inbound side of the stream. For example, MAX_STREAM_DATA. + // Inbound frames are never flow-controlled. + streamInSendMeta = streamState(1 << iota) + + // streamOutSendMeta is set when there are non-flow-controlled frames + // to send for the outbound side of the stream. For example, STREAM_DATA_BLOCKED. + // streamOutSendData is set when there are no non-flow-controlled outbound frames + // and the stream has data to send. + // + // At most one of streamOutSendMeta and streamOutSendData is set at any time. + streamOutSendMeta + streamOutSendData // streamInDone and streamOutDone are set when the inbound or outbound // sides of the stream are finished. When both are set, the stream @@ -79,8 +88,48 @@ const ( // streamConnRemoved is set when the stream has been removed from the conn. streamConnRemoved + + // streamQueueMeta and streamQueueData indicate which of the streamsState + // send queues the conn is currently on. + streamQueueMeta + streamQueueData ) +type streamQueue int + +const ( + noQueue = streamQueue(iota) + metaQueue // streamsState.queueMeta + dataQueue // streamsState.queueData +) + +// wantQueue returns the send queue the stream should be on. +func (s streamState) wantQueue() streamQueue { + switch { + case s&(streamInSendMeta|streamOutSendMeta) != 0: + return metaQueue + case s&(streamInDone|streamOutDone|streamConnRemoved) == streamInDone|streamOutDone: + return metaQueue + case s&streamOutSendData != 0: + // The stream has no non-flow-controlled frames to send, + // but does have data. Put it on the data queue, which is only + // processed when flow control is available. + return dataQueue + } + return noQueue +} + +// inQueue returns the send queue the stream is currently on. +func (s streamState) inQueue() streamQueue { + switch { + case s&streamQueueMeta != 0: + return metaQueue + case s&streamQueueData != 0: + return dataQueue + } + return noQueue +} + // newStream returns a new stream. // // The stream's ingate and outgate are locked. @@ -365,9 +414,7 @@ func (s *Stream) resetInternal(code uint64, userClosed bool) { // are done and the stream should be removed, it notifies the Conn. func (s *Stream) inUnlock() { state := s.inUnlockNoQueue() - if state&streamInSend != 0 || state == streamInDone|streamOutDone { - s.conn.queueStreamForSend(s) - } + s.conn.maybeQueueStreamForSend(s, state) } // inUnlockNoQueue is inUnlock, @@ -391,11 +438,11 @@ func (s *Stream) inUnlockNoQueue() streamState { state = streamInDone } case s.insendmax.shouldSend(): // STREAM_MAX_DATA - state = streamInSend + state = streamInSendMeta case s.inclosed.shouldSend(): // STOP_SENDING - state = streamInSend + state = streamInSendMeta } - const mask = streamInDone | streamInSend + const mask = streamInDone | streamInSendMeta return s.state.set(state, mask) } @@ -405,9 +452,7 @@ func (s *Stream) inUnlockNoQueue() streamState { // are done and the stream should be removed, it notifies the Conn. func (s *Stream) outUnlock() { state := s.outUnlockNoQueue() - if state&streamOutSend != 0 || state == streamInDone|streamOutDone { - s.conn.queueStreamForSend(s) - } + s.conn.maybeQueueStreamForSend(s, state) } // outUnlockNoQueue is outUnlock, @@ -442,18 +487,18 @@ func (s *Stream) outUnlockNoQueue() streamState { state = streamOutDone } case s.outreset.shouldSend(): // RESET_STREAM - state = streamOutSend + state = streamOutSendMeta case s.outreset.isSet(): // RESET_STREAM sent but not acknowledged + case s.outblocked.shouldSend(): // STREAM_DATA_BLOCKED + state = streamOutSendMeta case len(s.outunsent) > 0: // STREAM frame with data - state = streamOutSend - case s.outclosed.shouldSend(): // STREAM frame with FIN bit - state = streamOutSend + state = streamOutSendData + case s.outclosed.shouldSend(): // STREAM frame with FIN bit, all data already sent + state = streamOutSendMeta case s.outopened.shouldSend(): // STREAM frame with no data - state = streamOutSend - case s.outblocked.shouldSend(): // STREAM_DATA_BLOCKED - state = streamOutSend + state = streamOutSendMeta } - const mask = streamOutDone | streamOutSend + const mask = streamOutDone | streamOutSendMeta | streamOutSendData return s.state.set(state, mask) } @@ -678,6 +723,7 @@ func (s *Stream) appendOutFramesLocked(w *packetWriter, pnum packetNumber, pto b for { // STREAM off, size := dataToSend(min(s.out.start, s.outwin), min(s.out.end, s.outwin), s.outunsent, s.outacked, pto) + size = min(size, s.conn.streams.outflow.avail()) fin := s.outclosed.isSet() && off+size == s.out.end shouldSend := size > 0 || // have data to send s.outopened.shouldSendPTO(pto) || // should open the stream @@ -690,6 +736,7 @@ func (s *Stream) appendOutFramesLocked(w *packetWriter, pnum packetNumber, pto b return false } s.out.copy(off, b) + s.conn.streams.outflow.consume(int64(len(b))) s.outunsent.sub(off, off+int64(len(b))) s.frameOpensStream(pnum) if fin { diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index b014852871..750119614b 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -1270,3 +1270,11 @@ func permissiveTransportParameters(p *transportParameters) { p.initialMaxStreamDataBidiLocal = maxVarint p.initialMaxStreamDataUni = maxVarint } + +func makeTestData(n int) []byte { + b := make([]byte, n) + for i := 0; i < n; i++ { + b[i] = byte(i) + } + return b +} From 57bce0e9e9d357708bcc4486f0e8602471f59c78 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 12 Sep 2023 13:42:13 -0700 Subject: [PATCH 055/168] quic: move packetType.String out of test-only code This is also used when GODEBUG=quiclogpackets=1 is set. For golang/go#58547 Change-Id: I8ae27629090d12a8a23131e7f1adc93cc6ea8715 Reviewed-on: https://go-review.googlesource.com/c/net/+/527579 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/packet.go | 18 ++++++++++++++++++ internal/quic/packet_test.go | 17 ----------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/internal/quic/packet.go b/internal/quic/packet.go index 93a9102e8d..00c6714426 100644 --- a/internal/quic/packet.go +++ b/internal/quic/packet.go @@ -6,6 +6,8 @@ package quic +import "fmt" + // packetType is a QUIC packet type. // https://www.rfc-editor.org/rfc/rfc9000.html#section-17 type packetType byte @@ -20,6 +22,22 @@ const ( packetTypeVersionNegotiation ) +func (p packetType) String() string { + switch p { + case packetTypeInitial: + return "Initial" + case packetType0RTT: + return "0-RTT" + case packetTypeHandshake: + return "Handshake" + case packetTypeRetry: + return "Retry" + case packetType1RTT: + return "1-RTT" + } + return fmt.Sprintf("unknown packet type %v", byte(p)) +} + // Bits set in the first byte of a packet. const ( headerFormLong = 0x80 // https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2-3.2.1 diff --git a/internal/quic/packet_test.go b/internal/quic/packet_test.go index f3a8b7d570..b13a587e54 100644 --- a/internal/quic/packet_test.go +++ b/internal/quic/packet_test.go @@ -9,27 +9,10 @@ package quic import ( "bytes" "encoding/hex" - "fmt" "strings" "testing" ) -func (p packetType) String() string { - switch p { - case packetTypeInitial: - return "Initial" - case packetType0RTT: - return "0-RTT" - case packetTypeHandshake: - return "Handshake" - case packetTypeRetry: - return "Retry" - case packetType1RTT: - return "1-RTT" - } - return fmt.Sprintf("unknown packet type %v", byte(p)) -} - func TestPacketHeader(t *testing.T) { for _, test := range []struct { name string From 02eb0f3c0a13d33cb696b10ab2d257f46c616a8a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 12 Sep 2023 15:13:59 -0700 Subject: [PATCH 056/168] quic: avoid deadlock when updating inbound conn-level flow control handleStreamBytesReadOffLoop sends a message to the conn indicating that we need to send a MAX_DATA update. Calling this with a stream's gate locked can lead to a deadlock, when the conn's loop is processing an inbound frame for the same stream: The conn can't acquire the stream's ingate, and the gate won't be unlocked until the conn processes another event from its queue. Move the handleStreamBytesReadOffLoop calls out of the gate. No test in this CL, but a following CL contains a test which reliably exercises the condition. For golang/go#58547 Change-Id: Ic98888947f67408a4a1f6f4a3aaf68c3a2fe8e7f Reviewed-on: https://go-review.googlesource.com/c/net/+/527580 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_flow.go | 3 +++ internal/quic/conn_flow_test.go | 1 + internal/quic/stream.go | 12 +++++++----- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/internal/quic/conn_flow.go b/internal/quic/conn_flow.go index 265fdaf855..cd9a6a912b 100644 --- a/internal/quic/conn_flow.go +++ b/internal/quic/conn_flow.go @@ -47,6 +47,9 @@ func (c *Conn) inflowInit() { // // This is called indirectly by the user, via Read or CloseRead. func (c *Conn) handleStreamBytesReadOffLoop(n int64) { + if n == 0 { + return + } if c.shouldUpdateFlowControl(c.streams.inflow.credit.Add(n)) { // We should send a MAX_DATA update to the peer. // Record this on the Conn's main loop. diff --git a/internal/quic/conn_flow_test.go b/internal/quic/conn_flow_test.go index 28559b4695..2cd4e62469 100644 --- a/internal/quic/conn_flow_test.go +++ b/internal/quic/conn_flow_test.go @@ -180,6 +180,7 @@ func TestConnInflowMultipleStreams(t *testing.T) { max: 128 + 32 + 1 + 1 + 1, }) + tc.ignoreFrame(frameTypeStopSending) streams[2].CloseRead() tc.wantFrame("closed stream triggers another MAX_DATA update", packetType1RTT, debugFrameMaxData{ diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 923ff232ef..9310811c1b 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -181,11 +181,13 @@ func (s *Stream) ReadContext(ctx context.Context, b []byte) (n int, err error) { if s.IsWriteOnly() { return 0, errors.New("read from write-only stream") } - // Wait until data is available. if err := s.ingate.waitAndLock(ctx, s.conn.testHooks); err != nil { return 0, err } - defer s.inUnlock() + defer func() { + s.inUnlock() + s.conn.handleStreamBytesReadOffLoop(int64(n)) // must be done with ingate unlocked + }() if s.inresetcode != -1 { return 0, fmt.Errorf("stream reset by peer: %w", StreamErrorCode(s.inresetcode)) } @@ -205,7 +207,6 @@ func (s *Stream) ReadContext(ctx context.Context, b []byte) (n int, err error) { start := s.in.start end := start + int64(len(b)) s.in.copy(start, b) - s.conn.handleStreamBytesReadOffLoop(int64(len(b))) s.in.discardBefore(end) if s.insize == -1 || s.insize > s.inwin { if shouldUpdateFlowControl(s.inmaxbuf, s.in.start+s.inmaxbuf-s.inwin) { @@ -334,7 +335,6 @@ func (s *Stream) CloseRead() { return } s.ingate.lock() - defer s.inUnlock() if s.inset.isrange(0, s.insize) || s.inresetcode != -1 { // We've already received all data from the peer, // so there's no need to send STOP_SENDING. @@ -343,8 +343,10 @@ func (s *Stream) CloseRead() { } else { s.inclosed.set() } - s.conn.handleStreamBytesReadOffLoop(s.in.end - s.in.start) + discarded := s.in.end - s.in.start s.in.discardBefore(s.in.end) + s.inUnlock() + s.conn.handleStreamBytesReadOffLoop(discarded) // must be done with ingate unlocked } // CloseWrite aborts writes on the stream. From 47caaff48d7cdfbc45b155988009eb57a72bbeaf Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 12 Sep 2023 15:19:59 -0700 Subject: [PATCH 057/168] quic: send and receive UDP datagrams Add the Listener type, which manages a UDP socket. For golang/go#58547 Change-Id: Ia23a8b726ef46f8f84c9e052aa4dfc10eab034d6 Reviewed-on: https://go-review.googlesource.com/c/net/+/527758 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn.go | 36 ++--- internal/quic/conn_id.go | 48 ++++-- internal/quic/conn_id_test.go | 115 +++++++++----- internal/quic/conn_recv.go | 4 +- internal/quic/conn_test.go | 51 ++++-- internal/quic/listener.go | 280 +++++++++++++++++++++++++++++++++ internal/quic/listener_test.go | 88 +++++++++++ internal/quic/tls.go | 1 + 8 files changed, 532 insertions(+), 91 deletions(-) create mode 100644 internal/quic/listener.go create mode 100644 internal/quic/listener_test.go diff --git a/internal/quic/conn.go b/internal/quic/conn.go index c24e790325..0063965df2 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -20,13 +20,14 @@ import ( // Multiple goroutines may invoke methods on a Conn simultaneously. type Conn struct { side connSide - listener connListener + listener *Listener config *Config testHooks connTestHooks peerAddr netip.AddrPort msgc chan any donec chan struct{} // closed when conn loop exits + readyc chan struct{} // closed when TLS handshake completes exited bool // set to make the conn loop exit immediately w packetWriter @@ -61,21 +62,16 @@ type Conn struct { testSendPing sentVal } -// The connListener is the Conn's Listener. -// Defined as an interface so we can swap it out in tests. -type connListener interface { - sendDatagram(p []byte, addr netip.AddrPort) error -} - // connTestHooks override conn behavior in tests. type connTestHooks interface { nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any) handleTLSEvent(tls.QUICEvent) newConnID(seq int64) ([]byte, error) waitUntil(ctx context.Context, until func() bool) error + timeNow() time.Time } -func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l connListener, hooks connTestHooks) (*Conn, error) { +func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener, hooks connTestHooks) (*Conn, error) { c := &Conn{ side: side, listener: l, @@ -83,6 +79,7 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. peerAddr: peerAddr, msgc: make(chan any, 1), donec: make(chan struct{}), + readyc: make(chan struct{}), testHooks: hooks, maxIdleTimeout: defaultMaxIdleTimeout, idleTimeout: now.Add(defaultMaxIdleTimeout), @@ -94,12 +91,12 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. c.msgc = make(chan any, 1) if c.side == clientSide { - if err := c.connIDState.initClient(c.newConnIDFunc()); err != nil { + if err := c.connIDState.initClient(c); err != nil { return nil, err } initialConnID, _ = c.connIDState.dstConnID() } else { - if err := c.connIDState.initServer(c.newConnIDFunc(), initialConnID); err != nil { + if err := c.connIDState.initServer(c, initialConnID); err != nil { return nil, err } } @@ -134,6 +131,14 @@ func (c *Conn) String() string { return fmt.Sprintf("quic.Conn(%v,->%v)", c.side, c.peerAddr) } +func (c *Conn) Close() error { + // TODO: Implement shutdown for real. + c.runOnLoop(func(now time.Time, c *Conn) { + c.exited = true + }) + return nil +} + // confirmHandshake is called when the handshake is confirmed. // https://www.rfc-editor.org/rfc/rfc9001#section-4.1.2 func (c *Conn) confirmHandshake(now time.Time) { @@ -147,6 +152,7 @@ func (c *Conn) confirmHandshake(now time.Time) { if c.side == serverSide { // When the server confirms the handshake, it sends a HANDSHAKE_DONE. c.handshakeConfirmed.setUnsent() + c.listener.serverConnEstablished(c) } else { // The client never sends a HANDSHAKE_DONE, so we set handshakeConfirmed // to the received state, indicating that the handshake is confirmed and we @@ -177,7 +183,7 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error { c.streams.peerInitialMaxStreamDataRemote[uniStream] = p.initialMaxStreamDataUni c.peerAckDelayExponent = p.ackDelayExponent c.loss.setMaxAckDelay(p.maxAckDelay) - if err := c.connIDState.setPeerActiveConnIDLimit(p.activeConnIDLimit, c.newConnIDFunc()); err != nil { + if err := c.connIDState.setPeerActiveConnIDLimit(c, p.activeConnIDLimit); err != nil { return err } if p.preferredAddrConnID != nil { @@ -211,6 +217,7 @@ type ( func (c *Conn) loop(now time.Time) { defer close(c.donec) defer c.tls.Close() + defer c.listener.connDrained(c) // The connection timer sends a message to the connection loop on expiry. // We need to give it an expiry when creating it, so set the initial timeout to @@ -371,10 +378,3 @@ func firstTime(a, b time.Time) time.Time { return b } } - -func (c *Conn) newConnIDFunc() newConnIDFunc { - if c.testHooks != nil { - return c.testHooks.newConnID - } - return newRandomConnID -} diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index 561dea2c14..eb2f3ecc15 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -55,10 +55,10 @@ type connID struct { send sentVal } -func (s *connIDState) initClient(newID newConnIDFunc) error { +func (s *connIDState) initClient(c *Conn) error { // Client chooses its initial connection ID, and sends it // in the Source Connection ID field of the first Initial packet. - locid, err := newID(0) + locid, err := c.newConnID(0) if err != nil { return err } @@ -70,7 +70,7 @@ func (s *connIDState) initClient(newID newConnIDFunc) error { // Client chooses an initial, transient connection ID for the server, // and sends it in the Destination Connection ID field of the first Initial packet. - remid, err := newID(-1) + remid, err := c.newConnID(-1) if err != nil { return err } @@ -78,10 +78,12 @@ func (s *connIDState) initClient(newID newConnIDFunc) error { seq: -1, cid: remid, }) + const retired = false + c.listener.connIDsChanged(c, retired, s.local[:]) return nil } -func (s *connIDState) initServer(newID newConnIDFunc, dstConnID []byte) error { +func (s *connIDState) initServer(c *Conn, dstConnID []byte) error { // Client-chosen, transient connection ID received in the first Initial packet. // The server will not use this as the Source Connection ID of packets it sends, // but remembers it because it may receive packets sent to this destination. @@ -92,7 +94,7 @@ func (s *connIDState) initServer(newID newConnIDFunc, dstConnID []byte) error { // Server chooses a connection ID, and sends it in the Source Connection ID of // the response to the clent. - locid, err := newID(0) + locid, err := c.newConnID(0) if err != nil { return err } @@ -101,6 +103,8 @@ func (s *connIDState) initServer(newID newConnIDFunc, dstConnID []byte) error { cid: locid, }) s.nextLocalSeq = 1 + const retired = false + c.listener.connIDsChanged(c, retired, s.local[:]) return nil } @@ -125,20 +129,21 @@ func (s *connIDState) dstConnID() (cid []byte, ok bool) { // setPeerActiveConnIDLimit sets the active_connection_id_limit // transport parameter received from the peer. -func (s *connIDState) setPeerActiveConnIDLimit(lim int64, newID newConnIDFunc) error { +func (s *connIDState) setPeerActiveConnIDLimit(c *Conn, lim int64) error { s.peerActiveConnIDLimit = lim - return s.issueLocalIDs(newID) + return s.issueLocalIDs(c) } -func (s *connIDState) issueLocalIDs(newID newConnIDFunc) error { +func (s *connIDState) issueLocalIDs(c *Conn) error { toIssue := min(int(s.peerActiveConnIDLimit), maxPeerActiveConnIDLimit) for i := range s.local { if s.local[i].seq != -1 && !s.local[i].retired { toIssue-- } } + prev := len(s.local) for toIssue > 0 { - cid, err := newID(s.nextLocalSeq) + cid, err := c.newConnID(s.nextLocalSeq) if err != nil { return err } @@ -151,14 +156,16 @@ func (s *connIDState) issueLocalIDs(newID newConnIDFunc) error { s.needSend = true toIssue-- } + const retired = false + c.listener.connIDsChanged(c, retired, s.local[prev:]) return nil } // handlePacket updates the connection ID state during the handshake // (Initial and Handshake packets). -func (s *connIDState) handlePacket(side connSide, ptype packetType, srcConnID []byte) { +func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) { switch { - case ptype == packetTypeInitial && side == clientSide: + case ptype == packetTypeInitial && c.side == clientSide: if len(s.remote) == 1 && s.remote[0].seq == -1 { // We're a client connection processing the first Initial packet // from the server. Replace the transient remote connection ID @@ -168,7 +175,7 @@ func (s *connIDState) handlePacket(side connSide, ptype packetType, srcConnID [] cid: cloneBytes(srcConnID), } } - case ptype == packetTypeInitial && side == serverSide: + case ptype == packetTypeInitial && c.side == serverSide: if len(s.remote) == 0 { // We're a server connection processing the first Initial packet // from the client. Set the client's connection ID. @@ -177,11 +184,13 @@ func (s *connIDState) handlePacket(side connSide, ptype packetType, srcConnID [] cid: cloneBytes(srcConnID), }) } - case ptype == packetTypeHandshake && side == serverSide: + case ptype == packetTypeHandshake && c.side == serverSide: if len(s.local) > 0 && s.local[0].seq == -1 { // We're a server connection processing the first Handshake packet from // the client. Discard the transient, client-chosen connection ID used // for Initial packets; the client will never send it again. + const retired = true + c.listener.connIDsChanged(c, retired, s.local[0:1]) s.local = append(s.local[:0], s.local[1:]...) } } @@ -263,17 +272,19 @@ func (s *connIDState) retireRemote(rcid *connID) { s.needSend = true } -func (s *connIDState) handleRetireConnID(seq int64, newID newConnIDFunc) error { +func (s *connIDState) handleRetireConnID(c *Conn, seq int64) error { if seq >= s.nextLocalSeq { return localTransportError(errProtocolViolation) } for i := range s.local { if s.local[i].seq == seq { + const retired = true + c.listener.connIDsChanged(c, retired, s.local[i:i+1]) s.local = append(s.local[:i], s.local[i+1:]...) break } } - s.issueLocalIDs(newID) + s.issueLocalIDs(c) return nil } @@ -355,7 +366,12 @@ func cloneBytes(b []byte) []byte { return n } -type newConnIDFunc func(seq int64) ([]byte, error) +func (c *Conn) newConnID(seq int64) ([]byte, error) { + if c.testHooks != nil { + return c.testHooks.newConnID(seq) + } + return newRandomConnID(seq) +} func newRandomConnID(_ int64) ([]byte, error) { // It is not necessary for connection IDs to be cryptographically secure, diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index d479cd4a87..c5289583d3 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -11,100 +11,135 @@ import ( "crypto/tls" "fmt" "net/netip" - "reflect" + "strings" "testing" ) func TestConnIDClientHandshake(t *testing.T) { + tc := newTestConn(t, clientSide) // On initialization, the client chooses local and remote IDs. // // The order in which we allocate the two isn't actually important, // but test is a lot simpler if we assume. - var s connIDState - s.initClient(newConnIDSequence()) - if got, want := string(s.srcConnID()), "local-1"; got != want { - t.Errorf("after initClient: srcConnID = %q, want %q", got, want) + if got, want := tc.conn.connIDState.srcConnID(), testLocalConnID(0); !bytes.Equal(got, want) { + t.Errorf("after initialization: srcConnID = %x, want %x", got, want) } - dstConnID, _ := s.dstConnID() - if got, want := string(dstConnID), "local-2"; got != want { - t.Errorf("after initClient: dstConnID = %q, want %q", got, want) + dstConnID, _ := tc.conn.connIDState.dstConnID() + if got, want := dstConnID, testLocalConnID(-1); !bytes.Equal(got, want) { + t.Errorf("after initialization: dstConnID = %x, want %x", got, want) } // The server's first Initial packet provides the client with a // non-transient remote connection ID. - s.handlePacket(clientSide, packetTypeInitial, []byte("remote-1")) - dstConnID, _ = s.dstConnID() - if got, want := string(dstConnID), "remote-1"; got != want { - t.Errorf("after receiving Initial: dstConnID = %q, want %q", got, want) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + dstConnID, _ = tc.conn.connIDState.dstConnID() + if got, want := dstConnID, testPeerConnID(0); !bytes.Equal(got, want) { + t.Errorf("after receiving Initial: dstConnID = %x, want %x", got, want) } wantLocal := []connID{{ - cid: []byte("local-1"), + cid: testLocalConnID(0), seq: 0, }} - if !reflect.DeepEqual(s.local, wantLocal) { - t.Errorf("local ids: %v, want %v", s.local, wantLocal) + if got := tc.conn.connIDState.local; !connIDListEqual(got, wantLocal) { + t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } wantRemote := []connID{{ - cid: []byte("remote-1"), + cid: testPeerConnID(0), seq: 0, }} - if !reflect.DeepEqual(s.remote, wantRemote) { - t.Errorf("remote ids: %v, want %v", s.remote, wantRemote) + if got := tc.conn.connIDState.remote; !connIDListEqual(got, wantRemote) { + t.Errorf("remote ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantRemote)) } } func TestConnIDServerHandshake(t *testing.T) { + tc := newTestConn(t, serverSide) // On initialization, the server is provided with the client-chosen // transient connection ID, and allocates an ID of its own. // The Initial packet sets the remote connection ID. - var s connIDState - s.initServer(newConnIDSequence(), []byte("transient")) - s.handlePacket(serverSide, packetTypeInitial, []byte("remote-1")) - if got, want := string(s.srcConnID()), "local-1"; got != want { + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial][:1], + }) + if got, want := tc.conn.connIDState.srcConnID(), testLocalConnID(0); !bytes.Equal(got, want) { t.Errorf("after initClient: srcConnID = %q, want %q", got, want) } - dstConnID, _ := s.dstConnID() - if got, want := string(dstConnID), "remote-1"; got != want { + dstConnID, _ := tc.conn.connIDState.dstConnID() + if got, want := dstConnID, testPeerConnID(0); !bytes.Equal(got, want) { t.Errorf("after initClient: dstConnID = %q, want %q", got, want) } + // The Initial flight of CRYPTO data includes transport parameters, + // which cause us to allocate another local connection ID. + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + off: 1, + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial][1:], + }) wantLocal := []connID{{ - cid: []byte("transient"), + cid: testPeerConnID(-1), seq: -1, }, { - cid: []byte("local-1"), + cid: testLocalConnID(0), seq: 0, + }, { + cid: testLocalConnID(1), + seq: 1, }} - if !reflect.DeepEqual(s.local, wantLocal) { - t.Errorf("local ids: %v, want %v", s.local, wantLocal) + if got := tc.conn.connIDState.local; !connIDListEqual(got, wantLocal) { + t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } wantRemote := []connID{{ - cid: []byte("remote-1"), + cid: testPeerConnID(0), seq: 0, }} - if !reflect.DeepEqual(s.remote, wantRemote) { - t.Errorf("remote ids: %v, want %v", s.remote, wantRemote) + if got := tc.conn.connIDState.remote; !connIDListEqual(got, wantRemote) { + t.Errorf("remote ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantRemote)) } // The client's first Handshake packet permits the server to discard the // transient connection ID. - s.handlePacket(serverSide, packetTypeHandshake, []byte("remote-1")) + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) wantLocal = []connID{{ - cid: []byte("local-1"), + cid: testLocalConnID(0), seq: 0, + }, { + cid: testLocalConnID(1), + seq: 1, }} - if !reflect.DeepEqual(s.local, wantLocal) { - t.Errorf("after handshake local ids: %v, want %v", s.local, wantLocal) + if got := tc.conn.connIDState.local; !connIDListEqual(got, wantLocal) { + t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) + } +} + +func connIDListEqual(a, b []connID) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i].seq != b[i].seq { + return false + } + if !bytes.Equal(a[i].cid, b[i].cid) { + return false + } } + return true } -func newConnIDSequence() newConnIDFunc { - var n uint64 - return func(_ int64) ([]byte, error) { - n++ - return []byte(fmt.Sprintf("local-%v", n)), nil +func fmtConnIDList(s []connID) string { + var strs []string + for _, cid := range s { + strs = append(strs, fmt.Sprintf("[seq:%v cid:{%x}]", cid.seq, cid.cid)) } + return "{" + strings.Join(strs, " ") + "}" } func TestNewRandomConnID(t *testing.T) { diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 07f17e3ccb..b866d8a6db 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -63,7 +63,7 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa if logPackets { logInboundLongPacket(c, p) } - c.connIDState.handlePacket(c.side, p.ptype, p.srcConnID) + c.connIDState.handlePacket(c, p.ptype, p.srcConnID) ackEliciting := c.handleFrames(now, ptype, space, p.payload) c.acks[space].receive(now, space, p.num, ackEliciting) if p.ptype == packetTypeHandshake && c.side == serverSide { @@ -377,7 +377,7 @@ func (c *Conn) handleRetireConnectionIDFrame(now time.Time, space numberSpace, p if n < 0 { return -1 } - if err := c.connIDState.handleRetireConnID(seq, c.newConnIDFunc()); err != nil { + if err := c.connIDState.handleRetireConnID(c, seq); err != nil { c.abort(now, err) } return n diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index ea720d5754..cdbd4669e9 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -13,7 +13,9 @@ import ( "errors" "flag" "fmt" + "io" "math" + "net" "net/netip" "reflect" "strings" @@ -105,6 +107,7 @@ func (p testPacket) String() string { type testConn struct { t *testing.T conn *Conn + listener *Listener now time.Time timer time.Time timerLastFired time.Time @@ -142,6 +145,8 @@ type testConn struct { sentFrames []debugFrame lastPacket *testPacket + recvDatagram chan *datagram + // Transport parameters sent by the conn. sentTransportParameters *transportParameters @@ -173,6 +178,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { }, cryptoDataOut: make(map[tls.QUICEncryptionLevel][]byte), cryptoDataIn: make(map[tls.QUICEncryptionLevel][]byte), + recvDatagram: make(chan *datagram), } t.Cleanup(tc.cleanup) @@ -196,12 +202,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { var initialConnID []byte if side == serverSide { // The initial connection ID for the server is chosen by the client. - // When creating a server-side connection, pick a random connection ID here. - var err error - initialConnID, err = newRandomConnID(0) - if err != nil { - tc.t.Fatal(err) - } + initialConnID = testPeerConnID(-1) } peerQUICConfig := &tls.QUICConfig{TLSConfig: newTestTLSConfig(side.peer())} @@ -213,14 +214,12 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { tc.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) tc.peerTLSConn.Start(context.Background()) - conn, err := newConn( + tc.listener = newListener((*testConnUDPConn)(tc), config, (*testConnHooks)(tc)) + conn, err := tc.listener.newConn( tc.now, side, initialConnID, - netip.MustParseAddrPort("127.0.0.1:443"), - config, - (*testConnListener)(tc), - (*testConnHooks)(tc)) + netip.MustParseAddrPort("127.0.0.1:443")) if err != nil { tc.t.Fatal(err) } @@ -316,6 +315,7 @@ func (tc *testConn) cleanup() { return } tc.conn.exit() + tc.listener.Close(context.Background()) } func (tc *testConn) logDatagram(text string, d *testDatagram) { @@ -844,6 +844,10 @@ func (tc *testConnHooks) newConnID(seq int64) ([]byte, error) { return testLocalConnID(seq), nil } +func (tc *testConnHooks) timeNow() time.Time { + return tc.now +} + // testLocalConnID returns the connection ID with a given sequence number // used by a Conn under test. func testLocalConnID(seq int64) []byte { @@ -861,14 +865,31 @@ func testPeerConnID(seq int64) []byte { return []byte{0xbe, 0xee, 0xff, byte(seq)} } -// testConnListener implements connListener. -type testConnListener testConn +// testConnUDPConn implements UDPConn. +type testConnUDPConn testConn -func (tc *testConnListener) sendDatagram(p []byte, addr netip.AddrPort) error { - tc.sentDatagrams = append(tc.sentDatagrams, append([]byte(nil), p...)) +func (tc *testConnUDPConn) Close() error { + close(tc.recvDatagram) return nil } +func (tc *testConnUDPConn) LocalAddr() net.Addr { + return net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:443")) +} + +func (tc *testConnUDPConn) ReadMsgUDPAddrPort(b, control []byte) (n, controln, flags int, _ netip.AddrPort, _ error) { + for d := range tc.recvDatagram { + n = copy(b, d.b) + return n, 0, 0, d.addr, nil + } + return 0, 0, 0, netip.AddrPort{}, io.EOF +} + +func (tc *testConnUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { + tc.sentDatagrams = append(tc.sentDatagrams, append([]byte(nil), b...)) + return len(b), nil +} + // canceledContext returns a canceled Context. // // Functions which take a context preference progress over cancelation. diff --git a/internal/quic/listener.go b/internal/quic/listener.go new file mode 100644 index 0000000000..9869f6e228 --- /dev/null +++ b/internal/quic/listener.go @@ -0,0 +1,280 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "errors" + "net" + "net/netip" + "sync" + "sync/atomic" + "time" +) + +// A Listener listens for QUIC traffic on a network address. +// It can accept inbound connections or create outbound ones. +// +// Multiple goroutines may invoke methods on a Listener simultaneously. +type Listener struct { + config *Config + udpConn udpConn + testHooks connTestHooks + + acceptQueue queue[*Conn] // new inbound connections + + connsMu sync.Mutex + conns map[*Conn]struct{} + closing bool // set when Close is called + closec chan struct{} // closed when the listen loop exits + + // The datagram receive loop keeps a mapping of connection IDs to conns. + // When a conn's connection IDs change, we add it to connIDUpdates and set + // connIDUpdateNeeded, and the receive loop updates its map. + connIDUpdateMu sync.Mutex + connIDUpdateNeeded atomic.Bool + connIDUpdates []connIDUpdate +} + +// A udpConn is a UDP connection. +// It is implemented by net.UDPConn. +type udpConn interface { + Close() error + LocalAddr() net.Addr + ReadMsgUDPAddrPort(b, control []byte) (n, controln, flags int, _ netip.AddrPort, _ error) + WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) +} + +type connIDUpdate struct { + conn *Conn + retired bool + cid []byte +} + +// Listen listens on a local network address. +// The configuration config must be non-nil. +func Listen(network, address string, config *Config) (*Listener, error) { + if config.TLSConfig == nil { + return nil, errors.New("TLSConfig is not set") + } + a, err := net.ResolveUDPAddr(network, address) + if err != nil { + return nil, err + } + udpConn, err := net.ListenUDP(network, a) + if err != nil { + return nil, err + } + return newListener(udpConn, config, nil), nil +} + +func newListener(udpConn udpConn, config *Config, hooks connTestHooks) *Listener { + l := &Listener{ + config: config, + udpConn: udpConn, + testHooks: hooks, + conns: make(map[*Conn]struct{}), + acceptQueue: newQueue[*Conn](), + closec: make(chan struct{}), + } + go l.listen() + return l +} + +// LocalAddr returns the local network address. +func (l *Listener) LocalAddr() netip.AddrPort { + a, _ := l.udpConn.LocalAddr().(*net.UDPAddr) + return a.AddrPort() +} + +// Close closes the listener. +// Any blocked operations on the Listener or associated Conns and Stream will be unblocked +// and return errors. +// +// Close aborts every open connection. +// Data in stream read and write buffers is discarded. +// It waits for the peers of any open connection to acknowledge the connection has been closed. +func (l *Listener) Close(ctx context.Context) error { + l.acceptQueue.close(errors.New("listener closed")) + l.connsMu.Lock() + if !l.closing { + l.closing = true + for c := range l.conns { + c.Close() + } + if len(l.conns) == 0 { + l.udpConn.Close() + } + } + l.connsMu.Unlock() + select { + case <-l.closec: + case <-ctx.Done(): + l.connsMu.Lock() + for c := range l.conns { + c.exit() + } + l.connsMu.Unlock() + return ctx.Err() + } + return nil +} + +// Accept waits for and returns the next connection to the listener. +func (l *Listener) Accept(ctx context.Context) (*Conn, error) { + return l.acceptQueue.get(ctx, nil) +} + +// Dial creates and returns a connection to a network address. +func (l *Listener) Dial(ctx context.Context, network, address string) (*Conn, error) { + u, err := net.ResolveUDPAddr(network, address) + if err != nil { + return nil, err + } + addr := u.AddrPort() + addr = netip.AddrPortFrom(addr.Addr().Unmap(), addr.Port()) + c, err := l.newConn(time.Now(), clientSide, nil, addr) + if err != nil { + return nil, err + } + select { + case <-c.readyc: + case <-ctx.Done(): + c.Close() + return nil, ctx.Err() + } + return c, nil +} + +func (l *Listener) newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort) (*Conn, error) { + l.connsMu.Lock() + defer l.connsMu.Unlock() + if l.closing { + return nil, errors.New("listener closed") + } + c, err := newConn(now, side, initialConnID, peerAddr, l.config, l, l.testHooks) + if err != nil { + return nil, err + } + l.conns[c] = struct{}{} + return c, nil +} + +// serverConnEstablished is called by a conn when the handshake completes +// for an inbound (serverSide) connection. +func (l *Listener) serverConnEstablished(c *Conn) { + l.acceptQueue.put(c) +} + +// connDrained is called by a conn when it leaves the draining state, +// either when the peer acknowledges connection closure or the drain timeout expires. +func (l *Listener) connDrained(c *Conn) { + l.connsMu.Lock() + defer l.connsMu.Unlock() + delete(l.conns, c) + if l.closing && len(l.conns) == 0 { + l.udpConn.Close() + } +} + +// connIDsChanged is called by a conn when its connection IDs change. +func (l *Listener) connIDsChanged(c *Conn, retired bool, cids []connID) { + l.connIDUpdateMu.Lock() + defer l.connIDUpdateMu.Unlock() + for _, cid := range cids { + l.connIDUpdates = append(l.connIDUpdates, connIDUpdate{ + conn: c, + retired: retired, + cid: cid.cid, + }) + } + l.connIDUpdateNeeded.Store(true) +} + +// updateConnIDs is called by the datagram receive loop to update its connection ID map. +func (l *Listener) updateConnIDs(conns map[string]*Conn) { + l.connIDUpdateMu.Lock() + defer l.connIDUpdateMu.Unlock() + for i, u := range l.connIDUpdates { + if u.retired { + delete(conns, string(u.cid)) + } else { + conns[string(u.cid)] = u.conn + } + l.connIDUpdates[i] = connIDUpdate{} // drop refs + } + l.connIDUpdates = l.connIDUpdates[:0] + l.connIDUpdateNeeded.Store(false) +} + +func (l *Listener) listen() { + defer close(l.closec) + conns := map[string]*Conn{} + for { + m := newDatagram() + // TODO: Read and process the ECN (explicit congestion notification) field. + // https://tools.ietf.org/html/draft-ietf-quic-transport-32#section-13.4 + n, _, _, addr, err := l.udpConn.ReadMsgUDPAddrPort(m.b, nil) + if err != nil { + // The user has probably closed the listener. + // We currently don't surface errors from other causes; + // we could check to see if the listener has been closed and + // record the unexpected error if it has not. + return + } + if n == 0 { + continue + } + if l.connIDUpdateNeeded.Load() { + l.updateConnIDs(conns) + } + m.addr = addr + m.b = m.b[:n] + l.handleDatagram(m, conns) + } +} + +func (l *Listener) handleDatagram(m *datagram, conns map[string]*Conn) { + dstConnID, ok := dstConnIDForDatagram(m.b) + if !ok { + return + } + c := conns[string(dstConnID)] + if c == nil { + if getPacketType(m.b) != packetTypeInitial { + // This packet isn't trying to create a new connection. + // It might be associated with some connection we've lost state for. + // TODO: Send a stateless reset when appropriate. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.3 + return + } + var now time.Time + if l.testHooks != nil { + now = l.testHooks.timeNow() + } else { + now = time.Now() + } + var err error + c, err = l.newConn(now, serverSide, dstConnID, m.addr) + if err != nil { + // The accept queue is probably full. + // We could send a CONNECTION_CLOSE to the peer to reject the connection. + // Currently, we just drop the datagram. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-5.2.2-5 + return + } + } + + // TODO: This can block the listener while waiting for the conn to accept the dgram. + // Think about buffering between the receive loop and the conn. + c.sendMsg(m) +} + +func (l *Listener) sendDatagram(p []byte, addr netip.AddrPort) error { + _, err := l.udpConn.WriteToUDPAddrPort(p, addr) + return err +} diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go new file mode 100644 index 0000000000..a6e0b3464c --- /dev/null +++ b/internal/quic/listener_test.go @@ -0,0 +1,88 @@ +// 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" + "context" + "io" + "testing" +) + +func TestConnect(t *testing.T) { + newLocalConnPair(t, &Config{}, &Config{}) +} + +func TestStreamTransfer(t *testing.T) { + ctx := context.Background() + cli, srv := newLocalConnPair(t, &Config{}, &Config{}) + data := makeTestData(1 << 20) + + srvdone := make(chan struct{}) + go func() { + defer close(srvdone) + s, err := srv.AcceptStream(ctx) + if err != nil { + t.Errorf("AcceptStream: %v", err) + return + } + b, err := io.ReadAll(s) + if err != nil { + t.Errorf("io.ReadAll(s): %v", err) + return + } + if !bytes.Equal(b, data) { + t.Errorf("read data mismatch (got %v bytes, want %v", len(b), len(data)) + } + if err := s.Close(); err != nil { + t.Errorf("s.Close() = %v", err) + } + }() + + s, err := cli.NewStream(ctx) + if err != nil { + t.Fatalf("NewStream: %v", err) + } + n, err := io.Copy(s, bytes.NewBuffer(data)) + if n != int64(len(data)) || err != nil { + t.Fatalf("io.Copy(s, data) = %v, %v; want %v, nil", n, err, len(data)) + } + if err := s.Close(); err != nil { + t.Fatalf("s.Close() = %v", err) + } +} + +func newLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverConn *Conn) { + t.Helper() + ctx := context.Background() + l1 := newLocalListener(t, serverSide, conf1) + l2 := newLocalListener(t, clientSide, conf2) + c2, err := l2.Dial(ctx, "udp", l1.LocalAddr().String()) + if err != nil { + t.Fatal(err) + } + c1, err := l1.Accept(ctx) + if err != nil { + t.Fatal(err) + } + return c2, c1 +} + +func newLocalListener(t *testing.T, side connSide, conf *Config) *Listener { + t.Helper() + if conf.TLSConfig == nil { + conf.TLSConfig = newTestTLSConfig(side) + } + l, err := Listen("udp", "127.0.0.1:0", conf) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + l.Close(context.Background()) + }) + return l +} diff --git a/internal/quic/tls.go b/internal/quic/tls.go index 584316f0e4..1d07f17e42 100644 --- a/internal/quic/tls.go +++ b/internal/quic/tls.go @@ -73,6 +73,7 @@ func (c *Conn) handleTLSEvents(now time.Time) error { // https://www.rfc-editor.org/rfc/rfc9001#section-4.1.2-1 c.confirmHandshake(now) } + close(c.readyc) case tls.QUICTransportParameters: params, err := unmarshalTransportParams(e.Data) if err != nil { From ea4a2ff46a49439cba28d010c235b66555a5f25d Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 15 Sep 2023 17:03:11 -0700 Subject: [PATCH 058/168] quic: fix detection of reserved bits in 1-RTT packets The reserved bits are different for handshake and 1-RT packets. We were incorrectly checking the same bits for both. Remove the reservedBits field from longPacket/shortPacket. The packet parse functions remove header protection from the input packet, so the caller can just check the first byte of the packet directly. For golang/go#58547 Change-Id: Iee9ca5e88df140f115f63f63b5a0ea8d1ae02b95 Reviewed-on: https://go-review.googlesource.com/c/net/+/528697 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_recv.go | 6 ++++-- internal/quic/packet.go | 27 +++++++++++++-------------- internal/quic/packet_parser.go | 6 ------ 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index b866d8a6db..92ee8ea10f 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -50,7 +50,8 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa if n < 0 { return -1 } - if p.reservedBits != 0 { + if buf[0]&reservedLongBits != 0 { + // Reserved header bits must be 0. // https://www.rfc-editor.org/rfc/rfc9000#section-17.2-8.2.1 c.abort(now, localTransportError(errProtocolViolation)) return -1 @@ -89,7 +90,8 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int { if n < 0 { return -1 } - if p.reservedBits != 0 { + if buf[0]&reserved1RTTBits != 0 { + // Reserved header bits must be 0. // https://www.rfc-editor.org/rfc/rfc9000#section-17.3.1-4.8.1 c.abort(now, localTransportError(errProtocolViolation)) return -1 diff --git a/internal/quic/packet.go b/internal/quic/packet.go index 00c6714426..a1bcead97e 100644 --- a/internal/quic/packet.go +++ b/internal/quic/packet.go @@ -40,10 +40,11 @@ func (p packetType) String() string { // Bits set in the first byte of a packet. const ( - headerFormLong = 0x80 // https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2-3.2.1 - headerFormShort = 0x00 // https://www.rfc-editor.org/rfc/rfc9000.html#section-17.3.1-4.2.1 - fixedBit = 0x40 // https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2-3.4.1 - reservedBits = 0x0c // https://www.rfc-editor.org/rfc/rfc9000#section-17.2-8.2.1 + headerFormLong = 0x80 // https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2-3.2.1 + headerFormShort = 0x00 // https://www.rfc-editor.org/rfc/rfc9000.html#section-17.3.1-4.2.1 + fixedBit = 0x40 // https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2-3.4.1 + reservedLongBits = 0x0c // https://www.rfc-editor.org/rfc/rfc9000#section-17.2-8.2.1 + reserved1RTTBits = 0x18 // https://www.rfc-editor.org/rfc/rfc9000#section-17.3.1-4.8.1 ) // Long Packet Type bits. @@ -157,13 +158,12 @@ func dstConnIDForDatagram(pkt []byte) (id []byte, ok bool) { // A longPacket is a long header packet. type longPacket struct { - ptype packetType - reservedBits uint8 - version uint32 - num packetNumber - dstConnID []byte - srcConnID []byte - payload []byte + ptype packetType + version uint32 + num packetNumber + dstConnID []byte + srcConnID []byte + payload []byte // The extra data depends on the packet type: // Initial: Token. @@ -173,7 +173,6 @@ type longPacket struct { // A shortPacket is a short header (1-RTT) packet. type shortPacket struct { - reservedBits uint8 - num packetNumber - payload []byte + num packetNumber + payload []byte } diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index ca5b37b2bd..43238826f1 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -97,9 +97,6 @@ func parseLongHeaderPacket(pkt []byte, k keys, pnumMax packetNumber) (p longPack if err != nil { return longPacket{}, -1 } - // Reserved bits should always be zero, but this is handled - // as a protocol-level violation by the caller rather than a parse error. - p.reservedBits = pkt[0] & reservedBits } return p, len(pkt) } @@ -152,9 +149,6 @@ func parse1RTTPacket(pkt []byte, k keys, dstConnIDLen int, pnumMax packetNumber) if err != nil { return shortPacket{}, -1 } - // Reserved bits should always be zero, but this is handled - // as a protocol-level violation by the caller rather than a parse error. - p.reservedBits = pkt[0] & reservedBits return p, len(pkt) } From 6a4de22e0ea2f35ebeb0f36bf04f243acc86857b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 13 Sep 2023 11:44:47 -0700 Subject: [PATCH 059/168] quic: connection lifetime management Manage the closing and draining states. A connection enters the closing state after sending a CONNECTION_CLOSE frame to terminate the connection. A connection enters the draining state after receiving a CONNECTION_CLOSE frame. Handle retransmission of CONNECTION_CLOSE frames when in the closing state, and properly ignore received frames when in the draining state. RFC 9000, Section 10.2. For golang/go#58547 Change-Id: I550ca544bffc4de7c5626f87a32c8902d5e2bc86 Reviewed-on: https://go-review.googlesource.com/c/net/+/528016 Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn.go | 44 ++---- internal/quic/conn_close.go | 238 +++++++++++++++++++++++++++++++ internal/quic/conn_close_test.go | 186 ++++++++++++++++++++++++ internal/quic/conn_recv.go | 39 +++-- internal/quic/conn_send.go | 41 +++--- internal/quic/conn_test.go | 2 + internal/quic/errors.go | 8 +- internal/quic/listener.go | 8 +- internal/quic/tls.go | 2 +- internal/quic/tls_test.go | 2 + 10 files changed, 502 insertions(+), 68 deletions(-) create mode 100644 internal/quic/conn_close.go create mode 100644 internal/quic/conn_close_test.go diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 0063965df2..26c25f8956 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -27,19 +27,15 @@ type Conn struct { msgc chan any donec chan struct{} // closed when conn loop exits - readyc chan struct{} // closed when TLS handshake completes exited bool // set to make the conn loop exit immediately w packetWriter acks [numberSpaceCount]ackState // indexed by number space + lifetime lifetimeState connIDState connIDState loss lossState streams streamsState - // errForPeer is set when the connection is being closed. - errForPeer error - connCloseSent [numberSpaceCount]bool - // idleTimeout is the time at which the connection will be closed due to inactivity. // https://www.rfc-editor.org/rfc/rfc9000#section-10.1 maxIdleTimeout time.Duration @@ -79,7 +75,6 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. peerAddr: peerAddr, msgc: make(chan any, 1), donec: make(chan struct{}), - readyc: make(chan struct{}), testHooks: hooks, maxIdleTimeout: defaultMaxIdleTimeout, idleTimeout: now.Add(defaultMaxIdleTimeout), @@ -106,6 +101,7 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. const maxDatagramSize = 1200 c.loss.init(c.side, maxDatagramSize, now) c.streamsInit() + c.lifetimeInit() // TODO: initial_source_connection_id, retry_source_connection_id c.startTLS(now, initialConnID, transportParameters{ @@ -131,14 +127,6 @@ func (c *Conn) String() string { return fmt.Sprintf("quic.Conn(%v,->%v)", c.side, c.peerAddr) } -func (c *Conn) Close() error { - // TODO: Implement shutdown for real. - c.runOnLoop(func(now time.Time, c *Conn) { - c.exited = true - }) - return nil -} - // confirmHandshake is called when the handshake is confirmed. // https://www.rfc-editor.org/rfc/rfc9001#section-4.1.2 func (c *Conn) confirmHandshake(now time.Time) { @@ -241,8 +229,12 @@ func (c *Conn) loop(now time.Time) { // since the Initial and Handshake spaces always ack immediately. nextTimeout := sendTimeout nextTimeout = firstTime(nextTimeout, c.idleTimeout) - nextTimeout = firstTime(nextTimeout, c.loss.timer) - nextTimeout = firstTime(nextTimeout, c.acks[appDataSpace].nextAck) + if !c.isClosingOrDraining() { + nextTimeout = firstTime(nextTimeout, c.loss.timer) + nextTimeout = firstTime(nextTimeout, c.acks[appDataSpace].nextAck) + } else { + nextTimeout = firstTime(nextTimeout, c.lifetime.drainEndTime) + } var m any if hooks != nil { @@ -279,6 +271,11 @@ func (c *Conn) loop(now time.Time) { return } c.loss.advance(now, c.handleAckOrLoss) + if c.lifetimeAdvance(now) { + // The connection has completed the draining period, + // and may be shut down. + return + } case wakeEvent: // We're being woken up to try sending some frames. case func(time.Time, *Conn): @@ -350,21 +347,6 @@ func (c *Conn) waitOnDone(ctx context.Context, ch <-chan struct{}) error { return nil } -// abort terminates a connection with an error. -func (c *Conn) abort(now time.Time, err error) { - if c.errForPeer == nil { - c.errForPeer = err - } -} - -// exit fully terminates a connection immediately. -func (c *Conn) exit() { - c.runOnLoop(func(now time.Time, c *Conn) { - c.exited = true - }) - <-c.donec -} - // firstTime returns the earliest non-zero time, or zero if both times are zero. func firstTime(a, b time.Time) time.Time { switch { diff --git a/internal/quic/conn_close.go b/internal/quic/conn_close.go new file mode 100644 index 0000000000..ec0b7a3270 --- /dev/null +++ b/internal/quic/conn_close.go @@ -0,0 +1,238 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "errors" + "time" +) + +// lifetimeState tracks the state of a connection. +// +// This is fairly coupled to the rest of a Conn, but putting it in a struct of its own helps +// reason about operations that cause state transitions. +type lifetimeState struct { + readyc chan struct{} // closed when TLS handshake completes + drainingc chan struct{} // closed when entering the draining state + + // Possible states for the connection: + // + // Alive: localErr and finalErr are both nil. + // + // Closing: localErr is non-nil and finalErr is nil. + // We have sent a CONNECTION_CLOSE to the peer or are about to + // (if connCloseSentTime is zero) and are waiting for the peer to respond. + // drainEndTime is set to the time the closing state ends. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.1 + // + // Draining: finalErr is non-nil. + // If localErr is nil, we're waiting for the user to provide us with a final status + // to send to the peer. + // Otherwise, we've either sent a CONNECTION_CLOSE to the peer or are about to + // (if connCloseSentTime is zero). + // drainEndTime is set to the time the draining state ends. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2 + localErr error // error sent to the peer + finalErr error // error sent by the peer, or transport error; always set before draining + + connCloseSentTime time.Time // send time of last CONNECTION_CLOSE frame + connCloseDelay time.Duration // delay until next CONNECTION_CLOSE frame sent + drainEndTime time.Time // time the connection exits the draining state +} + +func (c *Conn) lifetimeInit() { + c.lifetime.readyc = make(chan struct{}) + c.lifetime.drainingc = make(chan struct{}) +} + +var errNoPeerResponse = errors.New("peer did not respond to CONNECTION_CLOSE") + +// advance is called when time passes. +func (c *Conn) lifetimeAdvance(now time.Time) (done bool) { + if c.lifetime.drainEndTime.IsZero() || c.lifetime.drainEndTime.After(now) { + return false + } + // The connection drain period has ended, and we can shut down. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2-7 + c.lifetime.drainEndTime = time.Time{} + if c.lifetime.finalErr == nil { + // The peer never responded to our CONNECTION_CLOSE. + c.enterDraining(errNoPeerResponse) + } + return true +} + +// confirmHandshake is called when the TLS handshake completes. +func (c *Conn) handshakeDone() { + close(c.lifetime.readyc) +} + +// isDraining reports whether the conn is in the draining state. +// +// The draining state is entered once an endpoint receives a CONNECTION_CLOSE frame. +// The endpoint will no longer send any packets, but we retain knowledge of the connection +// until the end of the drain period to ensure we discard packets for the connection +// rather than treating them as starting a new connection. +// +// https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2 +func (c *Conn) isDraining() bool { + return c.lifetime.finalErr != nil +} + +// isClosingOrDraining reports whether the conn is in the closing or draining states. +func (c *Conn) isClosingOrDraining() bool { + return c.lifetime.localErr != nil || c.lifetime.finalErr != nil +} + +// sendOK reports whether the conn can send frames at this time. +func (c *Conn) sendOK(now time.Time) bool { + if !c.isClosingOrDraining() { + return true + } + // We are closing or draining. + if c.lifetime.localErr == nil { + // We're waiting for the user to close the connection, providing us with + // a final status to send to the peer. + return false + } + // Past this point, returning true will result in the conn sending a CONNECTION_CLOSE + // due to localErr being set. + if c.lifetime.drainEndTime.IsZero() { + // The closing and draining states should last for at least three times + // the current PTO interval. We currently use exactly that minimum. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2-5 + // + // The drain period begins when we send or receive a CONNECTION_CLOSE, + // whichever comes first. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2-3 + c.lifetime.drainEndTime = now.Add(3 * c.loss.ptoBasePeriod()) + } + if c.lifetime.connCloseSentTime.IsZero() { + // We haven't sent a CONNECTION_CLOSE yet. Do so. + // Either we're initiating an immediate close + // (and will enter the closing state as soon as we send CONNECTION_CLOSE), + // or we've read a CONNECTION_CLOSE from our peer + // (and may send one CONNECTION_CLOSE before entering the draining state). + // + // Set the initial delay before we will send another CONNECTION_CLOSE. + // + // RFC 9000 states that we should rate limit CONNECTION_CLOSE frames, + // but leaves the implementation of the limit up to us. Here, we start + // with the same delay as the PTO timer (RFC 9002, Section 6.2.1), + // not including max_ack_delay, and double it on every CONNECTION_CLOSE sent. + c.lifetime.connCloseDelay = c.loss.rtt.smoothedRTT + max(4*c.loss.rtt.rttvar, timerGranularity) + c.lifetime.drainEndTime = now.Add(3 * c.loss.ptoBasePeriod()) + return true + } + if c.isDraining() { + // We are in the draining state, and will send no more packets. + return false + } + maxRecvTime := c.acks[initialSpace].maxRecvTime + if t := c.acks[handshakeSpace].maxRecvTime; t.After(maxRecvTime) { + maxRecvTime = t + } + if t := c.acks[appDataSpace].maxRecvTime; t.After(maxRecvTime) { + maxRecvTime = t + } + if maxRecvTime.Before(c.lifetime.connCloseSentTime.Add(c.lifetime.connCloseDelay)) { + // After sending CONNECTION_CLOSE, ignore packets from the peer for + // a delay. On the next packet received after the delay, send another + // CONNECTION_CLOSE. + return false + } + c.lifetime.connCloseSentTime = now + c.lifetime.connCloseDelay *= 2 + return true +} + +// enterDraining enters the draining state. +func (c *Conn) enterDraining(err error) { + if c.isDraining() { + return + } + if e, ok := c.lifetime.localErr.(localTransportError); ok && transportError(e) != errNo { + // If we've terminated the connection due to a peer protocol violation, + // record the final error on the connection as our reason for termination. + c.lifetime.finalErr = c.lifetime.localErr + } else { + c.lifetime.finalErr = err + } + close(c.lifetime.drainingc) + c.streams.queue.close(c.lifetime.finalErr) +} + +func (c *Conn) waitReady(ctx context.Context) error { + select { + case <-c.lifetime.readyc: + return nil + case <-c.lifetime.drainingc: + return c.lifetime.finalErr + case <-ctx.Done(): + return ctx.Err() + } +} + +// Close closes the connection. +// +// Close is equivalent to: +// +// conn.Abort(nil) +// err := conn.Wait(context.Background()) +func (c *Conn) Close() error { + c.Abort(nil) + <-c.lifetime.drainingc + return c.lifetime.finalErr +} + +// Wait waits for the peer to close the connection. +// +// If the connection is closed locally and the peer does not close its end of the connection, +// Wait will return with a non-nil error after the drain period expires. +// +// If the peer closes the connection with a NO_ERROR transport error, Wait returns nil. +// If the peer closes the connection with an application error, Wait returns an ApplicationError +// containing the peer's error code and reason. +// If the peer closes the connection with any other status, Wait returns a non-nil error. +func (c *Conn) Wait(ctx context.Context) error { + if err := c.waitOnDone(ctx, c.lifetime.drainingc); err != nil { + return err + } + return c.lifetime.finalErr +} + +// Abort closes the connection and returns immediately. +// +// If err is nil, Abort sends a transport error of NO_ERROR to the peer. +// If err is an ApplicationError, Abort sends its error code and text. +// Otherwise, Abort sends a transport error of APPLICATION_ERROR with the error's text. +func (c *Conn) Abort(err error) { + if err == nil { + err = localTransportError(errNo) + } + c.runOnLoop(func(now time.Time, c *Conn) { + c.abort(now, err) + }) +} + +// abort terminates a connection with an error. +func (c *Conn) abort(now time.Time, err error) { + if c.lifetime.localErr != nil { + return // already closing + } + c.lifetime.localErr = err +} + +// exit fully terminates a connection immediately. +func (c *Conn) exit() { + c.runOnLoop(func(now time.Time, c *Conn) { + c.enterDraining(errors.New("connection closed")) + c.exited = true + }) + <-c.donec +} diff --git a/internal/quic/conn_close_test.go b/internal/quic/conn_close_test.go new file mode 100644 index 0000000000..20c00e754c --- /dev/null +++ b/internal/quic/conn_close_test.go @@ -0,0 +1,186 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "crypto/tls" + "errors" + "testing" + "time" +) + +func TestConnCloseResponseBackoff(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.handshake() + + tc.conn.Abort(nil) + tc.wantFrame("aborting connection generates CONN_CLOSE", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errNo, + }) + + waiting := runAsync(tc, func(ctx context.Context) (struct{}, error) { + return struct{}{}, tc.conn.Wait(ctx) + }) + if _, err := waiting.result(); err != errNotDone { + t.Errorf("conn.Wait() = %v, want still waiting", err) + } + + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.wantIdle("packets received immediately after CONN_CLOSE receive no response") + + tc.advance(1100 * time.Microsecond) + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.wantFrame("receiving packet 1.1ms after CONN_CLOSE generates another CONN_CLOSE", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errNo, + }) + + tc.advance(1100 * time.Microsecond) + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.wantIdle("no response to packet, because CONN_CLOSE backoff is now 2ms") + + tc.advance(1000 * time.Microsecond) + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.wantFrame("2ms since last CONN_CLOSE, receiving a packet generates another CONN_CLOSE", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errNo, + }) + if _, err := waiting.result(); err != errNotDone { + t.Errorf("conn.Wait() = %v, want still waiting", err) + } + + tc.advance(100000 * time.Microsecond) + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.wantIdle("drain timer expired, no more responses") + + if _, err := waiting.result(); !errors.Is(err, errNoPeerResponse) { + t.Errorf("blocked conn.Wait() = %v, want errNoPeerResponse", err) + } + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errNoPeerResponse) { + t.Errorf("non-blocking conn.Wait() = %v, want errNoPeerResponse", err) + } +} + +func TestConnCloseWithPeerResponse(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.handshake() + + tc.conn.Abort(nil) + tc.wantFrame("aborting connection generates CONN_CLOSE", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errNo, + }) + + waiting := runAsync(tc, func(ctx context.Context) (struct{}, error) { + return struct{}{}, tc.conn.Wait(ctx) + }) + if _, err := waiting.result(); err != errNotDone { + t.Errorf("conn.Wait() = %v, want still waiting", err) + } + + tc.writeFrames(packetType1RTT, debugFrameConnectionCloseApplication{ + code: 20, + }) + + wantErr := &ApplicationError{ + Code: 20, + } + if _, err := waiting.result(); !errors.Is(err, wantErr) { + t.Errorf("blocked conn.Wait() = %v, want %v", err, wantErr) + } + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, wantErr) { + t.Errorf("non-blocking conn.Wait() = %v, want %v", err, wantErr) + } +} + +func TestConnClosePeerCloses(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.handshake() + + wantErr := &ApplicationError{ + Code: 42, + Reason: "why?", + } + tc.writeFrames(packetType1RTT, debugFrameConnectionCloseApplication{ + code: wantErr.Code, + reason: wantErr.Reason, + }) + tc.wantIdle("CONN_CLOSE response not sent until user closes this side") + + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, wantErr) { + t.Errorf("conn.Wait() = %v, want %v", err, wantErr) + } + + tc.conn.Abort(&ApplicationError{ + Code: 9, + Reason: "because", + }) + tc.wantFrame("CONN_CLOSE sent after user closes connection", + packetType1RTT, debugFrameConnectionCloseApplication{ + code: 9, + reason: "because", + }) +} + +func TestConnCloseReceiveInInitial(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.wantFrame("client sends Initial CRYPTO frame", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errConnectionRefused, + }) + tc.wantIdle("CONN_CLOSE response not sent until user closes this side") + + wantErr := peerTransportError{code: errConnectionRefused} + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, wantErr) { + t.Errorf("conn.Wait() = %v, want %v", err, wantErr) + } + + tc.conn.Abort(&ApplicationError{Code: 1}) + tc.wantFrame("CONN_CLOSE in Initial frame is APPLICATION_ERROR", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errApplicationError, + }) + tc.wantIdle("no more frames to send") +} + +func TestConnCloseReceiveInHandshake(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.ignoreFrame(frameTypeAck) + tc.wantFrame("client sends Initial CRYPTO frame", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeHandshake, debugFrameConnectionCloseTransport{ + code: errConnectionRefused, + }) + tc.wantIdle("CONN_CLOSE response not sent until user closes this side") + + wantErr := peerTransportError{code: errConnectionRefused} + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, wantErr) { + t.Errorf("conn.Wait() = %v, want %v", err, wantErr) + } + + // The conn has Initial and Handshake keys, so it will send CONN_CLOSE in both spaces. + tc.conn.Abort(&ApplicationError{Code: 1}) + tc.wantFrame("CONN_CLOSE in Initial frame is APPLICATION_ERROR", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errApplicationError, + }) + tc.wantFrame("CONN_CLOSE in Handshake frame is APPLICATION_ERROR", + packetTypeHandshake, debugFrameConnectionCloseTransport{ + code: errApplicationError, + }) + tc.wantIdle("no more frames to send") +} diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 92ee8ea10f..64e5f985fa 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -13,6 +13,9 @@ import ( func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { buf := dgram.b c.loss.datagramReceived(now, len(buf)) + if c.isDraining() { + return + } for len(buf) > 0 { var n int ptype := getPacketType(buf) @@ -220,15 +223,13 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, } n = c.handleRetireConnectionIDFrame(now, space, payload) case frameTypeConnectionCloseTransport: - // CONNECTION_CLOSE is OK in all spaces. - _, _, _, n = consumeConnectionCloseTransportFrame(payload) - // TODO: https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2 - c.abort(now, localTransportError(errNo)) + // Transport CONNECTION_CLOSE is OK in all spaces. + n = c.handleConnectionCloseTransportFrame(now, payload) case frameTypeConnectionCloseApplication: - // CONNECTION_CLOSE is OK in all spaces. - _, _, n = consumeConnectionCloseApplicationFrame(payload) - // TODO: https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2 - c.abort(now, localTransportError(errNo)) + if !frameOK(c, ptype, __01) { + return + } + n = c.handleConnectionCloseApplicationFrame(now, payload) case frameTypeHandshakeDone: if !frameOK(c, ptype, ___1) { return @@ -385,6 +386,24 @@ func (c *Conn) handleRetireConnectionIDFrame(now time.Time, space numberSpace, p return n } +func (c *Conn) handleConnectionCloseTransportFrame(now time.Time, payload []byte) int { + code, _, reason, n := consumeConnectionCloseTransportFrame(payload) + if n < 0 { + return -1 + } + c.enterDraining(peerTransportError{code: code, reason: reason}) + return n +} + +func (c *Conn) handleConnectionCloseApplicationFrame(now time.Time, payload []byte) int { + code, reason, n := consumeConnectionCloseApplicationFrame(payload) + if n < 0 { + return -1 + } + c.enterDraining(&ApplicationError{Code: code, Reason: reason}) + return n +} + func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payload []byte) int { if c.side == serverSide { // Clients should never send HANDSHAKE_DONE. @@ -392,6 +411,8 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa c.abort(now, localTransportError(errProtocolViolation)) return -1 } - c.confirmHandshake(now) + if !c.isClosingOrDraining() { + c.confirmHandshake(now) + } return 1 } diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 9d315fb392..853c8453fd 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -16,6 +16,8 @@ import ( // // If sending is blocked by pacing, it returns the next time // a datagram may be sent. +// +// If sending is blocked indefinitely, it returns the zero Time. func (c *Conn) maybeSend(now time.Time) (next time.Time) { // Assumption: The congestion window is not underutilized. // If congestion control, pacing, and anti-amplification all permit sending, @@ -39,6 +41,9 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // If anti-amplification blocks sending, then no packet can be sent. return next } + if !c.sendOK(now) { + return time.Time{} + } // We may still send ACKs, even if congestion control or pacing limit sending. // Prepare to write a datagram of at most maxSendSize bytes. @@ -162,23 +167,8 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, limit ccLimit) { - if c.errForPeer != nil { - // This is the bare minimum required to send a CONNECTION_CLOSE frame - // when closing a connection immediately, for example in response to a - // protocol error. - // - // This does not handle the closing and draining states - // (https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2), - // but it's enough to let us write tests that result in a CONNECTION_CLOSE, - // and have those tests still pass when we finish implementing - // connection shutdown. - // - // TODO: Finish implementing connection shutdown. - if !c.connCloseSent[space] { - c.exited = true - c.appendConnectionCloseFrame(c.errForPeer) - c.connCloseSent[space] = true - } + if c.lifetime.localErr != nil { + c.appendConnectionCloseFrame(now, space, c.lifetime.localErr) return } @@ -322,11 +312,20 @@ func (c *Conn) appendAckFrame(now time.Time, space numberSpace) bool { return c.w.appendAckFrame(seen, d) } -func (c *Conn) appendConnectionCloseFrame(err error) { - // TODO: Send application errors. +func (c *Conn) appendConnectionCloseFrame(now time.Time, space numberSpace, err error) { + c.lifetime.connCloseSentTime = now switch e := err.(type) { case localTransportError: c.w.appendConnectionCloseTransportFrame(transportError(e), 0, "") + case *ApplicationError: + if space != appDataSpace { + // "CONNECTION_CLOSE frames signaling application errors (type 0x1d) + // MUST only appear in the application data packet number space." + // https://www.rfc-editor.org/rfc/rfc9000#section-12.5-2.2 + c.w.appendConnectionCloseTransportFrame(errApplicationError, 0, "") + } else { + c.w.appendConnectionCloseApplicationFrame(e.Code, e.Reason) + } default: // TLS alerts are sent using error codes [0x0100,0x01ff). // https://www.rfc-editor.org/rfc/rfc9000#section-20.1-2.36.1 @@ -335,8 +334,8 @@ func (c *Conn) appendConnectionCloseFrame(err error) { // tls.AlertError is a uint8, so this can't exceed 0x01ff. code := errTLSBase + transportError(alert) c.w.appendConnectionCloseTransportFrame(code, 0, "") - return + } else { + c.w.appendConnectionCloseTransportFrame(errInternal, 0, "") } - c.w.appendConnectionCloseTransportFrame(errInternal, 0, "") } } diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index cdbd4669e9..4228ce7215 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -304,6 +304,8 @@ func (tc *testConn) wait() { select { case <-idlec: case <-tc.conn.donec: + // We may have async ops that can proceed now that the conn is done. + tc.wakeAsync() } if fail { panic(fail) diff --git a/internal/quic/errors.go b/internal/quic/errors.go index f156859325..8e01bb7cb7 100644 --- a/internal/quic/errors.go +++ b/internal/quic/errors.go @@ -114,7 +114,13 @@ type ApplicationError struct { Reason string } -func (e ApplicationError) Error() string { +func (e *ApplicationError) Error() string { // TODO: Include the Reason string here, but sanitize it first. return fmt.Sprintf("AppError %v", e.Code) } + +// Is reports a match if err is an *ApplicationError with a matching Code. +func (e *ApplicationError) Is(err error) bool { + e2, ok := err.(*ApplicationError) + return ok && e2.Code == e.Code +} diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 9869f6e228..a84286e894 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -141,11 +141,9 @@ func (l *Listener) Dial(ctx context.Context, network, address string) (*Conn, er if err != nil { return nil, err } - select { - case <-c.readyc: - case <-ctx.Done(): - c.Close() - return nil, ctx.Err() + if err := c.waitReady(ctx); err != nil { + c.Abort(nil) + return nil, err } return c, nil } diff --git a/internal/quic/tls.go b/internal/quic/tls.go index 1d07f17e42..e3a430ed72 100644 --- a/internal/quic/tls.go +++ b/internal/quic/tls.go @@ -73,7 +73,7 @@ func (c *Conn) handleTLSEvents(now time.Time) error { // https://www.rfc-editor.org/rfc/rfc9001#section-4.1.2-1 c.confirmHandshake(now) } - close(c.readyc) + c.handshakeDone() case tls.QUICTransportParameters: params, err := unmarshalTransportParams(e.Data) if err != nil { diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 0f22f4fb31..1c7b36d334 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -353,6 +353,7 @@ func TestConnKeysDiscardedClient(t *testing.T) { tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{code: errInternal}) + tc.conn.Abort(nil) tc.wantFrame("client closes connection after 1-RTT CONNECTION_CLOSE", packetType1RTT, debugFrameConnectionCloseTransport{ code: errNo, @@ -406,6 +407,7 @@ func TestConnKeysDiscardedServer(t *testing.T) { tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{code: errInternal}) + tc.conn.Abort(nil) tc.wantFrame("server closes connection after 1-RTT CONNECTION_CLOSE", packetType1RTT, debugFrameConnectionCloseTransport{ code: errNo, From 008c0af3180c4e981012a3d9d42a5ae435b45ba4 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 18 Sep 2023 08:52:27 -0700 Subject: [PATCH 060/168] quic: refactor keys for key updates Refactor how we store encryption keys in preparation for adding support for key updates. Previously, we had a single "keys" type containing header and packet protection key material. With key update, the 1-RTT header protection keys are consistent across the lifetime of a connection, while packet protection keys vary. Separate out the header and packet protection keys into distinct types. Add "fixed" key types for keys which remain fixed across a connection's lifetime and do not update. For the moment, 1-RTT keys are still fixed. Remove a number of can-never-happen error returns from key handling paths. We were previously inconsistent about where to panic and where to return an error on these paths; we now consistently panic in paths where errors can only occur due to a bug. (For example, attempting to create an AEAD with an incorrect secret size.) No functional changes, this is purely refactoring. For golang/go#58547 Change-Id: I49f83091517186e452845b65a1597add60e5fc92 Reviewed-on: https://go-review.googlesource.com/c/net/+/529155 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn.go | 17 +- internal/quic/conn_recv.go | 14 +- internal/quic/conn_send.go | 17 +- internal/quic/conn_test.go | 115 ++++--- internal/quic/packet_codec_test.go | 24 +- internal/quic/packet_parser.go | 4 +- internal/quic/packet_protection.go | 379 ++++++++++++++---------- internal/quic/packet_protection_test.go | 13 +- internal/quic/packet_writer.go | 15 +- internal/quic/tls.go | 66 ++--- 10 files changed, 384 insertions(+), 280 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 26c25f8956..4565e1a58c 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -42,10 +42,11 @@ type Conn struct { idleTimeout time.Time // Packet protection keys, CRYPTO streams, and TLS state. - rkeys [numberSpaceCount]keys - wkeys [numberSpaceCount]keys - crypto [numberSpaceCount]cryptoStream - tls *tls.QUICConn + keysInitial fixedKeyPair + keysHandshake fixedKeyPair + keysAppData fixedKeyPair + crypto [numberSpaceCount]cryptoStream + tls *tls.QUICConn // handshakeConfirmed is set when the handshake is confirmed. // For server connections, it tracks sending HANDSHAKE_DONE. @@ -156,8 +157,12 @@ func (c *Conn) confirmHandshake(now time.Time) { // discardKeys discards unused packet protection keys. // https://www.rfc-editor.org/rfc/rfc9001#section-4.9 func (c *Conn) discardKeys(now time.Time, space numberSpace) { - c.rkeys[space].discard() - c.wkeys[space].discard() + switch space { + case initialSpace: + c.keysInitial.discard() + case handshakeSpace: + c.keysHandshake.discard() + } c.loss.discardKeys(now, space) } diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 64e5f985fa..d1fa52d990 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -26,9 +26,9 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { // https://www.rfc-editor.org/rfc/rfc9000#section-14.1-4 return } - n = c.handleLongHeader(now, ptype, initialSpace, buf) + n = c.handleLongHeader(now, ptype, initialSpace, c.keysInitial.r, buf) case packetTypeHandshake: - n = c.handleLongHeader(now, ptype, handshakeSpace, buf) + n = c.handleLongHeader(now, ptype, handshakeSpace, c.keysHandshake.r, buf) case packetType1RTT: n = c.handle1RTT(now, buf) default: @@ -43,13 +43,13 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { } } -func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpace, buf []byte) int { - if !c.rkeys[space].isSet() { +func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpace, k fixedKeys, buf []byte) int { + if !k.isSet() { return skipLongHeaderPacket(buf) } pnumMax := c.acks[space].largestSeen() - p, n := parseLongHeaderPacket(buf, c.rkeys[space], pnumMax) + p, n := parseLongHeaderPacket(buf, k, pnumMax) if n < 0 { return -1 } @@ -82,14 +82,14 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa } func (c *Conn) handle1RTT(now time.Time, buf []byte) int { - if !c.rkeys[appDataSpace].isSet() { + if !c.keysAppData.canRead() { // 1-RTT packets extend to the end of the datagram, // so skip the remainder of the datagram if we can't parse this. return len(buf) } pnumMax := c.acks[appDataSpace].largestSeen() - p, n := parse1RTTPacket(buf, c.rkeys[appDataSpace], connIDLen, pnumMax) + p, n := parse1RTTPacket(buf, c.keysAppData.r, connIDLen, pnumMax) if n < 0 { return -1 } diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 853c8453fd..58a3df1078 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -59,7 +59,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // Initial packet. pad := false var sentInitial *sentPacket - if k := c.wkeys[initialSpace]; k.isSet() { + if c.keysInitial.canWrite() { pnumMaxAcked := c.acks[initialSpace].largestSeen() pnum := c.loss.nextNumber(initialSpace) p := longPacket{ @@ -74,7 +74,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { if logPackets { logSentPacket(c, packetTypeInitial, pnum, p.srcConnID, p.dstConnID, c.w.payload()) } - sentInitial = c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, k, p) + sentInitial = c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysInitial.w, p) if sentInitial != nil { // Client initial packets need to be sent in a datagram padded to // at least 1200 bytes. We can't add the padding yet, however, @@ -86,7 +86,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } // Handshake packet. - if k := c.wkeys[handshakeSpace]; k.isSet() { + if c.keysHandshake.canWrite() { pnumMaxAcked := c.acks[handshakeSpace].largestSeen() pnum := c.loss.nextNumber(handshakeSpace) p := longPacket{ @@ -101,7 +101,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { if logPackets { logSentPacket(c, packetTypeHandshake, pnum, p.srcConnID, p.dstConnID, c.w.payload()) } - if sent := c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, k, p); sent != nil { + if sent := c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysHandshake.w, p); sent != nil { c.loss.packetSent(now, handshakeSpace, sent) if c.side == clientSide { // "[...] a client MUST discard Initial keys when it first @@ -113,7 +113,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } // 1-RTT packet. - if k := c.wkeys[appDataSpace]; k.isSet() { + if c.keysAppData.canWrite() { pnumMaxAcked := c.acks[appDataSpace].largestSeen() pnum := c.loss.nextNumber(appDataSpace) c.w.start1RTTPacket(pnum, pnumMaxAcked, dstConnID) @@ -128,7 +128,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { if logPackets { logSentPacket(c, packetType1RTT, pnum, nil, dstConnID, c.w.payload()) } - if sent := c.w.finish1RTTPacket(pnum, pnumMaxAcked, dstConnID, k); sent != nil { + if sent := c.w.finish1RTTPacket(pnum, pnumMaxAcked, dstConnID, c.keysAppData.w); sent != nil { c.loss.packetSent(now, appDataSpace, sent) } } @@ -157,7 +157,10 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { sentInitial.inFlight = true } } - if k := c.wkeys[initialSpace]; k.isSet() { + // If we're a client and this Initial packet is coalesced + // with a Handshake packet, then we've discarded Initial keys + // since constructing the packet and shouldn't record it as in-flight. + if c.keysInitial.canWrite() { c.loss.packetSent(now, initialSpace, sentInitial) } } diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 4228ce7215..3fef62d507 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -113,15 +113,18 @@ type testConn struct { timerLastFired time.Time idlec chan struct{} // only accessed on the conn's loop - // Read and write keys are distinct from the conn's keys, + // Keys are distinct from the conn's keys, // because the test may know about keys before the conn does. // For example, when sending a datagram with coalesced // Initial and Handshake packets to a client conn, // we use Handshake keys to encrypt the packet. // The client only acquires those keys when it processes // the Initial packet. - rkeys [numberSpaceCount]keyData // for packets sent to the conn - wkeys [numberSpaceCount]keyData // for packets sent by the conn + keysInitial fixedKeyPair + keysHandshake fixedKeyPair + keysAppData fixedKeyPair + rsecrets [numberSpaceCount]testKeySecret + wsecrets [numberSpaceCount]testKeySecret // testConn uses a test hook to snoop on the conn's TLS events. // CRYPTO data produced by the conn's QUICConn is placed in @@ -156,10 +159,9 @@ type testConn struct { asyncTestState } -type keyData struct { +type testKeySecret struct { suite uint16 secret []byte - k keys } // newTestConn creates a Conn for testing. @@ -225,8 +227,8 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { } tc.conn = conn - tc.wkeys[initialSpace].k = conn.wkeys[initialSpace] - tc.rkeys[initialSpace].k = conn.rkeys[initialSpace] + tc.keysInitial.r = conn.keysInitial.w + tc.keysInitial.w = conn.keysInitial.r tc.wait() return tc @@ -611,14 +613,19 @@ func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { for _, f := range p.frames { f.write(&w) } - space := spaceForPacketType(p.ptype) - if !tc.rkeys[space].k.isSet() { - tc.t.Fatalf("sending packet with no %v keys available", space) - return nil - } w.appendPaddingTo(pad) if p.ptype != packetType1RTT { - w.finishProtectedLongHeaderPacket(pnumMaxAcked, tc.rkeys[space].k, longPacket{ + var k fixedKeyPair + switch p.ptype { + case packetTypeInitial: + k = tc.keysInitial + case packetTypeHandshake: + k = tc.keysHandshake + } + if !k.canWrite() { + tc.t.Fatalf("sending %v packet with no write key", p.ptype) + } + w.finishProtectedLongHeaderPacket(pnumMaxAcked, k.w, longPacket{ ptype: p.ptype, version: p.version, num: p.num, @@ -626,7 +633,10 @@ func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { srcConnID: p.srcConnID, }) } else { - w.finish1RTTPacket(p.num, pnumMaxAcked, p.dstConnID, tc.rkeys[space].k) + if !tc.keysAppData.canWrite() { + tc.t.Fatalf("sending %v packet with no write key", p.ptype) + } + w.finish1RTTPacket(p.num, pnumMaxAcked, p.dstConnID, tc.keysAppData.w) } return w.datagram() } @@ -642,13 +652,19 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { break } ptype := getPacketType(buf) - space := spaceForPacketType(ptype) - if !tc.wkeys[space].k.isSet() { - tc.t.Fatalf("no keys for space %v, packet type %v", space, ptype) - } if isLongHeader(buf[0]) { + var k fixedKeyPair + switch ptype { + case packetTypeInitial: + k = tc.keysInitial + case packetTypeHandshake: + k = tc.keysHandshake + } + if !k.canRead() { + tc.t.Fatalf("reading %v packet with no read key", ptype) + } var pnumMax packetNumber // TODO: Track packet numbers. - p, n := parseLongHeaderPacket(buf, tc.wkeys[space].k, pnumMax) + p, n := parseLongHeaderPacket(buf, k.r, pnumMax) if n < 0 { tc.t.Fatalf("packet parse error") } @@ -666,8 +682,11 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { }) buf = buf[n:] } else { + if !tc.keysAppData.canRead() { + tc.t.Fatalf("reading 1-RTT packet with no read key") + } var pnumMax packetNumber // TODO: Track packet numbers. - p, n := parse1RTTPacket(buf, tc.wkeys[space].k, len(tc.peerConnID), pnumMax) + p, n := parse1RTTPacket(buf, tc.keysAppData.r, len(tc.peerConnID), pnumMax) if n < 0 { tc.t.Fatalf("packet parse error") } @@ -747,12 +766,7 @@ type testConnHooks testConn // and verify that both sides of the connection are getting // matching keys. func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { - setKey := func(keys *[numberSpaceCount]keyData, e tls.QUICEvent) { - k, err := newKeys(e.Suite, e.Data) - if err != nil { - tc.t.Errorf("newKeys: %v", err) - return - } + checkKey := func(typ string, secrets *[numberSpaceCount]testKeySecret, e tls.QUICEvent) { var space numberSpace switch { case e.Level == tls.QUICEncryptionLevelHandshake: @@ -763,25 +777,30 @@ func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { tc.t.Errorf("unexpected encryption level %v", e.Level) return } - s := "read" - if keys == &tc.wkeys { - s = "write" - } - if keys[space].k.isSet() { - if keys[space].suite != e.Suite || !bytes.Equal(keys[space].secret, e.Data) { - tc.t.Errorf("%v key mismatch for level for level %v", s, e.Level) - } - return + if secrets[space].secret == nil { + secrets[space].suite = e.Suite + secrets[space].secret = append([]byte{}, e.Data...) + } else if secrets[space].suite != e.Suite || !bytes.Equal(secrets[space].secret, e.Data) { + tc.t.Errorf("%v key mismatch for level %v", typ, e.Level) } - keys[space].suite = e.Suite - keys[space].secret = append([]byte{}, e.Data...) - keys[space].k = k } switch e.Kind { case tls.QUICSetReadSecret: - setKey(&tc.rkeys, e) + checkKey("read", &tc.rsecrets, e) + switch e.Level { + case tls.QUICEncryptionLevelHandshake: + tc.keysHandshake.w.init(e.Suite, e.Data) + case tls.QUICEncryptionLevelApplication: + tc.keysAppData.w.init(e.Suite, e.Data) + } case tls.QUICSetWriteSecret: - setKey(&tc.wkeys, e) + checkKey("write", &tc.wsecrets, e) + switch e.Level { + case tls.QUICEncryptionLevelHandshake: + tc.keysHandshake.r.init(e.Suite, e.Data) + case tls.QUICEncryptionLevelApplication: + tc.keysAppData.r.init(e.Suite, e.Data) + } case tls.QUICWriteData: tc.cryptoDataOut[e.Level] = append(tc.cryptoDataOut[e.Level], e.Data...) tc.peerTLSConn.HandleData(e.Level, e.Data) @@ -792,9 +811,21 @@ func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { case tls.QUICNoEvent: return case tls.QUICSetReadSecret: - setKey(&tc.wkeys, e) + checkKey("write", &tc.wsecrets, e) + switch e.Level { + case tls.QUICEncryptionLevelHandshake: + tc.keysHandshake.r.init(e.Suite, e.Data) + case tls.QUICEncryptionLevelApplication: + tc.keysAppData.r.init(e.Suite, e.Data) + } case tls.QUICSetWriteSecret: - setKey(&tc.rkeys, e) + checkKey("read", &tc.rsecrets, e) + switch e.Level { + case tls.QUICEncryptionLevelHandshake: + tc.keysHandshake.w.init(e.Suite, e.Data) + case tls.QUICEncryptionLevelApplication: + tc.keysAppData.w.init(e.Suite, e.Data) + } case tls.QUICWriteData: tc.cryptoDataIn[e.Level] = append(tc.cryptoDataIn[e.Level], e.Data...) case tls.QUICTransportParameters: diff --git a/internal/quic/packet_codec_test.go b/internal/quic/packet_codec_test.go index 3503d24318..7f0846f3ec 100644 --- a/internal/quic/packet_codec_test.go +++ b/internal/quic/packet_codec_test.go @@ -17,7 +17,7 @@ func TestParseLongHeaderPacket(t *testing.T) { // Example Initial packet from: // https://www.rfc-editor.org/rfc/rfc9001.html#section-a.3 cid := unhex(`8394c8f03e515708`) - _, initialServerKeys := initialKeys(cid) + initialServerKeys := initialKeys(cid, clientSide).r pkt := unhex(` cf000000010008f067a5502a4262b500 4075c0d95a482cd0991cd25b0aac406a 5816b6394100f37a1c69797554780bb3 8cc5a99f5ede4cf73c3ec2493a1839b3 @@ -65,20 +65,21 @@ func TestParseLongHeaderPacket(t *testing.T) { } // Parse with the wrong keys. - _, invalidKeys := initialKeys([]byte{}) + invalidKeys := initialKeys([]byte{}, clientSide).w if _, n := parseLongHeaderPacket(pkt, invalidKeys, 0); n != -1 { t.Fatalf("parse long header packet with wrong keys: n=%v, want -1", n) } } func TestRoundtripEncodeLongPacket(t *testing.T) { - aes128Keys, _ := newKeys(tls.TLS_AES_128_GCM_SHA256, []byte("secret")) - aes256Keys, _ := newKeys(tls.TLS_AES_256_GCM_SHA384, []byte("secret")) - chachaKeys, _ := newKeys(tls.TLS_CHACHA20_POLY1305_SHA256, []byte("secret")) + var aes128Keys, aes256Keys, chachaKeys fixedKeys + aes128Keys.init(tls.TLS_AES_128_GCM_SHA256, []byte("secret")) + aes256Keys.init(tls.TLS_AES_256_GCM_SHA384, []byte("secret")) + chachaKeys.init(tls.TLS_CHACHA20_POLY1305_SHA256, []byte("secret")) for _, test := range []struct { desc string p longPacket - k keys + k fixedKeys }{{ desc: "Initial, 1-byte number, AES128", p: longPacket{ @@ -145,9 +146,10 @@ func TestRoundtripEncodeLongPacket(t *testing.T) { } func TestRoundtripEncodeShortPacket(t *testing.T) { - aes128Keys, _ := newKeys(tls.TLS_AES_128_GCM_SHA256, []byte("secret")) - aes256Keys, _ := newKeys(tls.TLS_AES_256_GCM_SHA384, []byte("secret")) - chachaKeys, _ := newKeys(tls.TLS_CHACHA20_POLY1305_SHA256, []byte("secret")) + var aes128Keys, aes256Keys, chachaKeys fixedKeys + aes128Keys.init(tls.TLS_AES_128_GCM_SHA256, []byte("secret")) + aes256Keys.init(tls.TLS_AES_256_GCM_SHA384, []byte("secret")) + chachaKeys.init(tls.TLS_CHACHA20_POLY1305_SHA256, []byte("secret")) connID := make([]byte, connIDLen) for i := range connID { connID[i] = byte(i) @@ -156,7 +158,7 @@ func TestRoundtripEncodeShortPacket(t *testing.T) { desc string num packetNumber payload []byte - k keys + k fixedKeys }{{ desc: "1-byte number, AES128", num: 0, // 1-byte encoding, @@ -700,7 +702,7 @@ func TestFrameDecodeErrors(t *testing.T) { func FuzzParseLongHeaderPacket(f *testing.F) { cid := unhex(`0000000000000000`) - _, initialServerKeys := initialKeys(cid) + initialServerKeys := initialKeys(cid, clientSide).r f.Fuzz(func(t *testing.T, in []byte) { parseLongHeaderPacket(in, initialServerKeys, 0) }) diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index 43238826f1..458cd3a93a 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -18,7 +18,7 @@ package quic // and its length in bytes. // // It returns an empty packet and -1 if the packet could not be parsed. -func parseLongHeaderPacket(pkt []byte, k keys, pnumMax packetNumber) (p longPacket, n int) { +func parseLongHeaderPacket(pkt []byte, k fixedKeys, pnumMax packetNumber) (p longPacket, n int) { if len(pkt) < 5 || !isLongHeader(pkt[0]) { return longPacket{}, -1 } @@ -143,7 +143,7 @@ func skipLongHeaderPacket(pkt []byte) int { // // On input, pkt contains a short header packet, k the decryption keys for the packet, // and pnumMax the largest packet number seen in the number space of this packet. -func parse1RTTPacket(pkt []byte, k keys, dstConnIDLen int, pnumMax packetNumber) (p shortPacket, n int) { +func parse1RTTPacket(pkt []byte, k fixedKeys, dstConnIDLen int, pnumMax packetNumber) (p shortPacket, n int) { var err error p.payload, p.num, err = k.unprotect(pkt, 1+dstConnIDLen, pnumMax) if err != nil { diff --git a/internal/quic/packet_protection.go b/internal/quic/packet_protection.go index 18470536ff..2f9b9cefb6 100644 --- a/internal/quic/packet_protection.go +++ b/internal/quic/packet_protection.go @@ -13,7 +13,6 @@ import ( "crypto/sha256" "crypto/tls" "errors" - "fmt" "hash" "golang.org/x/crypto/chacha20" @@ -24,135 +23,179 @@ import ( var errInvalidPacket = errors.New("quic: invalid packet") -// keys holds the cryptographic material used to protect packets -// at an encryption level and direction. (e.g., Initial client keys.) -// -// keys are not safe for concurrent use. -type keys struct { - // AEAD function used for packet protection. - aead cipher.AEAD - - // The header_protection function as defined in: - // https://www.rfc-editor.org/rfc/rfc9001#section-5.4.1 - // - // This function takes a sample of the packet ciphertext - // and returns a 5-byte mask which will be applied to the - // protected portions of the packet header. - headerProtection func(sample []byte) (mask [5]byte) - - // IV used to construct the AEAD nonce. - iv []byte +// headerProtectionSampleSize is the size of the ciphertext sample used for header protection. +// https://www.rfc-editor.org/rfc/rfc9001#section-5.4.2 +const headerProtectionSampleSize = 16 + +// aeadOverhead is the difference in size between the AEAD output and input. +// All cipher suites defined for use with QUIC have 16 bytes of overhead. +const aeadOverhead = 16 + +// A headerKey applies or removes header protection. +// https://www.rfc-editor.org/rfc/rfc9001#section-5.4 +type headerKey struct { + hp headerProtection } -// newKeys creates keys for a given cipher suite and secret. -// -// It returns an error if the suite is unknown. -func newKeys(suite uint16, secret []byte) (keys, error) { +func (k *headerKey) init(suite uint16, secret []byte) { + h, keySize := hashForSuite(suite) + hpKey := hkdfExpandLabel(h.New, secret, "quic hp", nil, keySize) switch suite { - case tls.TLS_AES_128_GCM_SHA256: - return newAESKeys(secret, crypto.SHA256, 128/8), nil - case tls.TLS_AES_256_GCM_SHA384: - return newAESKeys(secret, crypto.SHA384, 256/8), nil + case tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384: + c, err := aes.NewCipher(hpKey) + if err != nil { + panic(err) + } + k.hp = &aesHeaderProtection{cipher: c} case tls.TLS_CHACHA20_POLY1305_SHA256: - return newChaCha20Keys(secret), nil + k.hp = chaCha20HeaderProtection{hpKey} + default: + panic("BUG: unknown cipher suite") } - return keys{}, fmt.Errorf("unknown cipher suite %x", suite) } -func newAESKeys(secret []byte, h crypto.Hash, keyBytes int) keys { - // https://www.rfc-editor.org/rfc/rfc9001#section-5.1 - key := hkdfExpandLabel(h.New, secret, "quic key", nil, keyBytes) - c, err := aes.NewCipher(key) - if err != nil { - panic(err) +// protect applies header protection. +// pnumOff is the offset of the packet number in the packet. +func (k headerKey) protect(hdr []byte, pnumOff int) { + // Apply header protection. + pnumSize := int(hdr[0]&0x03) + 1 + sample := hdr[pnumOff+4:][:headerProtectionSampleSize] + mask := k.hp.headerProtection(sample) + if isLongHeader(hdr[0]) { + hdr[0] ^= mask[0] & 0x0f + } else { + hdr[0] ^= mask[0] & 0x1f } - aead, err := cipher.NewGCM(c) - if err != nil { - panic(err) + for i := 0; i < pnumSize; i++ { + hdr[pnumOff+i] ^= mask[1+i] } - iv := hkdfExpandLabel(h.New, secret, "quic iv", nil, aead.NonceSize()) - // https://www.rfc-editor.org/rfc/rfc9001#section-5.4.3 - hpKey := hkdfExpandLabel(h.New, secret, "quic hp", nil, keyBytes) - hp, err := aes.NewCipher(hpKey) - if err != nil { - panic(err) +} + +// unprotect removes header protection. +// pnumOff is the offset of the packet number in the packet. +// pnumMax is the largest packet number seen in the number space of this packet. +func (k headerKey) unprotect(pkt []byte, pnumOff int, pnumMax packetNumber) (hdr, pay []byte, pnum packetNumber, _ error) { + if len(pkt) < pnumOff+4+headerProtectionSampleSize { + return nil, nil, 0, errInvalidPacket } - var scratch [aes.BlockSize]byte - headerProtection := func(sample []byte) (mask [5]byte) { - hp.Encrypt(scratch[:], sample) - copy(mask[:], scratch[:]) - return mask + numpay := pkt[pnumOff:] + sample := numpay[4:][:headerProtectionSampleSize] + mask := k.hp.headerProtection(sample) + if isLongHeader(pkt[0]) { + pkt[0] ^= mask[0] & 0x0f + } else { + pkt[0] ^= mask[0] & 0x1f } - return keys{ - aead: aead, - iv: iv, - headerProtection: headerProtection, + pnumLen := int(pkt[0]&0x03) + 1 + pnum = packetNumber(0) + for i := 0; i < pnumLen; i++ { + numpay[i] ^= mask[1+i] + pnum = (pnum << 8) | packetNumber(numpay[i]) } + pnum = decodePacketNumber(pnumMax, pnum, pnumLen) + hdr = pkt[:pnumOff+pnumLen] + pay = numpay[pnumLen:] + return hdr, pay, pnum, nil } -func newChaCha20Keys(secret []byte) keys { - // https://www.rfc-editor.org/rfc/rfc9001#section-5.1 - key := hkdfExpandLabel(sha256.New, secret, "quic key", nil, chacha20poly1305.KeySize) - aead, err := chacha20poly1305.New(key) +// headerProtection is the header_protection function as defined in: +// https://www.rfc-editor.org/rfc/rfc9001#section-5.4.1 +// +// This function takes a sample of the packet ciphertext +// and returns a 5-byte mask which will be applied to the +// protected portions of the packet header. +type headerProtection interface { + headerProtection(sample []byte) (mask [5]byte) +} + +// AES-based header protection. +// https://www.rfc-editor.org/rfc/rfc9001#section-5.4.3 +type aesHeaderProtection struct { + cipher cipher.Block + scratch [aes.BlockSize]byte +} + +func (hp *aesHeaderProtection) headerProtection(sample []byte) (mask [5]byte) { + hp.cipher.Encrypt(hp.scratch[:], sample) + copy(mask[:], hp.scratch[:]) + return mask +} + +// ChaCha20-based header protection. +// https://www.rfc-editor.org/rfc/rfc9001#section-5.4.4 +type chaCha20HeaderProtection struct { + key []byte +} + +func (hp chaCha20HeaderProtection) headerProtection(sample []byte) (mask [5]byte) { + counter := uint32(sample[3])<<24 | uint32(sample[2])<<16 | uint32(sample[1])<<8 | uint32(sample[0]) + nonce := sample[4:16] + c, err := chacha20.NewUnauthenticatedCipher(hp.key, nonce) if err != nil { panic(err) } - iv := hkdfExpandLabel(sha256.New, secret, "quic iv", nil, aead.NonceSize()) - // https://www.rfc-editor.org/rfc/rfc9001#section-5.4.4 - hpKey := hkdfExpandLabel(sha256.New, secret, "quic hp", nil, chacha20.KeySize) - headerProtection := func(sample []byte) [5]byte { - counter := uint32(sample[3])<<24 | uint32(sample[2])<<16 | uint32(sample[1])<<8 | uint32(sample[0]) - nonce := sample[4:16] - c, err := chacha20.NewUnauthenticatedCipher(hpKey, nonce) - if err != nil { - panic(err) - } - c.SetCounter(counter) - var mask [5]byte - c.XORKeyStream(mask[:], mask[:]) - return mask - } - return keys{ - aead: aead, - iv: iv, - headerProtection: headerProtection, - } + c.SetCounter(counter) + c.XORKeyStream(mask[:], mask[:]) + return mask } -// https://www.rfc-editor.org/rfc/rfc9001#section-5.2-2 -var initialSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} +// A packetKey applies or removes packet protection. +// https://www.rfc-editor.org/rfc/rfc9001#section-5.1 +type packetKey struct { + aead cipher.AEAD // AEAD function used for packet protection. + iv []byte // IV used to construct the AEAD nonce. +} -// initialKeys returns the keys used to protect Initial packets. -// -// The Initial packet keys are derived from the Destination Connection ID -// field in the client's first Initial packet. -// -// https://www.rfc-editor.org/rfc/rfc9001#section-5.2 -func initialKeys(cid []byte) (clientKeys, serverKeys keys) { - initialSecret := hkdf.Extract(sha256.New, cid, initialSalt) - clientInitialSecret := hkdfExpandLabel(sha256.New, initialSecret, "client in", nil, sha256.Size) - clientKeys, err := newKeys(tls.TLS_AES_128_GCM_SHA256, clientInitialSecret) +func (k *packetKey) init(suite uint16, secret []byte) { + // https://www.rfc-editor.org/rfc/rfc9001#section-5.1 + h, keySize := hashForSuite(suite) + key := hkdfExpandLabel(h.New, secret, "quic key", nil, keySize) + switch suite { + case tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384: + k.aead = newAESAEAD(key) + case tls.TLS_CHACHA20_POLY1305_SHA256: + k.aead = newChaCha20AEAD(key) + default: + panic("BUG: unknown cipher suite") + } + k.iv = hkdfExpandLabel(h.New, secret, "quic iv", nil, k.aead.NonceSize()) +} + +func newAESAEAD(key []byte) cipher.AEAD { + c, err := aes.NewCipher(key) if err != nil { panic(err) } - - serverInitialSecret := hkdfExpandLabel(sha256.New, initialSecret, "server in", nil, sha256.Size) - serverKeys, err = newKeys(tls.TLS_AES_128_GCM_SHA256, serverInitialSecret) + aead, err := cipher.NewGCM(c) if err != nil { panic(err) } + return aead +} - return clientKeys, serverKeys +func newChaCha20AEAD(key []byte) cipher.AEAD { + var err error + aead, err := chacha20poly1305.New(key) + if err != nil { + panic(err) + } + return aead } -const headerProtectionSampleSize = 16 +func (k packetKey) protect(hdr, pay []byte, pnum packetNumber) []byte { + k.xorIV(pnum) + defer k.xorIV(pnum) + return k.aead.Seal(hdr, k.iv, pay, hdr) +} -// aeadOverhead is the difference in size between the AEAD output and input. -// All cipher suites defined for use with QUIC have 16 bytes of overhead. -const aeadOverhead = 16 +func (k packetKey) unprotect(hdr, pay []byte, pnum packetNumber) (dec []byte, err error) { + k.xorIV(pnum) + defer k.xorIV(pnum) + return k.aead.Open(pay[:0], k.iv, pay, hdr) +} // xorIV xors the packet protection IV with the packet number. -func (k keys) xorIV(pnum packetNumber) { +func (k packetKey) xorIV(pnum packetNumber) { k.iv[len(k.iv)-8] ^= uint8(pnum >> 56) k.iv[len(k.iv)-7] ^= uint8(pnum >> 48) k.iv[len(k.iv)-6] ^= uint8(pnum >> 40) @@ -163,17 +206,22 @@ func (k keys) xorIV(pnum packetNumber) { k.iv[len(k.iv)-1] ^= uint8(pnum) } -// isSet returns true if valid keys are available. -func (k keys) isSet() bool { - return k.aead != nil +// A fixedKeys is a header protection key and fixed packet protection key. +// The packet protection key is fixed (it does not update). +// +// Fixed keys are used for Initial and Handshake keys, which do not update. +type fixedKeys struct { + hdr headerKey + pkt packetKey } -// discard discards the keys (in the sense that we won't use them any more, -// not that the keys are securely erased). -// -// https://www.rfc-editor.org/rfc/rfc9001.html#section-4.9 -func (k *keys) discard() { - *k = keys{} +func (k *fixedKeys) init(suite uint16, secret []byte) { + k.hdr.init(suite, secret) + k.pkt.init(suite, secret) +} + +func (k fixedKeys) isSet() bool { + return k.hdr.hp != nil } // protect applies packet protection to a packet. @@ -184,25 +232,10 @@ func (k *keys) discard() { // // protect returns the result of appending the encrypted payload to hdr and // applying header protection. -func (k keys) protect(hdr, pay []byte, pnumOff int, pnum packetNumber) []byte { - k.xorIV(pnum) - hdr = k.aead.Seal(hdr, k.iv, pay, hdr) - k.xorIV(pnum) - - // Apply header protection. - pnumSize := int(hdr[0]&0x03) + 1 - sample := hdr[pnumOff+4:][:headerProtectionSampleSize] - mask := k.headerProtection(sample) - if isLongHeader(hdr[0]) { - hdr[0] ^= mask[0] & 0x0f - } else { - hdr[0] ^= mask[0] & 0x1f - } - for i := 0; i < pnumSize; i++ { - hdr[pnumOff+i] ^= mask[1+i] - } - - return hdr +func (k fixedKeys) protect(hdr, pay []byte, pnumOff int, pnum packetNumber) []byte { + pkt := k.pkt.protect(hdr, pay, pnum) + k.hdr.protect(pkt, pnumOff) + return pkt } // unprotect removes packet protection from a packet. @@ -213,36 +246,82 @@ func (k keys) protect(hdr, pay []byte, pnumOff int, pnum packetNumber) []byte { // // unprotect removes header protection from the header in pkt, and returns // the unprotected payload and packet number. -func (k keys) unprotect(pkt []byte, pnumOff int, pnumMax packetNumber) (pay []byte, num packetNumber, err error) { - if len(pkt) < pnumOff+4+headerProtectionSampleSize { - return nil, 0, errInvalidPacket +func (k fixedKeys) unprotect(pkt []byte, pnumOff int, pnumMax packetNumber) (pay []byte, num packetNumber, err error) { + hdr, pay, pnum, err := k.hdr.unprotect(pkt, pnumOff, pnumMax) + if err != nil { + return nil, 0, err } - numpay := pkt[pnumOff:] - sample := numpay[4:][:headerProtectionSampleSize] - mask := k.headerProtection(sample) - if isLongHeader(pkt[0]) { - pkt[0] ^= mask[0] & 0x0f - } else { - pkt[0] ^= mask[0] & 0x1f + pay, err = k.pkt.unprotect(hdr, pay, pnum) + if err != nil { + return nil, 0, err } - pnumLen := int(pkt[0]&0x03) + 1 - pnum := packetNumber(0) - for i := 0; i < pnumLen; i++ { - numpay[i] ^= mask[1+i] - pnum = (pnum << 8) | packetNumber(numpay[i]) + return pay, pnum, nil +} + +// A fixedKeyPair is a read/write pair of fixed keys. +type fixedKeyPair struct { + r, w fixedKeys +} + +func (k *fixedKeyPair) discard() { + *k = fixedKeyPair{} +} + +func (k *fixedKeyPair) canRead() bool { + return k.r.isSet() +} + +func (k *fixedKeyPair) canWrite() bool { + return k.w.isSet() +} + +// https://www.rfc-editor.org/rfc/rfc9001#section-5.2-2 +var initialSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} + +// initialKeys returns the keys used to protect Initial packets. +// +// The Initial packet keys are derived from the Destination Connection ID +// field in the client's first Initial packet. +// +// https://www.rfc-editor.org/rfc/rfc9001#section-5.2 +func initialKeys(cid []byte, side connSide) fixedKeyPair { + initialSecret := hkdf.Extract(sha256.New, cid, initialSalt) + var clientKeys fixedKeys + clientSecret := hkdfExpandLabel(sha256.New, initialSecret, "client in", nil, sha256.Size) + clientKeys.init(tls.TLS_AES_128_GCM_SHA256, clientSecret) + var serverKeys fixedKeys + serverSecret := hkdfExpandLabel(sha256.New, initialSecret, "server in", nil, sha256.Size) + serverKeys.init(tls.TLS_AES_128_GCM_SHA256, serverSecret) + if side == clientSide { + return fixedKeyPair{r: serverKeys, w: clientKeys} + } else { + return fixedKeyPair{w: serverKeys, r: clientKeys} } - pnum = decodePacketNumber(pnumMax, pnum, pnumLen) +} - hdr := pkt[:pnumOff+pnumLen] - pay = numpay[pnumLen:] - k.xorIV(pnum) - pay, err = k.aead.Open(pay[:0], k.iv, pay, hdr) - k.xorIV(pnum) - if err != nil { - return nil, 0, err +// checkCipherSuite returns an error if suite is not a supported cipher suite. +func checkCipherSuite(suite uint16) error { + switch suite { + case tls.TLS_AES_128_GCM_SHA256: + case tls.TLS_AES_256_GCM_SHA384: + case tls.TLS_CHACHA20_POLY1305_SHA256: + default: + return errors.New("invalid cipher suite") } + return nil +} - return pay, pnum, nil +func hashForSuite(suite uint16) (h crypto.Hash, keySize int) { + switch suite { + case tls.TLS_AES_128_GCM_SHA256: + return crypto.SHA256, 128 / 8 + case tls.TLS_AES_256_GCM_SHA384: + return crypto.SHA384, 256 / 8 + case tls.TLS_CHACHA20_POLY1305_SHA256: + return crypto.SHA256, chacha20.KeySize + default: + panic("BUG: unknown cipher suite") + } } // hdkfExpandLabel implements HKDF-Expand-Label from RFC 8446, Section 7.1. diff --git a/internal/quic/packet_protection_test.go b/internal/quic/packet_protection_test.go index 6495360a3b..1fe1307311 100644 --- a/internal/quic/packet_protection_test.go +++ b/internal/quic/packet_protection_test.go @@ -16,10 +16,11 @@ func TestPacketProtection(t *testing.T) { // Test cases from: // https://www.rfc-editor.org/rfc/rfc9001#section-appendix.a cid := unhex(`8394c8f03e515708`) - initialClientKeys, initialServerKeys := initialKeys(cid) + k := initialKeys(cid, clientSide) + initialClientKeys, initialServerKeys := k.w, k.r for _, test := range []struct { name string - k keys + k fixedKeys pnum packetNumber hdr []byte pay []byte @@ -103,15 +104,13 @@ func TestPacketProtection(t *testing.T) { `), }, { name: "ChaCha20_Poly1305 Short Header", - k: func() keys { + k: func() fixedKeys { secret := unhex(` 9ac312a7f877468ebe69422748ad00a1 5443f18203a07d6060f688f30f21632b `) - k, err := newKeys(tls.TLS_CHACHA20_POLY1305_SHA256, secret) - if err != nil { - t.Fatal(err) - } + var k fixedKeys + k.init(tls.TLS_CHACHA20_POLY1305_SHA256, secret) return k }(), pnum: 654360564, diff --git a/internal/quic/packet_writer.go b/internal/quic/packet_writer.go index a80b4711ee..2009895e04 100644 --- a/internal/quic/packet_writer.go +++ b/internal/quic/packet_writer.go @@ -100,7 +100,7 @@ func (w *packetWriter) startProtectedLongHeaderPacket(pnumMaxAcked packetNumber, // finishProtectedLongHeaderPacket finishes writing an Initial, 0-RTT, or Handshake packet, // canceling the packet if it contains no payload. // It returns a sentPacket describing the packet, or nil if no packet was written. -func (w *packetWriter) finishProtectedLongHeaderPacket(pnumMaxAcked packetNumber, k keys, p longPacket) *sentPacket { +func (w *packetWriter) finishProtectedLongHeaderPacket(pnumMaxAcked packetNumber, k fixedKeys, p longPacket) *sentPacket { if len(w.b) == w.payOff { // The payload is empty, so just abandon the packet. w.b = w.b[:w.pktOff] @@ -135,7 +135,8 @@ func (w *packetWriter) finishProtectedLongHeaderPacket(pnumMaxAcked packetNumber pnumOff := len(hdr) hdr = appendPacketNumber(hdr, p.num, pnumMaxAcked) - return w.protect(hdr[w.pktOff:], p.num, pnumOff, k) + k.protect(hdr[w.pktOff:], w.b[len(hdr):], pnumOff-w.pktOff, p.num) + return w.finish(p.num) } // start1RTTPacket starts writing a 1-RTT (short header) packet. @@ -162,7 +163,7 @@ func (w *packetWriter) start1RTTPacket(pnum, pnumMaxAcked packetNumber, dstConnI // finish1RTTPacket finishes writing a 1-RTT packet, // canceling the packet if it contains no payload. // It returns a sentPacket describing the packet, or nil if no packet was written. -func (w *packetWriter) finish1RTTPacket(pnum, pnumMaxAcked packetNumber, dstConnID []byte, k keys) *sentPacket { +func (w *packetWriter) finish1RTTPacket(pnum, pnumMaxAcked packetNumber, dstConnID []byte, k fixedKeys) *sentPacket { if len(w.b) == w.payOff { // The payload is empty, so just abandon the packet. w.b = w.b[:w.pktOff] @@ -177,7 +178,8 @@ func (w *packetWriter) finish1RTTPacket(pnum, pnumMaxAcked packetNumber, dstConn pnumOff := len(hdr) hdr = appendPacketNumber(hdr, pnum, pnumMaxAcked) w.padPacketLength(pnumLen) - return w.protect(hdr[w.pktOff:], pnum, pnumOff, k) + k.protect(hdr[w.pktOff:], w.b[len(hdr):], pnumOff-w.pktOff, pnum) + return w.finish(pnum) } // padPacketLength pads out the payload of the current packet to the minimum size, @@ -197,9 +199,8 @@ func (w *packetWriter) padPacketLength(pnumLen int) int { return plen } -// protect applies packet protection and finishes the current packet. -func (w *packetWriter) protect(hdr []byte, pnum packetNumber, pnumOff int, k keys) *sentPacket { - k.protect(hdr, w.b[w.pktOff+len(hdr):], pnumOff-w.pktOff, pnum) +// finish finishes the current packet after protection is applied. +func (w *packetWriter) finish(pnum packetNumber) *sentPacket { w.b = w.b[:len(w.b)+aeadOverhead] w.sent.size = len(w.b) - w.pktOff w.sent.num = pnum diff --git a/internal/quic/tls.go b/internal/quic/tls.go index e3a430ed72..a37e26fb8e 100644 --- a/internal/quic/tls.go +++ b/internal/quic/tls.go @@ -16,12 +16,7 @@ import ( // startTLS starts the TLS handshake. func (c *Conn) startTLS(now time.Time, initialConnID []byte, params transportParameters) error { - clientKeys, serverKeys := initialKeys(initialConnID) - if c.side == clientSide { - c.wkeys[initialSpace], c.rkeys[initialSpace] = clientKeys, serverKeys - } else { - c.wkeys[initialSpace], c.rkeys[initialSpace] = serverKeys, clientKeys - } + c.keysInitial = initialKeys(initialConnID, c.side) qconfig := &tls.QUICConfig{TLSConfig: c.config.TLSConfig} if c.side == clientSide { @@ -49,21 +44,36 @@ func (c *Conn) handleTLSEvents(now time.Time) error { case tls.QUICNoEvent: return nil case tls.QUICSetReadSecret: - space, k, err := tlsKey(e) - if err != nil { + if err := checkCipherSuite(e.Suite); err != nil { return err } - c.rkeys[space] = k + switch e.Level { + case tls.QUICEncryptionLevelHandshake: + c.keysHandshake.r.init(e.Suite, e.Data) + case tls.QUICEncryptionLevelApplication: + c.keysAppData.r.init(e.Suite, e.Data) + } case tls.QUICSetWriteSecret: - space, k, err := tlsKey(e) - if err != nil { + if err := checkCipherSuite(e.Suite); err != nil { return err } - c.wkeys[space] = k + switch e.Level { + case tls.QUICEncryptionLevelHandshake: + c.keysHandshake.w.init(e.Suite, e.Data) + case tls.QUICEncryptionLevelApplication: + c.keysAppData.w.init(e.Suite, e.Data) + } case tls.QUICWriteData: - space, err := spaceForLevel(e.Level) - if err != nil { - return err + var space numberSpace + switch e.Level { + case tls.QUICEncryptionLevelInitial: + space = initialSpace + case tls.QUICEncryptionLevelHandshake: + space = handshakeSpace + case tls.QUICEncryptionLevelApplication: + space = appDataSpace + default: + return fmt.Errorf("quic: internal error: write handshake data at level %v", e.Level) } c.crypto[space].write(e.Data) case tls.QUICHandshakeDone: @@ -86,32 +96,6 @@ func (c *Conn) handleTLSEvents(now time.Time) error { } } -// tlsKey returns the keys in a QUICSetReadSecret or QUICSetWriteSecret event. -func tlsKey(e tls.QUICEvent) (numberSpace, keys, error) { - space, err := spaceForLevel(e.Level) - if err != nil { - return 0, keys{}, err - } - k, err := newKeys(e.Suite, e.Data) - if err != nil { - return 0, keys{}, err - } - return space, k, nil -} - -func spaceForLevel(level tls.QUICEncryptionLevel) (numberSpace, error) { - switch level { - case tls.QUICEncryptionLevelInitial: - return initialSpace, nil - case tls.QUICEncryptionLevelHandshake: - return handshakeSpace, nil - case tls.QUICEncryptionLevelApplication: - return appDataSpace, nil - default: - return 0, fmt.Errorf("quic: internal error: write handshake data at level %v", level) - } -} - // handleCrypto processes data received in a CRYPTO frame. func (c *Conn) handleCrypto(now time.Time, space numberSpace, off int64, data []byte) error { var level tls.QUICEncryptionLevel From 18f20955de135ef11df0f3b59560e913c4c57bb9 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 15 Sep 2023 10:43:39 -0700 Subject: [PATCH 061/168] quic: handle peer-initiated key updates RFC 9001, Section 6. For golang/go#58547 Change-Id: I3700043d27ab41536521b547ecf5e632a08eb1b5 Reviewed-on: https://go-review.googlesource.com/c/net/+/528835 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn.go | 2 +- internal/quic/conn_recv.go | 7 +- internal/quic/conn_send.go | 19 +++- internal/quic/conn_test.go | 174 +++++++++++++++++++++-------- internal/quic/key_update_test.go | 163 +++++++++++++++++++++++++++ internal/quic/packet.go | 1 + internal/quic/packet_codec_test.go | 17 +-- internal/quic/packet_parser.go | 7 +- internal/quic/packet_protection.go | 146 ++++++++++++++++++++++++ internal/quic/packet_writer.go | 3 +- 10 files changed, 472 insertions(+), 67 deletions(-) create mode 100644 internal/quic/key_update_test.go diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 4565e1a58c..dc3a985e84 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -44,7 +44,7 @@ type Conn struct { // Packet protection keys, CRYPTO streams, and TLS state. keysInitial fixedKeyPair keysHandshake fixedKeyPair - keysAppData fixedKeyPair + keysAppData updatingKeyPair crypto [numberSpaceCount]cryptoStream tls *tls.QUICConn diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index d1fa52d990..4fc4eeccf0 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -89,7 +89,7 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int { } pnumMax := c.acks[appDataSpace].largestSeen() - p, n := parse1RTTPacket(buf, c.keysAppData.r, connIDLen, pnumMax) + p, n := parse1RTTPacket(buf, &c.keysAppData, connIDLen, pnumMax) if n < 0 { return -1 } @@ -247,7 +247,7 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) int { c.loss.receiveAckStart() - _, ackDelay, n := consumeAckFrame(payload, func(rangeIndex int, start, end packetNumber) { + 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(errProtocolViolation)) @@ -280,6 +280,9 @@ func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) delay = ackDelay.Duration(uint8(c.peerAckDelayExponent)) } c.loss.receiveAckEnd(now, space, delay, c.handleAckOrLoss) + if space == appDataSpace { + c.keysAppData.handleAckFor(largest) + } return n } diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 58a3df1078..63f65b5579 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -128,7 +128,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { if logPackets { logSentPacket(c, packetType1RTT, pnum, nil, dstConnID, c.w.payload()) } - if sent := c.w.finish1RTTPacket(pnum, pnumMaxAcked, dstConnID, c.keysAppData.w); sent != nil { + if sent := c.w.finish1RTTPacket(pnum, pnumMaxAcked, dstConnID, &c.keysAppData); sent != nil { c.loss.packetSent(now, appDataSpace, sent) } } @@ -197,16 +197,23 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, // All frames other than ACK and PADDING are ack-eliciting, // so if the packet is ack-eliciting we've added additional // frames to it. - if shouldSendAck || c.w.sent.ackEliciting { - // Either we are willing to send an ACK-only packet, - // or we've added additional frames. - c.acks[space].sentAck() - } else { + if !shouldSendAck && !c.w.sent.ackEliciting { // There's nothing in this packet but ACK frames, and // we don't want to send an ACK-only packet at this time. // Abandoning the packet means we wrote an ACK frame for // nothing, but constructing the frame is cheap. c.w.abandonPacket() + return + } + // Either we are willing to send an ACK-only packet, + // or we've added additional frames. + c.acks[space].sentAck() + if !c.w.sent.ackEliciting && c.keysAppData.needAckEliciting() { + // The peer has initiated a key update. + // We haven't sent them any packets yet in the new phase. + // Make this an ack-eliciting packet. + // Their ack of this packet will complete the key update. + c.w.appendPingFrame() } }() } diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 3fef62d507..76774cc39c 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -76,12 +76,14 @@ func (d testDatagram) String() string { } type testPacket struct { - ptype packetType - version uint32 - num packetNumber - dstConnID []byte - srcConnID []byte - frames []debugFrame + ptype packetType + version uint32 + num packetNumber + keyPhaseBit bool + keyNumber int + dstConnID []byte + srcConnID []byte + frames []debugFrame } func (p testPacket) String() string { @@ -102,6 +104,9 @@ func (p testPacket) String() string { return b.String() } +// maxTestKeyPhases is the maximum number of 1-RTT keys we'll generate in a test. +const maxTestKeyPhases = 3 + // A testConn is a Conn whose external interactions (sending and receiving packets, // setting timers) can be manipulated in tests. type testConn struct { @@ -122,9 +127,10 @@ type testConn struct { // the Initial packet. keysInitial fixedKeyPair keysHandshake fixedKeyPair - keysAppData fixedKeyPair - rsecrets [numberSpaceCount]testKeySecret - wsecrets [numberSpaceCount]testKeySecret + rkeyAppData test1RTTKeys + wkeyAppData test1RTTKeys + rsecrets [numberSpaceCount]keySecret + wsecrets [numberSpaceCount]keySecret // testConn uses a test hook to snoop on the conn's TLS events. // CRYPTO data produced by the conn's QUICConn is placed in @@ -156,10 +162,19 @@ type testConn struct { // Frame types to ignore in tests. ignoreFrames map[byte]bool + // Values to set in packets sent to the conn. + sendKeyNumber int + sendKeyPhaseBit bool + asyncTestState } -type testKeySecret struct { +type test1RTTKeys struct { + hdr headerKey + pkt [maxTestKeyPhases]packetKey +} + +type keySecret struct { suite uint16 secret []byte } @@ -333,12 +348,20 @@ func (tc *testConn) logDatagram(text string, d *testDatagram) { } tc.t.Logf("%v datagram%v", text, pad) for _, p := range d.packets { + var s string switch p.ptype { case packetType1RTT: - tc.t.Logf(" %v pnum=%v", p.ptype, p.num) + s = fmt.Sprintf(" %v pnum=%v", p.ptype, p.num) default: - tc.t.Logf(" %v pnum=%v ver=%v dst={%x} src={%x}", p.ptype, p.num, p.version, p.dstConnID, p.srcConnID) + s = fmt.Sprintf(" %v pnum=%v ver=%v dst={%x} src={%x}", p.ptype, p.num, p.version, p.dstConnID, p.srcConnID) + } + if p.keyPhaseBit { + s += fmt.Sprintf(" KeyPhase") } + if p.keyNumber != 0 { + s += fmt.Sprintf(" keynum=%v", p.keyNumber) + } + tc.t.Log(s) for _, f := range p.frames { tc.t.Logf(" %v", f) } @@ -381,12 +404,14 @@ func (tc *testConn) writeFrames(ptype packetType, frames ...debugFrame) { } d := &testDatagram{ packets: []*testPacket{{ - ptype: ptype, - num: tc.peerNextPacketNum[space], - frames: frames, - version: 1, - dstConnID: dstConnID, - srcConnID: tc.peerConnID, + ptype: ptype, + num: tc.peerNextPacketNum[space], + keyNumber: tc.sendKeyNumber, + keyPhaseBit: tc.sendKeyPhaseBit, + frames: frames, + version: 1, + dstConnID: dstConnID, + srcConnID: tc.peerConnID, }}, } if ptype == packetTypeInitial && tc.conn.side == serverSide { @@ -580,6 +605,22 @@ func (tc *testConn) wantFrame(expectation string, wantType packetType, want debu } } +// wantFrameType indicates that we expect the Conn to send a frame, +// although we don't care about the contents. +func (tc *testConn) wantFrameType(expectation string, wantType packetType, want debugFrame) { + tc.t.Helper() + got, gotType := tc.readFrame() + if got == nil { + tc.t.Fatalf("%v:\nconnection is idle\nwant %v frame: %v", expectation, wantType, want) + } + if gotType != wantType { + tc.t.Fatalf("%v:\ngot %v packet, want %v\ngot frame: %v", expectation, gotType, wantType, got) + } + if reflect.TypeOf(got) != reflect.TypeOf(want) { + tc.t.Fatalf("%v:\ngot frame: %v\nwant frame of type: %v", expectation, got, want) + } +} + // wantIdle indicates that we expect the Conn to not send any more frames. func (tc *testConn) wantIdle(expectation string) { tc.t.Helper() @@ -615,17 +656,17 @@ func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { } w.appendPaddingTo(pad) if p.ptype != packetType1RTT { - var k fixedKeyPair + var k fixedKeys switch p.ptype { case packetTypeInitial: - k = tc.keysInitial + k = tc.keysInitial.w case packetTypeHandshake: - k = tc.keysHandshake + k = tc.keysHandshake.w } - if !k.canWrite() { + if !k.isSet() { tc.t.Fatalf("sending %v packet with no write key", p.ptype) } - w.finishProtectedLongHeaderPacket(pnumMaxAcked, k.w, longPacket{ + w.finishProtectedLongHeaderPacket(pnumMaxAcked, k, longPacket{ ptype: p.ptype, version: p.version, num: p.num, @@ -633,10 +674,24 @@ func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { srcConnID: p.srcConnID, }) } else { - if !tc.keysAppData.canWrite() { - tc.t.Fatalf("sending %v packet with no write key", p.ptype) + if !tc.wkeyAppData.hdr.isSet() { + tc.t.Fatalf("sending 1-RTT packet with no write key") + } + // Somewhat hackish: Generate a temporary updatingKeyPair that will + // always use our desired key phase. + k := &updatingKeyPair{ + w: updatingKeys{ + hdr: tc.wkeyAppData.hdr, + pkt: [2]packetKey{ + tc.wkeyAppData.pkt[p.keyNumber], + tc.wkeyAppData.pkt[p.keyNumber], + }, + }, + } + if p.keyPhaseBit { + k.phase |= keyPhaseBit } - w.finish1RTTPacket(p.num, pnumMaxAcked, p.dstConnID, tc.keysAppData.w) + w.finish1RTTPacket(p.num, pnumMaxAcked, p.dstConnID, k) } return w.datagram() } @@ -682,25 +737,45 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { }) buf = buf[n:] } else { - if !tc.keysAppData.canRead() { + if !tc.rkeyAppData.hdr.isSet() { tc.t.Fatalf("reading 1-RTT packet with no read key") } var pnumMax packetNumber // TODO: Track packet numbers. - p, n := parse1RTTPacket(buf, tc.keysAppData.r, len(tc.peerConnID), pnumMax) - if n < 0 { - tc.t.Fatalf("packet parse error") + pnumOff := 1 + len(tc.peerConnID) + // Try unprotecting the packet with the first maxTestKeyPhases keys. + var phase int + var pnum packetNumber + var hdr []byte + var pay []byte + var err error + for phase = 0; phase < maxTestKeyPhases; phase++ { + b := append([]byte{}, buf...) + hdr, pay, pnum, err = tc.rkeyAppData.hdr.unprotect(b, pnumOff, pnumMax) + if err != nil { + tc.t.Fatalf("1-RTT packet header parse error") + } + k := tc.rkeyAppData.pkt[phase] + pay, err = k.unprotect(hdr, pay, pnum) + if err == nil { + break + } } - frames, err := tc.parseTestFrames(p.payload) + if err != nil { + tc.t.Fatalf("1-RTT packet payload parse error") + } + frames, err := tc.parseTestFrames(pay) if err != nil { tc.t.Fatal(err) } d.packets = append(d.packets, &testPacket{ - ptype: packetType1RTT, - num: p.num, - dstConnID: buf[1:][:len(tc.peerConnID)], - frames: frames, + ptype: packetType1RTT, + num: pnum, + dstConnID: hdr[1:][:len(tc.peerConnID)], + keyPhaseBit: hdr[0]&keyPhaseBit != 0, + keyNumber: phase, + frames: frames, }) - buf = buf[n:] + buf = buf[len(buf):] } } // This is rather hackish: If the last frame in the last packet @@ -766,7 +841,7 @@ type testConnHooks testConn // and verify that both sides of the connection are getting // matching keys. func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { - checkKey := func(typ string, secrets *[numberSpaceCount]testKeySecret, e tls.QUICEvent) { + checkKey := func(typ string, secrets *[numberSpaceCount]keySecret, e tls.QUICEvent) { var space numberSpace switch { case e.Level == tls.QUICEncryptionLevelHandshake: @@ -781,25 +856,32 @@ func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { secrets[space].suite = e.Suite secrets[space].secret = append([]byte{}, e.Data...) } else if secrets[space].suite != e.Suite || !bytes.Equal(secrets[space].secret, e.Data) { - tc.t.Errorf("%v key mismatch for level %v", typ, e.Level) + tc.t.Errorf("%v key mismatch for level for level %v", typ, e.Level) + } + } + setAppDataKey := func(suite uint16, secret []byte, k *test1RTTKeys) { + k.hdr.init(suite, secret) + for i := 0; i < len(k.pkt); i++ { + k.pkt[i].init(suite, secret) + secret = updateSecret(suite, secret) } } switch e.Kind { case tls.QUICSetReadSecret: - checkKey("read", &tc.rsecrets, e) + checkKey("write", &tc.wsecrets, e) switch e.Level { case tls.QUICEncryptionLevelHandshake: tc.keysHandshake.w.init(e.Suite, e.Data) case tls.QUICEncryptionLevelApplication: - tc.keysAppData.w.init(e.Suite, e.Data) + setAppDataKey(e.Suite, e.Data, &tc.wkeyAppData) } case tls.QUICSetWriteSecret: - checkKey("write", &tc.wsecrets, e) + checkKey("read", &tc.rsecrets, e) switch e.Level { case tls.QUICEncryptionLevelHandshake: tc.keysHandshake.r.init(e.Suite, e.Data) case tls.QUICEncryptionLevelApplication: - tc.keysAppData.r.init(e.Suite, e.Data) + setAppDataKey(e.Suite, e.Data, &tc.rkeyAppData) } case tls.QUICWriteData: tc.cryptoDataOut[e.Level] = append(tc.cryptoDataOut[e.Level], e.Data...) @@ -811,20 +893,20 @@ func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { case tls.QUICNoEvent: return case tls.QUICSetReadSecret: - checkKey("write", &tc.wsecrets, e) + checkKey("write", &tc.rsecrets, e) switch e.Level { case tls.QUICEncryptionLevelHandshake: tc.keysHandshake.r.init(e.Suite, e.Data) case tls.QUICEncryptionLevelApplication: - tc.keysAppData.r.init(e.Suite, e.Data) + setAppDataKey(e.Suite, e.Data, &tc.rkeyAppData) } case tls.QUICSetWriteSecret: - checkKey("read", &tc.rsecrets, e) + checkKey("read", &tc.wsecrets, e) switch e.Level { case tls.QUICEncryptionLevelHandshake: tc.keysHandshake.w.init(e.Suite, e.Data) case tls.QUICEncryptionLevelApplication: - tc.keysAppData.w.init(e.Suite, e.Data) + setAppDataKey(e.Suite, e.Data, &tc.wkeyAppData) } case tls.QUICWriteData: tc.cryptoDataIn[e.Level] = append(tc.cryptoDataIn[e.Level], e.Data...) diff --git a/internal/quic/key_update_test.go b/internal/quic/key_update_test.go new file mode 100644 index 0000000000..6b6bb79803 --- /dev/null +++ b/internal/quic/key_update_test.go @@ -0,0 +1,163 @@ +// 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" +) + +func TestKeyUpdatePeerUpdates(t *testing.T) { + tc := newTestConn(t, serverSide) + tc.handshake() + tc.ignoreFrames = nil // ignore nothing + + // Peer initiates a key update. + tc.sendKeyNumber = 1 + tc.sendKeyPhaseBit = true + tc.writeFrames(packetType1RTT, debugFramePing{}) + + // We update to the new key. + tc.advanceToTimer() + tc.wantFrameType("conn ACKs last packet", + packetType1RTT, debugFrameAck{}) + tc.wantFrame("first packet after a key update is always ack-eliciting", + packetType1RTT, debugFramePing{}) + if got, want := tc.lastPacket.keyNumber, 1; got != want { + t.Errorf("after key rotation, conn sent packet with key %v, want %v", got, want) + } + if !tc.lastPacket.keyPhaseBit { + t.Errorf("after key rotation, conn failed to change Key Phase bit") + } + tc.wantIdle("conn has nothing to send") + + // Peer's ACK of a packet we sent in the new phase completes the update. + tc.writeAckForAll() + + // Peer initiates a second key update. + tc.sendKeyNumber = 2 + tc.sendKeyPhaseBit = false + tc.writeFrames(packetType1RTT, debugFramePing{}) + + // We update to the new key. + tc.advanceToTimer() + tc.wantFrameType("conn ACKs last packet", + packetType1RTT, debugFrameAck{}) + tc.wantFrame("first packet after a key update is always ack-eliciting", + packetType1RTT, debugFramePing{}) + if got, want := tc.lastPacket.keyNumber, 2; got != want { + t.Errorf("after key rotation, conn sent packet with key %v, want %v", got, want) + } + if tc.lastPacket.keyPhaseBit { + t.Errorf("after second key rotation, conn failed to change Key Phase bit") + } + tc.wantIdle("conn has nothing to send") +} + +func TestKeyUpdateAcceptPreviousPhaseKeys(t *testing.T) { + // "An endpoint SHOULD retain old keys for some time after + // unprotecting a packet sent using the new keys." + // https://www.rfc-editor.org/rfc/rfc9001#section-6.1-8 + tc := newTestConn(t, serverSide) + tc.handshake() + tc.ignoreFrames = nil // ignore nothing + + // Peer initiates a key update, skipping one packet number. + pnum0 := tc.peerNextPacketNum[appDataSpace] + tc.peerNextPacketNum[appDataSpace]++ + tc.sendKeyNumber = 1 + tc.sendKeyPhaseBit = true + tc.writeFrames(packetType1RTT, debugFramePing{}) + + // We update to the new key. + // This ACK is not delayed, because we've skipped a packet number. + tc.wantFrame("conn ACKs last packet", + packetType1RTT, debugFrameAck{ + ranges: []i64range[packetNumber]{ + {0, pnum0}, + {pnum0 + 1, pnum0 + 2}, + }, + }) + tc.wantFrame("first packet after a key update is always ack-eliciting", + packetType1RTT, debugFramePing{}) + if got, want := tc.lastPacket.keyNumber, 1; got != want { + t.Errorf("after key rotation, conn sent packet with key %v, want %v", got, want) + } + if !tc.lastPacket.keyPhaseBit { + t.Errorf("after key rotation, conn failed to change Key Phase bit") + } + tc.wantIdle("conn has nothing to send") + + // We receive the previously-skipped packet in the earlier key phase. + tc.peerNextPacketNum[appDataSpace] = pnum0 + tc.sendKeyNumber = 0 + tc.sendKeyPhaseBit = false + tc.writeFrames(packetType1RTT, debugFramePing{}) + + // We ack the reordered packet immediately, still in the new key phase. + tc.wantFrame("conn ACKs reordered packet", + packetType1RTT, debugFrameAck{ + ranges: []i64range[packetNumber]{ + {0, pnum0 + 2}, + }, + }) + tc.wantIdle("packet is not ack-eliciting") + if got, want := tc.lastPacket.keyNumber, 1; got != want { + t.Errorf("after key rotation, conn sent packet with key %v, want %v", got, want) + } + if !tc.lastPacket.keyPhaseBit { + t.Errorf("after key rotation, conn failed to change Key Phase bit") + } +} + +func TestKeyUpdateRejectPacketFromPriorPhase(t *testing.T) { + // "Packets with higher packet numbers MUST be protected with either + // the same or newer packet protection keys than packets with lower packet numbers." + // https://www.rfc-editor.org/rfc/rfc9001#section-6.4-2 + tc := newTestConn(t, serverSide) + tc.handshake() + tc.ignoreFrames = nil // ignore nothing + + // Peer initiates a key update. + tc.sendKeyNumber = 1 + tc.sendKeyPhaseBit = true + tc.writeFrames(packetType1RTT, debugFramePing{}) + + // We update to the new key. + tc.advanceToTimer() + tc.wantFrameType("conn ACKs last packet", + packetType1RTT, debugFrameAck{}) + tc.wantFrame("first packet after a key update is always ack-eliciting", + packetType1RTT, debugFramePing{}) + if got, want := tc.lastPacket.keyNumber, 1; got != want { + t.Errorf("after key rotation, conn sent packet with key %v, want %v", got, want) + } + if !tc.lastPacket.keyPhaseBit { + t.Errorf("after key rotation, conn failed to change Key Phase bit") + } + tc.wantIdle("conn has nothing to send") + + // Peer sends an ack-eliciting packet using the prior phase keys. + // We fail to unprotect the packet and ignore it. + skipped := tc.peerNextPacketNum[appDataSpace] + tc.sendKeyNumber = 0 + tc.sendKeyPhaseBit = false + tc.writeFrames(packetType1RTT, debugFramePing{}) + + // Peer sends an ack-eliciting packet using the current phase keys. + tc.sendKeyNumber = 1 + tc.sendKeyPhaseBit = true + tc.writeFrames(packetType1RTT, debugFramePing{}) + + // We ack the peer's packets, not including the one sent with the wrong keys. + tc.wantFrame("conn ACKs packets, not including packet sent with wrong keys", + packetType1RTT, debugFrameAck{ + ranges: []i64range[packetNumber]{ + {0, skipped}, + {skipped + 1, skipped + 2}, + }, + }) +} diff --git a/internal/quic/packet.go b/internal/quic/packet.go index a1bcead97e..8242bd0a97 100644 --- a/internal/quic/packet.go +++ b/internal/quic/packet.go @@ -45,6 +45,7 @@ const ( fixedBit = 0x40 // https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2-3.4.1 reservedLongBits = 0x0c // https://www.rfc-editor.org/rfc/rfc9000#section-17.2-8.2.1 reserved1RTTBits = 0x18 // https://www.rfc-editor.org/rfc/rfc9000#section-17.3.1-4.8.1 + keyPhaseBit = 0x04 // https://www.rfc-editor.org/rfc/rfc9000#section-17.3.1-4.10.1 ) // Long Packet Type bits. diff --git a/internal/quic/packet_codec_test.go b/internal/quic/packet_codec_test.go index 7f0846f3ec..c8b1f9ba85 100644 --- a/internal/quic/packet_codec_test.go +++ b/internal/quic/packet_codec_test.go @@ -146,10 +146,13 @@ func TestRoundtripEncodeLongPacket(t *testing.T) { } func TestRoundtripEncodeShortPacket(t *testing.T) { - var aes128Keys, aes256Keys, chachaKeys fixedKeys - aes128Keys.init(tls.TLS_AES_128_GCM_SHA256, []byte("secret")) - aes256Keys.init(tls.TLS_AES_256_GCM_SHA384, []byte("secret")) - chachaKeys.init(tls.TLS_CHACHA20_POLY1305_SHA256, []byte("secret")) + var aes128Keys, aes256Keys, chachaKeys updatingKeyPair + aes128Keys.r.init(tls.TLS_AES_128_GCM_SHA256, []byte("secret")) + aes256Keys.r.init(tls.TLS_AES_256_GCM_SHA384, []byte("secret")) + chachaKeys.r.init(tls.TLS_CHACHA20_POLY1305_SHA256, []byte("secret")) + aes128Keys.w = aes128Keys.r + aes256Keys.w = aes256Keys.r + chachaKeys.w = chachaKeys.r connID := make([]byte, connIDLen) for i := range connID { connID[i] = byte(i) @@ -158,7 +161,7 @@ func TestRoundtripEncodeShortPacket(t *testing.T) { desc string num packetNumber payload []byte - k fixedKeys + k updatingKeyPair }{{ desc: "1-byte number, AES128", num: 0, // 1-byte encoding, @@ -185,9 +188,9 @@ func TestRoundtripEncodeShortPacket(t *testing.T) { w.reset(1200) w.start1RTTPacket(test.num, 0, connID) w.b = append(w.b, test.payload...) - w.finish1RTTPacket(test.num, 0, connID, test.k) + w.finish1RTTPacket(test.num, 0, connID, &test.k) pkt := w.datagram() - p, n := parse1RTTPacket(pkt, test.k, connIDLen, 0) + p, n := parse1RTTPacket(pkt, &test.k, connIDLen, 0) if n != len(pkt) { t.Errorf("parse1RTTPacket: n=%v, want %v", n, len(pkt)) } diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index 458cd3a93a..8bb3cae211 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -143,12 +143,13 @@ func skipLongHeaderPacket(pkt []byte) int { // // On input, pkt contains a short header packet, k the decryption keys for the packet, // and pnumMax the largest packet number seen in the number space of this packet. -func parse1RTTPacket(pkt []byte, k fixedKeys, dstConnIDLen int, pnumMax packetNumber) (p shortPacket, n int) { - var err error - p.payload, p.num, err = k.unprotect(pkt, 1+dstConnIDLen, pnumMax) +func parse1RTTPacket(pkt []byte, k *updatingKeyPair, dstConnIDLen int, pnumMax packetNumber) (p shortPacket, n int) { + pay, pnum, err := k.unprotect(pkt, 1+dstConnIDLen, pnumMax) if err != nil { return shortPacket{}, -1 } + p.num = pnum + p.payload = pay return p, len(pkt) } diff --git a/internal/quic/packet_protection.go b/internal/quic/packet_protection.go index 2f9b9cefb6..aab1eaf3ac 100644 --- a/internal/quic/packet_protection.go +++ b/internal/quic/packet_protection.go @@ -37,6 +37,10 @@ type headerKey struct { hp headerProtection } +func (k headerKey) isSet() bool { + return k.hp != nil +} + func (k *headerKey) init(suite uint16, secret []byte) { h, keySize := hashForSuite(suite) hpKey := hkdfExpandLabel(h.New, secret, "quic hp", nil, keySize) @@ -275,6 +279,148 @@ func (k *fixedKeyPair) canWrite() bool { return k.w.isSet() } +// An updatingKeys is a header protection key and updatable packet protection key. +// updatingKeys are used for 1-RTT keys, where the packet protection key changes +// over the lifetime of a connection. +// https://www.rfc-editor.org/rfc/rfc9001#section-6 +type updatingKeys struct { + suite uint16 + hdr headerKey + pkt [2]packetKey // current, next + nextSecret []byte // secret used to generate pkt[1] +} + +func (k *updatingKeys) init(suite uint16, secret []byte) { + k.suite = suite + k.hdr.init(suite, secret) + // Initialize pkt[1] with secret_0, and then call update to generate secret_1. + k.pkt[1].init(suite, secret) + k.nextSecret = secret + k.update() +} + +// update performs a key update. +// The current key in pkt[0] is discarded. +// The next key in pkt[1] becomes the current key. +// A new next key is generated in pkt[1]. +func (k *updatingKeys) update() { + k.nextSecret = updateSecret(k.suite, k.nextSecret) + k.pkt[0] = k.pkt[1] + k.pkt[1].init(k.suite, k.nextSecret) +} + +func updateSecret(suite uint16, secret []byte) (nextSecret []byte) { + h, _ := hashForSuite(suite) + return hkdfExpandLabel(h.New, secret, "quic ku", nil, len(secret)) +} + +// An updatingKeyPair is a read/write pair of updating keys. +// +// We keep two keys (current and next) in both read and write directions. +// When an incoming packet's phase matches the current phase bit, +// we unprotect it using the current keys; otherwise we use the next keys. +// +// When updating=false, outgoing packets are protected using the current phase. +// +// An update is initiated and updating is set to true when: +// - we decide to initiate a key update; or +// - we successfully unprotect a packet using the next keys, +// indicating the peer has initiated a key update. +// +// When updating=true, outgoing packets are protected using the next phase. +// We do not change the current phase bit or generate new keys yet. +// +// The update concludes when we receive an ACK frame for a packet sent +// with the next keys. At this time, we set updating to false, flip the +// phase bit, and update the keys. This permits us to handle up to 1-RTT +// of reordered packets before discarding the previous phase's keys after +// an update. +type updatingKeyPair struct { + phase uint8 // current key phase (r.pkt[0], w.pkt[0]) + updating bool + minSent packetNumber // min packet number sent since entering the updating state + minReceived packetNumber // min packet number received in the next phase + r, w updatingKeys +} + +func (k *updatingKeyPair) canRead() bool { + return k.r.hdr.hp != nil +} + +func (k *updatingKeyPair) canWrite() bool { + return k.w.hdr.hp != nil +} + +// handleAckFor finishes a key update after receiving an ACK for a packet in the next phase. +func (k *updatingKeyPair) handleAckFor(pnum packetNumber) { + if k.updating && pnum >= k.minSent { + k.updating = false + k.phase ^= keyPhaseBit + k.r.update() + k.w.update() + } +} + +// needAckEliciting reports whether we should send an ack-eliciting packet in the next phase. +// The first packet sent in a phase is ack-eliciting, since the peer must acknowledge a +// packet in the new phase for us to finish the update. +func (k *updatingKeyPair) needAckEliciting() bool { + return k.updating && k.minSent == maxPacketNumber +} + +// protect applies packet protection to a packet. +// Parameters and returns are as for fixedKeyPair.protect. +func (k *updatingKeyPair) protect(hdr, pay []byte, pnumOff int, pnum packetNumber) []byte { + // TODO: Initiate key updates as required to avoid the AEAD usage limit. + // https://www.rfc-editor.org/rfc/rfc9001#section-6.6 + var pkt []byte + if k.updating { + hdr[0] |= k.phase ^ keyPhaseBit + pkt = k.w.pkt[1].protect(hdr, pay, pnum) + k.minSent = min(pnum, k.minSent) + } else { + hdr[0] |= k.phase + pkt = k.w.pkt[0].protect(hdr, pay, pnum) + } + k.w.hdr.protect(pkt, pnumOff) + return pkt +} + +// unprotect removes packet protection from a packet. +// Parameters and returns are as for fixedKeyPair.unprotect. +func (k *updatingKeyPair) unprotect(pkt []byte, pnumOff int, pnumMax packetNumber) (pay []byte, pnum packetNumber, err error) { + hdr, pay, pnum, err := k.r.hdr.unprotect(pkt, pnumOff, pnumMax) + if err != nil { + return nil, 0, err + } + // To avoid timing signals that might indicate the key phase bit is invalid, + // we always attempt to unprotect the packet with one key. + // + // If the key phase bit matches and the packet number doesn't come after + // the start of an in-progress update, use the current phase. + // Otherwise, use the next phase. + if hdr[0]&keyPhaseBit == k.phase && (!k.updating || pnum < k.minReceived) { + pay, err = k.r.pkt[0].unprotect(hdr, pay, pnum) + if err != nil { + return nil, 0, err + } + } else { + pay, err = k.r.pkt[1].unprotect(hdr, pay, pnum) + if err != nil { + return nil, 0, err + } + if !k.updating { + // The peer has initiated a key update. + k.updating = true + k.minSent = maxPacketNumber + k.minReceived = pnum + } else { + k.minReceived = min(pnum, k.minReceived) + } + } + return pay, pnum, nil +} + // https://www.rfc-editor.org/rfc/rfc9001#section-5.2-2 var initialSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} diff --git a/internal/quic/packet_writer.go b/internal/quic/packet_writer.go index 2009895e04..0c2b2ee41e 100644 --- a/internal/quic/packet_writer.go +++ b/internal/quic/packet_writer.go @@ -163,14 +163,13 @@ func (w *packetWriter) start1RTTPacket(pnum, pnumMaxAcked packetNumber, dstConnI // finish1RTTPacket finishes writing a 1-RTT packet, // canceling the packet if it contains no payload. // It returns a sentPacket describing the packet, or nil if no packet was written. -func (w *packetWriter) finish1RTTPacket(pnum, pnumMaxAcked packetNumber, dstConnID []byte, k fixedKeys) *sentPacket { +func (w *packetWriter) finish1RTTPacket(pnum, pnumMaxAcked packetNumber, dstConnID []byte, k *updatingKeyPair) *sentPacket { if len(w.b) == w.payOff { // The payload is empty, so just abandon the packet. w.b = w.b[:w.pktOff] return nil } // TODO: Spin - // TODO: Key phase pnumLen := packetNumberLength(pnum, pnumMaxAcked) hdr := w.b[:w.pktOff] hdr = append(hdr, 0x40|byte(pnumLen-1)) From b3f1f23077d1f9e85cd09401f758821cbf542d4e Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 19 Sep 2023 10:50:20 -0700 Subject: [PATCH 062/168] quic: initiate key updates For golang/go#58547 Change-Id: If27c0745fc49cb9e8cb9906733ce2f453926b893 Reviewed-on: https://go-review.googlesource.com/c/net/+/529595 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn.go | 1 + internal/quic/conn_test.go | 2 + internal/quic/key_update_test.go | 71 ++++++++++++++++++++++++++++++ internal/quic/packet_codec_test.go | 3 ++ internal/quic/packet_protection.go | 27 +++++++++++- 5 files changed, 102 insertions(+), 2 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index dc3a985e84..5da0ba443b 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -100,6 +100,7 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. // The smallest allowed maximum QUIC datagram size is 1200 bytes. // TODO: PMTU discovery. const maxDatagramSize = 1200 + c.keysAppData.init() c.loss.init(c.side, maxDatagramSize, now) c.streamsInit() c.lifetimeInit() diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 76774cc39c..ac0543b1e3 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -242,6 +242,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { } tc.conn = conn + conn.keysAppData.updateAfter = maxPacketNumber // disable key updates tc.keysInitial.r = conn.keysInitial.w tc.keysInitial.w = conn.keysInitial.r @@ -687,6 +688,7 @@ func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { tc.wkeyAppData.pkt[p.keyNumber], }, }, + updateAfter: maxPacketNumber, } if p.keyPhaseBit { k.phase |= keyPhaseBit diff --git a/internal/quic/key_update_test.go b/internal/quic/key_update_test.go index 6b6bb79803..4a4d677713 100644 --- a/internal/quic/key_update_test.go +++ b/internal/quic/key_update_test.go @@ -161,3 +161,74 @@ func TestKeyUpdateRejectPacketFromPriorPhase(t *testing.T) { }, }) } + +func TestKeyUpdateLocallyInitiated(t *testing.T) { + const updateAfter = 4 // initiate key update after 1-RTT packet 4 + tc := newTestConn(t, serverSide) + tc.conn.keysAppData.updateAfter = updateAfter + tc.handshake() + + for { + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.advanceToTimer() + tc.wantFrameType("conn ACKs last packet", + packetType1RTT, debugFrameAck{}) + if tc.lastPacket.num > updateAfter { + break + } + if got, want := tc.lastPacket.keyNumber, 0; got != want { + t.Errorf("before key update, conn sent packet with key %v, want %v", got, want) + } + if tc.lastPacket.keyPhaseBit { + t.Errorf("before key update, keyPhaseBit is set, want unset") + } + } + if got, want := tc.lastPacket.keyNumber, 1; got != want { + t.Errorf("after key update, conn sent packet with key %v, want %v", got, want) + } + if !tc.lastPacket.keyPhaseBit { + t.Errorf("after key update, keyPhaseBit is unset, want set") + } + tc.wantFrame("first packet after a key update is always ack-eliciting", + packetType1RTT, debugFramePing{}) + tc.wantIdle("no more frames") + + // Peer sends another packet using the prior phase keys. + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.advanceToTimer() + tc.wantFrameType("conn ACKs packet in prior phase", + packetType1RTT, debugFrameAck{}) + tc.wantIdle("packet is not ack-eliciting") + if got, want := tc.lastPacket.keyNumber, 1; got != want { + t.Errorf("after key update, conn sent packet with key %v, want %v", got, want) + } + + // Peer updates to the next phase. + tc.sendKeyNumber = 1 + tc.sendKeyPhaseBit = true + tc.writeAckForAll() + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.advanceToTimer() + tc.wantFrameType("conn ACKs packet in current phase", + packetType1RTT, debugFrameAck{}) + tc.wantIdle("packet is not ack-eliciting") + if got, want := tc.lastPacket.keyNumber, 1; got != want { + t.Errorf("after key update, conn sent packet with key %v, want %v", got, want) + } + + // Peer initiates its own update. + tc.sendKeyNumber = 2 + tc.sendKeyPhaseBit = false + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.advanceToTimer() + tc.wantFrameType("conn ACKs packet in current phase", + packetType1RTT, debugFrameAck{}) + tc.wantFrame("first packet after a key update is always ack-eliciting", + packetType1RTT, debugFramePing{}) + if got, want := tc.lastPacket.keyNumber, 2; got != want { + t.Errorf("after peer key update, conn sent packet with key %v, want %v", got, want) + } + if tc.lastPacket.keyPhaseBit { + t.Errorf("after peer key update, keyPhaseBit is unset, want set") + } +} diff --git a/internal/quic/packet_codec_test.go b/internal/quic/packet_codec_test.go index c8b1f9ba85..4899a03944 100644 --- a/internal/quic/packet_codec_test.go +++ b/internal/quic/packet_codec_test.go @@ -153,6 +153,9 @@ func TestRoundtripEncodeShortPacket(t *testing.T) { aes128Keys.w = aes128Keys.r aes256Keys.w = aes256Keys.r chachaKeys.w = chachaKeys.r + aes128Keys.updateAfter = maxPacketNumber + aes256Keys.updateAfter = maxPacketNumber + chachaKeys.updateAfter = maxPacketNumber connID := make([]byte, connIDLen) for i := range connID { connID[i] = byte(i) diff --git a/internal/quic/packet_protection.go b/internal/quic/packet_protection.go index aab1eaf3ac..1377446133 100644 --- a/internal/quic/packet_protection.go +++ b/internal/quic/packet_protection.go @@ -340,9 +340,19 @@ type updatingKeyPair struct { updating bool minSent packetNumber // min packet number sent since entering the updating state minReceived packetNumber // min packet number received in the next phase + updateAfter packetNumber // packet number after which to initiate key update r, w updatingKeys } +func (k *updatingKeyPair) init() { + // 1-RTT packets until the first key update. + // + // We perform the first key update early in the connection so a peer + // which does not support key updates will fail rapidly, + // rather than after the connection has been long established. + k.updateAfter = 1000 +} + func (k *updatingKeyPair) canRead() bool { return k.r.hdr.hp != nil } @@ -371,8 +381,6 @@ func (k *updatingKeyPair) needAckEliciting() bool { // protect applies packet protection to a packet. // Parameters and returns are as for fixedKeyPair.protect. func (k *updatingKeyPair) protect(hdr, pay []byte, pnumOff int, pnum packetNumber) []byte { - // TODO: Initiate key updates as required to avoid the AEAD usage limit. - // https://www.rfc-editor.org/rfc/rfc9001#section-6.6 var pkt []byte if k.updating { hdr[0] |= k.phase ^ keyPhaseBit @@ -381,6 +389,21 @@ func (k *updatingKeyPair) protect(hdr, pay []byte, pnumOff int, pnum packetNumbe } else { hdr[0] |= k.phase pkt = k.w.pkt[0].protect(hdr, pay, pnum) + if pnum >= k.updateAfter { + // Initiate a key update, starting with the next packet we send. + // + // We do this after protecting the current packet + // to allow Conn.appendFrames to ensure that the first packet sent + // in the new phase is ack-eliciting. + k.updating = true + k.minSent = maxPacketNumber + k.minReceived = maxPacketNumber + // The lowest confidentiality limit for a supported AEAD is 2^23 packets. + // https://www.rfc-editor.org/rfc/rfc9001#section-6.6-5 + // + // Schedule our next update for half that. + k.updateAfter += (1 << 22) + } } k.w.hdr.protect(pkt, pnumOff) return pkt From 7c40cbd80055b0f4414cf98267b0651b156fa4df Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Sun, 17 Sep 2023 08:23:28 +0000 Subject: [PATCH 063/168] dns/dnsmessage: use map[string]uint16 instead of map[string]int The compression pointer is limited to 14 bits, so there is no need to use int, uint16 is fine. Change-Id: I2276cbf63761e26a7e8590f0337930db87895ea5 GitHub-Last-Rev: e04b451a634ef2fdbab67a817bcbdaa566e0cb1b GitHub-Pull-Request: golang/net#192 Reviewed-on: https://go-review.googlesource.com/c/net/+/528955 Reviewed-by: Matthew Dempsky Run-TryBot: Mateusz Poliwczak Reviewed-by: Ian Lance Taylor Auto-Submit: Ian Lance Taylor LUCI-TryBot-Result: Go LUCI TryBot-Result: Gopher Robot --- dns/dnsmessage/message.go | 40 +++++++++++++++++----------------- dns/dnsmessage/message_test.go | 10 ++++----- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index dda888a90a..b6b4f9c197 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -492,7 +492,7 @@ func (r *Resource) GoString() string { // A ResourceBody is a DNS resource record minus the header. type ResourceBody interface { // pack packs a Resource except for its header. - pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) + pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) // realType returns the actual type of the Resource. This is used to // fill in the header Type field. @@ -503,7 +503,7 @@ type ResourceBody interface { } // pack appends the wire format of the Resource to msg. -func (r *Resource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *Resource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { if r.Body == nil { return msg, errNilResouceBody } @@ -1129,7 +1129,7 @@ func (m *Message) AppendPack(b []byte) ([]byte, error) { // DNS messages can be a maximum of 512 bytes long. Without compression, // many DNS response messages are over this limit, so enabling // compression will help ensure compliance. - compression := map[string]int{} + compression := map[string]uint16{} for i := range m.Questions { var err error @@ -1220,7 +1220,7 @@ type Builder struct { // compression is a mapping from name suffixes to their starting index // in msg. - compression map[string]int + compression map[string]uint16 } // NewBuilder creates a new builder with compression disabled. @@ -1257,7 +1257,7 @@ func NewBuilder(buf []byte, h Header) Builder { // // Compression should be enabled before any sections are added for best results. func (b *Builder) EnableCompression() { - b.compression = map[string]int{} + b.compression = map[string]uint16{} } func (b *Builder) startCheck(s section) error { @@ -1673,7 +1673,7 @@ func (h *ResourceHeader) GoString() string { // pack appends the wire format of the ResourceHeader to oldMsg. // // lenOff is the offset in msg where the Length field was packed. -func (h *ResourceHeader) pack(oldMsg []byte, compression map[string]int, compressionOff int) (msg []byte, lenOff int, err error) { +func (h *ResourceHeader) pack(oldMsg []byte, compression map[string]uint16, compressionOff int) (msg []byte, lenOff int, err error) { msg = oldMsg if msg, err = h.Name.pack(msg, compression, compressionOff); err != nil { return oldMsg, 0, &nestedError{"Name", err} @@ -1946,7 +1946,7 @@ func (n *Name) GoString() string { // // The compression map will be updated with new domain suffixes. If compression // is nil, compression will not be used. -func (n *Name) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (n *Name) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { oldMsg := msg if n.Length > nonEncodedNameMax { @@ -2010,7 +2010,7 @@ func (n *Name) pack(msg []byte, compression map[string]int, compressionOff int) // multiple times (for next labels). nameAsStr = string(n.Data[:n.Length]) } - compression[nameAsStr[i:]] = newPtr + compression[nameAsStr[i:]] = uint16(newPtr) } } } @@ -2150,7 +2150,7 @@ type Question struct { } // pack appends the wire format of the Question to msg. -func (q *Question) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (q *Question) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { msg, err := q.Name.pack(msg, compression, compressionOff) if err != nil { return msg, &nestedError{"Name", err} @@ -2246,7 +2246,7 @@ func (r *CNAMEResource) realType() Type { } // pack appends the wire format of the CNAMEResource to msg. -func (r *CNAMEResource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *CNAMEResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { return r.CNAME.pack(msg, compression, compressionOff) } @@ -2274,7 +2274,7 @@ func (r *MXResource) realType() Type { } // pack appends the wire format of the MXResource to msg. -func (r *MXResource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *MXResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { oldMsg := msg msg = packUint16(msg, r.Pref) msg, err := r.MX.pack(msg, compression, compressionOff) @@ -2313,7 +2313,7 @@ func (r *NSResource) realType() Type { } // pack appends the wire format of the NSResource to msg. -func (r *NSResource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *NSResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { return r.NS.pack(msg, compression, compressionOff) } @@ -2340,7 +2340,7 @@ func (r *PTRResource) realType() Type { } // pack appends the wire format of the PTRResource to msg. -func (r *PTRResource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *PTRResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { return r.PTR.pack(msg, compression, compressionOff) } @@ -2377,7 +2377,7 @@ func (r *SOAResource) realType() Type { } // pack appends the wire format of the SOAResource to msg. -func (r *SOAResource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *SOAResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { oldMsg := msg msg, err := r.NS.pack(msg, compression, compressionOff) if err != nil { @@ -2449,7 +2449,7 @@ func (r *TXTResource) realType() Type { } // pack appends the wire format of the TXTResource to msg. -func (r *TXTResource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *TXTResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { oldMsg := msg for _, s := range r.TXT { var err error @@ -2505,7 +2505,7 @@ func (r *SRVResource) realType() Type { } // pack appends the wire format of the SRVResource to msg. -func (r *SRVResource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *SRVResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { oldMsg := msg msg = packUint16(msg, r.Priority) msg = packUint16(msg, r.Weight) @@ -2556,7 +2556,7 @@ func (r *AResource) realType() Type { } // pack appends the wire format of the AResource to msg. -func (r *AResource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *AResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { return packBytes(msg, r.A[:]), nil } @@ -2590,7 +2590,7 @@ func (r *AAAAResource) GoString() string { } // pack appends the wire format of the AAAAResource to msg. -func (r *AAAAResource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *AAAAResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { return packBytes(msg, r.AAAA[:]), nil } @@ -2630,7 +2630,7 @@ func (r *OPTResource) realType() Type { return TypeOPT } -func (r *OPTResource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *OPTResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { for _, opt := range r.Options { msg = packUint16(msg, opt.Code) l := uint16(len(opt.Data)) @@ -2688,7 +2688,7 @@ func (r *UnknownResource) realType() Type { } // pack appends the wire format of the UnknownResource to msg. -func (r *UnknownResource) pack(msg []byte, compression map[string]int, compressionOff int) ([]byte, error) { +func (r *UnknownResource) pack(msg []byte, compression map[string]uint16, compressionOff int) ([]byte, error) { return packBytes(msg, r.Data[:]), nil } diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index 23fb3d5748..c84d5a3aae 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -164,7 +164,7 @@ func TestQuestionPackUnpack(t *testing.T) { Type: TypeA, Class: ClassINET, } - buf, err := want.pack(make([]byte, 1, 50), map[string]int{}, 1) + buf, err := want.pack(make([]byte, 1, 50), map[string]uint16{}, 1) if err != nil { t.Fatal("Question.pack() =", err) } @@ -243,7 +243,7 @@ func TestNamePackUnpack(t *testing.T) { for _, test := range tests { in := MustNewName(test.in) - buf, err := in.pack(make([]byte, 0, 30), map[string]int{}, 0) + buf, err := in.pack(make([]byte, 0, 30), map[string]uint16{}, 0) if err != test.err { t.Errorf("got %q.pack() = %v, want = %v", test.in, err, test.err) continue @@ -305,7 +305,7 @@ func TestNameUnpackTooLongName(t *testing.T) { func TestIncompressibleName(t *testing.T) { name := MustNewName("example.com.") - compression := map[string]int{} + compression := map[string]uint16{} buf, err := name.pack(make([]byte, 0, 100), compression, 0) if err != nil { t.Fatal("first Name.pack() =", err) @@ -623,7 +623,7 @@ func TestVeryLongTxt(t *testing.T) { strings.Repeat(".", 255), }}, } - buf, err := want.pack(make([]byte, 0, 8000), map[string]int{}, 0) + buf, err := want.pack(make([]byte, 0, 8000), map[string]uint16{}, 0) if err != nil { t.Fatal("Resource.pack() =", err) } @@ -647,7 +647,7 @@ func TestVeryLongTxt(t *testing.T) { func TestTooLongTxt(t *testing.T) { rb := TXTResource{[]string{strings.Repeat(".", 256)}} - if _, err := rb.pack(make([]byte, 0, 8000), map[string]int{}, 0); err != errStringTooLong { + if _, err := rb.pack(make([]byte, 0, 8000), map[string]uint16{}, 0); err != errStringTooLong { t.Errorf("packing TXTResource with 256 character string: got err = %v, want = %v", err, errStringTooLong) } } From 8add2e195398868dc8d5c8963110f60aa1887f3d Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 19 Sep 2023 13:41:57 -0700 Subject: [PATCH 064/168] quic: enforce AEAD integrity limit Immediately close a connection after receiving too many packets which fail authentication. RFC 9001, section 6.6. For golang/go#58547 Change-Id: I646b1e89d93fc013f35a2e7b751c4f7b578f42a9 Reviewed-on: https://go-review.googlesource.com/c/net/+/529596 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_recv.go | 9 ++++- internal/quic/packet_codec_test.go | 6 +-- internal/quic/packet_parser.go | 6 +-- internal/quic/packet_protection.go | 56 +++++++++++++++++--------- internal/quic/tls_test.go | 63 ++++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 26 deletions(-) diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 4fc4eeccf0..6347ddae85 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -89,8 +89,13 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int { } pnumMax := c.acks[appDataSpace].largestSeen() - p, n := parse1RTTPacket(buf, &c.keysAppData, connIDLen, pnumMax) - if n < 0 { + p, err := parse1RTTPacket(buf, &c.keysAppData, connIDLen, pnumMax) + if err != nil { + // A localTransportError terminates the connection. + // Other errors indicate an unparseable packet, but otherwise may be ignored. + if _, ok := err.(localTransportError); ok { + c.abort(now, err) + } return -1 } if buf[0]&reserved1RTTBits != 0 { diff --git a/internal/quic/packet_codec_test.go b/internal/quic/packet_codec_test.go index 4899a03944..7b01bb00d6 100644 --- a/internal/quic/packet_codec_test.go +++ b/internal/quic/packet_codec_test.go @@ -193,9 +193,9 @@ func TestRoundtripEncodeShortPacket(t *testing.T) { w.b = append(w.b, test.payload...) w.finish1RTTPacket(test.num, 0, connID, &test.k) pkt := w.datagram() - p, n := parse1RTTPacket(pkt, &test.k, connIDLen, 0) - if n != len(pkt) { - t.Errorf("parse1RTTPacket: n=%v, want %v", n, len(pkt)) + p, err := parse1RTTPacket(pkt, &test.k, connIDLen, 0) + if err != nil { + t.Errorf("parse1RTTPacket: err=%v, want nil", err) } if p.num != test.num || !bytes.Equal(p.payload, test.payload) { t.Errorf("Round-trip encode/decode did not preserve packet.\nsent: num=%v, payload={%x}\ngot: num=%v, payload={%x}", test.num, test.payload, p.num, p.payload) diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index 8bb3cae211..ce04339025 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -143,14 +143,14 @@ func skipLongHeaderPacket(pkt []byte) int { // // On input, pkt contains a short header packet, k the decryption keys for the packet, // and pnumMax the largest packet number seen in the number space of this packet. -func parse1RTTPacket(pkt []byte, k *updatingKeyPair, dstConnIDLen int, pnumMax packetNumber) (p shortPacket, n int) { +func parse1RTTPacket(pkt []byte, k *updatingKeyPair, dstConnIDLen int, pnumMax packetNumber) (p shortPacket, err error) { pay, pnum, err := k.unprotect(pkt, 1+dstConnIDLen, pnumMax) if err != nil { - return shortPacket{}, -1 + return shortPacket{}, err } p.num = pnum p.payload = pay - return p, len(pkt) + return p, nil } // Consume functions return n=-1 on conditions which result in FRAME_ENCODING_ERROR, diff --git a/internal/quic/packet_protection.go b/internal/quic/packet_protection.go index 1377446133..7b141ac49e 100644 --- a/internal/quic/packet_protection.go +++ b/internal/quic/packet_protection.go @@ -336,12 +336,13 @@ func updateSecret(suite uint16, secret []byte) (nextSecret []byte) { // of reordered packets before discarding the previous phase's keys after // an update. type updatingKeyPair struct { - phase uint8 // current key phase (r.pkt[0], w.pkt[0]) - updating bool - minSent packetNumber // min packet number sent since entering the updating state - minReceived packetNumber // min packet number received in the next phase - updateAfter packetNumber // packet number after which to initiate key update - r, w updatingKeys + phase uint8 // current key phase (r.pkt[0], w.pkt[0]) + updating bool + authFailures int64 // total packet unprotect failures + minSent packetNumber // min packet number sent since entering the updating state + minReceived packetNumber // min packet number received in the next phase + updateAfter packetNumber // packet number after which to initiate key update + r, w updatingKeys } func (k *updatingKeyPair) init() { @@ -424,26 +425,45 @@ func (k *updatingKeyPair) unprotect(pkt []byte, pnumOff int, pnumMax packetNumbe // Otherwise, use the next phase. if hdr[0]&keyPhaseBit == k.phase && (!k.updating || pnum < k.minReceived) { pay, err = k.r.pkt[0].unprotect(hdr, pay, pnum) - if err != nil { - return nil, 0, err - } } else { pay, err = k.r.pkt[1].unprotect(hdr, pay, pnum) - if err != nil { - return nil, 0, err + if err == nil { + if !k.updating { + // The peer has initiated a key update. + k.updating = true + k.minSent = maxPacketNumber + k.minReceived = pnum + } else { + k.minReceived = min(pnum, k.minReceived) + } } - if !k.updating { - // The peer has initiated a key update. - k.updating = true - k.minSent = maxPacketNumber - k.minReceived = pnum - } else { - k.minReceived = min(pnum, k.minReceived) + } + if err != nil { + k.authFailures++ + if k.authFailures >= aeadIntegrityLimit(k.r.suite) { + return nil, 0, localTransportError(errAEADLimitReached) } + return nil, 0, err } return pay, pnum, nil } +// aeadIntegrityLimit returns the integrity limit for an AEAD: +// The maximum number of received packets that may fail authentication +// before closing the connection. +// +// https://www.rfc-editor.org/rfc/rfc9001#section-6.6-4 +func aeadIntegrityLimit(suite uint16) int64 { + switch suite { + case tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384: + return 1 << 52 + case tls.TLS_CHACHA20_POLY1305_SHA256: + return 1 << 36 + default: + panic("BUG: unknown cipher suite") + } +} + // https://www.rfc-editor.org/rfc/rfc9001#section-5.2-2 var initialSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 1c7b36d334..4167076889 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -538,3 +538,66 @@ func TestConnCryptoBufferSizeExceeded(t *testing.T) { code: errCryptoBufferExceeded, }) } + +func TestConnAEADLimitReached(t *testing.T) { + // "[...] endpoints MUST count the number of received packets that + // fail authentication during the lifetime of a connection. + // If the total number of received packets that fail authentication [...] + // exceeds the integrity limit for the selected AEAD, + // the endpoint MUST immediately close the connection [...]" + // https://www.rfc-editor.org/rfc/rfc9001#section-6.6-6 + tc := newTestConn(t, clientSide) + tc.handshake() + + var limit int64 + switch suite := tc.conn.keysAppData.r.suite; suite { + case tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384: + limit = 1 << 52 + case tls.TLS_CHACHA20_POLY1305_SHA256: + limit = 1 << 36 + default: + t.Fatalf("conn.keysAppData.r.suite = %v, unknown suite", suite) + } + + dstConnID := tc.conn.connIDState.local[0].cid + if tc.conn.connIDState.local[0].seq == -1 { + // Only use the transient connection ID in Initial packets. + dstConnID = tc.conn.connIDState.local[1].cid + } + invalid := tc.encodeTestPacket(&testPacket{ + ptype: packetType1RTT, + num: 1000, + frames: []debugFrame{debugFramePing{}}, + version: 1, + dstConnID: dstConnID, + srcConnID: tc.peerConnID, + }, 0) + invalid[len(invalid)-1] ^= 1 + sendInvalid := func() { + t.Logf("<- conn under test receives invalid datagram") + tc.conn.sendMsg(&datagram{ + b: invalid, + }) + tc.wait() + } + + // Set the conn's auth failure count to just before the AEAD integrity limit. + tc.conn.keysAppData.authFailures = limit - 1 + + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.advanceToTimer() + tc.wantFrameType("auth failures less than limit: conn ACKs packet", + packetType1RTT, debugFrameAck{}) + + sendInvalid() + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.advanceToTimer() + tc.wantFrameType("auth failures at limit: conn closes", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errAEADLimitReached, + }) + + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.advance(1 * time.Second) + tc.wantIdle("auth failures at limit: conn does not process additional packets") +} From 732b4bc7cb812ca66464f0c333772881f9568360 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 20 Sep 2023 11:49:35 -0700 Subject: [PATCH 065/168] quic: report initial TLS errors Pass errors from QUICConn.Start and the initial flight of TLS events up to the caller. For golang/go#58547 Change-Id: I3a32986bc19a2dd9bf43cd08e3fdd1fa93251a0c Reviewed-on: https://go-review.googlesource.com/c/net/+/529737 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 5da0ba443b..60979125da 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -106,7 +106,7 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. c.lifetimeInit() // TODO: initial_source_connection_id, retry_source_connection_id - c.startTLS(now, initialConnID, transportParameters{ + if err := c.startTLS(now, initialConnID, transportParameters{ initialSrcConnID: c.connIDState.srcConnID(), ackDelayExponent: ackDelayExponent, maxUDPPayloadSize: maxUDPPayloadSize, @@ -119,7 +119,9 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. initialMaxStreamsBidi: c.streams.remoteLimit[bidiStream].max, initialMaxStreamsUni: c.streams.remoteLimit[uniStream].max, activeConnIDLimit: activeConnIDLimit, - }) + }); err != nil { + return nil, err + } go c.loop(now) return c, nil From 3b0ab984dd641d155428ec791d7108be9628c20e Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 20 Sep 2023 14:13:20 -0700 Subject: [PATCH 066/168] quic: avoid deadlock on listener close Avoid holding Listener.connsMu while blocking on a Conn's loop, since the Conn can acquire the mutex while shutting down. Fix Conn.waitReady to check conn readiness before checking the Context doneness. This doesn't make a difference in the current exported API, but this simplifies some tests and will be useful once 0-RTT is implemented. Refactor a bit of the testConn datagram handling to use a testListener type, which helped expose the above deadlock and will be useful for writing tests which don't involve a Conn. Change-Id: I064fca99ae9a165631fc0ff46eb334d25d7dd957 Reviewed-on: https://go-review.googlesource.com/c/net/+/529935 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_close.go | 20 +++++++-- internal/quic/conn_test.go | 41 ++++--------------- internal/quic/listener.go | 2 +- internal/quic/listener_test.go | 75 ++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 38 deletions(-) diff --git a/internal/quic/conn_close.go b/internal/quic/conn_close.go index ec0b7a3270..b8b86fd6fb 100644 --- a/internal/quic/conn_close.go +++ b/internal/quic/conn_close.go @@ -168,6 +168,13 @@ func (c *Conn) enterDraining(err error) { } func (c *Conn) waitReady(ctx context.Context) error { + select { + case <-c.lifetime.readyc: + return nil + case <-c.lifetime.drainingc: + return c.lifetime.finalErr + default: + } select { case <-c.lifetime.readyc: return nil @@ -215,7 +222,7 @@ func (c *Conn) Abort(err error) { if err == nil { err = localTransportError(errNo) } - c.runOnLoop(func(now time.Time, c *Conn) { + c.sendMsg(func(now time.Time, c *Conn) { c.abort(now, err) }) } @@ -228,11 +235,18 @@ func (c *Conn) abort(now time.Time, err error) { c.lifetime.localErr = err } +// abortImmediately terminates a connection. +// The connection does not send a CONNECTION_CLOSE, and skips the draining period. +func (c *Conn) abortImmediately(now time.Time, err error) { + c.abort(now, err) + c.enterDraining(err) + c.exited = true +} + // exit fully terminates a connection immediately. func (c *Conn) exit() { - c.runOnLoop(func(now time.Time, c *Conn) { + c.sendMsg(func(now time.Time, c *Conn) { c.enterDraining(errors.New("connection closed")) c.exited = true }) - <-c.donec } diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index ac0543b1e3..d75b2eb690 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -13,9 +13,7 @@ import ( "errors" "flag" "fmt" - "io" "math" - "net" "net/netip" "reflect" "strings" @@ -112,7 +110,7 @@ const maxTestKeyPhases = 3 type testConn struct { t *testing.T conn *Conn - listener *Listener + listener *testListener now time.Time timer time.Time timerLastFired time.Time @@ -231,8 +229,8 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { tc.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) tc.peerTLSConn.Start(context.Background()) - tc.listener = newListener((*testConnUDPConn)(tc), config, (*testConnHooks)(tc)) - conn, err := tc.listener.newConn( + tc.listener = newTestListener(t, config, (*testConnHooks)(tc)) + conn, err := tc.listener.l.newConn( tc.now, side, initialConnID, @@ -335,7 +333,7 @@ func (tc *testConn) cleanup() { return } tc.conn.exit() - tc.listener.Close(context.Background()) + <-tc.conn.donec } func (tc *testConn) logDatagram(text string, d *testDatagram) { @@ -388,6 +386,7 @@ func (tc *testConn) write(d *testDatagram) { for len(buf) < d.paddedSize { buf = append(buf, 0) } + // TODO: This should use tc.listener.write. tc.conn.sendMsg(&datagram{ b: buf, }) @@ -457,11 +456,10 @@ func (tc *testConn) readDatagram() *testDatagram { tc.wait() tc.sentPackets = nil tc.sentFrames = nil - if len(tc.sentDatagrams) == 0 { + buf := tc.listener.read() + if buf == nil { return nil } - buf := tc.sentDatagrams[0] - tc.sentDatagrams = tc.sentDatagrams[1:] d := tc.parseTestDatagram(buf) // Log the datagram before removing ignored frames. // When things go wrong, it's useful to see all the frames. @@ -982,31 +980,6 @@ func testPeerConnID(seq int64) []byte { return []byte{0xbe, 0xee, 0xff, byte(seq)} } -// testConnUDPConn implements UDPConn. -type testConnUDPConn testConn - -func (tc *testConnUDPConn) Close() error { - close(tc.recvDatagram) - return nil -} - -func (tc *testConnUDPConn) LocalAddr() net.Addr { - return net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:443")) -} - -func (tc *testConnUDPConn) ReadMsgUDPAddrPort(b, control []byte) (n, controln, flags int, _ netip.AddrPort, _ error) { - for d := range tc.recvDatagram { - n = copy(b, d.b) - return n, 0, 0, d.addr, nil - } - return 0, 0, 0, netip.AddrPort{}, io.EOF -} - -func (tc *testConnUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { - tc.sentDatagrams = append(tc.sentDatagrams, append([]byte(nil), b...)) - return len(b), nil -} - // canceledContext returns a canceled Context. // // Functions which take a context preference progress over cancelation. diff --git a/internal/quic/listener.go b/internal/quic/listener.go index a84286e894..03d8ec65f4 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -104,7 +104,7 @@ func (l *Listener) Close(ctx context.Context) error { if !l.closing { l.closing = true for c := range l.conns { - c.Close() + c.Abort(errors.New("listener closed")) } if len(l.conns) == 0 { l.udpConn.Close() diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index a6e0b3464c..9d0f314ecc 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -10,6 +10,8 @@ import ( "bytes" "context" "io" + "net" + "net/netip" "testing" ) @@ -86,3 +88,76 @@ func newLocalListener(t *testing.T, side connSide, conf *Config) *Listener { }) return l } + +type testListener struct { + t *testing.T + l *Listener + recvc chan *datagram + idlec chan struct{} + sentDatagrams [][]byte +} + +func newTestListener(t *testing.T, config *Config, testHooks connTestHooks) *testListener { + tl := &testListener{ + t: t, + recvc: make(chan *datagram), + idlec: make(chan struct{}), + } + tl.l = newListener((*testListenerUDPConn)(tl), config, testHooks) + t.Cleanup(tl.cleanup) + return tl +} + +func (tl *testListener) cleanup() { + tl.l.Close(canceledContext()) +} + +func (tl *testListener) wait() { + tl.idlec <- struct{}{} +} + +func (tl *testListener) write(d *datagram) { + tl.recvc <- d + tl.wait() +} + +func (tl *testListener) read() []byte { + tl.wait() + if len(tl.sentDatagrams) == 0 { + return nil + } + d := tl.sentDatagrams[0] + tl.sentDatagrams = tl.sentDatagrams[1:] + return d +} + +// testListenerUDPConn implements UDPConn. +type testListenerUDPConn testListener + +func (tl *testListenerUDPConn) Close() error { + close(tl.recvc) + return nil +} + +func (tl *testListenerUDPConn) LocalAddr() net.Addr { + return net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:443")) +} + +func (tl *testListenerUDPConn) ReadMsgUDPAddrPort(b, control []byte) (n, controln, flags int, _ netip.AddrPort, _ error) { + for { + select { + case d, ok := <-tl.recvc: + if !ok { + return 0, 0, 0, netip.AddrPort{}, io.EOF + } + n = copy(b, d.b) + return n, 0, 0, d.addr, nil + case <-tl.idlec: + } + } +} + +func (tl *testListenerUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { + tl.sentDatagrams = append(tl.sentDatagrams, append([]byte(nil), b...)) + return len(b), nil +} From ddd8598e5694aa5e966e44573a53e895f6fa5eb2 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 19 Sep 2023 16:47:05 -0700 Subject: [PATCH 067/168] quic: version negotiation Servers respond to packets containing an unrecognized version with a Version Negotiation packet. Clients respond to Version Negotiation packets by aborting the connection attempt, since we support only one version. RFC 9000, Section 6 For golang/go#58547 Change-Id: I3f3a66a4d69950cc7dc22146ad2eddb93cbe34f7 Reviewed-on: https://go-review.googlesource.com/c/net/+/529739 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_recv.go | 47 +++++++++++++ internal/quic/conn_send.go | 4 +- internal/quic/conn_test.go | 2 +- internal/quic/listener.go | 88 ++++++++++++++++++------- internal/quic/packet.go | 71 +++++++++++++++++++- internal/quic/packet_test.go | 120 ++++++++++++++++++++++++++++++++++ internal/quic/quic.go | 7 ++ internal/quic/tls_test.go | 12 ++-- internal/quic/version_test.go | 110 +++++++++++++++++++++++++++++++ 9 files changed, 429 insertions(+), 32 deletions(-) create mode 100644 internal/quic/version_test.go diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 6347ddae85..19c43858c5 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -7,6 +7,9 @@ package quic import ( + "bytes" + "encoding/binary" + "errors" "time" ) @@ -31,6 +34,9 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { n = c.handleLongHeader(now, ptype, handshakeSpace, c.keysHandshake.r, buf) case packetType1RTT: n = c.handle1RTT(now, buf) + case packetTypeVersionNegotiation: + c.handleVersionNegotiation(now, buf) + return default: return } @@ -59,6 +65,11 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa c.abort(now, localTransportError(errProtocolViolation)) return -1 } + if p.version != quicVersion1 { + // The peer has changed versions on us mid-handshake? + c.abort(now, localTransportError(errProtocolViolation)) + return -1 + } if !c.acks[space].shouldProcess(p.num) { return n @@ -117,6 +128,42 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int { return len(buf) } +var errVersionNegotiation = errors.New("server does not support QUIC version 1") + +func (c *Conn) handleVersionNegotiation(now time.Time, pkt []byte) { + if c.side != clientSide { + return // servers don't handle Version Negotiation packets + } + // "A client MUST discard any Version Negotiation packet if it has + // received and successfully processed any other packet [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-6.2-2 + if !c.keysInitial.canRead() { + return // discarded Initial keys, connection is already established + } + if c.acks[initialSpace].seen.numRanges() != 0 { + return // processed at least one packet + } + _, srcConnID, versions := parseVersionNegotiation(pkt) + if len(c.connIDState.remote) < 1 || !bytes.Equal(c.connIDState.remote[0].cid, srcConnID) { + return // Source Connection ID doesn't match what we sent + } + for len(versions) >= 4 { + ver := binary.BigEndian.Uint32(versions) + if ver == 1 { + // "A client MUST discard a Version Negotiation packet that lists + // the QUIC version selected by the client." + // https://www.rfc-editor.org/rfc/rfc9000#section-6.2-2 + return + } + versions = versions[4:] + } + // "A client that supports only this version of QUIC MUST + // abandon the current connection attempt if it receives + // a Version Negotiation packet, [with the two exceptions handled above]." + // https://www.rfc-editor.org/rfc/rfc9000#section-6.2-2 + c.abortImmediately(now, errVersionNegotiation) +} + func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, payload []byte) (ackEliciting bool) { if len(payload) == 0 { // "An endpoint MUST treat receipt of a packet containing no frames diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 63f65b5579..00b02c2a31 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -64,7 +64,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { pnum := c.loss.nextNumber(initialSpace) p := longPacket{ ptype: packetTypeInitial, - version: 1, + version: quicVersion1, num: pnum, dstConnID: dstConnID, srcConnID: c.connIDState.srcConnID(), @@ -91,7 +91,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { pnum := c.loss.nextNumber(handshakeSpace) p := longPacket{ ptype: packetTypeHandshake, - version: 1, + version: quicVersion1, num: pnum, dstConnID: dstConnID, srcConnID: c.connIDState.srcConnID(), diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index d75b2eb690..fd9e6e42e2 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -409,7 +409,7 @@ func (tc *testConn) writeFrames(ptype packetType, frames ...debugFrame) { keyNumber: tc.sendKeyNumber, keyPhaseBit: tc.sendKeyPhaseBit, frames: frames, - version: 1, + version: quicVersion1, dstConnID: dstConnID, srcConnID: tc.peerConnID, }}, diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 03d8ec65f4..96b1e45934 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -239,32 +239,15 @@ func (l *Listener) listen() { func (l *Listener) handleDatagram(m *datagram, conns map[string]*Conn) { dstConnID, ok := dstConnIDForDatagram(m.b) if !ok { + m.recycle() return } c := conns[string(dstConnID)] if c == nil { - if getPacketType(m.b) != packetTypeInitial { - // This packet isn't trying to create a new connection. - // It might be associated with some connection we've lost state for. - // TODO: Send a stateless reset when appropriate. - // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.3 - return - } - var now time.Time - if l.testHooks != nil { - now = l.testHooks.timeNow() - } else { - now = time.Now() - } - var err error - c, err = l.newConn(now, serverSide, dstConnID, m.addr) - if err != nil { - // The accept queue is probably full. - // We could send a CONNECTION_CLOSE to the peer to reject the connection. - // Currently, we just drop the datagram. - // https://www.rfc-editor.org/rfc/rfc9000.html#section-5.2.2-5 - return - } + // TODO: Move this branch into a separate goroutine to avoid blocking + // the listener while processing packets. + l.handleUnknownDestinationDatagram(m) + return } // TODO: This can block the listener while waiting for the conn to accept the dgram. @@ -272,6 +255,67 @@ func (l *Listener) handleDatagram(m *datagram, conns map[string]*Conn) { c.sendMsg(m) } +func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { + defer func() { + if m != nil { + m.recycle() + } + }() + if len(m.b) < minimumClientInitialDatagramSize { + return + } + p, ok := parseGenericLongHeaderPacket(m.b) + if !ok { + // Not a long header packet, or not parseable. + // Short header (1-RTT) packets don't contain enough information + // to do anything useful with if we don't recognize the + // connection ID. + return + } + + switch p.version { + case quicVersion1: + case 0: + // Version Negotiation for an unknown connection. + return + default: + // Unknown version. + l.sendVersionNegotiation(p, m.addr) + return + } + if getPacketType(m.b) != packetTypeInitial { + // This packet isn't trying to create a new connection. + // It might be associated with some connection we've lost state for. + // TODO: Send a stateless reset when appropriate. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.3 + return + } + var now time.Time + if l.testHooks != nil { + now = l.testHooks.timeNow() + } else { + now = time.Now() + } + var err error + c, err := l.newConn(now, serverSide, p.dstConnID, m.addr) + if err != nil { + // The accept queue is probably full. + // We could send a CONNECTION_CLOSE to the peer to reject the connection. + // Currently, we just drop the datagram. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-5.2.2-5 + return + } + c.sendMsg(m) + m = nil // don't recycle, sendMsg takes ownership +} + +func (l *Listener) sendVersionNegotiation(p genericLongPacket, addr netip.AddrPort) { + m := newDatagram() + m.b = appendVersionNegotiation(m.b[:0], p.srcConnID, p.dstConnID, quicVersion1) + l.sendDatagram(m.b, addr) + m.recycle() +} + func (l *Listener) sendDatagram(p []byte, addr netip.AddrPort) error { _, err := l.udpConn.WriteToUDPAddrPort(p, addr) return err diff --git a/internal/quic/packet.go b/internal/quic/packet.go index 8242bd0a97..7d69f96d27 100644 --- a/internal/quic/packet.go +++ b/internal/quic/packet.go @@ -6,7 +6,10 @@ package quic -import "fmt" +import ( + "encoding/binary" + "fmt" +) // packetType is a QUIC packet type. // https://www.rfc-editor.org/rfc/rfc9000.html#section-17 @@ -157,6 +160,33 @@ func dstConnIDForDatagram(pkt []byte) (id []byte, ok bool) { return b[:n], true } +// parseVersionNegotiation parses a Version Negotiation packet. +// The returned versions is a slice of big-endian uint32s. +// It returns (nil, nil, nil) for an invalid packet. +func parseVersionNegotiation(pkt []byte) (dstConnID, srcConnID, versions []byte) { + p, ok := parseGenericLongHeaderPacket(pkt) + if !ok { + return nil, nil, nil + } + if len(p.data)%4 != 0 { + return nil, nil, nil + } + return p.dstConnID, p.srcConnID, p.data +} + +// appendVersionNegotiation appends a Version Negotiation packet to pkt, +// returning the result. +func appendVersionNegotiation(pkt, dstConnID, srcConnID []byte, versions ...uint32) []byte { + pkt = append(pkt, headerFormLong|fixedBit) // header byte + pkt = append(pkt, 0, 0, 0, 0) // Version (0 for Version Negotiation) + pkt = appendUint8Bytes(pkt, dstConnID) // Destination Connection ID + pkt = appendUint8Bytes(pkt, srcConnID) // Source Connection ID + for _, v := range versions { + pkt = binary.BigEndian.AppendUint32(pkt, v) // Supported Version + } + return pkt +} + // A longPacket is a long header packet. type longPacket struct { ptype packetType @@ -177,3 +207,42 @@ type shortPacket struct { num packetNumber payload []byte } + +// A genericLongPacket is a long header packet of an arbitrary QUIC version. +// https://www.rfc-editor.org/rfc/rfc8999#section-5.1 +type genericLongPacket struct { + version uint32 + dstConnID []byte + srcConnID []byte + data []byte +} + +func parseGenericLongHeaderPacket(b []byte) (p genericLongPacket, ok bool) { + if len(b) < 5 || !isLongHeader(b[0]) { + return genericLongPacket{}, false + } + b = b[1:] + // Version (32), + var n int + p.version, n = consumeUint32(b) + if n < 0 { + return genericLongPacket{}, false + } + b = b[n:] + // Destination Connection ID Length (8), + // Destination Connection ID (0..2048), + p.dstConnID, n = consumeUint8Bytes(b) + if n < 0 || len(p.dstConnID) > 2048/8 { + return genericLongPacket{}, false + } + b = b[n:] + // Source Connection ID Length (8), + // Source Connection ID (0..2048), + p.srcConnID, n = consumeUint8Bytes(b) + if n < 0 || len(p.dstConnID) > 2048/8 { + return genericLongPacket{}, false + } + b = b[n:] + p.data = b + return p, true +} diff --git a/internal/quic/packet_test.go b/internal/quic/packet_test.go index b13a587e54..58c584e162 100644 --- a/internal/quic/packet_test.go +++ b/internal/quic/packet_test.go @@ -8,7 +8,9 @@ package quic import ( "bytes" + "encoding/binary" "encoding/hex" + "reflect" "strings" "testing" ) @@ -112,6 +114,124 @@ func TestPacketHeader(t *testing.T) { } } +func TestEncodeDecodeVersionNegotiation(t *testing.T) { + dstConnID := []byte("this is a very long destination connection id") + srcConnID := []byte("this is a very long source connection id") + versions := []uint32{1, 0xffffffff} + got := appendVersionNegotiation([]byte{}, dstConnID, srcConnID, versions...) + want := bytes.Join([][]byte{{ + 0b1100_0000, // header byte + 0, 0, 0, 0, // Version + byte(len(dstConnID)), + }, dstConnID, { + byte(len(srcConnID)), + }, srcConnID, { + 0x00, 0x00, 0x00, 0x01, + 0xff, 0xff, 0xff, 0xff, + }}, nil) + if !bytes.Equal(got, want) { + t.Fatalf("appendVersionNegotiation(nil, %x, %x, %v):\ngot %x\nwant %x", + dstConnID, srcConnID, versions, got, want) + } + gotDst, gotSrc, gotVersionBytes := parseVersionNegotiation(got) + if got, want := gotDst, dstConnID; !bytes.Equal(got, want) { + t.Errorf("parseVersionNegotiation: got dstConnID = %x, want %x", got, want) + } + if got, want := gotSrc, srcConnID; !bytes.Equal(got, want) { + t.Errorf("parseVersionNegotiation: got srcConnID = %x, want %x", got, want) + } + var gotVersions []uint32 + for len(gotVersionBytes) >= 4 { + gotVersions = append(gotVersions, binary.BigEndian.Uint32(gotVersionBytes)) + gotVersionBytes = gotVersionBytes[4:] + } + if got, want := gotVersions, versions; !reflect.DeepEqual(got, want) { + t.Errorf("parseVersionNegotiation: got versions = %v, want %v", got, want) + } +} + +func TestParseGenericLongHeaderPacket(t *testing.T) { + for _, test := range []struct { + name string + packet []byte + version uint32 + dstConnID []byte + srcConnID []byte + data []byte + }{{ + name: "long header packet", + packet: unhex(` + 80 01020304 04a1a2a3a4 05b1b2b3b4b5 c1 + `), + version: 0x01020304, + dstConnID: unhex(`a1a2a3a4`), + srcConnID: unhex(`b1b2b3b4b5`), + data: unhex(`c1`), + }, { + name: "zero everything", + packet: unhex(` + 80 00000000 00 00 + `), + version: 0, + dstConnID: []byte{}, + srcConnID: []byte{}, + data: []byte{}, + }} { + t.Run(test.name, func(t *testing.T) { + p, ok := parseGenericLongHeaderPacket(test.packet) + if !ok { + t.Fatalf("parseGenericLongHeaderPacket() = _, false; want true") + } + if got, want := p.version, test.version; got != want { + t.Errorf("version = %v, want %v", got, want) + } + if got, want := p.dstConnID, test.dstConnID; !bytes.Equal(got, want) { + t.Errorf("Destination Connection ID = {%x}, want {%x}", got, want) + } + if got, want := p.srcConnID, test.srcConnID; !bytes.Equal(got, want) { + t.Errorf("Source Connection ID = {%x}, want {%x}", got, want) + } + if got, want := p.data, test.data; !bytes.Equal(got, want) { + t.Errorf("Data = {%x}, want {%x}", got, want) + } + }) + } +} + +func TestParseGenericLongHeaderPacketErrors(t *testing.T) { + for _, test := range []struct { + name string + packet []byte + }{{ + name: "short header packet", + packet: unhex(` + 00 01020304 04a1a2a3a4 05b1b2b3b4b5 c1 + `), + }, { + name: "packet too short", + packet: unhex(` + 80 000000 + `), + }, { + name: "destination id too long", + packet: unhex(` + 80 00000000 02 00 + `), + }, { + name: "source id too long", + packet: unhex(` + 80 00000000 00 01 + `), + }} { + t.Run(test.name, func(t *testing.T) { + _, ok := parseGenericLongHeaderPacket(test.packet) + if ok { + t.Fatalf("parseGenericLongHeaderPacket() = _, true; want false") + } + }) + } +} + func unhex(s string) []byte { b, err := hex.DecodeString(strings.Map(func(c rune) rune { switch c { diff --git a/internal/quic/quic.go b/internal/quic/quic.go index cf4137e810..9de97b6d88 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -10,6 +10,13 @@ import ( "time" ) +// QUIC versions. +// We only support v1 at this time. +const ( + quicVersion1 = 1 + quicVersion2 = 0x6b3343cf // https://www.rfc-editor.org/rfc/rfc9369 +) + // connIDLen is the length in bytes of connection IDs chosen by this package. // Since 1-RTT packets don't include a connection ID length field, // we use a consistent length for all our IDs. diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 4167076889..81d17b8587 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -97,7 +97,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { packets: []*testPacket{{ ptype: packetTypeInitial, num: 0, - version: 1, + version: quicVersion1, srcConnID: clientConnIDs[0], dstConnID: transientConnID, frames: []debugFrame{ @@ -110,7 +110,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { packets: []*testPacket{{ ptype: packetTypeInitial, num: 0, - version: 1, + version: quicVersion1, srcConnID: serverConnIDs[0], dstConnID: clientConnIDs[0], frames: []debugFrame{ @@ -122,7 +122,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { }, { ptype: packetTypeHandshake, num: 0, - version: 1, + version: quicVersion1, srcConnID: serverConnIDs[0], dstConnID: clientConnIDs[0], frames: []debugFrame{ @@ -144,7 +144,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { packets: []*testPacket{{ ptype: packetTypeInitial, num: 1, - version: 1, + version: quicVersion1, srcConnID: clientConnIDs[0], dstConnID: serverConnIDs[0], frames: []debugFrame{ @@ -155,7 +155,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { }, { ptype: packetTypeHandshake, num: 0, - version: 1, + version: quicVersion1, srcConnID: clientConnIDs[0], dstConnID: serverConnIDs[0], frames: []debugFrame{ @@ -568,7 +568,7 @@ func TestConnAEADLimitReached(t *testing.T) { ptype: packetType1RTT, num: 1000, frames: []debugFrame{debugFramePing{}}, - version: 1, + version: quicVersion1, dstConnID: dstConnID, srcConnID: tc.peerConnID, }, 0) diff --git a/internal/quic/version_test.go b/internal/quic/version_test.go new file mode 100644 index 0000000000..cfb7ce4be7 --- /dev/null +++ b/internal/quic/version_test.go @@ -0,0 +1,110 @@ +// 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" + "context" + "crypto/tls" + "testing" +) + +func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { + config := &Config{ + TLSConfig: newTestTLSConfig(serverSide), + } + tl := newTestListener(t, config, nil) + + // Packet of unknown contents for some unrecognized QUIC version. + dstConnID := []byte{1, 2, 3, 4} + srcConnID := []byte{5, 6, 7, 8} + pkt := []byte{ + 0b1000_0000, + 0x00, 0x00, 0x00, 0x0f, + } + pkt = append(pkt, byte(len(dstConnID))) + pkt = append(pkt, dstConnID...) + pkt = append(pkt, byte(len(srcConnID))) + pkt = append(pkt, srcConnID...) + for len(pkt) < minimumClientInitialDatagramSize { + pkt = append(pkt, 0) + } + + tl.write(&datagram{ + b: pkt, + }) + gotPkt := tl.read() + if gotPkt == nil { + t.Fatalf("got no response; want Version Negotiaion") + } + if got := getPacketType(gotPkt); got != packetTypeVersionNegotiation { + t.Fatalf("got packet type %v; want Version Negotiaion", got) + } + gotDst, gotSrc, versions := parseVersionNegotiation(gotPkt) + if got, want := gotDst, srcConnID; !bytes.Equal(got, want) { + t.Errorf("got Destination Connection ID %x, want %x", got, want) + } + if got, want := gotSrc, dstConnID; !bytes.Equal(got, want) { + t.Errorf("got Source Connection ID %x, want %x", got, want) + } + if got, want := versions, []byte{0, 0, 0, 1}; !bytes.Equal(got, want) { + t.Errorf("got Supported Version %x, want %x", got, want) + } +} + +func TestVersionNegotiationClientAborts(t *testing.T) { + tc := newTestConn(t, clientSide) + p := tc.readPacket() // client Initial packet + tc.listener.write(&datagram{ + b: appendVersionNegotiation(nil, p.srcConnID, p.dstConnID, 10), + }) + tc.wantIdle("connection does not send a CONNECTION_CLOSE") + if err := tc.conn.waitReady(canceledContext()); err != errVersionNegotiation { + t.Errorf("conn.waitReady() = %v, want errVersionNegotiation", err) + } +} + +func TestVersionNegotiationClientIgnoresAfterProcessingPacket(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.ignoreFrame(frameTypeAck) + p := tc.readPacket() // client Initial packet + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.listener.write(&datagram{ + b: appendVersionNegotiation(nil, p.srcConnID, p.dstConnID, 10), + }) + if err := tc.conn.waitReady(canceledContext()); err != context.Canceled { + t.Errorf("conn.waitReady() = %v, want context.Canceled", err) + } + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + tc.wantFrameType("conn ignores Version Negotiation and continues with handshake", + packetTypeHandshake, debugFrameCrypto{}) +} + +func TestVersionNegotiationClientIgnoresMismatchingSourceConnID(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.ignoreFrame(frameTypeAck) + p := tc.readPacket() // client Initial packet + tc.listener.write(&datagram{ + b: appendVersionNegotiation(nil, p.srcConnID, []byte("mismatch"), 10), + }) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + tc.wantFrameType("conn ignores Version Negotiation and continues with handshake", + packetTypeHandshake, debugFrameCrypto{}) +} From ea633599b58dc6a50d33c7f5438edfaa8bc313df Mon Sep 17 00:00:00 2001 From: Alexander Yastrebov Date: Fri, 21 Jul 2023 21:33:50 +0000 Subject: [PATCH 068/168] http2: check stream body is present on read timeout Check stream body is not nil in the handler to cover all callsites For golang/go#58237 Change-Id: Ibeb19f2597f12da71b8dfb73718e230b4b316d06 GitHub-Last-Rev: dc87befd81750670f48bb1be291e24f52d607a9d GitHub-Pull-Request: golang/net#162 Reviewed-on: https://go-review.googlesource.com/c/net/+/464936 Reviewed-by: Bryan Mills Reviewed-by: Matthew Dempsky Run-TryBot: Damien Neil TryBot-Result: Gopher Robot Reviewed-by: Damien Neil Auto-Submit: Bryan Mills Commit-Queue: Bryan Mills --- http2/server.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/http2/server.go b/http2/server.go index 6d5e008874..de60fa88f1 100644 --- a/http2/server.go +++ b/http2/server.go @@ -1892,9 +1892,11 @@ func (st *stream) copyTrailersToHandlerRequest() { // onReadTimeout is run on its own goroutine (from time.AfterFunc) // when the stream's ReadTimeout has fired. func (st *stream) onReadTimeout() { - // Wrap the ErrDeadlineExceeded to avoid callers depending on us - // returning the bare error. - st.body.CloseWithError(fmt.Errorf("%w", os.ErrDeadlineExceeded)) + if st.body != nil { + // Wrap the ErrDeadlineExceeded to avoid callers depending on us + // returning the bare error. + st.body.CloseWithError(fmt.Errorf("%w", os.ErrDeadlineExceeded)) + } } // onWriteTimeout is run on its own goroutine (from time.AfterFunc) @@ -2012,9 +2014,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error { // (in Go 1.8), though. That's a more sane option anyway. if sc.hs.ReadTimeout != 0 { sc.conn.SetReadDeadline(time.Time{}) - if st.body != nil { - st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout) - } + st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout) } go sc.runHandler(rw, req, handler) From a600b3518eed7a9a4e24380b4b249cb986d9b64d Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 20 Sep 2023 16:16:40 -0700 Subject: [PATCH 069/168] quic: avoid redundant MAX_DATA updates When Stream.Read determines that we should send a MAX_DATA update, it sends a message to the Conn to mark us as needing one. If a second Read happens before the message from the first read is processed, we may send a redundant MAX_DATA update. This is harmless, but inefficient. Double check that we still need to send an update before marking one as necessary. Change-Id: I0eb5a591eae6929b91da68b1ab6834a7795323ee Reviewed-on: https://go-review.googlesource.com/c/net/+/530035 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_flow.go | 5 +++- internal/quic/conn_flow_test.go | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/internal/quic/conn_flow.go b/internal/quic/conn_flow.go index cd9a6a912b..281c7084ff 100644 --- a/internal/quic/conn_flow.go +++ b/internal/quic/conn_flow.go @@ -54,7 +54,10 @@ func (c *Conn) handleStreamBytesReadOffLoop(n int64) { // We should send a MAX_DATA update to the peer. // Record this on the Conn's main loop. c.sendMsg(func(now time.Time, c *Conn) { - c.sendMaxDataUpdate() + // A MAX_DATA update may have already happened, so check again. + if c.shouldUpdateFlowControl(c.streams.inflow.credit.Load()) { + c.sendMaxDataUpdate() + } }) } } diff --git a/internal/quic/conn_flow_test.go b/internal/quic/conn_flow_test.go index 2cd4e62469..45c82f60d2 100644 --- a/internal/quic/conn_flow_test.go +++ b/internal/quic/conn_flow_test.go @@ -7,6 +7,7 @@ package quic import ( + "context" "testing" ) @@ -36,6 +37,56 @@ func TestConnInflowReturnOnRead(t *testing.T) { }) } +func TestConnInflowReturnOnRacingReads(t *testing.T) { + // Perform two reads at the same time, + // one for half of MaxConnReadBufferSize + // and one for one byte. + // + // We should observe a single MAX_DATA update. + // Depending on the ordering of events, + // this may include the credit from just the larger read + // or the credit from both. + ctx := canceledContext() + tc := newTestConn(t, serverSide, func(c *Config) { + c.MaxConnReadBufferSize = 64 + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, uniStream, 0), + data: make([]byte, 32), + }) + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, uniStream, 1), + data: make([]byte, 32), + }) + s1, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("conn.AcceptStream() = %v", err) + } + s2, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("conn.AcceptStream() = %v", err) + } + read1 := runAsync(tc, func(ctx context.Context) (int, error) { + return s1.ReadContext(ctx, make([]byte, 16)) + }) + read2 := runAsync(tc, func(ctx context.Context) (int, error) { + return s2.ReadContext(ctx, make([]byte, 1)) + }) + // This MAX_DATA might extend the window by 16 or 17, depending on + // whether the second write occurs before the update happens. + tc.wantFrameType("MAX_DATA update is sent", + packetType1RTT, debugFrameMaxData{}) + tc.wantIdle("redundant MAX_DATA is not sent") + if _, err := read1.result(); err != nil { + t.Errorf("ReadContext #1 = %v", err) + } + if _, err := read2.result(); err != nil { + t.Errorf("ReadContext #2 = %v", err) + } +} + func TestConnInflowReturnOnClose(t *testing.T) { tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { c.MaxConnReadBufferSize = 64 From 21814e71db756f39b69fb1a3e06350fa555a79b1 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 20 Sep 2023 18:29:51 -0700 Subject: [PATCH 070/168] quic: validate connection id transport parameters Validate the original_destination_connection_id and initial_source_connection_id transport parameters. RFC 9000, Section 7.3 For golang/go#58547 Change-Id: I8343fd53c5cc946f15d3410c632b3895205fd597 Reviewed-on: https://go-review.googlesource.com/c/net/+/530036 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn.go | 8 ++++++- internal/quic/conn_id.go | 44 +++++++++++++++++++++++++++++++---- internal/quic/conn_id_test.go | 38 ++++++++++++++++++++++++++++-- internal/quic/conn_test.go | 4 ++++ 4 files changed, 87 insertions(+), 7 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 60979125da..9db00fe092 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -86,6 +86,7 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. // non-blocking operation. c.msgc = make(chan any, 1) + var originalDstConnID []byte if c.side == clientSide { if err := c.connIDState.initClient(c); err != nil { return nil, err @@ -95,6 +96,7 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. if err := c.connIDState.initServer(c, initialConnID); err != nil { return nil, err } + originalDstConnID = initialConnID } // The smallest allowed maximum QUIC datagram size is 1200 bytes. @@ -105,9 +107,10 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. c.streamsInit() c.lifetimeInit() - // TODO: initial_source_connection_id, retry_source_connection_id + // TODO: retry_source_connection_id if err := c.startTLS(now, initialConnID, transportParameters{ initialSrcConnID: c.connIDState.srcConnID(), + originalDstConnID: originalDstConnID, ackDelayExponent: ackDelayExponent, maxUDPPayloadSize: maxUDPPayloadSize, maxAckDelay: maxAckDelay, @@ -171,6 +174,9 @@ func (c *Conn) discardKeys(now time.Time, space numberSpace) { // receiveTransportParameters applies transport parameters sent by the peer. func (c *Conn) receiveTransportParameters(p transportParameters) error { + if err := c.connIDState.validateTransportParameters(c.side, p); err != nil { + return err + } c.streams.outflow.setMaxData(p.initialMaxData) c.streams.localLimit[bidiStream].setMax(p.initialMaxStreamsBidi) c.streams.localLimit[uniStream].setMax(p.initialMaxStreamsUni) diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index eb2f3ecc15..045e646ac1 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -161,6 +161,39 @@ func (s *connIDState) issueLocalIDs(c *Conn) error { return nil } +// validateTransportParameters verifies the original_destination_connection_id and +// initial_source_connection_id transport parameters match the expected values. +func (s *connIDState) validateTransportParameters(side connSide, p transportParameters) error { + // TODO: Consider returning more detailed errors, for debugging. + switch side { + case clientSide: + // Verify original_destination_connection_id matches + // the transient remote connection ID we chose. + if len(s.remote) == 0 || s.remote[0].seq != -1 { + return localTransportError(errInternal) + } + if !bytes.Equal(s.remote[0].cid, p.originalDstConnID) { + return localTransportError(errTransportParameter) + } + // Remove the transient remote connection ID. + // We have no further need for it. + s.remote = append(s.remote[:0], s.remote[1:]...) + case serverSide: + if p.originalDstConnID != nil { + // Clients do not send original_destination_connection_id. + return localTransportError(errTransportParameter) + } + } + // Verify initial_source_connection_id matches the first remote connection ID. + if len(s.remote) == 0 || s.remote[0].seq != 0 { + return localTransportError(errInternal) + } + if !bytes.Equal(p.initialSrcConnID, s.remote[0].cid) { + return localTransportError(errTransportParameter) + } + return nil +} + // handlePacket updates the connection ID state during the handshake // (Initial and Handshake packets). func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) { @@ -170,10 +203,13 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) // We're a client connection processing the first Initial packet // from the server. Replace the transient remote connection ID // with the Source Connection ID from the packet. - s.remote[0] = connID{ + // Leave the transient ID the list for now, since we'll need it when + // processing the transport parameters. + s.remote[0].retired = true + s.remote = append(s.remote, connID{ seq: 0, cid: cloneBytes(srcConnID), - } + }) } case ptype == packetTypeInitial && c.side == serverSide: if len(s.remote) == 0 { @@ -185,7 +221,7 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) }) } case ptype == packetTypeHandshake && c.side == serverSide: - if len(s.local) > 0 && s.local[0].seq == -1 { + if len(s.local) > 0 && s.local[0].seq == -1 && !s.local[0].retired { // We're a server connection processing the first Handshake packet from // the client. Discard the transient, client-chosen connection ID used // for Initial packets; the client will never send it again. @@ -213,7 +249,7 @@ func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken active := 0 for i := range s.remote { rcid := &s.remote[i] - if !rcid.retired && rcid.seq < s.retireRemotePriorTo { + if !rcid.retired && rcid.seq >= 0 && rcid.seq < s.retireRemotePriorTo { s.retireRemote(rcid) } if !rcid.retired { diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index c5289583d3..44755ecf45 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -48,6 +48,9 @@ func TestConnIDClientHandshake(t *testing.T) { t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } wantRemote := []connID{{ + cid: testLocalConnID(-1), + seq: -1, + }, { cid: testPeerConnID(0), seq: 0, }} @@ -261,10 +264,12 @@ func TestConnIDPeerRetiresConnID(t *testing.T) { } func TestConnIDPeerWithZeroLengthConnIDSendsNewConnectionID(t *testing.T) { - // An endpoint that selects a zero-length connection ID during the handshake + // "An endpoint that selects a zero-length connection ID during the handshake // cannot issue a new connection ID." // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-8 - tc := newTestConn(t, clientSide) + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.initialSrcConnID = []byte{} + }) tc.peerConnID = []byte{} tc.ignoreFrame(frameTypeAck) tc.uncheckedHandshake() @@ -536,6 +541,7 @@ func TestConnIDPeerWithZeroLengthIDProvidesPreferredAddr(t *testing.T) { // Peer gives us more conn ids than our advertised limit, // including a conn id in the preferred address transport parameter. tc := newTestConn(t, serverSide, func(p *transportParameters) { + p.initialSrcConnID = []byte{} p.preferredAddrV4 = netip.MustParseAddrPort("0.0.0.0:0") p.preferredAddrV6 = netip.MustParseAddrPort("[::0]:0") p.preferredAddrConnID = testPeerConnID(1) @@ -552,3 +558,31 @@ func TestConnIDPeerWithZeroLengthIDProvidesPreferredAddr(t *testing.T) { code: errProtocolViolation, }) } + +func TestConnIDInitialSrcConnIDMismatch(t *testing.T) { + // "Endpoints MUST validate that received [initial_source_connection_id] + // parameters match received connection ID values." + // https://www.rfc-editor.org/rfc/rfc9000#section-7.3-3 + testSides(t, "", func(t *testing.T, side connSide) { + tc := newTestConn(t, side, func(p *transportParameters) { + p.initialSrcConnID = []byte("invalid") + }) + tc.ignoreFrame(frameTypeAck) + tc.ignoreFrame(frameTypeCrypto) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + if side == clientSide { + // Server transport parameters are carried in the Handshake packet. + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + } + tc.wantFrame("initial_source_connection_id transport parameter mismatch", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errTransportParameter, + }) + }) +} diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index fd9e6e42e2..6a359e89a1 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -201,6 +201,10 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { TLSConfig: newTestTLSConfig(side), } peerProvidedParams := defaultTransportParameters() + peerProvidedParams.initialSrcConnID = testPeerConnID(0) + if side == clientSide { + peerProvidedParams.originalDstConnID = testLocalConnID(-1) + } for _, o := range opts { switch o := o.(type) { case func(*Config): From 350aad2603e57013fafb1a9e2089a382fe67dc80 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 20 Sep 2023 21:48:07 -0700 Subject: [PATCH 071/168] quic: correctly extend peer's flow control window after MAX_DATA When sending the peer a connection-level flow control update in a MAX_DATA frame, we weren't recording the updated limit locally. When the peer wrote data past the old limit, we would incorrectly close the connection with a FLOW_CONTROL_ERROR. For golang/go#58547 Change-Id: I6879c0cccc3cfdc673b613a07b038138d9e285ff Reviewed-on: https://go-review.googlesource.com/c/net/+/530075 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_flow.go | 1 + internal/quic/conn_flow_test.go | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/internal/quic/conn_flow.go b/internal/quic/conn_flow.go index 281c7084ff..4f1ab6eafc 100644 --- a/internal/quic/conn_flow.go +++ b/internal/quic/conn_flow.go @@ -106,6 +106,7 @@ func (c *Conn) appendMaxDataFrame(w *packetWriter, pnum packetNumber, pto bool) if !w.appendMaxDataFrame(c.streams.inflow.newLimit) { return false } + c.streams.inflow.sentLimit += c.streams.inflow.newLimit c.streams.inflow.sent.setSent(pnum) } return true diff --git a/internal/quic/conn_flow_test.go b/internal/quic/conn_flow_test.go index 45c82f60d2..d5ee74ebda 100644 --- a/internal/quic/conn_flow_test.go +++ b/internal/quic/conn_flow_test.go @@ -35,6 +35,16 @@ func TestConnInflowReturnOnRead(t *testing.T) { packetType1RTT, debugFrameMaxData{ max: 128, }) + // Peer can write up to the new limit. + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + off: 64, + data: make([]byte, 64), + }) + tc.wantIdle("connection is idle") + if n, err := s.ReadContext(ctx, make([]byte, 64)); n != 64 || err != nil { + t.Fatalf("offset 64: s.Read() = %v, %v; want %v, nil", n, err, 64) + } } func TestConnInflowReturnOnRacingReads(t *testing.T) { From 5d5a036a503f8accd748f7453c0162115187be13 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 3 Oct 2023 13:49:48 -0700 Subject: [PATCH 072/168] quic: handle streams moving from the data queue to the meta queue In Conn.appendStreamFrames, a stream can be moved from the data queue (for streams with only flow-controlled frames to send) to the metadata queue (for streams with non-flow-controlled frames to send) if some other goroutine asynchronously modifies the stream state. Adjust the check at the end of this function to clear the needSend bool only if queueMeta and queueData are both empty, to avoid losing track of the need to send frames when this happens. For golang/go#58547 Change-Id: Ib9ad3b01f543cd7673f5233ceb58b2db9adfff5a Reviewed-on: https://go-review.googlesource.com/c/net/+/531656 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_streams.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index 7c6c8be2c0..a0793297e1 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -372,7 +372,9 @@ func (c *Conn) appendStreamFrames(w *packetWriter, pnum packetNumber, pto bool) state = s.state.set(0, streamQueueData) c.queueStreamForSendLocked(s, state) } - c.streams.needSend.Store(c.streams.queueData.head != nil) + if c.streams.queueMeta.head == nil && c.streams.queueData.head == nil { + c.streams.needSend.Store(false) + } return true } From 73d82efb96cacc0c378bc150b56675fc191894b9 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 4 Oct 2023 12:03:01 -0700 Subject: [PATCH 073/168] quic: handle DATA_BLOCKED frames We never send DATA_BLOCKED frames, and ignore ones sent by the peer, but we do need to parse them. For golang/go#58547 Change-Id: Ic9893245108fd1c32067d14811e2d44488ce1ab5 Reviewed-on: https://go-review.googlesource.com/c/net/+/532715 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_recv.go | 5 +++++ internal/quic/stream_test.go | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 19c43858c5..9b1ba1ae10 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -254,6 +254,11 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, return } n = c.handleMaxStreamsFrame(now, payload) + case frameTypeDataBlocked: + if !frameOK(c, ptype, __01) { + return + } + _, n = consumeDataBlockedFrame(payload) case frameTypeStreamsBlockedBidi, frameTypeStreamsBlockedUni: if !frameOK(c, ptype, __01) { return diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 750119614b..86eebc6989 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -1217,6 +1217,23 @@ func TestStreamPeerStopSendingForActiveStream(t *testing.T) { }) } +func TestStreamReceiveDataBlocked(t *testing.T) { + tc := newTestConn(t, serverSide, permissiveTransportParameters) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + // We don't do anything with these frames, + // but should accept them if the peer sends one. + tc.writeFrames(packetType1RTT, debugFrameStreamDataBlocked{ + id: newStreamID(clientSide, bidiStream, 0), + max: 100, + }) + tc.writeFrames(packetType1RTT, debugFrameDataBlocked{ + max: 100, + }) + tc.wantIdle("no response to STREAM_DATA_BLOCKED and DATA_BLOCKED") +} + type streamSide string const ( From 2b60a61f1e4cf3a5ecded0bd7e77ea168289e6de Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 3 Oct 2023 13:53:44 -0700 Subject: [PATCH 074/168] quic: fix several bugs in flow control accounting Connection-level flow control sets a bound on the total maximum stream offset of all data sent, not the total amount of bytes sent in STREAM frames. For example, if we send the bytes [0,10) for a stream, and then retransmit the same bytes due to packet loss, that consumes 10 bytes of connection-level flow, not 20. We were incorrectly tracking total bytes sent. Fix this. We were blocking retransmission of data in lost STREAM frames on availability of connection-level flow control. We now place a stream with retransmitted data on queueMeta (non-flow-controlled data), since we have already accounted for the flow control window consumption of the data. We were incorrectly marking a stream as being able to send an empty STREAM frame with a FIN bit, when the stream was actually blocked on stream-level flow control. Fix this. For golang/go#58547 Change-Id: Ib2ace94183750078a19d945256507060ea786735 Reviewed-on: https://go-review.googlesource.com/c/net/+/532716 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_flow_test.go | 34 +++++++++++++++++++++++++++++ internal/quic/stream.go | 23 +++++++++++++++----- internal/quic/stream_test.go | 38 +++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/internal/quic/conn_flow_test.go b/internal/quic/conn_flow_test.go index d5ee74ebda..03e0757a6d 100644 --- a/internal/quic/conn_flow_test.go +++ b/internal/quic/conn_flow_test.go @@ -394,3 +394,37 @@ func TestConnOutflowMetaAndData(t *testing.T) { data: data, }) } + +func TestConnOutflowResentData(t *testing.T) { + tc, s := newTestConnAndLocalStream(t, clientSide, bidiStream, + permissiveTransportParameters, + func(p *transportParameters) { + p.initialMaxData = 10 + }) + tc.ignoreFrame(frameTypeAck) + + data := makeTestData(15) + s.Write(data[:8]) + tc.wantFrame("data is under MAX_DATA limit, all sent", + packetType1RTT, debugFrameStream{ + id: s.id, + data: data[:8], + }) + + // Lose the last STREAM packet. + const pto = false + tc.triggerLossOrPTO(packetType1RTT, false) + tc.wantFrame("lost STREAM data is retransmitted", + packetType1RTT, debugFrameStream{ + id: s.id, + data: data[:8], + }) + + s.Write(data[8:]) + tc.wantFrame("new data is sent up to the MAX_DATA limit", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 8, + data: data[8:10], + }) +} diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 9310811c1b..89036b19b6 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -39,6 +39,7 @@ type Stream struct { outgate gate out pipe // buffered data to send outwin int64 // maximum MAX_STREAM_DATA received from the peer + outmaxsent int64 // maximum data offset we've sent to the peer outmaxbuf int64 // maximum amount of data we will buffer outunsent rangeset[int64] // ranges buffered but not yet sent outacked rangeset[int64] // ranges sent and acknowledged @@ -494,8 +495,12 @@ func (s *Stream) outUnlockNoQueue() streamState { case s.outblocked.shouldSend(): // STREAM_DATA_BLOCKED state = streamOutSendMeta case len(s.outunsent) > 0: // STREAM frame with data - state = streamOutSendData - case s.outclosed.shouldSend(): // STREAM frame with FIN bit, all data already sent + if s.outunsent.min() < s.outmaxsent { + state = streamOutSendMeta // resent data, will not consume flow control + } else { + state = streamOutSendData // new data, requires flow control + } + case s.outclosed.shouldSend() && s.out.end == s.outmaxsent: // empty STREAM frame with FIN bit state = streamOutSendMeta case s.outopened.shouldSend(): // STREAM frame with no data state = streamOutSendMeta @@ -725,7 +730,11 @@ func (s *Stream) appendOutFramesLocked(w *packetWriter, pnum packetNumber, pto b for { // STREAM off, size := dataToSend(min(s.out.start, s.outwin), min(s.out.end, s.outwin), s.outunsent, s.outacked, pto) - size = min(size, s.conn.streams.outflow.avail()) + if end := off + size; end > s.outmaxsent { + // This will require connection-level flow control to send. + end = min(end, s.outmaxsent+s.conn.streams.outflow.avail()) + size = end - off + } fin := s.outclosed.isSet() && off+size == s.out.end shouldSend := size > 0 || // have data to send s.outopened.shouldSendPTO(pto) || // should open the stream @@ -738,8 +747,12 @@ func (s *Stream) appendOutFramesLocked(w *packetWriter, pnum packetNumber, pto b return false } s.out.copy(off, b) - s.conn.streams.outflow.consume(int64(len(b))) - s.outunsent.sub(off, off+int64(len(b))) + end := off + int64(len(b)) + if end > s.outmaxsent { + s.conn.streams.outflow.consume(end - s.outmaxsent) + s.outmaxsent = end + } + s.outunsent.sub(off, end) s.frameOpensStream(pnum) if fin { s.outclosed.setSent(pnum) diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 86eebc6989..7c1377faee 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -1094,6 +1094,44 @@ func TestStreamCloseUnblocked(t *testing.T) { } } +func TestStreamCloseWriteWhenBlockedByStreamFlowControl(t *testing.T) { + ctx := canceledContext() + tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters, + func(p *transportParameters) { + //p.initialMaxData = 0 + p.initialMaxStreamDataUni = 0 + }) + tc.ignoreFrame(frameTypeStreamDataBlocked) + if _, err := s.WriteContext(ctx, []byte{0, 1}); err != nil { + t.Fatalf("s.Write = %v", err) + } + s.CloseWrite() + tc.wantIdle("stream write is blocked by flow control") + + tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: 1, + }) + tc.wantFrame("send data up to flow control limit", + packetType1RTT, debugFrameStream{ + id: s.id, + data: []byte{0}, + }) + tc.wantIdle("stream write is again blocked by flow control") + + tc.writeFrames(packetType1RTT, debugFrameMaxStreamData{ + id: s.id, + max: 2, + }) + tc.wantFrame("send remaining data and FIN", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 1, + data: []byte{1}, + fin: true, + }) +} + func TestStreamPeerResetsWithUnreadAndUnsentData(t *testing.T) { testStreamTypes(t, "", func(t *testing.T, styp streamType) { ctx := canceledContext() From 88194ad8ab44a02ea952c169883c3f57db6cf9f4 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Thu, 5 Oct 2023 15:37:07 +0000 Subject: [PATCH 075/168] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: I6a03cb993ffb84dff46b8cdcade2198da0491bd5 Reviewed-on: https://go-review.googlesource.com/c/net/+/533115 Reviewed-by: Heschi Kreinick Auto-Submit: Gopher Robot LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index b16f4e5e68..38ac82b446 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module golang.org/x/net go 1.17 require ( - golang.org/x/crypto v0.13.0 - golang.org/x/sys v0.12.0 - golang.org/x/term v0.12.0 + golang.org/x/crypto v0.14.0 + golang.org/x/sys v0.13.0 + golang.org/x/term v0.13.0 golang.org/x/text v0.13.0 ) diff --git a/go.sum b/go.sum index 0fd3311f48..dc4dc125c8 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -20,14 +20,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= From b225e7ca6dde1ef5a5ae5ce922861bda011cfabd Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 6 Oct 2023 09:51:19 -0700 Subject: [PATCH 076/168] http2: limit maximum handler goroutines to MaxConcurrentStreams When the peer opens a new stream while we have MaxConcurrentStreams handler goroutines running, defer starting a handler until one of the existing handlers exits. Fixes golang/go#63417 Fixes CVE-2023-39325 Change-Id: If0531e177b125700f3e24c5ebd24b1023098fa6d Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/2045854 TryBot-Result: Security TryBots Reviewed-by: Ian Cottrell Reviewed-by: Tatiana Bradley Run-TryBot: Damien Neil Reviewed-on: https://go-review.googlesource.com/c/net/+/534215 Reviewed-by: Michael Pratt Reviewed-by: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI Auto-Submit: Dmitri Shuralyov Reviewed-by: Damien Neil --- http2/server.go | 66 ++++++++++++++++++++++++- http2/server_test.go | 113 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 2 deletions(-) diff --git a/http2/server.go b/http2/server.go index de60fa88f1..02c88b6b3e 100644 --- a/http2/server.go +++ b/http2/server.go @@ -581,9 +581,11 @@ type serverConn struct { advMaxStreams uint32 // our SETTINGS_MAX_CONCURRENT_STREAMS advertised the client curClientStreams uint32 // number of open streams initiated by the client curPushedStreams uint32 // number of open streams initiated by server push + curHandlers uint32 // number of running handler goroutines maxClientStreamID uint32 // max ever seen from client (odd), or 0 if there have been no client requests maxPushPromiseID uint32 // ID of the last push promise (even), or 0 if there have been no pushes streams map[uint32]*stream + unstartedHandlers []unstartedHandler initialStreamSendWindowSize int32 maxFrameSize int32 peerMaxHeaderListSize uint32 // zero means unknown (default) @@ -981,6 +983,8 @@ func (sc *serverConn) serve() { return case gracefulShutdownMsg: sc.startGracefulShutdownInternal() + case handlerDoneMsg: + sc.handlerDone() default: panic("unknown timer") } @@ -1020,6 +1024,7 @@ var ( idleTimerMsg = new(serverMessage) shutdownTimerMsg = new(serverMessage) gracefulShutdownMsg = new(serverMessage) + handlerDoneMsg = new(serverMessage) ) func (sc *serverConn) onSettingsTimer() { sc.sendServeMsg(settingsTimerMsg) } @@ -2017,8 +2022,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error { st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout) } - go sc.runHandler(rw, req, handler) - return nil + return sc.scheduleHandler(id, rw, req, handler) } func (sc *serverConn) upgradeRequest(req *http.Request) { @@ -2038,6 +2042,10 @@ func (sc *serverConn) upgradeRequest(req *http.Request) { sc.conn.SetReadDeadline(time.Time{}) } + // This is the first request on the connection, + // so start the handler directly rather than going + // through scheduleHandler. + sc.curHandlers++ go sc.runHandler(rw, req, sc.handler.ServeHTTP) } @@ -2278,8 +2286,62 @@ func (sc *serverConn) newResponseWriter(st *stream, req *http.Request) *response return &responseWriter{rws: rws} } +type unstartedHandler struct { + streamID uint32 + rw *responseWriter + req *http.Request + handler func(http.ResponseWriter, *http.Request) +} + +// scheduleHandler starts a handler goroutine, +// or schedules one to start as soon as an existing handler finishes. +func (sc *serverConn) scheduleHandler(streamID uint32, rw *responseWriter, req *http.Request, handler func(http.ResponseWriter, *http.Request)) error { + sc.serveG.check() + maxHandlers := sc.advMaxStreams + if sc.curHandlers < maxHandlers { + sc.curHandlers++ + go sc.runHandler(rw, req, handler) + return nil + } + if len(sc.unstartedHandlers) > int(4*sc.advMaxStreams) { + return sc.countError("too_many_early_resets", ConnectionError(ErrCodeEnhanceYourCalm)) + } + sc.unstartedHandlers = append(sc.unstartedHandlers, unstartedHandler{ + streamID: streamID, + rw: rw, + req: req, + handler: handler, + }) + return nil +} + +func (sc *serverConn) handlerDone() { + sc.serveG.check() + sc.curHandlers-- + i := 0 + maxHandlers := sc.advMaxStreams + for ; i < len(sc.unstartedHandlers); i++ { + u := sc.unstartedHandlers[i] + if sc.streams[u.streamID] == nil { + // This stream was reset before its goroutine had a chance to start. + continue + } + if sc.curHandlers >= maxHandlers { + break + } + sc.curHandlers++ + go sc.runHandler(u.rw, u.req, u.handler) + sc.unstartedHandlers[i] = unstartedHandler{} // don't retain references + } + sc.unstartedHandlers = sc.unstartedHandlers[i:] + if len(sc.unstartedHandlers) == 0 { + sc.unstartedHandlers = nil + } +} + // Run on its own goroutine. func (sc *serverConn) runHandler(rw *responseWriter, req *http.Request, handler func(http.ResponseWriter, *http.Request)) { + defer sc.sendServeMsg(handlerDoneMsg) didPanic := true defer func() { rw.rws.stream.cancelCtx() diff --git a/http2/server_test.go b/http2/server_test.go index b99c5af549..22657cbfe4 100644 --- a/http2/server_test.go +++ b/http2/server_test.go @@ -4664,3 +4664,116 @@ func TestServerWriteDoesNotRetainBufferAfterServerClose(t *testing.T) { st.ts.Config.Close() <-donec } + +func TestServerMaxHandlerGoroutines(t *testing.T) { + const maxHandlers = 10 + handlerc := make(chan chan bool) + donec := make(chan struct{}) + defer close(donec) + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { + stopc := make(chan bool, 1) + select { + case handlerc <- stopc: + case <-donec: + } + select { + case shouldPanic := <-stopc: + if shouldPanic { + panic(http.ErrAbortHandler) + } + case <-donec: + } + }, func(s *Server) { + s.MaxConcurrentStreams = maxHandlers + }) + defer st.Close() + + st.writePreface() + st.writeInitialSettings() + st.writeSettingsAck() + + // Make maxHandlers concurrent requests. + // Reset them all, but only after the handler goroutines have started. + var stops []chan bool + streamID := uint32(1) + for i := 0; i < maxHandlers; i++ { + st.writeHeaders(HeadersFrameParam{ + StreamID: streamID, + BlockFragment: st.encodeHeader(), + EndStream: true, + EndHeaders: true, + }) + stops = append(stops, <-handlerc) + st.fr.WriteRSTStream(streamID, ErrCodeCancel) + streamID += 2 + } + + // Start another request, and immediately reset it. + st.writeHeaders(HeadersFrameParam{ + StreamID: streamID, + BlockFragment: st.encodeHeader(), + EndStream: true, + EndHeaders: true, + }) + st.fr.WriteRSTStream(streamID, ErrCodeCancel) + streamID += 2 + + // Start another two requests. Don't reset these. + for i := 0; i < 2; i++ { + st.writeHeaders(HeadersFrameParam{ + StreamID: streamID, + BlockFragment: st.encodeHeader(), + EndStream: true, + EndHeaders: true, + }) + streamID += 2 + } + + // The initial maxHandlers handlers are still executing, + // so the last two requests don't start any new handlers. + select { + case <-handlerc: + t.Errorf("handler unexpectedly started while maxHandlers are already running") + case <-time.After(1 * time.Millisecond): + } + + // Tell two handlers to exit. + // The pending requests which weren't reset start handlers. + stops[0] <- false // normal exit + stops[1] <- true // panic + stops = stops[2:] + stops = append(stops, <-handlerc) + stops = append(stops, <-handlerc) + + // Make a bunch more requests. + // Eventually, the server tells us to go away. + for i := 0; i < 5*maxHandlers; i++ { + st.writeHeaders(HeadersFrameParam{ + StreamID: streamID, + BlockFragment: st.encodeHeader(), + EndStream: true, + EndHeaders: true, + }) + st.fr.WriteRSTStream(streamID, ErrCodeCancel) + streamID += 2 + } +Frames: + for { + f, err := st.readFrame() + if err != nil { + st.t.Fatal(err) + } + switch f := f.(type) { + case *GoAwayFrame: + if f.ErrCode != ErrCodeEnhanceYourCalm { + t.Errorf("err code = %v; want %v", f.ErrCode, ErrCodeEnhanceYourCalm) + } + break Frames + default: + } + } + + for _, s := range stops { + close(s) + } +} From 92728b3b3c00280fd64b9ac7641d5e81f39afc4c Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Tue, 10 Oct 2023 15:15:25 +0000 Subject: [PATCH 077/168] dns/dnsmessage: document that Skip does not fully validate the header The Skip methods do not fully validate the name in header, the compression pointers are not followed Change-Id: If34a041d799a22117afac9bd23e76606f5d0c8f7 GitHub-Last-Rev: f8f2586fb2528308f0b130c64cc7c13ca7820607 GitHub-Pull-Request: golang/net#196 Reviewed-on: https://go-review.googlesource.com/c/net/+/534175 LUCI-TryBot-Result: Go LUCI Reviewed-by: Ian Lance Taylor Reviewed-by: Damien Neil Auto-Submit: Ian Lance Taylor --- dns/dnsmessage/message.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index b6b4f9c197..42987ab7c5 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -751,6 +751,9 @@ func (p *Parser) AllAnswers() ([]Resource, error) { } // SkipAnswer skips a single Answer Resource. +// +// It does not perform a complete validation of the resource header, which means +// it may return a nil error when the [AnswerHeader] would actually return an error. func (p *Parser) SkipAnswer() error { return p.skipResource(sectionAnswers) } @@ -801,6 +804,9 @@ func (p *Parser) AllAuthorities() ([]Resource, error) { } // SkipAuthority skips a single Authority Resource. +// +// It does not perform a complete validation of the resource header, which means +// it may return a nil error when the [AuthorityHeader] would actually return an error. func (p *Parser) SkipAuthority() error { return p.skipResource(sectionAuthorities) } @@ -851,6 +857,9 @@ func (p *Parser) AllAdditionals() ([]Resource, error) { } // SkipAdditional skips a single Additional Resource. +// +// It does not perform a complete validation of the resource header, which means +// it may return a nil error when the [AdditionalHeader] would actually return an error. func (p *Parser) SkipAdditional() error { return p.skipResource(sectionAdditionals) } From d23d9bc549229fd1a9d375dc91141fcf1385d257 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Tue, 10 Oct 2023 13:49:18 -0400 Subject: [PATCH 078/168] all: update go directive to 1.18 Done with: go get go@1.18 go mod tidy go fix ./... Using go1.21.3. With a manual change to keep golang.org/x/net/context testing itself, not context in the standard library. For golang/go#60268. Change-Id: I00682bf7cf1e3ba4370e2a3e7f63dc245b294a36 Reviewed-on: https://go-review.googlesource.com/c/net/+/534241 LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov Reviewed-by: Damien Neil Auto-Submit: Dmitri Shuralyov --- context/context_test.go | 1 - context/ctxhttp/ctxhttp_test.go | 1 - context/go17.go | 1 - context/go19.go | 1 - context/pre_go17.go | 1 - context/pre_go19.go | 1 - go.mod | 2 +- go.sum | 34 ------------------------ html/atom/gen.go | 1 - http/httpproxy/go19_test.go | 1 - http2/go111.go | 1 - http2/go115.go | 1 - http2/go118.go | 1 - http2/h2i/h2i.go | 1 - http2/hpack/gen.go | 1 - http2/not_go111.go | 1 - http2/not_go115.go | 1 - http2/not_go118.go | 1 - http2/transport_go117_test.go | 1 - icmp/helper_posix.go | 1 - icmp/listen_posix.go | 1 - icmp/listen_stub.go | 1 - idna/go118.go | 1 - idna/idna10.0.0.go | 1 - idna/idna9.0.0.go | 1 - idna/pre_go118.go | 1 - idna/tables10.0.0.go | 1 - idna/tables11.0.0.go | 1 - idna/tables12.0.0.go | 1 - idna/tables13.0.0.go | 1 - idna/tables15.0.0.go | 1 - idna/tables9.0.0.go | 1 - idna/trie12.0.0.go | 1 - idna/trie13.0.0.go | 1 - internal/iana/gen.go | 1 - internal/socket/cmsghdr.go | 1 - internal/socket/cmsghdr_bsd.go | 1 - internal/socket/cmsghdr_linux_32bit.go | 2 -- internal/socket/cmsghdr_linux_64bit.go | 2 -- internal/socket/cmsghdr_solaris_64bit.go | 1 - internal/socket/cmsghdr_stub.go | 1 - internal/socket/cmsghdr_unix.go | 1 - internal/socket/complete_dontwait.go | 1 - internal/socket/complete_nodontwait.go | 1 - internal/socket/defs_aix.go | 1 - internal/socket/defs_darwin.go | 1 - internal/socket/defs_dragonfly.go | 1 - internal/socket/defs_freebsd.go | 1 - internal/socket/defs_linux.go | 1 - internal/socket/defs_netbsd.go | 1 - internal/socket/defs_openbsd.go | 1 - internal/socket/defs_solaris.go | 1 - internal/socket/empty.s | 1 - internal/socket/error_unix.go | 1 - internal/socket/iovec_32bit.go | 2 -- internal/socket/iovec_64bit.go | 2 -- internal/socket/iovec_solaris_64bit.go | 1 - internal/socket/iovec_stub.go | 1 - internal/socket/mmsghdr_stub.go | 1 - internal/socket/mmsghdr_unix.go | 1 - internal/socket/msghdr_bsd.go | 1 - internal/socket/msghdr_bsdvar.go | 1 - internal/socket/msghdr_linux_32bit.go | 2 -- internal/socket/msghdr_linux_64bit.go | 2 -- internal/socket/msghdr_solaris_64bit.go | 1 - internal/socket/msghdr_stub.go | 1 - internal/socket/msghdr_zos_s390x.go | 1 - internal/socket/norace.go | 1 - internal/socket/race.go | 1 - internal/socket/rawconn_mmsg.go | 1 - internal/socket/rawconn_msg.go | 1 - internal/socket/rawconn_nommsg.go | 1 - internal/socket/rawconn_nomsg.go | 1 - internal/socket/socket_dontwait_test.go | 1 - internal/socket/socket_test.go | 1 - internal/socket/sys_bsd.go | 1 - internal/socket/sys_const_unix.go | 1 - internal/socket/sys_linux.go | 1 - internal/socket/sys_linux_loong64.go | 1 - internal/socket/sys_linux_riscv64.go | 1 - internal/socket/sys_posix.go | 1 - internal/socket/sys_stub.go | 1 - internal/socket/sys_unix.go | 1 - internal/socket/zsys_aix_ppc64.go | 1 - internal/socket/zsys_linux_loong64.go | 1 - internal/socket/zsys_linux_riscv64.go | 1 - ipv4/control_bsd.go | 1 - ipv4/control_pktinfo.go | 1 - ipv4/control_stub.go | 1 - ipv4/control_unix.go | 1 - ipv4/defs_aix.go | 1 - ipv4/defs_darwin.go | 1 - ipv4/defs_dragonfly.go | 1 - ipv4/defs_freebsd.go | 1 - ipv4/defs_linux.go | 1 - ipv4/defs_netbsd.go | 1 - ipv4/defs_openbsd.go | 1 - ipv4/defs_solaris.go | 1 - ipv4/errors_other_test.go | 1 - ipv4/errors_unix_test.go | 1 - ipv4/gen.go | 1 - ipv4/helper_posix_test.go | 1 - ipv4/helper_stub_test.go | 1 - ipv4/icmp_stub.go | 1 - ipv4/payload_cmsg.go | 1 - ipv4/payload_nocmsg.go | 1 - ipv4/sockopt_posix.go | 1 - ipv4/sockopt_stub.go | 1 - ipv4/sys_aix.go | 1 - ipv4/sys_asmreq.go | 1 - ipv4/sys_asmreq_stub.go | 1 - ipv4/sys_asmreqn.go | 1 - ipv4/sys_asmreqn_stub.go | 1 - ipv4/sys_bpf.go | 1 - ipv4/sys_bpf_stub.go | 1 - ipv4/sys_bsd.go | 1 - ipv4/sys_ssmreq.go | 1 - ipv4/sys_ssmreq_stub.go | 1 - ipv4/sys_stub.go | 1 - ipv4/zsys_aix_ppc64.go | 1 - ipv4/zsys_linux_loong64.go | 1 - ipv4/zsys_linux_riscv64.go | 1 - ipv6/control_rfc2292_unix.go | 1 - ipv6/control_rfc3542_unix.go | 1 - ipv6/control_stub.go | 1 - ipv6/control_unix.go | 1 - ipv6/defs_aix.go | 1 - ipv6/defs_darwin.go | 1 - ipv6/defs_dragonfly.go | 1 - ipv6/defs_freebsd.go | 1 - ipv6/defs_linux.go | 1 - ipv6/defs_netbsd.go | 1 - ipv6/defs_openbsd.go | 1 - ipv6/defs_solaris.go | 1 - ipv6/errors_other_test.go | 1 - ipv6/errors_unix_test.go | 1 - ipv6/gen.go | 1 - ipv6/helper_posix_test.go | 1 - ipv6/helper_stub_test.go | 1 - ipv6/helper_unix_test.go | 1 - ipv6/icmp_bsd.go | 1 - ipv6/icmp_stub.go | 1 - ipv6/payload_cmsg.go | 1 - ipv6/payload_nocmsg.go | 1 - ipv6/sockopt_posix.go | 1 - ipv6/sockopt_stub.go | 1 - ipv6/sys_aix.go | 1 - ipv6/sys_asmreq.go | 1 - ipv6/sys_asmreq_stub.go | 1 - ipv6/sys_bpf.go | 1 - ipv6/sys_bpf_stub.go | 1 - ipv6/sys_bsd.go | 1 - ipv6/sys_ssmreq.go | 1 - ipv6/sys_ssmreq_stub.go | 1 - ipv6/sys_stub.go | 1 - ipv6/zsys_aix_ppc64.go | 1 - ipv6/zsys_linux_loong64.go | 1 - ipv6/zsys_linux_riscv64.go | 1 - lif/address.go | 1 - lif/address_test.go | 1 - lif/binary.go | 1 - lif/defs_solaris.go | 1 - lif/lif.go | 1 - lif/link.go | 1 - lif/link_test.go | 1 - lif/sys.go | 1 - lif/syscall.go | 1 - nettest/conntest_test.go | 1 - nettest/nettest_stub.go | 1 - nettest/nettest_unix.go | 1 - publicsuffix/gen.go | 1 - route/address.go | 1 - route/address_test.go | 1 - route/binary.go | 1 - route/defs_darwin.go | 1 - route/defs_dragonfly.go | 1 - route/defs_freebsd.go | 1 - route/defs_netbsd.go | 1 - route/defs_openbsd.go | 1 - route/empty.s | 1 - route/interface.go | 1 - route/interface_announce.go | 1 - route/interface_classic.go | 1 - route/interface_multicast.go | 1 - route/message.go | 1 - route/message_test.go | 1 - route/route.go | 1 - route/route_classic.go | 1 - route/route_test.go | 1 - route/sys.go | 1 - route/syscall.go | 1 - webdav/litmus_test_server.go | 1 - 192 files changed, 1 insertion(+), 231 deletions(-) diff --git a/context/context_test.go b/context/context_test.go index e7bf0acc24..2cb54edb89 100644 --- a/context/context_test.go +++ b/context/context_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.7 -// +build !go1.7 package context diff --git a/context/ctxhttp/ctxhttp_test.go b/context/ctxhttp/ctxhttp_test.go index 21f7599cc1..d585f117f0 100644 --- a/context/ctxhttp/ctxhttp_test.go +++ b/context/ctxhttp/ctxhttp_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !plan9 -// +build !plan9 package ctxhttp diff --git a/context/go17.go b/context/go17.go index 2cb9c408f2..0c1b867937 100644 --- a/context/go17.go +++ b/context/go17.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.7 -// +build go1.7 package context diff --git a/context/go19.go b/context/go19.go index 64d31ecc3e..e31e35a904 100644 --- a/context/go19.go +++ b/context/go19.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.9 -// +build go1.9 package context diff --git a/context/pre_go17.go b/context/pre_go17.go index 7b6b685114..065ff3dfa5 100644 --- a/context/pre_go17.go +++ b/context/pre_go17.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.7 -// +build !go1.7 package context diff --git a/context/pre_go19.go b/context/pre_go19.go index 1f9715341f..ec5a638033 100644 --- a/context/pre_go19.go +++ b/context/pre_go19.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.9 -// +build !go1.9 package context diff --git a/go.mod b/go.mod index 38ac82b446..f83c0890ad 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module golang.org/x/net -go 1.17 +go 1.18 require ( golang.org/x/crypto v0.14.0 diff --git a/go.sum b/go.sum index dc4dc125c8..ddbbdd3ef0 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,8 @@ -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/html/atom/gen.go b/html/atom/gen.go index 5b0aaf7379..5d85c604d1 100644 --- a/html/atom/gen.go +++ b/html/atom/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore //go:generate go run gen.go //go:generate go run gen.go -test diff --git a/http/httpproxy/go19_test.go b/http/httpproxy/go19_test.go index 5f6e3d7ff1..5fca5ac454 100644 --- a/http/httpproxy/go19_test.go +++ b/http/httpproxy/go19_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.9 -// +build go1.9 package httpproxy_test diff --git a/http2/go111.go b/http2/go111.go index 5bf62b032e..4ced74a0b4 100644 --- a/http2/go111.go +++ b/http2/go111.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.11 -// +build go1.11 package http2 diff --git a/http2/go115.go b/http2/go115.go index 908af1ab93..13a2d9215d 100644 --- a/http2/go115.go +++ b/http2/go115.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.15 -// +build go1.15 package http2 diff --git a/http2/go118.go b/http2/go118.go index aca4b2b31a..a445ae1d58 100644 --- a/http2/go118.go +++ b/http2/go118.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.18 -// +build go1.18 package http2 diff --git a/http2/h2i/h2i.go b/http2/h2i/h2i.go index 901f6ca79a..ee7020dd9b 100644 --- a/http2/h2i/h2i.go +++ b/http2/h2i/h2i.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows /* The h2i command is an interactive HTTP/2 console. diff --git a/http2/hpack/gen.go b/http2/hpack/gen.go index de14ab0ec0..21a4198b33 100644 --- a/http2/hpack/gen.go +++ b/http2/hpack/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package main diff --git a/http2/not_go111.go b/http2/not_go111.go index cc0baa8197..f4d63f458b 100644 --- a/http2/not_go111.go +++ b/http2/not_go111.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.11 -// +build !go1.11 package http2 diff --git a/http2/not_go115.go b/http2/not_go115.go index e6c04cf7ac..635753408d 100644 --- a/http2/not_go115.go +++ b/http2/not_go115.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.15 -// +build !go1.15 package http2 diff --git a/http2/not_go118.go b/http2/not_go118.go index eab532c96b..b1b11c072d 100644 --- a/http2/not_go118.go +++ b/http2/not_go118.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !go1.18 -// +build !go1.18 package http2 diff --git a/http2/transport_go117_test.go b/http2/transport_go117_test.go index f5d4e0c1a6..0f257ad241 100644 --- a/http2/transport_go117_test.go +++ b/http2/transport_go117_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.17 -// +build go1.17 package http2 diff --git a/icmp/helper_posix.go b/icmp/helper_posix.go index 6c3ebfaed4..f625483f06 100644 --- a/icmp/helper_posix.go +++ b/icmp/helper_posix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows package icmp diff --git a/icmp/listen_posix.go b/icmp/listen_posix.go index 6aea804788..b7cb15b7dc 100644 --- a/icmp/listen_posix.go +++ b/icmp/listen_posix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows package icmp diff --git a/icmp/listen_stub.go b/icmp/listen_stub.go index 1acfb74b60..7b76be1cb3 100644 --- a/icmp/listen_stub.go +++ b/icmp/listen_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows package icmp diff --git a/idna/go118.go b/idna/go118.go index c5c4338dbe..712f1ad839 100644 --- a/idna/go118.go +++ b/idna/go118.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build go1.18 -// +build go1.18 package idna diff --git a/idna/idna10.0.0.go b/idna/idna10.0.0.go index 64ccf85feb..7b37178847 100644 --- a/idna/idna10.0.0.go +++ b/idna/idna10.0.0.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build go1.10 -// +build go1.10 // Package idna implements IDNA2008 using the compatibility processing // defined by UTS (Unicode Technical Standard) #46, which defines a standard to diff --git a/idna/idna9.0.0.go b/idna/idna9.0.0.go index ee1698cefb..cc6a892a4a 100644 --- a/idna/idna9.0.0.go +++ b/idna/idna9.0.0.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build !go1.10 -// +build !go1.10 // Package idna implements IDNA2008 using the compatibility processing // defined by UTS (Unicode Technical Standard) #46, which defines a standard to diff --git a/idna/pre_go118.go b/idna/pre_go118.go index 3aaccab1c5..40e74bb3d2 100644 --- a/idna/pre_go118.go +++ b/idna/pre_go118.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build !go1.18 -// +build !go1.18 package idna diff --git a/idna/tables10.0.0.go b/idna/tables10.0.0.go index d1d62ef459..c6c2bf10a6 100644 --- a/idna/tables10.0.0.go +++ b/idna/tables10.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build go1.10 && !go1.13 -// +build go1.10,!go1.13 package idna diff --git a/idna/tables11.0.0.go b/idna/tables11.0.0.go index 167efba712..76789393cc 100644 --- a/idna/tables11.0.0.go +++ b/idna/tables11.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build go1.13 && !go1.14 -// +build go1.13,!go1.14 package idna diff --git a/idna/tables12.0.0.go b/idna/tables12.0.0.go index ab40f7bcc3..0600cd2ae5 100644 --- a/idna/tables12.0.0.go +++ b/idna/tables12.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build go1.14 && !go1.16 -// +build go1.14,!go1.16 package idna diff --git a/idna/tables13.0.0.go b/idna/tables13.0.0.go index 66701eadfb..2fb768ef6d 100644 --- a/idna/tables13.0.0.go +++ b/idna/tables13.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build go1.16 && !go1.21 -// +build go1.16,!go1.21 package idna diff --git a/idna/tables15.0.0.go b/idna/tables15.0.0.go index 40033778f0..5ff05fe1af 100644 --- a/idna/tables15.0.0.go +++ b/idna/tables15.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build go1.21 -// +build go1.21 package idna diff --git a/idna/tables9.0.0.go b/idna/tables9.0.0.go index 4074b5332e..0f25e84ca2 100644 --- a/idna/tables9.0.0.go +++ b/idna/tables9.0.0.go @@ -1,7 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. //go:build !go1.10 -// +build !go1.10 package idna diff --git a/idna/trie12.0.0.go b/idna/trie12.0.0.go index bb63f904b3..8a75b96673 100644 --- a/idna/trie12.0.0.go +++ b/idna/trie12.0.0.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build !go1.16 -// +build !go1.16 package idna diff --git a/idna/trie13.0.0.go b/idna/trie13.0.0.go index 7d68a8dc13..fa45bb9074 100644 --- a/idna/trie13.0.0.go +++ b/idna/trie13.0.0.go @@ -5,7 +5,6 @@ // license that can be found in the LICENSE file. //go:build go1.16 -// +build go1.16 package idna diff --git a/internal/iana/gen.go b/internal/iana/gen.go index 34f0f7eeea..0fe65d8998 100644 --- a/internal/iana/gen.go +++ b/internal/iana/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore //go:generate go run gen.go diff --git a/internal/socket/cmsghdr.go b/internal/socket/cmsghdr.go index 4bdaaaf1ad..33a5bf59c3 100644 --- a/internal/socket/cmsghdr.go +++ b/internal/socket/cmsghdr.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package socket diff --git a/internal/socket/cmsghdr_bsd.go b/internal/socket/cmsghdr_bsd.go index 0d30e0a0f2..68f438c845 100644 --- a/internal/socket/cmsghdr_bsd.go +++ b/internal/socket/cmsghdr_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd -// +build aix darwin dragonfly freebsd netbsd openbsd package socket diff --git a/internal/socket/cmsghdr_linux_32bit.go b/internal/socket/cmsghdr_linux_32bit.go index 4936e8a6f3..058ea8de89 100644 --- a/internal/socket/cmsghdr_linux_32bit.go +++ b/internal/socket/cmsghdr_linux_32bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm || mips || mipsle || 386 || ppc) && linux -// +build arm mips mipsle 386 ppc -// +build linux package socket diff --git a/internal/socket/cmsghdr_linux_64bit.go b/internal/socket/cmsghdr_linux_64bit.go index f6877f98fd..3ca0d3a0ab 100644 --- a/internal/socket/cmsghdr_linux_64bit.go +++ b/internal/socket/cmsghdr_linux_64bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm64 || amd64 || loong64 || ppc64 || ppc64le || mips64 || mips64le || riscv64 || s390x) && linux -// +build arm64 amd64 loong64 ppc64 ppc64le mips64 mips64le riscv64 s390x -// +build linux package socket diff --git a/internal/socket/cmsghdr_solaris_64bit.go b/internal/socket/cmsghdr_solaris_64bit.go index d3dbe1b8e0..6d0e426cdd 100644 --- a/internal/socket/cmsghdr_solaris_64bit.go +++ b/internal/socket/cmsghdr_solaris_64bit.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build amd64 && solaris -// +build amd64,solaris package socket diff --git a/internal/socket/cmsghdr_stub.go b/internal/socket/cmsghdr_stub.go index 1d9f2ed625..7ca9cb7e78 100644 --- a/internal/socket/cmsghdr_stub.go +++ b/internal/socket/cmsghdr_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!zos package socket diff --git a/internal/socket/cmsghdr_unix.go b/internal/socket/cmsghdr_unix.go index 19d46789de..0211f225bf 100644 --- a/internal/socket/cmsghdr_unix.go +++ b/internal/socket/cmsghdr_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package socket diff --git a/internal/socket/complete_dontwait.go b/internal/socket/complete_dontwait.go index 5b1d50ae72..2038f29043 100644 --- a/internal/socket/complete_dontwait.go +++ b/internal/socket/complete_dontwait.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build darwin dragonfly freebsd linux netbsd openbsd solaris package socket diff --git a/internal/socket/complete_nodontwait.go b/internal/socket/complete_nodontwait.go index be63409583..70e6f448b0 100644 --- a/internal/socket/complete_nodontwait.go +++ b/internal/socket/complete_nodontwait.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || windows || zos -// +build aix windows zos package socket diff --git a/internal/socket/defs_aix.go b/internal/socket/defs_aix.go index 0bc1703ca6..2c847bbeb3 100644 --- a/internal/socket/defs_aix.go +++ b/internal/socket/defs_aix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_darwin.go b/internal/socket/defs_darwin.go index 0f07b57253..d94fff7558 100644 --- a/internal/socket/defs_darwin.go +++ b/internal/socket/defs_darwin.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_dragonfly.go b/internal/socket/defs_dragonfly.go index 0f07b57253..d94fff7558 100644 --- a/internal/socket/defs_dragonfly.go +++ b/internal/socket/defs_dragonfly.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_freebsd.go b/internal/socket/defs_freebsd.go index 0f07b57253..d94fff7558 100644 --- a/internal/socket/defs_freebsd.go +++ b/internal/socket/defs_freebsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_linux.go b/internal/socket/defs_linux.go index bbaafdf30a..d0d52bdfb7 100644 --- a/internal/socket/defs_linux.go +++ b/internal/socket/defs_linux.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_netbsd.go b/internal/socket/defs_netbsd.go index 5b57b0c426..8db525bf49 100644 --- a/internal/socket/defs_netbsd.go +++ b/internal/socket/defs_netbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_openbsd.go b/internal/socket/defs_openbsd.go index 0f07b57253..d94fff7558 100644 --- a/internal/socket/defs_openbsd.go +++ b/internal/socket/defs_openbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/defs_solaris.go b/internal/socket/defs_solaris.go index 0f07b57253..d94fff7558 100644 --- a/internal/socket/defs_solaris.go +++ b/internal/socket/defs_solaris.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package socket diff --git a/internal/socket/empty.s b/internal/socket/empty.s index 90ab4ca3d8..49d79791e0 100644 --- a/internal/socket/empty.s +++ b/internal/socket/empty.s @@ -3,6 +3,5 @@ // license that can be found in the LICENSE file. //go:build darwin && go1.12 -// +build darwin,go1.12 // This exists solely so we can linkname in symbols from syscall. diff --git a/internal/socket/error_unix.go b/internal/socket/error_unix.go index 78f4129047..7a5cc5c43e 100644 --- a/internal/socket/error_unix.go +++ b/internal/socket/error_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package socket diff --git a/internal/socket/iovec_32bit.go b/internal/socket/iovec_32bit.go index 2b8fbb3f3d..340e53fbda 100644 --- a/internal/socket/iovec_32bit.go +++ b/internal/socket/iovec_32bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm || mips || mipsle || 386 || ppc) && (darwin || dragonfly || freebsd || linux || netbsd || openbsd) -// +build arm mips mipsle 386 ppc -// +build darwin dragonfly freebsd linux netbsd openbsd package socket diff --git a/internal/socket/iovec_64bit.go b/internal/socket/iovec_64bit.go index 2e94e96f8b..26470c191a 100644 --- a/internal/socket/iovec_64bit.go +++ b/internal/socket/iovec_64bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm64 || amd64 || loong64 || ppc64 || ppc64le || mips64 || mips64le || riscv64 || s390x) && (aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || zos) -// +build arm64 amd64 loong64 ppc64 ppc64le mips64 mips64le riscv64 s390x -// +build aix darwin dragonfly freebsd linux netbsd openbsd zos package socket diff --git a/internal/socket/iovec_solaris_64bit.go b/internal/socket/iovec_solaris_64bit.go index f7da2bc4d4..8859ce1035 100644 --- a/internal/socket/iovec_solaris_64bit.go +++ b/internal/socket/iovec_solaris_64bit.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build amd64 && solaris -// +build amd64,solaris package socket diff --git a/internal/socket/iovec_stub.go b/internal/socket/iovec_stub.go index 14caf52483..da886b0326 100644 --- a/internal/socket/iovec_stub.go +++ b/internal/socket/iovec_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!zos package socket diff --git a/internal/socket/mmsghdr_stub.go b/internal/socket/mmsghdr_stub.go index 113e773cd5..4825b21e3e 100644 --- a/internal/socket/mmsghdr_stub.go +++ b/internal/socket/mmsghdr_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !linux && !netbsd -// +build !aix,!linux,!netbsd package socket diff --git a/internal/socket/mmsghdr_unix.go b/internal/socket/mmsghdr_unix.go index 41883c530c..311fd2c789 100644 --- a/internal/socket/mmsghdr_unix.go +++ b/internal/socket/mmsghdr_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || linux || netbsd -// +build aix linux netbsd package socket diff --git a/internal/socket/msghdr_bsd.go b/internal/socket/msghdr_bsd.go index 25f6847f99..ebff4f6e05 100644 --- a/internal/socket/msghdr_bsd.go +++ b/internal/socket/msghdr_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd -// +build aix darwin dragonfly freebsd netbsd openbsd package socket diff --git a/internal/socket/msghdr_bsdvar.go b/internal/socket/msghdr_bsdvar.go index 5b8e00f1cd..62e6fe8616 100644 --- a/internal/socket/msghdr_bsdvar.go +++ b/internal/socket/msghdr_bsdvar.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd -// +build aix darwin dragonfly freebsd netbsd package socket diff --git a/internal/socket/msghdr_linux_32bit.go b/internal/socket/msghdr_linux_32bit.go index b4658fbaeb..3dd07250a6 100644 --- a/internal/socket/msghdr_linux_32bit.go +++ b/internal/socket/msghdr_linux_32bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm || mips || mipsle || 386 || ppc) && linux -// +build arm mips mipsle 386 ppc -// +build linux package socket diff --git a/internal/socket/msghdr_linux_64bit.go b/internal/socket/msghdr_linux_64bit.go index 42411affad..5af9ddd6ab 100644 --- a/internal/socket/msghdr_linux_64bit.go +++ b/internal/socket/msghdr_linux_64bit.go @@ -3,8 +3,6 @@ // license that can be found in the LICENSE file. //go:build (arm64 || amd64 || loong64 || ppc64 || ppc64le || mips64 || mips64le || riscv64 || s390x) && linux -// +build arm64 amd64 loong64 ppc64 ppc64le mips64 mips64le riscv64 s390x -// +build linux package socket diff --git a/internal/socket/msghdr_solaris_64bit.go b/internal/socket/msghdr_solaris_64bit.go index 3098f5d783..e212b50f8d 100644 --- a/internal/socket/msghdr_solaris_64bit.go +++ b/internal/socket/msghdr_solaris_64bit.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build amd64 && solaris -// +build amd64,solaris package socket diff --git a/internal/socket/msghdr_stub.go b/internal/socket/msghdr_stub.go index eb79151f6a..e876776459 100644 --- a/internal/socket/msghdr_stub.go +++ b/internal/socket/msghdr_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!zos package socket diff --git a/internal/socket/msghdr_zos_s390x.go b/internal/socket/msghdr_zos_s390x.go index 324e9ee7d1..529db68ee3 100644 --- a/internal/socket/msghdr_zos_s390x.go +++ b/internal/socket/msghdr_zos_s390x.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build s390x && zos -// +build s390x,zos package socket diff --git a/internal/socket/norace.go b/internal/socket/norace.go index de0ad420fc..8af30ecfbb 100644 --- a/internal/socket/norace.go +++ b/internal/socket/norace.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !race -// +build !race package socket diff --git a/internal/socket/race.go b/internal/socket/race.go index f0a28a625d..9afa958083 100644 --- a/internal/socket/race.go +++ b/internal/socket/race.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build race -// +build race package socket diff --git a/internal/socket/rawconn_mmsg.go b/internal/socket/rawconn_mmsg.go index 8f79b38f74..0431390789 100644 --- a/internal/socket/rawconn_mmsg.go +++ b/internal/socket/rawconn_mmsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build linux -// +build linux package socket diff --git a/internal/socket/rawconn_msg.go b/internal/socket/rawconn_msg.go index f7d0b0d2b8..7c0d7410bc 100644 --- a/internal/socket/rawconn_msg.go +++ b/internal/socket/rawconn_msg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package socket diff --git a/internal/socket/rawconn_nommsg.go b/internal/socket/rawconn_nommsg.go index 02f3285566..e363fb5a89 100644 --- a/internal/socket/rawconn_nommsg.go +++ b/internal/socket/rawconn_nommsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !linux -// +build !linux package socket diff --git a/internal/socket/rawconn_nomsg.go b/internal/socket/rawconn_nomsg.go index dd785877b6..ff7a8baf0b 100644 --- a/internal/socket/rawconn_nomsg.go +++ b/internal/socket/rawconn_nomsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package socket diff --git a/internal/socket/socket_dontwait_test.go b/internal/socket/socket_dontwait_test.go index 8eab9900b1..1eb3580f63 100644 --- a/internal/socket/socket_dontwait_test.go +++ b/internal/socket/socket_dontwait_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build darwin dragonfly freebsd linux netbsd openbsd solaris package socket_test diff --git a/internal/socket/socket_test.go b/internal/socket/socket_test.go index 84907d8bc1..faba106063 100644 --- a/internal/socket/socket_test.go +++ b/internal/socket/socket_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package socket_test diff --git a/internal/socket/sys_bsd.go b/internal/socket/sys_bsd.go index b258879d44..e7664d48be 100644 --- a/internal/socket/sys_bsd.go +++ b/internal/socket/sys_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || openbsd || solaris -// +build aix darwin dragonfly freebsd openbsd solaris package socket diff --git a/internal/socket/sys_const_unix.go b/internal/socket/sys_const_unix.go index 5d99f2373f..d7627f87eb 100644 --- a/internal/socket/sys_const_unix.go +++ b/internal/socket/sys_const_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package socket diff --git a/internal/socket/sys_linux.go b/internal/socket/sys_linux.go index 76f5b8ae5d..08d4910778 100644 --- a/internal/socket/sys_linux.go +++ b/internal/socket/sys_linux.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build linux && !s390x && !386 -// +build linux,!s390x,!386 package socket diff --git a/internal/socket/sys_linux_loong64.go b/internal/socket/sys_linux_loong64.go index af964e6171..1d182470d0 100644 --- a/internal/socket/sys_linux_loong64.go +++ b/internal/socket/sys_linux_loong64.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build loong64 -// +build loong64 package socket diff --git a/internal/socket/sys_linux_riscv64.go b/internal/socket/sys_linux_riscv64.go index 5b128fbb2a..0e407d1257 100644 --- a/internal/socket/sys_linux_riscv64.go +++ b/internal/socket/sys_linux_riscv64.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build riscv64 -// +build riscv64 package socket diff --git a/internal/socket/sys_posix.go b/internal/socket/sys_posix.go index 42b8f2340e..58d8654824 100644 --- a/internal/socket/sys_posix.go +++ b/internal/socket/sys_posix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package socket diff --git a/internal/socket/sys_stub.go b/internal/socket/sys_stub.go index 7cfb349c0c..2e5b473c66 100644 --- a/internal/socket/sys_stub.go +++ b/internal/socket/sys_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package socket diff --git a/internal/socket/sys_unix.go b/internal/socket/sys_unix.go index de823932b9..93058db5b9 100644 --- a/internal/socket/sys_unix.go +++ b/internal/socket/sys_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package socket diff --git a/internal/socket/zsys_aix_ppc64.go b/internal/socket/zsys_aix_ppc64.go index 00691bd524..45bab004c1 100644 --- a/internal/socket/zsys_aix_ppc64.go +++ b/internal/socket/zsys_aix_ppc64.go @@ -3,7 +3,6 @@ // Added for go1.11 compatibility //go:build aix -// +build aix package socket diff --git a/internal/socket/zsys_linux_loong64.go b/internal/socket/zsys_linux_loong64.go index 6a94fec2c5..b6fc15a1a2 100644 --- a/internal/socket/zsys_linux_loong64.go +++ b/internal/socket/zsys_linux_loong64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build loong64 -// +build loong64 package socket diff --git a/internal/socket/zsys_linux_riscv64.go b/internal/socket/zsys_linux_riscv64.go index c066272ddd..e67fc3cbaa 100644 --- a/internal/socket/zsys_linux_riscv64.go +++ b/internal/socket/zsys_linux_riscv64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build riscv64 -// +build riscv64 package socket diff --git a/ipv4/control_bsd.go b/ipv4/control_bsd.go index b7385dfd95..c88da8cbe7 100644 --- a/ipv4/control_bsd.go +++ b/ipv4/control_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd -// +build aix darwin dragonfly freebsd netbsd openbsd package ipv4 diff --git a/ipv4/control_pktinfo.go b/ipv4/control_pktinfo.go index 0e748dbdc4..14ae2dae49 100644 --- a/ipv4/control_pktinfo.go +++ b/ipv4/control_pktinfo.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || linux || solaris -// +build darwin linux solaris package ipv4 diff --git a/ipv4/control_stub.go b/ipv4/control_stub.go index f27322c3ed..3ba6611609 100644 --- a/ipv4/control_stub.go +++ b/ipv4/control_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv4 diff --git a/ipv4/control_unix.go b/ipv4/control_unix.go index 2413e02f8f..2e765548f3 100644 --- a/ipv4/control_unix.go +++ b/ipv4/control_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package ipv4 diff --git a/ipv4/defs_aix.go b/ipv4/defs_aix.go index b70b618240..5e590a7df2 100644 --- a/ipv4/defs_aix.go +++ b/ipv4/defs_aix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_darwin.go b/ipv4/defs_darwin.go index 0ceadfce2e..2494ff86a9 100644 --- a/ipv4/defs_darwin.go +++ b/ipv4/defs_darwin.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_dragonfly.go b/ipv4/defs_dragonfly.go index a84630c5cd..43e9f67bb7 100644 --- a/ipv4/defs_dragonfly.go +++ b/ipv4/defs_dragonfly.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_freebsd.go b/ipv4/defs_freebsd.go index b068087a47..05899b3b4f 100644 --- a/ipv4/defs_freebsd.go +++ b/ipv4/defs_freebsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_linux.go b/ipv4/defs_linux.go index 7c8554d4b3..fc869b0194 100644 --- a/ipv4/defs_linux.go +++ b/ipv4/defs_linux.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_netbsd.go b/ipv4/defs_netbsd.go index a84630c5cd..43e9f67bb7 100644 --- a/ipv4/defs_netbsd.go +++ b/ipv4/defs_netbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_openbsd.go b/ipv4/defs_openbsd.go index a84630c5cd..43e9f67bb7 100644 --- a/ipv4/defs_openbsd.go +++ b/ipv4/defs_openbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/defs_solaris.go b/ipv4/defs_solaris.go index 0ceadfce2e..2494ff86a9 100644 --- a/ipv4/defs_solaris.go +++ b/ipv4/defs_solaris.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ diff --git a/ipv4/errors_other_test.go b/ipv4/errors_other_test.go index 6154353918..93a7f9d74c 100644 --- a/ipv4/errors_other_test.go +++ b/ipv4/errors_other_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !(aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris) -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris package ipv4_test diff --git a/ipv4/errors_unix_test.go b/ipv4/errors_unix_test.go index 566e070a50..7cff0097c9 100644 --- a/ipv4/errors_unix_test.go +++ b/ipv4/errors_unix_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package ipv4_test diff --git a/ipv4/gen.go b/ipv4/gen.go index e7b053a17b..121c7643e9 100644 --- a/ipv4/gen.go +++ b/ipv4/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore //go:generate go run gen.go diff --git a/ipv4/helper_posix_test.go b/ipv4/helper_posix_test.go index 4f6ecc0fd9..ab8ffd90dc 100644 --- a/ipv4/helper_posix_test.go +++ b/ipv4/helper_posix_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package ipv4_test diff --git a/ipv4/helper_stub_test.go b/ipv4/helper_stub_test.go index e47ddf7f36..791e6d4c0a 100644 --- a/ipv4/helper_stub_test.go +++ b/ipv4/helper_stub_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv4_test diff --git a/ipv4/icmp_stub.go b/ipv4/icmp_stub.go index cd4ee6e1c9..c2c4ce7ff5 100644 --- a/ipv4/icmp_stub.go +++ b/ipv4/icmp_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !linux -// +build !linux package ipv4 diff --git a/ipv4/payload_cmsg.go b/ipv4/payload_cmsg.go index 1bb370e25f..91c685e8fc 100644 --- a/ipv4/payload_cmsg.go +++ b/ipv4/payload_cmsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package ipv4 diff --git a/ipv4/payload_nocmsg.go b/ipv4/payload_nocmsg.go index 53f0794eb7..2afd4b50ef 100644 --- a/ipv4/payload_nocmsg.go +++ b/ipv4/payload_nocmsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!zos package ipv4 diff --git a/ipv4/sockopt_posix.go b/ipv4/sockopt_posix.go index eb07c1c02a..82e2c37838 100644 --- a/ipv4/sockopt_posix.go +++ b/ipv4/sockopt_posix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package ipv4 diff --git a/ipv4/sockopt_stub.go b/ipv4/sockopt_stub.go index cf036893b7..840108bf76 100644 --- a/ipv4/sockopt_stub.go +++ b/ipv4/sockopt_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv4 diff --git a/ipv4/sys_aix.go b/ipv4/sys_aix.go index 02730cdfd2..9244a68a38 100644 --- a/ipv4/sys_aix.go +++ b/ipv4/sys_aix.go @@ -4,7 +4,6 @@ // Added for go1.11 compatibility //go:build aix -// +build aix package ipv4 diff --git a/ipv4/sys_asmreq.go b/ipv4/sys_asmreq.go index 22322b387e..645f254c6d 100644 --- a/ipv4/sys_asmreq.go +++ b/ipv4/sys_asmreq.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || windows -// +build aix darwin dragonfly freebsd netbsd openbsd solaris windows package ipv4 diff --git a/ipv4/sys_asmreq_stub.go b/ipv4/sys_asmreq_stub.go index fde640142d..48cfb6db2f 100644 --- a/ipv4/sys_asmreq_stub.go +++ b/ipv4/sys_asmreq_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !windows -// +build !aix,!darwin,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!windows package ipv4 diff --git a/ipv4/sys_asmreqn.go b/ipv4/sys_asmreqn.go index 54eb9901b5..0b27b632f1 100644 --- a/ipv4/sys_asmreqn.go +++ b/ipv4/sys_asmreqn.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || freebsd || linux -// +build darwin freebsd linux package ipv4 diff --git a/ipv4/sys_asmreqn_stub.go b/ipv4/sys_asmreqn_stub.go index dcb15f25a5..303a5e2e68 100644 --- a/ipv4/sys_asmreqn_stub.go +++ b/ipv4/sys_asmreqn_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !darwin && !freebsd && !linux -// +build !darwin,!freebsd,!linux package ipv4 diff --git a/ipv4/sys_bpf.go b/ipv4/sys_bpf.go index fb11e324e2..1b4780df41 100644 --- a/ipv4/sys_bpf.go +++ b/ipv4/sys_bpf.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build linux -// +build linux package ipv4 diff --git a/ipv4/sys_bpf_stub.go b/ipv4/sys_bpf_stub.go index fc53a0d33a..b1f779b493 100644 --- a/ipv4/sys_bpf_stub.go +++ b/ipv4/sys_bpf_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !linux -// +build !linux package ipv4 diff --git a/ipv4/sys_bsd.go b/ipv4/sys_bsd.go index e191b2f14f..b7b032d260 100644 --- a/ipv4/sys_bsd.go +++ b/ipv4/sys_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build netbsd || openbsd -// +build netbsd openbsd package ipv4 diff --git a/ipv4/sys_ssmreq.go b/ipv4/sys_ssmreq.go index 6a4e7abf9b..a295e15ea0 100644 --- a/ipv4/sys_ssmreq.go +++ b/ipv4/sys_ssmreq.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || freebsd || linux || solaris -// +build darwin freebsd linux solaris package ipv4 diff --git a/ipv4/sys_ssmreq_stub.go b/ipv4/sys_ssmreq_stub.go index 157159fd50..74bd454e25 100644 --- a/ipv4/sys_ssmreq_stub.go +++ b/ipv4/sys_ssmreq_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !darwin && !freebsd && !linux && !solaris -// +build !darwin,!freebsd,!linux,!solaris package ipv4 diff --git a/ipv4/sys_stub.go b/ipv4/sys_stub.go index d550851658..20af4074c2 100644 --- a/ipv4/sys_stub.go +++ b/ipv4/sys_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv4 diff --git a/ipv4/zsys_aix_ppc64.go b/ipv4/zsys_aix_ppc64.go index b7f2d6e5c1..dd454025c7 100644 --- a/ipv4/zsys_aix_ppc64.go +++ b/ipv4/zsys_aix_ppc64.go @@ -3,7 +3,6 @@ // Added for go1.11 compatibility //go:build aix -// +build aix package ipv4 diff --git a/ipv4/zsys_linux_loong64.go b/ipv4/zsys_linux_loong64.go index e15c22c748..54f9e13948 100644 --- a/ipv4/zsys_linux_loong64.go +++ b/ipv4/zsys_linux_loong64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build loong64 -// +build loong64 package ipv4 diff --git a/ipv4/zsys_linux_riscv64.go b/ipv4/zsys_linux_riscv64.go index e2edebdb81..78374a5250 100644 --- a/ipv4/zsys_linux_riscv64.go +++ b/ipv4/zsys_linux_riscv64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build riscv64 -// +build riscv64 package ipv4 diff --git a/ipv6/control_rfc2292_unix.go b/ipv6/control_rfc2292_unix.go index 2733ddbe27..a8f04e7b3b 100644 --- a/ipv6/control_rfc2292_unix.go +++ b/ipv6/control_rfc2292_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin -// +build darwin package ipv6 diff --git a/ipv6/control_rfc3542_unix.go b/ipv6/control_rfc3542_unix.go index 9c90844aac..51fbbb1f17 100644 --- a/ipv6/control_rfc3542_unix.go +++ b/ipv6/control_rfc3542_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package ipv6 diff --git a/ipv6/control_stub.go b/ipv6/control_stub.go index b7e8643fc9..eb28ce7534 100644 --- a/ipv6/control_stub.go +++ b/ipv6/control_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv6 diff --git a/ipv6/control_unix.go b/ipv6/control_unix.go index 63e475db83..9c73b8647e 100644 --- a/ipv6/control_unix.go +++ b/ipv6/control_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package ipv6 diff --git a/ipv6/defs_aix.go b/ipv6/defs_aix.go index 97db07e8d6..de171ce2c8 100644 --- a/ipv6/defs_aix.go +++ b/ipv6/defs_aix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_darwin.go b/ipv6/defs_darwin.go index 1d31e22c18..3b9e6ba649 100644 --- a/ipv6/defs_darwin.go +++ b/ipv6/defs_darwin.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_dragonfly.go b/ipv6/defs_dragonfly.go index ddaed6597c..b40d34b136 100644 --- a/ipv6/defs_dragonfly.go +++ b/ipv6/defs_dragonfly.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_freebsd.go b/ipv6/defs_freebsd.go index 6f6bc6dbc3..fe9a0f70fb 100644 --- a/ipv6/defs_freebsd.go +++ b/ipv6/defs_freebsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_linux.go b/ipv6/defs_linux.go index 0adcbd92dc..b947c225ae 100644 --- a/ipv6/defs_linux.go +++ b/ipv6/defs_linux.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_netbsd.go b/ipv6/defs_netbsd.go index ddaed6597c..b40d34b136 100644 --- a/ipv6/defs_netbsd.go +++ b/ipv6/defs_netbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_openbsd.go b/ipv6/defs_openbsd.go index ddaed6597c..b40d34b136 100644 --- a/ipv6/defs_openbsd.go +++ b/ipv6/defs_openbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/defs_solaris.go b/ipv6/defs_solaris.go index 03193da9be..7981a04524 100644 --- a/ipv6/defs_solaris.go +++ b/ipv6/defs_solaris.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/ipv6/errors_other_test.go b/ipv6/errors_other_test.go index 5a87d73618..5f6c0cb270 100644 --- a/ipv6/errors_other_test.go +++ b/ipv6/errors_other_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !(aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris) -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris package ipv6_test diff --git a/ipv6/errors_unix_test.go b/ipv6/errors_unix_test.go index 978ae61f84..9e8efd3137 100644 --- a/ipv6/errors_unix_test.go +++ b/ipv6/errors_unix_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris package ipv6_test diff --git a/ipv6/gen.go b/ipv6/gen.go index bd53468eb0..2973dff5ce 100644 --- a/ipv6/gen.go +++ b/ipv6/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore //go:generate go run gen.go diff --git a/ipv6/helper_posix_test.go b/ipv6/helper_posix_test.go index 8ca6a3c3cb..f412a78cbc 100644 --- a/ipv6/helper_posix_test.go +++ b/ipv6/helper_posix_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package ipv6_test diff --git a/ipv6/helper_stub_test.go b/ipv6/helper_stub_test.go index 15e99fa94a..9412a4cf5d 100644 --- a/ipv6/helper_stub_test.go +++ b/ipv6/helper_stub_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv6_test diff --git a/ipv6/helper_unix_test.go b/ipv6/helper_unix_test.go index 5ccff9d9b2..c2459e320e 100644 --- a/ipv6/helper_unix_test.go +++ b/ipv6/helper_unix_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package ipv6_test diff --git a/ipv6/icmp_bsd.go b/ipv6/icmp_bsd.go index 120bf87758..2814534a0b 100644 --- a/ipv6/icmp_bsd.go +++ b/ipv6/icmp_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd -// +build aix darwin dragonfly freebsd netbsd openbsd package ipv6 diff --git a/ipv6/icmp_stub.go b/ipv6/icmp_stub.go index d60136a901..c92c9b51e1 100644 --- a/ipv6/icmp_stub.go +++ b/ipv6/icmp_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv6 diff --git a/ipv6/payload_cmsg.go b/ipv6/payload_cmsg.go index b0692e4304..be04e4d6ae 100644 --- a/ipv6/payload_cmsg.go +++ b/ipv6/payload_cmsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package ipv6 diff --git a/ipv6/payload_nocmsg.go b/ipv6/payload_nocmsg.go index cd0ff50838..29b9ccf691 100644 --- a/ipv6/payload_nocmsg.go +++ b/ipv6/payload_nocmsg.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!zos package ipv6 diff --git a/ipv6/sockopt_posix.go b/ipv6/sockopt_posix.go index 37c6287130..34dfed588e 100644 --- a/ipv6/sockopt_posix.go +++ b/ipv6/sockopt_posix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows zos package ipv6 diff --git a/ipv6/sockopt_stub.go b/ipv6/sockopt_stub.go index 32fd8664ce..a09c3aaf26 100644 --- a/ipv6/sockopt_stub.go +++ b/ipv6/sockopt_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv6 diff --git a/ipv6/sys_aix.go b/ipv6/sys_aix.go index a47182afb9..93c8efc468 100644 --- a/ipv6/sys_aix.go +++ b/ipv6/sys_aix.go @@ -4,7 +4,6 @@ // Added for go1.11 compatibility //go:build aix -// +build aix package ipv6 diff --git a/ipv6/sys_asmreq.go b/ipv6/sys_asmreq.go index 6ff9950d13..5c9cb44471 100644 --- a/ipv6/sys_asmreq.go +++ b/ipv6/sys_asmreq.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris windows package ipv6 diff --git a/ipv6/sys_asmreq_stub.go b/ipv6/sys_asmreq_stub.go index 485290cb82..dc70494680 100644 --- a/ipv6/sys_asmreq_stub.go +++ b/ipv6/sys_asmreq_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows package ipv6 diff --git a/ipv6/sys_bpf.go b/ipv6/sys_bpf.go index b5661fb8f0..e39f75f49f 100644 --- a/ipv6/sys_bpf.go +++ b/ipv6/sys_bpf.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build linux -// +build linux package ipv6 diff --git a/ipv6/sys_bpf_stub.go b/ipv6/sys_bpf_stub.go index cb00661872..8532a8f5de 100644 --- a/ipv6/sys_bpf_stub.go +++ b/ipv6/sys_bpf_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !linux -// +build !linux package ipv6 diff --git a/ipv6/sys_bsd.go b/ipv6/sys_bsd.go index bde41a6cef..9f3bc2afde 100644 --- a/ipv6/sys_bsd.go +++ b/ipv6/sys_bsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build dragonfly || netbsd || openbsd -// +build dragonfly netbsd openbsd package ipv6 diff --git a/ipv6/sys_ssmreq.go b/ipv6/sys_ssmreq.go index 023488a49c..b40f5c685b 100644 --- a/ipv6/sys_ssmreq.go +++ b/ipv6/sys_ssmreq.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || freebsd || linux || solaris || zos -// +build aix darwin freebsd linux solaris zos package ipv6 diff --git a/ipv6/sys_ssmreq_stub.go b/ipv6/sys_ssmreq_stub.go index acdf2e5cf7..6526aad581 100644 --- a/ipv6/sys_ssmreq_stub.go +++ b/ipv6/sys_ssmreq_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !freebsd && !linux && !solaris && !zos -// +build !aix,!darwin,!freebsd,!linux,!solaris,!zos package ipv6 diff --git a/ipv6/sys_stub.go b/ipv6/sys_stub.go index 5807bba392..76602c34e6 100644 --- a/ipv6/sys_stub.go +++ b/ipv6/sys_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package ipv6 diff --git a/ipv6/zsys_aix_ppc64.go b/ipv6/zsys_aix_ppc64.go index f604b0f3b4..668716df4d 100644 --- a/ipv6/zsys_aix_ppc64.go +++ b/ipv6/zsys_aix_ppc64.go @@ -3,7 +3,6 @@ // Added for go1.11 compatibility //go:build aix -// +build aix package ipv6 diff --git a/ipv6/zsys_linux_loong64.go b/ipv6/zsys_linux_loong64.go index 598fbfa06f..6a53284dbe 100644 --- a/ipv6/zsys_linux_loong64.go +++ b/ipv6/zsys_linux_loong64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build loong64 -// +build loong64 package ipv6 diff --git a/ipv6/zsys_linux_riscv64.go b/ipv6/zsys_linux_riscv64.go index d4f78e405a..13b3472057 100644 --- a/ipv6/zsys_linux_riscv64.go +++ b/ipv6/zsys_linux_riscv64.go @@ -2,7 +2,6 @@ // cgo -godefs defs_linux.go //go:build riscv64 -// +build riscv64 package ipv6 diff --git a/lif/address.go b/lif/address.go index 8eaddb508d..0ed62a2c4c 100644 --- a/lif/address.go +++ b/lif/address.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/address_test.go b/lif/address_test.go index fdaa7f3aa4..0e99b8d34e 100644 --- a/lif/address_test.go +++ b/lif/address_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/binary.go b/lif/binary.go index f31ca3ad07..8a6c456061 100644 --- a/lif/binary.go +++ b/lif/binary.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/defs_solaris.go b/lif/defs_solaris.go index dbed7c86ed..6bc8fa8e6b 100644 --- a/lif/defs_solaris.go +++ b/lif/defs_solaris.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore // +godefs map struct_in_addr [4]byte /* in_addr */ // +godefs map struct_in6_addr [16]byte /* in6_addr */ diff --git a/lif/lif.go b/lif/lif.go index f1fce48b34..e9f2a9e0ed 100644 --- a/lif/lif.go +++ b/lif/lif.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris // Package lif provides basic functions for the manipulation of // logical network interfaces and interface addresses on Solaris. diff --git a/lif/link.go b/lif/link.go index 00b78545b5..d0c615a0b3 100644 --- a/lif/link.go +++ b/lif/link.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/link_test.go b/lif/link_test.go index 40b3f3ff2b..fe56697f82 100644 --- a/lif/link_test.go +++ b/lif/link_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/sys.go b/lif/sys.go index d0b532d9dc..caba2fe90d 100644 --- a/lif/sys.go +++ b/lif/sys.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/lif/syscall.go b/lif/syscall.go index 8d03b4aa92..329a65fe63 100644 --- a/lif/syscall.go +++ b/lif/syscall.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build solaris -// +build solaris package lif diff --git a/nettest/conntest_test.go b/nettest/conntest_test.go index 7c5aeb9b32..c57e640048 100644 --- a/nettest/conntest_test.go +++ b/nettest/conntest_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build go1.8 -// +build go1.8 package nettest diff --git a/nettest/nettest_stub.go b/nettest/nettest_stub.go index 6e3a9312b9..1725b6aa18 100644 --- a/nettest/nettest_stub.go +++ b/nettest/nettest_stub.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows,!zos package nettest diff --git a/nettest/nettest_unix.go b/nettest/nettest_unix.go index b1cb8b2f3b..9ba269d020 100644 --- a/nettest/nettest_unix.go +++ b/nettest/nettest_unix.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package nettest diff --git a/publicsuffix/gen.go b/publicsuffix/gen.go index 2ad0abdc1a..21c191415f 100644 --- a/publicsuffix/gen.go +++ b/publicsuffix/gen.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package main diff --git a/route/address.go b/route/address.go index 5a3cc06549..5443d67223 100644 --- a/route/address.go +++ b/route/address.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/address_test.go b/route/address_test.go index bd7db4a1f7..31087576ed 100644 --- a/route/address_test.go +++ b/route/address_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/binary.go b/route/binary.go index a5e28f1e9c..db3f7e0c2a 100644 --- a/route/binary.go +++ b/route/binary.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/defs_darwin.go b/route/defs_darwin.go index 8da5845712..ec56ca02e1 100644 --- a/route/defs_darwin.go +++ b/route/defs_darwin.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package route diff --git a/route/defs_dragonfly.go b/route/defs_dragonfly.go index acf3d1c55f..9bf202dda4 100644 --- a/route/defs_dragonfly.go +++ b/route/defs_dragonfly.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package route diff --git a/route/defs_freebsd.go b/route/defs_freebsd.go index 3f115121bc..abb2dc0957 100644 --- a/route/defs_freebsd.go +++ b/route/defs_freebsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package route diff --git a/route/defs_netbsd.go b/route/defs_netbsd.go index c4304df84f..8e89934c5a 100644 --- a/route/defs_netbsd.go +++ b/route/defs_netbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package route diff --git a/route/defs_openbsd.go b/route/defs_openbsd.go index 9af0e1af57..8f3218bc63 100644 --- a/route/defs_openbsd.go +++ b/route/defs_openbsd.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore package route diff --git a/route/empty.s b/route/empty.s index 90ab4ca3d8..49d79791e0 100644 --- a/route/empty.s +++ b/route/empty.s @@ -3,6 +3,5 @@ // license that can be found in the LICENSE file. //go:build darwin && go1.12 -// +build darwin,go1.12 // This exists solely so we can linkname in symbols from syscall. diff --git a/route/interface.go b/route/interface.go index 9e9407830c..0aa70555ca 100644 --- a/route/interface.go +++ b/route/interface.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/interface_announce.go b/route/interface_announce.go index 8282bfe9e2..70614c1b1a 100644 --- a/route/interface_announce.go +++ b/route/interface_announce.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build dragonfly || freebsd || netbsd -// +build dragonfly freebsd netbsd package route diff --git a/route/interface_classic.go b/route/interface_classic.go index 903a196346..be1bf2652e 100644 --- a/route/interface_classic.go +++ b/route/interface_classic.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || netbsd -// +build darwin dragonfly netbsd package route diff --git a/route/interface_multicast.go b/route/interface_multicast.go index dd0b214baa..2ee37b9c74 100644 --- a/route/interface_multicast.go +++ b/route/interface_multicast.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd -// +build darwin dragonfly freebsd package route diff --git a/route/message.go b/route/message.go index 456a8363fe..dc8bfc5b3a 100644 --- a/route/message.go +++ b/route/message.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/message_test.go b/route/message_test.go index 61927d62c0..9381f1b2df 100644 --- a/route/message_test.go +++ b/route/message_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/route.go b/route/route.go index 3ab5bcdc01..ca2ce2b887 100644 --- a/route/route.go +++ b/route/route.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd // Package route provides basic functions for the manipulation of // packet routing facilities on BSD variants. diff --git a/route/route_classic.go b/route/route_classic.go index d6ee42f1b1..e273fe39ab 100644 --- a/route/route_classic.go +++ b/route/route_classic.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd -// +build darwin dragonfly freebsd netbsd package route diff --git a/route/route_test.go b/route/route_test.go index 55c8f23727..ba57702178 100644 --- a/route/route_test.go +++ b/route/route_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/sys.go b/route/sys.go index 7c75574f18..fcebee58ec 100644 --- a/route/sys.go +++ b/route/sys.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/route/syscall.go b/route/syscall.go index 68d37c9621..0ed53750a2 100644 --- a/route/syscall.go +++ b/route/syscall.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd -// +build darwin dragonfly freebsd netbsd openbsd package route diff --git a/webdav/litmus_test_server.go b/webdav/litmus_test_server.go index 6334d7e233..4d49072c4d 100644 --- a/webdav/litmus_test_server.go +++ b/webdav/litmus_test_server.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build ignore -// +build ignore /* This program is a server for the WebDAV 'litmus' compliance test at From 37479d671cd577ab022df2c2b7164ddc8ad735f7 Mon Sep 17 00:00:00 2001 From: Mauri de Souza Meneguzzo Date: Mon, 16 Oct 2023 00:15:08 +0000 Subject: [PATCH 079/168] http2: fix underflow in http2 server push After CL 534215 was merged to fix a CVE it introduced an underflow when we try to decrement sc.curHandlers in handlerDone. The func startPush calls runHandler without incrementing curHandlers. Seems to only affect users of http.Pusher. For golang/go#63511 Change-Id: Ic537c27c9945c2c2d4306ddb04e9527b65cee320 GitHub-Last-Rev: 249fe55f7501ca607f70e8050c6546995cd808e8 GitHub-Pull-Request: golang/net#197 Reviewed-on: https://go-review.googlesource.com/c/net/+/535595 Reviewed-by: Damien Neil Reviewed-by: Dmitri Shuralyov TryBot-Result: Gopher Robot Run-TryBot: Mauri de Souza Meneguzzo --- http2/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/http2/server.go b/http2/server.go index 02c88b6b3e..7f3bed9261 100644 --- a/http2/server.go +++ b/http2/server.go @@ -3187,6 +3187,7 @@ func (sc *serverConn) startPush(msg *startPushRequest) { panic(fmt.Sprintf("newWriterAndRequestNoBody(%+v): %v", msg.url, err)) } + sc.curHandlers++ go sc.runHandler(rw, req, sc.handler.ServeHTTP) return promisedID, nil } From 9ef1b7226adc64e857d2d883d930d189c15d6e54 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 6 Oct 2023 15:49:18 -0700 Subject: [PATCH 080/168] quic: move more testConn behavior into testListener Refactor the testConn/testListener relationship some. Move synthetic time tracking into the listener. Let the testListener create testConns. These changes will allow us to test Retry behavior, where the listener responds to a new connection request with a Retry packet, and only initiates the connection upon receiving a valid Retry token. For golang/go#58547 Change-Id: Ib6fc86a21819059f2a603fa6c9be14ab87a7a44c Reviewed-on: https://go-review.googlesource.com/c/net/+/535236 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn.go | 25 ++- internal/quic/conn_test.go | 301 ++++++++++++++++++--------------- internal/quic/listener.go | 12 +- internal/quic/listener_test.go | 180 +++++++++++++++++++- internal/quic/tls_test.go | 6 +- internal/quic/version_test.go | 2 +- 6 files changed, 374 insertions(+), 152 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 9db00fe092..ea03bbf981 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -61,14 +61,29 @@ type Conn struct { // connTestHooks override conn behavior in tests. type connTestHooks interface { + // init is called after a conn is created. + init() + + // nextMessage is called to request the next event from msgc. + // Used to give tests control of the connection event loop. nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any) + + // handleTLSEvent is called with each TLS event. handleTLSEvent(tls.QUICEvent) + + // newConnID is called to generate a new connection ID. + // Permits tests to generate consistent connection IDs rather than random ones. newConnID(seq int64) ([]byte, error) + + // waitUntil blocks until the until func returns true or the context is done. + // Used to synchronize asynchronous blocking operations in tests. waitUntil(ctx context.Context, until func() bool) error + + // timeNow returns the current time. timeNow() time.Time } -func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener, hooks connTestHooks) (*Conn, error) { +func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) { c := &Conn{ side: side, listener: l, @@ -76,7 +91,6 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. peerAddr: peerAddr, msgc: make(chan any, 1), donec: make(chan struct{}), - testHooks: hooks, maxIdleTimeout: defaultMaxIdleTimeout, idleTimeout: now.Add(defaultMaxIdleTimeout), peerAckDelayExponent: -1, @@ -86,6 +100,10 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. // non-blocking operation. c.msgc = make(chan any, 1) + if l.testHooks != nil { + l.testHooks.newConn(c) + } + var originalDstConnID []byte if c.side == clientSide { if err := c.connIDState.initClient(c); err != nil { @@ -126,6 +144,9 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. return nil, err } + if c.testHooks != nil { + c.testHooks.init() + } go c.loop(now) return c, nil } diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 6a359e89a1..ea47b0b295 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -33,12 +33,12 @@ func TestConnTestConn(t *testing.T) { tc.conn.runOnLoop(func(now time.Time, c *Conn) { ranAt = now }) - if !ranAt.Equal(tc.now) { - t.Errorf("func ran on loop at %v, want %v", ranAt, tc.now) + if !ranAt.Equal(tc.listener.now) { + t.Errorf("func ran on loop at %v, want %v", ranAt, tc.listener.now) } tc.wait() - nextTime := tc.now.Add(defaultMaxIdleTimeout / 2) + nextTime := tc.listener.now.Add(defaultMaxIdleTimeout / 2) tc.advanceTo(nextTime) tc.conn.runOnLoop(func(now time.Time, c *Conn) { ranAt = now @@ -57,6 +57,7 @@ func TestConnTestConn(t *testing.T) { type testDatagram struct { packets []*testPacket paddedSize int + addr netip.AddrPort } func (d testDatagram) String() string { @@ -74,14 +75,16 @@ func (d testDatagram) String() string { } type testPacket struct { - ptype packetType - version uint32 - num packetNumber - keyPhaseBit bool - keyNumber int - dstConnID []byte - srcConnID []byte - frames []debugFrame + ptype packetType + version uint32 + num packetNumber + keyPhaseBit bool + keyNumber int + dstConnID []byte + srcConnID []byte + token []byte + originalDstConnID []byte // used for encoding Retry packets + frames []debugFrame } func (p testPacket) String() string { @@ -96,6 +99,9 @@ func (p testPacket) String() string { if p.dstConnID != nil { fmt.Fprintf(&b, " dst={%x}", p.dstConnID) } + if p.token != nil { + fmt.Fprintf(&b, " token={%x}", p.token) + } for _, f := range p.frames { fmt.Fprintf(&b, "\n %v", f) } @@ -111,7 +117,6 @@ type testConn struct { t *testing.T conn *Conn listener *testListener - now time.Time timer time.Time timerLastFired time.Time idlec chan struct{} // only accessed on the conn's loop @@ -184,27 +189,10 @@ type keySecret struct { // by first ensuring the loop goroutine is idle. func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { t.Helper() - tc := &testConn{ - t: t, - now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - peerConnID: testPeerConnID(0), - ignoreFrames: map[byte]bool{ - frameTypePadding: true, // ignore PADDING by default - }, - cryptoDataOut: make(map[tls.QUICEncryptionLevel][]byte), - cryptoDataIn: make(map[tls.QUICEncryptionLevel][]byte), - recvDatagram: make(chan *datagram), - } - t.Cleanup(tc.cleanup) - config := &Config{ TLSConfig: newTestTLSConfig(side), } - peerProvidedParams := defaultTransportParameters() - peerProvidedParams.initialSrcConnID = testPeerConnID(0) - if side == clientSide { - peerProvidedParams.originalDstConnID = testLocalConnID(-1) - } + var configTransportParams []func(*transportParameters) for _, o := range opts { switch o := o.(type) { case func(*Config): @@ -212,7 +200,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { case func(*tls.Config): o(config.TLSConfig) case func(p *transportParameters): - o(&peerProvidedParams) + configTransportParams = append(configTransportParams, o) default: t.Fatalf("unknown newTestConn option %T", o) } @@ -224,52 +212,75 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { initialConnID = testPeerConnID(-1) } - peerQUICConfig := &tls.QUICConfig{TLSConfig: newTestTLSConfig(side.peer())} - if side == clientSide { - tc.peerTLSConn = tls.QUICServer(peerQUICConfig) - } else { - tc.peerTLSConn = tls.QUICClient(peerQUICConfig) - } - tc.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) - tc.peerTLSConn.Start(context.Background()) - - tc.listener = newTestListener(t, config, (*testConnHooks)(tc)) - conn, err := tc.listener.l.newConn( - tc.now, + listener := newTestListener(t, config) + listener.configTransportParams = configTransportParams + conn, err := listener.l.newConn( + listener.now, side, initialConnID, netip.MustParseAddrPort("127.0.0.1:443")) if err != nil { - tc.t.Fatal(err) + t.Fatal(err) + } + tc := listener.conns[conn] + tc.wait() + return tc +} + +func newTestConnForConn(t *testing.T, listener *testListener, conn *Conn) *testConn { + t.Helper() + tc := &testConn{ + t: t, + listener: listener, + conn: conn, + peerConnID: testPeerConnID(0), + ignoreFrames: map[byte]bool{ + frameTypePadding: true, // ignore PADDING by default + }, + cryptoDataOut: make(map[tls.QUICEncryptionLevel][]byte), + cryptoDataIn: make(map[tls.QUICEncryptionLevel][]byte), + recvDatagram: make(chan *datagram), } - tc.conn = conn + t.Cleanup(tc.cleanup) + conn.testHooks = (*testConnHooks)(tc) - conn.keysAppData.updateAfter = maxPacketNumber // disable key updates - tc.keysInitial.r = conn.keysInitial.w - tc.keysInitial.w = conn.keysInitial.r + if listener.peerTLSConn != nil { + tc.peerTLSConn = listener.peerTLSConn + listener.peerTLSConn = nil + return tc + } + + peerProvidedParams := defaultTransportParameters() + peerProvidedParams.initialSrcConnID = testPeerConnID(0) + if conn.side == clientSide { + peerProvidedParams.originalDstConnID = testLocalConnID(-1) + } + for _, f := range listener.configTransportParams { + f(&peerProvidedParams) + } + + peerQUICConfig := &tls.QUICConfig{TLSConfig: newTestTLSConfig(conn.side.peer())} + if conn.side == clientSide { + tc.peerTLSConn = tls.QUICServer(peerQUICConfig) + } else { + tc.peerTLSConn = tls.QUICClient(peerQUICConfig) + } + tc.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) + tc.peerTLSConn.Start(context.Background()) - tc.wait() return tc } // advance causes time to pass. func (tc *testConn) advance(d time.Duration) { tc.t.Helper() - tc.advanceTo(tc.now.Add(d)) + tc.listener.advance(d) } // advanceTo sets the current time. func (tc *testConn) advanceTo(now time.Time) { tc.t.Helper() - if tc.now.After(now) { - tc.t.Fatalf("time moved backwards: %v -> %v", tc.now, now) - } - tc.now = now - if tc.timer.After(tc.now) { - return - } - tc.conn.sendMsg(timerEvent{}) - tc.wait() + tc.listener.advanceTo(now) } // advanceToTimer sets the current time to the time of the Conn's next timer event. @@ -284,10 +295,10 @@ func (tc *testConn) timerDelay() time.Duration { if tc.timer.IsZero() { return math.MaxInt64 // infinite } - if tc.timer.Before(tc.now) { + if tc.timer.Before(tc.listener.now) { return 0 } - return tc.timer.Sub(tc.now) + return tc.timer.Sub(tc.listener.now) } const infiniteDuration = time.Duration(math.MaxInt64) @@ -297,10 +308,10 @@ func (tc *testConn) timeUntilEvent() time.Duration { if tc.timer.IsZero() { return infiniteDuration } - if tc.timer.Before(tc.now) { + if tc.timer.Before(tc.listener.now) { return 0 } - return tc.timer.Sub(tc.now) + return tc.timer.Sub(tc.listener.now) } // wait blocks until the conn becomes idle. @@ -340,8 +351,8 @@ func (tc *testConn) cleanup() { <-tc.conn.donec } -func (tc *testConn) logDatagram(text string, d *testDatagram) { - tc.t.Helper() +func logDatagram(t *testing.T, text string, d *testDatagram) { + t.Helper() if !*testVV { return } @@ -349,7 +360,7 @@ func (tc *testConn) logDatagram(text string, d *testDatagram) { if d.paddedSize > 0 { pad = fmt.Sprintf(" (padded to %v)", d.paddedSize) } - tc.t.Logf("%v datagram%v", text, pad) + t.Logf("%v datagram%v", text, pad) for _, p := range d.packets { var s string switch p.ptype { @@ -358,15 +369,18 @@ func (tc *testConn) logDatagram(text string, d *testDatagram) { default: s = fmt.Sprintf(" %v pnum=%v ver=%v dst={%x} src={%x}", p.ptype, p.num, p.version, p.dstConnID, p.srcConnID) } + if p.token != nil { + s += fmt.Sprintf(" token={%x}", p.token) + } if p.keyPhaseBit { s += fmt.Sprintf(" KeyPhase") } if p.keyNumber != 0 { s += fmt.Sprintf(" keynum=%v", p.keyNumber) } - tc.t.Log(s) + t.Log(s) for _, f := range p.frames { - tc.t.Logf(" %v", f) + t.Logf(" %v", f) } } } @@ -374,27 +388,7 @@ func (tc *testConn) logDatagram(text string, d *testDatagram) { // write sends the Conn a datagram. func (tc *testConn) write(d *testDatagram) { tc.t.Helper() - var buf []byte - tc.logDatagram("<- conn under test receives", d) - for _, p := range d.packets { - space := spaceForPacketType(p.ptype) - if p.num >= tc.peerNextPacketNum[space] { - tc.peerNextPacketNum[space] = p.num + 1 - } - pad := 0 - if p.ptype == packetType1RTT { - pad = d.paddedSize - } - buf = append(buf, tc.encodeTestPacket(p, pad)...) - } - for len(buf) < d.paddedSize { - buf = append(buf, 0) - } - // TODO: This should use tc.listener.write. - tc.conn.sendMsg(&datagram{ - b: buf, - }) - tc.wait() + tc.listener.writeDatagram(d) } // writeFrame sends the Conn a datagram containing the given frames. @@ -464,10 +458,10 @@ func (tc *testConn) readDatagram() *testDatagram { if buf == nil { return nil } - d := tc.parseTestDatagram(buf) + d := parseTestDatagram(tc.t, tc.listener, tc, buf) // Log the datagram before removing ignored frames. // When things go wrong, it's useful to see all the frames. - tc.logDatagram("-> conn under test sends", d) + logDatagram(tc.t, "-> conn under test sends", d) typeForFrame := func(f debugFrame) byte { // This is very clunky, and points at a problem // in how we specify what frames to ignore in tests. @@ -638,21 +632,23 @@ func (tc *testConn) wantIdle(expectation string) { } } -func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { - tc.t.Helper() +func encodeTestPacket(t *testing.T, tc *testConn, p *testPacket, pad int) []byte { + t.Helper() var w packetWriter w.reset(1200) var pnumMaxAcked packetNumber - if p.ptype != packetType1RTT { + switch p.ptype { + case packetType1RTT: + w.start1RTTPacket(p.num, pnumMaxAcked, p.dstConnID) + default: w.startProtectedLongHeaderPacket(pnumMaxAcked, longPacket{ ptype: p.ptype, version: p.version, num: p.num, dstConnID: p.dstConnID, srcConnID: p.srcConnID, + extra: p.token, }) - } else { - w.start1RTTPacket(p.num, pnumMaxAcked, p.dstConnID) } for _, f := range p.frames { f.write(&w) @@ -660,14 +656,22 @@ func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { w.appendPaddingTo(pad) if p.ptype != packetType1RTT { var k fixedKeys - switch p.ptype { - case packetTypeInitial: - k = tc.keysInitial.w - case packetTypeHandshake: - k = tc.keysHandshake.w + if tc == nil { + if p.ptype == packetTypeInitial { + k = initialKeys(p.dstConnID, serverSide).r + } else { + t.Fatalf("sending %v packet with no conn", p.ptype) + } + } else { + switch p.ptype { + case packetTypeInitial: + k = tc.keysInitial.w + case packetTypeHandshake: + k = tc.keysHandshake.w + } } if !k.isSet() { - tc.t.Fatalf("sending %v packet with no write key", p.ptype) + t.Fatalf("sending %v packet with no write key", p.ptype) } w.finishProtectedLongHeaderPacket(pnumMaxAcked, k, longPacket{ ptype: p.ptype, @@ -675,10 +679,11 @@ func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { num: p.num, dstConnID: p.dstConnID, srcConnID: p.srcConnID, + extra: p.token, }) } else { - if !tc.wkeyAppData.hdr.isSet() { - tc.t.Fatalf("sending 1-RTT packet with no write key") + if tc == nil || !tc.wkeyAppData.hdr.isSet() { + t.Fatalf("sending 1-RTT packet with no write key") } // Somewhat hackish: Generate a temporary updatingKeyPair that will // always use our desired key phase. @@ -700,8 +705,8 @@ func (tc *testConn) encodeTestPacket(p *testPacket, pad int) []byte { return w.datagram() } -func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { - tc.t.Helper() +func parseTestDatagram(t *testing.T, tl *testListener, tc *testConn, buf []byte) *testDatagram { + t.Helper() bufSize := len(buf) d := &testDatagram{} size := len(buf) @@ -711,25 +716,39 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { break } ptype := getPacketType(buf) - if isLongHeader(buf[0]) { - var k fixedKeyPair - switch ptype { - case packetTypeInitial: - k = tc.keysInitial - case packetTypeHandshake: - k = tc.keysHandshake + switch ptype { + case packetTypeInitial, packetTypeHandshake: + var k fixedKeys + if tc == nil { + if ptype == packetTypeInitial { + p, _ := parseGenericLongHeaderPacket(buf) + k = initialKeys(p.srcConnID, serverSide).w + } else { + t.Fatalf("reading %v packet with no conn", ptype) + } + } else { + switch ptype { + case packetTypeInitial: + k = tc.keysInitial.r + case packetTypeHandshake: + k = tc.keysHandshake.r + } } - if !k.canRead() { - tc.t.Fatalf("reading %v packet with no read key", ptype) + 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.r, pnumMax) + p, n := parseLongHeaderPacket(buf, k, pnumMax) if n < 0 { - tc.t.Fatalf("packet parse error") + t.Fatalf("packet parse error") } - frames, err := tc.parseTestFrames(p.payload) + frames, err := parseTestFrames(t, p.payload) if err != nil { - tc.t.Fatal(err) + t.Fatal(err) + } + var token []byte + if ptype == packetTypeInitial && len(p.extra) > 0 { + token = p.extra } d.packets = append(d.packets, &testPacket{ ptype: p.ptype, @@ -737,12 +756,13 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { num: p.num, dstConnID: p.dstConnID, srcConnID: p.srcConnID, + token: token, frames: frames, }) buf = buf[n:] - } else { - if !tc.rkeyAppData.hdr.isSet() { - tc.t.Fatalf("reading 1-RTT packet with no read key") + case packetType1RTT: + if tc == nil || !tc.rkeyAppData.hdr.isSet() { + t.Fatalf("reading 1-RTT packet with no read key") } var pnumMax packetNumber // TODO: Track packet numbers. pnumOff := 1 + len(tc.peerConnID) @@ -756,7 +776,7 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { b := append([]byte{}, buf...) hdr, pay, pnum, err = tc.rkeyAppData.hdr.unprotect(b, pnumOff, pnumMax) if err != nil { - tc.t.Fatalf("1-RTT packet header parse error") + t.Fatalf("1-RTT packet header parse error") } k := tc.rkeyAppData.pkt[phase] pay, err = k.unprotect(hdr, pay, pnum) @@ -765,11 +785,11 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { } } if err != nil { - tc.t.Fatalf("1-RTT packet payload parse error") + t.Fatalf("1-RTT packet payload parse error") } - frames, err := tc.parseTestFrames(pay) + frames, err := parseTestFrames(t, pay) if err != nil { - tc.t.Fatal(err) + t.Fatal(err) } d.packets = append(d.packets, &testPacket{ ptype: packetType1RTT, @@ -780,6 +800,8 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { frames: frames, }) buf = buf[len(buf):] + default: + t.Fatalf("unhandled packet type %v", ptype) } } // This is rather hackish: If the last frame in the last packet @@ -799,8 +821,8 @@ func (tc *testConn) parseTestDatagram(buf []byte) *testDatagram { return d } -func (tc *testConn) parseTestFrames(payload []byte) ([]debugFrame, error) { - tc.t.Helper() +func parseTestFrames(t *testing.T, payload []byte) ([]debugFrame, error) { + t.Helper() var frames []debugFrame for len(payload) > 0 { f, n := parseDebugFrame(payload) @@ -822,7 +844,7 @@ func spaceForPacketType(ptype packetType) numberSpace { case packetTypeHandshake: return handshakeSpace case packetTypeRetry: - panic("TODO: packetTypeRetry") + panic("retry packets have no number space") case packetType1RTT: return appDataSpace } @@ -832,6 +854,15 @@ func spaceForPacketType(ptype packetType) numberSpace { // testConnHooks implements connTestHooks. type testConnHooks testConn +func (tc *testConnHooks) init() { + tc.conn.keysAppData.updateAfter = maxPacketNumber // disable key updates + tc.keysInitial.r = tc.conn.keysInitial.w + tc.keysInitial.w = tc.conn.keysInitial.r + if tc.conn.side == serverSide { + tc.listener.acceptQueue = append(tc.listener.acceptQueue, (*testConn)(tc)) + } +} + // handleTLSEvent processes TLS events generated by // the connection under test's tls.QUICConn. // @@ -929,20 +960,20 @@ func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.Time, m any) { tc.timer = timer for { - if !timer.IsZero() && !timer.After(tc.now) { + if !timer.IsZero() && !timer.After(tc.listener.now) { if timer.Equal(tc.timerLastFired) { // If the connection timer fires at time T, the Conn should take some // action to advance the timer into the future. If the Conn reschedules // the timer for the same time, it isn't making progress and we have a bug. - tc.t.Errorf("connection timer spinning; now=%v timer=%v", tc.now, timer) + tc.t.Errorf("connection timer spinning; now=%v timer=%v", tc.listener.now, timer) } else { tc.timerLastFired = timer - return tc.now, timerEvent{} + return tc.listener.now, timerEvent{} } } select { case m := <-msgc: - return tc.now, m + return tc.listener.now, m default: } if !tc.wakeAsync() { @@ -956,7 +987,7 @@ func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.T close(idlec) } m = <-msgc - return tc.now, m + return tc.listener.now, m } func (tc *testConnHooks) newConnID(seq int64) ([]byte, error) { @@ -964,7 +995,7 @@ func (tc *testConnHooks) newConnID(seq int64) ([]byte, error) { } func (tc *testConnHooks) timeNow() time.Time { - return tc.now + return tc.listener.now } // testLocalConnID returns the connection ID with a given sequence number diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 96b1e45934..9f14b09047 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -23,7 +23,7 @@ import ( type Listener struct { config *Config udpConn udpConn - testHooks connTestHooks + testHooks listenerTestHooks acceptQueue queue[*Conn] // new inbound connections @@ -40,6 +40,11 @@ type Listener struct { connIDUpdates []connIDUpdate } +type listenerTestHooks interface { + timeNow() time.Time + newConn(c *Conn) +} + // A udpConn is a UDP connection. // It is implemented by net.UDPConn. type udpConn interface { @@ -72,7 +77,7 @@ func Listen(network, address string, config *Config) (*Listener, error) { return newListener(udpConn, config, nil), nil } -func newListener(udpConn udpConn, config *Config, hooks connTestHooks) *Listener { +func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) *Listener { l := &Listener{ config: config, udpConn: udpConn, @@ -154,11 +159,10 @@ func (l *Listener) newConn(now time.Time, side connSide, initialConnID []byte, p if l.closing { return nil, errors.New("listener closed") } - c, err := newConn(now, side, initialConnID, peerAddr, l.config, l, l.testHooks) + c, err := newConn(now, side, initialConnID, peerAddr, l.config, l) if err != nil { return nil, err } - l.conns[c] = struct{}{} return c, nil } diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 9d0f314ecc..77362aa9bd 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -9,10 +9,13 @@ package quic import ( "bytes" "context" + "crypto/tls" "io" "net" "net/netip" + "reflect" "testing" + "time" ) func TestConnect(t *testing.T) { @@ -90,20 +93,28 @@ func newLocalListener(t *testing.T, side connSide, conf *Config) *Listener { } type testListener struct { - t *testing.T - l *Listener - recvc chan *datagram - idlec chan struct{} - sentDatagrams [][]byte + t *testing.T + l *Listener + now time.Time + recvc chan *datagram + idlec chan struct{} + conns map[*Conn]*testConn + acceptQueue []*testConn + configTransportParams []func(*transportParameters) + sentDatagrams [][]byte + peerTLSConn *tls.QUICConn + lastInitialDstConnID []byte // for parsing Retry packets } -func newTestListener(t *testing.T, config *Config, testHooks connTestHooks) *testListener { +func newTestListener(t *testing.T, config *Config) *testListener { tl := &testListener{ t: t, + now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), recvc: make(chan *datagram), idlec: make(chan struct{}), + conns: make(map[*Conn]*testConn), } - tl.l = newListener((*testListenerUDPConn)(tl), config, testHooks) + tl.l = newListener((*testListenerUDPConn)(tl), config, (*testListenerHooks)(tl)) t.Cleanup(tl.cleanup) return tl } @@ -114,6 +125,20 @@ func (tl *testListener) cleanup() { func (tl *testListener) wait() { tl.idlec <- struct{}{} + for _, tc := range tl.conns { + tc.wait() + } +} + +// accept returns a server connection from the listener. +// Unlike Listener.Accept, connections are available as soon as they are created. +func (tl *testListener) accept() *testConn { + if len(tl.acceptQueue) == 0 { + tl.t.Fatalf("accept: expected available conn, but found none") + } + tc := tl.acceptQueue[0] + tl.acceptQueue = tl.acceptQueue[1:] + return tc } func (tl *testListener) write(d *datagram) { @@ -121,7 +146,66 @@ func (tl *testListener) write(d *datagram) { tl.wait() } +var testClientAddr = netip.MustParseAddrPort("10.0.0.1:8000") + +func (tl *testListener) writeDatagram(d *testDatagram) { + tl.t.Helper() + logDatagram(tl.t, "<- listener under test receives", d) + var buf []byte + for _, p := range d.packets { + tc := tl.connForDestination(p.dstConnID) + if p.ptype != packetTypeRetry && tc != nil { + space := spaceForPacketType(p.ptype) + if p.num >= tc.peerNextPacketNum[space] { + tc.peerNextPacketNum[space] = p.num + 1 + } + } + if p.ptype == packetTypeInitial { + tl.lastInitialDstConnID = p.dstConnID + } + pad := 0 + if p.ptype == packetType1RTT { + pad = d.paddedSize + } + buf = append(buf, encodeTestPacket(tl.t, tc, p, pad)...) + } + for len(buf) < d.paddedSize { + buf = append(buf, 0) + } + addr := d.addr + if !addr.IsValid() { + addr = testClientAddr + } + tl.write(&datagram{ + b: buf, + addr: addr, + }) +} + +func (tl *testListener) connForDestination(dstConnID []byte) *testConn { + for _, tc := range tl.conns { + for _, loc := range tc.conn.connIDState.local { + if bytes.Equal(loc.cid, dstConnID) { + return tc + } + } + } + return nil +} + +func (tl *testListener) connForSource(srcConnID []byte) *testConn { + for _, tc := range tl.conns { + for _, loc := range tc.conn.connIDState.remote { + if bytes.Equal(loc.cid, srcConnID) { + return tc + } + } + } + return nil +} + func (tl *testListener) read() []byte { + tl.t.Helper() tl.wait() if len(tl.sentDatagrams) == 0 { return nil @@ -131,6 +215,88 @@ func (tl *testListener) read() []byte { return d } +func (tl *testListener) readDatagram() *testDatagram { + tl.t.Helper() + buf := tl.read() + if buf == nil { + return nil + } + p, _ := parseGenericLongHeaderPacket(buf) + tc := tl.connForSource(p.dstConnID) + d := parseTestDatagram(tl.t, tl, tc, buf) + logDatagram(tl.t, "-> listener under test sends", d) + return d +} + +// wantDatagram indicates that we expect the Listener to send a datagram. +func (tl *testListener) wantDatagram(expectation string, want *testDatagram) { + tl.t.Helper() + got := tl.readDatagram() + if !reflect.DeepEqual(got, want) { + tl.t.Fatalf("%v:\ngot datagram: %v\nwant datagram: %v", expectation, got, want) + } +} + +func (tl *testListener) newClientTLS(srcConnID, dstConnID []byte) []byte { + peerProvidedParams := defaultTransportParameters() + peerProvidedParams.initialSrcConnID = srcConnID + peerProvidedParams.originalDstConnID = dstConnID + for _, f := range tl.configTransportParams { + f(&peerProvidedParams) + } + + config := &tls.QUICConfig{TLSConfig: newTestTLSConfig(clientSide)} + tl.peerTLSConn = tls.QUICClient(config) + tl.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) + tl.peerTLSConn.Start(context.Background()) + var data []byte + for { + e := tl.peerTLSConn.NextEvent() + switch e.Kind { + case tls.QUICNoEvent: + return data + case tls.QUICWriteData: + if e.Level != tls.QUICEncryptionLevelInitial { + tl.t.Fatal("initial data at unexpected level") + } + data = append(data, e.Data...) + } + } +} + +// advance causes time to pass. +func (tl *testListener) advance(d time.Duration) { + tl.t.Helper() + tl.advanceTo(tl.now.Add(d)) +} + +// advanceTo sets the current time. +func (tl *testListener) advanceTo(now time.Time) { + tl.t.Helper() + if tl.now.After(now) { + tl.t.Fatalf("time moved backwards: %v -> %v", tl.now, now) + } + tl.now = now + for _, tc := range tl.conns { + if !tc.timer.After(tl.now) { + tc.conn.sendMsg(timerEvent{}) + tc.wait() + } + } +} + +// testListenerHooks implements listenerTestHooks. +type testListenerHooks testListener + +func (tl *testListenerHooks) timeNow() time.Time { + return tl.now +} + +func (tl *testListenerHooks) newConn(c *Conn) { + tc := newTestConnForConn(tl.t, (*testListener)(tl), c) + tl.conns[c] = tc +} + // testListenerUDPConn implements UDPConn. type testListenerUDPConn testListener diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 81d17b8587..337657e32c 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -36,7 +36,7 @@ func (tc *testConn) handshake() { for { if i == len(dgrams)-1 { if tc.conn.side == clientSide { - want := tc.now.Add(maxAckDelay - timerGranularity) + want := tc.listener.now.Add(maxAckDelay - timerGranularity) if !tc.timer.Equal(want) { t.Fatalf("want timer = %v (max_ack_delay), got %v", want, tc.timer) } @@ -90,7 +90,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { } else { clientConnIDs = peerConnIDs serverConnIDs = localConnIDs - transientConnID = []byte{0xde, 0xad, 0xbe, 0xef} + transientConnID = testPeerConnID(-1) } return []*testDatagram{{ // Client Initial @@ -564,7 +564,7 @@ func TestConnAEADLimitReached(t *testing.T) { // Only use the transient connection ID in Initial packets. dstConnID = tc.conn.connIDState.local[1].cid } - invalid := tc.encodeTestPacket(&testPacket{ + invalid := encodeTestPacket(t, tc, &testPacket{ ptype: packetType1RTT, num: 1000, frames: []debugFrame{debugFramePing{}}, diff --git a/internal/quic/version_test.go b/internal/quic/version_test.go index cfb7ce4be7..264df9dbc6 100644 --- a/internal/quic/version_test.go +++ b/internal/quic/version_test.go @@ -17,7 +17,7 @@ func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { config := &Config{ TLSConfig: newTestTLSConfig(serverSide), } - tl := newTestListener(t, config, nil) + tl := newTestListener(t, config) // Packet of unknown contents for some unrecognized QUIC version. dstConnID := []byte{1, 2, 3, 4} From 48a597731ce9fbe97fed1733da5e808233b6cdcb Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 13 Oct 2023 09:44:23 -0700 Subject: [PATCH 081/168] quic: support Retry Add a RequireAddressValidation configuration setting to enable sending Retry packets on the server. Support receiving Retry packets on the client. RFC 9000, Section 8.1.2. For golang/go#58547 Change-Id: Ia78b9594a03ce1b1143b95cb3c1ef4c38b2b39ef Reviewed-on: https://go-review.googlesource.com/c/net/+/535237 LUCI-TryBot-Result: Go LUCI Reviewed-by: Roland Shoemaker Reviewed-by: Jonathan Amsterdam --- internal/quic/config.go | 8 + internal/quic/conn.go | 26 +- internal/quic/conn_id.go | 51 +-- internal/quic/conn_id_test.go | 3 - internal/quic/conn_recv.go | 39 +++ internal/quic/conn_send.go | 1 + internal/quic/conn_test.go | 28 +- internal/quic/listener.go | 53 ++- internal/quic/listener_test.go | 13 +- internal/quic/loss.go | 13 + internal/quic/packet.go | 3 + internal/quic/packet_parser.go | 4 +- internal/quic/retry.go | 235 ++++++++++++++ internal/quic/retry_test.go | 568 +++++++++++++++++++++++++++++++++ 14 files changed, 999 insertions(+), 46 deletions(-) create mode 100644 internal/quic/retry.go create mode 100644 internal/quic/retry_test.go diff --git a/internal/quic/config.go b/internal/quic/config.go index b390d6911e..99ef68fead 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -47,6 +47,14 @@ type Config struct { // If zero, the default value of 1MiB is used. // If negative, the limit is zero. MaxConnReadBufferSize int64 + + // RequireAddressValidation may be set to true to enable address validation + // of client connections prior to starting the handshake. + // + // Enabling this setting reduces the amount of work packets with spoofed + // source address information can cause a server to perform, + // at the cost of increased handshake latency. + RequireAddressValidation bool } func configDefault(v, def, limit int64) int64 { diff --git a/internal/quic/conn.go b/internal/quic/conn.go index ea03bbf981..4acf5ddfe6 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -48,6 +48,9 @@ type Conn struct { crypto [numberSpaceCount]cryptoStream tls *tls.QUICConn + // retryToken is the token provided by the peer in a Retry packet. + retryToken []byte + // handshakeConfirmed is set when the handshake is confirmed. // For server connections, it tracks sending HANDSHAKE_DONE. handshakeConfirmed sentVal @@ -83,7 +86,7 @@ type connTestHooks interface { timeNow() time.Time } -func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) { +func newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) { c := &Conn{ side: side, listener: l, @@ -104,17 +107,21 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. l.testHooks.newConn(c) } - var originalDstConnID []byte + // initialConnID is the connection ID used to generate Initial packet protection keys. + var initialConnID []byte if c.side == clientSide { if err := c.connIDState.initClient(c); err != nil { return nil, err } initialConnID, _ = c.connIDState.dstConnID() } else { + initialConnID = originalDstConnID + if retrySrcConnID != nil { + initialConnID = retrySrcConnID + } if err := c.connIDState.initServer(c, initialConnID); err != nil { return nil, err } - originalDstConnID = initialConnID } // The smallest allowed maximum QUIC datagram size is 1200 bytes. @@ -125,10 +132,10 @@ func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip. c.streamsInit() c.lifetimeInit() - // TODO: retry_source_connection_id if err := c.startTLS(now, initialConnID, transportParameters{ initialSrcConnID: c.connIDState.srcConnID(), originalDstConnID: originalDstConnID, + retrySrcConnID: retrySrcConnID, ackDelayExponent: ackDelayExponent, maxUDPPayloadSize: maxUDPPayloadSize, maxAckDelay: maxAckDelay, @@ -195,7 +202,8 @@ func (c *Conn) discardKeys(now time.Time, space numberSpace) { // receiveTransportParameters applies transport parameters sent by the peer. func (c *Conn) receiveTransportParameters(p transportParameters) error { - if err := c.connIDState.validateTransportParameters(c.side, p); err != nil { + isRetry := c.retryToken != nil + if err := c.connIDState.validateTransportParameters(c.side, isRetry, p); err != nil { return err } c.streams.outflow.setMaxData(p.initialMaxData) @@ -220,9 +228,11 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error { return err } } - - // TODO: Many more transport parameters to come. - + // TODO: max_idle_timeout + // TODO: stateless_reset_token + // TODO: max_udp_payload_size + // TODO: disable_active_migration + // TODO: preferred_address return nil } diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index 045e646ac1..ff7e2d1c6e 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -28,6 +28,9 @@ type connIDState struct { retireRemotePriorTo int64 // largest Retire Prior To value sent by the peer peerActiveConnIDLimit int64 // peer's active_connection_id_limit transport parameter + originalDstConnID []byte // expected original_destination_connection_id param + retrySrcConnID []byte // expected retry_source_connection_id param + needSend bool } @@ -78,6 +81,7 @@ func (s *connIDState) initClient(c *Conn) error { seq: -1, cid: remid, }) + s.originalDstConnID = remid const retired = false c.listener.connIDsChanged(c, retired, s.local[:]) return nil @@ -163,27 +167,21 @@ func (s *connIDState) issueLocalIDs(c *Conn) error { // validateTransportParameters verifies the original_destination_connection_id and // initial_source_connection_id transport parameters match the expected values. -func (s *connIDState) validateTransportParameters(side connSide, p transportParameters) error { +func (s *connIDState) validateTransportParameters(side connSide, isRetry bool, p transportParameters) error { // TODO: Consider returning more detailed errors, for debugging. - switch side { - case clientSide: - // Verify original_destination_connection_id matches - // the transient remote connection ID we chose. - if len(s.remote) == 0 || s.remote[0].seq != -1 { - return localTransportError(errInternal) - } - if !bytes.Equal(s.remote[0].cid, p.originalDstConnID) { - return localTransportError(errTransportParameter) - } - // Remove the transient remote connection ID. - // We have no further need for it. - s.remote = append(s.remote[:0], s.remote[1:]...) - case serverSide: - if p.originalDstConnID != nil { - // Clients do not send original_destination_connection_id. - return localTransportError(errTransportParameter) - } + // Verify original_destination_connection_id matches + // the transient remote connection ID we chose (client) + // or is empty (server). + if !bytes.Equal(s.originalDstConnID, p.originalDstConnID) { + return localTransportError(errTransportParameter) + } + s.originalDstConnID = nil // we have no further need for this + // Verify retry_source_connection_id matches the value from + // the server's Retry packet (when one was sent), or is empty. + if !bytes.Equal(p.retrySrcConnID, s.retrySrcConnID) { + return localTransportError(errTransportParameter) } + s.retrySrcConnID = nil // we have no further need for this // Verify initial_source_connection_id matches the first remote connection ID. if len(s.remote) == 0 || s.remote[0].seq != 0 { return localTransportError(errInternal) @@ -203,13 +201,10 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) // We're a client connection processing the first Initial packet // from the server. Replace the transient remote connection ID // with the Source Connection ID from the packet. - // Leave the transient ID the list for now, since we'll need it when - // processing the transport parameters. - s.remote[0].retired = true - s.remote = append(s.remote, connID{ + s.remote[0] = connID{ seq: 0, cid: cloneBytes(srcConnID), - }) + } } case ptype == packetTypeInitial && c.side == serverSide: if len(s.remote) == 0 { @@ -232,6 +227,14 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) } } +func (s *connIDState) handleRetryPacket(srcConnID []byte) { + if len(s.remote) != 1 || s.remote[0].seq != -1 { + panic("BUG: handling retry with non-transient remote conn id") + } + s.retrySrcConnID = cloneBytes(srcConnID) + s.remote[0].cid = s.retrySrcConnID +} + func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken [16]byte) error { if len(s.remote[0].cid) == 0 { // "An endpoint that is sending packets with a zero-length diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index 44755ecf45..784c5e2c48 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -48,9 +48,6 @@ func TestConnIDClientHandshake(t *testing.T) { t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } wantRemote := []connID{{ - cid: testLocalConnID(-1), - seq: -1, - }, { cid: testPeerConnID(0), seq: 0, }} diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 9b1ba1ae10..e789ae0455 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -34,6 +34,9 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { n = c.handleLongHeader(now, ptype, handshakeSpace, c.keysHandshake.r, buf) case packetType1RTT: n = c.handle1RTT(now, buf) + case packetTypeRetry: + c.handleRetry(now, buf) + return case packetTypeVersionNegotiation: c.handleVersionNegotiation(now, buf) return @@ -128,6 +131,42 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int { return len(buf) } +func (c *Conn) handleRetry(now time.Time, pkt []byte) { + if c.side != clientSide { + return // clients don't send Retry packets + } + // "After the client has received and processed an Initial or Retry packet + // from the server, it MUST discard any subsequent Retry packets that it receives." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1 + if !c.keysInitial.canRead() { + return // discarded Initial keys, connection is already established + } + if c.acks[initialSpace].seen.numRanges() != 0 { + return // processed at least one packet + } + if c.retryToken != nil { + return // received a Retry already + } + // "Clients MUST discard Retry packets that have a Retry Integrity Tag + // that cannot be validated." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2 + p, ok := parseRetryPacket(pkt, c.connIDState.originalDstConnID) + if !ok { + return + } + // "A client MUST discard a Retry packet with a zero-length Retry Token field." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2 + if len(p.token) == 0 { + return + } + c.retryToken = cloneBytes(p.token) + c.connIDState.handleRetryPacket(p.srcConnID) + // We need to resend any data we've already sent in Initial packets. + // We must not reuse already sent packet numbers. + c.loss.discardPackets(initialSpace, c.handleAckOrLoss) + // TODO: Discard 0-RTT packets as well, once we support 0-RTT. +} + var errVersionNegotiation = errors.New("server does not support QUIC version 1") func (c *Conn) handleVersionNegotiation(now time.Time, pkt []byte) { diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 00b02c2a31..efeb04fe39 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -68,6 +68,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { num: pnum, dstConnID: dstConnID, srcConnID: c.connIDState.srcConnID(), + extra: c.retryToken, } c.w.startProtectedLongHeaderPacket(pnumMaxAcked, p) c.appendFrames(now, initialSpace, pnum, limit) diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index ea47b0b295..cfb0d062c4 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -218,6 +218,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { listener.now, side, initialConnID, + nil, netip.MustParseAddrPort("127.0.0.1:443")) if err != nil { t.Fatal(err) @@ -545,7 +546,13 @@ func (tc *testConn) readPacket() *testPacket { if d == nil { return nil } - tc.sentPackets = d.packets + for _, p := range d.packets { + if len(p.frames) == 0 { + tc.lastPacket = p + continue + } + tc.sentPackets = append(tc.sentPackets, p) + } } p := tc.sentPackets[0] tc.sentPackets = tc.sentPackets[1:] @@ -638,6 +645,12 @@ func encodeTestPacket(t *testing.T, tc *testConn, p *testPacket, pad int) []byte w.reset(1200) var pnumMaxAcked packetNumber switch p.ptype { + case packetTypeRetry: + return encodeRetryPacket(p.originalDstConnID, retryPacket{ + srcConnID: p.srcConnID, + dstConnID: p.dstConnID, + token: p.token, + }) case packetType1RTT: w.start1RTTPacket(p.num, pnumMaxAcked, p.dstConnID) default: @@ -717,6 +730,19 @@ func parseTestDatagram(t *testing.T, tl *testListener, tc *testConn, buf []byte) } ptype := getPacketType(buf) switch ptype { + case packetTypeRetry: + retry, ok := parseRetryPacket(buf, tl.lastInitialDstConnID) + if !ok { + t.Fatalf("could not parse %v packet", ptype) + } + return &testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + dstConnID: retry.dstConnID, + srcConnID: retry.srcConnID, + token: retry.token, + }}, + } case packetTypeInitial, packetTypeHandshake: var k fixedKeys if tc == nil { diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 9f14b09047..aa258395e9 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -24,6 +24,7 @@ type Listener struct { config *Config udpConn udpConn testHooks listenerTestHooks + retry retryState acceptQueue queue[*Conn] // new inbound connections @@ -74,10 +75,10 @@ func Listen(network, address string, config *Config) (*Listener, error) { if err != nil { return nil, err } - return newListener(udpConn, config, nil), nil + return newListener(udpConn, config, nil) } -func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) *Listener { +func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) (*Listener, error) { l := &Listener{ config: config, udpConn: udpConn, @@ -86,8 +87,13 @@ func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) *List acceptQueue: newQueue[*Conn](), closec: make(chan struct{}), } + if config.RequireAddressValidation { + if err := l.retry.init(); err != nil { + return nil, err + } + } go l.listen() - return l + return l, nil } // LocalAddr returns the local network address. @@ -142,7 +148,7 @@ func (l *Listener) Dial(ctx context.Context, network, address string) (*Conn, er } addr := u.AddrPort() addr = netip.AddrPortFrom(addr.Addr().Unmap(), addr.Port()) - c, err := l.newConn(time.Now(), clientSide, nil, addr) + c, err := l.newConn(time.Now(), clientSide, nil, nil, addr) if err != nil { return nil, err } @@ -153,13 +159,13 @@ func (l *Listener) Dial(ctx context.Context, network, address string) (*Conn, er return c, nil } -func (l *Listener) newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort) (*Conn, error) { +func (l *Listener) newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []byte, peerAddr netip.AddrPort) (*Conn, error) { l.connsMu.Lock() defer l.connsMu.Unlock() if l.closing { return nil, errors.New("listener closed") } - c, err := newConn(now, side, initialConnID, peerAddr, l.config, l) + c, err := newConn(now, side, originalDstConnID, retrySrcConnID, peerAddr, l.config, l) if err != nil { return nil, err } @@ -300,8 +306,19 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { } else { now = time.Now() } + var originalDstConnID, retrySrcConnID []byte + if l.config.RequireAddressValidation { + var ok bool + retrySrcConnID = p.dstConnID + originalDstConnID, ok = l.validateInitialAddress(now, p, m.addr) + if !ok { + return + } + } else { + originalDstConnID = p.dstConnID + } var err error - c, err := l.newConn(now, serverSide, p.dstConnID, m.addr) + c, err := l.newConn(now, serverSide, originalDstConnID, retrySrcConnID, m.addr) if err != nil { // The accept queue is probably full. // We could send a CONNECTION_CLOSE to the peer to reject the connection. @@ -320,6 +337,28 @@ func (l *Listener) sendVersionNegotiation(p genericLongPacket, addr netip.AddrPo m.recycle() } +func (l *Listener) sendConnectionClose(in genericLongPacket, addr netip.AddrPort, code transportError) { + keys := initialKeys(in.dstConnID, serverSide) + var w packetWriter + p := longPacket{ + ptype: packetTypeInitial, + version: quicVersion1, + num: 0, + dstConnID: in.srcConnID, + srcConnID: in.dstConnID, + } + const pnumMaxAcked = 0 + w.reset(minimumClientInitialDatagramSize) + w.startProtectedLongHeaderPacket(pnumMaxAcked, p) + w.appendConnectionCloseTransportFrame(code, 0, "") + w.finishProtectedLongHeaderPacket(pnumMaxAcked, keys.w, p) + buf := w.datagram() + if len(buf) == 0 { + return + } + l.sendDatagram(buf, addr) +} + func (l *Listener) sendDatagram(p []byte, addr netip.AddrPort) error { _, err := l.udpConn.WriteToUDPAddrPort(p, addr) return err diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 77362aa9bd..346f81c386 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -114,7 +114,11 @@ func newTestListener(t *testing.T, config *Config) *testListener { idlec: make(chan struct{}), conns: make(map[*Conn]*testConn), } - tl.l = newListener((*testListenerUDPConn)(tl), config, (*testListenerHooks)(tl)) + var err error + tl.l, err = newListener((*testListenerUDPConn)(tl), config, (*testListenerHooks)(tl)) + if err != nil { + t.Fatal(err) + } t.Cleanup(tl.cleanup) return tl } @@ -237,6 +241,13 @@ func (tl *testListener) wantDatagram(expectation string, want *testDatagram) { } } +// wantIdle indicates that we expect the Listener to not send any more datagrams. +func (tl *testListener) wantIdle(expectation string) { + if got := tl.readDatagram(); got != nil { + tl.t.Fatalf("expect: %v\nunexpectedly got: %v", expectation, got) + } +} + func (tl *testListener) newClientTLS(srcConnID, dstConnID []byte) []byte { peerProvidedParams := defaultTransportParameters() peerProvidedParams.initialSrcConnID = srcConnID diff --git a/internal/quic/loss.go b/internal/quic/loss.go index 152815a291..c0f915b422 100644 --- a/internal/quic/loss.go +++ b/internal/quic/loss.go @@ -281,6 +281,19 @@ func (c *lossState) receiveAckEnd(now time.Time, space numberSpace, ackDelay tim c.cc.packetBatchEnd(now, space, &c.rtt, c.maxAckDelay) } +// discardPackets declares that packets within a number space will not be delivered +// and that data contained in them should be resent. +// For example, after receiving a Retry packet we discard already-sent Initial packets. +func (c *lossState) discardPackets(space numberSpace, lossf func(numberSpace, *sentPacket, packetFate)) { + for i := 0; i < c.spaces[space].size; i++ { + sent := c.spaces[space].nth(i) + sent.lost = true + c.cc.packetDiscarded(sent) + lossf(numberSpace(space), sent, packetLost) + } + c.spaces[space].clean() +} + // discardKeys is called when dropping packet protection keys for a number space. func (c *lossState) discardKeys(now time.Time, space numberSpace) { // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.4 diff --git a/internal/quic/packet.go b/internal/quic/packet.go index 7d69f96d27..df589cccac 100644 --- a/internal/quic/packet.go +++ b/internal/quic/packet.go @@ -97,6 +97,9 @@ const ( streamFinBit = 0x01 ) +// Maximum length of a connection ID. +const maxConnIDLen = 20 + // isLongHeader returns true if b is the first byte of a long header. func isLongHeader(b byte) bool { return b&headerFormLong == headerFormLong diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index ce04339025..8bcd8668eb 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -47,7 +47,7 @@ func parseLongHeaderPacket(pkt []byte, k fixedKeys, pnumMax packetNumber) (p lon // Destination Connection ID Length (8), // Destination Connection ID (0..160), p.dstConnID, n = consumeUint8Bytes(b) - if n < 0 || len(p.dstConnID) > 20 { + if n < 0 || len(p.dstConnID) > maxConnIDLen { return longPacket{}, -1 } b = b[n:] @@ -55,7 +55,7 @@ func parseLongHeaderPacket(pkt []byte, k fixedKeys, pnumMax packetNumber) (p lon // Source Connection ID Length (8), // Source Connection ID (0..160), p.srcConnID, n = consumeUint8Bytes(b) - if n < 0 || len(p.dstConnID) > 20 { + if n < 0 || len(p.dstConnID) > maxConnIDLen { return longPacket{}, -1 } b = b[n:] diff --git a/internal/quic/retry.go b/internal/quic/retry.go new file mode 100644 index 0000000000..e3d9f4d7d1 --- /dev/null +++ b/internal/quic/retry.go @@ -0,0 +1,235 @@ +// 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" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/binary" + "net/netip" + "time" + + "golang.org/x/crypto/chacha20poly1305" +) + +// AEAD and nonce used to compute the Retry Integrity Tag. +// https://www.rfc-editor.org/rfc/rfc9001#section-5.8 +var ( + retrySecret = []byte{0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a, 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, 0xc8, 0x4e} + retryNonce = []byte{0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb} + retryAEAD = func() cipher.AEAD { + c, err := aes.NewCipher(retrySecret) + if err != nil { + panic(err) + } + aead, err := cipher.NewGCM(c) + if err != nil { + panic(err) + } + return aead + }() +) + +// retryTokenValidityPeriod is how long we accept a Retry packet token after sending it. +const retryTokenValidityPeriod = 5 * time.Second + +// retryState generates and validates a listener's retry tokens. +type retryState struct { + aead cipher.AEAD +} + +func (rs *retryState) init() error { + // Retry tokens are authenticated using a per-server key chosen at start time. + // TODO: Provide a way for the user to set this key. + secret := make([]byte, chacha20poly1305.KeySize) + if _, err := rand.Read(secret); err != nil { + return err + } + aead, err := chacha20poly1305.NewX(secret) + if err != nil { + panic(err) + } + rs.aead = aead + return nil +} + +// Retry tokens are encrypted with an AEAD. +// The plaintext contains the time the token was created and +// the original destination connection ID. +// The additional data contains the sender's source address and original source connection ID. +// The token nonce is randomly generated. +// We use the nonce as the Source Connection ID of the Retry packet. +// Since the 24-byte XChaCha20-Poly1305 nonce is too large to fit in a 20-byte connection ID, +// we include the remaining 4 bytes of nonce in the token. +// +// Token { +// Last 4 Bytes of Nonce (32), +// Ciphertext (..), +// } +// +// Plaintext { +// Timestamp (64), +// Original Destination Connection ID, +// } +// +// +// Additional Data { +// Original Source Connection ID Length (8), +// Original Source Connection ID (..), +// IP Address (32..128), +// Port (16), +// } +// +// TODO: Consider using AES-256-GCM-SIV once crypto/tls supports it. + +func (rs *retryState) makeToken(now time.Time, srcConnID, origDstConnID []byte, addr netip.AddrPort) (token, newDstConnID []byte, err error) { + nonce := make([]byte, rs.aead.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return nil, nil, err + } + + var plaintext []byte + plaintext = binary.BigEndian.AppendUint64(plaintext, uint64(now.Unix())) + plaintext = append(plaintext, origDstConnID...) + + token = append(token, nonce[maxConnIDLen:]...) + token = rs.aead.Seal(token, nonce, plaintext, rs.additionalData(srcConnID, addr)) + return token, nonce[:maxConnIDLen], nil +} + +func (rs *retryState) validateToken(now time.Time, token, srcConnID, dstConnID []byte, addr netip.AddrPort) (origDstConnID []byte, ok bool) { + tokenNonceLen := rs.aead.NonceSize() - maxConnIDLen + if len(token) < tokenNonceLen { + return nil, false + } + nonce := append([]byte{}, dstConnID...) + nonce = append(nonce, token[:tokenNonceLen]...) + ciphertext := token[tokenNonceLen:] + + plaintext, err := rs.aead.Open(nil, nonce, ciphertext, rs.additionalData(srcConnID, addr)) + if err != nil { + return nil, false + } + if len(plaintext) < 8 { + return nil, false + } + when := time.Unix(int64(binary.BigEndian.Uint64(plaintext)), 0) + origDstConnID = plaintext[8:] + + // We allow for tokens created in the future (up to the validity period), + // which likely indicates that the system clock was adjusted backwards. + if d := abs(now.Sub(when)); d > retryTokenValidityPeriod { + return nil, false + } + + return origDstConnID, true +} + +func (rs *retryState) additionalData(srcConnID []byte, addr netip.AddrPort) []byte { + var additional []byte + additional = appendUint8Bytes(additional, srcConnID) + additional = append(additional, addr.Addr().AsSlice()...) + additional = binary.BigEndian.AppendUint16(additional, addr.Port()) + return additional +} + +func (l *Listener) validateInitialAddress(now time.Time, p genericLongPacket, addr netip.AddrPort) (origDstConnID []byte, ok bool) { + // The retry token is at the start of an Initial packet's data. + token, n := consumeUint8Bytes(p.data) + if n < 0 { + // We've already validated that the packet is at least 1200 bytes long, + // so there's no way for even a maximum size token to not fit. + // Check anyway. + return nil, false + } + if len(token) == 0 { + // The sender has not provided a token. + // Send a Retry packet to them with one. + l.sendRetry(now, p, addr) + return nil, false + } + origDstConnID, ok = l.retry.validateToken(now, token, p.srcConnID, p.dstConnID, addr) + if !ok { + // This does not seem to be a valid token. + // Close the connection with an INVALID_TOKEN error. + // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5 + l.sendConnectionClose(p, addr, errInvalidToken) + return nil, false + } + return origDstConnID, true +} + +func (l *Listener) sendRetry(now time.Time, p genericLongPacket, addr netip.AddrPort) { + token, srcConnID, err := l.retry.makeToken(now, p.srcConnID, p.dstConnID, addr) + if err != nil { + return + } + b := encodeRetryPacket(p.dstConnID, retryPacket{ + dstConnID: p.srcConnID, + srcConnID: srcConnID, + token: token, + }) + l.sendDatagram(b, addr) +} + +type retryPacket struct { + dstConnID []byte + srcConnID []byte + token []byte +} + +func encodeRetryPacket(originalDstConnID []byte, p retryPacket) []byte { + // Retry packets include an integrity tag, computed by AEAD_AES_128_GCM over + // the original destination connection ID followed by the Retry packet + // (less the integrity tag itself). + // https://www.rfc-editor.org/rfc/rfc9001#section-5.8 + // + // Create the pseudo-packet (including the original DCID), append the tag, + // and return the Retry packet. + var b []byte + b = appendUint8Bytes(b, originalDstConnID) // Original Destination Connection ID + start := len(b) // start of the Retry packet + b = append(b, headerFormLong|fixedBit|longPacketTypeRetry) + b = binary.BigEndian.AppendUint32(b, quicVersion1) // Version + b = appendUint8Bytes(b, p.dstConnID) // Destination Connection ID + b = appendUint8Bytes(b, p.srcConnID) // Source Connection ID + b = append(b, p.token...) // Token + b = retryAEAD.Seal(b, retryNonce, nil, b) // Retry Integrity Tag + return b[start:] +} + +func parseRetryPacket(b, origDstConnID []byte) (p retryPacket, ok bool) { + const retryIntegrityTagLength = 128 / 8 + + lp, ok := parseGenericLongHeaderPacket(b) + if !ok { + return retryPacket{}, false + } + if len(lp.data) < retryIntegrityTagLength { + return retryPacket{}, false + } + gotTag := lp.data[len(lp.data)-retryIntegrityTagLength:] + + // Create the pseudo-packet consisting of the original destination connection ID + // followed by the Retry packet (less the integrity tag). + // Use this to validate the packet integrity tag. + pseudo := appendUint8Bytes(nil, origDstConnID) + pseudo = append(pseudo, b[:len(b)-retryIntegrityTagLength]...) + wantTag := retryAEAD.Seal(nil, retryNonce, nil, pseudo) + if !bytes.Equal(gotTag, wantTag) { + return retryPacket{}, false + } + + token := lp.data[:len(lp.data)-retryIntegrityTagLength] + return retryPacket{ + dstConnID: lp.dstConnID, + srcConnID: lp.srcConnID, + token: token, + }, true +} diff --git a/internal/quic/retry_test.go b/internal/quic/retry_test.go new file mode 100644 index 0000000000..f754270a5e --- /dev/null +++ b/internal/quic/retry_test.go @@ -0,0 +1,568 @@ +// 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" + "context" + "crypto/tls" + "net/netip" + "testing" + "time" +) + +type retryServerTest struct { + tl *testListener + originalSrcConnID []byte + originalDstConnID []byte + retry retryPacket + initialCrypto []byte +} + +// newRetryServerTest creates a test server connection, +// sends the connection an Initial packet, +// and expects a Retry in response. +func newRetryServerTest(t *testing.T) *retryServerTest { + t.Helper() + config := &Config{ + TLSConfig: newTestTLSConfig(serverSide), + RequireAddressValidation: true, + } + tl := newTestListener(t, config) + srcID := testPeerConnID(0) + dstID := testLocalConnID(-1) + params := defaultTransportParameters() + params.initialSrcConnID = srcID + initialCrypto := initialClientCrypto(t, tl, params) + + // Initial packet with no Token. + // Server responds with a Retry containing a token. + tl.writeDatagram(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 0, + version: quicVersion1, + srcConnID: srcID, + dstConnID: dstID, + frames: []debugFrame{ + debugFrameCrypto{ + data: initialCrypto, + }, + }, + }}, + paddedSize: 1200, + }) + got := tl.readDatagram() + if len(got.packets) != 1 || got.packets[0].ptype != packetTypeRetry { + t.Fatalf("got datagram: %v\nwant Retry", got) + } + p := got.packets[0] + if got, want := p.dstConnID, srcID; !bytes.Equal(got, want) { + t.Fatalf("Retry destination = {%x}, want {%x}", got, want) + } + + return &retryServerTest{ + tl: tl, + originalSrcConnID: srcID, + originalDstConnID: dstID, + retry: retryPacket{ + dstConnID: p.dstConnID, + srcConnID: p.srcConnID, + token: p.token, + }, + initialCrypto: initialCrypto, + } +} + +func TestRetryServerSucceeds(t *testing.T) { + rt := newRetryServerTest(t) + tl := rt.tl + tl.advance(retryTokenValidityPeriod) + tl.writeDatagram(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 1, + version: quicVersion1, + srcConnID: rt.originalSrcConnID, + dstConnID: rt.retry.srcConnID, + token: rt.retry.token, + frames: []debugFrame{ + debugFrameCrypto{ + data: rt.initialCrypto, + }, + }, + }}, + paddedSize: 1200, + }) + tc := tl.accept() + initial := tc.readPacket() + if initial == nil || initial.ptype != packetTypeInitial { + t.Fatalf("got packet:\n%v\nwant: Initial", initial) + } + handshake := tc.readPacket() + if handshake == nil || handshake.ptype != packetTypeHandshake { + t.Fatalf("got packet:\n%v\nwant: Handshake", initial) + } + if got, want := tc.sentTransportParameters.retrySrcConnID, rt.retry.srcConnID; !bytes.Equal(got, want) { + t.Errorf("retry_source_connection_id = {%x}, want {%x}", got, want) + } + if got, want := tc.sentTransportParameters.initialSrcConnID, initial.srcConnID; !bytes.Equal(got, want) { + t.Errorf("initial_source_connection_id = {%x}, want {%x}", got, want) + } + if got, want := tc.sentTransportParameters.originalDstConnID, rt.originalDstConnID; !bytes.Equal(got, want) { + t.Errorf("original_destination_connection_id = {%x}, want {%x}", got, want) + } +} + +func TestRetryServerTokenInvalid(t *testing.T) { + // "If a server receives a client Initial that contains an invalid Retry token [...] + // the server SHOULD immediately close [...] the connection with an + // INVALID_TOKEN error." + // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5 + rt := newRetryServerTest(t) + tl := rt.tl + tl.writeDatagram(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 1, + version: quicVersion1, + srcConnID: rt.originalSrcConnID, + dstConnID: rt.retry.srcConnID, + token: append(rt.retry.token, 0), + frames: []debugFrame{ + debugFrameCrypto{ + data: rt.initialCrypto, + }, + }, + }}, + paddedSize: 1200, + }) + tl.wantDatagram("server closes connection after Initial with invalid Retry token", + initialConnectionCloseDatagram( + rt.retry.srcConnID, + rt.originalSrcConnID, + errInvalidToken)) +} + +func TestRetryServerTokenTooOld(t *testing.T) { + // "[...] a token SHOULD have an expiration time [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.3-3 + rt := newRetryServerTest(t) + tl := rt.tl + tl.advance(retryTokenValidityPeriod + time.Second) + tl.writeDatagram(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 1, + version: quicVersion1, + srcConnID: rt.originalSrcConnID, + dstConnID: rt.retry.srcConnID, + token: rt.retry.token, + frames: []debugFrame{ + debugFrameCrypto{ + data: rt.initialCrypto, + }, + }, + }}, + paddedSize: 1200, + }) + tl.wantDatagram("server closes connection after Initial with expired token", + initialConnectionCloseDatagram( + rt.retry.srcConnID, + rt.originalSrcConnID, + errInvalidToken)) +} + +func TestRetryServerTokenWrongIP(t *testing.T) { + // "Tokens sent in Retry packets SHOULD include information that allows the server + // to verify that the source IP address and port in client packets remain constant." + // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.4-3 + rt := newRetryServerTest(t) + tl := rt.tl + tl.writeDatagram(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 1, + version: quicVersion1, + srcConnID: rt.originalSrcConnID, + dstConnID: rt.retry.srcConnID, + token: rt.retry.token, + frames: []debugFrame{ + debugFrameCrypto{ + data: rt.initialCrypto, + }, + }, + }}, + paddedSize: 1200, + addr: netip.MustParseAddrPort("10.0.0.2:8000"), + }) + tl.wantDatagram("server closes connection after Initial from wrong address", + initialConnectionCloseDatagram( + rt.retry.srcConnID, + rt.originalSrcConnID, + errInvalidToken)) +} + +func TestRetryServerIgnoresRetry(t *testing.T) { + tc := newTestConn(t, serverSide) + tc.handshake() + tc.write(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: testLocalConnID(-1), + srcConnID: testPeerConnID(0), + dstConnID: testLocalConnID(0), + token: []byte{1, 2, 3, 4}, + }}, + }) + // Send two packets, to trigger an immediate ACK. + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.wantFrameType("server connection ignores spurious Retry packet", + packetType1RTT, debugFrameAck{}) +} + +func TestRetryClientSuccess(t *testing.T) { + // "This token MUST be repeated by the client in all Initial packets it sends + // for that connection after it receives the Retry packet." + // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-1 + tc := newTestConn(t, clientSide) + tc.wantFrame("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }) + newServerConnID := []byte("new_conn_id") + token := []byte("token") + tc.write(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: testLocalConnID(-1), + srcConnID: newServerConnID, + dstConnID: testLocalConnID(0), + token: token, + }}, + }) + tc.wantPacket("client sends a new Initial packet with a token", + &testPacket{ + ptype: packetTypeInitial, + num: 1, + version: quicVersion1, + srcConnID: testLocalConnID(0), + dstConnID: newServerConnID, + token: token, + frames: []debugFrame{ + debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }, + }, + }, + ) + tc.advanceToTimer() + tc.wantPacket("after PTO client sends another Initial packet with a token", + &testPacket{ + ptype: packetTypeInitial, + num: 2, + version: quicVersion1, + srcConnID: testLocalConnID(0), + dstConnID: newServerConnID, + token: token, + frames: []debugFrame{ + debugFrameCrypto{ + data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial], + }, + }, + }, + ) +} + +func TestRetryClientInvalidServerTransportParameters(t *testing.T) { + // Various permutations of missing or invalid values for transport parameters + // after a Retry. + // https://www.rfc-editor.org/rfc/rfc9000#section-7.3 + initialSrcConnID := testPeerConnID(0) + originalDstConnID := testLocalConnID(-1) + retrySrcConnID := testPeerConnID(100) + for _, test := range []struct { + name string + f func(*transportParameters) + ok bool + }{{ + name: "valid", + f: func(p *transportParameters) {}, + ok: true, + }, { + name: "missing initial_source_connection_id", + f: func(p *transportParameters) { + p.initialSrcConnID = nil + }, + }, { + name: "invalid initial_source_connection_id", + f: func(p *transportParameters) { + p.initialSrcConnID = []byte("invalid") + }, + }, { + name: "missing original_destination_connection_id", + f: func(p *transportParameters) { + p.originalDstConnID = nil + }, + }, { + name: "invalid original_destination_connection_id", + f: func(p *transportParameters) { + p.originalDstConnID = []byte("invalid") + }, + }, { + name: "missing retry_source_connection_id", + f: func(p *transportParameters) { + p.retrySrcConnID = nil + }, + }, { + name: "invalid retry_source_connection_id", + f: func(p *transportParameters) { + p.retrySrcConnID = []byte("invalid") + }, + }} { + t.Run(test.name, func(t *testing.T) { + tc := newTestConn(t, clientSide, + func(p *transportParameters) { + p.initialSrcConnID = initialSrcConnID + p.originalDstConnID = originalDstConnID + p.retrySrcConnID = retrySrcConnID + }, + test.f) + tc.ignoreFrame(frameTypeAck) + tc.wantFrameType("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + tc.write(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: originalDstConnID, + srcConnID: retrySrcConnID, + dstConnID: testLocalConnID(0), + token: []byte{1, 2, 3, 4}, + }}, + }) + tc.wantFrameType("client resends Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + if test.ok { + tc.wantFrameType("valid params, client sends Handshake", + packetTypeHandshake, debugFrameCrypto{}) + } else { + tc.wantFrame("invalid transport parameters", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errTransportParameter, + }) + } + }) + } +} + +func TestRetryClientIgnoresRetryAfterReceivingPacket(t *testing.T) { + // "After the client has received and processed an Initial or Retry packet + // from the server, it MUST discard any subsequent Retry packets that it receives." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1 + tc := newTestConn(t, clientSide) + tc.ignoreFrame(frameTypeAck) + tc.ignoreFrame(frameTypeNewConnectionID) + tc.wantFrameType("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + retry := &testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: testLocalConnID(-1), + srcConnID: testPeerConnID(100), + dstConnID: testLocalConnID(0), + token: []byte{1, 2, 3, 4}, + }}, + } + tc.write(retry) + tc.wantIdle("client ignores Retry after receiving Initial packet") + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + tc.wantFrameType("client Handshake CRYPTO data", + packetTypeHandshake, debugFrameCrypto{}) + tc.write(retry) + tc.wantIdle("client ignores Retry after discarding Initial keys") +} + +func TestRetryClientIgnoresRetryAfterReceivingRetry(t *testing.T) { + // "After the client has received and processed an Initial or Retry packet + // from the server, it MUST discard any subsequent Retry packets that it receives." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1 + tc := newTestConn(t, clientSide) + tc.wantFrameType("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + retry := &testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: testLocalConnID(-1), + srcConnID: testPeerConnID(100), + dstConnID: testLocalConnID(0), + token: []byte{1, 2, 3, 4}, + }}, + } + tc.write(retry) + tc.wantFrameType("client resends Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + tc.write(retry) + tc.wantIdle("client ignores second Retry") +} + +func TestRetryClientIgnoresRetryWithInvalidIntegrityTag(t *testing.T) { + tc := newTestConn(t, clientSide) + tc.wantFrameType("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + pkt := encodeRetryPacket(testLocalConnID(-1), retryPacket{ + srcConnID: testPeerConnID(100), + dstConnID: testLocalConnID(0), + token: []byte{1, 2, 3, 4}, + }) + pkt[len(pkt)-1] ^= 1 // invalidate the integrity tag + tc.listener.write(&datagram{ + b: pkt, + addr: testClientAddr, + }) + tc.wantIdle("client ignores Retry with invalid integrity tag") +} + +func TestRetryClientIgnoresRetryWithZeroLengthToken(t *testing.T) { + // "A client MUST discard a Retry packet with a zero-length Retry Token field." + // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2 + tc := newTestConn(t, clientSide) + tc.wantFrameType("client Initial CRYPTO data", + packetTypeInitial, debugFrameCrypto{}) + tc.write(&testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeRetry, + originalDstConnID: testLocalConnID(-1), + srcConnID: testPeerConnID(100), + dstConnID: testLocalConnID(0), + token: []byte{}, + }}, + }) + tc.wantIdle("client ignores Retry with zero-length token") +} + +func TestRetryStateValidateInvalidToken(t *testing.T) { + // Test handling of tokens that may have a valid signature, + // but unexpected contents. + var rs retryState + if err := rs.init(); err != nil { + t.Fatal(err) + } + nonce := make([]byte, rs.aead.NonceSize()) + now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + srcConnID := []byte{1, 2, 3, 4} + dstConnID := nonce[:20] + addr := testClientAddr + + for _, test := range []struct { + name string + token []byte + }{{ + name: "token too short", + token: []byte{1, 2, 3}, + }, { + name: "token plaintext too short", + token: func() []byte { + plaintext := make([]byte, 7) // not enough bytes of content + token := append([]byte{}, nonce[20:]...) + return rs.aead.Seal(token, nonce, plaintext, rs.additionalData(srcConnID, addr)) + }(), + }} { + t.Run(test.name, func(t *testing.T) { + if _, ok := rs.validateToken(now, test.token, srcConnID, dstConnID, addr); ok { + t.Errorf("validateToken succeeded, want failure") + } + }) + } +} + +func TestParseInvalidRetryPackets(t *testing.T) { + originalDstConnID := []byte{1, 2, 3, 4} + goodPkt := encodeRetryPacket(originalDstConnID, retryPacket{ + dstConnID: []byte{1}, + srcConnID: []byte{2}, + token: []byte{3}, + }) + for _, test := range []struct { + name string + pkt []byte + }{{ + name: "packet too short", + pkt: goodPkt[:len(goodPkt)-4], + }, { + name: "packet header invalid", + pkt: goodPkt[:5], + }, { + name: "integrity tag invalid", + pkt: func() []byte { + pkt := cloneBytes(goodPkt) + pkt[len(pkt)-1] ^= 1 + return pkt + }(), + }} { + t.Run(test.name, func(t *testing.T) { + if _, ok := parseRetryPacket(test.pkt, originalDstConnID); ok { + t.Errorf("parseRetryPacket succeded, want failure") + } + }) + } +} + +func initialClientCrypto(t *testing.T, l *testListener, p transportParameters) []byte { + t.Helper() + config := &tls.QUICConfig{TLSConfig: newTestTLSConfig(clientSide)} + tlsClient := tls.QUICClient(config) + tlsClient.SetTransportParameters(marshalTransportParameters(p)) + tlsClient.Start(context.Background()) + //defer tlsClient.Close() + l.peerTLSConn = tlsClient + var data []byte + for { + e := tlsClient.NextEvent() + switch e.Kind { + case tls.QUICNoEvent: + return data + case tls.QUICWriteData: + if e.Level != tls.QUICEncryptionLevelInitial { + t.Fatal("initial data at unexpected level") + } + data = append(data, e.Data...) + } + } +} + +func initialConnectionCloseDatagram(srcConnID, dstConnID []byte, code transportError) *testDatagram { + return &testDatagram{ + packets: []*testPacket{{ + ptype: packetTypeInitial, + num: 0, + version: quicVersion1, + srcConnID: srcConnID, + dstConnID: dstConnID, + frames: []debugFrame{ + debugFrameConnectionCloseTransport{ + code: code, + }, + }, + }}, + } +} From 4c7a5b64f145becd6fbd7d22f7ad4e9b891ce43a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 25 Oct 2023 09:49:26 -0700 Subject: [PATCH 082/168] http2: add test for push promise accounting underflow Verify that repeated requests resulting in a PUSH_PROMISE result all complete successfully, validating the fix in CL 535595. For golang/go#63511 Change-Id: I6bebdcfcecb6c53f076e4ac6873d61a150d1040e Reviewed-on: https://go-review.googlesource.com/c/net/+/537715 Auto-Submit: Damien Neil Reviewed-by: Dmitri Shuralyov Reviewed-by: Dmitri Shuralyov Reviewed-by: Mauri de Souza Meneguzzo LUCI-TryBot-Result: Go LUCI --- http2/server_push_test.go | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/http2/server_push_test.go b/http2/server_push_test.go index 6e57de0b7c..9882d9ef71 100644 --- a/http2/server_push_test.go +++ b/http2/server_push_test.go @@ -517,3 +517,55 @@ func TestServer_Push_RejectAfterGoAway(t *testing.T) { t.Error(err) } } + +func TestServer_Push_Underflow(t *testing.T) { + // Test for #63511: Send several requests which generate PUSH_PROMISE responses, + // verify they all complete successfully. + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { + switch r.URL.RequestURI() { + case "/": + opt := &http.PushOptions{ + Header: http.Header{"User-Agent": {"testagent"}}, + } + if err := w.(http.Pusher).Push("/pushed", opt); err != nil { + t.Errorf("error pushing: %v", err) + } + w.WriteHeader(200) + case "/pushed": + r.Header.Set("User-Agent", "newagent") + r.Header.Set("Cookie", "cookie") + w.WriteHeader(200) + default: + t.Errorf("unknown RequestURL %q", r.URL.RequestURI()) + } + }) + // Send several requests. + st.greet() + const numRequests = 4 + for i := 0; i < numRequests; i++ { + st.writeHeaders(HeadersFrameParam{ + StreamID: uint32(1 + i*2), // clients send odd numbers + BlockFragment: st.encodeHeader(), + EndStream: true, + EndHeaders: true, + }) + } + // Each request should result in one PUSH_PROMISE and two responses. + numPushPromises := 0 + numHeaders := 0 + for numHeaders < numRequests*2 || numPushPromises < numRequests { + f, err := st.readFrame() + if err != nil { + st.t.Fatal(err) + } + switch f := f.(type) { + case *HeadersFrame: + if !f.Flags.Has(FlagHeadersEndStream) { + t.Fatalf("got HEADERS frame with no END_STREAM, expected END_STREAM: %v", f) + } + numHeaders++ + case *PushPromiseFrame: + numPushPromises++ + } + } +} From 642f15ebba2e78391db8e6cccf2bc868cb6fc771 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 18 Oct 2023 13:33:05 -0400 Subject: [PATCH 083/168] quic: support stateless reset Add a StatelessResetKey config field to permit generating consistent stateless reset tokens across server restarts. Set the stateless_reset_token transport parameter and populate the Token field in NEW_CONNECTION_ID frames. Detect reset tokens in datagrams which cannot be associated with a connection or cannot be parsed. RFC 9000, Section 10.3. For golang/go#58547 Change-Id: Idb52ba07092ab5c08b323d6b531964a7e7e5ecea Reviewed-on: https://go-review.googlesource.com/c/net/+/536315 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam Auto-Submit: Damien Neil --- internal/quic/config.go | 17 ++ internal/quic/conn.go | 4 +- internal/quic/conn_close_test.go | 4 +- internal/quic/conn_id.go | 117 ++++++++--- internal/quic/conn_id_test.go | 98 ++++++++- internal/quic/conn_loss_test.go | 4 + internal/quic/conn_recv.go | 27 ++- internal/quic/conn_send.go | 2 +- internal/quic/conn_test.go | 10 +- internal/quic/frame_debug.go | 2 +- internal/quic/listener.go | 199 ++++++++++++------ internal/quic/packet_parser.go | 14 +- internal/quic/stateless_reset.go | 61 ++++++ internal/quic/stateless_reset_test.go | 277 ++++++++++++++++++++++++++ internal/quic/tls_test.go | 22 +- 15 files changed, 740 insertions(+), 118 deletions(-) create mode 100644 internal/quic/stateless_reset.go create mode 100644 internal/quic/stateless_reset_test.go diff --git a/internal/quic/config.go b/internal/quic/config.go index 99ef68fead..6278bf89c1 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -55,6 +55,23 @@ type Config struct { // source address information can cause a server to perform, // at the cost of increased handshake latency. RequireAddressValidation bool + + // StatelessResetKey is used to provide stateless reset of connections. + // A restart may leave an endpoint without access to the state of + // existing connections. Stateless reset permits an endpoint to respond + // to a packet for a connection it does not recognize. + // + // This field should be filled with random bytes. + // The contents should remain stable across restarts, + // to permit an endpoint to send a reset for + // connections created before a restart. + // + // The contents of the StatelessResetKey should not be exposed. + // An attacker can use knowledge of this field's value to + // reset existing connections. + // + // If this field is left as zero, stateless reset is disabled. + StatelessResetKey [32]byte } func configDefault(v, def, limit int64) int64 { diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 4acf5ddfe6..b3d6feabc7 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -203,7 +203,7 @@ func (c *Conn) discardKeys(now time.Time, space numberSpace) { // receiveTransportParameters applies transport parameters sent by the peer. func (c *Conn) receiveTransportParameters(p transportParameters) error { isRetry := c.retryToken != nil - if err := c.connIDState.validateTransportParameters(c.side, isRetry, p); err != nil { + if err := c.connIDState.validateTransportParameters(c, isRetry, p); err != nil { return err } c.streams.outflow.setMaxData(p.initialMaxData) @@ -224,7 +224,7 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error { resetToken [16]byte ) copy(resetToken[:], p.preferredAddrResetToken) - if err := c.connIDState.handleNewConnID(seq, retirePriorTo, p.preferredAddrConnID, resetToken); err != nil { + if err := c.connIDState.handleNewConnID(c, seq, retirePriorTo, p.preferredAddrConnID, resetToken); err != nil { return err } } diff --git a/internal/quic/conn_close_test.go b/internal/quic/conn_close_test.go index 20c00e754c..d5c3499e4a 100644 --- a/internal/quic/conn_close_test.go +++ b/internal/quic/conn_close_test.go @@ -15,7 +15,9 @@ import ( ) func TestConnCloseResponseBackoff(t *testing.T) { - tc := newTestConn(t, clientSide) + tc := newTestConn(t, clientSide, func(c *Config) { + clear(c.StatelessResetKey[:]) + }) tc.handshake() tc.conn.Abort(nil) diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index ff7e2d1c6e..c236137594 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -22,7 +22,7 @@ type connIDState struct { // // These are []connID rather than []*connID to minimize allocations. local []connID - remote []connID + remote []remoteConnID nextLocalSeq int64 retireRemotePriorTo int64 // largest Retire Prior To value sent by the peer @@ -58,6 +58,12 @@ type connID struct { send sentVal } +// A remoteConnID is a connection ID and stateless reset token. +type remoteConnID struct { + connID + resetToken statelessResetToken +} + func (s *connIDState) initClient(c *Conn) error { // Client chooses its initial connection ID, and sends it // in the Source Connection ID field of the first Initial packet. @@ -70,6 +76,9 @@ func (s *connIDState) initClient(c *Conn) error { cid: locid, }) s.nextLocalSeq = 1 + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.addConnID(c, locid) + }) // Client chooses an initial, transient connection ID for the server, // and sends it in the Destination Connection ID field of the first Initial packet. @@ -77,13 +86,13 @@ func (s *connIDState) initClient(c *Conn) error { if err != nil { return err } - s.remote = append(s.remote, connID{ - seq: -1, - cid: remid, + s.remote = append(s.remote, remoteConnID{ + connID: connID{ + seq: -1, + cid: remid, + }, }) s.originalDstConnID = remid - const retired = false - c.listener.connIDsChanged(c, retired, s.local[:]) return nil } @@ -107,8 +116,10 @@ func (s *connIDState) initServer(c *Conn, dstConnID []byte) error { cid: locid, }) s.nextLocalSeq = 1 - const retired = false - c.listener.connIDsChanged(c, retired, s.local[:]) + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.addConnID(c, dstConnID) + conns.addConnID(c, locid) + }) return nil } @@ -131,6 +142,19 @@ func (s *connIDState) dstConnID() (cid []byte, ok bool) { return nil, false } +// isValidStatelessResetToken reports whether the given reset token is +// associated with a non-retired connection ID which we have used. +func (s *connIDState) isValidStatelessResetToken(resetToken statelessResetToken) bool { + for i := range s.remote { + // We currently only use the first available remote connection ID, + // so any other reset token is not valid. + if !s.remote[i].retired { + return s.remote[i].resetToken == resetToken + } + } + return false +} + // setPeerActiveConnIDLimit sets the active_connection_id_limit // transport parameter received from the peer. func (s *connIDState) setPeerActiveConnIDLimit(c *Conn, lim int64) error { @@ -145,12 +169,13 @@ func (s *connIDState) issueLocalIDs(c *Conn) error { toIssue-- } } - prev := len(s.local) + var newIDs [][]byte for toIssue > 0 { cid, err := c.newConnID(s.nextLocalSeq) if err != nil { return err } + newIDs = append(newIDs, cid) s.local = append(s.local, connID{ seq: s.nextLocalSeq, cid: cid, @@ -160,14 +185,17 @@ func (s *connIDState) issueLocalIDs(c *Conn) error { s.needSend = true toIssue-- } - const retired = false - c.listener.connIDsChanged(c, retired, s.local[prev:]) + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + for _, cid := range newIDs { + conns.addConnID(c, cid) + } + }) return nil } // validateTransportParameters verifies the original_destination_connection_id and // initial_source_connection_id transport parameters match the expected values. -func (s *connIDState) validateTransportParameters(side connSide, isRetry bool, p transportParameters) error { +func (s *connIDState) validateTransportParameters(c *Conn, isRetry bool, p transportParameters) error { // TODO: Consider returning more detailed errors, for debugging. // Verify original_destination_connection_id matches // the transient remote connection ID we chose (client) @@ -189,6 +217,16 @@ func (s *connIDState) validateTransportParameters(side connSide, isRetry bool, p if !bytes.Equal(p.initialSrcConnID, s.remote[0].cid) { return localTransportError(errTransportParameter) } + if len(p.statelessResetToken) > 0 { + if c.side == serverSide { + return localTransportError(errTransportParameter) + } + token := statelessResetToken(p.statelessResetToken) + s.remote[0].resetToken = token + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.addResetToken(c, token) + }) + } return nil } @@ -201,18 +239,22 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) // We're a client connection processing the first Initial packet // from the server. Replace the transient remote connection ID // with the Source Connection ID from the packet. - s.remote[0] = connID{ - seq: 0, - cid: cloneBytes(srcConnID), + s.remote[0] = remoteConnID{ + connID: connID{ + seq: 0, + cid: cloneBytes(srcConnID), + }, } } case ptype == packetTypeInitial && c.side == serverSide: if len(s.remote) == 0 { // We're a server connection processing the first Initial packet // from the client. Set the client's connection ID. - s.remote = append(s.remote, connID{ - seq: 0, - cid: cloneBytes(srcConnID), + s.remote = append(s.remote, remoteConnID{ + connID: connID{ + seq: 0, + cid: cloneBytes(srcConnID), + }, }) } case ptype == packetTypeHandshake && c.side == serverSide: @@ -220,8 +262,10 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) // We're a server connection processing the first Handshake packet from // the client. Discard the transient, client-chosen connection ID used // for Initial packets; the client will never send it again. - const retired = true - c.listener.connIDsChanged(c, retired, s.local[0:1]) + cid := s.local[0].cid + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.retireConnID(c, cid) + }) s.local = append(s.local[:0], s.local[1:]...) } } @@ -235,7 +279,7 @@ func (s *connIDState) handleRetryPacket(srcConnID []byte) { s.remote[0].cid = s.retrySrcConnID } -func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken [16]byte) error { +func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, resetToken statelessResetToken) error { if len(s.remote[0].cid) == 0 { // "An endpoint that is sending packets with a zero-length // Destination Connection ID MUST treat receipt of a NEW_CONNECTION_ID @@ -254,6 +298,9 @@ func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken rcid := &s.remote[i] if !rcid.retired && rcid.seq >= 0 && rcid.seq < s.retireRemotePriorTo { s.retireRemote(rcid) + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.retireResetToken(c, rcid.resetToken) + }) } if !rcid.retired { active++ @@ -272,15 +319,21 @@ func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken // We could take steps to keep the list of remote connection IDs // sorted by sequence number, but there's no particular need // so we don't bother. - s.remote = append(s.remote, connID{ - seq: seq, - cid: cloneBytes(cid), + s.remote = append(s.remote, remoteConnID{ + connID: connID{ + seq: seq, + cid: cloneBytes(cid), + }, + resetToken: resetToken, }) if seq < s.retireRemotePriorTo { // This ID was already retired by a previous NEW_CONNECTION_ID frame. s.retireRemote(&s.remote[len(s.remote)-1]) } else { active++ + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.addResetToken(c, resetToken) + }) } } @@ -305,7 +358,7 @@ func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken } // retireRemote marks a remote connection ID as retired. -func (s *connIDState) retireRemote(rcid *connID) { +func (s *connIDState) retireRemote(rcid *remoteConnID) { rcid.retired = true rcid.send.setUnsent() s.needSend = true @@ -317,8 +370,10 @@ func (s *connIDState) handleRetireConnID(c *Conn, seq int64) error { } for i := range s.local { if s.local[i].seq == seq { - const retired = true - c.listener.connIDsChanged(c, retired, s.local[i:i+1]) + cid := s.local[i].cid + c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + conns.retireConnID(c, cid) + }) s.local = append(s.local[:i], s.local[i+1:]...) break } @@ -363,7 +418,7 @@ func (s *connIDState) ackOrLossRetireConnectionID(pnum packetNumber, seq int64, // // It returns true if no more frames need appending, // false if not everything fit in the current packet. -func (s *connIDState) appendFrames(w *packetWriter, pnum packetNumber, pto bool) bool { +func (s *connIDState) appendFrames(c *Conn, pnum packetNumber, pto bool) bool { if !s.needSend && !pto { // Fast path: We don't need to send anything. return true @@ -376,11 +431,11 @@ func (s *connIDState) appendFrames(w *packetWriter, pnum packetNumber, pto bool) if !s.local[i].send.shouldSendPTO(pto) { continue } - if !w.appendNewConnectionIDFrame( + if !c.w.appendNewConnectionIDFrame( s.local[i].seq, retireBefore, s.local[i].cid, - [16]byte{}, // TODO: stateless reset token + c.listener.resetGen.tokenForConnID(s.local[i].cid), ) { return false } @@ -390,7 +445,7 @@ func (s *connIDState) appendFrames(w *packetWriter, pnum packetNumber, pto bool) if !s.remote[i].send.shouldSendPTO(pto) { continue } - if !w.appendRetireConnectionIDFrame(s.remote[i].seq) { + if !c.w.appendRetireConnectionIDFrame(s.remote[i].seq) { return false } s.remote[i].send.setSent(pnum) diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index 784c5e2c48..63feec992e 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -47,12 +47,14 @@ func TestConnIDClientHandshake(t *testing.T) { if got := tc.conn.connIDState.local; !connIDListEqual(got, wantLocal) { t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } - wantRemote := []connID{{ - cid: testPeerConnID(0), - seq: 0, + wantRemote := []remoteConnID{{ + connID: connID{ + cid: testPeerConnID(0), + seq: 0, + }, }} - if got := tc.conn.connIDState.remote; !connIDListEqual(got, wantRemote) { - t.Errorf("remote ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantRemote)) + if got := tc.conn.connIDState.remote; !remoteConnIDListEqual(got, wantRemote) { + t.Errorf("remote ids: %v, want %v", fmtRemoteConnIDList(got), fmtRemoteConnIDList(wantRemote)) } } @@ -93,12 +95,14 @@ func TestConnIDServerHandshake(t *testing.T) { if got := tc.conn.connIDState.local; !connIDListEqual(got, wantLocal) { t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal)) } - wantRemote := []connID{{ - cid: testPeerConnID(0), - seq: 0, + wantRemote := []remoteConnID{{ + connID: connID{ + cid: testPeerConnID(0), + seq: 0, + }, }} - if got := tc.conn.connIDState.remote; !connIDListEqual(got, wantRemote) { - t.Errorf("remote ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantRemote)) + if got := tc.conn.connIDState.remote; !remoteConnIDListEqual(got, wantRemote) { + t.Errorf("remote ids: %v, want %v", fmtRemoteConnIDList(got), fmtRemoteConnIDList(wantRemote)) } // The client's first Handshake packet permits the server to discard the @@ -134,6 +138,24 @@ func connIDListEqual(a, b []connID) bool { return true } +func remoteConnIDListEqual(a, b []remoteConnID) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i].seq != b[i].seq { + return false + } + if !bytes.Equal(a[i].cid, b[i].cid) { + return false + } + if a[i].resetToken != b[i].resetToken { + return false + } + } + return true +} + func fmtConnIDList(s []connID) string { var strs []string for _, cid := range s { @@ -142,6 +164,14 @@ func fmtConnIDList(s []connID) string { return "{" + strings.Join(strs, " ") + "}" } +func fmtRemoteConnIDList(s []remoteConnID) string { + var strs []string + for _, cid := range s { + strs = append(strs, fmt.Sprintf("[seq:%v cid:{%x} token:{%x}]", cid.seq, cid.cid, cid.resetToken)) + } + return "{" + strings.Join(strs, " ") + "}" +} + func TestNewRandomConnID(t *testing.T) { cid, err := newRandomConnID(0) if len(cid) != connIDLen || err != nil { @@ -174,16 +204,19 @@ func TestConnIDPeerRequestsManyIDs(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testLocalConnID(1), + token: testLocalStatelessResetToken(1), }) tc.wantFrame("provide additional connection ID 2", packetType1RTT, debugFrameNewConnectionID{ seq: 2, connID: testLocalConnID(2), + token: testLocalStatelessResetToken(2), }) tc.wantFrame("provide additional connection ID 3", packetType1RTT, debugFrameNewConnectionID{ seq: 3, connID: testLocalConnID(3), + token: testLocalStatelessResetToken(3), }) tc.wantIdle("connection ID limit reached, no more to provide") } @@ -255,6 +288,7 @@ func TestConnIDPeerRetiresConnID(t *testing.T) { seq: 2, retirePriorTo: 1, connID: testLocalConnID(2), + token: testLocalStatelessResetToken(2), }) }) } @@ -455,6 +489,7 @@ func TestConnIDRepeatedRetireConnectionIDFrame(t *testing.T) { retirePriorTo: 1, seq: 2, connID: testLocalConnID(2), + token: testLocalStatelessResetToken(2), }) tc.wantIdle("repeated RETIRE_CONNECTION_ID frames are not an error") } @@ -583,3 +618,46 @@ func TestConnIDInitialSrcConnIDMismatch(t *testing.T) { }) }) } + +func TestConnIDsCleanedUpAfterClose(t *testing.T) { + testSides(t, "", func(t *testing.T, side connSide) { + tc := newTestConn(t, side, func(p *transportParameters) { + if side == clientSide { + token := testPeerStatelessResetToken(0) + p.statelessResetToken = token[:] + } + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 1, + connID: testPeerConnID(2), + token: testPeerStatelessResetToken(0), + }) + tc.wantFrame("peer asked for conn id 0 to be retired", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{}) + tc.conn.Abort(nil) + tc.wantFrame("CONN_CLOSE sent after user closes connection", + packetType1RTT, debugFrameConnectionCloseTransport{}) + + // Wait for the conn to drain. + // Then wait for the conn loop to exit, + // and force an immediate sync of the connsMap updates + // (normally only done by the listener read loop). + tc.advanceToTimer() + <-tc.conn.donec + tc.listener.l.connsMap.applyUpdates() + + if got := len(tc.listener.l.connsMap.byConnID); got != 0 { + t.Errorf("%v conn ids in listener map after closing, want 0", got) + } + if got := len(tc.listener.l.connsMap.byResetToken); got != 0 { + t.Errorf("%v reset tokens in listener map after closing, want 0", got) + } + }) +} diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index 9b88462518..5144be6acc 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -160,6 +160,7 @@ func TestLostCryptoFrame(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testLocalConnID(1), + token: testLocalStatelessResetToken(1), }) tc.triggerLossOrPTO(packetTypeHandshake, pto) tc.wantFrame("client resends Handshake CRYPTO frame", @@ -607,6 +608,7 @@ func TestLostNewConnectionIDFrame(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 2, connID: testLocalConnID(2), + token: testLocalStatelessResetToken(2), }) tc.triggerLossOrPTO(packetType1RTT, pto) @@ -614,6 +616,7 @@ func TestLostNewConnectionIDFrame(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 2, connID: testLocalConnID(2), + token: testLocalStatelessResetToken(2), }) }) } @@ -669,6 +672,7 @@ func TestLostHandshakeDoneFrame(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testLocalConnID(1), + token: testLocalStatelessResetToken(1), }) tc.writeFrames(packetTypeHandshake, debugFrameCrypto{ diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index e789ae0455..183316780e 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -41,9 +41,23 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { c.handleVersionNegotiation(now, buf) return default: - return + n = -1 } if n <= 0 { + // We don't expect to get a stateless reset with a valid + // destination connection ID, since the sender of a stateless + // reset doesn't know what the connection ID is. + // + // We're required to perform this check anyway. + // + // "[...] the comparison MUST be performed when the first packet + // in an incoming datagram [...] cannot be decrypted." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-2 + if len(buf) == len(dgram.b) && len(buf) > statelessResetTokenLen { + var token statelessResetToken + copy(token[:], buf[len(buf)-len(token):]) + c.handleStatelessReset(token) + } // Invalid data at the end of a datagram is ignored. break } @@ -468,7 +482,7 @@ func (c *Conn) handleNewConnectionIDFrame(now time.Time, space numberSpace, payl if n < 0 { return -1 } - if err := c.connIDState.handleNewConnID(seq, retire, connID, resetToken); err != nil { + if err := c.connIDState.handleNewConnID(c, seq, retire, connID, resetToken); err != nil { c.abort(now, err) } return n @@ -515,3 +529,12 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa } return 1 } + +var errStatelessReset = errors.New("received stateless reset") + +func (c *Conn) handleStatelessReset(resetToken statelessResetToken) { + if !c.connIDState.isValidStatelessResetToken(resetToken) { + return + } + c.enterDraining(errStatelessReset) +} diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index efeb04fe39..f512518efa 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -250,7 +250,7 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, } // NEW_CONNECTION_ID, RETIRE_CONNECTION_ID - if !c.connIDState.appendFrames(&c.w, pnum, pto) { + if !c.connIDState.appendFrames(c, pnum, pto) { return } diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index cfb0d062c4..df28907f44 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -190,7 +190,8 @@ type keySecret struct { func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { t.Helper() config := &Config{ - TLSConfig: newTestTLSConfig(side), + TLSConfig: newTestTLSConfig(side), + StatelessResetKey: testStatelessResetKey, } var configTransportParams []func(*transportParameters) for _, o := range opts { @@ -1041,6 +1042,13 @@ func testPeerConnID(seq int64) []byte { return []byte{0xbe, 0xee, 0xff, byte(seq)} } +func testPeerStatelessResetToken(seq int64) statelessResetToken { + return statelessResetToken{ + 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, + 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, byte(seq), + } +} + // canceledContext returns a canceled Context. // // Functions which take a context preference progress over cancelation. diff --git a/internal/quic/frame_debug.go b/internal/quic/frame_debug.go index 7a5aee57b1..dc80090375 100644 --- a/internal/quic/frame_debug.go +++ b/internal/quic/frame_debug.go @@ -368,7 +368,7 @@ type debugFrameNewConnectionID struct { seq int64 retirePriorTo int64 connID []byte - token [16]byte + token statelessResetToken } func parseDebugFrameNewConnectionID(b []byte) (f debugFrameNewConnectionID, n int) { diff --git a/internal/quic/listener.go b/internal/quic/listener.go index aa258395e9..668d270b39 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -8,6 +8,7 @@ package quic import ( "context" + "crypto/rand" "errors" "net" "net/netip" @@ -24,21 +25,16 @@ type Listener struct { config *Config udpConn udpConn testHooks listenerTestHooks + resetGen statelessResetTokenGenerator retry retryState acceptQueue queue[*Conn] // new inbound connections + connsMap connsMap // only accessed by the listen loop connsMu sync.Mutex conns map[*Conn]struct{} closing bool // set when Close is called closec chan struct{} // closed when the listen loop exits - - // The datagram receive loop keeps a mapping of connection IDs to conns. - // When a conn's connection IDs change, we add it to connIDUpdates and set - // connIDUpdateNeeded, and the receive loop updates its map. - connIDUpdateMu sync.Mutex - connIDUpdateNeeded atomic.Bool - connIDUpdates []connIDUpdate } type listenerTestHooks interface { @@ -55,12 +51,6 @@ type udpConn interface { WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) } -type connIDUpdate struct { - conn *Conn - retired bool - cid []byte -} - // Listen listens on a local network address. // The configuration config must be non-nil. func Listen(network, address string, config *Config) (*Listener, error) { @@ -87,6 +77,8 @@ func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) (*Lis acceptQueue: newQueue[*Conn](), closec: make(chan struct{}), } + l.resetGen.init(config.StatelessResetKey) + l.connsMap.init() if config.RequireAddressValidation { if err := l.retry.init(); err != nil { return nil, err @@ -181,6 +173,22 @@ func (l *Listener) serverConnEstablished(c *Conn) { // connDrained is called by a conn when it leaves the draining state, // either when the peer acknowledges connection closure or the drain timeout expires. func (l *Listener) connDrained(c *Conn) { + var cids [][]byte + for i := range c.connIDState.local { + cids = append(cids, c.connIDState.local[i].cid) + } + var tokens []statelessResetToken + for i := range c.connIDState.remote { + tokens = append(tokens, c.connIDState.remote[i].resetToken) + } + l.connsMap.updateConnIDs(func(conns *connsMap) { + for _, cid := range cids { + conns.retireConnID(c, cid) + } + for _, token := range tokens { + conns.retireResetToken(c, token) + } + }) l.connsMu.Lock() defer l.connsMu.Unlock() delete(l.conns, c) @@ -189,39 +197,8 @@ func (l *Listener) connDrained(c *Conn) { } } -// connIDsChanged is called by a conn when its connection IDs change. -func (l *Listener) connIDsChanged(c *Conn, retired bool, cids []connID) { - l.connIDUpdateMu.Lock() - defer l.connIDUpdateMu.Unlock() - for _, cid := range cids { - l.connIDUpdates = append(l.connIDUpdates, connIDUpdate{ - conn: c, - retired: retired, - cid: cid.cid, - }) - } - l.connIDUpdateNeeded.Store(true) -} - -// updateConnIDs is called by the datagram receive loop to update its connection ID map. -func (l *Listener) updateConnIDs(conns map[string]*Conn) { - l.connIDUpdateMu.Lock() - defer l.connIDUpdateMu.Unlock() - for i, u := range l.connIDUpdates { - if u.retired { - delete(conns, string(u.cid)) - } else { - conns[string(u.cid)] = u.conn - } - l.connIDUpdates[i] = connIDUpdate{} // drop refs - } - l.connIDUpdates = l.connIDUpdates[:0] - l.connIDUpdateNeeded.Store(false) -} - func (l *Listener) listen() { defer close(l.closec) - conns := map[string]*Conn{} for { m := newDatagram() // TODO: Read and process the ECN (explicit congestion notification) field. @@ -237,22 +214,22 @@ func (l *Listener) listen() { if n == 0 { continue } - if l.connIDUpdateNeeded.Load() { - l.updateConnIDs(conns) + if l.connsMap.updateNeeded.Load() { + l.connsMap.applyUpdates() } m.addr = addr m.b = m.b[:n] - l.handleDatagram(m, conns) + l.handleDatagram(m) } } -func (l *Listener) handleDatagram(m *datagram, conns map[string]*Conn) { +func (l *Listener) handleDatagram(m *datagram) { dstConnID, ok := dstConnIDForDatagram(m.b) if !ok { m.recycle() return } - c := conns[string(dstConnID)] + c := l.connsMap.byConnID[string(dstConnID)] if c == nil { // TODO: Move this branch into a separate goroutine to avoid blocking // the listener while processing packets. @@ -271,18 +248,29 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { m.recycle() } }() - if len(m.b) < minimumClientInitialDatagramSize { + const minimumValidPacketSize = 21 + if len(m.b) < minimumValidPacketSize { + return + } + // Check to see if this is a stateless reset. + var token statelessResetToken + copy(token[:], m.b[len(m.b)-len(token):]) + if c := l.connsMap.byResetToken[token]; c != nil { + c.sendMsg(func(now time.Time, c *Conn) { + c.handleStatelessReset(token) + }) + return + } + // If this is a 1-RTT packet, there's nothing productive we can do with it. + // Send a stateless reset if possible. + if !isLongHeader(m.b[0]) { + l.maybeSendStatelessReset(m.b, m.addr) return } p, ok := parseGenericLongHeaderPacket(m.b) - if !ok { - // Not a long header packet, or not parseable. - // Short header (1-RTT) packets don't contain enough information - // to do anything useful with if we don't recognize the - // connection ID. + if !ok || len(m.b) < minimumClientInitialDatagramSize { return } - switch p.version { case quicVersion1: case 0: @@ -296,8 +284,9 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { if getPacketType(m.b) != packetTypeInitial { // This packet isn't trying to create a new connection. // It might be associated with some connection we've lost state for. - // TODO: Send a stateless reset when appropriate. - // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.3 + // We are technically permitted to send a stateless reset for + // a long-header packet, but this isn't generally useful. See: + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-16 return } var now time.Time @@ -330,6 +319,50 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { m = nil // don't recycle, sendMsg takes ownership } +func (l *Listener) maybeSendStatelessReset(b []byte, addr netip.AddrPort) { + if !l.resetGen.canReset { + // Config.StatelessResetKey isn't set, so we don't send stateless resets. + return + } + // The smallest possible valid packet a peer can send us is: + // 1 byte of header + // connIDLen bytes of destination connection ID + // 1 byte of packet number + // 1 byte of payload + // 16 bytes AEAD expansion + if len(b) < 1+connIDLen+1+1+16 { + return + } + // TODO: Rate limit stateless resets. + cid := b[1:][:connIDLen] + token := l.resetGen.tokenForConnID(cid) + // We want to generate a stateless reset that is as short as possible, + // but long enough to be difficult to distinguish from a 1-RTT packet. + // + // The minimal 1-RTT packet is: + // 1 byte of header + // 0-20 bytes of destination connection ID + // 1-4 bytes of packet number + // 1 byte of payload + // 16 bytes AEAD expansion + // + // Assuming the maximum possible connection ID and packet number size, + // this gives 1 + 20 + 4 + 1 + 16 = 42 bytes. + // + // We also must generate a stateless reset that is shorter than the datagram + // we are responding to, in order to ensure that reset loops terminate. + // + // See: https://www.rfc-editor.org/rfc/rfc9000#section-10.3 + size := min(len(b)-1, 42) + // Reuse the input buffer for generating the stateless reset. + b = b[:size] + rand.Read(b[:len(b)-statelessResetTokenLen]) + b[0] &^= headerFormLong // clear long header bit + b[0] |= fixedBit // set fixed bit + copy(b[len(b)-statelessResetTokenLen:], token[:]) + l.sendDatagram(b, addr) +} + func (l *Listener) sendVersionNegotiation(p genericLongPacket, addr netip.AddrPort) { m := newDatagram() m.b = appendVersionNegotiation(m.b[:0], p.srcConnID, p.dstConnID, quicVersion1) @@ -363,3 +396,53 @@ func (l *Listener) sendDatagram(p []byte, addr netip.AddrPort) error { _, err := l.udpConn.WriteToUDPAddrPort(p, addr) return err } + +// A connsMap is a listener's mapping of conn ids and reset tokens to conns. +type connsMap struct { + byConnID map[string]*Conn + byResetToken map[statelessResetToken]*Conn + + updateMu sync.Mutex + updateNeeded atomic.Bool + updates []func(*connsMap) +} + +func (m *connsMap) init() { + m.byConnID = map[string]*Conn{} + m.byResetToken = map[statelessResetToken]*Conn{} +} + +func (m *connsMap) addConnID(c *Conn, cid []byte) { + m.byConnID[string(cid)] = c +} + +func (m *connsMap) retireConnID(c *Conn, cid []byte) { + delete(m.byConnID, string(cid)) +} + +func (m *connsMap) addResetToken(c *Conn, token statelessResetToken) { + m.byResetToken[token] = c +} + +func (m *connsMap) retireResetToken(c *Conn, token statelessResetToken) { + delete(m.byResetToken, token) +} + +func (m *connsMap) updateConnIDs(f func(*connsMap)) { + m.updateMu.Lock() + defer m.updateMu.Unlock() + m.updates = append(m.updates, f) + m.updateNeeded.Store(true) +} + +// applyConnIDUpdates is called by the datagram receive loop to update its connection ID map. +func (m *connsMap) applyUpdates() { + m.updateMu.Lock() + defer m.updateMu.Unlock() + for _, f := range m.updates { + f(m) + } + clear(m.updates) + m.updates = m.updates[:0] + m.updateNeeded.Store(false) +} diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index 8bcd8668eb..02ef9fb145 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -420,32 +420,32 @@ func consumeStreamsBlockedFrame(b []byte) (typ streamType, max int64, n int) { return typ, max, n } -func consumeNewConnectionIDFrame(b []byte) (seq, retire int64, connID []byte, resetToken [16]byte, n int) { +func consumeNewConnectionIDFrame(b []byte) (seq, retire int64, connID []byte, resetToken statelessResetToken, n int) { n = 1 var nn int seq, nn = consumeVarintInt64(b[n:]) if nn < 0 { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } n += nn retire, nn = consumeVarintInt64(b[n:]) if nn < 0 { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } n += nn if seq < retire { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } connID, nn = consumeVarintBytes(b[n:]) if nn < 0 { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } if len(connID) < 1 || len(connID) > 20 { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } n += nn if len(b[n:]) < len(resetToken) { - return 0, 0, nil, [16]byte{}, -1 + return 0, 0, nil, statelessResetToken{}, -1 } copy(resetToken[:], b[n:]) n += len(resetToken) diff --git a/internal/quic/stateless_reset.go b/internal/quic/stateless_reset.go new file mode 100644 index 0000000000..53c3ba5399 --- /dev/null +++ b/internal/quic/stateless_reset.go @@ -0,0 +1,61 @@ +// 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 ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "hash" + "sync" +) + +const statelessResetTokenLen = 128 / 8 + +// A statelessResetToken is a stateless reset token. +// https://www.rfc-editor.org/rfc/rfc9000#section-10.3 +type statelessResetToken [statelessResetTokenLen]byte + +type statelessResetTokenGenerator struct { + canReset bool + + // The hash.Hash interface is not concurrency safe, + // so we need a mutex here. + // + // There shouldn't be much contention on stateless reset token generation. + // If this proves to be a problem, we could avoid the mutex by using a separate + // generator per Conn, or by using a concurrency-safe generator. + mu sync.Mutex + mac hash.Hash +} + +func (g *statelessResetTokenGenerator) init(secret [32]byte) { + zero := true + for _, b := range secret { + if b != 0 { + zero = false + break + } + } + if zero { + // Generate tokens using a random secret, but don't send stateless resets. + rand.Read(secret[:]) + g.canReset = false + } else { + g.canReset = true + } + g.mac = hmac.New(sha256.New, secret[:]) +} + +func (g *statelessResetTokenGenerator) tokenForConnID(cid []byte) (token statelessResetToken) { + g.mu.Lock() + defer g.mu.Unlock() + defer g.mac.Reset() + g.mac.Write(cid) + copy(token[:], g.mac.Sum(nil)) + return token +} diff --git a/internal/quic/stateless_reset_test.go b/internal/quic/stateless_reset_test.go new file mode 100644 index 0000000000..b12e975607 --- /dev/null +++ b/internal/quic/stateless_reset_test.go @@ -0,0 +1,277 @@ +// 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" + "context" + "crypto/rand" + "crypto/tls" + "errors" + "net/netip" + "testing" +) + +func TestStatelessResetClientSendsStatelessResetTokenTransportParameter(t *testing.T) { + // "[The stateless_reset_token] transport parameter MUST NOT be sent by a client [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-18.2-4.6.1 + resetToken := testPeerStatelessResetToken(0) + tc := newTestConn(t, serverSide, func(p *transportParameters) { + p.statelessResetToken = resetToken[:] + }) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.wantFrame("client provided stateless_reset_token transport parameter", + packetTypeInitial, debugFrameConnectionCloseTransport{ + code: errTransportParameter, + }) +} + +var testStatelessResetKey = func() (key [32]byte) { + if _, err := rand.Read(key[:]); err != nil { + panic(err) + } + return key +}() + +func testStatelessResetToken(cid []byte) statelessResetToken { + var gen statelessResetTokenGenerator + gen.init(testStatelessResetKey) + return gen.tokenForConnID(cid) +} + +func testLocalStatelessResetToken(seq int64) statelessResetToken { + return testStatelessResetToken(testLocalConnID(seq)) +} + +func newDatagramForReset(cid []byte, size int, addr netip.AddrPort) *datagram { + dgram := append([]byte{headerFormShort | fixedBit}, cid...) + for len(dgram) < size { + dgram = append(dgram, byte(len(dgram))) // semi-random junk + } + return &datagram{ + b: dgram, + addr: addr, + } +} + +func TestStatelessResetSentSizes(t *testing.T) { + config := &Config{ + TLSConfig: newTestTLSConfig(serverSide), + StatelessResetKey: testStatelessResetKey, + } + addr := netip.MustParseAddr("127.0.0.1") + tl := newTestListener(t, config) + for i, test := range []struct { + reqSize int + wantSize int + }{{ + // Datagrams larger than 42 bytes result in a 42-byte stateless reset. + // This isn't specifically mandated by RFC 9000, but is implied. + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-11 + reqSize: 1200, + wantSize: 42, + }, { + // "An endpoint that sends a Stateless Reset in response to a packet + // that is 43 bytes or shorter SHOULD send a Stateless Reset that is + // one byte shorter than the packet it responds to." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-11 + reqSize: 43, + wantSize: 42, + }, { + reqSize: 42, + wantSize: 41, + }, { + // We should send a stateless reset in response to the smallest possible + // valid datagram the peer can send us. + // The smallest packet is 1-RTT: + // header byte, conn id, packet num, payload, AEAD. + reqSize: 1 + connIDLen + 1 + 1 + 16, + wantSize: 1 + connIDLen + 1 + 1 + 16 - 1, + }, { + // The smallest possible stateless reset datagram is 21 bytes. + // Since our response must be smaller than the incoming datagram, + // we must not respond to a 21 byte or smaller packet. + reqSize: 21, + wantSize: 0, + }} { + cid := testLocalConnID(int64(i)) + token := testStatelessResetToken(cid) + addrport := netip.AddrPortFrom(addr, uint16(8000+i)) + tl.write(newDatagramForReset(cid, test.reqSize, addrport)) + + got := tl.read() + if len(got) != test.wantSize { + t.Errorf("got %v-byte response to %v-byte req, want %v", + len(got), test.reqSize, test.wantSize) + } + if len(got) == 0 { + continue + } + // "Endpoints MUST send Stateless Resets formatted as + // a packet with a short header." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-15 + if isLongHeader(got[0]) { + t.Errorf("response to %v-byte request is not a short-header packet\ngot: %x", test.reqSize, got) + } + if !bytes.HasSuffix(got, token[:]) { + t.Errorf("response to %v-byte request does not end in stateless reset token\ngot: %x\nwant suffix: %x", test.reqSize, got, token) + } + } +} + +func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) { + // "[...] Stateless Reset Token field values from [...] NEW_CONNECTION_ID frames [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-1 + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + // Retire connection ID 0. + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + retirePriorTo: 1, + seq: 2, + connID: testPeerConnID(2), + }) + tc.wantFrame("peer requested we retire conn id 0", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + + resetToken := testPeerStatelessResetToken(1) // provided during handshake + dgram := append(make([]byte, 100), resetToken[:]...) + tc.listener.write(&datagram{ + b: dgram, + }) + + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { + t.Errorf("conn.Wait() = %v, want errStatelessReset", err) + } + tc.wantIdle("closed connection is idle") +} + +func TestStatelessResetSuccessfulTransportParameter(t *testing.T) { + // "[...] Stateless Reset Token field values from [...] + // the server's transport parameters [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-1 + resetToken := testPeerStatelessResetToken(0) + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.statelessResetToken = resetToken[:] + }) + tc.handshake() + + dgram := append(make([]byte, 100), resetToken[:]...) + tc.listener.write(&datagram{ + b: dgram, + }) + + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { + t.Errorf("conn.Wait() = %v, want errStatelessReset", err) + } + tc.wantIdle("closed connection is idle") +} + +func TestStatelessResetSuccessfulPrefix(t *testing.T) { + for _, test := range []struct { + name string + prefix []byte + size int + }{{ + name: "short header and fixed bit", + prefix: []byte{ + headerFormShort | fixedBit, + }, + size: 100, + }, { + // "[...] endpoints MUST treat [long header packets] ending in a + // valid stateless reset token as a Stateless Reset [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-15 + name: "long header no fixed bit", + prefix: []byte{ + headerFormLong, + }, + size: 100, + }, { + // "[...] the comparison MUST be performed when the first packet + // in an incoming datagram [...] cannot be decrypted." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-2 + name: "short header valid DCID", + prefix: append([]byte{ + headerFormShort | fixedBit, + }, testLocalConnID(0)...), + size: 100, + }, { + name: "handshake valid DCID", + prefix: append([]byte{ + headerFormLong | fixedBit | longPacketTypeHandshake, + }, testLocalConnID(0)...), + size: 100, + }, { + name: "no fixed bit valid DCID", + prefix: append([]byte{ + 0, + }, testLocalConnID(0)...), + size: 100, + }} { + t.Run(test.name, func(t *testing.T) { + resetToken := testPeerStatelessResetToken(0) + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.statelessResetToken = resetToken[:] + }) + tc.handshake() + + dgram := test.prefix + for len(dgram) < test.size-len(resetToken) { + dgram = append(dgram, byte(len(dgram))) // semi-random junk + } + dgram = append(dgram, resetToken[:]...) + tc.listener.write(&datagram{ + b: dgram, + }) + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { + t.Errorf("conn.Wait() = %v, want errStatelessReset", err) + } + }) + } +} + +func TestStatelessResetRetiredConnID(t *testing.T) { + // "An endpoint MUST NOT check for any stateless reset tokens [...] + // for connection IDs that have been retired." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-3 + resetToken := testPeerStatelessResetToken(0) + tc := newTestConn(t, clientSide, func(p *transportParameters) { + p.statelessResetToken = resetToken[:] + }) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + // We retire connection ID 0. + tc.writeFrames(packetType1RTT, + debugFrameNewConnectionID{ + seq: 2, + retirePriorTo: 1, + connID: testPeerConnID(2), + }) + tc.wantFrame("peer asked for conn id 0 to be retired", + packetType1RTT, debugFrameRetireConnectionID{ + seq: 0, + }) + + // Receive a stateless reset for connection ID 0. + dgram := append(make([]byte, 100), resetToken[:]...) + tc.listener.write(&datagram{ + b: dgram, + }) + + if err := tc.conn.Wait(canceledContext()); !errors.Is(err, context.Canceled) { + t.Errorf("conn.Wait() = %v, want connection to be alive", err) + } +} diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 337657e32c..6f4e06522a 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -71,9 +71,11 @@ func (tc *testConn) handshake() { func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { var ( - clientConnIDs [][]byte - serverConnIDs [][]byte - transientConnID []byte + clientConnIDs [][]byte + serverConnIDs [][]byte + clientResetToken statelessResetToken + serverResetToken statelessResetToken + transientConnID []byte ) localConnIDs := [][]byte{ testLocalConnID(0), @@ -83,13 +85,19 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { testPeerConnID(0), testPeerConnID(1), } + localResetToken := tc.listener.l.resetGen.tokenForConnID(localConnIDs[1]) + peerResetToken := testPeerStatelessResetToken(1) if tc.conn.side == clientSide { clientConnIDs = localConnIDs serverConnIDs = peerConnIDs + clientResetToken = localResetToken + serverResetToken = peerResetToken transientConnID = testLocalConnID(-1) } else { clientConnIDs = peerConnIDs serverConnIDs = localConnIDs + clientResetToken = peerResetToken + serverResetToken = localResetToken transientConnID = testPeerConnID(-1) } return []*testDatagram{{ @@ -136,6 +144,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { debugFrameNewConnectionID{ seq: 1, connID: serverConnIDs[1], + token: serverResetToken, }, }, }}, @@ -175,6 +184,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { debugFrameNewConnectionID{ seq: 1, connID: clientConnIDs[1], + token: clientResetToken, }, }, }}, @@ -337,6 +347,7 @@ func TestConnKeysDiscardedClient(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testLocalConnID(1), + token: testLocalStatelessResetToken(1), }) // The client discards Initial keys after sending a Handshake packet. @@ -390,6 +401,7 @@ func TestConnKeysDiscardedServer(t *testing.T) { packetType1RTT, debugFrameNewConnectionID{ seq: 1, connID: testLocalConnID(1), + token: testLocalStatelessResetToken(1), }) tc.wantIdle("server has discarded Initial keys, cannot read CONNECTION_CLOSE") @@ -546,7 +558,9 @@ func TestConnAEADLimitReached(t *testing.T) { // exceeds the integrity limit for the selected AEAD, // the endpoint MUST immediately close the connection [...]" // https://www.rfc-editor.org/rfc/rfc9001#section-6.6-6 - tc := newTestConn(t, clientSide) + tc := newTestConn(t, clientSide, func(c *Config) { + clear(c.StatelessResetKey[:]) + }) tc.handshake() var limit int64 From 6d267b1f96339103f3a322266f022b21a1d0688d Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 26 Oct 2023 11:34:01 -0700 Subject: [PATCH 084/168] quic: properly shut down connections on listener close We were failing to add new connections to the listener's set of live connections, so closing a listener wouldn't abort connections or wait for them to shut down. We were also aborting active connections with an error that resulted in the connection closing with an INTERNAL_ERROR status. Close with NO_ERROR instead. For golang/go#58547 Change-Id: I89b6c4fabf744ae5178c0cae655929db1ae40ee4 Reviewed-on: https://go-review.googlesource.com/c/net/+/537935 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_close_test.go | 12 ++++++++++++ internal/quic/listener.go | 3 ++- internal/quic/listener_test.go | 5 ++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/quic/conn_close_test.go b/internal/quic/conn_close_test.go index d5c3499e4a..d583ae92a0 100644 --- a/internal/quic/conn_close_test.go +++ b/internal/quic/conn_close_test.go @@ -186,3 +186,15 @@ func TestConnCloseReceiveInHandshake(t *testing.T) { }) tc.wantIdle("no more frames to send") } + +func TestConnCloseClosedByListener(t *testing.T) { + ctx := canceledContext() + tc := newTestConn(t, clientSide) + tc.handshake() + + tc.listener.l.Close(ctx) + tc.wantFrame("listener closes connection before exiting", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errNo, + }) +} diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 668d270b39..cfe45b1376 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -107,7 +107,7 @@ func (l *Listener) Close(ctx context.Context) error { if !l.closing { l.closing = true for c := range l.conns { - c.Abort(errors.New("listener closed")) + c.Abort(localTransportError(errNo)) } if len(l.conns) == 0 { l.udpConn.Close() @@ -161,6 +161,7 @@ func (l *Listener) newConn(now time.Time, side connSide, originalDstConnID, retr if err != nil { return nil, err } + l.conns[c] = struct{}{} return c, nil } diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 346f81c386..a5cc690ac4 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -128,7 +128,10 @@ func (tl *testListener) cleanup() { } func (tl *testListener) wait() { - tl.idlec <- struct{}{} + select { + case tl.idlec <- struct{}{}: + case <-tl.l.closec: + } for _, tc := range tl.conns { tc.wait() } From 0526b49b345664cadb8ea67dd4b7c02964c55b3a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 30 Oct 2023 11:01:23 -0700 Subject: [PATCH 085/168] quic: fix data race caused by aliased DCID The initServer function was retaining a reference to a []byte that aliases a packet buffer, which is subsequently recycled. Make a copy of the data before retaining it. Fixes golang/go#63783 Change-Id: I3dbb0cdfd78681014dec97ff9909ff6c7dbf82ba Reviewed-on: https://go-review.googlesource.com/c/net/+/538615 LUCI-TryBot-Result: Go LUCI Auto-Submit: Damien Neil Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_id.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index c236137594..91ccaade14 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -97,12 +97,13 @@ func (s *connIDState) initClient(c *Conn) error { } func (s *connIDState) initServer(c *Conn, dstConnID []byte) error { + dstConnID = cloneBytes(dstConnID) // Client-chosen, transient connection ID received in the first Initial packet. // The server will not use this as the Source Connection ID of packets it sends, // but remembers it because it may receive packets sent to this destination. s.local = append(s.local, connID{ seq: -1, - cid: cloneBytes(dstConnID), + cid: dstConnID, }) // Server chooses a connection ID, and sends it in the Source Connection ID of From 05086a79fc73b01ecd1d1c303fc3f4a1311afc17 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 31 Oct 2023 08:49:27 -0700 Subject: [PATCH 086/168] quic: fix panic when handling resent CRYPTO data When pipe.discardBefore was called with an offset greater than the current pipe.end position, we would update pipe.start but not pipe.end, leaving the pipe in an inconsistent state where start > end. This could then subsequently cause a panic when writing data that lies before pipe.start. This sequence occurs when handling several in-order CRYPTO frames (where we skip writing in-order data to the pipe, but still call discardBefore), followed by an out-of-order frame containing resent data. Change-Id: Ibac0caad53cd30dac1cd4719a825226809872d96 Reviewed-on: https://go-review.googlesource.com/c/net/+/538775 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/crypto_stream_test.go | 15 +++++++++++++++ internal/quic/pipe.go | 1 + internal/quic/pipe_test.go | 9 +++++++++ 3 files changed, 25 insertions(+) diff --git a/internal/quic/crypto_stream_test.go b/internal/quic/crypto_stream_test.go index a6c1e1b521..6bee8bb9f6 100644 --- a/internal/quic/crypto_stream_test.go +++ b/internal/quic/crypto_stream_test.go @@ -94,6 +94,21 @@ func TestCryptoStreamReceive(t *testing.T) { end: 3000, want: 4000, }}, + }, { + name: "resent consumed data", + frames: []frame{{ + start: 0, + end: 1000, + want: 1000, + }, { + start: 1000, + end: 2000, + want: 2000, + }, { + start: 0, + end: 1000, + want: 2000, + }}, }} { t.Run(test.name, func(t *testing.T) { var s cryptoStream diff --git a/internal/quic/pipe.go b/internal/quic/pipe.go index 978a4f3d8b..d3a448df34 100644 --- a/internal/quic/pipe.go +++ b/internal/quic/pipe.go @@ -146,4 +146,5 @@ func (p *pipe) discardBefore(off int64) { p.tail = nil } p.start = off + p.end = max(p.end, off) } diff --git a/internal/quic/pipe_test.go b/internal/quic/pipe_test.go index 7a05ff4d47..bcb3a8bc05 100644 --- a/internal/quic/pipe_test.go +++ b/internal/quic/pipe_test.go @@ -61,6 +61,12 @@ func TestPipeWrites(t *testing.T) { discardBeforeOp{10000}, writeOp{10000, 20000}, }, + }, { + desc: "discard before writing", + ops: []op{ + discardBeforeOp{1000}, + writeOp{0, 1}, + }, }} { var p pipe var wantset rangeset[int64] @@ -78,6 +84,9 @@ func TestPipeWrites(t *testing.T) { p.discardBefore(o.off) wantset.sub(0, o.off) wantStart = o.off + if o.off > wantEnd { + wantEnd = o.off + } } if p.start != wantStart || p.end != wantEnd { t.Errorf("%v: after %#v p contains [%v,%v), want [%v,%v)", test.desc, test.ops[:i+1], p.start, p.end, wantStart, wantEnd) From 770149e9886ec895bb824b608bd02d661fce552d Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 31 Oct 2023 09:37:48 -0700 Subject: [PATCH 087/168] quic: pad ack-eliciting server Initial datagrams UDP datagrams containing Initial packets are expanded to 1200 bytes to validate that the path is capable of supporting the smallest allowed maximum QUIC datagram size. (In addition, client Initial packets must be sent in datagrams of at least 1200 bytes, to defend against amplification attacks.) We were expanding client datagrams containing Initial packets, but not server datagrams. Fix this. (More specifically, server datagrams must be expanded to 1200 bytes when they contain ack-eliciting Initial packets.) RFC 9000, Section 14.1. Change-Id: I0c0c36321c055e960be3e29a49d7cb7620640b82 Reviewed-on: https://go-review.googlesource.com/c/net/+/538776 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_recv.go | 2 +- internal/quic/conn_send.go | 13 +++++++------ internal/quic/listener.go | 4 ++-- internal/quic/listener_test.go | 2 +- internal/quic/quic.go | 5 +++-- internal/quic/tls_test.go | 1 + internal/quic/version_test.go | 2 +- 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 183316780e..e966b7ef55 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -24,7 +24,7 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { ptype := getPacketType(buf) switch ptype { case packetTypeInitial: - if c.side == serverSide && len(dgram.b) < minimumClientInitialDatagramSize { + if c.side == serverSide && len(dgram.b) < paddedInitialDatagramSize { // Discard client-sent Initial packets in too-short datagrams. // https://www.rfc-editor.org/rfc/rfc9000#section-14.1-4 return diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index f512518efa..64e5d75482 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -77,10 +77,11 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } sentInitial = c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysInitial.w, p) if sentInitial != nil { - // Client initial packets need to be sent in a datagram padded to - // at least 1200 bytes. We can't add the padding yet, however, - // since we may want to coalesce additional packets with this one. - if c.side == clientSide { + // Client initial packets and ack-eliciting server initial packaets + // need to be sent in a datagram padded to at least 1200 bytes. + // We can't add the padding yet, however, since we may want to + // coalesce additional packets with this one. + if c.side == clientSide || sentInitial.ackEliciting { pad = true } } @@ -123,7 +124,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // 1-RTT packets have no length field and extend to the end // of the datagram, so if we're sending a datagram that needs // padding we need to add it inside the 1-RTT packet. - c.w.appendPaddingTo(minimumClientInitialDatagramSize) + c.w.appendPaddingTo(paddedInitialDatagramSize) pad = false } if logPackets { @@ -149,7 +150,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // Pad out the datagram with zeros, coalescing the Initial // packet with invalid packets that will be ignored by the peer. // https://www.rfc-editor.org/rfc/rfc9000.html#section-14.1-1 - for len(buf) < minimumClientInitialDatagramSize { + for len(buf) < paddedInitialDatagramSize { buf = append(buf, 0) // Technically this padding isn't in any packet, but // account it to the Initial packet in this datagram diff --git a/internal/quic/listener.go b/internal/quic/listener.go index cfe45b1376..08f011092a 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -269,7 +269,7 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { return } p, ok := parseGenericLongHeaderPacket(m.b) - if !ok || len(m.b) < minimumClientInitialDatagramSize { + if !ok || len(m.b) < paddedInitialDatagramSize { return } switch p.version { @@ -382,7 +382,7 @@ func (l *Listener) sendConnectionClose(in genericLongPacket, addr netip.AddrPort srcConnID: in.dstConnID, } const pnumMaxAcked = 0 - w.reset(minimumClientInitialDatagramSize) + w.reset(paddedInitialDatagramSize) w.startProtectedLongHeaderPacket(pnumMaxAcked, p) w.appendConnectionCloseTransportFrame(code, 0, "") w.finishProtectedLongHeaderPacket(pnumMaxAcked, keys.w, p) diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index a5cc690ac4..21717e2516 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -172,7 +172,7 @@ func (tl *testListener) writeDatagram(d *testDatagram) { } pad := 0 if p.ptype == packetType1RTT { - pad = d.paddedSize + pad = d.paddedSize - len(buf) } buf = append(buf, encodeTestPacket(tl.t, tc, p, pad)...) } diff --git a/internal/quic/quic.go b/internal/quic/quic.go index 9de97b6d88..084887be67 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -58,9 +58,10 @@ const ( // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.1.2-6 const timerGranularity = 1 * time.Millisecond -// Minimum size of a UDP datagram sent by a client carrying an Initial packet. +// Minimum size of a UDP datagram sent by a client carrying an Initial packet, +// or a server containing an ack-eliciting Initial packet. // https://www.rfc-editor.org/rfc/rfc9000#section-14.1 -const minimumClientInitialDatagramSize = 1200 +const paddedInitialDatagramSize = 1200 // Maximum number of streams of a given type which may be created. // https://www.rfc-editor.org/rfc/rfc9000.html#section-4.6-2 diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 6f4e06522a..fa339b9faf 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -148,6 +148,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { }, }, }}, + paddedSize: 1200, }, { // Client Initial + Handshake + 1-RTT packets: []*testPacket{{ diff --git a/internal/quic/version_test.go b/internal/quic/version_test.go index 264df9dbc6..830e0e1c83 100644 --- a/internal/quic/version_test.go +++ b/internal/quic/version_test.go @@ -30,7 +30,7 @@ func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { pkt = append(pkt, dstConnID...) pkt = append(pkt, byte(len(srcConnID))) pkt = append(pkt, srcConnID...) - for len(pkt) < minimumClientInitialDatagramSize { + for len(pkt) < paddedInitialDatagramSize { pkt = append(pkt, 0) } From 4865e2af27b9cc42cbb25a29294154b519cc8e56 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 3 Oct 2023 10:57:24 -0700 Subject: [PATCH 088/168] internal/quic/cmd/interop: add interop test runner The QUIC interop tests at https://interop.seemann.io/ invoke a program and instruct it to perform some set of operations (mostly serve files from a directory, or download a set of files). The cmd/interop binary executes test cases for our implementation. For golang/go#58547 Change-Id: Ic1c8be2f3f49a30464650d9eaa5ded74c92fa5a7 Reviewed-on: https://go-review.googlesource.com/c/net/+/532435 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam Auto-Submit: Damien Neil --- internal/quic/cmd/interop/Dockerfile | 32 +++ internal/quic/cmd/interop/README.md | 7 + internal/quic/cmd/interop/main.go | 262 ++++++++++++++++++++++ internal/quic/cmd/interop/main_test.go | 155 +++++++++++++ internal/quic/cmd/interop/run_endpoint.sh | 17 ++ 5 files changed, 473 insertions(+) create mode 100644 internal/quic/cmd/interop/Dockerfile create mode 100644 internal/quic/cmd/interop/README.md create mode 100644 internal/quic/cmd/interop/main.go create mode 100644 internal/quic/cmd/interop/main_test.go create mode 100644 internal/quic/cmd/interop/run_endpoint.sh diff --git a/internal/quic/cmd/interop/Dockerfile b/internal/quic/cmd/interop/Dockerfile new file mode 100644 index 0000000000..4b52e5356d --- /dev/null +++ b/internal/quic/cmd/interop/Dockerfile @@ -0,0 +1,32 @@ +FROM martenseemann/quic-network-simulator-endpoint:latest AS builder + +ARG TARGETPLATFORM +RUN echo "TARGETPLATFORM: ${TARGETPLATFORM}" + +RUN apt-get update && apt-get install -y wget tar git + +ENV GOVERSION=1.21.1 + +RUN platform=$(echo ${TARGETPLATFORM} | tr '/' '-') && \ + filename="go${GOVERSION}.${platform}.tar.gz" && \ + wget https://dl.google.com/go/${filename} && \ + tar xfz ${filename} && \ + rm ${filename} + +ENV PATH="/go/bin:${PATH}" + +RUN git clone https://go.googlesource.com/net + +WORKDIR /net +RUN go build -o /interop ./internal/quic/cmd/interop + +FROM martenseemann/quic-network-simulator-endpoint:latest + +WORKDIR /go-x-net + +COPY --from=builder /interop ./ + +# copy run script and run it +COPY run_endpoint.sh . +RUN chmod +x run_endpoint.sh +ENTRYPOINT [ "./run_endpoint.sh" ] diff --git a/internal/quic/cmd/interop/README.md b/internal/quic/cmd/interop/README.md new file mode 100644 index 0000000000..aca0571b91 --- /dev/null +++ b/internal/quic/cmd/interop/README.md @@ -0,0 +1,7 @@ +This directory contains configuration and programs used to +integrate with the QUIC Interop Test Runner. + +The QUIC Interop Test Runner executes a variety of test cases +against a matrix of clients and servers. + +https://github.com/marten-seemann/quic-interop-runner diff --git a/internal/quic/cmd/interop/main.go b/internal/quic/cmd/interop/main.go new file mode 100644 index 0000000000..cc5292e9ea --- /dev/null +++ b/internal/quic/cmd/interop/main.go @@ -0,0 +1,262 @@ +// 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 + +// The interop command is the client and server used by QUIC interoperability tests. +// +// https://github.com/marten-seemann/quic-interop-runner +package main + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "flag" + "fmt" + "io" + "log" + "net" + "net/url" + "os" + "path/filepath" + "sync" + + "golang.org/x/net/internal/quic" +) + +var ( + listen = flag.String("listen", "", "listen address") + cert = flag.String("cert", "", "certificate") + pkey = flag.String("key", "", "private key") + root = flag.String("root", "", "serve files from this root") + output = flag.String("output", "", "directory to write files to") +) + +func main() { + ctx := context.Background() + flag.Parse() + urls := flag.Args() + + config := &quic.Config{ + TLSConfig: &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS13, + NextProtos: []string{"hq-interop"}, + }, + MaxBidiRemoteStreams: -1, + MaxUniRemoteStreams: -1, + } + if *cert != "" { + c, err := tls.LoadX509KeyPair(*cert, *pkey) + if err != nil { + log.Fatal(err) + } + config.TLSConfig.Certificates = []tls.Certificate{c} + } + if *root != "" { + config.MaxBidiRemoteStreams = 100 + } + if keylog := os.Getenv("SSLKEYLOGFILE"); keylog != "" { + f, err := os.Create(keylog) + if err != nil { + log.Fatal(err) + } + defer f.Close() + config.TLSConfig.KeyLogWriter = f + } + + testcase := os.Getenv("TESTCASE") + switch testcase { + case "handshake", "keyupdate": + basicTest(ctx, config, urls) + return + case "chacha20": + // "[...] offer only ChaCha20 as a ciphersuite." + // + // crypto/tls does not support configuring TLS 1.3 ciphersuites, + // so we can't support this test. + case "transfer": + // "The client should use small initial flow control windows + // for both stream- and connection-level flow control + // such that the during the transfer of files on the order of 1 MB + // the flow control window needs to be increased." + config.MaxStreamReadBufferSize = 64 << 10 + config.MaxConnReadBufferSize = 64 << 10 + basicTest(ctx, config, urls) + return + case "http3": + // TODO + case "multiconnect": + // TODO + case "resumption": + // TODO + case "retry": + // TODO + case "versionnegotiation": + // "The client should start a connection using + // an unsupported version number [...]" + // + // We don't support setting the client's version, + // so only run this test as a server. + if *listen != "" && len(urls) == 0 { + basicTest(ctx, config, urls) + return + } + case "v2": + // We do not support QUIC v2. + case "zerortt": + // TODO + } + fmt.Printf("unsupported test case %q\n", testcase) + os.Exit(127) +} + +// basicTest runs the standard test setup. +// +// As a server, it serves the contents of the -root directory. +// As a client, it downloads all the provided URLs in parallel, +// making one connection to each destination server. +func basicTest(ctx context.Context, config *quic.Config, urls []string) { + l, err := quic.Listen("udp", *listen, config) + if err != nil { + log.Fatal(err) + } + log.Printf("listening on %v", l.LocalAddr()) + + byAuthority := map[string][]*url.URL{} + for _, s := range urls { + u, addr, err := parseURL(s) + if err != nil { + log.Fatal(err) + } + byAuthority[addr] = append(byAuthority[addr], u) + } + var g sync.WaitGroup + defer g.Wait() + for addr, u := range byAuthority { + addr, u := addr, u + g.Add(1) + go func() { + defer g.Done() + fetchFrom(ctx, l, addr, u) + }() + } + + if config.MaxBidiRemoteStreams >= 0 { + serve(ctx, l) + } +} + +func serve(ctx context.Context, l *quic.Listener) error { + for { + c, err := l.Accept(ctx) + if err != nil { + return err + } + go serveConn(ctx, c) + } +} + +func serveConn(ctx context.Context, c *quic.Conn) { + for { + s, err := c.AcceptStream(ctx) + if err != nil { + return + } + go func() { + if err := serveReq(ctx, s); err != nil { + log.Print("serveReq:", err) + } + }() + } +} + +func serveReq(ctx context.Context, s *quic.Stream) error { + defer s.Close() + req, err := io.ReadAll(s) + if err != nil { + return err + } + if !bytes.HasSuffix(req, []byte("\r\n")) { + return errors.New("invalid request") + } + req = bytes.TrimSuffix(req, []byte("\r\n")) + if !bytes.HasPrefix(req, []byte("GET /")) { + return errors.New("invalid request") + } + req = bytes.TrimPrefix(req, []byte("GET /")) + if !filepath.IsLocal(string(req)) { + return errors.New("invalid request") + } + f, err := os.Open(filepath.Join(*root, string(req))) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(s, f) + return err +} + +func parseURL(s string) (u *url.URL, authority string, err error) { + u, err = url.Parse(s) + if err != nil { + return nil, "", err + } + host := u.Hostname() + port := u.Port() + if port == "" { + port = "443" + } + authority = net.JoinHostPort(host, port) + return u, authority, nil +} + +func fetchFrom(ctx context.Context, l *quic.Listener, addr string, urls []*url.URL) { + conn, err := l.Dial(ctx, "udp", addr) + if err != nil { + log.Printf("%v: %v", addr, err) + return + } + log.Printf("connected to %v", addr) + defer conn.Close() + var g sync.WaitGroup + for _, u := range urls { + u := u + g.Add(1) + go func() { + defer g.Done() + if err := fetchOne(ctx, conn, u); err != nil { + log.Printf("fetch %v: %v", u, err) + } else { + log.Printf("fetched %v", u) + } + }() + } + g.Wait() +} + +func fetchOne(ctx context.Context, conn *quic.Conn, u *url.URL) error { + if len(u.Path) == 0 || u.Path[0] != '/' || !filepath.IsLocal(u.Path[1:]) { + return errors.New("invalid path") + } + file, err := os.Create(filepath.Join(*output, u.Path[1:])) + if err != nil { + return err + } + s, err := conn.NewStream(ctx) + if err != nil { + return err + } + defer s.Close() + if _, err := s.Write([]byte("GET " + u.Path + "\r\n")); err != nil { + return err + } + s.CloseWrite() + if _, err := io.Copy(file, s); err != nil { + return err + } + return nil +} diff --git a/internal/quic/cmd/interop/main_test.go b/internal/quic/cmd/interop/main_test.go new file mode 100644 index 0000000000..9afd224339 --- /dev/null +++ b/internal/quic/cmd/interop/main_test.go @@ -0,0 +1,155 @@ +// 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 main + +import ( + "bufio" + "bytes" + "context" + "net" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func init() { + // We reexec the test binary with CMD_INTEROP_MAIN=1 to run main. + if os.Getenv("CMD_INTEROP_MAIN") == "1" { + main() + os.Exit(0) + } +} + +type interopTest struct { + donec chan struct{} + addr string + cmd *exec.Cmd +} + +func run(ctx context.Context, t *testing.T, name, testcase string, args []string) *interopTest { + ctx, cancel := context.WithCancel(ctx) + cmd := exec.CommandContext(ctx, os.Args[0], args...) + out, err := cmd.StderrPipe() + if err != nil { + t.Fatal(err) + } + cmd.Stdout = cmd.Stderr + cmd.Env = []string{ + "CMD_INTEROP_MAIN=1", + "TESTCASE=" + testcase, + } + t.Logf("run %v: %v", name, args) + err = cmd.Start() + if err != nil { + t.Fatal(err) + } + + addrc := make(chan string, 1) + donec := make(chan struct{}) + go func() { + defer t.Logf("%v done", name) + defer close(addrc) + defer close(donec) + s := bufio.NewScanner(out) + for s.Scan() { + line := s.Text() + t.Logf("%v: %v", name, line) + _, addr, ok := strings.Cut(line, "listening on ") + if ok { + select { + case addrc <- addr: + default: + } + } + } + }() + + t.Cleanup(func() { + cancel() + <-donec + }) + + addr, ok := <-addrc + if !ok { + t.Fatal(cmd.Wait()) + } + _, port, _ := net.SplitHostPort(addr) + addr = net.JoinHostPort("localhost", port) + + iop := &interopTest{ + cmd: cmd, + donec: donec, + addr: addr, + } + return iop +} + +func (iop *interopTest) wait() { + <-iop.donec +} + +func TestTransfer(t *testing.T) { + ctx := context.Background() + src := t.TempDir() + dst := t.TempDir() + certs := t.TempDir() + certFile := filepath.Join(certs, "cert.pem") + keyFile := filepath.Join(certs, "key.pem") + sourceName := "source" + content := []byte("hello, world\n") + + os.WriteFile(certFile, localhostCert, 0600) + os.WriteFile(keyFile, localhostKey, 0600) + os.WriteFile(filepath.Join(src, sourceName), content, 0600) + + srv := run(ctx, t, "server", "transfer", []string{ + "-listen", "localhost:0", + "-cert", filepath.Join(certs, "cert.pem"), + "-key", filepath.Join(certs, "key.pem"), + "-root", src, + }) + cli := run(ctx, t, "client", "transfer", []string{ + "-output", dst, "https://" + srv.addr + "/" + sourceName, + }) + cli.wait() + + got, err := os.ReadFile(filepath.Join(dst, "source")) + if err != nil { + t.Fatalf("reading downloaded file: %v", err) + } + if !bytes.Equal(got, content) { + t.Fatalf("got downloaded file: %q, want %q", string(got), string(content)) + } +} + +// localhostCert is a PEM-encoded TLS cert with SAN IPs +// "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. +// generated from src/crypto/tls: +// go run generate_cert.go --ecdsa-curve P256 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var localhostCert = []byte(`-----BEGIN CERTIFICATE----- +MIIBrDCCAVKgAwIBAgIPCvPhO+Hfv+NW76kWxULUMAoGCCqGSM49BAMCMBIxEDAO +BgNVBAoTB0FjbWUgQ28wIBcNNzAwMTAxMDAwMDAwWhgPMjA4NDAxMjkxNjAwMDBa +MBIxEDAOBgNVBAoTB0FjbWUgQ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARh +WRF8p8X9scgW7JjqAwI9nYV8jtkdhqAXG9gyEgnaFNN5Ze9l3Tp1R9yCDBMNsGms +PyfMPe5Jrha/LmjgR1G9o4GIMIGFMA4GA1UdDwEB/wQEAwIChDATBgNVHSUEDDAK +BggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSOJri/wLQxq6oC +Y6ZImms/STbTljAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAA +AAAAAAAAAAAAATAKBggqhkjOPQQDAgNIADBFAiBUguxsW6TGhixBAdORmVNnkx40 +HjkKwncMSDbUaeL9jQIhAJwQ8zV9JpQvYpsiDuMmqCuW35XXil3cQ6Drz82c+fvE +-----END CERTIFICATE-----`) + +// localhostKey is the private key for localhostCert. +var localhostKey = []byte(testingKey(`-----BEGIN TESTING KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgY1B1eL/Bbwf/MDcs +rnvvWhFNr1aGmJJR59PdCN9lVVqhRANCAARhWRF8p8X9scgW7JjqAwI9nYV8jtkd +hqAXG9gyEgnaFNN5Ze9l3Tp1R9yCDBMNsGmsPyfMPe5Jrha/LmjgR1G9 +-----END TESTING KEY-----`)) + +// testingKey helps keep security scanners from getting excited about a private key in this file. +func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } diff --git a/internal/quic/cmd/interop/run_endpoint.sh b/internal/quic/cmd/interop/run_endpoint.sh new file mode 100644 index 0000000000..d72335d8e4 --- /dev/null +++ b/internal/quic/cmd/interop/run_endpoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Set up the routing needed for the simulation +/setup.sh + +# The following variables are available for use: +# - ROLE contains the role of this execution context, client or server +# - SERVER_PARAMS contains user-supplied command line parameters +# - CLIENT_PARAMS contains user-supplied command line parameters + +if [ "$ROLE" == "client" ]; then + # Wait for the simulator to start up. + /wait-for-it.sh sim:57832 -s -t 30 + ./interop -output=/downloads $CLIENT_PARAMS $REQUESTS +elif [ "$ROLE" == "server" ]; then + ./interop -cert=/certs/cert.pem -key=/certs/priv.key -listen=:443 -root=/www "$@" $SERVER_PARAMS +fi From 5791239e3d7093c8867413137a35e5fcaaf8277b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 1 Nov 2023 12:22:13 -0700 Subject: [PATCH 089/168] internal/quic/cmd/interop: skip tests when exec is unavailable Some platforms, such as js and wasip1, can't exec. Skip tests that need exec when it isn't available. Change-Id: Id3787b28c2ffe780eb24800c59fe69d12e04bbdd Reviewed-on: https://go-review.googlesource.com/c/net/+/539035 Reviewed-by: Bryan Mills LUCI-TryBot-Result: Go LUCI --- internal/quic/cmd/interop/main_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/quic/cmd/interop/main_test.go b/internal/quic/cmd/interop/main_test.go index 9afd224339..6fd9c0f2d5 100644 --- a/internal/quic/cmd/interop/main_test.go +++ b/internal/quic/cmd/interop/main_test.go @@ -15,6 +15,7 @@ import ( "os/exec" "path/filepath" "strings" + "sync" "testing" ) @@ -26,6 +27,23 @@ func init() { } } +var ( + tryExecOnce sync.Once + tryExecErr error +) + +// needsExec skips the test if we can't use exec.Command. +func needsExec(t *testing.T) { + tryExecOnce.Do(func() { + cmd := exec.Command(os.Args[0], "-test.list=^$") + cmd.Env = []string{} + tryExecErr = cmd.Run() + }) + if tryExecErr != nil { + t.Skipf("skipping test: cannot exec subprocess: %v", tryExecErr) + } +} + type interopTest struct { donec chan struct{} addr string @@ -33,6 +51,7 @@ type interopTest struct { } func run(ctx context.Context, t *testing.T, name, testcase string, args []string) *interopTest { + needsExec(t) ctx, cancel := context.WithCancel(ctx) cmd := exec.CommandContext(ctx, os.Args[0], args...) out, err := cmd.StderrPipe() From ec29a9498a02f880ede985f4671b24c62016f936 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 3 Nov 2023 16:37:26 -0700 Subject: [PATCH 090/168] quic: provide source conn ID when creating server conns New server-side conns need to know a variety of connection IDs, such as the Initial DCID used to create Initial encryption keys. We've been providing these as an ever-growing list of []byte parameters to newConn. Bundle them all up into a struct. Add the client's SCID to the set of IDs we pass to newConn. Up until now, we've been setting this when processing the first Initial packet from the client. Passing it to newConn will makes it available when logging the connection_started event. Update some test infrastructure to deal with the fact that we need to know the peer's SCID earlier in the test now. Change-Id: I760ee94af36125acf21c5bf135f1168830ba1ab8 Reviewed-on: https://go-review.googlesource.com/c/net/+/539341 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn.go | 22 ++++++++++++------ internal/quic/conn_id.go | 12 ++++++++-- internal/quic/conn_id_test.go | 5 +++- internal/quic/conn_test.go | 24 ++++++++++++------- internal/quic/listener.go | 19 ++++++++------- internal/quic/listener_test.go | 42 ++++++++-------------------------- 6 files changed, 65 insertions(+), 59 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index b3d6feabc7..1292f2b20e 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -86,7 +86,15 @@ type connTestHooks interface { timeNow() time.Time } -func newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) { +// newServerConnIDs is connection IDs associated with a new server connection. +type newServerConnIDs struct { + srcConnID []byte // source from client's current Initial + dstConnID []byte // destination from client's current Initial + originalDstConnID []byte // destination from client's first Initial + retrySrcConnID []byte // source from server's Retry +} + +func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) { c := &Conn{ side: side, listener: l, @@ -115,11 +123,11 @@ func newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []b } initialConnID, _ = c.connIDState.dstConnID() } else { - initialConnID = originalDstConnID - if retrySrcConnID != nil { - initialConnID = retrySrcConnID + initialConnID = cids.originalDstConnID + if cids.retrySrcConnID != nil { + initialConnID = cids.retrySrcConnID } - if err := c.connIDState.initServer(c, initialConnID); err != nil { + if err := c.connIDState.initServer(c, cids); err != nil { return nil, err } } @@ -134,8 +142,8 @@ func newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []b if err := c.startTLS(now, initialConnID, transportParameters{ initialSrcConnID: c.connIDState.srcConnID(), - originalDstConnID: originalDstConnID, - retrySrcConnID: retrySrcConnID, + originalDstConnID: cids.originalDstConnID, + retrySrcConnID: cids.retrySrcConnID, ackDelayExponent: ackDelayExponent, maxUDPPayloadSize: maxUDPPayloadSize, maxAckDelay: maxAckDelay, diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index 91ccaade14..b77ad8edf2 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -96,8 +96,8 @@ func (s *connIDState) initClient(c *Conn) error { return nil } -func (s *connIDState) initServer(c *Conn, dstConnID []byte) error { - dstConnID = cloneBytes(dstConnID) +func (s *connIDState) initServer(c *Conn, cids newServerConnIDs) error { + dstConnID := cloneBytes(cids.dstConnID) // Client-chosen, transient connection ID received in the first Initial packet. // The server will not use this as the Source Connection ID of packets it sends, // but remembers it because it may receive packets sent to this destination. @@ -121,6 +121,14 @@ func (s *connIDState) initServer(c *Conn, dstConnID []byte) error { conns.addConnID(c, dstConnID) conns.addConnID(c, locid) }) + + // Client chose its own connection ID. + s.remote = append(s.remote, remoteConnID{ + connID: connID{ + seq: 0, + cid: cloneBytes(cids.srcConnID), + }, + }) return nil } diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index 63feec992e..314a6b3845 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -578,8 +578,11 @@ func TestConnIDPeerWithZeroLengthIDProvidesPreferredAddr(t *testing.T) { p.preferredAddrV6 = netip.MustParseAddrPort("[::0]:0") p.preferredAddrConnID = testPeerConnID(1) p.preferredAddrResetToken = make([]byte, 16) + }, func(cids *newServerConnIDs) { + cids.srcConnID = []byte{} + }, func(tc *testConn) { + tc.peerConnID = []byte{} }) - tc.peerConnID = []byte{} tc.writeFrames(packetTypeInitial, debugFrameCrypto{ diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index df28907f44..248be9641b 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -193,33 +193,38 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { TLSConfig: newTestTLSConfig(side), StatelessResetKey: testStatelessResetKey, } + var cids newServerConnIDs + if side == serverSide { + // The initial connection ID for the server is chosen by the client. + cids.srcConnID = testPeerConnID(0) + cids.dstConnID = testPeerConnID(-1) + } var configTransportParams []func(*transportParameters) + var configTestConn []func(*testConn) for _, o := range opts { switch o := o.(type) { case func(*Config): o(config) case func(*tls.Config): o(config.TLSConfig) + case func(cids *newServerConnIDs): + o(&cids) case func(p *transportParameters): configTransportParams = append(configTransportParams, o) + case func(p *testConn): + configTestConn = append(configTestConn, o) default: t.Fatalf("unknown newTestConn option %T", o) } } - var initialConnID []byte - if side == serverSide { - // The initial connection ID for the server is chosen by the client. - initialConnID = testPeerConnID(-1) - } - listener := newTestListener(t, config) listener.configTransportParams = configTransportParams + listener.configTestConn = configTestConn conn, err := listener.l.newConn( listener.now, side, - initialConnID, - nil, + cids, netip.MustParseAddrPort("127.0.0.1:443")) if err != nil { t.Fatal(err) @@ -244,6 +249,9 @@ func newTestConnForConn(t *testing.T, listener *testListener, conn *Conn) *testC recvDatagram: make(chan *datagram), } t.Cleanup(tc.cleanup) + for _, f := range listener.configTestConn { + f(tc) + } conn.testHooks = (*testConnHooks)(tc) if listener.peerTLSConn != nil { diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 08f011092a..24484eb6f2 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -140,7 +140,7 @@ func (l *Listener) Dial(ctx context.Context, network, address string) (*Conn, er } addr := u.AddrPort() addr = netip.AddrPortFrom(addr.Addr().Unmap(), addr.Port()) - c, err := l.newConn(time.Now(), clientSide, nil, nil, addr) + c, err := l.newConn(time.Now(), clientSide, newServerConnIDs{}, addr) if err != nil { return nil, err } @@ -151,13 +151,13 @@ func (l *Listener) Dial(ctx context.Context, network, address string) (*Conn, er return c, nil } -func (l *Listener) newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []byte, peerAddr netip.AddrPort) (*Conn, error) { +func (l *Listener) newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort) (*Conn, error) { l.connsMu.Lock() defer l.connsMu.Unlock() if l.closing { return nil, errors.New("listener closed") } - c, err := newConn(now, side, originalDstConnID, retrySrcConnID, peerAddr, l.config, l) + c, err := newConn(now, side, cids, peerAddr, l.config, l) if err != nil { return nil, err } @@ -296,19 +296,22 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { } else { now = time.Now() } - var originalDstConnID, retrySrcConnID []byte + cids := newServerConnIDs{ + srcConnID: p.srcConnID, + dstConnID: p.dstConnID, + } if l.config.RequireAddressValidation { var ok bool - retrySrcConnID = p.dstConnID - originalDstConnID, ok = l.validateInitialAddress(now, p, m.addr) + cids.retrySrcConnID = p.dstConnID + cids.originalDstConnID, ok = l.validateInitialAddress(now, p, m.addr) if !ok { return } } else { - originalDstConnID = p.dstConnID + cids.originalDstConnID = p.dstConnID } var err error - c, err := l.newConn(now, serverSide, originalDstConnID, retrySrcConnID, m.addr) + c, err := l.newConn(now, serverSide, cids, m.addr) if err != nil { // The accept queue is probably full. // We could send a CONNECTION_CLOSE to the peer to reject the connection. diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 21717e2516..674d4e4a16 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -19,12 +19,12 @@ import ( ) func TestConnect(t *testing.T) { - newLocalConnPair(t, &Config{}, &Config{}) + NewLocalConnPair(t, &Config{}, &Config{}) } func TestStreamTransfer(t *testing.T) { ctx := context.Background() - cli, srv := newLocalConnPair(t, &Config{}, &Config{}) + cli, srv := NewLocalConnPair(t, &Config{}, &Config{}) data := makeTestData(1 << 20) srvdone := make(chan struct{}) @@ -61,11 +61,11 @@ func TestStreamTransfer(t *testing.T) { } } -func newLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverConn *Conn) { +func NewLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverConn *Conn) { t.Helper() ctx := context.Background() - l1 := newLocalListener(t, serverSide, conf1) - l2 := newLocalListener(t, clientSide, conf2) + l1 := NewLocalListener(t, serverSide, conf1) + l2 := NewLocalListener(t, clientSide, conf2) c2, err := l2.Dial(ctx, "udp", l1.LocalAddr().String()) if err != nil { t.Fatal(err) @@ -77,9 +77,11 @@ func newLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverCon return c2, c1 } -func newLocalListener(t *testing.T, side connSide, conf *Config) *Listener { +func NewLocalListener(t *testing.T, side connSide, conf *Config) *Listener { t.Helper() if conf.TLSConfig == nil { + newConf := *conf + conf = &newConf conf.TLSConfig = newTestTLSConfig(side) } l, err := Listen("udp", "127.0.0.1:0", conf) @@ -101,6 +103,7 @@ type testListener struct { conns map[*Conn]*testConn acceptQueue []*testConn configTransportParams []func(*transportParameters) + configTestConn []func(*testConn) sentDatagrams [][]byte peerTLSConn *tls.QUICConn lastInitialDstConnID []byte // for parsing Retry packets @@ -251,33 +254,6 @@ func (tl *testListener) wantIdle(expectation string) { } } -func (tl *testListener) newClientTLS(srcConnID, dstConnID []byte) []byte { - peerProvidedParams := defaultTransportParameters() - peerProvidedParams.initialSrcConnID = srcConnID - peerProvidedParams.originalDstConnID = dstConnID - for _, f := range tl.configTransportParams { - f(&peerProvidedParams) - } - - config := &tls.QUICConfig{TLSConfig: newTestTLSConfig(clientSide)} - tl.peerTLSConn = tls.QUICClient(config) - tl.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) - tl.peerTLSConn.Start(context.Background()) - var data []byte - for { - e := tl.peerTLSConn.NextEvent() - switch e.Kind { - case tls.QUICNoEvent: - return data - case tls.QUICWriteData: - if e.Level != tls.QUICEncryptionLevelInitial { - tl.t.Fatal("initial data at unexpected level") - } - data = append(data, e.Data...) - } - } -} - // advance causes time to pass. func (tl *testListener) advance(d time.Duration) { tl.t.Helper() From 434956a1a8671fa67f9ec468cda2fd83937227b7 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 3 Nov 2023 16:43:48 -0700 Subject: [PATCH 091/168] quic: include more detail in connection close errors When closing a connection with an error, include a reason string in the CONNECTION_CLOSE frame as well as the error code, when the code isn't sufficient to explain the error. Change-Id: I055a4e11b222e87d1ff01d8c45fcb7cc17fe4196 Reviewed-on: https://go-review.googlesource.com/c/net/+/539342 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_close.go | 4 +- internal/quic/conn_flow.go | 5 ++- internal/quic/conn_id.go | 61 ++++++++++++++++++++---------- internal/quic/conn_recv.go | 40 ++++++++++++++++---- internal/quic/conn_send.go | 2 +- internal/quic/conn_streams.go | 10 ++++- internal/quic/conn_test.go | 44 ++++++++++++++++++++- internal/quic/crypto_stream.go | 5 ++- internal/quic/errors.go | 10 ++++- internal/quic/listener.go | 2 +- internal/quic/packet_protection.go | 2 +- internal/quic/stream.go | 20 ++++++++-- internal/quic/stream_limits.go | 5 ++- internal/quic/stream_test.go | 3 +- internal/quic/transport_params.go | 26 ++++++------- 15 files changed, 178 insertions(+), 61 deletions(-) diff --git a/internal/quic/conn_close.go b/internal/quic/conn_close.go index b8b86fd6fb..daf425b76a 100644 --- a/internal/quic/conn_close.go +++ b/internal/quic/conn_close.go @@ -156,7 +156,7 @@ func (c *Conn) enterDraining(err error) { if c.isDraining() { return } - if e, ok := c.lifetime.localErr.(localTransportError); ok && transportError(e) != errNo { + if e, ok := c.lifetime.localErr.(localTransportError); ok && e.code != errNo { // If we've terminated the connection due to a peer protocol violation, // record the final error on the connection as our reason for termination. c.lifetime.finalErr = c.lifetime.localErr @@ -220,7 +220,7 @@ func (c *Conn) Wait(ctx context.Context) error { // Otherwise, Abort sends a transport error of APPLICATION_ERROR with the error's text. func (c *Conn) Abort(err error) { if err == nil { - err = localTransportError(errNo) + err = localTransportError{code: errNo} } c.sendMsg(func(now time.Time, c *Conn) { c.abort(now, err) diff --git a/internal/quic/conn_flow.go b/internal/quic/conn_flow.go index 4f1ab6eafc..8b69ef7dba 100644 --- a/internal/quic/conn_flow.go +++ b/internal/quic/conn_flow.go @@ -90,7 +90,10 @@ func (c *Conn) shouldUpdateFlowControl(credit int64) bool { func (c *Conn) handleStreamBytesReceived(n int64) error { c.streams.inflow.usedLimit += n if c.streams.inflow.usedLimit > c.streams.inflow.sentLimit { - return localTransportError(errFlowControl) + return localTransportError{ + code: errFlowControl, + reason: "stream exceeded flow control limit", + } } return nil } diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index b77ad8edf2..439c221237 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -210,25 +210,40 @@ func (s *connIDState) validateTransportParameters(c *Conn, isRetry bool, p trans // the transient remote connection ID we chose (client) // or is empty (server). if !bytes.Equal(s.originalDstConnID, p.originalDstConnID) { - return localTransportError(errTransportParameter) + return localTransportError{ + code: errTransportParameter, + reason: "original_destination_connection_id mismatch", + } } s.originalDstConnID = nil // we have no further need for this // Verify retry_source_connection_id matches the value from // the server's Retry packet (when one was sent), or is empty. if !bytes.Equal(p.retrySrcConnID, s.retrySrcConnID) { - return localTransportError(errTransportParameter) + return localTransportError{ + code: errTransportParameter, + reason: "retry_source_connection_id mismatch", + } } s.retrySrcConnID = nil // we have no further need for this // Verify initial_source_connection_id matches the first remote connection ID. if len(s.remote) == 0 || s.remote[0].seq != 0 { - return localTransportError(errInternal) + return localTransportError{ + code: errInternal, + reason: "remote connection id missing", + } } if !bytes.Equal(p.initialSrcConnID, s.remote[0].cid) { - return localTransportError(errTransportParameter) + return localTransportError{ + code: errTransportParameter, + reason: "initial_source_connection_id mismatch", + } } if len(p.statelessResetToken) > 0 { if c.side == serverSide { - return localTransportError(errTransportParameter) + return localTransportError{ + code: errTransportParameter, + reason: "client sent stateless_reset_token", + } } token := statelessResetToken(p.statelessResetToken) s.remote[0].resetToken = token @@ -255,17 +270,6 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) }, } } - case ptype == packetTypeInitial && c.side == serverSide: - if len(s.remote) == 0 { - // We're a server connection processing the first Initial packet - // from the client. Set the client's connection ID. - s.remote = append(s.remote, remoteConnID{ - connID: connID{ - seq: 0, - cid: cloneBytes(srcConnID), - }, - }) - } case ptype == packetTypeHandshake && c.side == serverSide: if len(s.local) > 0 && s.local[0].seq == -1 && !s.local[0].retired { // We're a server connection processing the first Handshake packet from @@ -294,7 +298,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re // Destination Connection ID MUST treat receipt of a NEW_CONNECTION_ID // frame as a connection error of type PROTOCOL_VIOLATION." // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.15-6 - return localTransportError(errProtocolViolation) + return localTransportError{ + code: errProtocolViolation, + reason: "NEW_CONNECTION_ID from peer with zero-length DCID", + } } if retire > s.retireRemotePriorTo { @@ -316,7 +323,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re } if rcid.seq == seq { if !bytes.Equal(rcid.cid, cid) { - return localTransportError(errProtocolViolation) + return localTransportError{ + code: errProtocolViolation, + reason: "NEW_CONNECTION_ID does not match prior id", + } } have = true // yes, we've seen this sequence number } @@ -350,7 +360,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re // Retired connection IDs (including newly-retired ones) do not count // against the limit. // https://www.rfc-editor.org/rfc/rfc9000.html#section-5.1.1-5 - return localTransportError(errConnectionIDLimit) + return localTransportError{ + code: errConnectionIDLimit, + reason: "active_connection_id_limit exceeded", + } } // "An endpoint SHOULD limit the number of connection IDs it has retired locally @@ -360,7 +373,10 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re // Set a limit of four times the active_connection_id_limit for // the total number of remote connection IDs we keep state for locally. if len(s.remote) > 4*activeConnIDLimit { - return localTransportError(errConnectionIDLimit) + return localTransportError{ + code: errConnectionIDLimit, + reason: "too many unacknowledged RETIRE_CONNECTION_ID frames", + } } return nil @@ -375,7 +391,10 @@ func (s *connIDState) retireRemote(rcid *remoteConnID) { func (s *connIDState) handleRetireConnID(c *Conn, seq int64) error { if seq >= s.nextLocalSeq { - return localTransportError(errProtocolViolation) + return localTransportError{ + code: errProtocolViolation, + reason: "RETIRE_CONNECTION_ID for unissued sequence number", + } } for i := range s.local { if s.local[i].seq == seq { diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index e966b7ef55..8fa3a39067 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -79,12 +79,18 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa if buf[0]&reservedLongBits != 0 { // Reserved header bits must be 0. // https://www.rfc-editor.org/rfc/rfc9000#section-17.2-8.2.1 - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "reserved header bits are not zero", + }) return -1 } if p.version != quicVersion1 { // The peer has changed versions on us mid-handshake? - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "protocol version changed during handshake", + }) return -1 } @@ -129,7 +135,10 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int { if buf[0]&reserved1RTTBits != 0 { // Reserved header bits must be 0. // https://www.rfc-editor.org/rfc/rfc9000#section-17.3.1-4.8.1 - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "reserved header bits are not zero", + }) return -1 } @@ -222,7 +231,10 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, // "An endpoint MUST treat receipt of a packet containing no frames // as a connection error of type PROTOCOL_VIOLATION." // https://www.rfc-editor.org/rfc/rfc9000#section-12.4-3 - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "packet contains no frames", + }) return false } // frameOK verifies that ptype is one of the packets in mask. @@ -232,7 +244,10 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, // that is not permitted as a connection error of type // PROTOCOL_VIOLATION." // https://www.rfc-editor.org/rfc/rfc9000#section-12.4-3 - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "frame not allowed in packet", + }) return false } return true @@ -347,7 +362,10 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, n = c.handleHandshakeDoneFrame(now, space, payload) } if n < 0 { - c.abort(now, localTransportError(errFrameEncoding)) + c.abort(now, localTransportError{ + code: errFrameEncoding, + reason: "frame encoding error", + }) return false } payload = payload[n:] @@ -360,7 +378,10 @@ func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) 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(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "acknowledgement for unsent packet", + }) return } c.loss.receiveAckRange(now, space, rangeIndex, start, end, c.handleAckOrLoss) @@ -521,7 +542,10 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa if c.side == serverSide { // Clients should never send HANDSHAKE_DONE. // https://www.rfc-editor.org/rfc/rfc9000#section-19.20-4 - c.abort(now, localTransportError(errProtocolViolation)) + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "client sent HANDSHAKE_DONE", + }) return -1 } if !c.isClosingOrDraining() { diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 64e5d75482..22e780479f 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -328,7 +328,7 @@ func (c *Conn) appendConnectionCloseFrame(now time.Time, space numberSpace, err c.lifetime.connCloseSentTime = now switch e := err.(type) { case localTransportError: - c.w.appendConnectionCloseTransportFrame(transportError(e), 0, "") + c.w.appendConnectionCloseTransportFrame(e.code, 0, e.reason) case *ApplicationError: if space != appDataSpace { // "CONNECTION_CLOSE frames signaling application errors (type 0x1d) diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index a0793297e1..83ab5554c4 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -127,7 +127,10 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) if (id.initiator() == c.side) != (ftype == sendStream) { // Received an invalid frame for unidirectional stream. // For example, a RESET_STREAM frame for a send-only stream. - c.abort(now, localTransportError(errStreamState)) + c.abort(now, localTransportError{ + code: errStreamState, + reason: "invalid frame for unidirectional stream", + }) return nil } } @@ -148,7 +151,10 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) } // Received a frame for a stream that should be originated by us, // but which we never created. - c.abort(now, localTransportError(errStreamState)) + c.abort(now, localTransportError{ + code: errStreamState, + reason: "received frame for unknown stream", + }) return nil } else { // if isOpen, this is a stream that was implicitly opened by a diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 248be9641b..c70c58ef00 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -594,6 +594,20 @@ func (tc *testConn) wantDatagram(expectation string, want *testDatagram) { } } +func datagramEqual(a, b *testDatagram) bool { + if a.paddedSize != b.paddedSize || + a.addr != b.addr || + len(a.packets) != len(b.packets) { + return false + } + for i := range a.packets { + if !packetEqual(a.packets[i], b.packets[i]) { + return false + } + } + return true +} + // wantPacket indicates that we expect the Conn to send a packet. func (tc *testConn) wantPacket(expectation string, want *testPacket) { tc.t.Helper() @@ -603,6 +617,25 @@ func (tc *testConn) wantPacket(expectation string, want *testPacket) { } } +func packetEqual(a, b *testPacket) bool { + ac := *a + ac.frames = nil + bc := *b + bc.frames = nil + if !reflect.DeepEqual(ac, bc) { + return false + } + if len(a.frames) != len(b.frames) { + return false + } + for i := range a.frames { + if !frameEqual(a.frames[i], b.frames[i]) { + return false + } + } + return true +} + // wantFrame indicates that we expect the Conn to send a frame. func (tc *testConn) wantFrame(expectation string, wantType packetType, want debugFrame) { tc.t.Helper() @@ -613,11 +646,20 @@ func (tc *testConn) wantFrame(expectation string, wantType packetType, want debu if gotType != wantType { tc.t.Fatalf("%v:\ngot %v packet, want %v\ngot frame: %v", expectation, gotType, wantType, got) } - if !reflect.DeepEqual(got, want) { + if !frameEqual(got, want) { tc.t.Fatalf("%v:\ngot frame: %v\nwant frame: %v", expectation, got, want) } } +func frameEqual(a, b debugFrame) bool { + switch af := a.(type) { + case debugFrameConnectionCloseTransport: + bf, ok := b.(debugFrameConnectionCloseTransport) + return ok && af.code == bf.code + } + return reflect.DeepEqual(a, b) +} + // wantFrameType indicates that we expect the Conn to send a frame, // although we don't care about the contents. func (tc *testConn) wantFrameType(expectation string, wantType packetType, want debugFrame) { diff --git a/internal/quic/crypto_stream.go b/internal/quic/crypto_stream.go index 8aa8f7b828..a4dcb32eb7 100644 --- a/internal/quic/crypto_stream.go +++ b/internal/quic/crypto_stream.go @@ -30,7 +30,10 @@ type cryptoStream struct { func (s *cryptoStream) handleCrypto(off int64, b []byte, f func([]byte) error) error { end := off + int64(len(b)) if end-s.inset.min() > cryptoBufferSize { - return localTransportError(errCryptoBufferExceeded) + return localTransportError{ + code: errCryptoBufferExceeded, + reason: "crypto buffer exceeded", + } } s.inset.add(off, end) if off == s.in.start { diff --git a/internal/quic/errors.go b/internal/quic/errors.go index 8e01bb7cb7..954793cfc0 100644 --- a/internal/quic/errors.go +++ b/internal/quic/errors.go @@ -83,10 +83,16 @@ func (e transportError) String() string { } // A localTransportError is an error sent to the peer. -type localTransportError transportError +type localTransportError struct { + code transportError + reason string +} func (e localTransportError) Error() string { - return "closed connection: " + transportError(e).String() + if e.reason == "" { + return fmt.Sprintf("closed connection: %v", e.code) + } + return fmt.Sprintf("closed connection: %v: %q", e.code, e.reason) } // A peerTransportError is an error received from the peer. diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 24484eb6f2..8b31dcbe88 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -107,7 +107,7 @@ func (l *Listener) Close(ctx context.Context) error { if !l.closing { l.closing = true for c := range l.conns { - c.Abort(localTransportError(errNo)) + c.Abort(localTransportError{code: errNo}) } if len(l.conns) == 0 { l.udpConn.Close() diff --git a/internal/quic/packet_protection.go b/internal/quic/packet_protection.go index 7b141ac49e..1f939f491d 100644 --- a/internal/quic/packet_protection.go +++ b/internal/quic/packet_protection.go @@ -441,7 +441,7 @@ func (k *updatingKeyPair) unprotect(pkt []byte, pnumOff int, pnumMax packetNumbe if err != nil { k.authFailures++ if k.authFailures >= aeadIntegrityLimit(k.r.suite) { - return nil, 0, localTransportError(errAEADLimitReached) + return nil, 0, localTransportError{code: errAEADLimitReached} } return nil, 0, err } diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 89036b19b6..58d84ed1b0 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -567,19 +567,31 @@ func (s *Stream) handleReset(code uint64, finalSize int64) error { func (s *Stream) checkStreamBounds(end int64, fin bool) error { if end > s.inwin { // The peer sent us data past the maximum flow control window we gave them. - return localTransportError(errFlowControl) + return localTransportError{ + code: errFlowControl, + reason: "stream flow control window exceeded", + } } if s.insize != -1 && end > s.insize { // The peer sent us data past the final size of the stream they previously gave us. - return localTransportError(errFinalSize) + return localTransportError{ + code: errFinalSize, + reason: "data received past end of stream", + } } if fin && s.insize != -1 && end != s.insize { // The peer changed the final size of the stream. - return localTransportError(errFinalSize) + return localTransportError{ + code: errFinalSize, + reason: "final size of stream changed", + } } if fin && end < s.in.end { // The peer has previously sent us data past the final size. - return localTransportError(errFinalSize) + return localTransportError{ + code: errFinalSize, + reason: "end of stream occurs before prior data", + } } return nil } diff --git a/internal/quic/stream_limits.go b/internal/quic/stream_limits.go index 6eda7883b9..2f42cf4180 100644 --- a/internal/quic/stream_limits.go +++ b/internal/quic/stream_limits.go @@ -66,7 +66,10 @@ func (lim *remoteStreamLimits) init(maxOpen int64) { func (lim *remoteStreamLimits) open(id streamID) error { num := id.num() if num >= lim.max { - return localTransportError(errStreamLimit) + return localTransportError{ + code: errStreamLimit, + reason: "stream limit exceeded", + } } if num >= lim.opened { lim.opened = num + 1 diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 7c1377faee..9bf2b5871d 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -13,7 +13,6 @@ import ( "errors" "fmt" "io" - "reflect" "strings" "testing" ) @@ -848,7 +847,7 @@ func TestStreamOffsetTooLarge(t *testing.T) { got, _ := tc.readFrame() want1 := debugFrameConnectionCloseTransport{code: errFrameEncoding} want2 := debugFrameConnectionCloseTransport{code: errFlowControl} - if !reflect.DeepEqual(got, want1) && !reflect.DeepEqual(got, want2) { + if !frameEqual(got, want1) && !frameEqual(got, want2) { t.Fatalf("STREAM offset exceeds 2^62-1\ngot: %v\nwant: %v\n or: %v", got, want1, want2) } } diff --git a/internal/quic/transport_params.go b/internal/quic/transport_params.go index dc76d16509..3cc56f4e44 100644 --- a/internal/quic/transport_params.go +++ b/internal/quic/transport_params.go @@ -169,12 +169,12 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { for len(params) > 0 { id, n := consumeVarint(params) if n < 0 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } params = params[n:] val, n := consumeVarintBytes(params) if n < 0 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } params = params[n:] n = 0 @@ -193,14 +193,14 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { p.maxIdleTimeout = time.Duration(v) * time.Millisecond case paramStatelessResetToken: if len(val) != 16 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } p.statelessResetToken = val n = 16 case paramMaxUDPPayloadSize: p.maxUDPPayloadSize, n = consumeVarintInt64(val) if p.maxUDPPayloadSize < 1200 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } case paramInitialMaxData: p.initialMaxData, n = consumeVarintInt64(val) @@ -213,32 +213,32 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { case paramInitialMaxStreamsBidi: p.initialMaxStreamsBidi, n = consumeVarintInt64(val) if p.initialMaxStreamsBidi > maxStreamsLimit { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } case paramInitialMaxStreamsUni: p.initialMaxStreamsUni, n = consumeVarintInt64(val) if p.initialMaxStreamsUni > maxStreamsLimit { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } case paramAckDelayExponent: var v uint64 v, n = consumeVarint(val) if v > 20 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } p.ackDelayExponent = int8(v) case paramMaxAckDelay: var v uint64 v, n = consumeVarint(val) if v >= 1<<14 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } p.maxAckDelay = time.Duration(v) * time.Millisecond case paramDisableActiveMigration: p.disableActiveMigration = true case paramPreferredAddress: if len(val) < 4+2+16+2+1 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } p.preferredAddrV4 = netip.AddrPortFrom( netip.AddrFrom4(*(*[4]byte)(val[:4])), @@ -253,18 +253,18 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { var nn int p.preferredAddrConnID, nn = consumeUint8Bytes(val) if nn < 0 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } val = val[nn:] if len(val) != 16 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } p.preferredAddrResetToken = val val = nil case paramActiveConnectionIDLimit: p.activeConnIDLimit, n = consumeVarintInt64(val) if p.activeConnIDLimit < 2 { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } case paramInitialSourceConnectionID: p.initialSrcConnID = val @@ -276,7 +276,7 @@ func unmarshalTransportParams(params []byte) (transportParameters, error) { n = len(val) } if n != len(val) { - return p, localTransportError(errTransportParameter) + return p, localTransportError{code: errTransportParameter} } } return p, nil From 45fa4142093c874c1b07cada45cf09b1dff803f8 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 6 Nov 2023 10:12:03 -0800 Subject: [PATCH 092/168] quic: undo accidental rename of test helpers https://go.dev/cl/539341 inadvertently made the newLocalConnPair and newLocalListener helpers exported. These are test-only functions, so the change isn't really important, but undo the rename to keep them consistent with other test helpers. Change-Id: Ie3860db3584fc83c0c0aa2ad0dda4cc5cb03351a Reviewed-on: https://go-review.googlesource.com/c/net/+/540116 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/listener_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 674d4e4a16..037fb21b40 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -19,12 +19,12 @@ import ( ) func TestConnect(t *testing.T) { - NewLocalConnPair(t, &Config{}, &Config{}) + newLocalConnPair(t, &Config{}, &Config{}) } func TestStreamTransfer(t *testing.T) { ctx := context.Background() - cli, srv := NewLocalConnPair(t, &Config{}, &Config{}) + cli, srv := newLocalConnPair(t, &Config{}, &Config{}) data := makeTestData(1 << 20) srvdone := make(chan struct{}) @@ -61,11 +61,11 @@ func TestStreamTransfer(t *testing.T) { } } -func NewLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverConn *Conn) { +func newLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverConn *Conn) { t.Helper() ctx := context.Background() - l1 := NewLocalListener(t, serverSide, conf1) - l2 := NewLocalListener(t, clientSide, conf2) + l1 := newLocalListener(t, serverSide, conf1) + l2 := newLocalListener(t, clientSide, conf2) c2, err := l2.Dial(ctx, "udp", l1.LocalAddr().String()) if err != nil { t.Fatal(err) @@ -77,7 +77,7 @@ func NewLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverCon return c2, c1 } -func NewLocalListener(t *testing.T, side connSide, conf *Config) *Listener { +func newLocalListener(t *testing.T, side connSide, conf *Config) *Listener { t.Helper() if conf.TLSConfig == nil { newConf := *conf From 39c9d01355726eae01c9adf4745b7f05e4734576 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 6 Nov 2023 11:22:05 -0800 Subject: [PATCH 093/168] quic: don't send CONNECTION_CLOSE after stateless reset After receiving a stateless reset, we must enter the draining state and send no further packets (including CONNECTION_CLOSE). We were sending one last CONNECTION_CLOSE after the user closed the Conn; fix this. RFC 9000, Section 10.3.1. Change-Id: I6a9cc6019470a25476df518022a32eefe0c50fcd Reviewed-on: https://go-review.googlesource.com/c/net/+/540117 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_close.go | 16 +++++++++++----- internal/quic/conn_recv.go | 10 +++++----- internal/quic/listener.go | 14 +++++++------- internal/quic/stateless_reset_test.go | 5 ++++- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/internal/quic/conn_close.go b/internal/quic/conn_close.go index daf425b76a..a9ef0db5e3 100644 --- a/internal/quic/conn_close.go +++ b/internal/quic/conn_close.go @@ -62,7 +62,7 @@ func (c *Conn) lifetimeAdvance(now time.Time) (done bool) { c.lifetime.drainEndTime = time.Time{} if c.lifetime.finalErr == nil { // The peer never responded to our CONNECTION_CLOSE. - c.enterDraining(errNoPeerResponse) + c.enterDraining(now, errNoPeerResponse) } return true } @@ -152,11 +152,17 @@ func (c *Conn) sendOK(now time.Time) bool { } // enterDraining enters the draining state. -func (c *Conn) enterDraining(err error) { +func (c *Conn) enterDraining(now time.Time, err error) { if c.isDraining() { return } - if e, ok := c.lifetime.localErr.(localTransportError); ok && e.code != errNo { + if err == errStatelessReset { + // If we've received a stateless reset, then we must not send a CONNECTION_CLOSE. + // Setting connCloseSentTime here prevents us from doing so. + c.lifetime.finalErr = errStatelessReset + c.lifetime.localErr = errStatelessReset + c.lifetime.connCloseSentTime = now + } else if e, ok := c.lifetime.localErr.(localTransportError); ok && e.code != errNo { // If we've terminated the connection due to a peer protocol violation, // record the final error on the connection as our reason for termination. c.lifetime.finalErr = c.lifetime.localErr @@ -239,14 +245,14 @@ func (c *Conn) abort(now time.Time, err error) { // The connection does not send a CONNECTION_CLOSE, and skips the draining period. func (c *Conn) abortImmediately(now time.Time, err error) { c.abort(now, err) - c.enterDraining(err) + c.enterDraining(now, err) c.exited = true } // exit fully terminates a connection immediately. func (c *Conn) exit() { c.sendMsg(func(now time.Time, c *Conn) { - c.enterDraining(errors.New("connection closed")) + c.enterDraining(now, errors.New("connection closed")) c.exited = true }) } diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 8fa3a39067..896c6d74ed 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -56,7 +56,7 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { if len(buf) == len(dgram.b) && len(buf) > statelessResetTokenLen { var token statelessResetToken copy(token[:], buf[len(buf)-len(token):]) - c.handleStatelessReset(token) + c.handleStatelessReset(now, token) } // Invalid data at the end of a datagram is ignored. break @@ -525,7 +525,7 @@ func (c *Conn) handleConnectionCloseTransportFrame(now time.Time, payload []byte if n < 0 { return -1 } - c.enterDraining(peerTransportError{code: code, reason: reason}) + c.enterDraining(now, peerTransportError{code: code, reason: reason}) return n } @@ -534,7 +534,7 @@ func (c *Conn) handleConnectionCloseApplicationFrame(now time.Time, payload []by if n < 0 { return -1 } - c.enterDraining(&ApplicationError{Code: code, Reason: reason}) + c.enterDraining(now, &ApplicationError{Code: code, Reason: reason}) return n } @@ -556,9 +556,9 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa var errStatelessReset = errors.New("received stateless reset") -func (c *Conn) handleStatelessReset(resetToken statelessResetToken) { +func (c *Conn) handleStatelessReset(now time.Time, resetToken statelessResetToken) { if !c.connIDState.isValidStatelessResetToken(resetToken) { return } - c.enterDraining(errStatelessReset) + c.enterDraining(now, errStatelessReset) } diff --git a/internal/quic/listener.go b/internal/quic/listener.go index 8b31dcbe88..ca8f9b25a7 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -253,12 +253,18 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { if len(m.b) < minimumValidPacketSize { return } + var now time.Time + if l.testHooks != nil { + now = l.testHooks.timeNow() + } else { + now = time.Now() + } // Check to see if this is a stateless reset. var token statelessResetToken copy(token[:], m.b[len(m.b)-len(token):]) if c := l.connsMap.byResetToken[token]; c != nil { c.sendMsg(func(now time.Time, c *Conn) { - c.handleStatelessReset(token) + c.handleStatelessReset(now, token) }) return } @@ -290,12 +296,6 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-16 return } - var now time.Time - if l.testHooks != nil { - now = l.testHooks.timeNow() - } else { - now = time.Now() - } cids := newServerConnIDs{ srcConnID: p.srcConnID, dstConnID: p.dstConnID, diff --git a/internal/quic/stateless_reset_test.go b/internal/quic/stateless_reset_test.go index b12e975607..8a16597c4e 100644 --- a/internal/quic/stateless_reset_test.go +++ b/internal/quic/stateless_reset_test.go @@ -14,6 +14,7 @@ import ( "errors" "net/netip" "testing" + "time" ) func TestStatelessResetClientSendsStatelessResetTokenTransportParameter(t *testing.T) { @@ -154,7 +155,9 @@ func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) { if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { t.Errorf("conn.Wait() = %v, want errStatelessReset", err) } - tc.wantIdle("closed connection is idle") + tc.wantIdle("closed connection is idle in draining") + tc.advance(1 * time.Second) // long enough to exit the draining state + tc.wantIdle("closed connection is idle after draining") } func TestStatelessResetSuccessfulTransportParameter(t *testing.T) { From 26ea8175a156eecf99974b64a6906b0fa4c76532 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 10 Oct 2023 15:32:20 +0200 Subject: [PATCH 094/168] http2: unconditionally recycle responseWriterState CL 46008 fixed golang/go#20704 by not recycling the responseWriterState if any previous Write call failed, as there could be outstanding goroutines referencing the responseWriterState memory. More recently, CL 467355 fixed a variant of the same issue by not referencing that memory after exiting the Write call. This fix supersedes the fix in CL 46008, as it is more general and does not require the caller to know whether any previous Write calls failed. This CL partially reverts CL 46008 just leaving the test case to ensure that golang/go#20704 does not regress. Change-Id: I18ea4d27420265a94cc7af21f1dffa3f7dc3bd34 Reviewed-on: https://go-review.googlesource.com/c/net/+/534315 TryBot-Result: Gopher Robot Auto-Submit: Damien Neil Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Run-TryBot: Quim Muntal Commit-Queue: Damien Neil Reviewed-by: Bryan Mills Reviewed-by: Dmitri Shuralyov --- http2/server.go | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/http2/server.go b/http2/server.go index 7f3bed9261..ae94c6408d 100644 --- a/http2/server.go +++ b/http2/server.go @@ -2549,7 +2549,6 @@ type responseWriterState struct { wroteHeader bool // WriteHeader called (explicitly or implicitly). Not necessarily sent to user yet. sentHeader bool // have we sent the header frame? handlerDone bool // handler has finished - dirty bool // a Write failed; don't reuse this responseWriterState sentContentLen int64 // non-zero if handler set a Content-Length header wroteBytes int64 @@ -2669,7 +2668,6 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) { date: date, }) if err != nil { - rws.dirty = true return 0, err } if endStream { @@ -2690,7 +2688,6 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) { if len(p) > 0 || endStream { // only send a 0 byte DATA frame if we're ending the stream. if err := rws.conn.writeDataFromHandler(rws.stream, p, endStream); err != nil { - rws.dirty = true return 0, err } } @@ -2702,9 +2699,6 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) { trailers: rws.trailers, endStream: true, }) - if err != nil { - rws.dirty = true - } return len(p), err } return len(p), nil @@ -2920,14 +2914,12 @@ func (rws *responseWriterState) writeHeader(code int) { h.Del("Transfer-Encoding") } - if rws.conn.writeHeaders(rws.stream, &writeResHeaders{ + rws.conn.writeHeaders(rws.stream, &writeResHeaders{ streamID: rws.stream.id, httpResCode: code, h: h, endStream: rws.handlerDone && !rws.hasTrailers(), - }) != nil { - rws.dirty = true - } + }) return } @@ -2992,19 +2984,10 @@ func (w *responseWriter) write(lenData int, dataB []byte, dataS string) (n int, func (w *responseWriter) handlerDone() { rws := w.rws - dirty := rws.dirty rws.handlerDone = true w.Flush() w.rws = nil - if !dirty { - // Only recycle the pool if all prior Write calls to - // the serverConn goroutine completed successfully. If - // they returned earlier due to resets from the peer - // there might still be write goroutines outstanding - // from the serverConn referencing the rws memory. See - // issue 20704. - responseWriterStatePool.Put(rws) - } + responseWriterStatePool.Put(rws) } // Push errors. From a7ef1a2680592dd6662ec0bde823885ae1f3fa5a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 6 Nov 2023 15:22:28 -0800 Subject: [PATCH 095/168] internal/quic/cmd/interop: don't t.Log after test finishes Fixes golang/go#63971 Change-Id: I795356202880daa2d4a0cfd019c542e5820e8020 Reviewed-on: https://go-review.googlesource.com/c/net/+/539857 Reviewed-by: Bryan Mills Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI --- internal/quic/cmd/interop/main_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/quic/cmd/interop/main_test.go b/internal/quic/cmd/interop/main_test.go index 6fd9c0f2d5..4119740e6c 100644 --- a/internal/quic/cmd/interop/main_test.go +++ b/internal/quic/cmd/interop/main_test.go @@ -72,9 +72,9 @@ func run(ctx context.Context, t *testing.T, name, testcase string, args []string addrc := make(chan string, 1) donec := make(chan struct{}) go func() { - defer t.Logf("%v done", name) defer close(addrc) defer close(donec) + defer t.Logf("%v done", name) s := bufio.NewScanner(out) for s.Scan() { line := s.Text() From a720b30cbc2733d7b4daaab7a8cac65f3ff5131d Mon Sep 17 00:00:00 2001 From: Jorropo Date: Sun, 5 Nov 2023 06:54:42 +0100 Subject: [PATCH 096/168] http2: allocate buffer pools using pointers to arrays This remove the allocation for the slice header in sync.Pool.New and putDataBufferChunk. It also divide the number of allocations kept alive by the pool. Change-Id: Icf493ebc568ae80a4e73e9768a6f1c7fce8e1365 Reviewed-on: https://go-review.googlesource.com/c/net/+/539915 Reviewed-by: Bryan Mills Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Brad Fitzpatrick Reviewed-by: Damien Neil TryBot-Result: Gopher Robot Run-TryBot: qiulaidongfeng <2645477756@qq.com> Reviewed-by: qiulaidongfeng <2645477756@qq.com> --- http2/databuffer.go | 59 ++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/http2/databuffer.go b/http2/databuffer.go index a3067f8de7..e6f55cbd16 100644 --- a/http2/databuffer.go +++ b/http2/databuffer.go @@ -20,41 +20,44 @@ import ( // TODO: Benchmark to determine if the pools are necessary. The GC may have // improved enough that we can instead allocate chunks like this: // make([]byte, max(16<<10, expectedBytesRemaining)) -var ( - dataChunkSizeClasses = []int{ - 1 << 10, - 2 << 10, - 4 << 10, - 8 << 10, - 16 << 10, - } - dataChunkPools = [...]sync.Pool{ - {New: func() interface{} { return make([]byte, 1<<10) }}, - {New: func() interface{} { return make([]byte, 2<<10) }}, - {New: func() interface{} { return make([]byte, 4<<10) }}, - {New: func() interface{} { return make([]byte, 8<<10) }}, - {New: func() interface{} { return make([]byte, 16<<10) }}, - } -) +var dataChunkPools = [...]sync.Pool{ + {New: func() interface{} { return new([1 << 10]byte) }}, + {New: func() interface{} { return new([2 << 10]byte) }}, + {New: func() interface{} { return new([4 << 10]byte) }}, + {New: func() interface{} { return new([8 << 10]byte) }}, + {New: func() interface{} { return new([16 << 10]byte) }}, +} func getDataBufferChunk(size int64) []byte { - i := 0 - for ; i < len(dataChunkSizeClasses)-1; i++ { - if size <= int64(dataChunkSizeClasses[i]) { - break - } + switch { + case size <= 1<<10: + return dataChunkPools[0].Get().(*[1 << 10]byte)[:] + case size <= 2<<10: + return dataChunkPools[1].Get().(*[2 << 10]byte)[:] + case size <= 4<<10: + return dataChunkPools[2].Get().(*[4 << 10]byte)[:] + case size <= 8<<10: + return dataChunkPools[3].Get().(*[8 << 10]byte)[:] + default: + return dataChunkPools[4].Get().(*[16 << 10]byte)[:] } - return dataChunkPools[i].Get().([]byte) } func putDataBufferChunk(p []byte) { - for i, n := range dataChunkSizeClasses { - if len(p) == n { - dataChunkPools[i].Put(p) - return - } + switch len(p) { + case 1 << 10: + dataChunkPools[0].Put((*[1 << 10]byte)(p)) + case 2 << 10: + dataChunkPools[1].Put((*[2 << 10]byte)(p)) + case 4 << 10: + dataChunkPools[2].Put((*[4 << 10]byte)(p)) + case 8 << 10: + dataChunkPools[3].Put((*[8 << 10]byte)(p)) + case 16 << 10: + dataChunkPools[4].Put((*[16 << 10]byte)(p)) + default: + panic(fmt.Sprintf("unexpected buffer len=%v", len(p))) } - panic(fmt.Sprintf("unexpected buffer len=%v", len(p))) } // dataBuffer is an io.ReadWriter backed by a list of data chunks. From cc6f4d19f58efdd0be1ead82856f76a446496516 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 6 Nov 2023 20:04:58 -0800 Subject: [PATCH 097/168] http2: remove ancient build-tagged files for unsupported Go versions x/net requires Go 1.18. No need to keep untested Go 1.11, Go 1.15, etc support around. Change-Id: I3588d273b543dec9ca120894ab36255f845abc20 Reviewed-on: https://go-review.googlesource.com/c/net/+/540236 Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Heschi Kreinick Reviewed-by: Christopher Taylor Run-TryBot: Brad Fitzpatrick TryBot-Result: Gopher Robot --- http2/go111.go | 29 ------ http2/go115.go | 26 ------ http2/go118.go | 16 ---- http2/not_go111.go | 20 ---- http2/not_go115.go | 30 ------ http2/not_go118.go | 16 ---- http2/transport.go | 33 ++++++- http2/transport_go117_test.go | 168 ---------------------------------- http2/transport_test.go | 151 ++++++++++++++++++++++++++++++ 9 files changed, 183 insertions(+), 306 deletions(-) delete mode 100644 http2/go111.go delete mode 100644 http2/go115.go delete mode 100644 http2/go118.go delete mode 100644 http2/not_go111.go delete mode 100644 http2/not_go115.go delete mode 100644 http2/not_go118.go delete mode 100644 http2/transport_go117_test.go diff --git a/http2/go111.go b/http2/go111.go deleted file mode 100644 index 4ced74a0b4..0000000000 --- a/http2/go111.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2018 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.11 - -package http2 - -import ( - "net/http/httptrace" - "net/textproto" -) - -func traceHasWroteHeaderField(trace *httptrace.ClientTrace) bool { - return trace != nil && trace.WroteHeaderField != nil -} - -func traceWroteHeaderField(trace *httptrace.ClientTrace, k, v string) { - if trace != nil && trace.WroteHeaderField != nil { - trace.WroteHeaderField(k, []string{v}) - } -} - -func traceGot1xxResponseFunc(trace *httptrace.ClientTrace) func(int, textproto.MIMEHeader) error { - if trace != nil { - return trace.Got1xxResponse - } - return nil -} diff --git a/http2/go115.go b/http2/go115.go deleted file mode 100644 index 13a2d9215d..0000000000 --- a/http2/go115.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021 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.15 - -package http2 - -import ( - "context" - "crypto/tls" -) - -// dialTLSWithContext uses tls.Dialer, added in Go 1.15, to open a TLS -// connection. -func (t *Transport) dialTLSWithContext(ctx context.Context, network, addr string, cfg *tls.Config) (*tls.Conn, error) { - dialer := &tls.Dialer{ - Config: cfg, - } - cn, err := dialer.DialContext(ctx, network, addr) - if err != nil { - return nil, err - } - tlsCn := cn.(*tls.Conn) // DialContext comment promises this will always succeed - return tlsCn, nil -} diff --git a/http2/go118.go b/http2/go118.go deleted file mode 100644 index a445ae1d58..0000000000 --- a/http2/go118.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2021 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.18 - -package http2 - -import ( - "crypto/tls" - "net" -) - -func tlsUnderlyingConn(tc *tls.Conn) net.Conn { - return tc.NetConn() -} diff --git a/http2/not_go111.go b/http2/not_go111.go deleted file mode 100644 index f4d63f458b..0000000000 --- a/http2/not_go111.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2018 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.11 - -package http2 - -import ( - "net/http/httptrace" - "net/textproto" -) - -func traceHasWroteHeaderField(trace *httptrace.ClientTrace) bool { return false } - -func traceWroteHeaderField(trace *httptrace.ClientTrace, k, v string) {} - -func traceGot1xxResponseFunc(trace *httptrace.ClientTrace) func(int, textproto.MIMEHeader) error { - return nil -} diff --git a/http2/not_go115.go b/http2/not_go115.go deleted file mode 100644 index 635753408d..0000000000 --- a/http2/not_go115.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2021 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.15 - -package http2 - -import ( - "context" - "crypto/tls" -) - -// dialTLSWithContext opens a TLS connection. -func (t *Transport) dialTLSWithContext(ctx context.Context, network, addr string, cfg *tls.Config) (*tls.Conn, error) { - cn, err := tls.Dial(network, addr, cfg) - if err != nil { - return nil, err - } - if err := cn.Handshake(); err != nil { - return nil, err - } - if cfg.InsecureSkipVerify { - return cn, nil - } - if err := cn.VerifyHostname(cfg.ServerName); err != nil { - return nil, err - } - return cn, nil -} diff --git a/http2/not_go118.go b/http2/not_go118.go deleted file mode 100644 index b1b11c072d..0000000000 --- a/http2/not_go118.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2021 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.18 - -package http2 - -import ( - "crypto/tls" - "net" -) - -func tlsUnderlyingConn(tc *tls.Conn) net.Conn { - return nil -} diff --git a/http2/transport.go b/http2/transport.go index 4515b22c4a..df578b86c6 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -1018,7 +1018,7 @@ func (cc *ClientConn) forceCloseConn() { if !ok { return } - if nc := tlsUnderlyingConn(tc); nc != nil { + if nc := tc.NetConn(); nc != nil { nc.Close() } } @@ -3201,3 +3201,34 @@ func traceFirstResponseByte(trace *httptrace.ClientTrace) { trace.GotFirstResponseByte() } } + +func traceHasWroteHeaderField(trace *httptrace.ClientTrace) bool { + return trace != nil && trace.WroteHeaderField != nil +} + +func traceWroteHeaderField(trace *httptrace.ClientTrace, k, v string) { + if trace != nil && trace.WroteHeaderField != nil { + trace.WroteHeaderField(k, []string{v}) + } +} + +func traceGot1xxResponseFunc(trace *httptrace.ClientTrace) func(int, textproto.MIMEHeader) error { + if trace != nil { + return trace.Got1xxResponse + } + return nil +} + +// dialTLSWithContext uses tls.Dialer, added in Go 1.15, to open a TLS +// connection. +func (t *Transport) dialTLSWithContext(ctx context.Context, network, addr string, cfg *tls.Config) (*tls.Conn, error) { + dialer := &tls.Dialer{ + Config: cfg, + } + cn, err := dialer.DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + tlsCn := cn.(*tls.Conn) // DialContext comment promises this will always succeed + return tlsCn, nil +} diff --git a/http2/transport_go117_test.go b/http2/transport_go117_test.go deleted file mode 100644 index 0f257ad241..0000000000 --- a/http2/transport_go117_test.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2021 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.17 - -package http2 - -import ( - "context" - "crypto/tls" - "errors" - "net/http" - "net/http/httptest" - - "testing" -) - -func TestTransportDialTLSContext(t *testing.T) { - blockCh := make(chan struct{}) - serverTLSConfigFunc := func(ts *httptest.Server) { - ts.Config.TLSConfig = &tls.Config{ - // Triggers the server to request the clients certificate - // during TLS handshake. - ClientAuth: tls.RequestClientCert, - } - } - ts := newServerTester(t, - func(w http.ResponseWriter, r *http.Request) {}, - optOnlyServer, - serverTLSConfigFunc, - ) - defer ts.Close() - tr := &Transport{ - TLSClientConfig: &tls.Config{ - GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { - // Tests that the context provided to `req` is - // passed into this function. - close(blockCh) - <-cri.Context().Done() - return nil, cri.Context().Err() - }, - InsecureSkipVerify: true, - }, - } - defer tr.CloseIdleConnections() - req, err := http.NewRequest(http.MethodGet, ts.ts.URL, nil) - if err != nil { - t.Fatal(err) - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - req = req.WithContext(ctx) - errCh := make(chan error) - go func() { - defer close(errCh) - res, err := tr.RoundTrip(req) - if err != nil { - errCh <- err - return - } - res.Body.Close() - }() - // Wait for GetClientCertificate handler to be called - <-blockCh - // Cancel the context - cancel() - // Expect the cancellation error here - err = <-errCh - if err == nil { - t.Fatal("cancelling context during client certificate fetch did not error as expected") - return - } - if !errors.Is(err, context.Canceled) { - t.Fatalf("unexpected error returned after cancellation: %v", err) - } -} - -// TestDialRaceResumesDial tests that, given two concurrent requests -// to the same address, when the first Dial is interrupted because -// the first request's context is cancelled, the second request -// resumes the dial automatically. -func TestDialRaceResumesDial(t *testing.T) { - blockCh := make(chan struct{}) - serverTLSConfigFunc := func(ts *httptest.Server) { - ts.Config.TLSConfig = &tls.Config{ - // Triggers the server to request the clients certificate - // during TLS handshake. - ClientAuth: tls.RequestClientCert, - } - } - ts := newServerTester(t, - func(w http.ResponseWriter, r *http.Request) {}, - optOnlyServer, - serverTLSConfigFunc, - ) - defer ts.Close() - tr := &Transport{ - TLSClientConfig: &tls.Config{ - GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { - select { - case <-blockCh: - // If we already errored, return without error. - return &tls.Certificate{}, nil - default: - } - close(blockCh) - <-cri.Context().Done() - return nil, cri.Context().Err() - }, - InsecureSkipVerify: true, - }, - } - defer tr.CloseIdleConnections() - req, err := http.NewRequest(http.MethodGet, ts.ts.URL, nil) - if err != nil { - t.Fatal(err) - } - // Create two requests with independent cancellation. - ctx1, cancel1 := context.WithCancel(context.Background()) - defer cancel1() - req1 := req.WithContext(ctx1) - ctx2, cancel2 := context.WithCancel(context.Background()) - defer cancel2() - req2 := req.WithContext(ctx2) - errCh := make(chan error) - go func() { - res, err := tr.RoundTrip(req1) - if err != nil { - errCh <- err - return - } - res.Body.Close() - }() - successCh := make(chan struct{}) - go func() { - // Don't start request until first request - // has initiated the handshake. - <-blockCh - res, err := tr.RoundTrip(req2) - if err != nil { - errCh <- err - return - } - res.Body.Close() - // Close successCh to indicate that the second request - // made it to the server successfully. - close(successCh) - }() - // Wait for GetClientCertificate handler to be called - <-blockCh - // Cancel the context first - cancel1() - // Expect the cancellation error here - err = <-errCh - if err == nil { - t.Fatal("cancelling context during client certificate fetch did not error as expected") - return - } - if !errors.Is(err, context.Canceled) { - t.Fatalf("unexpected error returned after cancellation: %v", err) - } - select { - case err := <-errCh: - t.Fatalf("unexpected second error: %v", err) - case <-successCh: - } -} diff --git a/http2/transport_test.go b/http2/transport_test.go index 99848485b9..a81131f294 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -6369,3 +6369,154 @@ func TestTransportSlowClose(t *testing.T) { } res.Body.Close() } + +func TestTransportDialTLSContext(t *testing.T) { + blockCh := make(chan struct{}) + serverTLSConfigFunc := func(ts *httptest.Server) { + ts.Config.TLSConfig = &tls.Config{ + // Triggers the server to request the clients certificate + // during TLS handshake. + ClientAuth: tls.RequestClientCert, + } + } + ts := newServerTester(t, + func(w http.ResponseWriter, r *http.Request) {}, + optOnlyServer, + serverTLSConfigFunc, + ) + defer ts.Close() + tr := &Transport{ + TLSClientConfig: &tls.Config{ + GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { + // Tests that the context provided to `req` is + // passed into this function. + close(blockCh) + <-cri.Context().Done() + return nil, cri.Context().Err() + }, + InsecureSkipVerify: true, + }, + } + defer tr.CloseIdleConnections() + req, err := http.NewRequest(http.MethodGet, ts.ts.URL, nil) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + req = req.WithContext(ctx) + errCh := make(chan error) + go func() { + defer close(errCh) + res, err := tr.RoundTrip(req) + if err != nil { + errCh <- err + return + } + res.Body.Close() + }() + // Wait for GetClientCertificate handler to be called + <-blockCh + // Cancel the context + cancel() + // Expect the cancellation error here + err = <-errCh + if err == nil { + t.Fatal("cancelling context during client certificate fetch did not error as expected") + return + } + if !errors.Is(err, context.Canceled) { + t.Fatalf("unexpected error returned after cancellation: %v", err) + } +} + +// TestDialRaceResumesDial tests that, given two concurrent requests +// to the same address, when the first Dial is interrupted because +// the first request's context is cancelled, the second request +// resumes the dial automatically. +func TestDialRaceResumesDial(t *testing.T) { + blockCh := make(chan struct{}) + serverTLSConfigFunc := func(ts *httptest.Server) { + ts.Config.TLSConfig = &tls.Config{ + // Triggers the server to request the clients certificate + // during TLS handshake. + ClientAuth: tls.RequestClientCert, + } + } + ts := newServerTester(t, + func(w http.ResponseWriter, r *http.Request) {}, + optOnlyServer, + serverTLSConfigFunc, + ) + defer ts.Close() + tr := &Transport{ + TLSClientConfig: &tls.Config{ + GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { + select { + case <-blockCh: + // If we already errored, return without error. + return &tls.Certificate{}, nil + default: + } + close(blockCh) + <-cri.Context().Done() + return nil, cri.Context().Err() + }, + InsecureSkipVerify: true, + }, + } + defer tr.CloseIdleConnections() + req, err := http.NewRequest(http.MethodGet, ts.ts.URL, nil) + if err != nil { + t.Fatal(err) + } + // Create two requests with independent cancellation. + ctx1, cancel1 := context.WithCancel(context.Background()) + defer cancel1() + req1 := req.WithContext(ctx1) + ctx2, cancel2 := context.WithCancel(context.Background()) + defer cancel2() + req2 := req.WithContext(ctx2) + errCh := make(chan error) + go func() { + res, err := tr.RoundTrip(req1) + if err != nil { + errCh <- err + return + } + res.Body.Close() + }() + successCh := make(chan struct{}) + go func() { + // Don't start request until first request + // has initiated the handshake. + <-blockCh + res, err := tr.RoundTrip(req2) + if err != nil { + errCh <- err + return + } + res.Body.Close() + // Close successCh to indicate that the second request + // made it to the server successfully. + close(successCh) + }() + // Wait for GetClientCertificate handler to be called + <-blockCh + // Cancel the context first + cancel1() + // Expect the cancellation error here + err = <-errCh + if err == nil { + t.Fatal("cancelling context during client certificate fetch did not error as expected") + return + } + if !errors.Is(err, context.Canceled) { + t.Fatalf("unexpected error returned after cancellation: %v", err) + } + select { + case err := <-errCh: + t.Fatalf("unexpected second error: %v", err) + case <-successCh: + } +} From fbaf41277f28102c36926d1368dafbe2b54b4c1d Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Wed, 8 Nov 2023 19:30:54 +0000 Subject: [PATCH 098/168] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: I828e052f9d32ff73d75d07087fcd25c8ed61d9de Reviewed-on: https://go-review.googlesource.com/c/net/+/540816 LUCI-TryBot-Result: Go LUCI Auto-Submit: Gopher Robot Reviewed-by: Carlos Amedee Reviewed-by: Dmitri Shuralyov --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index f83c0890ad..21deffd4bb 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module golang.org/x/net go 1.18 require ( - golang.org/x/crypto v0.14.0 - golang.org/x/sys v0.13.0 - golang.org/x/term v0.13.0 - golang.org/x/text v0.13.0 + golang.org/x/crypto v0.15.0 + golang.org/x/sys v0.14.0 + golang.org/x/term v0.14.0 + golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index ddbbdd3ef0..54759e4894 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= From 7b5abfaf7fdcca5801931b9726eb8ce9ae586ba1 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 26 Oct 2023 11:57:09 -0700 Subject: [PATCH 099/168] quic: basic qlog support Add the structure for generating and writing qlog events. Events are generated as slog events using the structure of the qlog events (draft-ietf-quic-qlog-quic-events-03). The qlog package contains a slog Handler implementation that converts the quic package events to qlog JSON. This CL generates events for connection creation and closure. Future CLs will add additional events. Events follow draft-ietf-quic-qlog-quic-events-03, which is the most recent draft supported by the qvis visualization tool. https://www.ietf.org/archive/id/draft-ietf-quic-qlog-main-schema-04.html https://www.ietf.org/archive/id/draft-ietf-quic-qlog-quic-events-03.html For golang/go#58547 Change-Id: I5fb1b7653d0257cb86726bd5bc9e8775da74686a Reviewed-on: https://go-review.googlesource.com/c/net/+/537936 Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/cmd/interop/main.go | 17 +- internal/quic/cmd/interop/run_endpoint.sh | 4 +- internal/quic/config.go | 11 + internal/quic/conn.go | 15 +- internal/quic/conn_close_test.go | 22 +- internal/quic/conn_test.go | 1 + internal/quic/qlog.go | 141 ++++++++++++ internal/quic/qlog/handler.go | 76 ++++++ internal/quic/qlog/json_writer.go | 194 ++++++++++++++++ internal/quic/qlog/json_writer_test.go | 186 +++++++++++++++ internal/quic/qlog/qlog.go | 267 ++++++++++++++++++++++ internal/quic/qlog/qlog_test.go | 151 ++++++++++++ internal/quic/qlog_test.go | 132 +++++++++++ internal/quic/stateless_reset_test.go | 10 +- 14 files changed, 1216 insertions(+), 11 deletions(-) create mode 100644 internal/quic/qlog.go create mode 100644 internal/quic/qlog/handler.go create mode 100644 internal/quic/qlog/json_writer.go create mode 100644 internal/quic/qlog/json_writer_test.go create mode 100644 internal/quic/qlog/qlog.go create mode 100644 internal/quic/qlog/qlog_test.go create mode 100644 internal/quic/qlog_test.go diff --git a/internal/quic/cmd/interop/main.go b/internal/quic/cmd/interop/main.go index cc5292e9ea..2ca5d652ad 100644 --- a/internal/quic/cmd/interop/main.go +++ b/internal/quic/cmd/interop/main.go @@ -18,6 +18,7 @@ import ( "fmt" "io" "log" + "log/slog" "net" "net/url" "os" @@ -25,14 +26,16 @@ import ( "sync" "golang.org/x/net/internal/quic" + "golang.org/x/net/internal/quic/qlog" ) var ( - listen = flag.String("listen", "", "listen address") - cert = flag.String("cert", "", "certificate") - pkey = flag.String("key", "", "private key") - root = flag.String("root", "", "serve files from this root") - output = flag.String("output", "", "directory to write files to") + listen = flag.String("listen", "", "listen address") + cert = flag.String("cert", "", "certificate") + pkey = flag.String("key", "", "private key") + root = flag.String("root", "", "serve files from this root") + output = flag.String("output", "", "directory to write files to") + qlogdir = flag.String("qlog", "", "directory to write qlog output to") ) func main() { @@ -48,6 +51,10 @@ func main() { }, MaxBidiRemoteStreams: -1, MaxUniRemoteStreams: -1, + QLogLogger: slog.New(qlog.NewJSONHandler(qlog.HandlerOptions{ + Level: quic.QLogLevelFrame, + Dir: *qlogdir, + })), } if *cert != "" { c, err := tls.LoadX509KeyPair(*cert, *pkey) diff --git a/internal/quic/cmd/interop/run_endpoint.sh b/internal/quic/cmd/interop/run_endpoint.sh index d72335d8e4..442039bc07 100644 --- a/internal/quic/cmd/interop/run_endpoint.sh +++ b/internal/quic/cmd/interop/run_endpoint.sh @@ -11,7 +11,7 @@ if [ "$ROLE" == "client" ]; then # Wait for the simulator to start up. /wait-for-it.sh sim:57832 -s -t 30 - ./interop -output=/downloads $CLIENT_PARAMS $REQUESTS + ./interop -output=/downloads -qlog=$QLOGDIR $CLIENT_PARAMS $REQUESTS elif [ "$ROLE" == "server" ]; then - ./interop -cert=/certs/cert.pem -key=/certs/priv.key -listen=:443 -root=/www "$@" $SERVER_PARAMS + ./interop -cert=/certs/cert.pem -key=/certs/priv.key -qlog=$QLOGDIR -listen=:443 -root=/www "$@" $SERVER_PARAMS fi diff --git a/internal/quic/config.go b/internal/quic/config.go index 6278bf89c1..b10ecc79e9 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -8,6 +8,7 @@ package quic import ( "crypto/tls" + "log/slog" ) // A Config structure configures a QUIC endpoint. @@ -72,6 +73,16 @@ type Config struct { // // If this field is left as zero, stateless reset is disabled. StatelessResetKey [32]byte + + // QLogLogger receives qlog events. + // + // Events currently correspond to the definitions in draft-ietf-qlog-quic-events-03. + // This is not the latest version of the draft, but is the latest version supported + // by common event log viewers as of the time this paragraph was written. + // + // The qlog package contains a slog.Handler which serializes qlog events + // to a standard JSON representation. + QLogLogger *slog.Logger } func configDefault(v, def, limit int64) int64 { diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 1292f2b20e..cca11166ca 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -11,6 +11,7 @@ import ( "crypto/tls" "errors" "fmt" + "log/slog" "net/netip" "time" ) @@ -60,6 +61,8 @@ type Conn struct { // Tests only: Send a PING in a specific number space. testSendPingSpace numberSpace testSendPing sentVal + + log *slog.Logger } // connTestHooks override conn behavior in tests. @@ -94,7 +97,7 @@ type newServerConnIDs struct { retrySrcConnID []byte // source from server's Retry } -func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) { +func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort, config *Config, l *Listener) (conn *Conn, _ error) { c := &Conn{ side: side, listener: l, @@ -106,6 +109,14 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip idleTimeout: now.Add(defaultMaxIdleTimeout), peerAckDelayExponent: -1, } + defer func() { + // If we hit an error in newConn, close donec so tests don't get stuck waiting for it. + // This is only relevant if we've got a bug, but it makes tracking that bug down + // much easier. + if conn == nil { + close(c.donec) + } + }() // A one-element buffer allows us to wake a Conn's event loop as a // non-blocking operation. @@ -135,6 +146,7 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip // The smallest allowed maximum QUIC datagram size is 1200 bytes. // TODO: PMTU discovery. const maxDatagramSize = 1200 + c.logConnectionStarted(cids.originalDstConnID, peerAddr) c.keysAppData.init() c.loss.init(c.side, maxDatagramSize, now) c.streamsInit() @@ -259,6 +271,7 @@ func (c *Conn) loop(now time.Time) { defer close(c.donec) defer c.tls.Close() defer c.listener.connDrained(c) + defer c.logConnectionClosed() // The connection timer sends a message to the connection loop on expiry. // We need to give it an expiry when creating it, so set the initial timeout to diff --git a/internal/quic/conn_close_test.go b/internal/quic/conn_close_test.go index d583ae92a0..0dd46dd203 100644 --- a/internal/quic/conn_close_test.go +++ b/internal/quic/conn_close_test.go @@ -70,7 +70,8 @@ func TestConnCloseResponseBackoff(t *testing.T) { } func TestConnCloseWithPeerResponse(t *testing.T) { - tc := newTestConn(t, clientSide) + qr := &qlogRecord{} + tc := newTestConn(t, clientSide, qr.config) tc.handshake() tc.conn.Abort(nil) @@ -99,10 +100,19 @@ func TestConnCloseWithPeerResponse(t *testing.T) { if err := tc.conn.Wait(canceledContext()); !errors.Is(err, wantErr) { t.Errorf("non-blocking conn.Wait() = %v, want %v", err, wantErr) } + + tc.advance(1 * time.Second) // long enough to exit the draining state + qr.wantEvents(t, jsonEvent{ + "name": "connectivity:connection_closed", + "data": map[string]any{ + "trigger": "application", + }, + }) } func TestConnClosePeerCloses(t *testing.T) { - tc := newTestConn(t, clientSide) + qr := &qlogRecord{} + tc := newTestConn(t, clientSide, qr.config) tc.handshake() wantErr := &ApplicationError{ @@ -128,6 +138,14 @@ func TestConnClosePeerCloses(t *testing.T) { code: 9, reason: "because", }) + + tc.advance(1 * time.Second) // long enough to exit the draining state + qr.wantEvents(t, jsonEvent{ + "name": "connectivity:connection_closed", + "data": map[string]any{ + "trigger": "application", + }, + }) } func TestConnCloseReceiveInInitial(t *testing.T) { diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index c70c58ef00..514a8775e9 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -198,6 +198,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { // The initial connection ID for the server is chosen by the client. cids.srcConnID = testPeerConnID(0) cids.dstConnID = testPeerConnID(-1) + cids.originalDstConnID = cids.dstConnID } var configTransportParams []func(*transportParameters) var configTestConn []func(*testConn) diff --git a/internal/quic/qlog.go b/internal/quic/qlog.go new file mode 100644 index 0000000000..29875693ec --- /dev/null +++ b/internal/quic/qlog.go @@ -0,0 +1,141 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "encoding/hex" + "log/slog" + "net/netip" +) + +// Log levels for qlog events. +const ( + // QLogLevelFrame includes per-frame information. + // When this level is enabled, packet_sent and packet_received events will + // contain information on individual frames sent/received. + QLogLevelFrame = slog.Level(-6) + + // QLogLevelPacket events occur at most once per packet sent or received. + // + // For example: packet_sent, packet_received. + QLogLevelPacket = slog.Level(-4) + + // QLogLevelConn events occur multiple times over a connection's lifetime, + // but less often than the frequency of individual packets. + // + // For example: connection_state_updated. + QLogLevelConn = slog.Level(-2) + + // QLogLevelEndpoint events occur at most once per connection. + // + // For example: connection_started, connection_closed. + QLogLevelEndpoint = slog.Level(0) +) + +func (c *Conn) logEnabled(level slog.Level) bool { + return c.log != nil && c.log.Enabled(context.Background(), level) +} + +// slogHexstring returns a slog.Attr for a value of the hexstring type. +// +// https://www.ietf.org/archive/id/draft-ietf-quic-qlog-main-schema-04.html#section-1.1.1 +func slogHexstring(key string, value []byte) slog.Attr { + return slog.String(key, hex.EncodeToString(value)) +} + +func slogAddr(key string, value netip.Addr) slog.Attr { + return slog.String(key, value.String()) +} + +func (c *Conn) logConnectionStarted(originalDstConnID []byte, peerAddr netip.AddrPort) { + if c.config.QLogLogger == nil || + !c.config.QLogLogger.Enabled(context.Background(), QLogLevelEndpoint) { + return + } + var vantage string + if c.side == clientSide { + vantage = "client" + originalDstConnID = c.connIDState.originalDstConnID + } else { + vantage = "server" + } + // A qlog Trace container includes some metadata (title, description, vantage_point) + // and a list of Events. The Trace also includes a common_fields field setting field + // values common to all events in the trace. + // + // Trace = { + // ? title: text + // ? description: text + // ? configuration: Configuration + // ? common_fields: CommonFields + // ? vantage_point: VantagePoint + // events: [* Event] + // } + // + // To map this into slog's data model, we start each per-connection trace with a With + // call that includes both the trace metadata and the common fields. + // + // This means that in slog's model, each trace event will also include + // the Trace metadata fields (vantage_point), which is a divergence from the qlog model. + c.log = c.config.QLogLogger.With( + // The group_id permits associating traces taken from different vantage points + // for the same connection. + // + // We use the original destination connection ID as the group ID. + // + // https://www.ietf.org/archive/id/draft-ietf-quic-qlog-main-schema-04.html#section-3.4.6 + slogHexstring("group_id", originalDstConnID), + slog.Group("vantage_point", + slog.String("name", "go quic"), + slog.String("type", vantage), + ), + ) + localAddr := c.listener.LocalAddr() + // https://www.ietf.org/archive/id/draft-ietf-quic-qlog-quic-events-03.html#section-4.2 + c.log.LogAttrs(context.Background(), QLogLevelEndpoint, + "connectivity:connection_started", + slogAddr("src_ip", localAddr.Addr()), + slog.Int("src_port", int(localAddr.Port())), + slogHexstring("src_cid", c.connIDState.local[0].cid), + slogAddr("dst_ip", peerAddr.Addr()), + slog.Int("dst_port", int(peerAddr.Port())), + slogHexstring("dst_cid", c.connIDState.remote[0].cid), + ) +} + +func (c *Conn) logConnectionClosed() { + if !c.logEnabled(QLogLevelEndpoint) { + return + } + err := c.lifetime.finalErr + trigger := "error" + switch e := err.(type) { + case *ApplicationError: + // TODO: Distinguish between peer and locally-initiated close. + trigger = "application" + case localTransportError: + if e.code == errNo { + trigger = "clean" + } + case peerTransportError: + if e.code == errNo { + trigger = "clean" + } + default: + switch err { + case errStatelessReset: + trigger = "stateless_reset" + } + // TODO: idle_timeout, handshake_timeout + } + // https://www.ietf.org/archive/id/draft-ietf-quic-qlog-quic-events-03.html#section-4.3 + c.log.LogAttrs(context.Background(), QLogLevelEndpoint, + "connectivity:connection_closed", + slog.String("trigger", trigger), + ) +} diff --git a/internal/quic/qlog/handler.go b/internal/quic/qlog/handler.go new file mode 100644 index 0000000000..35a66cf8bf --- /dev/null +++ b/internal/quic/qlog/handler.go @@ -0,0 +1,76 @@ +// 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 qlog + +import ( + "context" + "log/slog" +) + +type withAttrsHandler struct { + attrs []slog.Attr + h slog.Handler +} + +func withAttrs(h slog.Handler, attrs []slog.Attr) slog.Handler { + if len(attrs) == 0 { + return h + } + return &withAttrsHandler{attrs: attrs, h: h} +} + +func (h *withAttrsHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.h.Enabled(ctx, level) +} + +func (h *withAttrsHandler) Handle(ctx context.Context, r slog.Record) error { + r.AddAttrs(h.attrs...) + return h.h.Handle(ctx, r) +} + +func (h *withAttrsHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return withAttrs(h, attrs) +} + +func (h *withAttrsHandler) WithGroup(name string) slog.Handler { + return withGroup(h, name) +} + +type withGroupHandler struct { + name string + h slog.Handler +} + +func withGroup(h slog.Handler, name string) slog.Handler { + if name == "" { + return h + } + return &withGroupHandler{name: name, h: h} +} + +func (h *withGroupHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.h.Enabled(ctx, level) +} + +func (h *withGroupHandler) Handle(ctx context.Context, r slog.Record) error { + var attrs []slog.Attr + r.Attrs(func(a slog.Attr) bool { + attrs = append(attrs, a) + return true + }) + nr := slog.NewRecord(r.Time, r.Level, r.Message, r.PC) + nr.Add(slog.Any(h.name, slog.GroupValue(attrs...))) + return h.h.Handle(ctx, nr) +} + +func (h *withGroupHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return withAttrs(h, attrs) +} + +func (h *withGroupHandler) WithGroup(name string) slog.Handler { + return withGroup(h, name) +} diff --git a/internal/quic/qlog/json_writer.go b/internal/quic/qlog/json_writer.go new file mode 100644 index 0000000000..50cf33bc50 --- /dev/null +++ b/internal/quic/qlog/json_writer.go @@ -0,0 +1,194 @@ +// 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 qlog + +import ( + "bytes" + "fmt" + "io" + "log/slog" + "strconv" + "sync" + "time" +) + +// A jsonWriter writes JSON-SEQ (RFC 7464). +// +// A JSON-SEQ file consists of a series of JSON text records, +// each beginning with an RS (0x1e) character and ending with LF (0x0a). +type jsonWriter struct { + mu sync.Mutex + w io.WriteCloser + buf bytes.Buffer +} + +// writeRecordStart writes the start of a JSON-SEQ record. +func (w *jsonWriter) writeRecordStart() { + w.mu.Lock() + w.buf.WriteByte(0x1e) + w.buf.WriteByte('{') +} + +// writeRecordEnd finishes writing a JSON-SEQ record. +func (w *jsonWriter) writeRecordEnd() { + w.buf.WriteByte('}') + w.buf.WriteByte('\n') + w.w.Write(w.buf.Bytes()) + w.buf.Reset() + w.mu.Unlock() +} + +// writeAttrsField writes a []slog.Attr as an object field. +func (w *jsonWriter) writeAttrsField(name string, attrs []slog.Attr) { + w.writeName(name) + w.buf.WriteByte('{') + for _, a := range attrs { + w.writeAttr(a) + } + w.buf.WriteByte('}') +} + +// writeAttr writes a slog.Attr as an object field. +func (w *jsonWriter) writeAttr(a slog.Attr) { + v := a.Value.Resolve() + switch v.Kind() { + case slog.KindAny: + w.writeStringField(a.Key, fmt.Sprint(v.Any())) + case slog.KindBool: + w.writeBoolField(a.Key, v.Bool()) + case slog.KindDuration: + w.writeDurationField(a.Key, v.Duration()) + case slog.KindFloat64: + w.writeFloat64Field(a.Key, v.Float64()) + case slog.KindInt64: + w.writeInt64Field(a.Key, v.Int64()) + case slog.KindString: + w.writeStringField(a.Key, v.String()) + case slog.KindTime: + w.writeTimeField(a.Key, v.Time()) + case slog.KindUint64: + w.writeUint64Field(a.Key, v.Uint64()) + case slog.KindGroup: + w.writeAttrsField(a.Key, v.Group()) + default: + w.writeString("unhandled kind") + } +} + +// writeName writes an object field name followed by a colon. +func (w *jsonWriter) writeName(name string) { + if b := w.buf.Bytes(); len(b) > 0 && b[len(b)-1] != '{' { + // Add the comma separating this from the previous field. + w.buf.WriteByte(',') + } + w.writeString(name) + w.buf.WriteByte(':') +} + +// writeObject writes an object-valued object field. +// The function f is called to write the contents. +func (w *jsonWriter) writeObjectField(name string, f func()) { + w.writeName(name) + w.buf.WriteByte('{') + f() + w.buf.WriteByte('}') +} + +// writeRawField writes an field with a raw JSON value. +func (w *jsonWriter) writeRawField(name, v string) { + w.writeName(name) + w.buf.WriteString(v) +} + +// writeBoolField writes a bool-valued object field. +func (w *jsonWriter) writeBoolField(name string, v bool) { + w.writeName(name) + if v { + w.buf.WriteString("true") + } else { + w.buf.WriteString("false") + } +} + +// writeDurationField writes a millisecond duration-valued object field. +func (w *jsonWriter) writeDurationField(name string, v time.Duration) { + w.writeName(name) + fmt.Fprintf(&w.buf, "%d.%06d", v.Milliseconds(), v%time.Millisecond) +} + +// writeFloat64Field writes an float64-valued object field. +func (w *jsonWriter) writeFloat64Field(name string, v float64) { + w.writeName(name) + w.buf.Write(strconv.AppendFloat(w.buf.AvailableBuffer(), v, 'f', -1, 64)) +} + +// writeInt64Field writes an int64-valued object field. +func (w *jsonWriter) writeInt64Field(name string, v int64) { + w.writeName(name) + w.buf.Write(strconv.AppendInt(w.buf.AvailableBuffer(), v, 10)) +} + +// writeUint64Field writes a uint64-valued object field. +func (w *jsonWriter) writeUint64Field(name string, v uint64) { + w.writeName(name) + w.buf.Write(strconv.AppendUint(w.buf.AvailableBuffer(), v, 10)) +} + +// writeStringField writes a string-valued object field. +func (w *jsonWriter) writeStringField(name, v string) { + w.writeName(name) + w.writeString(v) +} + +// writeTimeField writes a time-valued object field. +func (w *jsonWriter) writeTimeField(name string, v time.Time) { + w.writeName(name) + fmt.Fprintf(&w.buf, "%d.%06d", v.UnixMilli(), v.Nanosecond()%int(time.Millisecond)) +} + +func jsonSafeSet(c byte) bool { + // mask is a 128-bit bitmap with 1s for allowed bytes, + // so that the byte c can be tested with a shift and an and. + // If c > 128, then 1<>64)) != 0 +} + +func jsonNeedsEscape(s string) bool { + for i := range s { + if !jsonSafeSet(s[i]) { + return true + } + } + return false +} + +// writeString writes an ASCII string. +// +// qlog fields should never contain anything that isn't ASCII, +// so we do the bare minimum to avoid producing invalid output if we +// do write something unexpected. +func (w *jsonWriter) writeString(v string) { + w.buf.WriteByte('"') + if !jsonNeedsEscape(v) { + w.buf.WriteString(v) + } else { + for i := range v { + if jsonSafeSet(v[i]) { + w.buf.WriteByte(v[i]) + } else { + fmt.Fprintf(&w.buf, `\u%04x`, v[i]) + } + } + } + w.buf.WriteByte('"') +} diff --git a/internal/quic/qlog/json_writer_test.go b/internal/quic/qlog/json_writer_test.go new file mode 100644 index 0000000000..7ba5e17378 --- /dev/null +++ b/internal/quic/qlog/json_writer_test.go @@ -0,0 +1,186 @@ +// 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 qlog + +import ( + "bytes" + "errors" + "fmt" + "log/slog" + "strings" + "sync" + "testing" + "time" +) + +type testJSONOut struct { + bytes.Buffer +} + +func (o *testJSONOut) Close() error { return nil } + +func newTestJSONWriter() *jsonWriter { + return &jsonWriter{w: &testJSONOut{}} +} + +func wantJSONRecord(t *testing.T, w *jsonWriter, want string) { + t.Helper() + want = "\x1e" + want + "\n" + got := w.w.(*testJSONOut).String() + if got != want { + t.Errorf("jsonWriter contains unexpected output\ngot: %q\nwant: %q", got, want) + } +} + +func TestJSONWriterWriteConcurrentRecords(t *testing.T) { + w := newTestJSONWriter() + var wg sync.WaitGroup + for i := 0; i < 3; i++ { + wg.Add(1) + go func() { + defer wg.Done() + w.writeRecordStart() + w.writeInt64Field("field", 0) + w.writeRecordEnd() + }() + } + wg.Wait() + wantJSONRecord(t, w, strings.Join([]string{ + `{"field":0}`, + `{"field":0}`, + `{"field":0}`, + }, "\n\x1e")) +} + +func TestJSONWriterAttrs(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeAttrsField("field", []slog.Attr{ + slog.Any("any", errors.New("value")), + slog.Bool("bool", true), + slog.Duration("duration", 1*time.Second), + slog.Float64("float64", 1), + slog.Int64("int64", 1), + slog.String("string", "value"), + slog.Time("time", time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + slog.Uint64("uint64", 1), + slog.Group("group", "a", 1), + }) + w.writeRecordEnd() + wantJSONRecord(t, w, + `{"field":{`+ + `"any":"value",`+ + `"bool":true,`+ + `"duration":1000.000000,`+ + `"float64":1,`+ + `"int64":1,`+ + `"string":"value",`+ + `"time":946684800000.000000,`+ + `"uint64":1,`+ + `"group":{"a":1}`+ + `}}`) +} + +func TestJSONWriterObjectEmpty(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeObjectField("field", func() {}) + w.writeRecordEnd() + wantJSONRecord(t, w, `{"field":{}}`) +} + +func TestJSONWriterObjectFields(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeObjectField("field", func() { + w.writeStringField("a", "value") + w.writeInt64Field("b", 10) + }) + w.writeRecordEnd() + wantJSONRecord(t, w, `{"field":{"a":"value","b":10}}`) +} + +func TestJSONWriterRawField(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeRawField("field", `[1]`) + w.writeRecordEnd() + wantJSONRecord(t, w, `{"field":[1]}`) +} + +func TestJSONWriterBoolField(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeBoolField("true", true) + w.writeBoolField("false", false) + w.writeRecordEnd() + wantJSONRecord(t, w, `{"true":true,"false":false}`) +} + +func TestJSONWriterDurationField(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeDurationField("field", (10*time.Millisecond)+(2*time.Nanosecond)) + w.writeRecordEnd() + wantJSONRecord(t, w, `{"field":10.000002}`) +} + +func TestJSONWriterFloat64Field(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeFloat64Field("field", 1.1) + w.writeRecordEnd() + wantJSONRecord(t, w, `{"field":1.1}`) +} + +func TestJSONWriterInt64Field(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeInt64Field("field", 1234) + w.writeRecordEnd() + wantJSONRecord(t, w, `{"field":1234}`) +} + +func TestJSONWriterUint64Field(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeUint64Field("field", 1234) + w.writeRecordEnd() + wantJSONRecord(t, w, `{"field":1234}`) +} + +func TestJSONWriterStringField(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeStringField("field", "value") + w.writeRecordEnd() + wantJSONRecord(t, w, `{"field":"value"}`) +} + +func TestJSONWriterStringFieldEscaped(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeStringField("field", "va\x00ue") + w.writeRecordEnd() + wantJSONRecord(t, w, `{"field":"va\u0000ue"}`) +} + +func TestJSONWriterStringEscaping(t *testing.T) { + for c := 0; c <= 0xff; c++ { + w := newTestJSONWriter() + w.writeRecordStart() + w.writeStringField("field", string([]byte{byte(c)})) + w.writeRecordEnd() + var want string + if (c >= 0x20 && c <= 0x21) || (c >= 0x23 && c <= 0x5b) || (c >= 0x5d && c <= 0x7e) { + want = fmt.Sprintf(`%c`, c) + } else { + want = fmt.Sprintf(`\u%04x`, c) + } + wantJSONRecord(t, w, `{"field":"`+want+`"}`) + } +} diff --git a/internal/quic/qlog/qlog.go b/internal/quic/qlog/qlog.go new file mode 100644 index 0000000000..0e71d71aae --- /dev/null +++ b/internal/quic/qlog/qlog.go @@ -0,0 +1,267 @@ +// 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 qlog serializes qlog events. +package qlog + +import ( + "bytes" + "context" + "errors" + "io" + "log/slog" + "os" + "path/filepath" + "sync" + "time" +) + +// Vantage is the vantage point of a trace. +type Vantage string + +const ( + // VantageEndpoint traces contain events not specific to a single connection. + VantageEndpoint = Vantage("endpoint") + + // VantageClient traces follow a connection from the client's perspective. + VantageClient = Vantage("client") + + // VantageClient traces follow a connection from the server's perspective. + VantageServer = Vantage("server") +) + +// TraceInfo contains information about a trace. +type TraceInfo struct { + // Vantage is the vantage point of the trace. + Vantage Vantage + + // GroupID identifies the logical group the trace belongs to. + // For a connection trace, the group will be the same for + // both the client and server vantage points. + GroupID string +} + +// HandlerOptions are options for a JSONHandler. +type HandlerOptions struct { + // Level reports the minimum record level that will be logged. + // If Level is nil, the handler assumes QLogLevelEndpoint. + Level slog.Leveler + + // Dir is the directory in which to create trace files. + // The handler will create one file per connection. + // If NewTrace is non-nil or Dir is "", the handler will not create files. + Dir string + + // NewTrace is called to create a new trace. + // If NewTrace is nil and Dir is set, + // the handler will create a new file in Dir for each trace. + NewTrace func(TraceInfo) (io.WriteCloser, error) +} + +type endpointHandler struct { + opts HandlerOptions + + traceOnce sync.Once + trace *jsonTraceHandler +} + +// NewJSONHandler returns a handler which serializes qlog events to JSON. +// +// The handler will write an endpoint-wide trace, +// and a separate trace for each connection. +// The HandlerOptions control the location traces are written. +// +// It uses the streamable JSON Text Sequences mapping (JSON-SEQ) +// defined in draft-ietf-quic-qlog-main-schema-04, Section 6.2. +// +// A JSONHandler may be used as the handler for a quic.Config.QLogLogger. +// It is not a general-purpose slog handler, +// and may not properly handle events from other sources. +func NewJSONHandler(opts HandlerOptions) slog.Handler { + if opts.Dir == "" && opts.NewTrace == nil { + return slogDiscard{} + } + return &endpointHandler{ + opts: opts, + } +} + +func (h *endpointHandler) Enabled(ctx context.Context, level slog.Level) bool { + return enabled(h.opts.Level, level) +} + +func (h *endpointHandler) Handle(ctx context.Context, r slog.Record) error { + h.traceOnce.Do(func() { + h.trace, _ = newJSONTraceHandler(h.opts, nil) + }) + if h.trace != nil { + h.trace.Handle(ctx, r) + } + return nil +} + +func (h *endpointHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + // Create a new trace output file for each top-level WithAttrs. + tr, err := newJSONTraceHandler(h.opts, attrs) + if err != nil { + return withAttrs(h, attrs) + } + return tr +} + +func (h *endpointHandler) WithGroup(name string) slog.Handler { + return withGroup(h, name) +} + +type jsonTraceHandler struct { + level slog.Leveler + w jsonWriter + start time.Time + buf bytes.Buffer +} + +func newJSONTraceHandler(opts HandlerOptions, attrs []slog.Attr) (*jsonTraceHandler, error) { + w, err := newTraceWriter(opts, traceInfoFromAttrs(attrs)) + if err != nil { + return nil, err + } + + // For testing, it might be nice to set the start time used for relative timestamps + // to the time of the first event. + // + // At the expense of some additional complexity here, we could defer writing + // the reference_time header field until the first event is processed. + // + // Just use the current time for now. + start := time.Now() + + h := &jsonTraceHandler{ + w: jsonWriter{w: w}, + level: opts.Level, + start: start, + } + h.writeHeader(attrs) + return h, nil +} + +func traceInfoFromAttrs(attrs []slog.Attr) TraceInfo { + info := TraceInfo{ + Vantage: VantageEndpoint, // default if not specified + } + for _, a := range attrs { + if a.Key == "group_id" && a.Value.Kind() == slog.KindString { + info.GroupID = a.Value.String() + } + if a.Key == "vantage_point" && a.Value.Kind() == slog.KindGroup { + for _, aa := range a.Value.Group() { + if aa.Key == "type" && aa.Value.Kind() == slog.KindString { + info.Vantage = Vantage(aa.Value.String()) + } + } + } + } + return info +} + +func newTraceWriter(opts HandlerOptions, info TraceInfo) (io.WriteCloser, error) { + var w io.WriteCloser + var err error + if opts.NewTrace != nil { + w, err = opts.NewTrace(info) + } else if opts.Dir != "" { + var filename string + if info.GroupID != "" { + filename = info.GroupID + "_" + } + filename += string(info.Vantage) + ".sqlog" + if !filepath.IsLocal(filename) { + return nil, errors.New("invalid trace filename") + } + w, err = os.Create(filepath.Join(opts.Dir, filename)) + } else { + err = errors.New("no log destination") + } + return w, err +} + +func (h *jsonTraceHandler) writeHeader(attrs []slog.Attr) { + h.w.writeRecordStart() + defer h.w.writeRecordEnd() + + // At the time of writing this comment the most recent version is 0.4, + // but qvis only supports up to 0.3. + h.w.writeStringField("qlog_version", "0.3") + h.w.writeStringField("qlog_format", "JSON-SEQ") + + // The attrs flatten both common trace event fields and Trace fields. + // This identifies the fields that belong to the Trace. + isTraceSeqField := func(s string) bool { + switch s { + case "title", "description", "configuration", "vantage_point": + return true + } + return false + } + + h.w.writeObjectField("trace", func() { + h.w.writeObjectField("common_fields", func() { + h.w.writeRawField("protocol_type", `["QUIC"]`) + h.w.writeStringField("time_format", "relative") + h.w.writeTimeField("reference_time", h.start) + for _, a := range attrs { + if !isTraceSeqField(a.Key) { + h.w.writeAttr(a) + } + } + }) + for _, a := range attrs { + if isTraceSeqField(a.Key) { + h.w.writeAttr(a) + } + } + }) +} + +func (h *jsonTraceHandler) Enabled(ctx context.Context, level slog.Level) bool { + return enabled(h.level, level) +} + +func (h *jsonTraceHandler) Handle(ctx context.Context, r slog.Record) error { + h.w.writeRecordStart() + defer h.w.writeRecordEnd() + h.w.writeDurationField("time", r.Time.Sub(h.start)) + h.w.writeStringField("name", r.Message) + h.w.writeObjectField("data", func() { + r.Attrs(func(a slog.Attr) bool { + h.w.writeAttr(a) + return true + }) + }) + return nil +} + +func (h *jsonTraceHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return withAttrs(h, attrs) +} + +func (h *jsonTraceHandler) WithGroup(name string) slog.Handler { + return withGroup(h, name) +} + +func enabled(leveler slog.Leveler, level slog.Level) bool { + var minLevel slog.Level + if leveler != nil { + minLevel = leveler.Level() + } + return level >= minLevel +} + +type slogDiscard struct{} + +func (slogDiscard) Enabled(context.Context, slog.Level) bool { return false } +func (slogDiscard) Handle(ctx context.Context, r slog.Record) error { return nil } +func (slogDiscard) WithAttrs(attrs []slog.Attr) slog.Handler { return slogDiscard{} } +func (slogDiscard) WithGroup(name string) slog.Handler { return slogDiscard{} } diff --git a/internal/quic/qlog/qlog_test.go b/internal/quic/qlog/qlog_test.go new file mode 100644 index 0000000000..7575cd890e --- /dev/null +++ b/internal/quic/qlog/qlog_test.go @@ -0,0 +1,151 @@ +// 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 qlog + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log/slog" + "reflect" + "testing" + "time" +) + +// QLog tests are mostly in the quic package, where we can test event generation +// and serialization together. + +func TestQLogHandlerEvents(t *testing.T) { + for _, test := range []struct { + name string + f func(*slog.Logger) + want []map[string]any // events, not counting the trace header + }{{ + name: "various types", + f: func(log *slog.Logger) { + log.Info("message", + "bool", true, + "duration", time.Duration(1*time.Second), + "float", 0.0, + "int", 0, + "string", "value", + "uint", uint64(0), + slog.Group("group", + "a", 0, + ), + ) + }, + want: []map[string]any{{ + "name": "message", + "data": map[string]any{ + "bool": true, + "duration": float64(1000), + "float": float64(0.0), + "int": float64(0), + "string": "value", + "uint": float64(0), + "group": map[string]any{ + "a": float64(0), + }, + }, + }}, + }, { + name: "WithAttrs", + f: func(log *slog.Logger) { + log = log.With( + "with_a", "a", + "with_b", "b", + ) + log.Info("m1", "field", "1") + log.Info("m2", "field", "2") + }, + want: []map[string]any{{ + "name": "m1", + "data": map[string]any{ + "with_a": "a", + "with_b": "b", + "field": "1", + }, + }, { + "name": "m2", + "data": map[string]any{ + "with_a": "a", + "with_b": "b", + "field": "2", + }, + }}, + }, { + name: "WithGroup", + f: func(log *slog.Logger) { + log = log.With( + "with_a", "a", + "with_b", "b", + ) + log.Info("m1", "field", "1") + log.Info("m2", "field", "2") + }, + want: []map[string]any{{ + "name": "m1", + "data": map[string]any{ + "with_a": "a", + "with_b": "b", + "field": "1", + }, + }, { + "name": "m2", + "data": map[string]any{ + "with_a": "a", + "with_b": "b", + "field": "2", + }, + }}, + }} { + var out bytes.Buffer + opts := HandlerOptions{ + Level: slog.LevelDebug, + NewTrace: func(TraceInfo) (io.WriteCloser, error) { + return nopCloseWriter{&out}, nil + }, + } + h, err := newJSONTraceHandler(opts, []slog.Attr{ + slog.String("group_id", "group"), + slog.Group("vantage_point", + slog.String("type", "client"), + ), + }) + if err != nil { + t.Fatal(err) + } + log := slog.New(h) + test.f(log) + got := []map[string]any{} + for i, e := range bytes.Split(out.Bytes(), []byte{0x1e}) { + // i==0: empty string before the initial record separator + // i==1: trace header; not part of this test + if i < 2 { + continue + } + var val map[string]any + if err := json.Unmarshal(e, &val); err != nil { + panic(fmt.Errorf("log unmarshal failure: %v\n%q", err, string(e))) + } + delete(val, "time") + got = append(got, val) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("event mismatch\ngot: %v\nwant: %v", got, test.want) + } + } + +} + +type nopCloseWriter struct { + io.Writer +} + +func (nopCloseWriter) Close() error { return nil } diff --git a/internal/quic/qlog_test.go b/internal/quic/qlog_test.go new file mode 100644 index 0000000000..5a2858b8bd --- /dev/null +++ b/internal/quic/qlog_test.go @@ -0,0 +1,132 @@ +// 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 ( + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log/slog" + "reflect" + "testing" + + "golang.org/x/net/internal/quic/qlog" +) + +func TestQLogHandshake(t *testing.T) { + testSides(t, "", func(t *testing.T, side connSide) { + qr := &qlogRecord{} + tc := newTestConn(t, side, qr.config) + tc.handshake() + tc.conn.Abort(nil) + tc.wantFrame("aborting connection generates CONN_CLOSE", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errNo, + }) + tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{}) + tc.advanceToTimer() // let the conn finish draining + + var src, dst []byte + if side == clientSide { + src = testLocalConnID(0) + dst = testLocalConnID(-1) + } else { + src = testPeerConnID(-1) + dst = testPeerConnID(0) + } + qr.wantEvents(t, jsonEvent{ + "name": "connectivity:connection_started", + "data": map[string]any{ + "src_cid": hex.EncodeToString(src), + "dst_cid": hex.EncodeToString(dst), + }, + }, jsonEvent{ + "name": "connectivity:connection_closed", + "data": map[string]any{ + "trigger": "clean", + }, + }) + }) +} + +type nopCloseWriter struct { + io.Writer +} + +func (nopCloseWriter) Close() error { return nil } + +type jsonEvent map[string]any + +func (j jsonEvent) String() string { + b, _ := json.MarshalIndent(j, "", " ") + return string(b) +} + +// eventPartialEqual verifies that every field set in want matches the corresponding field in got. +// It ignores additional fields in got. +func eventPartialEqual(got, want jsonEvent) bool { + for k := range want { + ge, gok := got[k].(map[string]any) + we, wok := want[k].(map[string]any) + if gok && wok { + if !eventPartialEqual(ge, we) { + return false + } + } else { + if !reflect.DeepEqual(got[k], want[k]) { + return false + } + } + } + return true +} + +// A qlogRecord records events. +type qlogRecord struct { + ev []jsonEvent +} + +func (q *qlogRecord) Write(b []byte) (int, error) { + // This relies on the property that the Handler always makes one Write call per event. + if len(b) < 1 || b[0] != 0x1e { + panic(fmt.Errorf("trace Write should start with record separator, got %q", string(b))) + } + var val map[string]any + if err := json.Unmarshal(b[1:], &val); err != nil { + panic(fmt.Errorf("log unmarshal failure: %v\n%v", err, string(b))) + } + q.ev = append(q.ev, val) + return len(b), nil +} + +func (q *qlogRecord) Close() error { return nil } + +// config may be passed to newTestConn to configure the conn to use this logger. +func (q *qlogRecord) config(c *Config) { + c.QLogLogger = slog.New(qlog.NewJSONHandler(qlog.HandlerOptions{ + NewTrace: func(info qlog.TraceInfo) (io.WriteCloser, error) { + return q, nil + }, + })) +} + +// wantEvents checks that every event in want occurs in the order specified. +func (q *qlogRecord) wantEvents(t *testing.T, want ...jsonEvent) { + t.Helper() + got := q.ev + unseen := want + for _, g := range got { + if eventPartialEqual(g, unseen[0]) { + unseen = unseen[1:] + if len(unseen) == 0 { + return + } + } + } + t.Fatalf("got events:\n%v\n\nwant events:\n%v", got, want) +} diff --git a/internal/quic/stateless_reset_test.go b/internal/quic/stateless_reset_test.go index 8a16597c4e..c01375fbd6 100644 --- a/internal/quic/stateless_reset_test.go +++ b/internal/quic/stateless_reset_test.go @@ -130,7 +130,8 @@ func TestStatelessResetSentSizes(t *testing.T) { func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) { // "[...] Stateless Reset Token field values from [...] NEW_CONNECTION_ID frames [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-1 - tc := newTestConn(t, clientSide) + qr := &qlogRecord{} + tc := newTestConn(t, clientSide, qr.config) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -158,6 +159,13 @@ func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) { tc.wantIdle("closed connection is idle in draining") tc.advance(1 * time.Second) // long enough to exit the draining state tc.wantIdle("closed connection is idle after draining") + + qr.wantEvents(t, jsonEvent{ + "name": "connectivity:connection_closed", + "data": map[string]any{ + "trigger": "stateless_reset", + }, + }) } func TestStatelessResetSuccessfulTransportParameter(t *testing.T) { From d87f99be5d1813013851ce74ed7d22743fa33f21 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 30 Oct 2023 10:51:45 -0700 Subject: [PATCH 100/168] quic: idle timeouts, handshake timeouts, and keepalive Negotiate the connection idle timeout based on the sent and received max_idle_timeout transport parameter values. Set a configurable limit on how long a handshake can take to complete. Add a configuration option to send keep-alive PING frames to avoid connection closure due to the idle timeout. RFC 9000, Section 10.1. For golang/go#58547 Change-Id: If6a611090ab836cd6937fcfbb1360a0f07425102 Reviewed-on: https://go-review.googlesource.com/c/net/+/540895 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/config.go | 36 ++++- internal/quic/conn.go | 33 ++--- internal/quic/conn_close.go | 270 ++++++++++++++++++++++-------------- internal/quic/conn_recv.go | 11 +- internal/quic/conn_send.go | 14 +- internal/quic/conn_test.go | 5 +- internal/quic/idle.go | 170 +++++++++++++++++++++++ internal/quic/idle_test.go | 225 ++++++++++++++++++++++++++++++ internal/quic/loss.go | 9 +- internal/quic/qlog.go | 12 +- internal/quic/qlog_test.go | 70 ++++++++++ internal/quic/quic.go | 6 + 12 files changed, 721 insertions(+), 140 deletions(-) create mode 100644 internal/quic/idle.go create mode 100644 internal/quic/idle_test.go diff --git a/internal/quic/config.go b/internal/quic/config.go index b10ecc79e9..b045b7b92c 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -9,6 +9,8 @@ package quic import ( "crypto/tls" "log/slog" + "math" + "time" ) // A Config structure configures a QUIC endpoint. @@ -74,6 +76,26 @@ type Config struct { // If this field is left as zero, stateless reset is disabled. StatelessResetKey [32]byte + // HandshakeTimeout is the maximum time in which a connection handshake must complete. + // If zero, the default of 10 seconds is used. + // If negative, there is no handshake timeout. + HandshakeTimeout time.Duration + + // MaxIdleTimeout is the maximum time after which an idle connection will be closed. + // If zero, the default of 30 seconds is used. + // If negative, idle connections are never closed. + // + // The idle timeout for a connection is the minimum of the maximum idle timeouts + // of the endpoints. + MaxIdleTimeout time.Duration + + // KeepAlivePeriod is the time after which a packet will be sent to keep + // an idle connection alive. + // If zero, keep alive packets are not sent. + // If greater than zero, the keep alive period is the smaller of KeepAlivePeriod and + // half the connection idle timeout. + KeepAlivePeriod time.Duration + // QLogLogger receives qlog events. // // Events currently correspond to the definitions in draft-ietf-qlog-quic-events-03. @@ -85,7 +107,7 @@ type Config struct { QLogLogger *slog.Logger } -func configDefault(v, def, limit int64) int64 { +func configDefault[T ~int64](v, def, limit T) T { switch { case v == 0: return def @@ -115,3 +137,15 @@ func (c *Config) maxStreamWriteBufferSize() int64 { func (c *Config) maxConnReadBufferSize() int64 { return configDefault(c.MaxConnReadBufferSize, 1<<20, maxVarint) } + +func (c *Config) handshakeTimeout() time.Duration { + return configDefault(c.HandshakeTimeout, defaultHandshakeTimeout, math.MaxInt64) +} + +func (c *Config) maxIdleTimeout() time.Duration { + return configDefault(c.MaxIdleTimeout, defaultMaxIdleTimeout, math.MaxInt64) +} + +func (c *Config) keepAlivePeriod() time.Duration { + return configDefault(c.KeepAlivePeriod, defaultKeepAlivePeriod, math.MaxInt64) +} diff --git a/internal/quic/conn.go b/internal/quic/conn.go index cca11166ca..b2b6a0877a 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -26,22 +26,17 @@ type Conn struct { testHooks connTestHooks peerAddr netip.AddrPort - msgc chan any - donec chan struct{} // closed when conn loop exits - exited bool // set to make the conn loop exit immediately + msgc chan any + donec chan struct{} // closed when conn loop exits w packetWriter acks [numberSpaceCount]ackState // indexed by number space lifetime lifetimeState + idle idleState connIDState connIDState loss lossState streams streamsState - // idleTimeout is the time at which the connection will be closed due to inactivity. - // https://www.rfc-editor.org/rfc/rfc9000#section-10.1 - maxIdleTimeout time.Duration - idleTimeout time.Time - // Packet protection keys, CRYPTO streams, and TLS state. keysInitial fixedKeyPair keysHandshake fixedKeyPair @@ -105,8 +100,6 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip peerAddr: peerAddr, msgc: make(chan any, 1), donec: make(chan struct{}), - maxIdleTimeout: defaultMaxIdleTimeout, - idleTimeout: now.Add(defaultMaxIdleTimeout), peerAckDelayExponent: -1, } defer func() { @@ -151,6 +144,7 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip c.loss.init(c.side, maxDatagramSize, now) c.streamsInit() c.lifetimeInit() + c.restartIdleTimer(now) if err := c.startTLS(now, initialConnID, transportParameters{ initialSrcConnID: c.connIDState.srcConnID(), @@ -202,6 +196,7 @@ func (c *Conn) confirmHandshake(now time.Time) { // don't need to send anything. c.handshakeConfirmed.setReceived() } + c.restartIdleTimer(now) c.loss.confirmHandshake() // "An endpoint MUST discard its Handshake keys when the TLS handshake is confirmed" // https://www.rfc-editor.org/rfc/rfc9001#section-4.9.2-1 @@ -232,6 +227,7 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error { c.streams.peerInitialMaxStreamDataBidiLocal = p.initialMaxStreamDataBidiLocal c.streams.peerInitialMaxStreamDataRemote[bidiStream] = p.initialMaxStreamDataBidiRemote c.streams.peerInitialMaxStreamDataRemote[uniStream] = p.initialMaxStreamDataUni + c.receivePeerMaxIdleTimeout(p.maxIdleTimeout) c.peerAckDelayExponent = p.ackDelayExponent c.loss.setMaxAckDelay(p.maxAckDelay) if err := c.connIDState.setPeerActiveConnIDLimit(c, p.activeConnIDLimit); err != nil { @@ -248,7 +244,6 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error { return err } } - // TODO: max_idle_timeout // TODO: stateless_reset_token // TODO: max_udp_payload_size // TODO: disable_active_migration @@ -261,6 +256,8 @@ type ( wakeEvent struct{} ) +var errIdleTimeout = errors.New("idle timeout") + // loop is the connection main loop. // // Except where otherwise noted, all connection state is owned by the loop goroutine. @@ -288,14 +285,14 @@ func (c *Conn) loop(now time.Time) { defer timer.Stop() } - for !c.exited { + for c.lifetime.state != connStateDone { sendTimeout := c.maybeSend(now) // try sending // Note that we only need to consider the ack timer for the App Data space, // since the Initial and Handshake spaces always ack immediately. nextTimeout := sendTimeout - nextTimeout = firstTime(nextTimeout, c.idleTimeout) - if !c.isClosingOrDraining() { + nextTimeout = firstTime(nextTimeout, c.idle.nextTimeout) + if c.isAlive() { nextTimeout = firstTime(nextTimeout, c.loss.timer) nextTimeout = firstTime(nextTimeout, c.acks[appDataSpace].nextAck) } else { @@ -329,11 +326,9 @@ func (c *Conn) loop(now time.Time) { m.recycle() case timerEvent: // A connection timer has expired. - if !now.Before(c.idleTimeout) { - // "[...] the connection is silently closed and - // its state is discarded [...]" - // https://www.rfc-editor.org/rfc/rfc9000#section-10.1-1 - c.exited = true + if c.idleAdvance(now) { + // The connection idle timer has expired. + c.abortImmediately(now, errIdleTimeout) return } c.loss.advance(now, c.handleAckOrLoss) diff --git a/internal/quic/conn_close.go b/internal/quic/conn_close.go index a9ef0db5e3..246a126382 100644 --- a/internal/quic/conn_close.go +++ b/internal/quic/conn_close.go @@ -12,33 +12,54 @@ import ( "time" ) +// connState is the state of a connection. +type connState int + +const ( + // A connection is alive when it is first created. + connStateAlive = connState(iota) + + // The connection has received a CONNECTION_CLOSE frame from the peer, + // and has not yet sent a CONNECTION_CLOSE in response. + // + // We will send a CONNECTION_CLOSE, and then enter the draining state. + connStatePeerClosed + + // The connection is in the closing state. + // + // We will send CONNECTION_CLOSE frames to the peer + // (once upon entering the closing state, and possibly again in response to peer packets). + // + // If we receive a CONNECTION_CLOSE from the peer, we will enter the draining state. + // Otherwise, we will eventually time out and move to the done state. + // + // https://www.rfc-editor.org/rfc/rfc9000#section-10.2.1 + connStateClosing + + // The connection is in the draining state. + // + // We will neither send packets nor process received packets. + // When the drain timer expires, we move to the done state. + // + // https://www.rfc-editor.org/rfc/rfc9000#section-10.2.2 + connStateDraining + + // The connection is done, and the conn loop will exit. + connStateDone +) + // lifetimeState tracks the state of a connection. // // This is fairly coupled to the rest of a Conn, but putting it in a struct of its own helps // reason about operations that cause state transitions. type lifetimeState struct { - readyc chan struct{} // closed when TLS handshake completes - drainingc chan struct{} // closed when entering the draining state + state connState + + readyc chan struct{} // closed when TLS handshake completes + donec chan struct{} // closed when finalErr is set - // Possible states for the connection: - // - // Alive: localErr and finalErr are both nil. - // - // Closing: localErr is non-nil and finalErr is nil. - // We have sent a CONNECTION_CLOSE to the peer or are about to - // (if connCloseSentTime is zero) and are waiting for the peer to respond. - // drainEndTime is set to the time the closing state ends. - // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.1 - // - // Draining: finalErr is non-nil. - // If localErr is nil, we're waiting for the user to provide us with a final status - // to send to the peer. - // Otherwise, we've either sent a CONNECTION_CLOSE to the peer or are about to - // (if connCloseSentTime is zero). - // drainEndTime is set to the time the draining state ends. - // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2 localErr error // error sent to the peer - finalErr error // error sent by the peer, or transport error; always set before draining + finalErr error // error sent by the peer, or transport error; set before closing donec connCloseSentTime time.Time // send time of last CONNECTION_CLOSE frame connCloseDelay time.Duration // delay until next CONNECTION_CLOSE frame sent @@ -47,7 +68,7 @@ type lifetimeState struct { func (c *Conn) lifetimeInit() { c.lifetime.readyc = make(chan struct{}) - c.lifetime.drainingc = make(chan struct{}) + c.lifetime.donec = make(chan struct{}) } var errNoPeerResponse = errors.New("peer did not respond to CONNECTION_CLOSE") @@ -60,13 +81,25 @@ func (c *Conn) lifetimeAdvance(now time.Time) (done bool) { // The connection drain period has ended, and we can shut down. // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2-7 c.lifetime.drainEndTime = time.Time{} - if c.lifetime.finalErr == nil { - // The peer never responded to our CONNECTION_CLOSE. - c.enterDraining(now, errNoPeerResponse) + if c.lifetime.state != connStateDraining { + // We were in the closing state, waiting for a CONNECTION_CLOSE from the peer. + c.setFinalError(errNoPeerResponse) } + c.setState(now, connStateDone) return true } +// setState sets the conn state. +func (c *Conn) setState(now time.Time, state connState) { + switch state { + case connStateClosing, connStateDraining: + if c.lifetime.drainEndTime.IsZero() { + c.lifetime.drainEndTime = now.Add(3 * c.loss.ptoBasePeriod()) + } + } + c.lifetime.state = state +} + // confirmHandshake is called when the TLS handshake completes. func (c *Conn) handshakeDone() { close(c.lifetime.readyc) @@ -81,44 +114,66 @@ func (c *Conn) handshakeDone() { // // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2 func (c *Conn) isDraining() bool { - return c.lifetime.finalErr != nil + switch c.lifetime.state { + case connStateDraining, connStateDone: + return true + } + return false } -// isClosingOrDraining reports whether the conn is in the closing or draining states. -func (c *Conn) isClosingOrDraining() bool { - return c.lifetime.localErr != nil || c.lifetime.finalErr != nil +// isAlive reports whether the conn is handling packets. +func (c *Conn) isAlive() bool { + return c.lifetime.state == connStateAlive } // sendOK reports whether the conn can send frames at this time. func (c *Conn) sendOK(now time.Time) bool { - if !c.isClosingOrDraining() { + switch c.lifetime.state { + case connStateAlive: return true - } - // We are closing or draining. - if c.lifetime.localErr == nil { - // We're waiting for the user to close the connection, providing us with - // a final status to send to the peer. + case connStatePeerClosed: + if c.lifetime.localErr == nil { + // We're waiting for the user to close the connection, providing us with + // a final status to send to the peer. + return false + } + // We should send a CONNECTION_CLOSE. + return true + case connStateClosing: + if c.lifetime.connCloseSentTime.IsZero() { + return true + } + maxRecvTime := c.acks[initialSpace].maxRecvTime + if t := c.acks[handshakeSpace].maxRecvTime; t.After(maxRecvTime) { + maxRecvTime = t + } + if t := c.acks[appDataSpace].maxRecvTime; t.After(maxRecvTime) { + maxRecvTime = t + } + if maxRecvTime.Before(c.lifetime.connCloseSentTime.Add(c.lifetime.connCloseDelay)) { + // After sending CONNECTION_CLOSE, ignore packets from the peer for + // a delay. On the next packet received after the delay, send another + // CONNECTION_CLOSE. + return false + } + return true + case connStateDraining: + // We are in the draining state, and will send no more packets. return false + case connStateDone: + return false + default: + panic("BUG: unhandled connection state") } - // Past this point, returning true will result in the conn sending a CONNECTION_CLOSE - // due to localErr being set. - if c.lifetime.drainEndTime.IsZero() { - // The closing and draining states should last for at least three times - // the current PTO interval. We currently use exactly that minimum. - // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2-5 - // - // The drain period begins when we send or receive a CONNECTION_CLOSE, - // whichever comes first. - // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2-3 - c.lifetime.drainEndTime = now.Add(3 * c.loss.ptoBasePeriod()) +} + +// sendConnectionClose reports that the conn has sent a CONNECTION_CLOSE to the peer. +func (c *Conn) sentConnectionClose(now time.Time) { + switch c.lifetime.state { + case connStatePeerClosed: + c.enterDraining(now) } if c.lifetime.connCloseSentTime.IsZero() { - // We haven't sent a CONNECTION_CLOSE yet. Do so. - // Either we're initiating an immediate close - // (and will enter the closing state as soon as we send CONNECTION_CLOSE), - // or we've read a CONNECTION_CLOSE from our peer - // (and may send one CONNECTION_CLOSE before entering the draining state). - // // Set the initial delay before we will send another CONNECTION_CLOSE. // // RFC 9000 states that we should rate limit CONNECTION_CLOSE frames, @@ -126,65 +181,56 @@ func (c *Conn) sendOK(now time.Time) bool { // with the same delay as the PTO timer (RFC 9002, Section 6.2.1), // not including max_ack_delay, and double it on every CONNECTION_CLOSE sent. c.lifetime.connCloseDelay = c.loss.rtt.smoothedRTT + max(4*c.loss.rtt.rttvar, timerGranularity) - c.lifetime.drainEndTime = now.Add(3 * c.loss.ptoBasePeriod()) - return true - } - if c.isDraining() { - // We are in the draining state, and will send no more packets. - return false - } - maxRecvTime := c.acks[initialSpace].maxRecvTime - if t := c.acks[handshakeSpace].maxRecvTime; t.After(maxRecvTime) { - maxRecvTime = t - } - if t := c.acks[appDataSpace].maxRecvTime; t.After(maxRecvTime) { - maxRecvTime = t - } - if maxRecvTime.Before(c.lifetime.connCloseSentTime.Add(c.lifetime.connCloseDelay)) { - // After sending CONNECTION_CLOSE, ignore packets from the peer for - // a delay. On the next packet received after the delay, send another - // CONNECTION_CLOSE. - return false + } else if !c.lifetime.connCloseSentTime.Equal(now) { + // If connCloseSentTime == now, we're sending two CONNECTION_CLOSE frames + // coalesced into the same datagram. We only want to increase the delay once. + c.lifetime.connCloseDelay *= 2 } c.lifetime.connCloseSentTime = now - c.lifetime.connCloseDelay *= 2 - return true } -// enterDraining enters the draining state. -func (c *Conn) enterDraining(now time.Time, err error) { - if c.isDraining() { - return +// handlePeerConnectionClose handles a CONNECTION_CLOSE from the peer. +func (c *Conn) handlePeerConnectionClose(now time.Time, err error) { + c.setFinalError(err) + switch c.lifetime.state { + case connStateAlive: + c.setState(now, connStatePeerClosed) + case connStatePeerClosed: + // Duplicate CONNECTION_CLOSE, ignore. + case connStateClosing: + if c.lifetime.connCloseSentTime.IsZero() { + c.setState(now, connStatePeerClosed) + } else { + c.setState(now, connStateDraining) + } + case connStateDraining: + case connStateDone: } - if err == errStatelessReset { - // If we've received a stateless reset, then we must not send a CONNECTION_CLOSE. - // Setting connCloseSentTime here prevents us from doing so. - c.lifetime.finalErr = errStatelessReset - c.lifetime.localErr = errStatelessReset - c.lifetime.connCloseSentTime = now - } else if e, ok := c.lifetime.localErr.(localTransportError); ok && e.code != errNo { - // If we've terminated the connection due to a peer protocol violation, - // record the final error on the connection as our reason for termination. - c.lifetime.finalErr = c.lifetime.localErr - } else { - c.lifetime.finalErr = err +} + +// setFinalError records the final connection status we report to the user. +func (c *Conn) setFinalError(err error) { + select { + case <-c.lifetime.donec: + return // already set + default: } - close(c.lifetime.drainingc) - c.streams.queue.close(c.lifetime.finalErr) + c.lifetime.finalErr = err + close(c.lifetime.donec) } func (c *Conn) waitReady(ctx context.Context) error { select { case <-c.lifetime.readyc: return nil - case <-c.lifetime.drainingc: + case <-c.lifetime.donec: return c.lifetime.finalErr default: } select { case <-c.lifetime.readyc: return nil - case <-c.lifetime.drainingc: + case <-c.lifetime.donec: return c.lifetime.finalErr case <-ctx.Done(): return ctx.Err() @@ -199,7 +245,7 @@ func (c *Conn) waitReady(ctx context.Context) error { // err := conn.Wait(context.Background()) func (c *Conn) Close() error { c.Abort(nil) - <-c.lifetime.drainingc + <-c.lifetime.donec return c.lifetime.finalErr } @@ -213,7 +259,7 @@ func (c *Conn) Close() error { // containing the peer's error code and reason. // If the peer closes the connection with any other status, Wait returns a non-nil error. func (c *Conn) Wait(ctx context.Context) error { - if err := c.waitOnDone(ctx, c.lifetime.drainingc); err != nil { + if err := c.waitOnDone(ctx, c.lifetime.donec); err != nil { return err } return c.lifetime.finalErr @@ -229,30 +275,46 @@ func (c *Conn) Abort(err error) { err = localTransportError{code: errNo} } c.sendMsg(func(now time.Time, c *Conn) { - c.abort(now, err) + c.enterClosing(now, err) }) } // abort terminates a connection with an error. func (c *Conn) abort(now time.Time, err error) { - if c.lifetime.localErr != nil { - return // already closing - } - c.lifetime.localErr = err + c.setFinalError(err) // this error takes precedence over the peer's CONNECTION_CLOSE + c.enterClosing(now, err) } // abortImmediately terminates a connection. // The connection does not send a CONNECTION_CLOSE, and skips the draining period. func (c *Conn) abortImmediately(now time.Time, err error) { - c.abort(now, err) - c.enterDraining(now, err) - c.exited = true + c.setFinalError(err) + c.setState(now, connStateDone) +} + +// enterClosing starts an immediate close. +// We will send a CONNECTION_CLOSE to the peer and wait for their response. +func (c *Conn) enterClosing(now time.Time, err error) { + switch c.lifetime.state { + case connStateAlive: + c.lifetime.localErr = err + c.setState(now, connStateClosing) + case connStatePeerClosed: + c.lifetime.localErr = err + } +} + +// enterDraining moves directly to the draining state, without sending a CONNECTION_CLOSE. +func (c *Conn) enterDraining(now time.Time) { + switch c.lifetime.state { + case connStateAlive, connStatePeerClosed, connStateClosing: + c.setState(now, connStateDraining) + } } // exit fully terminates a connection immediately. func (c *Conn) exit() { c.sendMsg(func(now time.Time, c *Conn) { - c.enterDraining(now, errors.New("connection closed")) - c.exited = true + c.abortImmediately(now, errors.New("connection closed")) }) } diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 896c6d74ed..156ef5dd50 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -61,7 +61,7 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { // Invalid data at the end of a datagram is ignored. break } - c.idleTimeout = now.Add(c.maxIdleTimeout) + c.idleHandlePacketReceived(now) buf = buf[n:] } } @@ -525,7 +525,7 @@ func (c *Conn) handleConnectionCloseTransportFrame(now time.Time, payload []byte if n < 0 { return -1 } - c.enterDraining(now, peerTransportError{code: code, reason: reason}) + c.handlePeerConnectionClose(now, peerTransportError{code: code, reason: reason}) return n } @@ -534,7 +534,7 @@ func (c *Conn) handleConnectionCloseApplicationFrame(now time.Time, payload []by if n < 0 { return -1 } - c.enterDraining(now, &ApplicationError{Code: code, Reason: reason}) + c.handlePeerConnectionClose(now, &ApplicationError{Code: code, Reason: reason}) return n } @@ -548,7 +548,7 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa }) return -1 } - if !c.isClosingOrDraining() { + if c.isAlive() { c.confirmHandshake(now) } return 1 @@ -560,5 +560,6 @@ func (c *Conn) handleStatelessReset(now time.Time, resetToken statelessResetToke if !c.connIDState.isValidStatelessResetToken(resetToken) { return } - c.enterDraining(now, errStatelessReset) + c.setFinalError(errStatelessReset) + c.enterDraining(now) } diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 22e780479f..e45dc8af3d 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -77,6 +77,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } sentInitial = c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysInitial.w, p) if sentInitial != nil { + c.idleHandlePacketSent(now, sentInitial) // Client initial packets and ack-eliciting server initial packaets // need to be sent in a datagram padded to at least 1200 bytes. // We can't add the padding yet, however, since we may want to @@ -104,6 +105,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { logSentPacket(c, packetTypeHandshake, pnum, p.srcConnID, p.dstConnID, c.w.payload()) } if sent := c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysHandshake.w, p); sent != nil { + c.idleHandlePacketSent(now, sent) c.loss.packetSent(now, handshakeSpace, sent) if c.side == clientSide { // "[...] a client MUST discard Initial keys when it first @@ -131,6 +133,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { logSentPacket(c, packetType1RTT, pnum, nil, dstConnID, c.w.payload()) } if sent := c.w.finish1RTTPacket(pnum, pnumMaxAcked, dstConnID, &c.keysAppData); sent != nil { + c.idleHandlePacketSent(now, sent) c.loss.packetSent(now, appDataSpace, sent) } } @@ -261,6 +264,10 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, if !c.appendStreamFrames(&c.w, pnum, pto) { return } + + if !c.appendKeepAlive(now) { + return + } } // If this is a PTO probe and we haven't added an ack-eliciting frame yet, @@ -325,7 +332,7 @@ func (c *Conn) appendAckFrame(now time.Time, space numberSpace) bool { } func (c *Conn) appendConnectionCloseFrame(now time.Time, space numberSpace, err error) { - c.lifetime.connCloseSentTime = now + c.sentConnectionClose(now) switch e := err.(type) { case localTransportError: c.w.appendConnectionCloseTransportFrame(e.code, 0, e.reason) @@ -342,11 +349,12 @@ func (c *Conn) appendConnectionCloseFrame(now time.Time, space numberSpace, err // TLS alerts are sent using error codes [0x0100,0x01ff). // https://www.rfc-editor.org/rfc/rfc9000#section-20.1-2.36.1 var alert tls.AlertError - if errors.As(err, &alert) { + switch { + case errors.As(err, &alert): // tls.AlertError is a uint8, so this can't exceed 0x01ff. code := errTLSBase + transportError(alert) c.w.appendConnectionCloseTransportFrame(code, 0, "") - } else { + default: c.w.appendConnectionCloseTransportFrame(errInternal, 0, "") } } diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 514a8775e9..70ba7b3926 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -25,6 +25,7 @@ var testVV = flag.Bool("vv", false, "even more verbose test output") func TestConnTestConn(t *testing.T) { tc := newTestConn(t, serverSide) + tc.handshake() if got, want := tc.timeUntilEvent(), defaultMaxIdleTimeout; got != want { t.Errorf("new conn timeout=%v, want %v (max_idle_timeout)", got, want) } @@ -49,8 +50,8 @@ func TestConnTestConn(t *testing.T) { tc.wait() tc.advanceToTimer() - if !tc.conn.exited { - t.Errorf("after advancing to idle timeout, exited = false, want true") + if got := tc.conn.lifetime.state; got != connStateDone { + t.Errorf("after advancing to idle timeout, conn state = %v, want done", got) } } diff --git a/internal/quic/idle.go b/internal/quic/idle.go new file mode 100644 index 0000000000..f5b2422adb --- /dev/null +++ b/internal/quic/idle.go @@ -0,0 +1,170 @@ +// 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" +) + +// idleState tracks connection idle events. +// +// Before the handshake is confirmed, the idle timeout is Config.HandshakeTimeout. +// +// After the handshake is confirmed, the idle timeout is +// the minimum of Config.MaxIdleTimeout and the peer's max_idle_timeout transport parameter. +// +// If KeepAlivePeriod is set, keep-alive pings are sent. +// Keep-alives are only sent after the handshake is confirmed. +// +// https://www.rfc-editor.org/rfc/rfc9000#section-10.1 +type idleState struct { + // idleDuration is the negotiated idle timeout for the connection. + idleDuration time.Duration + + // idleTimeout is the time at which the connection will be closed due to inactivity. + idleTimeout time.Time + + // nextTimeout is the time of the next idle event. + // If nextTimeout == idleTimeout, this is the idle timeout. + // Otherwise, this is the keep-alive timeout. + nextTimeout time.Time + + // sentSinceLastReceive is set if we have sent an ack-eliciting packet + // since the last time we received and processed a packet from the peer. + sentSinceLastReceive bool +} + +// receivePeerMaxIdleTimeout handles the peer's max_idle_timeout transport parameter. +func (c *Conn) receivePeerMaxIdleTimeout(peerMaxIdleTimeout time.Duration) { + localMaxIdleTimeout := c.config.maxIdleTimeout() + switch { + case localMaxIdleTimeout == 0: + c.idle.idleDuration = peerMaxIdleTimeout + case peerMaxIdleTimeout == 0: + c.idle.idleDuration = localMaxIdleTimeout + default: + c.idle.idleDuration = min(localMaxIdleTimeout, peerMaxIdleTimeout) + } +} + +func (c *Conn) idleHandlePacketReceived(now time.Time) { + if !c.handshakeConfirmed.isSet() { + return + } + // "An endpoint restarts its idle timer when a packet from its peer is + // received and processed successfully." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.1-3 + c.idle.sentSinceLastReceive = false + c.restartIdleTimer(now) +} + +func (c *Conn) idleHandlePacketSent(now time.Time, sent *sentPacket) { + // "An endpoint also restarts its idle timer when sending an ack-eliciting packet + // if no other ack-eliciting packets have been sent since + // last receiving and processing a packet." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.1-3 + if c.idle.sentSinceLastReceive || !sent.ackEliciting || !c.handshakeConfirmed.isSet() { + return + } + c.idle.sentSinceLastReceive = true + c.restartIdleTimer(now) +} + +func (c *Conn) restartIdleTimer(now time.Time) { + if !c.isAlive() { + // Connection is closing, disable timeouts. + c.idle.idleTimeout = time.Time{} + c.idle.nextTimeout = time.Time{} + return + } + var idleDuration time.Duration + if c.handshakeConfirmed.isSet() { + idleDuration = c.idle.idleDuration + } else { + idleDuration = c.config.handshakeTimeout() + } + if idleDuration == 0 { + c.idle.idleTimeout = time.Time{} + } else { + // "[...] endpoints MUST increase the idle timeout period to be + // at least three times the current Probe Timeout (PTO)." + // https://www.rfc-editor.org/rfc/rfc9000#section-10.1-4 + idleDuration = max(idleDuration, 3*c.loss.ptoPeriod()) + c.idle.idleTimeout = now.Add(idleDuration) + } + // Set the time of our next event: + // The idle timer if no keep-alive is set, or the keep-alive timer if one is. + c.idle.nextTimeout = c.idle.idleTimeout + keepAlive := c.config.keepAlivePeriod() + switch { + case !c.handshakeConfirmed.isSet(): + // We do not send keep-alives before the handshake is complete. + case keepAlive <= 0: + // Keep-alives are not enabled. + case c.idle.sentSinceLastReceive: + // We have sent an ack-eliciting packet to the peer. + // If they don't acknowledge it, loss detection will follow up with PTO probes, + // which will function as keep-alives. + // We don't need to send further pings. + case idleDuration == 0: + // The connection does not have a negotiated idle timeout. + // Send keep-alives anyway, since they may be required to keep middleboxes + // from losing state. + c.idle.nextTimeout = now.Add(keepAlive) + default: + // Schedule our next keep-alive. + // If our configured keep-alive period is greater than half the negotiated + // connection idle timeout, we reduce the keep-alive period to half + // the idle timeout to ensure we have time for the ping to arrive. + c.idle.nextTimeout = now.Add(min(keepAlive, idleDuration/2)) + } +} + +func (c *Conn) appendKeepAlive(now time.Time) bool { + if c.idle.nextTimeout.IsZero() || c.idle.nextTimeout.After(now) { + return true // timer has not expired + } + if c.idle.nextTimeout.Equal(c.idle.idleTimeout) { + return true // no keepalive timer set, only idle + } + if c.idle.sentSinceLastReceive { + return true // already sent an ack-eliciting packet + } + if c.w.sent.ackEliciting { + return true // this packet is already ack-eliciting + } + // Send an ack-eliciting PING frame to the peer to keep the connection alive. + return c.w.appendPingFrame() +} + +var errHandshakeTimeout error = localTransportError{ + code: errConnectionRefused, + reason: "handshake timeout", +} + +func (c *Conn) idleAdvance(now time.Time) (shouldExit bool) { + if c.idle.idleTimeout.IsZero() || now.Before(c.idle.idleTimeout) { + return false + } + c.idle.idleTimeout = time.Time{} + c.idle.nextTimeout = time.Time{} + if !c.handshakeConfirmed.isSet() { + // Handshake timeout has expired. + // If we're a server, we're refusing the too-slow client. + // If we're a client, we're giving up. + // In either case, we're going to send a CONNECTION_CLOSE frame and + // enter the closing state rather than unceremoniously dropping the connection, + // since the peer might still be trying to complete the handshake. + c.abort(now, errHandshakeTimeout) + return false + } + // Idle timeout has expired. + // + // "[...] the connection is silently closed and its state is discarded [...]" + // https://www.rfc-editor.org/rfc/rfc9000#section-10.1-1 + return true +} diff --git a/internal/quic/idle_test.go b/internal/quic/idle_test.go new file mode 100644 index 0000000000..18f6a690a4 --- /dev/null +++ b/internal/quic/idle_test.go @@ -0,0 +1,225 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "crypto/tls" + "fmt" + "testing" + "time" +) + +func TestHandshakeTimeoutExpiresServer(t *testing.T) { + const timeout = 5 * time.Second + tc := newTestConn(t, serverSide, func(c *Config) { + c.HandshakeTimeout = timeout + }) + tc.ignoreFrame(frameTypeAck) + tc.ignoreFrame(frameTypeNewConnectionID) + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + // Server starts its end of the handshake. + // Client acks these packets to avoid starting the PTO timer. + tc.wantFrameType("server sends Initial CRYPTO flight", + packetTypeInitial, debugFrameCrypto{}) + tc.writeAckForAll() + tc.wantFrameType("server sends Handshake CRYPTO flight", + packetTypeHandshake, debugFrameCrypto{}) + tc.writeAckForAll() + + if got, want := tc.timerDelay(), timeout; got != want { + t.Errorf("connection timer = %v, want %v (handshake timeout)", got, want) + } + + // Client sends a packet, but this does not extend the handshake timer. + tc.advance(1 * time.Second) + tc.writeFrames(packetTypeHandshake, debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][:1], // partial data + }) + tc.wantIdle("handshake is not complete") + + tc.advance(timeout - 1*time.Second) + tc.wantFrame("server closes connection after handshake timeout", + packetTypeHandshake, debugFrameConnectionCloseTransport{ + code: errConnectionRefused, + }) +} + +func TestHandshakeTimeoutExpiresClient(t *testing.T) { + const timeout = 5 * time.Second + tc := newTestConn(t, clientSide, func(c *Config) { + c.HandshakeTimeout = timeout + }) + tc.ignoreFrame(frameTypeAck) + tc.ignoreFrame(frameTypeNewConnectionID) + // Start the handshake. + // The client always sets a PTO timer until it gets an ack for a handshake packet + // or confirms the handshake, so proceed far enough through the handshake to + // let us not worry about PTO. + tc.wantFrameType("client sends Initial CRYPTO flight", + packetTypeInitial, debugFrameCrypto{}) + tc.writeAckForAll() + tc.writeFrames(packetTypeInitial, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], + }) + tc.writeFrames(packetTypeHandshake, + debugFrameCrypto{ + data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake], + }) + tc.wantFrameType("client sends Handshake CRYPTO flight", + packetTypeHandshake, debugFrameCrypto{}) + tc.writeAckForAll() + tc.wantIdle("client is waiting for end of handshake") + + if got, want := tc.timerDelay(), timeout; got != want { + t.Errorf("connection timer = %v, want %v (handshake timeout)", got, want) + } + tc.advance(timeout) + tc.wantFrame("client closes connection after handshake timeout", + packetTypeHandshake, debugFrameConnectionCloseTransport{ + code: errConnectionRefused, + }) +} + +func TestIdleTimeoutExpires(t *testing.T) { + for _, test := range []struct { + localMaxIdleTimeout time.Duration + peerMaxIdleTimeout time.Duration + wantTimeout time.Duration + }{{ + localMaxIdleTimeout: 10 * time.Second, + peerMaxIdleTimeout: 20 * time.Second, + wantTimeout: 10 * time.Second, + }, { + localMaxIdleTimeout: 20 * time.Second, + peerMaxIdleTimeout: 10 * time.Second, + wantTimeout: 10 * time.Second, + }, { + localMaxIdleTimeout: 0, + peerMaxIdleTimeout: 10 * time.Second, + wantTimeout: 10 * time.Second, + }, { + localMaxIdleTimeout: 10 * time.Second, + peerMaxIdleTimeout: 0, + wantTimeout: 10 * time.Second, + }} { + name := fmt.Sprintf("local=%v/peer=%v", test.localMaxIdleTimeout, test.peerMaxIdleTimeout) + t.Run(name, func(t *testing.T) { + tc := newTestConn(t, serverSide, func(p *transportParameters) { + p.maxIdleTimeout = test.peerMaxIdleTimeout + }, func(c *Config) { + c.MaxIdleTimeout = test.localMaxIdleTimeout + }) + tc.handshake() + if got, want := tc.timeUntilEvent(), test.wantTimeout; got != want { + t.Errorf("new conn timeout=%v, want %v (idle timeout)", got, want) + } + tc.advance(test.wantTimeout - 1) + tc.wantIdle("connection is idle and alive prior to timeout") + ctx := canceledContext() + if err := tc.conn.Wait(ctx); err != context.Canceled { + t.Fatalf("conn.Wait() = %v, want Canceled", err) + } + tc.advance(1) + tc.wantIdle("connection exits after timeout") + if err := tc.conn.Wait(ctx); err != errIdleTimeout { + t.Fatalf("conn.Wait() = %v, want errIdleTimeout", err) + } + }) + } +} + +func TestIdleTimeoutKeepAlive(t *testing.T) { + for _, test := range []struct { + idleTimeout time.Duration + keepAlive time.Duration + wantTimeout time.Duration + }{{ + idleTimeout: 30 * time.Second, + keepAlive: 10 * time.Second, + wantTimeout: 10 * time.Second, + }, { + idleTimeout: 10 * time.Second, + keepAlive: 30 * time.Second, + wantTimeout: 5 * time.Second, + }, { + idleTimeout: -1, // disabled + keepAlive: 30 * time.Second, + wantTimeout: 30 * time.Second, + }} { + name := fmt.Sprintf("idle_timeout=%v/keepalive=%v", test.idleTimeout, test.keepAlive) + t.Run(name, func(t *testing.T) { + tc := newTestConn(t, serverSide, func(c *Config) { + c.MaxIdleTimeout = test.idleTimeout + c.KeepAlivePeriod = test.keepAlive + }) + tc.handshake() + if got, want := tc.timeUntilEvent(), test.wantTimeout; got != want { + t.Errorf("new conn timeout=%v, want %v (keepalive timeout)", got, want) + } + tc.advance(test.wantTimeout - 1) + tc.wantIdle("connection is idle prior to timeout") + tc.advance(1) + tc.wantFrameType("keep-alive ping is sent", packetType1RTT, + debugFramePing{}) + }) + } +} + +func TestIdleLongTermKeepAliveSent(t *testing.T) { + // This test examines a connection sitting idle and sending periodic keep-alive pings. + const keepAlivePeriod = 30 * time.Second + tc := newTestConn(t, clientSide, func(c *Config) { + c.KeepAlivePeriod = keepAlivePeriod + c.MaxIdleTimeout = -1 + }) + tc.handshake() + // The handshake will have completed a little bit after the point at which the + // keepalive timer was set. Send two PING frames to the conn, triggering an immediate ack + // and resetting the timer. + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.writeFrames(packetType1RTT, debugFramePing{}) + tc.wantFrameType("conn acks received pings", packetType1RTT, debugFrameAck{}) + for i := 0; i < 10; i++ { + tc.wantIdle("conn has nothing more to send") + if got, want := tc.timeUntilEvent(), keepAlivePeriod; got != want { + t.Errorf("i=%v conn timeout=%v, want %v (keepalive timeout)", i, got, want) + } + tc.advance(keepAlivePeriod) + tc.wantFrameType("keep-alive ping is sent", packetType1RTT, + debugFramePing{}) + tc.writeAckForAll() + } +} + +func TestIdleLongTermKeepAliveReceived(t *testing.T) { + // This test examines a connection sitting idle, but receiving periodic peer + // traffic to keep the connection alive. + const idleTimeout = 30 * time.Second + tc := newTestConn(t, serverSide, func(c *Config) { + c.MaxIdleTimeout = idleTimeout + }) + tc.handshake() + for i := 0; i < 10; i++ { + tc.advance(idleTimeout - 1*time.Second) + tc.writeFrames(packetType1RTT, debugFramePing{}) + if got, want := tc.timeUntilEvent(), maxAckDelay-timerGranularity; got != want { + t.Errorf("i=%v conn timeout=%v, want %v (max_ack_delay)", i, got, want) + } + tc.advanceToTimer() + tc.wantFrameType("conn acks received ping", packetType1RTT, debugFrameAck{}) + } + // Connection is still alive. + ctx := canceledContext() + if err := tc.conn.Wait(ctx); err != context.Canceled { + t.Fatalf("conn.Wait() = %v, want Canceled", err) + } +} diff --git a/internal/quic/loss.go b/internal/quic/loss.go index c0f915b422..4a0767bd0b 100644 --- a/internal/quic/loss.go +++ b/internal/quic/loss.go @@ -431,12 +431,15 @@ func (c *lossState) scheduleTimer(now time.Time) { c.timer = time.Time{} return } - // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.2.1 - pto := c.ptoBasePeriod() << c.ptoBackoffCount - c.timer = last.Add(pto) + c.timer = last.Add(c.ptoPeriod()) c.ptoTimerArmed = true } +func (c *lossState) ptoPeriod() time.Duration { + // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.2.1 + return c.ptoBasePeriod() << c.ptoBackoffCount +} + func (c *lossState) ptoBasePeriod() time.Duration { // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.2.1 pto := c.rtt.smoothedRTT + max(4*c.rtt.rttvar, timerGranularity) diff --git a/internal/quic/qlog.go b/internal/quic/qlog.go index 29875693ec..c8ee429fec 100644 --- a/internal/quic/qlog.go +++ b/internal/quic/qlog.go @@ -119,8 +119,13 @@ func (c *Conn) logConnectionClosed() { // TODO: Distinguish between peer and locally-initiated close. trigger = "application" case localTransportError: - if e.code == errNo { - trigger = "clean" + switch err { + case errHandshakeTimeout: + trigger = "handshake_timeout" + default: + if e.code == errNo { + trigger = "clean" + } } case peerTransportError: if e.code == errNo { @@ -128,10 +133,11 @@ func (c *Conn) logConnectionClosed() { } default: switch err { + case errIdleTimeout: + trigger = "idle_timeout" case errStatelessReset: trigger = "stateless_reset" } - // TODO: idle_timeout, handshake_timeout } // https://www.ietf.org/archive/id/draft-ietf-quic-qlog-quic-events-03.html#section-4.3 c.log.LogAttrs(context.Background(), QLogLevelEndpoint, diff --git a/internal/quic/qlog_test.go b/internal/quic/qlog_test.go index 5a2858b8bd..119f5d16af 100644 --- a/internal/quic/qlog_test.go +++ b/internal/quic/qlog_test.go @@ -14,6 +14,7 @@ import ( "log/slog" "reflect" "testing" + "time" "golang.org/x/net/internal/quic/qlog" ) @@ -54,6 +55,75 @@ func TestQLogHandshake(t *testing.T) { }) } +func TestQLogConnectionClosedTrigger(t *testing.T) { + for _, test := range []struct { + trigger string + connOpts []any + f func(*testConn) + }{{ + trigger: "clean", + f: func(tc *testConn) { + tc.handshake() + tc.conn.Abort(nil) + }, + }, { + trigger: "handshake_timeout", + connOpts: []any{ + func(c *Config) { + c.HandshakeTimeout = 5 * time.Second + }, + }, + f: func(tc *testConn) { + tc.ignoreFrame(frameTypeCrypto) + tc.ignoreFrame(frameTypeAck) + tc.ignoreFrame(frameTypePing) + tc.advance(5 * time.Second) + }, + }, { + trigger: "idle_timeout", + connOpts: []any{ + func(c *Config) { + c.MaxIdleTimeout = 5 * time.Second + }, + }, + f: func(tc *testConn) { + tc.handshake() + tc.advance(5 * time.Second) + }, + }, { + trigger: "error", + f: func(tc *testConn) { + tc.handshake() + tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{ + code: errProtocolViolation, + }) + tc.conn.Abort(nil) + }, + }} { + t.Run(test.trigger, func(t *testing.T) { + qr := &qlogRecord{} + tc := newTestConn(t, clientSide, append(test.connOpts, qr.config)...) + test.f(tc) + fr, ptype := tc.readFrame() + switch fr := fr.(type) { + case debugFrameConnectionCloseTransport: + tc.writeFrames(ptype, fr) + case nil: + default: + t.Fatalf("unexpected frame: %v", fr) + } + tc.wantIdle("connection should be idle while closing") + tc.advance(5 * time.Second) // long enough for the drain timer to expire + qr.wantEvents(t, jsonEvent{ + "name": "connectivity:connection_closed", + "data": map[string]any{ + "trigger": test.trigger, + }, + }) + }) + } +} + type nopCloseWriter struct { io.Writer } diff --git a/internal/quic/quic.go b/internal/quic/quic.go index 084887be67..6b60db869a 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -54,6 +54,12 @@ const ( maxPeerActiveConnIDLimit = 4 ) +// Time limit for completing the handshake. +const defaultHandshakeTimeout = 10 * time.Second + +// Keep-alive ping frequency. +const defaultKeepAlivePeriod = 0 + // Local timer granularity. // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.1.2-6 const timerGranularity = 1 * time.Millisecond From 399218d6bcdde008df7f43cf82a92b69e842c049 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 30 Oct 2023 15:07:39 -0700 Subject: [PATCH 101/168] quic: implement stream flush Do not commit data written to a stream to the network until the user explicitly flushes the stream, the stream output buffer fills, or the output buffer contains enough data to fill a packet. We could write data immediately (as net.TCPConn does), but this can require the user to put their own buffer in front of the stream. Since we necessarily need to maintain a retransmit buffer in the stream, this is redundant. We could do something like Nagle's algorithm, but nobody wants that. So make flushes explicit. For golang/go#58547 Change-Id: I29dc9d79556c7a358a360ef79beb38b45040b6bc Reviewed-on: https://go-review.googlesource.com/c/net/+/543083 Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn.go | 4 +- internal/quic/conn_flow_test.go | 7 ++ internal/quic/conn_loss_test.go | 5 +- internal/quic/conn_streams_test.go | 16 ++--- internal/quic/quic.go | 6 +- internal/quic/stream.go | 55 +++++++++++---- internal/quic/stream_test.go | 108 ++++++++++++++++++++++++++++- 7 files changed, 171 insertions(+), 30 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index b2b6a0877a..ff96ff7600 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -136,12 +136,10 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip } } - // The smallest allowed maximum QUIC datagram size is 1200 bytes. // TODO: PMTU discovery. - const maxDatagramSize = 1200 c.logConnectionStarted(cids.originalDstConnID, peerAddr) c.keysAppData.init() - c.loss.init(c.side, maxDatagramSize, now) + c.loss.init(c.side, smallestMaxDatagramSize, now) c.streamsInit() c.lifetimeInit() c.restartIdleTimer(now) diff --git a/internal/quic/conn_flow_test.go b/internal/quic/conn_flow_test.go index 03e0757a6d..39c879346c 100644 --- a/internal/quic/conn_flow_test.go +++ b/internal/quic/conn_flow_test.go @@ -262,6 +262,7 @@ func TestConnOutflowBlocked(t *testing.T) { if n != len(data) || err != nil { t.Fatalf("s.Write() = %v, %v; want %v, nil", n, err, len(data)) } + s.Flush() tc.wantFrame("stream writes data up to MAX_DATA limit", packetType1RTT, debugFrameStream{ @@ -310,6 +311,7 @@ func TestConnOutflowMaxDataDecreases(t *testing.T) { if n != len(data) || err != nil { t.Fatalf("s.Write() = %v, %v; want %v, nil", n, err, len(data)) } + s.Flush() tc.wantFrame("stream writes data up to MAX_DATA limit", packetType1RTT, debugFrameStream{ @@ -337,7 +339,9 @@ func TestConnOutflowMaxDataRoundRobin(t *testing.T) { } s1.Write(make([]byte, 10)) + s1.Flush() s2.Write(make([]byte, 10)) + s2.Flush() tc.writeFrames(packetType1RTT, debugFrameMaxData{ max: 1, @@ -378,6 +382,7 @@ func TestConnOutflowMetaAndData(t *testing.T) { data := makeTestData(32) s.Write(data) + s.Flush() s.CloseRead() tc.wantFrame("CloseRead sends a STOP_SENDING, not flow controlled", @@ -405,6 +410,7 @@ func TestConnOutflowResentData(t *testing.T) { data := makeTestData(15) s.Write(data[:8]) + s.Flush() tc.wantFrame("data is under MAX_DATA limit, all sent", packetType1RTT, debugFrameStream{ id: s.id, @@ -421,6 +427,7 @@ func TestConnOutflowResentData(t *testing.T) { }) s.Write(data[8:]) + s.Flush() tc.wantFrame("new data is sent up to the MAX_DATA limit", packetType1RTT, debugFrameStream{ id: s.id, diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index 5144be6acc..818816335b 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -183,7 +183,7 @@ func TestLostStreamFrameEmpty(t *testing.T) { if err != nil { t.Fatalf("NewStream: %v", err) } - c.Write(nil) // open the stream + c.Flush() // open the stream tc.wantFrame("created bidirectional stream 0", packetType1RTT, debugFrameStream{ id: newStreamID(clientSide, bidiStream, 0), @@ -213,6 +213,7 @@ func TestLostStreamWithData(t *testing.T) { p.initialMaxStreamDataUni = 1 << 20 }) s.Write(data[:4]) + s.Flush() tc.wantFrame("send [0,4)", packetType1RTT, debugFrameStream{ id: s.id, @@ -220,6 +221,7 @@ func TestLostStreamWithData(t *testing.T) { data: data[:4], }) s.Write(data[4:8]) + s.Flush() tc.wantFrame("send [4,8)", packetType1RTT, debugFrameStream{ id: s.id, @@ -263,6 +265,7 @@ func TestLostStreamPartialLoss(t *testing.T) { }) for i := range data { s.Write(data[i : i+1]) + s.Flush() tc.wantFrame(fmt.Sprintf("send STREAM frame with byte %v", i), packetType1RTT, debugFrameStream{ id: s.id, diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go index 69f982c3a6..c90354db8a 100644 --- a/internal/quic/conn_streams_test.go +++ b/internal/quic/conn_streams_test.go @@ -19,33 +19,33 @@ func TestStreamsCreate(t *testing.T) { tc := newTestConn(t, clientSide, permissiveTransportParameters) tc.handshake() - c, err := tc.conn.NewStream(ctx) + s, err := tc.conn.NewStream(ctx) if err != nil { t.Fatalf("NewStream: %v", err) } - c.Write(nil) // open the stream + s.Flush() // open the stream tc.wantFrame("created bidirectional stream 0", packetType1RTT, debugFrameStream{ id: 0, // client-initiated, bidi, number 0 data: []byte{}, }) - c, err = tc.conn.NewSendOnlyStream(ctx) + s, err = tc.conn.NewSendOnlyStream(ctx) if err != nil { t.Fatalf("NewStream: %v", err) } - c.Write(nil) // open the stream + s.Flush() // open the stream tc.wantFrame("created unidirectional stream 0", packetType1RTT, debugFrameStream{ id: 2, // client-initiated, uni, number 0 data: []byte{}, }) - c, err = tc.conn.NewStream(ctx) + s, err = tc.conn.NewStream(ctx) if err != nil { t.Fatalf("NewStream: %v", err) } - c.Write(nil) // open the stream + s.Flush() // open the stream tc.wantFrame("created bidirectional stream 1", packetType1RTT, debugFrameStream{ id: 4, // client-initiated, uni, number 4 @@ -177,11 +177,11 @@ func TestStreamsStreamSendOnly(t *testing.T) { tc := newTestConn(t, serverSide, permissiveTransportParameters) tc.handshake() - c, err := tc.conn.NewSendOnlyStream(ctx) + s, err := tc.conn.NewSendOnlyStream(ctx) if err != nil { t.Fatalf("NewStream: %v", err) } - c.Write(nil) // open the stream + s.Flush() // open the stream tc.wantFrame("created unidirectional stream 0", packetType1RTT, debugFrameStream{ id: 3, // server-initiated, uni, number 0 diff --git a/internal/quic/quic.go b/internal/quic/quic.go index 6b60db869a..e4d0d77c7f 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -64,10 +64,14 @@ const defaultKeepAlivePeriod = 0 // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.1.2-6 const timerGranularity = 1 * time.Millisecond +// The smallest allowed maximum datagram size. +// https://www.rfc-editor.org/rfc/rfc9000#section-14 +const smallestMaxDatagramSize = 1200 + // Minimum size of a UDP datagram sent by a client carrying an Initial packet, // or a server containing an ack-eliciting Initial packet. // https://www.rfc-editor.org/rfc/rfc9000#section-14.1 -const paddedInitialDatagramSize = 1200 +const paddedInitialDatagramSize = smallestMaxDatagramSize // Maximum number of streams of a given type which may be created. // https://www.rfc-editor.org/rfc/rfc9000.html#section-4.6-2 diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 58d84ed1b0..36c80f6af0 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -38,10 +38,11 @@ type Stream struct { // the write will fail. outgate gate out pipe // buffered data to send + outflushed int64 // offset of last flush call outwin int64 // maximum MAX_STREAM_DATA received from the peer outmaxsent int64 // maximum data offset we've sent to the peer outmaxbuf int64 // maximum amount of data we will buffer - outunsent rangeset[int64] // ranges buffered but not yet sent + outunsent rangeset[int64] // ranges buffered but not yet sent (only flushed data) outacked rangeset[int64] // ranges sent and acknowledged outopened sentVal // set if we should open the stream outclosed sentVal // set by CloseWrite @@ -240,8 +241,6 @@ func (s *Stream) Write(b []byte) (n int, err error) { // WriteContext writes data to the stream write buffer. // Buffered data is only sent when the buffer is sufficiently full. // Call the Flush method to ensure buffered data is sent. -// -// TODO: Implement Flush. func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) { if s.IsReadOnly() { return 0, errors.New("write to read-only stream") @@ -269,10 +268,6 @@ func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) s.outUnlock() return n, errors.New("write to closed stream") } - // We set outopened here rather than below, - // so if this is a zero-length write we still - // open the stream despite not writing any data to it. - s.outopened.set() if len(b) == 0 { break } @@ -282,13 +277,26 @@ func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) // Amount to write is min(the full buffer, data up to the write limit). // This is a number of bytes. nn := min(int64(len(b)), lim-s.out.end) - // Copy the data into the output buffer and mark it as unsent. - if s.out.end <= s.outwin { - s.outunsent.add(s.out.end, min(s.out.end+nn, s.outwin)) - } + // Copy the data into the output buffer. s.out.writeAt(b[:nn], s.out.end) b = b[nn:] n += int(nn) + // Possibly flush the output buffer. + // We automatically flush if: + // - We have enough data to consume the send window. + // Sending this data may cause the peer to extend the window. + // - We have buffered as much data as we're willing do. + // We need to send data to clear out buffer space. + // - We have enough data to fill a 1-RTT packet using the smallest + // possible maximum datagram size (1200 bytes, less header byte, + // connection ID, packet number, and AEAD overhead). + const autoFlushSize = smallestMaxDatagramSize - 1 - connIDLen - 1 - aeadOverhead + shouldFlush := s.out.end >= s.outwin || // peer send window is full + s.out.end >= lim || // local send buffer is full + (s.out.end-s.outflushed) >= autoFlushSize // enough data buffered + if shouldFlush { + s.flushLocked() + } if s.out.end > s.outwin { // We're blocked by flow control. // Send a STREAM_DATA_BLOCKED frame to let the peer know. @@ -301,6 +309,23 @@ func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) return n, nil } +// Flush flushes data written to the stream. +// It does not wait for the peer to acknowledge receipt of the data. +// Use CloseContext to wait for the peer's acknowledgement. +func (s *Stream) Flush() { + s.outgate.lock() + defer s.outUnlock() + s.flushLocked() +} + +func (s *Stream) flushLocked() { + s.outopened.set() + if s.outflushed < s.outwin { + s.outunsent.add(s.outflushed, min(s.outwin, s.out.end)) + } + s.outflushed = s.out.end +} + // Close closes the stream. // See CloseContext for more details. func (s *Stream) Close() error { @@ -363,6 +388,7 @@ func (s *Stream) CloseWrite() { s.outgate.lock() defer s.outUnlock() s.outclosed.set() + s.flushLocked() } // Reset aborts writes on the stream and notifies the peer @@ -612,8 +638,8 @@ func (s *Stream) handleMaxStreamData(maxStreamData int64) error { if maxStreamData <= s.outwin { return nil } - if s.out.end > s.outwin { - s.outunsent.add(s.outwin, min(maxStreamData, s.out.end)) + if s.outflushed > s.outwin { + s.outunsent.add(s.outwin, min(maxStreamData, s.outflushed)) } s.outwin = maxStreamData if s.out.end > s.outwin { @@ -741,10 +767,11 @@ func (s *Stream) appendOutFramesLocked(w *packetWriter, pnum packetNumber, pto b } for { // STREAM - off, size := dataToSend(min(s.out.start, s.outwin), min(s.out.end, s.outwin), s.outunsent, s.outacked, pto) + off, size := dataToSend(min(s.out.start, s.outwin), min(s.outflushed, s.outwin), s.outunsent, s.outacked, pto) if end := off + size; end > s.outmaxsent { // This will require connection-level flow control to send. end = min(end, s.outmaxsent+s.conn.streams.outflow.avail()) + end = max(end, off) size = end - off } fin := s.outclosed.isSet() && off+size == s.out.end diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 9bf2b5871d..93c8839fff 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -38,6 +38,7 @@ func TestStreamWriteBlockedByOutputBuffer(t *testing.T) { if n != writeBufferSize || err != context.Canceled { t.Fatalf("s.WriteContext() = %v, %v; want %v, context.Canceled", n, err, writeBufferSize) } + s.Flush() tc.wantFrame("first write buffer of data sent", packetType1RTT, debugFrameStream{ id: s.id, @@ -47,7 +48,9 @@ func TestStreamWriteBlockedByOutputBuffer(t *testing.T) { // Blocking write, which must wait for buffer space. w := runAsync(tc, func(ctx context.Context) (int, error) { - return s.WriteContext(ctx, want[writeBufferSize:]) + n, err := s.WriteContext(ctx, want[writeBufferSize:]) + s.Flush() + return n, err }) tc.wantIdle("write buffer is full, no more data can be sent") @@ -170,6 +173,7 @@ func TestStreamIgnoresMaxStreamDataReduction(t *testing.T) { t.Fatal(err) } s.WriteContext(ctx, want[:1]) + s.Flush() tc.wantFrame("sent data (1 byte) fits within flow control limit", packetType1RTT, debugFrameStream{ id: s.id, @@ -723,7 +727,7 @@ func testStreamSendFrameInvalidState(t *testing.T, f func(sid streamID) debugFra if err != nil { t.Fatal(err) } - s.Write(nil) // open the stream + s.Flush() // open the stream tc.wantFrame("new stream is opened", packetType1RTT, debugFrameStream{ id: sid, @@ -968,7 +972,9 @@ func TestStreamWriteMoreThanOnePacketOfData(t *testing.T) { want := make([]byte, 4096) rand.Read(want) // doesn't need to be crypto/rand, but non-deprecated and harmless w := runAsync(tc, func(ctx context.Context) (int, error) { - return s.WriteContext(ctx, want) + n, err := s.WriteContext(ctx, want) + s.Flush() + return n, err }) got := make([]byte, 0, len(want)) for { @@ -998,6 +1004,7 @@ func TestStreamCloseWaitsForAcks(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) data := make([]byte, 100) s.WriteContext(ctx, data) + s.Flush() tc.wantFrame("conn sends data for the stream", packetType1RTT, debugFrameStream{ id: s.id, @@ -1064,6 +1071,7 @@ func TestStreamCloseUnblocked(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) data := make([]byte, 100) s.WriteContext(ctx, data) + s.Flush() tc.wantFrame("conn sends data for the stream", packetType1RTT, debugFrameStream{ id: s.id, @@ -1228,6 +1236,7 @@ func TestStreamPeerStopSendingForActiveStream(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, styp, permissiveTransportParameters) for i := 0; i < 4; i++ { s.Write([]byte{byte(i)}) + s.Flush() tc.wantFrame("write sends a STREAM frame to peer", packetType1RTT, debugFrameStream{ id: s.id, @@ -1271,6 +1280,99 @@ func TestStreamReceiveDataBlocked(t *testing.T) { tc.wantIdle("no response to STREAM_DATA_BLOCKED and DATA_BLOCKED") } +func TestStreamFlushExplicit(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc, s := newTestConnAndLocalStream(t, clientSide, styp, permissiveTransportParameters) + want := []byte{0, 1, 2, 3} + n, err := s.Write(want) + if n != len(want) || err != nil { + t.Fatalf("s.Write() = %v, %v; want %v, nil", n, err, len(want)) + } + tc.wantIdle("unflushed data is not sent") + s.Flush() + tc.wantFrame("data is sent after flush", + packetType1RTT, debugFrameStream{ + id: s.id, + data: want, + }) + }) +} + +func TestStreamFlushImplicitExact(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + const writeBufferSize = 4 + tc, s := newTestConnAndLocalStream(t, clientSide, styp, + permissiveTransportParameters, + func(c *Config) { + c.MaxStreamWriteBufferSize = writeBufferSize + }) + want := []byte{0, 1, 2, 3, 4, 5, 6} + + // This write doesn't quite fill the output buffer. + n, err := s.Write(want[:3]) + if n != 3 || err != nil { + t.Fatalf("s.Write() = %v, %v; want %v, nil", n, err, len(want)) + } + tc.wantIdle("unflushed data is not sent") + + // This write fills the output buffer exactly. + n, err = s.Write(want[3:4]) + if n != 1 || err != nil { + t.Fatalf("s.Write() = %v, %v; want %v, nil", n, err, len(want)) + } + tc.wantFrame("data is sent after write buffer fills", + packetType1RTT, debugFrameStream{ + id: s.id, + data: want[0:4], + }) + + }) +} + +func TestStreamFlushImplicitLargerThanBuffer(t *testing.T) { + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + const writeBufferSize = 4 + tc, s := newTestConnAndLocalStream(t, clientSide, styp, + permissiveTransportParameters, + func(c *Config) { + c.MaxStreamWriteBufferSize = writeBufferSize + }) + want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + + w := runAsync(tc, func(ctx context.Context) (int, error) { + n, err := s.WriteContext(ctx, want) + return n, err + }) + + tc.wantFrame("data is sent after write buffer fills", + packetType1RTT, debugFrameStream{ + id: s.id, + data: want[0:4], + }) + tc.writeAckForAll() + tc.wantFrame("ack permits sending more data", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 4, + data: want[4:8], + }) + tc.writeAckForAll() + + tc.wantIdle("write buffer is not full") + if n, err := w.result(); n != len(want) || err != nil { + t.Fatalf("Write() = %v, %v; want %v, nil", n, err, len(want)) + } + + s.Flush() + tc.wantFrame("flush sends last buffer of data", + packetType1RTT, debugFrameStream{ + id: s.id, + off: 8, + data: want[8:], + }) + }) +} + type streamSide string const ( From e26b9a44574ff997838ad359431007e3a9ee6766 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 30 Oct 2023 15:18:17 -0700 Subject: [PATCH 102/168] quic: rename Listener to Endpoint The name Listener is confusing, because unlike a net.Listener a quic.Listener manages outgoing connections as well as inbound ones. Rename to "endpoint" which doesn't map to any existing net package name and matches the terminology of the QUIC RFCs. For golang/go#58547 Change-Id: If87f8c67ac7dd15d89d2d082a8ba2c63ea7f6e26 Reviewed-on: https://go-review.googlesource.com/c/net/+/543298 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/cmd/interop/main.go | 4 +- internal/quic/conn.go | 14 +- internal/quic/conn_close_test.go | 6 +- internal/quic/conn_id.go | 18 +-- internal/quic/conn_id_test.go | 12 +- internal/quic/conn_send.go | 2 +- internal/quic/conn_test.go | 70 +++++----- internal/quic/listener.go | 170 ++++++++++++------------ internal/quic/listener_test.go | 180 +++++++++++++------------- internal/quic/qlog.go | 2 +- internal/quic/retry.go | 16 +-- internal/quic/retry_test.go | 46 +++---- internal/quic/stateless_reset_test.go | 14 +- internal/quic/tls_test.go | 4 +- internal/quic/version_test.go | 12 +- 15 files changed, 285 insertions(+), 285 deletions(-) diff --git a/internal/quic/cmd/interop/main.go b/internal/quic/cmd/interop/main.go index 2ca5d652ad..20f737b525 100644 --- a/internal/quic/cmd/interop/main.go +++ b/internal/quic/cmd/interop/main.go @@ -157,7 +157,7 @@ func basicTest(ctx context.Context, config *quic.Config, urls []string) { } } -func serve(ctx context.Context, l *quic.Listener) error { +func serve(ctx context.Context, l *quic.Endpoint) error { for { c, err := l.Accept(ctx) if err != nil { @@ -221,7 +221,7 @@ func parseURL(s string) (u *url.URL, authority string, err error) { return u, authority, nil } -func fetchFrom(ctx context.Context, l *quic.Listener, addr string, urls []*url.URL) { +func fetchFrom(ctx context.Context, l *quic.Endpoint, addr string, urls []*url.URL) { conn, err := l.Dial(ctx, "udp", addr) if err != nil { log.Printf("%v: %v", addr, err) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index ff96ff7600..31e789b1da 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -21,7 +21,7 @@ import ( // Multiple goroutines may invoke methods on a Conn simultaneously. type Conn struct { side connSide - listener *Listener + endpoint *Endpoint config *Config testHooks connTestHooks peerAddr netip.AddrPort @@ -92,10 +92,10 @@ type newServerConnIDs struct { retrySrcConnID []byte // source from server's Retry } -func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort, config *Config, l *Listener) (conn *Conn, _ error) { +func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort, config *Config, e *Endpoint) (conn *Conn, _ error) { c := &Conn{ side: side, - listener: l, + endpoint: e, config: config, peerAddr: peerAddr, msgc: make(chan any, 1), @@ -115,8 +115,8 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip // non-blocking operation. c.msgc = make(chan any, 1) - if l.testHooks != nil { - l.testHooks.newConn(c) + if e.testHooks != nil { + e.testHooks.newConn(c) } // initialConnID is the connection ID used to generate Initial packet protection keys. @@ -187,7 +187,7 @@ func (c *Conn) confirmHandshake(now time.Time) { if c.side == serverSide { // When the server confirms the handshake, it sends a HANDSHAKE_DONE. c.handshakeConfirmed.setUnsent() - c.listener.serverConnEstablished(c) + c.endpoint.serverConnEstablished(c) } else { // The client never sends a HANDSHAKE_DONE, so we set handshakeConfirmed // to the received state, indicating that the handshake is confirmed and we @@ -265,7 +265,7 @@ var errIdleTimeout = errors.New("idle timeout") func (c *Conn) loop(now time.Time) { defer close(c.donec) defer c.tls.Close() - defer c.listener.connDrained(c) + defer c.endpoint.connDrained(c) defer c.logConnectionClosed() // The connection timer sends a message to the connection loop on expiry. diff --git a/internal/quic/conn_close_test.go b/internal/quic/conn_close_test.go index 0dd46dd203..49881e62fd 100644 --- a/internal/quic/conn_close_test.go +++ b/internal/quic/conn_close_test.go @@ -205,13 +205,13 @@ func TestConnCloseReceiveInHandshake(t *testing.T) { tc.wantIdle("no more frames to send") } -func TestConnCloseClosedByListener(t *testing.T) { +func TestConnCloseClosedByEndpoint(t *testing.T) { ctx := canceledContext() tc := newTestConn(t, clientSide) tc.handshake() - tc.listener.l.Close(ctx) - tc.wantFrame("listener closes connection before exiting", + tc.endpoint.e.Close(ctx) + tc.wantFrame("endpoint closes connection before exiting", packetType1RTT, debugFrameConnectionCloseTransport{ code: errNo, }) diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go index 439c221237..2efe8d6b5d 100644 --- a/internal/quic/conn_id.go +++ b/internal/quic/conn_id.go @@ -76,7 +76,7 @@ func (s *connIDState) initClient(c *Conn) error { cid: locid, }) s.nextLocalSeq = 1 - c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + c.endpoint.connsMap.updateConnIDs(func(conns *connsMap) { conns.addConnID(c, locid) }) @@ -117,7 +117,7 @@ func (s *connIDState) initServer(c *Conn, cids newServerConnIDs) error { cid: locid, }) s.nextLocalSeq = 1 - c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + c.endpoint.connsMap.updateConnIDs(func(conns *connsMap) { conns.addConnID(c, dstConnID) conns.addConnID(c, locid) }) @@ -194,7 +194,7 @@ func (s *connIDState) issueLocalIDs(c *Conn) error { s.needSend = true toIssue-- } - c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + c.endpoint.connsMap.updateConnIDs(func(conns *connsMap) { for _, cid := range newIDs { conns.addConnID(c, cid) } @@ -247,7 +247,7 @@ func (s *connIDState) validateTransportParameters(c *Conn, isRetry bool, p trans } token := statelessResetToken(p.statelessResetToken) s.remote[0].resetToken = token - c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + c.endpoint.connsMap.updateConnIDs(func(conns *connsMap) { conns.addResetToken(c, token) }) } @@ -276,7 +276,7 @@ func (s *connIDState) handlePacket(c *Conn, ptype packetType, srcConnID []byte) // the client. Discard the transient, client-chosen connection ID used // for Initial packets; the client will never send it again. cid := s.local[0].cid - c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + c.endpoint.connsMap.updateConnIDs(func(conns *connsMap) { conns.retireConnID(c, cid) }) s.local = append(s.local[:0], s.local[1:]...) @@ -314,7 +314,7 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re rcid := &s.remote[i] if !rcid.retired && rcid.seq >= 0 && rcid.seq < s.retireRemotePriorTo { s.retireRemote(rcid) - c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + c.endpoint.connsMap.updateConnIDs(func(conns *connsMap) { conns.retireResetToken(c, rcid.resetToken) }) } @@ -350,7 +350,7 @@ func (s *connIDState) handleNewConnID(c *Conn, seq, retire int64, cid []byte, re s.retireRemote(&s.remote[len(s.remote)-1]) } else { active++ - c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + c.endpoint.connsMap.updateConnIDs(func(conns *connsMap) { conns.addResetToken(c, resetToken) }) } @@ -399,7 +399,7 @@ func (s *connIDState) handleRetireConnID(c *Conn, seq int64) error { for i := range s.local { if s.local[i].seq == seq { cid := s.local[i].cid - c.listener.connsMap.updateConnIDs(func(conns *connsMap) { + c.endpoint.connsMap.updateConnIDs(func(conns *connsMap) { conns.retireConnID(c, cid) }) s.local = append(s.local[:i], s.local[i+1:]...) @@ -463,7 +463,7 @@ func (s *connIDState) appendFrames(c *Conn, pnum packetNumber, pto bool) bool { s.local[i].seq, retireBefore, s.local[i].cid, - c.listener.resetGen.tokenForConnID(s.local[i].cid), + c.endpoint.resetGen.tokenForConnID(s.local[i].cid), ) { return false } diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go index 314a6b3845..d44472e813 100644 --- a/internal/quic/conn_id_test.go +++ b/internal/quic/conn_id_test.go @@ -651,16 +651,16 @@ func TestConnIDsCleanedUpAfterClose(t *testing.T) { // Wait for the conn to drain. // Then wait for the conn loop to exit, // and force an immediate sync of the connsMap updates - // (normally only done by the listener read loop). + // (normally only done by the endpoint read loop). tc.advanceToTimer() <-tc.conn.donec - tc.listener.l.connsMap.applyUpdates() + tc.endpoint.e.connsMap.applyUpdates() - if got := len(tc.listener.l.connsMap.byConnID); got != 0 { - t.Errorf("%v conn ids in listener map after closing, want 0", got) + if got := len(tc.endpoint.e.connsMap.byConnID); got != 0 { + t.Errorf("%v conn ids in endpoint map after closing, want 0", got) } - if got := len(tc.listener.l.connsMap.byResetToken); got != 0 { - t.Errorf("%v reset tokens in listener map after closing, want 0", got) + if got := len(tc.endpoint.e.connsMap.byResetToken); got != 0 { + t.Errorf("%v reset tokens in endpoint map after closing, want 0", got) } }) } diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index e45dc8af3d..4065474d2c 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -170,7 +170,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } } - c.listener.sendDatagram(buf, c.peerAddr) + c.endpoint.sendDatagram(buf, c.peerAddr) } } diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 70ba7b3926..c57ba1487c 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -34,12 +34,12 @@ func TestConnTestConn(t *testing.T) { tc.conn.runOnLoop(func(now time.Time, c *Conn) { ranAt = now }) - if !ranAt.Equal(tc.listener.now) { - t.Errorf("func ran on loop at %v, want %v", ranAt, tc.listener.now) + if !ranAt.Equal(tc.endpoint.now) { + t.Errorf("func ran on loop at %v, want %v", ranAt, tc.endpoint.now) } tc.wait() - nextTime := tc.listener.now.Add(defaultMaxIdleTimeout / 2) + nextTime := tc.endpoint.now.Add(defaultMaxIdleTimeout / 2) tc.advanceTo(nextTime) tc.conn.runOnLoop(func(now time.Time, c *Conn) { ranAt = now @@ -117,7 +117,7 @@ const maxTestKeyPhases = 3 type testConn struct { t *testing.T conn *Conn - listener *testListener + endpoint *testEndpoint timer time.Time timerLastFired time.Time idlec chan struct{} // only accessed on the conn's loop @@ -220,27 +220,27 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { } } - listener := newTestListener(t, config) - listener.configTransportParams = configTransportParams - listener.configTestConn = configTestConn - conn, err := listener.l.newConn( - listener.now, + endpoint := newTestEndpoint(t, config) + endpoint.configTransportParams = configTransportParams + endpoint.configTestConn = configTestConn + conn, err := endpoint.e.newConn( + endpoint.now, side, cids, netip.MustParseAddrPort("127.0.0.1:443")) if err != nil { t.Fatal(err) } - tc := listener.conns[conn] + tc := endpoint.conns[conn] tc.wait() return tc } -func newTestConnForConn(t *testing.T, listener *testListener, conn *Conn) *testConn { +func newTestConnForConn(t *testing.T, endpoint *testEndpoint, conn *Conn) *testConn { t.Helper() tc := &testConn{ t: t, - listener: listener, + endpoint: endpoint, conn: conn, peerConnID: testPeerConnID(0), ignoreFrames: map[byte]bool{ @@ -251,14 +251,14 @@ func newTestConnForConn(t *testing.T, listener *testListener, conn *Conn) *testC recvDatagram: make(chan *datagram), } t.Cleanup(tc.cleanup) - for _, f := range listener.configTestConn { + for _, f := range endpoint.configTestConn { f(tc) } conn.testHooks = (*testConnHooks)(tc) - if listener.peerTLSConn != nil { - tc.peerTLSConn = listener.peerTLSConn - listener.peerTLSConn = nil + if endpoint.peerTLSConn != nil { + tc.peerTLSConn = endpoint.peerTLSConn + endpoint.peerTLSConn = nil return tc } @@ -267,7 +267,7 @@ func newTestConnForConn(t *testing.T, listener *testListener, conn *Conn) *testC if conn.side == clientSide { peerProvidedParams.originalDstConnID = testLocalConnID(-1) } - for _, f := range listener.configTransportParams { + for _, f := range endpoint.configTransportParams { f(&peerProvidedParams) } @@ -286,13 +286,13 @@ func newTestConnForConn(t *testing.T, listener *testListener, conn *Conn) *testC // advance causes time to pass. func (tc *testConn) advance(d time.Duration) { tc.t.Helper() - tc.listener.advance(d) + tc.endpoint.advance(d) } // advanceTo sets the current time. func (tc *testConn) advanceTo(now time.Time) { tc.t.Helper() - tc.listener.advanceTo(now) + tc.endpoint.advanceTo(now) } // advanceToTimer sets the current time to the time of the Conn's next timer event. @@ -307,10 +307,10 @@ func (tc *testConn) timerDelay() time.Duration { if tc.timer.IsZero() { return math.MaxInt64 // infinite } - if tc.timer.Before(tc.listener.now) { + if tc.timer.Before(tc.endpoint.now) { return 0 } - return tc.timer.Sub(tc.listener.now) + return tc.timer.Sub(tc.endpoint.now) } const infiniteDuration = time.Duration(math.MaxInt64) @@ -320,10 +320,10 @@ func (tc *testConn) timeUntilEvent() time.Duration { if tc.timer.IsZero() { return infiniteDuration } - if tc.timer.Before(tc.listener.now) { + if tc.timer.Before(tc.endpoint.now) { return 0 } - return tc.timer.Sub(tc.listener.now) + return tc.timer.Sub(tc.endpoint.now) } // wait blocks until the conn becomes idle. @@ -400,7 +400,7 @@ func logDatagram(t *testing.T, text string, d *testDatagram) { // write sends the Conn a datagram. func (tc *testConn) write(d *testDatagram) { tc.t.Helper() - tc.listener.writeDatagram(d) + tc.endpoint.writeDatagram(d) } // writeFrame sends the Conn a datagram containing the given frames. @@ -466,11 +466,11 @@ func (tc *testConn) readDatagram() *testDatagram { tc.wait() tc.sentPackets = nil tc.sentFrames = nil - buf := tc.listener.read() + buf := tc.endpoint.read() if buf == nil { return nil } - d := parseTestDatagram(tc.t, tc.listener, tc, buf) + d := parseTestDatagram(tc.t, tc.endpoint, tc, buf) // Log the datagram before removing ignored frames. // When things go wrong, it's useful to see all the frames. logDatagram(tc.t, "-> conn under test sends", d) @@ -771,7 +771,7 @@ func encodeTestPacket(t *testing.T, tc *testConn, p *testPacket, pad int) []byte return w.datagram() } -func parseTestDatagram(t *testing.T, tl *testListener, tc *testConn, buf []byte) *testDatagram { +func parseTestDatagram(t *testing.T, te *testEndpoint, tc *testConn, buf []byte) *testDatagram { t.Helper() bufSize := len(buf) d := &testDatagram{} @@ -784,7 +784,7 @@ func parseTestDatagram(t *testing.T, tl *testListener, tc *testConn, buf []byte) ptype := getPacketType(buf) switch ptype { case packetTypeRetry: - retry, ok := parseRetryPacket(buf, tl.lastInitialDstConnID) + retry, ok := parseRetryPacket(buf, te.lastInitialDstConnID) if !ok { t.Fatalf("could not parse %v packet", ptype) } @@ -938,7 +938,7 @@ func (tc *testConnHooks) init() { tc.keysInitial.r = tc.conn.keysInitial.w tc.keysInitial.w = tc.conn.keysInitial.r if tc.conn.side == serverSide { - tc.listener.acceptQueue = append(tc.listener.acceptQueue, (*testConn)(tc)) + tc.endpoint.acceptQueue = append(tc.endpoint.acceptQueue, (*testConn)(tc)) } } @@ -1039,20 +1039,20 @@ func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.Time, m any) { tc.timer = timer for { - if !timer.IsZero() && !timer.After(tc.listener.now) { + if !timer.IsZero() && !timer.After(tc.endpoint.now) { if timer.Equal(tc.timerLastFired) { // If the connection timer fires at time T, the Conn should take some // action to advance the timer into the future. If the Conn reschedules // the timer for the same time, it isn't making progress and we have a bug. - tc.t.Errorf("connection timer spinning; now=%v timer=%v", tc.listener.now, timer) + tc.t.Errorf("connection timer spinning; now=%v timer=%v", tc.endpoint.now, timer) } else { tc.timerLastFired = timer - return tc.listener.now, timerEvent{} + return tc.endpoint.now, timerEvent{} } } select { case m := <-msgc: - return tc.listener.now, m + return tc.endpoint.now, m default: } if !tc.wakeAsync() { @@ -1066,7 +1066,7 @@ func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.T close(idlec) } m = <-msgc - return tc.listener.now, m + return tc.endpoint.now, m } func (tc *testConnHooks) newConnID(seq int64) ([]byte, error) { @@ -1074,7 +1074,7 @@ func (tc *testConnHooks) newConnID(seq int64) ([]byte, error) { } func (tc *testConnHooks) timeNow() time.Time { - return tc.listener.now + return tc.endpoint.now } // testLocalConnID returns the connection ID with a given sequence number diff --git a/internal/quic/listener.go b/internal/quic/listener.go index ca8f9b25a7..82a08a18c2 100644 --- a/internal/quic/listener.go +++ b/internal/quic/listener.go @@ -17,14 +17,14 @@ import ( "time" ) -// A Listener listens for QUIC traffic on a network address. +// An Endpoint handles QUIC traffic on a network address. // It can accept inbound connections or create outbound ones. // -// Multiple goroutines may invoke methods on a Listener simultaneously. -type Listener struct { +// Multiple goroutines may invoke methods on an Endpoint simultaneously. +type Endpoint struct { config *Config udpConn udpConn - testHooks listenerTestHooks + testHooks endpointTestHooks resetGen statelessResetTokenGenerator retry retryState @@ -37,7 +37,7 @@ type Listener struct { closec chan struct{} // closed when the listen loop exits } -type listenerTestHooks interface { +type endpointTestHooks interface { timeNow() time.Time newConn(c *Conn) } @@ -53,7 +53,7 @@ type udpConn interface { // Listen listens on a local network address. // The configuration config must be non-nil. -func Listen(network, address string, config *Config) (*Listener, error) { +func Listen(network, address string, config *Config) (*Endpoint, error) { if config.TLSConfig == nil { return nil, errors.New("TLSConfig is not set") } @@ -65,11 +65,11 @@ func Listen(network, address string, config *Config) (*Listener, error) { if err != nil { return nil, err } - return newListener(udpConn, config, nil) + return newEndpoint(udpConn, config, nil) } -func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) (*Listener, error) { - l := &Listener{ +func newEndpoint(udpConn udpConn, config *Config, hooks endpointTestHooks) (*Endpoint, error) { + e := &Endpoint{ config: config, udpConn: udpConn, testHooks: hooks, @@ -77,70 +77,70 @@ func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) (*Lis acceptQueue: newQueue[*Conn](), closec: make(chan struct{}), } - l.resetGen.init(config.StatelessResetKey) - l.connsMap.init() + e.resetGen.init(config.StatelessResetKey) + e.connsMap.init() if config.RequireAddressValidation { - if err := l.retry.init(); err != nil { + if err := e.retry.init(); err != nil { return nil, err } } - go l.listen() - return l, nil + go e.listen() + return e, nil } // LocalAddr returns the local network address. -func (l *Listener) LocalAddr() netip.AddrPort { - a, _ := l.udpConn.LocalAddr().(*net.UDPAddr) +func (e *Endpoint) LocalAddr() netip.AddrPort { + a, _ := e.udpConn.LocalAddr().(*net.UDPAddr) return a.AddrPort() } -// Close closes the listener. -// Any blocked operations on the Listener or associated Conns and Stream will be unblocked +// Close closes the Endpoint. +// Any blocked operations on the Endpoint or associated Conns and Stream will be unblocked // and return errors. // // Close aborts every open connection. // Data in stream read and write buffers is discarded. // It waits for the peers of any open connection to acknowledge the connection has been closed. -func (l *Listener) Close(ctx context.Context) error { - l.acceptQueue.close(errors.New("listener closed")) - l.connsMu.Lock() - if !l.closing { - l.closing = true - for c := range l.conns { +func (e *Endpoint) Close(ctx context.Context) error { + e.acceptQueue.close(errors.New("endpoint closed")) + e.connsMu.Lock() + if !e.closing { + e.closing = true + for c := range e.conns { c.Abort(localTransportError{code: errNo}) } - if len(l.conns) == 0 { - l.udpConn.Close() + if len(e.conns) == 0 { + e.udpConn.Close() } } - l.connsMu.Unlock() + e.connsMu.Unlock() select { - case <-l.closec: + case <-e.closec: case <-ctx.Done(): - l.connsMu.Lock() - for c := range l.conns { + e.connsMu.Lock() + for c := range e.conns { c.exit() } - l.connsMu.Unlock() + e.connsMu.Unlock() return ctx.Err() } return nil } -// Accept waits for and returns the next connection to the listener. -func (l *Listener) Accept(ctx context.Context) (*Conn, error) { - return l.acceptQueue.get(ctx, nil) +// Accept waits for and returns the next connection. +func (e *Endpoint) Accept(ctx context.Context) (*Conn, error) { + return e.acceptQueue.get(ctx, nil) } // Dial creates and returns a connection to a network address. -func (l *Listener) Dial(ctx context.Context, network, address string) (*Conn, error) { +func (e *Endpoint) Dial(ctx context.Context, network, address string) (*Conn, error) { u, err := net.ResolveUDPAddr(network, address) if err != nil { return nil, err } addr := u.AddrPort() addr = netip.AddrPortFrom(addr.Addr().Unmap(), addr.Port()) - c, err := l.newConn(time.Now(), clientSide, newServerConnIDs{}, addr) + c, err := e.newConn(time.Now(), clientSide, newServerConnIDs{}, addr) if err != nil { return nil, err } @@ -151,29 +151,29 @@ func (l *Listener) Dial(ctx context.Context, network, address string) (*Conn, er return c, nil } -func (l *Listener) newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort) (*Conn, error) { - l.connsMu.Lock() - defer l.connsMu.Unlock() - if l.closing { - return nil, errors.New("listener closed") +func (e *Endpoint) newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort) (*Conn, error) { + e.connsMu.Lock() + defer e.connsMu.Unlock() + if e.closing { + return nil, errors.New("endpoint closed") } - c, err := newConn(now, side, cids, peerAddr, l.config, l) + c, err := newConn(now, side, cids, peerAddr, e.config, e) if err != nil { return nil, err } - l.conns[c] = struct{}{} + e.conns[c] = struct{}{} return c, nil } // serverConnEstablished is called by a conn when the handshake completes // for an inbound (serverSide) connection. -func (l *Listener) serverConnEstablished(c *Conn) { - l.acceptQueue.put(c) +func (e *Endpoint) serverConnEstablished(c *Conn) { + e.acceptQueue.put(c) } // connDrained is called by a conn when it leaves the draining state, // either when the peer acknowledges connection closure or the drain timeout expires. -func (l *Listener) connDrained(c *Conn) { +func (e *Endpoint) connDrained(c *Conn) { var cids [][]byte for i := range c.connIDState.local { cids = append(cids, c.connIDState.local[i].cid) @@ -182,7 +182,7 @@ func (l *Listener) connDrained(c *Conn) { for i := range c.connIDState.remote { tokens = append(tokens, c.connIDState.remote[i].resetToken) } - l.connsMap.updateConnIDs(func(conns *connsMap) { + e.connsMap.updateConnIDs(func(conns *connsMap) { for _, cid := range cids { conns.retireConnID(c, cid) } @@ -190,60 +190,60 @@ func (l *Listener) connDrained(c *Conn) { conns.retireResetToken(c, token) } }) - l.connsMu.Lock() - defer l.connsMu.Unlock() - delete(l.conns, c) - if l.closing && len(l.conns) == 0 { - l.udpConn.Close() + e.connsMu.Lock() + defer e.connsMu.Unlock() + delete(e.conns, c) + if e.closing && len(e.conns) == 0 { + e.udpConn.Close() } } -func (l *Listener) listen() { - defer close(l.closec) +func (e *Endpoint) listen() { + defer close(e.closec) for { m := newDatagram() // TODO: Read and process the ECN (explicit congestion notification) field. // https://tools.ietf.org/html/draft-ietf-quic-transport-32#section-13.4 - n, _, _, addr, err := l.udpConn.ReadMsgUDPAddrPort(m.b, nil) + n, _, _, addr, err := e.udpConn.ReadMsgUDPAddrPort(m.b, nil) if err != nil { - // The user has probably closed the listener. + // The user has probably closed the endpoint. // We currently don't surface errors from other causes; - // we could check to see if the listener has been closed and + // we could check to see if the endpoint has been closed and // record the unexpected error if it has not. return } if n == 0 { continue } - if l.connsMap.updateNeeded.Load() { - l.connsMap.applyUpdates() + if e.connsMap.updateNeeded.Load() { + e.connsMap.applyUpdates() } m.addr = addr m.b = m.b[:n] - l.handleDatagram(m) + e.handleDatagram(m) } } -func (l *Listener) handleDatagram(m *datagram) { +func (e *Endpoint) handleDatagram(m *datagram) { dstConnID, ok := dstConnIDForDatagram(m.b) if !ok { m.recycle() return } - c := l.connsMap.byConnID[string(dstConnID)] + c := e.connsMap.byConnID[string(dstConnID)] if c == nil { // TODO: Move this branch into a separate goroutine to avoid blocking - // the listener while processing packets. - l.handleUnknownDestinationDatagram(m) + // the endpoint while processing packets. + e.handleUnknownDestinationDatagram(m) return } - // TODO: This can block the listener while waiting for the conn to accept the dgram. + // TODO: This can block the endpoint while waiting for the conn to accept the dgram. // Think about buffering between the receive loop and the conn. c.sendMsg(m) } -func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { +func (e *Endpoint) handleUnknownDestinationDatagram(m *datagram) { defer func() { if m != nil { m.recycle() @@ -254,15 +254,15 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { return } var now time.Time - if l.testHooks != nil { - now = l.testHooks.timeNow() + if e.testHooks != nil { + now = e.testHooks.timeNow() } else { now = time.Now() } // Check to see if this is a stateless reset. var token statelessResetToken copy(token[:], m.b[len(m.b)-len(token):]) - if c := l.connsMap.byResetToken[token]; c != nil { + if c := e.connsMap.byResetToken[token]; c != nil { c.sendMsg(func(now time.Time, c *Conn) { c.handleStatelessReset(now, token) }) @@ -271,7 +271,7 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { // If this is a 1-RTT packet, there's nothing productive we can do with it. // Send a stateless reset if possible. if !isLongHeader(m.b[0]) { - l.maybeSendStatelessReset(m.b, m.addr) + e.maybeSendStatelessReset(m.b, m.addr) return } p, ok := parseGenericLongHeaderPacket(m.b) @@ -285,7 +285,7 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { return default: // Unknown version. - l.sendVersionNegotiation(p, m.addr) + e.sendVersionNegotiation(p, m.addr) return } if getPacketType(m.b) != packetTypeInitial { @@ -300,10 +300,10 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { srcConnID: p.srcConnID, dstConnID: p.dstConnID, } - if l.config.RequireAddressValidation { + if e.config.RequireAddressValidation { var ok bool cids.retrySrcConnID = p.dstConnID - cids.originalDstConnID, ok = l.validateInitialAddress(now, p, m.addr) + cids.originalDstConnID, ok = e.validateInitialAddress(now, p, m.addr) if !ok { return } @@ -311,7 +311,7 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { cids.originalDstConnID = p.dstConnID } var err error - c, err := l.newConn(now, serverSide, cids, m.addr) + c, err := e.newConn(now, serverSide, cids, m.addr) if err != nil { // The accept queue is probably full. // We could send a CONNECTION_CLOSE to the peer to reject the connection. @@ -323,8 +323,8 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) { m = nil // don't recycle, sendMsg takes ownership } -func (l *Listener) maybeSendStatelessReset(b []byte, addr netip.AddrPort) { - if !l.resetGen.canReset { +func (e *Endpoint) maybeSendStatelessReset(b []byte, addr netip.AddrPort) { + if !e.resetGen.canReset { // Config.StatelessResetKey isn't set, so we don't send stateless resets. return } @@ -339,7 +339,7 @@ func (l *Listener) maybeSendStatelessReset(b []byte, addr netip.AddrPort) { } // TODO: Rate limit stateless resets. cid := b[1:][:connIDLen] - token := l.resetGen.tokenForConnID(cid) + token := e.resetGen.tokenForConnID(cid) // We want to generate a stateless reset that is as short as possible, // but long enough to be difficult to distinguish from a 1-RTT packet. // @@ -364,17 +364,17 @@ func (l *Listener) maybeSendStatelessReset(b []byte, addr netip.AddrPort) { b[0] &^= headerFormLong // clear long header bit b[0] |= fixedBit // set fixed bit copy(b[len(b)-statelessResetTokenLen:], token[:]) - l.sendDatagram(b, addr) + e.sendDatagram(b, addr) } -func (l *Listener) sendVersionNegotiation(p genericLongPacket, addr netip.AddrPort) { +func (e *Endpoint) sendVersionNegotiation(p genericLongPacket, addr netip.AddrPort) { m := newDatagram() m.b = appendVersionNegotiation(m.b[:0], p.srcConnID, p.dstConnID, quicVersion1) - l.sendDatagram(m.b, addr) + e.sendDatagram(m.b, addr) m.recycle() } -func (l *Listener) sendConnectionClose(in genericLongPacket, addr netip.AddrPort, code transportError) { +func (e *Endpoint) sendConnectionClose(in genericLongPacket, addr netip.AddrPort, code transportError) { keys := initialKeys(in.dstConnID, serverSide) var w packetWriter p := longPacket{ @@ -393,15 +393,15 @@ func (l *Listener) sendConnectionClose(in genericLongPacket, addr netip.AddrPort if len(buf) == 0 { return } - l.sendDatagram(buf, addr) + e.sendDatagram(buf, addr) } -func (l *Listener) sendDatagram(p []byte, addr netip.AddrPort) error { - _, err := l.udpConn.WriteToUDPAddrPort(p, addr) +func (e *Endpoint) sendDatagram(p []byte, addr netip.AddrPort) error { + _, err := e.udpConn.WriteToUDPAddrPort(p, addr) return err } -// A connsMap is a listener's mapping of conn ids and reset tokens to conns. +// A connsMap is an endpoint's mapping of conn ids and reset tokens to conns. type connsMap struct { byConnID map[string]*Conn byResetToken map[statelessResetToken]*Conn diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go index 037fb21b40..f9fc801520 100644 --- a/internal/quic/listener_test.go +++ b/internal/quic/listener_test.go @@ -64,39 +64,39 @@ func TestStreamTransfer(t *testing.T) { func newLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverConn *Conn) { t.Helper() ctx := context.Background() - l1 := newLocalListener(t, serverSide, conf1) - l2 := newLocalListener(t, clientSide, conf2) - c2, err := l2.Dial(ctx, "udp", l1.LocalAddr().String()) + e1 := newLocalEndpoint(t, serverSide, conf1) + e2 := newLocalEndpoint(t, clientSide, conf2) + c2, err := e2.Dial(ctx, "udp", e1.LocalAddr().String()) if err != nil { t.Fatal(err) } - c1, err := l1.Accept(ctx) + c1, err := e1.Accept(ctx) if err != nil { t.Fatal(err) } return c2, c1 } -func newLocalListener(t *testing.T, side connSide, conf *Config) *Listener { +func newLocalEndpoint(t *testing.T, side connSide, conf *Config) *Endpoint { t.Helper() if conf.TLSConfig == nil { newConf := *conf conf = &newConf conf.TLSConfig = newTestTLSConfig(side) } - l, err := Listen("udp", "127.0.0.1:0", conf) + e, err := Listen("udp", "127.0.0.1:0", conf) if err != nil { t.Fatal(err) } t.Cleanup(func() { - l.Close(context.Background()) + e.Close(context.Background()) }) - return l + return e } -type testListener struct { +type testEndpoint struct { t *testing.T - l *Listener + e *Endpoint now time.Time recvc chan *datagram idlec chan struct{} @@ -109,8 +109,8 @@ type testListener struct { lastInitialDstConnID []byte // for parsing Retry packets } -func newTestListener(t *testing.T, config *Config) *testListener { - tl := &testListener{ +func newTestEndpoint(t *testing.T, config *Config) *testEndpoint { + te := &testEndpoint{ t: t, now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), recvc: make(chan *datagram), @@ -118,52 +118,52 @@ func newTestListener(t *testing.T, config *Config) *testListener { conns: make(map[*Conn]*testConn), } var err error - tl.l, err = newListener((*testListenerUDPConn)(tl), config, (*testListenerHooks)(tl)) + te.e, err = newEndpoint((*testEndpointUDPConn)(te), config, (*testEndpointHooks)(te)) if err != nil { t.Fatal(err) } - t.Cleanup(tl.cleanup) - return tl + t.Cleanup(te.cleanup) + return te } -func (tl *testListener) cleanup() { - tl.l.Close(canceledContext()) +func (te *testEndpoint) cleanup() { + te.e.Close(canceledContext()) } -func (tl *testListener) wait() { +func (te *testEndpoint) wait() { select { - case tl.idlec <- struct{}{}: - case <-tl.l.closec: + case te.idlec <- struct{}{}: + case <-te.e.closec: } - for _, tc := range tl.conns { + for _, tc := range te.conns { tc.wait() } } -// accept returns a server connection from the listener. -// Unlike Listener.Accept, connections are available as soon as they are created. -func (tl *testListener) accept() *testConn { - if len(tl.acceptQueue) == 0 { - tl.t.Fatalf("accept: expected available conn, but found none") +// accept returns a server connection from the endpoint. +// Unlike Endpoint.Accept, connections are available as soon as they are created. +func (te *testEndpoint) accept() *testConn { + if len(te.acceptQueue) == 0 { + te.t.Fatalf("accept: expected available conn, but found none") } - tc := tl.acceptQueue[0] - tl.acceptQueue = tl.acceptQueue[1:] + tc := te.acceptQueue[0] + te.acceptQueue = te.acceptQueue[1:] return tc } -func (tl *testListener) write(d *datagram) { - tl.recvc <- d - tl.wait() +func (te *testEndpoint) write(d *datagram) { + te.recvc <- d + te.wait() } var testClientAddr = netip.MustParseAddrPort("10.0.0.1:8000") -func (tl *testListener) writeDatagram(d *testDatagram) { - tl.t.Helper() - logDatagram(tl.t, "<- listener under test receives", d) +func (te *testEndpoint) writeDatagram(d *testDatagram) { + te.t.Helper() + logDatagram(te.t, "<- endpoint under test receives", d) var buf []byte for _, p := range d.packets { - tc := tl.connForDestination(p.dstConnID) + tc := te.connForDestination(p.dstConnID) if p.ptype != packetTypeRetry && tc != nil { space := spaceForPacketType(p.ptype) if p.num >= tc.peerNextPacketNum[space] { @@ -171,13 +171,13 @@ func (tl *testListener) writeDatagram(d *testDatagram) { } } if p.ptype == packetTypeInitial { - tl.lastInitialDstConnID = p.dstConnID + te.lastInitialDstConnID = p.dstConnID } pad := 0 if p.ptype == packetType1RTT { pad = d.paddedSize - len(buf) } - buf = append(buf, encodeTestPacket(tl.t, tc, p, pad)...) + buf = append(buf, encodeTestPacket(te.t, tc, p, pad)...) } for len(buf) < d.paddedSize { buf = append(buf, 0) @@ -186,14 +186,14 @@ func (tl *testListener) writeDatagram(d *testDatagram) { if !addr.IsValid() { addr = testClientAddr } - tl.write(&datagram{ + te.write(&datagram{ b: buf, addr: addr, }) } -func (tl *testListener) connForDestination(dstConnID []byte) *testConn { - for _, tc := range tl.conns { +func (te *testEndpoint) connForDestination(dstConnID []byte) *testConn { + for _, tc := range te.conns { for _, loc := range tc.conn.connIDState.local { if bytes.Equal(loc.cid, dstConnID) { return tc @@ -203,8 +203,8 @@ func (tl *testListener) connForDestination(dstConnID []byte) *testConn { return nil } -func (tl *testListener) connForSource(srcConnID []byte) *testConn { - for _, tc := range tl.conns { +func (te *testEndpoint) connForSource(srcConnID []byte) *testConn { + for _, tc := range te.conns { for _, loc := range tc.conn.connIDState.remote { if bytes.Equal(loc.cid, srcConnID) { return tc @@ -214,106 +214,106 @@ func (tl *testListener) connForSource(srcConnID []byte) *testConn { return nil } -func (tl *testListener) read() []byte { - tl.t.Helper() - tl.wait() - if len(tl.sentDatagrams) == 0 { +func (te *testEndpoint) read() []byte { + te.t.Helper() + te.wait() + if len(te.sentDatagrams) == 0 { return nil } - d := tl.sentDatagrams[0] - tl.sentDatagrams = tl.sentDatagrams[1:] + d := te.sentDatagrams[0] + te.sentDatagrams = te.sentDatagrams[1:] return d } -func (tl *testListener) readDatagram() *testDatagram { - tl.t.Helper() - buf := tl.read() +func (te *testEndpoint) readDatagram() *testDatagram { + te.t.Helper() + buf := te.read() if buf == nil { return nil } p, _ := parseGenericLongHeaderPacket(buf) - tc := tl.connForSource(p.dstConnID) - d := parseTestDatagram(tl.t, tl, tc, buf) - logDatagram(tl.t, "-> listener under test sends", d) + tc := te.connForSource(p.dstConnID) + d := parseTestDatagram(te.t, te, tc, buf) + logDatagram(te.t, "-> endpoint under test sends", d) return d } -// wantDatagram indicates that we expect the Listener to send a datagram. -func (tl *testListener) wantDatagram(expectation string, want *testDatagram) { - tl.t.Helper() - got := tl.readDatagram() +// wantDatagram indicates that we expect the Endpoint to send a datagram. +func (te *testEndpoint) wantDatagram(expectation string, want *testDatagram) { + te.t.Helper() + got := te.readDatagram() if !reflect.DeepEqual(got, want) { - tl.t.Fatalf("%v:\ngot datagram: %v\nwant datagram: %v", expectation, got, want) + te.t.Fatalf("%v:\ngot datagram: %v\nwant datagram: %v", expectation, got, want) } } -// wantIdle indicates that we expect the Listener to not send any more datagrams. -func (tl *testListener) wantIdle(expectation string) { - if got := tl.readDatagram(); got != nil { - tl.t.Fatalf("expect: %v\nunexpectedly got: %v", expectation, got) +// wantIdle indicates that we expect the Endpoint to not send any more datagrams. +func (te *testEndpoint) wantIdle(expectation string) { + if got := te.readDatagram(); got != nil { + te.t.Fatalf("expect: %v\nunexpectedly got: %v", expectation, got) } } // advance causes time to pass. -func (tl *testListener) advance(d time.Duration) { - tl.t.Helper() - tl.advanceTo(tl.now.Add(d)) +func (te *testEndpoint) advance(d time.Duration) { + te.t.Helper() + te.advanceTo(te.now.Add(d)) } // advanceTo sets the current time. -func (tl *testListener) advanceTo(now time.Time) { - tl.t.Helper() - if tl.now.After(now) { - tl.t.Fatalf("time moved backwards: %v -> %v", tl.now, now) +func (te *testEndpoint) advanceTo(now time.Time) { + te.t.Helper() + if te.now.After(now) { + te.t.Fatalf("time moved backwards: %v -> %v", te.now, now) } - tl.now = now - for _, tc := range tl.conns { - if !tc.timer.After(tl.now) { + te.now = now + for _, tc := range te.conns { + if !tc.timer.After(te.now) { tc.conn.sendMsg(timerEvent{}) tc.wait() } } } -// testListenerHooks implements listenerTestHooks. -type testListenerHooks testListener +// testEndpointHooks implements endpointTestHooks. +type testEndpointHooks testEndpoint -func (tl *testListenerHooks) timeNow() time.Time { - return tl.now +func (te *testEndpointHooks) timeNow() time.Time { + return te.now } -func (tl *testListenerHooks) newConn(c *Conn) { - tc := newTestConnForConn(tl.t, (*testListener)(tl), c) - tl.conns[c] = tc +func (te *testEndpointHooks) newConn(c *Conn) { + tc := newTestConnForConn(te.t, (*testEndpoint)(te), c) + te.conns[c] = tc } -// testListenerUDPConn implements UDPConn. -type testListenerUDPConn testListener +// testEndpointUDPConn implements UDPConn. +type testEndpointUDPConn testEndpoint -func (tl *testListenerUDPConn) Close() error { - close(tl.recvc) +func (te *testEndpointUDPConn) Close() error { + close(te.recvc) return nil } -func (tl *testListenerUDPConn) LocalAddr() net.Addr { +func (te *testEndpointUDPConn) LocalAddr() net.Addr { return net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:443")) } -func (tl *testListenerUDPConn) ReadMsgUDPAddrPort(b, control []byte) (n, controln, flags int, _ netip.AddrPort, _ error) { +func (te *testEndpointUDPConn) ReadMsgUDPAddrPort(b, control []byte) (n, controln, flags int, _ netip.AddrPort, _ error) { for { select { - case d, ok := <-tl.recvc: + case d, ok := <-te.recvc: if !ok { return 0, 0, 0, netip.AddrPort{}, io.EOF } n = copy(b, d.b) return n, 0, 0, d.addr, nil - case <-tl.idlec: + case <-te.idlec: } } } -func (tl *testListenerUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { - tl.sentDatagrams = append(tl.sentDatagrams, append([]byte(nil), b...)) +func (te *testEndpointUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { + te.sentDatagrams = append(te.sentDatagrams, append([]byte(nil), b...)) return len(b), nil } diff --git a/internal/quic/qlog.go b/internal/quic/qlog.go index c8ee429fec..ea53cab1e8 100644 --- a/internal/quic/qlog.go +++ b/internal/quic/qlog.go @@ -95,7 +95,7 @@ func (c *Conn) logConnectionStarted(originalDstConnID []byte, peerAddr netip.Add slog.String("type", vantage), ), ) - localAddr := c.listener.LocalAddr() + localAddr := c.endpoint.LocalAddr() // https://www.ietf.org/archive/id/draft-ietf-quic-qlog-quic-events-03.html#section-4.2 c.log.LogAttrs(context.Background(), QLogLevelEndpoint, "connectivity:connection_started", diff --git a/internal/quic/retry.go b/internal/quic/retry.go index e3d9f4d7d1..31cb57b880 100644 --- a/internal/quic/retry.go +++ b/internal/quic/retry.go @@ -39,7 +39,7 @@ var ( // retryTokenValidityPeriod is how long we accept a Retry packet token after sending it. const retryTokenValidityPeriod = 5 * time.Second -// retryState generates and validates a listener's retry tokens. +// retryState generates and validates an endpoint's retry tokens. type retryState struct { aead cipher.AEAD } @@ -139,7 +139,7 @@ func (rs *retryState) additionalData(srcConnID []byte, addr netip.AddrPort) []by return additional } -func (l *Listener) validateInitialAddress(now time.Time, p genericLongPacket, addr netip.AddrPort) (origDstConnID []byte, ok bool) { +func (e *Endpoint) validateInitialAddress(now time.Time, p genericLongPacket, addr netip.AddrPort) (origDstConnID []byte, ok bool) { // The retry token is at the start of an Initial packet's data. token, n := consumeUint8Bytes(p.data) if n < 0 { @@ -151,22 +151,22 @@ func (l *Listener) validateInitialAddress(now time.Time, p genericLongPacket, ad if len(token) == 0 { // The sender has not provided a token. // Send a Retry packet to them with one. - l.sendRetry(now, p, addr) + e.sendRetry(now, p, addr) return nil, false } - origDstConnID, ok = l.retry.validateToken(now, token, p.srcConnID, p.dstConnID, addr) + origDstConnID, ok = e.retry.validateToken(now, token, p.srcConnID, p.dstConnID, addr) if !ok { // This does not seem to be a valid token. // Close the connection with an INVALID_TOKEN error. // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5 - l.sendConnectionClose(p, addr, errInvalidToken) + e.sendConnectionClose(p, addr, errInvalidToken) return nil, false } return origDstConnID, true } -func (l *Listener) sendRetry(now time.Time, p genericLongPacket, addr netip.AddrPort) { - token, srcConnID, err := l.retry.makeToken(now, p.srcConnID, p.dstConnID, addr) +func (e *Endpoint) sendRetry(now time.Time, p genericLongPacket, addr netip.AddrPort) { + token, srcConnID, err := e.retry.makeToken(now, p.srcConnID, p.dstConnID, addr) if err != nil { return } @@ -175,7 +175,7 @@ func (l *Listener) sendRetry(now time.Time, p genericLongPacket, addr netip.Addr srcConnID: srcConnID, token: token, }) - l.sendDatagram(b, addr) + e.sendDatagram(b, addr) } type retryPacket struct { diff --git a/internal/quic/retry_test.go b/internal/quic/retry_test.go index f754270a5e..4a21a4ca1d 100644 --- a/internal/quic/retry_test.go +++ b/internal/quic/retry_test.go @@ -16,7 +16,7 @@ import ( ) type retryServerTest struct { - tl *testListener + te *testEndpoint originalSrcConnID []byte originalDstConnID []byte retry retryPacket @@ -32,16 +32,16 @@ func newRetryServerTest(t *testing.T) *retryServerTest { TLSConfig: newTestTLSConfig(serverSide), RequireAddressValidation: true, } - tl := newTestListener(t, config) + te := newTestEndpoint(t, config) srcID := testPeerConnID(0) dstID := testLocalConnID(-1) params := defaultTransportParameters() params.initialSrcConnID = srcID - initialCrypto := initialClientCrypto(t, tl, params) + initialCrypto := initialClientCrypto(t, te, params) // Initial packet with no Token. // Server responds with a Retry containing a token. - tl.writeDatagram(&testDatagram{ + te.writeDatagram(&testDatagram{ packets: []*testPacket{{ ptype: packetTypeInitial, num: 0, @@ -56,7 +56,7 @@ func newRetryServerTest(t *testing.T) *retryServerTest { }}, paddedSize: 1200, }) - got := tl.readDatagram() + got := te.readDatagram() if len(got.packets) != 1 || got.packets[0].ptype != packetTypeRetry { t.Fatalf("got datagram: %v\nwant Retry", got) } @@ -66,7 +66,7 @@ func newRetryServerTest(t *testing.T) *retryServerTest { } return &retryServerTest{ - tl: tl, + te: te, originalSrcConnID: srcID, originalDstConnID: dstID, retry: retryPacket{ @@ -80,9 +80,9 @@ func newRetryServerTest(t *testing.T) *retryServerTest { func TestRetryServerSucceeds(t *testing.T) { rt := newRetryServerTest(t) - tl := rt.tl - tl.advance(retryTokenValidityPeriod) - tl.writeDatagram(&testDatagram{ + te := rt.te + te.advance(retryTokenValidityPeriod) + te.writeDatagram(&testDatagram{ packets: []*testPacket{{ ptype: packetTypeInitial, num: 1, @@ -98,7 +98,7 @@ func TestRetryServerSucceeds(t *testing.T) { }}, paddedSize: 1200, }) - tc := tl.accept() + tc := te.accept() initial := tc.readPacket() if initial == nil || initial.ptype != packetTypeInitial { t.Fatalf("got packet:\n%v\nwant: Initial", initial) @@ -124,8 +124,8 @@ func TestRetryServerTokenInvalid(t *testing.T) { // INVALID_TOKEN error." // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5 rt := newRetryServerTest(t) - tl := rt.tl - tl.writeDatagram(&testDatagram{ + te := rt.te + te.writeDatagram(&testDatagram{ packets: []*testPacket{{ ptype: packetTypeInitial, num: 1, @@ -141,7 +141,7 @@ func TestRetryServerTokenInvalid(t *testing.T) { }}, paddedSize: 1200, }) - tl.wantDatagram("server closes connection after Initial with invalid Retry token", + te.wantDatagram("server closes connection after Initial with invalid Retry token", initialConnectionCloseDatagram( rt.retry.srcConnID, rt.originalSrcConnID, @@ -152,9 +152,9 @@ func TestRetryServerTokenTooOld(t *testing.T) { // "[...] a token SHOULD have an expiration time [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.3-3 rt := newRetryServerTest(t) - tl := rt.tl - tl.advance(retryTokenValidityPeriod + time.Second) - tl.writeDatagram(&testDatagram{ + te := rt.te + te.advance(retryTokenValidityPeriod + time.Second) + te.writeDatagram(&testDatagram{ packets: []*testPacket{{ ptype: packetTypeInitial, num: 1, @@ -170,7 +170,7 @@ func TestRetryServerTokenTooOld(t *testing.T) { }}, paddedSize: 1200, }) - tl.wantDatagram("server closes connection after Initial with expired token", + te.wantDatagram("server closes connection after Initial with expired token", initialConnectionCloseDatagram( rt.retry.srcConnID, rt.originalSrcConnID, @@ -182,8 +182,8 @@ func TestRetryServerTokenWrongIP(t *testing.T) { // to verify that the source IP address and port in client packets remain constant." // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.4-3 rt := newRetryServerTest(t) - tl := rt.tl - tl.writeDatagram(&testDatagram{ + te := rt.te + te.writeDatagram(&testDatagram{ packets: []*testPacket{{ ptype: packetTypeInitial, num: 1, @@ -200,7 +200,7 @@ func TestRetryServerTokenWrongIP(t *testing.T) { paddedSize: 1200, addr: netip.MustParseAddrPort("10.0.0.2:8000"), }) - tl.wantDatagram("server closes connection after Initial from wrong address", + te.wantDatagram("server closes connection after Initial from wrong address", initialConnectionCloseDatagram( rt.retry.srcConnID, rt.originalSrcConnID, @@ -435,7 +435,7 @@ func TestRetryClientIgnoresRetryWithInvalidIntegrityTag(t *testing.T) { token: []byte{1, 2, 3, 4}, }) pkt[len(pkt)-1] ^= 1 // invalidate the integrity tag - tc.listener.write(&datagram{ + tc.endpoint.write(&datagram{ b: pkt, addr: testClientAddr, }) @@ -527,14 +527,14 @@ func TestParseInvalidRetryPackets(t *testing.T) { } } -func initialClientCrypto(t *testing.T, l *testListener, p transportParameters) []byte { +func initialClientCrypto(t *testing.T, e *testEndpoint, p transportParameters) []byte { t.Helper() config := &tls.QUICConfig{TLSConfig: newTestTLSConfig(clientSide)} tlsClient := tls.QUICClient(config) tlsClient.SetTransportParameters(marshalTransportParameters(p)) tlsClient.Start(context.Background()) //defer tlsClient.Close() - l.peerTLSConn = tlsClient + e.peerTLSConn = tlsClient var data []byte for { e := tlsClient.NextEvent() diff --git a/internal/quic/stateless_reset_test.go b/internal/quic/stateless_reset_test.go index c01375fbd6..45a49e81e6 100644 --- a/internal/quic/stateless_reset_test.go +++ b/internal/quic/stateless_reset_test.go @@ -68,7 +68,7 @@ func TestStatelessResetSentSizes(t *testing.T) { StatelessResetKey: testStatelessResetKey, } addr := netip.MustParseAddr("127.0.0.1") - tl := newTestListener(t, config) + te := newTestEndpoint(t, config) for i, test := range []struct { reqSize int wantSize int @@ -105,9 +105,9 @@ func TestStatelessResetSentSizes(t *testing.T) { cid := testLocalConnID(int64(i)) token := testStatelessResetToken(cid) addrport := netip.AddrPortFrom(addr, uint16(8000+i)) - tl.write(newDatagramForReset(cid, test.reqSize, addrport)) + te.write(newDatagramForReset(cid, test.reqSize, addrport)) - got := tl.read() + got := te.read() if len(got) != test.wantSize { t.Errorf("got %v-byte response to %v-byte req, want %v", len(got), test.reqSize, test.wantSize) @@ -149,7 +149,7 @@ func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) { resetToken := testPeerStatelessResetToken(1) // provided during handshake dgram := append(make([]byte, 100), resetToken[:]...) - tc.listener.write(&datagram{ + tc.endpoint.write(&datagram{ b: dgram, }) @@ -179,7 +179,7 @@ func TestStatelessResetSuccessfulTransportParameter(t *testing.T) { tc.handshake() dgram := append(make([]byte, 100), resetToken[:]...) - tc.listener.write(&datagram{ + tc.endpoint.write(&datagram{ b: dgram, }) @@ -243,7 +243,7 @@ func TestStatelessResetSuccessfulPrefix(t *testing.T) { dgram = append(dgram, byte(len(dgram))) // semi-random junk } dgram = append(dgram, resetToken[:]...) - tc.listener.write(&datagram{ + tc.endpoint.write(&datagram{ b: dgram, }) if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) { @@ -278,7 +278,7 @@ func TestStatelessResetRetiredConnID(t *testing.T) { // Receive a stateless reset for connection ID 0. dgram := append(make([]byte, 100), resetToken[:]...) - tc.listener.write(&datagram{ + tc.endpoint.write(&datagram{ b: dgram, }) diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index fa339b9faf..14f74a00aa 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -36,7 +36,7 @@ func (tc *testConn) handshake() { for { if i == len(dgrams)-1 { if tc.conn.side == clientSide { - want := tc.listener.now.Add(maxAckDelay - timerGranularity) + want := tc.endpoint.now.Add(maxAckDelay - timerGranularity) if !tc.timer.Equal(want) { t.Fatalf("want timer = %v (max_ack_delay), got %v", want, tc.timer) } @@ -85,7 +85,7 @@ func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) { testPeerConnID(0), testPeerConnID(1), } - localResetToken := tc.listener.l.resetGen.tokenForConnID(localConnIDs[1]) + localResetToken := tc.endpoint.e.resetGen.tokenForConnID(localConnIDs[1]) peerResetToken := testPeerStatelessResetToken(1) if tc.conn.side == clientSide { clientConnIDs = localConnIDs diff --git a/internal/quic/version_test.go b/internal/quic/version_test.go index 830e0e1c83..92fabd7b3d 100644 --- a/internal/quic/version_test.go +++ b/internal/quic/version_test.go @@ -17,7 +17,7 @@ func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { config := &Config{ TLSConfig: newTestTLSConfig(serverSide), } - tl := newTestListener(t, config) + te := newTestEndpoint(t, config) // Packet of unknown contents for some unrecognized QUIC version. dstConnID := []byte{1, 2, 3, 4} @@ -34,10 +34,10 @@ func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { pkt = append(pkt, 0) } - tl.write(&datagram{ + te.write(&datagram{ b: pkt, }) - gotPkt := tl.read() + gotPkt := te.read() if gotPkt == nil { t.Fatalf("got no response; want Version Negotiaion") } @@ -59,7 +59,7 @@ func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { func TestVersionNegotiationClientAborts(t *testing.T) { tc := newTestConn(t, clientSide) p := tc.readPacket() // client Initial packet - tc.listener.write(&datagram{ + tc.endpoint.write(&datagram{ b: appendVersionNegotiation(nil, p.srcConnID, p.dstConnID, 10), }) tc.wantIdle("connection does not send a CONNECTION_CLOSE") @@ -76,7 +76,7 @@ func TestVersionNegotiationClientIgnoresAfterProcessingPacket(t *testing.T) { debugFrameCrypto{ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial], }) - tc.listener.write(&datagram{ + tc.endpoint.write(&datagram{ b: appendVersionNegotiation(nil, p.srcConnID, p.dstConnID, 10), }) if err := tc.conn.waitReady(canceledContext()); err != context.Canceled { @@ -94,7 +94,7 @@ func TestVersionNegotiationClientIgnoresMismatchingSourceConnID(t *testing.T) { tc := newTestConn(t, clientSide) tc.ignoreFrame(frameTypeAck) p := tc.readPacket() // client Initial packet - tc.listener.write(&datagram{ + tc.endpoint.write(&datagram{ b: appendVersionNegotiation(nil, p.srcConnID, []byte("mismatch"), 10), }) tc.writeFrames(packetTypeInitial, From 13e88dd2f74327f590622c561597594022c45de5 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 17 Nov 2023 13:23:46 -0800 Subject: [PATCH 103/168] quic: rename listener{_test}.go to endpoint{_test}.go Separate from CL 543298 to help git recognize that this is a rename. Change-Id: I1cbdffeb66d0960c951a564b8fc1a3dcf2cf40f6 Reviewed-on: https://go-review.googlesource.com/c/net/+/543299 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/{listener.go => endpoint.go} | 0 internal/quic/{listener_test.go => endpoint_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename internal/quic/{listener.go => endpoint.go} (100%) rename internal/quic/{listener_test.go => endpoint_test.go} (100%) diff --git a/internal/quic/listener.go b/internal/quic/endpoint.go similarity index 100% rename from internal/quic/listener.go rename to internal/quic/endpoint.go diff --git a/internal/quic/listener_test.go b/internal/quic/endpoint_test.go similarity index 100% rename from internal/quic/listener_test.go rename to internal/quic/endpoint_test.go From a8e0109124268a0a063b5900bce0c2b33398ec01 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Mon, 27 Nov 2023 17:02:04 +0000 Subject: [PATCH 104/168] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: Ia3b446633ffc0b3264692cfaae765bfb79063dab Reviewed-on: https://go-review.googlesource.com/c/net/+/545175 Auto-Submit: Gopher Robot LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Knyszek Reviewed-by: Dmitri Shuralyov --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 21deffd4bb..8ab3f40e13 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module golang.org/x/net go 1.18 require ( - golang.org/x/crypto v0.15.0 - golang.org/x/sys v0.14.0 - golang.org/x/term v0.14.0 + golang.org/x/crypto v0.16.0 + golang.org/x/sys v0.15.0 + golang.org/x/term v0.15.0 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index 54759e4894..bb6ed68a00 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= From f812076c5dd92f30fe0b9ed860869246746c9954 Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Wed, 29 Nov 2023 13:48:34 -0800 Subject: [PATCH 105/168] http2: explicitly set minimum TLS version in tests Fixes tests when using 1.22 in certain cases where the go.mod 'go' directive is not being respected. Change-Id: Ia986a7c900287abd67f0a05f662906a665cdeb87 Reviewed-on: https://go-review.googlesource.com/c/net/+/546115 LUCI-TryBot-Result: Go LUCI Auto-Submit: Roland Shoemaker Reviewed-by: Damien Neil --- http2/server_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/http2/server_test.go b/http2/server_test.go index 22657cbfe4..1fdd191ef7 100644 --- a/http2/server_test.go +++ b/http2/server_test.go @@ -145,6 +145,12 @@ func newServerTester(t testing.TB, handler http.HandlerFunc, opts ...interface{} ConfigureServer(ts.Config, h2server) + // Go 1.22 changes the default minimum TLS version to TLS 1.2, + // in order to properly test cases where we want to reject low + // TLS versions, we need to explicitly configure the minimum + // version here. + ts.Config.TLSConfig.MinVersion = tls.VersionTLS10 + st := &serverTester{ t: t, ts: ts, From 491f3545934c0aa6f51ce63beb323406693597ec Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 10 Nov 2023 08:01:03 -0800 Subject: [PATCH 106/168] quic: log packets and frames For golang/go#58547 Change-Id: I601f1e74417c0de206f71da58cef5938bba6e860 Reviewed-on: https://go-review.googlesource.com/c/net/+/543084 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_recv.go | 6 + internal/quic/conn_send.go | 9 ++ internal/quic/frame_debug.go | 220 ++++++++++++++++++++++++++++- internal/quic/packet.go | 16 +++ internal/quic/packet_codec_test.go | 71 ++++++++++ internal/quic/qlog.go | 102 +++++++++++++ internal/quic/qlog/json_writer.go | 125 ++++++++++++---- internal/quic/qlog_test.go | 108 +++++++++++--- internal/quic/quic.go | 11 ++ 9 files changed, 616 insertions(+), 52 deletions(-) diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 156ef5dd50..045bf861cd 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -101,6 +101,9 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa if logPackets { logInboundLongPacket(c, p) } + if c.logEnabled(QLogLevelPacket) { + c.logLongPacketReceived(p, buf[:n]) + } c.connIDState.handlePacket(c, p.ptype, p.srcConnID) ackEliciting := c.handleFrames(now, ptype, space, p.payload) c.acks[space].receive(now, space, p.num, ackEliciting) @@ -149,6 +152,9 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int { if logPackets { logInboundShortPacket(c, p) } + if c.logEnabled(QLogLevelPacket) { + c.log1RTTPacketReceived(p, buf) + } ackEliciting := c.handleFrames(now, packetType1RTT, appDataSpace, p.payload) c.acks[appDataSpace].receive(now, appDataSpace, p.num, ackEliciting) return len(buf) diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 4065474d2c..e2240f2fdb 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -75,6 +75,9 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { if logPackets { logSentPacket(c, packetTypeInitial, pnum, p.srcConnID, p.dstConnID, c.w.payload()) } + if c.logEnabled(QLogLevelPacket) && len(c.w.payload()) > 0 { + c.logPacketSent(packetTypeInitial, pnum, p.srcConnID, p.dstConnID, c.w.payload()) + } sentInitial = c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysInitial.w, p) if sentInitial != nil { c.idleHandlePacketSent(now, sentInitial) @@ -104,6 +107,9 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { if logPackets { logSentPacket(c, packetTypeHandshake, pnum, p.srcConnID, p.dstConnID, c.w.payload()) } + if c.logEnabled(QLogLevelPacket) && len(c.w.payload()) > 0 { + c.logPacketSent(packetTypeHandshake, pnum, p.srcConnID, p.dstConnID, c.w.payload()) + } if sent := c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysHandshake.w, p); sent != nil { c.idleHandlePacketSent(now, sent) c.loss.packetSent(now, handshakeSpace, sent) @@ -132,6 +138,9 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { if logPackets { logSentPacket(c, packetType1RTT, pnum, nil, dstConnID, c.w.payload()) } + if c.logEnabled(QLogLevelPacket) && len(c.w.payload()) > 0 { + c.logPacketSent(packetType1RTT, pnum, nil, dstConnID, c.w.payload()) + } if sent := c.w.finish1RTTPacket(pnum, pnumMaxAcked, dstConnID, &c.keysAppData); sent != nil { c.idleHandlePacketSent(now, sent) c.loss.packetSent(now, appDataSpace, sent) diff --git a/internal/quic/frame_debug.go b/internal/quic/frame_debug.go index dc80090375..0902c385f6 100644 --- a/internal/quic/frame_debug.go +++ b/internal/quic/frame_debug.go @@ -8,6 +8,9 @@ package quic import ( "fmt" + "log/slog" + "strconv" + "time" ) // A debugFrame is a representation of the contents of a QUIC frame, @@ -15,6 +18,7 @@ import ( type debugFrame interface { String() string write(w *packetWriter) bool + LogValue() slog.Value } func parseDebugFrame(b []byte) (f debugFrame, n int) { @@ -97,6 +101,13 @@ func (f debugFramePadding) write(w *packetWriter) bool { return true } +func (f debugFramePadding) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "padding"), + slog.Int("length", f.size), + ) +} + // debugFramePing is a PING frame. type debugFramePing struct{} @@ -112,6 +123,12 @@ func (f debugFramePing) write(w *packetWriter) bool { return w.appendPingFrame() } +func (f debugFramePing) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "ping"), + ) +} + // debugFrameAck is an ACK frame. type debugFrameAck struct { ackDelay unscaledAckDelay @@ -126,7 +143,7 @@ func parseDebugFrameAck(b []byte) (f debugFrameAck, n int) { end: end, }) }) - // Ranges are parsed smallest to highest; reverse ranges slice to order them high to low. + // Ranges are parsed high to low; reverse ranges slice to order them low to high. for i := 0; i < len(f.ranges)/2; i++ { j := len(f.ranges) - 1 f.ranges[i], f.ranges[j] = f.ranges[j], f.ranges[i] @@ -146,6 +163,61 @@ func (f debugFrameAck) write(w *packetWriter) bool { return w.appendAckFrame(rangeset[packetNumber](f.ranges), f.ackDelay) } +func (f debugFrameAck) LogValue() slog.Value { + return slog.StringValue("error: debugFrameAck should not appear as a slog Value") +} + +// debugFrameScaledAck is an ACK frame with scaled ACK Delay. +// +// This type is used in qlog events, which need access to the delay as a duration. +type debugFrameScaledAck struct { + ackDelay time.Duration + ranges []i64range[packetNumber] +} + +func (f debugFrameScaledAck) LogValue() slog.Value { + var ackDelay slog.Attr + if f.ackDelay >= 0 { + ackDelay = slog.Duration("ack_delay", f.ackDelay) + } + return slog.GroupValue( + slog.String("frame_type", "ack"), + // Rather than trying to convert the ack ranges into the slog data model, + // pass a value that can JSON-encode itself. + slog.Any("acked_ranges", debugAckRanges(f.ranges)), + ackDelay, + ) +} + +type debugAckRanges []i64range[packetNumber] + +// AppendJSON appends a JSON encoding of the ack ranges to b, and returns it. +// This is different than the standard json.Marshaler, but more efficient. +// Since we only use this in cooperation with the qlog package, +// encoding/json compatibility is irrelevant. +func (r debugAckRanges) AppendJSON(b []byte) []byte { + b = append(b, '[') + for i, ar := range r { + start, end := ar.start, ar.end-1 // qlog ranges are closed-closed + if i != 0 { + b = append(b, ',') + } + b = append(b, '[') + b = strconv.AppendInt(b, int64(start), 10) + if start != end { + b = append(b, ',') + b = strconv.AppendInt(b, int64(end), 10) + } + b = append(b, ']') + } + b = append(b, ']') + return b +} + +func (r debugAckRanges) String() string { + return string(r.AppendJSON(nil)) +} + // debugFrameResetStream is a RESET_STREAM frame. type debugFrameResetStream struct { id streamID @@ -166,6 +238,14 @@ func (f debugFrameResetStream) write(w *packetWriter) bool { return w.appendResetStreamFrame(f.id, f.code, f.finalSize) } +func (f debugFrameResetStream) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "reset_stream"), + slog.Uint64("stream_id", uint64(f.id)), + slog.Uint64("final_size", uint64(f.finalSize)), + ) +} + // debugFrameStopSending is a STOP_SENDING frame. type debugFrameStopSending struct { id streamID @@ -185,6 +265,14 @@ func (f debugFrameStopSending) write(w *packetWriter) bool { return w.appendStopSendingFrame(f.id, f.code) } +func (f debugFrameStopSending) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "stop_sending"), + slog.Uint64("stream_id", uint64(f.id)), + slog.Uint64("error_code", uint64(f.code)), + ) +} + // debugFrameCrypto is a CRYPTO frame. type debugFrameCrypto struct { off int64 @@ -206,6 +294,14 @@ func (f debugFrameCrypto) write(w *packetWriter) bool { return added } +func (f debugFrameCrypto) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "crypto"), + slog.Int64("offset", f.off), + slog.Int("length", len(f.data)), + ) +} + // debugFrameNewToken is a NEW_TOKEN frame. type debugFrameNewToken struct { token []byte @@ -224,6 +320,13 @@ func (f debugFrameNewToken) write(w *packetWriter) bool { return w.appendNewTokenFrame(f.token) } +func (f debugFrameNewToken) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "new_token"), + slogHexstring("token", f.token), + ) +} + // debugFrameStream is a STREAM frame. type debugFrameStream struct { id streamID @@ -251,6 +354,20 @@ func (f debugFrameStream) write(w *packetWriter) bool { return added } +func (f debugFrameStream) LogValue() slog.Value { + var fin slog.Attr + if f.fin { + fin = slog.Bool("fin", true) + } + return slog.GroupValue( + slog.String("frame_type", "stream"), + slog.Uint64("stream_id", uint64(f.id)), + slog.Int64("offset", f.off), + slog.Int("length", len(f.data)), + fin, + ) +} + // debugFrameMaxData is a MAX_DATA frame. type debugFrameMaxData struct { max int64 @@ -269,6 +386,13 @@ func (f debugFrameMaxData) write(w *packetWriter) bool { return w.appendMaxDataFrame(f.max) } +func (f debugFrameMaxData) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "max_data"), + slog.Int64("maximum", f.max), + ) +} + // debugFrameMaxStreamData is a MAX_STREAM_DATA frame. type debugFrameMaxStreamData struct { id streamID @@ -288,6 +412,14 @@ func (f debugFrameMaxStreamData) write(w *packetWriter) bool { return w.appendMaxStreamDataFrame(f.id, f.max) } +func (f debugFrameMaxStreamData) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "max_stream_data"), + slog.Uint64("stream_id", uint64(f.id)), + slog.Int64("maximum", f.max), + ) +} + // debugFrameMaxStreams is a MAX_STREAMS frame. type debugFrameMaxStreams struct { streamType streamType @@ -307,6 +439,14 @@ func (f debugFrameMaxStreams) write(w *packetWriter) bool { return w.appendMaxStreamsFrame(f.streamType, f.max) } +func (f debugFrameMaxStreams) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "max_streams"), + slog.String("stream_type", f.streamType.qlogString()), + slog.Int64("maximum", f.max), + ) +} + // debugFrameDataBlocked is a DATA_BLOCKED frame. type debugFrameDataBlocked struct { max int64 @@ -325,6 +465,13 @@ func (f debugFrameDataBlocked) write(w *packetWriter) bool { return w.appendDataBlockedFrame(f.max) } +func (f debugFrameDataBlocked) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "data_blocked"), + slog.Int64("limit", f.max), + ) +} + // debugFrameStreamDataBlocked is a STREAM_DATA_BLOCKED frame. type debugFrameStreamDataBlocked struct { id streamID @@ -344,6 +491,14 @@ func (f debugFrameStreamDataBlocked) write(w *packetWriter) bool { return w.appendStreamDataBlockedFrame(f.id, f.max) } +func (f debugFrameStreamDataBlocked) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "stream_data_blocked"), + slog.Uint64("stream_id", uint64(f.id)), + slog.Int64("limit", f.max), + ) +} + // debugFrameStreamsBlocked is a STREAMS_BLOCKED frame. type debugFrameStreamsBlocked struct { streamType streamType @@ -363,6 +518,14 @@ func (f debugFrameStreamsBlocked) write(w *packetWriter) bool { return w.appendStreamsBlockedFrame(f.streamType, f.max) } +func (f debugFrameStreamsBlocked) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "streams_blocked"), + slog.String("stream_type", f.streamType.qlogString()), + slog.Int64("limit", f.max), + ) +} + // debugFrameNewConnectionID is a NEW_CONNECTION_ID frame. type debugFrameNewConnectionID struct { seq int64 @@ -384,6 +547,16 @@ func (f debugFrameNewConnectionID) write(w *packetWriter) bool { return w.appendNewConnectionIDFrame(f.seq, f.retirePriorTo, f.connID, f.token) } +func (f debugFrameNewConnectionID) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "new_connection_id"), + slog.Int64("sequence_number", f.seq), + slog.Int64("retire_prior_to", f.retirePriorTo), + slogHexstring("connection_id", f.connID), + slogHexstring("stateless_reset_token", f.token[:]), + ) +} + // debugFrameRetireConnectionID is a NEW_CONNECTION_ID frame. type debugFrameRetireConnectionID struct { seq int64 @@ -402,6 +575,13 @@ func (f debugFrameRetireConnectionID) write(w *packetWriter) bool { return w.appendRetireConnectionIDFrame(f.seq) } +func (f debugFrameRetireConnectionID) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "retire_connection_id"), + slog.Int64("sequence_number", f.seq), + ) +} + // debugFramePathChallenge is a PATH_CHALLENGE frame. type debugFramePathChallenge struct { data uint64 @@ -420,6 +600,13 @@ func (f debugFramePathChallenge) write(w *packetWriter) bool { return w.appendPathChallengeFrame(f.data) } +func (f debugFramePathChallenge) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "path_challenge"), + slog.String("data", fmt.Sprintf("%016x", f.data)), + ) +} + // debugFramePathResponse is a PATH_RESPONSE frame. type debugFramePathResponse struct { data uint64 @@ -438,6 +625,13 @@ func (f debugFramePathResponse) write(w *packetWriter) bool { return w.appendPathResponseFrame(f.data) } +func (f debugFramePathResponse) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "path_response"), + slog.String("data", fmt.Sprintf("%016x", f.data)), + ) +} + // debugFrameConnectionCloseTransport is a CONNECTION_CLOSE frame carrying a transport error. type debugFrameConnectionCloseTransport struct { code transportError @@ -465,6 +659,15 @@ func (f debugFrameConnectionCloseTransport) write(w *packetWriter) bool { return w.appendConnectionCloseTransportFrame(f.code, f.frameType, f.reason) } +func (f debugFrameConnectionCloseTransport) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "connection_close"), + slog.String("error_space", "transport"), + slog.Uint64("error_code_value", uint64(f.code)), + slog.String("reason", f.reason), + ) +} + // debugFrameConnectionCloseApplication is a CONNECTION_CLOSE frame carrying an application error. type debugFrameConnectionCloseApplication struct { code uint64 @@ -488,6 +691,15 @@ func (f debugFrameConnectionCloseApplication) write(w *packetWriter) bool { return w.appendConnectionCloseApplicationFrame(f.code, f.reason) } +func (f debugFrameConnectionCloseApplication) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "connection_close"), + slog.String("error_space", "application"), + slog.Uint64("error_code_value", uint64(f.code)), + slog.String("reason", f.reason), + ) +} + // debugFrameHandshakeDone is a HANDSHAKE_DONE frame. type debugFrameHandshakeDone struct{} @@ -502,3 +714,9 @@ func (f debugFrameHandshakeDone) String() string { func (f debugFrameHandshakeDone) write(w *packetWriter) bool { return w.appendHandshakeDoneFrame() } + +func (f debugFrameHandshakeDone) LogValue() slog.Value { + return slog.GroupValue( + slog.String("frame_type", "handshake_done"), + ) +} diff --git a/internal/quic/packet.go b/internal/quic/packet.go index df589cccac..7a874319d7 100644 --- a/internal/quic/packet.go +++ b/internal/quic/packet.go @@ -41,6 +41,22 @@ func (p packetType) String() string { return fmt.Sprintf("unknown packet type %v", byte(p)) } +func (p packetType) qlogString() string { + switch p { + case packetTypeInitial: + return "initial" + case packetType0RTT: + return "0RTT" + case packetTypeHandshake: + return "handshake" + case packetTypeRetry: + return "retry" + case packetType1RTT: + return "1RTT" + } + return "unknown" +} + // Bits set in the first byte of a packet. const ( headerFormLong = 0x80 // https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2-3.2.1 diff --git a/internal/quic/packet_codec_test.go b/internal/quic/packet_codec_test.go index 7b01bb00d6..475e18c1da 100644 --- a/internal/quic/packet_codec_test.go +++ b/internal/quic/packet_codec_test.go @@ -9,8 +9,13 @@ package quic import ( "bytes" "crypto/tls" + "io" + "log/slog" "reflect" "testing" + "time" + + "golang.org/x/net/internal/quic/qlog" ) func TestParseLongHeaderPacket(t *testing.T) { @@ -207,11 +212,13 @@ func TestRoundtripEncodeShortPacket(t *testing.T) { func TestFrameEncodeDecode(t *testing.T) { for _, test := range []struct { s string + j string f debugFrame b []byte truncated []byte }{{ s: "PADDING*1", + j: `{"frame_type":"padding","length":1}`, f: debugFramePadding{ size: 1, }, @@ -221,12 +228,14 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "PING", + j: `{"frame_type":"ping"}`, f: debugFramePing{}, b: []byte{ 0x01, // TYPE(i) = 0x01 }, }, { s: "ACK Delay=10 [0,16) [17,32) [48,64)", + j: `"error: debugFrameAck should not appear as a slog Value"`, f: debugFrameAck{ ackDelay: 10, ranges: []i64range[packetNumber]{ @@ -257,6 +266,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "RESET_STREAM ID=1 Code=2 FinalSize=3", + j: `{"frame_type":"reset_stream","stream_id":1,"final_size":3}`, f: debugFrameResetStream{ id: 1, code: 2, @@ -270,6 +280,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "STOP_SENDING ID=1 Code=2", + j: `{"frame_type":"stop_sending","stream_id":1,"error_code":2}`, f: debugFrameStopSending{ id: 1, code: 2, @@ -281,6 +292,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "CRYPTO Offset=1 Length=2", + j: `{"frame_type":"crypto","offset":1,"length":2}`, f: debugFrameCrypto{ off: 1, data: []byte{3, 4}, @@ -299,6 +311,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "NEW_TOKEN Token=0304", + j: `{"frame_type":"new_token","token":"0304"}`, f: debugFrameNewToken{ token: []byte{3, 4}, }, @@ -309,6 +322,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "STREAM ID=1 Offset=0 Length=0", + j: `{"frame_type":"stream","stream_id":1,"offset":0,"length":0}`, f: debugFrameStream{ id: 1, fin: false, @@ -324,6 +338,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "STREAM ID=100 Offset=4 Length=3", + j: `{"frame_type":"stream","stream_id":100,"offset":4,"length":3}`, f: debugFrameStream{ id: 100, fin: false, @@ -346,6 +361,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "STREAM ID=100 FIN Offset=4 Length=3", + j: `{"frame_type":"stream","stream_id":100,"offset":4,"length":3,"fin":true}`, f: debugFrameStream{ id: 100, fin: true, @@ -368,6 +384,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "STREAM ID=1 FIN Offset=100 Length=0", + j: `{"frame_type":"stream","stream_id":1,"offset":100,"length":0,"fin":true}`, f: debugFrameStream{ id: 1, fin: true, @@ -383,6 +400,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "MAX_DATA Max=10", + j: `{"frame_type":"max_data","maximum":10}`, f: debugFrameMaxData{ max: 10, }, @@ -392,6 +410,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "MAX_STREAM_DATA ID=1 Max=10", + j: `{"frame_type":"max_stream_data","stream_id":1,"maximum":10}`, f: debugFrameMaxStreamData{ id: 1, max: 10, @@ -403,6 +422,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "MAX_STREAMS Type=bidi Max=1", + j: `{"frame_type":"max_streams","stream_type":"bidirectional","maximum":1}`, f: debugFrameMaxStreams{ streamType: bidiStream, max: 1, @@ -413,6 +433,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "MAX_STREAMS Type=uni Max=1", + j: `{"frame_type":"max_streams","stream_type":"unidirectional","maximum":1}`, f: debugFrameMaxStreams{ streamType: uniStream, max: 1, @@ -423,6 +444,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "DATA_BLOCKED Max=1", + j: `{"frame_type":"data_blocked","limit":1}`, f: debugFrameDataBlocked{ max: 1, }, @@ -432,6 +454,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "STREAM_DATA_BLOCKED ID=1 Max=2", + j: `{"frame_type":"stream_data_blocked","stream_id":1,"limit":2}`, f: debugFrameStreamDataBlocked{ id: 1, max: 2, @@ -443,6 +466,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "STREAMS_BLOCKED Type=bidi Max=1", + j: `{"frame_type":"streams_blocked","stream_type":"bidirectional","limit":1}`, f: debugFrameStreamsBlocked{ streamType: bidiStream, max: 1, @@ -453,6 +477,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "STREAMS_BLOCKED Type=uni Max=1", + j: `{"frame_type":"streams_blocked","stream_type":"unidirectional","limit":1}`, f: debugFrameStreamsBlocked{ streamType: uniStream, max: 1, @@ -463,6 +488,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "NEW_CONNECTION_ID Seq=3 Retire=2 ID=a0a1a2a3 Token=0102030405060708090a0b0c0d0e0f10", + j: `{"frame_type":"new_connection_id","sequence_number":3,"retire_prior_to":2,"connection_id":"a0a1a2a3","stateless_reset_token":"0102030405060708090a0b0c0d0e0f10"}`, f: debugFrameNewConnectionID{ seq: 3, retirePriorTo: 2, @@ -479,6 +505,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "RETIRE_CONNECTION_ID Seq=1", + j: `{"frame_type":"retire_connection_id","sequence_number":1}`, f: debugFrameRetireConnectionID{ seq: 1, }, @@ -488,6 +515,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "PATH_CHALLENGE Data=0123456789abcdef", + j: `{"frame_type":"path_challenge","data":"0123456789abcdef"}`, f: debugFramePathChallenge{ data: 0x0123456789abcdef, }, @@ -497,6 +525,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "PATH_RESPONSE Data=0123456789abcdef", + j: `{"frame_type":"path_response","data":"0123456789abcdef"}`, f: debugFramePathResponse{ data: 0x0123456789abcdef, }, @@ -506,6 +535,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: `CONNECTION_CLOSE Code=INTERNAL_ERROR FrameType=2 Reason="oops"`, + j: `{"frame_type":"connection_close","error_space":"transport","error_code_value":1,"reason":"oops"}`, f: debugFrameConnectionCloseTransport{ code: 1, frameType: 2, @@ -520,6 +550,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: `CONNECTION_CLOSE AppCode=1 Reason="oops"`, + j: `{"frame_type":"connection_close","error_space":"application","error_code_value":1,"reason":"oops"}`, f: debugFrameConnectionCloseApplication{ code: 1, reason: "oops", @@ -532,6 +563,7 @@ func TestFrameEncodeDecode(t *testing.T) { }, }, { s: "HANDSHAKE_DONE", + j: `{"frame_type":"handshake_done"}`, f: debugFrameHandshakeDone{}, b: []byte{ 0x1e, // Type (i) = 0x1e, @@ -554,6 +586,9 @@ func TestFrameEncodeDecode(t *testing.T) { if got, want := test.f.String(), test.s; got != want { t.Errorf("frame.String():\ngot %q\nwant %q", got, want) } + if got, want := frameJSON(test.f), test.j; got != want { + t.Errorf("frame.LogValue():\ngot %q\nwant %q", got, want) + } // Try encoding the frame into too little space. // Most frames will result in an error; some (like STREAM frames) will truncate @@ -579,6 +614,42 @@ func TestFrameEncodeDecode(t *testing.T) { } } +func TestFrameScaledAck(t *testing.T) { + for _, test := range []struct { + j string + f debugFrameScaledAck + }{{ + j: `{"frame_type":"ack","acked_ranges":[[0,15],[17],[48,63]],"ack_delay":10.000000}`, + f: debugFrameScaledAck{ + ackDelay: 10 * time.Millisecond, + ranges: []i64range[packetNumber]{ + {0x00, 0x10}, + {0x11, 0x12}, + {0x30, 0x40}, + }, + }, + }} { + if got, want := frameJSON(test.f), test.j; got != want { + t.Errorf("frame.LogValue():\ngot %q\nwant %q", got, want) + } + } +} + +func frameJSON(f slog.LogValuer) string { + var buf bytes.Buffer + h := qlog.NewJSONHandler(qlog.HandlerOptions{ + Level: QLogLevelFrame, + NewTrace: func(info qlog.TraceInfo) (io.WriteCloser, error) { + return nopCloseWriter{&buf}, nil + }, + }) + // Log the frame, and then trim out everything but the frame from the log. + slog.New(h).Info("message", slog.Any("frame", f)) + _, b, _ := bytes.Cut(buf.Bytes(), []byte(`"frame":`)) + b = bytes.TrimSuffix(b, []byte("}}\n")) + return string(b) +} + func TestFrameDecode(t *testing.T) { for _, test := range []struct { desc string diff --git a/internal/quic/qlog.go b/internal/quic/qlog.go index ea53cab1e8..fea8b38eef 100644 --- a/internal/quic/qlog.go +++ b/internal/quic/qlog.go @@ -11,6 +11,7 @@ import ( "encoding/hex" "log/slog" "net/netip" + "time" ) // Log levels for qlog events. @@ -145,3 +146,104 @@ func (c *Conn) logConnectionClosed() { slog.String("trigger", trigger), ) } + +func (c *Conn) logLongPacketReceived(p longPacket, pkt []byte) { + pnumLen := 1 + int(pkt[0]&0x03) + length := pnumLen + len(p.payload) + var frames slog.Attr + if c.logEnabled(QLogLevelFrame) { + frames = c.packetFramesAttr(p.payload) + } + c.log.LogAttrs(context.Background(), QLogLevelPacket, + "transport:packet_received", + slog.Group("header", + slog.String("packet_type", p.ptype.qlogString()), + slog.Uint64("packet_number", uint64(p.num)), + slog.Uint64("flags", uint64(pkt[0])), + slogHexstring("scid", p.srcConnID), + slogHexstring("dcid", p.dstConnID), + slog.Int("length", length), + ), + frames, + ) +} + +func (c *Conn) log1RTTPacketReceived(p shortPacket, pkt []byte) { + var frames slog.Attr + if c.logEnabled(QLogLevelFrame) { + frames = c.packetFramesAttr(p.payload) + } + dstConnID, _ := dstConnIDForDatagram(pkt) + c.log.LogAttrs(context.Background(), QLogLevelPacket, + "transport:packet_received", + slog.Group("header", + slog.String("packet_type", packetType1RTT.qlogString()), + slog.Uint64("packet_number", uint64(p.num)), + slog.Uint64("flags", uint64(pkt[0])), + slog.String("scid", ""), + slogHexstring("dcid", dstConnID), + ), + frames, + ) +} + +func (c *Conn) logPacketSent(ptype packetType, pnum packetNumber, src, dst, payload []byte) { + var frames slog.Attr + if c.logEnabled(QLogLevelFrame) { + frames = c.packetFramesAttr(payload) + } + var scid slog.Attr + if len(src) > 0 { + scid = slogHexstring("scid", src) + } + c.log.LogAttrs(context.Background(), QLogLevelPacket, + "transport:packet_sent", + slog.Group("header", + slog.String("packet_type", ptype.qlogString()), + slog.Uint64("packet_number", uint64(pnum)), + scid, + slogHexstring("dcid", dst), + ), + frames, + ) +} + +// packetFramesAttr returns the "frames" attribute containing the frames in a packet. +// We currently pass this as a slog Any containing a []slog.Value, +// where each Value is a debugFrame that implements slog.LogValuer. +// +// This isn't tremendously efficient, but avoids the need to put a JSON encoder +// in the quic package or a frame parser in the qlog package. +func (c *Conn) packetFramesAttr(payload []byte) slog.Attr { + var frames []slog.Value + for len(payload) > 0 { + f, n := parseDebugFrame(payload) + if n < 0 { + break + } + payload = payload[n:] + switch f := f.(type) { + case debugFrameAck: + // The qlog ACK frame contains the ACK Delay field as a duration. + // Interpreting the contents of this field as a duration requires + // knowing the peer's ack_delay_exponent transport parameter, + // and it's possible for us to parse an ACK frame before we've + // received that parameter. + // + // We could plumb connection state down into the frame parser, + // but for now let's minimize the amount of code that needs to + // deal with this and convert the unscaled value into a scaled one here. + ackDelay := time.Duration(-1) + if c.peerAckDelayExponent >= 0 { + ackDelay = f.ackDelay.Duration(uint8(c.peerAckDelayExponent)) + } + frames = append(frames, slog.AnyValue(debugFrameScaledAck{ + ranges: f.ranges, + ackDelay: ackDelay, + })) + default: + frames = append(frames, slog.AnyValue(f)) + } + } + return slog.Any("frames", frames) +} diff --git a/internal/quic/qlog/json_writer.go b/internal/quic/qlog/json_writer.go index 50cf33bc50..3950ab42f6 100644 --- a/internal/quic/qlog/json_writer.go +++ b/internal/quic/qlog/json_writer.go @@ -42,38 +42,56 @@ func (w *jsonWriter) writeRecordEnd() { w.mu.Unlock() } -// writeAttrsField writes a []slog.Attr as an object field. -func (w *jsonWriter) writeAttrsField(name string, attrs []slog.Attr) { - w.writeName(name) +func (w *jsonWriter) writeAttrs(attrs []slog.Attr) { w.buf.WriteByte('{') for _, a := range attrs { + if a.Key == "" { + continue + } w.writeAttr(a) } w.buf.WriteByte('}') } -// writeAttr writes a slog.Attr as an object field. func (w *jsonWriter) writeAttr(a slog.Attr) { - v := a.Value.Resolve() + w.writeName(a.Key) + w.writeValue(a.Value) +} + +// writeAttr writes a []slog.Attr as an object field. +func (w *jsonWriter) writeAttrsField(name string, attrs []slog.Attr) { + w.writeName(name) + w.writeAttrs(attrs) +} + +func (w *jsonWriter) writeValue(v slog.Value) { + v = v.Resolve() switch v.Kind() { case slog.KindAny: - w.writeStringField(a.Key, fmt.Sprint(v.Any())) + switch v := v.Any().(type) { + case []slog.Value: + w.writeArray(v) + case interface{ AppendJSON([]byte) []byte }: + w.buf.Write(v.AppendJSON(w.buf.AvailableBuffer())) + default: + w.writeString(fmt.Sprint(v)) + } case slog.KindBool: - w.writeBoolField(a.Key, v.Bool()) + w.writeBool(v.Bool()) case slog.KindDuration: - w.writeDurationField(a.Key, v.Duration()) + w.writeDuration(v.Duration()) case slog.KindFloat64: - w.writeFloat64Field(a.Key, v.Float64()) + w.writeFloat64(v.Float64()) case slog.KindInt64: - w.writeInt64Field(a.Key, v.Int64()) + w.writeInt64(v.Int64()) case slog.KindString: - w.writeStringField(a.Key, v.String()) + w.writeString(v.String()) case slog.KindTime: - w.writeTimeField(a.Key, v.Time()) + w.writeTime(v.Time()) case slog.KindUint64: - w.writeUint64Field(a.Key, v.Uint64()) + w.writeUint64(v.Uint64()) case slog.KindGroup: - w.writeAttrsField(a.Key, v.Group()) + w.writeAttrs(v.Group()) default: w.writeString("unhandled kind") } @@ -89,24 +107,41 @@ func (w *jsonWriter) writeName(name string) { w.buf.WriteByte(':') } -// writeObject writes an object-valued object field. -// The function f is called to write the contents. -func (w *jsonWriter) writeObjectField(name string, f func()) { - w.writeName(name) +func (w *jsonWriter) writeObject(f func()) { w.buf.WriteByte('{') f() w.buf.WriteByte('}') } -// writeRawField writes an field with a raw JSON value. -func (w *jsonWriter) writeRawField(name, v string) { +// writeObject writes an object-valued object field. +// The function f is called to write the contents. +func (w *jsonWriter) writeObjectField(name string, f func()) { w.writeName(name) + w.writeObject(f) +} + +func (w *jsonWriter) writeArray(vals []slog.Value) { + w.buf.WriteByte('[') + for i, v := range vals { + if i != 0 { + w.buf.WriteByte(',') + } + w.writeValue(v) + } + w.buf.WriteByte(']') +} + +func (w *jsonWriter) writeRaw(v string) { w.buf.WriteString(v) } -// writeBoolField writes a bool-valued object field. -func (w *jsonWriter) writeBoolField(name string, v bool) { +// writeRawField writes a field with a raw JSON value. +func (w *jsonWriter) writeRawField(name, v string) { w.writeName(name) + w.writeRaw(v) +} + +func (w *jsonWriter) writeBool(v bool) { if v { w.buf.WriteString("true") } else { @@ -114,40 +149,62 @@ func (w *jsonWriter) writeBoolField(name string, v bool) { } } +// writeBoolField writes a bool-valued object field. +func (w *jsonWriter) writeBoolField(name string, v bool) { + w.writeName(name) + w.writeBool(v) +} + +// writeDuration writes a duration as milliseconds. +func (w *jsonWriter) writeDuration(v time.Duration) { + fmt.Fprintf(&w.buf, "%d.%06d", v.Milliseconds(), v%time.Millisecond) +} + // writeDurationField writes a millisecond duration-valued object field. func (w *jsonWriter) writeDurationField(name string, v time.Duration) { w.writeName(name) - fmt.Fprintf(&w.buf, "%d.%06d", v.Milliseconds(), v%time.Millisecond) + w.writeDuration(v) +} + +func (w *jsonWriter) writeFloat64(v float64) { + w.buf.Write(strconv.AppendFloat(w.buf.AvailableBuffer(), v, 'f', -1, 64)) } // writeFloat64Field writes an float64-valued object field. func (w *jsonWriter) writeFloat64Field(name string, v float64) { w.writeName(name) - w.buf.Write(strconv.AppendFloat(w.buf.AvailableBuffer(), v, 'f', -1, 64)) + w.writeFloat64(v) +} + +func (w *jsonWriter) writeInt64(v int64) { + w.buf.Write(strconv.AppendInt(w.buf.AvailableBuffer(), v, 10)) } // writeInt64Field writes an int64-valued object field. func (w *jsonWriter) writeInt64Field(name string, v int64) { w.writeName(name) - w.buf.Write(strconv.AppendInt(w.buf.AvailableBuffer(), v, 10)) + w.writeInt64(v) +} + +func (w *jsonWriter) writeUint64(v uint64) { + w.buf.Write(strconv.AppendUint(w.buf.AvailableBuffer(), v, 10)) } // writeUint64Field writes a uint64-valued object field. func (w *jsonWriter) writeUint64Field(name string, v uint64) { w.writeName(name) - w.buf.Write(strconv.AppendUint(w.buf.AvailableBuffer(), v, 10)) + w.writeUint64(v) } -// writeStringField writes a string-valued object field. -func (w *jsonWriter) writeStringField(name, v string) { - w.writeName(name) - w.writeString(v) +// writeTime writes a time as seconds since the Unix epoch. +func (w *jsonWriter) writeTime(v time.Time) { + fmt.Fprintf(&w.buf, "%d.%06d", v.UnixMilli(), v.Nanosecond()%int(time.Millisecond)) } // writeTimeField writes a time-valued object field. func (w *jsonWriter) writeTimeField(name string, v time.Time) { w.writeName(name) - fmt.Fprintf(&w.buf, "%d.%06d", v.UnixMilli(), v.Nanosecond()%int(time.Millisecond)) + w.writeTime(v) } func jsonSafeSet(c byte) bool { @@ -192,3 +249,9 @@ func (w *jsonWriter) writeString(v string) { } w.buf.WriteByte('"') } + +// writeStringField writes a string-valued object field. +func (w *jsonWriter) writeStringField(name, v string) { + w.writeName(name) + w.writeString(v) +} diff --git a/internal/quic/qlog_test.go b/internal/quic/qlog_test.go index 119f5d16af..e98b118386 100644 --- a/internal/quic/qlog_test.go +++ b/internal/quic/qlog_test.go @@ -55,6 +55,41 @@ func TestQLogHandshake(t *testing.T) { }) } +func TestQLogPacketFrames(t *testing.T) { + qr := &qlogRecord{} + tc := newTestConn(t, clientSide, qr.config) + tc.handshake() + tc.conn.Abort(nil) + tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{}) + tc.advanceToTimer() // let the conn finish draining + + qr.wantEvents(t, jsonEvent{ + "name": "transport:packet_sent", + "data": map[string]any{ + "header": map[string]any{ + "packet_type": "initial", + "packet_number": 0, + "dcid": hex.EncodeToString(testLocalConnID(-1)), + "scid": hex.EncodeToString(testLocalConnID(0)), + }, + "frames": []any{ + map[string]any{"frame_type": "crypto"}, + }, + }, + }, jsonEvent{ + "name": "transport:packet_received", + "data": map[string]any{ + "header": map[string]any{ + "packet_type": "initial", + "packet_number": 0, + "dcid": hex.EncodeToString(testLocalConnID(0)), + "scid": hex.EncodeToString(testPeerConnID(0)), + }, + "frames": []any{map[string]any{"frame_type": "crypto"}}, + }, + }) +} + func TestQLogConnectionClosedTrigger(t *testing.T) { for _, test := range []struct { trigger string @@ -137,21 +172,60 @@ func (j jsonEvent) String() string { return string(b) } -// eventPartialEqual verifies that every field set in want matches the corresponding field in got. -// It ignores additional fields in got. -func eventPartialEqual(got, want jsonEvent) bool { - for k := range want { - ge, gok := got[k].(map[string]any) - we, wok := want[k].(map[string]any) - if gok && wok { - if !eventPartialEqual(ge, we) { - return false +// jsonPartialEqual compares two JSON structures. +// It ignores fields not set in want (see below for specifics). +func jsonPartialEqual(got, want any) (equal bool) { + cmpval := func(v any) any { + // Map certain types to a common representation. + switch v := v.(type) { + case int: + // JSON uses float64s rather than ints for numbers. + // Map int->float64 so we can use integers in expectations. + return float64(v) + case jsonEvent: + return (map[string]any)(v) + case []jsonEvent: + s := []any{} + for _, e := range v { + s = append(s, e) } - } else { - if !reflect.DeepEqual(got[k], want[k]) { + return s + } + return v + } + got = cmpval(got) + want = cmpval(want) + if reflect.TypeOf(got) != reflect.TypeOf(want) { + return false + } + switch w := want.(type) { + case nil: + // Match anything. + case map[string]any: + // JSON object: Every field in want must match a field in got. + g := got.(map[string]any) + for k := range w { + if !jsonPartialEqual(g[k], w[k]) { return false } } + case []any: + // JSON slice: Every field in want must match a field in got, in order. + // So want=[2,4] matches got=[1,2,3,4] but not [4,2]. + g := got.([]any) + for _, ge := range g { + if jsonPartialEqual(ge, w[0]) { + w = w[1:] + if len(w) == 0 { + return true + } + } + } + return false + default: + if !reflect.DeepEqual(got, want) { + return false + } } return true } @@ -179,6 +253,7 @@ func (q *qlogRecord) Close() error { return nil } // config may be passed to newTestConn to configure the conn to use this logger. func (q *qlogRecord) config(c *Config) { c.QLogLogger = slog.New(qlog.NewJSONHandler(qlog.HandlerOptions{ + Level: QLogLevelFrame, NewTrace: func(info qlog.TraceInfo) (io.WriteCloser, error) { return q, nil }, @@ -189,14 +264,7 @@ func (q *qlogRecord) config(c *Config) { func (q *qlogRecord) wantEvents(t *testing.T, want ...jsonEvent) { t.Helper() got := q.ev - unseen := want - for _, g := range got { - if eventPartialEqual(g, unseen[0]) { - unseen = unseen[1:] - if len(unseen) == 0 { - return - } - } + if !jsonPartialEqual(got, want) { + t.Fatalf("got events:\n%v\n\nwant events:\n%v", got, want) } - t.Fatalf("got events:\n%v\n\nwant events:\n%v", got, want) } diff --git a/internal/quic/quic.go b/internal/quic/quic.go index e4d0d77c7f..3e62d7cd94 100644 --- a/internal/quic/quic.go +++ b/internal/quic/quic.go @@ -144,6 +144,17 @@ const ( streamTypeCount ) +func (s streamType) qlogString() string { + switch s { + case bidiStream: + return "bidirectional" + case uniStream: + return "unidirectional" + default: + return "BUG" + } +} + func (s streamType) String() string { switch s { case bidiStream: From c1b6eee3f608179effef5e5964776391ef81e619 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 17 Nov 2023 14:49:16 -0800 Subject: [PATCH 107/168] quic: send occasional ack-eliciting packets A receiver that is sending only non-ack-eliciting packets (for example, a connection reading data from a stream but not sending anything other than ACKs in response) can accumulate a large amount of state for in-flight, unacknowledged packets. Add an occasional PING frame when in this state, to cause the peer to send an ACK for our outstanding packets. Change-Id: Iaf6b5a9735fa356fdebaff24200420a280b0c9a5 Reviewed-on: https://go-review.googlesource.com/c/net/+/545215 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_send.go | 30 ++++++++++++++++++++----- internal/quic/conn_send_test.go | 40 +++++++++++++++++++++++++++++++++ internal/quic/loss.go | 8 +++++++ 3 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 internal/quic/conn_send_test.go diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index e2240f2fdb..a8d9308989 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -222,11 +222,7 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, // Either we are willing to send an ACK-only packet, // or we've added additional frames. c.acks[space].sentAck() - if !c.w.sent.ackEliciting && c.keysAppData.needAckEliciting() { - // The peer has initiated a key update. - // We haven't sent them any packets yet in the new phase. - // Make this an ack-eliciting packet. - // Their ack of this packet will complete the key update. + if !c.w.sent.ackEliciting && c.shouldMakePacketAckEliciting() { c.w.appendPingFrame() } }() @@ -331,6 +327,30 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, } } +// shouldMakePacketAckEliciting is called when sending a packet containing nothing but an ACK frame. +// It reports whether we should add a PING frame to the packet to make it ack-eliciting. +func (c *Conn) shouldMakePacketAckEliciting() bool { + if c.keysAppData.needAckEliciting() { + // The peer has initiated a key update. + // We haven't sent them any packets yet in the new phase. + // Make this an ack-eliciting packet. + // Their ack of this packet will complete the key update. + return true + } + if c.loss.consecutiveNonAckElicitingPackets >= 19 { + // We've sent a run of non-ack-eliciting packets. + // Add in an ack-eliciting one every once in a while so the peer + // lets us know which ones have arrived. + // + // Google QUICHE injects a PING after sending 19 packets. We do the same. + // + // https://www.rfc-editor.org/rfc/rfc9000#section-13.2.4-2 + return true + } + // TODO: Consider making every packet sent when in PTO ack-eliciting to speed up recovery. + return false +} + func (c *Conn) appendAckFrame(now time.Time, space numberSpace) bool { seen, delay := c.acks[space].acksToSend(now) if len(seen) == 0 { diff --git a/internal/quic/conn_send_test.go b/internal/quic/conn_send_test.go new file mode 100644 index 0000000000..822783c416 --- /dev/null +++ b/internal/quic/conn_send_test.go @@ -0,0 +1,40 @@ +// 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 TestAckElicitingAck(t *testing.T) { + // "A receiver that sends only non-ack-eliciting packets [...] might not receive + // an acknowledgment for a long period of time. + // [...] a receiver could send a [...] ack-eliciting frame occasionally [...] + // to elicit an ACK from the peer." + // https://www.rfc-editor.org/rfc/rfc9000#section-13.2.4-2 + // + // Send a bunch of ack-eliciting packets, verify that the conn doesn't just + // send ACKs in response. + tc := newTestConn(t, clientSide, permissiveTransportParameters) + tc.handshake() + const count = 100 + for i := 0; i < count; i++ { + tc.advance(1 * time.Millisecond) + tc.writeFrames(packetType1RTT, + debugFramePing{}, + ) + got, _ := tc.readFrame() + switch got.(type) { + case debugFrameAck: + continue + case debugFramePing: + return + } + } + t.Errorf("after sending %v PINGs, got no ack-eliciting response", count) +} diff --git a/internal/quic/loss.go b/internal/quic/loss.go index 4a0767bd0b..a59081fd57 100644 --- a/internal/quic/loss.go +++ b/internal/quic/loss.go @@ -50,6 +50,9 @@ type lossState struct { // https://www.rfc-editor.org/rfc/rfc9000#section-8-2 antiAmplificationLimit int + // Count of non-ack-eliciting packets (ACKs) sent since the last ack-eliciting one. + consecutiveNonAckElicitingPackets int + rtt rttState pacer pacerState cc *ccReno @@ -192,6 +195,11 @@ func (c *lossState) packetSent(now time.Time, space numberSpace, sent *sentPacke } c.scheduleTimer(now) } + if sent.ackEliciting { + c.consecutiveNonAckElicitingPackets = 0 + } else { + c.consecutiveNonAckElicitingPackets++ + } } // datagramReceived records a datagram (not packet!) received from the peer. From 08a78b1eeae5f15e658ca8972aa74b6857e3b37b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 20 Nov 2023 16:41:14 -0800 Subject: [PATCH 108/168] quic: unblock operations when closing conns Blocking operations associated with a connection, such as accepting a stream or writing data to a stream, should be canceled when the connection is closed. Change-Id: I3b25789885a6c1a2b5aa2178a8d6219a8ea77cbb Reviewed-on: https://go-review.googlesource.com/c/net/+/545216 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam Auto-Submit: Damien Neil --- internal/quic/conn.go | 12 ++++--- internal/quic/conn_async_test.go | 15 ++++---- internal/quic/conn_close.go | 15 ++++++-- internal/quic/conn_close_test.go | 61 ++++++++++++++++++++++++++++++++ internal/quic/conn_streams.go | 11 ++++++ internal/quic/endpoint_test.go | 2 +- internal/quic/stream.go | 41 ++++++++++++++++++++- internal/quic/stream_limits.go | 17 +++++++-- internal/quic/stream_test.go | 30 ++++++++++++---- 9 files changed, 180 insertions(+), 24 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 31e789b1da..4abc74030c 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -263,10 +263,7 @@ var errIdleTimeout = errors.New("idle timeout") // The loop processes messages from c.msgc and timer events. // Other goroutines may examine or modify conn state by sending the loop funcs to execute. func (c *Conn) loop(now time.Time) { - defer close(c.donec) - defer c.tls.Close() - defer c.endpoint.connDrained(c) - defer c.logConnectionClosed() + defer c.cleanup() // The connection timer sends a message to the connection loop on expiry. // We need to give it an expiry when creating it, so set the initial timeout to @@ -346,6 +343,13 @@ func (c *Conn) loop(now time.Time) { } } +func (c *Conn) cleanup() { + c.logConnectionClosed() + c.endpoint.connDrained(c) + c.tls.Close() + close(c.donec) +} + // sendMsg sends a message to the conn's loop. // It does not wait for the message to be processed. // The conn may close before processing the message, in which case it is lost. diff --git a/internal/quic/conn_async_test.go b/internal/quic/conn_async_test.go index dc2a57f9dd..fcc101d190 100644 --- a/internal/quic/conn_async_test.go +++ b/internal/quic/conn_async_test.go @@ -41,7 +41,7 @@ type asyncOp[T any] struct { err error caller string - state *asyncTestState + tc *testConn donec chan struct{} cancelFunc context.CancelFunc } @@ -55,7 +55,7 @@ func (a *asyncOp[T]) cancel() { default: } a.cancelFunc() - <-a.state.notify + <-a.tc.asyncTestState.notify select { case <-a.donec: default: @@ -73,6 +73,7 @@ var errNotDone = errors.New("async op is not done") // control over the progress of operations, an asyncOp can only // become done in reaction to the test taking some action. func (a *asyncOp[T]) result() (v T, err error) { + a.tc.wait() select { case <-a.donec: return a.v, a.err @@ -94,8 +95,8 @@ type asyncContextKey struct{} // The function f should call a blocking function such as // Stream.Write or Conn.AcceptStream and return its result. // It must use the provided context. -func runAsync[T any](ts *testConn, f func(context.Context) (T, error)) *asyncOp[T] { - as := &ts.asyncTestState +func runAsync[T any](tc *testConn, f func(context.Context) (T, error)) *asyncOp[T] { + as := &tc.asyncTestState if as.notify == nil { as.notify = make(chan struct{}) as.mu.Lock() @@ -106,7 +107,7 @@ func runAsync[T any](ts *testConn, f func(context.Context) (T, error)) *asyncOp[ ctx := context.WithValue(context.Background(), asyncContextKey{}, true) ctx, cancel := context.WithCancel(ctx) a := &asyncOp[T]{ - state: as, + tc: tc, caller: fmt.Sprintf("%v:%v", filepath.Base(file), line), donec: make(chan struct{}), cancelFunc: cancel, @@ -116,9 +117,9 @@ func runAsync[T any](ts *testConn, f func(context.Context) (T, error)) *asyncOp[ close(a.donec) as.notify <- struct{}{} }() - ts.t.Cleanup(func() { + tc.t.Cleanup(func() { if _, err := a.result(); err == errNotDone { - ts.t.Errorf("%v: async operation is still executing at end of test", a.caller) + tc.t.Errorf("%v: async operation is still executing at end of test", a.caller) a.cancel() } }) diff --git a/internal/quic/conn_close.go b/internal/quic/conn_close.go index 246a126382..1798d0536f 100644 --- a/internal/quic/conn_close.go +++ b/internal/quic/conn_close.go @@ -71,7 +71,10 @@ func (c *Conn) lifetimeInit() { c.lifetime.donec = make(chan struct{}) } -var errNoPeerResponse = errors.New("peer did not respond to CONNECTION_CLOSE") +var ( + errNoPeerResponse = errors.New("peer did not respond to CONNECTION_CLOSE") + errConnClosed = errors.New("connection closed") +) // advance is called when time passes. func (c *Conn) lifetimeAdvance(now time.Time) (done bool) { @@ -91,13 +94,21 @@ func (c *Conn) lifetimeAdvance(now time.Time) (done bool) { // setState sets the conn state. func (c *Conn) setState(now time.Time, state connState) { + if c.lifetime.state == state { + return + } + c.lifetime.state = state switch state { case connStateClosing, connStateDraining: if c.lifetime.drainEndTime.IsZero() { c.lifetime.drainEndTime = now.Add(3 * c.loss.ptoBasePeriod()) } + case connStateDone: + c.setFinalError(nil) + } + if state != connStateAlive { + c.streamsCleanup() } - c.lifetime.state = state } // confirmHandshake is called when the TLS handshake completes. diff --git a/internal/quic/conn_close_test.go b/internal/quic/conn_close_test.go index 49881e62fd..63d4911e8a 100644 --- a/internal/quic/conn_close_test.go +++ b/internal/quic/conn_close_test.go @@ -216,3 +216,64 @@ func TestConnCloseClosedByEndpoint(t *testing.T) { code: errNo, }) } + +func testConnCloseUnblocks(t *testing.T, f func(context.Context, *testConn) error, opts ...any) { + tc := newTestConn(t, clientSide, opts...) + tc.handshake() + op := runAsync(tc, func(ctx context.Context) (struct{}, error) { + return struct{}{}, f(ctx, tc) + }) + if _, err := op.result(); err != errNotDone { + t.Fatalf("before abort, op = %v, want errNotDone", err) + } + tc.conn.Abort(nil) + if _, err := op.result(); err == nil || err == errNotDone { + t.Fatalf("after abort, op = %v, want error", err) + } +} + +func TestConnCloseUnblocksAcceptStream(t *testing.T) { + testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { + _, err := tc.conn.AcceptStream(ctx) + return err + }, permissiveTransportParameters) +} + +func TestConnCloseUnblocksNewStream(t *testing.T) { + testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { + _, err := tc.conn.NewStream(ctx) + return err + }) +} + +func TestConnCloseUnblocksStreamRead(t *testing.T) { + testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { + s := newLocalStream(t, tc, bidiStream) + buf := make([]byte, 16) + _, err := s.ReadContext(ctx, buf) + return err + }, permissiveTransportParameters) +} + +func TestConnCloseUnblocksStreamWrite(t *testing.T) { + testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { + s := newLocalStream(t, tc, bidiStream) + buf := make([]byte, 32) + _, err := s.WriteContext(ctx, buf) + return err + }, permissiveTransportParameters, func(c *Config) { + c.MaxStreamWriteBufferSize = 16 + }) +} + +func TestConnCloseUnblocksStreamClose(t *testing.T) { + testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { + s := newLocalStream(t, tc, bidiStream) + buf := make([]byte, 16) + _, err := s.WriteContext(ctx, buf) + if err != nil { + return err + } + return s.CloseContext(ctx) + }, permissiveTransportParameters) +} diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index 83ab5554c4..818ec3e577 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -49,6 +49,17 @@ func (c *Conn) streamsInit() { c.inflowInit() } +func (c *Conn) streamsCleanup() { + c.streams.queue.close(errConnClosed) + c.streams.localLimit[bidiStream].connHasClosed() + c.streams.localLimit[uniStream].connHasClosed() + for _, s := range c.streams.streams { + if s != nil { + s.connHasClosed() + } + } +} + // AcceptStream waits for and returns the next stream created by the peer. func (c *Conn) AcceptStream(ctx context.Context) (*Stream, error) { return c.streams.queue.get(ctx, c.testHooks) diff --git a/internal/quic/endpoint_test.go b/internal/quic/endpoint_test.go index f9fc801520..2a6daa0769 100644 --- a/internal/quic/endpoint_test.go +++ b/internal/quic/endpoint_test.go @@ -48,7 +48,7 @@ func TestStreamTransfer(t *testing.T) { } }() - s, err := cli.NewStream(ctx) + s, err := cli.NewSendOnlyStream(ctx) if err != nil { t.Fatalf("NewStream: %v", err) } diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 36c80f6af0..fb9c1cf3c0 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "math" ) type Stream struct { @@ -105,6 +106,11 @@ const ( dataQueue // streamsState.queueData ) +// streamResetByConnClose is assigned to Stream.inresetcode to indicate that a stream +// was implicitly reset when the connection closed. It's out of the range of +// possible reset codes the peer can send. +const streamResetByConnClose = math.MaxInt64 + // wantQueue returns the send queue the stream should be on. func (s streamState) wantQueue() streamQueue { switch { @@ -347,7 +353,15 @@ func (s *Stream) CloseContext(ctx context.Context) error { } s.CloseWrite() // TODO: Return code from peer's RESET_STREAM frame? - return s.conn.waitOnDone(ctx, s.outdone) + if err := s.conn.waitOnDone(ctx, s.outdone); err != nil { + return err + } + s.outgate.lock() + defer s.outUnlock() + if s.outclosed.isReceived() && s.outacked.isrange(0, s.out.end) { + return nil + } + return errors.New("stream reset") } // CloseRead aborts reads on the stream. @@ -437,6 +451,31 @@ func (s *Stream) resetInternal(code uint64, userClosed bool) { s.outblocked.clear() } +// connHasClosed indicates the stream's conn has closed. +func (s *Stream) connHasClosed() { + // If we're in the closing state, the user closed the conn. + // Otherwise, we the peer initiated the close. + // This only matters for the error we're going to return from stream operations. + localClose := s.conn.lifetime.state == connStateClosing + + s.ingate.lock() + if !s.inset.isrange(0, s.insize) && s.inresetcode == -1 { + if localClose { + s.inclosed.set() + } else { + s.inresetcode = streamResetByConnClose + } + } + s.inUnlock() + + s.outgate.lock() + if localClose { + s.outclosed.set() + } + s.outreset.set() + s.outUnlock() +} + // inUnlock unlocks s.ingate. // It sets the gate condition if reads from s will not block. // If s has receive-related frames to write or if both directions diff --git a/internal/quic/stream_limits.go b/internal/quic/stream_limits.go index 2f42cf4180..71cc291351 100644 --- a/internal/quic/stream_limits.go +++ b/internal/quic/stream_limits.go @@ -21,7 +21,7 @@ import ( type localStreamLimits struct { gate gate max int64 // peer-provided MAX_STREAMS - opened int64 // number of streams opened by us + opened int64 // number of streams opened by us, -1 when conn is closed } func (lim *localStreamLimits) init() { @@ -34,10 +34,21 @@ func (lim *localStreamLimits) open(ctx context.Context, c *Conn) (num int64, err if err := lim.gate.waitAndLock(ctx, c.testHooks); err != nil { return 0, err } - n := lim.opened + if lim.opened < 0 { + lim.gate.unlock(true) + return 0, errConnClosed + } + num = lim.opened lim.opened++ lim.gate.unlock(lim.opened < lim.max) - return n, nil + return num, nil +} + +// connHasClosed indicates the connection has been closed, locally or by the peer. +func (lim *localStreamLimits) connHasClosed() { + lim.gate.lock() + lim.opened = -1 + lim.gate.unlock(true) } // setMax sets the MAX_STREAMS provided by the peer. diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 93c8839fff..00e392dbab 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -1047,11 +1047,13 @@ func TestStreamCloseUnblocked(t *testing.T) { for _, test := range []struct { name string unblock func(tc *testConn, s *Stream) + success bool }{{ name: "data received", unblock: func(tc *testConn, s *Stream) { tc.writeAckForAll() }, + success: true, }, { name: "stop sending received", unblock: func(tc *testConn, s *Stream) { @@ -1094,7 +1096,13 @@ func TestStreamCloseUnblocked(t *testing.T) { t.Fatalf("s.CloseContext() = %v, want it to block waiting for acks", err) } test.unblock(tc, s) - if _, err := closing.result(); err != nil { + _, err := closing.result() + switch { + case err == errNotDone: + t.Fatalf("s.CloseContext() still blocking; want it to have returned") + case err == nil && !test.success: + t.Fatalf("s.CloseContext() = nil, want error") + case err != nil && test.success: t.Fatalf("s.CloseContext() = %v, want nil (all data acked)", err) } }) @@ -1390,31 +1398,41 @@ func newTestConnAndStream(t *testing.T, side connSide, sside streamSide, styp st func newTestConnAndLocalStream(t *testing.T, side connSide, styp streamType, opts ...any) (*testConn, *Stream) { t.Helper() - ctx := canceledContext() tc := newTestConn(t, side, opts...) tc.handshake() tc.ignoreFrame(frameTypeAck) + return tc, newLocalStream(t, tc, styp) +} + +func newLocalStream(t *testing.T, tc *testConn, styp streamType) *Stream { + t.Helper() + ctx := canceledContext() s, err := tc.conn.newLocalStream(ctx, styp) if err != nil { t.Fatalf("conn.newLocalStream(%v) = %v", styp, err) } - return tc, s + return s } func newTestConnAndRemoteStream(t *testing.T, side connSide, styp streamType, opts ...any) (*testConn, *Stream) { t.Helper() - ctx := canceledContext() tc := newTestConn(t, side, opts...) tc.handshake() tc.ignoreFrame(frameTypeAck) + return tc, newRemoteStream(t, tc, styp) +} + +func newRemoteStream(t *testing.T, tc *testConn, styp streamType) *Stream { + t.Helper() + ctx := canceledContext() tc.writeFrames(packetType1RTT, debugFrameStream{ - id: newStreamID(side.peer(), styp, 0), + id: newStreamID(tc.conn.side.peer(), styp, 0), }) s, err := tc.conn.AcceptStream(ctx) if err != nil { t.Fatalf("conn.AcceptStream() = %v", err) } - return tc, s + return s } // permissiveTransportParameters may be passed as an option to newTestConn. From 65efbad9474a514a2f3c08716b8cf38011fa2736 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 5 Dec 2023 15:05:16 -0800 Subject: [PATCH 109/168] quic: avoid leaking tls goroutines in tests Change-Id: Iaf273294ba3245bfeb387a72e068c048d0fcf93a Reviewed-on: https://go-review.googlesource.com/c/net/+/547736 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_test.go | 3 +++ internal/quic/main_test.go | 52 +++++++++++++++++++++++++++++++++++++ internal/quic/retry_test.go | 4 ++- 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 internal/quic/main_test.go diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index c57ba1487c..b48bee803a 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -279,6 +279,9 @@ func newTestConnForConn(t *testing.T, endpoint *testEndpoint, conn *Conn) *testC } tc.peerTLSConn.SetTransportParameters(marshalTransportParameters(peerProvidedParams)) tc.peerTLSConn.Start(context.Background()) + t.Cleanup(func() { + tc.peerTLSConn.Close() + }) return tc } diff --git a/internal/quic/main_test.go b/internal/quic/main_test.go new file mode 100644 index 0000000000..5ad0042fa1 --- /dev/null +++ b/internal/quic/main_test.go @@ -0,0 +1,52 @@ +// 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" + "fmt" + "os" + "runtime" + "testing" + "time" +) + +func TestMain(m *testing.M) { + defer os.Exit(m.Run()) + + // Look for leaked goroutines. + // + // Checking after every test makes it easier to tell which test is the culprit, + // but checking once at the end is faster and less likely to miss something. + start := time.Now() + warned := false + for { + buf := make([]byte, 2<<20) + buf = buf[:runtime.Stack(buf, true)] + leaked := false + for _, g := range bytes.Split(buf, []byte("\n\n")) { + if bytes.Contains(g, []byte("quic.TestMain")) || + bytes.Contains(g, []byte("created by os/signal.Notify")) || + bytes.Contains(g, []byte("gotraceback_test.go")) { + continue + } + leaked = true + } + if !leaked { + break + } + if !warned && time.Since(start) > 1*time.Second { + // Print a warning quickly, in case this is an interactive session. + // Keep waiting until the test times out, in case this is a slow trybot. + fmt.Printf("Tests seem to have leaked some goroutines, still waiting.\n\n") + fmt.Print(string(buf)) + warned = true + } + // Goroutines might still be shutting down. + time.Sleep(1 * time.Millisecond) + } +} diff --git a/internal/quic/retry_test.go b/internal/quic/retry_test.go index 4a21a4ca1d..8f36e1bd3e 100644 --- a/internal/quic/retry_test.go +++ b/internal/quic/retry_test.go @@ -533,7 +533,9 @@ func initialClientCrypto(t *testing.T, e *testEndpoint, p transportParameters) [ tlsClient := tls.QUICClient(config) tlsClient.SetTransportParameters(marshalTransportParameters(p)) tlsClient.Start(context.Background()) - //defer tlsClient.Close() + t.Cleanup(func() { + tlsClient.Close() + }) e.peerTLSConn = tlsClient var data []byte for { From 577e44a5cee023bd639dd2dcc4008644bcb71472 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 8 Dec 2023 07:44:43 -0800 Subject: [PATCH 110/168] quic: skip leaked goroutine check on GOOS=js Fixes golang/go#64620 Change-Id: I3b5ff4d1e1132a47b7cc7eb00861e9f7b76f8764 Reviewed-on: https://go-review.googlesource.com/c/net/+/548455 Auto-Submit: Damien Neil Reviewed-by: Bryan Mills LUCI-TryBot-Result: Go LUCI --- internal/quic/main_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/quic/main_test.go b/internal/quic/main_test.go index 5ad0042fa1..ecd0b1e9f6 100644 --- a/internal/quic/main_test.go +++ b/internal/quic/main_test.go @@ -22,6 +22,11 @@ func TestMain(m *testing.M) { // // Checking after every test makes it easier to tell which test is the culprit, // but checking once at the end is faster and less likely to miss something. + if runtime.GOOS == "js" { + // The js-wasm runtime creates an additional background goroutine. + // Just skip the leak check there. + return + } start := time.Now() warned := false for { From b952594c266f3a75031e9ba2b43483a735526d39 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 20 Nov 2023 15:52:36 -0800 Subject: [PATCH 111/168] quic: fix data race in connection close We were failing to hold streamsState.streamsMu when removing a closed stream from the conn's stream map. Rework this to remove the mutex entirely. The only access to the map that isn't on the conn's loop is during stream creation. Send a message to the loop to register the stream instead of using a mutex. Change-Id: I2e87089e87c61a6ade8219dfb8acec3809bf95de Reviewed-on: https://go-review.googlesource.com/c/net/+/545217 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn.go | 31 +++++++++++++++++++-- internal/quic/conn_async_test.go | 1 + internal/quic/conn_streams.go | 20 ++++++-------- internal/quic/conn_streams_test.go | 44 ++++++++++++++++++++++++++++++ internal/quic/conn_test.go | 19 ++++++++----- 5 files changed, 93 insertions(+), 22 deletions(-) diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 4abc74030c..6d79013eb4 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -369,12 +369,37 @@ func (c *Conn) wake() { } // runOnLoop executes a function within the conn's loop goroutine. -func (c *Conn) runOnLoop(f func(now time.Time, c *Conn)) error { +func (c *Conn) runOnLoop(ctx context.Context, f func(now time.Time, c *Conn)) error { donec := make(chan struct{}) - c.sendMsg(func(now time.Time, c *Conn) { + msg := func(now time.Time, c *Conn) { defer close(donec) f(now, c) - }) + } + if c.testHooks != nil { + // In tests, we can't rely on being able to send a message immediately: + // c.msgc might be full, and testConnHooks.nextMessage might be waiting + // for us to block before it processes the next message. + // To avoid a deadlock, we send the message in waitUntil. + // If msgc is empty, the message is buffered. + // If msgc is full, we block and let nextMessage process the queue. + msgc := c.msgc + c.testHooks.waitUntil(ctx, func() bool { + for { + select { + case msgc <- msg: + msgc = nil // send msg only once + case <-donec: + return true + case <-c.donec: + return true + default: + return false + } + } + }) + } else { + c.sendMsg(msg) + } select { case <-donec: case <-c.donec: diff --git a/internal/quic/conn_async_test.go b/internal/quic/conn_async_test.go index fcc101d190..4671f8340e 100644 --- a/internal/quic/conn_async_test.go +++ b/internal/quic/conn_async_test.go @@ -125,6 +125,7 @@ func runAsync[T any](tc *testConn, f func(context.Context) (T, error)) *asyncOp[ }) // Wait for the operation to either finish or block. <-as.notify + tc.wait() return a } diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index 818ec3e577..dc82f8b0f5 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -14,10 +14,8 @@ import ( ) type streamsState struct { - queue queue[*Stream] // new, peer-created streams - - streamsMu sync.Mutex - streams map[streamID]*Stream + queue queue[*Stream] // new, peer-created streams + streams map[streamID]*Stream // Limits on the number of streams, indexed by streamType. localLimit [streamTypeCount]localStreamLimits @@ -82,9 +80,6 @@ func (c *Conn) NewSendOnlyStream(ctx context.Context) (*Stream, error) { } func (c *Conn) newLocalStream(ctx context.Context, styp streamType) (*Stream, error) { - c.streams.streamsMu.Lock() - defer c.streams.streamsMu.Unlock() - num, err := c.streams.localLimit[styp].open(ctx, c) if err != nil { return nil, err @@ -100,7 +95,12 @@ func (c *Conn) newLocalStream(ctx context.Context, styp streamType) (*Stream, er s.inUnlock() s.outUnlock() - c.streams.streams[s.id] = s + // Modify c.streams on the conn's loop. + if err := c.runOnLoop(ctx, func(now time.Time, c *Conn) { + c.streams.streams[s.id] = s + }); err != nil { + return nil, err + } return s, nil } @@ -119,8 +119,6 @@ const ( // streamForID returns the stream with the given id. // If the stream does not exist, it returns nil. func (c *Conn) streamForID(id streamID) *Stream { - c.streams.streamsMu.Lock() - defer c.streams.streamsMu.Unlock() return c.streams.streams[id] } @@ -146,8 +144,6 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) } } - c.streams.streamsMu.Lock() - defer c.streams.streamsMu.Unlock() s, isOpen := c.streams.streams[id] if s != nil { return s diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go index c90354db8a..90f5cb75c3 100644 --- a/internal/quic/conn_streams_test.go +++ b/internal/quic/conn_streams_test.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "math" + "sync" "testing" ) @@ -478,3 +479,46 @@ func TestStreamsCreateAndCloseRemote(t *testing.T) { t.Fatalf("after test, stream send queue is not empty; should be") } } + +func TestStreamsCreateConcurrency(t *testing.T) { + cli, srv := newLocalConnPair(t, &Config{}, &Config{}) + + srvdone := make(chan int) + go func() { + defer close(srvdone) + for streams := 0; ; streams++ { + s, err := srv.AcceptStream(context.Background()) + if err != nil { + srvdone <- streams + return + } + s.Close() + } + }() + + var wg sync.WaitGroup + const concurrency = 10 + const streams = 10 + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < streams; j++ { + s, err := cli.NewStream(context.Background()) + if err != nil { + t.Errorf("NewStream: %v", err) + return + } + s.Flush() + s.Close() + } + }() + } + wg.Wait() + + cli.Abort(nil) + srv.Abort(nil) + if got, want := <-srvdone, concurrency*streams; got != want { + t.Errorf("accepted %v streams, want %v", got, want) + } +} diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index b48bee803a..058aa7edc2 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -30,10 +30,12 @@ func TestConnTestConn(t *testing.T) { t.Errorf("new conn timeout=%v, want %v (max_idle_timeout)", got, want) } - var ranAt time.Time - tc.conn.runOnLoop(func(now time.Time, c *Conn) { - ranAt = now - }) + ranAt, _ := runAsync(tc, func(ctx context.Context) (when time.Time, _ error) { + tc.conn.runOnLoop(ctx, func(now time.Time, c *Conn) { + when = now + }) + return + }).result() if !ranAt.Equal(tc.endpoint.now) { t.Errorf("func ran on loop at %v, want %v", ranAt, tc.endpoint.now) } @@ -41,9 +43,12 @@ func TestConnTestConn(t *testing.T) { nextTime := tc.endpoint.now.Add(defaultMaxIdleTimeout / 2) tc.advanceTo(nextTime) - tc.conn.runOnLoop(func(now time.Time, c *Conn) { - ranAt = now - }) + ranAt, _ = runAsync(tc, func(ctx context.Context) (when time.Time, _ error) { + tc.conn.runOnLoop(ctx, func(now time.Time, c *Conn) { + when = now + }) + return + }).result() if !ranAt.Equal(nextTime) { t.Errorf("func ran on loop at %v, want %v", ranAt, nextTime) } From b0eb4d6c942abf81c513c88af3ea23aaaaa5a4e0 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 27 Nov 2023 16:42:25 -0800 Subject: [PATCH 112/168] quic: compute pnum len from max ack received, not sent QUIC packet numbers are truncated to include only the least significant bits of the packet number. The number of bits which must be retained is computed based on the largest packet number known to have been received by the peer. See RFC 9000, section 17.1. We were incorrectly using the largest packet number we have received *from* the peer. Oops. (Test infrastructure change: Include the header byte in the testPacket structure, so we can see how many bytes the packet number was encoded with. Ignore this byte when comparing packets.) Change-Id: Iec17c69f007f8b39d14d24b0ca216c6a0018ae22 Reviewed-on: https://go-review.googlesource.com/c/net/+/545575 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_send.go | 6 ++--- internal/quic/conn_send_test.go | 43 +++++++++++++++++++++++++++++++++ internal/quic/conn_test.go | 15 ++++++++++-- internal/quic/endpoint_test.go | 3 +-- internal/quic/tls_test.go | 3 +-- 5 files changed, 61 insertions(+), 9 deletions(-) diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index a8d9308989..c2d8d146b9 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -60,7 +60,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { pad := false var sentInitial *sentPacket if c.keysInitial.canWrite() { - pnumMaxAcked := c.acks[initialSpace].largestSeen() + pnumMaxAcked := c.loss.spaces[initialSpace].maxAcked pnum := c.loss.nextNumber(initialSpace) p := longPacket{ ptype: packetTypeInitial, @@ -93,7 +93,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // Handshake packet. if c.keysHandshake.canWrite() { - pnumMaxAcked := c.acks[handshakeSpace].largestSeen() + pnumMaxAcked := c.loss.spaces[handshakeSpace].maxAcked pnum := c.loss.nextNumber(handshakeSpace) p := longPacket{ ptype: packetTypeHandshake, @@ -124,7 +124,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // 1-RTT packet. if c.keysAppData.canWrite() { - pnumMaxAcked := c.acks[appDataSpace].largestSeen() + pnumMaxAcked := c.loss.spaces[appDataSpace].maxAcked pnum := c.loss.nextNumber(appDataSpace) c.w.start1RTTPacket(pnum, pnumMaxAcked, dstConnID) c.appendFrames(now, appDataSpace, pnum, limit) diff --git a/internal/quic/conn_send_test.go b/internal/quic/conn_send_test.go index 822783c416..2205ff2f79 100644 --- a/internal/quic/conn_send_test.go +++ b/internal/quic/conn_send_test.go @@ -38,3 +38,46 @@ func TestAckElicitingAck(t *testing.T) { } t.Errorf("after sending %v PINGs, got no ack-eliciting response", count) } + +func TestSendPacketNumberSize(t *testing.T) { + tc := newTestConn(t, clientSide, permissiveTransportParameters) + tc.handshake() + + recvPing := func() *testPacket { + t.Helper() + tc.conn.ping(appDataSpace) + p := tc.readPacket() + if p == nil { + t.Fatalf("want packet containing PING, got none") + } + return p + } + + // Desynchronize the packet numbers the conn is sending and the ones it is receiving, + // by having the conn send a number of unacked packets. + for i := 0; i < 16; i++ { + recvPing() + } + + // Establish the maximum packet number the conn has received an ACK for. + maxAcked := recvPing().num + tc.writeAckForAll() + + // Make the conn send a sequence of packets. + // Check that the packet number is encoded with two bytes once the difference between the + // current packet and the max acked one is sufficiently large. + for want := maxAcked + 1; want < maxAcked+0x100; want++ { + p := recvPing() + if p.num != want { + t.Fatalf("received packet number %v, want %v", p.num, want) + } + gotPnumLen := int(p.header&0x03) + 1 + wantPnumLen := 1 + if p.num-maxAcked >= 0x80 { + wantPnumLen = 2 + } + if gotPnumLen != wantPnumLen { + t.Fatalf("packet number 0x%x encoded with %v bytes, want %v (max acked = %v)", p.num, gotPnumLen, wantPnumLen, maxAcked) + } + } +} diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 058aa7edc2..abf7eede70 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -82,6 +82,7 @@ func (d testDatagram) String() string { type testPacket struct { ptype packetType + header byte version uint32 num packetNumber keyPhaseBit bool @@ -599,12 +600,18 @@ func (tc *testConn) readFrame() (debugFrame, packetType) { func (tc *testConn) wantDatagram(expectation string, want *testDatagram) { tc.t.Helper() got := tc.readDatagram() - if !reflect.DeepEqual(got, want) { + if !datagramEqual(got, want) { tc.t.Fatalf("%v:\ngot datagram: %v\nwant datagram: %v", expectation, got, want) } } func datagramEqual(a, b *testDatagram) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } if a.paddedSize != b.paddedSize || a.addr != b.addr || len(a.packets) != len(b.packets) { @@ -622,7 +629,7 @@ func datagramEqual(a, b *testDatagram) bool { func (tc *testConn) wantPacket(expectation string, want *testPacket) { tc.t.Helper() got := tc.readPacket() - if !reflect.DeepEqual(got, want) { + if !packetEqual(got, want) { tc.t.Fatalf("%v:\ngot packet: %v\nwant packet: %v", expectation, got, want) } } @@ -630,8 +637,10 @@ func (tc *testConn) wantPacket(expectation string, want *testPacket) { func packetEqual(a, b *testPacket) bool { ac := *a ac.frames = nil + ac.header = 0 bc := *b bc.frames = nil + bc.header = 0 if !reflect.DeepEqual(ac, bc) { return false } @@ -839,6 +848,7 @@ func parseTestDatagram(t *testing.T, te *testEndpoint, tc *testConn, buf []byte) } d.packets = append(d.packets, &testPacket{ ptype: p.ptype, + header: buf[0], version: p.version, num: p.num, dstConnID: p.dstConnID, @@ -880,6 +890,7 @@ func parseTestDatagram(t *testing.T, te *testEndpoint, tc *testConn, buf []byte) } d.packets = append(d.packets, &testPacket{ ptype: packetType1RTT, + header: hdr[0], num: pnum, dstConnID: hdr[1:][:len(tc.peerConnID)], keyPhaseBit: hdr[0]&keyPhaseBit != 0, diff --git a/internal/quic/endpoint_test.go b/internal/quic/endpoint_test.go index 2a6daa0769..452d26052c 100644 --- a/internal/quic/endpoint_test.go +++ b/internal/quic/endpoint_test.go @@ -13,7 +13,6 @@ import ( "io" "net" "net/netip" - "reflect" "testing" "time" ) @@ -242,7 +241,7 @@ func (te *testEndpoint) readDatagram() *testDatagram { func (te *testEndpoint) wantDatagram(expectation string, want *testDatagram) { te.t.Helper() got := te.readDatagram() - if !reflect.DeepEqual(got, want) { + if !datagramEqual(got, want) { te.t.Fatalf("%v:\ngot datagram: %v\nwant datagram: %v", expectation, got, want) } } diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go index 14f74a00aa..9c1dd364ec 100644 --- a/internal/quic/tls_test.go +++ b/internal/quic/tls_test.go @@ -10,7 +10,6 @@ import ( "crypto/tls" "crypto/x509" "errors" - "reflect" "testing" "time" ) @@ -56,7 +55,7 @@ func (tc *testConn) handshake() { fillCryptoFrames(want, tc.cryptoDataOut) i++ } - if !reflect.DeepEqual(got, want) { + if !datagramEqual(got, want) { t.Fatalf("dgram %v:\ngot %v\n\nwant %v", i, got, want) } if i >= len(dgrams) { From 1e59a7e58ce15106ab0248605c5de0701624072b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 28 Nov 2023 09:17:02 -0800 Subject: [PATCH 113/168] quic/qlog: correctly write negative durations "-10.000001", not "10.-000001". Change-Id: I84f6487bad15ab3a190e73e655236376b1781e85 Reviewed-on: https://go-review.googlesource.com/c/net/+/545576 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/qlog/json_writer.go | 4 ++++ internal/quic/qlog/json_writer_test.go | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/quic/qlog/json_writer.go b/internal/quic/qlog/json_writer.go index 3950ab42f6..b2fa3e03e5 100644 --- a/internal/quic/qlog/json_writer.go +++ b/internal/quic/qlog/json_writer.go @@ -157,6 +157,10 @@ func (w *jsonWriter) writeBoolField(name string, v bool) { // writeDuration writes a duration as milliseconds. func (w *jsonWriter) writeDuration(v time.Duration) { + if v < 0 { + w.buf.WriteByte('-') + v = -v + } fmt.Fprintf(&w.buf, "%d.%06d", v.Milliseconds(), v%time.Millisecond) } diff --git a/internal/quic/qlog/json_writer_test.go b/internal/quic/qlog/json_writer_test.go index 7ba5e17378..6da5566412 100644 --- a/internal/quic/qlog/json_writer_test.go +++ b/internal/quic/qlog/json_writer_test.go @@ -124,9 +124,10 @@ func TestJSONWriterBoolField(t *testing.T) { func TestJSONWriterDurationField(t *testing.T) { w := newTestJSONWriter() w.writeRecordStart() - w.writeDurationField("field", (10*time.Millisecond)+(2*time.Nanosecond)) + w.writeDurationField("field1", (10*time.Millisecond)+(2*time.Nanosecond)) + w.writeDurationField("field2", -((10 * time.Millisecond) + (2 * time.Nanosecond))) w.writeRecordEnd() - wantJSONRecord(t, w, `{"field":10.000002}`) + wantJSONRecord(t, w, `{"field1":10.000002,"field2":-10.000002}`) } func TestJSONWriterFloat64Field(t *testing.T) { From 2b416c3c961a9829f7ca97dd44690e71719f68f2 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 28 Nov 2023 09:20:32 -0800 Subject: [PATCH 114/168] quic/qlog: create log files with O_EXCL Avoid confusing log corruption if two loggers try to write to the same file simultaneously. Change-Id: I3bfbcf56aa55c778ada0178d7c662c414878c9d1 Reviewed-on: https://go-review.googlesource.com/c/net/+/545577 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/qlog/qlog.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/quic/qlog/qlog.go b/internal/quic/qlog/qlog.go index 0e71d71aae..e54c839f0a 100644 --- a/internal/quic/qlog/qlog.go +++ b/internal/quic/qlog/qlog.go @@ -180,7 +180,7 @@ func newTraceWriter(opts HandlerOptions, info TraceInfo) (io.WriteCloser, error) if !filepath.IsLocal(filename) { return nil, errors.New("invalid trace filename") } - w, err = os.Create(filepath.Join(opts.Dir, filename)) + w, err = os.OpenFile(filepath.Join(opts.Dir, filename), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0666) } else { err = errors.New("no log destination") } From c337daf7db6b2f45306e9b972588478201259c0d Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 28 Nov 2023 09:19:54 -0800 Subject: [PATCH 115/168] quic: enable qlog output in tests Set QLOG=/some/dir to enable qlog logging in tests. Change-Id: Id4006c66fd555ad0ca47914d0af9f9ab46467c9c Reviewed-on: https://go-review.googlesource.com/c/net/+/550796 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_test.go | 12 +++++++++++- internal/quic/endpoint_test.go | 9 +++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index abf7eede70..ddf0740e22 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -13,15 +13,21 @@ import ( "errors" "flag" "fmt" + "log/slog" "math" "net/netip" "reflect" "strings" "testing" "time" + + "golang.org/x/net/internal/quic/qlog" ) -var testVV = flag.Bool("vv", false, "even more verbose test output") +var ( + testVV = flag.Bool("vv", false, "even more verbose test output") + qlogdir = flag.String("qlog", "", "write qlog logs to directory") +) func TestConnTestConn(t *testing.T) { tc := newTestConn(t, serverSide) @@ -199,6 +205,10 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { config := &Config{ TLSConfig: newTestTLSConfig(side), StatelessResetKey: testStatelessResetKey, + QLogLogger: slog.New(qlog.NewJSONHandler(qlog.HandlerOptions{ + Level: QLogLevelFrame, + Dir: *qlogdir, + })), } var cids newServerConnIDs if side == serverSide { diff --git a/internal/quic/endpoint_test.go b/internal/quic/endpoint_test.go index 452d26052c..ab6cd1cf51 100644 --- a/internal/quic/endpoint_test.go +++ b/internal/quic/endpoint_test.go @@ -11,10 +11,13 @@ import ( "context" "crypto/tls" "io" + "log/slog" "net" "net/netip" "testing" "time" + + "golang.org/x/net/internal/quic/qlog" ) func TestConnect(t *testing.T) { @@ -83,6 +86,12 @@ func newLocalEndpoint(t *testing.T, side connSide, conf *Config) *Endpoint { conf = &newConf conf.TLSConfig = newTestTLSConfig(side) } + if conf.QLogLogger == nil { + conf.QLogLogger = slog.New(qlog.NewJSONHandler(qlog.HandlerOptions{ + Level: QLogLevelFrame, + Dir: *qlogdir, + })) + } e, err := Listen("udp", "127.0.0.1:0", conf) if err != nil { t.Fatal(err) From f9726a9e4a0fba67ce78802b47601ba194d15b3f Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 11 Dec 2023 13:54:56 -0800 Subject: [PATCH 116/168] quic: fix packet size logging The qlog schema puts packet sizes as part of a "raw" field of type RawInfo, not in the packet_sent/packet_received event. Move to the correct location. Change-Id: I4308d4bdb961cf83e29af014b60f50ed029cb915 Reviewed-on: https://go-review.googlesource.com/c/net/+/550797 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_send.go | 6 +++--- internal/quic/packet_writer.go | 5 +++++ internal/quic/qlog.go | 15 ++++++++++----- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index c2d8d146b9..ccb467591b 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -76,7 +76,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { logSentPacket(c, packetTypeInitial, pnum, p.srcConnID, p.dstConnID, c.w.payload()) } if c.logEnabled(QLogLevelPacket) && len(c.w.payload()) > 0 { - c.logPacketSent(packetTypeInitial, pnum, p.srcConnID, p.dstConnID, c.w.payload()) + c.logPacketSent(packetTypeInitial, pnum, p.srcConnID, p.dstConnID, c.w.packetLen(), c.w.payload()) } sentInitial = c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysInitial.w, p) if sentInitial != nil { @@ -108,7 +108,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { logSentPacket(c, packetTypeHandshake, pnum, p.srcConnID, p.dstConnID, c.w.payload()) } if c.logEnabled(QLogLevelPacket) && len(c.w.payload()) > 0 { - c.logPacketSent(packetTypeHandshake, pnum, p.srcConnID, p.dstConnID, c.w.payload()) + c.logPacketSent(packetTypeHandshake, pnum, p.srcConnID, p.dstConnID, c.w.packetLen(), c.w.payload()) } if sent := c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysHandshake.w, p); sent != nil { c.idleHandlePacketSent(now, sent) @@ -139,7 +139,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { logSentPacket(c, packetType1RTT, pnum, nil, dstConnID, c.w.payload()) } if c.logEnabled(QLogLevelPacket) && len(c.w.payload()) > 0 { - c.logPacketSent(packetType1RTT, pnum, nil, dstConnID, c.w.payload()) + c.logPacketSent(packetType1RTT, pnum, nil, dstConnID, c.w.packetLen(), c.w.payload()) } if sent := c.w.finish1RTTPacket(pnum, pnumMaxAcked, dstConnID, &c.keysAppData); sent != nil { c.idleHandlePacketSent(now, sent) diff --git a/internal/quic/packet_writer.go b/internal/quic/packet_writer.go index 0c2b2ee41e..b4e54ce4b0 100644 --- a/internal/quic/packet_writer.go +++ b/internal/quic/packet_writer.go @@ -47,6 +47,11 @@ func (w *packetWriter) datagram() []byte { return w.b } +// packet returns the size of the current packet. +func (w *packetWriter) packetLen() int { + return len(w.b[w.pktOff:]) + aeadOverhead +} + // payload returns the payload of the current packet. func (w *packetWriter) payload() []byte { return w.b[w.payOff:] diff --git a/internal/quic/qlog.go b/internal/quic/qlog.go index fea8b38eef..82ad92ac8c 100644 --- a/internal/quic/qlog.go +++ b/internal/quic/qlog.go @@ -148,8 +148,6 @@ func (c *Conn) logConnectionClosed() { } func (c *Conn) logLongPacketReceived(p longPacket, pkt []byte) { - pnumLen := 1 + int(pkt[0]&0x03) - length := pnumLen + len(p.payload) var frames slog.Attr if c.logEnabled(QLogLevelFrame) { frames = c.packetFramesAttr(p.payload) @@ -162,7 +160,9 @@ func (c *Conn) logLongPacketReceived(p longPacket, pkt []byte) { slog.Uint64("flags", uint64(pkt[0])), slogHexstring("scid", p.srcConnID), slogHexstring("dcid", p.dstConnID), - slog.Int("length", length), + ), + slog.Group("raw", + slog.Int("length", len(pkt)), ), frames, ) @@ -180,14 +180,16 @@ func (c *Conn) log1RTTPacketReceived(p shortPacket, pkt []byte) { slog.String("packet_type", packetType1RTT.qlogString()), slog.Uint64("packet_number", uint64(p.num)), slog.Uint64("flags", uint64(pkt[0])), - slog.String("scid", ""), slogHexstring("dcid", dstConnID), ), + slog.Group("raw", + slog.Int("length", len(pkt)), + ), frames, ) } -func (c *Conn) logPacketSent(ptype packetType, pnum packetNumber, src, dst, payload []byte) { +func (c *Conn) logPacketSent(ptype packetType, pnum packetNumber, src, dst []byte, pktLen int, payload []byte) { var frames slog.Attr if c.logEnabled(QLogLevelFrame) { frames = c.packetFramesAttr(payload) @@ -204,6 +206,9 @@ func (c *Conn) logPacketSent(ptype packetType, pnum packetNumber, src, dst, payl scid, slogHexstring("dcid", dst), ), + slog.Group("raw", + slog.Int("length", pktLen), + ), frames, ) } From c136d0c937afa54dca414a69603bb1570a28879f Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 19 Dec 2023 09:01:27 -0800 Subject: [PATCH 117/168] quic: avoid panic when PTO expires and implicitly-created streams exist The streams map contains nil entries for implicitly-created streams. (Receiving a packet for stream N implicitly creates all streams of the same type LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_streams.go | 45 ++++++++++++++++++++---------- internal/quic/conn_streams_test.go | 35 +++++++++++++++++++++++ 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/internal/quic/conn_streams.go b/internal/quic/conn_streams.go index dc82f8b0f5..87cfd297ed 100644 --- a/internal/quic/conn_streams.go +++ b/internal/quic/conn_streams.go @@ -14,8 +14,16 @@ import ( ) type streamsState struct { - queue queue[*Stream] // new, peer-created streams - streams map[streamID]*Stream + queue queue[*Stream] // new, peer-created streams + + // All peer-created streams. + // + // Implicitly created streams are included as an empty entry in the map. + // (For example, if we receive a frame for stream 4, we implicitly create stream 0 and + // insert an empty entry for it to the map.) + // + // The map value is maybeStream rather than *Stream as a reminder that values can be nil. + streams map[streamID]maybeStream // Limits on the number of streams, indexed by streamType. localLimit [streamTypeCount]localStreamLimits @@ -37,8 +45,13 @@ type streamsState struct { queueData streamRing // streams with only flow-controlled frames } +// maybeStream is a possibly nil *Stream. See streamsState.streams. +type maybeStream struct { + s *Stream +} + func (c *Conn) streamsInit() { - c.streams.streams = make(map[streamID]*Stream) + c.streams.streams = make(map[streamID]maybeStream) c.streams.queue = newQueue[*Stream]() c.streams.localLimit[bidiStream].init() c.streams.localLimit[uniStream].init() @@ -52,8 +65,8 @@ func (c *Conn) streamsCleanup() { c.streams.localLimit[bidiStream].connHasClosed() c.streams.localLimit[uniStream].connHasClosed() for _, s := range c.streams.streams { - if s != nil { - s.connHasClosed() + if s.s != nil { + s.s.connHasClosed() } } } @@ -97,7 +110,7 @@ func (c *Conn) newLocalStream(ctx context.Context, styp streamType) (*Stream, er // Modify c.streams on the conn's loop. if err := c.runOnLoop(ctx, func(now time.Time, c *Conn) { - c.streams.streams[s.id] = s + c.streams.streams[s.id] = maybeStream{s} }); err != nil { return nil, err } @@ -119,7 +132,7 @@ const ( // streamForID returns the stream with the given id. // If the stream does not exist, it returns nil. func (c *Conn) streamForID(id streamID) *Stream { - return c.streams.streams[id] + return c.streams.streams[id].s } // streamForFrame returns the stream with the given id. @@ -144,9 +157,9 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) } } - s, isOpen := c.streams.streams[id] - if s != nil { - return s + ms, isOpen := c.streams.streams[id] + if ms.s != nil { + return ms.s } num := id.num() @@ -183,10 +196,10 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) // with the same initiator and type and a lower number. // Add a nil entry to the streams map for each implicitly created stream. for n := newStreamID(id.initiator(), id.streamType(), prevOpened); n < id; n += 4 { - c.streams.streams[n] = nil + c.streams.streams[n] = maybeStream{} } - s = newStream(c, id) + s := newStream(c, id) s.inmaxbuf = c.config.maxStreamReadBufferSize() s.inwin = c.config.maxStreamReadBufferSize() if id.streamType() == bidiStream { @@ -196,7 +209,7 @@ func (c *Conn) streamForFrame(now time.Time, id streamID, ftype streamFrameType) s.inUnlock() s.outUnlock() - c.streams.streams[id] = s + c.streams.streams[id] = maybeStream{s} c.streams.queue.put(s) return s } @@ -400,7 +413,11 @@ func (c *Conn) appendStreamFramesPTO(w *packetWriter, pnum packetNumber) bool { c.streams.sendMu.Lock() defer c.streams.sendMu.Unlock() const pto = true - for _, s := range c.streams.streams { + for _, ms := range c.streams.streams { + s := ms.s + if s == nil { + continue + } const pto = true s.ingate.lock() inOK := s.appendInFramesLocked(w, pnum, pto) diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go index 90f5cb75c3..fb9af47eb4 100644 --- a/internal/quic/conn_streams_test.go +++ b/internal/quic/conn_streams_test.go @@ -522,3 +522,38 @@ func TestStreamsCreateConcurrency(t *testing.T) { t.Errorf("accepted %v streams, want %v", got, want) } } + +func TestStreamsPTOWithImplicitStream(t *testing.T) { + ctx := canceledContext() + tc := newTestConn(t, serverSide, permissiveTransportParameters) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + + // Peer creates stream 1, and implicitly creates stream 0. + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, bidiStream, 1), + }) + + // We accept stream 1 and write data to it. + data := []byte("data") + s, err := tc.conn.AcceptStream(ctx) + if err != nil { + t.Fatalf("conn.AcceptStream() = %v, want stream", err) + } + s.Write(data) + s.Flush() + tc.wantFrame("data written to stream", + packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, bidiStream, 1), + data: data, + }) + + // PTO expires, and the data is resent. + const pto = true + tc.triggerLossOrPTO(packetType1RTT, true) + tc.wantFrame("data resent after PTO expires", + packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, bidiStream, 1), + data: data, + }) +} From f12db26b1c9293fa3eb95c936e548d2c1fba4ba9 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 19 Dec 2023 11:33:13 -0800 Subject: [PATCH 118/168] internal/quic/cmd/interop: use wget --no-verbose in Dockerfile Pass --no-verbose to wget to avoid spamming the build logs with progress indicators. Change-Id: I36a0b91f8dac09cc4055c5d5db3fc61c9b269d6e Reviewed-on: https://go-review.googlesource.com/c/net/+/551495 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/cmd/interop/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/quic/cmd/interop/Dockerfile b/internal/quic/cmd/interop/Dockerfile index 4b52e5356d..b60999a862 100644 --- a/internal/quic/cmd/interop/Dockerfile +++ b/internal/quic/cmd/interop/Dockerfile @@ -9,7 +9,7 @@ ENV GOVERSION=1.21.1 RUN platform=$(echo ${TARGETPLATFORM} | tr '/' '-') && \ filename="go${GOVERSION}.${platform}.tar.gz" && \ - wget https://dl.google.com/go/${filename} && \ + wget --no-verbose https://dl.google.com/go/${filename} && \ tar xfz ${filename} && \ rm ${filename} From 689bbc7005f6bbf9fac1a8333bf03436fa4b4b2a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 4 Jan 2024 10:29:48 -0800 Subject: [PATCH 119/168] quic: deflake TestStreamsCreateConcurrency This test assumed that creating a stream and flushing it on the client ensured the server had accepted the stream. This isn't the case; the stream has been delivered to the server, but there's no guarantee that it been accepted by the user layer. Change the test to make a full loop: The client creates a stream, and then waits for the server to close it. Fixes golang/go#64788 Change-Id: I24f08502e9f5d8bd5a17e680b0aa19dcc2623841 Reviewed-on: https://go-review.googlesource.com/c/net/+/554175 Reviewed-by: Bryan Mills LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_streams_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go index fb9af47eb4..6815e403ef 100644 --- a/internal/quic/conn_streams_test.go +++ b/internal/quic/conn_streams_test.go @@ -510,6 +510,10 @@ func TestStreamsCreateConcurrency(t *testing.T) { return } s.Flush() + _, err = io.ReadAll(s) + if err != nil { + t.Errorf("ReadFull: %v", err) + } s.Close() } }() From cb5b10f0bbc51089bf49030ce3bd43bbfee08c23 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Mon, 8 Jan 2024 17:35:41 +0000 Subject: [PATCH 120/168] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: I77f3c5560bd989f4e9c6b8c3f36e900fefe9bb0e Reviewed-on: https://go-review.googlesource.com/c/net/+/554675 Reviewed-by: Than McIntosh Reviewed-by: Dmitri Shuralyov Auto-Submit: Gopher Robot LUCI-TryBot-Result: Go LUCI --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 8ab3f40e13..3bd487f5a3 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module golang.org/x/net go 1.18 require ( - golang.org/x/crypto v0.16.0 - golang.org/x/sys v0.15.0 - golang.org/x/term v0.15.0 + golang.org/x/crypto v0.18.0 + golang.org/x/sys v0.16.0 + golang.org/x/term v0.16.0 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index bb6ed68a00..8eeaf16c60 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= From 26b646ea024741dd5d8e141fc33d8149c465686a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 8 Jan 2024 09:36:16 -0800 Subject: [PATCH 121/168] quic: avoid deadlock in Endpoint.Close Don't hold Endpoint.connsMu while calling Conn methods that can indirectly depend on acquiring it. Also change test cleanup to not wait for connections to drain when closing a test Endpoint, removing an unnecessary 0.1s delay in test runtime. Fixes golang/go#64982. Change-Id: If336e63b0a7f5b8d2ef63986d36f9ee38a92c477 Reviewed-on: https://go-review.googlesource.com/c/net/+/554695 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/endpoint.go | 16 +++++++++++----- internal/quic/endpoint_test.go | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/internal/quic/endpoint.go b/internal/quic/endpoint.go index 82a08a18c2..8ed67de542 100644 --- a/internal/quic/endpoint.go +++ b/internal/quic/endpoint.go @@ -103,25 +103,31 @@ func (e *Endpoint) LocalAddr() netip.AddrPort { // It waits for the peers of any open connection to acknowledge the connection has been closed. func (e *Endpoint) Close(ctx context.Context) error { e.acceptQueue.close(errors.New("endpoint closed")) + + // It isn't safe to call Conn.Abort or conn.exit with connsMu held, + // so copy the list of conns. + var conns []*Conn e.connsMu.Lock() if !e.closing { - e.closing = true + e.closing = true // setting e.closing prevents new conns from being created for c := range e.conns { - c.Abort(localTransportError{code: errNo}) + conns = append(conns, c) } if len(e.conns) == 0 { e.udpConn.Close() } } e.connsMu.Unlock() + + for _, c := range conns { + c.Abort(localTransportError{code: errNo}) + } select { case <-e.closec: case <-ctx.Done(): - e.connsMu.Lock() - for c := range e.conns { + for _, c := range conns { c.exit() } - e.connsMu.Unlock() return ctx.Err() } return nil diff --git a/internal/quic/endpoint_test.go b/internal/quic/endpoint_test.go index ab6cd1cf51..16c3e0bce7 100644 --- a/internal/quic/endpoint_test.go +++ b/internal/quic/endpoint_test.go @@ -97,7 +97,7 @@ func newLocalEndpoint(t *testing.T, side connSide, conf *Config) *Endpoint { t.Fatal(err) } t.Cleanup(func() { - e.Close(context.Background()) + e.Close(canceledContext()) }) return e } From 07e05fd6e95ab445ebe48840c81a027dbace3b8e Mon Sep 17 00:00:00 2001 From: Nikita Sivukhin Date: Fri, 5 Jan 2024 11:25:21 +0400 Subject: [PATCH 122/168] http2: remove suspicious uint32->v conversion in frame code Function maxHeaderStringLen(...) uses uint32(int(v)) == v check to validate if length will fit in the int type. This check is a no-op on any architecture because int type always has at least 32 bits, so we can potentially encounter negative return values from maxHeaderStringLen(...) function. This can be bad as this outcome clearly breaks code intention and maybe some further code invariants. This patch replaces uint32(int(v)) == v check with more robust and simpler int(v) > 0 validation which is correct for our case when we operating with uint32 Fixes golang/go#64961 Change-Id: I31f95709df9d25593ade3200696ac5cef9f88652 Reviewed-on: https://go-review.googlesource.com/c/net/+/554235 Auto-Submit: Dmitri Shuralyov Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov --- http2/frame.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/http2/frame.go b/http2/frame.go index c1f6b90dc3..e2b298d859 100644 --- a/http2/frame.go +++ b/http2/frame.go @@ -1510,13 +1510,12 @@ func (mh *MetaHeadersFrame) checkPseudos() error { } func (fr *Framer) maxHeaderStringLen() int { - v := fr.maxHeaderListSize() - if uint32(int(v)) == v { - return int(v) + v := int(fr.maxHeaderListSize()) + if v < 0 { + // If maxHeaderListSize overflows an int, use no limit (0). + return 0 } - // They had a crazy big number for MaxHeaderBytes anyway, - // so give them unlimited header lengths: - return 0 + return v } // readMetaFrame returns 0 or more CONTINUATION frames from fr and From 0d0b98c1378dba60d10c77c383c40f94c1641cfc Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Mon, 22 Jan 2024 16:26:00 -0500 Subject: [PATCH 123/168] http2: avoid goroutine starvation in TestServer_Push_RejectAfterGoAway CL 557037 added a runtime.Gosched to prevent goroutine starvation in the wasm fake-net stack. Unfortunately, that Gosched causes the scheduler to enter a very similar starvation loop in this test. Add another runtime.Gosched to break this new loop. For golang/go#65178. Change-Id: I24b3f50dd728800462f71f27290b0d8f99d5ae5b Cq-Include-Trybots: luci.golang.try:x_net-gotip-wasip1-wasm_wasmtime,x_net-gotip-wasip1-wasm_wazero,x_net-gotip-js-wasm Reviewed-on: https://go-review.googlesource.com/c/net/+/557615 Auto-Submit: Bryan Mills LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Pratt --- http2/server_push_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/http2/server_push_test.go b/http2/server_push_test.go index 9882d9ef71..cda8f43367 100644 --- a/http2/server_push_test.go +++ b/http2/server_push_test.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "net/http" "reflect" + "runtime" "strconv" "sync" "testing" @@ -483,11 +484,7 @@ func TestServer_Push_RejectAfterGoAway(t *testing.T) { ready := make(chan struct{}) errc := make(chan error, 2) st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { - select { - case <-ready: - case <-time.After(5 * time.Second): - errc <- fmt.Errorf("timeout waiting for GOAWAY to be processed") - } + <-ready if got, want := w.(http.Pusher).Push("https://"+r.Host+"/pushed", nil), http.ErrNotSupported; got != want { errc <- fmt.Errorf("Push()=%v, want %v", got, want) } @@ -505,6 +502,10 @@ func TestServer_Push_RejectAfterGoAway(t *testing.T) { case <-ready: return default: + if runtime.GOARCH == "wasm" { + // Work around https://go.dev/issue/65178 to avoid goroutine starvation. + runtime.Gosched() + } } st.sc.serveMsgCh <- func(loopNum int) { if !st.sc.pushEnabled { From b2208d046df5625a4f78624149cba7722c4ccfee Mon Sep 17 00:00:00 2001 From: btwiuse <54848194+btwiuse@users.noreply.github.com> Date: Sun, 14 Jan 2024 13:20:58 +0000 Subject: [PATCH 124/168] internal/quic/qlog: fix typo VantageClient -> VantageServer Change-Id: Ie9738cffb06f03f961815853247e6f9cbe7fe466 GitHub-Last-Rev: 5d440ad29c49ef4cd529a076449114696662afec GitHub-Pull-Request: golang/net#202 Reviewed-on: https://go-review.googlesource.com/c/net/+/555795 LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Knyszek Reviewed-by: Damien Neil Auto-Submit: Damien Neil --- internal/quic/qlog/qlog.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/quic/qlog/qlog.go b/internal/quic/qlog/qlog.go index e54c839f0a..f33c6b0fd9 100644 --- a/internal/quic/qlog/qlog.go +++ b/internal/quic/qlog/qlog.go @@ -29,7 +29,7 @@ const ( // VantageClient traces follow a connection from the client's perspective. VantageClient = Vantage("client") - // VantageClient traces follow a connection from the server's perspective. + // VantageServer traces follow a connection from the server's perspective. VantageServer = Vantage("server") ) From 73e4b50dadcf3bd6015efb8b6e8ddbeb7dfe74c5 Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak <19653795+mateusz834@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:48:44 +0000 Subject: [PATCH 125/168] dns/dnsmessage: allow name compression for SRV resource parsing As per RFC 3597: Receiving servers MUST decompress domain names in RRs of well-known type, and SHOULD also decompress RRs of type RP, AFSDB, RT, SIG, PX, NXT, NAPTR, and SRV (although the current specification of the SRV RR in RFC2782 prohibits compression, RFC2052 mandated it, and some servers following that earlier specification are still in use). This change allows SRV resource decompression. Updates golang/go#36718 Updates golang/go#37362 Change-Id: I473c0d3803758e5b12886f378d2ed54bd5392144 GitHub-Last-Rev: 88d2e0642a7c7ba618d642801ebc72ba82ef30b7 GitHub-Pull-Request: golang/net#199 Reviewed-on: https://go-review.googlesource.com/c/net/+/540375 LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee Auto-Submit: Damien Neil Reviewed-by: Damien Neil --- dns/dnsmessage/message.go | 10 +--------- dns/dnsmessage/message_test.go | 22 ---------------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/dns/dnsmessage/message.go b/dns/dnsmessage/message.go index 42987ab7c5..a656efc128 100644 --- a/dns/dnsmessage/message.go +++ b/dns/dnsmessage/message.go @@ -273,7 +273,6 @@ var ( errTooManyAdditionals = errors.New("too many Additionals to pack (>65535)") errNonCanonicalName = errors.New("name is not in canonical format (it must end with a .)") errStringTooLong = errors.New("character string exceeds maximum length (255)") - errCompressedSRV = errors.New("compressed name in SRV resource data") ) // Internal constants. @@ -2028,10 +2027,6 @@ func (n *Name) pack(msg []byte, compression map[string]uint16, compressionOff in // unpack unpacks a domain name. func (n *Name) unpack(msg []byte, off int) (int, error) { - return n.unpackCompressed(msg, off, true /* allowCompression */) -} - -func (n *Name) unpackCompressed(msg []byte, off int, allowCompression bool) (int, error) { // currOff is the current working offset. currOff := off @@ -2076,9 +2071,6 @@ Loop: name = append(name, '.') currOff = endOff case 0xC0: // Pointer - if !allowCompression { - return off, errCompressedSRV - } if currOff >= len(msg) { return off, errInvalidPtr } @@ -2549,7 +2541,7 @@ func unpackSRVResource(msg []byte, off int) (SRVResource, error) { return SRVResource{}, &nestedError{"Port", err} } var target Name - if _, err := target.unpackCompressed(msg, off, false /* allowCompression */); err != nil { + if _, err := target.unpack(msg, off); err != nil { return SRVResource{}, &nestedError{"Target", err} } return SRVResource{priority, weight, port, target}, nil diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index c84d5a3aae..e60ec42d90 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -303,28 +303,6 @@ func TestNameUnpackTooLongName(t *testing.T) { } } -func TestIncompressibleName(t *testing.T) { - name := MustNewName("example.com.") - compression := map[string]uint16{} - buf, err := name.pack(make([]byte, 0, 100), compression, 0) - if err != nil { - t.Fatal("first Name.pack() =", err) - } - buf, err = name.pack(buf, compression, 0) - if err != nil { - t.Fatal("second Name.pack() =", err) - } - var n1 Name - off, err := n1.unpackCompressed(buf, 0, false /* allowCompression */) - if err != nil { - t.Fatal("unpacking incompressible name without pointers failed:", err) - } - var n2 Name - if _, err := n2.unpackCompressed(buf, off, false /* allowCompression */); err != errCompressedSRV { - t.Errorf("unpacking compressed incompressible name with pointers: got %v, want = %v", err, errCompressedSRV) - } -} - func checkErrorPrefix(err error, prefix string) bool { e, ok := err.(*nestedError) return ok && e.s == prefix From 643fd162e36ae58085b92ff4c0fec0bafe5a46a7 Mon Sep 17 00:00:00 2001 From: Maciej Mionskowski Date: Thu, 19 Oct 2023 20:16:20 +0000 Subject: [PATCH 126/168] html: fix SOLIDUS '/' handling in attribute parsing Calling the Tokenizer with HTML elements containing SOLIDUS (/) character in the attribute name results in incorrect tokenization. This is due to violation of the following rule transitions in the WHATWG spec: - https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state, where we are not reconsuming the character if '/' is encountered - https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-name-state, where we are not switching to self closing state Fixes golang/go#63402 Change-Id: I90d998dd8decde877bd63aa664f3657aa6161024 GitHub-Last-Rev: 3546db808c5fbf46ea25a10cdadb2802f763b6de GitHub-Pull-Request: golang/net#195 Reviewed-on: https://go-review.googlesource.com/c/net/+/533518 LUCI-TryBot-Result: Go LUCI Auto-Submit: Michael Pratt Reviewed-by: Roland Shoemaker Reviewed-by: David Chase --- html/token.go | 12 ++++++++---- html/token_test.go | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/html/token.go b/html/token.go index de67f938a1..3c57880d69 100644 --- a/html/token.go +++ b/html/token.go @@ -910,9 +910,6 @@ func (z *Tokenizer) readTagAttrKey() { return } switch c { - case ' ', '\n', '\r', '\t', '\f', '/': - z.pendingAttr[0].end = z.raw.end - 1 - return case '=': if z.pendingAttr[0].start+1 == z.raw.end { // WHATWG 13.2.5.32, if we see an equals sign before the attribute name @@ -920,7 +917,9 @@ func (z *Tokenizer) readTagAttrKey() { continue } fallthrough - case '>': + case ' ', '\n', '\r', '\t', '\f', '/', '>': + // WHATWG 13.2.5.33 Attribute name state + // We need to reconsume the char in the after attribute name state to support the / character z.raw.end-- z.pendingAttr[0].end = z.raw.end return @@ -939,6 +938,11 @@ func (z *Tokenizer) readTagAttrVal() { if z.err != nil { return } + if c == '/' { + // WHATWG 13.2.5.34 After attribute name state + // U+002F SOLIDUS (/) - Switch to the self-closing start tag state. + return + } if c != '=' { z.raw.end-- return diff --git a/html/token_test.go b/html/token_test.go index b2383a951c..8b0d5aab63 100644 --- a/html/token_test.go +++ b/html/token_test.go @@ -601,6 +601,21 @@ var tokenTests = []tokenTest{ `

`, `

`, }, + { + "forward slash before attribute name", + `

`, + `

`, + }, + { + "forward slash before attribute name with spaces around", + `

`, + `

`, + }, + { + "forward slash after attribute name followed by a character", + `

`, + `

`, + }, } func TestTokenizer(t *testing.T) { From 73d21fdbb4d7dc7115b50526b93b6c37a4e3377f Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Wed, 7 Feb 2024 19:22:03 +0000 Subject: [PATCH 127/168] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: I314af161ceac84fec04c729a71860ad35335513b Reviewed-on: https://go-review.googlesource.com/c/net/+/562495 Auto-Submit: Gopher Robot Reviewed-by: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI Reviewed-by: Than McIntosh --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 3bd487f5a3..7f512d7033 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module golang.org/x/net go 1.18 require ( - golang.org/x/crypto v0.18.0 - golang.org/x/sys v0.16.0 - golang.org/x/term v0.16.0 + golang.org/x/crypto v0.19.0 + golang.org/x/sys v0.17.0 + golang.org/x/term v0.17.0 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index 8eeaf16c60..683b469d6f 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= From 5a444b4f2fe893ea00f0376da46aa5376c3f3e28 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 29 Nov 2023 10:41:41 -0800 Subject: [PATCH 128/168] quic: add Stream.Set{Read,Write}Context, drop {Read,Write,Close}Context The ReadContext, WriteContext, and CloseContext Stream methods are difficult to use in conjunction with functions that operate on an io.Reader, io.Writer, or io.Closer. For example, it's reasonable to want to use io.ReadFull with a Stream, but doing so with a context is cumbersome. Drop the Stream methods that take a Context in favor of stateful methods that set the Context to use for read and write operations. (Close counts as a write operation, since it blocks waiting for data to be sent.) Intentionally make Set{Read,Write}Context not concurrency safe, to allow the race detector to catch misuse. This shouldn't be a problem for correct programs, since reads and writes are inherently not concurrency-safe. For golang/go#58547 Change-Id: I41378eb552d89a720921fc8644d3637c1a545676 Reviewed-on: https://go-review.googlesource.com/c/net/+/550795 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/conn_close_test.go | 11 ++- internal/quic/conn_flow_test.go | 19 ++-- internal/quic/conn_loss_test.go | 9 +- internal/quic/conn_streams_test.go | 26 +++--- internal/quic/conn_test.go | 11 +++ internal/quic/stream.go | 73 ++++++++------- internal/quic/stream_limits_test.go | 9 +- internal/quic/stream_test.go | 139 ++++++++++++++-------------- 8 files changed, 154 insertions(+), 143 deletions(-) diff --git a/internal/quic/conn_close_test.go b/internal/quic/conn_close_test.go index 63d4911e8a..2139750119 100644 --- a/internal/quic/conn_close_test.go +++ b/internal/quic/conn_close_test.go @@ -249,8 +249,9 @@ func TestConnCloseUnblocksNewStream(t *testing.T) { func TestConnCloseUnblocksStreamRead(t *testing.T) { testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { s := newLocalStream(t, tc, bidiStream) + s.SetReadContext(ctx) buf := make([]byte, 16) - _, err := s.ReadContext(ctx, buf) + _, err := s.Read(buf) return err }, permissiveTransportParameters) } @@ -258,8 +259,9 @@ func TestConnCloseUnblocksStreamRead(t *testing.T) { func TestConnCloseUnblocksStreamWrite(t *testing.T) { testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { s := newLocalStream(t, tc, bidiStream) + s.SetWriteContext(ctx) buf := make([]byte, 32) - _, err := s.WriteContext(ctx, buf) + _, err := s.Write(buf) return err }, permissiveTransportParameters, func(c *Config) { c.MaxStreamWriteBufferSize = 16 @@ -269,11 +271,12 @@ func TestConnCloseUnblocksStreamWrite(t *testing.T) { func TestConnCloseUnblocksStreamClose(t *testing.T) { testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { s := newLocalStream(t, tc, bidiStream) + s.SetWriteContext(ctx) buf := make([]byte, 16) - _, err := s.WriteContext(ctx, buf) + _, err := s.Write(buf) if err != nil { return err } - return s.CloseContext(ctx) + return s.Close() }, permissiveTransportParameters) } diff --git a/internal/quic/conn_flow_test.go b/internal/quic/conn_flow_test.go index 39c879346c..8e04e20d9e 100644 --- a/internal/quic/conn_flow_test.go +++ b/internal/quic/conn_flow_test.go @@ -12,7 +12,6 @@ import ( ) func TestConnInflowReturnOnRead(t *testing.T) { - ctx := canceledContext() tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { c.MaxConnReadBufferSize = 64 }) @@ -21,14 +20,14 @@ func TestConnInflowReturnOnRead(t *testing.T) { data: make([]byte, 64), }) const readSize = 8 - if n, err := s.ReadContext(ctx, make([]byte, readSize)); n != readSize || err != nil { + if n, err := s.Read(make([]byte, readSize)); n != readSize || err != nil { t.Fatalf("s.Read() = %v, %v; want %v, nil", n, err, readSize) } tc.wantFrame("available window increases, send a MAX_DATA", packetType1RTT, debugFrameMaxData{ max: 64 + readSize, }) - if n, err := s.ReadContext(ctx, make([]byte, 64)); n != 64-readSize || err != nil { + if n, err := s.Read(make([]byte, 64)); n != 64-readSize || err != nil { t.Fatalf("s.Read() = %v, %v; want %v, nil", n, err, 64-readSize) } tc.wantFrame("available window increases, send a MAX_DATA", @@ -42,7 +41,7 @@ func TestConnInflowReturnOnRead(t *testing.T) { data: make([]byte, 64), }) tc.wantIdle("connection is idle") - if n, err := s.ReadContext(ctx, make([]byte, 64)); n != 64 || err != nil { + if n, err := s.Read(make([]byte, 64)); n != 64 || err != nil { t.Fatalf("offset 64: s.Read() = %v, %v; want %v, nil", n, err, 64) } } @@ -79,10 +78,10 @@ func TestConnInflowReturnOnRacingReads(t *testing.T) { t.Fatalf("conn.AcceptStream() = %v", err) } read1 := runAsync(tc, func(ctx context.Context) (int, error) { - return s1.ReadContext(ctx, make([]byte, 16)) + return s1.Read(make([]byte, 16)) }) read2 := runAsync(tc, func(ctx context.Context) (int, error) { - return s2.ReadContext(ctx, make([]byte, 1)) + return s2.Read(make([]byte, 1)) }) // This MAX_DATA might extend the window by 16 or 17, depending on // whether the second write occurs before the update happens. @@ -90,10 +89,10 @@ func TestConnInflowReturnOnRacingReads(t *testing.T) { packetType1RTT, debugFrameMaxData{}) tc.wantIdle("redundant MAX_DATA is not sent") if _, err := read1.result(); err != nil { - t.Errorf("ReadContext #1 = %v", err) + t.Errorf("Read #1 = %v", err) } if _, err := read2.result(); err != nil { - t.Errorf("ReadContext #2 = %v", err) + t.Errorf("Read #2 = %v", err) } } @@ -227,13 +226,13 @@ func TestConnInflowMultipleStreams(t *testing.T) { t.Fatalf("AcceptStream() = %v", err) } streams = append(streams, s) - if n, err := s.ReadContext(ctx, make([]byte, 1)); err != nil || n != 1 { + if n, err := s.Read(make([]byte, 1)); err != nil || n != 1 { t.Fatalf("s.Read() = %v, %v; want 1, nil", n, err) } } tc.wantIdle("streams have read data, but not enough to update MAX_DATA") - if n, err := streams[0].ReadContext(ctx, make([]byte, 32)); err != nil || n != 31 { + if n, err := streams[0].Read(make([]byte, 32)); err != nil || n != 31 { t.Fatalf("s.Read() = %v, %v; want 31, nil", n, err) } tc.wantFrame("read enough data to trigger a MAX_DATA update", diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index 818816335b..876ffd093d 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -433,7 +433,8 @@ func TestLostMaxStreamsFrameMostRecent(t *testing.T) { if err != nil { t.Fatalf("AcceptStream() = %v", err) } - s.CloseContext(ctx) + s.SetWriteContext(ctx) + s.Close() if styp == bidiStream { tc.wantFrame("stream is closed", packetType1RTT, debugFrameStream{ @@ -480,7 +481,7 @@ func TestLostMaxStreamsFrameNotMostRecent(t *testing.T) { if err != nil { t.Fatalf("AcceptStream() = %v", err) } - if err := s.CloseContext(ctx); err != nil { + if err := s.Close(); err != nil { t.Fatalf("stream.Close() = %v", err) } tc.wantFrame("closing stream updates peer's MAX_STREAMS", @@ -512,7 +513,7 @@ func TestLostStreamDataBlockedFrame(t *testing.T) { }) w := runAsync(tc, func(ctx context.Context) (int, error) { - return s.WriteContext(ctx, []byte{0, 1, 2, 3}) + return s.Write([]byte{0, 1, 2, 3}) }) defer w.cancel() tc.wantFrame("write is blocked by flow control", @@ -564,7 +565,7 @@ func TestLostStreamDataBlockedFrameAfterStreamUnblocked(t *testing.T) { data := []byte{0, 1, 2, 3} w := runAsync(tc, func(ctx context.Context) (int, error) { - return s.WriteContext(ctx, data) + return s.Write(data) }) defer w.cancel() tc.wantFrame("write is blocked by flow control", diff --git a/internal/quic/conn_streams_test.go b/internal/quic/conn_streams_test.go index 6815e403ef..dc81ad9913 100644 --- a/internal/quic/conn_streams_test.go +++ b/internal/quic/conn_streams_test.go @@ -230,8 +230,8 @@ func TestStreamsWriteQueueFairness(t *testing.T) { t.Fatal(err) } streams = append(streams, s) - if n, err := s.WriteContext(ctx, data); n != len(data) || err != nil { - t.Fatalf("s.WriteContext() = %v, %v; want %v, nil", n, err, len(data)) + if n, err := s.Write(data); n != len(data) || err != nil { + t.Fatalf("s.Write() = %v, %v; want %v, nil", n, err, len(data)) } // Wait for the stream to finish writing whatever frames it can before // congestion control blocks it. @@ -298,7 +298,7 @@ func TestStreamsShutdown(t *testing.T) { side: localStream, styp: uniStream, setup: func(t *testing.T, tc *testConn, s *Stream) { - s.CloseContext(canceledContext()) + s.Close() }, shutdown: func(t *testing.T, tc *testConn, s *Stream) { tc.writeAckForAll() @@ -311,7 +311,7 @@ func TestStreamsShutdown(t *testing.T) { tc.writeFrames(packetType1RTT, debugFrameResetStream{ id: s.id, }) - s.CloseContext(canceledContext()) + s.Close() }, shutdown: func(t *testing.T, tc *testConn, s *Stream) { tc.writeAckForAll() @@ -321,8 +321,8 @@ func TestStreamsShutdown(t *testing.T) { side: localStream, styp: bidiStream, setup: func(t *testing.T, tc *testConn, s *Stream) { - s.CloseContext(canceledContext()) - tc.wantIdle("all frames after CloseContext are ignored") + s.Close() + tc.wantIdle("all frames after Close are ignored") tc.writeAckForAll() }, shutdown: func(t *testing.T, tc *testConn, s *Stream) { @@ -335,13 +335,12 @@ func TestStreamsShutdown(t *testing.T) { side: remoteStream, styp: uniStream, setup: func(t *testing.T, tc *testConn, s *Stream) { - ctx := canceledContext() tc.writeFrames(packetType1RTT, debugFrameStream{ id: s.id, fin: true, }) - if n, err := s.ReadContext(ctx, make([]byte, 16)); n != 0 || err != io.EOF { - t.Errorf("ReadContext() = %v, %v; want 0, io.EOF", n, err) + if n, err := s.Read(make([]byte, 16)); n != 0 || err != io.EOF { + t.Errorf("Read() = %v, %v; want 0, io.EOF", n, err) } }, shutdown: func(t *testing.T, tc *testConn, s *Stream) { @@ -451,17 +450,14 @@ func TestStreamsCreateAndCloseRemote(t *testing.T) { id: op.id, }) case acceptOp: - s, err := tc.conn.AcceptStream(ctx) - if err != nil { - t.Fatalf("AcceptStream() = %q; want stream %v", err, stringID(op.id)) - } + s := tc.acceptStream() if s.id != op.id { - t.Fatalf("accepted stram %v; want stream %v", err, stringID(op.id)) + t.Fatalf("accepted stream %v; want stream %v", stringID(s.id), stringID(op.id)) } t.Logf("accepted stream %v", stringID(op.id)) // Immediately close the stream, so the stream becomes done when the // peer closes its end. - s.CloseContext(ctx) + s.Close() } p := tc.readPacket() if p != nil { diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index ddf0740e22..2d3c946d6c 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -382,6 +382,17 @@ func (tc *testConn) cleanup() { <-tc.conn.donec } +func (tc *testConn) acceptStream() *Stream { + tc.t.Helper() + s, err := tc.conn.AcceptStream(canceledContext()) + if err != nil { + tc.t.Fatalf("conn.AcceptStream() = %v, want stream", err) + } + s.SetReadContext(canceledContext()) + s.SetWriteContext(canceledContext()) + return s +} + func logDatagram(t *testing.T, text string, d *testDatagram) { t.Helper() if !*testVV { diff --git a/internal/quic/stream.go b/internal/quic/stream.go index fb9c1cf3c0..d0122b9518 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -18,6 +18,11 @@ type Stream struct { id streamID conn *Conn + // Contexts used for read/write operations. + // Intentionally not mutex-guarded, to allow the race detector to catch concurrent access. + inctx context.Context + outctx context.Context + // ingate's lock guards all receive-related state. // // The gate condition is set if a read from the stream will not block, @@ -152,6 +157,8 @@ func newStream(c *Conn, id streamID) *Stream { inresetcode: -1, // -1 indicates no RESET_STREAM received ingate: newLockedGate(), outgate: newLockedGate(), + inctx: context.Background(), + outctx: context.Background(), } if !s.IsReadOnly() { s.outdone = make(chan struct{}) @@ -159,6 +166,22 @@ func newStream(c *Conn, id streamID) *Stream { return s } +// SetReadContext sets the context used for reads from the stream. +// +// It is not safe to call SetReadContext concurrently. +func (s *Stream) SetReadContext(ctx context.Context) { + s.inctx = ctx +} + +// SetWriteContext sets the context used for writes to the stream. +// The write context is also used by Close when waiting for writes to be +// received by the peer. +// +// It is not safe to call SetWriteContext concurrently. +func (s *Stream) SetWriteContext(ctx context.Context) { + s.outctx = ctx +} + // IsReadOnly reports whether the stream is read-only // (a unidirectional stream created by the peer). func (s *Stream) IsReadOnly() bool { @@ -172,24 +195,18 @@ func (s *Stream) IsWriteOnly() bool { } // Read reads data from the stream. -// See ReadContext for more details. -func (s *Stream) Read(b []byte) (n int, err error) { - return s.ReadContext(context.Background(), b) -} - -// ReadContext reads data from the stream. // -// ReadContext returns as soon as at least one byte of data is available. +// Read returns as soon as at least one byte of data is available. // -// If the peer closes the stream cleanly, ReadContext returns io.EOF after +// If the peer closes the stream cleanly, Read returns io.EOF after // returning all data sent by the peer. -// If the peer aborts reads on the stream, ReadContext returns +// If the peer aborts reads on the stream, Read returns // an error wrapping StreamResetCode. -func (s *Stream) ReadContext(ctx context.Context, b []byte) (n int, err error) { +func (s *Stream) Read(b []byte) (n int, err error) { if s.IsWriteOnly() { return 0, errors.New("read from write-only stream") } - if err := s.ingate.waitAndLock(ctx, s.conn.testHooks); err != nil { + if err := s.ingate.waitAndLock(s.inctx, s.conn.testHooks); err != nil { return 0, err } defer func() { @@ -237,17 +254,11 @@ func shouldUpdateFlowControl(maxWindow, addedWindow int64) bool { } // Write writes data to the stream. -// See WriteContext for more details. -func (s *Stream) Write(b []byte) (n int, err error) { - return s.WriteContext(context.Background(), b) -} - -// WriteContext writes data to the stream. // -// WriteContext writes data to the stream write buffer. +// Write writes data to the stream write buffer. // Buffered data is only sent when the buffer is sufficiently full. // Call the Flush method to ensure buffered data is sent. -func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) { +func (s *Stream) Write(b []byte) (n int, err error) { if s.IsReadOnly() { return 0, errors.New("write to read-only stream") } @@ -259,7 +270,7 @@ func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) if len(b) > 0 && !canWrite { // Our send buffer is full. Wait for the peer to ack some data. s.outUnlock() - if err := s.outgate.waitAndLock(ctx, s.conn.testHooks); err != nil { + if err := s.outgate.waitAndLock(s.outctx, s.conn.testHooks); err != nil { return n, err } // Successfully returning from waitAndLockGate means we are no longer @@ -317,7 +328,7 @@ func (s *Stream) WriteContext(ctx context.Context, b []byte) (n int, err error) // Flush flushes data written to the stream. // It does not wait for the peer to acknowledge receipt of the data. -// Use CloseContext to wait for the peer's acknowledgement. +// Use Close to wait for the peer's acknowledgement. func (s *Stream) Flush() { s.outgate.lock() defer s.outUnlock() @@ -333,27 +344,21 @@ func (s *Stream) flushLocked() { } // Close closes the stream. -// See CloseContext for more details. -func (s *Stream) Close() error { - return s.CloseContext(context.Background()) -} - -// CloseContext closes the stream. // Any blocked stream operations will be unblocked and return errors. // -// CloseContext flushes any data in the stream write buffer and waits for the peer to +// Close flushes any data in the stream write buffer and waits for the peer to // acknowledge receipt of the data. // If the stream has been reset, it waits for the peer to acknowledge the reset. // If the context expires before the peer receives the stream's data, -// CloseContext discards the buffer and returns the context error. -func (s *Stream) CloseContext(ctx context.Context) error { +// Close discards the buffer and returns the context error. +func (s *Stream) Close() error { s.CloseRead() if s.IsReadOnly() { return nil } s.CloseWrite() // TODO: Return code from peer's RESET_STREAM frame? - if err := s.conn.waitOnDone(ctx, s.outdone); err != nil { + if err := s.conn.waitOnDone(s.outctx, s.outdone); err != nil { return err } s.outgate.lock() @@ -369,7 +374,7 @@ func (s *Stream) CloseContext(ctx context.Context) error { // // CloseRead notifies the peer that the stream has been closed for reading. // It does not wait for the peer to acknowledge the closure. -// Use CloseContext to wait for the peer's acknowledgement. +// Use Close to wait for the peer's acknowledgement. func (s *Stream) CloseRead() { if s.IsWriteOnly() { return @@ -394,7 +399,7 @@ func (s *Stream) CloseRead() { // // CloseWrite sends any data in the stream write buffer to the peer. // It does not wait for the peer to acknowledge receipt of the data. -// Use CloseContext to wait for the peer's acknowledgement. +// Use Close to wait for the peer's acknowledgement. func (s *Stream) CloseWrite() { if s.IsReadOnly() { return @@ -412,7 +417,7 @@ func (s *Stream) CloseWrite() { // Reset sends the application protocol error code, which must be // less than 2^62, to the peer. // It does not wait for the peer to acknowledge receipt of the error. -// Use CloseContext to wait for the peer's acknowledgement. +// Use Close to wait for the peer's acknowledgement. // // Reset does not affect reads. // Use CloseRead to abort reads on the stream. diff --git a/internal/quic/stream_limits_test.go b/internal/quic/stream_limits_test.go index 3f291e9f4c..9c2f71ec1e 100644 --- a/internal/quic/stream_limits_test.go +++ b/internal/quic/stream_limits_test.go @@ -200,7 +200,6 @@ func TestStreamLimitMaxStreamsFrameTooLarge(t *testing.T) { func TestStreamLimitSendUpdatesMaxStreams(t *testing.T) { testStreamTypes(t, "", func(t *testing.T, styp streamType) { - ctx := canceledContext() tc := newTestConn(t, serverSide, func(c *Config) { if styp == uniStream { c.MaxUniRemoteStreams = 4 @@ -218,13 +217,9 @@ func TestStreamLimitSendUpdatesMaxStreams(t *testing.T) { id: newStreamID(clientSide, styp, int64(i)), fin: true, }) - s, err := tc.conn.AcceptStream(ctx) - if err != nil { - t.Fatalf("AcceptStream = %v", err) - } - streams = append(streams, s) + streams = append(streams, tc.acceptStream()) } - streams[3].CloseContext(ctx) + streams[3].Close() if styp == bidiStream { tc.wantFrame("stream is closed", packetType1RTT, debugFrameStream{ diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 00e392dbab..08e89b24c4 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -19,7 +19,6 @@ import ( func TestStreamWriteBlockedByOutputBuffer(t *testing.T) { testStreamTypes(t, "", func(t *testing.T, styp streamType) { - ctx := canceledContext() want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} const writeBufferSize = 4 tc := newTestConn(t, clientSide, permissiveTransportParameters, func(c *Config) { @@ -28,15 +27,12 @@ func TestStreamWriteBlockedByOutputBuffer(t *testing.T) { tc.handshake() tc.ignoreFrame(frameTypeAck) - s, err := tc.conn.newLocalStream(ctx, styp) - if err != nil { - t.Fatal(err) - } + s := newLocalStream(t, tc, styp) // Non-blocking write. - n, err := s.WriteContext(ctx, want) + n, err := s.Write(want) if n != writeBufferSize || err != context.Canceled { - t.Fatalf("s.WriteContext() = %v, %v; want %v, context.Canceled", n, err, writeBufferSize) + t.Fatalf("s.Write() = %v, %v; want %v, context.Canceled", n, err, writeBufferSize) } s.Flush() tc.wantFrame("first write buffer of data sent", @@ -48,7 +44,8 @@ func TestStreamWriteBlockedByOutputBuffer(t *testing.T) { // Blocking write, which must wait for buffer space. w := runAsync(tc, func(ctx context.Context) (int, error) { - n, err := s.WriteContext(ctx, want[writeBufferSize:]) + s.SetWriteContext(ctx) + n, err := s.Write(want[writeBufferSize:]) s.Flush() return n, err }) @@ -75,7 +72,7 @@ func TestStreamWriteBlockedByOutputBuffer(t *testing.T) { }) if n, err := w.result(); n != len(want)-writeBufferSize || err != nil { - t.Fatalf("s.WriteContext() = %v, %v; want %v, nil", + t.Fatalf("s.Write() = %v, %v; want %v, nil", len(want)-writeBufferSize, err, writeBufferSize) } }) @@ -99,7 +96,7 @@ func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) { } // Data is written to the stream output buffer, but we have no flow control. - _, err = s.WriteContext(ctx, want[:1]) + _, err = s.Write(want[:1]) if err != nil { t.Fatalf("write with available output buffer: unexpected error: %v", err) } @@ -110,7 +107,7 @@ func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) { }) // Write more data. - _, err = s.WriteContext(ctx, want[1:]) + _, err = s.Write(want[1:]) if err != nil { t.Fatalf("write with available output buffer: unexpected error: %v", err) } @@ -172,7 +169,7 @@ func TestStreamIgnoresMaxStreamDataReduction(t *testing.T) { if err != nil { t.Fatal(err) } - s.WriteContext(ctx, want[:1]) + s.Write(want[:1]) s.Flush() tc.wantFrame("sent data (1 byte) fits within flow control limit", packetType1RTT, debugFrameStream{ @@ -188,7 +185,7 @@ func TestStreamIgnoresMaxStreamDataReduction(t *testing.T) { }) // Write [1,4). - s.WriteContext(ctx, want[1:]) + s.Write(want[1:]) tc.wantFrame("stream limit is 4 bytes, ignoring decrease in MAX_STREAM_DATA", packetType1RTT, debugFrameStream{ id: s.id, @@ -208,7 +205,7 @@ func TestStreamIgnoresMaxStreamDataReduction(t *testing.T) { }) // Write [1,4). - s.WriteContext(ctx, want[4:]) + s.Write(want[4:]) tc.wantFrame("stream limit is 8 bytes, ignoring decrease in MAX_STREAM_DATA", packetType1RTT, debugFrameStream{ id: s.id, @@ -220,7 +217,6 @@ func TestStreamIgnoresMaxStreamDataReduction(t *testing.T) { func TestStreamWriteBlockedByWriteBufferLimit(t *testing.T) { testStreamTypes(t, "", func(t *testing.T, styp streamType) { - ctx := canceledContext() want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} const maxWriteBuffer = 4 tc := newTestConn(t, clientSide, func(p *transportParameters) { @@ -238,12 +234,10 @@ func TestStreamWriteBlockedByWriteBufferLimit(t *testing.T) { // Write more data than StreamWriteBufferSize. // The peer has given us plenty of flow control, // so we're just blocked by our local limit. - s, err := tc.conn.newLocalStream(ctx, styp) - if err != nil { - t.Fatal(err) - } + s := newLocalStream(t, tc, styp) w := runAsync(tc, func(ctx context.Context) (int, error) { - return s.WriteContext(ctx, want) + s.SetWriteContext(ctx) + return s.Write(want) }) tc.wantFrame("stream write should send as much data as write buffer allows", packetType1RTT, debugFrameStream{ @@ -266,7 +260,7 @@ func TestStreamWriteBlockedByWriteBufferLimit(t *testing.T) { w.cancel() n, err := w.result() if n != 2*maxWriteBuffer || err == nil { - t.Fatalf("WriteContext() = %v, %v; want %v bytes, error", n, err, 2*maxWriteBuffer) + t.Fatalf("Write() = %v, %v; want %v bytes, error", n, err, 2*maxWriteBuffer) } }) } @@ -397,7 +391,6 @@ func TestStreamReceive(t *testing.T) { }}, }} { testStreamTypes(t, test.name, func(t *testing.T, styp streamType) { - ctx := canceledContext() tc := newTestConn(t, serverSide) tc.handshake() sid := newStreamID(clientSide, styp, 0) @@ -413,21 +406,17 @@ func TestStreamReceive(t *testing.T) { fin: f.fin, }) if s == nil { - var err error - s, err = tc.conn.AcceptStream(ctx) - if err != nil { - tc.t.Fatalf("conn.AcceptStream() = %v", err) - } + s = tc.acceptStream() } for { - n, err := s.ReadContext(ctx, got[total:]) - t.Logf("s.ReadContext() = %v, %v", n, err) + n, err := s.Read(got[total:]) + t.Logf("s.Read() = %v, %v", n, err) total += n if f.wantEOF && err != io.EOF { - t.Fatalf("ReadContext() error = %v; want io.EOF", err) + t.Fatalf("Read() error = %v; want io.EOF", err) } if !f.wantEOF && err == io.EOF { - t.Fatalf("ReadContext() error = io.EOF, want something else") + t.Fatalf("Read() error = io.EOF, want something else") } if err != nil { break @@ -468,8 +457,8 @@ func TestStreamReceiveExtendsStreamWindow(t *testing.T) { } tc.wantIdle("stream window is not extended before data is read") buf := make([]byte, maxWindowSize+1) - if n, err := s.ReadContext(ctx, buf); n != maxWindowSize || err != nil { - t.Fatalf("s.ReadContext() = %v, %v; want %v, nil", n, err, maxWindowSize) + if n, err := s.Read(buf); n != maxWindowSize || err != nil { + t.Fatalf("s.Read() = %v, %v; want %v, nil", n, err, maxWindowSize) } tc.wantFrame("stream window is extended after reading data", packetType1RTT, debugFrameMaxStreamData{ @@ -482,8 +471,8 @@ func TestStreamReceiveExtendsStreamWindow(t *testing.T) { data: make([]byte, maxWindowSize), fin: true, }) - if n, err := s.ReadContext(ctx, buf); n != maxWindowSize || err != io.EOF { - t.Fatalf("s.ReadContext() = %v, %v; want %v, io.EOF", n, err, maxWindowSize) + if n, err := s.Read(buf); n != maxWindowSize || err != io.EOF { + t.Fatalf("s.Read() = %v, %v; want %v, io.EOF", n, err, maxWindowSize) } tc.wantIdle("stream window is not extended after FIN") }) @@ -673,18 +662,19 @@ func TestStreamReceiveUnblocksReader(t *testing.T) { t.Fatalf("AcceptStream() = %v", err) } - // ReadContext succeeds immediately, since we already have data. + // Read succeeds immediately, since we already have data. got := make([]byte, len(want)) read := runAsync(tc, func(ctx context.Context) (int, error) { - return s.ReadContext(ctx, got) + return s.Read(got) }) if n, err := read.result(); n != write1size || err != nil { - t.Fatalf("ReadContext = %v, %v; want %v, nil", n, err, write1size) + t.Fatalf("Read = %v, %v; want %v, nil", n, err, write1size) } - // ReadContext blocks waiting for more data. + // Read blocks waiting for more data. read = runAsync(tc, func(ctx context.Context) (int, error) { - return s.ReadContext(ctx, got[write1size:]) + s.SetReadContext(ctx) + return s.Read(got[write1size:]) }) tc.writeFrames(packetType1RTT, debugFrameStream{ id: sid, @@ -693,7 +683,7 @@ func TestStreamReceiveUnblocksReader(t *testing.T) { fin: true, }) if n, err := read.result(); n != len(want)-write1size || err != io.EOF { - t.Fatalf("ReadContext = %v, %v; want %v, io.EOF", n, err, len(want)-write1size) + t.Fatalf("Read = %v, %v; want %v, io.EOF", n, err, len(want)-write1size) } if !bytes.Equal(got, want) { t.Fatalf("read bytes %x, want %x", got, want) @@ -935,7 +925,8 @@ func TestStreamResetBlockedStream(t *testing.T) { }) tc.ignoreFrame(frameTypeStreamDataBlocked) writing := runAsync(tc, func(ctx context.Context) (int, error) { - return s.WriteContext(ctx, []byte{0, 1, 2, 3, 4, 5, 6, 7}) + s.SetWriteContext(ctx) + return s.Write([]byte{0, 1, 2, 3, 4, 5, 6, 7}) }) tc.wantFrame("stream writes data until write buffer fills", packetType1RTT, debugFrameStream{ @@ -972,7 +963,7 @@ func TestStreamWriteMoreThanOnePacketOfData(t *testing.T) { want := make([]byte, 4096) rand.Read(want) // doesn't need to be crypto/rand, but non-deprecated and harmless w := runAsync(tc, func(ctx context.Context) (int, error) { - n, err := s.WriteContext(ctx, want) + n, err := s.Write(want) s.Flush() return n, err }) @@ -992,7 +983,7 @@ func TestStreamWriteMoreThanOnePacketOfData(t *testing.T) { got = append(got, sf.data...) } if n, err := w.result(); n != len(want) || err != nil { - t.Fatalf("s.WriteContext() = %v, %v; want %v, nil", n, err, len(want)) + t.Fatalf("s.Write() = %v, %v; want %v, nil", n, err, len(want)) } if !bytes.Equal(got, want) { t.Fatalf("mismatch in received stream data") @@ -1000,17 +991,16 @@ func TestStreamWriteMoreThanOnePacketOfData(t *testing.T) { } func TestStreamCloseWaitsForAcks(t *testing.T) { - ctx := canceledContext() tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) data := make([]byte, 100) - s.WriteContext(ctx, data) + s.Write(data) s.Flush() tc.wantFrame("conn sends data for the stream", packetType1RTT, debugFrameStream{ id: s.id, data: data, }) - if err := s.CloseContext(ctx); err != context.Canceled { + if err := s.Close(); err != context.Canceled { t.Fatalf("s.Close() = %v, want context.Canceled (data not acked yet)", err) } tc.wantFrame("conn sends FIN for closed stream", @@ -1021,21 +1011,22 @@ func TestStreamCloseWaitsForAcks(t *testing.T) { data: []byte{}, }) closing := runAsync(tc, func(ctx context.Context) (struct{}, error) { - return struct{}{}, s.CloseContext(ctx) + s.SetWriteContext(ctx) + return struct{}{}, s.Close() }) if _, err := closing.result(); err != errNotDone { - t.Fatalf("s.CloseContext() = %v, want it to block waiting for acks", err) + t.Fatalf("s.Close() = %v, want it to block waiting for acks", err) } tc.writeAckForAll() if _, err := closing.result(); err != nil { - t.Fatalf("s.CloseContext() = %v, want nil (all data acked)", err) + t.Fatalf("s.Close() = %v, want nil (all data acked)", err) } } func TestStreamCloseReadOnly(t *testing.T) { tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, permissiveTransportParameters) - if err := s.CloseContext(canceledContext()); err != nil { - t.Errorf("s.CloseContext() = %v, want nil", err) + if err := s.Close(); err != nil { + t.Errorf("s.Close() = %v, want nil", err) } tc.wantFrame("closed stream sends STOP_SENDING", packetType1RTT, debugFrameStopSending{ @@ -1069,17 +1060,16 @@ func TestStreamCloseUnblocked(t *testing.T) { }, }} { t.Run(test.name, func(t *testing.T) { - ctx := canceledContext() tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) data := make([]byte, 100) - s.WriteContext(ctx, data) + s.Write(data) s.Flush() tc.wantFrame("conn sends data for the stream", packetType1RTT, debugFrameStream{ id: s.id, data: data, }) - if err := s.CloseContext(ctx); err != context.Canceled { + if err := s.Close(); err != context.Canceled { t.Fatalf("s.Close() = %v, want context.Canceled (data not acked yet)", err) } tc.wantFrame("conn sends FIN for closed stream", @@ -1090,34 +1080,34 @@ func TestStreamCloseUnblocked(t *testing.T) { data: []byte{}, }) closing := runAsync(tc, func(ctx context.Context) (struct{}, error) { - return struct{}{}, s.CloseContext(ctx) + s.SetWriteContext(ctx) + return struct{}{}, s.Close() }) if _, err := closing.result(); err != errNotDone { - t.Fatalf("s.CloseContext() = %v, want it to block waiting for acks", err) + t.Fatalf("s.Close() = %v, want it to block waiting for acks", err) } test.unblock(tc, s) _, err := closing.result() switch { case err == errNotDone: - t.Fatalf("s.CloseContext() still blocking; want it to have returned") + t.Fatalf("s.Close() still blocking; want it to have returned") case err == nil && !test.success: - t.Fatalf("s.CloseContext() = nil, want error") + t.Fatalf("s.Close() = nil, want error") case err != nil && test.success: - t.Fatalf("s.CloseContext() = %v, want nil (all data acked)", err) + t.Fatalf("s.Close() = %v, want nil (all data acked)", err) } }) } } func TestStreamCloseWriteWhenBlockedByStreamFlowControl(t *testing.T) { - ctx := canceledContext() tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters, func(p *transportParameters) { //p.initialMaxData = 0 p.initialMaxStreamDataUni = 0 }) tc.ignoreFrame(frameTypeStreamDataBlocked) - if _, err := s.WriteContext(ctx, []byte{0, 1}); err != nil { + if _, err := s.Write([]byte{0, 1}); err != nil { t.Fatalf("s.Write = %v", err) } s.CloseWrite() @@ -1149,7 +1139,6 @@ func TestStreamCloseWriteWhenBlockedByStreamFlowControl(t *testing.T) { func TestStreamPeerResetsWithUnreadAndUnsentData(t *testing.T) { testStreamTypes(t, "", func(t *testing.T, styp streamType) { - ctx := canceledContext() tc, s := newTestConnAndRemoteStream(t, serverSide, styp) data := []byte{0, 1, 2, 3, 4, 5, 6, 7} tc.writeFrames(packetType1RTT, debugFrameStream{ @@ -1157,7 +1146,7 @@ func TestStreamPeerResetsWithUnreadAndUnsentData(t *testing.T) { data: data, }) got := make([]byte, 4) - if n, err := s.ReadContext(ctx, got); n != len(got) || err != nil { + if n, err := s.Read(got); n != len(got) || err != nil { t.Fatalf("Read start of stream: got %v, %v; want %v, nil", n, err, len(got)) } const sentCode = 42 @@ -1167,7 +1156,7 @@ func TestStreamPeerResetsWithUnreadAndUnsentData(t *testing.T) { code: sentCode, }) wantErr := StreamErrorCode(sentCode) - if n, err := s.ReadContext(ctx, got); n != 0 || !errors.Is(err, wantErr) { + if n, err := s.Read(got); n != 0 || !errors.Is(err, wantErr) { t.Fatalf("Read reset stream: got %v, %v; want 0, %v", n, err, wantErr) } }) @@ -1177,8 +1166,9 @@ func TestStreamPeerResetWakesBlockedRead(t *testing.T) { testStreamTypes(t, "", func(t *testing.T, styp streamType) { tc, s := newTestConnAndRemoteStream(t, serverSide, styp) reader := runAsync(tc, func(ctx context.Context) (int, error) { + s.SetReadContext(ctx) got := make([]byte, 4) - return s.ReadContext(ctx, got) + return s.Read(got) }) const sentCode = 42 tc.writeFrames(packetType1RTT, debugFrameResetStream{ @@ -1348,7 +1338,8 @@ func TestStreamFlushImplicitLargerThanBuffer(t *testing.T) { want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} w := runAsync(tc, func(ctx context.Context) (int, error) { - n, err := s.WriteContext(ctx, want) + s.SetWriteContext(ctx) + n, err := s.Write(want) return n, err }) @@ -1401,7 +1392,10 @@ func newTestConnAndLocalStream(t *testing.T, side connSide, styp streamType, opt tc := newTestConn(t, side, opts...) tc.handshake() tc.ignoreFrame(frameTypeAck) - return tc, newLocalStream(t, tc, styp) + s := newLocalStream(t, tc, styp) + s.SetReadContext(canceledContext()) + s.SetWriteContext(canceledContext()) + return tc, s } func newLocalStream(t *testing.T, tc *testConn, styp streamType) *Stream { @@ -1411,6 +1405,8 @@ func newLocalStream(t *testing.T, tc *testConn, styp streamType) *Stream { if err != nil { t.Fatalf("conn.newLocalStream(%v) = %v", styp, err) } + s.SetReadContext(canceledContext()) + s.SetWriteContext(canceledContext()) return s } @@ -1419,7 +1415,10 @@ func newTestConnAndRemoteStream(t *testing.T, side connSide, styp streamType, op tc := newTestConn(t, side, opts...) tc.handshake() tc.ignoreFrame(frameTypeAck) - return tc, newRemoteStream(t, tc, styp) + s := newRemoteStream(t, tc, styp) + s.SetReadContext(canceledContext()) + s.SetWriteContext(canceledContext()) + return tc, s } func newRemoteStream(t *testing.T, tc *testConn, styp streamType) *Stream { @@ -1432,6 +1431,8 @@ func newRemoteStream(t *testing.T, tc *testConn, styp streamType) *Stream { if err != nil { t.Fatalf("conn.AcceptStream() = %v", err) } + s.SetReadContext(canceledContext()) + s.SetWriteContext(canceledContext()) return s } From 840656f9213922d0bb729d201162410b0bd74d9b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 13 Feb 2024 15:36:02 -0800 Subject: [PATCH 129/168] quic/qlog: don't output empty slog.Attrs For golang/go#58547 Change-Id: I49a27ab82781c817511c6f7da0268529abc3f27f Reviewed-on: https://go-review.googlesource.com/c/net/+/564015 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/qlog/json_writer.go | 6 +++--- internal/quic/qlog/json_writer_test.go | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/quic/qlog/json_writer.go b/internal/quic/qlog/json_writer.go index b2fa3e03e5..6fb8d33b25 100644 --- a/internal/quic/qlog/json_writer.go +++ b/internal/quic/qlog/json_writer.go @@ -45,15 +45,15 @@ func (w *jsonWriter) writeRecordEnd() { func (w *jsonWriter) writeAttrs(attrs []slog.Attr) { w.buf.WriteByte('{') for _, a := range attrs { - if a.Key == "" { - continue - } w.writeAttr(a) } w.buf.WriteByte('}') } func (w *jsonWriter) writeAttr(a slog.Attr) { + if a.Key == "" { + return + } w.writeName(a.Key) w.writeValue(a.Value) } diff --git a/internal/quic/qlog/json_writer_test.go b/internal/quic/qlog/json_writer_test.go index 6da5566412..03cf6947ce 100644 --- a/internal/quic/qlog/json_writer_test.go +++ b/internal/quic/qlog/json_writer_test.go @@ -85,6 +85,15 @@ func TestJSONWriterAttrs(t *testing.T) { `}}`) } +func TestJSONWriterAttrEmpty(t *testing.T) { + w := newTestJSONWriter() + w.writeRecordStart() + var a slog.Attr + w.writeAttr(a) + w.writeRecordEnd() + wantJSONRecord(t, w, `{}`) +} + func TestJSONWriterObjectEmpty(t *testing.T) { w := newTestJSONWriter() w.writeRecordStart() From 6e383c4aaf0635c980378ed3217f2a65391895a5 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 17 Nov 2023 08:06:43 -0800 Subject: [PATCH 130/168] quic: add qlog recovery metrics Log events for various congestion control and loss recovery metrics. For golang/go#58547 Change-Id: Ife3b3897f6ca731049c78b934a7123aa1ed4aee2 Reviewed-on: https://go-review.googlesource.com/c/net/+/564016 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/congestion_reno.go | 59 +++++++++++++++++++-- internal/quic/congestion_reno_test.go | 6 +-- internal/quic/conn.go | 2 +- internal/quic/conn_loss.go | 4 ++ internal/quic/conn_recv.go | 4 +- internal/quic/conn_send.go | 21 +++++--- internal/quic/loss.go | 47 ++++++++++++++--- internal/quic/loss_test.go | 10 ++-- internal/quic/packet_writer.go | 7 +-- internal/quic/qlog.go | 16 +++++- internal/quic/qlog_test.go | 76 ++++++++++++++++++++++++++- internal/quic/sent_packet.go | 7 +-- 12 files changed, 222 insertions(+), 37 deletions(-) diff --git a/internal/quic/congestion_reno.go b/internal/quic/congestion_reno.go index 982cbf4bb4..a539835247 100644 --- a/internal/quic/congestion_reno.go +++ b/internal/quic/congestion_reno.go @@ -7,6 +7,8 @@ package quic import ( + "context" + "log/slog" "math" "time" ) @@ -40,6 +42,9 @@ type ccReno struct { // true if we haven't sent that packet yet. sendOnePacketInRecovery bool + // inRecovery is set when we are in the recovery state. + inRecovery bool + // underutilized is set if the congestion window is underutilized // due to insufficient application data, flow control limits, or // anti-amplification limits. @@ -100,12 +105,19 @@ func (c *ccReno) canSend() bool { // congestion controller permits sending data, but no data is sent. // // https://www.rfc-editor.org/rfc/rfc9002#section-7.8 -func (c *ccReno) setUnderutilized(v bool) { +func (c *ccReno) setUnderutilized(log *slog.Logger, v bool) { + if c.underutilized == v { + return + } + oldState := c.state() c.underutilized = v + if logEnabled(log, QLogLevelPacket) { + logCongestionStateUpdated(log, oldState, c.state()) + } } // packetSent indicates that a packet has been sent. -func (c *ccReno) packetSent(now time.Time, space numberSpace, sent *sentPacket) { +func (c *ccReno) packetSent(now time.Time, log *slog.Logger, space numberSpace, sent *sentPacket) { if !sent.inFlight { return } @@ -185,7 +197,11 @@ func (c *ccReno) packetLost(now time.Time, space numberSpace, sent *sentPacket, } // packetBatchEnd is called at the end of processing a batch of acked or lost packets. -func (c *ccReno) packetBatchEnd(now time.Time, space numberSpace, rtt *rttState, maxAckDelay time.Duration) { +func (c *ccReno) packetBatchEnd(now time.Time, log *slog.Logger, space numberSpace, rtt *rttState, maxAckDelay time.Duration) { + if logEnabled(log, QLogLevelPacket) { + oldState := c.state() + defer func() { logCongestionStateUpdated(log, oldState, c.state()) }() + } if !c.ackLastLoss.IsZero() && !c.ackLastLoss.Before(c.recoveryStartTime) { // Enter the recovery state. // https://www.rfc-editor.org/rfc/rfc9002.html#section-7.3.2 @@ -196,8 +212,10 @@ func (c *ccReno) packetBatchEnd(now time.Time, space numberSpace, rtt *rttState, // Clear congestionPendingAcks to avoid increasing the congestion // window based on acks in a frame that sends us into recovery. c.congestionPendingAcks = 0 + c.inRecovery = true } else if c.congestionPendingAcks > 0 { // We are in slow start or congestion avoidance. + c.inRecovery = false if c.congestionWindow < c.slowStartThreshold { // When the congestion window is less than the slow start threshold, // we are in slow start and increase the window by the number of @@ -253,3 +271,38 @@ func (c *ccReno) minimumCongestionWindow() int { // https://www.rfc-editor.org/rfc/rfc9002.html#section-7.2-4 return 2 * c.maxDatagramSize } + +func logCongestionStateUpdated(log *slog.Logger, oldState, newState congestionState) { + if oldState == newState { + return + } + log.LogAttrs(context.Background(), QLogLevelPacket, + "recovery:congestion_state_updated", + slog.String("old", oldState.String()), + slog.String("new", newState.String()), + ) +} + +type congestionState string + +func (s congestionState) String() string { return string(s) } + +const ( + congestionSlowStart = congestionState("slow_start") + congestionCongestionAvoidance = congestionState("congestion_avoidance") + congestionApplicationLimited = congestionState("application_limited") + congestionRecovery = congestionState("recovery") +) + +func (c *ccReno) state() congestionState { + switch { + case c.inRecovery: + return congestionRecovery + case c.underutilized: + return congestionApplicationLimited + case c.congestionWindow < c.slowStartThreshold: + return congestionSlowStart + default: + return congestionCongestionAvoidance + } +} diff --git a/internal/quic/congestion_reno_test.go b/internal/quic/congestion_reno_test.go index e9af6452ca..cda7a90a80 100644 --- a/internal/quic/congestion_reno_test.go +++ b/internal/quic/congestion_reno_test.go @@ -470,7 +470,7 @@ func (c *ccTest) setRTT(smoothedRTT, rttvar time.Duration) { func (c *ccTest) setUnderutilized(v bool) { c.t.Helper() c.t.Logf("set underutilized = %v", v) - c.cc.setUnderutilized(v) + c.cc.setUnderutilized(nil, v) } func (c *ccTest) packetSent(space numberSpace, size int, fns ...func(*sentPacket)) *sentPacket { @@ -488,7 +488,7 @@ func (c *ccTest) packetSent(space numberSpace, size int, fns ...func(*sentPacket f(sent) } c.t.Logf("packet sent: num=%v.%v, size=%v", space, sent.num, sent.size) - c.cc.packetSent(c.now, space, sent) + c.cc.packetSent(c.now, nil, space, sent) return sent } @@ -519,7 +519,7 @@ func (c *ccTest) packetDiscarded(space numberSpace, sent *sentPacket) { func (c *ccTest) packetBatchEnd(space numberSpace) { c.t.Helper() c.t.Logf("(end of batch)") - c.cc.packetBatchEnd(c.now, space, &c.rtt, c.maxAckDelay) + c.cc.packetBatchEnd(c.now, nil, space, &c.rtt, c.maxAckDelay) } func (c *ccTest) wantCanSend(want bool) { diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 6d79013eb4..020bc81a45 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -210,7 +210,7 @@ func (c *Conn) discardKeys(now time.Time, space numberSpace) { case handshakeSpace: c.keysHandshake.discard() } - c.loss.discardKeys(now, space) + c.loss.discardKeys(now, c.log, space) } // receiveTransportParameters applies transport parameters sent by the peer. diff --git a/internal/quic/conn_loss.go b/internal/quic/conn_loss.go index 85bda314ec..623ebdd7c6 100644 --- a/internal/quic/conn_loss.go +++ b/internal/quic/conn_loss.go @@ -20,6 +20,10 @@ import "fmt" // See RFC 9000, Section 13.3 for a complete list of information which is retransmitted on loss. // https://www.rfc-editor.org/rfc/rfc9000#section-13.3 func (c *Conn) handleAckOrLoss(space numberSpace, sent *sentPacket, fate packetFate) { + if fate == packetLost && c.logEnabled(QLogLevelPacket) { + c.logPacketLost(space, sent) + } + // The list of frames in a sent packet is marshaled into a buffer in the sentPacket // by the packetWriter. Unmarshal that buffer here. This code must be kept in sync with // packetWriter.append*. diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 045bf861cd..b666ce8ebb 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -192,7 +192,7 @@ func (c *Conn) handleRetry(now time.Time, pkt []byte) { c.connIDState.handleRetryPacket(p.srcConnID) // We need to resend any data we've already sent in Initial packets. // We must not reuse already sent packet numbers. - c.loss.discardPackets(initialSpace, c.handleAckOrLoss) + c.loss.discardPackets(initialSpace, c.log, c.handleAckOrLoss) // TODO: Discard 0-RTT packets as well, once we support 0-RTT. } @@ -416,7 +416,7 @@ func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) if c.peerAckDelayExponent >= 0 { delay = ackDelay.Duration(uint8(c.peerAckDelayExponent)) } - c.loss.receiveAckEnd(now, space, delay, c.handleAckOrLoss) + c.loss.receiveAckEnd(now, c.log, space, delay, c.handleAckOrLoss) if space == appDataSpace { c.keysAppData.handleAckFor(largest) } diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index ccb467591b..575b8f9b4b 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -22,7 +22,10 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // Assumption: The congestion window is not underutilized. // If congestion control, pacing, and anti-amplification all permit sending, // but we have no packet to send, then we will declare the window underutilized. - c.loss.cc.setUnderutilized(false) + underutilized := false + defer func() { + c.loss.cc.setUnderutilized(c.log, underutilized) + }() // Send one datagram on each iteration of this loop, // until we hit a limit or run out of data to send. @@ -80,7 +83,6 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } sentInitial = c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysInitial.w, p) if sentInitial != nil { - c.idleHandlePacketSent(now, sentInitial) // Client initial packets and ack-eliciting server initial packaets // need to be sent in a datagram padded to at least 1200 bytes. // We can't add the padding yet, however, since we may want to @@ -111,8 +113,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { c.logPacketSent(packetTypeHandshake, pnum, p.srcConnID, p.dstConnID, c.w.packetLen(), c.w.payload()) } if sent := c.w.finishProtectedLongHeaderPacket(pnumMaxAcked, c.keysHandshake.w, p); sent != nil { - c.idleHandlePacketSent(now, sent) - c.loss.packetSent(now, handshakeSpace, sent) + c.packetSent(now, handshakeSpace, sent) if c.side == clientSide { // "[...] a client MUST discard Initial keys when it first // sends a Handshake packet [...]" @@ -142,8 +143,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { c.logPacketSent(packetType1RTT, pnum, nil, dstConnID, c.w.packetLen(), c.w.payload()) } if sent := c.w.finish1RTTPacket(pnum, pnumMaxAcked, dstConnID, &c.keysAppData); sent != nil { - c.idleHandlePacketSent(now, sent) - c.loss.packetSent(now, appDataSpace, sent) + c.packetSent(now, appDataSpace, sent) } } @@ -152,7 +152,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { if limit == ccOK { // We have nothing to send, and congestion control does not // block sending. The congestion window is underutilized. - c.loss.cc.setUnderutilized(true) + underutilized = true } return next } @@ -175,7 +175,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { // with a Handshake packet, then we've discarded Initial keys // since constructing the packet and shouldn't record it as in-flight. if c.keysInitial.canWrite() { - c.loss.packetSent(now, initialSpace, sentInitial) + c.packetSent(now, initialSpace, sentInitial) } } @@ -183,6 +183,11 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } } +func (c *Conn) packetSent(now time.Time, space numberSpace, sent *sentPacket) { + c.idleHandlePacketSent(now, sent) + c.loss.packetSent(now, c.log, space, sent) +} + func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, limit ccLimit) { if c.lifetime.localErr != nil { c.appendConnectionCloseFrame(now, space, c.lifetime.localErr) diff --git a/internal/quic/loss.go b/internal/quic/loss.go index a59081fd57..796b5f7a34 100644 --- a/internal/quic/loss.go +++ b/internal/quic/loss.go @@ -7,6 +7,8 @@ package quic import ( + "context" + "log/slog" "math" "time" ) @@ -179,7 +181,7 @@ func (c *lossState) nextNumber(space numberSpace) packetNumber { } // packetSent records a sent packet. -func (c *lossState) packetSent(now time.Time, space numberSpace, sent *sentPacket) { +func (c *lossState) packetSent(now time.Time, log *slog.Logger, space numberSpace, sent *sentPacket) { sent.time = now c.spaces[space].add(sent) size := sent.size @@ -187,13 +189,16 @@ func (c *lossState) packetSent(now time.Time, space numberSpace, sent *sentPacke c.antiAmplificationLimit = max(0, c.antiAmplificationLimit-size) } if sent.inFlight { - c.cc.packetSent(now, space, sent) + c.cc.packetSent(now, log, space, sent) c.pacer.packetSent(now, size, c.cc.congestionWindow, c.rtt.smoothedRTT) if sent.ackEliciting { c.spaces[space].lastAckEliciting = sent.num c.ptoExpired = false // reset expired PTO timer after sending probe } c.scheduleTimer(now) + if logEnabled(log, QLogLevelPacket) { + logBytesInFlight(log, c.cc.bytesInFlight) + } } if sent.ackEliciting { c.consecutiveNonAckElicitingPackets = 0 @@ -267,7 +272,7 @@ func (c *lossState) receiveAckRange(now time.Time, space numberSpace, rangeIndex // receiveAckEnd finishes processing an ack frame. // The lossf function is called for each packet newly detected as lost. -func (c *lossState) receiveAckEnd(now time.Time, space numberSpace, ackDelay time.Duration, lossf func(numberSpace, *sentPacket, packetFate)) { +func (c *lossState) receiveAckEnd(now time.Time, log *slog.Logger, space numberSpace, ackDelay time.Duration, lossf func(numberSpace, *sentPacket, packetFate)) { c.spaces[space].sentPacketList.clean() // Update the RTT sample when the largest acknowledged packet in the ACK frame // is newly acknowledged, and at least one newly acknowledged packet is ack-eliciting. @@ -286,13 +291,30 @@ func (c *lossState) receiveAckEnd(now time.Time, space numberSpace, ackDelay tim // https://www.rfc-editor.org/rfc/rfc9002.html#section-6.2.2.1-3 c.timer = time.Time{} c.detectLoss(now, lossf) - c.cc.packetBatchEnd(now, space, &c.rtt, c.maxAckDelay) + c.cc.packetBatchEnd(now, log, space, &c.rtt, c.maxAckDelay) + + if logEnabled(log, QLogLevelPacket) { + var ssthresh slog.Attr + if c.cc.slowStartThreshold != math.MaxInt { + ssthresh = slog.Int("ssthresh", c.cc.slowStartThreshold) + } + log.LogAttrs(context.Background(), QLogLevelPacket, + "recovery:metrics_updated", + slog.Duration("min_rtt", c.rtt.minRTT), + slog.Duration("smoothed_rtt", c.rtt.smoothedRTT), + slog.Duration("latest_rtt", c.rtt.latestRTT), + slog.Duration("rtt_variance", c.rtt.rttvar), + slog.Int("congestion_window", c.cc.congestionWindow), + slog.Int("bytes_in_flight", c.cc.bytesInFlight), + ssthresh, + ) + } } // discardPackets declares that packets within a number space will not be delivered // and that data contained in them should be resent. // For example, after receiving a Retry packet we discard already-sent Initial packets. -func (c *lossState) discardPackets(space numberSpace, lossf func(numberSpace, *sentPacket, packetFate)) { +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 @@ -300,10 +322,13 @@ func (c *lossState) discardPackets(space numberSpace, lossf func(numberSpace, *s lossf(numberSpace(space), sent, packetLost) } c.spaces[space].clean() + if logEnabled(log, QLogLevelPacket) { + logBytesInFlight(log, c.cc.bytesInFlight) + } } // discardKeys is called when dropping packet protection keys for a number space. -func (c *lossState) discardKeys(now time.Time, space numberSpace) { +func (c *lossState) discardKeys(now time.Time, log *slog.Logger, space numberSpace) { // 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) @@ -313,6 +338,9 @@ func (c *lossState) discardKeys(now time.Time, space numberSpace) { c.spaces[space].maxAcked = -1 c.spaces[space].lastAckEliciting = -1 c.scheduleTimer(now) + if logEnabled(log, QLogLevelPacket) { + logBytesInFlight(log, c.cc.bytesInFlight) + } } func (c *lossState) lossDuration() time.Duration { @@ -459,3 +487,10 @@ func (c *lossState) ptoBasePeriod() time.Duration { } return pto } + +func logBytesInFlight(log *slog.Logger, bytesInFlight int) { + log.LogAttrs(context.Background(), QLogLevelPacket, + "recovery:metrics_updated", + slog.Int("bytes_in_flight", bytesInFlight), + ) +} diff --git a/internal/quic/loss_test.go b/internal/quic/loss_test.go index efbf1649ec..1fb9662e4c 100644 --- a/internal/quic/loss_test.go +++ b/internal/quic/loss_test.go @@ -1060,7 +1060,7 @@ func TestLossPersistentCongestion(t *testing.T) { maxDatagramSize: 1200, }) test.send(initialSpace, 0, testSentPacketSize(1200)) - test.c.cc.setUnderutilized(true) + test.c.cc.setUnderutilized(nil, true) test.advance(10 * time.Millisecond) test.ack(initialSpace, 0*time.Millisecond, i64range[packetNumber]{0, 1}) @@ -1377,7 +1377,7 @@ func (c *lossTest) setRTTVar(d time.Duration) { func (c *lossTest) setUnderutilized(v bool) { c.t.Logf("set congestion window underutilized: %v", v) - c.c.cc.setUnderutilized(v) + c.c.cc.setUnderutilized(nil, v) } func (c *lossTest) advance(d time.Duration) { @@ -1438,7 +1438,7 @@ func (c *lossTest) send(spaceID numberSpace, opts ...any) { sent := &sentPacket{} *sent = prototype sent.num = num - c.c.packetSent(c.now, spaceID, sent) + c.c.packetSent(c.now, nil, spaceID, sent) } } @@ -1462,7 +1462,7 @@ func (c *lossTest) ack(spaceID numberSpace, ackDelay time.Duration, rs ...i64ran c.t.Logf("ack %v delay=%v [%v,%v)", spaceID, ackDelay, r.start, r.end) c.c.receiveAckRange(c.now, spaceID, i, r.start, r.end, c.onAckOrLoss) } - c.c.receiveAckEnd(c.now, spaceID, ackDelay, c.onAckOrLoss) + c.c.receiveAckEnd(c.now, nil, spaceID, ackDelay, c.onAckOrLoss) } func (c *lossTest) onAckOrLoss(space numberSpace, sent *sentPacket, fate packetFate) { @@ -1491,7 +1491,7 @@ func (c *lossTest) discardKeys(spaceID numberSpace) { c.t.Helper() c.checkUnexpectedEvents() c.t.Logf("discard %s keys", spaceID) - c.c.discardKeys(c.now, spaceID) + c.c.discardKeys(c.now, nil, spaceID) } func (c *lossTest) setMaxAckDelay(d time.Duration) { diff --git a/internal/quic/packet_writer.go b/internal/quic/packet_writer.go index b4e54ce4b0..85149f6071 100644 --- a/internal/quic/packet_writer.go +++ b/internal/quic/packet_writer.go @@ -141,7 +141,7 @@ func (w *packetWriter) finishProtectedLongHeaderPacket(pnumMaxAcked packetNumber hdr = appendPacketNumber(hdr, p.num, pnumMaxAcked) k.protect(hdr[w.pktOff:], w.b[len(hdr):], pnumOff-w.pktOff, p.num) - return w.finish(p.num) + return w.finish(p.ptype, p.num) } // start1RTTPacket starts writing a 1-RTT (short header) packet. @@ -183,7 +183,7 @@ func (w *packetWriter) finish1RTTPacket(pnum, pnumMaxAcked packetNumber, dstConn hdr = appendPacketNumber(hdr, pnum, pnumMaxAcked) w.padPacketLength(pnumLen) k.protect(hdr[w.pktOff:], w.b[len(hdr):], pnumOff-w.pktOff, pnum) - return w.finish(pnum) + return w.finish(packetType1RTT, pnum) } // padPacketLength pads out the payload of the current packet to the minimum size, @@ -204,9 +204,10 @@ func (w *packetWriter) padPacketLength(pnumLen int) int { } // finish finishes the current packet after protection is applied. -func (w *packetWriter) finish(pnum packetNumber) *sentPacket { +func (w *packetWriter) finish(ptype packetType, pnum packetNumber) *sentPacket { w.b = w.b[:len(w.b)+aeadOverhead] w.sent.size = len(w.b) - w.pktOff + w.sent.ptype = ptype w.sent.num = pnum sent := w.sent w.sent = nil diff --git a/internal/quic/qlog.go b/internal/quic/qlog.go index 82ad92ac8c..e37e2f8cea 100644 --- a/internal/quic/qlog.go +++ b/internal/quic/qlog.go @@ -39,7 +39,11 @@ const ( ) func (c *Conn) logEnabled(level slog.Level) bool { - return c.log != nil && c.log.Enabled(context.Background(), level) + return logEnabled(c.log, level) +} + +func logEnabled(log *slog.Logger, level slog.Level) bool { + return log != nil && log.Enabled(context.Background(), level) } // slogHexstring returns a slog.Attr for a value of the hexstring type. @@ -252,3 +256,13 @@ func (c *Conn) packetFramesAttr(payload []byte) slog.Attr { } return slog.Any("frames", frames) } + +func (c *Conn) logPacketLost(space numberSpace, sent *sentPacket) { + c.log.LogAttrs(context.Background(), QLogLevelPacket, + "recovery:packet_lost", + slog.Group("header", + slog.String("packet_type", sent.ptype.qlogString()), + slog.Uint64("packet_number", uint64(sent.num)), + ), + ) +} diff --git a/internal/quic/qlog_test.go b/internal/quic/qlog_test.go index e98b118386..7ad65524c5 100644 --- a/internal/quic/qlog_test.go +++ b/internal/quic/qlog_test.go @@ -159,6 +159,77 @@ func TestQLogConnectionClosedTrigger(t *testing.T) { } } +func TestQLogRecovery(t *testing.T) { + qr := &qlogRecord{} + tc, s := newTestConnAndLocalStream(t, clientSide, uniStream, + permissiveTransportParameters, qr.config) + + // Ignore events from the handshake. + qr.ev = nil + + data := make([]byte, 16) + s.Write(data) + s.CloseWrite() + tc.wantFrame("created stream 0", + packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, uniStream, 0), + fin: true, + data: data, + }) + tc.writeAckForAll() + tc.wantIdle("connection should be idle now") + + // Don't check the contents of fields, but verify that recovery metrics are logged. + qr.wantEvents(t, jsonEvent{ + "name": "recovery:metrics_updated", + "data": map[string]any{ + "bytes_in_flight": nil, + }, + }, jsonEvent{ + "name": "recovery:metrics_updated", + "data": map[string]any{ + "bytes_in_flight": 0, + "congestion_window": nil, + "latest_rtt": nil, + "min_rtt": nil, + "rtt_variance": nil, + "smoothed_rtt": nil, + }, + }) +} + +func TestQLogLoss(t *testing.T) { + qr := &qlogRecord{} + tc, s := newTestConnAndLocalStream(t, clientSide, uniStream, + permissiveTransportParameters, qr.config) + + // Ignore events from the handshake. + qr.ev = nil + + data := make([]byte, 16) + s.Write(data) + s.CloseWrite() + tc.wantFrame("created stream 0", + packetType1RTT, debugFrameStream{ + id: newStreamID(clientSide, uniStream, 0), + fin: true, + data: data, + }) + + const pto = false + tc.triggerLossOrPTO(packetType1RTT, pto) + + qr.wantEvents(t, jsonEvent{ + "name": "recovery:packet_lost", + "data": map[string]any{ + "header": map[string]any{ + "packet_number": nil, + "packet_type": "1RTT", + }, + }, + }) +} + type nopCloseWriter struct { io.Writer } @@ -193,14 +264,15 @@ func jsonPartialEqual(got, want any) (equal bool) { } return v } + if want == nil { + return true // match anything + } got = cmpval(got) want = cmpval(want) if reflect.TypeOf(got) != reflect.TypeOf(want) { return false } switch w := want.(type) { - case nil: - // Match anything. case map[string]any: // JSON object: Every field in want must match a field in got. g := got.(map[string]any) diff --git a/internal/quic/sent_packet.go b/internal/quic/sent_packet.go index 4f11aa1368..194cdc9fa0 100644 --- a/internal/quic/sent_packet.go +++ b/internal/quic/sent_packet.go @@ -14,9 +14,10 @@ import ( // A sentPacket tracks state related to an in-flight packet we sent, // to be committed when the peer acks it or resent if the packet is lost. type sentPacket struct { - num packetNumber - size int // size in bytes - time time.Time // time sent + num packetNumber + size int // size in bytes + time time.Time // time sent + ptype packetType 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 From 93be8fe122ca52e008630144471e9473d94cc43f Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 28 Nov 2023 09:19:01 -0800 Subject: [PATCH 131/168] quic: log packet_dropped events Log unparsable or otherwise discarded packets. For golang/go#58547 Change-Id: Ief64174d91c93691bd524515aa6518e487543ced Reviewed-on: https://go-review.googlesource.com/c/net/+/564017 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_recv.go | 15 ++++++++++++--- internal/quic/qlog_test.go | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index b666ce8ebb..1b3219723d 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -8,6 +8,7 @@ package quic import ( "bytes" + "context" "encoding/binary" "errors" "time" @@ -56,9 +57,16 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { if len(buf) == len(dgram.b) && len(buf) > statelessResetTokenLen { var token statelessResetToken copy(token[:], buf[len(buf)-len(token):]) - c.handleStatelessReset(now, token) + if c.handleStatelessReset(now, token) { + return + } } // Invalid data at the end of a datagram is ignored. + if c.logEnabled(QLogLevelPacket) { + c.log.LogAttrs(context.Background(), QLogLevelPacket, + "connectivity:packet_dropped", + ) + } break } c.idleHandlePacketReceived(now) @@ -562,10 +570,11 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa var errStatelessReset = errors.New("received stateless reset") -func (c *Conn) handleStatelessReset(now time.Time, resetToken statelessResetToken) { +func (c *Conn) handleStatelessReset(now time.Time, resetToken statelessResetToken) (valid bool) { if !c.connIDState.isValidStatelessResetToken(resetToken) { - return + return false } c.setFinalError(errStatelessReset) c.enterDraining(now) + return true } diff --git a/internal/quic/qlog_test.go b/internal/quic/qlog_test.go index 7ad65524c5..6c79c6cf49 100644 --- a/internal/quic/qlog_test.go +++ b/internal/quic/qlog_test.go @@ -7,6 +7,7 @@ package quic import ( + "bytes" "encoding/hex" "encoding/json" "fmt" @@ -230,6 +231,27 @@ func TestQLogLoss(t *testing.T) { }) } +func TestQLogPacketDropped(t *testing.T) { + qr := &qlogRecord{} + tc := newTestConn(t, clientSide, permissiveTransportParameters, qr.config) + tc.handshake() + + // A garbage-filled datagram with a DCID matching this connection. + dgram := bytes.Join([][]byte{ + {headerFormShort | fixedBit}, + testLocalConnID(0), + make([]byte, 100), + []byte{1, 2, 3, 4}, // random data, to avoid this looking like a stateless reset + }, nil) + tc.endpoint.write(&datagram{ + b: dgram, + }) + + qr.wantEvents(t, jsonEvent{ + "name": "connectivity:packet_dropped", + }) +} + type nopCloseWriter struct { io.Writer } From 117945d00a55197e260d73c6272a2588d39bdebe Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 20 Nov 2023 16:05:25 -0800 Subject: [PATCH 132/168] quic: add throughput and stream creation benchmarks For golang/go#58547 Change-Id: Ie62fcf596bf020bda5a167f7a0d3d95bac9e591a Reviewed-on: https://go-review.googlesource.com/c/net/+/564475 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/bench_test.go | 99 ++++++++++++++++++++++++++++++++++ internal/quic/endpoint_test.go | 4 +- 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 internal/quic/bench_test.go diff --git a/internal/quic/bench_test.go b/internal/quic/bench_test.go new file mode 100644 index 0000000000..f883b788c2 --- /dev/null +++ b/internal/quic/bench_test.go @@ -0,0 +1,99 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package quic + +import ( + "context" + "fmt" + "io" + "math" + "testing" +) + +// BenchmarkThroughput is based on the crypto/tls benchmark of the same name. +func BenchmarkThroughput(b *testing.B) { + for size := 1; size <= 64; size <<= 1 { + name := fmt.Sprintf("%dMiB", size) + b.Run(name, func(b *testing.B) { + throughput(b, int64(size<<20)) + }) + } +} + +func throughput(b *testing.B, totalBytes int64) { + // Same buffer size as crypto/tls's BenchmarkThroughput, for consistency. + const bufsize = 32 << 10 + + cli, srv := newLocalConnPair(b, &Config{}, &Config{}) + + go func() { + buf := make([]byte, bufsize) + for i := 0; i < b.N; i++ { + sconn, err := srv.AcceptStream(context.Background()) + if err != nil { + panic(fmt.Errorf("AcceptStream: %v", err)) + } + if _, err := io.CopyBuffer(sconn, sconn, buf); err != nil { + panic(fmt.Errorf("CopyBuffer: %v", err)) + } + sconn.Close() + } + }() + + b.SetBytes(totalBytes) + buf := make([]byte, bufsize) + chunks := int(math.Ceil(float64(totalBytes) / float64(len(buf)))) + for i := 0; i < b.N; i++ { + cconn, err := cli.NewStream(context.Background()) + if err != nil { + b.Fatalf("NewStream: %v", err) + } + closec := make(chan struct{}) + go func() { + defer close(closec) + buf := make([]byte, bufsize) + if _, err := io.CopyBuffer(io.Discard, cconn, buf); err != nil { + panic(fmt.Errorf("Discard: %v", err)) + } + }() + for j := 0; j < chunks; j++ { + _, err := cconn.Write(buf) + if err != nil { + b.Fatalf("Write: %v", err) + } + } + cconn.CloseWrite() + <-closec + cconn.Close() + } +} + +func BenchmarkStreamCreation(b *testing.B) { + cli, srv := newLocalConnPair(b, &Config{}, &Config{}) + + go func() { + for i := 0; i < b.N; i++ { + sconn, err := srv.AcceptStream(context.Background()) + if err != nil { + panic(fmt.Errorf("AcceptStream: %v", err)) + } + sconn.Close() + } + }() + + buf := make([]byte, 1) + for i := 0; i < b.N; i++ { + cconn, err := cli.NewStream(context.Background()) + if err != nil { + b.Fatalf("NewStream: %v", err) + } + cconn.Write(buf) + cconn.Flush() + cconn.Read(buf) + cconn.Close() + } +} diff --git a/internal/quic/endpoint_test.go b/internal/quic/endpoint_test.go index 16c3e0bce7..6d103f0612 100644 --- a/internal/quic/endpoint_test.go +++ b/internal/quic/endpoint_test.go @@ -63,7 +63,7 @@ func TestStreamTransfer(t *testing.T) { } } -func newLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverConn *Conn) { +func newLocalConnPair(t testing.TB, conf1, conf2 *Config) (clientConn, serverConn *Conn) { t.Helper() ctx := context.Background() e1 := newLocalEndpoint(t, serverSide, conf1) @@ -79,7 +79,7 @@ func newLocalConnPair(t *testing.T, conf1, conf2 *Config) (clientConn, serverCon return c2, c1 } -func newLocalEndpoint(t *testing.T, side connSide, conf *Config) *Endpoint { +func newLocalEndpoint(t testing.TB, side connSide, conf *Config) *Endpoint { t.Helper() if conf.TLSConfig == nil { newConf := *conf From e94da73eedb3c3244dcc3857c74accb642dd8eac Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 12 Dec 2023 15:48:00 -0800 Subject: [PATCH 133/168] quic: reduce ack frequency after the first 100 packets RFC 9000 recommends sending an ack for every second ack-eliciting packet received. This frequency is high enough to have a noticeable impact on performance. Follow the approach used by Google QUICHE: Ack every other packet for the first 100 packets, and then switch to acking every 10th packet. (Various other implementations also use a reduced ack frequency; see Custura et al., 2022.) For golang/go#58547 Change-Id: Idc7051cec23c279811030eb555bc49bb888d6795 Reviewed-on: https://go-review.googlesource.com/c/net/+/564476 Reviewed-by: Jonathan Amsterdam Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI --- internal/quic/acks.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/internal/quic/acks.go b/internal/quic/acks.go index ba860efb2b..039b7b46e6 100644 --- a/internal/quic/acks.go +++ b/internal/quic/acks.go @@ -130,12 +130,19 @@ func (acks *ackState) mustAckImmediately(space numberSpace, num packetNumber) bo // 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 + // "[...] 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 + // + // This ack frequency takes a substantial toll on performance, however. + // Follow the behavior of Google QUICHE: + // Ack every other packet for the first 100 packets, and then ack every 10th packet. + // This keeps ack frequency high during the beginning of slow start when CWND is + // increasing rapidly. + packetsBeforeAck := 2 + if acks.seen.max() > 100 { + packetsBeforeAck = 10 } - return false + return acks.unackedAckEliciting >= packetsBeforeAck } // shouldSendAck reports whether the connection should send an ACK frame at this time, From dda3687b193e5e1fb31df72be5e0bc6ae7841d2e Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 31 Oct 2023 14:06:26 -0700 Subject: [PATCH 134/168] quic: add Stream.ReadByte, Stream.WriteByte Currently unoptimized and slow. Adding along with a benchmark to compare to the fast-path followup. For golang/go#58547 Change-Id: If02b65e6e7cfc770d3f949e5fb9fbb9d8a765a90 Reviewed-on: https://go-review.googlesource.com/c/net/+/564477 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/bench_test.go | 71 +++++++++++++++++++++++++++++++++++++ internal/quic/stream.go | 14 ++++++++ 2 files changed, 85 insertions(+) diff --git a/internal/quic/bench_test.go b/internal/quic/bench_test.go index f883b788c2..636b71327e 100644 --- a/internal/quic/bench_test.go +++ b/internal/quic/bench_test.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "math" + "sync" "testing" ) @@ -72,6 +73,76 @@ func throughput(b *testing.B, totalBytes int64) { } } +func BenchmarkReadByte(b *testing.B) { + cli, srv := newLocalConnPair(b, &Config{}, &Config{}) + + var wg sync.WaitGroup + defer wg.Wait() + + wg.Add(1) + go func() { + defer wg.Done() + buf := make([]byte, 1<<20) + sconn, err := srv.AcceptStream(context.Background()) + if err != nil { + panic(fmt.Errorf("AcceptStream: %v", err)) + } + for { + if _, err := sconn.Write(buf); err != nil { + break + } + sconn.Flush() + } + }() + + b.SetBytes(1) + cconn, err := cli.NewStream(context.Background()) + if err != nil { + b.Fatalf("NewStream: %v", err) + } + cconn.Flush() + for i := 0; i < b.N; i++ { + _, err := cconn.ReadByte() + if err != nil { + b.Fatalf("ReadByte: %v", err) + } + } + cconn.Close() +} + +func BenchmarkWriteByte(b *testing.B) { + cli, srv := newLocalConnPair(b, &Config{}, &Config{}) + + var wg sync.WaitGroup + defer wg.Wait() + + wg.Add(1) + go func() { + defer wg.Done() + sconn, err := srv.AcceptStream(context.Background()) + if err != nil { + panic(fmt.Errorf("AcceptStream: %v", err)) + } + n, err := io.Copy(io.Discard, sconn) + if n != int64(b.N) || err != nil { + b.Errorf("server io.Copy() = %v, %v; want %v, nil", n, err, b.N) + } + }() + + b.SetBytes(1) + cconn, err := cli.NewStream(context.Background()) + if err != nil { + b.Fatalf("NewStream: %v", err) + } + cconn.Flush() + for i := 0; i < b.N; i++ { + if err := cconn.WriteByte(0); err != nil { + b.Fatalf("WriteByte: %v", err) + } + } + cconn.Close() +} + func BenchmarkStreamCreation(b *testing.B) { cli, srv := newLocalConnPair(b, &Config{}, &Config{}) diff --git a/internal/quic/stream.go b/internal/quic/stream.go index d0122b9518..670b342639 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -245,6 +245,13 @@ func (s *Stream) Read(b []byte) (n int, err error) { return len(b), nil } +// ReadByte reads and returns a single byte from the stream. +func (s *Stream) ReadByte() (byte, error) { + var b [1]byte + _, err := s.Read(b[:]) + return b[0], err +} + // shouldUpdateFlowControl determines whether to send a flow control window update. // // We want to balance keeping the peer well-supplied with flow control with not sending @@ -326,6 +333,13 @@ func (s *Stream) Write(b []byte) (n int, err error) { return n, nil } +// WriteBytes writes a single byte to the stream. +func (s *Stream) WriteByte(c byte) error { + b := [1]byte{c} + _, err := s.Write(b[:]) + return err +} + // Flush flushes data written to the stream. // It does not wait for the peer to acknowledge receipt of the data. // Use Close to wait for the peer's acknowledgement. From cc568eace4e2768d6befe9748ee0f3cd4edd9a10 Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Tue, 20 Feb 2024 12:29:30 +0100 Subject: [PATCH 135/168] internal/quic: use slices.Equal in TestAcksSent The module go.mod uses go 1.18 and acks_test.go has a go:build go1.21 tag. Change-Id: Ic0785bcb4795bedecc6a752f5e67a967851237e6 Reviewed-on: https://go-review.googlesource.com/c/net/+/565137 Reviewed-by: Than McIntosh Auto-Submit: Tobias Klauser Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/acks_test.go | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/internal/quic/acks_test.go b/internal/quic/acks_test.go index 4f1032910f..d10f917ad9 100644 --- a/internal/quic/acks_test.go +++ b/internal/quic/acks_test.go @@ -7,6 +7,7 @@ package quic import ( + "slices" "testing" "time" ) @@ -198,7 +199,7 @@ func TestAcksSent(t *testing.T) { if len(gotNums) == 0 { wantDelay = 0 } - if !slicesEqual(gotNums, test.wantAcks) || gotDelay != wantDelay { + if !slices.Equal(gotNums, test.wantAcks) || gotDelay != wantDelay { t.Errorf("acks.acksToSend(T+%v) = %v, %v; want %v, %v", delay, gotNums, gotDelay, test.wantAcks, wantDelay) } } @@ -206,20 +207,6 @@ func TestAcksSent(t *testing.T) { } } -// 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() From 08d27e39b9ef291f25ae7e4d34440c8d89d6b7f7 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 27 Nov 2023 09:07:49 -0800 Subject: [PATCH 136/168] quic: fast path for stream reads Keep a reference to the next chunk of bytes available for reading in an unsynchronized buffer. Read and ReadByte calls read from this buffer when possible, avoiding the need to lock the stream. This change makes it unnecessary to wrap a stream in a *bytes.Buffer when making small reads, at the expense of making reads concurrency-unsafe. Since the quic package is a low-level one and this lets us avoid an extra buffer in the HTTP/3 implementation, the tradeoff seems worthwhile. For golang/go#58547 Change-Id: Ib3ca446311974571c2367295b302f36a6349b00d Reviewed-on: https://go-review.googlesource.com/c/net/+/564495 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn_flow_test.go | 52 ++++++++++++------------- internal/quic/conn_loss_test.go | 20 +++++++--- internal/quic/pipe.go | 23 ++++++++--- internal/quic/stream.go | 67 +++++++++++++++++++++++++++------ internal/quic/stream_test.go | 30 ++++++++++++++- 5 files changed, 143 insertions(+), 49 deletions(-) diff --git a/internal/quic/conn_flow_test.go b/internal/quic/conn_flow_test.go index 8e04e20d9e..260684bdbc 100644 --- a/internal/quic/conn_flow_test.go +++ b/internal/quic/conn_flow_test.go @@ -17,33 +17,29 @@ func TestConnInflowReturnOnRead(t *testing.T) { }) tc.writeFrames(packetType1RTT, debugFrameStream{ id: s.id, - data: make([]byte, 64), + data: make([]byte, 8), }) - const readSize = 8 - if n, err := s.Read(make([]byte, readSize)); n != readSize || err != nil { - t.Fatalf("s.Read() = %v, %v; want %v, nil", n, err, readSize) - } - tc.wantFrame("available window increases, send a MAX_DATA", - packetType1RTT, debugFrameMaxData{ - max: 64 + readSize, - }) - if n, err := s.Read(make([]byte, 64)); n != 64-readSize || err != nil { - t.Fatalf("s.Read() = %v, %v; want %v, nil", n, err, 64-readSize) + if n, err := s.Read(make([]byte, 8)); n != 8 || err != nil { + t.Fatalf("s.Read() = %v, %v; want %v, nil", n, err, 8) } tc.wantFrame("available window increases, send a MAX_DATA", packetType1RTT, debugFrameMaxData{ - max: 128, + max: 64 + 8, }) // Peer can write up to the new limit. tc.writeFrames(packetType1RTT, debugFrameStream{ id: s.id, - off: 64, + off: 8, data: make([]byte, 64), }) - tc.wantIdle("connection is idle") - if n, err := s.Read(make([]byte, 64)); n != 64 || err != nil { - t.Fatalf("offset 64: s.Read() = %v, %v; want %v, nil", n, err, 64) + if n, err := s.Read(make([]byte, 64+1)); n != 64 { + t.Fatalf("s.Read() = %v, %v; want %v, anything", n, err, 64) } + tc.wantFrame("available window increases, send a MAX_DATA", + packetType1RTT, debugFrameMaxData{ + max: 64 + 8 + 64, + }) + tc.wantIdle("connection is idle") } func TestConnInflowReturnOnRacingReads(t *testing.T) { @@ -63,11 +59,11 @@ func TestConnInflowReturnOnRacingReads(t *testing.T) { tc.ignoreFrame(frameTypeAck) tc.writeFrames(packetType1RTT, debugFrameStream{ id: newStreamID(clientSide, uniStream, 0), - data: make([]byte, 32), + data: make([]byte, 16), }) tc.writeFrames(packetType1RTT, debugFrameStream{ id: newStreamID(clientSide, uniStream, 1), - data: make([]byte, 32), + data: make([]byte, 1), }) s1, err := tc.conn.AcceptStream(ctx) if err != nil { @@ -203,7 +199,6 @@ func TestConnInflowResetViolation(t *testing.T) { } func TestConnInflowMultipleStreams(t *testing.T) { - ctx := canceledContext() tc := newTestConn(t, serverSide, func(c *Config) { c.MaxConnReadBufferSize = 128 }) @@ -219,12 +214,9 @@ func TestConnInflowMultipleStreams(t *testing.T) { } { tc.writeFrames(packetType1RTT, debugFrameStream{ id: id, - data: make([]byte, 32), + data: make([]byte, 1), }) - s, err := tc.conn.AcceptStream(ctx) - if err != nil { - t.Fatalf("AcceptStream() = %v", err) - } + s := tc.acceptStream() streams = append(streams, s) if n, err := s.Read(make([]byte, 1)); err != nil || n != 1 { t.Fatalf("s.Read() = %v, %v; want 1, nil", n, err) @@ -232,8 +224,16 @@ func TestConnInflowMultipleStreams(t *testing.T) { } tc.wantIdle("streams have read data, but not enough to update MAX_DATA") - if n, err := streams[0].Read(make([]byte, 32)); err != nil || n != 31 { - t.Fatalf("s.Read() = %v, %v; want 31, nil", n, err) + for _, s := range streams { + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + off: 1, + data: make([]byte, 31), + }) + } + + if n, err := streams[0].Read(make([]byte, 32)); n != 31 { + t.Fatalf("s.Read() = %v, %v; want 31, anything", n, err) } tc.wantFrame("read enough data to trigger a MAX_DATA update", packetType1RTT, debugFrameMaxData{ diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index 876ffd093d..86ef23db0b 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -308,9 +308,9 @@ func TestLostMaxDataFrame(t *testing.T) { tc.writeFrames(packetType1RTT, debugFrameStream{ id: s.id, off: 0, - data: make([]byte, maxWindowSize), + data: make([]byte, maxWindowSize-1), }) - if n, err := s.Read(buf[:maxWindowSize-1]); err != nil || n != maxWindowSize-1 { + if n, err := s.Read(buf[:maxWindowSize]); err != nil || n != maxWindowSize-1 { t.Fatalf("Read() = %v, %v; want %v, nil", n, err, maxWindowSize-1) } tc.wantFrame("conn window is extended after reading data", @@ -319,7 +319,12 @@ func TestLostMaxDataFrame(t *testing.T) { }) // MAX_DATA = 64, which is only one more byte, so we don't send the frame. - if n, err := s.Read(buf); err != nil || n != 1 { + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + off: maxWindowSize - 1, + data: make([]byte, 1), + }) + if n, err := s.Read(buf[:1]); err != nil || n != 1 { t.Fatalf("Read() = %v, %v; want %v, nil", n, err, 1) } tc.wantIdle("read doesn't extend window enough to send another MAX_DATA") @@ -348,9 +353,9 @@ func TestLostMaxStreamDataFrame(t *testing.T) { tc.writeFrames(packetType1RTT, debugFrameStream{ id: s.id, off: 0, - data: make([]byte, maxWindowSize), + data: make([]byte, maxWindowSize-1), }) - if n, err := s.Read(buf[:maxWindowSize-1]); err != nil || n != maxWindowSize-1 { + if n, err := s.Read(buf[:maxWindowSize]); err != nil || n != maxWindowSize-1 { t.Fatalf("Read() = %v, %v; want %v, nil", n, err, maxWindowSize-1) } tc.wantFrame("stream window is extended after reading data", @@ -360,6 +365,11 @@ func TestLostMaxStreamDataFrame(t *testing.T) { }) // MAX_STREAM_DATA = 64, which is only one more byte, so we don't send the frame. + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + off: maxWindowSize - 1, + data: make([]byte, 1), + }) if n, err := s.Read(buf); err != nil || n != 1 { t.Fatalf("Read() = %v, %v; want %v, nil", n, err, 1) } diff --git a/internal/quic/pipe.go b/internal/quic/pipe.go index d3a448df34..42a0049da9 100644 --- a/internal/quic/pipe.go +++ b/internal/quic/pipe.go @@ -17,14 +17,14 @@ import ( // Writing past the end of the window extends it. // Data may be discarded from the start of the pipe, advancing the window. type pipe struct { - start int64 - end int64 - head *pipebuf - tail *pipebuf + start int64 // stream position of first stored byte + end int64 // stream position just past the last stored byte + head *pipebuf // if non-nil, then head.off + len(head.b) > start + tail *pipebuf // if non-nil, then tail.off + len(tail.b) == end } type pipebuf struct { - off int64 + off int64 // stream position of b[0] b []byte next *pipebuf } @@ -111,6 +111,7 @@ func (p *pipe) copy(off int64, b []byte) { // read calls f with the data in [off, off+n) // The data may be provided sequentially across multiple calls to f. +// Note that read (unlike an io.Reader) does not consume the read data. func (p *pipe) read(off int64, n int, f func([]byte) error) error { if off < p.start { panic("invalid read range") @@ -135,6 +136,18 @@ func (p *pipe) read(off int64, n int, f func([]byte) error) error { return nil } +// peek returns a reference to up to n bytes of internal data buffer, starting at p.start. +// The returned slice is valid until the next call to discardBefore. +// The length of the returned slice will be in the range [0,n]. +func (p *pipe) peek(n int64) []byte { + pb := p.head + if pb == nil { + return nil + } + b := pb.b[p.start-pb.off:] + return b[:min(int64(len(b)), n)] +} + // discardBefore discards all data prior to off. func (p *pipe) discardBefore(off int64) { for p.head != nil && p.head.end() < off { diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 670b342639..17ca8b7d6d 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -23,7 +23,7 @@ type Stream struct { inctx context.Context outctx context.Context - // ingate's lock guards all receive-related state. + // ingate's lock guards receive-related state. // // The gate condition is set if a read from the stream will not block, // either because the stream has available data or because the read will fail. @@ -37,7 +37,7 @@ type Stream struct { inclosed sentVal // set by CloseRead inresetcode int64 // RESET_STREAM code received from the peer; -1 if not reset - // outgate's lock guards all send-related state. + // outgate's lock guards send-related state. // // The gate condition is set if a write to the stream will not block, // either because the stream has available flow control or because @@ -57,6 +57,10 @@ type Stream struct { outresetcode uint64 // reset code to send in RESET_STREAM outdone chan struct{} // closed when all data sent + // Unsynchronized buffers, used for lock-free fast path. + inbuf []byte // received data + inbufoff int // bytes of inbuf which have been consumed + // Atomic stream state bits. // // These bits provide a fast way to coordinate between the @@ -202,16 +206,35 @@ func (s *Stream) IsWriteOnly() bool { // returning all data sent by the peer. // If the peer aborts reads on the stream, Read returns // an error wrapping StreamResetCode. +// +// It is not safe to call Read concurrently. func (s *Stream) Read(b []byte) (n int, err error) { if s.IsWriteOnly() { return 0, errors.New("read from write-only stream") } + if len(s.inbuf) > s.inbufoff { + // Fast path: If s.inbuf contains unread bytes, return them immediately + // without taking a lock. + n = copy(b, s.inbuf[s.inbufoff:]) + s.inbufoff += n + return n, nil + } if err := s.ingate.waitAndLock(s.inctx, s.conn.testHooks); err != nil { return 0, err } + if s.inbufoff > 0 { + // Discard bytes consumed by the fast path above. + s.in.discardBefore(s.in.start + int64(s.inbufoff)) + s.inbufoff = 0 + s.inbuf = nil + } + // bytesRead contains the number of bytes of connection-level flow control to return. + // We return flow control for bytes read by this Read call, as well as bytes moved + // to the fast-path read buffer (s.inbuf). + var bytesRead int64 defer func() { s.inUnlock() - s.conn.handleStreamBytesReadOffLoop(int64(n)) // must be done with ingate unlocked + s.conn.handleStreamBytesReadOffLoop(bytesRead) // must be done with ingate unlocked }() if s.inresetcode != -1 { return 0, fmt.Errorf("stream reset by peer: %w", StreamErrorCode(s.inresetcode)) @@ -229,27 +252,48 @@ func (s *Stream) Read(b []byte) (n int, err error) { if size := int(s.inset[0].end - s.in.start); size < len(b) { b = b[:size] } + bytesRead = int64(len(b)) start := s.in.start end := start + int64(len(b)) s.in.copy(start, b) s.in.discardBefore(end) + if end == s.insize { + // We have read up to the end of the stream. + // No need to update stream flow control. + return len(b), io.EOF + } + if len(s.inset) > 0 && s.inset[0].start <= s.in.start && s.inset[0].end > s.in.start { + // If we have more readable bytes available, put the next chunk of data + // in s.inbuf for lock-free reads. + s.inbuf = s.in.peek(s.inset[0].end - s.in.start) + bytesRead += int64(len(s.inbuf)) + } if s.insize == -1 || s.insize > s.inwin { - if shouldUpdateFlowControl(s.inmaxbuf, s.in.start+s.inmaxbuf-s.inwin) { + newWindow := s.in.start + int64(len(s.inbuf)) + s.inmaxbuf + addedWindow := newWindow - s.inwin + if shouldUpdateFlowControl(s.inmaxbuf, addedWindow) { // Update stream flow control with a STREAM_MAX_DATA frame. s.insendmax.setUnsent() } } - if end == s.insize { - return len(b), io.EOF - } return len(b), nil } // ReadByte reads and returns a single byte from the stream. +// +// It is not safe to call ReadByte concurrently. func (s *Stream) ReadByte() (byte, error) { + if len(s.inbuf) > s.inbufoff { + b := s.inbuf[s.inbufoff] + s.inbufoff++ + return b, nil + } var b [1]byte - _, err := s.Read(b[:]) - return b[0], err + n, err := s.Read(b[:]) + if n > 0 { + return b[0], err + } + return 0, err } // shouldUpdateFlowControl determines whether to send a flow control window update. @@ -507,8 +551,9 @@ func (s *Stream) inUnlock() { // inUnlockNoQueue is inUnlock, // but reports whether s has frames to write rather than notifying the Conn. func (s *Stream) inUnlockNoQueue() streamState { - canRead := s.inset.contains(s.in.start) || // data available to read - s.insize == s.in.start || // at EOF + nextByte := s.in.start + int64(len(s.inbuf)) + canRead := s.inset.contains(nextByte) || // data available to read + s.insize == s.in.start+int64(len(s.inbuf)) || // at EOF s.inresetcode != -1 || // reset by peer s.inclosed.isSet() // closed locally defer s.ingate.unlock(canRead) diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index 08e89b24c4..d1cfb34db0 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -538,6 +538,32 @@ func TestStreamReceiveDuplicateDataDoesNotViolateLimits(t *testing.T) { }) } +func TestStreamReceiveEmptyEOF(t *testing.T) { + // A stream receives some data, we read a byte of that data + // (causing the rest to be pulled into the s.inbuf buffer), + // and then we receive a FIN with no additional data. + testStreamTypes(t, "", func(t *testing.T, styp streamType) { + tc, s := newTestConnAndRemoteStream(t, serverSide, styp, permissiveTransportParameters) + want := []byte{1, 2, 3} + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + data: want, + }) + if got, err := s.ReadByte(); got != want[0] || err != nil { + t.Fatalf("s.ReadByte() = %v, %v; want %v, nil", got, err, want[0]) + } + + tc.writeFrames(packetType1RTT, debugFrameStream{ + id: s.id, + off: 3, + fin: true, + }) + if got, err := io.ReadAll(s); !bytes.Equal(got, want[1:]) || err != nil { + t.Fatalf("io.ReadAll(s) = {%x}, %v; want {%x}, nil", got, err, want[1:]) + } + }) +} + func finalSizeTest(t *testing.T, wantErr transportError, f func(tc *testConn, sid streamID) (finalSize int64), opts ...any) { testStreamTypes(t, "", func(t *testing.T, styp streamType) { for _, test := range []struct { @@ -1156,8 +1182,8 @@ func TestStreamPeerResetsWithUnreadAndUnsentData(t *testing.T) { code: sentCode, }) wantErr := StreamErrorCode(sentCode) - if n, err := s.Read(got); n != 0 || !errors.Is(err, wantErr) { - t.Fatalf("Read reset stream: got %v, %v; want 0, %v", n, err, wantErr) + if _, err := io.ReadAll(s); !errors.Is(err, wantErr) { + t.Fatalf("Read reset stream: ReadAll got error %v; want %v", err, wantErr) } }) } From 5e097125fdec6a2b4d9123a57f9551c2b89c7315 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 14 Feb 2024 12:31:31 -0800 Subject: [PATCH 137/168] quic: fast path for stream writes Similar to the fast-path for reads, writes are buffered in an unsynchronized []byte allowing for lock-free small writes. For golang/go#58547 Change-Id: I305cb5f91eff662a473f44a4bc051acc7c213e4c Reviewed-on: https://go-review.googlesource.com/c/net/+/564496 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/pipe.go | 12 +++++++++ internal/quic/stream.go | 50 ++++++++++++++++++++++++++++++++++-- internal/quic/stream_test.go | 3 ++- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/internal/quic/pipe.go b/internal/quic/pipe.go index 42a0049da9..75cf76db21 100644 --- a/internal/quic/pipe.go +++ b/internal/quic/pipe.go @@ -148,6 +148,18 @@ func (p *pipe) peek(n int64) []byte { return b[:min(int64(len(b)), n)] } +// availableBuffer returns the available contiguous, allocated buffer space +// following the pipe window. +// +// This is used by the stream write fast path, which makes multiple writes into the pipe buffer +// without a lock, and then adjusts p.end at a later time with a lock held. +func (p *pipe) availableBuffer() []byte { + if p.tail == nil { + return nil + } + return p.tail.b[p.end-p.tail.off:] +} + // discardBefore discards all data prior to off. func (p *pipe) discardBefore(off int64) { for p.head != nil && p.head.end() < off { diff --git a/internal/quic/stream.go b/internal/quic/stream.go index 17ca8b7d6d..c5fafdf1d3 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -58,8 +58,10 @@ type Stream struct { outdone chan struct{} // closed when all data sent // Unsynchronized buffers, used for lock-free fast path. - inbuf []byte // received data - inbufoff int // bytes of inbuf which have been consumed + inbuf []byte // received data + inbufoff int // bytes of inbuf which have been consumed + outbuf []byte // written data + outbufoff int // bytes of outbuf which contain data to write // Atomic stream state bits. // @@ -313,7 +315,14 @@ func (s *Stream) Write(b []byte) (n int, err error) { if s.IsReadOnly() { return 0, errors.New("write to read-only stream") } + if len(b) > 0 && len(s.outbuf)-s.outbufoff >= len(b) { + // Fast path: The data to write fits in s.outbuf. + copy(s.outbuf[s.outbufoff:], b) + s.outbufoff += len(b) + return len(b), nil + } canWrite := s.outgate.lock() + s.flushFastOutputBuffer() for { // The first time through this loop, we may or may not be write blocked. // We exit the loop after writing all data, so on subsequent passes through @@ -373,17 +382,51 @@ func (s *Stream) Write(b []byte) (n int, err error) { // If we have bytes left to send, we're blocked. canWrite = false } + if lim := s.out.start + s.outmaxbuf - s.out.end - 1; lim > 0 { + // If s.out has space allocated and available to be written into, + // then reference it in s.outbuf for fast-path writes. + // + // It's perhaps a bit pointless to limit s.outbuf to the send buffer limit. + // We've already allocated this buffer so we aren't saving any memory + // by not using it. + // For now, we limit it anyway to make it easier to reason about limits. + // + // We set the limit to one less than the send buffer limit (the -1 above) + // so that a write which completely fills the buffer will overflow + // s.outbuf and trigger a flush. + s.outbuf = s.out.availableBuffer() + if int64(len(s.outbuf)) > lim { + s.outbuf = s.outbuf[:lim] + } + } s.outUnlock() return n, nil } // WriteBytes writes a single byte to the stream. func (s *Stream) WriteByte(c byte) error { + if s.outbufoff < len(s.outbuf) { + s.outbuf[s.outbufoff] = c + s.outbufoff++ + return nil + } b := [1]byte{c} _, err := s.Write(b[:]) return err } +func (s *Stream) flushFastOutputBuffer() { + if s.outbuf == nil { + return + } + // Commit data previously written to s.outbuf. + // s.outbuf is a reference to a buffer in s.out, so we just need to record + // that the output buffer has been extended. + s.out.end += int64(s.outbufoff) + s.outbuf = nil + s.outbufoff = 0 +} + // Flush flushes data written to the stream. // It does not wait for the peer to acknowledge receipt of the data. // Use Close to wait for the peer's acknowledgement. @@ -394,6 +437,7 @@ func (s *Stream) Flush() { } func (s *Stream) flushLocked() { + s.flushFastOutputBuffer() s.outopened.set() if s.outflushed < s.outwin { s.outunsent.add(s.outflushed, min(s.outwin, s.out.end)) @@ -509,6 +553,8 @@ func (s *Stream) resetInternal(code uint64, userClosed bool) { // extra RESET_STREAM in this case is harmless. s.outreset.set() s.outresetcode = code + s.outbuf = nil + s.outbufoff = 0 s.out.discardBefore(s.out.end) s.outunsent = rangeset[int64]{} s.outblocked.clear() diff --git a/internal/quic/stream_test.go b/internal/quic/stream_test.go index d1cfb34db0..9f857f29d4 100644 --- a/internal/quic/stream_test.go +++ b/internal/quic/stream_test.go @@ -100,6 +100,7 @@ func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) { if err != nil { t.Fatalf("write with available output buffer: unexpected error: %v", err) } + s.Flush() tc.wantFrame("write blocked by flow control triggers a STREAM_DATA_BLOCKED frame", packetType1RTT, debugFrameStreamDataBlocked{ id: s.id, @@ -111,6 +112,7 @@ func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) { if err != nil { t.Fatalf("write with available output buffer: unexpected error: %v", err) } + s.Flush() tc.wantIdle("adding more blocked data does not trigger another STREAM_DATA_BLOCKED") // Provide some flow control window. @@ -1349,7 +1351,6 @@ func TestStreamFlushImplicitExact(t *testing.T) { id: s.id, data: want[0:4], }) - }) } From 2a8baeab1851a3c0f336a9185a02c177a0365232 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Sun, 18 Feb 2024 12:13:12 -0800 Subject: [PATCH 138/168] quic: don't record fin bit as sent when it wasn't When appendStreamFrame is provided with the last chunk of data for a stream, doesn't have enough space in the packet to include all the data, don't incorrectly record the packet as including a FIN bit. We were correctly sending a STREAM frame with no FIN bit--it's just the sent packet accounting that was off. No test, because I can't figure out a scenario where this actually has an observable effect, since we're always going to send the FIN when the remaining stream data is sent. Change-Id: I0ee81273165fcf10a52da76b33d2bf1b9c4f3523 Reviewed-on: https://go-review.googlesource.com/c/net/+/564796 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/packet_writer.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/quic/packet_writer.go b/internal/quic/packet_writer.go index 85149f6071..9ed393502c 100644 --- a/internal/quic/packet_writer.go +++ b/internal/quic/packet_writer.go @@ -388,11 +388,7 @@ func (w *packetWriter) appendStreamFrame(id streamID, off int64, size int, fin b w.b = appendVarint(w.b, uint64(size)) start := len(w.b) w.b = w.b[:start+size] - if fin { - w.sent.appendAckElicitingFrame(frameTypeStreamBase | streamFinBit) - } else { - w.sent.appendAckElicitingFrame(frameTypeStreamBase) - } + w.sent.appendAckElicitingFrame(typ & (frameTypeStreamBase | streamFinBit)) w.sent.appendInt(uint64(id)) w.sent.appendOffAndSize(off, size) return w.b[start:][:size], true From a6a24dd292f82221e069bd497ff2a93756f63d20 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 15 Feb 2024 09:52:29 -0800 Subject: [PATCH 139/168] quic: source address and ECN support in the network layer Make the abstraction over UDP connections higher level, and add support for setting the source address and ECN bits in sent packets, and receving the destination address and ECN bits in received packets. There is no good way that I can find to identify the source IP address of packets we send. Look up the destination IP address of the first packet received on each connection, and use this as the source address for all future packets we send. This avoids unexpected path migration, where the address we send from changes without our knowing it. Reject received packets sent from an unexpected peer address. In the future, when we support path migration, we will want to relax these restrictions. ECN bits may be used to detect network congestion. We don't make use of them at this time, but this CL adds the necessary UDP layer support to do so in the future. This CL also lays the groundwork for using more efficient platform APIs to send/receive packets in the future. (sendmmsg/recvmmsg/GSO/GRO) These features require platform-specific APIs. Add support for Darwin and Linux to start with, with a graceful fallback on other OSs. For golang/go#58547 Change-Id: I1c97cc0d3e52fff18e724feaaac4a50d3df671bc Reviewed-on: https://go-review.googlesource.com/c/net/+/565255 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn.go | 9 +- internal/quic/conn_recv.go | 38 ++-- internal/quic/conn_send.go | 5 +- internal/quic/conn_test.go | 7 + internal/quic/dgram.go | 23 ++- internal/quic/endpoint.go | 90 +++++----- internal/quic/endpoint_test.go | 26 ++- internal/quic/qlog.go | 6 + internal/quic/retry.go | 17 +- internal/quic/retry_test.go | 4 +- internal/quic/stateless_reset_test.go | 4 +- internal/quic/udp.go | 30 ++++ internal/quic/udp_darwin.go | 13 ++ internal/quic/udp_linux.go | 13 ++ internal/quic/udp_msg.go | 248 ++++++++++++++++++++++++++ internal/quic/udp_other.go | 62 +++++++ internal/quic/udp_test.go | 176 ++++++++++++++++++ 17 files changed, 676 insertions(+), 95 deletions(-) create mode 100644 internal/quic/udp.go create mode 100644 internal/quic/udp_darwin.go create mode 100644 internal/quic/udp_linux.go create mode 100644 internal/quic/udp_msg.go create mode 100644 internal/quic/udp_other.go create mode 100644 internal/quic/udp_test.go diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 020bc81a45..5738b6dbb0 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -25,6 +25,7 @@ type Conn struct { config *Config testHooks connTestHooks peerAddr netip.AddrPort + localAddr netip.AddrPort msgc chan any donec chan struct{} // closed when conn loop exits @@ -97,7 +98,7 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip side: side, endpoint: e, config: config, - peerAddr: peerAddr, + peerAddr: unmapAddrPort(peerAddr), msgc: make(chan any, 1), donec: make(chan struct{}), peerAckDelayExponent: -1, @@ -317,7 +318,11 @@ func (c *Conn) loop(now time.Time) { } switch m := m.(type) { case *datagram: - c.handleDatagram(now, m) + if !c.handleDatagram(now, m) { + if c.logEnabled(QLogLevelPacket) { + c.logPacketDropped(m) + } + } m.recycle() case timerEvent: // A connection timer has expired. diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index 1b3219723d..c8d70d85c6 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -8,17 +8,33 @@ package quic import ( "bytes" - "context" "encoding/binary" "errors" "time" ) -func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { +func (c *Conn) handleDatagram(now time.Time, dgram *datagram) (handled bool) { + if !c.localAddr.IsValid() { + // We don't have any way to tell in the general case what address we're + // sending packets from. Set our address from the destination address of + // the first packet received from the peer. + c.localAddr = dgram.localAddr + } + if dgram.peerAddr.IsValid() && dgram.peerAddr != c.peerAddr { + if c.side == clientSide { + // "If a client receives packets from an unknown server address, + // the client MUST discard these packets." + // https://www.rfc-editor.org/rfc/rfc9000#section-9-6 + return false + } + // We currently don't support connection migration, + // so for now the server also drops packets from an unknown address. + return false + } buf := dgram.b c.loss.datagramReceived(now, len(buf)) if c.isDraining() { - return + return false } for len(buf) > 0 { var n int @@ -28,7 +44,7 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { if c.side == serverSide && len(dgram.b) < paddedInitialDatagramSize { // Discard client-sent Initial packets in too-short datagrams. // https://www.rfc-editor.org/rfc/rfc9000#section-14.1-4 - return + return false } n = c.handleLongHeader(now, ptype, initialSpace, c.keysInitial.r, buf) case packetTypeHandshake: @@ -37,10 +53,10 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { n = c.handle1RTT(now, buf) case packetTypeRetry: c.handleRetry(now, buf) - return + return true case packetTypeVersionNegotiation: c.handleVersionNegotiation(now, buf) - return + return true default: n = -1 } @@ -58,20 +74,16 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) { var token statelessResetToken copy(token[:], buf[len(buf)-len(token):]) if c.handleStatelessReset(now, token) { - return + return true } } // Invalid data at the end of a datagram is ignored. - if c.logEnabled(QLogLevelPacket) { - c.log.LogAttrs(context.Background(), QLogLevelPacket, - "connectivity:packet_dropped", - ) - } - break + return false } c.idleHandlePacketReceived(now) buf = buf[n:] } + return true } func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpace, k fixedKeys, buf []byte) int { diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 575b8f9b4b..12bcfe3081 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -179,7 +179,10 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) { } } - c.endpoint.sendDatagram(buf, c.peerAddr) + c.endpoint.sendDatagram(datagram{ + b: buf, + peerAddr: c.peerAddr, + }) } } diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 2d3c946d6c..a8f3fc7fdf 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -453,6 +453,7 @@ func (tc *testConn) writeFrames(ptype packetType, frames ...debugFrame) { dstConnID: dstConnID, srcConnID: tc.peerConnID, }}, + addr: tc.conn.peerAddr, } if ptype == packetTypeInitial && tc.conn.side == serverSide { d.paddedSize = 1200 @@ -656,6 +657,12 @@ func (tc *testConn) wantPacket(expectation string, want *testPacket) { } func packetEqual(a, b *testPacket) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } ac := *a ac.frames = nil ac.header = 0 diff --git a/internal/quic/dgram.go b/internal/quic/dgram.go index 79e6650fa4..6155893732 100644 --- a/internal/quic/dgram.go +++ b/internal/quic/dgram.go @@ -12,10 +12,25 @@ import ( ) type datagram struct { - b []byte - addr netip.AddrPort + b []byte + localAddr netip.AddrPort + peerAddr netip.AddrPort + ecn ecnBits } +// Explicit Congestion Notification bits. +// +// https://www.rfc-editor.org/rfc/rfc3168.html#section-5 +type ecnBits byte + +const ( + ecnMask = 0b000000_11 + ecnNotECT = 0b000000_00 + ecnECT1 = 0b000000_01 + ecnECT0 = 0b000000_10 + ecnCE = 0b000000_11 +) + var datagramPool = sync.Pool{ New: func() any { return &datagram{ @@ -26,7 +41,9 @@ var datagramPool = sync.Pool{ func newDatagram() *datagram { m := datagramPool.Get().(*datagram) - m.b = m.b[:cap(m.b)] + *m = datagram{ + b: m.b[:cap(m.b)], + } return m } diff --git a/internal/quic/endpoint.go b/internal/quic/endpoint.go index 8ed67de542..6631708b82 100644 --- a/internal/quic/endpoint.go +++ b/internal/quic/endpoint.go @@ -22,11 +22,11 @@ import ( // // Multiple goroutines may invoke methods on an Endpoint simultaneously. type Endpoint struct { - config *Config - udpConn udpConn - testHooks endpointTestHooks - resetGen statelessResetTokenGenerator - retry retryState + config *Config + packetConn packetConn + testHooks endpointTestHooks + resetGen statelessResetTokenGenerator + retry retryState acceptQueue queue[*Conn] // new inbound connections connsMap connsMap // only accessed by the listen loop @@ -42,13 +42,12 @@ type endpointTestHooks interface { newConn(c *Conn) } -// A udpConn is a UDP connection. -// It is implemented by net.UDPConn. -type udpConn interface { +// A packetConn is the interface to sending and receiving UDP packets. +type packetConn interface { Close() error - LocalAddr() net.Addr - ReadMsgUDPAddrPort(b, control []byte) (n, controln, flags int, _ netip.AddrPort, _ error) - WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) + LocalAddr() netip.AddrPort + Read(f func(*datagram)) + Write(datagram) error } // Listen listens on a local network address. @@ -65,13 +64,17 @@ func Listen(network, address string, config *Config) (*Endpoint, error) { if err != nil { return nil, err } - return newEndpoint(udpConn, config, nil) + pc, err := newNetUDPConn(udpConn) + if err != nil { + return nil, err + } + return newEndpoint(pc, config, nil) } -func newEndpoint(udpConn udpConn, config *Config, hooks endpointTestHooks) (*Endpoint, error) { +func newEndpoint(pc packetConn, config *Config, hooks endpointTestHooks) (*Endpoint, error) { e := &Endpoint{ config: config, - udpConn: udpConn, + packetConn: pc, testHooks: hooks, conns: make(map[*Conn]struct{}), acceptQueue: newQueue[*Conn](), @@ -90,8 +93,7 @@ func newEndpoint(udpConn udpConn, config *Config, hooks endpointTestHooks) (*End // LocalAddr returns the local network address. func (e *Endpoint) LocalAddr() netip.AddrPort { - a, _ := e.udpConn.LocalAddr().(*net.UDPAddr) - return a.AddrPort() + return e.packetConn.LocalAddr() } // Close closes the Endpoint. @@ -114,7 +116,7 @@ func (e *Endpoint) Close(ctx context.Context) error { conns = append(conns, c) } if len(e.conns) == 0 { - e.udpConn.Close() + e.packetConn.Close() } } e.connsMu.Unlock() @@ -200,34 +202,18 @@ func (e *Endpoint) connDrained(c *Conn) { defer e.connsMu.Unlock() delete(e.conns, c) if e.closing && len(e.conns) == 0 { - e.udpConn.Close() + e.packetConn.Close() } } func (e *Endpoint) listen() { defer close(e.closec) - for { - m := newDatagram() - // TODO: Read and process the ECN (explicit congestion notification) field. - // https://tools.ietf.org/html/draft-ietf-quic-transport-32#section-13.4 - n, _, _, addr, err := e.udpConn.ReadMsgUDPAddrPort(m.b, nil) - if err != nil { - // The user has probably closed the endpoint. - // We currently don't surface errors from other causes; - // we could check to see if the endpoint has been closed and - // record the unexpected error if it has not. - return - } - if n == 0 { - continue - } + e.packetConn.Read(func(m *datagram) { if e.connsMap.updateNeeded.Load() { e.connsMap.applyUpdates() } - m.addr = addr - m.b = m.b[:n] e.handleDatagram(m) - } + }) } func (e *Endpoint) handleDatagram(m *datagram) { @@ -277,7 +263,7 @@ func (e *Endpoint) handleUnknownDestinationDatagram(m *datagram) { // If this is a 1-RTT packet, there's nothing productive we can do with it. // Send a stateless reset if possible. if !isLongHeader(m.b[0]) { - e.maybeSendStatelessReset(m.b, m.addr) + e.maybeSendStatelessReset(m.b, m.peerAddr) return } p, ok := parseGenericLongHeaderPacket(m.b) @@ -291,7 +277,7 @@ func (e *Endpoint) handleUnknownDestinationDatagram(m *datagram) { return default: // Unknown version. - e.sendVersionNegotiation(p, m.addr) + e.sendVersionNegotiation(p, m.peerAddr) return } if getPacketType(m.b) != packetTypeInitial { @@ -309,7 +295,7 @@ func (e *Endpoint) handleUnknownDestinationDatagram(m *datagram) { if e.config.RequireAddressValidation { var ok bool cids.retrySrcConnID = p.dstConnID - cids.originalDstConnID, ok = e.validateInitialAddress(now, p, m.addr) + cids.originalDstConnID, ok = e.validateInitialAddress(now, p, m.peerAddr) if !ok { return } @@ -317,7 +303,7 @@ func (e *Endpoint) handleUnknownDestinationDatagram(m *datagram) { cids.originalDstConnID = p.dstConnID } var err error - c, err := e.newConn(now, serverSide, cids, m.addr) + c, err := e.newConn(now, serverSide, cids, m.peerAddr) if err != nil { // The accept queue is probably full. // We could send a CONNECTION_CLOSE to the peer to reject the connection. @@ -329,7 +315,7 @@ func (e *Endpoint) handleUnknownDestinationDatagram(m *datagram) { m = nil // don't recycle, sendMsg takes ownership } -func (e *Endpoint) maybeSendStatelessReset(b []byte, addr netip.AddrPort) { +func (e *Endpoint) maybeSendStatelessReset(b []byte, peerAddr netip.AddrPort) { if !e.resetGen.canReset { // Config.StatelessResetKey isn't set, so we don't send stateless resets. return @@ -370,17 +356,21 @@ func (e *Endpoint) maybeSendStatelessReset(b []byte, addr netip.AddrPort) { b[0] &^= headerFormLong // clear long header bit b[0] |= fixedBit // set fixed bit copy(b[len(b)-statelessResetTokenLen:], token[:]) - e.sendDatagram(b, addr) + e.sendDatagram(datagram{ + b: b, + peerAddr: peerAddr, + }) } -func (e *Endpoint) sendVersionNegotiation(p genericLongPacket, addr netip.AddrPort) { +func (e *Endpoint) sendVersionNegotiation(p genericLongPacket, peerAddr netip.AddrPort) { m := newDatagram() m.b = appendVersionNegotiation(m.b[:0], p.srcConnID, p.dstConnID, quicVersion1) - e.sendDatagram(m.b, addr) + m.peerAddr = peerAddr + e.sendDatagram(*m) m.recycle() } -func (e *Endpoint) sendConnectionClose(in genericLongPacket, addr netip.AddrPort, code transportError) { +func (e *Endpoint) sendConnectionClose(in genericLongPacket, peerAddr netip.AddrPort, code transportError) { keys := initialKeys(in.dstConnID, serverSide) var w packetWriter p := longPacket{ @@ -399,12 +389,14 @@ func (e *Endpoint) sendConnectionClose(in genericLongPacket, addr netip.AddrPort if len(buf) == 0 { return } - e.sendDatagram(buf, addr) + e.sendDatagram(datagram{ + b: buf, + peerAddr: peerAddr, + }) } -func (e *Endpoint) sendDatagram(p []byte, addr netip.AddrPort) error { - _, err := e.udpConn.WriteToUDPAddrPort(p, addr) - return err +func (e *Endpoint) sendDatagram(dgram datagram) error { + return e.packetConn.Write(dgram) } // A connsMap is an endpoint's mapping of conn ids and reset tokens to conns. diff --git a/internal/quic/endpoint_test.go b/internal/quic/endpoint_test.go index 6d103f0612..b9fb55fb34 100644 --- a/internal/quic/endpoint_test.go +++ b/internal/quic/endpoint_test.go @@ -12,7 +12,6 @@ import ( "crypto/tls" "io" "log/slog" - "net" "net/netip" "testing" "time" @@ -190,13 +189,9 @@ func (te *testEndpoint) writeDatagram(d *testDatagram) { for len(buf) < d.paddedSize { buf = append(buf, 0) } - addr := d.addr - if !addr.IsValid() { - addr = testClientAddr - } te.write(&datagram{ - b: buf, - addr: addr, + b: buf, + peerAddr: d.addr, }) } @@ -303,25 +298,24 @@ func (te *testEndpointUDPConn) Close() error { return nil } -func (te *testEndpointUDPConn) LocalAddr() net.Addr { - return net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:443")) +func (te *testEndpointUDPConn) LocalAddr() netip.AddrPort { + return netip.MustParseAddrPort("127.0.0.1:443") } -func (te *testEndpointUDPConn) ReadMsgUDPAddrPort(b, control []byte) (n, controln, flags int, _ netip.AddrPort, _ error) { +func (te *testEndpointUDPConn) Read(f func(*datagram)) { for { select { case d, ok := <-te.recvc: if !ok { - return 0, 0, 0, netip.AddrPort{}, io.EOF + return } - n = copy(b, d.b) - return n, 0, 0, d.addr, nil + f(d) case <-te.idlec: } } } -func (te *testEndpointUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { - te.sentDatagrams = append(te.sentDatagrams, append([]byte(nil), b...)) - return len(b), nil +func (te *testEndpointUDPConn) Write(dgram datagram) error { + te.sentDatagrams = append(te.sentDatagrams, append([]byte(nil), dgram.b...)) + return nil } diff --git a/internal/quic/qlog.go b/internal/quic/qlog.go index e37e2f8cea..36831252c6 100644 --- a/internal/quic/qlog.go +++ b/internal/quic/qlog.go @@ -151,6 +151,12 @@ func (c *Conn) logConnectionClosed() { ) } +func (c *Conn) logPacketDropped(dgram *datagram) { + c.log.LogAttrs(context.Background(), QLogLevelPacket, + "connectivity:packet_dropped", + ) +} + func (c *Conn) logLongPacketReceived(p longPacket, pkt []byte) { var frames slog.Attr if c.logEnabled(QLogLevelFrame) { diff --git a/internal/quic/retry.go b/internal/quic/retry.go index 31cb57b880..5dc39d1d9d 100644 --- a/internal/quic/retry.go +++ b/internal/quic/retry.go @@ -139,7 +139,7 @@ func (rs *retryState) additionalData(srcConnID []byte, addr netip.AddrPort) []by return additional } -func (e *Endpoint) validateInitialAddress(now time.Time, p genericLongPacket, addr netip.AddrPort) (origDstConnID []byte, ok bool) { +func (e *Endpoint) validateInitialAddress(now time.Time, p genericLongPacket, peerAddr netip.AddrPort) (origDstConnID []byte, ok bool) { // The retry token is at the start of an Initial packet's data. token, n := consumeUint8Bytes(p.data) if n < 0 { @@ -151,22 +151,22 @@ func (e *Endpoint) validateInitialAddress(now time.Time, p genericLongPacket, ad if len(token) == 0 { // The sender has not provided a token. // Send a Retry packet to them with one. - e.sendRetry(now, p, addr) + e.sendRetry(now, p, peerAddr) return nil, false } - origDstConnID, ok = e.retry.validateToken(now, token, p.srcConnID, p.dstConnID, addr) + origDstConnID, ok = e.retry.validateToken(now, token, p.srcConnID, p.dstConnID, peerAddr) if !ok { // This does not seem to be a valid token. // Close the connection with an INVALID_TOKEN error. // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5 - e.sendConnectionClose(p, addr, errInvalidToken) + e.sendConnectionClose(p, peerAddr, errInvalidToken) return nil, false } return origDstConnID, true } -func (e *Endpoint) sendRetry(now time.Time, p genericLongPacket, addr netip.AddrPort) { - token, srcConnID, err := e.retry.makeToken(now, p.srcConnID, p.dstConnID, addr) +func (e *Endpoint) sendRetry(now time.Time, p genericLongPacket, peerAddr netip.AddrPort) { + token, srcConnID, err := e.retry.makeToken(now, p.srcConnID, p.dstConnID, peerAddr) if err != nil { return } @@ -175,7 +175,10 @@ func (e *Endpoint) sendRetry(now time.Time, p genericLongPacket, addr netip.Addr srcConnID: srcConnID, token: token, }) - e.sendDatagram(b, addr) + e.sendDatagram(datagram{ + b: b, + peerAddr: peerAddr, + }) } type retryPacket struct { diff --git a/internal/quic/retry_test.go b/internal/quic/retry_test.go index 8f36e1bd3e..42f2bdd4a5 100644 --- a/internal/quic/retry_test.go +++ b/internal/quic/retry_test.go @@ -436,8 +436,8 @@ func TestRetryClientIgnoresRetryWithInvalidIntegrityTag(t *testing.T) { }) pkt[len(pkt)-1] ^= 1 // invalidate the integrity tag tc.endpoint.write(&datagram{ - b: pkt, - addr: testClientAddr, + b: pkt, + peerAddr: testClientAddr, }) tc.wantIdle("client ignores Retry with invalid integrity tag") } diff --git a/internal/quic/stateless_reset_test.go b/internal/quic/stateless_reset_test.go index 45a49e81e6..9458d2ea9d 100644 --- a/internal/quic/stateless_reset_test.go +++ b/internal/quic/stateless_reset_test.go @@ -57,8 +57,8 @@ func newDatagramForReset(cid []byte, size int, addr netip.AddrPort) *datagram { dgram = append(dgram, byte(len(dgram))) // semi-random junk } return &datagram{ - b: dgram, - addr: addr, + b: dgram, + peerAddr: addr, } } diff --git a/internal/quic/udp.go b/internal/quic/udp.go new file mode 100644 index 0000000000..0a578286b2 --- /dev/null +++ b/internal/quic/udp.go @@ -0,0 +1,30 @@ +// 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 "net/netip" + +// Per-plaform consts describing support for various features. +// +// const udpECNSupport indicates whether the platform supports setting +// the ECN (Explicit Congestion Notification) IP header bits. +// +// const udpInvalidLocalAddrIsError indicates whether sending a packet +// from an local address not associated with the system is an error. +// For example, assuming 127.0.0.2 is not a local address, does sending +// from it (using IP_PKTINFO or some other such feature) result in an error? + +// unmapAddrPort returns a with any IPv4-mapped IPv6 address prefix removed. +func unmapAddrPort(a netip.AddrPort) netip.AddrPort { + if a.Addr().Is4In6() { + return netip.AddrPortFrom( + a.Addr().Unmap(), + a.Port(), + ) + } + return a +} diff --git a/internal/quic/udp_darwin.go b/internal/quic/udp_darwin.go new file mode 100644 index 0000000000..3868a36a8e --- /dev/null +++ b/internal/quic/udp_darwin.go @@ -0,0 +1,13 @@ +// 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 && darwin + +package quic + +// See udp.go. +const ( + udpECNSupport = true + udpInvalidLocalAddrIsError = true +) diff --git a/internal/quic/udp_linux.go b/internal/quic/udp_linux.go new file mode 100644 index 0000000000..2ba3e6f2f4 --- /dev/null +++ b/internal/quic/udp_linux.go @@ -0,0 +1,13 @@ +// 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 && linux + +package quic + +// See udp.go. +const ( + udpECNSupport = true + udpInvalidLocalAddrIsError = false +) diff --git a/internal/quic/udp_msg.go b/internal/quic/udp_msg.go new file mode 100644 index 0000000000..bdc1b710df --- /dev/null +++ b/internal/quic/udp_msg.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 && !quicbasicnet && (darwin || linux) + +package quic + +import ( + "net" + "net/netip" + "sync" + "unsafe" + + "golang.org/x/sys/unix" +) + +// Network interface for platforms using sendmsg/recvmsg with cmsgs. + +type netUDPConn struct { + c *net.UDPConn + localAddr netip.AddrPort +} + +func newNetUDPConn(uc *net.UDPConn) (*netUDPConn, error) { + a, _ := uc.LocalAddr().(*net.UDPAddr) + localAddr := a.AddrPort() + if localAddr.Addr().IsUnspecified() { + // If the conn is not bound to a specified (non-wildcard) address, + // then set localAddr.Addr to an invalid netip.Addr. + // This better conveys that this is not an address we should be using, + // and is a bit more efficient to test against. + localAddr = netip.AddrPortFrom(netip.Addr{}, localAddr.Port()) + } + + sc, err := uc.SyscallConn() + if err != nil { + return nil, err + } + sc.Control(func(fd uintptr) { + // Ask for ECN info and (when we aren't bound to a fixed local address) + // destination info. + // + // If any of these calls fail, we won't get the requested information. + // That's fine, we'll gracefully handle the lack. + unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVTOS, 1) + unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_RECVTCLASS, 1) + if !localAddr.IsValid() { + unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_PKTINFO, 1) + unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_RECVPKTINFO, 1) + } + }) + + return &netUDPConn{ + c: uc, + localAddr: localAddr, + }, nil +} + +func (c *netUDPConn) Close() error { return c.c.Close() } + +func (c *netUDPConn) LocalAddr() netip.AddrPort { + a, _ := c.c.LocalAddr().(*net.UDPAddr) + return a.AddrPort() +} + +func (c *netUDPConn) Read(f func(*datagram)) { + // We shouldn't ever see all of these messages at the same time, + // but the total is small so just allocate enough space for everything we use. + const ( + inPktinfoSize = 12 // int + in_addr + in_addr + in6PktinfoSize = 20 // in6_addr + int + ipTOSSize = 4 + ipv6TclassSize = 4 + ) + control := make([]byte, 0+ + unix.CmsgSpace(inPktinfoSize)+ + unix.CmsgSpace(in6PktinfoSize)+ + unix.CmsgSpace(ipTOSSize)+ + unix.CmsgSpace(ipv6TclassSize)) + + for { + d := newDatagram() + n, controlLen, _, peerAddr, err := c.c.ReadMsgUDPAddrPort(d.b, control) + if err != nil { + return + } + if n == 0 { + continue + } + d.localAddr = c.localAddr + d.peerAddr = unmapAddrPort(peerAddr) + d.b = d.b[:n] + parseControl(d, control[:controlLen]) + f(d) + } +} + +var cmsgPool = sync.Pool{ + New: func() any { + return new([]byte) + }, +} + +func (c *netUDPConn) Write(dgram datagram) error { + controlp := cmsgPool.Get().(*[]byte) + control := *controlp + defer func() { + *controlp = control[:0] + cmsgPool.Put(controlp) + }() + + localIP := dgram.localAddr.Addr() + if localIP.IsValid() { + if localIP.Is4() { + control = appendCmsgIPSourceAddrV4(control, localIP) + } else { + control = appendCmsgIPSourceAddrV6(control, localIP) + } + } + if dgram.ecn != ecnNotECT { + if dgram.peerAddr.Addr().Is4() { + control = appendCmsgECNv4(control, dgram.ecn) + } else { + control = appendCmsgECNv6(control, dgram.ecn) + } + } + + _, _, err := c.c.WriteMsgUDPAddrPort(dgram.b, control, dgram.peerAddr) + return err +} + +func parseControl(d *datagram, control []byte) { + for len(control) > 0 { + hdr, data, remainder, err := unix.ParseOneSocketControlMessage(control) + if err != nil { + return + } + control = remainder + switch hdr.Level { + case unix.IPPROTO_IP: + switch hdr.Type { + case unix.IP_TOS, unix.IP_RECVTOS: + // Single byte containing the IP TOS field. + // The low two bits are the ECN field. + // + // (Linux sets the type to IP_TOS, Darwin to IP_RECVTOS, + // jus check for both.) + if len(data) < 1 { + break + } + d.ecn = ecnBits(data[0] & ecnMask) + case unix.IP_PKTINFO: + if a, ok := parseInPktinfo(data); ok { + d.localAddr = netip.AddrPortFrom(a, d.localAddr.Port()) + } + } + case unix.IPPROTO_IPV6: + switch hdr.Type { + case unix.IPV6_TCLASS: + // Single byte containing the traffic class field. + // The low two bits are the ECN field. + if len(data) < 1 { + break + } + d.ecn = ecnBits(data[0] & ecnMask) + case unix.IPV6_PKTINFO: + if a, ok := parseIn6Pktinfo(data); ok { + d.localAddr = netip.AddrPortFrom(a, d.localAddr.Port()) + } + } + } + } +} + +func parseInPktinfo(b []byte) (netip.Addr, bool) { + // struct in_pktinfo { + // unsigned int ipi_ifindex; /* send/recv interface index */ + // struct in_addr ipi_spec_dst; /* Local address */ + // struct in_addr ipi_addr; /* IP Header dst address */ + // }; + if len(b) != 12 { + return netip.Addr{}, false + } + return netip.AddrFrom4([4]byte(b[8:][:4])), true +} + +func parseIn6Pktinfo(b []byte) (netip.Addr, bool) { + // struct in6_pktinfo { + // struct in6_addr ipi6_addr; /* src/dst IPv6 address */ + // unsigned int ipi6_ifindex; /* send/recv interface index */ + // }; + if len(b) != 20 { + return netip.Addr{}, false + } + return netip.AddrFrom16([16]byte(b[:16])).Unmap(), true +} + +// appendCmsgIPSourceAddrV4 appends an IP_PKTINFO setting the source address +// for an outbound datagram. +func appendCmsgIPSourceAddrV4(b []byte, src netip.Addr) []byte { + // struct in_pktinfo { + // unsigned int ipi_ifindex; /* send/recv interface index */ + // struct in_addr ipi_spec_dst; /* Local address */ + // struct in_addr ipi_addr; /* IP Header dst address */ + // }; + b, data := appendCmsg(b, unix.IPPROTO_IP, unix.IP_PKTINFO, 12) + ip := src.As4() + copy(data[4:], ip[:]) + return b +} + +// appendCmsgIPSourceAddrV6 appends an IP_PKTINFO or IPV6_PKTINFO +// setting the source address for an outbound datagram. +func appendCmsgIPSourceAddrV6(b []byte, src netip.Addr) []byte { + // struct in6_pktinfo { + // struct in6_addr ipi6_addr; /* src/dst IPv6 address */ + // unsigned int ipi6_ifindex; /* send/recv interface index */ + // }; + b, data := appendCmsg(b, unix.IPPROTO_IPV6, unix.IPV6_PKTINFO, 20) + ip := src.As16() + copy(data[0:], ip[:]) + return b +} + +func appendCmsgECNv4(b []byte, ecn ecnBits) []byte { + b, data := appendCmsg(b, unix.IPPROTO_IP, unix.IP_TOS, 4) + data[0] = byte(ecn) + return b +} + +func appendCmsgECNv6(b []byte, ecn ecnBits) []byte { + b, data := appendCmsg(b, unix.IPPROTO_IPV6, unix.IPV6_TCLASS, 4) + data[0] = byte(ecn) + return b +} + +// appendCmsg appends a cmsg with the given level, type, and size to b. +// It returns the new buffer, and the data section of the cmsg. +func appendCmsg(b []byte, level, typ int32, size int) (_, data []byte) { + off := len(b) + b = append(b, make([]byte, unix.CmsgSpace(size))...) + h := (*unix.Cmsghdr)(unsafe.Pointer(&b[off])) + h.Level = level + h.Type = typ + h.SetLen(unix.CmsgLen(size)) + return b, b[off+unix.CmsgSpace(0):][:size] +} diff --git a/internal/quic/udp_other.go b/internal/quic/udp_other.go new file mode 100644 index 0000000000..28be6d2006 --- /dev/null +++ b/internal/quic/udp_other.go @@ -0,0 +1,62 @@ +// 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 && (quicbasicnet || !(darwin || linux)) + +package quic + +import ( + "net" + "net/netip" +) + +// Lowest common denominator network interface: Basic net.UDPConn, no cmsgs. +// We will not be able to send or receive ECN bits, +// and we will not know what our local address is. +// +// The quicbasicnet build tag allows selecting this interface on any platform. + +// See udp.go. +const ( + udpECNSupport = false + udpInvalidLocalAddrIsError = false +) + +type netUDPConn struct { + c *net.UDPConn +} + +func newNetUDPConn(uc *net.UDPConn) (*netUDPConn, error) { + return &netUDPConn{ + c: uc, + }, nil +} + +func (c *netUDPConn) Close() error { return c.c.Close() } + +func (c *netUDPConn) LocalAddr() netip.AddrPort { + a, _ := c.c.LocalAddr().(*net.UDPAddr) + return a.AddrPort() +} + +func (c *netUDPConn) Read(f func(*datagram)) { + for { + dgram := newDatagram() + n, _, _, peerAddr, err := c.c.ReadMsgUDPAddrPort(dgram.b, nil) + if err != nil { + return + } + if n == 0 { + continue + } + dgram.peerAddr = unmapAddrPort(peerAddr) + dgram.b = dgram.b[:n] + f(dgram) + } +} + +func (c *netUDPConn) Write(dgram datagram) error { + _, err := c.c.WriteToUDPAddrPort(dgram.b, dgram.peerAddr) + return err +} diff --git a/internal/quic/udp_test.go b/internal/quic/udp_test.go new file mode 100644 index 0000000000..27eddf811d --- /dev/null +++ b/internal/quic/udp_test.go @@ -0,0 +1,176 @@ +// 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" + "fmt" + "net" + "net/netip" + "runtime" + "testing" +) + +func TestUDPSourceUnspecified(t *testing.T) { + // Send datagram with no source address set. + runUDPTest(t, func(t *testing.T, test udpTest) { + data := []byte("source unspecified") + if err := test.src.Write(datagram{ + b: data, + peerAddr: test.dstAddr, + }); err != nil { + t.Fatalf("Write: %v", err) + } + got := <-test.dgramc + if !bytes.Equal(got.b, data) { + t.Errorf("got datagram {%x}, want {%x}", got.b, data) + } + }) +} + +func TestUDPSourceSpecified(t *testing.T) { + // Send datagram with source address set. + runUDPTest(t, func(t *testing.T, test udpTest) { + data := []byte("source specified") + if err := test.src.Write(datagram{ + b: data, + peerAddr: test.dstAddr, + localAddr: test.src.LocalAddr(), + }); err != nil { + t.Fatalf("Write: %v", err) + } + got := <-test.dgramc + if !bytes.Equal(got.b, data) { + t.Errorf("got datagram {%x}, want {%x}", got.b, data) + } + }) +} + +func TestUDPSourceInvalid(t *testing.T) { + // Send datagram with source address set to an address not associated with the connection. + if !udpInvalidLocalAddrIsError { + t.Skipf("%v: sending from invalid source succeeds", runtime.GOOS) + } + runUDPTest(t, func(t *testing.T, test udpTest) { + var localAddr netip.AddrPort + if test.src.LocalAddr().Addr().Is4() { + localAddr = netip.MustParseAddrPort("127.0.0.2:1234") + } else { + localAddr = netip.MustParseAddrPort("[::2]:1234") + } + data := []byte("source invalid") + if err := test.src.Write(datagram{ + b: data, + peerAddr: test.dstAddr, + localAddr: localAddr, + }); err == nil { + t.Errorf("Write with invalid localAddr succeeded; want error") + } + }) +} + +func TestUDPECN(t *testing.T) { + if !udpECNSupport { + t.Skipf("%v: no ECN support", runtime.GOOS) + } + // Send datagrams with ECN bits set, verify the ECN bits are received. + runUDPTest(t, func(t *testing.T, test udpTest) { + for _, ecn := range []ecnBits{ecnNotECT, ecnECT1, ecnECT0, ecnCE} { + if err := test.src.Write(datagram{ + b: []byte{1, 2, 3, 4}, + peerAddr: test.dstAddr, + ecn: ecn, + }); err != nil { + t.Fatalf("Write: %v", err) + } + got := <-test.dgramc + if got.ecn != ecn { + t.Errorf("sending ECN bits %x, got %x", ecn, got.ecn) + } + } + }) +} + +type udpTest struct { + src *netUDPConn + dst *netUDPConn + dstAddr netip.AddrPort + dgramc chan *datagram +} + +// runUDPTest calls f with a pair of UDPConns in a matrix of network variations: +// udp, udp4, and udp6, and variations on binding to an unspecified address (0.0.0.0) +// or a specified one. +func runUDPTest(t *testing.T, f func(t *testing.T, u udpTest)) { + for _, test := range []struct { + srcNet, srcAddr, dstNet, dstAddr string + }{ + {"udp4", "127.0.0.1", "udp", ""}, + {"udp4", "127.0.0.1", "udp4", ""}, + {"udp4", "127.0.0.1", "udp4", "127.0.0.1"}, + {"udp6", "::1", "udp", ""}, + {"udp6", "::1", "udp6", ""}, + {"udp6", "::1", "udp6", "::1"}, + } { + spec := "spec" + if test.dstAddr == "" { + spec = "unspec" + } + t.Run(fmt.Sprintf("%v/%v/%v", test.srcNet, test.dstNet, spec), func(t *testing.T) { + srcAddr := netip.AddrPortFrom(netip.MustParseAddr(test.srcAddr), 0) + srcConn, err := net.ListenUDP(test.srcNet, net.UDPAddrFromAddrPort(srcAddr)) + if err != nil { + // If ListenUDP fails here, we presumably don't have + // IPv4/IPv6 configured. + t.Skipf("ListenUDP(%q, %v) = %v", test.srcNet, srcAddr, err) + } + t.Cleanup(func() { srcConn.Close() }) + src, err := newNetUDPConn(srcConn) + if err != nil { + t.Fatalf("newNetUDPConn: %v", err) + } + + var dstAddr netip.AddrPort + if test.dstAddr != "" { + dstAddr = netip.AddrPortFrom(netip.MustParseAddr(test.dstAddr), 0) + } + dstConn, err := net.ListenUDP(test.dstNet, net.UDPAddrFromAddrPort(dstAddr)) + if err != nil { + t.Skipf("ListenUDP(%q, nil) = %v", test.dstNet, err) + } + dst, err := newNetUDPConn(dstConn) + if err != nil { + dstConn.Close() + t.Fatalf("newNetUDPConn: %v", err) + } + + dgramc := make(chan *datagram) + go func() { + defer close(dgramc) + dst.Read(func(dgram *datagram) { + dgramc <- dgram + }) + }() + t.Cleanup(func() { + dstConn.Close() + for range dgramc { + t.Errorf("test read unexpected datagram") + } + }) + + f(t, udpTest{ + src: src, + dst: dst, + dstAddr: netip.AddrPortFrom( + srcAddr.Addr(), + dst.LocalAddr().Port(), + ), + dgramc: dgramc, + }) + }) + } +} From 57e4cc7d885a72ee5111b227ba5790ea5a170656 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 28 Nov 2023 15:31:58 -0800 Subject: [PATCH 140/168] quic: handle PATH_CHALLENGE and PATH_RESPONSE frames We do not support path migration yet, and will ignore packets sent from anything other than the peer's original address. Handle PATH_CHALLENGE frames by sending a PATH_RESPONSE. Handle PATH_RESPONSE frames by closing the connection (since we never send a challenge to respond to). For golang/go#58547 Change-Id: I828b9dcb23e17f5edf3d605b8f04efdafb392807 Reviewed-on: https://go-review.googlesource.com/c/net/+/565795 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/conn.go | 1 + internal/quic/conn_loss_test.go | 23 ++++++++ internal/quic/conn_recv.go | 44 ++++++++++++--- internal/quic/conn_send.go | 7 +++ internal/quic/conn_test.go | 2 + internal/quic/frame_debug.go | 17 ++++-- internal/quic/packet_codec_test.go | 4 +- internal/quic/packet_parser.go | 11 ++-- internal/quic/packet_writer.go | 17 +++--- internal/quic/path.go | 89 ++++++++++++++++++++++++++++++ internal/quic/path_test.go | 66 ++++++++++++++++++++++ internal/quic/sent_packet.go | 6 ++ 12 files changed, 255 insertions(+), 32 deletions(-) create mode 100644 internal/quic/path.go create mode 100644 internal/quic/path_test.go diff --git a/internal/quic/conn.go b/internal/quic/conn.go index 5738b6dbb0..d462e9617a 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -37,6 +37,7 @@ type Conn struct { connIDState connIDState loss lossState streams streamsState + path pathState // Packet protection keys, CRYPTO streams, and TLS state. keysInitial fixedKeyPair diff --git a/internal/quic/conn_loss_test.go b/internal/quic/conn_loss_test.go index 86ef23db0b..81d537803d 100644 --- a/internal/quic/conn_loss_test.go +++ b/internal/quic/conn_loss_test.go @@ -663,6 +663,29 @@ func TestLostRetireConnectionIDFrame(t *testing.T) { }) } +func TestLostPathResponseFrame(t *testing.T) { + // "Responses to path validation using PATH_RESPONSE frames are sent just once." + // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.12 + lostFrameTest(t, func(t *testing.T, pto bool) { + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.ignoreFrame(frameTypePing) + + data := pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef} + tc.writeFrames(packetType1RTT, debugFramePathChallenge{ + data: data, + }) + tc.wantFrame("response to PATH_CHALLENGE", + packetType1RTT, debugFramePathResponse{ + data: data, + }) + + tc.triggerLossOrPTO(packetType1RTT, pto) + tc.wantIdle("lost PATH_RESPONSE frame is not retransmitted") + }) +} + func TestLostHandshakeDoneFrame(t *testing.T) { // "The HANDSHAKE_DONE frame MUST be retransmitted until it is acknowledged." // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.16 diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go index c8d70d85c6..b1354cd3a1 100644 --- a/internal/quic/conn_recv.go +++ b/internal/quic/conn_recv.go @@ -46,11 +46,11 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) (handled bool) { // https://www.rfc-editor.org/rfc/rfc9000#section-14.1-4 return false } - n = c.handleLongHeader(now, ptype, initialSpace, c.keysInitial.r, buf) + n = c.handleLongHeader(now, dgram, ptype, initialSpace, c.keysInitial.r, buf) case packetTypeHandshake: - n = c.handleLongHeader(now, ptype, handshakeSpace, c.keysHandshake.r, buf) + n = c.handleLongHeader(now, dgram, ptype, handshakeSpace, c.keysHandshake.r, buf) case packetType1RTT: - n = c.handle1RTT(now, buf) + n = c.handle1RTT(now, dgram, buf) case packetTypeRetry: c.handleRetry(now, buf) return true @@ -86,7 +86,7 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) (handled bool) { return true } -func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpace, k fixedKeys, buf []byte) int { +func (c *Conn) handleLongHeader(now time.Time, dgram *datagram, ptype packetType, space numberSpace, k fixedKeys, buf []byte) int { if !k.isSet() { return skipLongHeaderPacket(buf) } @@ -125,7 +125,7 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa c.logLongPacketReceived(p, buf[:n]) } c.connIDState.handlePacket(c, p.ptype, p.srcConnID) - ackEliciting := c.handleFrames(now, ptype, space, p.payload) + ackEliciting := c.handleFrames(now, dgram, ptype, space, p.payload) c.acks[space].receive(now, space, p.num, ackEliciting) if p.ptype == packetTypeHandshake && c.side == serverSide { c.loss.validateClientAddress() @@ -138,7 +138,7 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa return n } -func (c *Conn) handle1RTT(now time.Time, buf []byte) int { +func (c *Conn) handle1RTT(now time.Time, dgram *datagram, buf []byte) int { if !c.keysAppData.canRead() { // 1-RTT packets extend to the end of the datagram, // so skip the remainder of the datagram if we can't parse this. @@ -175,7 +175,7 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int { if c.logEnabled(QLogLevelPacket) { c.log1RTTPacketReceived(p, buf) } - ackEliciting := c.handleFrames(now, packetType1RTT, appDataSpace, p.payload) + ackEliciting := c.handleFrames(now, dgram, packetType1RTT, appDataSpace, p.payload) c.acks[appDataSpace].receive(now, appDataSpace, p.num, ackEliciting) return len(buf) } @@ -252,7 +252,7 @@ func (c *Conn) handleVersionNegotiation(now time.Time, pkt []byte) { c.abortImmediately(now, errVersionNegotiation) } -func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, payload []byte) (ackEliciting bool) { +func (c *Conn) handleFrames(now time.Time, dgram *datagram, ptype packetType, space numberSpace, payload []byte) (ackEliciting bool) { if len(payload) == 0 { // "An endpoint MUST treat receipt of a packet containing no frames // as a connection error of type PROTOCOL_VIOLATION." @@ -373,6 +373,16 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, return } n = c.handleRetireConnectionIDFrame(now, space, payload) + case frameTypePathChallenge: + if !frameOK(c, ptype, __01) { + return + } + n = c.handlePathChallengeFrame(now, dgram, space, payload) + case frameTypePathResponse: + if !frameOK(c, ptype, ___1) { + return + } + n = c.handlePathResponseFrame(now, space, payload) case frameTypeConnectionCloseTransport: // Transport CONNECTION_CLOSE is OK in all spaces. n = c.handleConnectionCloseTransportFrame(now, payload) @@ -546,6 +556,24 @@ func (c *Conn) handleRetireConnectionIDFrame(now time.Time, space numberSpace, p return n } +func (c *Conn) handlePathChallengeFrame(now time.Time, dgram *datagram, space numberSpace, payload []byte) int { + data, n := consumePathChallengeFrame(payload) + if n < 0 { + return -1 + } + c.handlePathChallenge(now, dgram, data) + return n +} + +func (c *Conn) handlePathResponseFrame(now time.Time, space numberSpace, payload []byte) int { + data, n := consumePathResponseFrame(payload) + if n < 0 { + return -1 + } + c.handlePathResponse(now, data) + return n +} + func (c *Conn) handleConnectionCloseTransportFrame(now time.Time, payload []byte) int { code, _, reason, n := consumeConnectionCloseTransportFrame(payload) if n < 0 { diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go index 12bcfe3081..a87cac232e 100644 --- a/internal/quic/conn_send.go +++ b/internal/quic/conn_send.go @@ -271,6 +271,13 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, return } + // PATH_RESPONSE + if pad, ok := c.appendPathFrames(); !ok { + return + } else if pad { + defer c.w.appendPaddingTo(smallestMaxDatagramSize) + } + // All stream-related frames. This should come last in the packet, // so large amounts of STREAM data don't crowd out other frames // we may need to send. diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index a8f3fc7fdf..16ee3cf2fc 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -168,6 +168,7 @@ type testConn struct { sentDatagrams [][]byte sentPackets []*testPacket sentFrames []debugFrame + lastDatagram *testDatagram lastPacket *testPacket recvDatagram chan *datagram @@ -576,6 +577,7 @@ func (tc *testConn) readDatagram() *testDatagram { } p.frames = frames } + tc.lastDatagram = d return d } diff --git a/internal/quic/frame_debug.go b/internal/quic/frame_debug.go index 0902c385f6..17234dd7cd 100644 --- a/internal/quic/frame_debug.go +++ b/internal/quic/frame_debug.go @@ -77,6 +77,7 @@ func parseDebugFrame(b []byte) (f debugFrame, n int) { // debugFramePadding is a sequence of PADDING frames. type debugFramePadding struct { size int + to int // alternate for writing packets: pad to } func parseDebugFramePadding(b []byte) (f debugFramePadding, n int) { @@ -95,6 +96,10 @@ func (f debugFramePadding) write(w *packetWriter) bool { if w.avail() == 0 { return false } + if f.to > 0 { + w.appendPaddingTo(f.to) + return true + } for i := 0; i < f.size && w.avail() > 0; i++ { w.b = append(w.b, frameTypePadding) } @@ -584,7 +589,7 @@ func (f debugFrameRetireConnectionID) LogValue() slog.Value { // debugFramePathChallenge is a PATH_CHALLENGE frame. type debugFramePathChallenge struct { - data uint64 + data pathChallengeData } func parseDebugFramePathChallenge(b []byte) (f debugFramePathChallenge, n int) { @@ -593,7 +598,7 @@ func parseDebugFramePathChallenge(b []byte) (f debugFramePathChallenge, n int) { } func (f debugFramePathChallenge) String() string { - return fmt.Sprintf("PATH_CHALLENGE Data=%016x", f.data) + return fmt.Sprintf("PATH_CHALLENGE Data=%x", f.data) } func (f debugFramePathChallenge) write(w *packetWriter) bool { @@ -603,13 +608,13 @@ func (f debugFramePathChallenge) write(w *packetWriter) bool { func (f debugFramePathChallenge) LogValue() slog.Value { return slog.GroupValue( slog.String("frame_type", "path_challenge"), - slog.String("data", fmt.Sprintf("%016x", f.data)), + slog.String("data", fmt.Sprintf("%x", f.data)), ) } // debugFramePathResponse is a PATH_RESPONSE frame. type debugFramePathResponse struct { - data uint64 + data pathChallengeData } func parseDebugFramePathResponse(b []byte) (f debugFramePathResponse, n int) { @@ -618,7 +623,7 @@ func parseDebugFramePathResponse(b []byte) (f debugFramePathResponse, n int) { } func (f debugFramePathResponse) String() string { - return fmt.Sprintf("PATH_RESPONSE Data=%016x", f.data) + return fmt.Sprintf("PATH_RESPONSE Data=%x", f.data) } func (f debugFramePathResponse) write(w *packetWriter) bool { @@ -628,7 +633,7 @@ func (f debugFramePathResponse) write(w *packetWriter) bool { func (f debugFramePathResponse) LogValue() slog.Value { return slog.GroupValue( slog.String("frame_type", "path_response"), - slog.String("data", fmt.Sprintf("%016x", f.data)), + slog.String("data", fmt.Sprintf("%x", f.data)), ) } diff --git a/internal/quic/packet_codec_test.go b/internal/quic/packet_codec_test.go index 475e18c1da..98b3bbb059 100644 --- a/internal/quic/packet_codec_test.go +++ b/internal/quic/packet_codec_test.go @@ -517,7 +517,7 @@ func TestFrameEncodeDecode(t *testing.T) { s: "PATH_CHALLENGE Data=0123456789abcdef", j: `{"frame_type":"path_challenge","data":"0123456789abcdef"}`, f: debugFramePathChallenge{ - data: 0x0123456789abcdef, + data: pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}, }, b: []byte{ 0x1a, // Type (i) = 0x1a, @@ -527,7 +527,7 @@ func TestFrameEncodeDecode(t *testing.T) { s: "PATH_RESPONSE Data=0123456789abcdef", j: `{"frame_type":"path_response","data":"0123456789abcdef"}`, f: debugFramePathResponse{ - data: 0x0123456789abcdef, + data: pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}, }, b: []byte{ 0x1b, // Type (i) = 0x1b, diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go index 02ef9fb145..feef9eac7f 100644 --- a/internal/quic/packet_parser.go +++ b/internal/quic/packet_parser.go @@ -463,18 +463,17 @@ func consumeRetireConnectionIDFrame(b []byte) (seq int64, n int) { return seq, n } -func consumePathChallengeFrame(b []byte) (data uint64, n int) { +func consumePathChallengeFrame(b []byte) (data pathChallengeData, n int) { n = 1 - var nn int - data, nn = consumeUint64(b[n:]) - if nn < 0 { - return 0, -1 + nn := copy(data[:], b[n:]) + if nn != len(data) { + return data, -1 } n += nn return data, n } -func consumePathResponseFrame(b []byte) (data uint64, n int) { +func consumePathResponseFrame(b []byte) (data pathChallengeData, n int) { return consumePathChallengeFrame(b) // identical frame format } diff --git a/internal/quic/packet_writer.go b/internal/quic/packet_writer.go index 9ed393502c..e4d71e622b 100644 --- a/internal/quic/packet_writer.go +++ b/internal/quic/packet_writer.go @@ -243,10 +243,7 @@ func (w *packetWriter) appendPingFrame() (added bool) { return false } w.b = append(w.b, frameTypePing) - // Mark this packet as ack-eliciting and in-flight, - // but there's no need to record the presence of a PING frame in it. - w.sent.ackEliciting = true - w.sent.inFlight = true + w.sent.markAckEliciting() // no need to record the frame itself return true } @@ -495,23 +492,23 @@ func (w *packetWriter) appendRetireConnectionIDFrame(seq int64) (added bool) { return true } -func (w *packetWriter) appendPathChallengeFrame(data uint64) (added bool) { +func (w *packetWriter) appendPathChallengeFrame(data pathChallengeData) (added bool) { if w.avail() < 1+8 { return false } w.b = append(w.b, frameTypePathChallenge) - w.b = binary.BigEndian.AppendUint64(w.b, data) - w.sent.appendAckElicitingFrame(frameTypePathChallenge) + w.b = append(w.b, data[:]...) + w.sent.markAckEliciting() // no need to record the frame itself return true } -func (w *packetWriter) appendPathResponseFrame(data uint64) (added bool) { +func (w *packetWriter) appendPathResponseFrame(data pathChallengeData) (added bool) { if w.avail() < 1+8 { return false } w.b = append(w.b, frameTypePathResponse) - w.b = binary.BigEndian.AppendUint64(w.b, data) - w.sent.appendAckElicitingFrame(frameTypePathResponse) + w.b = append(w.b, data[:]...) + w.sent.markAckEliciting() // no need to record the frame itself return true } diff --git a/internal/quic/path.go b/internal/quic/path.go new file mode 100644 index 0000000000..8c237dd45f --- /dev/null +++ b/internal/quic/path.go @@ -0,0 +1,89 @@ +// Copyright 2024 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" + +type pathState struct { + // Response to a peer's PATH_CHALLENGE. + // This is not a sentVal, because we don't resend lost PATH_RESPONSE frames. + // We only track the most recent PATH_CHALLENGE. + // If the peer sends a second PATH_CHALLENGE before we respond to the first, + // we'll drop the first response. + sendPathResponse pathResponseType + data pathChallengeData +} + +// pathChallengeData is data carried in a PATH_CHALLENGE or PATH_RESPONSE frame. +type pathChallengeData [64 / 8]byte + +type pathResponseType uint8 + +const ( + pathResponseNotNeeded = pathResponseType(iota) + pathResponseSmall // send PATH_RESPONSE, do not expand datagram + pathResponseExpanded // send PATH_RESPONSE, expand datagram to 1200 bytes +) + +func (c *Conn) handlePathChallenge(_ time.Time, dgram *datagram, data pathChallengeData) { + // A PATH_RESPONSE is sent in a datagram expanded to 1200 bytes, + // except when this would exceed the anti-amplification limit. + // + // Rather than maintaining anti-amplification state for each path + // we may be sending a PATH_RESPONSE on, follow the following heuristic: + // + // If we receive a PATH_CHALLENGE in an expanded datagram, + // respond with an expanded datagram. + // + // If we receive a PATH_CHALLENGE in a non-expanded datagram, + // then the peer is presumably blocked by its own anti-amplification limit. + // Respond with a non-expanded datagram. Receiving this PATH_RESPONSE + // will validate the path to the peer, remove its anti-amplification limit, + // and permit it to send a followup PATH_CHALLENGE in an expanded datagram. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-8.2.1 + if len(dgram.b) >= smallestMaxDatagramSize { + c.path.sendPathResponse = pathResponseExpanded + } else { + c.path.sendPathResponse = pathResponseSmall + } + c.path.data = data +} + +func (c *Conn) handlePathResponse(now time.Time, _ pathChallengeData) { + // "If the content of a PATH_RESPONSE frame does not match the content of + // a PATH_CHALLENGE frame previously sent by the endpoint, + // the endpoint MAY generate a connection error of type PROTOCOL_VIOLATION." + // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.18-4 + // + // We never send PATH_CHALLENGE frames. + c.abort(now, localTransportError{ + code: errProtocolViolation, + reason: "PATH_RESPONSE received when no PATH_CHALLENGE sent", + }) +} + +// appendPathFrames appends path validation related frames to the current packet. +// If the return value pad is true, then the packet should be padded to 1200 bytes. +func (c *Conn) appendPathFrames() (pad, ok bool) { + if c.path.sendPathResponse == pathResponseNotNeeded { + return pad, true + } + // We're required to send the PATH_RESPONSE on the path where the + // PATH_CHALLENGE was received (RFC 9000, Section 8.2.2). + // + // At the moment, we don't support path migration and reject packets if + // the peer changes its source address, so just sending the PATH_RESPONSE + // in a regular datagram is fine. + if !c.w.appendPathResponseFrame(c.path.data) { + return pad, false + } + if c.path.sendPathResponse == pathResponseExpanded { + pad = true + } + c.path.sendPathResponse = pathResponseNotNeeded + return pad, true +} diff --git a/internal/quic/path_test.go b/internal/quic/path_test.go new file mode 100644 index 0000000000..a309ed14ba --- /dev/null +++ b/internal/quic/path_test.go @@ -0,0 +1,66 @@ +// 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" +) + +func TestPathChallengeReceived(t *testing.T) { + for _, test := range []struct { + name string + padTo int + wantPadding int + }{{ + name: "unexpanded", + padTo: 0, + wantPadding: 0, + }, { + name: "expanded", + padTo: 1200, + wantPadding: 1200, + }} { + // "The recipient of [a PATH_CHALLENGE] frame MUST generate + // a PATH_RESPONSE frame [...] containing the same Data value." + // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.17-7 + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + data := pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef} + tc.writeFrames(packetType1RTT, debugFramePathChallenge{ + data: data, + }, debugFramePadding{ + to: test.padTo, + }) + tc.wantFrame("response to PATH_CHALLENGE", + packetType1RTT, debugFramePathResponse{ + data: data, + }) + if got, want := tc.lastDatagram.paddedSize, test.wantPadding; got != want { + t.Errorf("PATH_RESPONSE expanded to %v bytes, want %v", got, want) + } + tc.wantIdle("connection is idle") + } +} + +func TestPathResponseMismatchReceived(t *testing.T) { + // "If the content of a PATH_RESPONSE frame does not match the content of + // a PATH_CHALLENGE frame previously sent by the endpoint, + // the endpoint MAY generate a connection error of type PROTOCOL_VIOLATION." + // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.18-4 + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + tc.writeFrames(packetType1RTT, debugFramePathResponse{ + data: pathChallengeData{}, + }) + tc.wantFrame("invalid PATH_RESPONSE causes the connection to close", + packetType1RTT, debugFrameConnectionCloseTransport{ + code: errProtocolViolation, + }, + ) +} diff --git a/internal/quic/sent_packet.go b/internal/quic/sent_packet.go index 194cdc9fa0..226152327d 100644 --- a/internal/quic/sent_packet.go +++ b/internal/quic/sent_packet.go @@ -59,6 +59,12 @@ func (sent *sentPacket) reset() { } } +// markAckEliciting marks the packet as containing an ack-eliciting frame. +func (sent *sentPacket) markAckEliciting() { + sent.ackEliciting = true + sent.inFlight = true +} + // The append* methods record information about frames in the packet. func (sent *sentPacket) appendNonAckElicitingFrame(frameType byte) { From 22cbde9a565f4e40b5060a41d5e5171adcff673e Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 20 Feb 2024 14:58:00 -0800 Subject: [PATCH 141/168] quic: set ServerName in client connection TLSConfig Client connections must set tls.Config.ServerName to authenticate the identity of the server. (RFC 9001, Section 4.4.) Previously, we specified a single tls.Config per Endpoint. Change the Config passed to Listen to only apply to client connections accepted by the endpoint. Add a Config parameter to Listener.Dial to allow specifying a separate config per outbound connection, allowing the user to set the ServerName field. When the user does not set ServerName, set it ourselves. For golang/go#58547 Change-Id: Ie2500ae7c7a85400e6cc1c10cefa2bd4c746e313 Reviewed-on: https://go-review.googlesource.com/c/net/+/565796 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/cmd/interop/main.go | 6 ++-- internal/quic/config.go | 7 ++++ internal/quic/conn.go | 4 +-- internal/quic/conn_test.go | 2 ++ internal/quic/endpoint.go | 59 ++++++++++++++++++------------- internal/quic/endpoint_test.go | 31 ++++++++++------ internal/quic/tls.go | 14 ++++++-- 7 files changed, 81 insertions(+), 42 deletions(-) diff --git a/internal/quic/cmd/interop/main.go b/internal/quic/cmd/interop/main.go index 20f737b525..0899e0f1e4 100644 --- a/internal/quic/cmd/interop/main.go +++ b/internal/quic/cmd/interop/main.go @@ -148,7 +148,7 @@ func basicTest(ctx context.Context, config *quic.Config, urls []string) { g.Add(1) go func() { defer g.Done() - fetchFrom(ctx, l, addr, u) + fetchFrom(ctx, config, l, addr, u) }() } @@ -221,8 +221,8 @@ func parseURL(s string) (u *url.URL, authority string, err error) { return u, authority, nil } -func fetchFrom(ctx context.Context, l *quic.Endpoint, addr string, urls []*url.URL) { - conn, err := l.Dial(ctx, "udp", addr) +func fetchFrom(ctx context.Context, config *quic.Config, l *quic.Endpoint, addr string, urls []*url.URL) { + conn, err := l.Dial(ctx, "udp", addr, config) if err != nil { log.Printf("%v: %v", addr, err) return diff --git a/internal/quic/config.go b/internal/quic/config.go index b045b7b92c..5d420312bb 100644 --- a/internal/quic/config.go +++ b/internal/quic/config.go @@ -107,6 +107,13 @@ type Config struct { QLogLogger *slog.Logger } +// Clone returns a shallow clone of c, or nil if c is nil. +// It is safe to clone a [Config] that is being used concurrently by a QUIC endpoint. +func (c *Config) Clone() *Config { + n := *c + return &n +} + func configDefault[T ~int64](v, def, limit T) T { switch { case v == 0: diff --git a/internal/quic/conn.go b/internal/quic/conn.go index d462e9617a..38e8fe8f4e 100644 --- a/internal/quic/conn.go +++ b/internal/quic/conn.go @@ -94,7 +94,7 @@ type newServerConnIDs struct { retrySrcConnID []byte // source from server's Retry } -func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort, config *Config, e *Endpoint) (conn *Conn, _ error) { +func newConn(now time.Time, side connSide, cids newServerConnIDs, peerHostname string, peerAddr netip.AddrPort, config *Config, e *Endpoint) (conn *Conn, _ error) { c := &Conn{ side: side, endpoint: e, @@ -146,7 +146,7 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip c.lifetimeInit() c.restartIdleTimer(now) - if err := c.startTLS(now, initialConnID, transportParameters{ + if err := c.startTLS(now, initialConnID, peerHostname, transportParameters{ initialSrcConnID: c.connIDState.srcConnID(), originalDstConnID: cids.originalDstConnID, retrySrcConnID: cids.retrySrcConnID, diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go index 16ee3cf2fc..a765ad60cd 100644 --- a/internal/quic/conn_test.go +++ b/internal/quic/conn_test.go @@ -242,8 +242,10 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { endpoint.configTestConn = configTestConn conn, err := endpoint.e.newConn( endpoint.now, + config, side, cids, + "", netip.MustParseAddrPort("127.0.0.1:443")) if err != nil { t.Fatal(err) diff --git a/internal/quic/endpoint.go b/internal/quic/endpoint.go index 6631708b82..a55336b240 100644 --- a/internal/quic/endpoint.go +++ b/internal/quic/endpoint.go @@ -22,11 +22,11 @@ import ( // // Multiple goroutines may invoke methods on an Endpoint simultaneously. type Endpoint struct { - config *Config - packetConn packetConn - testHooks endpointTestHooks - resetGen statelessResetTokenGenerator - retry retryState + listenConfig *Config + packetConn packetConn + testHooks endpointTestHooks + resetGen statelessResetTokenGenerator + retry retryState acceptQueue queue[*Conn] // new inbound connections connsMap connsMap // only accessed by the listen loop @@ -51,9 +51,11 @@ type packetConn interface { } // Listen listens on a local network address. -// The configuration config must be non-nil. -func Listen(network, address string, config *Config) (*Endpoint, error) { - if config.TLSConfig == nil { +// +// The config is used to for connections accepted by the endpoint. +// If the config is nil, the endpoint will not accept connections. +func Listen(network, address string, listenConfig *Config) (*Endpoint, error) { + if listenConfig != nil && listenConfig.TLSConfig == nil { return nil, errors.New("TLSConfig is not set") } a, err := net.ResolveUDPAddr(network, address) @@ -68,21 +70,25 @@ func Listen(network, address string, config *Config) (*Endpoint, error) { if err != nil { return nil, err } - return newEndpoint(pc, config, nil) + return newEndpoint(pc, listenConfig, nil) } func newEndpoint(pc packetConn, config *Config, hooks endpointTestHooks) (*Endpoint, error) { e := &Endpoint{ - config: config, - packetConn: pc, - testHooks: hooks, - conns: make(map[*Conn]struct{}), - acceptQueue: newQueue[*Conn](), - closec: make(chan struct{}), - } - e.resetGen.init(config.StatelessResetKey) + listenConfig: config, + packetConn: pc, + testHooks: hooks, + conns: make(map[*Conn]struct{}), + acceptQueue: newQueue[*Conn](), + closec: make(chan struct{}), + } + var statelessResetKey [32]byte + if config != nil { + statelessResetKey = config.StatelessResetKey + } + e.resetGen.init(statelessResetKey) e.connsMap.init() - if config.RequireAddressValidation { + if config != nil && config.RequireAddressValidation { if err := e.retry.init(); err != nil { return nil, err } @@ -141,14 +147,15 @@ func (e *Endpoint) Accept(ctx context.Context) (*Conn, error) { } // Dial creates and returns a connection to a network address. -func (e *Endpoint) Dial(ctx context.Context, network, address string) (*Conn, error) { +// The config cannot be nil. +func (e *Endpoint) Dial(ctx context.Context, network, address string, config *Config) (*Conn, error) { u, err := net.ResolveUDPAddr(network, address) if err != nil { return nil, err } addr := u.AddrPort() addr = netip.AddrPortFrom(addr.Addr().Unmap(), addr.Port()) - c, err := e.newConn(time.Now(), clientSide, newServerConnIDs{}, addr) + c, err := e.newConn(time.Now(), config, clientSide, newServerConnIDs{}, address, addr) if err != nil { return nil, err } @@ -159,13 +166,13 @@ func (e *Endpoint) Dial(ctx context.Context, network, address string) (*Conn, er return c, nil } -func (e *Endpoint) newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip.AddrPort) (*Conn, error) { +func (e *Endpoint) newConn(now time.Time, config *Config, side connSide, cids newServerConnIDs, peerHostname string, peerAddr netip.AddrPort) (*Conn, error) { e.connsMu.Lock() defer e.connsMu.Unlock() if e.closing { return nil, errors.New("endpoint closed") } - c, err := newConn(now, side, cids, peerAddr, e.config, e) + c, err := newConn(now, side, cids, peerHostname, peerAddr, config, e) if err != nil { return nil, err } @@ -288,11 +295,15 @@ func (e *Endpoint) handleUnknownDestinationDatagram(m *datagram) { // https://www.rfc-editor.org/rfc/rfc9000#section-10.3-16 return } + if e.listenConfig == nil { + // We are not configured to accept connections. + return + } cids := newServerConnIDs{ srcConnID: p.srcConnID, dstConnID: p.dstConnID, } - if e.config.RequireAddressValidation { + if e.listenConfig.RequireAddressValidation { var ok bool cids.retrySrcConnID = p.dstConnID cids.originalDstConnID, ok = e.validateInitialAddress(now, p, m.peerAddr) @@ -303,7 +314,7 @@ func (e *Endpoint) handleUnknownDestinationDatagram(m *datagram) { cids.originalDstConnID = p.dstConnID } var err error - c, err := e.newConn(now, serverSide, cids, m.peerAddr) + c, err := e.newConn(now, e.listenConfig, serverSide, cids, "", m.peerAddr) if err != nil { // The accept queue is probably full. // We could send a CONNECTION_CLOSE to the peer to reject the connection. diff --git a/internal/quic/endpoint_test.go b/internal/quic/endpoint_test.go index b9fb55fb34..b6669fc837 100644 --- a/internal/quic/endpoint_test.go +++ b/internal/quic/endpoint_test.go @@ -67,7 +67,8 @@ func newLocalConnPair(t testing.TB, conf1, conf2 *Config) (clientConn, serverCon ctx := context.Background() e1 := newLocalEndpoint(t, serverSide, conf1) e2 := newLocalEndpoint(t, clientSide, conf2) - c2, err := e2.Dial(ctx, "udp", e1.LocalAddr().String()) + conf2 = makeTestConfig(conf2, clientSide) + c2, err := e2.Dial(ctx, "udp", e1.LocalAddr().String(), conf2) if err != nil { t.Fatal(err) } @@ -80,9 +81,24 @@ func newLocalConnPair(t testing.TB, conf1, conf2 *Config) (clientConn, serverCon func newLocalEndpoint(t testing.TB, side connSide, conf *Config) *Endpoint { t.Helper() + conf = makeTestConfig(conf, side) + e, err := Listen("udp", "127.0.0.1:0", conf) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + e.Close(canceledContext()) + }) + return e +} + +func makeTestConfig(conf *Config, side connSide) *Config { + if conf == nil { + return nil + } + newConf := *conf + conf = &newConf if conf.TLSConfig == nil { - newConf := *conf - conf = &newConf conf.TLSConfig = newTestTLSConfig(side) } if conf.QLogLogger == nil { @@ -91,14 +107,7 @@ func newLocalEndpoint(t testing.TB, side connSide, conf *Config) *Endpoint { Dir: *qlogdir, })) } - e, err := Listen("udp", "127.0.0.1:0", conf) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - e.Close(canceledContext()) - }) - return e + return conf } type testEndpoint struct { diff --git a/internal/quic/tls.go b/internal/quic/tls.go index a37e26fb8e..e2f2e5bde1 100644 --- a/internal/quic/tls.go +++ b/internal/quic/tls.go @@ -11,14 +11,24 @@ import ( "crypto/tls" "errors" "fmt" + "net" "time" ) // startTLS starts the TLS handshake. -func (c *Conn) startTLS(now time.Time, initialConnID []byte, params transportParameters) error { +func (c *Conn) startTLS(now time.Time, initialConnID []byte, peerHostname string, params transportParameters) error { + tlsConfig := c.config.TLSConfig + if a, _, err := net.SplitHostPort(peerHostname); err == nil { + peerHostname = a + } + if tlsConfig.ServerName == "" && peerHostname != "" { + tlsConfig = tlsConfig.Clone() + tlsConfig.ServerName = peerHostname + } + c.keysInitial = initialKeys(initialConnID, c.side) - qconfig := &tls.QUICConfig{TLSConfig: c.config.TLSConfig} + qconfig := &tls.QUICConfig{TLSConfig: tlsConfig} if c.side == clientSide { c.tls = tls.QUICClient(qconfig) } else { From 4bdc6df28ea746166f486314f8848eb9b25b9073 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 14 Feb 2024 11:22:21 -0800 Subject: [PATCH 142/168] quic: expand package docs, and document Stream For golang/go#58547 Change-Id: Ie5dd0ed383ea7a5b3a45103cb730ff62792f62e1 Reviewed-on: https://go-review.googlesource.com/c/net/+/565797 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- internal/quic/doc.go | 42 ++++++++++++++++++++++++++++++++++++++--- internal/quic/stream.go | 15 +++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/internal/quic/doc.go b/internal/quic/doc.go index 2fe17fe226..2fd10f0878 100644 --- a/internal/quic/doc.go +++ b/internal/quic/doc.go @@ -2,8 +2,44 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package quic is an experimental, incomplete implementation of the QUIC protocol. -// This package is a work in progress, and is not ready for use at this time. +// Package quic implements the QUIC protocol. // -// This package implements (or will implement) RFC 9000, RFC 9001, and RFC 9002. +// This package is a work in progress. +// It is not ready for production usage. +// Its API is subject to change without notice. +// +// This package is low-level. +// Most users will use it indirectly through an HTTP/3 implementation. +// +// # Usage +// +// An [Endpoint] sends and receives traffic on a network address. +// Create an Endpoint to either accept inbound QUIC connections +// or create outbound ones. +// +// A [Conn] is a QUIC connection. +// +// A [Stream] is a QUIC stream, an ordered, reliable byte stream. +// +// # Cancelation +// +// All blocking operations may be canceled using a context.Context. +// When performing an operation with a canceled context, the operation +// will succeed if doing so does not require blocking. For example, +// reading from a stream will return data when buffered data is available, +// even if the stream context is canceled. +// +// # Limitations +// +// This package is a work in progress. +// Known limitations include: +// +// - Performance is untuned. +// - 0-RTT is not supported. +// - Address migration is not supported. +// - Server preferred addresses are not supported. +// - The latency spin bit is not supported. +// - Stream send/receive windows are configurable, +// but are fixed and do not adapt to available throughput. +// - Path MTU discovery is not implemented. package quic diff --git a/internal/quic/stream.go b/internal/quic/stream.go index c5fafdf1d3..cb45534f82 100644 --- a/internal/quic/stream.go +++ b/internal/quic/stream.go @@ -14,6 +14,21 @@ import ( "math" ) +// A Stream is an ordered byte stream. +// +// Streams may be bidirectional, read-only, or write-only. +// Methods inappropriate for a stream's direction +// (for example, [Write] to a read-only stream) +// return errors. +// +// It is not safe to perform concurrent reads from or writes to a stream. +// It is safe, however, to read and write at the same time. +// +// Reads and writes are buffered. +// It is generally not necessary to wrap a stream in a [bufio.ReadWriter] +// or otherwise apply additional buffering. +// +// To cancel reads or writes, use the [SetReadContext] and [SetWriteContext] methods. type Stream struct { id streamID conn *Conn From 34cc4464c5cb7947126d80f9d75b4c16d229337d Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 23 Feb 2024 08:53:01 -0800 Subject: [PATCH 143/168] quic: temporarily disable networking tests failing on various platforms For golang/go#65906 For golang/go#65907 Change-Id: I5fe83a27f47b6f2337d280465bf134dbd883809d Reviewed-on: https://go-review.googlesource.com/c/net/+/566098 Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Bryan Mills --- internal/quic/udp_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/quic/udp_test.go b/internal/quic/udp_test.go index 27eddf811d..450351b6b4 100644 --- a/internal/quic/udp_test.go +++ b/internal/quic/udp_test.go @@ -16,6 +16,7 @@ import ( ) func TestUDPSourceUnspecified(t *testing.T) { + t.Skip("https://go.dev/issue/65906 - temporarily skipped pending fix") // Send datagram with no source address set. runUDPTest(t, func(t *testing.T, test udpTest) { data := []byte("source unspecified") @@ -33,6 +34,7 @@ func TestUDPSourceUnspecified(t *testing.T) { } func TestUDPSourceSpecified(t *testing.T) { + t.Skip("https://go.dev/issue/65906 - temporarily skipped pending fix") // Send datagram with source address set. runUDPTest(t, func(t *testing.T, test udpTest) { data := []byte("source specified") @@ -51,6 +53,7 @@ func TestUDPSourceSpecified(t *testing.T) { } func TestUDPSourceInvalid(t *testing.T) { + t.Skip("https://go.dev/issue/65906 - temporarily skipped pending fix") // Send datagram with source address set to an address not associated with the connection. if !udpInvalidLocalAddrIsError { t.Skipf("%v: sending from invalid source succeeds", runtime.GOOS) @@ -74,6 +77,7 @@ func TestUDPSourceInvalid(t *testing.T) { } func TestUDPECN(t *testing.T) { + t.Skip("https://go.dev/issue/65907 - temporarily skipped pending fix") if !udpECNSupport { t.Skipf("%v: no ECN support", runtime.GOOS) } From 591be7f10be18b4b24250868fb61a93c2e5af3f4 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 23 Feb 2024 09:54:56 -0800 Subject: [PATCH 144/168] quic: fix UDP on big-endian Linux, tests on various architectures The following cmsgs contain a native-endian 32-bit integer: - IP_TOS, passed to sendmsg - IPV6_TCLASS, always IP_TOS received from recvmsg contains a single byte, because why not. We were inadvertently assuming little-endian integers in all cases. Add endianness conversion as appropriate. Disable tests that rely on IPv4-in-IPv6 mapped sockets on dragonfly and openbsd, which don't support this feature. (A "udp" socket cannot receive IPv6 packets on these platforms.) Disable IPv6 tests on wasm, where the simulated networking appears to generally not support IPv6. Fixes golang/go#65906 Fixes golang/go#65907 Change-Id: Ie50af12e182a1a5d685ce4fbdf008748f6aee339 Reviewed-on: https://go-review.googlesource.com/c/net/+/566296 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam Reviewed-by: Bryan Mills --- internal/quic/udp_darwin.go | 25 +++++++++++ internal/quic/udp_linux.go | 20 +++++++++ internal/quic/udp_msg.go | 89 ++++++++++++++++++------------------- internal/quic/udp_test.go | 17 +++++-- 4 files changed, 102 insertions(+), 49 deletions(-) diff --git a/internal/quic/udp_darwin.go b/internal/quic/udp_darwin.go index 3868a36a8e..2eb2e9f9f0 100644 --- a/internal/quic/udp_darwin.go +++ b/internal/quic/udp_darwin.go @@ -6,8 +6,33 @@ package quic +import ( + "encoding/binary" + + "golang.org/x/sys/unix" +) + // See udp.go. const ( udpECNSupport = true udpInvalidLocalAddrIsError = true ) + +// Confusingly, on Darwin the contents of the IP_TOS option differ depending on whether +// it is used as an inbound or outbound cmsg. + +func parseIPTOS(b []byte) (ecnBits, bool) { + // Single byte. The low two bits are the ECN field. + if len(b) != 1 { + return 0, false + } + return ecnBits(b[0] & ecnMask), true +} + +func appendCmsgECNv4(b []byte, ecn ecnBits) []byte { + // 32-bit integer. + // https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/netinet/in_tclass.c#L1062-L1073 + b, data := appendCmsg(b, unix.IPPROTO_IP, unix.IP_TOS, 4) + binary.NativeEndian.PutUint32(data, uint32(ecn)) + return b +} diff --git a/internal/quic/udp_linux.go b/internal/quic/udp_linux.go index 2ba3e6f2f4..6f191ed398 100644 --- a/internal/quic/udp_linux.go +++ b/internal/quic/udp_linux.go @@ -6,8 +6,28 @@ package quic +import ( + "golang.org/x/sys/unix" +) + // See udp.go. const ( udpECNSupport = true udpInvalidLocalAddrIsError = false ) + +// The IP_TOS socket option is a single byte containing the IP TOS field. +// The low two bits are the ECN field. + +func parseIPTOS(b []byte) (ecnBits, bool) { + if len(b) != 1 { + return 0, false + } + return ecnBits(b[0] & ecnMask), true +} + +func appendCmsgECNv4(b []byte, ecn ecnBits) []byte { + b, data := appendCmsg(b, unix.IPPROTO_IP, unix.IP_TOS, 1) + data[0] = byte(ecn) + return b +} diff --git a/internal/quic/udp_msg.go b/internal/quic/udp_msg.go index bdc1b710df..0b600a2b46 100644 --- a/internal/quic/udp_msg.go +++ b/internal/quic/udp_msg.go @@ -7,6 +7,7 @@ package quic import ( + "encoding/binary" "net" "net/netip" "sync" @@ -141,15 +142,11 @@ func parseControl(d *datagram, control []byte) { case unix.IPPROTO_IP: switch hdr.Type { case unix.IP_TOS, unix.IP_RECVTOS: - // Single byte containing the IP TOS field. - // The low two bits are the ECN field. - // // (Linux sets the type to IP_TOS, Darwin to IP_RECVTOS, - // jus check for both.) - if len(data) < 1 { - break + // just check for both.) + if ecn, ok := parseIPTOS(data); ok { + d.ecn = ecn } - d.ecn = ecnBits(data[0] & ecnMask) case unix.IP_PKTINFO: if a, ok := parseInPktinfo(data); ok { d.localAddr = netip.AddrPortFrom(a, d.localAddr.Port()) @@ -158,12 +155,11 @@ func parseControl(d *datagram, control []byte) { case unix.IPPROTO_IPV6: switch hdr.Type { case unix.IPV6_TCLASS: - // Single byte containing the traffic class field. + // 32-bit integer containing the traffic class field. // The low two bits are the ECN field. - if len(data) < 1 { - break + if ecn, ok := parseIPv6TCLASS(data); ok { + d.ecn = ecn } - d.ecn = ecnBits(data[0] & ecnMask) case unix.IPV6_PKTINFO: if a, ok := parseIn6Pktinfo(data); ok { d.localAddr = netip.AddrPortFrom(a, d.localAddr.Port()) @@ -173,27 +169,33 @@ func parseControl(d *datagram, control []byte) { } } -func parseInPktinfo(b []byte) (netip.Addr, bool) { - // struct in_pktinfo { - // unsigned int ipi_ifindex; /* send/recv interface index */ - // struct in_addr ipi_spec_dst; /* Local address */ - // struct in_addr ipi_addr; /* IP Header dst address */ - // }; - if len(b) != 12 { - return netip.Addr{}, false +// IPV6_TCLASS is specified by RFC 3542 as an int. + +func parseIPv6TCLASS(b []byte) (ecnBits, bool) { + if len(b) != 4 { + return 0, false } - return netip.AddrFrom4([4]byte(b[8:][:4])), true + return ecnBits(binary.NativeEndian.Uint32(b) & ecnMask), true } -func parseIn6Pktinfo(b []byte) (netip.Addr, bool) { - // struct in6_pktinfo { - // struct in6_addr ipi6_addr; /* src/dst IPv6 address */ - // unsigned int ipi6_ifindex; /* send/recv interface index */ - // }; - if len(b) != 20 { +func appendCmsgECNv6(b []byte, ecn ecnBits) []byte { + b, data := appendCmsg(b, unix.IPPROTO_IPV6, unix.IPV6_TCLASS, 4) + binary.NativeEndian.PutUint32(data, uint32(ecn)) + return b +} + +// struct in_pktinfo { +// unsigned int ipi_ifindex; /* send/recv interface index */ +// struct in_addr ipi_spec_dst; /* Local address */ +// struct in_addr ipi_addr; /* IP Header dst address */ +// }; + +// parseInPktinfo returns the destination address from an IP_PKTINFO. +func parseInPktinfo(b []byte) (dst netip.Addr, ok bool) { + if len(b) != 12 { return netip.Addr{}, false } - return netip.AddrFrom16([16]byte(b[:16])).Unmap(), true + return netip.AddrFrom4([4]byte(b[8:][:4])), true } // appendCmsgIPSourceAddrV4 appends an IP_PKTINFO setting the source address @@ -210,31 +212,28 @@ func appendCmsgIPSourceAddrV4(b []byte, src netip.Addr) []byte { return b } -// appendCmsgIPSourceAddrV6 appends an IP_PKTINFO or IPV6_PKTINFO -// setting the source address for an outbound datagram. +// struct in6_pktinfo { +// struct in6_addr ipi6_addr; /* src/dst IPv6 address */ +// unsigned int ipi6_ifindex; /* send/recv interface index */ +// }; + +// parseIn6Pktinfo returns the destination address from an IPV6_PKTINFO. +func parseIn6Pktinfo(b []byte) (netip.Addr, bool) { + if len(b) != 20 { + return netip.Addr{}, false + } + return netip.AddrFrom16([16]byte(b[:16])).Unmap(), true +} + +// appendCmsgIPSourceAddrV6 appends an IPV6_PKTINFO setting the source address +// for an outbound datagram. func appendCmsgIPSourceAddrV6(b []byte, src netip.Addr) []byte { - // struct in6_pktinfo { - // struct in6_addr ipi6_addr; /* src/dst IPv6 address */ - // unsigned int ipi6_ifindex; /* send/recv interface index */ - // }; b, data := appendCmsg(b, unix.IPPROTO_IPV6, unix.IPV6_PKTINFO, 20) ip := src.As16() copy(data[0:], ip[:]) return b } -func appendCmsgECNv4(b []byte, ecn ecnBits) []byte { - b, data := appendCmsg(b, unix.IPPROTO_IP, unix.IP_TOS, 4) - data[0] = byte(ecn) - return b -} - -func appendCmsgECNv6(b []byte, ecn ecnBits) []byte { - b, data := appendCmsg(b, unix.IPPROTO_IPV6, unix.IPV6_TCLASS, 4) - data[0] = byte(ecn) - return b -} - // appendCmsg appends a cmsg with the given level, type, and size to b. // It returns the new buffer, and the data section of the cmsg. func appendCmsg(b []byte, level, typ int32, size int) (_, data []byte) { diff --git a/internal/quic/udp_test.go b/internal/quic/udp_test.go index 450351b6b4..d3732c140e 100644 --- a/internal/quic/udp_test.go +++ b/internal/quic/udp_test.go @@ -16,9 +16,9 @@ import ( ) func TestUDPSourceUnspecified(t *testing.T) { - t.Skip("https://go.dev/issue/65906 - temporarily skipped pending fix") // Send datagram with no source address set. runUDPTest(t, func(t *testing.T, test udpTest) { + t.Logf("%v", test.dstAddr) data := []byte("source unspecified") if err := test.src.Write(datagram{ b: data, @@ -34,7 +34,6 @@ func TestUDPSourceUnspecified(t *testing.T) { } func TestUDPSourceSpecified(t *testing.T) { - t.Skip("https://go.dev/issue/65906 - temporarily skipped pending fix") // Send datagram with source address set. runUDPTest(t, func(t *testing.T, test udpTest) { data := []byte("source specified") @@ -53,7 +52,6 @@ func TestUDPSourceSpecified(t *testing.T) { } func TestUDPSourceInvalid(t *testing.T) { - t.Skip("https://go.dev/issue/65906 - temporarily skipped pending fix") // Send datagram with source address set to an address not associated with the connection. if !udpInvalidLocalAddrIsError { t.Skipf("%v: sending from invalid source succeeds", runtime.GOOS) @@ -77,7 +75,6 @@ func TestUDPSourceInvalid(t *testing.T) { } func TestUDPECN(t *testing.T) { - t.Skip("https://go.dev/issue/65907 - temporarily skipped pending fix") if !udpECNSupport { t.Skipf("%v: no ECN support", runtime.GOOS) } @@ -125,6 +122,18 @@ func runUDPTest(t *testing.T, f func(t *testing.T, u udpTest)) { spec = "unspec" } t.Run(fmt.Sprintf("%v/%v/%v", test.srcNet, test.dstNet, spec), func(t *testing.T) { + // See: https://go.googlesource.com/go/+/refs/tags/go1.22.0/src/net/ipsock.go#47 + // On these platforms, conns with network="udp" cannot accept IPv6. + switch runtime.GOOS { + case "dragonfly", "openbsd": + if test.srcNet == "udp6" && test.dstNet == "udp" { + t.Skipf("%v: no support for mapping IPv4 address to IPv6", runtime.GOOS) + } + } + if runtime.GOARCH == "wasm" && test.srcNet == "udp6" { + t.Skipf("%v: IPv6 tests fail when using wasm fake net", runtime.GOARCH) + } + srcAddr := netip.AddrPortFrom(netip.MustParseAddr(test.srcAddr), 0) srcConn, err := net.ListenUDP(test.srcNet, net.UDPAddrFromAddrPort(srcAddr)) if err != nil { From fa1142799318d3fa3632ecfd9f318ffa040e7c4c Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 22 Feb 2024 17:31:38 -0800 Subject: [PATCH 145/168] quic: move package out of internal For golang/go#58547 Change-Id: I119d820824f82bfdd236c6826f960d0c934745ca Reviewed-on: https://go-review.googlesource.com/c/net/+/566295 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- internal/quic/cmd/interop/main.go | 4 ++-- {internal/quic => quic}/ack_delay.go | 0 {internal/quic => quic}/ack_delay_test.go | 0 {internal/quic => quic}/acks.go | 0 {internal/quic => quic}/acks_test.go | 0 {internal/quic => quic}/atomic_bits.go | 0 {internal/quic => quic}/bench_test.go | 0 {internal/quic => quic}/config.go | 0 {internal/quic => quic}/config_test.go | 0 {internal/quic => quic}/congestion_reno.go | 0 {internal/quic => quic}/congestion_reno_test.go | 0 {internal/quic => quic}/conn.go | 0 {internal/quic => quic}/conn_async_test.go | 0 {internal/quic => quic}/conn_close.go | 0 {internal/quic => quic}/conn_close_test.go | 0 {internal/quic => quic}/conn_flow.go | 0 {internal/quic => quic}/conn_flow_test.go | 0 {internal/quic => quic}/conn_id.go | 0 {internal/quic => quic}/conn_id_test.go | 0 {internal/quic => quic}/conn_loss.go | 0 {internal/quic => quic}/conn_loss_test.go | 0 {internal/quic => quic}/conn_recv.go | 0 {internal/quic => quic}/conn_send.go | 0 {internal/quic => quic}/conn_send_test.go | 0 {internal/quic => quic}/conn_streams.go | 0 {internal/quic => quic}/conn_streams_test.go | 0 {internal/quic => quic}/conn_test.go | 2 +- {internal/quic => quic}/crypto_stream.go | 0 {internal/quic => quic}/crypto_stream_test.go | 0 {internal/quic => quic}/dgram.go | 0 {internal/quic => quic}/doc.go | 0 {internal/quic => quic}/endpoint.go | 0 {internal/quic => quic}/endpoint_test.go | 2 +- {internal/quic => quic}/errors.go | 0 {internal/quic => quic}/files_test.go | 0 {internal/quic => quic}/frame_debug.go | 0 {internal/quic => quic}/gate.go | 0 {internal/quic => quic}/gate_test.go | 0 {internal/quic => quic}/gotraceback_test.go | 0 {internal/quic => quic}/idle.go | 0 {internal/quic => quic}/idle_test.go | 0 {internal/quic => quic}/key_update_test.go | 0 {internal/quic => quic}/log.go | 0 {internal/quic => quic}/loss.go | 0 {internal/quic => quic}/loss_test.go | 0 {internal/quic => quic}/main_test.go | 0 {internal/quic => quic}/math.go | 0 {internal/quic => quic}/pacer.go | 0 {internal/quic => quic}/pacer_test.go | 0 {internal/quic => quic}/packet.go | 0 {internal/quic => quic}/packet_codec_test.go | 2 +- {internal/quic => quic}/packet_number.go | 0 {internal/quic => quic}/packet_number_test.go | 0 {internal/quic => quic}/packet_parser.go | 0 {internal/quic => quic}/packet_protection.go | 0 {internal/quic => quic}/packet_protection_test.go | 0 {internal/quic => quic}/packet_test.go | 0 {internal/quic => quic}/packet_writer.go | 0 {internal/quic => quic}/path.go | 0 {internal/quic => quic}/path_test.go | 0 {internal/quic => quic}/ping.go | 0 {internal/quic => quic}/ping_test.go | 0 {internal/quic => quic}/pipe.go | 0 {internal/quic => quic}/pipe_test.go | 0 {internal/quic => quic}/qlog.go | 0 {internal/quic => quic}/qlog/handler.go | 0 {internal/quic => quic}/qlog/json_writer.go | 0 {internal/quic => quic}/qlog/json_writer_test.go | 0 {internal/quic => quic}/qlog/qlog.go | 0 {internal/quic => quic}/qlog/qlog_test.go | 0 {internal/quic => quic}/qlog_test.go | 2 +- {internal/quic => quic}/queue.go | 0 {internal/quic => quic}/queue_test.go | 0 {internal/quic => quic}/quic.go | 0 {internal/quic => quic}/quic_test.go | 0 {internal/quic => quic}/rangeset.go | 0 {internal/quic => quic}/rangeset_test.go | 0 {internal/quic => quic}/retry.go | 0 {internal/quic => quic}/retry_test.go | 0 {internal/quic => quic}/rtt.go | 0 {internal/quic => quic}/rtt_test.go | 0 {internal/quic => quic}/sent_packet.go | 0 {internal/quic => quic}/sent_packet_list.go | 0 {internal/quic => quic}/sent_packet_list_test.go | 0 {internal/quic => quic}/sent_packet_test.go | 0 {internal/quic => quic}/sent_val.go | 0 {internal/quic => quic}/sent_val_test.go | 0 {internal/quic => quic}/stateless_reset.go | 0 {internal/quic => quic}/stateless_reset_test.go | 0 {internal/quic => quic}/stream.go | 0 {internal/quic => quic}/stream_limits.go | 0 {internal/quic => quic}/stream_limits_test.go | 0 {internal/quic => quic}/stream_test.go | 0 {internal/quic => quic}/tls.go | 0 {internal/quic => quic}/tls_test.go | 0 {internal/quic => quic}/tlsconfig_test.go | 0 {internal/quic => quic}/transport_params.go | 0 {internal/quic => quic}/transport_params_test.go | 0 {internal/quic => quic}/udp.go | 0 {internal/quic => quic}/udp_darwin.go | 0 {internal/quic => quic}/udp_linux.go | 0 {internal/quic => quic}/udp_msg.go | 0 {internal/quic => quic}/udp_other.go | 0 {internal/quic => quic}/udp_test.go | 0 {internal/quic => quic}/version_test.go | 0 {internal/quic => quic}/wire.go | 0 {internal/quic => quic}/wire_test.go | 0 107 files changed, 6 insertions(+), 6 deletions(-) rename {internal/quic => quic}/ack_delay.go (100%) rename {internal/quic => quic}/ack_delay_test.go (100%) rename {internal/quic => quic}/acks.go (100%) rename {internal/quic => quic}/acks_test.go (100%) rename {internal/quic => quic}/atomic_bits.go (100%) rename {internal/quic => quic}/bench_test.go (100%) rename {internal/quic => quic}/config.go (100%) rename {internal/quic => quic}/config_test.go (100%) rename {internal/quic => quic}/congestion_reno.go (100%) rename {internal/quic => quic}/congestion_reno_test.go (100%) rename {internal/quic => quic}/conn.go (100%) rename {internal/quic => quic}/conn_async_test.go (100%) rename {internal/quic => quic}/conn_close.go (100%) rename {internal/quic => quic}/conn_close_test.go (100%) rename {internal/quic => quic}/conn_flow.go (100%) rename {internal/quic => quic}/conn_flow_test.go (100%) rename {internal/quic => quic}/conn_id.go (100%) rename {internal/quic => quic}/conn_id_test.go (100%) rename {internal/quic => quic}/conn_loss.go (100%) rename {internal/quic => quic}/conn_loss_test.go (100%) rename {internal/quic => quic}/conn_recv.go (100%) rename {internal/quic => quic}/conn_send.go (100%) rename {internal/quic => quic}/conn_send_test.go (100%) rename {internal/quic => quic}/conn_streams.go (100%) rename {internal/quic => quic}/conn_streams_test.go (100%) rename {internal/quic => quic}/conn_test.go (99%) rename {internal/quic => quic}/crypto_stream.go (100%) rename {internal/quic => quic}/crypto_stream_test.go (100%) rename {internal/quic => quic}/dgram.go (100%) rename {internal/quic => quic}/doc.go (100%) rename {internal/quic => quic}/endpoint.go (100%) rename {internal/quic => quic}/endpoint_test.go (99%) rename {internal/quic => quic}/errors.go (100%) rename {internal/quic => quic}/files_test.go (100%) rename {internal/quic => quic}/frame_debug.go (100%) rename {internal/quic => quic}/gate.go (100%) rename {internal/quic => quic}/gate_test.go (100%) rename {internal/quic => quic}/gotraceback_test.go (100%) rename {internal/quic => quic}/idle.go (100%) rename {internal/quic => quic}/idle_test.go (100%) rename {internal/quic => quic}/key_update_test.go (100%) rename {internal/quic => quic}/log.go (100%) rename {internal/quic => quic}/loss.go (100%) rename {internal/quic => quic}/loss_test.go (100%) rename {internal/quic => quic}/main_test.go (100%) rename {internal/quic => quic}/math.go (100%) rename {internal/quic => quic}/pacer.go (100%) rename {internal/quic => quic}/pacer_test.go (100%) rename {internal/quic => quic}/packet.go (100%) rename {internal/quic => quic}/packet_codec_test.go (99%) rename {internal/quic => quic}/packet_number.go (100%) rename {internal/quic => quic}/packet_number_test.go (100%) rename {internal/quic => quic}/packet_parser.go (100%) rename {internal/quic => quic}/packet_protection.go (100%) rename {internal/quic => quic}/packet_protection_test.go (100%) rename {internal/quic => quic}/packet_test.go (100%) rename {internal/quic => quic}/packet_writer.go (100%) rename {internal/quic => quic}/path.go (100%) rename {internal/quic => quic}/path_test.go (100%) rename {internal/quic => quic}/ping.go (100%) rename {internal/quic => quic}/ping_test.go (100%) rename {internal/quic => quic}/pipe.go (100%) rename {internal/quic => quic}/pipe_test.go (100%) rename {internal/quic => quic}/qlog.go (100%) rename {internal/quic => quic}/qlog/handler.go (100%) rename {internal/quic => quic}/qlog/json_writer.go (100%) rename {internal/quic => quic}/qlog/json_writer_test.go (100%) rename {internal/quic => quic}/qlog/qlog.go (100%) rename {internal/quic => quic}/qlog/qlog_test.go (100%) rename {internal/quic => quic}/qlog_test.go (99%) rename {internal/quic => quic}/queue.go (100%) rename {internal/quic => quic}/queue_test.go (100%) rename {internal/quic => quic}/quic.go (100%) rename {internal/quic => quic}/quic_test.go (100%) rename {internal/quic => quic}/rangeset.go (100%) rename {internal/quic => quic}/rangeset_test.go (100%) rename {internal/quic => quic}/retry.go (100%) rename {internal/quic => quic}/retry_test.go (100%) rename {internal/quic => quic}/rtt.go (100%) rename {internal/quic => quic}/rtt_test.go (100%) rename {internal/quic => quic}/sent_packet.go (100%) rename {internal/quic => quic}/sent_packet_list.go (100%) rename {internal/quic => quic}/sent_packet_list_test.go (100%) rename {internal/quic => quic}/sent_packet_test.go (100%) rename {internal/quic => quic}/sent_val.go (100%) rename {internal/quic => quic}/sent_val_test.go (100%) rename {internal/quic => quic}/stateless_reset.go (100%) rename {internal/quic => quic}/stateless_reset_test.go (100%) rename {internal/quic => quic}/stream.go (100%) rename {internal/quic => quic}/stream_limits.go (100%) rename {internal/quic => quic}/stream_limits_test.go (100%) rename {internal/quic => quic}/stream_test.go (100%) rename {internal/quic => quic}/tls.go (100%) rename {internal/quic => quic}/tls_test.go (100%) rename {internal/quic => quic}/tlsconfig_test.go (100%) rename {internal/quic => quic}/transport_params.go (100%) rename {internal/quic => quic}/transport_params_test.go (100%) rename {internal/quic => quic}/udp.go (100%) rename {internal/quic => quic}/udp_darwin.go (100%) rename {internal/quic => quic}/udp_linux.go (100%) rename {internal/quic => quic}/udp_msg.go (100%) rename {internal/quic => quic}/udp_other.go (100%) rename {internal/quic => quic}/udp_test.go (100%) rename {internal/quic => quic}/version_test.go (100%) rename {internal/quic => quic}/wire.go (100%) rename {internal/quic => quic}/wire_test.go (100%) diff --git a/internal/quic/cmd/interop/main.go b/internal/quic/cmd/interop/main.go index 0899e0f1e4..5b652a2b15 100644 --- a/internal/quic/cmd/interop/main.go +++ b/internal/quic/cmd/interop/main.go @@ -25,8 +25,8 @@ import ( "path/filepath" "sync" - "golang.org/x/net/internal/quic" - "golang.org/x/net/internal/quic/qlog" + "golang.org/x/net/quic" + "golang.org/x/net/quic/qlog" ) var ( diff --git a/internal/quic/ack_delay.go b/quic/ack_delay.go similarity index 100% rename from internal/quic/ack_delay.go rename to quic/ack_delay.go diff --git a/internal/quic/ack_delay_test.go b/quic/ack_delay_test.go similarity index 100% rename from internal/quic/ack_delay_test.go rename to quic/ack_delay_test.go diff --git a/internal/quic/acks.go b/quic/acks.go similarity index 100% rename from internal/quic/acks.go rename to quic/acks.go diff --git a/internal/quic/acks_test.go b/quic/acks_test.go similarity index 100% rename from internal/quic/acks_test.go rename to quic/acks_test.go diff --git a/internal/quic/atomic_bits.go b/quic/atomic_bits.go similarity index 100% rename from internal/quic/atomic_bits.go rename to quic/atomic_bits.go diff --git a/internal/quic/bench_test.go b/quic/bench_test.go similarity index 100% rename from internal/quic/bench_test.go rename to quic/bench_test.go diff --git a/internal/quic/config.go b/quic/config.go similarity index 100% rename from internal/quic/config.go rename to quic/config.go diff --git a/internal/quic/config_test.go b/quic/config_test.go similarity index 100% rename from internal/quic/config_test.go rename to quic/config_test.go diff --git a/internal/quic/congestion_reno.go b/quic/congestion_reno.go similarity index 100% rename from internal/quic/congestion_reno.go rename to quic/congestion_reno.go diff --git a/internal/quic/congestion_reno_test.go b/quic/congestion_reno_test.go similarity index 100% rename from internal/quic/congestion_reno_test.go rename to quic/congestion_reno_test.go diff --git a/internal/quic/conn.go b/quic/conn.go similarity index 100% rename from internal/quic/conn.go rename to quic/conn.go diff --git a/internal/quic/conn_async_test.go b/quic/conn_async_test.go similarity index 100% rename from internal/quic/conn_async_test.go rename to quic/conn_async_test.go diff --git a/internal/quic/conn_close.go b/quic/conn_close.go similarity index 100% rename from internal/quic/conn_close.go rename to quic/conn_close.go diff --git a/internal/quic/conn_close_test.go b/quic/conn_close_test.go similarity index 100% rename from internal/quic/conn_close_test.go rename to quic/conn_close_test.go diff --git a/internal/quic/conn_flow.go b/quic/conn_flow.go similarity index 100% rename from internal/quic/conn_flow.go rename to quic/conn_flow.go diff --git a/internal/quic/conn_flow_test.go b/quic/conn_flow_test.go similarity index 100% rename from internal/quic/conn_flow_test.go rename to quic/conn_flow_test.go diff --git a/internal/quic/conn_id.go b/quic/conn_id.go similarity index 100% rename from internal/quic/conn_id.go rename to quic/conn_id.go diff --git a/internal/quic/conn_id_test.go b/quic/conn_id_test.go similarity index 100% rename from internal/quic/conn_id_test.go rename to quic/conn_id_test.go diff --git a/internal/quic/conn_loss.go b/quic/conn_loss.go similarity index 100% rename from internal/quic/conn_loss.go rename to quic/conn_loss.go diff --git a/internal/quic/conn_loss_test.go b/quic/conn_loss_test.go similarity index 100% rename from internal/quic/conn_loss_test.go rename to quic/conn_loss_test.go diff --git a/internal/quic/conn_recv.go b/quic/conn_recv.go similarity index 100% rename from internal/quic/conn_recv.go rename to quic/conn_recv.go diff --git a/internal/quic/conn_send.go b/quic/conn_send.go similarity index 100% rename from internal/quic/conn_send.go rename to quic/conn_send.go diff --git a/internal/quic/conn_send_test.go b/quic/conn_send_test.go similarity index 100% rename from internal/quic/conn_send_test.go rename to quic/conn_send_test.go diff --git a/internal/quic/conn_streams.go b/quic/conn_streams.go similarity index 100% rename from internal/quic/conn_streams.go rename to quic/conn_streams.go diff --git a/internal/quic/conn_streams_test.go b/quic/conn_streams_test.go similarity index 100% rename from internal/quic/conn_streams_test.go rename to quic/conn_streams_test.go diff --git a/internal/quic/conn_test.go b/quic/conn_test.go similarity index 99% rename from internal/quic/conn_test.go rename to quic/conn_test.go index a765ad60cd..f4f1818a64 100644 --- a/internal/quic/conn_test.go +++ b/quic/conn_test.go @@ -21,7 +21,7 @@ import ( "testing" "time" - "golang.org/x/net/internal/quic/qlog" + "golang.org/x/net/quic/qlog" ) var ( diff --git a/internal/quic/crypto_stream.go b/quic/crypto_stream.go similarity index 100% rename from internal/quic/crypto_stream.go rename to quic/crypto_stream.go diff --git a/internal/quic/crypto_stream_test.go b/quic/crypto_stream_test.go similarity index 100% rename from internal/quic/crypto_stream_test.go rename to quic/crypto_stream_test.go diff --git a/internal/quic/dgram.go b/quic/dgram.go similarity index 100% rename from internal/quic/dgram.go rename to quic/dgram.go diff --git a/internal/quic/doc.go b/quic/doc.go similarity index 100% rename from internal/quic/doc.go rename to quic/doc.go diff --git a/internal/quic/endpoint.go b/quic/endpoint.go similarity index 100% rename from internal/quic/endpoint.go rename to quic/endpoint.go diff --git a/internal/quic/endpoint_test.go b/quic/endpoint_test.go similarity index 99% rename from internal/quic/endpoint_test.go rename to quic/endpoint_test.go index b6669fc837..d5f436e6d7 100644 --- a/internal/quic/endpoint_test.go +++ b/quic/endpoint_test.go @@ -16,7 +16,7 @@ import ( "testing" "time" - "golang.org/x/net/internal/quic/qlog" + "golang.org/x/net/quic/qlog" ) func TestConnect(t *testing.T) { diff --git a/internal/quic/errors.go b/quic/errors.go similarity index 100% rename from internal/quic/errors.go rename to quic/errors.go diff --git a/internal/quic/files_test.go b/quic/files_test.go similarity index 100% rename from internal/quic/files_test.go rename to quic/files_test.go diff --git a/internal/quic/frame_debug.go b/quic/frame_debug.go similarity index 100% rename from internal/quic/frame_debug.go rename to quic/frame_debug.go diff --git a/internal/quic/gate.go b/quic/gate.go similarity index 100% rename from internal/quic/gate.go rename to quic/gate.go diff --git a/internal/quic/gate_test.go b/quic/gate_test.go similarity index 100% rename from internal/quic/gate_test.go rename to quic/gate_test.go diff --git a/internal/quic/gotraceback_test.go b/quic/gotraceback_test.go similarity index 100% rename from internal/quic/gotraceback_test.go rename to quic/gotraceback_test.go diff --git a/internal/quic/idle.go b/quic/idle.go similarity index 100% rename from internal/quic/idle.go rename to quic/idle.go diff --git a/internal/quic/idle_test.go b/quic/idle_test.go similarity index 100% rename from internal/quic/idle_test.go rename to quic/idle_test.go diff --git a/internal/quic/key_update_test.go b/quic/key_update_test.go similarity index 100% rename from internal/quic/key_update_test.go rename to quic/key_update_test.go diff --git a/internal/quic/log.go b/quic/log.go similarity index 100% rename from internal/quic/log.go rename to quic/log.go diff --git a/internal/quic/loss.go b/quic/loss.go similarity index 100% rename from internal/quic/loss.go rename to quic/loss.go diff --git a/internal/quic/loss_test.go b/quic/loss_test.go similarity index 100% rename from internal/quic/loss_test.go rename to quic/loss_test.go diff --git a/internal/quic/main_test.go b/quic/main_test.go similarity index 100% rename from internal/quic/main_test.go rename to quic/main_test.go diff --git a/internal/quic/math.go b/quic/math.go similarity index 100% rename from internal/quic/math.go rename to quic/math.go diff --git a/internal/quic/pacer.go b/quic/pacer.go similarity index 100% rename from internal/quic/pacer.go rename to quic/pacer.go diff --git a/internal/quic/pacer_test.go b/quic/pacer_test.go similarity index 100% rename from internal/quic/pacer_test.go rename to quic/pacer_test.go diff --git a/internal/quic/packet.go b/quic/packet.go similarity index 100% rename from internal/quic/packet.go rename to quic/packet.go diff --git a/internal/quic/packet_codec_test.go b/quic/packet_codec_test.go similarity index 99% rename from internal/quic/packet_codec_test.go rename to quic/packet_codec_test.go index 98b3bbb059..3b39795ef5 100644 --- a/internal/quic/packet_codec_test.go +++ b/quic/packet_codec_test.go @@ -15,7 +15,7 @@ import ( "testing" "time" - "golang.org/x/net/internal/quic/qlog" + "golang.org/x/net/quic/qlog" ) func TestParseLongHeaderPacket(t *testing.T) { diff --git a/internal/quic/packet_number.go b/quic/packet_number.go similarity index 100% rename from internal/quic/packet_number.go rename to quic/packet_number.go diff --git a/internal/quic/packet_number_test.go b/quic/packet_number_test.go similarity index 100% rename from internal/quic/packet_number_test.go rename to quic/packet_number_test.go diff --git a/internal/quic/packet_parser.go b/quic/packet_parser.go similarity index 100% rename from internal/quic/packet_parser.go rename to quic/packet_parser.go diff --git a/internal/quic/packet_protection.go b/quic/packet_protection.go similarity index 100% rename from internal/quic/packet_protection.go rename to quic/packet_protection.go diff --git a/internal/quic/packet_protection_test.go b/quic/packet_protection_test.go similarity index 100% rename from internal/quic/packet_protection_test.go rename to quic/packet_protection_test.go diff --git a/internal/quic/packet_test.go b/quic/packet_test.go similarity index 100% rename from internal/quic/packet_test.go rename to quic/packet_test.go diff --git a/internal/quic/packet_writer.go b/quic/packet_writer.go similarity index 100% rename from internal/quic/packet_writer.go rename to quic/packet_writer.go diff --git a/internal/quic/path.go b/quic/path.go similarity index 100% rename from internal/quic/path.go rename to quic/path.go diff --git a/internal/quic/path_test.go b/quic/path_test.go similarity index 100% rename from internal/quic/path_test.go rename to quic/path_test.go diff --git a/internal/quic/ping.go b/quic/ping.go similarity index 100% rename from internal/quic/ping.go rename to quic/ping.go diff --git a/internal/quic/ping_test.go b/quic/ping_test.go similarity index 100% rename from internal/quic/ping_test.go rename to quic/ping_test.go diff --git a/internal/quic/pipe.go b/quic/pipe.go similarity index 100% rename from internal/quic/pipe.go rename to quic/pipe.go diff --git a/internal/quic/pipe_test.go b/quic/pipe_test.go similarity index 100% rename from internal/quic/pipe_test.go rename to quic/pipe_test.go diff --git a/internal/quic/qlog.go b/quic/qlog.go similarity index 100% rename from internal/quic/qlog.go rename to quic/qlog.go diff --git a/internal/quic/qlog/handler.go b/quic/qlog/handler.go similarity index 100% rename from internal/quic/qlog/handler.go rename to quic/qlog/handler.go diff --git a/internal/quic/qlog/json_writer.go b/quic/qlog/json_writer.go similarity index 100% rename from internal/quic/qlog/json_writer.go rename to quic/qlog/json_writer.go diff --git a/internal/quic/qlog/json_writer_test.go b/quic/qlog/json_writer_test.go similarity index 100% rename from internal/quic/qlog/json_writer_test.go rename to quic/qlog/json_writer_test.go diff --git a/internal/quic/qlog/qlog.go b/quic/qlog/qlog.go similarity index 100% rename from internal/quic/qlog/qlog.go rename to quic/qlog/qlog.go diff --git a/internal/quic/qlog/qlog_test.go b/quic/qlog/qlog_test.go similarity index 100% rename from internal/quic/qlog/qlog_test.go rename to quic/qlog/qlog_test.go diff --git a/internal/quic/qlog_test.go b/quic/qlog_test.go similarity index 99% rename from internal/quic/qlog_test.go rename to quic/qlog_test.go index 6c79c6cf49..c0b5cd170f 100644 --- a/internal/quic/qlog_test.go +++ b/quic/qlog_test.go @@ -17,7 +17,7 @@ import ( "testing" "time" - "golang.org/x/net/internal/quic/qlog" + "golang.org/x/net/quic/qlog" ) func TestQLogHandshake(t *testing.T) { diff --git a/internal/quic/queue.go b/quic/queue.go similarity index 100% rename from internal/quic/queue.go rename to quic/queue.go diff --git a/internal/quic/queue_test.go b/quic/queue_test.go similarity index 100% rename from internal/quic/queue_test.go rename to quic/queue_test.go diff --git a/internal/quic/quic.go b/quic/quic.go similarity index 100% rename from internal/quic/quic.go rename to quic/quic.go diff --git a/internal/quic/quic_test.go b/quic/quic_test.go similarity index 100% rename from internal/quic/quic_test.go rename to quic/quic_test.go diff --git a/internal/quic/rangeset.go b/quic/rangeset.go similarity index 100% rename from internal/quic/rangeset.go rename to quic/rangeset.go diff --git a/internal/quic/rangeset_test.go b/quic/rangeset_test.go similarity index 100% rename from internal/quic/rangeset_test.go rename to quic/rangeset_test.go diff --git a/internal/quic/retry.go b/quic/retry.go similarity index 100% rename from internal/quic/retry.go rename to quic/retry.go diff --git a/internal/quic/retry_test.go b/quic/retry_test.go similarity index 100% rename from internal/quic/retry_test.go rename to quic/retry_test.go diff --git a/internal/quic/rtt.go b/quic/rtt.go similarity index 100% rename from internal/quic/rtt.go rename to quic/rtt.go diff --git a/internal/quic/rtt_test.go b/quic/rtt_test.go similarity index 100% rename from internal/quic/rtt_test.go rename to quic/rtt_test.go diff --git a/internal/quic/sent_packet.go b/quic/sent_packet.go similarity index 100% rename from internal/quic/sent_packet.go rename to quic/sent_packet.go diff --git a/internal/quic/sent_packet_list.go b/quic/sent_packet_list.go similarity index 100% rename from internal/quic/sent_packet_list.go rename to quic/sent_packet_list.go diff --git a/internal/quic/sent_packet_list_test.go b/quic/sent_packet_list_test.go similarity index 100% rename from internal/quic/sent_packet_list_test.go rename to quic/sent_packet_list_test.go diff --git a/internal/quic/sent_packet_test.go b/quic/sent_packet_test.go similarity index 100% rename from internal/quic/sent_packet_test.go rename to quic/sent_packet_test.go diff --git a/internal/quic/sent_val.go b/quic/sent_val.go similarity index 100% rename from internal/quic/sent_val.go rename to quic/sent_val.go diff --git a/internal/quic/sent_val_test.go b/quic/sent_val_test.go similarity index 100% rename from internal/quic/sent_val_test.go rename to quic/sent_val_test.go diff --git a/internal/quic/stateless_reset.go b/quic/stateless_reset.go similarity index 100% rename from internal/quic/stateless_reset.go rename to quic/stateless_reset.go diff --git a/internal/quic/stateless_reset_test.go b/quic/stateless_reset_test.go similarity index 100% rename from internal/quic/stateless_reset_test.go rename to quic/stateless_reset_test.go diff --git a/internal/quic/stream.go b/quic/stream.go similarity index 100% rename from internal/quic/stream.go rename to quic/stream.go diff --git a/internal/quic/stream_limits.go b/quic/stream_limits.go similarity index 100% rename from internal/quic/stream_limits.go rename to quic/stream_limits.go diff --git a/internal/quic/stream_limits_test.go b/quic/stream_limits_test.go similarity index 100% rename from internal/quic/stream_limits_test.go rename to quic/stream_limits_test.go diff --git a/internal/quic/stream_test.go b/quic/stream_test.go similarity index 100% rename from internal/quic/stream_test.go rename to quic/stream_test.go diff --git a/internal/quic/tls.go b/quic/tls.go similarity index 100% rename from internal/quic/tls.go rename to quic/tls.go diff --git a/internal/quic/tls_test.go b/quic/tls_test.go similarity index 100% rename from internal/quic/tls_test.go rename to quic/tls_test.go diff --git a/internal/quic/tlsconfig_test.go b/quic/tlsconfig_test.go similarity index 100% rename from internal/quic/tlsconfig_test.go rename to quic/tlsconfig_test.go diff --git a/internal/quic/transport_params.go b/quic/transport_params.go similarity index 100% rename from internal/quic/transport_params.go rename to quic/transport_params.go diff --git a/internal/quic/transport_params_test.go b/quic/transport_params_test.go similarity index 100% rename from internal/quic/transport_params_test.go rename to quic/transport_params_test.go diff --git a/internal/quic/udp.go b/quic/udp.go similarity index 100% rename from internal/quic/udp.go rename to quic/udp.go diff --git a/internal/quic/udp_darwin.go b/quic/udp_darwin.go similarity index 100% rename from internal/quic/udp_darwin.go rename to quic/udp_darwin.go diff --git a/internal/quic/udp_linux.go b/quic/udp_linux.go similarity index 100% rename from internal/quic/udp_linux.go rename to quic/udp_linux.go diff --git a/internal/quic/udp_msg.go b/quic/udp_msg.go similarity index 100% rename from internal/quic/udp_msg.go rename to quic/udp_msg.go diff --git a/internal/quic/udp_other.go b/quic/udp_other.go similarity index 100% rename from internal/quic/udp_other.go rename to quic/udp_other.go diff --git a/internal/quic/udp_test.go b/quic/udp_test.go similarity index 100% rename from internal/quic/udp_test.go rename to quic/udp_test.go diff --git a/internal/quic/version_test.go b/quic/version_test.go similarity index 100% rename from internal/quic/version_test.go rename to quic/version_test.go diff --git a/internal/quic/wire.go b/quic/wire.go similarity index 100% rename from internal/quic/wire.go rename to quic/wire.go diff --git a/internal/quic/wire_test.go b/quic/wire_test.go similarity index 100% rename from internal/quic/wire_test.go rename to quic/wire_test.go From 3dfd003ad338913e62ad1e56020aee316f1ffe59 Mon Sep 17 00:00:00 2001 From: Aleksei Besogonov Date: Fri, 12 Jan 2024 07:38:27 +0000 Subject: [PATCH 146/168] websocket: add support for dialing with context Right now there is no way to pass context.Context to websocket.Dial. In addition, this method can block indefinitely in the NewClient call. Fixes golang/go#57953. Change-Id: Ic52d4b8306cd0850e78d683abb1bf11f0d4247ca GitHub-Last-Rev: 5e8c3a7cbaa324d6165ff40d2ee9ea6c4433b036 GitHub-Pull-Request: golang/net#160 Reviewed-on: https://go-review.googlesource.com/c/net/+/463097 Auto-Submit: Damien Neil Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov --- websocket/client.go | 56 +++++++++++++++++++++++++++++++++--------- websocket/dial.go | 11 ++++++--- websocket/dial_test.go | 37 ++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 15 deletions(-) diff --git a/websocket/client.go b/websocket/client.go index 69a4ac7eef..2c737f77a8 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -6,10 +6,12 @@ package websocket import ( "bufio" + "context" "io" "net" "net/http" "net/url" + "time" ) // DialError is an error that occurs while dialling a websocket server. @@ -77,30 +79,60 @@ func parseAuthority(location *url.URL) string { return location.Host } -// DialConfig opens a new client connection to a WebSocket with a config. func DialConfig(config *Config) (ws *Conn, err error) { - var client net.Conn + return config.DialContext(context.Background()) +} + +// DialContext opens a new client connection to a WebSocket, with context support for timeouts/cancellation. +func (config *Config) DialContext(ctx context.Context) (*Conn, error) { if config.Location == nil { return nil, &DialError{config, ErrBadWebSocketLocation} } if config.Origin == nil { return nil, &DialError{config, ErrBadWebSocketOrigin} } + dialer := config.Dialer if dialer == nil { dialer = &net.Dialer{} } - client, err = dialWithDialer(dialer, config) - if err != nil { - goto Error - } - ws, err = NewClient(config, client) + + client, err := dialWithDialer(ctx, dialer, config) if err != nil { - client.Close() - goto Error + return nil, &DialError{config, err} } - return -Error: - return nil, &DialError{config, err} + // Cleanup the connection if we fail to create the websocket successfully + success := false + defer func() { + if !success { + _ = client.Close() + } + }() + + var ws *Conn + var wsErr error + doneConnecting := make(chan struct{}) + go func() { + defer close(doneConnecting) + ws, err = NewClient(config, client) + if err != nil { + wsErr = &DialError{config, err} + } + }() + + // The websocket.NewClient() function can block indefinitely, make sure that we + // respect the deadlines specified by the context. + select { + case <-ctx.Done(): + // Force the pending operations to fail, terminating the pending connection attempt + _ = client.SetDeadline(time.Now()) + <-doneConnecting // Wait for the goroutine that tries to establish the connection to finish + return nil, &DialError{config, ctx.Err()} + case <-doneConnecting: + if wsErr == nil { + success = true // Disarm the deferred connection cleanup + } + return ws, wsErr + } } diff --git a/websocket/dial.go b/websocket/dial.go index 2dab943a48..8a2d83c473 100644 --- a/websocket/dial.go +++ b/websocket/dial.go @@ -5,18 +5,23 @@ package websocket import ( + "context" "crypto/tls" "net" ) -func dialWithDialer(dialer *net.Dialer, config *Config) (conn net.Conn, err error) { +func dialWithDialer(ctx context.Context, dialer *net.Dialer, config *Config) (conn net.Conn, err error) { switch config.Location.Scheme { case "ws": - conn, err = dialer.Dial("tcp", parseAuthority(config.Location)) + conn, err = dialer.DialContext(ctx, "tcp", parseAuthority(config.Location)) case "wss": - conn, err = tls.DialWithDialer(dialer, "tcp", parseAuthority(config.Location), config.TlsConfig) + tlsDialer := &tls.Dialer{ + NetDialer: dialer, + Config: config.TlsConfig, + } + conn, err = tlsDialer.DialContext(ctx, "tcp", parseAuthority(config.Location)) default: err = ErrBadScheme } diff --git a/websocket/dial_test.go b/websocket/dial_test.go index aa03e30dd1..dd844872c9 100644 --- a/websocket/dial_test.go +++ b/websocket/dial_test.go @@ -5,10 +5,13 @@ package websocket import ( + "context" "crypto/tls" + "errors" "fmt" "log" "net" + "net/http" "net/http/httptest" "testing" "time" @@ -41,3 +44,37 @@ func TestDialConfigTLSWithDialer(t *testing.T) { t.Fatalf("expected timeout error, got %#v", neterr) } } + +func TestDialConfigTLSWithTimeouts(t *testing.T) { + t.Parallel() + + finishedRequest := make(chan bool) + + // Context for cancellation + ctx, cancel := context.WithCancel(context.Background()) + + // This is a TLS server that blocks each request indefinitely (and cancels the context) + tlsServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cancel() + <-finishedRequest + })) + + tlsServerAddr := tlsServer.Listener.Addr().String() + log.Print("Test TLS WebSocket server listening on ", tlsServerAddr) + defer tlsServer.Close() + defer close(finishedRequest) + + config, _ := NewConfig(fmt.Sprintf("wss://%s/echo", tlsServerAddr), "http://localhost") + config.TlsConfig = &tls.Config{ + InsecureSkipVerify: true, + } + + _, err := config.DialContext(ctx) + dialerr, ok := err.(*DialError) + if !ok { + t.Fatalf("DialError expected, got %#v", err) + } + if !errors.Is(dialerr.Err, context.Canceled) { + t.Fatalf("context.Canceled error expected, got %#v", dialerr.Err) + } +} From 9fb4a8c9216d09f29d58e45a79cc2065d1b5bbf5 Mon Sep 17 00:00:00 2001 From: bestgopher <84328409@qq.com> Date: Tue, 6 Feb 2024 03:08:04 +0000 Subject: [PATCH 147/168] http2: send an error of FLOW_CONTROL_ERROR when exceed the maximum octets According to rfc9113 "https://www.rfc-editor.org/rfc/rfc9113.html#section-6.9.1-7", if a sender receives a WINDOW_UPDATE that causes a flow-control window to exceed this maximum, it MUST terminate either the stream or the connection, as appropriate. For streams, the sender sends a RST_STREAM with an error code of FLOW_CONTROL_ERROR. Change-Id: I5e14db247012ebc860a23053f73e70b83c7cd85d GitHub-Last-Rev: d1a85d3381f634904fc292c9c0a920dd1341adfd GitHub-Pull-Request: golang/net#204 Reviewed-on: https://go-review.googlesource.com/c/net/+/561035 Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee Reviewed-by: Damien Neil --- http2/transport.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/http2/transport.go b/http2/transport.go index df578b86c6..c2a5b44b3d 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -2911,6 +2911,15 @@ func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error { fl = &cs.flow } if !fl.add(int32(f.Increment)) { + // For stream, the sender sends RST_STREAM with an error code of FLOW_CONTROL_ERROR + if cs != nil { + rl.endStreamError(cs, StreamError{ + StreamID: f.StreamID, + Code: ErrCodeFlowControl, + }) + return nil + } + return ConnectionError(ErrCodeFlowControl) } cc.cond.Broadcast() From c289c7ab4f437bb502e685daaae72426126d5595 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Sat, 2 Mar 2024 18:37:38 -0500 Subject: [PATCH 148/168] websocket: re-add documentation for DialConfig The comment of the DialConfig function was dropped during CL 463097. There doesn't seem to be a good reason to do that, so bring it back. For golang/go#57953. Change-Id: I3e458b7d18cdab95763f003da5a644c8287b54ad Reviewed-on: https://go-review.googlesource.com/c/net/+/568198 Reviewed-by: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI Reviewed-by: Damien Neil Auto-Submit: Dmitri Shuralyov --- websocket/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/websocket/client.go b/websocket/client.go index 2c737f77a8..1e64157f3e 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -79,6 +79,7 @@ func parseAuthority(location *url.URL) string { return location.Host } +// DialConfig opens a new client connection to a WebSocket with a config. func DialConfig(config *Config) (ws *Conn, err error) { return config.DialContext(context.Background()) } From 7ee34a078aecd23a99f205bded144e5246a27d7c Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Mon, 4 Mar 2024 18:45:30 +0000 Subject: [PATCH 149/168] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: I6d2aa8edee71b255fb6970eb5d817a20df7cc357 Reviewed-on: https://go-review.googlesource.com/c/net/+/568895 Auto-Submit: Gopher Robot Reviewed-by: Than McIntosh LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Knyszek --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 7f512d7033..36207106dc 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module golang.org/x/net go 1.18 require ( - golang.org/x/crypto v0.19.0 - golang.org/x/sys v0.17.0 - golang.org/x/term v0.17.0 + golang.org/x/crypto v0.21.0 + golang.org/x/sys v0.18.0 + golang.org/x/term v0.18.0 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index 683b469d6f..69fb104980 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= From ab271c317248ea0f18481852f96d12d5eca05cf8 Mon Sep 17 00:00:00 2001 From: David Bell Date: Tue, 29 Aug 2023 20:50:33 +0000 Subject: [PATCH 150/168] http2: add IdleConnTimeout to http2.Transport Exposes an IdleConnTimeout on http2.Transport directly, rather than rely on configuring it through the underlying http1 transport. For golang/go#57893 Change-Id: Ibe506da39e314aebec1cd6df64937982182a37ca GitHub-Last-Rev: cc8f1710ed543da8e937aa2446b0a3982dec6ce3 GitHub-Pull-Request: golang/net#173 Reviewed-on: https://go-review.googlesource.com/c/net/+/497195 Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Matthew Dempsky --- http2/transport.go | 14 ++++++++++ http2/transport_test.go | 62 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/http2/transport.go b/http2/transport.go index c2a5b44b3d..b599197e7e 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -147,6 +147,12 @@ type Transport struct { // waiting for their turn. StrictMaxConcurrentStreams bool + // IdleConnTimeout is the maximum amount of time an idle + // (keep-alive) connection will remain idle before closing + // itself. + // Zero means no limit. + IdleConnTimeout time.Duration + // ReadIdleTimeout is the timeout after which a health check using ping // frame will be carried out if no frame is received on the connection. // Note that a ping response will is considered a received frame, so if @@ -3150,9 +3156,17 @@ func (rt noDialH2RoundTripper) RoundTrip(req *http.Request) (*http.Response, err } func (t *Transport) idleConnTimeout() time.Duration { + // to keep things backwards compatible, we use non-zero values of + // IdleConnTimeout, followed by using the IdleConnTimeout on the underlying + // http1 transport, followed by 0 + if t.IdleConnTimeout != 0 { + return t.IdleConnTimeout + } + if t.t1 != nil { return t.t1.IdleConnTimeout } + return 0 } diff --git a/http2/transport_test.go b/http2/transport_test.go index a81131f294..6ac8e978b2 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -95,6 +95,68 @@ func startH2cServer(t *testing.T) net.Listener { return l } +func TestIdleConnTimeout(t *testing.T) { + for _, test := range []struct { + idleConnTimeout time.Duration + wait time.Duration + baseTransport *http.Transport + wantConns int32 + }{{ + idleConnTimeout: 2 * time.Second, + wait: 1 * time.Second, + baseTransport: nil, + wantConns: 1, + }, { + idleConnTimeout: 1 * time.Second, + wait: 2 * time.Second, + baseTransport: nil, + wantConns: 5, + }, { + idleConnTimeout: 0 * time.Second, + wait: 1 * time.Second, + baseTransport: &http.Transport{ + IdleConnTimeout: 2 * time.Second, + }, + wantConns: 1, + }} { + var gotConns int32 + + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, r.RemoteAddr) + }, optOnlyServer) + defer st.Close() + + tr := &Transport{ + IdleConnTimeout: test.idleConnTimeout, + TLSClientConfig: tlsConfigInsecure, + } + defer tr.CloseIdleConnections() + + for i := 0; i < 5; i++ { + req, _ := http.NewRequest("GET", st.ts.URL, http.NoBody) + trace := &httptrace.ClientTrace{ + GotConn: func(connInfo httptrace.GotConnInfo) { + if !connInfo.Reused { + atomic.AddInt32(&gotConns, 1) + } + }, + } + req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) + + _, err := tr.RoundTrip(req) + if err != nil { + t.Fatalf("%v", err) + } + + <-time.After(test.wait) + } + + if gotConns != test.wantConns { + t.Errorf("incorrect gotConns: %d != %d", gotConns, test.wantConns) + } + } +} + func TestTransportH2c(t *testing.T) { l := startH2cServer(t) defer l.Close() From 8c07e20f924fb9dec8d39d2793f72a42c3261a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=8E=AE=E6=96=87?= Date: Sun, 3 Sep 2023 14:21:31 +0800 Subject: [PATCH 151/168] httpproxy: allow any scheme currently only http/https/socks5 scheme are allowed. However, any scheme could be possible if user provides their own implementation. Specifically, the widely used "socks5h://localhost" is parsed as Scheme="http" Host="socks5h:", which does not make sense because host name cannot contain ":". This patch allows any scheme to appear in the proxy config. And only fallback to http scheme if parsed scheme or host is empty. url.Parse() result of fallback cases: localhost => Scheme="localhost" localhost:1234 => Scheme="localhost" Opaque="1234" example.com => Path="example.com" Updates golang/go#24135 Change-Id: Ia2c041e37e2ac61be16220fd41d6cb6fabeeca3d Reviewed-on: https://go-review.googlesource.com/c/net/+/525257 LUCI-TryBot-Result: Go LUCI Run-TryBot: Damien Neil Reviewed-by: Michael Knyszek Reviewed-by: Damien Neil TryBot-Result: Gopher Robot Auto-Submit: Damien Neil --- http/httpproxy/proxy.go | 5 +---- http/httpproxy/proxy_test.go | 12 ++++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/http/httpproxy/proxy.go b/http/httpproxy/proxy.go index c3bd9a1eeb..6404aaf157 100644 --- a/http/httpproxy/proxy.go +++ b/http/httpproxy/proxy.go @@ -149,10 +149,7 @@ func parseProxy(proxy string) (*url.URL, error) { } proxyURL, err := url.Parse(proxy) - if err != nil || - (proxyURL.Scheme != "http" && - proxyURL.Scheme != "https" && - proxyURL.Scheme != "socks5") { + if err != nil || proxyURL.Scheme == "" || proxyURL.Host == "" { // proxy was bogus. Try prepending "http://" to it and // see if that parses correctly. If not, we fall // through and complain about the original one. diff --git a/http/httpproxy/proxy_test.go b/http/httpproxy/proxy_test.go index d763732950..790afdab77 100644 --- a/http/httpproxy/proxy_test.go +++ b/http/httpproxy/proxy_test.go @@ -68,6 +68,12 @@ var proxyForURLTests = []proxyForURLTest{{ HTTPProxy: "cache.corp.example.com", }, want: "http://cache.corp.example.com", +}, { + // single label domain is recognized as scheme by url.Parse + cfg: httpproxy.Config{ + HTTPProxy: "localhost", + }, + want: "http://localhost", }, { cfg: httpproxy.Config{ HTTPProxy: "https://cache.corp.example.com", @@ -88,6 +94,12 @@ var proxyForURLTests = []proxyForURLTest{{ HTTPProxy: "socks5://127.0.0.1", }, want: "socks5://127.0.0.1", +}, { + // Preserve unknown schemes. + cfg: httpproxy.Config{ + HTTPProxy: "foo://host", + }, + want: "foo://host", }, { // Don't use secure for http cfg: httpproxy.Config{ From ea095bc79b94b4bdc6939ce2dbd0300520089a1f Mon Sep 17 00:00:00 2001 From: Andy Pan Date: Mon, 19 Feb 2024 19:53:31 +0800 Subject: [PATCH 152/168] http2: only set up positive deadlines Fixes golang/go#65785 Change-Id: Icd95d7cae5ed26b8a2fe656daf8365e27a7785d8 Reviewed-on: https://go-review.googlesource.com/c/net/+/565195 LUCI-TryBot-Result: Go LUCI Reviewed-by: Damien Neil Reviewed-by: Carlos Amedee --- http2/server.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/http2/server.go b/http2/server.go index ae94c6408d..905206f3e6 100644 --- a/http2/server.go +++ b/http2/server.go @@ -434,7 +434,7 @@ func (s *Server) ServeConn(c net.Conn, opts *ServeConnOpts) { // passes the connection off to us with the deadline already set. // Write deadlines are set per stream in serverConn.newStream. // Disarm the net.Conn write deadline here. - if sc.hs.WriteTimeout != 0 { + if sc.hs.WriteTimeout > 0 { sc.conn.SetWriteDeadline(time.Time{}) } @@ -2017,7 +2017,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error { // similar to how the http1 server works. Here it's // technically more like the http1 Server's ReadHeaderTimeout // (in Go 1.8), though. That's a more sane option anyway. - if sc.hs.ReadTimeout != 0 { + if sc.hs.ReadTimeout > 0 { sc.conn.SetReadDeadline(time.Time{}) st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout) } @@ -2038,7 +2038,7 @@ func (sc *serverConn) upgradeRequest(req *http.Request) { // Disable any read deadline set by the net/http package // prior to the upgrade. - if sc.hs.ReadTimeout != 0 { + if sc.hs.ReadTimeout > 0 { sc.conn.SetReadDeadline(time.Time{}) } @@ -2116,7 +2116,7 @@ func (sc *serverConn) newStream(id, pusherID uint32, state streamState) *stream st.flow.conn = &sc.flow // link to conn-level counter st.flow.add(sc.initialStreamSendWindowSize) st.inflow.init(sc.srv.initialStreamRecvWindowSize()) - if sc.hs.WriteTimeout != 0 { + if sc.hs.WriteTimeout > 0 { st.writeDeadline = time.AfterFunc(sc.hs.WriteTimeout, st.onWriteTimeout) } From 57a6a7a86bc0e47508781ed988adcecbe8ff2580 Mon Sep 17 00:00:00 2001 From: Andy Pan Date: Sun, 25 Feb 2024 15:09:48 +0800 Subject: [PATCH 153/168] http2: prevent uninitialized pipe from being written For golang/go#65927 Change-Id: I6f48706156384e026968cf9a6d9e0ec76b46fabf Reviewed-on: https://go-review.googlesource.com/c/net/+/566675 Reviewed-by: Damien Neil Reviewed-by: Carlos Amedee LUCI-TryBot-Result: Go LUCI --- http2/pipe.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/http2/pipe.go b/http2/pipe.go index 684d984fd9..3b9f06b962 100644 --- a/http2/pipe.go +++ b/http2/pipe.go @@ -77,7 +77,10 @@ func (p *pipe) Read(d []byte) (n int, err error) { } } -var errClosedPipeWrite = errors.New("write on closed buffer") +var ( + errClosedPipeWrite = errors.New("write on closed buffer") + errUninitializedPipeWrite = errors.New("write on uninitialized buffer") +) // Write copies bytes from p into the buffer and wakes a reader. // It is an error to write more data than the buffer can hold. @@ -91,6 +94,12 @@ func (p *pipe) Write(d []byte) (n int, err error) { if p.err != nil || p.breakErr != nil { return 0, errClosedPipeWrite } + // pipe.setBuffer is never invoked, leaving the buffer uninitialized. + // We shouldn't try to write to an uninitialized pipe, + // but returning an error is better than panicking. + if p.b == nil { + return 0, errUninitializedPipeWrite + } return p.b.Write(d) } From d600ae05799943851536e26ab37ee23294912c3d Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Fri, 9 Feb 2024 18:11:52 -0800 Subject: [PATCH 154/168] http2: add testClientConn for testing client RoundTrips Many RoundTrip tests involve testing against a test-defined server with specific behaviors. For example: Testing RoundTrip's behavior when the server violates flow control limits. Existing tests mostly use the clientTester type, which starts separate goroutines for the Transport and a fake server. This results in tests where the control flow bounces around the test function, and requires each test to manage its own synchronization. Introduce a new framework for writing RoundTrip tests. testClientConn allows client tests to be written linearly, with synchronization provided by the test framework. For example, a testClientConn test can, as a linear sequence of actions: - start RoundTrip; - check the request headers sent; - provide data to the request body; - check that a DATA frame is sent; - send response headers from the server to the client; - check that RoundTrip returns. See TestTestClientConn at the top of clientconn_test.go for a full example. To enable synchronization with tests, this CL instruments the RoundTrip path to record when goroutines start, exit, and block waiting for events. This adds a certain amount of noise and bookkeeping to the client implementation, but (in my opinion) this is more than repaid in improved testability. The testClientConn also permits use of synthetic time in tests. At the moment, this is limited to the response header timeout, but extending it to other timeouts (read, 100-continue) should be straightforward. This CL converts a number of existing clientTester tests to use the new framework, but not all. Change-Id: Ief963889969363ec8469cd3c3de0becb2fc548f9 Reviewed-on: https://go-review.googlesource.com/c/net/+/563540 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- http2/clientconn_test.go | 634 +++++++++++++++++++++ http2/testsync.go | 246 ++++++++ http2/transport.go | 145 ++++- http2/transport_test.go | 1154 ++++++++++++++------------------------ 4 files changed, 1414 insertions(+), 765 deletions(-) create mode 100644 http2/clientconn_test.go create mode 100644 http2/testsync.go diff --git a/http2/clientconn_test.go b/http2/clientconn_test.go new file mode 100644 index 0000000000..6d94762e56 --- /dev/null +++ b/http2/clientconn_test.go @@ -0,0 +1,634 @@ +// Copyright 2024 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. + +// Infrastructure for testing ClientConn.RoundTrip. +// Put actual tests in transport_test.go. + +package http2 + +import ( + "bytes" + "fmt" + "io" + "net" + "net/http" + "reflect" + "testing" + "time" + + "golang.org/x/net/http2/hpack" +) + +// TestTestClientConn demonstrates usage of testClientConn. +func TestTestClientConn(t *testing.T) { + // newTestClientConn creates a *ClientConn and surrounding test infrastructure. + tc := newTestClientConn(t) + + // tc.greet reads the client's initial SETTINGS and WINDOW_UPDATE frames, + // and sends a SETTINGS frame to the client. + // + // Additional settings may be provided as optional parameters to greet. + tc.greet() + + // Request bodies must either be constant (bytes.Buffer, strings.Reader) + // or created with newRequestBody. + body := tc.newRequestBody() + body.writeBytes(10) // 10 arbitrary bytes... + body.closeWithError(io.EOF) // ...followed by EOF. + + // tc.roundTrip calls RoundTrip, but does not wait for it to return. + // It returns a testRoundTrip. + req, _ := http.NewRequest("PUT", "https://dummy.tld/", body) + rt := tc.roundTrip(req) + + // tc has a number of methods to check for expected frames sent. + // Here, we look for headers and the request body. + tc.wantHeaders(wantHeader{ + streamID: rt.streamID(), + endStream: false, + header: http.Header{ + ":authority": []string{"dummy.tld"}, + ":method": []string{"PUT"}, + ":path": []string{"/"}, + }, + }) + // Expect 10 bytes of request body in DATA frames. + tc.wantData(wantData{ + streamID: rt.streamID(), + endStream: true, + size: 10, + }) + + // tc.writeHeaders sends a HEADERS frame back to the client. + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) + + // Now that we've received headers, RoundTrip has finished. + // testRoundTrip has various methods to examine the response, + // or to fetch the response and/or error returned by RoundTrip + rt.wantStatus(200) + rt.wantBody(nil) +} + +// A testClientConn allows testing ClientConn.RoundTrip against a fake server. +// +// A test using testClientConn consists of: +// - actions on the client (calling RoundTrip, making data available to Request.Body); +// - validation of frames sent by the client to the server; and +// - providing frames from the server to the client. +// +// testClientConn manages synchronization, so tests can generally be written as +// a linear sequence of actions and validations without additional synchronization. +type testClientConn struct { + t *testing.T + + tr *Transport + fr *Framer + cc *ClientConn + hooks *testSyncHooks + + encbuf bytes.Buffer + enc *hpack.Encoder + + roundtrips []*testRoundTrip + + rerr error // returned by Read + rbuf bytes.Buffer // sent to the test conn + wbuf bytes.Buffer // sent by the test conn +} + +func newTestClientConn(t *testing.T, opts ...func(*Transport)) *testClientConn { + t.Helper() + + tr := &Transport{} + for _, o := range opts { + o(tr) + } + + tc := &testClientConn{ + t: t, + tr: tr, + hooks: newTestSyncHooks(), + } + tc.enc = hpack.NewEncoder(&tc.encbuf) + tc.fr = NewFramer(&tc.rbuf, &tc.wbuf) + tc.fr.ReadMetaHeaders = hpack.NewDecoder(initialHeaderTableSize, nil) + tc.fr.SetMaxReadFrameSize(10 << 20) + + t.Cleanup(func() { + if tc.rerr == nil { + tc.rerr = io.EOF + } + tc.sync() + if tc.hooks.total != 0 { + t.Errorf("%v goroutines still running after test completed", tc.hooks.total) + } + + }) + + tc.hooks.newclientconn = func(cc *ClientConn) { + tc.cc = cc + } + const singleUse = false + _, err := tc.tr.newClientConn((*testClientConnNetConn)(tc), singleUse, tc.hooks) + if err != nil { + t.Fatal(err) + } + tc.sync() + tc.hooks.newclientconn = nil + + // Read the client's HTTP/2 preface, sent prior to any HTTP/2 frames. + buf := make([]byte, len(clientPreface)) + if _, err := io.ReadFull(&tc.wbuf, buf); err != nil { + t.Fatalf("reading preface: %v", err) + } + if !bytes.Equal(buf, clientPreface) { + t.Fatalf("client preface: %q, want %q", buf, clientPreface) + } + + return tc +} + +// sync waits for the ClientConn under test to reach a stable state, +// with all goroutines blocked on some input. +func (tc *testClientConn) sync() { + tc.hooks.waitInactive() +} + +// advance advances synthetic time by a duration. +func (tc *testClientConn) advance(d time.Duration) { + tc.hooks.advance(d) + tc.sync() +} + +// hasFrame reports whether a frame is available to be read. +func (tc *testClientConn) hasFrame() bool { + return tc.wbuf.Len() > 0 +} + +// readFrame reads the next frame from the conn. +func (tc *testClientConn) readFrame() Frame { + if tc.wbuf.Len() == 0 { + return nil + } + fr, err := tc.fr.ReadFrame() + if err != nil { + return nil + } + return fr +} + +// testClientConnReadFrame reads a frame of a specific type from the conn. +func testClientConnReadFrame[T any](tc *testClientConn) T { + tc.t.Helper() + var v T + fr := tc.readFrame() + if fr == nil { + tc.t.Fatalf("got no frame, want frame %v", v) + } + v, ok := fr.(T) + if !ok { + tc.t.Fatalf("got frame %T, want %T", fr, v) + } + return v +} + +// wantFrameType reads the next frame from the conn. +// It produces an error if the frame type is not the expected value. +func (tc *testClientConn) wantFrameType(want FrameType) { + fr := tc.readFrame() + if fr == nil { + tc.t.Fatalf("got no frame, want frame %v", want) + } + if got := fr.Header().Type; got != want { + tc.t.Fatalf("got frame %v, want %v", got, want) + } +} + +type wantHeader struct { + streamID uint32 + endStream bool + header http.Header +} + +// wantHeaders reads a HEADERS frame and potential CONTINUATION frames, +// and asserts that they contain the expected headers. +func (tc *testClientConn) wantHeaders(want wantHeader) { + fr := tc.readFrame() + got, ok := fr.(*MetaHeadersFrame) + if !ok { + tc.t.Fatalf("got %v, want HEADERS frame", want) + } + if got, want := got.StreamID, want.streamID; got != want { + tc.t.Fatalf("got stream ID %v, want %v", got, want) + } + if got, want := got.StreamEnded(), want.endStream; got != want { + tc.t.Fatalf("got stream ended %v, want %v", got, want) + } + gotHeader := make(http.Header) + for _, f := range got.Fields { + gotHeader[f.Name] = append(gotHeader[f.Name], f.Value) + } + for k, v := range want.header { + if !reflect.DeepEqual(v, gotHeader[k]) { + tc.t.Fatalf("got header %q = %q; want %q", k, v, gotHeader[k]) + } + } +} + +type wantData struct { + streamID uint32 + endStream bool + size int +} + +// wantData reads zero or more DATA frames, and asserts that they match the expectation. +func (tc *testClientConn) wantData(want wantData) { + tc.t.Helper() + gotSize := 0 + gotEndStream := false + for tc.hasFrame() && !gotEndStream { + data := testClientConnReadFrame[*DataFrame](tc) + gotSize += len(data.Data()) + if data.StreamEnded() { + gotEndStream = true + } + } + if gotSize != want.size { + tc.t.Fatalf("got %v bytes of DATA frames, want %v", gotSize, want.size) + } + if gotEndStream != want.endStream { + tc.t.Fatalf("after %v bytes of DATA frames, got END_STREAM=%v; want %v", gotSize, gotEndStream, want.endStream) + } +} + +// testRequestBody is a Request.Body for use in tests. +type testRequestBody struct { + tc *testClientConn + + // At most one of buf or bytes can be set at any given time: + buf bytes.Buffer // specific bytes to read from the body + bytes int // body contains this many arbitrary bytes + + err error // read error (comes after any available bytes) +} + +func (tc *testClientConn) newRequestBody() *testRequestBody { + b := &testRequestBody{ + tc: tc, + } + return b +} + +// Read is called by the ClientConn to read from a request body. +func (b *testRequestBody) Read(p []byte) (n int, _ error) { + b.tc.cc.syncHooks.blockUntil(func() bool { + return b.buf.Len() > 0 || b.bytes > 0 || b.err != nil + }) + switch { + case b.buf.Len() > 0: + return b.buf.Read(p) + case b.bytes > 0: + if len(p) > b.bytes { + p = p[:b.bytes] + } + b.bytes -= len(p) + for i := range p { + p[i] = 'A' + } + return len(p), nil + default: + return 0, b.err + } +} + +// Close is called by the ClientConn when it is done reading from a request body. +func (b *testRequestBody) Close() error { + return nil +} + +// writeBytes adds n arbitrary bytes to the body. +func (b *testRequestBody) writeBytes(n int) { + b.bytes += n + b.checkWrite() + b.tc.sync() +} + +// Write adds bytes to the body. +func (b *testRequestBody) Write(p []byte) (int, error) { + n, err := b.buf.Write(p) + b.checkWrite() + b.tc.sync() + return n, err +} + +func (b *testRequestBody) checkWrite() { + if b.bytes > 0 && b.buf.Len() > 0 { + b.tc.t.Fatalf("can't interleave Write and writeBytes on request body") + } + if b.err != nil { + b.tc.t.Fatalf("can't write to request body after closeWithError") + } +} + +// closeWithError sets an error which will be returned by Read. +func (b *testRequestBody) closeWithError(err error) { + b.err = err + b.tc.sync() +} + +// roundTrip starts a RoundTrip call. +// +// (Note that the RoundTrip won't complete until response headers are received, +// the request times out, or some other terminal condition is reached.) +func (tc *testClientConn) roundTrip(req *http.Request) *testRoundTrip { + rt := &testRoundTrip{ + tc: tc, + donec: make(chan struct{}), + } + tc.roundtrips = append(tc.roundtrips, rt) + tc.hooks.newstream = func(cs *clientStream) { rt.cs = cs } + tc.cc.goRun(func() { + defer close(rt.donec) + rt.resp, rt.respErr = tc.cc.RoundTrip(req) + }) + tc.sync() + tc.hooks.newstream = nil + + tc.t.Cleanup(func() { + res, _ := rt.result() + if res != nil { + res.Body.Close() + } + }) + + return rt +} + +func (tc *testClientConn) greet(settings ...Setting) { + tc.wantFrameType(FrameSettings) + tc.wantFrameType(FrameWindowUpdate) + tc.writeSettings(settings...) + tc.writeSettingsAck() + tc.wantFrameType(FrameSettings) // acknowledgement +} + +func (tc *testClientConn) writeSettings(settings ...Setting) { + tc.t.Helper() + if err := tc.fr.WriteSettings(settings...); err != nil { + tc.t.Fatal(err) + } + tc.sync() +} + +func (tc *testClientConn) writeSettingsAck() { + tc.t.Helper() + if err := tc.fr.WriteSettingsAck(); err != nil { + tc.t.Fatal(err) + } + tc.sync() +} + +func (tc *testClientConn) writeData(streamID uint32, endStream bool, data []byte) { + tc.t.Helper() + if err := tc.fr.WriteData(streamID, endStream, data); err != nil { + tc.t.Fatal(err) + } + tc.sync() +} + +// makeHeaderBlockFragment encodes headers in a form suitable for inclusion +// in a HEADERS or CONTINUATION frame. +// +// It takes a list of alernating names and values. +func (tc *testClientConn) makeHeaderBlockFragment(s ...string) []byte { + if len(s)%2 != 0 { + tc.t.Fatalf("uneven list of header name/value pairs") + } + tc.encbuf.Reset() + for i := 0; i < len(s); i += 2 { + tc.enc.WriteField(hpack.HeaderField{Name: s[i], Value: s[i+1]}) + } + return tc.encbuf.Bytes() +} + +func (tc *testClientConn) writeHeaders(p HeadersFrameParam) { + tc.t.Helper() + if err := tc.fr.WriteHeaders(p); err != nil { + tc.t.Fatal(err) + } + tc.sync() +} + +// writeHeadersMode writes header frames, as modified by mode: +// +// - noHeader: Don't write the header. +// - oneHeader: Write a single HEADERS frame. +// - splitHeader: Write a HEADERS frame and CONTINUATION frame. +func (tc *testClientConn) writeHeadersMode(mode headerType, p HeadersFrameParam) { + tc.t.Helper() + switch mode { + case noHeader: + case oneHeader: + tc.writeHeaders(p) + case splitHeader: + if len(p.BlockFragment) < 2 { + panic("too small") + } + contData := p.BlockFragment[1:] + contEnd := p.EndHeaders + p.BlockFragment = p.BlockFragment[:1] + p.EndHeaders = false + tc.writeHeaders(p) + tc.writeContinuation(p.StreamID, contEnd, contData) + default: + panic("bogus mode") + } +} + +func (tc *testClientConn) writeContinuation(streamID uint32, endHeaders bool, headerBlockFragment []byte) { + tc.t.Helper() + if err := tc.fr.WriteContinuation(streamID, endHeaders, headerBlockFragment); err != nil { + tc.t.Fatal(err) + } + tc.sync() +} + +func (tc *testClientConn) writeGoAway(maxStreamID uint32, code ErrCode, debugData []byte) { + tc.t.Helper() + if err := tc.fr.WriteGoAway(maxStreamID, code, debugData); err != nil { + tc.t.Fatal(err) + } + tc.sync() +} + +func (tc *testClientConn) writeWindowUpdate(streamID, incr uint32) { + tc.t.Helper() + if err := tc.fr.WriteWindowUpdate(streamID, incr); err != nil { + tc.t.Fatal(err) + } + tc.sync() +} + +// closeWrite causes the net.Conn used by the ClientConn to return a error +// from Read calls. +func (tc *testClientConn) closeWrite(err error) { + tc.rerr = err + tc.sync() +} + +// testRoundTrip manages a RoundTrip in progress. +type testRoundTrip struct { + tc *testClientConn + resp *http.Response + respErr error + donec chan struct{} + cs *clientStream +} + +// streamID returns the HTTP/2 stream ID of the request. +func (rt *testRoundTrip) streamID() uint32 { + return rt.cs.ID +} + +// done reports whether RoundTrip has returned. +func (rt *testRoundTrip) done() bool { + select { + case <-rt.donec: + return true + default: + return false + } +} + +// result returns the result of the RoundTrip. +func (rt *testRoundTrip) result() (*http.Response, error) { + t := rt.tc.t + t.Helper() + select { + case <-rt.donec: + default: + t.Fatalf("RoundTrip (stream %v) is not done; want it to be", rt.streamID()) + } + return rt.resp, rt.respErr +} + +// response returns the response of a successful RoundTrip. +// If the RoundTrip unexpectedly failed, it calls t.Fatal. +func (rt *testRoundTrip) response() *http.Response { + t := rt.tc.t + t.Helper() + resp, err := rt.result() + if err != nil { + t.Fatalf("RoundTrip returned unexpected error: %v", rt.respErr) + } + if resp == nil { + t.Fatalf("RoundTrip returned nil *Response and nil error") + } + return resp +} + +// err returns the (possibly nil) error result of RoundTrip. +func (rt *testRoundTrip) err() error { + t := rt.tc.t + t.Helper() + _, err := rt.result() + return err +} + +// wantStatus indicates the expected response StatusCode. +func (rt *testRoundTrip) wantStatus(want int) { + t := rt.tc.t + t.Helper() + if got := rt.response().StatusCode; got != want { + t.Fatalf("got response status %v, want %v", got, want) + } +} + +// body reads the contents of the response body. +func (rt *testRoundTrip) readBody() ([]byte, error) { + t := rt.tc.t + t.Helper() + return io.ReadAll(rt.response().Body) +} + +// wantBody indicates the expected response body. +// (Note that this consumes the body.) +func (rt *testRoundTrip) wantBody(want []byte) { + t := rt.tc.t + t.Helper() + got, err := rt.readBody() + if err != nil { + t.Fatalf("unexpected error reading response body: %v", err) + } + if !bytes.Equal(got, want) { + t.Fatalf("unexpected response body:\ngot: %q\nwant: %q", got, want) + } +} + +// wantHeaders indicates the expected response headers. +func (rt *testRoundTrip) wantHeaders(want http.Header) { + t := rt.tc.t + t.Helper() + res := rt.response() + if diff := diffHeaders(res.Header, want); diff != "" { + t.Fatalf("unexpected response headers:\n%v", diff) + } +} + +// wantTrailers indicates the expected response trailers. +func (rt *testRoundTrip) wantTrailers(want http.Header) { + t := rt.tc.t + t.Helper() + res := rt.response() + if diff := diffHeaders(res.Trailer, want); diff != "" { + t.Fatalf("unexpected response trailers:\n%v", diff) + } +} + +func diffHeaders(got, want http.Header) string { + // nil and 0-length non-nil are equal. + if len(got) == 0 && len(want) == 0 { + return "" + } + // We could do a more sophisticated diff here. + // DeepEqual is good enough for now. + if reflect.DeepEqual(got, want) { + return "" + } + return fmt.Sprintf("got: %v\nwant: %v", got, want) +} + +// testClientConnNetConn implements net.Conn. +type testClientConnNetConn testClientConn + +func (nc *testClientConnNetConn) Read(b []byte) (n int, err error) { + nc.cc.syncHooks.blockUntil(func() bool { + return nc.rerr != nil || nc.rbuf.Len() > 0 + }) + if nc.rbuf.Len() > 0 { + return nc.rbuf.Read(b) + } + return 0, nc.rerr +} + +func (nc *testClientConnNetConn) Write(b []byte) (n int, err error) { + return nc.wbuf.Write(b) +} + +func (*testClientConnNetConn) Close() error { + return nil +} + +func (*testClientConnNetConn) LocalAddr() (_ net.Addr) { return } +func (*testClientConnNetConn) RemoteAddr() (_ net.Addr) { return } +func (*testClientConnNetConn) SetDeadline(t time.Time) error { return nil } +func (*testClientConnNetConn) SetReadDeadline(t time.Time) error { return nil } +func (*testClientConnNetConn) SetWriteDeadline(t time.Time) error { return nil } diff --git a/http2/testsync.go b/http2/testsync.go new file mode 100644 index 0000000000..b8335c0fbe --- /dev/null +++ b/http2/testsync.go @@ -0,0 +1,246 @@ +// Copyright 2024 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 http2 + +import ( + "sync" + "time" +) + +// testSyncHooks coordinates goroutines in tests. +// +// For example, a call to ClientConn.RoundTrip involves several goroutines, including: +// - the goroutine running RoundTrip; +// - the clientStream.doRequest goroutine, which writes the request; and +// - the clientStream.readLoop goroutine, which reads the response. +// +// Using testSyncHooks, a test can start a RoundTrip and identify when all these goroutines +// are blocked waiting for some condition such as reading the Request.Body or waiting for +// flow control to become available. +// +// The testSyncHooks also manage timers and synthetic time in tests. +// This permits us to, for example, start a request and cause it to time out waiting for +// response headers without resorting to time.Sleep calls. +type testSyncHooks struct { + // active/inactive act as a mutex and condition variable. + // + // - neither chan contains a value: testSyncHooks is locked. + // - active contains a value: unlocked, and at least one goroutine is not blocked + // - inactive contains a value: unlocked, and all goroutines are blocked + active chan struct{} + inactive chan struct{} + + // goroutine counts + total int // total goroutines + condwait map[*sync.Cond]int // blocked in sync.Cond.Wait + blocked []*testBlockedGoroutine // otherwise blocked + + // fake time + now time.Time + timers []*fakeTimer + + // Transport testing: Report various events. + newclientconn func(*ClientConn) + newstream func(*clientStream) +} + +// testBlockedGoroutine is a blocked goroutine. +type testBlockedGoroutine struct { + f func() bool // blocked until f returns true + ch chan struct{} // closed when unblocked +} + +func newTestSyncHooks() *testSyncHooks { + h := &testSyncHooks{ + active: make(chan struct{}, 1), + inactive: make(chan struct{}, 1), + condwait: map[*sync.Cond]int{}, + } + h.inactive <- struct{}{} + h.now = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + return h +} + +// lock acquires the testSyncHooks mutex. +func (h *testSyncHooks) lock() { + select { + case <-h.active: + case <-h.inactive: + } +} + +// waitInactive waits for all goroutines to become inactive. +func (h *testSyncHooks) waitInactive() { + for { + <-h.inactive + if !h.unlock() { + break + } + } +} + +// unlock releases the testSyncHooks mutex. +// It reports whether any goroutines are active. +func (h *testSyncHooks) unlock() (active bool) { + // Look for a blocked goroutine which can be unblocked. + blocked := h.blocked[:0] + unblocked := false + for _, b := range h.blocked { + if !unblocked && b.f() { + unblocked = true + close(b.ch) + } else { + blocked = append(blocked, b) + } + } + h.blocked = blocked + + // Count goroutines blocked on condition variables. + condwait := 0 + for _, count := range h.condwait { + condwait += count + } + + if h.total > condwait+len(blocked) { + h.active <- struct{}{} + return true + } else { + h.inactive <- struct{}{} + return false + } +} + +// goRun starts a new goroutine. +func (h *testSyncHooks) goRun(f func()) { + h.lock() + h.total++ + h.unlock() + go func() { + defer func() { + h.lock() + h.total-- + h.unlock() + }() + f() + }() +} + +// blockUntil indicates that a goroutine is blocked waiting for some condition to become true. +// It waits until f returns true before proceeding. +// +// Example usage: +// +// h.blockUntil(func() bool { +// // Is the context done yet? +// select { +// case <-ctx.Done(): +// default: +// return false +// } +// return true +// }) +// // Wait for the context to become done. +// <-ctx.Done() +// +// The function f passed to blockUntil must be non-blocking and idempotent. +func (h *testSyncHooks) blockUntil(f func() bool) { + if f() { + return + } + ch := make(chan struct{}) + h.lock() + h.blocked = append(h.blocked, &testBlockedGoroutine{ + f: f, + ch: ch, + }) + h.unlock() + <-ch +} + +// broadcast is sync.Cond.Broadcast. +func (h *testSyncHooks) condBroadcast(cond *sync.Cond) { + h.lock() + delete(h.condwait, cond) + h.unlock() + cond.Broadcast() +} + +// broadcast is sync.Cond.Wait. +func (h *testSyncHooks) condWait(cond *sync.Cond) { + h.lock() + h.condwait[cond]++ + h.unlock() +} + +// newTimer creates a new timer: A time.Timer if h is nil, or a synthetic timer in tests. +func (h *testSyncHooks) newTimer(d time.Duration) timer { + h.lock() + defer h.unlock() + t := &fakeTimer{ + when: h.now.Add(d), + c: make(chan time.Time), + } + h.timers = append(h.timers, t) + return t +} + +// advance advances time and causes synthetic timers to fire. +func (h *testSyncHooks) advance(d time.Duration) { + h.lock() + defer h.unlock() + h.now = h.now.Add(d) + timers := h.timers[:0] + for _, t := range h.timers { + t.mu.Lock() + switch { + case t.when.After(h.now): + timers = append(timers, t) + case t.when.IsZero(): + // stopped timer + default: + t.when = time.Time{} + close(t.c) + } + t.mu.Unlock() + } + h.timers = timers +} + +// A timer wraps a time.Timer, or a synthetic equivalent in tests. +// Unlike time.Timer, timer is single-use: The timer channel is closed when the timer expires. +type timer interface { + C() <-chan time.Time + Stop() bool +} + +type timeTimer struct { + t *time.Timer + c chan time.Time +} + +func newTimeTimer(d time.Duration) timer { + ch := make(chan time.Time) + t := time.AfterFunc(d, func() { + close(ch) + }) + return &timeTimer{t, ch} +} + +func (t timeTimer) C() <-chan time.Time { return t.c } +func (t timeTimer) Stop() bool { return t.t.Stop() } + +type fakeTimer struct { + mu sync.Mutex + when time.Time + c chan time.Time +} + +func (t *fakeTimer) C() <-chan time.Time { return t.c } +func (t *fakeTimer) Stop() bool { + t.mu.Lock() + defer t.mu.Unlock() + stopped := t.when.IsZero() + t.when = time.Time{} + return stopped +} diff --git a/http2/transport.go b/http2/transport.go index b599197e7e..04db29275b 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -350,6 +350,45 @@ type ClientConn struct { werr error // first write error that has occurred hbuf bytes.Buffer // HPACK encoder writes into this henc *hpack.Encoder + + syncHooks *testSyncHooks // can be nil +} + +// Hook points used for testing. +// Outside of tests, cc.syncHooks is nil and these all have minimal implementations. +// Inside tests, see the testSyncHooks function docs. + +// goRun starts a new goroutine. +func (cc *ClientConn) goRun(f func()) { + if cc.syncHooks != nil { + cc.syncHooks.goRun(f) + return + } + go f() +} + +// condBroadcast is cc.cond.Broadcast. +func (cc *ClientConn) condBroadcast() { + if cc.syncHooks != nil { + cc.syncHooks.condBroadcast(cc.cond) + } + cc.cond.Broadcast() +} + +// condWait is cc.cond.Wait. +func (cc *ClientConn) condWait() { + if cc.syncHooks != nil { + cc.syncHooks.condWait(cc.cond) + } + cc.cond.Wait() +} + +// newTimer creates a new time.Timer, or a synthetic timer in tests. +func (cc *ClientConn) newTimer(d time.Duration) timer { + if cc.syncHooks != nil { + return cc.syncHooks.newTimer(d) + } + return newTimeTimer(d) } // clientStream is the state for a single HTTP/2 stream. One of these @@ -431,7 +470,7 @@ func (cs *clientStream) abortStreamLocked(err error) { // TODO(dneil): Clean up tests where cs.cc.cond is nil. if cs.cc.cond != nil { // Wake up writeRequestBody if it is waiting on flow control. - cs.cc.cond.Broadcast() + cs.cc.condBroadcast() } } @@ -441,7 +480,7 @@ func (cs *clientStream) abortRequestBodyWrite() { defer cc.mu.Unlock() if cs.reqBody != nil && cs.reqBodyClosed == nil { cs.closeReqBodyLocked() - cc.cond.Broadcast() + cc.condBroadcast() } } @@ -451,10 +490,10 @@ func (cs *clientStream) closeReqBodyLocked() { } cs.reqBodyClosed = make(chan struct{}) reqBodyClosed := cs.reqBodyClosed - go func() { + cs.cc.goRun(func() { cs.reqBody.Close() close(reqBodyClosed) - }() + }) } type stickyErrWriter struct { @@ -672,7 +711,7 @@ func (t *Transport) dialClientConn(ctx context.Context, addr string, singleUse b if err != nil { return nil, err } - return t.newClientConn(tconn, singleUse) + return t.newClientConn(tconn, singleUse, nil) } func (t *Transport) newTLSConfig(host string) *tls.Config { @@ -738,10 +777,10 @@ func (t *Transport) maxEncoderHeaderTableSize() uint32 { } func (t *Transport) NewClientConn(c net.Conn) (*ClientConn, error) { - return t.newClientConn(c, t.disableKeepAlives()) + return t.newClientConn(c, t.disableKeepAlives(), nil) } -func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, error) { +func (t *Transport) newClientConn(c net.Conn, singleUse bool, hooks *testSyncHooks) (*ClientConn, error) { cc := &ClientConn{ t: t, tconn: c, @@ -756,6 +795,10 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro wantSettingsAck: true, pings: make(map[[8]byte]chan struct{}), reqHeaderMu: make(chan struct{}, 1), + syncHooks: hooks, + } + if hooks != nil { + hooks.newclientconn(cc) } if d := t.idleConnTimeout(); d != 0 { cc.idleTimeout = d @@ -824,7 +867,7 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro return nil, cc.werr } - go cc.readLoop() + cc.goRun(cc.readLoop) return cc, nil } @@ -1062,7 +1105,7 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error { // Wait for all in-flight streams to complete or connection to close done := make(chan struct{}) cancelled := false // guarded by cc.mu - go func() { + cc.goRun(func() { cc.mu.Lock() defer cc.mu.Unlock() for { @@ -1074,9 +1117,9 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error { if cancelled { break } - cc.cond.Wait() + cc.condWait() } - }() + }) shutdownEnterWaitStateHook() select { case <-done: @@ -1086,7 +1129,7 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error { cc.mu.Lock() // Free the goroutine above cancelled = true - cc.cond.Broadcast() + cc.condBroadcast() cc.mu.Unlock() return ctx.Err() } @@ -1124,7 +1167,7 @@ func (cc *ClientConn) closeForError(err error) { for _, cs := range cc.streams { cs.abortStreamLocked(err) } - cc.cond.Broadcast() + cc.condBroadcast() cc.mu.Unlock() cc.closeConn() } @@ -1221,6 +1264,10 @@ func (cc *ClientConn) decrStreamReservationsLocked() { } func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) { + return cc.roundTrip(req, nil) +} + +func (cc *ClientConn) roundTrip(req *http.Request, streamf func(*clientStream)) (*http.Response, error) { ctx := req.Context() cs := &clientStream{ cc: cc, @@ -1235,9 +1282,23 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) { respHeaderRecv: make(chan struct{}), donec: make(chan struct{}), } - go cs.doRequest(req) + cc.goRun(func() { + cs.doRequest(req) + }) waitDone := func() error { + if cc.syncHooks != nil { + cc.syncHooks.blockUntil(func() bool { + select { + case <-cs.donec: + case <-ctx.Done(): + case <-cs.reqCancel: + default: + return false + } + return true + }) + } select { case <-cs.donec: return nil @@ -1298,7 +1359,24 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) { return err } + if streamf != nil { + streamf(cs) + } + for { + if cc.syncHooks != nil { + cc.syncHooks.blockUntil(func() bool { + select { + case <-cs.respHeaderRecv: + case <-cs.abort: + case <-ctx.Done(): + case <-cs.reqCancel: + default: + return false + } + return true + }) + } select { case <-cs.respHeaderRecv: return handleResponseHeaders() @@ -1378,6 +1456,10 @@ func (cs *clientStream) writeRequest(req *http.Request) (err error) { } cc.mu.Unlock() + if cc.syncHooks != nil { + cc.syncHooks.newstream(cs) + } + // TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere? if !cc.t.disableCompression() && req.Header.Get("Accept-Encoding") == "" && @@ -1458,15 +1540,30 @@ func (cs *clientStream) writeRequest(req *http.Request) (err error) { var respHeaderTimer <-chan time.Time var respHeaderRecv chan struct{} if d := cc.responseHeaderTimeout(); d != 0 { - timer := time.NewTimer(d) + timer := cc.newTimer(d) defer timer.Stop() - respHeaderTimer = timer.C + respHeaderTimer = timer.C() respHeaderRecv = cs.respHeaderRecv } // Wait until the peer half-closes its end of the stream, // or until the request is aborted (via context, error, or otherwise), // whichever comes first. for { + if cc.syncHooks != nil { + cc.syncHooks.blockUntil(func() bool { + select { + case <-cs.peerClosed: + case <-respHeaderTimer: + case <-respHeaderRecv: + case <-cs.abort: + case <-ctx.Done(): + case <-cs.reqCancel: + default: + return false + } + return true + }) + } select { case <-cs.peerClosed: return nil @@ -1615,7 +1712,7 @@ func (cc *ClientConn) awaitOpenSlotForStreamLocked(cs *clientStream) error { return nil } cc.pendingRequests++ - cc.cond.Wait() + cc.condWait() cc.pendingRequests-- select { case <-cs.abort: @@ -1877,7 +1974,7 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error) cs.flow.take(take) return take, nil } - cc.cond.Wait() + cc.condWait() } } @@ -2149,7 +2246,7 @@ func (cc *ClientConn) forgetStreamID(id uint32) { } // Wake up writeRequestBody via clientStream.awaitFlowControl and // wake up RoundTrip if there is a pending request. - cc.cond.Broadcast() + cc.condBroadcast() closeOnIdle := cc.singleUse || cc.doNotReuse || cc.t.disableKeepAlives() || cc.goAway != nil if closeOnIdle && cc.streamsReserved == 0 && len(cc.streams) == 0 { @@ -2237,7 +2334,7 @@ func (rl *clientConnReadLoop) cleanup() { cs.abortStreamLocked(err) } } - cc.cond.Broadcast() + cc.condBroadcast() cc.mu.Unlock() } @@ -2873,7 +2970,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error { for _, cs := range cc.streams { cs.flow.add(delta) } - cc.cond.Broadcast() + cc.condBroadcast() cc.initialWindowSize = s.Val case SettingHeaderTableSize: @@ -2928,7 +3025,7 @@ func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error { return ConnectionError(ErrCodeFlowControl) } - cc.cond.Broadcast() + cc.condBroadcast() return nil } @@ -2971,7 +3068,7 @@ func (cc *ClientConn) Ping(ctx context.Context) error { cc.mu.Unlock() } errc := make(chan error, 1) - go func() { + cc.goRun(func() { cc.wmu.Lock() defer cc.wmu.Unlock() if err := cc.fr.WritePing(false, p); err != nil { @@ -2982,7 +3079,7 @@ func (cc *ClientConn) Ping(ctx context.Context) error { errc <- err return } - }() + }) select { case <-c: return nil diff --git a/http2/transport_test.go b/http2/transport_test.go index 6ac8e978b2..f889cd12b5 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -1014,131 +1014,65 @@ func TestTransportReqBodyAfterResponse_403(t *testing.T) { testTransportReqBodyA func testTransportReqBodyAfterResponse(t *testing.T, status int) { const bodySize = 10 << 20 - clientDone := make(chan struct{}) - ct := newClientTester(t) - recvLen := make(chan int64, 1) - ct.client = func() error { - defer ct.cc.(*net.TCPConn).CloseWrite() - if runtime.GOOS == "plan9" { - // CloseWrite not supported on Plan 9; Issue 17906 - defer ct.cc.(*net.TCPConn).Close() - } - defer close(clientDone) - body := &pipe{b: new(bytes.Buffer)} - io.Copy(body, io.LimitReader(neverEnding('A'), bodySize/2)) - req, err := http.NewRequest("PUT", "https://dummy.tld/", body) - if err != nil { - return err - } - res, err := ct.tr.RoundTrip(req) - if err != nil { - return fmt.Errorf("RoundTrip: %v", err) - } - if res.StatusCode != status { - return fmt.Errorf("status code = %v; want %v", res.StatusCode, status) - } - io.Copy(body, io.LimitReader(neverEnding('A'), bodySize/2)) - body.CloseWithError(io.EOF) - slurp, err := ioutil.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("Slurp: %v", err) - } - if len(slurp) > 0 { - return fmt.Errorf("unexpected body: %q", slurp) - } - res.Body.Close() - if status == 200 { - if got := <-recvLen; got != bodySize { - return fmt.Errorf("For 200 response, Transport wrote %d bytes; want %d", got, bodySize) - } - } else { - if got := <-recvLen; got == 0 || got >= bodySize { - return fmt.Errorf("For %d response, Transport wrote %d bytes; want (0,%d) exclusive", status, got, bodySize) - } - } - return nil + tc := newTestClientConn(t) + tc.greet() + + body := tc.newRequestBody() + body.writeBytes(bodySize / 2) + req, _ := http.NewRequest("PUT", "https://dummy.tld/", body) + rt := tc.roundTrip(req) + + tc.wantHeaders(wantHeader{ + streamID: rt.streamID(), + endStream: false, + header: http.Header{ + ":authority": []string{"dummy.tld"}, + ":method": []string{"PUT"}, + ":path": []string{"/"}, + }, + }) + + // Provide enough congestion window for the full request body. + tc.writeWindowUpdate(0, bodySize) + tc.writeWindowUpdate(rt.streamID(), bodySize) + + tc.wantData(wantData{ + streamID: rt.streamID(), + endStream: false, + size: bodySize / 2, + }) + + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", strconv.Itoa(status), + ), + }) + + res := rt.response() + if res.StatusCode != status { + t.Fatalf("status code = %v; want %v", res.StatusCode, status) } - ct.server = func() error { - ct.greet() - defer close(recvLen) - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - var dataRecv int64 - var closed bool - for { - f, err := ct.fr.ReadFrame() - if err != nil { - select { - case <-clientDone: - // If the client's done, it - // will have reported any - // errors on its side. - return nil - default: - return err - } - } - //println(fmt.Sprintf("server got frame: %v", f)) - ended := false - switch f := f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - case *HeadersFrame: - if !f.HeadersEnded() { - return fmt.Errorf("headers should have END_HEADERS be ended: %v", f) - } - if f.StreamEnded() { - return fmt.Errorf("headers contains END_STREAM unexpectedly: %v", f) - } - case *DataFrame: - dataLen := len(f.Data()) - if dataLen > 0 { - if dataRecv == 0 { - enc.WriteField(hpack.HeaderField{Name: ":status", Value: strconv.Itoa(status)}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) - } - if err := ct.fr.WriteWindowUpdate(0, uint32(dataLen)); err != nil { - return err - } - if err := ct.fr.WriteWindowUpdate(f.StreamID, uint32(dataLen)); err != nil { - return err - } - } - dataRecv += int64(dataLen) - if !closed && ((status != 200 && dataRecv > 0) || - (status == 200 && f.StreamEnded())) { - closed = true - if err := ct.fr.WriteData(f.StreamID, true, nil); err != nil { - return err - } - } + body.writeBytes(bodySize / 2) + body.closeWithError(io.EOF) - if f.StreamEnded() { - ended = true - } - case *RSTStreamFrame: - if status == 200 { - return fmt.Errorf("Unexpected client frame %v", f) - } - ended = true - default: - return fmt.Errorf("Unexpected client frame %v", f) - } - if ended { - select { - case recvLen <- dataRecv: - default: - } - } - } + if status == 200 { + // After a 200 response, client sends the remaining request body. + tc.wantData(wantData{ + streamID: rt.streamID(), + endStream: true, + size: bodySize / 2, + }) + } else { + // After a 403 response, client gives up and resets the stream. + tc.wantFrameType(FrameRSTStream) } - ct.run() + + rt.wantBody(nil) } // See golang.org/issue/13444 @@ -1319,121 +1253,74 @@ func testTransportResPattern(t *testing.T, expect100Continue, resHeader headerTy panic("invalid combination") } - ct := newClientTester(t) - ct.client = func() error { - req, _ := http.NewRequest("POST", "https://dummy.tld/", strings.NewReader(reqBody)) - if expect100Continue != noHeader { - req.Header.Set("Expect", "100-continue") - } - res, err := ct.tr.RoundTrip(req) - if err != nil { - return fmt.Errorf("RoundTrip: %v", err) - } - defer res.Body.Close() - if res.StatusCode != 200 { - return fmt.Errorf("status code = %v; want 200", res.StatusCode) - } - slurp, err := ioutil.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("Slurp: %v", err) - } - wantBody := resBody - if !withData { - wantBody = "" - } - if string(slurp) != wantBody { - return fmt.Errorf("body = %q; want %q", slurp, wantBody) - } - if trailers == noHeader { - if len(res.Trailer) > 0 { - t.Errorf("Trailer = %v; want none", res.Trailer) - } - } else { - want := http.Header{"Some-Trailer": {"some-value"}} - if !reflect.DeepEqual(res.Trailer, want) { - t.Errorf("Trailer = %v; want %v", res.Trailer, want) - } - } - return nil + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("POST", "https://dummy.tld/", strings.NewReader(reqBody)) + if expect100Continue != noHeader { + req.Header.Set("Expect", "100-continue") } - ct.server = func() error { - ct.greet() - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) + rt := tc.roundTrip(req) - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } - endStream := false - send := func(mode headerType) { - hbf := buf.Bytes() - switch mode { - case oneHeader: - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.Header().StreamID, - EndHeaders: true, - EndStream: endStream, - BlockFragment: hbf, - }) - case splitHeader: - if len(hbf) < 2 { - panic("too small") - } - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.Header().StreamID, - EndHeaders: false, - EndStream: endStream, - BlockFragment: hbf[:1], - }) - ct.fr.WriteContinuation(f.Header().StreamID, true, hbf[1:]) - default: - panic("bogus mode") - } - } - switch f := f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - case *DataFrame: - if !f.StreamEnded() { - // No need to send flow control tokens. The test request body is tiny. - continue - } - // Response headers (1+ frames; 1 or 2 in this test, but never 0) - { - buf.Reset() - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - enc.WriteField(hpack.HeaderField{Name: "x-foo", Value: "blah"}) - enc.WriteField(hpack.HeaderField{Name: "x-bar", Value: "more"}) - if trailers != noHeader { - enc.WriteField(hpack.HeaderField{Name: "trailer", Value: "some-trailer"}) - } - endStream = withData == false && trailers == noHeader - send(resHeader) - } - if withData { - endStream = trailers == noHeader - ct.fr.WriteData(f.StreamID, endStream, []byte(resBody)) - } - if trailers != noHeader { - endStream = true - buf.Reset() - enc.WriteField(hpack.HeaderField{Name: "some-trailer", Value: "some-value"}) - send(trailers) - } - if endStream { - return nil - } - case *HeadersFrame: - if expect100Continue != noHeader { - buf.Reset() - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "100"}) - send(expect100Continue) - } - } - } + tc.wantFrameType(FrameHeaders) + + // Possibly 100-continue, or skip when noHeader. + tc.writeHeadersMode(expect100Continue, HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "100", + ), + }) + + // Client sends request body. + tc.wantData(wantData{ + streamID: rt.streamID(), + endStream: true, + size: len(reqBody), + }) + + hdr := []string{ + ":status", "200", + "x-foo", "blah", + "x-bar", "more", + } + if trailers != noHeader { + hdr = append(hdr, "trailer", "some-trailer") + } + tc.writeHeadersMode(resHeader, HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: withData == false && trailers == noHeader, + BlockFragment: tc.makeHeaderBlockFragment(hdr...), + }) + if withData { + endStream := trailers == noHeader + tc.writeData(rt.streamID(), endStream, []byte(resBody)) + } + tc.writeHeadersMode(trailers, HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + "some-trailer", "some-value", + ), + }) + + rt.wantStatus(200) + if !withData { + rt.wantBody(nil) + } else { + rt.wantBody([]byte(resBody)) + } + if trailers == noHeader { + rt.wantTrailers(nil) + } else { + rt.wantTrailers(http.Header{ + "Some-Trailer": {"some-value"}, + }) } - ct.run() } // Issue 26189, Issue 17739: ignore unknown 1xx responses @@ -1445,130 +1332,76 @@ func TestTransportUnknown1xx(t *testing.T) { return nil } - ct := newClientTester(t) - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if err != nil { - return fmt.Errorf("RoundTrip: %v", err) - } - defer res.Body.Close() - if res.StatusCode != 204 { - return fmt.Errorf("status code = %v; want 204", res.StatusCode) - } - want := `code=110 header=map[Foo-Bar:[110]] + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + + for i := 110; i <= 114; i++ { + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", fmt.Sprint(i), + "foo-bar", fmt.Sprint(i), + ), + }) + } + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "204", + ), + }) + + res := rt.response() + if res.StatusCode != 204 { + t.Fatalf("status code = %v; want 204", res.StatusCode) + } + want := `code=110 header=map[Foo-Bar:[110]] code=111 header=map[Foo-Bar:[111]] code=112 header=map[Foo-Bar:[112]] code=113 header=map[Foo-Bar:[113]] code=114 header=map[Foo-Bar:[114]] ` - if got := buf.String(); got != want { - t.Errorf("Got trace:\n%s\nWant:\n%s", got, want) - } - return nil - } - ct.server = func() error { - ct.greet() - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } - switch f := f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - case *HeadersFrame: - for i := 110; i <= 114; i++ { - buf.Reset() - enc.WriteField(hpack.HeaderField{Name: ":status", Value: fmt.Sprint(i)}) - enc.WriteField(hpack.HeaderField{Name: "foo-bar", Value: fmt.Sprint(i)}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) - } - buf.Reset() - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "204"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) - return nil - } - } + if got := buf.String(); got != want { + t.Errorf("Got trace:\n%s\nWant:\n%s", got, want) } - ct.run() - } func TestTransportReceiveUndeclaredTrailer(t *testing.T) { - ct := newClientTester(t) - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if err != nil { - return fmt.Errorf("RoundTrip: %v", err) - } - defer res.Body.Close() - if res.StatusCode != 200 { - return fmt.Errorf("status code = %v; want 200", res.StatusCode) - } - slurp, err := ioutil.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("res.Body ReadAll error = %q, %v; want %v", slurp, err, nil) - } - if len(slurp) > 0 { - return fmt.Errorf("body = %q; want nothing", slurp) - } - if _, ok := res.Trailer["Some-Trailer"]; !ok { - return fmt.Errorf("expected Some-Trailer") - } - return nil - } - ct.server = func() error { - ct.greet() - - var n int - var hf *HeadersFrame - for hf == nil && n < 10 { - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } - hf, _ = f.(*HeadersFrame) - n++ - } - - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - - // send headers without Trailer header - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: hf.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + "some-trailer", "I'm an undeclared Trailer!", + ), + }) - // send trailers - buf.Reset() - enc.WriteField(hpack.HeaderField{Name: "some-trailer", Value: "I'm an undeclared Trailer!"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: hf.StreamID, - EndHeaders: true, - EndStream: true, - BlockFragment: buf.Bytes(), - }) - return nil - } - ct.run() + rt.wantStatus(200) + rt.wantBody(nil) + rt.wantTrailers(http.Header{ + "Some-Trailer": []string{"I'm an undeclared Trailer!"}, + }) } func TestTransportInvalidTrailer_Pseudo1(t *testing.T) { @@ -1578,10 +1411,10 @@ func TestTransportInvalidTrailer_Pseudo2(t *testing.T) { testTransportInvalidTrailer_Pseudo(t, splitHeader) } func testTransportInvalidTrailer_Pseudo(t *testing.T, trailers headerType) { - testInvalidTrailer(t, trailers, pseudoHeaderError(":colon"), func(enc *hpack.Encoder) { - enc.WriteField(hpack.HeaderField{Name: ":colon", Value: "foo"}) - enc.WriteField(hpack.HeaderField{Name: "foo", Value: "bar"}) - }) + testInvalidTrailer(t, trailers, pseudoHeaderError(":colon"), + ":colon", "foo", + "foo", "bar", + ) } func TestTransportInvalidTrailer_Capital1(t *testing.T) { @@ -1591,102 +1424,54 @@ func TestTransportInvalidTrailer_Capital2(t *testing.T) { testTransportInvalidTrailer_Capital(t, splitHeader) } func testTransportInvalidTrailer_Capital(t *testing.T, trailers headerType) { - testInvalidTrailer(t, trailers, headerFieldNameError("Capital"), func(enc *hpack.Encoder) { - enc.WriteField(hpack.HeaderField{Name: "foo", Value: "bar"}) - enc.WriteField(hpack.HeaderField{Name: "Capital", Value: "bad"}) - }) + testInvalidTrailer(t, trailers, headerFieldNameError("Capital"), + "foo", "bar", + "Capital", "bad", + ) } func TestTransportInvalidTrailer_EmptyFieldName(t *testing.T) { - testInvalidTrailer(t, oneHeader, headerFieldNameError(""), func(enc *hpack.Encoder) { - enc.WriteField(hpack.HeaderField{Name: "", Value: "bad"}) - }) + testInvalidTrailer(t, oneHeader, headerFieldNameError(""), + "", "bad", + ) } func TestTransportInvalidTrailer_BinaryFieldValue(t *testing.T) { - testInvalidTrailer(t, oneHeader, headerFieldValueError("x"), func(enc *hpack.Encoder) { - enc.WriteField(hpack.HeaderField{Name: "x", Value: "has\nnewline"}) - }) + testInvalidTrailer(t, oneHeader, headerFieldValueError("x"), + "x", "has\nnewline", + ) } -func testInvalidTrailer(t *testing.T, trailers headerType, wantErr error, writeTrailer func(*hpack.Encoder)) { - ct := newClientTester(t) - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if err != nil { - return fmt.Errorf("RoundTrip: %v", err) - } - defer res.Body.Close() - if res.StatusCode != 200 { - return fmt.Errorf("status code = %v; want 200", res.StatusCode) - } - slurp, err := ioutil.ReadAll(res.Body) - se, ok := err.(StreamError) - if !ok || se.Cause != wantErr { - return fmt.Errorf("res.Body ReadAll error = %q, %#v; want StreamError with cause %T, %#v", slurp, err, wantErr, wantErr) - } - if len(slurp) > 0 { - return fmt.Errorf("body = %q; want nothing", slurp) - } - return nil - } - ct.server = func() error { - ct.greet() - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) +func testInvalidTrailer(t *testing.T, mode headerType, wantErr error, trailers ...string) { + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + "trailer", "declared", + ), + }) + tc.writeHeadersMode(mode, HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment(trailers...), + }) - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } - switch f := f.(type) { - case *HeadersFrame: - var endStream bool - send := func(mode headerType) { - hbf := buf.Bytes() - switch mode { - case oneHeader: - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: endStream, - BlockFragment: hbf, - }) - case splitHeader: - if len(hbf) < 2 { - panic("too small") - } - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: false, - EndStream: endStream, - BlockFragment: hbf[:1], - }) - ct.fr.WriteContinuation(f.StreamID, true, hbf[1:]) - default: - panic("bogus mode") - } - } - // Response headers (1+ frames; 1 or 2 in this test, but never 0) - { - buf.Reset() - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - enc.WriteField(hpack.HeaderField{Name: "trailer", Value: "declared"}) - endStream = false - send(oneHeader) - } - // Trailers: - { - endStream = true - buf.Reset() - writeTrailer(enc) - send(trailers) - } - return nil - } - } + rt.wantStatus(200) + body, err := rt.readBody() + se, ok := err.(StreamError) + if !ok || se.Cause != wantErr { + t.Fatalf("res.Body ReadAll error = %q, %#v; want StreamError with cause %T, %#v", body, err, wantErr, wantErr) + } + if len(body) > 0 { + t.Fatalf("body = %q; want nothing", body) } - ct.run() } // headerListSize returns the HTTP2 header list size of h. @@ -1962,115 +1747,80 @@ func TestTransportChecksRequestHeaderListSize(t *testing.T) { } func TestTransportChecksResponseHeaderListSize(t *testing.T) { - ct := newClientTester(t) - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if e, ok := err.(StreamError); ok { - err = e.Cause - } - if err != errResponseHeaderListSize { - size := int64(0) - if res != nil { - res.Body.Close() - for k, vv := range res.Header { - for _, v := range vv { - size += int64(len(k)) + int64(len(v)) + 32 - } - } - } - return fmt.Errorf("RoundTrip Error = %v (and %d bytes of response headers); want errResponseHeaderListSize", err, size) - } - return nil - } - ct.server = func() error { - ct.greet() - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + + tc.wantFrameType(FrameHeaders) + + hdr := []string{":status", "200"} + large := strings.Repeat("a", 1<<10) + for i := 0; i < 5042; i++ { + hdr = append(hdr, large, large) + } + hbf := tc.makeHeaderBlockFragment(hdr...) + // Note: this number might change if our hpack implementation changes. + // That's fine. This is just a sanity check that our response can fit in a single + // header block fragment frame. + if size, want := len(hbf), 6329; size != want { + t.Fatalf("encoding over 10MB of duplicate keypairs took %d bytes; expected %d", size, want) + } + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: hbf, + }) - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } - switch f := f.(type) { - case *HeadersFrame: - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - large := strings.Repeat("a", 1<<10) - for i := 0; i < 5042; i++ { - enc.WriteField(hpack.HeaderField{Name: large, Value: large}) - } - if size, want := buf.Len(), 6329; size != want { - // Note: this number might change if - // our hpack implementation - // changes. That's fine. This is - // just a sanity check that our - // response can fit in a single - // header block fragment frame. - return fmt.Errorf("encoding over 10MB of duplicate keypairs took %d bytes; expected %d", size, want) + res, err := rt.result() + if e, ok := err.(StreamError); ok { + err = e.Cause + } + if err != errResponseHeaderListSize { + size := int64(0) + if res != nil { + res.Body.Close() + for k, vv := range res.Header { + for _, v := range vv { + size += int64(len(k)) + int64(len(v)) + 32 } - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: true, - BlockFragment: buf.Bytes(), - }) - return nil } } + t.Fatalf("RoundTrip Error = %v (and %d bytes of response headers); want errResponseHeaderListSize", err, size) } - ct.run() } func TestTransportCookieHeaderSplit(t *testing.T) { - ct := newClientTester(t) - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - req.Header.Add("Cookie", "a=b;c=d; e=f;") - req.Header.Add("Cookie", "e=f;g=h; ") - req.Header.Add("Cookie", "i=j") - _, err := ct.tr.RoundTrip(req) - return err - } - ct.server = func() error { - ct.greet() - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } - switch f := f.(type) { - case *HeadersFrame: - dec := hpack.NewDecoder(initialHeaderTableSize, nil) - hfs, err := dec.DecodeFull(f.HeaderBlockFragment()) - if err != nil { - return err - } - got := []string{} - want := []string{"a=b", "c=d", "e=f", "e=f", "g=h", "i=j"} - for _, hf := range hfs { - if hf.Name == "cookie" { - got = append(got, hf.Value) - } - } - if !reflect.DeepEqual(got, want) { - t.Errorf("Cookies = %#v, want %#v", got, want) - } + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + req.Header.Add("Cookie", "a=b;c=d; e=f;") + req.Header.Add("Cookie", "e=f;g=h; ") + req.Header.Add("Cookie", "i=j") + rt := tc.roundTrip(req) + + tc.wantHeaders(wantHeader{ + streamID: rt.streamID(), + endStream: true, + header: http.Header{ + "cookie": []string{"a=b", "c=d", "e=f", "e=f", "g=h", "i=j"}, + }, + }) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "204", + ), + }) - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: true, - BlockFragment: buf.Bytes(), - }) - return nil - } - } + if err := rt.err(); err != nil { + t.Fatalf("RoundTrip = %v, want success", err) } - ct.run() } // Test that the Transport returns a typed error from Response.Body.Read calls @@ -2286,55 +2036,49 @@ func TestTransportResponseHeaderTimeout_Body(t *testing.T) { } func testTransportResponseHeaderTimeout(t *testing.T, body bool) { - ct := newClientTester(t) - ct.tr.t1 = &http.Transport{ - ResponseHeaderTimeout: 5 * time.Millisecond, - } - ct.client = func() error { - c := &http.Client{Transport: ct.tr} - var err error - var n int64 - const bodySize = 4 << 20 - if body { - _, err = c.Post("https://dummy.tld/", "text/foo", io.LimitReader(countingReader{&n}, bodySize)) - } else { - _, err = c.Get("https://dummy.tld/") - } - if !isTimeout(err) { - t.Errorf("client expected timeout error; got %#v", err) - } - if body && n != bodySize { - t.Errorf("only read %d bytes of body; want %d", n, bodySize) + const bodySize = 4 << 20 + tc := newTestClientConn(t, func(tr *Transport) { + tr.t1 = &http.Transport{ + ResponseHeaderTimeout: 5 * time.Millisecond, } - return nil + }) + tc.greet() + + var req *http.Request + var reqBody *testRequestBody + if body { + reqBody = tc.newRequestBody() + reqBody.writeBytes(bodySize) + reqBody.closeWithError(io.EOF) + req, _ = http.NewRequest("POST", "https://dummy.tld/", reqBody) + req.Header.Set("Content-Type", "text/foo") + } else { + req, _ = http.NewRequest("GET", "https://dummy.tld/", nil) } - ct.server = func() error { - ct.greet() - for { - f, err := ct.fr.ReadFrame() - if err != nil { - t.Logf("ReadFrame: %v", err) - return nil - } - switch f := f.(type) { - case *DataFrame: - dataLen := len(f.Data()) - if dataLen > 0 { - if err := ct.fr.WriteWindowUpdate(0, uint32(dataLen)); err != nil { - return err - } - if err := ct.fr.WriteWindowUpdate(f.StreamID, uint32(dataLen)); err != nil { - return err - } - } - case *RSTStreamFrame: - if f.StreamID == 1 && f.ErrCode == ErrCodeCancel { - return nil - } - } - } + + rt := tc.roundTrip(req) + + tc.wantFrameType(FrameHeaders) + + tc.writeWindowUpdate(0, bodySize) + tc.writeWindowUpdate(rt.streamID(), bodySize) + + if body { + tc.wantData(wantData{ + endStream: true, + size: bodySize, + }) + } + + tc.advance(4 * time.Millisecond) + if rt.done() { + t.Fatalf("RoundTrip is done after 4ms; want still waiting") + } + tc.advance(1 * time.Millisecond) + + if err := rt.err(); !isTimeout(err) { + t.Fatalf("RoundTrip error: %v; want timeout error", err) } - ct.run() } func TestTransportDisableCompression(t *testing.T) { @@ -2720,115 +2464,61 @@ func TestTransportNewTLSConfig(t *testing.T) { // without END_STREAM, followed by a 0-length DATA frame with // END_STREAM. Make sure we don't get confused by that. (We did.) func TestTransportReadHeadResponse(t *testing.T) { - ct := newClientTester(t) - clientDone := make(chan struct{}) - ct.client = func() error { - defer close(clientDone) - req, _ := http.NewRequest("HEAD", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if err != nil { - return err - } - if res.ContentLength != 123 { - return fmt.Errorf("Content-Length = %d; want 123", res.ContentLength) - } - slurp, err := ioutil.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("ReadAll: %v", err) - } - if len(slurp) > 0 { - return fmt.Errorf("Unexpected non-empty ReadAll body: %q", slurp) - } - return nil - } - ct.server = func() error { - ct.greet() - for { - f, err := ct.fr.ReadFrame() - if err != nil { - t.Logf("ReadFrame: %v", err) - return nil - } - hf, ok := f.(*HeadersFrame) - if !ok { - continue - } - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - enc.WriteField(hpack.HeaderField{Name: "content-length", Value: "123"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: hf.StreamID, - EndHeaders: true, - EndStream: false, // as the GFE does - BlockFragment: buf.Bytes(), - }) - ct.fr.WriteData(hf.StreamID, true, nil) + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("HEAD", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + + tc.wantFrameType(FrameHeaders) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, // as the GFE does + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + "content-length", "123", + ), + }) + tc.writeData(rt.streamID(), true, nil) - <-clientDone - return nil - } + res := rt.response() + if res.ContentLength != 123 { + t.Fatalf("Content-Length = %d; want 123", res.ContentLength) } - ct.run() + rt.wantBody(nil) } func TestTransportReadHeadResponseWithBody(t *testing.T) { - // This test use not valid response format. - // Discarding logger output to not spam tests output. - log.SetOutput(ioutil.Discard) + // This test uses an invalid response format. + // Discard logger output to not spam tests output. + log.SetOutput(io.Discard) defer log.SetOutput(os.Stderr) response := "redirecting to /elsewhere" - ct := newClientTester(t) - clientDone := make(chan struct{}) - ct.client = func() error { - defer close(clientDone) - req, _ := http.NewRequest("HEAD", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if err != nil { - return err - } - if res.ContentLength != int64(len(response)) { - return fmt.Errorf("Content-Length = %d; want %d", res.ContentLength, len(response)) - } - slurp, err := ioutil.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("ReadAll: %v", err) - } - if len(slurp) > 0 { - return fmt.Errorf("Unexpected non-empty ReadAll body: %q", slurp) - } - return nil - } - ct.server = func() error { - ct.greet() - for { - f, err := ct.fr.ReadFrame() - if err != nil { - t.Logf("ReadFrame: %v", err) - return nil - } - hf, ok := f.(*HeadersFrame) - if !ok { - continue - } - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - enc.WriteField(hpack.HeaderField{Name: "content-length", Value: strconv.Itoa(len(response))}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: hf.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) - ct.fr.WriteData(hf.StreamID, true, []byte(response)) + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("HEAD", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + + tc.wantFrameType(FrameHeaders) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + "content-length", strconv.Itoa(len(response)), + ), + }) + tc.writeData(rt.streamID(), true, []byte(response)) - <-clientDone - return nil - } + res := rt.response() + if res.ContentLength != int64(len(response)) { + t.Fatalf("Content-Length = %d; want %d", res.ContentLength, len(response)) } - ct.run() + rt.wantBody(nil) } type neverEnding byte @@ -2953,71 +2643,53 @@ func TestTransportUsesGoAwayDebugError_Body(t *testing.T) { } func testTransportUsesGoAwayDebugError(t *testing.T, failMidBody bool) { - ct := newClientTester(t) - clientDone := make(chan struct{}) + tc := newTestClientConn(t) + tc.greet() const goAwayErrCode = ErrCodeHTTP11Required // arbitrary const goAwayDebugData = "some debug data" - ct.client = func() error { - defer close(clientDone) - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if failMidBody { - if err != nil { - return fmt.Errorf("unexpected client RoundTrip error: %v", err) - } - _, err = io.Copy(ioutil.Discard, res.Body) - res.Body.Close() - } - want := GoAwayError{ - LastStreamID: 5, - ErrCode: goAwayErrCode, - DebugData: goAwayDebugData, - } - if !reflect.DeepEqual(err, want) { - t.Errorf("RoundTrip error = %T: %#v, want %T (%#v)", err, err, want, want) - } - return nil + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + + tc.wantFrameType(FrameHeaders) + + if failMidBody { + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + "content-length", "123", + ), + }) } - ct.server = func() error { - ct.greet() - for { - f, err := ct.fr.ReadFrame() - if err != nil { - t.Logf("ReadFrame: %v", err) - return nil - } - hf, ok := f.(*HeadersFrame) - if !ok { - continue - } - if failMidBody { - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - enc.WriteField(hpack.HeaderField{Name: "content-length", Value: "123"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: hf.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) - } - // Write two GOAWAY frames, to test that the Transport takes - // the interesting parts of both. - ct.fr.WriteGoAway(5, ErrCodeNo, []byte(goAwayDebugData)) - ct.fr.WriteGoAway(5, goAwayErrCode, nil) - ct.sc.(*net.TCPConn).CloseWrite() - if runtime.GOOS == "plan9" { - // CloseWrite not supported on Plan 9; Issue 17906 - ct.sc.(*net.TCPConn).Close() - } - <-clientDone - return nil + + // Write two GOAWAY frames, to test that the Transport takes + // the interesting parts of both. + tc.writeGoAway(5, ErrCodeNo, []byte(goAwayDebugData)) + tc.writeGoAway(5, goAwayErrCode, nil) + tc.closeWrite(io.EOF) + + res, err := rt.result() + whence := "RoundTrip" + if failMidBody { + whence = "Body.Read" + if err != nil { + t.Fatalf("RoundTrip error = %v, want success", err) } + _, err = res.Body.Read(make([]byte, 1)) + } + + want := GoAwayError{ + LastStreamID: 5, + ErrCode: goAwayErrCode, + DebugData: goAwayDebugData, + } + if !reflect.DeepEqual(err, want) { + t.Errorf("%v error = %T: %#v, want %T (%#v)", whence, err, err, want, want) } - ct.run() } func testTransportReturnsUnusedFlowControl(t *testing.T, oneDataFrame bool) { @@ -6049,7 +5721,7 @@ func TestClientConnReservations(t *testing.T) { tr := &Transport{TLSClientConfig: tlsConfigInsecure} defer tr.CloseIdleConnections() - cc, err := tr.newClientConn(st.cc, false) + cc, err := tr.newClientConn(st.cc, false, nil) if err != nil { t.Fatal(err) } From 12ddef72728707026a3d9adbbc28affa76faf688 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 26 Feb 2024 12:25:37 -0800 Subject: [PATCH 155/168] http2: reject DATA frames after 1xx and before final headers When checking to see if a DATA frame can be accepted, check to see if we have received a non-1xx header, not whether we have received any header. Fixes golang/go#65927 Change-Id: Id4fae1862de6179f8fc95e02dec7d4c47a7640e1 Reviewed-on: https://go-review.googlesource.com/c/net/+/567175 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- http2/transport.go | 2 +- http2/transport_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/http2/transport.go b/http2/transport.go index 04db29275b..44845bafd2 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -2787,7 +2787,7 @@ func (rl *clientConnReadLoop) processData(f *DataFrame) error { }) return nil } - if !cs.firstByte { + if !cs.pastHeaders { cc.logf("protocol error: received DATA before a HEADERS frame") rl.endStreamError(cs, StreamError{ StreamID: f.StreamID, diff --git a/http2/transport_test.go b/http2/transport_test.go index f889cd12b5..836d455931 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -6254,3 +6254,32 @@ func TestDialRaceResumesDial(t *testing.T) { case <-successCh: } } + +func TestTransportDataAfter1xxHeader(t *testing.T) { + // Discard logger output to avoid spamming stderr. + log.SetOutput(io.Discard) + defer log.SetOutput(os.Stderr) + + // https://go.dev/issue/65927 - server sends a 1xx response, followed by a DATA frame. + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + + tc.wantFrameType(FrameHeaders) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "100", + ), + }) + tc.writeData(rt.streamID(), true, []byte{0}) + err := rt.err() + if err, ok := err.(StreamError); !ok || err.Code != ErrCodeProtocol { + t.Errorf("RoundTrip error: %v; want ErrCodeProtocol", err) + } + tc.wantFrameType(FrameRSTStream) +} From 31d9683ed011ab20a0aa6ab62de563611851a2b8 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 18 Mar 2024 12:58:12 -0700 Subject: [PATCH 156/168] http2: mark several testing functions as helpers Change-Id: Ib5519fd882b3692efadd6191fbebbf042c9aa77d Reviewed-on: https://go-review.googlesource.com/c/net/+/572376 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- http2/clientconn_test.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/http2/clientconn_test.go b/http2/clientconn_test.go index 6d94762e56..9a5b2b013a 100644 --- a/http2/clientconn_test.go +++ b/http2/clientconn_test.go @@ -191,7 +191,7 @@ func testClientConnReadFrame[T any](tc *testClientConn) T { var v T fr := tc.readFrame() if fr == nil { - tc.t.Fatalf("got no frame, want frame %v", v) + tc.t.Fatalf("got no frame, want frame %T", v) } v, ok := fr.(T) if !ok { @@ -203,6 +203,7 @@ func testClientConnReadFrame[T any](tc *testClientConn) T { // wantFrameType reads the next frame from the conn. // It produces an error if the frame type is not the expected value. func (tc *testClientConn) wantFrameType(want FrameType) { + tc.t.Helper() fr := tc.readFrame() if fr == nil { tc.t.Fatalf("got no frame, want frame %v", want) @@ -221,11 +222,8 @@ type wantHeader struct { // wantHeaders reads a HEADERS frame and potential CONTINUATION frames, // and asserts that they contain the expected headers. func (tc *testClientConn) wantHeaders(want wantHeader) { - fr := tc.readFrame() - got, ok := fr.(*MetaHeadersFrame) - if !ok { - tc.t.Fatalf("got %v, want HEADERS frame", want) - } + tc.t.Helper() + got := testClientConnReadFrame[*MetaHeadersFrame](tc) if got, want := got.StreamID, want.streamID; got != want { tc.t.Fatalf("got stream ID %v, want %v", got, want) } From 9e0498de4d22259990fc8eb8440eafd7c353c19c Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 18 Mar 2024 13:06:45 -0700 Subject: [PATCH 157/168] http2: use synthetic timers for ping timeouts in tests Change-Id: I642890519b066937ade3c13e8387c31d29e912f4 Reviewed-on: https://go-review.googlesource.com/c/net/+/572377 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jonathan Amsterdam --- http2/clientconn_test.go | 9 +++ http2/testsync.go | 101 +++++++++++++++++++++++++++--- http2/transport.go | 69 +++++++++++++++++---- http2/transport_test.go | 131 ++++++++++++++++++++++++--------------- 4 files changed, 240 insertions(+), 70 deletions(-) diff --git a/http2/clientconn_test.go b/http2/clientconn_test.go index 9a5b2b013a..97f884c66e 100644 --- a/http2/clientconn_test.go +++ b/http2/clientconn_test.go @@ -123,6 +123,7 @@ func newTestClientConn(t *testing.T, opts ...func(*Transport)) *testClientConn { tc.fr.SetMaxReadFrameSize(10 << 20) t.Cleanup(func() { + tc.sync() if tc.rerr == nil { tc.rerr = io.EOF } @@ -459,6 +460,14 @@ func (tc *testClientConn) writeContinuation(streamID uint32, endHeaders bool, he tc.sync() } +func (tc *testClientConn) writePing(ack bool, data [8]byte) { + tc.t.Helper() + if err := tc.fr.WritePing(ack, data); err != nil { + tc.t.Fatal(err) + } + tc.sync() +} + func (tc *testClientConn) writeGoAway(maxStreamID uint32, code ErrCode, debugData []byte) { tc.t.Helper() if err := tc.fr.WriteGoAway(maxStreamID, code, debugData); err != nil { diff --git a/http2/testsync.go b/http2/testsync.go index b8335c0fbe..61075bd16d 100644 --- a/http2/testsync.go +++ b/http2/testsync.go @@ -4,6 +4,7 @@ package http2 import ( + "context" "sync" "time" ) @@ -173,18 +174,56 @@ func (h *testSyncHooks) condWait(cond *sync.Cond) { h.unlock() } -// newTimer creates a new timer: A time.Timer if h is nil, or a synthetic timer in tests. +// newTimer creates a new fake timer. func (h *testSyncHooks) newTimer(d time.Duration) timer { h.lock() defer h.unlock() t := &fakeTimer{ - when: h.now.Add(d), - c: make(chan time.Time), + hooks: h, + when: h.now.Add(d), + c: make(chan time.Time), } h.timers = append(h.timers, t) return t } +// afterFunc creates a new fake AfterFunc timer. +func (h *testSyncHooks) afterFunc(d time.Duration, f func()) timer { + h.lock() + defer h.unlock() + t := &fakeTimer{ + hooks: h, + when: h.now.Add(d), + f: f, + } + h.timers = append(h.timers, t) + return t +} + +func (h *testSyncHooks) contextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(ctx) + t := h.afterFunc(d, cancel) + return ctx, func() { + t.Stop() + cancel() + } +} + +func (h *testSyncHooks) timeUntilEvent() time.Duration { + h.lock() + defer h.unlock() + var next time.Time + for _, t := range h.timers { + if next.IsZero() || t.when.Before(next) { + next = t.when + } + } + if d := next.Sub(h.now); d > 0 { + return d + } + return 0 +} + // advance advances time and causes synthetic timers to fire. func (h *testSyncHooks) advance(d time.Duration) { h.lock() @@ -192,6 +231,7 @@ func (h *testSyncHooks) advance(d time.Duration) { h.now = h.now.Add(d) timers := h.timers[:0] for _, t := range h.timers { + t := t // remove after go.mod depends on go1.22 t.mu.Lock() switch { case t.when.After(h.now): @@ -200,7 +240,20 @@ func (h *testSyncHooks) advance(d time.Duration) { // stopped timer default: t.when = time.Time{} - close(t.c) + if t.c != nil { + close(t.c) + } + if t.f != nil { + h.total++ + go func() { + defer func() { + h.lock() + h.total-- + h.unlock() + }() + t.f() + }() + } } t.mu.Unlock() } @@ -212,13 +265,16 @@ func (h *testSyncHooks) advance(d time.Duration) { type timer interface { C() <-chan time.Time Stop() bool + Reset(d time.Duration) bool } +// timeTimer implements timer using real time. type timeTimer struct { t *time.Timer c chan time.Time } +// newTimeTimer creates a new timer using real time. func newTimeTimer(d time.Duration) timer { ch := make(chan time.Time) t := time.AfterFunc(d, func() { @@ -227,16 +283,29 @@ func newTimeTimer(d time.Duration) timer { return &timeTimer{t, ch} } -func (t timeTimer) C() <-chan time.Time { return t.c } -func (t timeTimer) Stop() bool { return t.t.Stop() } +// newTimeAfterFunc creates an AfterFunc timer using real time. +func newTimeAfterFunc(d time.Duration, f func()) timer { + return &timeTimer{ + t: time.AfterFunc(d, f), + } +} +func (t timeTimer) C() <-chan time.Time { return t.c } +func (t timeTimer) Stop() bool { return t.t.Stop() } +func (t timeTimer) Reset(d time.Duration) bool { return t.t.Reset(d) } + +// fakeTimer implements timer using fake time. type fakeTimer struct { + hooks *testSyncHooks + mu sync.Mutex - when time.Time - c chan time.Time + when time.Time // when the timer will fire + c chan time.Time // closed when the timer fires; mutually exclusive with f + f func() // called when the timer fires; mutually exclusive with c } func (t *fakeTimer) C() <-chan time.Time { return t.c } + func (t *fakeTimer) Stop() bool { t.mu.Lock() defer t.mu.Unlock() @@ -244,3 +313,19 @@ func (t *fakeTimer) Stop() bool { t.when = time.Time{} return stopped } + +func (t *fakeTimer) Reset(d time.Duration) bool { + if t.c != nil || t.f == nil { + panic("fakeTimer only supports Reset on AfterFunc timers") + } + t.mu.Lock() + defer t.mu.Unlock() + t.hooks.lock() + defer t.hooks.unlock() + active := !t.when.IsZero() + t.when = t.hooks.now.Add(d) + if !active { + t.hooks.timers = append(t.hooks.timers, t) + } + return active +} diff --git a/http2/transport.go b/http2/transport.go index 44845bafd2..1ce5f125c8 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -391,6 +391,21 @@ func (cc *ClientConn) newTimer(d time.Duration) timer { return newTimeTimer(d) } +// afterFunc creates a new time.AfterFunc timer, or a synthetic timer in tests. +func (cc *ClientConn) afterFunc(d time.Duration, f func()) timer { + if cc.syncHooks != nil { + return cc.syncHooks.afterFunc(d, f) + } + return newTimeAfterFunc(d, f) +} + +func (cc *ClientConn) contextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) { + if cc.syncHooks != nil { + return cc.syncHooks.contextWithTimeout(ctx, d) + } + return context.WithTimeout(ctx, d) +} + // clientStream is the state for a single HTTP/2 stream. One of these // is created for each Transport.RoundTrip call. type clientStream struct { @@ -875,7 +890,7 @@ func (cc *ClientConn) healthCheck() { pingTimeout := cc.t.pingTimeout() // We don't need to periodically ping in the health check, because the readLoop of ClientConn will // trigger the healthCheck again if there is no frame received. - ctx, cancel := context.WithTimeout(context.Background(), pingTimeout) + ctx, cancel := cc.contextWithTimeout(context.Background(), pingTimeout) defer cancel() cc.vlogf("http2: Transport sending health check") err := cc.Ping(ctx) @@ -1432,6 +1447,21 @@ func (cs *clientStream) writeRequest(req *http.Request) (err error) { if cc.reqHeaderMu == nil { panic("RoundTrip on uninitialized ClientConn") // for tests } + var newStreamHook func(*clientStream) + if cc.syncHooks != nil { + newStreamHook = cc.syncHooks.newstream + cc.syncHooks.blockUntil(func() bool { + select { + case cc.reqHeaderMu <- struct{}{}: + <-cc.reqHeaderMu + case <-cs.reqCancel: + case <-ctx.Done(): + default: + return false + } + return true + }) + } select { case cc.reqHeaderMu <- struct{}{}: case <-cs.reqCancel: @@ -1456,8 +1486,8 @@ func (cs *clientStream) writeRequest(req *http.Request) (err error) { } cc.mu.Unlock() - if cc.syncHooks != nil { - cc.syncHooks.newstream(cs) + if newStreamHook != nil { + newStreamHook(cs) } // TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere? @@ -2369,10 +2399,9 @@ func (rl *clientConnReadLoop) run() error { cc := rl.cc gotSettings := false readIdleTimeout := cc.t.ReadIdleTimeout - var t *time.Timer + var t timer if readIdleTimeout != 0 { - t = time.AfterFunc(readIdleTimeout, cc.healthCheck) - defer t.Stop() + t = cc.afterFunc(readIdleTimeout, cc.healthCheck) } for { f, err := cc.fr.ReadFrame() @@ -3067,24 +3096,38 @@ func (cc *ClientConn) Ping(ctx context.Context) error { } cc.mu.Unlock() } - errc := make(chan error, 1) + var pingError error + errc := make(chan struct{}) cc.goRun(func() { cc.wmu.Lock() defer cc.wmu.Unlock() - if err := cc.fr.WritePing(false, p); err != nil { - errc <- err + if pingError = cc.fr.WritePing(false, p); pingError != nil { + close(errc) return } - if err := cc.bw.Flush(); err != nil { - errc <- err + if pingError = cc.bw.Flush(); pingError != nil { + close(errc) return } }) + if cc.syncHooks != nil { + cc.syncHooks.blockUntil(func() bool { + select { + case <-c: + case <-errc: + case <-ctx.Done(): + case <-cc.readerDone: + default: + return false + } + return true + }) + } select { case <-c: return nil - case err := <-errc: - return err + case <-errc: + return pingError case <-ctx.Done(): return ctx.Err() case <-cc.readerDone: diff --git a/http2/transport_test.go b/http2/transport_test.go index 836d455931..bab2472f33 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -3310,26 +3310,24 @@ func TestTransportNoRaceOnRequestObjectAfterRequestComplete(t *testing.T) { } func TestTransportCloseAfterLostPing(t *testing.T) { - clientDone := make(chan struct{}) - ct := newClientTester(t) - ct.tr.PingTimeout = 1 * time.Second - ct.tr.ReadIdleTimeout = 1 * time.Second - ct.client = func() error { - defer ct.cc.(*net.TCPConn).CloseWrite() - defer close(clientDone) - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - _, err := ct.tr.RoundTrip(req) - if err == nil || !strings.Contains(err.Error(), "client connection lost") { - return fmt.Errorf("expected to get error about \"connection lost\", got %v", err) - } - return nil - } - ct.server = func() error { - ct.greet() - <-clientDone - return nil + tc := newTestClientConn(t, func(tr *Transport) { + tr.PingTimeout = 1 * time.Second + tr.ReadIdleTimeout = 1 * time.Second + }) + tc.greet() + + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + tc.wantFrameType(FrameHeaders) + + tc.advance(1 * time.Second) + tc.wantFrameType(FramePing) + + tc.advance(1 * time.Second) + err := rt.err() + if err == nil || !strings.Contains(err.Error(), "client connection lost") { + t.Fatalf("expected to get error about \"connection lost\", got %v", err) } - ct.run() } func TestTransportPingWriteBlocks(t *testing.T) { @@ -3362,38 +3360,73 @@ func TestTransportPingWriteBlocks(t *testing.T) { } } -func TestTransportPingWhenReading(t *testing.T) { - testCases := []struct { - name string - readIdleTimeout time.Duration - deadline time.Duration - expectedPingCount int - }{ - { - name: "two pings", - readIdleTimeout: 100 * time.Millisecond, - deadline: time.Second, - expectedPingCount: 2, - }, - { - name: "zero ping", - readIdleTimeout: time.Second, - deadline: 200 * time.Millisecond, - expectedPingCount: 0, - }, - { - name: "0 readIdleTimeout means no ping", - readIdleTimeout: 0 * time.Millisecond, - deadline: 500 * time.Millisecond, - expectedPingCount: 0, - }, +func TestTransportPingWhenReadingMultiplePings(t *testing.T) { + tc := newTestClientConn(t, func(tr *Transport) { + tr.ReadIdleTimeout = 1000 * time.Millisecond + }) + tc.greet() + + ctx, cancel := context.WithCancel(context.Background()) + req, _ := http.NewRequestWithContext(ctx, "GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + + tc.wantFrameType(FrameHeaders) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) + + for i := 0; i < 5; i++ { + // No ping yet... + tc.advance(999 * time.Millisecond) + if f := tc.readFrame(); f != nil { + t.Fatalf("unexpected frame: %v", f) + } + + // ...ping now. + tc.advance(1 * time.Millisecond) + f := testClientConnReadFrame[*PingFrame](tc) + tc.writePing(true, f.Data) } - for _, tc := range testCases { - tc := tc // capture range variable - t.Run(tc.name, func(t *testing.T) { - testTransportPingWhenReading(t, tc.readIdleTimeout, tc.deadline, tc.expectedPingCount) - }) + // Cancel the request, Transport resets it and returns an error from body reads. + cancel() + tc.sync() + + tc.wantFrameType(FrameRSTStream) + _, err := rt.readBody() + if err == nil { + t.Fatalf("Response.Body.Read() = %v, want error", err) + } +} + +func TestTransportPingWhenReadingPingDisabled(t *testing.T) { + tc := newTestClientConn(t, func(tr *Transport) { + tr.ReadIdleTimeout = 0 // PINGs disabled + }) + tc.greet() + + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + + tc.wantFrameType(FrameHeaders) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) + + // No PING is sent, even after a long delay. + tc.advance(1 * time.Minute) + if f := tc.readFrame(); f != nil { + t.Fatalf("unexpected frame: %v", f) } } From 6e2c99c943496e33025da68db088edff5dc7d07b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 18 Mar 2024 13:01:26 -0700 Subject: [PATCH 158/168] http2: allow testing Transports with testSyncHooks Change-Id: Icafc4860ef0691e5133221a0b53bb1d2158346cc Reviewed-on: https://go-review.googlesource.com/c/net/+/572378 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- http2/clientconn_test.go | 202 ++++++++++++++++++++------ http2/transport.go | 35 +++-- http2/transport_test.go | 301 ++++++++++++++------------------------- 3 files changed, 288 insertions(+), 250 deletions(-) diff --git a/http2/clientconn_test.go b/http2/clientconn_test.go index 97f884c66e..73ceefd7ba 100644 --- a/http2/clientconn_test.go +++ b/http2/clientconn_test.go @@ -99,62 +99,57 @@ type testClientConn struct { roundtrips []*testRoundTrip - rerr error // returned by Read - rbuf bytes.Buffer // sent to the test conn - wbuf bytes.Buffer // sent by the test conn + rerr error // returned by Read + netConnClosed bool // set when the ClientConn closes the net.Conn + rbuf bytes.Buffer // sent to the test conn + wbuf bytes.Buffer // sent by the test conn } -func newTestClientConn(t *testing.T, opts ...func(*Transport)) *testClientConn { - t.Helper() - - tr := &Transport{} - for _, o := range opts { - o(tr) - } - +func newTestClientConnFromClientConn(t *testing.T, cc *ClientConn) *testClientConn { tc := &testClientConn{ t: t, - tr: tr, - hooks: newTestSyncHooks(), + tr: cc.t, + cc: cc, + hooks: cc.t.syncHooks, } + cc.tconn = (*testClientConnNetConn)(tc) tc.enc = hpack.NewEncoder(&tc.encbuf) tc.fr = NewFramer(&tc.rbuf, &tc.wbuf) tc.fr.ReadMetaHeaders = hpack.NewDecoder(initialHeaderTableSize, nil) tc.fr.SetMaxReadFrameSize(10 << 20) - t.Cleanup(func() { tc.sync() if tc.rerr == nil { tc.rerr = io.EOF } tc.sync() - if tc.hooks.total != 0 { - t.Errorf("%v goroutines still running after test completed", tc.hooks.total) - } - }) + return tc +} - tc.hooks.newclientconn = func(cc *ClientConn) { - tc.cc = cc - } - const singleUse = false - _, err := tc.tr.newClientConn((*testClientConnNetConn)(tc), singleUse, tc.hooks) - if err != nil { - t.Fatal(err) - } - tc.sync() - tc.hooks.newclientconn = nil - +func (tc *testClientConn) readClientPreface() { + tc.t.Helper() // Read the client's HTTP/2 preface, sent prior to any HTTP/2 frames. buf := make([]byte, len(clientPreface)) if _, err := io.ReadFull(&tc.wbuf, buf); err != nil { - t.Fatalf("reading preface: %v", err) + tc.t.Fatalf("reading preface: %v", err) } if !bytes.Equal(buf, clientPreface) { - t.Fatalf("client preface: %q, want %q", buf, clientPreface) + tc.t.Fatalf("client preface: %q, want %q", buf, clientPreface) } +} - return tc +func newTestClientConn(t *testing.T, opts ...func(*Transport)) *testClientConn { + t.Helper() + + tt := newTestTransport(t, opts...) + const singleUse = false + _, err := tt.tr.newClientConn(nil, singleUse, tt.tr.syncHooks) + if err != nil { + t.Fatalf("newClientConn: %v", err) + } + + return tt.getConn() } // sync waits for the ClientConn under test to reach a stable state, @@ -349,7 +344,7 @@ func (b *testRequestBody) closeWithError(err error) { // the request times out, or some other terminal condition is reached.) func (tc *testClientConn) roundTrip(req *http.Request) *testRoundTrip { rt := &testRoundTrip{ - tc: tc, + t: tc.t, donec: make(chan struct{}), } tc.roundtrips = append(tc.roundtrips, rt) @@ -362,6 +357,9 @@ func (tc *testClientConn) roundTrip(req *http.Request) *testRoundTrip { tc.hooks.newstream = nil tc.t.Cleanup(func() { + if !rt.done() { + return + } res, _ := rt.result() if res != nil { res.Body.Close() @@ -460,6 +458,14 @@ func (tc *testClientConn) writeContinuation(streamID uint32, endHeaders bool, he tc.sync() } +func (tc *testClientConn) writeRSTStream(streamID uint32, code ErrCode) { + tc.t.Helper() + if err := tc.fr.WriteRSTStream(streamID, code); err != nil { + tc.t.Fatal(err) + } + tc.sync() +} + func (tc *testClientConn) writePing(ack bool, data [8]byte) { tc.t.Helper() if err := tc.fr.WritePing(ack, data); err != nil { @@ -491,9 +497,25 @@ func (tc *testClientConn) closeWrite(err error) { tc.sync() } +// inflowWindow returns the amount of inbound flow control available for a stream, +// or for the connection if streamID is 0. +func (tc *testClientConn) inflowWindow(streamID uint32) int32 { + tc.cc.mu.Lock() + defer tc.cc.mu.Unlock() + if streamID == 0 { + return tc.cc.inflow.avail + tc.cc.inflow.unsent + } + cs := tc.cc.streams[streamID] + if cs == nil { + tc.t.Errorf("no stream with id %v", streamID) + return -1 + } + return cs.inflow.avail + cs.inflow.unsent +} + // testRoundTrip manages a RoundTrip in progress. type testRoundTrip struct { - tc *testClientConn + t *testing.T resp *http.Response respErr error donec chan struct{} @@ -502,6 +524,9 @@ type testRoundTrip struct { // streamID returns the HTTP/2 stream ID of the request. func (rt *testRoundTrip) streamID() uint32 { + if rt.cs == nil { + panic("stream ID unknown") + } return rt.cs.ID } @@ -517,12 +542,12 @@ func (rt *testRoundTrip) done() bool { // result returns the result of the RoundTrip. func (rt *testRoundTrip) result() (*http.Response, error) { - t := rt.tc.t + t := rt.t t.Helper() select { case <-rt.donec: default: - t.Fatalf("RoundTrip (stream %v) is not done; want it to be", rt.streamID()) + t.Fatalf("RoundTrip is not done; want it to be") } return rt.resp, rt.respErr } @@ -530,7 +555,7 @@ func (rt *testRoundTrip) result() (*http.Response, error) { // response returns the response of a successful RoundTrip. // If the RoundTrip unexpectedly failed, it calls t.Fatal. func (rt *testRoundTrip) response() *http.Response { - t := rt.tc.t + t := rt.t t.Helper() resp, err := rt.result() if err != nil { @@ -544,7 +569,7 @@ func (rt *testRoundTrip) response() *http.Response { // err returns the (possibly nil) error result of RoundTrip. func (rt *testRoundTrip) err() error { - t := rt.tc.t + t := rt.t t.Helper() _, err := rt.result() return err @@ -552,7 +577,7 @@ func (rt *testRoundTrip) err() error { // wantStatus indicates the expected response StatusCode. func (rt *testRoundTrip) wantStatus(want int) { - t := rt.tc.t + t := rt.t t.Helper() if got := rt.response().StatusCode; got != want { t.Fatalf("got response status %v, want %v", got, want) @@ -561,7 +586,7 @@ func (rt *testRoundTrip) wantStatus(want int) { // body reads the contents of the response body. func (rt *testRoundTrip) readBody() ([]byte, error) { - t := rt.tc.t + t := rt.t t.Helper() return io.ReadAll(rt.response().Body) } @@ -569,7 +594,7 @@ func (rt *testRoundTrip) readBody() ([]byte, error) { // wantBody indicates the expected response body. // (Note that this consumes the body.) func (rt *testRoundTrip) wantBody(want []byte) { - t := rt.tc.t + t := rt.t t.Helper() got, err := rt.readBody() if err != nil { @@ -582,7 +607,7 @@ func (rt *testRoundTrip) wantBody(want []byte) { // wantHeaders indicates the expected response headers. func (rt *testRoundTrip) wantHeaders(want http.Header) { - t := rt.tc.t + t := rt.t t.Helper() res := rt.response() if diff := diffHeaders(res.Header, want); diff != "" { @@ -592,7 +617,7 @@ func (rt *testRoundTrip) wantHeaders(want http.Header) { // wantTrailers indicates the expected response trailers. func (rt *testRoundTrip) wantTrailers(want http.Header) { - t := rt.tc.t + t := rt.t t.Helper() res := rt.response() if diff := diffHeaders(res.Trailer, want); diff != "" { @@ -630,7 +655,8 @@ func (nc *testClientConnNetConn) Write(b []byte) (n int, err error) { return nc.wbuf.Write(b) } -func (*testClientConnNetConn) Close() error { +func (nc *testClientConnNetConn) Close() error { + nc.netConnClosed = true return nil } @@ -639,3 +665,91 @@ func (*testClientConnNetConn) RemoteAddr() (_ net.Addr) { return } func (*testClientConnNetConn) SetDeadline(t time.Time) error { return nil } func (*testClientConnNetConn) SetReadDeadline(t time.Time) error { return nil } func (*testClientConnNetConn) SetWriteDeadline(t time.Time) error { return nil } + +// A testTransport allows testing Transport.RoundTrip against fake servers. +// Tests that aren't specifically exercising RoundTrip's retry loop or connection pooling +// should use testClientConn instead. +type testTransport struct { + t *testing.T + tr *Transport + + ccs []*testClientConn +} + +func newTestTransport(t *testing.T, opts ...func(*Transport)) *testTransport { + tr := &Transport{ + syncHooks: newTestSyncHooks(), + } + for _, o := range opts { + o(tr) + } + + tt := &testTransport{ + t: t, + tr: tr, + } + tr.syncHooks.newclientconn = func(cc *ClientConn) { + tt.ccs = append(tt.ccs, newTestClientConnFromClientConn(t, cc)) + } + + t.Cleanup(func() { + tt.sync() + if len(tt.ccs) > 0 { + t.Fatalf("%v test ClientConns created, but not examined by test", len(tt.ccs)) + } + if tt.tr.syncHooks.total != 0 { + t.Errorf("%v goroutines still running after test completed", tt.tr.syncHooks.total) + } + }) + + return tt +} + +func (tt *testTransport) sync() { + tt.tr.syncHooks.waitInactive() +} + +func (tt *testTransport) advance(d time.Duration) { + tt.tr.syncHooks.advance(d) + tt.sync() +} + +func (tt *testTransport) hasConn() bool { + return len(tt.ccs) > 0 +} + +func (tt *testTransport) getConn() *testClientConn { + tt.t.Helper() + if len(tt.ccs) == 0 { + tt.t.Fatalf("no new ClientConns created; wanted one") + } + tc := tt.ccs[0] + tt.ccs = tt.ccs[1:] + tc.sync() + tc.readClientPreface() + return tc +} + +func (tt *testTransport) roundTrip(req *http.Request) *testRoundTrip { + rt := &testRoundTrip{ + t: tt.t, + donec: make(chan struct{}), + } + tt.tr.syncHooks.goRun(func() { + defer close(rt.donec) + rt.resp, rt.respErr = tt.tr.RoundTrip(req) + }) + tt.sync() + + tt.t.Cleanup(func() { + if !rt.done() { + return + } + res, _ := rt.result() + if res != nil { + res.Body.Close() + } + }) + + return rt +} diff --git a/http2/transport.go b/http2/transport.go index 1ce5f125c8..bf1dacd35a 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -184,6 +184,8 @@ type Transport struct { connPoolOnce sync.Once connPoolOrDef ClientConnPool // non-nil version of ConnPool + + syncHooks *testSyncHooks } func (t *Transport) maxHeaderListSize() uint32 { @@ -597,15 +599,6 @@ func authorityAddr(scheme string, authority string) (addr string) { return net.JoinHostPort(host, port) } -var retryBackoffHook func(time.Duration) *time.Timer - -func backoffNewTimer(d time.Duration) *time.Timer { - if retryBackoffHook != nil { - return retryBackoffHook(d) - } - return time.NewTimer(d) -} - // RoundTripOpt is like RoundTrip, but takes options. func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) { if !(req.URL.Scheme == "https" || (req.URL.Scheme == "http" && t.AllowHTTP)) { @@ -633,13 +626,27 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res backoff := float64(uint(1) << (uint(retry) - 1)) backoff += backoff * (0.1 * mathrand.Float64()) d := time.Second * time.Duration(backoff) - timer := backoffNewTimer(d) + var tm timer + if t.syncHooks != nil { + tm = t.syncHooks.newTimer(d) + t.syncHooks.blockUntil(func() bool { + select { + case <-tm.C(): + case <-req.Context().Done(): + default: + return false + } + return true + }) + } else { + tm = newTimeTimer(d) + } select { - case <-timer.C: + case <-tm.C(): t.vlogf("RoundTrip retrying after failure: %v", roundTripErr) continue case <-req.Context().Done(): - timer.Stop() + tm.Stop() err = req.Context().Err() } } @@ -718,6 +725,9 @@ func canRetryError(err error) bool { } func (t *Transport) dialClientConn(ctx context.Context, addr string, singleUse bool) (*ClientConn, error) { + if t.syncHooks != nil { + return t.newClientConn(nil, singleUse, t.syncHooks) + } host, _, err := net.SplitHostPort(addr) if err != nil { return nil, err @@ -814,6 +824,7 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool, hooks *testSyncHoo } if hooks != nil { hooks.newclientconn(cc) + c = cc.tconn } if d := t.idleConnTimeout(); d != 0 { cc.idleTimeout = d diff --git a/http2/transport_test.go b/http2/transport_test.go index bab2472f33..5de0ad8c40 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -3688,61 +3688,49 @@ func TestTransportRetryAfterRefusedStream(t *testing.T) { } func TestTransportRetryHasLimit(t *testing.T) { - // Skip in short mode because the total expected delay is 1s+2s+4s+8s+16s=29s. - if testing.Short() { - t.Skip("skipping long test in short mode") - } - retryBackoffHook = func(d time.Duration) *time.Timer { - return time.NewTimer(0) // fires immediately - } - defer func() { - retryBackoffHook = nil - }() - clientDone := make(chan struct{}) - ct := newClientTester(t) - ct.client = func() error { - defer ct.cc.(*net.TCPConn).CloseWrite() - if runtime.GOOS == "plan9" { - // CloseWrite not supported on Plan 9; Issue 17906 - defer ct.cc.(*net.TCPConn).Close() - } - defer close(clientDone) - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - resp, err := ct.tr.RoundTrip(req) - if err == nil { - return fmt.Errorf("RoundTrip expected error, got response: %+v", resp) + tt := newTestTransport(t) + + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tt.roundTrip(req) + + // First attempt: Server sends a GOAWAY. + tc := tt.getConn() + tc.wantFrameType(FrameSettings) + tc.wantFrameType(FrameWindowUpdate) + + var totalDelay time.Duration + count := 0 + for streamID := uint32(1); ; streamID += 2 { + count++ + tc.wantHeaders(wantHeader{ + streamID: streamID, + endStream: true, + }) + if streamID == 1 { + tc.writeSettings() + tc.wantFrameType(FrameSettings) // settings ACK } - t.Logf("expected error, got: %v", err) - return nil - } - ct.server = func() error { - ct.greet() - for { - f, err := ct.fr.ReadFrame() - if err != nil { - select { - case <-clientDone: - // If the client's done, it - // will have reported any - // errors on its side. - return nil - default: - return err - } - } - switch f := f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - case *HeadersFrame: - if !f.HeadersEnded() { - return fmt.Errorf("headers should have END_HEADERS be ended: %v", f) - } - ct.fr.WriteRSTStream(f.StreamID, ErrCodeRefusedStream) - default: - return fmt.Errorf("Unexpected client frame %v", f) + tc.writeRSTStream(streamID, ErrCodeRefusedStream) + + d := tt.tr.syncHooks.timeUntilEvent() + if d == 0 { + if streamID == 1 { + continue } + break + } + totalDelay += d + if totalDelay > 5*time.Minute { + t.Fatalf("RoundTrip still retrying after %v, should have given up", totalDelay) } + tt.advance(d) + } + if got, want := count, 5; got < count { + t.Errorf("RoundTrip made %v attempts, want at least %v", got, want) + } + if rt.err() == nil { + t.Errorf("RoundTrip succeeded, want error") } - ct.run() } func TestTransportResponseDataBeforeHeaders(t *testing.T) { @@ -5593,155 +5581,80 @@ func TestTransportCloseRequestBody(t *testing.T) { } } -// collectClientsConnPool is a ClientConnPool that wraps lower and -// collects what calls were made on it. -type collectClientsConnPool struct { - lower ClientConnPool - - mu sync.Mutex - getErrs int - got []*ClientConn -} - -func (p *collectClientsConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) { - cc, err := p.lower.GetClientConn(req, addr) - p.mu.Lock() - defer p.mu.Unlock() - if err != nil { - p.getErrs++ - return nil, err - } - p.got = append(p.got, cc) - return cc, nil -} - -func (p *collectClientsConnPool) MarkDead(cc *ClientConn) { - p.lower.MarkDead(cc) -} - func TestTransportRetriesOnStreamProtocolError(t *testing.T) { - ct := newClientTester(t) - pool := &collectClientsConnPool{ - lower: &clientConnPool{t: ct.tr}, - } - ct.tr.ConnPool = pool + // This test verifies that + // - receiving a protocol error on a connection does not interfere with + // other requests in flight on that connection; + // - the connection is not reused for further requests; and + // - the failed request is retried on a new connecection. + tt := newTestTransport(t) + + // Start two requests. The first is a long request + // that will finish after the second. The second one + // will result in the protocol error. + + // Request #1: The long request. + req1, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt1 := tt.roundTrip(req1) + tc1 := tt.getConn() + tc1.wantFrameType(FrameSettings) + tc1.wantFrameType(FrameWindowUpdate) + tc1.wantHeaders(wantHeader{ + streamID: 1, + endStream: true, + }) + tc1.writeSettings() + tc1.wantFrameType(FrameSettings) // settings ACK + + // Request #2(a): The short request. + req2, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt2 := tt.roundTrip(req2) + tc1.wantHeaders(wantHeader{ + streamID: 3, + endStream: true, + }) - gotProtoError := make(chan bool, 1) - ct.tr.CountError = func(errType string) { - if errType == "recv_rststream_PROTOCOL_ERROR" { - select { - case gotProtoError <- true: - default: - } - } + // Request #2(a) fails with ErrCodeProtocol. + tc1.writeRSTStream(3, ErrCodeProtocol) + if rt1.done() { + t.Fatalf("After protocol error on RoundTrip #2, RoundTrip #1 is done; want still in progress") } - ct.client = func() error { - // Start two requests. The first is a long request - // that will finish after the second. The second one - // will result in the protocol error. We check that - // after the first one closes, the connection then - // shuts down. - - // The long, outer request. - req1, _ := http.NewRequest("GET", "https://dummy.tld/long", nil) - res1, err := ct.tr.RoundTrip(req1) - if err != nil { - return err - } - if got, want := res1.Header.Get("Is-Long"), "1"; got != want { - return fmt.Errorf("First response's Is-Long header = %q; want %q", got, want) - } - - req, _ := http.NewRequest("POST", "https://dummy.tld/fails", nil) - res, err := ct.tr.RoundTrip(req) - const want = "only one dial allowed in test mode" - if got := fmt.Sprint(err); got != want { - t.Errorf("didn't dial again: got %#q; want %#q", got, want) - } - if res != nil { - res.Body.Close() - } - select { - case <-gotProtoError: - default: - t.Errorf("didn't get stream protocol error") - } - - if n, err := res1.Body.Read(make([]byte, 10)); err != io.EOF || n != 0 { - t.Errorf("unexpected body read %v, %v", n, err) - } - - pool.mu.Lock() - defer pool.mu.Unlock() - if pool.getErrs != 1 { - t.Errorf("pool get errors = %v; want 1", pool.getErrs) - } - if len(pool.got) == 2 { - if pool.got[0] != pool.got[1] { - t.Errorf("requests went on different connections") - } - cc := pool.got[0] - cc.mu.Lock() - if !cc.doNotReuse { - t.Error("ClientConn not marked doNotReuse") - } - cc.mu.Unlock() - - select { - case <-cc.readerDone: - case <-time.After(5 * time.Second): - t.Errorf("timeout waiting for reader to be done") - } - } else { - t.Errorf("pool get success = %v; want 2", len(pool.got)) - } - return nil + if rt2.done() { + t.Fatalf("After protocol error on RoundTrip #2, RoundTrip #2 is done; want still in progress") } - ct.server = func() error { - ct.greet() - var sentErr bool - var numHeaders int - var firstStreamID uint32 - var hbuf bytes.Buffer - enc := hpack.NewEncoder(&hbuf) + // Request #2(b): The short request is retried on a new connection. + tc2 := tt.getConn() + tc2.wantFrameType(FrameSettings) + tc2.wantFrameType(FrameWindowUpdate) + tc2.wantHeaders(wantHeader{ + streamID: 1, + endStream: true, + }) + tc2.writeSettings() + tc2.wantFrameType(FrameSettings) // settings ACK - for { - f, err := ct.fr.ReadFrame() - if err == io.EOF { - // Client hung up on us, as it should at the end. - return nil - } - if err != nil { - return nil - } - switch f := f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - case *HeadersFrame: - numHeaders++ - if numHeaders == 1 { - firstStreamID = f.StreamID - hbuf.Reset() - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - enc.WriteField(hpack.HeaderField{Name: "is-long", Value: "1"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: hbuf.Bytes(), - }) - continue - } - if !sentErr { - sentErr = true - ct.fr.WriteRSTStream(f.StreamID, ErrCodeProtocol) - ct.fr.WriteData(firstStreamID, true, nil) - continue - } - } - } - } - ct.run() + // Request #2(b) succeeds. + tc2.writeHeaders(HeadersFrameParam{ + StreamID: 1, + EndHeaders: true, + EndStream: true, + BlockFragment: tc1.makeHeaderBlockFragment( + ":status", "201", + ), + }) + rt2.wantStatus(201) + + // Request #1 succeeds. + tc1.writeHeaders(HeadersFrameParam{ + StreamID: 1, + EndHeaders: true, + EndStream: true, + BlockFragment: tc1.makeHeaderBlockFragment( + ":status", "200", + ), + }) + rt1.wantStatus(200) } func TestClientConnReservations(t *testing.T) { From 89f602b7bbf237abe0467031a18b42fc742ced08 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Tue, 19 Mar 2024 00:18:26 -0700 Subject: [PATCH 159/168] http2: validate client/outgoing trailers This change is a counterpart to the HTTP/1.1 trailers validation CL 572615. This change will ensure that we validate trailers that were set on the HTTP client before they are transformed to HTTP/2 equivalents. Updates golang/go#64766 Change-Id: Id1afd7f7e9af820ea969ef226bbb16e4de6d57a5 Reviewed-on: https://go-review.googlesource.com/c/net/+/572655 Auto-Submit: Damien Neil TryBot-Result: Gopher Robot Reviewed-by: Damien Neil Run-TryBot: Emmanuel Odeke LUCI-TryBot-Result: Go LUCI Reviewed-by: David Chase --- http2/transport.go | 33 ++++++++++++++++++++++----------- http2/transport_test.go | 13 ++++++++++++- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/http2/transport.go b/http2/transport.go index bf1dacd35a..ba0956e225 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -2019,6 +2019,22 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error) } } +func validateHeaders(hdrs http.Header) string { + for k, vv := range hdrs { + if !httpguts.ValidHeaderFieldName(k) { + return fmt.Sprintf("name %q", k) + } + for _, v := range vv { + if !httpguts.ValidHeaderFieldValue(v) { + // Don't include the value in the error, + // because it may be sensitive. + return fmt.Sprintf("value for header %q", k) + } + } + } + return "" +} + var errNilRequestURL = errors.New("http2: Request.URI is nil") // requires cc.wmu be held. @@ -2056,19 +2072,14 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail } } - // Check for any invalid headers and return an error before we + // Check for any invalid headers+trailers and return an error before we // potentially pollute our hpack state. (We want to be able to // continue to reuse the hpack encoder for future requests) - for k, vv := range req.Header { - if !httpguts.ValidHeaderFieldName(k) { - return nil, fmt.Errorf("invalid HTTP header name %q", k) - } - for _, v := range vv { - if !httpguts.ValidHeaderFieldValue(v) { - // Don't include the value in the error, because it may be sensitive. - return nil, fmt.Errorf("invalid HTTP header value for header %q", k) - } - } + if err := validateHeaders(req.Header); err != "" { + return nil, fmt.Errorf("invalid HTTP header %s", err) + } + if err := validateHeaders(req.Trailer); err != "" { + return nil, fmt.Errorf("invalid HTTP trailer %s", err) } enumerateHeaders := func(f func(name, value string)) { diff --git a/http2/transport_test.go b/http2/transport_test.go index 5de0ad8c40..5226a61f7f 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -2290,7 +2290,8 @@ func TestTransportRejectsContentLengthWithSign(t *testing.T) { } // golang.org/issue/14048 -func TestTransportFailsOnInvalidHeaders(t *testing.T) { +// golang.org/issue/64766 +func TestTransportFailsOnInvalidHeadersAndTrailers(t *testing.T) { st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { var got []string for k := range r.Header { @@ -2303,6 +2304,7 @@ func TestTransportFailsOnInvalidHeaders(t *testing.T) { tests := [...]struct { h http.Header + t http.Header wantErr string }{ 0: { @@ -2321,6 +2323,14 @@ func TestTransportFailsOnInvalidHeaders(t *testing.T) { h: http.Header{"foo": {"foo\x01bar"}}, wantErr: `invalid HTTP header value for header "foo"`, }, + 4: { + t: http.Header{"foo": {"foo\x01bar"}}, + wantErr: `invalid HTTP trailer value for header "foo"`, + }, + 5: { + t: http.Header{"x-\r\nda": {"foo\x01bar"}}, + wantErr: `invalid HTTP trailer name "x-\r\nda"`, + }, } tr := &Transport{TLSClientConfig: tlsConfigInsecure} @@ -2329,6 +2339,7 @@ func TestTransportFailsOnInvalidHeaders(t *testing.T) { for i, tt := range tests { req, _ := http.NewRequest("GET", st.ts.URL, nil) req.Header = tt.h + req.Trailer = tt.t res, err := tr.RoundTrip(req) var bad bool if tt.wantErr == "" { From d73acffdc9493532acb85777105bb4a351eea702 Mon Sep 17 00:00:00 2001 From: Andy Pan Date: Sun, 10 Mar 2024 23:40:56 +0800 Subject: [PATCH 160/168] http2: only set up deadline when Server.IdleTimeout is positive Check out https://go-review.googlesource.com/c/go/+/570396 Change-Id: I8bda17acebc27308c2a1723191ea1e4a9e64d585 Reviewed-on: https://go-review.googlesource.com/c/net/+/570455 LUCI-TryBot-Result: Go LUCI Reviewed-by: David Chase Reviewed-by: Damien Neil Auto-Submit: Damien Neil --- http2/server.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/http2/server.go b/http2/server.go index 905206f3e6..ce2e8b40ee 100644 --- a/http2/server.go +++ b/http2/server.go @@ -124,6 +124,7 @@ type Server struct { // IdleTimeout specifies how long until idle clients should be // closed with a GOAWAY frame. PING frames are not considered // activity for the purposes of IdleTimeout. + // If zero or negative, there is no timeout. IdleTimeout time.Duration // MaxUploadBufferPerConnection is the size of the initial flow @@ -924,7 +925,7 @@ func (sc *serverConn) serve() { sc.setConnState(http.StateActive) sc.setConnState(http.StateIdle) - if sc.srv.IdleTimeout != 0 { + if sc.srv.IdleTimeout > 0 { sc.idleTimer = time.AfterFunc(sc.srv.IdleTimeout, sc.onIdleTimer) defer sc.idleTimer.Stop() } @@ -1637,7 +1638,7 @@ func (sc *serverConn) closeStream(st *stream, err error) { delete(sc.streams, st.id) if len(sc.streams) == 0 { sc.setConnState(http.StateIdle) - if sc.srv.IdleTimeout != 0 { + if sc.srv.IdleTimeout > 0 { sc.idleTimer.Reset(sc.srv.IdleTimeout) } if h1ServerKeepAlivesDisabled(sc.hs) { From d8870b0bf2f2426fc8d19a9332f652da5c25418f Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 18 Mar 2024 13:51:41 -0700 Subject: [PATCH 161/168] http2: use synthetic time in TestIdleConnTimeout Rewrite TestIdleConnTimeout to use the new synthetic time and synchronization test facilities, rather than using real time and sleeps. Reduces the test time from 20 seconds to 0. Reduces all package tests on my laptop from 32 seconds to 12. Change-Id: I33838488168450a7acd6a462777b5a4caf7f5307 Reviewed-on: https://go-review.googlesource.com/c/net/+/572379 Reviewed-by: Jonathan Amsterdam Reviewed-by: Emmanuel Odeke LUCI-TryBot-Result: Go LUCI --- http2/transport.go | 4 +- http2/transport_test.go | 90 +++++++++++++++++++++++++---------------- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/http2/transport.go b/http2/transport.go index ba0956e225..ce375c8c75 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -310,7 +310,7 @@ type ClientConn struct { readerErr error // set before readerDone is closed idleTimeout time.Duration // or 0 for never - idleTimer *time.Timer + idleTimer timer mu sync.Mutex // guards following cond *sync.Cond // hold mu; broadcast on flow/closed changes @@ -828,7 +828,7 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool, hooks *testSyncHoo } if d := t.idleConnTimeout(); d != 0 { cc.idleTimeout = d - cc.idleTimer = time.AfterFunc(d, cc.onIdleTimeout) + cc.idleTimer = cc.afterFunc(d, cc.onIdleTimeout) } if VerboseLogs { t.vlogf("http2: Transport creating client conn %p to %v", cc, c.RemoteAddr()) diff --git a/http2/transport_test.go b/http2/transport_test.go index 5226a61f7f..18d4db3ed5 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -97,63 +97,83 @@ func startH2cServer(t *testing.T) net.Listener { func TestIdleConnTimeout(t *testing.T) { for _, test := range []struct { + name string idleConnTimeout time.Duration wait time.Duration baseTransport *http.Transport - wantConns int32 + wantNewConn bool }{{ + name: "NoExpiry", idleConnTimeout: 2 * time.Second, wait: 1 * time.Second, baseTransport: nil, - wantConns: 1, + wantNewConn: false, }, { + name: "H2TransportTimeoutExpires", idleConnTimeout: 1 * time.Second, wait: 2 * time.Second, baseTransport: nil, - wantConns: 5, + wantNewConn: true, }, { + name: "H1TransportTimeoutExpires", idleConnTimeout: 0 * time.Second, wait: 1 * time.Second, baseTransport: &http.Transport{ IdleConnTimeout: 2 * time.Second, }, - wantConns: 1, + wantNewConn: false, }} { - var gotConns int32 - - st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, r.RemoteAddr) - }, optOnlyServer) - defer st.Close() + t.Run(test.name, func(t *testing.T) { + tt := newTestTransport(t, func(tr *Transport) { + tr.IdleConnTimeout = test.idleConnTimeout + }) + var tc *testClientConn + for i := 0; i < 3; i++ { + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tt.roundTrip(req) + + // This request happens on a new conn if it's the first request + // (and there is no cached conn), or if the test timeout is long + // enough that old conns are being closed. + wantConn := i == 0 || test.wantNewConn + if has := tt.hasConn(); has != wantConn { + t.Fatalf("request %v: hasConn=%v, want %v", i, has, wantConn) + } + if wantConn { + tc = tt.getConn() + // Read client's SETTINGS and first WINDOW_UPDATE, + // send our SETTINGS. + tc.wantFrameType(FrameSettings) + tc.wantFrameType(FrameWindowUpdate) + tc.writeSettings() + } + if tt.hasConn() { + t.Fatalf("request %v: Transport has more than one conn", i) + } - tr := &Transport{ - IdleConnTimeout: test.idleConnTimeout, - TLSClientConfig: tlsConfigInsecure, - } - defer tr.CloseIdleConnections() + // Respond to the client's request. + hf := testClientConnReadFrame[*MetaHeadersFrame](tc) + tc.writeHeaders(HeadersFrameParam{ + StreamID: hf.StreamID, + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) + rt.wantStatus(200) - for i := 0; i < 5; i++ { - req, _ := http.NewRequest("GET", st.ts.URL, http.NoBody) - trace := &httptrace.ClientTrace{ - GotConn: func(connInfo httptrace.GotConnInfo) { - if !connInfo.Reused { - atomic.AddInt32(&gotConns, 1) - } - }, - } - req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) + // If this was a newly-accepted conn, read the SETTINGS ACK. + if wantConn { + tc.wantFrameType(FrameSettings) // ACK to our settings + } - _, err := tr.RoundTrip(req) - if err != nil { - t.Fatalf("%v", err) + tt.advance(test.wait) + if got, want := tc.netConnClosed, test.wantNewConn; got != want { + t.Fatalf("after waiting %v, conn closed=%v; want %v", test.wait, got, want) + } } - - <-time.After(test.wait) - } - - if gotConns != test.wantConns { - t.Errorf("incorrect gotConns: %d != %d", gotConns, test.wantConns) - } + }) } } From c7877ac4213b2f859831366f5a35b353e0dc9f66 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 18 Mar 2024 13:24:51 -0700 Subject: [PATCH 162/168] http2: convert the remaining clientTester tests to testClientConn Change-Id: Ia7f213346baff48504fef6dfdc112575a5459f35 Reviewed-on: https://go-review.googlesource.com/c/net/+/572380 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- http2/clientconn_test.go | 74 ++ http2/transport_test.go | 1651 +++++++++++--------------------------- 2 files changed, 535 insertions(+), 1190 deletions(-) diff --git a/http2/clientconn_test.go b/http2/clientconn_test.go index 73ceefd7ba..4237b14364 100644 --- a/http2/clientconn_test.go +++ b/http2/clientconn_test.go @@ -14,6 +14,7 @@ import ( "net" "net/http" "reflect" + "slices" "testing" "time" @@ -209,6 +210,71 @@ func (tc *testClientConn) wantFrameType(want FrameType) { } } +// wantUnorderedFrames reads frames from the conn until every condition in want has been satisfied. +// +// want is a list of func(*SomeFrame) bool. +// wantUnorderedFrames will call each func with frames of the appropriate type +// until the func returns true. +// It calls t.Fatal if an unexpected frame is received (no func has that frame type, +// or all funcs with that type have returned true), or if the conn runs out of frames +// with unsatisfied funcs. +// +// Example: +// +// // Read a SETTINGS frame, and any number of DATA frames for a stream. +// // The SETTINGS frame may appear anywhere in the sequence. +// // The last DATA frame must indicate the end of the stream. +// tc.wantUnorderedFrames( +// func(f *SettingsFrame) bool { +// return true +// }, +// func(f *DataFrame) bool { +// return f.StreamEnded() +// }, +// ) +func (tc *testClientConn) wantUnorderedFrames(want ...any) { + tc.t.Helper() + want = slices.Clone(want) + seen := 0 +frame: + for seen < len(want) && !tc.t.Failed() { + fr := tc.readFrame() + if fr == nil { + break + } + for i, f := range want { + if f == nil { + continue + } + typ := reflect.TypeOf(f) + if typ.Kind() != reflect.Func || + typ.NumIn() != 1 || + typ.NumOut() != 1 || + typ.Out(0) != reflect.TypeOf(true) { + tc.t.Fatalf("expected func(*SomeFrame) bool, got %T", f) + } + if typ.In(0) == reflect.TypeOf(fr) { + out := reflect.ValueOf(f).Call([]reflect.Value{reflect.ValueOf(fr)}) + if out[0].Bool() { + want[i] = nil + seen++ + } + continue frame + } + } + tc.t.Errorf("got unexpected frame type %T", fr) + } + if seen < len(want) { + for _, f := range want { + if f == nil { + continue + } + tc.t.Errorf("did not see expected frame: %v", reflect.TypeOf(f).In(0)) + } + tc.t.Fatalf("did not see %v expected frame types", len(want)-seen) + } +} + type wantHeader struct { streamID uint32 endStream bool @@ -401,6 +467,14 @@ func (tc *testClientConn) writeData(streamID uint32, endStream bool, data []byte tc.sync() } +func (tc *testClientConn) writeDataPadded(streamID uint32, endStream bool, data, pad []byte) { + tc.t.Helper() + if err := tc.fr.WriteDataPadded(streamID, endStream, data, pad); err != nil { + tc.t.Fatal(err) + } + tc.sync() +} + // makeHeaderBlockFragment encodes headers in a form suitable for inclusion // in a HEADERS or CONTINUATION frame. // diff --git a/http2/transport_test.go b/http2/transport_test.go index 18d4db3ed5..855c107efb 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -2724,122 +2724,75 @@ func testTransportUsesGoAwayDebugError(t *testing.T, failMidBody bool) { } func testTransportReturnsUnusedFlowControl(t *testing.T, oneDataFrame bool) { - ct := newClientTester(t) - - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if err != nil { - return err - } + tc := newTestClientConn(t) + tc.greet() - if n, err := res.Body.Read(make([]byte, 1)); err != nil || n != 1 { - return fmt.Errorf("body read = %v, %v; want 1, nil", n, err) - } - res.Body.Close() // leaving 4999 bytes unread + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) - return nil + tc.wantFrameType(FrameHeaders) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + "content-length", "5000", + ), + }) + initialInflow := tc.inflowWindow(0) + + // Two cases: + // - Send one DATA frame with 5000 bytes. + // - Send two DATA frames with 1 and 4999 bytes each. + // + // In both cases, the client should consume one byte of data, + // refund that byte, then refund the following 4999 bytes. + // + // In the second case, the server waits for the client to reset the + // stream before sending the second DATA frame. This tests the case + // where the client receives a DATA frame after it has reset the stream. + const streamNotEnded = false + if oneDataFrame { + tc.writeData(rt.streamID(), streamNotEnded, make([]byte, 5000)) + } else { + tc.writeData(rt.streamID(), streamNotEnded, make([]byte, 1)) } - ct.server = func() error { - ct.greet() - - var hf *HeadersFrame - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return fmt.Errorf("ReadFrame while waiting for Headers: %v", err) - } - switch f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - continue - } - var ok bool - hf, ok = f.(*HeadersFrame) - if !ok { - return fmt.Errorf("Got %T; want HeadersFrame", f) - } - break - } - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - enc.WriteField(hpack.HeaderField{Name: "content-length", Value: "5000"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: hf.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) - initialInflow := ct.inflowWindow(0) - - // Two cases: - // - Send one DATA frame with 5000 bytes. - // - Send two DATA frames with 1 and 4999 bytes each. - // - // In both cases, the client should consume one byte of data, - // refund that byte, then refund the following 4999 bytes. - // - // In the second case, the server waits for the client to reset the - // stream before sending the second DATA frame. This tests the case - // where the client receives a DATA frame after it has reset the stream. - if oneDataFrame { - ct.fr.WriteData(hf.StreamID, false /* don't end stream */, make([]byte, 5000)) - } else { - ct.fr.WriteData(hf.StreamID, false /* don't end stream */, make([]byte, 1)) - } + res := rt.response() + if n, err := res.Body.Read(make([]byte, 1)); err != nil || n != 1 { + t.Fatalf("body read = %v, %v; want 1, nil", n, err) + } + res.Body.Close() // leaving 4999 bytes unread + tc.sync() - wantRST := true - wantWUF := true - if !oneDataFrame { - wantWUF = false // flow control update is small, and will not be sent - } - for wantRST || wantWUF { - f, err := ct.readNonSettingsFrame() - if err != nil { - return err + sentAdditionalData := false + tc.wantUnorderedFrames( + func(f *RSTStreamFrame) bool { + if f.ErrCode != ErrCodeCancel { + t.Fatalf("Expected a RSTStreamFrame with code cancel; got %v", summarizeFrame(f)) } - switch f := f.(type) { - case *RSTStreamFrame: - if !wantRST { - return fmt.Errorf("Unexpected frame: %v", summarizeFrame(f)) - } - if f.ErrCode != ErrCodeCancel { - return fmt.Errorf("Expected a RSTStreamFrame with code cancel; got %v", summarizeFrame(f)) - } - wantRST = false - case *WindowUpdateFrame: - if !wantWUF { - return fmt.Errorf("Unexpected frame: %v", summarizeFrame(f)) - } - if f.Increment != 5000 { - return fmt.Errorf("Expected WindowUpdateFrames for 5000 bytes; got %v", summarizeFrame(f)) - } - wantWUF = false - default: - return fmt.Errorf("Unexpected frame: %v", summarizeFrame(f)) + if !oneDataFrame { + // Send the remaining data now. + tc.writeData(rt.streamID(), streamNotEnded, make([]byte, 4999)) + sentAdditionalData = true } - } - if !oneDataFrame { - ct.fr.WriteData(hf.StreamID, false /* don't end stream */, make([]byte, 4999)) - f, err := ct.readNonSettingsFrame() - if err != nil { - return err + return true + }, + func(f *WindowUpdateFrame) bool { + if !oneDataFrame && !sentAdditionalData { + t.Fatalf("Got WindowUpdateFrame, don't expect one yet") } - wuf, ok := f.(*WindowUpdateFrame) - if !ok || wuf.Increment != 5000 { - return fmt.Errorf("want WindowUpdateFrame for 5000 bytes; got %v", summarizeFrame(f)) + if f.Increment != 5000 { + t.Fatalf("Expected WindowUpdateFrames for 5000 bytes; got %v", summarizeFrame(f)) } - } - if err := ct.writeReadPing(); err != nil { - return err - } - if got, want := ct.inflowWindow(0), initialInflow; got != want { - return fmt.Errorf("connection flow tokens = %v, want %v", got, want) - } - return nil + return true + }, + ) + + if got, want := tc.inflowWindow(0), initialInflow; got != want { + t.Fatalf("connection flow tokens = %v, want %v", got, want) } - ct.run() } // See golang.org/issue/16481 @@ -2855,199 +2808,124 @@ func TestTransportReturnsUnusedFlowControlMultipleWrites(t *testing.T) { // Issue 16612: adjust flow control on open streams when transport // receives SETTINGS with INITIAL_WINDOW_SIZE from server. func TestTransportAdjustsFlowControl(t *testing.T) { - ct := newClientTester(t) - clientDone := make(chan struct{}) - const bodySize = 1 << 20 - ct.client = func() error { - defer ct.cc.(*net.TCPConn).CloseWrite() - if runtime.GOOS == "plan9" { - // CloseWrite not supported on Plan 9; Issue 17906 - defer ct.cc.(*net.TCPConn).Close() - } - defer close(clientDone) + tc := newTestClientConn(t) + tc.wantFrameType(FrameSettings) + tc.wantFrameType(FrameWindowUpdate) + // Don't write our SETTINGS yet. - req, _ := http.NewRequest("POST", "https://dummy.tld/", struct{ io.Reader }{io.LimitReader(neverEnding('A'), bodySize)}) - res, err := ct.tr.RoundTrip(req) - if err != nil { - return err + body := tc.newRequestBody() + body.writeBytes(bodySize) + body.closeWithError(io.EOF) + + req, _ := http.NewRequest("POST", "https://dummy.tld/", body) + rt := tc.roundTrip(req) + + tc.wantFrameType(FrameHeaders) + + gotBytes := int64(0) + for { + f := testClientConnReadFrame[*DataFrame](tc) + gotBytes += int64(len(f.Data())) + // After we've got half the client's initial flow control window's worth + // of request body data, give it just enough flow control to finish. + if gotBytes >= initialWindowSize/2 { + break } - res.Body.Close() - return nil } - ct.server = func() error { - _, err := io.ReadFull(ct.sc, make([]byte, len(ClientPreface))) - if err != nil { - return fmt.Errorf("reading client preface: %v", err) - } - var gotBytes int64 - var sentSettings bool - for { - f, err := ct.fr.ReadFrame() - if err != nil { - select { - case <-clientDone: - return nil - default: - return fmt.Errorf("ReadFrame while waiting for Headers: %v", err) - } - } - switch f := f.(type) { - case *DataFrame: - gotBytes += int64(len(f.Data())) - // After we've got half the client's - // initial flow control window's worth - // of request body data, give it just - // enough flow control to finish. - if gotBytes >= initialWindowSize/2 && !sentSettings { - sentSettings = true - - ct.fr.WriteSettings(Setting{ID: SettingInitialWindowSize, Val: bodySize}) - ct.fr.WriteWindowUpdate(0, bodySize) - ct.fr.WriteSettingsAck() - } + tc.writeSettings(Setting{ID: SettingInitialWindowSize, Val: bodySize}) + tc.writeWindowUpdate(0, bodySize) + tc.writeSettingsAck() - if f.StreamEnded() { - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: true, - BlockFragment: buf.Bytes(), - }) - } - } - } + tc.wantUnorderedFrames( + func(f *SettingsFrame) bool { return true }, + func(f *DataFrame) bool { + gotBytes += int64(len(f.Data())) + return f.StreamEnded() + }, + ) + + if gotBytes != bodySize { + t.Fatalf("server received %v bytes of body, want %v", gotBytes, bodySize) } - ct.run() + + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) + rt.wantStatus(200) } // See golang.org/issue/16556 func TestTransportReturnsDataPaddingFlowControl(t *testing.T) { - ct := newClientTester(t) + tc := newTestClientConn(t) + tc.greet() - unblockClient := make(chan bool, 1) + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if err != nil { - return err - } - defer res.Body.Close() - <-unblockClient - return nil - } - ct.server = func() error { - ct.greet() + tc.wantFrameType(FrameHeaders) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + "content-length", "5000", + ), + }) - var hf *HeadersFrame - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return fmt.Errorf("ReadFrame while waiting for Headers: %v", err) - } - switch f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - continue - } - var ok bool - hf, ok = f.(*HeadersFrame) - if !ok { - return fmt.Errorf("Got %T; want HeadersFrame", f) - } - break - } + initialConnWindow := tc.inflowWindow(0) + initialStreamWindow := tc.inflowWindow(rt.streamID()) - initialConnWindow := ct.inflowWindow(0) + pad := make([]byte, 5) + tc.writeDataPadded(rt.streamID(), false, make([]byte, 5000), pad) - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - enc.WriteField(hpack.HeaderField{Name: "content-length", Value: "5000"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: hf.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) - initialStreamWindow := ct.inflowWindow(hf.StreamID) - pad := make([]byte, 5) - ct.fr.WriteDataPadded(hf.StreamID, false, make([]byte, 5000), pad) // without ending stream - if err := ct.writeReadPing(); err != nil { - return err - } - // Padding flow control should have been returned. - if got, want := ct.inflowWindow(0), initialConnWindow-5000; got != want { - t.Errorf("conn inflow window = %v, want %v", got, want) - } - if got, want := ct.inflowWindow(hf.StreamID), initialStreamWindow-5000; got != want { - t.Errorf("stream inflow window = %v, want %v", got, want) - } - unblockClient <- true - return nil + // Padding flow control should have been returned. + if got, want := tc.inflowWindow(0), initialConnWindow-5000; got != want { + t.Errorf("conn inflow window = %v, want %v", got, want) + } + if got, want := tc.inflowWindow(rt.streamID()), initialStreamWindow-5000; got != want { + t.Errorf("stream inflow window = %v, want %v", got, want) } - ct.run() } // golang.org/issue/16572 -- RoundTrip shouldn't hang when it gets a // StreamError as a result of the response HEADERS func TestTransportReturnsErrorOnBadResponseHeaders(t *testing.T) { - ct := newClientTester(t) + tc := newTestClientConn(t) + tc.greet() - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if err == nil { - res.Body.Close() - return errors.New("unexpected successful GET") - } - want := StreamError{1, ErrCodeProtocol, headerFieldNameError(" content-type")} - if !reflect.DeepEqual(want, err) { - t.Errorf("RoundTrip error = %#v; want %#v", err, want) - } - return nil - } - ct.server = func() error { - ct.greet() + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) - hf, err := ct.firstHeaders() - if err != nil { - return err - } - - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - enc.WriteField(hpack.HeaderField{Name: " content-type", Value: "bogus"}) // bogus spaces - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: hf.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) + tc.wantFrameType(FrameHeaders) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + " content-type", "bogus", + ), + }) - for { - fr, err := ct.readFrame() - if err != nil { - return fmt.Errorf("error waiting for RST_STREAM from client: %v", err) - } - if _, ok := fr.(*SettingsFrame); ok { - continue - } - if rst, ok := fr.(*RSTStreamFrame); !ok || rst.StreamID != 1 || rst.ErrCode != ErrCodeProtocol { - t.Errorf("Frame = %v; want RST_STREAM for stream 1 with ErrCodeProtocol", summarizeFrame(fr)) - } - break - } + err := rt.err() + want := StreamError{1, ErrCodeProtocol, headerFieldNameError(" content-type")} + if !reflect.DeepEqual(err, want) { + t.Fatalf("RoundTrip error = %#v; want %#v", err, want) + } - return nil + fr := testClientConnReadFrame[*RSTStreamFrame](tc) + if fr.StreamID != 1 || fr.ErrCode != ErrCodeProtocol { + t.Errorf("Frame = %v; want RST_STREAM for stream 1 with ErrCodeProtocol", summarizeFrame(fr)) } - ct.run() } // byteAndEOFReader returns is in an io.Reader which reads one byte @@ -3461,261 +3339,84 @@ func TestTransportPingWhenReadingPingDisabled(t *testing.T) { } } -func testTransportPingWhenReading(t *testing.T, readIdleTimeout, deadline time.Duration, expectedPingCount int) { - var pingCount int - ct := newClientTester(t) - ct.tr.ReadIdleTimeout = readIdleTimeout - - ctx, cancel := context.WithTimeout(context.Background(), deadline) - defer cancel() - ct.client = func() error { - defer ct.cc.(*net.TCPConn).CloseWrite() - if runtime.GOOS == "plan9" { - // CloseWrite not supported on Plan 9; Issue 17906 - defer ct.cc.(*net.TCPConn).Close() - } - req, _ := http.NewRequestWithContext(ctx, "GET", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if err != nil { - return fmt.Errorf("RoundTrip: %v", err) - } - defer res.Body.Close() - if res.StatusCode != 200 { - return fmt.Errorf("status code = %v; want %v", res.StatusCode, 200) - } - _, err = ioutil.ReadAll(res.Body) - if expectedPingCount == 0 && errors.Is(ctx.Err(), context.DeadlineExceeded) { - return nil - } +func TestTransportRetryAfterGOAWAY(t *testing.T) { + tt := newTestTransport(t) - cancel() - return err - } + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tt.roundTrip(req) - ct.server = func() error { - ct.greet() - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - var streamID uint32 - for { - f, err := ct.fr.ReadFrame() - if err != nil { - select { - case <-ctx.Done(): - // If the client's done, it - // will have reported any - // errors on its side. - return nil - default: - return err - } - } - switch f := f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - case *HeadersFrame: - if !f.HeadersEnded() { - return fmt.Errorf("headers should have END_HEADERS be ended: %v", f) - } - enc.WriteField(hpack.HeaderField{Name: ":status", Value: strconv.Itoa(200)}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) - streamID = f.StreamID - case *PingFrame: - pingCount++ - if pingCount == expectedPingCount { - if err := ct.fr.WriteData(streamID, true, []byte("hello, this is last server data frame")); err != nil { - return err - } - } - if err := ct.fr.WritePing(true, f.Data); err != nil { - return err - } - case *RSTStreamFrame: - default: - return fmt.Errorf("Unexpected client frame %v", f) - } - } + // First attempt: Server sends a GOAWAY. + tc := tt.getConn() + tc.wantFrameType(FrameSettings) + tc.wantFrameType(FrameWindowUpdate) + tc.wantHeaders(wantHeader{ + streamID: 1, + endStream: true, + }) + tc.writeSettings() + tc.writeGoAway(0 /*max id*/, ErrCodeNo, nil) + if rt.done() { + t.Fatalf("after GOAWAY, RoundTrip is done; want it to be retrying") } - ct.run() -} - -func testClientMultipleDials(t *testing.T, client func(*Transport), server func(int, *clientTester)) { - ln := newLocalListener(t) - defer ln.Close() - var ( - mu sync.Mutex - count int - conns []net.Conn - ) - var wg sync.WaitGroup - tr := &Transport{ - TLSClientConfig: tlsConfigInsecure, - } - tr.DialTLS = func(network, addr string, cfg *tls.Config) (net.Conn, error) { - mu.Lock() - defer mu.Unlock() - count++ - cc, err := net.Dial("tcp", ln.Addr().String()) - if err != nil { - return nil, fmt.Errorf("dial error: %v", err) - } - conns = append(conns, cc) - sc, err := ln.Accept() - if err != nil { - return nil, fmt.Errorf("accept error: %v", err) - } - conns = append(conns, sc) - ct := &clientTester{ - t: t, - tr: tr, - cc: cc, - sc: sc, - fr: NewFramer(sc, sc), - } - wg.Add(1) - go func(count int) { - defer wg.Done() - server(count, ct) - }(count) - return cc, nil - } + // Second attempt succeeds on a new connection. + tc = tt.getConn() + tc.wantFrameType(FrameSettings) + tc.wantFrameType(FrameWindowUpdate) + tc.wantHeaders(wantHeader{ + streamID: 1, + endStream: true, + }) + tc.writeSettings() + tc.writeHeaders(HeadersFrameParam{ + StreamID: 1, + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) - client(tr) - tr.CloseIdleConnections() - ln.Close() - for _, c := range conns { - c.Close() - } - wg.Wait() + rt.wantStatus(200) } -func TestTransportRetryAfterGOAWAY(t *testing.T) { - client := func(tr *Transport) { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - res, err := tr.RoundTrip(req) - if res != nil { - res.Body.Close() - if got := res.Header.Get("Foo"); got != "bar" { - err = fmt.Errorf("foo header = %q; want bar", got) - } - } - if err != nil { - t.Errorf("RoundTrip: %v", err) - } - } - - server := func(count int, ct *clientTester) { - switch count { - case 1: - ct.greet() - hf, err := ct.firstHeaders() - if err != nil { - t.Errorf("server1 failed reading HEADERS: %v", err) - return - } - t.Logf("server1 got %v", hf) - if err := ct.fr.WriteGoAway(0 /*max id*/, ErrCodeNo, nil); err != nil { - t.Errorf("server1 failed writing GOAWAY: %v", err) - return - } - case 2: - ct.greet() - hf, err := ct.firstHeaders() - if err != nil { - t.Errorf("server2 failed reading HEADERS: %v", err) - return - } - t.Logf("server2 got %v", hf) - - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - enc.WriteField(hpack.HeaderField{Name: "foo", Value: "bar"}) - err = ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: hf.StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) - if err != nil { - t.Errorf("server2 failed writing response HEADERS: %v", err) - } - default: - t.Errorf("unexpected number of dials") - return - } - } +func TestTransportRetryAfterRefusedStream(t *testing.T) { + tt := newTestTransport(t) - testClientMultipleDials(t, client, server) -} + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tt.roundTrip(req) -func TestTransportRetryAfterRefusedStream(t *testing.T) { - clientDone := make(chan struct{}) - client := func(tr *Transport) { - defer close(clientDone) - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - resp, err := tr.RoundTrip(req) - if err != nil { - t.Errorf("RoundTrip: %v", err) - return - } - resp.Body.Close() - if resp.StatusCode != 204 { - t.Errorf("Status = %v; want 204", resp.StatusCode) - return - } + // First attempt: Server sends a RST_STREAM. + tc := tt.getConn() + tc.wantFrameType(FrameSettings) + tc.wantFrameType(FrameWindowUpdate) + tc.wantHeaders(wantHeader{ + streamID: 1, + endStream: true, + }) + tc.writeSettings() + tc.wantFrameType(FrameSettings) // settings ACK + tc.writeRSTStream(1, ErrCodeRefusedStream) + if rt.done() { + t.Fatalf("after RST_STREAM, RoundTrip is done; want it to be retrying") } - server := func(_ int, ct *clientTester) { - ct.greet() - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - var count int - for { - f, err := ct.fr.ReadFrame() - if err != nil { - select { - case <-clientDone: - // If the client's done, it - // will have reported any - // errors on its side. - default: - t.Error(err) - } - return - } - switch f := f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - case *HeadersFrame: - if !f.HeadersEnded() { - t.Errorf("headers should have END_HEADERS be ended: %v", f) - return - } - count++ - if count == 1 { - ct.fr.WriteRSTStream(f.StreamID, ErrCodeRefusedStream) - } else { - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "204"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: true, - BlockFragment: buf.Bytes(), - }) - } - default: - t.Errorf("Unexpected client frame %v", f) - return - } - } - } + // Second attempt succeeds on the same connection. + tc.wantHeaders(wantHeader{ + streamID: 3, + endStream: true, + }) + tc.writeSettings() + tc.writeHeaders(HeadersFrameParam{ + StreamID: 3, + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "204", + ), + }) - testClientMultipleDials(t, client, server) + rt.wantStatus(204) } func TestTransportRetryHasLimit(t *testing.T) { @@ -3765,67 +3466,34 @@ func TestTransportRetryHasLimit(t *testing.T) { } func TestTransportResponseDataBeforeHeaders(t *testing.T) { - // This test use not valid response format. - // Discarding logger output to not spam tests output. - log.SetOutput(ioutil.Discard) - defer log.SetOutput(os.Stderr) + // Discard log output complaining about protocol error. + log.SetOutput(io.Discard) + t.Cleanup(func() { log.SetOutput(os.Stderr) }) // after other cleanup is done - ct := newClientTester(t) - ct.client = func() error { - defer ct.cc.(*net.TCPConn).CloseWrite() - if runtime.GOOS == "plan9" { - // CloseWrite not supported on Plan 9; Issue 17906 - defer ct.cc.(*net.TCPConn).Close() - } - req := httptest.NewRequest("GET", "https://dummy.tld/", nil) - // First request is normal to ensure the check is per stream and not per connection. - _, err := ct.tr.RoundTrip(req) - if err != nil { - return fmt.Errorf("RoundTrip expected no error, got: %v", err) - } - // Second request returns a DATA frame with no HEADERS. - resp, err := ct.tr.RoundTrip(req) - if err == nil { - return fmt.Errorf("RoundTrip expected error, got response: %+v", resp) - } - if err, ok := err.(StreamError); !ok || err.Code != ErrCodeProtocol { - return fmt.Errorf("expected stream PROTOCOL_ERROR, got: %v", err) - } - return nil - } - ct.server = func() error { - ct.greet() - for { - f, err := ct.fr.ReadFrame() - if err == io.EOF { - return nil - } else if err != nil { - return err - } - switch f := f.(type) { - case *WindowUpdateFrame, *SettingsFrame, *RSTStreamFrame: - case *HeadersFrame: - switch f.StreamID { - case 1: - // Send a valid response to first request. - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: true, - BlockFragment: buf.Bytes(), - }) - case 3: - ct.fr.WriteData(f.StreamID, true, []byte("payload")) - } - default: - return fmt.Errorf("Unexpected client frame %v", f) - } - } + tc := newTestClientConn(t) + tc.greet() + + // First request is normal to ensure the check is per stream and not per connection. + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt1 := tc.roundTrip(req) + tc.wantFrameType(FrameHeaders) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt1.streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) + rt1.wantStatus(200) + + // Second request returns a DATA frame with no HEADERS. + rt2 := tc.roundTrip(req) + tc.wantFrameType(FrameHeaders) + tc.writeData(rt2.streamID(), true, []byte("payload")) + if err, ok := rt2.err().(StreamError); !ok || err.Code != ErrCodeProtocol { + t.Fatalf("expected stream PROTOCOL_ERROR, got: %v", err) } - ct.run() } func TestTransportMaxFrameReadSize(t *testing.T) { @@ -3839,30 +3507,17 @@ func TestTransportMaxFrameReadSize(t *testing.T) { maxReadFrameSize: 1024, want: minMaxFrameSize, }} { - ct := newClientTester(t) - ct.tr.MaxReadFrameSize = test.maxReadFrameSize - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", http.NoBody) - ct.tr.RoundTrip(req) - return nil - } - ct.server = func() error { - defer ct.cc.(*net.TCPConn).Close() - ct.greet() - var got uint32 - ct.settings.ForeachSetting(func(s Setting) error { - switch s.ID { - case SettingMaxFrameSize: - got = s.Val - } - return nil - }) - if got != test.want { - t.Errorf("Transport.MaxReadFrameSize = %v; server got %v, want %v", test.maxReadFrameSize, got, test.want) - } - return nil + tc := newTestClientConn(t, func(tr *Transport) { + tr.MaxReadFrameSize = test.maxReadFrameSize + }) + + fr := testClientConnReadFrame[*SettingsFrame](tc) + got, ok := fr.Value(SettingMaxFrameSize) + if !ok { + t.Errorf("Transport.MaxReadFrameSize = %v; server got no setting, want %v", test.maxReadFrameSize, test.want) + } else if got != test.want { + t.Errorf("Transport.MaxReadFrameSize = %v; server got %v, want %v", test.maxReadFrameSize, got, test.want) } - ct.run() } } @@ -3902,337 +3557,126 @@ func TestTransportRequestsLowServerLimit(t *testing.T) { t.Errorf("StatusCode = %v; want %v", got, want) } if res != nil && res.Body != nil { - res.Body.Close() - } - } - - if connCount != 1 { - t.Errorf("created %v connections for %v requests, want 1", connCount, reqCount) - } -} - -// tests Transport.StrictMaxConcurrentStreams -func TestTransportRequestsStallAtServerLimit(t *testing.T) { - const maxConcurrent = 2 - - greet := make(chan struct{}) // server sends initial SETTINGS frame - gotRequest := make(chan struct{}) // server received a request - clientDone := make(chan struct{}) - cancelClientRequest := make(chan struct{}) - - // Collect errors from goroutines. - var wg sync.WaitGroup - errs := make(chan error, 100) - defer func() { - wg.Wait() - close(errs) - for err := range errs { - t.Error(err) - } - }() - - // We will send maxConcurrent+2 requests. This checker goroutine waits for the - // following stages: - // 1. The first maxConcurrent requests are received by the server. - // 2. The client will cancel the next request - // 3. The server is unblocked so it can service the first maxConcurrent requests - // 4. The client will send the final request - wg.Add(1) - unblockClient := make(chan struct{}) - clientRequestCancelled := make(chan struct{}) - unblockServer := make(chan struct{}) - go func() { - defer wg.Done() - // Stage 1. - for k := 0; k < maxConcurrent; k++ { - <-gotRequest - } - // Stage 2. - close(unblockClient) - <-clientRequestCancelled - // Stage 3: give some time for the final RoundTrip call to be scheduled and - // verify that the final request is not sent. - time.Sleep(50 * time.Millisecond) - select { - case <-gotRequest: - errs <- errors.New("last request did not stall") - close(unblockServer) - return - default: - } - close(unblockServer) - // Stage 4. - <-gotRequest - }() - - ct := newClientTester(t) - ct.tr.StrictMaxConcurrentStreams = true - ct.client = func() error { - var wg sync.WaitGroup - defer func() { - wg.Wait() - close(clientDone) - ct.cc.(*net.TCPConn).CloseWrite() - if runtime.GOOS == "plan9" { - // CloseWrite not supported on Plan 9; Issue 17906 - ct.cc.(*net.TCPConn).Close() - } - }() - for k := 0; k < maxConcurrent+2; k++ { - wg.Add(1) - go func(k int) { - defer wg.Done() - // Don't send the second request until after receiving SETTINGS from the server - // to avoid a race where we use the default SettingMaxConcurrentStreams, which - // is much larger than maxConcurrent. We have to send the first request before - // waiting because the first request triggers the dial and greet. - if k > 0 { - <-greet - } - // Block until maxConcurrent requests are sent before sending any more. - if k >= maxConcurrent { - <-unblockClient - } - body := newStaticCloseChecker("") - req, _ := http.NewRequest("GET", fmt.Sprintf("https://dummy.tld/%d", k), body) - if k == maxConcurrent { - // This request will be canceled. - req.Cancel = cancelClientRequest - close(cancelClientRequest) - _, err := ct.tr.RoundTrip(req) - close(clientRequestCancelled) - if err == nil { - errs <- fmt.Errorf("RoundTrip(%d) should have failed due to cancel", k) - return - } - } else { - resp, err := ct.tr.RoundTrip(req) - if err != nil { - errs <- fmt.Errorf("RoundTrip(%d): %v", k, err) - return - } - ioutil.ReadAll(resp.Body) - resp.Body.Close() - if resp.StatusCode != 204 { - errs <- fmt.Errorf("Status = %v; want 204", resp.StatusCode) - return - } - } - if err := body.isClosed(); err != nil { - errs <- fmt.Errorf("RoundTrip(%d): %v", k, err) - } - }(k) + res.Body.Close() } - return nil } - ct.server = func() error { - var wg sync.WaitGroup - defer wg.Wait() + if connCount != 1 { + t.Errorf("created %v connections for %v requests, want 1", connCount, reqCount) + } +} - ct.greet(Setting{SettingMaxConcurrentStreams, maxConcurrent}) +// tests Transport.StrictMaxConcurrentStreams +func TestTransportRequestsStallAtServerLimit(t *testing.T) { + const maxConcurrent = 2 - // Server write loop. - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - writeResp := make(chan uint32, maxConcurrent+1) + tc := newTestClientConn(t, func(tr *Transport) { + tr.StrictMaxConcurrentStreams = true + }) + tc.greet(Setting{SettingMaxConcurrentStreams, maxConcurrent}) - wg.Add(1) - go func() { - defer wg.Done() - <-unblockServer - for id := range writeResp { - buf.Reset() - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "204"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: id, - EndHeaders: true, - EndStream: true, - BlockFragment: buf.Bytes(), - }) - } - }() + cancelClientRequest := make(chan struct{}) - // Server read loop. - var nreq int - for { - f, err := ct.fr.ReadFrame() - if err != nil { - select { - case <-clientDone: - // If the client's done, it will have reported any errors on its side. - return nil - default: - return err - } - } - switch f := f.(type) { - case *WindowUpdateFrame: - case *SettingsFrame: - // Wait for the client SETTINGS ack until ending the greet. - close(greet) - case *HeadersFrame: - if !f.HeadersEnded() { - return fmt.Errorf("headers should have END_HEADERS be ended: %v", f) - } - gotRequest <- struct{}{} - nreq++ - writeResp <- f.StreamID - if nreq == maxConcurrent+1 { - close(writeResp) - } - case *DataFrame: - default: - return fmt.Errorf("Unexpected client frame %v", f) + // Start maxConcurrent+2 requests. + // The server does not respond to any of them yet. + var rts []*testRoundTrip + for k := 0; k < maxConcurrent+2; k++ { + req, _ := http.NewRequest("GET", fmt.Sprintf("https://dummy.tld/%d", k), nil) + if k == maxConcurrent { + req.Cancel = cancelClientRequest + } + rt := tc.roundTrip(req) + rts = append(rts, rt) + + if k < maxConcurrent { + // We are under the stream limit, so the client sends the request. + tc.wantHeaders(wantHeader{ + streamID: rt.streamID(), + endStream: true, + header: http.Header{ + ":authority": []string{"dummy.tld"}, + ":method": []string{"GET"}, + ":path": []string{fmt.Sprintf("/%d", k)}, + }, + }) + } else { + // We have reached the stream limit, + // so the client cannot send the request. + if fr := tc.readFrame(); fr != nil { + t.Fatalf("after making new request while at stream limit, got unexpected frame: %v", fr) } } + + if rt.done() { + t.Fatalf("rt %v done", k) + } + } + + // Cancel the maxConcurrent'th request. + // The request should fail. + close(cancelClientRequest) + tc.sync() + if err := rts[maxConcurrent].err(); err == nil { + t.Fatalf("RoundTrip(%d) should have failed due to cancel, did not", maxConcurrent) + } + + // No requests should be complete, except for the canceled one. + for i, rt := range rts { + if i != maxConcurrent && rt.done() { + t.Fatalf("RoundTrip(%d) is done, but should not be", i) + } } - ct.run() + // Server responds to a request, unblocking the last one. + tc.writeHeaders(HeadersFrameParam{ + StreamID: rts[0].streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) + tc.wantHeaders(wantHeader{ + streamID: rts[maxConcurrent+1].streamID(), + endStream: true, + header: http.Header{ + ":authority": []string{"dummy.tld"}, + ":method": []string{"GET"}, + ":path": []string{fmt.Sprintf("/%d", maxConcurrent+1)}, + }, + }) + rts[0].wantStatus(200) } func TestTransportMaxDecoderHeaderTableSize(t *testing.T) { - ct := newClientTester(t) var reqSize, resSize uint32 = 8192, 16384 - ct.tr.MaxDecoderHeaderTableSize = reqSize - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - cc, err := ct.tr.NewClientConn(ct.cc) - if err != nil { - return err - } - _, err = cc.RoundTrip(req) - if err != nil { - return err - } - if got, want := cc.peerMaxHeaderTableSize, resSize; got != want { - return fmt.Errorf("peerHeaderTableSize = %d, want %d", got, want) - } - return nil + tc := newTestClientConn(t, func(tr *Transport) { + tr.MaxDecoderHeaderTableSize = reqSize + }) + + fr := testClientConnReadFrame[*SettingsFrame](tc) + if v, ok := fr.Value(SettingHeaderTableSize); !ok { + t.Fatalf("missing SETTINGS_HEADER_TABLE_SIZE setting") + } else if v != reqSize { + t.Fatalf("received SETTINGS_HEADER_TABLE_SIZE = %d, want %d", v, reqSize) } - ct.server = func() error { - buf := make([]byte, len(ClientPreface)) - _, err := io.ReadFull(ct.sc, buf) - if err != nil { - return fmt.Errorf("reading client preface: %v", err) - } - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } - sf, ok := f.(*SettingsFrame) - if !ok { - ct.t.Fatalf("wanted client settings frame; got %v", f) - _ = sf // stash it away? - } - var found bool - err = sf.ForeachSetting(func(s Setting) error { - if s.ID == SettingHeaderTableSize { - found = true - if got, want := s.Val, reqSize; got != want { - return fmt.Errorf("received SETTINGS_HEADER_TABLE_SIZE = %d, want %d", got, want) - } - } - return nil - }) - if err != nil { - return err - } - if !found { - return fmt.Errorf("missing SETTINGS_HEADER_TABLE_SIZE setting") - } - if err := ct.fr.WriteSettings(Setting{SettingHeaderTableSize, resSize}); err != nil { - ct.t.Fatal(err) - } - if err := ct.fr.WriteSettingsAck(); err != nil { - ct.t.Fatal(err) - } - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } - switch f := f.(type) { - case *HeadersFrame: - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: true, - BlockFragment: buf.Bytes(), - }) - return nil - } - } + tc.writeSettings(Setting{SettingHeaderTableSize, resSize}) + if got, want := tc.cc.peerMaxHeaderTableSize, resSize; got != want { + t.Fatalf("peerHeaderTableSize = %d, want %d", got, want) } - ct.run() } func TestTransportMaxEncoderHeaderTableSize(t *testing.T) { - ct := newClientTester(t) var peerAdvertisedMaxHeaderTableSize uint32 = 16384 - ct.tr.MaxEncoderHeaderTableSize = 8192 - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - cc, err := ct.tr.NewClientConn(ct.cc) - if err != nil { - return err - } - _, err = cc.RoundTrip(req) - if err != nil { - return err - } - if got, want := cc.henc.MaxDynamicTableSize(), ct.tr.MaxEncoderHeaderTableSize; got != want { - return fmt.Errorf("henc.MaxDynamicTableSize() = %d, want %d", got, want) - } - return nil - } - ct.server = func() error { - buf := make([]byte, len(ClientPreface)) - _, err := io.ReadFull(ct.sc, buf) - if err != nil { - return fmt.Errorf("reading client preface: %v", err) - } - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } - sf, ok := f.(*SettingsFrame) - if !ok { - ct.t.Fatalf("wanted client settings frame; got %v", f) - _ = sf // stash it away? - } - if err := ct.fr.WriteSettings(Setting{SettingHeaderTableSize, peerAdvertisedMaxHeaderTableSize}); err != nil { - ct.t.Fatal(err) - } - if err := ct.fr.WriteSettingsAck(); err != nil { - ct.t.Fatal(err) - } + tc := newTestClientConn(t, func(tr *Transport) { + tr.MaxEncoderHeaderTableSize = 8192 + }) + tc.greet(Setting{SettingHeaderTableSize, peerAdvertisedMaxHeaderTableSize}) - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } - switch f := f.(type) { - case *HeadersFrame: - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: true, - BlockFragment: buf.Bytes(), - }) - return nil - } - } + if got, want := tc.cc.henc.MaxDynamicTableSize(), tc.tr.MaxEncoderHeaderTableSize; got != want { + t.Fatalf("henc.MaxDynamicTableSize() = %d, want %d", got, want) } - ct.run() } func TestAuthorityAddr(t *testing.T) { @@ -4316,40 +3760,24 @@ func TestTransportAllocationsAfterResponseBodyClose(t *testing.T) { // Issue 18891: make sure Request.Body == NoBody means no DATA frame // is ever sent, even if empty. func TestTransportNoBodyMeansNoDATA(t *testing.T) { - ct := newClientTester(t) - - unblockClient := make(chan bool) + tc := newTestClientConn(t) + tc.greet() - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", http.NoBody) - ct.tr.RoundTrip(req) - <-unblockClient - return nil - } - ct.server = func() error { - defer close(unblockClient) - defer ct.cc.(*net.TCPConn).Close() - ct.greet() + req, _ := http.NewRequest("GET", "https://dummy.tld/", http.NoBody) + rt := tc.roundTrip(req) - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return fmt.Errorf("ReadFrame while waiting for Headers: %v", err) - } - switch f := f.(type) { - default: - return fmt.Errorf("Got %T; want HeadersFrame", f) - case *WindowUpdateFrame, *SettingsFrame: - continue - case *HeadersFrame: - if !f.StreamEnded() { - return fmt.Errorf("got headers frame without END_STREAM") - } - return nil - } - } + tc.wantHeaders(wantHeader{ + streamID: rt.streamID(), + endStream: true, // END_STREAM should be set when body is http.NoBody + header: http.Header{ + ":authority": []string{"dummy.tld"}, + ":method": []string{"GET"}, + ":path": []string{"/"}, + }, + }) + if fr := tc.readFrame(); fr != nil { + t.Fatalf("unexpected frame after headers: %v", fr) } - ct.run() } func benchSimpleRoundTrip(b *testing.B, nReqHeaders, nResHeader int) { @@ -4428,41 +3856,22 @@ func TestTransportResponseAndResetWithoutConsumingBodyRace(t *testing.T) { // Verify transport doesn't crash when receiving bogus response lacking a :status header. // Issue 22880. func TestTransportHandlesInvalidStatuslessResponse(t *testing.T) { - ct := newClientTester(t) - ct.client = func() error { - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - _, err := ct.tr.RoundTrip(req) - const substr = "malformed response from server: missing status pseudo header" - if !strings.Contains(fmt.Sprint(err), substr) { - return fmt.Errorf("RoundTrip error = %v; want substring %q", err, substr) - } - return nil - } - ct.server = func() error { - ct.greet() - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) + tc := newTestClientConn(t) + tc.greet() - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } - switch f := f.(type) { - case *HeadersFrame: - enc.WriteField(hpack.HeaderField{Name: "content-type", Value: "text/html"}) // no :status header - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.StreamID, - EndHeaders: true, - EndStream: false, // we'll send some DATA to try to crash the transport - BlockFragment: buf.Bytes(), - }) - ct.fr.WriteData(f.StreamID, true, []byte("payload")) - return nil - } - } - } - ct.run() + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) + + tc.wantFrameType(FrameHeaders) + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: false, // we'll send some DATA to try to crash the transport + BlockFragment: tc.makeHeaderBlockFragment( + "content-type", "text/html", // no :status header + ), + }) + tc.writeData(rt.streamID(), true, []byte("payload")) } func BenchmarkClientRequestHeaders(b *testing.B) { @@ -4810,95 +4219,42 @@ func (r *errReader) Read(p []byte) (int, error) { } func testTransportBodyReadError(t *testing.T, body []byte) { - if runtime.GOOS == "windows" || runtime.GOOS == "plan9" { - // So far we've only seen this be flaky on Windows and Plan 9, - // perhaps due to TCP behavior on shutdowns while - // unread data is in flight. This test should be - // fixed, but a skip is better than annoying people - // for now. - t.Skipf("skipping flaky test on %s; https://golang.org/issue/31260", runtime.GOOS) - } - clientDone := make(chan struct{}) - ct := newClientTester(t) - ct.client = func() error { - defer ct.cc.(*net.TCPConn).CloseWrite() - if runtime.GOOS == "plan9" { - // CloseWrite not supported on Plan 9; Issue 17906 - defer ct.cc.(*net.TCPConn).Close() - } - defer close(clientDone) - - checkNoStreams := func() error { - cp, ok := ct.tr.connPool().(*clientConnPool) - if !ok { - return fmt.Errorf("conn pool is %T; want *clientConnPool", ct.tr.connPool()) - } - cp.mu.Lock() - defer cp.mu.Unlock() - conns, ok := cp.conns["dummy.tld:443"] - if !ok { - return fmt.Errorf("missing connection") - } - if len(conns) != 1 { - return fmt.Errorf("conn pool size: %v; expect 1", len(conns)) - } - if activeStreams(conns[0]) != 0 { - return fmt.Errorf("active streams count: %v; want 0", activeStreams(conns[0])) - } - return nil - } - bodyReadError := errors.New("body read error") - body := &errReader{body, bodyReadError} - req, err := http.NewRequest("PUT", "https://dummy.tld/", body) - if err != nil { - return err - } - _, err = ct.tr.RoundTrip(req) - if err != bodyReadError { - return fmt.Errorf("err = %v; want %v", err, bodyReadError) - } - if err = checkNoStreams(); err != nil { - return err + tc := newTestClientConn(t) + tc.greet() + + bodyReadError := errors.New("body read error") + b := tc.newRequestBody() + b.Write(body) + b.closeWithError(bodyReadError) + req, _ := http.NewRequest("PUT", "https://dummy.tld/", b) + rt := tc.roundTrip(req) + + tc.wantFrameType(FrameHeaders) + var receivedBody []byte +readFrames: + for { + switch f := tc.readFrame().(type) { + case *DataFrame: + receivedBody = append(receivedBody, f.Data()...) + case *RSTStreamFrame: + break readFrames + default: + t.Fatalf("unexpected frame: %v", f) + case nil: + t.Fatalf("transport is idle, want RST_STREAM") } - return nil } - ct.server = func() error { - ct.greet() - var receivedBody []byte - var resetCount int - for { - f, err := ct.fr.ReadFrame() - t.Logf("server: ReadFrame = %v, %v", f, err) - if err != nil { - select { - case <-clientDone: - // If the client's done, it - // will have reported any - // errors on its side. - if bytes.Compare(receivedBody, body) != 0 { - return fmt.Errorf("body: %q; expected %q", receivedBody, body) - } - if resetCount != 1 { - return fmt.Errorf("stream reset count: %v; expected: 1", resetCount) - } - return nil - default: - return err - } - } - switch f := f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - case *HeadersFrame: - case *DataFrame: - receivedBody = append(receivedBody, f.Data()...) - case *RSTStreamFrame: - resetCount++ - default: - return fmt.Errorf("Unexpected client frame %v", f) - } - } + if !bytes.Equal(receivedBody, body) { + t.Fatalf("body: %q; expected %q", receivedBody, body) + } + + if err := rt.err(); err != bodyReadError { + t.Fatalf("err = %v; want %v", err, bodyReadError) + } + + if got := activeStreams(tc.cc); got != 0 { + t.Fatalf("active streams count: %v; want 0", got) } - ct.run() } func TestTransportBodyReadError_Immediately(t *testing.T) { testTransportBodyReadError(t, nil) } @@ -4911,59 +4267,18 @@ func TestTransportBodyEagerEndStream(t *testing.T) { const reqBody = "some request body" const resBody = "some response body" - ct := newClientTester(t) - ct.client = func() error { - defer ct.cc.(*net.TCPConn).CloseWrite() - if runtime.GOOS == "plan9" { - // CloseWrite not supported on Plan 9; Issue 17906 - defer ct.cc.(*net.TCPConn).Close() - } - body := strings.NewReader(reqBody) - req, err := http.NewRequest("PUT", "https://dummy.tld/", body) - if err != nil { - return err - } - _, err = ct.tr.RoundTrip(req) - if err != nil { - return err - } - return nil - } - ct.server = func() error { - ct.greet() + tc := newTestClientConn(t) + tc.greet() - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return err - } + body := strings.NewReader(reqBody) + req, _ := http.NewRequest("PUT", "https://dummy.tld/", body) + tc.roundTrip(req) - switch f := f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - case *HeadersFrame: - case *DataFrame: - if !f.StreamEnded() { - ct.fr.WriteRSTStream(f.StreamID, ErrCodeRefusedStream) - return fmt.Errorf("data frame without END_STREAM %v", f) - } - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: f.Header().StreamID, - EndHeaders: true, - EndStream: false, - BlockFragment: buf.Bytes(), - }) - ct.fr.WriteData(f.StreamID, true, []byte(resBody)) - return nil - case *RSTStreamFrame: - default: - return fmt.Errorf("Unexpected client frame %v", f) - } - } + tc.wantFrameType(FrameHeaders) + f := testClientConnReadFrame[*DataFrame](tc) + if !f.StreamEnded() { + t.Fatalf("data frame without END_STREAM %v", f) } - ct.run() } type chunkReader struct { @@ -5737,39 +5052,27 @@ func TestClientConnReservations(t *testing.T) { } func TestTransportTimeoutServerHangs(t *testing.T) { - clientDone := make(chan struct{}) - ct := newClientTester(t) - ct.client = func() error { - defer ct.cc.(*net.TCPConn).CloseWrite() - defer close(clientDone) + tc := newTestClientConn(t) + tc.greet() - req, err := http.NewRequest("PUT", "https://dummy.tld/", nil) - if err != nil { - return err - } + ctx, cancel := context.WithCancel(context.Background()) + req, _ := http.NewRequestWithContext(ctx, "PUT", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - req = req.WithContext(ctx) - req.Header.Add("Big", strings.Repeat("a", 1<<20)) - _, err = ct.tr.RoundTrip(req) - if err == nil { - return errors.New("error should not be nil") - } - if ne, ok := err.(net.Error); !ok || !ne.Timeout() { - return fmt.Errorf("error should be a net error timeout: %v", err) - } - return nil + tc.wantFrameType(FrameHeaders) + tc.advance(5 * time.Second) + if f := tc.readFrame(); f != nil { + t.Fatalf("unexpected frame: %v", f) } - ct.server = func() error { - ct.greet() - select { - case <-time.After(5 * time.Second): - case <-clientDone: - } - return nil + if rt.done() { + t.Fatalf("after 5 seconds with no response, RoundTrip unexpectedly returned") + } + + cancel() + tc.sync() + if rt.err() != context.Canceled { + t.Fatalf("RoundTrip error: %v; want context.Canceled", rt.err()) } - ct.run() } func TestTransportContentLengthWithoutBody(t *testing.T) { @@ -5962,20 +5265,6 @@ func TestTransportClosesConnAfterGoAwayLastStream(t *testing.T) { testTransportClosesConnAfterGoAway(t, 1) } -type closeOnceConn struct { - net.Conn - closed uint32 -} - -var errClosed = errors.New("Close of closed connection") - -func (c *closeOnceConn) Close() error { - if atomic.CompareAndSwapUint32(&c.closed, 0, 1) { - return c.Conn.Close() - } - return errClosed -} - // testTransportClosesConnAfterGoAway verifies that the transport // closes a connection after reading a GOAWAY from it. // @@ -5983,53 +5272,35 @@ func (c *closeOnceConn) Close() error { // When 0, the transport (unsuccessfully) retries the request (stream 1); // when 1, the transport reads the response after receiving the GOAWAY. func testTransportClosesConnAfterGoAway(t *testing.T, lastStream uint32) { - ct := newClientTester(t) - ct.cc = &closeOnceConn{Conn: ct.cc} + tc := newTestClientConn(t) + tc.greet() - var wg sync.WaitGroup - wg.Add(1) - ct.client = func() error { - defer wg.Done() - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) - res, err := ct.tr.RoundTrip(req) - if err == nil { - res.Body.Close() - } - if gotErr, wantErr := err != nil, lastStream == 0; gotErr != wantErr { - t.Errorf("RoundTrip got error %v (want error: %v)", err, wantErr) - } - if err = ct.cc.Close(); err != errClosed { - return fmt.Errorf("ct.cc.Close() = %v, want errClosed", err) - } - return nil - } + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tc.roundTrip(req) - ct.server = func() error { - defer wg.Wait() - ct.greet() - hf, err := ct.firstHeaders() - if err != nil { - return fmt.Errorf("server failed reading HEADERS: %v", err) - } - if err := ct.fr.WriteGoAway(lastStream, ErrCodeNo, nil); err != nil { - return fmt.Errorf("server failed writing GOAWAY: %v", err) - } - if lastStream > 0 { - // Send a valid response to first request. - var buf bytes.Buffer - enc := hpack.NewEncoder(&buf) - enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"}) - ct.fr.WriteHeaders(HeadersFrameParam{ - StreamID: hf.StreamID, - EndHeaders: true, - EndStream: true, - BlockFragment: buf.Bytes(), - }) - } - return nil + tc.wantFrameType(FrameHeaders) + tc.writeGoAway(lastStream, ErrCodeNo, nil) + + if lastStream > 0 { + // Send a valid response to first request. + tc.writeHeaders(HeadersFrameParam{ + StreamID: rt.streamID(), + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) } - ct.run() + tc.closeWrite(io.EOF) + err := rt.err() + if gotErr, wantErr := err != nil, lastStream == 0; gotErr != wantErr { + t.Errorf("RoundTrip got error %v (want error: %v)", err, wantErr) + } + if !tc.netConnClosed { + t.Errorf("ClientConn did not close its net.Conn, expected it to") + } } type slowCloser struct { From 448c44f9287b6745f958d74aa2a17ec7761c2f13 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 19 Mar 2024 10:37:19 -0700 Subject: [PATCH 163/168] http2: remove clientTester All tests which use clientTester have been converted to use testClientConn, so delete clientTester. Change-Id: Id9a88bf7ee6760fada8442d383d5e68455c6dc3e Reviewed-on: https://go-review.googlesource.com/c/net/+/572815 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- http2/transport_test.go | 195 ---------------------------------------- 1 file changed, 195 deletions(-) diff --git a/http2/transport_test.go b/http2/transport_test.go index 855c107efb..11ff67b4c8 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -822,53 +822,6 @@ func (fw flushWriter) Write(p []byte) (n int, err error) { return } -type clientTester struct { - t *testing.T - tr *Transport - sc, cc net.Conn // server and client conn - fr *Framer // server's framer - settings *SettingsFrame - client func() error - server func() error -} - -func newClientTester(t *testing.T) *clientTester { - var dialOnce struct { - sync.Mutex - dialed bool - } - ct := &clientTester{ - t: t, - } - ct.tr = &Transport{ - TLSClientConfig: tlsConfigInsecure, - DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { - dialOnce.Lock() - defer dialOnce.Unlock() - if dialOnce.dialed { - return nil, errors.New("only one dial allowed in test mode") - } - dialOnce.dialed = true - return ct.cc, nil - }, - } - - ln := newLocalListener(t) - cc, err := net.Dial("tcp", ln.Addr().String()) - if err != nil { - t.Fatal(err) - } - sc, err := ln.Accept() - if err != nil { - t.Fatal(err) - } - ln.Close() - ct.cc = cc - ct.sc = sc - ct.fr = NewFramer(sc, sc) - return ct -} - func newLocalListener(t *testing.T) net.Listener { ln, err := net.Listen("tcp4", "127.0.0.1:0") if err == nil { @@ -881,154 +834,6 @@ func newLocalListener(t *testing.T) net.Listener { return ln } -func (ct *clientTester) greet(settings ...Setting) { - buf := make([]byte, len(ClientPreface)) - _, err := io.ReadFull(ct.sc, buf) - if err != nil { - ct.t.Fatalf("reading client preface: %v", err) - } - f, err := ct.fr.ReadFrame() - if err != nil { - ct.t.Fatalf("Reading client settings frame: %v", err) - } - var ok bool - if ct.settings, ok = f.(*SettingsFrame); !ok { - ct.t.Fatalf("Wanted client settings frame; got %v", f) - } - if err := ct.fr.WriteSettings(settings...); err != nil { - ct.t.Fatal(err) - } - if err := ct.fr.WriteSettingsAck(); err != nil { - ct.t.Fatal(err) - } -} - -func (ct *clientTester) readNonSettingsFrame() (Frame, error) { - for { - f, err := ct.fr.ReadFrame() - if err != nil { - return nil, err - } - if _, ok := f.(*SettingsFrame); ok { - continue - } - return f, nil - } -} - -// writeReadPing sends a PING and immediately reads the PING ACK. -// It will fail if any other unread data was pending on the connection, -// aside from SETTINGS frames. -func (ct *clientTester) writeReadPing() error { - data := [8]byte{1, 2, 3, 4, 5, 6, 7, 8} - if err := ct.fr.WritePing(false, data); err != nil { - return fmt.Errorf("Error writing PING: %v", err) - } - f, err := ct.readNonSettingsFrame() - if err != nil { - return err - } - p, ok := f.(*PingFrame) - if !ok { - return fmt.Errorf("got a %v, want a PING ACK", f) - } - if p.Flags&FlagPingAck == 0 { - return fmt.Errorf("got a PING, want a PING ACK") - } - if p.Data != data { - return fmt.Errorf("got PING data = %x, want %x", p.Data, data) - } - return nil -} - -func (ct *clientTester) inflowWindow(streamID uint32) int32 { - pool := ct.tr.connPoolOrDef.(*clientConnPool) - pool.mu.Lock() - defer pool.mu.Unlock() - if n := len(pool.keys); n != 1 { - ct.t.Errorf("clientConnPool contains %v keys, expected 1", n) - return -1 - } - for cc := range pool.keys { - cc.mu.Lock() - defer cc.mu.Unlock() - if streamID == 0 { - return cc.inflow.avail + cc.inflow.unsent - } - cs := cc.streams[streamID] - if cs == nil { - ct.t.Errorf("no stream with id %v", streamID) - return -1 - } - return cs.inflow.avail + cs.inflow.unsent - } - return -1 -} - -func (ct *clientTester) cleanup() { - ct.tr.CloseIdleConnections() - - // close both connections, ignore the error if its already closed - ct.sc.Close() - ct.cc.Close() -} - -func (ct *clientTester) run() { - var errOnce sync.Once - var wg sync.WaitGroup - - run := func(which string, fn func() error) { - defer wg.Done() - if err := fn(); err != nil { - errOnce.Do(func() { - ct.t.Errorf("%s: %v", which, err) - ct.cleanup() - }) - } - } - - wg.Add(2) - go run("client", ct.client) - go run("server", ct.server) - wg.Wait() - - errOnce.Do(ct.cleanup) // clean up if no error -} - -func (ct *clientTester) readFrame() (Frame, error) { - return ct.fr.ReadFrame() -} - -func (ct *clientTester) firstHeaders() (*HeadersFrame, error) { - for { - f, err := ct.readFrame() - if err != nil { - return nil, fmt.Errorf("ReadFrame while waiting for Headers: %v", err) - } - switch f.(type) { - case *WindowUpdateFrame, *SettingsFrame: - continue - } - hf, ok := f.(*HeadersFrame) - if !ok { - return nil, fmt.Errorf("Got %T; want HeadersFrame", f) - } - return hf, nil - } -} - -type countingReader struct { - n *int64 -} - -func (r countingReader) Read(p []byte) (n int, err error) { - for i := range p { - p[i] = byte(i) - } - atomic.AddInt64(r.n, int64(len(p))) - return len(p), err -} - func TestTransportReqBodyAfterResponse_200(t *testing.T) { testTransportReqBodyAfterResponse(t, 200) } func TestTransportReqBodyAfterResponse_403(t *testing.T) { testTransportReqBodyAfterResponse(t, 403) } From 3678185f8a652e52864c44049a9ea96b7bcc066a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 19 Mar 2024 13:42:48 -0700 Subject: [PATCH 164/168] http2: make TestCanonicalHeaderCacheGrowth faster Lower the number of iterations that this test runs for. Reduces runtime with -race from 27s on my M1 Mac to 0.06s. Change-Id: Ibd4b225277c79d9030c0a21b3077173a787cc4c1 Reviewed-on: https://go-review.googlesource.com/c/net/+/572656 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI --- http2/server_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/http2/server_test.go b/http2/server_test.go index 1fdd191ef7..afccd9ecd4 100644 --- a/http2/server_test.go +++ b/http2/server_test.go @@ -4578,13 +4578,16 @@ func TestCanonicalHeaderCacheGrowth(t *testing.T) { sc := &serverConn{ serveG: newGoroutineLock(), } - const count = 1000 - for i := 0; i < count; i++ { - h := fmt.Sprintf("%v-%v", base, i) + count := 0 + added := 0 + for added < 10*maxCachedCanonicalHeadersKeysSize { + h := fmt.Sprintf("%v-%v", base, count) c := sc.canonicalHeader(h) if len(h) != len(c) { t.Errorf("sc.canonicalHeader(%q) = %q, want same length", h, c) } + count++ + added += len(h) } total := 0 for k, v := range sc.canonHeader { From ebc8168ac8ac742194df729305175940790c55a2 Mon Sep 17 00:00:00 2001 From: vitalmotif Date: Wed, 20 Mar 2024 09:32:28 +0000 Subject: [PATCH 165/168] all: fix some typos Change-Id: I7e2c867efcc960553da77e395b0069ab6776cd9f GitHub-Last-Rev: eaa122d1b6086c22f329227053411b0a73a5215b GitHub-Pull-Request: golang/net#205 Reviewed-on: https://go-review.googlesource.com/c/net/+/572995 Reviewed-by: Emmanuel Odeke Reviewed-by: David Chase Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Damien Neil --- dns/dnsmessage/message_test.go | 2 +- quic/rangeset.go | 2 +- quic/retry_test.go | 2 +- quic/stream_limits_test.go | 2 +- quic/version_test.go | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index e60ec42d90..2555305980 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go @@ -1635,7 +1635,7 @@ func FuzzUnpackPack(f *testing.F) { msgPacked, err := m.Pack() if err != nil { - t.Fatalf("failed to pack message that was succesfully unpacked: %v", err) + t.Fatalf("failed to pack message that was successfully unpacked: %v", err) } var m2 Message diff --git a/quic/rangeset.go b/quic/rangeset.go index 4966a99d2c..b8b2e93672 100644 --- a/quic/rangeset.go +++ b/quic/rangeset.go @@ -50,7 +50,7 @@ func (s *rangeset[T]) add(start, end T) { if end <= r.end { return } - // Possibly coalesce subsquent ranges into range i. + // Possibly coalesce subsequent ranges into range i. r.end = end j := i + 1 for ; j < len(*s) && r.end >= (*s)[j].start; j++ { diff --git a/quic/retry_test.go b/quic/retry_test.go index 42f2bdd4a5..c898ad331d 100644 --- a/quic/retry_test.go +++ b/quic/retry_test.go @@ -521,7 +521,7 @@ func TestParseInvalidRetryPackets(t *testing.T) { }} { t.Run(test.name, func(t *testing.T) { if _, ok := parseRetryPacket(test.pkt, originalDstConnID); ok { - t.Errorf("parseRetryPacket succeded, want failure") + t.Errorf("parseRetryPacket succeeded, want failure") } }) } diff --git a/quic/stream_limits_test.go b/quic/stream_limits_test.go index 9c2f71ec1e..8fed825d74 100644 --- a/quic/stream_limits_test.go +++ b/quic/stream_limits_test.go @@ -249,7 +249,7 @@ func TestStreamLimitStopSendingDoesNotUpdateMaxStreams(t *testing.T) { tc.writeFrames(packetType1RTT, debugFrameStopSending{ id: s.id, }) - tc.wantFrame("recieved STOP_SENDING, send RESET_STREAM", + tc.wantFrame("received STOP_SENDING, send RESET_STREAM", packetType1RTT, debugFrameResetStream{ id: s.id, }) diff --git a/quic/version_test.go b/quic/version_test.go index 92fabd7b3d..0bd8bac14b 100644 --- a/quic/version_test.go +++ b/quic/version_test.go @@ -39,10 +39,10 @@ func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { }) gotPkt := te.read() if gotPkt == nil { - t.Fatalf("got no response; want Version Negotiaion") + t.Fatalf("got no response; want Version Negotiation") } if got := getPacketType(gotPkt); got != packetTypeVersionNegotiation { - t.Fatalf("got packet type %v; want Version Negotiaion", got) + t.Fatalf("got packet type %v; want Version Negotiation", got) } gotDst, gotSrc, versions := parseVersionNegotiation(gotPkt) if got, want := gotDst, srcConnID; !bytes.Equal(got, want) { From ba872109ef2dc8f1da778651bd1fd3792d0e4587 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 10 Jan 2024 13:41:39 -0800 Subject: [PATCH 166/168] http2: close connections when receiving too many headers Maintaining HPACK state requires that we parse and process all HEADERS and CONTINUATION frames on a connection. When a request's headers exceed MaxHeaderBytes, we don't allocate memory to store the excess headers but we do parse them. This permits an attacker to cause an HTTP/2 endpoint to read arbitrary amounts of data, all associated with a request which is going to be rejected. Set a limit on the amount of excess header frames we will process before closing a connection. Thanks to Bartek Nowotarski for reporting this issue. Fixes CVE-2023-45288 Fixes golang/go#65051 Change-Id: I15df097268df13bb5a9e9d3a5c04a8a141d850f6 Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/2130527 Reviewed-by: Roland Shoemaker Reviewed-by: Tatiana Bradley Reviewed-on: https://go-review.googlesource.com/c/net/+/576155 Reviewed-by: Dmitri Shuralyov Auto-Submit: Dmitri Shuralyov Reviewed-by: Than McIntosh LUCI-TryBot-Result: Go LUCI --- http2/frame.go | 31 ++++++++++++++++ http2/server_test.go | 84 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/http2/frame.go b/http2/frame.go index e2b298d859..a5a94411da 100644 --- a/http2/frame.go +++ b/http2/frame.go @@ -1564,6 +1564,7 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) { if size > remainSize { hdec.SetEmitEnabled(false) mh.Truncated = true + remainSize = 0 return } remainSize -= size @@ -1576,6 +1577,36 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) { var hc headersOrContinuation = hf for { frag := hc.HeaderBlockFragment() + + // Avoid parsing large amounts of headers that we will then discard. + // If the sender exceeds the max header list size by too much, + // skip parsing the fragment and close the connection. + // + // "Too much" is either any CONTINUATION frame after we've already + // exceeded the max header list size (in which case remainSize is 0), + // or a frame whose encoded size is more than twice the remaining + // header list bytes we're willing to accept. + if int64(len(frag)) > int64(2*remainSize) { + if VerboseLogs { + log.Printf("http2: header list too large") + } + // It would be nice to send a RST_STREAM before sending the GOAWAY, + // but the struture of the server's frame writer makes this difficult. + return nil, ConnectionError(ErrCodeProtocol) + } + + // Also close the connection after any CONTINUATION frame following an + // invalid header, since we stop tracking the size of the headers after + // an invalid one. + if invalid != nil { + if VerboseLogs { + log.Printf("http2: invalid header: %v", invalid) + } + // It would be nice to send a RST_STREAM before sending the GOAWAY, + // but the struture of the server's frame writer makes this difficult. + return nil, ConnectionError(ErrCodeProtocol) + } + if _, err := hdec.Write(frag); err != nil { return nil, ConnectionError(ErrCodeCompression) } diff --git a/http2/server_test.go b/http2/server_test.go index afccd9ecd4..d400990d28 100644 --- a/http2/server_test.go +++ b/http2/server_test.go @@ -4786,3 +4786,87 @@ Frames: close(s) } } + +func TestServerContinuationFlood(t *testing.T) { + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { + fmt.Println(r.Header) + }, func(ts *httptest.Server) { + ts.Config.MaxHeaderBytes = 4096 + }) + defer st.Close() + + st.writePreface() + st.writeInitialSettings() + st.writeSettingsAck() + + st.writeHeaders(HeadersFrameParam{ + StreamID: 1, + BlockFragment: st.encodeHeader(), + EndStream: true, + }) + for i := 0; i < 1000; i++ { + st.fr.WriteContinuation(1, false, st.encodeHeaderRaw( + fmt.Sprintf("x-%v", i), "1234567890", + )) + } + st.fr.WriteContinuation(1, true, st.encodeHeaderRaw( + "x-last-header", "1", + )) + + var sawGoAway bool + for { + f, err := st.readFrame() + if err != nil { + break + } + switch f.(type) { + case *GoAwayFrame: + sawGoAway = true + case *HeadersFrame: + t.Fatalf("received HEADERS frame; want GOAWAY") + } + } + if !sawGoAway { + t.Errorf("connection closed with no GOAWAY frame; want one") + } +} + +func TestServerContinuationAfterInvalidHeader(t *testing.T) { + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { + fmt.Println(r.Header) + }) + defer st.Close() + + st.writePreface() + st.writeInitialSettings() + st.writeSettingsAck() + + st.writeHeaders(HeadersFrameParam{ + StreamID: 1, + BlockFragment: st.encodeHeader(), + EndStream: true, + }) + st.fr.WriteContinuation(1, false, st.encodeHeaderRaw( + "x-invalid-header", "\x00", + )) + st.fr.WriteContinuation(1, true, st.encodeHeaderRaw( + "x-valid-header", "1", + )) + + var sawGoAway bool + for { + f, err := st.readFrame() + if err != nil { + break + } + switch f.(type) { + case *GoAwayFrame: + sawGoAway = true + case *HeadersFrame: + t.Fatalf("received HEADERS frame; want GOAWAY") + } + } + if !sawGoAway { + t.Errorf("connection closed with no GOAWAY frame; want one") + } +} From 762b58d1cf6e0779780decad89c6c1523386638d Mon Sep 17 00:00:00 2001 From: Ian Lance Taylor Date: Wed, 3 Apr 2024 09:32:37 -0700 Subject: [PATCH 167/168] http2: fix tipos in comment Change-Id: I20cd0f8db534fe2a849306eb7e0c8ee5b434e88f Reviewed-on: https://go-review.googlesource.com/c/net/+/576175 Auto-Submit: Ian Lance Taylor Reviewed-by: Ian Lance Taylor LUCI-TryBot-Result: Go LUCI Reviewed-by: Damien Neil --- http2/frame.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http2/frame.go b/http2/frame.go index a5a94411da..43557ab7e9 100644 --- a/http2/frame.go +++ b/http2/frame.go @@ -1591,7 +1591,7 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) { log.Printf("http2: header list too large") } // It would be nice to send a RST_STREAM before sending the GOAWAY, - // but the struture of the server's frame writer makes this difficult. + // but the structure of the server's frame writer makes this difficult. return nil, ConnectionError(ErrCodeProtocol) } @@ -1603,7 +1603,7 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) { log.Printf("http2: invalid header: %v", invalid) } // It would be nice to send a RST_STREAM before sending the GOAWAY, - // but the struture of the server's frame writer makes this difficult. + // but the structure of the server's frame writer makes this difficult. return nil, ConnectionError(ErrCodeProtocol) } From c48da131589f122489348be5dfbcb6457640046f Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 3 Apr 2024 10:17:18 -0700 Subject: [PATCH 168/168] http2: fix TestServerContinuationFlood flakes This test causes the server to send a GOAWAY and close a connection. The server GOAWAY path writes a GOAWAY frame asynchronously, and closes the connection if the write doesn't complete within 1s. This is causing failures on some builders, when the frame write doesn't complete in time. The important aspect of this test is that the connection be closed. Drop the check for the GOAWAY frame. Change-Id: I099413be9c4dfe71d8fe83d2c6242e82e282293e Reviewed-on: https://go-review.googlesource.com/c/net/+/576235 Reviewed-by: Dmitri Shuralyov Reviewed-by: Dmitri Shuralyov Reviewed-by: Than McIntosh LUCI-TryBot-Result: Go LUCI --- http2/server_test.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/http2/server_test.go b/http2/server_test.go index d400990d28..a931a06e57 100644 --- a/http2/server_test.go +++ b/http2/server_test.go @@ -4813,22 +4813,24 @@ func TestServerContinuationFlood(t *testing.T) { "x-last-header", "1", )) - var sawGoAway bool for { f, err := st.readFrame() if err != nil { break } switch f.(type) { - case *GoAwayFrame: - sawGoAway = true case *HeadersFrame: - t.Fatalf("received HEADERS frame; want GOAWAY") + t.Fatalf("received HEADERS frame; want GOAWAY and a closed connection") } } - if !sawGoAway { - t.Errorf("connection closed with no GOAWAY frame; want one") - } + // We expect to have seen a GOAWAY before the connection closes, + // but the server will close the connection after one second + // whether or not it has finished sending the GOAWAY. On windows-amd64-race + // builders, this fairly consistently results in the connection closing without + // the GOAWAY being sent. + // + // Since the server's behavior is inherently racy here and the important thing + // is that the connection is closed, don't check for the GOAWAY having been sent. } func TestServerContinuationAfterInvalidHeader(t *testing.T) {