parser

package
v1.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2026 License: BSD-3-Clause Imports: 21 Imported by: 0

Documentation

Index

Constants

View Source
const (
	TLSRecordTypeHandshake  = 0x16
	TLSHandshakeClientHello = 0x01
	TLSHandshakeServerHello = 0x02
)

TLS handshake types.

View Source
const (
	ExtSNI                 = 0x0000
	ExtALPN                = 0x0010
	ExtSignatureAlgorithms = 0x000d
	ExtSupportedVersions   = 0x002b
)

TLS extension type IDs.

View Source
const (
	TLS13KeyLabel = "key"
	TLS13IVLabel  = "iv"
)

The HKDF-Expand-Label labels of the TLS 1.3 record layer.

RFC 8446 section 7.3 states both:

[sender]_write_key = HKDF-Expand-Label(Secret, "key", "", key_length)
[sender]_write_iv  = HKDF-Expand-Label(Secret, "iv", "", iv_length)

QUIC renames them, so `DeriveQUICKeys` reads `quic key` and `quic iv` instead. RFC 9001 section 5.1 states that rename.

View Source
const (
	TLS13ClientHandshakeSecretLabel   = "CLIENT_HANDSHAKE_TRAFFIC_SECRET"
	TLS13ServerHandshakeSecretLabel   = "SERVER_HANDSHAKE_TRAFFIC_SECRET"
	TLS13ClientApplicationSecretLabel = "CLIENT_TRAFFIC_SECRET_0"
	TLS13ServerApplicationSecretLabel = "SERVER_TRAFFIC_SECRET_0"
)

The key log labels of the TLS 1.3 secrets.

`draft-ietf-tls-keylogfile` names each one, and `internal/keylog` reads the name from a key log line. A caller passes one of these to `KeyLog.Secret` of the root package.

View Source
const DefaultMaxSegments = 4096

DefaultMaxSegments is the segment count that one stream stores at most.

The Python port ships this rule and this value. `ja4plus/utils/tcp_stream.py:45` at tag v1.1.0 states `DEFAULT_MAX_STREAM_SEGMENTS = 4096`, and `.claude/rules/parity.md` rule 2 gives the port the interface where this project shipped no name.

No vector of the shared corpus reaches this bound. A run of the conformance suite on 2026-08-14 read 703 stream keys, and the largest held 788 segments. So 4096 sits above five times the binding reading.

The port's comment cites 1336, which is the count that a stream reaches without a byte cap. This library holds a byte cap, so 788 is the reading that binds here.

The port attributes its 788 to `http2-with-cookies.pcapng`, and this library measures its 788 on the SSH stream `192.168.1.197:22->192.168.1.169:49237`. The largest stream on port 443 reaches 750 here. The count agrees and the attribution does not, and issue #596 reports that difference. `Crank-Git/ja4plus#620` carries the port half of the question.

This comment states the attribution that this library measured, and never the port's.

View Source
const EmptyHash = "000000000000"
View Source
const HTTP2ClientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"

HTTP2ClientPreface holds the 24 octets that open the client half of every HTTP/2 connection.

RFC 9113 section 3.4 states them: `The client connection preface starts with a sequence of 24 octets, which in hex notation is: 0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a`. The reader uses the preface as the gate of the whole walk, because a decrypted stream that carries another protocol then costs one prefix comparison.

View Source
const MaxCryptoBufferBytes = 16384

MaxCryptoBufferBytes is the highest number of bytes one connection collects from CRYPTO frames. RFC 9000 Section 16 lets a CRYPTO frame offset reach 4611686018427387903, and ReassembleCryptoFrames allocates a buffer that reaches the highest offset. A client hello reaches a few kilobytes, so this bound holds every real handshake message.

View Source
const MaxTunnelDepth = 4

MaxTunnelDepth is the count of tunnel layers the parser reads inside one packet. A crafted packet nests one tunnel inside another without a bound, so the parser stops here. The limit is a security control, and `docs/specs/features/05-conformance-gaps.md` FR-gaps-11 and FR-gaps-12 state it.

View Source
const TLS13MaxRecordBytes = tls13RecordHeaderLength + tls13MaxRecordLength

TLS13MaxRecordBytes is the byte count of the longest record that RFC 8446 permits.

`tls13MaxRecordLength` states the bound of section 5.2, and the record header adds 5 more octets. A reader that holds a part of one record therefore holds this many bytes at most.

View Source
const TLSRecordTypeApplicationData = 0x17

TLSRecordTypeApplicationData names the outer content type of every protected TLS 1.3 record.

RFC 8446 section 5.2 states the rule: `The outer opaque_type field of a TLSCiphertext record is always set to the value 23 (application_data) for outward compatibility with middleboxes accustomed to parsing previous versions of TLS.` So a TLS 1.3 server writes the Certificate message under this type, and never under TLSRecordTypeHandshake.

Variables

View Source
var ErrNoSecret = errors.New("parser: the caller supplied no secret for the connection")

ErrNoSecret reports that the caller supplied no secret for the connection.

View Source
var ErrTunnelDepthExceeded = fmt.Errorf("the packet nests more than %d tunnel layers", MaxTunnelDepth)

ErrTunnelDepthExceeded reports that the packet nests more tunnel layers than MaxTunnelDepth allows. The caller produces no fingerprint from such a packet.

