# `Linx.Netlink.Rtnl.Diff`
[🔗](https://github.com/oshlabs/linx/blob/v0.2.0/lib/linx/netlink/rtnl/diff.ex#L1)

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 `t: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

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

# `op`

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

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

# `address_key`

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

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

# `addresses`

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

# `link_key`

```elixir
@spec link_key(struct()) :: term()
```

Identity of a link: its `name`.

# `links`

```elixir
@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`

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

Identity of a neighbour: `{ifindex, dst}`.

# `neighbours`

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

Diffs desired neighbours against observed, deleting only owned keys.

# `route_key`

```elixir
@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`

```elixir
@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`

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

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

# `rules`

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

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

# `three_way`

```elixir
@spec three_way([struct()], [struct()], MapSet.t(), (struct() -&gt; term()), (struct() -&gt;
                                                                       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`

```elixir
@spec two_way([struct()], [struct()], (struct() -&gt; term()), (struct() -&gt; 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).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
