nft

package
v0.1.0 Latest Latest
Warning

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

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

Documentation

Overview

Package nft is the Linux enforcement backend: it installs a rendered WorkloadPolicy into one nftables table this agent owns, as a single atomic transaction, and never touches state outside that table (ADR-0003, ADR-0020).

The table holds one named set per resolved rule and address family, carrying the rule's peer prefixes, and one base chain on the input hook. The chain accepts loopback, accepts established and related traffic, then accepts what each rule permits (source in the rule's set, the rule's protocol and ports) while marking the connection with the rule's stable value from the Allocator, and ends in a terminal rule that decides what the policy did not permit: in enforced mode it logs the packet and drops it; in simulation mode it logs the packet, marks the connection as one enforcement would have dropped, and accepts it. Everything else is identical between the two modes, so a simulation verdict is evidence about exactly the ruleset enforcement would install. Visibility mode installs the table with no chain at all.

Index

Constants

View Source
const (
	// MarkShift is the position of the region's lowest bit.
	MarkShift = 16
	// MarkMask selects the region.
	MarkMask uint32 = 0xffff0000
	// ForeignMask selects the bits the agent never writes.
	ForeignMask uint32 = 0x0000ffff
	// WouldBlockIndex is the region value the simulation terminal rule
	// sets: the connection is one enforcement would have dropped.
	WouldBlockIndex uint32 = 0xffff
	// MaxRules is how many rules one policy can carry, one region value
	// each, leaving WouldBlockIndex free.
	MaxRules = int(WouldBlockIndex) - 1
)

The agent claims one sixteen-bit region of the 32-bit connection mark, bits 16 through 31, and nothing outside it. Every write the ruleset makes keeps the other sixteen bits as they were, and every read the collector makes looks only at the region, so a host that already uses the connection mark for something else in the low bits keeps working and its marks survive ours (ADR-0020).

View Source
const ChainName = "inbound"

ChainName is the one base chain in the owned table.

View Source
const DefaultNflogGroup uint16 = 200

DefaultNflogGroup is the netlink log group the terminal rule logs to.

View Source
const DefaultTable = "innerwall"

DefaultTable is the name of the owned table.

View Source
const LogPrefix = "innerwall "

LogPrefix is the prefix on every logged packet.

View Source
const MarksFileName = "marks.bin"

MarksFileName is the file in the agent state directory, beside the policy file, that holds the mark allocation.

View Source
const Priority = "filter + 10"

Priority is the input-hook priority of the chain, as nft spells it: ten after the conventional filter priority, so that a host's own filter chains run first and their verdict on traffic they reject stands. An accept in one base chain never skips another, so both this chain and the host's must accept a packet for it to be delivered; a drop in either is final. That is the coexistence stance: the agent adds constraints and removes none (ADR-0003).

View Source
const WouldBlockMark uint32 = WouldBlockIndex << MarkShift

WouldBlockMark is the region value of a would-block connection, in place: the mark with all region bits set and the foreign bits clear.

Variables

View Source
var ErrCorruptMarksFile = errors.New("nft: persisted mark allocation is corrupt")

ErrCorruptMarksFile is returned when the mark allocation file exists but does not verify.

View Source
var ErrMarksExhausted = errors.New("nft: every connection-mark value is held by a rule of the incoming or the installed policy")

ErrMarksExhausted is returned when a policy's new rules cannot all be given a value: every value is held by a rule of the incoming policy or of the installed one. Only two consecutive policies that each come near MaxRules with different rules can reach it.

View Source
var ErrTooManyRules = errors.New("nft: policy has more rules than the connection-mark region can number")

ErrTooManyRules is returned when a policy has more rules than the mark region can number.

View Source
var ErrUnallocatedRule = errors.New("nft: rule has no allocated connection-mark value")

ErrUnallocatedRule is returned by Render for a rule the mark allocation gives no value. The store allocates every rule before rendering, so it is a defect, refused before the kernel sees it.

Functions

func MarksPath

func MarksPath(stateDir string) string

MarksPath returns the mark allocation's path inside a state directory.

func Render

func Render(policy *innerwallv1.WorkloadPolicy, marks map[string]uint32, opts Options) (string, error)

Render returns the nft script that installs policy as the complete content of the owned table, as one transaction: the table is declared (a no-op when it exists), deleted, and recreated with its new content, so the kernel holds either the previous ruleset or this one and never a mixture (ADR-0003, ADR-0015). Each rule writes the value marks gives it, which the Allocator keeps stable across versions; a rule without one is refused. A policy with more rules than the mark region can number is refused by the store before it reaches here.

func RenderDelta

func RenderDelta(from, to *innerwallv1.WorkloadPolicy, opts Options) (string, bool)

