assemble

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package assemble turns a packet stream into completed TLS handshake observations.

It exists because a naive "parse the ClientHello out of one packet" approach is wrong in exactly the case that matters most. A post-quantum key share is over a kilobyte on its own, so a ClientHello offering X25519MLKEM768 routinely exceeds a single TCP segment. A parser that does not reassemble silently drops the handshakes a post-quantum inventory exists to count, and reports a cleaner, more classical world than the one on the wire.

TCP reassembly is delegated to gopacket's reassembly package, which handles retransmission, overlap and reordering. What this package adds is a bounded prefix per direction, content-based TLS detection, and the pairing of the two directions into one observation.

Index

Constants

View Source
const (
	// DefaultMaxStreamPrefix bounds how much of each direction is retained.
	// A handshake is decided in the first few kilobytes; beyond that is
	// application data of no inventory interest. 32 KiB comfortably holds a
	// post-quantum ClientHello and a TLS 1.2 certificate chain with a long
	// intermediate list, which an 8 KiB cap would truncate.
	DefaultMaxStreamPrefix = 32 << 10

	// DefaultCloseTimeout is how long a quiet stream is kept before it is
	// flushed and reported with whatever was seen.
	DefaultCloseTimeout = 2 * time.Minute

	// DefaultMaxStreams caps concurrently tracked connections.
	//
	// M1 bounded memory per stream but not the number of streams, which is
	// fine for a capture file and not fine for a live host: a SYN flood, a
	// port scan, or simply a busy server multiplies the per-stream prefix
	// by an unbounded factor. At this cap the worst case is roughly
	// MaxStreams * MaxStreamPrefix * 2, so 64Ki * 32KiB * 2 would be far
	// too much — hence a cap chosen for an endpoint, not a tap.
	DefaultMaxStreams = 8192
)

Defaults chosen for endpoint inventory rather than for bulk capture.

View Source
const (
	TransportTCP  = "tcp"
	TransportQUIC = "quic"
)

Transport names the protocol carrying the handshake.

Variables

This section is empty.

Functions

This section is empty.

Types

type Assembler

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

Assembler feeds packets through TCP reassembly and reports TLS flows. It is not safe for concurrent use.

func New

func New(h Handler, opts Options) *Assembler

New returns an Assembler that calls h for every flow carrying a ClientHello.

func (*Assembler) Close

func (a *Assembler) Close()

Close flushes every remaining stream, emitting partial handshakes.

func (*Assembler) FlushOlderThan

func (a *Assembler) FlushOlderThan(t time.Time)

FlushOlderThan closes streams idle since before t, emitting whatever they yielded.

Live capture needs this on a timer rather than on packet arrival: a flow that completes on a quiet interface would otherwise sit unreported until the next packet from anyone, which on an idle host can be minutes.

func (*Assembler) Packet

func (a *Assembler) Packet(data []byte, ci gopacket.CaptureInfo, linkType layers.LinkType)

Packet feeds one captured packet.

Every TCP port is examined, not just 443. Restricting an inventory to the well-known port is how STARTTLS on 587, a database on 5432 and an internal service on 8443 all end up invisible, and "we found no weak ciphers" then means "we did not look". Non-TLS flows are rejected cheaply by content, on the first few bytes of each direction.

func (*Assembler) Stats

func (a *Assembler) Stats() Stats

Stats returns a snapshot of the counters.

type Flow

type Flow struct {
	// Transport is "tcp" or "quic". The handshake is the same either way —
	// QUIC carries TLS 1.3 — but what is visible differs: over QUIC the
	// certificate is at the Handshake level and never readable.
	Transport string

	ClientIP   netip.Addr
	ServerIP   netip.Addr
	ClientPort uint16
	ServerPort uint16
	FirstSeen  time.Time
	LastSeen   time.Time

	Client *tlsparse.ClientHello
	Server *tlsparse.ServerHello

	// PrefixTruncated is set when a direction hit MaxStreamPrefix, meaning
	// later handshake messages may have been dropped.
	PrefixTruncated bool
}

Flow is one observed connection and whatever of its TLS handshake was visible. Server is nil when the response was never captured; Client is never nil in an emitted flow.

type Handler

type Handler func(*Flow)

Handler receives each completed flow exactly once.

type Options

type Options struct {
	MaxStreamPrefix int
	CloseTimeout    time.Duration
	// MaxStreams caps concurrently tracked connections. Beyond it, new
	// connections are counted and ignored rather than tracked.
	MaxStreams int
}

Options configures an Assembler. The zero value is usable.

type Stats

type Stats struct {
	Packets     int64
	TCPPackets  int64
	UDPPackets  int64
	Streams     int64
	TLSFlows    int64
	RejectedTCP int64
	// StreamsDropped counts connections ignored because MaxStreams was
	// already reached. A non-zero value means the inventory is incomplete,
	// so it is reported rather than silently absorbed.
	StreamsDropped int64
	// LiveStreams is the number currently tracked.
	LiveStreams int64
}

Stats reports what the assembler saw. Streams counts every TCP connection examined; TLSFlows counts those that yielded a ClientHello.

Jump to

Keyboard shortcuts

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