Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/ex_ice/app.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule ExICE.App do
@moduledoc false
use Application

@impl true
def start(_type, _args) do
children = [{ExICE.MDNS.Resolver, :gen_udp}]
Supervisor.start_link(children, strategy: :one_for_one)
end
end
14 changes: 11 additions & 3 deletions lib/ex_ice/candidate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule ExICE.Candidate do

@type t() :: %__MODULE__{
id: integer(),
address: :inet.ip_address(),
address: :inet.ip_address() | String.t(),
base_address: :inet.ip_address() | nil,
base_port: :inet.port_number() | nil,
foundation: integer(),
Expand All @@ -36,7 +36,7 @@ defmodule ExICE.Candidate do

@spec new(
type(),
:inet.ip_address(),
:inet.ip_address() | String.t(),
:inet.port_number(),
:inet.ip_address() | nil,
:inet.port_number() | nil,
Expand Down Expand Up @@ -90,7 +90,7 @@ defmodule ExICE.Candidate do
{_component_id, ""} <- Integer.parse(c_str),
{:ok, transport} <- parse_transport(String.downcase(tr_str)),
{priority, ""} <- Integer.parse(pr_str),
{:ok, address} <- :inet.parse_address(String.to_charlist(a_str)),
{:ok, address} <- parse_address(a_str),
{port, ""} <- Integer.parse(po_str),
{:ok, type} <- parse_type(ty_str) do
{:ok,
Expand Down Expand Up @@ -134,6 +134,14 @@ defmodule ExICE.Candidate do
defp parse_transport("udp"), do: {:ok, :udp}
defp parse_transport(_other), do: {:error, :invalid_transport}

defp parse_address(address) do
if String.ends_with?(address, ".local") do
{:ok, address}
else
:inet.parse_address(String.to_charlist(address))
end
end

defp parse_type("host" <> _rest), do: {:ok, :host}
defp parse_type("srflx" <> _rest), do: {:ok, :srflx}
defp parse_type("prflx" <> _rest), do: {:ok, :prflx}
Expand Down
40 changes: 31 additions & 9 deletions lib/ex_ice/ice_agent/impl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -235,17 +235,39 @@ defmodule ExICE.ICEAgent.Impl do
def add_remote_candidate(ice_agent, remote_cand) do
Logger.debug("New remote candidate: #{inspect(remote_cand)}")

case Candidate.unmarshal(remote_cand) do
{:ok, remote_cand} ->
ice_agent = do_add_remote_candidate(ice_agent, remote_cand)
Logger.debug("Successfully added remote candidate.")
resolve_address = fn
remote_cand when is_binary(remote_cand.address) ->
Comment on lines +238 to +239
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering why this isn't just a private function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put it here as it is very specific to the place where it is used

Logger.debug("Trying to resolve addr: #{remote_cand.address}")

case ExICE.MDNS.Resolver.gethostbyname(remote_cand.address) do
{:ok, addr} ->
Logger.debug("Successfully resolved #{remote_cand.address} to #{inspect(addr)}")
remote_cand = %Candidate{remote_cand | address: addr}
{:ok, remote_cand}

{:error, reason} = err ->
Logger.debug("Couldn't resolve #{remote_cand.address}, reason: #{reason}")
err
end

ice_agent
|> update_connection_state()
|> update_ta_timer()
remote_cand ->
{:ok, remote_cand}
end

with {_, {:ok, remote_cand}} <- {:unmarshal, Candidate.unmarshal(remote_cand)},
{_, {:ok, remote_cand}} <- {:resolve_address, resolve_address.(remote_cand)} do
ice_agent = do_add_remote_candidate(ice_agent, remote_cand)
Logger.debug("Successfully added remote candidate.")

ice_agent
|> update_connection_state()
|> update_ta_timer()
else
{operation, {:error, reason}} ->
Logger.warning("""
Invalid remote candidate. Couldn't #{operation}, reason: #{inspect(reason)}. Ignoring.
""")

{:error, reason} ->
Logger.warning("Invalid remote candidate, reason: #{inspect(reason)}. Ignoring.")
ice_agent
end
end
Expand Down
135 changes: 135 additions & 0 deletions lib/ex_ice/mdns/resolver.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
defmodule ExICE.MDNS.Resolver do
@moduledoc false
# This is based on https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-mdns-ice-candidates#section-3.2.1

use GenServer, restart: :transient

require Logger

@mdns_port 5353
@multicast_addr {{224, 0, 0, 251}, @mdns_port}
@response_timeout_ms 300

@spec start_link(module()) :: GenServer.on_start()
def start_link(transport_module \\ :gen_udp) do
GenServer.start_link(__MODULE__, transport_module, name: __MODULE__)
end

@spec gethostbyname(String.t()) :: {:ok, :inet.ip_address()} | {:error, term()}
def gethostbyname(addr) do
try do
GenServer.call(__MODULE__, {:gethostbyname, addr})
catch
:exit, {:timeout, _} ->
{:error, :timeout}
end
end

@impl true
def init(transport_module) do
Logger.debug("Starting MDNS Resolver")
{:ok, %{transport_module: transport_module}, {:continue, nil}}
end

@impl true
def handle_continue(_, state) do
ret =
state.transport_module.open(
# Listen on the port specific to mDNS traffic.
# `add_membership` option only defines an address.
@mdns_port,
mode: :binary,
reuseaddr: true,
active: true,
# Allow other apps to bind to @mdns_port.
# If there are multiple sockets, bound to the same port,
# and subscribed to the same group (in fact, if one socket
# subscribes to some group, all other sockets bound to
# the same port also join this gorup), all those sockets
# will receive every message. In other words, `reuseport` for
# multicast works differently than for casual sockets.
reuseport: true,
# Support running two ICE agents on a single machine.
# In other case, our request won't be delivered to the mDNS address owner
# running on the same machine (e.g., a web browser).
multicast_loop: true,
# Receive responses - they are sent to the multicast address.
# The second argument specifies interfaces where we should listen
# for multicast traffic.
# This option works on interfaces i.e. it affects all sockets
# bound to the same port.
add_membership: {{224, 0, 0, 251}, {0, 0, 0, 0}}
)

case ret do
{:ok, socket} ->
state = Map.merge(state, %{socket: socket, queries: %{}})
{:noreply, state}

{:error, reason} ->
Logger.warning("""
Couldn't start MDNS resolver, reason: #{reason}. MDNS candidates won't be resolved.
""")

{:stop, {:shutdown, reason}, state}
end
end

@impl true
def handle_call({:gethostbyname, addr}, from, state) do
query =
%ExICE.DNS.Message{
question: [
%{
qname: addr,
qtype: :a,
qclass: :in,
unicast_response: true
}
]
}
|> ExICE.DNS.Message.encode()

case state.transport_module.send(state.socket, @multicast_addr, query) do
:ok ->
state = put_in(state, [:queries, addr], from)
Process.send_after(self(), {:response_timeout, addr}, @response_timeout_ms)
{:noreply, state}

{:error, reason} ->
{:reply, {:error, reason}, state}
end
end

@impl true
def handle_info({:udp, _socket, _ip, _port, packet}, state) do
case ExICE.DNS.Message.decode(packet) do
# Only accept query response with one resource record.
# See https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-mdns-ice-candidates#section-3.2.2
{:ok, %{qr: true, aa: true, answer: [%{type: :a, class: :in, rdata: <<a, b, c, d>>} = rr]}} ->
{from, state} = pop_in(state, [:queries, rr.name])

if from do
addr = {a, b, c, d}
GenServer.reply(from, {:ok, addr})
end

{:noreply, state}

_other ->
{:noreply, state}
end
end

@impl true
def handle_info({:response_timeout, addr}, state) do
case pop_in(state, [:queries, addr]) do
{nil, state} ->
{:noreply, state}

{from, state} ->
GenServer.reply(from, {:error, :timeout})
{:noreply, state}
end
end
end
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ defmodule ExICE.MixProject do

def application do
[
extra_applications: [:logger]
extra_applications: [:logger],
mod: {ExICE.App, []}
]
end

Expand Down
4 changes: 2 additions & 2 deletions test/dns/message_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule ExICE.DNS.MessageTest do
0x34, 0x35, 0x65, 0x38, 0x62, 0x34, 0x36, 0x39, 0x2D, 0x36, 0x62, 0x32, 0x32,
0x2D, 0x34, 0x66, 0x34, 0x31, 0x2D, 0x61, 0x31, 0x30, 0x32, 0x2D, 0x66, 0x64,
0x39, 0x65, 0x61, 0x38, 0x62, 0x64, 0x36, 0x31, 0x38, 0x32, 0x05, 0x6C, 0x6F,
0x63, 0x61, 0x6C, 0x00, 0x00, 0x01, 0x00, 0x01>>
0x63, 0x61, 0x6C, 0x00, 0x00, 0x01, 0x80, 0x01>>

@mdns_query_response <<0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x24, 0x34, 0x35, 0x65, 0x38, 0x62, 0x34, 0x36, 0x39, 0x2D, 0x36, 0x62,
Expand All @@ -29,7 +29,7 @@ defmodule ExICE.DNS.MessageTest do
qname: @addr,
qtype: :a,
qclass: :in,
unicast_response: false
unicast_response: true
}
]
}
Expand Down
18 changes: 9 additions & 9 deletions test/ice_agent/impl_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ defmodule ExICE.ICEAgent.ImplTest do
raw_request
)

