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, admitting the same traffic unmarked when the kernel does not track it, 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 no sets and no verdict: the table holds one base chain on the input hook, ObserveChainName, whose one rule counts and accepts each new inbound connection. The kernel registers its connection tracking hooks in a network namespace only once a loaded ruleset references connection tracking, so without that rule a host on which nothing else does would track nothing, and the agent, which observes through connection tracking, would observe nothing. The chain accepts under a policy of accept, and an accept in one base chain never skips another, so it decides nothing: traffic is observed, never blocked (ADR-0001, ADR-0020). Nothing in it logs, so the log source has nothing to report in visibility; it observes what enforcement drops and what simulation would have, and nothing else.
Index ¶
- Constants
- Variables
- func MarksPath(stateDir string) string
- func Render(policy *innerwallv1.WorkloadPolicy, marks map[string]uint32, opts Options) (string, error)
- func RenderDelta(from, to *innerwallv1.WorkloadPolicy, opts Options) (string, bool)
- func RenderTeardown(opts Options) string
- func RuleIndex(mark uint32) uint32
- func RuleMark(index uint32) uint32
- func SetName(ruleID string, v6 bool) string
- func Teardown(ctx context.Context, runner Runner, opts Options) error
- type Allocator
- type ApplyKind
- type Config
- type ExecRunner
- type Options
- type Runner
- type Store
- func (s *Store) Apply(ctx context.Context, policy *innerwallv1.WorkloadPolicy) error
- func (s *Store) Classify(mark uint32) (innerwallv1.PolicyDecision, string)
- func (s *Store) Current() *innerwallv1.WorkloadPolicy
- func (s *Store) LastApply() ApplyKind
- func (s *Store) List(ctx context.Context) (string, error)
- func (s *Store) Load() error
- func (s *Store) Mark(ruleID string) (uint32, bool)
- func (s *Store) Mode() innerwallv1.EnforcementMode
- func (s *Store) Restore(ctx context.Context) error
- func (s *Store) TablePresent(ctx context.Context) (bool, error)
- func (s *Store) TerminalDecision() innerwallv1.PolicyDecision
Constants ¶
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).
const ChainName = "inbound"
ChainName is the one base chain in an enforced or simulated table.
const DefaultNflogGroup uint16 = 200
DefaultNflogGroup is the netlink log group the terminal rule logs to.
const DefaultTable = "innerwall"
DefaultTable is the name of the owned table.
const LogPrefix = "innerwall "
LogPrefix is the prefix on every logged packet.
const MarksFileName = "marks.bin"
MarksFileName is the file in the agent state directory, beside the policy file, that holds the mark allocation.
const ObserveChainName = "observe"
ObserveChainName is the one base chain in a visibility table.
const Priority = "filter + 10"
Priority is the input-hook priority of either 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).
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 ¶
var ErrCorruptMarksFile = errors.New("nft: persisted mark allocation is corrupt")
ErrCorruptMarksFile is returned when the mark allocation file exists but does not verify.
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.
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.
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 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).
It also reports false when the installed table carries no sets, a visibility table: peers still change there (the rendered policy keeps its rules in every mode), but there is nothing in the kernel to update, and a set update would only be refused before the full rendering ran anyway (issue #45).
func RenderTeardown ¶
RenderTeardown returns the script that deletes the owned table and nothing else: the local kill switch (ADR-0011).
func RuleIndex ¶
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 ¶
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 ¶
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.
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 ¶
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.
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.
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)
Tables(ctx context.Context) ([]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. Tables returns the names of the inet tables the kernel holds, which is how the agent tells whether its own is still there; it reads and changes nothing else.
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 (*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) Load ¶
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 ¶
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 ¶
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) TablePresent ¶ added in v0.2.0
TablePresent reports whether the owned table is in the kernel. The local kill switch deletes it without the daemon (ADR-0011), so what the store last applied is not evidence that it is still installed.
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, by decision: the observation chain has no log rule (ADR-0020). A stray event is reported as unevaluated.