policy

package
v0.33.0 Latest Latest
Warning

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

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

Documentation

Overview

Package policy implements the `solo-provisioner network policy` verb family: it renders per-category classification / ACL rules into the `inet weaver-workload-policy` nftables table (the workload traffic plane), maintains a per-policy registry under /etc/solo-provisioner/policies/, and applies the full rendered chain to the live kernel with `nft -f` while atomically persisting /etc/solo-provisioner/network-weaver-workload-policy.nft.

Index

Constants

View Source
const (
	// TableName is the nftables table this package owns.
	TableName = "inet weaver-workload-policy"

	// WeaverNftPath is the on-disk artifact replayed at boot by the shared
	// solo-provisioner-network-nft.service oneshot. It lives under /etc (host OS
	// config on the root filesystem) so it is available early at boot, before any
	// late mounts. This package only writes the file and ensures the unit is
	// enabled.
	WeaverNftPath = "/etc/solo-provisioner/network-weaver-workload-policy.nft"

	// HostNftPath is the inet weaver-host-firewall artifact, owned by internal/network/firewall.
	// This package never writes it; it only checks for its presence to decide
	// whether the shared oneshot may be disabled.
	HostNftPath = "/etc/solo-provisioner/network-weaver-host-firewall.nft"

	// RegistryDir holds one JSON file per policy. The registry is the source of
	// truth for the static policy definition and drives the tier-order chain
	// re-render on every create/delete. CIDR membership is NOT stored here — it
	// is owned by the daemon poll loop, which reconciles it into the live nft
	// sets and persists it as set elements in WeaverNftPath so it survives a
	// reboot.
	RegistryDir = "/etc/solo-provisioner/policies"

	// NetworkNftService is the shared oneshot unit that loads the network nft
	// tables at boot. It is shared with internal/network/firewall; this package
	// ensures it is installed and enabled on the first policy mutation but never
	// disables it.
	NetworkNftService = "solo-provisioner-network-nft.service"

	// NetworkNftServiceUnitPath is the absolute path where the shared unit file
	// is installed so systemd can discover it.
	NetworkNftServiceUnitPath = "/usr/lib/systemd/system/" + NetworkNftService

	// LockDir holds the cross-command apply lock, on tmpfs (/run) so it is
	// auto-cleared on reboot. Shared with internal/network/firewall and the
	// daemon poll loop.
	LockDir = "/run/solo-provisioner/network"

	// LockPath is the flock acquired (LOCK_EX) for the duration of any mutating
	// verb, so a hand-run operator command and the daemon poll loop can never
	// interleave nft transactions on the shared network tables.
	//
	// NOTE: these path/service constants intentionally mirror
	// internal/network/firewall by value. Hoisting them into a shared
	// internal/network package is a deliberate follow-up (kept out of scope here
	// to avoid churning already-merged firewall code); the values MUST stay in
	// sync until then, which the render/lock tests guard indirectly.
	LockPath = "/run/solo-provisioner/network/.applying"
)

Variables

This section is empty.

Functions

func CanonicalizeElements added in v0.24.0

func CanonicalizeElements(elements []string) []string

CanonicalizeElements normalizes a list of nft set element tokens into a stable, deduplicated, numerically-sorted slice. It accepts both plain IPv4 elements (bare "10.1.0.2" or CIDR "10.4.0.0/24") and compound "<ip> . <port>" keys, and renders each in the form `nft list set` prints for an interval ipv4_addr set: a single host as a bare address (a /32 is collapsed), a wider range as "<network>/<bits>". Passing both the desired membership and the live membership through this before diffing guarantees the two agree on spelling and order regardless of how the caller or the kernel originally wrote them, so the same normalized form drives both the comparison and the apply args.

Tokens that don't parse are preserved as-is and sorted after the parseable ones, so an unexpected kernel rendering surfaces as a diff rather than being silently dropped.

func CompoundElement added in v0.24.0

func CompoundElement(ipPort string) (string, error)