assert [{_socket, packet}] = :ets.lookup(:transport_mock, local_cand.socket)
assert packet = Transport.Mock.recv(local_cand.socket)
assert {:ok, msg} = ExSTUN.Message.decode(packet)
assert msg.type == %ExSTUN.Message.Type{class: :success_response, method: :binding}
assert msg.transaction_id == request.transaction_id
Expand Down Expand Up @@ -315,7 +315,7 @@ defmodule ExICE.ICEAgent.ImplTest do
end

defp assert_bad_request_error_response(socket, request) do
assert [{_socket, packet}] = :ets.lookup(:transport_mock, socket)
assert packet = Transport.Mock.recv(socket)
assert is_binary(packet)
assert {:ok, msg} = ExSTUN.Message.decode(packet)
assert msg.type == %ExSTUN.Message.Type{class: :error_response, method: :binding}
Expand All @@ -329,7 +329,7 @@ defmodule ExICE.ICEAgent.ImplTest do
end

defp assert_unauthenticated_error_response(socket, request) do
assert [{_socket, packet}] = :ets.lookup(:transport_mock, socket)
assert packet = Transport.Mock.recv(socket)
assert is_binary(packet)
assert {:ok, msg} = ExSTUN.Message.decode(packet)
assert msg.type == %ExSTUN.Message.Type{class: :error_response, method: :binding}
Expand All @@ -343,7 +343,7 @@ defmodule ExICE.ICEAgent.ImplTest do
end

