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

Discovers ICE candidates: host, server-reflexive (srflx), and TURN relay.

## Candidate gathering in plain terms

Before checks can run, each side must advertise addresses it might be
reachable on:

1. **Host** -- read local NIC addresses (synchronous; always available).
2. **srflx** -- ask a STUN server "what public address do you see?" so peers
   behind NAT learn a candidate that might work on the open internet.
3. **relay** -- ask a TURN server to allocate a relay address that always
   works when direct paths fail (at the cost of an extra hop).

Host gather is synchronous. srflx and relay honor `gather_timeout_ms`
(default 3000). Prefer waiting for gather before publishing answer SDP;
host-only answers remain valid when STUN/TURN is slow or unconfigured.

## RFCs

* RFC 8445 -- host / srflx / relay candidate gathering
* RFC 8489 -- STUN Binding for server-reflexive discovery
* RFC 8656 -- TURN Allocate for relay candidates

# `build_relay`

```elixir
@spec build_relay(
  {:inet.ip_address(), :inet.port_number()},
  non_neg_integer()
) :: XMediaLib.SDP.Candidate.t()
```

Builds a relay `%Candidate{}` from a TURN relayed transport address.

# `build_srflx`

```elixir
@spec build_srflx(
  {:inet.ip_address(), :inet.port_number()},
  XMediaLib.SDP.Candidate.t(),
  non_neg_integer()
) :: XMediaLib.SDP.Candidate.t()
```

Builds an srflx candidate linked to a host base (same foundation, raddr/rport).

# `gather_host`

```elixir
@spec gather_host({:inet.ip_address(), :inet.port_number()}, keyword()) :: [
  XMediaLib.SDP.Candidate.t()
]
```

Gathers host candidates for a bound UDP socket (alias for `host_candidates/2`).

# `gather_relay`

```elixir
@spec gather_relay({:inet.ip_address(), :inet.port_number()}, keyword()) :: [
  %{
    candidate: XMediaLib.SDP.Candidate.t(),
    allocation: XGW.TURN.Client.allocation()
  }
]
```

Gathers relay candidates concurrently (one attempt per `turn_servers` entry).

# `gather_srflx`

```elixir
@spec gather_srflx(
  {:inet.ip_address(), :inet.port_number()},
  [XMediaLib.SDP.Candidate.t()],
  keyword()
) ::
  [XMediaLib.SDP.Candidate.t()]
```

Gathers srflx candidates for all `(host, stun_server)` pairs concurrently.

# `gather_with_timeout`

```elixir
@spec gather_with_timeout({:inet.ip_address(), :inet.port_number()}, keyword()) :: %{
  host: [XMediaLib.SDP.Candidate.t()],
  srflx: [XMediaLib.SDP.Candidate.t()],
  relay: [XMediaLib.SDP.Candidate.t()]
}
```

Gathers host candidates and waits up to `gather_timeout_ms` for srflx and relay.

srflx and relay run concurrently under one timeout budget (not sequential).

Returns `%{host: [...], srflx: [...], relay: [...]}`.

# `host_addresses`

```elixir
@spec host_addresses(keyword()) :: [:inet.ip_address()]
```

Resolves host IP addresses for candidate advertisement.

Options:

  * `:advertised_ips` - explicit list (wins over all other sources)
  * `:advertised_ip` - single override
  * `:include_loopback` - include `{127,0,0,1}` when auto-detecting (default `true`)
  * `:getifaddrs` - injectable `fn -> {:ok, ifaddrs} | {:error, _}` for tests

# `host_candidates`

```elixir
@spec host_candidates({:inet.ip_address(), :inet.port_number()}, keyword()) :: [
  XMediaLib.SDP.Candidate.t()
]
```

Builds host `%Candidate{}` structs for one UDP bind port.

# `parse_server_host`

Parses host[:port] from a URI body (shared by STUN/TURN parsers).

# `parse_stun_server`

```elixir
@spec parse_stun_server(String.t()) ::
  {:ok, {:inet.ip_address(), :inet.port_number()}} | {:error, term()}
```

Parses a STUN server URI (`stun:host:port`) to `{ip, port}`.

# `parse_turn_server`

```elixir
@spec parse_turn_server(String.t()) ::
  {:ok, {:inet.ip_address(), :inet.port_number()}} | {:error, term()}
```

Parses a TURN server URI (`turn:host:port`) to `{ip, port}`.

# `query_relay`

```elixir
@spec query_relay(
  {:inet.ip_address(), :inet.port_number()},
  String.t() | {:inet.ip_address(), :inet.port_number()},
  keyword()
) ::
  {:ok,
   %{
     candidate: XMediaLib.SDP.Candidate.t(),
     allocation: XGW.TURN.Client.allocation()
   }}
  | {:error, term()}
```

Queries one TURN server for a relay candidate using injectable `:send_recv`.

Returns `{:ok, %{candidate: cand, allocation: alloc}}` on success.

# `query_srflx`

```elixir
@spec query_srflx(
  {:inet.ip_address(), :inet.port_number()},
  XMediaLib.SDP.Candidate.t(),
  String.t() | {:inet.ip_address(), :inet.port_number()},
  keyword()
) :: {:ok, XMediaLib.SDP.Candidate.t()} | {:error, term()}
```

Queries one STUN server for srflx using injectable `:send_recv`.

`:send_recv` is `fn request_wire, {stun_ip, stun_port} -> {:ok, response_wire} | {:error, _}`.

# `relay_from_allocate`

```elixir
@spec relay_from_allocate(XMediaLib.Stun.t(), non_neg_integer()) ::
  {:ok, XMediaLib.SDP.Candidate.t()} | {:error, term()}
```

Derives relay `%Candidate{}` from a successful TURN Allocate response.

# `srflx_from_response`

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

Derives srflx `%Candidate{}` from a Binding success and host base.

# `stun_binding_request`

```elixir
@spec stun_binding_request() :: binary()
```

Plain RFC 8489 STUN Binding request wire (no ICE credentials).

# `tcp_passive_candidates`

```elixir
@spec tcp_passive_candidates({:inet.ip_address(), :inet.port_number()}, keyword()) ::
  [
    XMediaLib.SDP.Candidate.t()
  ]
```

Builds passive ICE-TCP host candidates (RFC 6544 `tcptype passive`).

---

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