CompoundElement converts an "ip:port" pair into the nft compound set element token "<ip> . <port>" used by --reply-stamp policies' sets (e.g. bn-backfill). It is the single conversion the apply path (`network policy set`) and the daemon poll loop's diff both call, so desired membership is built byte for byte identical to what gets written to the kernel. Both address families are accepted (IPv6 hosts must be bracketed, e.g. "[2001:db8::1]:443"); the caller routes the element to the ipv4_addr or ipv6_addr compound set by family. The returned token is canonical (host normalized, port stripped of any leading zeros) and a malformed pair is rejected here rather than poisoning a later batch apply.

func Exists added in v0.25.0

func Exists(name string) (bool, error)

Exists reports whether a policy with the given name is present in the registry at RegistryDir. A missing file is not an error (returns false). It lets install orchestration tell a genuinely new policy from one that already existed before a create — e.g. so rollback deletes only what it created and never an operator-owned policy replaced via --force.

func ExtractPodCIDRs added in v0.27.0

func ExtractPodCIDRs(content string) []string

ExtractPodCIDRs recovers the pod CIDR(s) last used to render network-weaver-workload-policy.nft (IPv4 first, then IPv6 if present). Mirrors internal/network/firewall's Parse(): it understands only the exact format Render produces, not a general nft parser, and exists so a caller that doesn't supply --pod-cidr (as --deny never does) can still correctly re-render unchanged --stamp siblings using the value they were already rendered with, instead of requiring every call to re-supply or re-detect a value that's effectively a deployment-wide constant. Returns nil if none is found (e.g. a deny-only chain, or the file doesn't exist yet).

func IsRegistryEmpty added in v0.23.0

func IsRegistryEmpty(dir string) (bool, error)

IsRegistryEmpty reports whether the policy registry at dir contains no entries. A missing directory is treated as empty. On error the bool is false so a failed read can never be mistaken for "empty" (len(nil)==0) and cause a caller to skip work it should not.

func PortsSetName added in v0.27.0

func PortsSetName(policyName string) string

PortsSetName returns the nft set name that holds a policy's listener ports (`<name>_ports`). Rendered by renderSetDecls, referenced by the stamp rule's `tcp dport/sport @<name>_ports` clause, and written by Manager.ApplyPorts.

func PruneContainedCIDRs added in v0.29.0

func PruneContainedCIDRs(elements []string) []string

PruneContainedCIDRs returns elements with every token strictly contained by another retained token removed. The result is always a SUBSET of the input in input order -- no prefix is rewritten, merged, or invented -- so the kernel stores exactly the spellings we sent, ListElements round-trips them, and DiffElements sees the same canonical forms it would have seen anyway. Dropping the covered prefix is semantically identical to merging it: the covering member already matches every address in it.