defp assert_silently_discarded(socket) do
assert [{_socket, nil}] = :ets.lookup(:transport_mock, socket)
assert nil == Transport.Mock.recv(socket)
end
end

Expand All @@ -370,7 +370,7 @@ defmodule ExICE.ICEAgent.ImplTest do

ice_agent = ICEAgent.Impl.handle_timeout(ice_agent)

assert [{_socket, packet}] = :ets.lookup(:transport_mock, local_cand.socket)
assert packet = Transport.Mock.recv(local_cand.socket)
assert is_binary(packet)
assert {:ok, req} = ExSTUN.Message.decode(packet)
assert :ok = ExSTUN.Message.check_fingerprint(req)
Expand Down Expand Up @@ -531,7 +531,7 @@ defmodule ExICE.ICEAgent.ImplTest do
end

defp read_binding_request(socket, remote_pwd) do
[{_socket, packet}] = :ets.lookup(:transport_mock, socket)
packet = Transport.Mock.recv(socket)
{:ok, req} = ExSTUN.Message.decode(packet)
{:ok, key} = ExSTUN.Message.authenticate_st(req, remote_pwd)
{key, req}
Expand All @@ -554,7 +554,7 @@ defmodule ExICE.ICEAgent.ImplTest do
[local_cand] = ice_agent.local_cands

# assert no transactions are started until handle_timeout is called
assert [{_socket, nil}] = :ets.lookup(:transport_mock, local_cand.socket)
assert nil == Transport.Mock.recv(local_cand.socket)

%{ice_agent: ice_agent}
end
Expand All @@ -564,7 +564,7 @@ defmodule ExICE.ICEAgent.ImplTest do

ice_agent = ICEAgent.Impl.handle_timeout(ice_agent)

assert [{_socket, packet}] = :ets.lookup(:transport_mock, local_cand.socket)
assert packet = Transport.Mock.recv(local_cand.socket)
assert {:ok, req} = ExSTUN.Message.decode(packet)

resp =
Expand All @@ -590,7 +590,7 @@ defmodule ExICE.ICEAgent.ImplTest do

ice_agent = ICEAgent.Impl.handle_timeout(ice_agent)

assert [{_socket, packet}] = :ets.lookup(:transport_mock, local_cand.socket)
assert packet = Transport.Mock.recv(local_cand.socket)
assert {:ok, req} = ExSTUN.Message.decode(packet)

resp =
Expand Down
Loading