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

rtnetlink routes — the `RTM_*ROUTE` messages.

`list/1` reads routes; `add/5`, `add_default/3`, `replace/5`, `delete/5`
and `delete_default/3` install, update, and remove them. IPv4 and IPv6 are
both supported — the address family is detected from the destination and
gateway, which must agree.

Address-typed fields (`:dst`, `:gateway`) on a decoded `%Route{}` are
`Linx.IP` structs; verbs accept strings or `Linx.IP`s.

## Options

`add/5`, `replace/5`, `delete/5` (and their `*_default` forms) take an
options keyword:

  * `:table` — routing table (1..2^32-1). Default `254` (the main table).
  * `:protocol` — the route's origin tag (`rtm_protocol`). An integer
    0..255, or one of `:kernel` (2), `:boot` (3, the default), `:static`
    (4), `:ra` (9), `:dhcp` (16). This is the ownership marker a reconciler
    uses to manage only its own routes (see the reconcile design notes).
  * `:metric` — the route metric / `RTA_PRIORITY` (a `u32`); omitted when
    not given, which the kernel treats as metric 0.

## add vs replace

  * `add/5` uses `NLM_F_CREATE | NLM_F_EXCL` — it errors (`:eexist`) if a
    matching route already exists.
  * `replace/5` uses `NLM_F_CREATE | NLM_F_REPLACE` — create-or-replace. It
    installs the route if absent and overwrites it (e.g. a changed gateway)
    if present. This is the idempotent upsert a reconciler applies, and the
    only way to change a route's mutable attributes in place.

## The two table representations

`struct rtmsg`'s `table` is a `u8`, so tables above 255 are conveyed via the
separate `RTA_TABLE` attribute (mapped to `:table_ext`); the kernel sets the
header byte to `RT_TABLE_UNSPEC` in that case. `target_table/1` returns the
effective table — taking `:table_ext` when present, falling back to `:table`.

## Example

    {:ok, sock} = Rtnl.open()

    :ok = Route.add_default(sock, "10.0.0.1")
    :ok = Route.add(sock, "192.168.9.0", 24, "10.0.0.254")

    # Change the gateway in place (idempotent upsert):
    :ok = Route.replace(sock, "192.168.9.0", 24, "10.0.0.253")

    # A route in a custom table, tagged with a dedicated protocol:
    :ok = Route.add(sock, "10.50.0.0", 24, "10.0.0.1", table: 100, protocol: 4)

    {:ok, routes} = Route.list(sock)
    {:ok, route} = Route.get(sock, "192.168.9.7")

    :ok = Route.delete_default(sock, "10.0.0.1")

The wire format — `struct rtmsg` and the `RTA_*` attributes
(`include/uapi/linux/rtnetlink.h`) — is declared with the
`Linx.Netlink.Codec` DSL.

# `opts`

```elixir
@type opts() :: [
  table: pos_integer(),
  protocol: 0..255 | atom(),
  metric: non_neg_integer()
]
```

Options for `add/5`, `replace/5`, and `delete/5`. See the moduledoc.

# `t`

```elixir
@type t() :: %Linx.Netlink.Rtnl.Route{
  dst: term(),
  dst_len: term(),
  family: term(),
  flags: term(),
  gateway: term(),
  oif: term(),
  priority: term(),
  protocol: term(),
  scope: term(),
  src_len: term(),
  table: term(),
  table_ext: term(),
  tos: term(),
  type: term()
}
```

# `add`

```elixir
@spec add(
  Linx.Netlink.Socket.t(),
  binary() | Linx.IP.t(),
  non_neg_integer(),
  binary() | Linx.IP.t(),
  opts()
) :: :ok | {:error, term()}
```

Adds a route to `destination`/`prefix` via `gateway`.

Strict create: errors with `:eexist` if a matching route already exists.
`destination` and `gateway` are each a string or an `Linx.IP`, and must
share an address family. See the moduledoc for `opts`.

# `add_default`

```elixir
@spec add_default(Linx.Netlink.Socket.t(), binary() | Linx.IP.t(), opts()) ::
  :ok | {:error, term()}
```

Adds the default route (`0.0.0.0/0` for IPv4, `::/0` for IPv6) via
`gateway`.

# `decode`

```elixir
@spec decode(binary()) :: t()
```

Decodes a netlink message body into a `t:t/0`.

# `delete`

```elixir
@spec delete(
  Linx.Netlink.Socket.t(),
  binary() | Linx.IP.t(),
  non_neg_integer(),
  binary() | Linx.IP.t(),
  opts()
) :: :ok | {:error, term()}
```

Deletes the route to `destination`/`prefix` via `gateway`.

On delete the kernel filters by an option only when it is set, so by default
`:protocol` is left unspecified (`RTPROT_UNSPEC`) — the route is matched
regardless of which protocol installed it. Pass `:protocol` (and `:table` /
`:metric`) to narrow the match when several routes share a destination.

# `delete_default`

```elixir
@spec delete_default(Linx.Netlink.Socket.t(), binary() | Linx.IP.t(), opts()) ::
  :ok | {:error, term()}
```

Deletes the default route via `gateway`.

# `encode`

```elixir
@spec encode(t()) :: binary()
```

Encodes a `t:t/0` into its netlink message body.

# `get`

```elixir
@spec get(Linx.Netlink.Socket.t(), binary() | Linx.IP.t()) ::
  {:ok, t()} | {:error, term()}
```

Looks up the route the kernel would use to reach `destination`.

The kernel-side `ip route get` equivalent: an `RTM_GETROUTE` *without*
`NLM_F_DUMP`, with `RTA_DST` set, so the kernel resolves the lookup and
returns the matching `%Route{}`. For an unroutable destination the
kernel returns `ENETUNREACH` — surfaced as
`{:error, %Linx.Netlink.Error{}}`.

`destination` is a string (`"10.0.0.1"`, `"fc00::1"`) or an `Linx.IP`.

# `list`

```elixir
@spec list(Linx.Netlink.Socket.t()) :: {:ok, [t()]} | {:error, term()}
```

Lists every route in the socket's network namespace.

# `replace`

```elixir
@spec replace(
  Linx.Netlink.Socket.t(),
  binary() | Linx.IP.t(),
  non_neg_integer(),
  binary() | Linx.IP.t(),
  opts()
) :: :ok | {:error, term()}
```

Create-or-replace: installs the route if absent, overwrites it in place
(e.g. a changed gateway) if present.

Idempotent — the reconciler's apply verb. Same arguments as `add/5`.

# `target_table`

```elixir
@spec target_table(t()) :: non_neg_integer()
```

Returns the effective routing table for a route, handling both the in-header
byte form and the `RTA_TABLE` extension used for tables above 255.

---

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