View Source
var ErrTunnelPayloadUnread = errors.New("the parser reads no inner packet inside the tunnel")

ErrTunnelPayloadUnread reports that the packet carries a tunnel layer and that the parser reads no network layer inside it. A GRE header that names an unknown protocol type reaches this error, and so does a truncated inner frame.

Functions

func ALPNValue

func ALPNValue(protocols []string) string

ALPNValue returns the two ALPN characters that JA4 and JA4S carry.

It returns `00` when the protocol list is empty, and when the first ALPN value is empty. It returns the first byte and the last byte when both bytes fall inside the printable ASCII range 0x20-0x7E. It repeats the byte when the first ALPN value holds one alphanumeric byte. It returns `99` in every other case.

The FoxIO prose states a different rule, and a measurement contradicts the prose. `technical_details/JA4.md:95` states the first and last character of the hexadecimal form of the whole first ALPN value. The FoxIO vector `tls-non-ascii-alpn.pcapng` holds `99` for the first ALPN value `0xba 0xad`, and `.claude/rules/parity.md` rule 1 states that the vector decides. `docs/specs/foxio/JA4.md` R18 and R19 record the split, and Reading 5 records the tshark text form that causes it.

The port issues `Crank-Git/ja4plus#127`, `Crank-Git/ja4plus#141` and `Crank-Git/ja4plus#162` hold the ruling, and `ja4_alpn_parity_test.go` holds the separating packets. Issue #50 adopted the rule here.

Every `%c` below writes a byte of 0x7E or lower, and each guard keeps that true. `%c` reads its argument as a code point, so a byte above 0x7F would reach the fingerprint as two UTF-8 bytes. A guard that widens past 0x7E must build the string from a byte slice.

func BuildClientHello

func BuildClientHello(version uint16, ciphers []uint16, extensions []TLSExtension) []byte

BuildClientHello constructs a raw TLS ClientHello payload from components.

func BuildServerHello

func BuildServerHello(version uint16, cipher uint16, extensions []TLSExtension) []byte

BuildServerHello constructs a raw TLS ServerHello payload.

func CheckTunnel

func CheckTunnel(packet gopacket.Packet) error

CheckTunnel returns a non-fatal error when the parser reads no inner packet from a tunnel. It returns nil for a packet that carries no tunnel.

It returns ErrTunnelDepthExceeded when the packet nests more than MaxTunnelDepth tunnel layers. It returns ErrTunnelPayloadUnread when it reads neither an IPv4 layer nor an IPv6 layer inside the innermost tunnel layer.

func ComputeHASSH

func ComputeHASSH(info *KEXINITInfo, isServer bool) string

ComputeHASSH computes the HASSH fingerprint from a KEXINIT. For client (isServer=false): MD5(kex;encryption_c2s;mac_c2s;compression_c2s) For server (isServer=true): MD5(kex;encryption_s2c;mac_s2c;compression_s2c)

func DecodeVarint

func DecodeVarint(data []byte, pos int) (uint64, int, error)

DecodeVarint decodes a QUIC variable-length integer from data at position pos. Returns the decoded value and the new position after the varint, or an error.

func DecryptQUICPacketWithSecret

func DecryptQUICPacketWithSecret(payload, secret []byte, connectionIDLength int) ([]byte, error)

DecryptQUICPacketWithSecret returns the frame bytes of one QUIC packet, which the caller's traffic secret protects. The payload starts at the first byte of the packet, and a longer buffer is allowed, because one datagram carries more than one packet. connectionIDLength states the Destination Connection ID length of a short header packet. A long header packet carries its own lengths, so it ignores that argument. It returns ErrNoSecret when the caller supplies no secret. It returns a non-fatal error for a packet it cannot read, and it never panics.

func DeriveInitialKeys

func DeriveInitialKeys(dcid []byte, version uint32) (key, iv, hpKey []byte, err error)

DeriveInitialKeys derives the client key, IV, and header protection key from the Destination Connection ID for a QUIC Initial packet.

func DeriveQUICKeys

func DeriveQUICKeys(secret []byte) (key, iv, hpKey []byte, err error)

DeriveQUICKeys returns the packet protection key, the initialization vector and the header protection key of one TLS traffic secret. RFC 9001 Section 5.1 states the three labels `quic key`, `quic iv` and `quic hp`. It returns an error for a secret of another length, because the library reads the SHA-256 key schedule of TLS_AES_128_GCM_SHA256 only.

func DeriveServerInitialKeys

func DeriveServerInitialKeys(dcid []byte, version uint32) (key, iv, hpKey []byte, err error)

DeriveServerInitialKeys derives the server key, IV, and header protection key from the Destination Connection ID of the client's Initial packet.

func FilterGreaseValues

func FilterGreaseValues(values []uint16) []uint16

FilterGreaseValues returns a new slice with all GREASE values removed.

func GetGroupingIPInfo

func GetGroupingIPInfo(packet gopacket.Packet) (srcIP, dstIP string, ok bool)

GetGroupingIPInfo returns the address pair that collects packets into one connection. It reads the innermost address layer. It reports false when the packet carries no address layer the parser reads.

