quic

package
v0.662.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 9, 2026 License: AGPL-3.0 Imports: 11 Imported by: 0

Documentation

Overview

Package quic decodes QUIC long-header packets per RFC 9000. The short header (1-RTT, post-handshake) and frame-level dissection inside encrypted payloads are out of scope; this package gets you the connection-setup visibility (Initial / 0-RTT / Handshake / Retry / Version Negotiation) that's useful for HTTP/3 + QUIC traffic forensics.

Wrap-vs-native judgement

Native. RFC 9000 is fully public; the long-header wire
format is a tight bit-packed byte (header form / fixed
bit / long packet type / type-specific bits) plus a
32-bit version + 1-byte-length-prefixed Destination
Connection ID + 1-byte-length-prefixed Source Connection
ID + per-type body. Variable-Length Integer encoding
(§16) is a 2-bit-prefix variant with payload lengths of
1/2/4/8 bytes. No crypto at the long-header layer —
the payload is header-protected and packet-protected,
but the SCID / DCID / Version / Token are all in the
clear. Operators paste UDP-payload bytes from a
Wireshark Follow-UDP-Stream view, a `tcpdump -X udp
port 443` line, or any QUIC-emitting tool and inspect
the cleartext header fields.

What this package covers

  • **First-byte dispatch**:

  • high bit 1 = long header (this package)

  • high bit 0 = short header (not decoded; surfaced with a note pointing at the truncated header)

  • **Version Negotiation** is the special case where the Version field is 0x00000000; the packet then carries a list of supported versions.

  • **Long header common** (RFC 9000 §17.2):

  • byte 0: Header Form (1 bit) + Fixed Bit (1 bit) + Long Packet Type (2 bits) + Type-Specific (4 bits; low 2 bits are the encrypted-length of the packet number, but the high 2 bits are header-protected so we surface the raw type-specific nibble verbatim).

  • Version (4 bytes BE): 0x00000001 = QUIC v1 (canonical); 0x00000000 = Version Negotiation; 0x6B3343CF = QUIC v2 (RFC 9369); IANA-registered Force-Greasing version 0xFAFAFAFA etc.

  • DCID Length (1 byte; 0-160 per RFC 9000 §17.2).

  • DCID (DCID Length bytes).

  • SCID Length (1 byte; 0-160).

  • SCID (SCID Length bytes).

  • **Long Packet Types** (RFC 9000 §17.2):

  • 0 Initial: Token Length (VLI) + Token + Length (VLI)

  • Packet Number (1-4 bytes, header-protected) + Protected Payload.

  • 1 0-RTT: Length (VLI) + Packet Number + Protected Payload.

  • 2 Handshake: Length (VLI) + Packet Number + Protected Payload.

  • 3 Retry: Retry Token (variable) + Retry Integrity Tag (16 bytes, AES-128-GCM tag covering the original DCID). The tag is VERIFIABLE offline given the original DCID — see VerifyRetryIntegrity / retry_integrity.go.

  • **Variable-Length Integer** (RFC 9000 §16):

  • 0b00 prefix: 6-bit value in 1 byte

  • 0b01 prefix: 14-bit value in 2 bytes

  • 0b10 prefix: 30-bit value in 4 bytes

  • 0b11 prefix: 62-bit value in 8 bytes

  • **Version Negotiation** (RFC 9000 §17.2.1): if Version == 0, the bytes after SCID are a list of uint32 BE supported versions chosen by the server.

What this package does NOT cover (deliberately out of scope)

  • Short-header (1-RTT) packets — the packet number length and key-phase bits are in the header-protected first byte, so without the header-protection key we can't unambiguously parse the packet number. A future Spec could surface the cleartext fields (DCID — but only if the operator already knows the agreed DCID length, which varies per connection).

  • 0-RTT / Handshake / 1-RTT payload decryption — requires the TLS-handshake secrets, which are not on the wire. Those protected payloads are surfaced as hex. (The **Initial** payload is the exception: its keys are public, so it IS decrypted — see initial_decrypt.go.)

  • UDP / IP framing — feed the UDP payload bytes after the IP+UDP headers.

What this package additionally covers (see initial_decrypt.go)

  • **QUIC v1 Initial decryption.** The Initial packet is protected with keys derived deterministically from the clear-text Destination Connection ID and a fixed salt (RFC 9001 §5.2), so it is fully decryptable offline. For v1 Initials this package removes header protection, runs AES-128-GCM, dissects the frames (PADDING / PING / ACK / CRYPTO / CONNECTION_CLOSE) and reassembles the CRYPTO stream into the TLS ClientHello / ServerHello — the bytes QUIC otherwise hides, ready for tls_handshake_decode.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func VerifyRetryIntegrity added in v0.528.0

func VerifyRetryIntegrity(packet, odcid []byte) (bool, error)

