XGW.ICE (xgw v0.1.0)

Copy Markdown View Source

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)

Summary

Types

ICE candidate type (RFC 8445).

Remote UDP endpoint {ip, port}.

ICE agent connection phase (full ICE state machine).

ICE controlling vs controlled role.

ICE candidate transport.

Functions

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

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

Coerces trickle input to %Candidate{}.

Returns {ip, port} for a parsed candidate.

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

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

RFC 8445 section 5.1.2 host candidate priority.

Local preference encoded in a host candidate priority.

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

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

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

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

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

Maps SDP transport string to an atom.

Maps SDP candidate type string to an atom.

RFC 8445 section 5.1.2 type preference for a candidate type.

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

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

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

Types

candidate_type()

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

ICE candidate type (RFC 8445).

endpoint()

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

Remote UDP endpoint {ip, port}.

ice_phase()

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

ICE agent connection phase (full ICE state machine).

role()

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

ICE controlling vs controlled role.

transport()

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

ICE candidate transport.

Functions

candidate_line(c)

@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(type, component_id, local_preference)

@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(c)

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

Coerces trickle input to %Candidate{}.

endpoint(candidate)

@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(ip)

@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(type, ip, transport)

@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(component_id, local_preference \\ 65535)

@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(candidate)

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

Local preference encoded in a host candidate priority.

merge_remote(existing, incoming)

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(role, local_priority, remote_priority)

@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(arg, priority, transport \\ "udp")

@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(arg1, arg2)

@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?(a, b)

@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(t)

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

Maps SDP transport string to an atom.

Examples

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

type_atom(arg1)

@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(atom)

@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?(cand)

@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?(candidate)

@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(arg1, mids)

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

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