The grouping pair reads the inner layer, because a mirror sends both directions of one session from one outer address pair. The outer pair separates no direction there, and one connection then holds two measurement points that belong to two endpoints.

It returns no time-to-live, because the time-to-live reads the outer layer that GetIPInfo returns.

func GetIPInfo

func GetIPInfo(packet gopacket.Packet) (srcIP, dstIP string, ttl uint8, ok bool)

GetIPInfo extracts source/destination IP addresses and TTL from a packet. Supports both IPv4 and IPv6. For IPv6, ttl is the HopLimit field.

It reads the outermost address layer, which is the layer a `FingerprintResult` reports and the layer the time-to-live comes from. Use GetGroupingIPInfo for the address pair that collects packets into one connection. `docs/specs/spec.md` `## Changelog` records the ruling of 2026-08-11 that separates the two.

func GetPacketTimestamp

func GetPacketTimestamp(packet gopacket.Packet) time.Time

GetPacketTimestamp returns the packet's capture timestamp.

func GetTCPLayer

func GetTCPLayer(packet gopacket.Packet) *layers.TCP

GetTCPLayer extracts the TCP layer of the innermost packet, or nil if not present.

A tunnel carries its own transport layer, and that layer names the tunnel and not the connection. The search therefore starts after the innermost tunnel layer.

func GetTCPPayload

func GetTCPPayload(packet gopacket.Packet) []byte

GetTCPPayload extracts the TCP payload bytes from a packet.

func GetUDPLayer

func GetUDPLayer(packet gopacket.Packet) *layers.UDP

GetUDPLayer extracts the UDP layer of the innermost packet, or nil if not present.

A tunnel carries its own transport layer, and that layer names the tunnel and not the connection. The search therefore starts after the innermost tunnel layer. A VXLAN packet on UDP port 4789 that carries TCP inside returns nil.

func HTTPMessageIsComplete

func HTTPMessageIsComplete(payload []byte, req *HTTPRequest) bool

HTTPMessageIsComplete reports whether the payload holds the whole HTTP request.

A request that names a byte count in its `Content-Length` header is complete when the payload after the header block reaches that count. A request that names no count is complete at the end of the header block.

The maintainer ruled this gate on 2026-08-13, at issue #455. The port's issue Crank-Git/ja4plus#607 carries the other half. `zeek/ja4h/main.zeek:186` computes the JA4H value in `event http_message_done(c: connection, is_orig: bool, stat: http_message_stat)`. That file holds no handler that flushes a partial request. This gate follows that shape, so a request whose body never completes reaches no value.

A `Content-Length` value that is not a byte count names no count, so the request is complete at the end of the header block. That reading keeps a malformed header from holding a value for the life of the stream.

func HasQUICLongHeader

func HasQUICLongHeader(payload []byte) bool

HasQUICLongHeader reports whether a UDP payload carries a QUIC long-header packet. It returns false for each of these payloads:

  • a payload shorter than 5 bytes;
  • a payload that carries a short header;
  • a payload that carries a version negotiation packet.

It reads no packet type, because a version this parser does not know still carries a long header. RFC 9000 Section 17.2 states the form.

func HoldsAHeaderBlockTerminator

func HoldsAHeaderBlockTerminator(payload []byte) bool

HoldsAHeaderBlockTerminator reports whether the payload holds the empty line that ends an HTTP header block.

`holdsACompleteHTTPRequest` in `ja4l.go` reads this function. That gate held its own pair of fixed byte groups until #685, and the pair declined the terminator `\n\r\n`. One reader now answers the question for every caller, so the JA4H value and the JA4L measurement point read one rule.

**It converts no payload to text, and the JA4L packet path still pays one conversion before it reaches this function.** `holdsACompleteHTTPRequest` in `ja4l.go` calls `IsHTTPRequest` first on the same bytes, and `IsHTTPRequest` writes `s := string(payload)`. Escape analysis of `IsHTTPRequest` reports `string(payload) escapes to heap`, measured on an Apple M4 on 2026-08-15 UTC. **This citation names the function and no line.** The `file:line` that `-gcflags='-m'` prints moves with every comment above it, and the #440 convention keeps a citation that a later edit cannot falsify. #685 measured one 8192-byte conversion at 8192 B and one allocation on the same machine on the same day.

**So the `[]byte` parameter saves a second conversion on the JA4L path.** It saves the only conversion of a caller that reads no request line first. The doc comment of #685 named the JA4L path as the reason for the whole saving. Batch #708 measured that the path already pays one. **Issue #685 is the reversal path.**

func IsGreaseValue

func IsGreaseValue(value uint16) bool

IsGreaseValue checks if a TLS value is a GREASE value. GREASE values match the pattern 0x?A?A where the high byte equals the low byte.

func IsHTTPRequest

func IsHTTPRequest(payload []byte) bool

IsHTTPRequest returns true if payload looks like an HTTP request.

func IsQUICHandshakePacket

func IsQUICHandshakePacket(payload []byte) bool

IsQUICHandshakePacket reports whether a UDP payload carries a QUIC Handshake packet. It returns false for every payload that HasQUICLongHeader returns false for. A version this parser does not know reads the version 1 type values, because RFC 9000 Section 17.2 states them and only RFC 9369 Section 3.2 moves them.

func IsSSHPacket

func IsSSHPacket(payload []byte) bool

