Skip to content

Commit 75acda0

Browse files
committed
wip
1 parent 3d98186 commit 75acda0

File tree

7 files changed

+438
-2
lines changed

7 files changed

+438
-2
lines changed

lib/ex_ice/app.ex

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

lib/ex_ice/dns_message.ex

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
defmodule ExICE.DNS.Message do
2+
@moduledoc false
3+
# DNS Message encoder/decoder implementation.
4+
# See RFC 1035 (DNS) and RFC 6762 (mDNS).
5+
# The latter, repurposes the top bit of query and rr class.
6+
# Limitations:
7+
# * no support for name compression both when decoding and encoding
8+
9+
@type t() :: %__MODULE__{
10+
id: non_neg_integer(),
11+
qr: boolean(),
12+
opcode: non_neg_integer(),
13+
aa: boolean(),
14+
tc: boolean(),
15+
rd: boolean(),
16+
ra: boolean(),
17+
z: non_neg_integer(),
18+
rcode: non_neg_integer(),
19+
question: [map()],
20+
answer: [map()],
21+
authority: [map()],
22+
additional: [map()]
23+
}
24+
25+
defstruct id: 0,
26+
qr: false,
27+
opcode: 0,
28+
aa: false,
29+
tc: false,
30+
rd: false,
31+
ra: false,
32+
z: 0,
33+
rcode: 0,
34+
question: [],
35+
answer: [],
36+
authority: [],
37+
additional: []
38+
39+
@spec decode(binary()) :: {:ok, t()} | :error
40+
def decode(data) do
41+
with {:ok, header, data} <- decode_header(data),
42+
{:ok, body, <<>>} <- decode_body(data, header) do
43+
header = Map.drop(header, [:qdcount, :ancount, :nscount, :arcount])
44+
msg = Map.merge(header, body)
45+
struct!(__MODULE__, msg)
46+
else
47+
_ -> :error
48+
end
49+
end
50+
51+
@spec encode(t()) :: binary()
52+
def encode(message) do
53+
header = encode_header(message)
54+
body = encode_body(message)
55+
header <> body
56+
end
57+
58+
# PRIVATE FUNCTIONS
59+
60+
defp decode_header(
61+
<<id::16, qr::1, opcode::4, aa::1, tc::1, rd::1, ra::1, z::3, rcode::4, qdcount::16,
62+
ancount::16, nscount::16, arcount::16, data::binary>>
63+
) do
64+
header =
65+
%{
66+
id: id,
67+
qr: qr == 1,
68+
opcode: opcode,
69+
aa: aa == 1,
70+
tc: tc == 1,
71+
rd: rd == 1,
72+
ra: ra == 1,
73+
z: z,
74+
rcode: rcode,
75+
qdcount: qdcount,
76+
ancount: ancount,
77+
nscount: nscount,
78+
arcount: arcount
79+
}
80+
81+
{:ok, header, data}
82+
end
83+
84+
defp decode_header(_other), do: :error
85+
86+
defp decode_body(data, header) do
87+
with {:ok, question, data} <- decode_query_section(data, header.qdcount),
88+
{:ok, answer, data} <- decode_rr_section(data, header.ancount),
89+
{:ok, authority, data} <- decode_rr_section(data, header.nscount),
90+
{:ok, additional, data} <- decode_rr_section(data, header.arcount) do
91+
body = %{question: question, answer: answer, authority: authority, additional: additional}
92+
{:ok, body, data}
93+
end
94+
end
95+
96+
defp decode_query_section(data, qdcount, acc \\ [])
97+
98+
defp decode_query_section(data, 0, acc), do: {:ok, Enum.reverse(acc), data}
99+
100+
defp decode_query_section(data, qdcount, acc) do
101+
with {:ok, qname, data} <- decode_name(data),
102+
{:ok, qtype, data} <- decode_type(data),
103+
{:ok, unicast_response, qclass, data} <- decode_class(data) do
104+
question = %{
105+
qname: qname,
106+
qtype: qtype,
107+
qclass: qclass,
108+
unicast_response: unicast_response
109+
}
110+
111+
decode_query_section(data, qdcount - 1, [question | acc])
112+
end
113+
end
114+
115+
defp decode_rr_section(data, rr_count, acc \\ [])
116+
117+
defp decode_rr_section(data, 0, acc), do: {:ok, Enum.reverse(acc), data}
118+
119+
defp decode_rr_section(data, rr_count, acc) do
120+
with {:ok, name, data} <- decode_name(data),
121+
{:ok, type, data} <- decode_type(data),
122+
{:ok, flush_cache, class, data} <- decode_class(data),
123+
{:ok, ttl, data} <- decode_ttl(data),
124+
{:ok, rdata, data} <- decode_rdata(data) do
125+
rr = %{
126+
name: name,
127+
type: type,
128+
flush_cache: flush_cache,
129+
class: class,
130+
ttl: ttl,
131+
rdata: rdata
132+
}
133+
134+
decode_rr_section(data, rr_count - 1, [rr | acc])
135+
end
136+
end
137+
138+
defp decode_name(data, acc \\ [])
139+
140+
defp decode_name(<<0, rest::binary>>, acc) do
141+
name =
142+
acc
143+
|> Enum.reverse()
144+
|> Enum.join(".")
145+
146+
{:ok, name, rest}
147+
end
148+
149+
# we don't support pointers right now
150+
defp decode_name(<<0::2, label_len::6, label::binary-size(label_len), labels::binary>>, acc) do
151+
decode_name(labels, [label | acc])
152+
end
153+
154+
defp decode_name(_, _), do: :error
155+
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}
176+
defp decode_type(_), do: :error
177+
178+
# In mDNS, the top bit has special meaning.
179+
# 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}
185+
defp decode_class(_), do: :error
186+
187+
defp decode_ttl(<<ttl::32, data::binary>>), do: {:ok, ttl, data}
188+
defp decode_ttl(_), do: :error
189+
190+
# leave rdata interpretation to the user
191+
defp decode_rdata(<<rdlen::16, rdata::binary-size(rdlen), data::binary>>),
192+
do: {:ok, rdata, data}
193+
194+
defp decode_rdata(_), do: :error
195+
196+
defp encode_header(msg) do
197+
qr = to_int(msg.qr)
198+
aa = to_int(msg.aa)
199+
tc = to_int(msg.tc)
200+
rd = to_int(msg.rd)
201+
ra = to_int(msg.ra)
202+
203+
qdcount = length(msg.question)
204+
ancount = length(msg.answer)
205+
nscount = length(msg.authority)
206+
arcount = length(msg.additional)
207+
208+
<<msg.id::16, qr::1, msg.opcode::4, aa::1, tc::1, rd::1, ra::1, msg.z::3, msg.rcode::4,
209+
qdcount::16, ancount::16, nscount::16, arcount::16>>
210+
end
211+
212+
defp encode_body(msg) do
213+
encode_query_section(msg.question) <>
214+
encode_rr_section(msg.answer) <>
215+
encode_rr_section(msg.authority) <>
216+
encode_rr_section(msg.additional)
217+
end
218+
219+
defp encode_query_section(queries, acc \\ <<>>)
220+
defp encode_query_section([], acc), do: acc
221+
222+
defp encode_query_section([query | queries], acc) do
223+
name = encode_name(query.qname)
224+
type = encode_type(query.qtype)
225+
class = encode_class(query.qclass, query.unicast_response)
226+
227+
acc = acc <> <<name::binary, type::binary, class::binary>>
228+
encode_query_section(queries, acc)
229+
end
230+
231+
defp encode_rr_section(rr, acc \\ <<>>)
232+
defp encode_rr_section([], acc), do: acc
233+
234+
defp encode_rr_section([rr | rrs], acc) do
235+
name = encode_name(rr.name)
236+
type = encode_type(rr.type)
237+
class = encode_class(rr.class, rr.flush_cache)
238+
ttl = <<rr.ttl::32>>
239+
rdlen = <<byte_size(rr.rdata)::16>>
240+
241+
encoded_rr =
242+
<<name::binary, type::binary, class::binary, ttl::binary, rdlen::binary, rr.rdata::binary>>
243+
244+
acc = acc <> encoded_rr
245+
encode_rr_section(rrs, acc)
246+
end
247+
248+
defp encode_name(name) do
249+
for label <- String.split(name, "."), into: <<>> do
250+
size = byte_size(label)
251+
if size > 63, do: raise("Label #{label} too long. Max length: 63.")
252+
<<size, label::binary>>
253+
end <> <<0>>
254+
end
255+
256+
defp encode_type(:a), do: <<1::16>>
257+
258+
defp encode_class(class, top_bit_set) when is_boolean(top_bit_set),
259+
do: encode_class(class, to_int(top_bit_set))
260+
261+
defp encode_class(:in, top_bit), do: <<top_bit::1, 1::15>>
262+
263+
defp to_int(true), do: 1
264+
defp to_int(false), do: 0
265+
end

lib/ex_ice/ice_agent/impl.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,17 @@ defmodule ExICE.ICEAgent.Impl do
233233
end
234234

235235
def add_remote_candidate(ice_agent, remote_cand) do
236-
Logger.debug("New remote candidate: #{inspect(remote_cand)}")
236+
Logger.debug("New remote candidate: #{remote_cand}")
237+
238+
case String.split(remote_cand, " ", parts: 8) do
239+
[_f_str, _c_str, _tr_str, _pr_str, a_str, _po_str, "typ", _ty_str] ->
240+
if String.ends_with?(a_str, ".local") do
241+
ExICE.MDNS.Resolver.resolve(a_str)
242+
end
243+
244+
_ ->
245+
:ok
246+
end
237247

238248
case Candidate.unmarshal(remote_cand) do
239249
{:ok, remote_cand} ->

lib/ex_ice/mdns_resolver.ex

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
defmodule ExICE.MDNS.Resolver do
2+
use GenServer, restart: :transient
3+
4+
require Logger
5+
6+
@mdns_port 5353
7+
@multicast_addr {{224, 0, 0, 251}, @mdns_port}
8+
9+
@spec start_link(term()) :: GenServer.on_start()
10+
def start_link(args) do
11+
GenServer.start_link(__MODULE__, args, name: __MODULE__)
12+
end
13+
14+
@spec resolve(String.t()) :: {:ok, :inet.ip_address()} | {:error, term()}
15+
def resolve(addr) do
16+
GenServer.call(__MODULE__, {:resolve, addr})
17+
end
18+
19+
@impl true
20+
def init(_args) do
21+
Logger.debug("Starting MDNS Resolver")
22+
23+
ret =
24+
:gen_udp.open(
25+
# Listen on the port specific to mDNS traffic.
26+
# `add_membership` option only defines an address.
27+
@mdns_port,
28+
mode: :binary,
29+
reuseaddr: true,
30+
active: true,
31+
# Allow other apps to bind to @mdns_port.
32+
# If there are multiple sockets, bound to the same port,
33+
# and subscribed to the same group (in fact, if one socket
34+
# subscribes to some group, all other sockets bound to
35+
# the same port also join this gorup), all those sockets
36+
# will receive every message. In other words, `reuseport` for
37+
# multicast works differently than for casual sockets.
38+
reuseport: true,
39+
# Support running two ICE agents on a single machine.
40+
# In other case, our request won't be delivered to the mDNS address owner
41+
# running on the same machine (e.g., a web browser).
42+
multicast_loop: true,
43+
# Receive responses - they are sent to the multicast address.
44+
# The second argument specifies interfaces where we should listen
45+
# for multicast traffic.
46+
# This option works on interfaces i.e. it affects all sockets
47+
# bound to the same port.
48+
add_membership: {{224, 0, 0, 251}, {0, 0, 0, 0}}
49+
)
50+
51+
case ret do
52+
{:ok, socket} ->
53+
{:ok, %{socket: socket, queries: %{}}}
54+
55+
{:error, reason} ->
56+
Logger.error(
57+
"Couldn't start MDNS resolver, reason: #{reason}. MDNS candidates won't be resolved."
58+
)
59+
60+
{:stop, {:shutdown, reason}}
61+
end
62+
end
63+
64+
@impl true
65+
def handle_call({:resolve, addr}, from, state) do
66+
Logger.debug("Trying to resolve addr: #{addr}")
67+
68+
query =
69+
%ExICE.DNS.Message{
70+
question: [
71+
%{
72+
qname: addr,
73+
qtype: :a,
74+
qclass: :in,
75+
unicast_response: false
76+
}
77+
]
78+
}
79+
|> ExICE.DNS.Message.encode()
80+
81+
state = put_in(state, [:queries, addr], from)
82+
83+
case :gen_udp.send(state.socket, @multicast_addr, query) do
84+
:ok -> {:noreply, state}
85+
{:error, reason} -> {:reply, {:error, reason}, state}
86+
end
87+
end
88+
89+
@impl true
90+
def handle_info({:udp, _socket, _ip, _port, packet}, state) do
91+
ExICE.DNS.Message.decode(packet)
92+
{:noreply, state}
93+
end
94+
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

0 commit comments

Comments
 (0)