Structural diff between two %Linx.Netfilter.Ruleset{} values,
producing a %Linx.Netfilter.Patch{} of the minimum mutations
that turn one into the other.
Identity rules
- Tables, chains, sets, maps: identity is
name(within family for tables, within table for everything else). - Rules within a chain: identity is
:tagwhen set; for untagged rules the diff falls back to positional index. - Set elements: identity is the element value itself (set semantics — no "modified", just add and remove).
In-place vs delete+recreate
An entity attribute change is rendered as an in-place op when the kernel supports it, otherwise as a delete+create pair:
- Rules: any structural change →
:replace_rule(NLM_F_REPLACEover the kernel-assigned handle from the current state). Requires the rule to have a:tag(or stable positional position with no neighbour changes). - Chains: most attribute changes (type, hook, priority)
can't be replaced — emit
:delete_chain+:create_chain. We conservatively delete+create on any difference. - Tables: flags / use_count differences → no-op (the
diff treats tables as opaque containers; their lifecycle is
managed via the
:ownerflag). - Sets / maps: declaration changes (key_type, data_type, flags) → delete+create. Element changes → element-level add/remove ops.
Untagged rules
When a chain's rule list differs and any of its rules is
untagged, the diff cannot use :tag-based identity. It falls
back to: if the lists are identical → no-op; otherwise emit
:delete_rule for every current rule and :create_rule for
every desired rule (full chain rebuild). The :reconcile push
mode rejects this case at its entry point — see
Linx.Netfilter.Diff.validate_for_reconcile/1.
Summary
Functions
Returns the %Patch{} that transforms from into to. Both
inputs are %Ruleset{} values — the same shape pull/1 returns
and push/2 consumes.
Validates that desired is reconcile-safe: every chain with
more than one rule has tags on all of its rules.
Functions
@spec diff(Linx.Netfilter.Ruleset.t(), Linx.Netfilter.Ruleset.t()) :: Linx.Netfilter.Patch.t()
Returns the %Patch{} that transforms from into to. Both
inputs are %Ruleset{} values — the same shape pull/1 returns
and push/2 consumes.
The patch is topologically sorted (deletes before creates of
their dependencies). Patch.empty?/1 is true iff the rulesets
are structurally equal modulo handle / kernel-assigned values.
@spec validate_for_reconcile(Linx.Netfilter.Ruleset.t()) :: :ok | {:error, {:tag_required, {atom(), String.t(), String.t()}}}
Validates that desired is reconcile-safe: every chain with
more than one rule has tags on all of its rules.
Returns :ok or {:error, {:tag_required, {family, table, chain}}}.