Used by the derived path (the traffic-shaper daemon's ApplyMembership / ApplySets), where membership is recomputed from upstream and fully replaced every tick and individual elements are never deleted, so there is no authored granularity to lose. Rejecting there would wedge the reconcile on an upstream quirk and leave a restrict-policy set empty -- a fail-open.

func Render

func Render(policies []*Policy, membership map[string][]string, podCIDRs ...string) (string, error)

Render produces the full `inet weaver-workload-policy` nft document for the given set of registry policies, in tier order. The same output feeds both the kernel apply (`nft -f`) and the on-disk artifact, so the live table and the persisted file can never diverge.

membership carries the daemon-owned set contents, keyed by nft set name — so `bn-publisher`, its v6 companion `bn-publisher6`, and its listener-port set `bn-publisher_ports` are three separate keys. A set with no entry (or an empty one) renders as a bare schema, which is what an operator re-render produces before the daemon has ever polled. Rendering membership is what makes the sets boot-persistent: the shared nft oneshot replays this document and is ordered ahead of the daemon, so a quarantine peer is dropped from the first packet after a reboot rather than from the first successful statusz poll.

Membership is supplied by the caller from live kernel state, never re-parsed out of the previous document. A caller that has no view of the kernel (RenderWeaverNft) renders bare schemas; the daemon's next successful poll replaces every daemon-owned set wholesale, so the artifact converges without a recovery path here.

The hooked `forward` chain holds no rules of its own beyond a `meta nfproto` dispatch into forward_ipv4 / forward_ipv6, so a packet never evaluates rules belonging to the other address family. Its policy is `accept`: classification for the HTB hierarchy is what this table exists for, so traffic no rule matches falls through carrying no `meta priority` and lands in the HTB default class. The deny tier is the exception — those rules drop, and they are the only enforcement here (workload isolation at large is Cilium's).

Rule position *within a family chain* is determined by action type and match specificity, never by creation order:

  1. deny drops (both directions)
  2. asymmetric reply-stamp restore
  3. stamp classification — specific (has an IP-set match)
  4. stamp classification — fallthrough (--from-entity world)

func RenderWeaverNft added in v0.23.0

func RenderWeaverNft(registryDir, weaverNftPath string, podCIDRs ...string) error

RenderWeaverNft loads all policies from registryDir, renders the full `inet weaver-workload-policy` nft document, and atomically writes it to weaverNftPath (mode 0644). If the on-disk content is already identical (SHA-256 match) the write is skipped — making it safe to call from idempotent install flows.

podCIDRs (mixed IPv4/IPv6) are required only when at least one registered policy is a --stamp policy. If the caller passes none and the existing weaverNftPath is readable, the pod CIDRs are recovered from that file automatically — the same recovery path Manager.Create uses. An error is returned only when a stamp policy is present and no podCIDR can be resolved.

func RestartNetworkNftService added in v0.23.0

func RestartNetworkNftService(ctx context.Context) error

RestartNetworkNftService restarts the shared solo-provisioner-network-nft.service oneshot so the kernel picks up any nft files written since the last restart. The unit's RemainAfterExit=yes state is updated to reflect both network-weaver-host-firewall.nft and network-weaver-workload-policy.nft as loaded.

func V6SetName added in v0.27.0

func V6SetName(name string) string

V6SetName returns the IPv6 companion set name for a policy (or its compound set): the base policy name with a "6" suffix. The IPv4 set keeps the bare policy name so the on-disk registry and existing element tooling are unchanged; the v6 set is the parallel ipv6_addr(. inet_service) set the dual-stack renderer declares. The renderer, the Manager's apply/snapshot paths, and the traffic-shaper daemon all derive the v6 set name through this one function so render, diff, and apply can never disagree on the spelling.

Types

type Action

type Action string

Action is the nft verdict a policy renders: classify-and-accept (stamp) or drop (deny). Every policy is exactly one of the two.

const (
	// ActionStamp classifies matching packets into an HTB priority class
	// (`meta priority set <value> accept`).
	ActionStamp Action = "stamp"
	// ActionDeny drops matching packets, before the established/related
	// fast-path. A membership deny drops both directions; adding --ports narrows
	// it to a listener port and drops the request leg only (see renderDenyRules).
	ActionDeny Action = "deny"
)

type Config

type Config struct {
	Runner        Runner
	WeaverNftPath string
	RegistryDir   string
	LockPath      string
	EnsureService func(ctx context.Context) error
}

Config customises a Manager. The zero value is not useful; prefer NewManager. Tests inject a fake Runner, temp paths, and a no-op service func so the package builds and runs on any platform.

type Direction

type Direction string

Direction selects which half of the forward chain a stamp rule renders into. It is empty for deny policies (whose direction follows from the match). For stamp policies it is not a caller-supplied value: Validate derives it from the --stamp class (every class in the mark map has exactly one direction), so it can never contradict the class it names.

const (
	// DirectionIngress renders into the peer→pod block (`ip daddr POD_CIDR …
	// tcp dport`).
	DirectionIngress Direction = "ingress"
	// DirectionEgress renders into the pod→peer block (`ip saddr POD_CIDR …
	// tcp sport`).
	DirectionEgress Direction = "egress"
)

type Manager

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

Manager implements `network policy create` against the `inet weaver-workload-policy` table. create takes the shared apply lock, writes the policy's registry file, re-renders the full chain from the registry in tier order, applies it to the live kernel with `nft -f`, atomically rewrites network-weaver-workload-policy.nft, and ensures the shared boot oneshot is enabled.

create is create-if-missing, mirroring internal/network/firewall: an existing policy is left untouched (warn, no-op) unless --force is passed, which replaces its config and membership from the given flags/--cidrs.

The rendered chain always begins with `delete table; add table`, so every Apply() destroys and recreates every policy's live set, not just the one being created. Create snapshots every policy's membership first and renders it back into the document, so the table comes up already populated rather than being refilled by a second pass -- see snapshotMembership and persistMembership below. That inline membership is also what makes the sets boot-persistent: the shared oneshot replays this same document ahead of the daemon.

func NewManager

func NewManager() *Manager

NewManager returns a Manager wired to the live kernel and the production paths.

func NewManagerWithConfig

func NewManagerWithConfig(cfg Config) *Manager

NewManagerWithConfig returns a Manager, filling any unset Config field with its production default.

func (*Manager) Add

func (m *Manager) Add(ctx context.Context, name string, cidrs []string) error

Add appends cidrs to the live set for a named policy. The chain itself is not re-rendered — the set is mutated directly with `nft add element` — but network-weaver-workload-policy.nft is rewritten afterwards so the addition survives a reboot rather than silently reverting on the next boot. Returns an error if the policy does not exist, has no CIDR set (--from-entity world), or the live kernel table is not present.

func (*Manager) ApplyMembership added in v0.24.0

func (m *Manager) ApplyMembership(ctx context.Context, desired map[string][]string) (applied bool, err error)

ApplyMembership pushes desired CIDR membership into one or more policies' live `inet weaver-workload-policy` sets. It is the traffic-shaper daemon's write path, and it reuses the same per-policy transaction as the hand-run `network policy set` CLI (applySet -> runner.SetElements), so both produce identical kernel state for the same input.

Each map entry is a full-list replace (like `set`): an empty slice clears that policy's set. Policies are applied in deterministic (sorted) name order, one `nft -f` transaction per policy for atomicity.

The whole batch runs under a SINGLE non-blocking acquisition of the shared apply flock. The return value is:

  • (false, nil): the lock was already held by a hand-run operator command, so nothing was written and the caller skips this tick rather than blocking or interleaving nft transactions;
  • (true, nil): the lock was acquired and every policy applied cleanly;
  • (false, err): the lock was acquired but a policy failed mid-batch — the batch is not considered applied (see the partial-commit note below).

ApplyMembership never calls `create`: a name absent from the registry, or one carrying no CIDR set, is an error (the policy structure is fixed by `block node install`). An error mid-batch stops the batch — policies applied before it stay committed and the caller re-drives on the next tick, which is safe because the apply is idempotent.

It acquires the lock itself, so it must NOT be called while the caller already holds withLock/withLockNB (doing so would self-deadlock on a second open-file-description of the same lock file).

func (*Manager) ApplyPorts added in v0.27.0

func (m *Manager) ApplyPorts(ctx context.Context, desired map[string][]string) (applied bool, err error)

ApplyPorts pushes desired listener ports into one or more managed-ports policies' live `<name>_ports` sets. It is the traffic-shaper daemon's listener-port write path — the exact counterpart of ApplyMembership for the CIDR sets — reconciling each set from the BN's statusz local.port. It shares ApplyMembership's batching, single non-blocking acquisition of the shared apply flock, and return contract:

  • (false, nil): the lock was already held by a hand-run operator command, so nothing was written and the caller skips this tick;
  • (true, nil): the lock was acquired and every policy applied cleanly;
  • (false, err): the lock was acquired but a policy failed mid-batch.

Each map entry is a full-list replace: an empty slice clears that policy's ports set. A name absent from the registry, or one whose ports are static rather than daemon-managed, is an error (the policy structure is fixed by `block node install`). Like ApplyMembership it acquires the lock itself, so it must NOT be called while the caller already holds withLock/withLockNB.

func (*Manager) ApplySets added in v0.27.0

func (m *Manager) ApplySets(ctx context.Context, membership, ports map[string][]string) (applied bool, err error)

ApplySets pushes desired CIDR membership AND listener ports to the live nft sets under a SINGLE non-blocking acquisition of the shared apply flock, so a reconcile tick is atomic: either both dimensions are written, or the whole tick is skipped because an operator command holds the lock. It is the traffic-shaper daemon's write path. Applying the two dimensions under two separate acquisitions (ApplyMembership then ApplyPorts) would open a partial-apply window where an operator could take the lock between them and leave one dimension stale until the next force-resync.

membership maps a policy name to its desired CIDR set; ports maps a managed-ports policy name to its desired `<name>_ports` set. Each entry is a full-list replace (an empty slice clears that set). Membership sets are applied first, then port sets, each one `nft -f` transaction, in sorted name order. The return contract matches ApplyMembership:

  • (false, nil): the lock was held by a hand-run operator command, nothing was written, the caller skips this tick;
  • (true, nil): the lock was acquired and both dimensions applied cleanly;
  • (false, err): the lock was acquired but a set failed mid-batch.

It acquires the lock itself, so it must NOT be called while the caller already holds withLock/withLockNB. Empty maps are NOT a no-op: the lock is still taken and the artifact still re-persisted. A converged node produces no deltas tick after tick, so an early return here would mean the artifact is only ever written when membership changes — leaving a node that was already converged when this build landed with a permanently stale artifact, and no way to notice.

func (*Manager) Create

func (m *Manager) Create(ctx context.Context, p *Policy, cidrs []string, podCIDRs []string, force bool) (bool, error)

Create adds a policy, or replaces an existing one when force is set. create-if-missing: a policy that doesn't exist is always created. A policy that already exists is left untouched (returns false) unless force is true, in which case its config and membership are replaced (not merged) from p and cidrs. cidrs is set membership: it never enters the registry, but it is rendered into the .nft as set elements along with every sibling's live membership, so the re-applied table and the boot artifact agree.

func (*Manager) Delete

func (m *Manager) Delete(ctx context.Context, name string) error

Delete removes a named policy: re-renders the `inet weaver-workload-policy` chain without it, applies the result to the live kernel, restores the remaining policies' live membership (which the destructive re-render wipes), removes the registry file, and atomically rewrites network-weaver-workload-policy.nft.

If this is the last policy, the table is torn down entirely — live and on disk — rather than left registered with nothing to classify. The boot oneshot is left enabled.

func (*Manager) Remove

func (m *Manager) Remove(ctx context.Context, name string, cidrs []string) error

Remove deletes cidrs from the live set for a named policy. Like Add, the chain is not re-rendered, but the .nft is rewritten so the removal is not undone by the next boot replay.

func (*Manager) Set

func (m *Manager) Set(ctx context.Context, name string, cidrs []string) error

Set atomically replaces the live set for a named policy with cidrs in a single `flush set + add element` kernel transaction. An empty cidrs slice clears the set. Like Add/Remove, the chain is not re-rendered, but the .nft is rewritten so the replacement survives a reboot.

func (*Manager) Show

func (m *Manager) Show(ctx context.Context, name string) (string, error)

Show returns a human-readable summary of a named policy: its registry config (action, class, ports, created_at) followed by the live set membership from the kernel (`nft list set inet weaver-workload-policy <name>`). No lock is taken — Show is read-only.

func (*Manager) ShowAll added in v0.27.0

func (m *Manager) ShowAll(ctx context.Context) (string, error)

ShowAll returns a summary of every configured policy, sorted by name, in the same format Show produces for a single policy. With no policies configured it returns a friendly message rather than an error, mirroring `network shape show`. No lock is taken — it is read-only.

type Policy

type Policy struct {
	Name            string    `json:"name"`
	Action          Action    `json:"action"`
	Stamp           string    `json:"stamp"`             // HTB class (from --stamp); "" for deny
	ReplyStamp      string    `json:"reply_stamp"`       // reply class (from --reply-stamp); "" if unset
	Direction       Direction `json:"direction"`         // derived from Stamp's class by Validate; "" for deny
	Ports           []string  `json:"ports"`             // static workload listener ports (from --ports); nil if none or ManagedPorts
	ManagedPorts    bool      `json:"managed_ports"`     // true when <name>_ports is filled by the daemon from statusz, not seeded here
	FromEntityWorld bool      `json:"from_entity_world"` // true if --from-entity world (no IP-set clause)
	CreatedAt       time.Time `json:"created_at"`        // tiebreaker within a tier, preserved across a --force replace
}

Policy is the static definition of one named category, mirroring the registry JSON schema. CIDR membership is deliberately NOT a field: it is owned by the daemon poll loop, not by the operator-authored policy definition, so it never enters the registry. It is persisted — but as set elements in the rendered .nft, written from live kernel state (see Manager.persistMembership), not as registry config. The initial `--cidrs` membership supplied at create time is applied to the live kernel separately (see Manager.Create).

func (*Policy) Validate

func (p *Policy) Validate(cidrs []string) error

Validate rejects any policy + initial-CIDR combination that would be unsafe or nonsensical to render. It is the single gate before the renderer; every untrusted token (name, class, ports, CIDRs) is checked so a malformed value can never break the atomic nft transaction or smuggle in nft syntax.

type Runner

type Runner interface {
	// Apply loads a full nft document into the kernel (`nft -f -`). The
	// rendered document carries the idempotent `add table / delete table / add
	// table` prefix, so a re-apply atomically replaces the inet weaver-workload-policy table.
	Apply(ctx context.Context, doc string) error
	// AddElements adds membership to a policy's set
	// (`nft add element inet weaver-workload-policy <set> { … }`). It writes the
	// live kernel only; the caller persists by re-rendering the artifact
	// afterwards. It must never be used to re-seed a set the rendered document
	// already declared with elements: redundant on the interval sets, and an
	// outright failure on the plain compound/listener-port ones.
	AddElements(ctx context.Context, set string, elements []string) error
	// DeleteElements removes specific elements from a policy's set
	// (`nft delete element inet weaver-workload-policy <set> { … }`). Applied to the live
	// kernel only.
	DeleteElements(ctx context.Context, set string, elements []string) error
	// SetElements atomically replaces a policy's set membership
	// (`flush set inet weaver-workload-policy <set>` followed by `add element … { … }` in a
	// single nft document — one kernel transaction). An empty elements slice
	// clears the set without adding any new entries.
	SetElements(ctx context.Context, set string, elements []string) error
	// ListElements returns one set's live elements
	// (`nft list set inet weaver-workload-policy <set>`), or nil if the set has no elements
	// or does not exist. Used by Manager.Create to snapshot every policy's
	// membership before the destructive delete/recreate Apply() performs, so
	// it can be restored afterward (see Manager.Create for why).
	ListElements(ctx context.Context, set string) ([]string, error)
	// List returns the rendered inet weaver-workload-policy table (`nft list table inet weaver-workload-policy`).
	List(ctx context.Context) (string, error)
	// Delete removes the inet weaver-workload-policy table (`nft delete table inet weaver-workload-policy`).
	// An already-absent table is not an error.
	Delete(ctx context.Context) error
	// Exists reports whether the inet weaver-workload-policy table is live. An
	// error means presence is unknown, not absent.
	Exists(ctx context.Context) (bool, error)
}

Runner is the seam over the system `nft` binary. Unlike the firewall package (which applies via a systemd service restart), `network policy` applies the rendered chain directly with `nft -f` for the live path, and separately persists network-weaver-workload-policy.nft so the shared nft oneshot can replay it at boot. Tests substitute a fake so the package builds and unit-tests on any platform (including macOS) without touching the kernel.

func NewExecRunner

func NewExecRunner() Runner

NewExecRunner resolves the nft binary path and returns a Runner that applies changes to the live kernel.

type SetDelta added in v0.24.0

type SetDelta struct {
	Adds    []string
	Deletes []string
}

SetDelta is the membership change to transform a policy's live nft set into its desired state: elements to add and elements to delete, each canonicalized and numerically ordered.

func DiffElements added in v0.24.0

func DiffElements(desired, live []string) SetDelta

DiffElements computes the SetDelta that turns live membership into desired membership. Both inputs are canonicalized first, so the result is independent of input order or spelling: Adds are canonical elements present in desired but not live; Deletes are present in live but not desired.

func (SetDelta) Empty added in v0.24.0

func (d SetDelta) Empty() bool

Empty reports whether the delta is a no-op (live already matches desired).

Jump to

Keyboard shortcuts

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