# `XGW.ICE`
[🔗](https://github.com/Lazarus404/xgw/blob/main/lib/xgw/ice.ex#L1)

Shared Interactive Connectivity Establishment (ICE) types and pure helpers.

## What ICE is

Home and office networks usually put devices behind a **NAT**: the address
your laptop thinks it has is not the address the public internet sees.
Firewalls may also block inbound UDP. **ICE** (RFC 8445) is the WebRTC
procedure for discovering workable address/port pairs (**candidates**),
probing them with **STUN Binding** checks, and nominating one pair for media.

Candidate kinds you will see in XGW:

- **host** -- a local interface address on this machine
- **srflx** (server reflexive) -- the public address discovered via STUN
- **relay** -- an address on a TURN server that forwards packets for you
- **prflx** (peer reflexive) -- learned from the far side during a check

This module holds types and pure functions used by both **ICE-lite**
(`XGW.ICE.Lite`, controlled-only responses) and **ICE-full**
(`XGW.ICE.Full`, gather + checklist + outbound checks). Gathering lives in
`XGW.ICE.Gather`; pair state in `XGW.ICE.Checklist`. Candidates on the wire
are `%XMediaLib.SDP.Candidate{}` structs.

## RFCs

* RFC 8445 -- ICE (candidates, roles, priorities, foundations, nomination)
* RFC 8489 -- STUN Binding used as connectivity checks
* RFC 8656 -- TURN relay candidates (gather/checklist integration)

# `candidate_type`

```elixir
@type candidate_type() :: :host | :srflx | :relay | :prflx
```

ICE candidate type (RFC 8445).

# `endpoint`

```elixir
@type endpoint() :: {:inet.ip_address(), :inet.port_number()}
```

Remote UDP endpoint `{ip, port}`.

# `ice_phase`

```elixir
@type ice_phase() ::
  :gathering | :checking | :connected | :completed | :failed | :disconnected
```

ICE agent connection phase (full ICE state machine).

# `role`

```elixir
@type role() :: :controlling | :controlled
```

ICE controlling vs controlled role.

# `transport`

```elixir
@type transport() :: :udp | :tcp
```

ICE candidate transport.

# `candidate_line`

```elixir
@spec candidate_line(XMediaLib.SDP.Candidate.t()) :: String.t()
```

SDP `a=candidate:` attribute value (without the `a=` prefix).

## Examples

    iex> c = %XMediaLib.SDP.Candidate{
    ...>   foundation: "1",
    ...>   component_id: 1,
    ...>   transport: "udp",
    ...>   priority: 100,
    ...>   ip: "192.0.2.1",
    ...>   port: 5000,
    ...>   type: "host"
    ...> }
    iex> XGW.ICE.candidate_line(c)
    "candidate:1 1 UDP 100 192.0.2.1 5000 typ host"

# `candidate_priority`

```elixir
@spec candidate_priority(candidate_type(), non_neg_integer(), non_neg_integer()) ::
  non_neg_integer()
```

RFC 8445 section 5.1.2 candidate priority from type, component, local preference.

## Examples

    iex> XGW.ICE.candidate_priority(:host, 1, 65_535) == XGW.ICE.host_priority(1)
    true

# `coerce_candidate`

```elixir
@spec coerce_candidate(XMediaLib.SDP.Candidate.t() | String.t()) ::
  {:ok, XMediaLib.SDP.Candidate.t()} | {:error, term()}
```

Coerces trickle input to `%Candidate{}`.

# `endpoint`

```elixir
@spec endpoint(XMediaLib.SDP.Candidate.t()) :: endpoint()
```

Returns `{ip, port}` for a parsed candidate.

## Examples

    iex> XGW.ICE.endpoint(%XMediaLib.SDP.Candidate{ip: "192.0.2.1", port: 5000})
    {{192, 0, 2, 1}, 5000}

# `format_ip`

```elixir
@spec format_ip(:inet.ip_address()) :: String.t()
```

Formats an IPv4/IPv6 tuple as a dotted/colon string for SDP.

## Examples

    iex> XGW.ICE.format_ip({192, 0, 2, 1})
    "192.0.2.1"

# `foundation`

```elixir
@spec foundation(String.t(), :inet.ip_address() | String.t(), String.t()) ::
  String.t()
```

RFC 8445 section 5.1.1 candidate foundation (stable for agent lifetime).

Hash of `(type, base_ip, transport)` masked to 32 bits.

## Examples

    iex> XGW.ICE.foundation("host", {192, 0, 2, 1}, "udp") ==
    ...>   XGW.ICE.foundation("host", "192.0.2.1", "UDP")
    true

# `host_priority`

```elixir
@spec host_priority(non_neg_integer(), non_neg_integer()) :: non_neg_integer()
```

RFC 8445 section 5.1.2 host candidate priority.

`local_preference` defaults to 65535 (single interface).

## Examples

    iex> XGW.ICE.host_priority(1, 65_535) == XGW.ICE.candidate_priority(:host, 1, 65_535)
    true

# `local_preference_from_priority`

```elixir
@spec local_preference_from_priority(XMediaLib.SDP.Candidate.t()) :: non_neg_integer()
```

Local preference encoded in a host candidate priority.

# `merge_remote`

```elixir
@spec merge_remote([XMediaLib.SDP.Candidate.t()], [XMediaLib.SDP.Candidate.t()]) :: [
  XMediaLib.SDP.Candidate.t()
]
```

Merges remote candidates: dedup by `{foundation, component_id, ip, port}`,
sort descending by priority (RFC 8445).

## Examples

    iex> low = %XMediaLib.SDP.Candidate{
    ...>   foundation: "1", component_id: 1, transport: "udp",
    ...>   priority: 100, ip: "192.0.2.1", port: 5000, type: "host"
    ...> }
    iex> high = %{low | foundation: "2", priority: 200, ip: "192.0.2.2", port: 5001}
    iex> Enum.map(XGW.ICE.merge_remote([low], [high, high]), & &1.priority)
    [200, 100]

# `pair_priority`

```elixir
@spec pair_priority(role(), non_neg_integer(), non_neg_integer()) :: non_neg_integer()
```

RFC 8445 section 6.1.2.1 pair priority from controlling/controlled priorities.

## Examples

    iex> XGW.ICE.pair_priority(:controlling, 100, 50)
    Bitwise.bsl(50, 32) + 2 * 100 + 1

    iex> XGW.ICE.pair_priority(:controlled, 100, 50)
    Bitwise.bsl(50, 32) + 2 * 100 + 0

# `prflx_candidate`

```elixir
@spec prflx_candidate(endpoint(), non_neg_integer(), String.t()) ::
  XMediaLib.SDP.Candidate.t()
```

Builds a peer-reflexive remote candidate from an inbound check source address.

# `role`

```elixir
@spec role(XMediaLib.SDP.Media.setup(), XMediaLib.SDP.Media.setup()) ::
  {:ok, role()} | {:error, :unsupported_setup}
```

Maps answerer DTLS `setup` to ICE role for full-ICE answers.

XGW convention (not RFC 8445 pt.5.1.1): offer `actpass` + answer `passive`
(DTLS server) -> ICE `:controlled`; offer `actpass` + answer `active` ->
`:controlling`. ICE-lite agents are always controlled per RFC 8445
pt.6.1.1 regardless of this helper.

## Examples

    iex> XGW.ICE.role(:actpass, :passive)
    {:ok, :controlled}

    iex> XGW.ICE.role(:actpass, :active)
    {:ok, :controlling}

    iex> XGW.ICE.role(:active, :passive)
    {:error, :unsupported_setup}

# `same_address_family?`

```elixir
@spec same_address_family?(XMediaLib.SDP.Candidate.t(), XMediaLib.SDP.Candidate.t()) ::
  boolean()
```

True when both candidates resolve to the same IP version (IPv4 or IPv6).

## Examples

    iex> a = %XMediaLib.SDP.Candidate{ip: "192.0.2.1", port: 1}
    iex> b = %XMediaLib.SDP.Candidate{ip: "198.51.100.1", port: 2}
    iex> XGW.ICE.same_address_family?(a, b)
    true

# `transport_atom`

```elixir
@spec transport_atom(String.t()) :: transport() | nil
```

Maps SDP transport string to an atom.

## Examples

    iex> XGW.ICE.transport_atom("UDP")
    :udp

# `type_atom`

```elixir
@spec type_atom(String.t()) :: candidate_type() | nil
```

Maps SDP candidate type string to an atom.

## Examples

    iex> XGW.ICE.type_atom("host")
    :host

    iex> XGW.ICE.type_atom("nope")
    nil

# `type_preference`

```elixir
@spec type_preference(candidate_type()) :: non_neg_integer()
```

RFC 8445 section 5.1.2 type preference for a candidate type.

## Examples

    iex> XGW.ICE.type_preference(:host)
    126

    iex> XGW.ICE.type_preference(:relay)
    0

# `usable_endpoint?`

```elixir
@spec usable_endpoint?(XMediaLib.SDP.Candidate.t()) :: boolean()
```

True when `candidate` has a parseable, non-unspecified IP (not mDNS / 0.0.0.0).

## Examples

    iex> XGW.ICE.usable_endpoint?(%XMediaLib.SDP.Candidate{ip: "192.0.2.1", port: 1})
    true

    iex> XGW.ICE.usable_endpoint?(%XMediaLib.SDP.Candidate{ip: "0.0.0.0", port: 1})
    false

# `valid?`

```elixir
@spec valid?(XMediaLib.SDP.Candidate.t()) :: boolean()
```

Returns true when the candidate has a foundation and allowed type.

## Examples

    iex> XGW.ICE.valid?(%XMediaLib.SDP.Candidate{
    ...>   foundation: "1", type: "host", ip: "192.0.2.1", port: 1
    ...> })
    true

    iex> XGW.ICE.valid?(%XMediaLib.SDP.Candidate{foundation: nil, type: "host"})
    false

# `validate_mid`

```elixir
@spec validate_mid(map(), [String.t()]) :: :ok | {:error, :media_not_found}
```

Validates trickle `:mid` / `:mline_index` against bundled mids when present.

---

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