collect

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package collect is the agent's flow collection loop: a Source observes connections on the host, an Aggregator folds them into one record per (source, destination, port, protocol, decision, rule) over the reporting window the control plane configures, holding at most a bounded number of distinct keys, a bounded Buffer holds closed windows until the Reporter ships them over ReportFlows, and overflow at either bound drops what does not fit and records the loss as an evidence gap. Per-connection records never leave the host (ADR-0009); the conntrack source is the first implementation behind the Source interface, and an eBPF source is a later additive one (ADR-0003).

Index

Constants

View Source
const DefaultBatchMax = 1000

DefaultBatchMax caps the records in one ReportFlowsRequest before the control plane has said otherwise in HelloAck.

View Source
const DefaultBufferRecords = 50_000

DefaultBufferRecords is the buffer capacity in records unless configured otherwise: an hour of busy windows in memory, and a bound the agent enforces on itself before it would ever burden the host (ADR-0011).

View Source
const DefaultGapCapacity = 1024

DefaultGapCapacity bounds the gaps held until they are shipped. Past it, gaps of the same kind and source are merged into wider intervals, so the bound costs precision, never the fact of a loss (ADR-0011).

View Source
const DefaultNetlinkBuffer = 4 << 20

DefaultNetlinkBuffer is the receive buffer a flow source asks for on its netlink socket unless configured otherwise: enough for a burst of tens of thousands of events while the source is busy, at a few megabytes of kernel memory per socket (ADR-0011).

View Source
const DefaultWindow = 60 * time.Second

DefaultWindow is the aggregation window before the control plane has said otherwise in HelloAck.

View Source
const DefaultWindowKeys = DefaultBufferRecords

DefaultWindowKeys bounds the distinct keys one open window holds unless configured otherwise: the closed-window buffer's figure, so one window can never hold more than the buffer would keep (ADR-0011).

Variables

This section is empty.

Functions

func Batches

func Batches(w *Window, batchMax int) []*innerwallv1.ReportFlowsRequest

Batches splits a window into requests of at most batchMax records, each carrying the window's bounds; an empty window is one request with none.

Types

type Aggregator

type Aggregator struct {
	// Gaps receives the overflow gap when a window closes; nil records
	// nothing.
	Gaps *Gaps
	// contains filtered or unexported fields
}

Aggregator folds observations into the open window. The window holds at most a bounded number of distinct keys: an observation for a new key past the bound is dropped, and the window records the loss as a window_overflow gap from the first drop to the window's close, with the observations dropped. Observations for keys already held still fold in.

func NewAggregator

func NewAggregator(start time.Time) *Aggregator

NewAggregator opens a window starting at start, holding at most DefaultWindowKeys keys.

func NewBoundedAggregator

func NewBoundedAggregator(start time.Time, maxKeys int) *Aggregator

NewBoundedAggregator opens a window starting at start holding at most maxKeys distinct keys; DefaultWindowKeys when maxKeys is not positive.

func (*Aggregator) Add

func (a *Aggregator) Add(o Observation)

Add folds one observation into the open window, or drops it when its key is new and the window is full.

func (*Aggregator) Close

func (a *Aggregator) Close(end time.Time) *Window

Close ends the open window at end, returns it, and opens the next one starting at end. A window that dropped observations records its window_overflow gap now: from the first drop to end, because every new key in that interval was lost.

func (*Aggregator) Cut added in v0.2.0

func (a *Aggregator) Cut(end, next time.Time) *Window

Cut ends the open window at end and opens the next one at next, for a window the wall clock stepped back under (see closeBounds): end is where the window's own clock reads at its close, and next is the stepped clock. Every observation in the window happened between its bounds on the window's own clock, whatever the stepped clock stamped on it, so each record's first and last seen, and the first drop of a window overflow, are held within the bounds.

func (*Aggregator) Dropped

func (a *Aggregator) Dropped() uint64

Dropped returns the observations the open window has dropped so far.

func (*Aggregator) Len

func (a *Aggregator) Len() int

Len returns the number of distinct keys in the open window.

func (*Aggregator) Start added in v0.2.0

