Skip to content

Commit ddd8598

Browse files
committed
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 <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]>
1 parent 3b0ab98 commit ddd8598

File tree

9 files changed

+429
-32
lines changed

9 files changed

+429
-32
lines changed

internal/quic/conn_recv.go

+47
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
package quic
88

99
import (
10+
"bytes"
11+
"encoding/binary"
12+
"errors"
1013
"time"
1114
)
1215

@@ -31,6 +34,9 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) {
3134
n = c.handleLongHeader(now, ptype, handshakeSpace, c.keysHandshake.r, buf)
3235
case packetType1RTT:
3336
n = c.handle1RTT(now, buf)
37+
case packetTypeVersionNegotiation:
38+
c.handleVersionNegotiation(now, buf)
39+
return
3440
default:
3541
return
3642
}
@@ -59,6 +65,11 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa
5965
c.abort(now, localTransportError(errProtocolViolation))
6066
return -1
6167
}
68+
if p.version != quicVersion1 {
69+
// The peer has changed versions on us mid-handshake?
70+
c.abort(now, localTransportError(errProtocolViolation))
71+
return -1
72+
}
6273

6374
if !c.acks[space].shouldProcess(p.num) {
6475
return n
@@ -117,6 +128,42 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int {
117128
return len(buf)
118129
}
119130

131+
var errVersionNegotiation = errors.New("server does not support QUIC version 1")
132+
133+
func (c *Conn) handleVersionNegotiation(now time.Time, pkt []byte) {
134+
if c.side != clientSide {
135+
return // servers don't handle Version Negotiation packets
136+
}
137+
// "A client MUST discard any Version Negotiation packet if it has
138+
// received and successfully processed any other packet [...]"
139+
// https://www.rfc-editor.org/rfc/rfc9000#section-6.2-2
140+
if !c.keysInitial.canRead() {
141+
return // discarded Initial keys, connection is already established
142+
}
143+
if c.acks[initialSpace].seen.numRanges() != 0 {
144+
return // processed at least one packet
145+
}
146+
_, srcConnID, versions := parseVersionNegotiation(pkt)
147+
if len(c.connIDState.remote) < 1 || !bytes.Equal(c.connIDState.remote[0].cid, srcConnID) {
148+
return // Source Connection ID doesn't match what we sent
149+
}
150+
for len(versions) >= 4 {
151+
ver := binary.BigEndian.Uint32(versions)
152+
if ver == 1 {
153+
// "A client MUST discard a Version Negotiation packet that lists
154+
// the QUIC version selected by the client."
155+
// https://www.rfc-editor.org/rfc/rfc9000#section-6.2-2
156+
return
157+
}
158+
versions = versions[4:]
159+
}
160+
// "A client that supports only this version of QUIC MUST
161+
// abandon the current connection attempt if it receives
162+
// a Version Negotiation packet, [with the two exceptions handled above]."
163+
// https://www.rfc-editor.org/rfc/rfc9000#section-6.2-2
164+
c.abortImmediately(now, errVersionNegotiation)
165+
}
166+
120167
func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, payload []byte) (ackEliciting bool) {
121168
if len(payload) == 0 {
122169
// "An endpoint MUST treat receipt of a packet containing no frames

internal/quic/conn_send.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) {
6464
pnum := c.loss.nextNumber(initialSpace)
6565
p := longPacket{
6666
ptype: packetTypeInitial,
67-
version: 1,
67+
version: quicVersion1,
6868
num: pnum,
6969
dstConnID: dstConnID,
7070
srcConnID: c.connIDState.srcConnID(),
@@ -91,7 +91,7 @@ func (c *Conn) maybeSend(now time.Time) (next time.Time) {
9191
pnum := c.loss.nextNumber(handshakeSpace)
9292
p := longPacket{
9393
ptype: packetTypeHandshake,
94-
version: 1,
94+
version: quicVersion1,
9595
num: pnum,
9696
dstConnID: dstConnID,
9797
srcConnID: c.connIDState.srcConnID(),

internal/quic/conn_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ func (tc *testConn) writeFrames(ptype packetType, frames ...debugFrame) {
409409
keyNumber: tc.sendKeyNumber,
410410
keyPhaseBit: tc.sendKeyPhaseBit,
411411
frames: frames,
412-
version: 1,
412+
version: quicVersion1,
413413
dstConnID: dstConnID,
414414
srcConnID: tc.peerConnID,
415415
}},

internal/quic/listener.go

+66-22
Original file line numberDiff line numberDiff line change
@@ -239,39 +239,83 @@ func (l *Listener) listen() {
239239
func (l *Listener) handleDatagram(m *datagram, conns map[string]*Conn) {
240240
dstConnID, ok := dstConnIDForDatagram(m.b)
241241
if !ok {
242+
m.recycle()
242243
return
243244
}
244245
c := conns[string(dstConnID)]
245246
if c == nil {
246-
if getPacketType(m.b) != packetTypeInitial {
247-
// This packet isn't trying to create a new connection.
248-
// It might be associated with some connection we've lost state for.
249-
// TODO: Send a stateless reset when appropriate.
250-
// https://www.rfc-editor.org/rfc/rfc9000.html#section-10.3
251-
return
252-
}
253-
var now time.Time
254-
if l.testHooks != nil {
255-
now = l.testHooks.timeNow()
256-
} else {
257-
now = time.Now()
258-
}
259-
var err error
260-
c, err = l.newConn(now, serverSide, dstConnID, m.addr)
261-
if err != nil {
262-
// The accept queue is probably full.
263-
// We could send a CONNECTION_CLOSE to the peer to reject the connection.
264-
// Currently, we just drop the datagram.
265-
// https://www.rfc-editor.org/rfc/rfc9000.html#section-5.2.2-5
266-
return
267-
}
247+
// TODO: Move this branch into a separate goroutine to avoid blocking
248+
// the listener while processing packets.
249+
l.handleUnknownDestinationDatagram(m)
250+
return
268251
}
269252

270253
// TODO: This can block the listener while waiting for the conn to accept the dgram.
271254
// Think about buffering between the receive loop and the conn.
272255
c.sendMsg(m)
273256
}
274257

258+
func (l *Listener) handleUnknownDestinationDatagram(m *datagram) {
259+
defer func() {
260+
if m != nil {
261+
m.recycle()
262+
}
263+
}()
264+
if len(m.b) < minimumClientInitialDatagramSize {
265+
return
266+
}
267+
p, ok := parseGenericLongHeaderPacket(m.b)
268+
if !ok {
269+
// Not a long header packet, or not parseable.
270+
// Short header (1-RTT) packets don't contain enough information
271+
// to do anything useful with if we don't recognize the
272+
// connection ID.
273+
return
274+
}
275+
276+
switch p.version {
277+
case quicVersion1:
278+
case 0:
279+
// Version Negotiation for an unknown connection.
280+
return
281+
default:
282+
// Unknown version.
283+
l.sendVersionNegotiation(p, m.addr)
284+
return
285+
}
286+
if getPacketType(m.b) != packetTypeInitial {
287+
// This packet isn't trying to create a new connection.
288+
// It might be associated with some connection we've lost state for.
289+
// TODO: Send a stateless reset when appropriate.
290+
// https://www.rfc-editor.org/rfc/rfc9000.html#section-10.3
291+
return
292+
}
293+
var now time.Time
294+
if l.testHooks != nil {
295+
now = l.testHooks.timeNow()
296+
} else {
297+
now = time.Now()
298+
}
299+
var err error
300+
c, err := l.newConn(now, serverSide, p.dstConnID, m.addr)
301+
if err != nil {
302+
// The accept queue is probably full.
303+
// We could send a CONNECTION_CLOSE to the peer to reject the connection.
304+
// Currently, we just drop the datagram.
305+
// https://www.rfc-editor.org/rfc/rfc9000.html#section-5.2.2-5
306+
return
307+
}
308+
c.sendMsg(m)
309+
m = nil // don't recycle, sendMsg takes ownership
310+
}
311+
312+
func (l *Listener) sendVersionNegotiation(p genericLongPacket, addr netip.AddrPort) {
313+
m := newDatagram()
314+
m.b = appendVersionNegotiation(m.b[:0], p.srcConnID, p.dstConnID, quicVersion1)
315+
l.sendDatagram(m.b, addr)
316+
m.recycle()
317+
}
318+
275319
func (l *Listener) sendDatagram(p []byte, addr netip.AddrPort) error {
276320
_, err := l.udpConn.WriteToUDPAddrPort(p, addr)
277321
return err

internal/quic/packet.go

+70-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
package quic
88

9-
import "fmt"
9+
import (
10+
"encoding/binary"
11+
"fmt"
12+
)
1013

1114
// packetType is a QUIC packet type.
1215
// https://www.rfc-editor.org/rfc/rfc9000.html#section-17
@@ -157,6 +160,33 @@ func dstConnIDForDatagram(pkt []byte) (id []byte, ok bool) {
157160
return b[:n], true
158161
}
159162

163+
// parseVersionNegotiation parses a Version Negotiation packet.
164+
// The returned versions is a slice of big-endian uint32s.
165+
// It returns (nil, nil, nil) for an invalid packet.
166+
func parseVersionNegotiation(pkt []byte) (dstConnID, srcConnID, versions []byte) {
167+
p, ok := parseGenericLongHeaderPacket(pkt)
168+
if !ok {
169+
return nil, nil, nil
170+
}
171+
if len(p.data)%4 != 0 {
172+
return nil, nil, nil
173+
}
174+
return p.dstConnID, p.srcConnID, p.data
175+
}
176+
177+
// appendVersionNegotiation appends a Version Negotiation packet to pkt,
178+
// returning the result.
179+
func appendVersionNegotiation(pkt, dstConnID, srcConnID []byte, versions ...uint32) []byte {
180+
pkt = append(pkt, headerFormLong|fixedBit) // header byte
181+
pkt = append(pkt, 0, 0, 0, 0) // Version (0 for Version Negotiation)
182+
pkt = appendUint8Bytes(pkt, dstConnID) // Destination Connection ID
183+
pkt = appendUint8Bytes(pkt, srcConnID) // Source Connection ID
184+
for _, v := range versions {
185+
pkt = binary.BigEndian.AppendUint32(pkt, v) // Supported Version
186+
}
187+
return pkt
188+
}
189+
160190
// A longPacket is a long header packet.
161191
type longPacket struct {
162192
ptype packetType
@@ -177,3 +207,42 @@ type shortPacket struct {
177207
num packetNumber
178208
payload []byte
179209
}
210+
211+
// A genericLongPacket is a long header packet of an arbitrary QUIC version.
212+
// https://www.rfc-editor.org/rfc/rfc8999#section-5.1
213+
type genericLongPacket struct {
214+
version uint32
215+
dstConnID []byte
216+
srcConnID []byte
217+
data []byte
218+
}
219+
220+
func parseGenericLongHeaderPacket(b []byte) (p genericLongPacket, ok bool) {
221+
if len(b) < 5 || !isLongHeader(b[0]) {
222+
return genericLongPacket{}, false
223+
}
224+
b = b[1:]
225+
// Version (32),
226+
var n int
227+
p.version, n = consumeUint32(b)
228+
if n < 0 {
229+
return genericLongPacket{}, false
230+
}
231+
b = b[n:]
232+
// Destination Connection ID Length (8),
233+
// Destination Connection ID (0..2048),
234+
p.dstConnID, n = consumeUint8Bytes(b)
235+
if n < 0 || len(p.dstConnID) > 2048/8 {
236+
return genericLongPacket{}, false
237+
}
238+
b = b[n:]
239+
// Source Connection ID Length (8),
240+
// Source Connection ID (0..2048),
241+
p.srcConnID, n = consumeUint8Bytes(b)
242+
if n < 0 || len(p.dstConnID) > 2048/8 {
243+
return genericLongPacket{}, false
244+
}
245+
b = b[n:]
246+
p.data = b
247+
return p, true
248+
}

0 commit comments

Comments
 (0)