Skip to content

Commit 5e678bb

Browse files
committed
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 <jba@google.com> Run-TryBot: Damien Neil <dneil@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
1 parent dd5bc96 commit 5e678bb

File tree

2 files changed

+424
-0
lines changed

2 files changed

+424
-0
lines changed

internal/quic/crypto_stream.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.21
6+
7+
package quic
8+
9+
// "Implementations MUST support buffering at least 4096 bytes of data
10+
// received in out-of-order CRYPTO frames."
11+
// https://www.rfc-editor.org/rfc/rfc9000.html#section-7.5-2
12+
//
13+
// 4096 is too small for real-world cases, however, so we allow more.
14+
const cryptoBufferSize = 1 << 20
15+
16+
// A cryptoStream is the stream of data passed in CRYPTO frames.
17+
// There is one cryptoStream per packet number space.
18+
type cryptoStream struct {
19+
// CRYPTO data received from the peer.
20+
in pipe
21+
inset rangeset[int64] // bytes received
22+
23+
// CRYPTO data queued for transmission to the peer.
24+
out pipe
25+
outunsent rangeset[int64] // bytes in need of sending
26+
outacked rangeset[int64] // bytes acked by peer
27+
}
28+
29+
// handleCrypto processes data received in a CRYPTO frame.
30+
func (s *cryptoStream) handleCrypto(off int64, b []byte, f func([]byte) error) error {
31+
end := off + int64(len(b))
32+
if end-s.inset.min() > cryptoBufferSize {
33+
return localTransportError(errCryptoBufferExceeded)
34+
}
35+
s.inset.add(off, end)
36+
if off == s.in.start {
37+
// Fast path: This is the next chunk of data in the stream,
38+
// so just handle it immediately.
39+
if err := f(b); err != nil {
40+
return err
41+
}
42+
s.in.discardBefore(end)
43+
} else {
44+
// This is either data we've already processed,
45+
// data we can't process yet, or a mix of both.
46+
s.in.writeAt(b, off)
47+
}
48+
// s.in.start is the next byte in sequence.
49+
// If it's in s.inset, we have bytes to provide.
50+
// If it isn't, we don't--we're either out of data,
51+
// or only have data that comes after the next byte.
52+
if !s.inset.contains(s.in.start) {
53+
return nil
54+
}
55+
// size is the size of the first contiguous chunk of bytes
56+
// that have not been processed yet.
57+
size := int(s.inset[0].end - s.in.start)
58+
if size <= 0 {
59+
return nil
60+
}
61+
err := s.in.read(s.in.start, size, f)
62+
s.in.discardBefore(s.inset[0].end)
63+
return err
64+
}
65+
66+
// write queues data for sending to the peer.
67+
// It does not block or limit the amount of buffered data.
68+
// QUIC connections don't communicate the amount of CRYPTO data they are willing to buffer,
69+
// so we send what we have and the peer can close the connection if it is too much.
70+
func (s *cryptoStream) write(b []byte) {
71+
start := s.out.end
72+
s.out.writeAt(b, start)
73+
s.outunsent.add(start, s.out.end)
74+
}
75+
76+
// ackOrLoss reports that an CRYPTO frame sent by us has been acknowledged by the peer, or lost.
77+
func (s *cryptoStream) ackOrLoss(start, end int64, fate packetFate) {
78+
switch fate {
79+
case packetAcked:
80+
s.outacked.add(start, end)
81+
s.outunsent.sub(start, end)
82+
// If this ack is for data at the start of the send buffer, we can now discard it.
83+
if s.outacked.contains(s.out.start) {
84+
s.out.discardBefore(s.outacked[0].end)
85+
}
86+
case packetLost:
87+
// Mark everything lost, but not previously acked, as needing retransmission.
88+
// We do this by adding all the lost bytes to outunsent, and then
89+
// removing everything already acked.
90+
s.outunsent.add(start, end)
91+
for _, a := range s.outacked {
92+
s.outunsent.sub(a.start, a.end)
93+
}
94+
}
95+
}
96+
97+
// dataToSend reports what data should be sent in CRYPTO frames to the peer.
98+
// It calls f with each range of data to send.
99+
// f uses sendData to get the bytes to send, and returns the number of bytes sent.
100+
// dataToSend calls f until no data is left, or f returns 0.
101+
//
102+
// This function is unusually indirect (why not just return a []byte,
103+
// or implement io.Reader?).
104+
//
105+
// Returning a []byte to the caller either requires that we store the
106+
// data to send contiguously (which we don't), allocate a temporary buffer
107+
// and copy into it (inefficient), or return less data than we have available
108+
// (requires complexity to avoid unnecessarily breaking data across frames).
109+
//
110+
// Accepting a []byte from the caller (io.Reader) makes packet construction
111+
// difficult. Since CRYPTO data is encoded with a varint length prefix, the
112+
// location of the data depends on the length of the data. (We could hardcode
113+
// a 2-byte length, of course.)
114+
//
115+
// Instead, we tell the caller how much data is, the caller figures out where
116+
// to put it (and possibly decides that it doesn't have space for this data
117+
// in the packet after all), and the caller then makes a separate call to
118+
// copy the data it wants into position.
119+
func (s *cryptoStream) dataToSend(pto bool, f func(off, size int64) (sent int64)) {
120+
for {
121+
var off, size int64
122+
if pto {
123+
// On PTO, resend unacked data that fits in the probe packet.
124+
// For simplicity, we send the range starting at s.out.start
125+
// (which is definitely unacked, or else we would have discarded it)
126+
// up to the next acked byte (if any).
127+
//
128+
// This may miss unacked data starting after that acked byte,
129+
// but avoids resending data the peer has acked.
130+
off = s.out.start
131+
end := s.out.end
132+
for _, r := range s.outacked {
133+
if r.start > off {
134+
end = r.start
135+
break
136+
}
137+
}
138+
size = end - s.out.start
139+
} else if s.outunsent.numRanges() > 0 {
140+
off = s.outunsent.min()
141+
size = s.outunsent[0].size()
142+
}
143+
if size == 0 {
144+
return
145+
}
146+
n := f(off, size)
147+
if n == 0 || pto {
148+
return
149+
}
150+
}
151+
}
152+
153+
// sendData fills b with data to send to the peer, starting at off,
154+
// and marks the data as sent. The caller must have already ascertained
155+
// that there is data to send in this region using dataToSend.
156+
func (s *cryptoStream) sendData(off int64, b []byte) {
157+
s.out.copy(off, b)
158+
s.outunsent.sub(off, off+int64(len(b)))
159+
}

0 commit comments

Comments
 (0)