Per-resource diffs for rtnetlink — the minimal set of create / update / delete operations that converge observed kernel state onto a desired state.
This is the diff half of declarative reconciliation for rtnl, mirroring
Linx.Netfilter.Diff. It is pure: it operates on lists of decoded structs
(%Route{}, %Address{}, …) and returns ops; applying them, observing, and
ordering across resource types are the reconciler's job (a later phase).
Ops
Each diff returns a list of op/0:
{:create, item}— desired, absent from the kernel.itemis the desired struct to install.{:update, item}— present but with a changed mutable value (a route's gateway, a neighbour's link-layer address).itemis the desired struct; apply it with the resource's replace verb (NLM_F_REPLACE).{:delete, item}— owned, present, no longer desired.itemis the observed struct (it carries the kernel's index/handle).
Ownership: two-way vs three-way
A reconciler must delete only what it owns, never another writer's state. The kernel gives us the ownership marker for exactly one resource:
Routes — two-way.
rtm_protocoltags every route with its origin, so ownership lives in the kernel.routes/3filters observed routes to a given protocol and diffs against that; connected routes (RTPROT_KERNEL) and other writers' routes simply never enter the managed set. Tag desired routes with the same protocol (seeLinx.Netlink.Rtnl.Route's:protocoloption).Everything else — three-way.
ifaddrmsg, links, rules, and neighbours carry no ownership field, so deletion is gated by alast_appliedkey set — the keys this reconciler installed on a previous pass (thekubectl applylast-applied-configuration trick). Only an observed item whose key is in that set and is no longer desired is deleted; foreign state that merely appeared is left alone.
The last_applied set is a MapSet of the keys returned by this module's
*_key/1 functions; it is reconciler-held and never persisted. The
reconciler rebuilds it each pass from the keys it successfully applied.
Keys (identity) and mutable values
| Resource | Key | Mutable value (→ :update) |
|---|---|---|
| Route | {table, family, dst, dst_len, metric} | gateway |
| Address | {index, family, address, prefixlen} | — (key is the whole identity) |
| Neighbour | {ifindex, dst} | lladdr |
| Rule | {priority, family, src/dst/fwmark/table} | — |
| Link | name | — (existence only; attribute reconcile is out of scope) |
Addresses are additionally filtered to RT_SCOPE_UNIVERSE, dropping
fe80::/64 link-locals the kernel manages itself.
Desired and observed must be keyed in the same space. Authoring is by
interface name, but :index-bearing structs are the diff currency, so the
reconciler resolves names to indices (from a fresh Link.list) before
diffing — see the reconcile design notes.
Summary
Functions
Identity of an address: {index, family, address, prefixlen}.
Diffs desired addresses against observed, deleting only owned keys. Observed
addresses outside RT_SCOPE_UNIVERSE (link-locals) are dropped first.
Identity of a link: its name.
Diffs desired links against observed by name, deleting only owned names.
Identity of a neighbour: {ifindex, dst}.
Diffs desired neighbours against observed, deleting only owned keys.
Identity of a route: {table, family, dst, dst_len, metric}.
Diffs desired routes against observed, owning by protocol (an
rtm_protocol integer, or the same atoms Route accepts). Observed routes
carrying a different protocol — connected, DHCP, other writers — are ignored.
Identity of a rule: priority, family, selectors, and target table.
Diffs desired policy-routing rules against observed, deleting only owned keys.
Three-way diff: an observed item is deletable only if its key is in
owned_keys (a MapSet of key/1 values from a previous pass). Foreign
state is left untouched.
Two-way diff: ownership is encoded in observed (already filtered to what we
own), so every observed item not desired is deletable.
Types
Functions
Identity of an address: {index, family, address, prefixlen}.
Diffs desired addresses against observed, deleting only owned keys. Observed
addresses outside RT_SCOPE_UNIVERSE (link-locals) are dropped first.
Identity of a link: its name.
Diffs desired links against observed by name, deleting only owned names.
Existence only — reconciling link attributes (MTU, admin state, address) is a separate concern and out of scope here.
Identity of a neighbour: {ifindex, dst}.
Diffs desired neighbours against observed, deleting only owned keys.
@spec route_key(Linx.Netlink.Rtnl.Route.t()) :: term()
Identity of a route: {table, family, dst, dst_len, metric}.
A dst_len of 0 is the default route; its dst is canonicalised to
:default so a desired default (built with dst = 0.0.0.0/::) keys the
same as a kernel-observed one (which omits RTA_DST, leaving dst = nil).
@spec routes( [Linx.Netlink.Rtnl.Route.t()], [Linx.Netlink.Rtnl.Route.t()], 0..255 | atom() ) :: [op()]
Diffs desired routes against observed, owning by protocol (an
rtm_protocol integer, or the same atoms Route accepts). Observed routes
carrying a different protocol — connected, DHCP, other writers — are ignored.
Identity of a rule: priority, family, selectors, and target table.
Diffs desired policy-routing rules against observed, deleting only owned keys.
@spec three_way([struct()], [struct()], MapSet.t(), (struct() -> term()), (struct() -> term())) :: [ op() ]
Three-way diff: an observed item is deletable only if its key is in
owned_keys (a MapSet of key/1 values from a previous pass). Foreign
state is left untouched.
Two-way diff: ownership is encoded in observed (already filtered to what we
own), so every observed item not desired is deletable.
key maps a struct to its identity; value maps it to its mutable value
(default: a constant, i.e. no :update ops).