func (a *Aggregator) Start() time.Time

Start returns the open window's start.

type Buffer

type Buffer struct {
	// Gaps records every overflow as a buffer_overflow gap over the
	// dropped window's bounds, with the records dropped; nil records
	// nothing.
	Gaps *Gaps
	// contains filtered or unexported fields
}

Buffer holds closed windows between the collector and the reporter. It is bounded in records; when a window does not fit, the oldest windows are dropped until it does and every dropped record is counted, so the heartbeat can tell the operator the flow map is incomplete.

func NewBuffer

func NewBuffer(capacity int) *Buffer

NewBuffer returns a buffer holding at most capacity records; DefaultBufferRecords when capacity is not positive.

func (*Buffer) Carry

func (b *Buffer) Carry(w *Window)

Carry queues an empty window, unless a window is already queued, so that gaps waiting to be shipped ride it when no records are.

func (*Buffer) Dropped

func (b *Buffer) Dropped() uint64

Dropped returns the number of records dropped to overflow since the buffer was created. It is what the heartbeat reports.

func (*Buffer) Len

func (b *Buffer) Len() int

Len returns the number of queued windows.

func (*Buffer) Pop

func (b *Buffer) Pop(ctx context.Context) (*Window, bool)

Pop removes and returns the oldest window, waiting until one is queued or ctx ends.

func (*Buffer) Push

func (b *Buffer) Push(w *Window)

Push appends w as the newest window. An empty window is not queued; Carry queues one when gaps need it.

func (*Buffer) Records

func (b *Buffer) Records() int

Records returns the number of queued records.

func (*Buffer) Requeue

func (b *Buffer) Requeue(w *Window)

Requeue puts w back as the oldest window after a failed delivery.

type Collector

type Collector struct {
	Source Source
	Buffer *Buffer
	// Gaps holds the evidence gaps waiting to be shipped: a window that
	// closes empty is still queued while any are, so they ride it.
	Gaps *Gaps
	Log  *slog.Logger
	// Now is the clock; time.Now if nil.
	Now func() time.Time
	// Monotonic reads a clock that never steps, as the time since any
	// fixed origin; the runtime's monotonic clock if nil. A window's
	// length is measured on it, so a wall clock stepped while a window was
	// open is seen when the window closes.
	Monotonic func() time.Duration
	// WindowKeys bounds the distinct keys one open window holds;
	// DefaultWindowKeys when zero.
	WindowKeys int
	// BackoffBase and BackoffCap bound the wait before a failed source is
	// restarted: one second and one minute when zero.
	BackoffBase time.Duration
	BackoffCap  time.Duration
	// contains filtered or unexported fields
}

Collector runs a Source, aggregates what it emits, and closes a window into the Buffer every window length. The window length follows SyncConfig.flow_aggregation_window_seconds and takes effect at the next rotation.

func (*Collector) Run

func (c *Collector) Run(ctx context.Context) error

Run collects until ctx ends. The source is restarted with jittered backoff whenever it fails, because a host without a working source still holds its policy and its stream; what it loses is telemetry, and that is reported, not fatal. The window open when ctx ends is closed and queued so a clean shutdown loses nothing that was observed.

func (*Collector) SetConfig

func (c *Collector) SetConfig(cfg *innerwallv1.SyncConfig)

SetConfig adopts the control plane's aggregation window.

func (*Collector) Window

func (c *Collector) Window() time.Duration

Window returns the current window length.

type Dialer

Dialer opens a connection to the control plane with the workload credential. The reporter dials for itself so that telemetry never shares a connection's fate with the sync stream, and reconnects with its own backoff (ADR-0015).

type Gap

type Gap struct {
	Kind     GapKind
	Source   GapSource
	From, To time.Time
	Count    uint64
	HasCount bool
}

Gap is one interval of known evidence loss, [From, To). Count is the number of records or entries lost when HasCount is set; a kernel-side loss has no count.

func (Gap) Wire

func (gap Gap) Wire() *innerwallv1.EvidenceGap

Wire returns the gap as ReportFlows carries it.

type GapKind

type GapKind string

GapKind says how evidence was lost.

