Linx.Netlink.Rtnl.Diff (Linx v0.1.0)

Copy Markdown View Source

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. item is the desired struct to install.
  • {:update, item} — present but with a changed mutable value (a route's gateway, a neighbour's link-layer address). item is the desired struct; apply it with the resource's replace verb (NLM_F_REPLACE).
  • {:delete, item} — owned, present, no longer desired. item is 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_protocol tags every route with its origin, so ownership lives in the kernel. routes/3 filters 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 (see Linx.Netlink.Rtnl.Route's :protocol option).

  • Everything else — three-way. ifaddrmsg, links, rules, and neighbours carry no ownership field, so deletion is gated by a last_applied key set — the keys this reconciler installed on a previous pass (the kubectl apply last-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

ResourceKeyMutable 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}
Linkname— (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

Types

A diff operation. :create/:update carry the desired struct; :delete the observed one.

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

op()

@type op() :: {:create, struct()} | {:update, struct()} | {:delete, struct()}

A diff operation. :create/:update carry the desired struct; :delete the observed one.

Functions

address_key(map)

@spec address_key(struct()) :: term()

Identity of an address: {index, family, address, prefixlen}.

addresses(desired, observed, owned_keys)

@spec addresses([struct()], [struct()], MapSet.t()) :: [op()]

Diffs desired addresses against observed, deleting only owned keys. Observed addresses outside RT_SCOPE_UNIVERSE (link-locals) are dropped first.

links(desired, observed, owned_keys)

@spec links([struct()], [struct()], MapSet.t()) :: [op()]

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.

neighbour_key(map)

@spec neighbour_key(struct()) :: term()

Identity of a neighbour: {ifindex, dst}.

neighbours(desired, observed, owned_keys)

@spec neighbours([struct()], [struct()], MapSet.t()) :: [op()]

Diffs desired neighbours against observed, deleting only owned keys.

route_key(r)

@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).

routes(desired, observed, protocol)

@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.

rule_key(rule)

@spec rule_key(struct()) :: term()

Identity of a rule: priority, family, selectors, and target table.

rules(desired, observed, owned_keys)

@spec rules([struct()], [struct()], MapSet.t()) :: [op()]

Diffs desired policy-routing rules against observed, deleting only owned keys.

three_way(desired, observed, owned_keys, key, value \\ &no_value/1)

@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(desired, observed, key, value \\ &no_value/1)

@spec two_way([struct()], [struct()], (struct() -> term()), (struct() -> term())) :: [
  op()
]

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).