IsSSHPacket checks if payload looks like SSH traffic. SSH packets start with "SSH-" (banner) or have SSH binary packet framing.

func IsTLSHandshake

func IsTLSHandshake(payload []byte) bool

IsTLSHandshake returns true if the payload begins with a TLS Handshake record header.

func OIDToHex

func OIDToHex(oidString string) string

OIDToHex converts a dotted OID string to its ASN.1 DER hex encoding.

The first two components are combined per ASN.1 rules: first*40 + second. Every component, including that sum, uses Variable-Length Quantity (VLQ) encoding.

Example: "2.5.4.3" -> "550403" (0x55 = 2*40+5, 0x04 = 4, 0x03 = 3)

func QuotedTCPHeader

func QuotedTCPHeader(packet gopacket.Packet) *layers.TCP

QuotedTCPHeader returns the TCP header that an ICMP error message of the packet quotes. It returns nil when the packet carries no ICMP error message, and it returns nil when the quoted bytes hold no complete TCP header.

The maintainer ruled split T1 on 2026-08-14, under #484, and the library reads that header. `gopacket` decodes no IP layer and no TCP layer inside an ICMP payload, so GetTCPLayer reads none of them and this function reads the payload itself. #484 is the reversal path, and the port half is `Crank-Git/ja4plus#610`.

The payload is untrusted input, so the read bounds the IP header length, the IP total length, the TCP data offset and the TCP option list before it slices. It reads an IPv4 header alone, because no capture of the corpus carries an ICMPv6 error message.

func ReassembleCryptoFrames

func ReassembleCryptoFrames(fragments []CryptoFragment) []byte

ReassembleCryptoFrames reassembles potentially fragmented CRYPTO frame data into a contiguous byte slice ordered by offset. It drops a fragment that reaches past MaxCryptoBufferBytes, so the buffer it returns holds MaxCryptoBufferBytes bytes at most. The port drops one fragment and keeps the rest at `ja4plus/utils/quic_utils.py:322`, and this reader matches that rule.

func TLS13ContentOfStream added in v1.1.0

func TLS13ContentOfStream(stream []byte, keys *TLS13RecordKeys, skip uint64, innerType byte) ([]byte, uint64)

TLS13ContentOfStream returns the content of each protected record of one stream that carries the wanted inner content type, and the count of records the keys opened.

The walk reads the stream from the first byte, and it steps over each record by the length field of that record. It counts the protected records, because RFC 8446 section 5.3 states that the sequence number restarts at 0 at each key change and no record carries its own number. An unprotected record carries no sequence number, so the count reads the records of type TLSRecordTypeApplicationData alone.

skip names the count of protected records that an earlier traffic key protects. A caller that reads the handshake of a stream passes 0, and a caller that reads the application data passes the count this function returned for the handshake.

It stops at the first protected record the keys do not open, because the sender changes the traffic key after the handshake. It returns the content it read up to that record. It reads no record of a stream whose length field passes the end.

func TLSVersionString

func TLSVersionString(version uint16) string

TLSVersionString maps a TLS version number to the JA4 version string.

func TruncatedHash

func TruncatedHash(input string) string

TruncatedHash computes SHA-256 of the input string and returns the first 12 hex characters (6 bytes) of the lowercase hex digest. Returns "000000000000" for empty input (NOT the SHA-256 of an empty string).

func TruncatedHashNoSentinel

func TruncatedHashNoSentinel(input string) string

TruncatedHashNoSentinel returns the first 12 characters of the lowercase SHA-256 hex digest of the input. It hashes the empty string, so an empty list reaches `e3b0c44298fc` and never the zero sentinel.

JA4H part b and the three JA4X parts are the callers outside this file, and `TruncatedHash` above calls it for a non-empty input. R18 of `docs/specs/foxio/JA4H.md` states `Truncated SHA256 hash of Headers, in the order they appear`, and it names no sentinel. R27 of the same page confines the sentinel to part c and to part d. The Rust reference and the Zeek package write the sentinel in part b, and a rank 1 image rule outranks an implementation. The maintainer ruled the split on 2026-08-14.

Issue #527 is the reversal path of the JA4H half, and the port half is `Crank-Git/ja4plus#612`. Issue #582 is the reversal path of the JA4X half, and the port half is `Crank-Git/ja4plus#619`. R12 of `docs/specs/foxio/JA4X.md` holds the JA4X split.

func TunnelDepth

func TunnelDepth(packet gopacket.Packet) int

TunnelDepth returns the count of tunnel layers the packet carries. It returns 0 for a packet that carries no tunnel.

Types

type ClientHello

type ClientHello struct {
	// Random holds the 32-byte Random field of the message, and nil when the message is
	// too short to carry it.
	//
	// A key log names the connection by this value, so a caller that decrypts a record
	// reads it. `KeyLog.Secret` of the root package takes it as the connection name.
	Random              []byte
	Version             uint16
	CipherSuites        []uint16
	Extensions          []uint16 // extension type IDs in original order
	SNI                 string   // hostname, or "" if absent/malformed
	HasSNI              bool     // true if SNI extension (0x0000) was present
	ALPNProtocols       []string
	SupportedVersions   []uint16
	SignatureAlgorithms []uint16
	IsQUIC              bool
	IsDTLS              bool
}

