# `XGW.TURN.Client`
[🔗](https://github.com/Lazarus404/xgw/blob/main/lib/xgw/turn/client.ex#L1)

Client for TURN: allocate a relay address and send/receive through it.

## What TURN solves

When direct UDP (and sometimes TCP) paths between two peers fail -- strict
NATs, symmetric NAT combinations, corporate firewalls -- **TURN** (Traversal
Using Relays around NAT) provides a relay on a publicly reachable server.
Your peer sends to the relay; the relay forwards to the far side.

This module speaks TURN over STUN (`XMediaLib.Stun`) without depending on the
xturn server application. It covers Allocate / Refresh / Delete,
CreatePermission, ChannelBind, Send Indication, and ChannelData -- enough
for XGW to gather `relay` candidates and keep allocations alive.

## RFCs

* RFC 8656 -- TURN (Allocate, Refresh, CreatePermission, ChannelBind, indications, ChannelData)
* RFC 8489 -- STUN long-term credentials and MESSAGE-INTEGRITY
* RFC 5766 -- ChannelData layout still used on the wire (channel number range)

# `allocation`

```elixir
@type allocation() :: %{
  turn_server: {:inet.ip_address(), :inet.port_number()},
  relay: {:inet.ip_address(), :inet.port_number()},
  lifetime_s: pos_integer(),
  username: String.t(),
  realm: String.t(),
  nonce: String.t(),
  integrity_key: binary()
}
```

Active TURN allocation state for refresh and teardown.

## Fields

* `:turn_server` - TURN server `{ip, port}` used for Allocate
* `:relay` - allocated relay `{ip, port}` advertised as a local candidate
* `:lifetime_s` - allocation lifetime from the success response
* `:username` / `:realm` / `:nonce` - long-term credential challenge state
* `:integrity_key` - MESSAGE-INTEGRITY key for subsequent TURN requests

# `allocate`

```elixir
@spec allocate(
  (binary(), {:inet.ip_address(), :inet.port_number()} -&gt;
     {:ok, binary()} | {:error, term()}),
  {:inet.ip_address(), :inet.port_number()},
  keyword()
) :: {:ok, allocation()} | {:error, term()}
```

Performs TURN Allocate with optional 401 challenge handling.

`cred_opts` keyword: `:turn_username`, `:turn_password`, `:turn_realm`,
`:turn_secret`, `:turn_user_id`, `:turn_ttl_s`.

# `allocate_request`

```elixir
@spec allocate_request(keyword()) :: binary()
```

Unauthenticated TURN Allocate request wire.

# `channel_bind`

```elixir
@spec channel_bind(
  (binary(), {:inet.ip_address(), :inet.port_number()} -&gt;
     {:ok, binary()} | {:error, term()}),
  allocation(),
  {:inet.ip_address(), :inet.port_number()},
  16384..32767
) :: :ok | {:error, term()}
```

Binds TURN channel `channel` (0x4000..0x7FFF) to `peer_address` on the allocation.

# `channel_bind_request`

```elixir
@spec channel_bind_request(
  allocation(),
  {:inet.ip_address(), :inet.port_number()},
  16384..32767
) :: binary()
```

Builds a ChannelBind request binding `channel` to `peer_address`.

# `create_permission`

```elixir
@spec create_permission(
  (binary(), {:inet.ip_address(), :inet.port_number()} -&gt;
     {:ok, binary()} | {:error, term()}),
  allocation(),
  {:inet.ip_address(), :inet.port_number()}
) :: :ok | {:error, term()}
```

Creates a TURN permission toward `peer_address` (`{ip, port}`).

# `create_permission_request`

```elixir
@spec create_permission_request(
  allocation(),
  {:inet.ip_address(), :inet.port_number()}
) :: binary()
```

Builds a CreatePermission request for `peer_address` on `allocation`.

# `decode_channel_data`

```elixir
@spec decode_channel_data(binary()) :: {:ok, 16384..32767, binary()} | :error
```

Decodes an RFC 5766 ChannelData message.

Returns `{:ok, channel, app}` or `:error`.

## Examples

    iex> XGW.TURN.Client.decode_channel_data(<<0x40, 0x00, 0, 2, "hi", 0, 0>>)
    {:ok, 0x4000, "hi"}

    iex> XGW.TURN.Client.decode_channel_data(<<1, 2, 3>>)
    :error

# `delete`

```elixir
@spec delete(
  (binary(), {:inet.ip_address(), :inet.port_number()} -&gt;
     {:ok, binary()} | {:error, term()}),
  allocation()
) :: :ok | {:error, term()}
```

Deletes a TURN allocation (Refresh with lifetime 0).

# `encode_channel_data`

```elixir
@spec encode_channel_data(16384..32767, binary()) :: binary()
```

Encodes an RFC 5766 ChannelData message (4-byte header + application data + pad).

## Examples

    iex> bin = XGW.TURN.Client.encode_channel_data(0x4000, "hi")
    iex> {:ok, 0x4000, "hi"} = XGW.TURN.Client.decode_channel_data(bin)
    iex> true
    true

# `long_term_key`

```elixir
@spec long_term_key(String.t(), String.t(), String.t()) :: binary()
```

Long-term credential MESSAGE-INTEGRITY key (MD5 of `username:realm:password`).

## Examples

    iex> key = XGW.TURN.Client.long_term_key("user", "example.com", "secret")
    iex> key == :crypto.hash(:md5, "user:example.com:secret")
    true

# `parse_server_host`

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

Parses host[:port] from a STUN/TURN URI body (after the scheme prefix).

# `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}`.

## Examples

    iex> XGW.TURN.Client.parse_turn_server("turn:127.0.0.1:3478")
    {:ok, {{127, 0, 0, 1}, 3478}}

    iex> XGW.TURN.Client.parse_turn_server("turns:127.0.0.1:5349")
    {:error, :unsupported_turns}

# `refresh`

```elixir
@spec refresh(
  (binary(), {:inet.ip_address(), :inet.port_number()} -&gt;
     {:ok, binary()} | {:error, term()}),
  allocation(),
  non_neg_integer()
) :: {:ok, allocation()} | {:error, term()}
```

Sends TURN Refresh to extend or delete an allocation.

# `rest_credentials`

```elixir
@spec rest_credentials(String.t(), binary(), pos_integer()) ::
  {String.t(), String.t()}
```

REST shared-secret credentials (`expiry:user_id` + Base64 HMAC-SHA1 password).

Time-bound username form used by many hosted TURN providers. The password is
not stored long-term; it is derived from `secret` and the username string.

## Examples

    iex> {user, pass} = XGW.TURN.Client.rest_credentials("alice", "s3cret", 3600)
    iex> String.contains?(user, ":alice") and byte_size(pass) > 0
    true

# `send_indication`

```elixir
@spec send_indication(
  allocation(),
  {:inet.ip_address(), :inet.port_number()},
  binary()
) :: binary()
```

Sends a Send Indication with `data` toward `peer_address` through the allocation.

---

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