Skip to content

Commit 0a58d1c

Browse files
committed
Add DNS message encoder/decoder
1 parent 3d98186 commit 0a58d1c

File tree

3 files changed

+322
-0
lines changed

3 files changed

+322
-0
lines changed

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

mix.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
44
"credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"},
55
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
6+
"dns": {:hex, :dns, "2.4.0", "44790a0375b28bdc7b59fc894460bfcb03ffeec4c5984e2c3e8b0797b1518327", [:mix], [], "hexpm", "e178e353c469820d02ba889d6a80d01c8c27b47dfcda4016a9cbc6218e3eed64"},
67
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
78
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
89
"ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"},
@@ -15,6 +16,8 @@
1516
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
1617
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
1718
"makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"},
19+
"mdns": {:hex, :mdns, "1.0.13", "7e56a193942826232a799171a2bc50977acc41981e2dc3693196df61ca272005", [:mix], [{:dns, "~> 2.4", [hex: :dns, repo: "hexpm", optional: false]}], "hexpm", "c16129884f9fb49233d5f03d00148e813ef5841864fc4b629e6b8e1307eaecbc"},
20+
"mdns_lite": {:hex, :mdns_lite, "0.8.10", "d89183b737f95c243da94297c8d25460493b3ab2452100b2037987a07642070d", [:mix], [{:vintage_net, "~> 0.7", [hex: :vintage_net, repo: "hexpm", optional: true]}], "hexpm", "2723259aa4587b269a625ff61d1cff0cd30a8940288b7b998aec0e4139ec98eb"},
1821
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
1922
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
2023
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},

test/dns/message_test.exs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
defmodule ExICE.DNS.MessageTest do
2+
use ExUnit.Case, async: true
3+
4+
@mdns_query <<0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24,
5+
0x34, 0x35, 0x65, 0x38, 0x62, 0x34, 0x36, 0x39, 0x2D, 0x36, 0x62, 0x32, 0x32,
6+
0x2D, 0x34, 0x66, 0x34, 0x31, 0x2D, 0x61, 0x31, 0x30, 0x32, 0x2D, 0x66, 0x64,
7+
0x39, 0x65, 0x61, 0x38, 0x62, 0x64, 0x36, 0x31, 0x38, 0x32, 0x05, 0x6C, 0x6F,
8+
0x63, 0x61, 0x6C, 0x00, 0x00, 0x01, 0x00, 0x01>>
9+
10+
@mdns_query_response <<0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
11+
0x24, 0x34, 0x35, 0x65, 0x38, 0x62, 0x34, 0x36, 0x39, 0x2D, 0x36, 0x62,
12+
0x32, 0x32, 0x2D, 0x34, 0x66, 0x34, 0x31, 0x2D, 0x61, 0x31, 0x30, 0x32,
13+
0x2D, 0x66, 0x64, 0x39, 0x65, 0x61, 0x38, 0x62, 0x64, 0x36, 0x31, 0x38,
14+
0x32, 0x05, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x00, 0x00, 0x01, 0x80, 0x01,
15+
0x00, 0x00, 0x00, 0x78, 0x00, 0x04, 0xC0, 0xA8, 0x00, 0x01>>
16+
17+
@addr "45e8b469-6b22-4f41-a102-fd9ea8bd6182.local"
18+
19+
test "mdns query" do
20+
query = %ExICE.DNS.Message{
21+
question: [
22+
%{
23+
qname: @addr,
24+
qtype: :a,
25+
qclass: :in,
26+
unicast_response: false
27+
}
28+
]
29+
}
30+
31+
assert ExICE.DNS.Message.decode(@mdns_query) == query
32+
assert ExICE.DNS.Message.encode(query) == @mdns_query
33+
end
34+
35+
test "mdns query response" do
36+
query_response = %ExICE.DNS.Message{
37+
qr: true,
38+
aa: true,
39+
answer: [
40+
%{
41+
name: @addr,
42+
type: :a,
43+
ttl: 120,
44+
flush_cache: true,
45+
class: :in,
46+
rdata: <<192, 168, 0, 1>>
47+
}
48+
]
49+
}
50+
51+
assert ExICE.DNS.Message.decode(@mdns_query_response) == query_response
52+
assert ExICE.DNS.Message.encode(query_response) == @mdns_query_response
53+
end
54+
end

0 commit comments

Comments
 (0)