ClientHello holds parsed fields from a TLS ClientHello message.

func ClientHelloFromCryptoFragments

func ClientHelloFromCryptoFragments(fragments []CryptoFragment) (*ClientHello, error)

ClientHelloFromCryptoFragments reassembles CRYPTO fragments and parses a ClientHello. Returns nil, nil if the data is not a ClientHello. Returns nil, nil while a fragment of the handshake message is still missing, so that the caller collects the fragments of another QUIC Initial packet.

func ParseClientHello

func ParseClientHello(payload []byte) (*ClientHello, error)

ParseClientHello parses a TLS ClientHello from raw TCP payload bytes. Returns nil, nil if the payload is not a TLS ClientHello. Returns nil, error if it looks like a ClientHello but is truncated/malformed.

It reads the first handshake record of the payload. It steps over each record in front of that one. A TLS 1.3 client sends a ChangeCipherSpec record before its second client hello. Issue #295 records the values that the first-byte reader missed.

func ParseQUICInitial

func ParseQUICInitial(payload []byte) (*ClientHello, error)

ParseQUICInitial parses a QUIC Initial packet and extracts the TLS ClientHello. Returns nil, nil if the payload is not a QUIC Initial packet. Returns nil, error if it looks like a QUIC Initial but decryption/parsing fails. Returns nil, nil when the CRYPTO fragments of the packet cover no complete handshake message. It reads one datagram, so a client that splits a client hello across two datagrams reaches this decline. ClientHelloFromCryptoFragments serves the caller that collects the fragments of several packets.

type CryptoFragment

type CryptoFragment struct {
	Offset uint64
	Data   []byte
}

CryptoFragment represents a CRYPTO frame fragment with offset and data.

func CollectCryptoFragments

func CollectCryptoFragments(collected []CryptoFragment, fragments []CryptoFragment) ([]CryptoFragment, error)

CollectCryptoFragments adds the fragments of one packet to the fragments the caller holds, and returns the whole set. It drops a fragment that names an offset above MaxCryptoBufferBytes, because such a fragment describes no real client hello. It returns an error when the collected bytes reach MaxCryptoBufferBytes. The caller then drops the connection state, because the sender never completes a client hello.

func DecryptQUICInitialCrypto

func DecryptQUICInitialCrypto(payload []byte) (fragments []CryptoFragment, dcid []byte, err error)

DecryptQUICInitialCrypto decrypts a QUIC Initial packet and returns the raw CRYPTO frame fragments without attempting to parse a ClientHello. Returns the DCID for key correlation across packets. Returns nil, nil, nil if the payload is not a QUIC Initial.

It declines a packet that the derived key does not authenticate, and it returns no error for one. It derives the client keys of the Destination Connection ID that the packet holds, so it authenticates a client Initial packet alone. The server derives its own keys from the Destination Connection ID that the client sent, and RFC 9001 Section 5.2 states that derivation input. So a server Initial packet is a packet of the other role, and never a defect of the input. Issue #501 records the decline, and it is the reversal path.

The decline also covers a corrupted client Initial packet. `crypto/cipher` reports one error value for every failure of Open, so this function separates a wrong role from a corrupted packet at no point after Open.

It reports an error for a malformed packet that it reads before Open, and for a malformed frame that it reads after Open. A payload shorter than the authentication tag reaches the first case. A truncated CRYPTO frame reaches the second case.

func ParseCryptoFrames

func ParseCryptoFrames(data []byte) ([]CryptoFragment, error)

ParseCryptoFrames extracts CRYPTO frame fragments from decrypted QUIC payload.

type HTTP2Reader added in v1.1.0

type HTTP2Reader struct {
	// contains filtered or unexported fields
}

HTTP2Reader decodes the client half of one HTTP/2 connection, one chunk at a time.

**One reader holds one HPACK decoder, and that is the reason this type exists.** RFC 7541 section 2.3.2 states that the dynamic table serves the whole connection, so a block that names an entry of an earlier block decodes only after that earlier block. A caller that starts a decoder at each chunk therefore reads wrong header names.

The reader holds the bytes of one field block that no chunk completed, and http2MaxFieldBlockBytes bounds them. It stores no byte of a frame that carries no field block, so a request body of any length costs no memory.

One HTTP2Reader serves one connection, and one goroutine. `.claude/rules/concurrency.md` states that contract.

func NewHTTP2Reader added in v1.1.0

func NewHTTP2Reader() *HTTP2Reader

NewHTTP2Reader returns a reader of the client half of one HTTP/2 connection.

func (*HTTP2Reader) Failed added in v1.1.0

func (r *HTTP2Reader) Failed() bool

Failed reports whether the reader stopped. A reader that stopped reads no later request.

func (*HTTP2Reader) Pending added in v1.1.0

func (r *HTTP2Reader) Pending() int

Pending returns the bytes that no call read.

func (*HTTP2Reader) Read added in v1.1.0

func (r *HTTP2Reader) Read(chunk []byte) []*HTTPRequest

Read returns one HTTPRequest for each request that the chunk completes.

chunk holds the decrypted client bytes that follow the bytes of every earlier call. The first call opens at the first byte of the connection preface. It returns nil for a stream that carries no connection preface, and nil for a chunk that completes no header block. It reads no length field before it bounds that field, and it never panics.

