policy

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package policy implements network policy enforcement for plexd mesh nodes.

Index

Constants

View Source
const DefaultChainName = "plexd-mesh"

DefaultChainName is the default iptables chain name for policy enforcement.

Variables

View Source
var ErrInvalidRuleset = errors.New("policy: invalid ruleset")

ErrInvalidRuleset marks a policy revision the engine cannot translate into firewall rules. It is a permanent property of the revision's rules, not a transient backend failure: retrying the identical revision can never succeed, so callers must not hold state back waiting for it to clear.

Functions

func HandlePolicyUpdated

func HandlePolicyUpdated(trigger ReconcileTrigger) api.EventHandler

HandlePolicyUpdated returns an api.EventHandler that triggers reconciliation when a policy_updated SSE event is received.

func ReconcileHandler

func ReconcileHandler(enforcer *Enforcer, iface string) reconcile.ReconcileHandler

ReconcileHandler returns a reconcile.ReconcileHandler that applies the merged policy's firewall ruleset when the policy fingerprint changes. It returns nil unless diff.PolicyChanged.

The fingerprint short-circuit lives in the differ (PolicyChanged stays false on a fingerprint match), so this handler — and its "policy ruleset applied" log — never fires for revision-only bumps. That log is emitted only once the enforcer confirms the ruleset reached the kernel, so it cannot claim an enforcement that a disabled config or a missing firewall backend skipped.

A transient apply failure propagates so the reconciler holds the snapshot back and retries next cycle. ErrInvalidRuleset does not: the rejected rules are a permanent property of the revision, so propagating it would hold the snapshot back and re-run every handler at the reconcile interval forever with no chance of the same rules parsing. Such a revision is logged and swallowed so the rest of the snapshot converges; the firewall keeps the last successfully applied ruleset, which is fail-closed.

Types

type Config

type Config struct {
	// Enabled controls whether policy enforcement is active. A pointer, so an
	// unset key stays distinguishable from an explicit false: enforcement is the
	// deny-by-default posture and its absence is fatal at startup, so
	// `enabled: false` is the operator's only way to run a node without it and
	// must survive ApplyDefaults exactly as written.
	// Default: true (nil reads as enabled).
	Enabled *bool `yaml:"enabled"`

	// ChainName is the iptables chain name for firewall rules.
	// Default: plexd-mesh.
	ChainName string `yaml:"chain_name"`
}

Config holds the configuration for network policy enforcement.

func (*Config) ApplyDefaults

func (c *Config) ApplyDefaults()

ApplyDefaults sets default values for zero-valued fields. Enabled is left untouched — nil already means enabled, and writing a value into it would erase the difference between an omitted key and an operator's `false`.

func (*Config) IsEnabled added in v0.3.0

func (c *Config) IsEnabled() bool

IsEnabled returns the effective Enabled setting: true unless explicitly set to false.

func (*Config) Validate

func (c *Config) Validate() error

Validate checks that configuration values are within acceptable ranges.

type Enforcer

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

Enforcer combines a PolicyEngine with a FirewallController to enforce network policies on the local node.

func NewEnforcer

func NewEnforcer(engine *PolicyEngine, firewall FirewallController, cfg Config, logger *slog.Logger) *Enforcer

NewEnforcer creates an Enforcer. The firewall parameter may be nil if no firewall backend is available; in that case ApplyFirewallRules is a no-op.

func (*Enforcer) ApplyFirewallRules

func (e *Enforcer) ApplyFirewallRules(policy *api.PolicySnapshot, iface string) (bool, error)

ApplyFirewallRules builds firewall rules from the merged policy and applies them via the FirewallController. A nil policy yields a default-deny-only ruleset — deny-by-default is plexd's documented posture.

The bool reports whether the ruleset actually reached the kernel. Enforcement being disabled and the absence of a firewall backend are no-ops that return (false, nil), so callers cannot mistake either for a successful apply and claim an enforcement that never happened.

A ruleset the engine refuses to translate is reported as ErrInvalidRuleset so callers can tell a permanently broken revision from a transient netlink failure.

func (*Enforcer) Preflight added in v0.3.0

func (e *Enforcer) Preflight() error

Preflight reports whether this node can enforce policy at all, without changing any kernel state. It answers the question the first ApplyFirewallRules would otherwise answer — but that call happens after registration has spent a one-shot bootstrap token, so a node that can never install a chain has to learn it here instead.

The two no-op paths of ApplyFirewallRules are no-ops here as well: with enforcement disabled or no firewall backend compiled in, there is no enforcement to be unable to perform. A backend that fails the probe is an error — that node was told to enforce and cannot.

func (*Enforcer) Teardown

func (e *Enforcer) Teardown() error

Teardown removes the firewall chain and its rules. It is safe to call when the firewall backend is nil.

type FirewallController