const (
	// GapSourceOverrun: the kernel dropped events because the source's
	// socket receive buffer was full. How many is unknown.
	GapSourceOverrun GapKind = "source_overrun"
	// GapSourceRestart: the source was down between a failure and its
	// next successful subscribe. How many events is unknown.
	GapSourceRestart GapKind = "source_restart"
	// GapBufferOverflow: closed windows were dropped because the buffer
	// was full. The record count is known.
	GapBufferOverflow GapKind = "buffer_overflow"
	// GapDumpTruncated: the table dump at subscribe held more entries than
	// the source processes; the count of skipped entries is known.
	GapDumpTruncated GapKind = "dump_truncated"
	// GapWindowOverflow: the open aggregation window held as many distinct
	// keys as it may, and observations for new keys were dropped until it
	// closed. The count of dropped observations is known.
	GapWindowOverflow GapKind = "window_overflow"
	// GapWindowRejected: the control plane refused a closed window as
	// malformed, and it was dropped rather than retried. The record count
	// is known.
	GapWindowRejected GapKind = "window_rejected"
	// GapTableRemoved: the owned table was gone from the kernel, and with
	// it the observation and classification that depend on it. It stays
	// open until the table is installed again, and is cut and shipped with
	// every window that closes meanwhile, so a removal that lasts is never
	// read as a quiet host (issue #64). No count.
	GapTableRemoved GapKind = "table_removed"
)

The ways a source or the buffer can lose evidence. Each is an interval in which the flow map is known to be incomplete; what was lost inside it cannot be recovered, only bounded in time.

type GapSource

type GapSource string

GapSource names the source that lost the evidence.

const (
	GapNoSource  GapSource = ""
	GapConntrack GapSource = "conntrack"
	GapNflog     GapSource = "nflog"
)

The flow sources that record gaps. A loss that is not one source's (a buffer or window overflow, or a rejected window, drops records of every source) names none.

type Gapped

type Gapped interface {
	EvidenceGaps() (*Gaps, GapSource)
}

Gapped is a source that records the evidence it loses. When such a source fails, the restart loop opens a restart gap for it at the failure; the source closes it when it next subscribes, so the gap spans exactly the time nothing was observed.

type Gaps

type Gaps struct {
	Log *slog.Logger
	// Capacity bounds the held gaps; DefaultGapCapacity when zero.
	Capacity int
	// contains filtered or unexported fields
}

Gaps records evidence gaps for the agent: closed intervals, and at most one open interval per source between a failure and the subscribe that ends it. It also counts kernel-side overruns per source. Gaps are logged as they are recorded and held, bounded, until they are taken.

func (*Gaps) Close

func (g *Gaps) Close(source GapSource, to time.Time)

Close ends the open interval for source at to, the instant the source is observing again, and records it. Nothing happens when none is open.

func (*Gaps) CutOpen added in v0.2.0

func (g *Gaps) CutOpen(to time.Time)

CutOpen records the part up to to of every open interval of a kind that can last indefinitely, and keeps it open from to. The collector calls it as each window closes, so the window carries the loss so far. A nil Gaps records nothing.

func (*Gaps) Held

func (g *Gaps) Held() int

Held returns the number of gaps waiting to be shipped.

func (*Gaps) Open

func (g *Gaps) Open(source GapSource, kind GapKind, from time.Time)

Open starts an interval of loss for source at from, unless one is already open: the earlier start stands, because nothing was observed since. An open interval of another kind is closed at from and the new kind takes over, so each recorded gap names what happened. A nil Gaps records nothing.

func (*Gaps) Overrun

func (g *Gaps) Overrun()

Overrun counts one kernel-side overrun. A nil Gaps counts nothing.

func (*Gaps) Overruns

func (g *Gaps) Overruns() uint64

Overruns returns the overruns counted since the agent started.

func (*Gaps) Pending

func (g *Gaps) Pending() []Gap

Pending returns the held gaps, oldest first, without taking them.

func (*Gaps) Record

func (g *Gaps) Record(gap Gap)

Record records a closed interval.

func (*Gaps) Restore

func (g *Gaps) Restore(gaps []Gap)

