Single-shot declarative reconciliation for rtnetlink — observe the kernel, diff against a desired state, and apply the minimal change, in one caller-driven pass scoped to the socket's network namespace.
This is the mechanism half of declarative networking: it holds no long-lived
state and owns no process. The cadence, persistence, and supervision are the
consumer's (see the reconcile design notes). It composes
Linx.Netlink.Rtnl.Diff (the per-resource diffs) and the Route/Address
actuation verbs into an ordered apply pass.
Scope
This pass reconciles addresses and routes on interfaces that already
exist in the namespace. Link lifecycle (creating veth/ipvlan, MTU, admin
state) is kind-specific and belongs to the composite/consumer; rules and
neighbours reuse the same Diff engine and slot in the same way. Interfaces
are observed (to resolve names to indices) but not created or deleted here.
Desired state
A map authored by interface name (indices are ephemeral, so you never write them down — they are resolved against a fresh link list each pass):
%{
addresses: [
{"eth0", "10.0.0.2", 24},
{"eth0", "fc00::1", 64}
],
routes: [
{"10.50.0.0", 24, "10.0.0.1"},
{"10.60.0.0", 24, "10.0.0.1", table: 100, metric: 50},
{:default, "10.0.0.1"}
]
}Address entries are {interface, ip, prefixlen}. Route entries are
{dst, prefix, gateway} / {dst, prefix, gateway, opts} /
{:default, gateway} / {:default, gateway, opts}, where opts is
Route's :table/:metric (the :protocol is forced to this
reconciler's ownership tag).
Ownership
Routes are owned by rtm_protocol: every route this reconciler installs is
tagged with :protocol (default default_protocol/0), and the route diff
considers only observed routes carrying that tag — connected routes and
other writers are invisible to it, and never deleted. Addresses have no
ownership field, so they are owned three-way via the last_applied set
threaded between passes (foreign addresses that merely appeared are left
alone). See Linx.Netlink.Rtnl.Diff.
Strategy and ordering
Apply order follows the kernel's layering: address creates first (so a route's gateway is reachable), then route creates/updates, then route deletes, then address deletes. The pass is fail-fast — it stops at the first error and reports the rest as pending; the next pass re-converges.
Example
{:ok, sock} = Rtnl.open()
desired = %{
addresses: [{"eth0", "10.0.0.2", 24}],
routes: [{:default, "10.0.0.1"}]
}
{:ok, r} = Reconcile.reconcile(sock, desired)
r.converged?
{:ok, r2} = Reconcile.reconcile(sock, desired, r.last_applied) # idempotent
Summary
Types
Address entry: {interface_name, ip, prefixlen}.
Desired state, authored by interface name.
Reconciler-held ownership, threaded between passes.
Route entry — see the moduledoc.
Functions
The default route-ownership rtm_protocol (76, ASCII 'L').
Runs one reconcile pass against desired in the socket's namespace.
Types
@type address_spec() :: {String.t(), binary() | Linx.IP.t(), non_neg_integer()}
Address entry: {interface_name, ip, prefixlen}.
@type desired() :: %{ optional(:addresses) => [address_spec()], optional(:routes) => [route_spec()] }
Desired state, authored by interface name.
@type last_applied() :: %{optional(:addresses) => MapSet.t()}
Reconciler-held ownership, threaded between passes.
@type route_spec() :: {binary() | Linx.IP.t(), non_neg_integer(), binary() | Linx.IP.t()} | {binary() | Linx.IP.t(), non_neg_integer(), binary() | Linx.IP.t(), keyword()} | {:default, binary() | Linx.IP.t()} | {:default, binary() | Linx.IP.t(), keyword()}
Route entry — see the moduledoc.
Functions
@spec default_protocol() :: pos_integer()
The default route-ownership rtm_protocol (76, ASCII 'L').
@spec reconcile(Linx.Netlink.Socket.t(), desired(), last_applied(), keyword()) :: {:ok, Linx.Netlink.Rtnl.Reconcile.Report.t()} | {:error, term()}
Runs one reconcile pass against desired in the socket's namespace.
Returns {:ok, %Report{}} — per-op kernel failures live in the report's
:failed, since a partial apply is a normal transient state the next pass
corrects. Returns {:error, {:normalize, reason}} if the desired state is
invalid (an unknown interface, an unparseable address, a family mismatch) —
nothing is applied in that case. Returns {:error, term} if observing the
kernel fails.
Options
:protocol— the route-ownership tag (integer or aRouteprotocol atom). Defaultdefault_protocol/0.