flow

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package flow maintains the live table of bidirectional conversations assembled from the packet stream.

The table is the memory of the sensor: detectors ask it "what has this host been doing" rather than trying to reason about individual packets. Because it sits on the hot path — every single packet touches it — its cost model matters more than anything else in the program.

Index

Constants

View Source
const (
	DefaultIdleTimeout = 2 * time.Minute
	DefaultMaxFlows    = 500_000
)

Default table tuning. These are deliberately conservative: a mid-range host running tracehound on a busy /24 sits comfortably inside them.

Variables

This section is empty.

Functions

This section is empty.

Types

type Options

type Options struct {
	// IdleTimeout is how long a flow may go unobserved before it is reaped.
	IdleTimeout time.Duration
	// MaxFlows caps table size. When exceeded, the least-recently-touched
	// flows are evicted early. This bounds memory against a scan or a flood
	// that would otherwise create millions of one-packet flows.
	MaxFlows int
	// OnEvict is called for each flow dropped to stay under MaxFlows.
	//
	// Eviction used to be silent, and everything holding per-flow state keyed
	// off Reap instead, so an evicted flow's state was never released. It is
	// called after the table's lock is dropped, so an implementation may take
	// its own locks, and it must not call back into the table.
	OnEvict func(model.FlowKey)
}

Options configures a Table.

type Stats

type Stats struct {
	Active  int    `json:"active"`
	Created uint64 `json:"created"`
	Expired uint64 `json:"expired"`
	Evicted uint64 `json:"evicted"`
}

Stats reports table counters for the /healthz endpoint and benchmarks.

type Table

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

Table is a concurrent map of active flows ordered by recency.

Expiry is the interesting part. The obvious implementation scans every entry on a timer, which is O(n) per sweep and degrades exactly when the table is large — i.e. during the scan or flood you most want to detect. Instead the table threads every entry onto a doubly-linked list ordered by touch time. Reaping then pops from the head while the head is too old, costing O(expired) rather than O(total), and capacity eviction is the same pop from the head.

The ordering assumption is that packet timestamps are non-decreasing. That holds for live capture and for well-formed PCAPs; badly reordered input can leave a flow reaped slightly early, which is harmless (it is re-created on the next packet) and is why Reap compares against LastSeen rather than trusting list position alone.

func New

func New(opts Options) *Table

New returns an empty flow table.

func (*Table) Drain

func (t *Table) Drain() []model.Flow

Drain removes and returns every remaining flow. Used at end-of-capture so that flows still open when a PCAP runs out are not silently dropped.

func (*Table) Len

func (t *Table) Len() int

Len returns the number of active flows.

func (*Table) Observe

func (t *Table) Observe(p *model.Packet) (f *model.Flow, isNew bool)

Observe folds a packet into its flow, creating the flow if needed.

The returned pointer is owned by the table and is only safe to read while the caller is still on the packet-processing goroutine; callers that need to keep a flow must copy it. isNew reports whether this packet created the flow, which detectors use as the "new conversation" signal.

func (*Table) Reap

func (t *Table) Reap(now time.Time) []model.Flow

Reap removes every flow untouched since now-IdleTimeout and returns them.

Returned flows are copies, so the caller may hand them to detectors or storage on another goroutine without racing the table.

func (*Table) SetFingerprint added in v0.2.0

func (t *Table) SetFingerprint(key model.FlowKey, ja4, ja3, sni, alpn string)

SetFingerprint records TLS attributes on a flow, under the table lock.

The pipeline holds a *model.Flow returned by Observe and is the only goroutine that mutates it, but the API reads the same records concurrently through Snapshot. Assigning these fields directly would be a data race on a string header, which can hand a reader a pointer and a length that do not belong together. It happens at most once per flow, so the extra lock acquisition costs nothing measurable.

func (*Table) SetServerFingerprint added in v0.3.0

func (t *Table) SetServerFingerprint(key model.FlowKey, ja4s string)

SetServerFingerprint records the JA4S of a flow's server, under the table lock, for the same reason SetFingerprint takes it.

func (*Table) Snapshot

func (t *Table) Snapshot(limit int) []model.Flow

Snapshot copies the active flows, most recently touched first. limit <= 0 means no limit. This backs the API's flow listing.

func (*Table) Stats

func (t *Table) Stats() Stats

Stats returns a snapshot of the table counters.

Jump to

Keyboard shortcuts

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