Skip to content

Commit

Permalink
quic: version negotiation
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
neild committed Sep 27, 2023
1 parent 3b0ab98 commit ddd8598
Show file tree
Hide file tree
Showing 9 changed files with 429 additions and 32 deletions.
47 changes: 47 additions & 0 deletions internal/quic/conn_recv.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
package quic

import (
"bytes"
"encoding/binary"
"errors"
"time"
)

Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions internal/quic/conn_send.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion internal/quic/conn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}},
Expand Down
88 changes: 66 additions & 22 deletions internal/quic/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,39 +239,83 @@ 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.
// Think about buffering between the receive loop and the 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
Expand Down
71 changes: 70 additions & 1 deletion internal/quic/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Loading

0 comments on commit ddd8598

Please sign in to comment.