It returns http2MaxRequests requests at most. The reader holds the bytes above that bound, so the next call returns the requests they carry. It stops at the first header block the decoder does not read, because the dynamic table then disagrees with the encoder and every later block decodes to the wrong names.

type HTTPRequest

type HTTPRequest struct {
	Method      string            // e.g. "GET", "POST"
	Path        string            // request path
	Version     string            // e.g. "HTTP/1.1"
	HeaderNames []string          // header names in original wire order, original case
	Headers     map[string]string // lowercase header name -> value
	Cookies     map[string]string // cookie name -> value
	CookieNames []string          // cookie field names in parse order
	Language    string            // Accept-Language value
	Referer     string            // Referer value
}

HTTPRequest holds parsed HTTP request data with headers in original order. Parsed from raw TCP payload bytes without using net/http (which sorts headers).

func HTTP2Requests added in v1.1.0

func HTTP2Requests(stream []byte) []*HTTPRequest

HTTP2Requests returns one HTTPRequest for each request that the client half of one HTTP/2 connection carries.

stream holds the decrypted client bytes from the first byte of the connection preface, and it holds the whole connection. It returns nil for a stream that carries no connection preface, and nil for a stream that completes no header block.

**A caller that reads one connection over several packets calls `HTTP2Reader` instead.** This function starts one decoder, so it reads the whole stream at each call and a suffix of the stream decodes to wrong header names. It returns http2MaxRequests requests at most.

func ParseHTTPRequest

func ParseHTTPRequest(payload []byte) *HTTPRequest

ParseHTTPRequest parses an HTTP request from raw TCP payload bytes. Returns nil if the payload is not a valid HTTP request. Returns nil when the header block has not ended, because a later segment carries a header that changes the fingerprint. #286 records the early answer this rule replaces. Headers are preserved in their original wire order in HeaderNames.

type KEXINITInfo

type KEXINITInfo struct {
	KexAlgorithms           string // index 0
	ServerHostKeyAlgorithms string // index 1
	EncryptionC2S           string // index 2
	EncryptionS2C           string // index 3
	MACC2S                  string // index 4
	MACS2C                  string // index 5
	CompressionC2S          string // index 6
	CompressionS2C          string // index 7
}

KEXINITInfo holds parsed SSH KEXINIT algorithm lists. The 10 name-lists are: kex_algorithms, server_host_key_algorithms, encryption_c2s, encryption_s2c, mac_c2s, mac_s2c, compression_c2s, compression_s2c, languages_c2s, languages_s2c.

func ParseKEXINIT

func ParseKEXINIT(payload []byte) *KEXINITInfo

ParseKEXINIT parses algorithm name-lists from an SSH KEXINIT message. The payload must start at the msg_type byte (0x14 = 20).

func ParseKEXINITFromPacket

func ParseKEXINITFromPacket(data []byte) *KEXINITInfo

ParseKEXINITFromPacket returns the KEXINIT fields of one SSH binary packet payload. The payload starts at the 4-byte packet length. It returns nil when the payload holds no KEXINIT that this parser reads.

type SSHMessageTracker

type SSHMessageTracker struct {
	// contains filtered or unexported fields
}

SSHMessageTracker reports which TCP segment of one direction completes an SSH message.

The FoxIO reference counts the packets `tshark` labels `ssh`. `tshark` reassembles an SSH message that spans two TCP segments, and it labels only the segment that completes the message. `wireshark/source/packet-ja4.c:1469` counts one packet for each `ssh.direction` field, and `python/ja4ssh.py:94` counts the packet whose protocol list holds `ssh`. This type reproduces that boundary, so the JA4SSH packet count reads the SSH message and not the TCP segment.

The tracker reads the length field only while the direction sends plaintext. Before the version line, and after SSH_MSG_NEWKEYS, it reports every segment as one SSH packet, because neither phase carries a length a reader trusts.

One tracker follows one direction of one connection. The zero value is ready to read the first segment of a direction. One tracker serves one goroutine, and no lock guards it.

func NewSSHMessageTracker

func NewSSHMessageTracker() *SSHMessageTracker

NewSSHMessageTracker returns a tracker that stands before the version line of one direction. The zero value of SSHMessageTracker reads the same way, so a caller that embeds the type needs no call.

func (*SSHMessageTracker) AddSegment

func (t *SSHMessageTracker) AddSegment(payload []byte, seq uint32) []int

AddSegment returns the length of each segment that completes an SSH message.

The tracker reads the payload of the direction in sequence order. It drops a segment the direction already sent, and it holds a segment that arrives before its predecessor until the predecessor arrives.

The returned slice holds one entry for each segment the FoxIO reference counts as one SSH packet, in sequence order. It is empty for an empty payload, for a duplicate segment, and for a segment that holds part of a message and no message end.

func (*SSHMessageTracker) CompletesMessage

func (t *SSHMessageTracker) CompletesMessage(payload []byte) bool

CompletesMessage reports whether at least one SSH message ends in this segment.

It reports false for an empty segment, and false for a segment that holds part of a message and no message end. A caller that holds no sequence number calls this method, and it reads the segments in the order they arrive.

type SSHPacketInfo