VerifyRetryIntegrity recomputes the Retry Integrity Tag (RFC 9001 §5.8) over the Retry Pseudo-Packet and reports whether it matches the 16-byte tag carried at the end of the packet. odcid is the Destination Connection ID the client chose in its first Initial (echoed by the server's integrity check but absent from the Retry packet itself). The comparison is constant-time.

func VerifyRetryIntegrityHex added in v0.528.0

func VerifyRetryIntegrityHex(packetHex, odcidHex string) (bool, error)

VerifyRetryIntegrityHex is the hex front door to VerifyRetryIntegrity, accepting the same separators ('-', ':', '_', whitespace, '0x') as Decode for both the Retry packet and the original DCID.

Types

type DecryptedInitial added in v0.526.0

type DecryptedInitial struct {
	Role            string      `json:"role"` // "client" or "server"
	PacketNumber    uint64      `json:"packet_number"`
	PacketNumberLen int         `json:"packet_number_length"`
	KeyHex          string      `json:"key_hex"`
	IVHex           string      `json:"iv_hex"`
	HPHex           string      `json:"hp_hex"`
	PayloadLen      int         `json:"decrypted_payload_length"`
	Frames          []QUICFrame `json:"frames"`
	CryptoStreamLen int         `json:"crypto_stream_length,omitempty"`
	CryptoStreamHex string      `json:"crypto_stream_hex,omitempty"`
	TLSMessage      string      `json:"tls_message,omitempty"`
	// JA4 is the QUIC client/server fingerprint (protocol prefix "q")
	// computed over the reassembled ClientHello / ServerHello. JA4 for a
	// ClientHello, JA4S for a ServerHello — empty if the CRYPTO stream is
	// incomplete or not a (Client/Server)Hello.
	JA4   string   `json:"ja4,omitempty"`
	Notes []string `json:"notes,omitempty"`
}

DecryptedInitial is the cleartext view of a QUIC Initial packet recovered with the public Initial keys.

func DecryptInitial added in v0.526.0

func DecryptInitial(packet []byte, role string) (*DecryptedInitial, error)

DecryptInitial recovers the cleartext payload of a QUIC v1 Initial packet from its full on-the-wire bytes. role is "client" for a client Initial and "server" for a server Initial — they derive from different HKDF labels ("client in" / "server in"). Returns an error only on a malformed header or a GCM authentication failure (wrong role / corrupt packet / not actually an Initial).

type InitialPacket

type InitialPacket struct {
	TokenLength         uint64 `json:"token_length"`
	TokenHex            string `json:"token_hex,omitempty"`
	Length              uint64 `json:"length"`
	ProtectedPayloadLen int    `json:"protected_payload_length"`
	ProtectedPayloadHex string `json:"protected_payload_hex,omitempty"`

	// Decrypted is populated for QUIC v1 Initials, whose protection
	// keys are public (derived from the DCID + a fixed salt). nil when
	// decryption is not applicable or fails authentication.
	Decrypted *DecryptedInitial `json:"decrypted,omitempty"`
}

InitialPacket is the type-0 long-header body.

type LengthOnlyPacket

type LengthOnlyPacket struct {
	Length              uint64 `json:"length"`
	ProtectedPayloadLen int    `json:"protected_payload_length"`
	ProtectedPayloadHex string `json:"protected_payload_hex,omitempty"`
}

LengthOnlyPacket covers 0-RTT and Handshake (same body shape: length + protected packet number + protected payload).

type QUICFrame added in v0.526.0

type QUICFrame struct {
	Type     string `json:"type"`
	TypeByte uint64 `json:"type_byte"`
	Count    int    `json:"count,omitempty"`  // PADDING run length
	Offset   uint64 `json:"offset,omitempty"` // CRYPTO
	Length   uint64 `json:"length,omitempty"` // CRYPTO data length
}

QUICFrame is one parsed frame from the decrypted payload.

type Result

type Result struct {
	HeaderForm         string `json:"header_form"`
	IsLongHeader       bool   `json:"is_long_header"`
	FixedBit           bool   `json:"fixed_bit"`
	LongPacketType     int    `json:"long_packet_type,omitempty"`
	LongPacketTypeName string `json:"long_packet_type_name,omitempty"`
	TypeSpecificNibble int    `json:"type_specific_nibble,omitempty"`
	FirstByteHex       string `json:"first_byte_hex"`
	Version            uint32 `json:"version"`
	VersionName        string `json:"version_name"`
	VersionHex         string `json:"version_hex"`
	DCIDLength         int    `json:"dcid_length"`
	DCIDHex            string `json:"dcid_hex,omitempty"`
	SCIDLength         int    `json:"scid_length"`
	SCIDHex            string `json:"scid_hex,omitempty"`

	Initial            *InitialPacket    `json:"initial,omitempty"`
	ZeroRTT            *LengthOnlyPacket `json:"zero_rtt,omitempty"`
	Handshake          *LengthOnlyPacket `json:"handshake,omitempty"`
	Retry              *RetryPacket      `json:"retry,omitempty"`
	VersionNegotiation *VersionNeg       `json:"version_negotiation,omitempty"`

	TotalBytes int      `json:"total_bytes"`
	Notes      []string `json:"notes,omitempty"`
}

Result is the top-level decoded view.

func Decode

func Decode(hexStr string) (*Result, error)

Decode parses a QUIC packet from hex.

type RetryPacket

type RetryPacket struct {
	RetryTokenLen   int    `json:"retry_token_length"`
	RetryTokenHex   string `json:"retry_token_hex,omitempty"`
	IntegrityTagHex string `json:"integrity_tag_hex"`
	// IntegrityVerified is set when the caller supplies the original
	// Destination Connection ID: true if the Retry Integrity Tag is
	// authentic (RFC 9001 §5.8), false if it does not match. nil when no
	// ODCID was supplied or verification is not applicable.
	IntegrityVerified *bool  `json:"integrity_verified,omitempty"`
	IntegrityNote     string `json:"integrity_note,omitempty"`
}

RetryPacket is the type-3 long-header body.

type VersionNeg

type VersionNeg struct {
	SupportedVersions    []uint32 `json:"supported_versions"`
	SupportedVersionsHex []string `json:"supported_versions_hex"`
}

VersionNeg is the Version Negotiation packet body (RFC 9000 §17.2.1).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL