Skip to content

Commit 785b06d

Browse files
committed
Add MDNS resolver
1 parent 93114e6 commit 785b06d

File tree

10 files changed

+327
-54
lines changed

10 files changed

+327
-54
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: 32 additions & 26 deletions
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
@@ -153,37 +153,43 @@ defmodule ExICE.DNS.Message do
153153

154154
defp decode_name(_, _), do: :error
155155

156-
defp decode_type(<<1::16, data::binary>>), do: {:ok, :a, data}
157-
defp decode_type(<<2::16, data::binary>>), do: {:ok, :ns, data}
158-
defp decode_type(<<3::16, data::binary>>), do: {:ok, :md, data}
159-
defp decode_type(<<4::16, data::binary>>), do: {:ok, :mf, data}
160-
defp decode_type(<<5::16, data::binary>>), do: {:ok, :cname, data}
161-
defp decode_type(<<6::16, data::binary>>), do: {:ok, :soa, data}
162-
defp decode_type(<<7::16, data::binary>>), do: {:ok, :mb, data}
163-
defp decode_type(<<8::16, data::binary>>), do: {:ok, :mg, data}
164-
defp decode_type(<<9::16, data::binary>>), do: {:ok, :mr, data}
165-
defp decode_type(<<10::16, data::binary>>), do: {:ok, :null, data}
166-
defp decode_type(<<11::16, data::binary>>), do: {:ok, :wks, data}
167-
defp decode_type(<<12::16, data::binary>>), do: {:ok, :ptr, data}
168-
defp decode_type(<<13::16, data::binary>>), do: {:ok, :hinfo, data}
169-
defp decode_type(<<14::16, data::binary>>), do: {:ok, :minfo, data}
170-
defp decode_type(<<15::16, data::binary>>), do: {:ok, :mx, data}
171-
defp decode_type(<<16::16, data::binary>>), do: {:ok, :txt, data}
172-
defp decode_type(<<252::16, data::binary>>), do: {:ok, :afxr, data}
173-
defp decode_type(<<253::16, data::binary>>), do: {:ok, :mailb, data}
174-
defp decode_type(<<254::16, data::binary>>), do: {:ok, :maila, data}
175-
defp decode_type(<<255::16, data::binary>>), do: {:ok, :*, data}
156+
defp decode_type(<<type::16, data::binary>>), do: {:ok, do_decode_type(type), data}
176157
defp decode_type(_), do: :error
177158

159+
defp do_decode_type(1), do: :a
160+
defp do_decode_type(2), do: :ns
161+
defp do_decode_type(3), do: :md
162+
defp do_decode_type(4), do: :mf
163+
defp do_decode_type(5), do: :cname
164+
defp do_decode_type(6), do: :soa
165+
defp do_decode_type(7), do: :mb
166+
defp do_decode_type(8), do: :mg
167+
defp do_decode_type(9), do: :mr
168+
defp do_decode_type(10), do: :null
169+
defp do_decode_type(11), do: :wks
170+
defp do_decode_type(12), do: :ptr
171+
defp do_decode_type(13), do: :hinfo
172+
defp do_decode_type(14), do: :minfo
173+
defp do_decode_type(15), do: :mx
174+
defp do_decode_type(16), do: :txt
175+
defp do_decode_type(252), do: :afxr
176+
defp do_decode_type(253), do: :mailb
177+
defp do_decode_type(254), do: :maila
178+
defp do_decode_type(255), do: :*
179+
178180
# In mDNS, the top bit has special meaning.
179181
# See RFC 6762, sec. 18.12 and 18.13.
180-
defp decode_class(<<top_bit::1, 1::15, data::binary>>), do: {:ok, top_bit == 1, :in, data}
181-
defp decode_class(<<top_bit::1, 2::15, data::binary>>), do: {:ok, top_bit == 1, :cs, data}
182-
defp decode_class(<<top_bit::1, 3::15, data::binary>>), do: {:ok, top_bit == 1, :ch, data}
183-
defp decode_class(<<top_bit::1, 4::15, data::binary>>), do: {:ok, top_bit == 1, :hs, data}
184-
defp decode_class(<<top_bit::1, 255::15, data::binary>>), do: {:ok, top_bit == 1, :*, data}
182+
defp decode_class(<<top_bit::1, class::15, data::binary>>),
183+
do: {:ok, top_bit == 1, do_decode_class(class), data}
184+
185185
defp decode_class(_), do: :error
186186

187+
defp do_decode_class(1), do: :in
188+
defp do_decode_class(2), do: :cs
189+
defp do_decode_class(3), do: :ch
190+
defp do_decode_class(4), do: :hs
191+
defp do_decode_class(255), do: :*
192+
187193
defp decode_ttl(<<ttl::32, data::binary>>), do: {:ok, ttl, data}
188194
defp decode_ttl(_), do: :error
189195

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

0 commit comments

Comments
 (0)