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 01/76] 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 b9632380e..b20c74917 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 d3156208c..99848485b 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 02/76] 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 1577d4a19..37da3de4d 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 03/76] 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 ce2716e42..64c6db86d 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 04/76] 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 a9ebbe4b7..55d32f310 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 b33d8b00f..31f69e47d 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 05/76] 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 000000000..ba860efb2 --- /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 000000000..4f1032910 --- /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 5339c5ac5..4966a99d2 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 308046905..2027f14b8 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 06/76] 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 000000000..deea70d32 --- /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 000000000..7c31e9d56 --- /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 07/76] 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 000000000..d6dbac1a9 --- /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 000000000..a1709958e --- /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 982c6751b..c69c0b984 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 08/76] 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 000000000..c22702faa --- /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 09/76] 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 d6dbac1a9..cdf79d607 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 000000000..11ed42dbb --- /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 000000000..d5a3b8cb0 --- /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 000000000..3a51ceb28 --- /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 a1709958e..6bb12e210 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 000000000..79e6650fa --- /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 945bb9d1f..3009a0450 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 908a82ed9..c22f03103 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 1f0a735e8..18470536f 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 b13a587e5..f3a8b7d57 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 97987e0c2..6c4c452cd 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 000000000..3e7d9c51b --- /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 000000000..4a732ed54 --- /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 c69c0b984..9df7f7e2b 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 e5a80be3b..4f11aa136 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 000000000..1cdb727e2 --- /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 10/76] 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 000000000..416bfb867 --- /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 000000000..e1c45ca0e --- /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 11/76] 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 000000000..978a4f3d8 --- /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 000000000..7a05ff4d4 --- /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 12/76] 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 cdf79d607..e6375e86a 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 6bb12e210..fda1d4b86 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 13/76] 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 000000000..6cda6578c --- /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 000000000..a6c1e1b52 --- /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 14/76] 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 000000000..7d1b7433a --- /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 e6375e86a..8130c549b 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 11ed42dbb..6cb459c33 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 000000000..be4f5fb2c --- /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 d5a3b8cb0..7eb03e727 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 3a51ceb28..71d24e6f0 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 fda1d4b86..511fb97a0 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 4a732ed54..c370aaf1d 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 9df7f7e2b..a61c91f16 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 1cdb727e2..4306a3e46 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 000000000..df0782008 --- /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 000000000..47bfb0598 --- /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 416bfb867..89ea69fb9 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 15/76] 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 8130c549b..ff03bd7f8 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 7eb03e727..3baa79a0c 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 71d24e6f0..62c9b62ec 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 000000000..d7248343b --- /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 16/76] 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 ff03bd7f8..77ecea0d6 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 deea70d32..561dea2c1 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 7c31e9d56..74905578d 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 6cb459c33..57570d086 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 be4f5fb2c..021c86c87 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 3baa79a0c..7992a619f 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 62c9b62ec..d410548a9 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 511fb97a0..317ca8f81 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 3009a0450..7a5aee57b 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 c22f03103..052007897 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 6c4c452cd..a80b4711e 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 c370aaf1d..a8fdf2567 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 a61c91f16..84ce2bda1 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 4306a3e46..ed848c6a1 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 df0782008..3768dc0c0 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 17/76] 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 000000000..efb28daf8 --- /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 000000000..0122e3986 --- /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 000000000..9bb71ca3f --- /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 000000000..8debeff11 --- /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 18/76] 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 8b2803190..e8c123345 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 08e592be2..22d08641a 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 19/76] 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 77ecea0d6..5601b989e 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 57570d086..ca178089d 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 021c86c87..3c9e6149a 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 7992a619f..45ef3844e 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 d410548a9..6e6fbc585 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 000000000..82e902860 --- /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 000000000..8481a604c --- /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 317ca8f81..1fe1e7b84 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 052007897..9a00da756 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 84ce2bda1..8cd61aed0 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 000000000..b55f927e0 --- /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 000000000..8ae9dbc82 --- /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 20/76] 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 b20c74917..b0d482f9f 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 21/76] 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 74905578d..04baf0eda 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 3c9e6149a..2e30b5af6 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 ed848c6a1..584316f0e 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 3768dc0c0..45ed2517e 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 22/76] 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 2e30b5af6..e3d16a7ba 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 1fe1e7b84..110b0a9f9 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 45ed2517e..1e3d6b622 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 23/76] 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 5601b989e..90e673963 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 000000000..2078325a5 --- /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 82e902860..f626323b5 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 8481a604c..bcbbe81ce 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 110b0a9f9..5aad69f4d 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 9bb71ca3f..489721a8a 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 24/76] 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 018af6f4e..90f428f40 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 a9f84de71..c39d83131 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 25/76] 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 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 literal 2876 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 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 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

