Skip to content

Commit 93114e6

Browse files
committed
Add DNS message encoder/decoder
1 parent 3d98186 commit 93114e6

File tree

3 files changed

+325
-8
lines changed

3 files changed

+325
-8
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: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
%{
22
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
3-
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
43
"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"},
54
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
65
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
@@ -9,16 +8,9 @@
98
"ex_stun": {:hex, :ex_stun, "0.1.0", "252474bf4c8519fbf4bc0fbfc6a1b846a634b1478c65dbbfb4b6ab4e33c2a95a", [:mix], [], "hexpm", "629fc8be45b624a92522f81d85ba001877b1f0745889a2419bdb678790d7480c"},
109
"excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"},
1110
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
12-
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
13-
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
1411
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
1512
"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"},
1613
"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"},
1714
"makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"},
18-
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
19-
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
2015
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
21-
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
22-
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
23-
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
2416
}

test/dns/message_test.exs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 "invalid message" do
20+
assert ExICE.DNS.Message.decode(<<>>) == :error
21+
# header too short (should be 96)
22+
assert ExICE.DNS.Message.decode(<<0::95>>) == :error
23+
end
24+
25+
test "mdns query" do
26+
query = %ExICE.DNS.Message{
27+
question: [
28+
%{
29+
qname: @addr,
30+
qtype: :a,
31+
qclass: :in,
32+
unicast_response: false
33+
}
34+
]
35+
}
36+
37+
assert ExICE.DNS.Message.decode(@mdns_query) == query
38+
assert ExICE.DNS.Message.encode(query) == @mdns_query
39+
end
40+
41+
test "mdns query response" do
42+
query_response = %ExICE.DNS.Message{
43+
qr: true,
44+
aa: true,
45+
answer: [
46+
%{
47+
name: @addr,
48+
type: :a,
49+
ttl: 120,
50+
flush_cache: true,
51+
class: :in,
52+
rdata: <<192, 168, 0, 1>>
53+
}
54+
]
55+
}
56+
57+
assert ExICE.DNS.Message.decode(@mdns_query_response) == query_response
58+
assert ExICE.DNS.Message.encode(query_response) == @mdns_query_response
59+
end
60+
end

0 commit comments

Comments
 (0)