type FirewallController interface {
	// Probe reports whether the backend is usable, without changing any kernel
	// state. It must exercise the same privileged path the mutating calls take,
	// so a missing capability or an unavailable subsystem surfaces here rather
	// than on the first EnsureChain.
	Probe() error
	// EnsureChain creates the named iptables chain if it does not already exist.
	EnsureChain(chain string) error
	// ApplyRules replaces all rules in the named chain atomically.
	ApplyRules(chain string, rules []FirewallRule) error
	// FlushChain removes all rules from the named chain.
	FlushChain(chain string) error
	// DeleteChain deletes the named chain.
	// Implementations must be idempotent: deleting a non-existent chain must return nil.
	DeleteChain(chain string) error
}

FirewallController abstracts OS-level iptables operations for testability.

type FirewallRule

type FirewallRule struct {
	Interface string // network interface name
	SrcIP     string // source IP (CIDR or single IP)
	DstIP     string // destination IP (CIDR or single IP)
	Port      int    // destination port (0 = any)
	PortTo    int    // inclusive end of a destination port range, 0 means single-Port match
	Protocol  string // "tcp", "udp", "icmp", or "" (any)
	Action    string // "allow" or "deny"
}

FirewallRule describes a single iptables-style packet filter rule.

func (*FirewallRule) Validate

func (r *FirewallRule) Validate() error

Validate checks the rule for semantic correctness and returns an error if any field contains an invalid value.

type NftablesController

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

NftablesController implements FirewallController using the Linux nftables subsystem via the google/nftables netlink library. It manages a single IPv4 filter table ("plexd") and creates/destroys chains within it.

func NewNftablesController

func NewNftablesController(logger *slog.Logger) *NftablesController

NewNftablesController returns a new NftablesController.

func (*NftablesController) ApplyRules

func (c *NftablesController) ApplyRules(chain string, rules []FirewallRule) error

ApplyRules replaces all rules in the named chain atomically. It flushes the chain first, then adds each FirewallRule as an nftables rule with appropriate match expressions and verdict.

func (*NftablesController) DeleteChain

func (c *NftablesController) DeleteChain(chain string) error

DeleteChain deletes the named chain. It is idempotent: deleting a non-existent chain returns nil.

func (*NftablesController) EnsureChain

func (c *NftablesController) EnsureChain(chain string) error

EnsureChain creates the named nftables chain if it does not already exist. The chain is created as a base chain with a forward hook in the plexd filter table so that the kernel evaluates its rules for forwarded traffic.

func (*NftablesController) FlushChain

func (c *NftablesController) FlushChain(chain string) error

FlushChain removes all rules from the named chain.

func (*NftablesController) Probe added in v0.3.0

func (c *NftablesController) Probe() error

Probe reports whether the nftables backend is usable, without changing any kernel state. It dumps the IPv4 table list: nfnetlink gates every message on CAP_NET_ADMIN, dumps included, so a dropped capability or an nf_tables subsystem the kernel does not offer is reported here rather than on the first EnsureChain — which by then has already added a table to its batch.

A read is deliberate: the caller runs this before registration, so it must not leave a table behind on a node that goes on to fail startup for another reason.

type PolicyEngine

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

PolicyEngine translates the control plane's merged policy rules into concrete firewall rules for the local node.

func NewPolicyEngine

func NewPolicyEngine(logger *slog.Logger) *PolicyEngine

NewPolicyEngine creates a PolicyEngine with the given logger.

func (*PolicyEngine) BuildFirewallRules

func (e *PolicyEngine) BuildFirewallRules(rules []api.PolicyRule, iface string) ([]FirewallRule, error)

BuildFirewallRules converts the merged policy's five-tuple rules into concrete FirewallRule entries for the local node.

The ruleset is ordered and nftables verdicts are terminal: the first matching rule decides accept or drop. Dropping one rule out of the middle therefore changes the verdict for the traffic it covered rather than merely omitting an entry — a deny that carves an exception out of a following broad allow would fail open. Every unparseable rule is therefore an error for the whole set, so the caller keeps the previously installed ruleset and retries.

Actions allow and deny are kept and a log action is the sole skip: it is observational only (the nftables layer has no log verdict) and non-terminating in the control plane's policy language, so omitting it cannot change the accept/drop outcome. Protocols tcp/udp/icmp are kept and any maps to "" (match all). Both CIDRs must be parseable IPv4 prefixes — an empty string reaches nftables as "match any address" and would silently widen the rule. Ports are valid only for tcp/udp and must be a bounded, non-inverted range; a zero or out-of-range port would drop the port match entirely (widening the rule to every port) or wrap through uint16 onto an unrelated port.

The trailing default-deny rule is always appended — including when rules is empty or nil — for a deny-by-default posture.

type ReconcileTrigger

type ReconcileTrigger interface {
	TriggerReconcile()
}

ReconcileTrigger is satisfied by *reconcile.Reconciler.

Jump to

Keyboard shortcuts

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