type SSHPacketInfo struct {
	Type    string // "banner", "kexinit", "data"
	Payload []byte
}

SSHPacketInfo holds parsed SSH packet information.

func ParseSSHPacket

func ParseSSHPacket(payload []byte) *SSHPacketInfo

ParseSSHPacket extracts SSH packet info (type, payload size). Returns nil if the payload is not a recognized SSH packet.

type ServerHello

type ServerHello struct {
	Version           uint16
	CipherSuite       uint16
	Extensions        []uint16
	ALPNProtocol      string
	SupportedVersions []uint16
	IsQUIC            bool
	IsDTLS            bool
}

ServerHello holds parsed fields from a TLS ServerHello message.

func ParseQUICServerInitial

func ParseQUICServerInitial(payload []byte, clientDCID []byte) (*ServerHello, error)

ParseQUICServerInitial parses a QUIC server Initial packet and extracts the TLS ServerHello. The clientDCID is the Destination Connection ID from the client's Initial packet, needed to derive the server's decryption keys. Returns nil, nil if the payload is not a QUIC Initial packet.

func ParseServerHello

func ParseServerHello(payload []byte) (*ServerHello, error)

ParseServerHello parses a TLS ServerHello from raw TCP payload bytes. Returns nil, nil if the payload is not a TLS ServerHello.

type TCPStreamReassembler

type TCPStreamReassembler struct {

	// MaxStreams bounds the count of the streams the table holds.
	MaxStreams int
	// MaxBytes bounds one stream twice. It bounds the bytes that the stream stores, and it
	// bounds the run that GetStream returns. Issue #567 added the first of the two, because
	// a stream that stored every segment grew without a bound.
	MaxBytes int
	// MaxSegments bounds the segments that one stream stores. The maintainer ruled it on
	// 2026-08-14, and issue #596 holds the ruling and the reversal path.
	//
	// MaxBytes bounds the bytes and it does not bound the count, so a sender of one-byte
	// segments reaches 1048576 segments inside a byte bound of 1048576. The deduplication
	// index of issue #596 costs 53.3 bytes for each stored segment, measured on 2026-08-14,
	// so the count drives the memory rather than the bytes.
	//
	// NewTCPStreamReassembler sets DefaultMaxSegments. A caller states another value on this
	// field, as it does on MaxBytes.
	MaxSegments int
	// contains filtered or unexported fields
}

TCPStreamReassembler reassembles TCP streams using sequence numbers. Handles out-of-order segments, duplicates, and overlaps. Evicts oldest streams (LRU) when MaxStreams is exceeded.

One TCPStreamReassembler serves one goroutine. It holds state that no lock guards, and `.claude/rules/concurrency.md` states that contract.

func NewTCPStreamReassembler

func NewTCPStreamReassembler(maxStreams, maxBytes int) *TCPStreamReassembler

NewTCPStreamReassembler creates a reassembler with the given limits.

The segment bound takes DefaultMaxSegments, so this signature states two limits and the reassembler holds three. A caller that needs another segment bound writes MaxSegments.

func (*TCPStreamReassembler) AddSegment

func (r *TCPStreamReassembler) AddSegment(key string, seq uint32, data []byte)

AddSegment adds a TCP segment to a stream identified by key.

func (*TCPStreamReassembler) GetStream

func (r *TCPStreamReassembler) GetStream(key string) []byte

GetStream reassembles and returns contiguous data from the lowest sequence number. Returns data up to the first gap or MaxBytes, whichever comes first.

func (*TCPStreamReassembler) RemoveStream

func (r *TCPStreamReassembler) RemoveStream(key string)

RemoveStream removes a stream from tracking.

type TLS13RecordKeys added in v1.1.0

type TLS13RecordKeys struct {
	// Key holds the [sender]_write_key of RFC 8446 section 7.3.
	Key []byte
	// IV holds the [sender]_write_iv of RFC 8446 section 7.3.
	IV []byte
}

TLS13RecordKeys holds the write key and the write initialization vector of one TLS 1.3 traffic secret.

One TLS13RecordKeys value does not change after DeriveTLS13RecordKeys returns it, so any number of goroutines read one value. It holds no sequence number, because the sequence number belongs to the stream and not to the key.

func DeriveTLS13RecordKeys added in v1.1.0

func DeriveTLS13RecordKeys(secret []byte) (*TLS13RecordKeys, error)

DeriveTLS13RecordKeys returns the record keys of one TLS 1.3 traffic secret.

RFC 8446 section 7.3 states the two derivations, and this function writes the labels TLS13KeyLabel and TLS13IVLabel. It returns ErrNoSecret when the caller supplies no secret. It returns an error for a secret of another length, because the library reads the SHA-256 key schedule of TLS_AES_128_GCM_SHA256 only. A SHA-384 suite states a 48-byte secret, and this error names it. A ChaCha20-Poly1305 suite states a 32-byte secret, so this function returns keys for it and the record then fails the authentication check. Issue #492 is the reversal path of that decline.

func (*TLS13RecordKeys) Open added in v1.1.0

func (k *TLS13RecordKeys) Open(record []byte, sequence uint64) ([]byte, byte, error)

Open returns the content and the inner content type of one protected TLS 1.3 record.

