quic: basic packet operations
The type of a QUIC packet can be identified by inspecting its first
byte, and the destination connection ID can be determined without
decrypting and parsing the entire packet.
For golang/go#58547
Change-Id: Ie298c0f6c0017343168a0974543e37ab7a569b0f
Reviewed-on: https://go-review.googlesource.com/c/net/+/475437
Run-TryBot: Damien Neil <dneil@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Matt Layher <mdlayher@gmail.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/quic/packet.go b/internal/quic/packet.go
new file mode 100644
index 0000000..4645ae7
--- /dev/null
+++ b/internal/quic/packet.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.
+
+package quic
+
+// packetType is a QUIC packet type.
+// https://www.rfc-editor.org/rfc/rfc9000.html#section-17
+type packetType byte
+
+const (
+ packetTypeInvalid = packetType(iota)
+ packetTypeInitial
+ packetType0RTT
+ packetTypeHandshake
+ packetTypeRetry
+ packetType1RTT
+ packetTypeVersionNegotiation
+)
+
+// 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
+)
+
+// Long Packet Type bits.
+// https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2-3.6.1
+const (
+ longPacketTypeInitial = 0 << 4
+ longPacketType0RTT = 1 << 4
+ longPacketTypeHandshake = 2 << 4
+ longPacketTypeRetry = 3 << 4
+)
+
+// Frame types.
+// https://www.rfc-editor.org/rfc/rfc9000.html#section-19
+const (
+ frameTypePadding = 0x00
+ frameTypePing = 0x01
+ frameTypeAck = 0x02
+ frameTypeAckECN = 0x03
+ frameTypeResetStream = 0x04
+ frameTypeStopSending = 0x05
+ frameTypeCrypto = 0x06
+ frameTypeNewToken = 0x07
+ frameTypeStreamBase = 0x08 // low three bits carry stream flags
+ frameTypeMaxData = 0x10
+ frameTypeMaxStreamData = 0x11
+ frameTypeMaxStreamsBidi = 0x12
+ frameTypeMaxStreamsUni = 0x13
+ frameTypeDataBlocked = 0x14
+ frameTypeStreamDataBlocked = 0x15
+ frameTypeStreamsBlockedBidi = 0x16
+ frameTypeStreamsBlockedUni = 0x17
+ frameTypeNewConnectionID = 0x18
+ frameTypeRetireConnectionID = 0x19
+ frameTypePathChallenge = 0x1a
+ frameTypePathResponse = 0x1b
+ frameTypeConnectionCloseTransport = 0x1c
+ frameTypeConnectionCloseApplication = 0x1d
+ frameTypeHandshakeDone = 0x1e
+)
+
+// The low three bits of STREAM frames.
+// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.8
+const (
+ streamOffBit = 0x04
+ streamLenBit = 0x02
+ streamFinBit = 0x01
+)
+
+// isLongHeader returns true if b is the first byte of a long header.
+func isLongHeader(b byte) bool {
+ return b&headerFormLong == headerFormLong
+}
+
+// getPacketType returns the type of a packet.
+func getPacketType(b []byte) packetType {
+ if len(b) == 0 {
+ return packetTypeInvalid
+ }
+ if !isLongHeader(b[0]) {
+ if b[0]&fixedBit != fixedBit {
+ return packetTypeInvalid
+ }
+ return packetType1RTT
+ }
+ if len(b) < 5 {
+ return packetTypeInvalid
+ }
+ if b[1] == 0 && b[2] == 0 && b[3] == 0 && b[4] == 0 {
+ // Version Negotiation packets don't necessarily set the fixed bit.
+ return packetTypeVersionNegotiation
+ }
+ if b[0]&fixedBit != fixedBit {
+ return packetTypeInvalid
+ }
+ switch b[0] & 0x30 {
+ case longPacketTypeInitial:
+ return packetTypeInitial
+ case longPacketType0RTT:
+ return packetType0RTT
+ case longPacketTypeHandshake:
+ return packetTypeHandshake
+ case longPacketTypeRetry:
+ return packetTypeRetry
+ }
+ return packetTypeInvalid
+}
+
+// dstConnIDForDatagram returns the destination connection ID field of the
+// first QUIC packet in a datagram.
+func dstConnIDForDatagram(pkt []byte) (id []byte, ok bool) {
+ if len(pkt) < 1 {
+ return nil, false
+ }
+ var n int
+ var b []byte
+ if isLongHeader(pkt[0]) {
+ if len(pkt) < 6 {
+ return nil, false
+ }
+ n = int(pkt[5])
+ b = pkt[6:]
+ } else {
+ n = connIDLen
+ b = pkt[1:]
+ }
+ if len(b) < n {
+ return nil, false
+ }
+ return b[:n], true
+}
+
+// A longPacket is a long header packet.
+type longPacket struct {
+ ptype packetType
+ reservedBits uint8
+ version uint32
+ num packetNumber
+ dstConnID []byte
+ srcConnID []byte
+ payload []byte
+
+ // The extra data depends on the packet type:
+ // Initial: Token.
+ // Retry: Retry token and integrity tag.
+ extra []byte
+}
+
+// A shortPacket is a short header (1-RTT) packet.
+type shortPacket struct {
+ reservedBits uint8
+ num packetNumber
+ payload []byte
+}
diff --git a/internal/quic/packet_test.go b/internal/quic/packet_test.go
new file mode 100644
index 0000000..3011dda
--- /dev/null
+++ b/internal/quic/packet_test.go
@@ -0,0 +1,125 @@
+// 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.
+
+package quic
+
+import (
+ "bytes"
+ "encoding/hex"
+ "strings"
+ "testing"
+)
+
+func TestPacketHeader(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ packet []byte
+ isLongHeader bool
+ packetType packetType
+ dstConnID []byte
+ }{{
+ // Initial packet from https://www.rfc-editor.org/rfc/rfc9001#section-a.1
+ // (truncated)
+ name: "rfc9001_a1",
+ packet: unhex(`
+ c000000001088394c8f03e5157080000 449e7b9aec34d1b1c98dd7689fb8ec11
+ `),
+ isLongHeader: true,
+ packetType: packetTypeInitial,
+ dstConnID: unhex(`8394c8f03e515708`),
+ }, {
+ // Initial packet from https://www.rfc-editor.org/rfc/rfc9001#section-a.3
+ // (truncated)
+ name: "rfc9001_a3",
+ packet: unhex(`
+ cf000000010008f067a5502a4262b500 4075c0d95a482cd0991cd25b0aac406a
+ `),
+ isLongHeader: true,
+ packetType: packetTypeInitial,
+ dstConnID: []byte{},
+ }, {
+ // Retry packet from https://www.rfc-editor.org/rfc/rfc9001#section-a.4
+ name: "rfc9001_a4",
+ packet: unhex(`
+ ff000000010008f067a5502a4262b574 6f6b656e04a265ba2eff4d829058fb3f
+ 0f2496ba
+ `),
+ isLongHeader: true,
+ packetType: packetTypeRetry,
+ dstConnID: []byte{},
+ }, {
+ // Short header packet from https://www.rfc-editor.org/rfc/rfc9001#section-a.5
+ name: "rfc9001_a5",
+ packet: unhex(`
+ 4cfe4189655e5cd55c41f69080575d7999c25a5bfb
+ `),
+ isLongHeader: false,
+ packetType: packetType1RTT,
+ dstConnID: unhex(`fe4189655e5cd55c`),
+ }, {
+ // Version Negotiation packet.
+ name: "version_negotiation",
+ packet: unhex(`
+ 80 00000000 01ff0001020304
+ `),
+ isLongHeader: true,
+ packetType: packetTypeVersionNegotiation,
+ dstConnID: []byte{0xff},
+ }, {
+ // Too-short packet.
+ name: "truncated_after_connid_length",
+ packet: unhex(`
+ cf0000000105
+ `),
+ isLongHeader: true,
+ packetType: packetTypeInitial,
+ dstConnID: nil,
+ }, {
+ // Too-short packet.
+ name: "truncated_after_version",
+ packet: unhex(`
+ cf00000001
+ `),
+ isLongHeader: true,
+ packetType: packetTypeInitial,
+ dstConnID: nil,
+ }, {
+ // Much too short packet.
+ name: "truncated_in_version",
+ packet: unhex(`
+ cf000000
+ `),
+ isLongHeader: true,
+ packetType: packetTypeInvalid,
+ dstConnID: nil,
+ }} {
+ t.Run(test.name, func(t *testing.T) {
+ if got, want := isLongHeader(test.packet[0]), test.isLongHeader; got != want {
+ t.Errorf("packet %x:\nisLongHeader(packet) = %v, want %v", test.packet, got, want)
+ }
+ if got, want := getPacketType(test.packet), test.packetType; got != want {
+ t.Errorf("packet %x:\ngetPacketType(packet) = %v, want %v", test.packet, got, want)
+ }
+ gotConnID, gotOK := dstConnIDForDatagram(test.packet)
+ wantConnID, wantOK := test.dstConnID, test.dstConnID != nil
+ if !bytes.Equal(gotConnID, wantConnID) || gotOK != wantOK {
+ t.Errorf("packet %x:\ndstConnIDForDatagram(packet) = {%x}, %v; want {%x}, %v", test.packet, gotConnID, gotOK, wantConnID, wantOK)
+ }
+ })
+ }
+}
+
+func unhex(s string) []byte {
+ b, err := hex.DecodeString(strings.Map(func(c rune) rune {
+ switch c {
+ case ' ', '\t', '\n':
+ return -1
+ }
+ return c
+ }, s))
+ if err != nil {
+ panic(err)
+ }
+ return b
+}