Skip to content

Commit fd2c0d5

Browse files
committed
Add MDNS resolver
1 parent 60a28e7 commit fd2c0d5

File tree

9 files changed

+323
-26
lines changed

9 files changed

+323
-26
lines changed

lib/ex_ice/app.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
defmodule ExICE.App do
2+
@moduledoc false
3+
use Application
4+
5+
@impl true
6+
def start(_type, _args) do
7+
children = [{ExICE.MDNS.Resolver, :gen_udp}]
8+
Supervisor.start_link(children, strategy: :one_for_one)
9+
end
10+
end

lib/ex_ice/candidate.ex

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule ExICE.Candidate do
99

1010
@type t() :: %__MODULE__{
1111
id: integer(),
12-
address: :inet.ip_address(),
12+
address: :inet.ip_address() | String.t(),
1313
base_address: :inet.ip_address() | nil,
1414
base_port: :inet.port_number() | nil,
1515
foundation: integer(),
@@ -36,7 +36,7 @@ defmodule ExICE.Candidate do
3636

3737
@spec new(
3838
type(),
39-
:inet.ip_address(),
39+
:inet.ip_address() | String.t(),
4040
:inet.port_number(),
4141
:inet.ip_address() | nil,
4242
:inet.port_number() | nil,
@@ -90,7 +90,7 @@ defmodule ExICE.Candidate do
9090
{_component_id, ""} <- Integer.parse(c_str),
9191
{:ok, transport} <- parse_transport(String.downcase(tr_str)),
9292
{priority, ""} <- Integer.parse(pr_str),
93-
{:ok, address} <- :inet.parse_address(String.to_charlist(a_str)),
93+
{:ok, address} <- parse_address(a_str),
9494
{port, ""} <- Integer.parse(po_str),
9595
{:ok, type} <- parse_type(ty_str) do
9696
{:ok,
@@ -134,6 +134,14 @@ defmodule ExICE.Candidate do
134134
defp parse_transport("udp"), do: {:ok, :udp}
135135
defp parse_transport(_other), do: {:error, :invalid_transport}
136136

137+
defp parse_address(address) do
138+
if String.ends_with?(address, ".local") do
139+
{:ok, address}
140+
else
141+
:inet.parse_address(String.to_charlist(address))
142+
end
143+
end
144+
137145
defp parse_type("host" <> _rest), do: {:ok, :host}
138146
defp parse_type("srflx" <> _rest), do: {:ok, :srflx}
139147
defp parse_type("prflx" <> _rest), do: {:ok, :prflx}

lib/ex_ice/ice_agent/impl.ex

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -235,17 +235,39 @@ defmodule ExICE.ICEAgent.Impl do
235235
def add_remote_candidate(ice_agent, remote_cand) do
236236
Logger.debug("New remote candidate: #{inspect(remote_cand)}")
237237

238-
case Candidate.unmarshal(remote_cand) do
239-
{:ok, remote_cand} ->
240-
ice_agent = do_add_remote_candidate(ice_agent, remote_cand)
241-
Logger.debug("Successfully added remote candidate.")
238+
resolve_address = fn
239+
remote_cand when is_binary(remote_cand.address) ->
240+
Logger.debug("Trying to resolve addr: #{remote_cand.address}")
241+
242+
case ExICE.MDNS.Resolver.gethostbyname(remote_cand.address) do
243+
{:ok, addr} ->
244+
Logger.debug("Successfully resolved #{remote_cand.address} to #{inspect(addr)}")
245+
remote_cand = %Candidate{remote_cand | address: addr}
246+
{:ok, remote_cand}
247+
248+
{:error, reason} = err ->
249+
Logger.debug("Couldn't resolve #{remote_cand.address}, reason: #{reason}")
250+
err
251+
end
242252

243-
ice_agent
244-
|> update_connection_state()
245-
|> update_ta_timer()
253+
remote_cand ->
254+
{:ok, remote_cand}
255+
end
256+
257+
with {_, {:ok, remote_cand}} <- {:unmarshal, Candidate.unmarshal(remote_cand)},
258+
{_, {:ok, remote_cand}} <- {:resolve_address, resolve_address.(remote_cand)} do
259+
ice_agent = do_add_remote_candidate(ice_agent, remote_cand)
260+
Logger.debug("Successfully added remote candidate.")
261+
262+
ice_agent
263+
|> update_connection_state()
264+
|> update_ta_timer()
265+
else
266+
{operation, {:error, reason}} ->
267+
Logger.warning("""
268+
Invalid remote candidate. Couldn't #{operation}, reason: #{inspect(reason)}. Ignoring.
269+
""")
246270

247-
{:error, reason} ->
248-
Logger.warning("Invalid remote candidate, reason: #{inspect(reason)}. Ignoring.")
249271
ice_agent
250272
end
251273
end

lib/ex_ice/mdns/resolver.ex

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
defmodule ExICE.MDNS.Resolver do
2+
@moduledoc false
3+
# This is based on https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-mdns-ice-candidates#section-3.2.1
4+
5+
use GenServer, restart: :transient
6+
7+
require Logger
8+
9+
@mdns_port 5353
10+
@multicast_addr {{224, 0, 0, 251}, @mdns_port}
11+
@response_timeout_ms 300
12+
13+
@spec start_link(module()) :: GenServer.on_start()
14+
def start_link(transport_module \\ :gen_udp) do
15+
GenServer.start_link(__MODULE__, transport_module, name: __MODULE__)
16+
end
17+
18+
@spec gethostbyname(String.t()) :: {:ok, :inet.ip_address()} | {:error, term()}
19+
def gethostbyname(addr) do
20+
try do
21+
GenServer.call(__MODULE__, {:gethostbyname, addr})
22+
catch
23+
:exit, {:timeout, _} ->
24+
{:error, :timeout}
25+
end
26+
end
27+
28+
@impl true
29+
def init(transport_module) do
30+
Logger.debug("Starting MDNS Resolver")
31+
{:ok, %{transport_module: transport_module}, {:continue, nil}}
32+
end
33+
34+
@impl true
35+
def handle_continue(_, state) do
36+
ret =
37+
state.transport_module.open(
38+
# Listen on the port specific to mDNS traffic.
39+
# `add_membership` option only defines an address.
40+
@mdns_port,
41+
mode: :binary,
42+
reuseaddr: true,
43+
active: true,
44+
# Allow other apps to bind to @mdns_port.
45+
# If there are multiple sockets, bound to the same port,
46+
# and subscribed to the same group (in fact, if one socket
47+
# subscribes to some group, all other sockets bound to
48+
# the same port also join this gorup), all those sockets
49+
# will receive every message. In other words, `reuseport` for
50+
# multicast works differently than for casual sockets.
51+
reuseport: true,
52+
# Support running two ICE agents on a single machine.
53+
# In other case, our request won't be delivered to the mDNS address owner
54+
# running on the same machine (e.g., a web browser).
55+
multicast_loop: true,
56+
# Receive responses - they are sent to the multicast address.
57+
# The second argument specifies interfaces where we should listen
58+
# for multicast traffic.
59+
# This option works on interfaces i.e. it affects all sockets
60+
# bound to the same port.
61+
add_membership: {{224, 0, 0, 251}, {0, 0, 0, 0}}
62+
)
63+
64+
case ret do
65+
{:ok, socket} ->
66+
state = Map.merge(state, %{socket: socket, queries: %{}})
67+
{:noreply, state}
68+
69+
{:error, reason} ->
70+
Logger.warning("""
71+
Couldn't start MDNS resolver, reason: #{reason}. MDNS candidates won't be resolved.
72+
""")
73+
74+
{:stop, {:shutdown, reason}, state}
75+
end
76+
end
77+
78+
@impl true
79+
def handle_call({:gethostbyname, addr}, from, state) do
80+
query =
81+
%ExICE.DNS.Message{
82+
question: [
83+
%{
84+
qname: addr,
85+
qtype: :a,
86+
qclass: :in,
87+
unicast_response: true
88+
}
89+
]
90+
}
91+
|> ExICE.DNS.Message.encode()
92+
93+
case state.transport_module.send(state.socket, @multicast_addr, query) do
94+
:ok ->
95+
state = put_in(state, [:queries, addr], from)
96+
Process.send_after(self(), {:response_timeout, addr}, @response_timeout_ms)
97+
{:noreply, state}
98+
99+
{:error, reason} ->
100+
{:reply, {:error, reason}, state}
101+
end
102+
end
103+
104+
@impl true
105+
def handle_info({:udp, _socket, _ip, _port, packet}, state) do
106+
case ExICE.DNS.Message.decode(packet) do
107+
# Only accept query response with one resource record.
108+
# See https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-mdns-ice-candidates#section-3.2.2
109+
{:ok, %{qr: true, aa: true, answer: [%{type: :a, class: :in, rdata: <<a, b, c, d>>} = rr]}} ->
110+
{from, state} = pop_in(state, [:queries, rr.name])
111+
112+
if from do
113+
addr = {a, b, c, d}
114+
GenServer.reply(from, {:ok, addr})
115+
end
116+
117+
{:noreply, state}
118+
119+
_other ->
120+
{:noreply, state}
121+
end
122+
end
123+
124+
@impl true
125+
def handle_info({:response_timeout, addr}, state) do
126+
case pop_in(state, [:queries, addr]) do
127+
{nil, state} ->
128+
{:noreply, state}
129+
130+
{from, state} ->
131+
GenServer.reply(from, {:error, :timeout})
132+
{:noreply, state}
133+
end
134+
end
135+
end

mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ defmodule ExICE.MixProject do
3333

3434
def application do
3535
[
36-
extra_applications: [:logger]
36+
extra_applications: [:logger],
37+
mod: {ExICE.App, []}
3738
]
3839
end
3940

test/ice_agent/impl_test.exs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ defmodule ExICE.ICEAgent.ImplTest do
9595
raw_request
9696
)
9797

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

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

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

345345
defp assert_silently_discarded(socket) do
346-
assert [{_socket, nil}] = :ets.lookup(:transport_mock, socket)
346+
assert nil == Transport.Mock.recv(socket)
347347
end
348348
end
349349

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

371371
ice_agent = ICEAgent.Impl.handle_timeout(ice_agent)
372372

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

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

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

559559
%{ice_agent: ice_agent}
560560
end
@@ -564,7 +564,7 @@ defmodule ExICE.ICEAgent.ImplTest do
564564

565565
ice_agent = ICEAgent.Impl.handle_timeout(ice_agent)
566566

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

570570
resp =
@@ -590,7 +590,7 @@ defmodule ExICE.ICEAgent.ImplTest do
590590

591591
ice_agent = ICEAgent.Impl.handle_timeout(ice_agent)
592592

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

596596
resp =

0 commit comments

Comments
 (0)