record starts at the first byte of the record header, and it holds the whole record. A longer buffer is allowed, because one stream carries more than one record. sequence names the record sequence number of the direction. RFC 8446 section 5.3 states that the first record under one traffic key uses the number 0. It returns a non-fatal error for a record it cannot read, and it never panics.

The construction follows RFC 8446 section 5.2 and section 5.3. The additional data is the record header:

additional_data = TLSCiphertext.opaque_type ||
                  TLSCiphertext.legacy_record_version ||
                  TLSCiphertext.length

The nonce takes two steps:

  1. The 64-bit record sequence number is encoded in network byte order and padded to the left with zeros to iv_length.

  2. The padded sequence number is XORed with either the static client_write_iv or server_write_iv (depending on the role).

type TLS13StreamReader added in v1.1.0

type TLS13StreamReader struct {
	// contains filtered or unexported fields
}

TLS13StreamReader opens the protected records of one direction, one chunk at a time.

**The reader reads each byte once, and `TLS13ContentOfStream` reads the whole stream at each call.** A caller that holds a growing stream therefore bounds that stream, and the bound then stops the connection rather than delaying it. #753 records the limit that this reader removes, and `JA4HFingerprinter` is the caller that reaches it.

The reader holds the sequence number of each key. RFC 8446 section 5.3 states that the number restarts at 0 at each key change, and no record carries its own number. The reader also holds the bytes of one incomplete record, and TLS13MaxRecordBytes bounds them.

One TLS13StreamReader serves one direction of one connection, and one goroutine. `.claude/rules/concurrency.md` states that contract.

func NewTLS13StreamReader added in v1.1.0

func NewTLS13StreamReader(handshake, application *TLS13RecordKeys) *TLS13StreamReader

NewTLS13StreamReader returns a reader of one direction of one connection.

handshake names the client handshake traffic key, and the caller passes nil where the direction carries no handshake record under that key. application names the client application traffic key, and a reader without it opens no record.

func (*TLS13StreamReader) Failed added in v1.1.0

func (r *TLS13StreamReader) Failed() bool

Failed reports whether the reader stopped. A reader that stopped opens no later record.

func (*TLS13StreamReader) Pending added in v1.1.0

func (r *TLS13StreamReader) Pending() int

Pending returns the bytes of the record that no call completed.

func (*TLS13StreamReader) Read added in v1.1.0

func (r *TLS13StreamReader) Read(chunk []byte) []byte

Read returns the application-data content of each record that the chunk completes.

chunk holds the bytes that follow the bytes of every earlier call, and never a byte that an earlier call already read. The reader holds the part of a record that the chunk leaves incomplete, so the caller keeps no byte of its own. It returns nil after a record that the keys do not open, because the sequence number of every later record then disagrees with the sender. It reads no length field before it bounds that field, and it never panics.

type TLSExtension

type TLSExtension struct {
	Typ  uint16
	Data []byte
}

TLSExtension is a helper type for building TLS extension data in tests.

func MakeALPNExtension

func MakeALPNExtension(protocols ...string) TLSExtension

MakeALPNExtension creates a TLS ALPN extension with the given protocols.

func MakeSNIExtension

func MakeSNIExtension(hostname string) TLSExtension

MakeSNIExtension creates a TLS SNI extension with the given hostname.

func MakeSignatureAlgorithmsExtension

func MakeSignatureAlgorithmsExtension(algs ...uint16) TLSExtension

MakeSignatureAlgorithmsExtension creates a signature_algorithms extension.

func MakeSupportedVersionsClientExtension

func MakeSupportedVersionsClientExtension(versions ...uint16) TLSExtension

MakeSupportedVersionsClientExtension creates a client supported_versions extension.

func MakeSupportedVersionsServerExtension

func MakeSupportedVersionsServerExtension(version uint16) TLSExtension

MakeSupportedVersionsServerExtension creates a server supported_versions extension.

type X509Identifiers

type X509Identifiers struct {
	// Issuer holds the identifiers of the issuer RDNSequence, in the order the certificate
	// states them.
	Issuer []string
	// Subject holds the identifiers of the subject RDNSequence, in the same order.
	Subject []string
	// Extensions holds the identifier of each extension. It is empty when the certificate
	// carries no extension, because RFC 5280 section 4.1 makes the field optional.
	Extensions []string
}

X509Identifiers holds the three object identifier lists that JA4X reads.

Each string is the lowercase hexadecimal form of the content octets of one ASN.1 OBJECT IDENTIFIER. R9 of `docs/specs/foxio/JA4X.md` states that form, and `testdata/foxio/reference/rust/ja4x/src/lib.rs:68` writes `hex::encode(a.attr_type().as_bytes())` for the issuer list. `:80` writes `hex::encode(ext.oid.as_bytes())` for the extension list.

func ReadX509Identifiers

func ReadX509Identifiers(der []byte) (X509Identifiers, bool)

ReadX509Identifiers returns the issuer, subject and extension object identifiers of a DER-encoded certificate. The second return value is false when the certificate does not read.

The reader walks the ASN.1 structure of TBSCertificate, and it skips the public key. So it answers true for a certificate that `x509.ParseCertificate` refuses. It validates no signature, and it makes no trust decision.

Every packet is untrusted input, so the reader compares each length field with the remaining input before it slices.

Jump to

Keyboard shortcuts

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