RenderDelta returns the script that moves the installed table from policy from to policy to when the difference is peer membership only: set element additions and deletions, in one transaction. It reports false when the difference touches anything else (a rule's protocol or ports, a rule added or removed, the mode), in which case the caller installs to as a full rendering. Peer membership is the high-churn quantity, and a set update leaves every rule and every other set untouched (ADR-0015, ADR-0020).

func RenderTeardown

func RenderTeardown(opts Options) string

RenderTeardown returns the script that deletes the owned table and nothing else: the local kill switch (ADR-0011).

func RuleIndex

func RuleIndex(mark uint32) uint32

RuleIndex reads the region out of a connection mark: the value of the rule that accepted the connection, WouldBlockIndex, or 0 when the agent never marked it. The foreign bits are ignored.

func RuleMark

func RuleMark(index uint32) uint32

RuleMark returns the mark value, in place, of region value index (from 1, as the Allocator hands them out), with the foreign bits clear.

func SetName

func SetName(ruleID string, v6 bool) string

SetName returns the name of the set holding a rule's peers for one address family. The rule id is kept whole, with every character that is not a letter, digit, or underscore replaced by an underscore, so that the authored rule id can be read back from `nft list ruleset` alone.

func Teardown

func Teardown(ctx context.Context, runner Runner, opts Options) error

Teardown deletes the owned table: the local kill switch. It touches no other table and leaves the persisted policy in place, so the next daemon start re-applies it; stopping enforcement for good means stopping the daemon too, and the command that calls this says so (ADR-0011).

Types

type Allocator

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

Allocator gives every resolved rule a stable value in the mark region (ADR-0020). A rule keeps its value for as long as the allocation remembers it: a policy change never renumbers a rule, so a connection the kernel marked under an earlier version still names the rule that admitted it. A rule that leaves the policy frees its value but keeps it: values are handed out in increasing order and a freed one is not given to another rule until every value has been used once, and then the value freed longest ago goes first. Until that happens a freed value still names the removed rule, so a connection that outlived its rule is attributed to that rule and never to another. The boundary is documented: past MaxRules values handed out over the agent's life, a connection older than the reuse of its value attributes to the value's new holder.

The allocation is persisted beside the policy file and saved before it, so a rule's value survives restarts with the policy that uses it.

func LoadAllocator

func LoadAllocator(path string) (*Allocator, error)

LoadAllocator reads the allocation at path. It returns nil, nil when no file exists and ErrCorruptMarksFile when one exists but does not verify or does not describe a consistent allocation.

func NewAllocator

func NewAllocator() *Allocator

NewAllocator returns an empty allocation over the whole region.

func (*Allocator) Allocate

func (a *Allocator) Allocate(next, installed *innerwallv1.WorkloadPolicy) (map[string]uint32, error)

Allocate gives every rule of next a value and returns them by rule id, for rendering. A rule the allocation already knows keeps its value; a new rule takes the lowest value never handed out, or once those are gone, the value freed longest ago. A value held by a rule of installed, the policy the kernel holds now, is never taken, because live connections may carry it. On error the allocator may be partly updated; the store allocates on a clone.

func (*Allocator) Clone

func (a *Allocator) Clone() *Allocator

Clone returns an independent copy.

func (*Allocator) Mark

func (a *Allocator) Mark(ruleID string) (uint32, bool)

Mark returns a rule's value, if the allocation knows the rule.

func (*Allocator) Rule

func (a *Allocator) Rule(index uint32) (string, bool)

Rule returns the rule a region value names: the rule that holds it, or the removed rule that held it last and still keeps it.

func (*Allocator) Save

func (a *Allocator) Save(path string) error

Save writes the allocation durably to path, under a magic line and a SHA-256 trailer like the policy file.

type ApplyKind

type ApplyKind string

ApplyKind says how the last apply reached the kernel.

const (
	ApplyNone  ApplyKind = ""
	ApplyFull  ApplyKind = "full"
	ApplyDelta ApplyKind = "delta"
)

Apply kinds.

type Config

type Config struct {
	// Runner executes nft; ExecRunner{} when nil.
	Runner Runner
	// StateDir holds the persisted policy file and the mark allocation.
	StateDir string
	// Table and NflogGroup parameterize the rendering.
	Table      string
	NflogGroup uint16
	Log        *slog.Logger
}

Config configures a Store.

type ExecRunner

type ExecRunner struct {
	// Binary is the nft executable; "nft" on PATH when empty.
	Binary string
}

ExecRunner runs the nft binary.

func (ExecRunner) Apply

func (r ExecRunner) Apply(ctx context.Context, script string) error

Apply implements Runner.

func (ExecRunner) List

func (r ExecRunner) List(ctx context.Context, table string) (string, error)

List implements Runner.

type Options

type Options struct {
	// Table is the owned table's name; DefaultTable when empty.
	Table string
	// NflogGroup is the log group; DefaultNflogGroup when zero.
	NflogGroup uint16
}

Options parameterize a rendering.

type Runner

type Runner interface {
	Apply(ctx context.Context, script string) error
	List(ctx context.Context, table string) (string, error)
}

Runner executes nft scripts. Apply hands one script to `nft -f`, which commits the whole script as a single transaction: on any error the kernel's ruleset is exactly what it was before. List returns the owned table as nft prints it, for diagnostics and tests.

type Store

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

Store is the enforce.PolicyStore over the owned nftables table. Every Apply persists the policy to disk first, durably, and only then runs one nft transaction, so a policy the agent acknowledges always has its persisted copy and the next daemon start re-applies it before connecting (ADR-0011, ADR-0020). The kernel rules outlive the daemon: nothing here removes them on exit, and only Teardown does so at all.

func New

func New(cfg Config) *Store

New builds a store. Nothing is read or applied until Load and Restore.

func (*Store) Apply

func (s *Store) Apply(ctx context.Context, policy *innerwallv1.WorkloadPolicy) error

Apply implements enforce.PolicyStore. The policy is persisted first: its rules are given their stable mark values, and the allocation and then the policy are written durably to the state directory. Only then does the kernel see it. A change that is peer membership only is applied as set element updates; anything else is a full rendering. Both are single transactions, and a failed set update falls back to a full rendering once before the apply is reported failed.

A persistence failure fails the apply with the kernel untouched, so the installed policy, the persisted one, and the last acknowledgement all still agree. A kernel failure after the policy was persisted fails the apply with the file one version ahead of the kernel. That is benign under the desired-state model: the file is the desired state, applying it again is idempotent, the control plane answers the failure with a snapshot, and a reboot inside the window restores the desired version rather than an older one (ADR-0020).

func (*Store) Classify

func (s *Store) Classify(mark uint32) (innerwallv1.PolicyDecision, string)

Classify maps a connection mark to the decision and rule that admitted the connection, reading only the agent's region of the mark and resolving it through the stable allocation, never through the installed policy's order: a rule's value is ALLOWED by that rule, whichever version installed it; the would-block value is WOULD_BLOCK; anything else was not evaluated (visibility mode, or a connection older than any ruleset) and is OBSERVED. A connection whose rule has since been removed is ALLOWED by the removed rule, named by its own id, which the installed policy no longer carries: it is never attributed to another rule, because a freed value is not reused until the region is exhausted. The foreign bits are ignored.

func (*Store) Current

func (s *Store) Current() *innerwallv1.WorkloadPolicy

Current implements enforce.PolicyStore.

func (*Store) LastApply

func (s *Store) LastApply() ApplyKind

LastApply reports how the most recent apply reached the kernel.

func (*Store) List

func (s *Store) List(ctx context.Context) (string, error)

List returns the owned table as the kernel holds it.

func (*Store) Load

func (s *Store) Load() error

Load reads the persisted policy and mark allocation into the store without touching the kernel. A missing policy file leaves the store empty. A corrupt policy file is an error and nothing is loaded from it: the caller reports it and carries on with whatever the kernel already holds, never a partial apply.

The allocation is saved before the policy, so it covers every rule of the policy beside it. When it is missing (a state directory written before allocation was persisted, which numbered rules by canonical position) it is seeded from the policy, which reproduces exactly that numbering. When it is corrupt it is seeded the same way and the loss is logged: the policy is still restored, and only connections marked before the restart can be misattributed.

func (*Store) Mark

func (s *Store) Mark(ruleID string) (uint32, bool)

Mark returns the mark value, in place, the allocation gives a rule, for diagnostics and tests.

func (*Store) Mode

func (s *Store) Mode() innerwallv1.EnforcementMode

Mode returns the installed policy's mode; visibility before any apply.

func (*Store) Restore

func (s *Store) Restore(ctx context.Context) error

Restore installs the loaded policy as a full rendering, so that after a reboot the kernel holds what the agent last acknowledged before the control plane is even dialed. It is a no-op with nothing loaded.

func (*Store) TerminalDecision

func (s *Store) TerminalDecision() innerwallv1.PolicyDecision

TerminalDecision is what a packet logged by the terminal rule means under the installed mode: BLOCKED when enforced, WOULD_BLOCK when simulated. In visibility mode nothing is logged; a stray event is reported as unevaluated.

Jump to

Keyboard shortcuts

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