Restore puts back gaps that were taken but not delivered, ahead of any recorded since and unmerged, so a delivery that partly landed repeats them exactly and the receiver can recognize the repeat.

func (*Gaps) Take

func (g *Gaps) Take() []Gap

Take removes and returns the held gaps, oldest first, for shipping.

type Key

type Key struct {
	Src      netip.Addr
	Dst      netip.Addr
	DstPort  uint16
	Protocol innerwallv1.Protocol
	Decision innerwallv1.PolicyDecision
	RuleID   string
}

Key is what records aggregate on within a window. The ephemeral source port is deliberately absent (ADR-0015).

type Observation

type Observation struct {
	At          time.Time
	Src         netip.Addr
	Dst         netip.Addr
	DstPort     uint16
	Protocol    innerwallv1.Protocol
	Decision    innerwallv1.PolicyDecision
	RuleID      string
	Connections uint64
	Bytes       uint64
	ProcessName string
}

Observation is one event from a Source: a connection seen inbound to this host. A source reports the start of a connection with Connections set and its byte total, once known, with Bytes set; an aggregator adds both into the record for the connection's key.

type Reporter

type Reporter struct {
	Buffer *Buffer
	// Gaps are taken and shipped with each window, and put back when its
	// stream fails; nil ships none.
	Gaps *Gaps
	Dial Dialer
	Log  *slog.Logger
	// BackoffBase and BackoffCap bound the wait after a failed report:
	// one second and five minutes when zero.
	BackoffBase time.Duration
	BackoffCap  time.Duration
	// SendTimeout bounds one window's stream; 30 seconds when zero.
	SendTimeout time.Duration
	// Now is the clock; time.Now if nil.
	Now func() time.Time
	// contains filtered or unexported fields
}

Reporter ships closed windows over ReportFlows: one stream per window, one request per batch of at most the configured records, each stamped with the agent's clock as it is sent, closed for the accepted count, with the evidence gaps recorded since the last delivery on the first request. A window whose stream fails is requeued, its gaps put back, and both retried after a jittered backoff; the buffer's bound decides what is dropped if the control plane stays away, and what it drops becomes a gap of its own. A window the control plane refuses as malformed is the exception: resending it can only be refused again, and as the oldest window it would hold every later one behind it, so it is dropped, its loss recorded as a window_rejected gap, and its gaps put back to ride the next window.

func (*Reporter) BatchMax

func (r *Reporter) BatchMax() int

BatchMax returns the current batch cap.

func (*Reporter) Run

func (r *Reporter) Run(ctx context.Context) error

Run reports until ctx ends.

func (*Reporter) SetConfig

func (r *Reporter) SetConfig(cfg *innerwallv1.SyncConfig)

SetConfig adopts the control plane's batch cap.

type Source

type Source interface {
	Run(ctx context.Context, emit func(Observation)) error
}

Source emits observations until ctx ends. It returns an error when it cannot observe at all (the collector restarts it with backoff); an observation that cannot be classified is dropped by the source, never reported as an error.

type Sources

type Sources []Source

Sources runs several sources as one stream of observations. Each member is restarted on its own failure with jittered backoff, so one source that cannot run (a missing kernel facility) never stops another.

func (Sources) Run

func (ss Sources) Run(ctx context.Context, emit func(Observation)) error

Run implements Source. It returns when ctx ends and never reports an error: a member's failures are logged and retried.

type Window

type Window struct {
	Start   time.Time
	End     time.Time
	Records []*innerwallv1.FlowRecord
}

Window is one closed reporting window: its bounds and the aggregated records, in a deterministic order.

func (*Window) Len

func (w *Window) Len() int

Len returns the number of records.

Directories

Path Synopsis
Package conntrack observes inbound connections through the kernel's connection tracking over netlink (ADR-0003).
Package conntrack observes inbound connections through the kernel's connection tracking over netlink (ADR-0003).
Package nflog observes the packets the terminal rule of the owned table logs: what the policy did not permit.
Package nflog observes the packets the terminal rule of the owned table logs: what the policy did not permit.

Jump to

Keyboard shortcuts

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