kM-i?FIu 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 diff --git a/publicsuffix/data/text b/publicsuffix/data/text index 124dcd61f..7e516413f 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 3f44dcfe7..c051dac8e 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 6bdadcc44..78d400fa6 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 99698271a..a297b3b0d 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 26/76] 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 7d1b7433a..df493579f 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 90e673963..0952a79e8 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 ca178089d..f42f7e528 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 e3d16a7ba..d9445150a 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 45ef3844e..00985b670 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 f626323b5..7a531f52b 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 5aad69f4d..2480f9cb0 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 6cda6578c..75dea87d0 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 efb28daf8..27ab07a6f 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 000000000..1281b54ee --- /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 b55f927e0..83215dfd3 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 8ae9dbc82..d158e72af 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 27/76] 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 7a531f52b..dd35e34cf 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 bcbbe81ce..877dbb94f 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 28/76] 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 37da3de4d..69938d54f 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 64c6db86d..83fac7812 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 29/76] 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 033b6e6db..6d5e00887 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 30/76] 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 69938d54f..19ea8f17c 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 83fac7812..ddb062b1e 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 31/76] 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 19ea8f17c..cd997bab0 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 ddb062b1e..1b7f3cb35 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 32/76] 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 1e3d6b622..35cb8bf00 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 33/76] 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 0952a79e8..ee8f011f8 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 2078325a5..0da3ddb45 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 f42f7e528..103db9fa4 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 d9445150a..dc0dc6cd3 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 00985b670..e0a91ab00 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 55d32f310..f15685932 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 83215dfd3..12117dbd3 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 d158e72af..5904a9342 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 f0643c922..848602915 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 34/76] 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 cd997bab0..9ddf2c229 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 1b7f3cb35..ee42febbc 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 35/76] 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 851224595..000000000 --- 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 55fd826f7..000000000 --- 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 f77c08a10..a16774b7f 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 cd73291ea..b99c5af54 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 36/76] 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 04baf0eda..d479cd4a8 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 dc0dc6cd3..bb4303033 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 2480f9cb0..2aa38fcf3 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 5904a9342..bafd236c9 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 35cb8bf00..180ea8bee 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 37/76] 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 000000000..cec57c5e3 --- /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 ee8f011f8..04dcd7b6b 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 2aa38fcf3..8ebe49e0e 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 38/76] 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 6e6fbc585..9d315fb39 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 39/76] 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 9a00da756..ca5b37b2b 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 8cd61aed0..71738e129 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 bafd236c9..7b8ba2c54 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 89ea69fb9..dc76d1650 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 e1c45ca0e..cc88e83fd 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 40/76] 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 0da3ddb45..5b419c4e5 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 41/76] 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 9ddf2c229..0215a5dde 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 ee42febbc..23fb3d574 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 42/76] 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 12117dbd3..1033cbb40 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 7b8ba2c54..79377c6a4 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 43/76] 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 8ebe49e0e..d8c44558d 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 44/76] 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 b0d482f9f..4515b22c4 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 45/76] 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 000000000..e1e2594d1 --- /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 dd35e34cf..0ede284e2 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 877dbb94f..9bbc994b1 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 d8c44558d..ea720d575 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 1033cbb40..2dbf4461b 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 79377c6a4..e22e0432e 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 46/76] 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 90f428f40..b16f4e5e6 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 c39d83131..0fd3311f4 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 47/76] 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 0215a5dde..dda888a90 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 48/76] 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 df493579f..f78e81192 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 cec57c5e3..8d67ef0d4 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 04dcd7b6b..642c50761 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 103db9fa4..b8146a425 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 bb4303033..d426aa690 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 e0a91ab00..faf3a37f1 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 0ede284e2..716ed2d50 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 9bbc994b1..ab1df1a24 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 71738e129..cf4137e81 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 2dbf4461b..b759e406c 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 000000000..5ea7146b5 --- /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 000000000..3f291e9f4 --- /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 e22e0432e..fb21255a4 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 49/76] 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 642c50761..707b335be 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 5b419c4e5..dc2a57f9d 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 716ed2d50..9ec2fa0d6 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 27ab07a6f..a2fb53711 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 0122e3986..9e84a84bd 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 489721a8a..7085e578b 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 8debeff11..d78216b0e 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 b759e406c..d2f2cd7a3 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 5ea7146b5..db3ab2292 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 50/76] 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 f78e81192..d68e2c7ad 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 8d67ef0d4..b99ffef64 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 707b335be..117364f55 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 d426aa690..d8368f021 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 9ec2fa0d6..5816d49f3 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 ab1df1a24..8ae007ccc 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 75dea87d0..8aa8f7b82 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 d2f2cd7a3..fbc36334b 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 fb21255a4..b01485287 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 180ea8bee..0f22f4fb3 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 51/76] 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 d8368f021..f74ec7e64 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 52/76] 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 5816d49f3..76e9bf94c 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 db3ab2292..6eda7883b 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 53/76] 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 d68e2c7ad..b390d6911 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 b99ffef64..d292854f5 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 117364f55..0ab6f6947 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 000000000..790210b4a --- /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 000000000..f01a7389c --- /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 b8146a425..85bda314e 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 f74ec7e64..9b8846251 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 76e9bf94c..0a72d26eb 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 fbc36334b..84c437d89 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 54/76] 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 0ab6f6947..c24e79032 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 790210b4a..265fdaf85 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 f01a7389c..28559b469 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 faf3a37f1..07f17e3cc 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 0a72d26eb..7c6c8be2c 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 8ae007ccc..69f982c3a 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 84c437d89..923ff232e 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 b01485287..750119614 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 55/76] 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 93a9102e8..00c671442 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 f3a8b7d57..b13a587e5 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 56/76] 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 265fdaf85..cd9a6a912 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 28559b469..2cd4e6246 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 923ff232e..9310811c1 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 57/76] 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 c24e79032..0063965df 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 561dea2c1..eb2f3ecc1 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 d479cd4a8..c5289583d 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 07f17e3cc..b866d8a6d 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 ea720d575..cdbd4669e 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 000000000..9869f6e22 --- /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 000000000..a6e0b3464 --- /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 584316f0e..1d07f17e4 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 58/76] 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 b866d8a6d..92ee8ea10 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 00c671442..a1bcead97 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 ca5b37b2b..43238826f 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 59/76] 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 0063965df..26c25f895 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 000000000..ec0b7a327 --- /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 000000000..20c00e754 --- /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 92ee8ea10..64e5f985f 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 9d315fb39..853c8453f 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 cdbd4669e..4228ce721 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 f15685932..8e01bb7cb 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 9869f6e22..a84286e89 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 1d07f17e4..e3a430ed7 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 0f22f4fb3..1c7b36d33 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 60/76] 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 26c25f895..4565e1a58 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 64e5f985f..d1fa52d99 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 853c8453f..58a3df107 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 4228ce721..3fef62d50 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 3503d2431..7f0846f3e 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 43238826f..458cd3a93 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 18470536f..2f9b9cefb 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 6495360a3..1fe130731 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 a80b4711e..2009895e0 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 e3a430ed7..a37e26fb8 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 61/76] 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 4565e1a58..dc3a985e8 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 d1fa52d99..4fc4eeccf 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 58a3df107..63f65b557 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 3fef62d50..76774cc39 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 000000000..6b6bb7980 --- /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 a1bcead97..8242bd0a9 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 7f0846f3e..c8b1f9ba8 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 458cd3a93..8bb3cae21 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 2f9b9cefb..aab1eaf3a 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 2009895e0..0c2b2ee41 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 62/76] 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 dc3a985e8..5da0ba443 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 76774cc39..ac0543b1e 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 6b6bb7980..4a4d67771 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 c8b1f9ba8..4899a0394 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 aab1eaf3a..137744613 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 63/76] 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 dda888a90..b6b4f9c19 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 23fb3d574..c84d5a3aa 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 64/76] 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 4fc4eeccf..6347ddae8 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 4899a0394..7b01bb00d 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 8bb3cae21..ce0433902 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 137744613..7b141ac49 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 1c7b36d33..416707688 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 65/76] 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 5da0ba443..60979125d 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 66/76] 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 ec0b7a327..b8b86fd6f 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 ac0543b1e..d75b2eb69 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 a84286e89..03d8ec65f 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 a6e0b3464..9d0f314ec 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 67/76] 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 6347ddae8..19c43858c 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 63f65b557..00b02c2a3 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 d75b2eb69..fd9e6e42e 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 03d8ec65f..96b1e4593 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 8242bd0a9..7d69f96d2 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 b13a587e5..58c584e16 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 cf4137e81..9de97b6d8 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 416707688..81d17b858 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 000000000..cfb7ce4be --- /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 68/76] 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 6d5e00887..de60fa88f 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 69/76] 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 cd9a6a912..281c7084f 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 2cd4e6246..45c82f60d 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 70/76] 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 60979125d..9db00fe09 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 eb2f3ecc1..045e646ac 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 c5289583d..44755ecf4 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 fd9e6e42e..6a359e89a 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 71/76] 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 281c7084f..4f1ab6eaf 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 45c82f60d..d5ee74ebd 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 72/76] 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 7c6c8be2c..a0793297e 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 73/76] 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 19c43858c..9b1ba1ae1 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 750119614..86eebc698 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 74/76] 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 d5ee74ebd..03e0757a6 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 9310811c1..89036b19b 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 86eebc698..7c1377fae 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 75/76] 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 b16f4e5e6..38ac82b44 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 0fd3311f4..dc4dc125c 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 76/76] 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 de60fa88f..02c88b6b3 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 b99c5af54..22657cbfe 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) + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy