Skip to content

Commit ff85009

Browse files
committed
Add MDNS resolver
1 parent 93114e6 commit ff85009

File tree

10 files changed

+296
-29
lines changed

10 files changed

+296
-29
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/dns/message.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ defmodule ExICE.DNS.Message do
4242
{:ok, body, <<>>} <- decode_body(data, header) do
4343
header = Map.drop(header, [:qdcount, :ancount, :nscount, :arcount])
4444
msg = Map.merge(header, body)
45-
struct!(__MODULE__, msg)
45+
{:ok, struct!(__MODULE__, msg)}
4646
else
4747
_ -> :error
4848
end

lib/ex_ice/ice_agent/impl.ex

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -235,17 +235,35 @@ 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+
case ExICE.MDNS.Resolver.gethostbyname(remote_cand.address) do
241+
{:ok, addr} ->
242+
remote_cand = %Candidate{remote_cand | address: addr}
243+
{:ok, remote_cand}
244+
245+
{:error, _reason} = err ->
246+
err
247+
end
242248

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

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

lib/ex_ice/mdns/resolver.ex

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ defmodule ExICE.DNS.MessageTest do
3434
]
3535
}
3636

37-
assert ExICE.DNS.Message.decode(@mdns_query) == query
38-
assert ExICE.DNS.Message.encode(query) == @mdns_query
37+
assert {:ok, query} == ExICE.DNS.Message.decode(@mdns_query)
38+
assert @mdns_query == ExICE.DNS.Message.encode(query)
3939
end
4040

4141
test "mdns query response" do
@@ -54,7 +54,7 @@ defmodule ExICE.DNS.MessageTest do
5454
]
5555
}
5656

57-
assert ExICE.DNS.Message.decode(@mdns_query_response) == query_response
58-
assert ExICE.DNS.Message.encode(query_response) == @mdns_query_response
57+
assert {:ok, query_response} == ExICE.DNS.Message.decode(@mdns_query_response)
58+
assert @mdns_query_response == ExICE.DNS.Message.encode(query_response)
5959
end
6060
end

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)