Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions lib/ex_ice/dns/message.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
defmodule ExICE.DNS.Message do
@moduledoc false
# DNS Message encoder/decoder implementation.
# See RFC 1035 (DNS) and RFC 6762 (mDNS).
# The latter, repurposes the top bit of query and rr class.
# Limitations:
# * no support for name compression both when decoding and encoding

@type t() :: %__MODULE__{
id: non_neg_integer(),
qr: boolean(),
opcode: non_neg_integer(),
aa: boolean(),
tc: boolean(),
rd: boolean(),
ra: boolean(),
z: non_neg_integer(),
rcode: non_neg_integer(),
question: [map()],
answer: [map()],
authority: [map()],
additional: [map()]
}

defstruct id: 0,
qr: false,
opcode: 0,
aa: false,
tc: false,
rd: false,
ra: false,
z: 0,
rcode: 0,
question: [],
answer: [],
authority: [],
additional: []

@spec decode(binary()) :: {:ok, t()} | :error
def decode(data) do
with {:ok, header, data} <- decode_header(data),
{:ok, body, <<>>} <- decode_body(data, header) do
header = Map.drop(header, [:qdcount, :ancount, :nscount, :arcount])
msg = Map.merge(header, body)
{:ok, struct!(__MODULE__, msg)}
else
_ -> :error
end
end

@spec encode(t()) :: binary()
def encode(message) do
header = encode_header(message)
body = encode_body(message)
header <> body
end

# PRIVATE FUNCTIONS

defp decode_header(
<<id::16, qr::1, opcode::4, aa::1, tc::1, rd::1, ra::1, z::3, rcode::4, qdcount::16,
ancount::16, nscount::16, arcount::16, data::binary>>
) do
header =
%{
id: id,
qr: qr == 1,
opcode: opcode,
aa: aa == 1,
tc: tc == 1,
rd: rd == 1,
ra: ra == 1,
z: z,
rcode: rcode,
qdcount: qdcount,
ancount: ancount,
nscount: nscount,
arcount: arcount
}

{:ok, header, data}
end

defp decode_header(_other), do: :error

defp decode_body(data, header) do
with {:ok, question, data} <- decode_query_section(data, header.qdcount),
{:ok, answer, data} <- decode_rr_section(data, header.ancount),
{:ok, authority, data} <- decode_rr_section(data, header.nscount),
{:ok, additional, data} <- decode_rr_section(data, header.arcount) do
body = %{question: question, answer: answer, authority: authority, additional: additional}
{:ok, body, data}
end
end

defp decode_query_section(data, qdcount, acc \\ [])

defp decode_query_section(data, 0, acc), do: {:ok, Enum.reverse(acc), data}

defp decode_query_section(data, qdcount, acc) do
with {:ok, qname, data} <- decode_name(data),
{:ok, qtype, data} <- decode_type(data),
{:ok, unicast_response, qclass, data} <- decode_class(data) do
question = %{
qname: qname,
qtype: qtype,
qclass: qclass,
unicast_response: unicast_response
}

decode_query_section(data, qdcount - 1, [question | acc])
end
end

defp decode_rr_section(data, rr_count, acc \\ [])

defp decode_rr_section(data, 0, acc), do: {:ok, Enum.reverse(acc), data}

defp decode_rr_section(data, rr_count, acc) do
with {:ok, name, data} <- decode_name(data),
{:ok, type, data} <- decode_type(data),
{:ok, flush_cache, class, data} <- decode_class(data),
{:ok, ttl, data} <- decode_ttl(data),
{:ok, rdata, data} <- decode_rdata(data) do
rr = %{
name: name,
type: type,
flush_cache: flush_cache,
class: class,
ttl: ttl,
rdata: rdata
}

decode_rr_section(data, rr_count - 1, [rr | acc])
end
end

defp decode_name(data, acc \\ [])

defp decode_name(<<0, rest::binary>>, acc) do
name =
acc
|> Enum.reverse()
|> Enum.join(".")

{:ok, name, rest}
end

# we don't support pointers right now
defp decode_name(<<0::2, label_len::6, label::binary-size(label_len), labels::binary>>, acc) do
decode_name(labels, [label | acc])
end

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

defp decode_type(<<1::16, data::binary>>), do: {:ok, :a, data}
defp decode_type(<<2::16, data::binary>>), do: {:ok, :ns, data}
defp decode_type(<<3::16, data::binary>>), do: {:ok, :md, data}
Comment on lines +156 to +158
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: wouldn't it be nicer to do @spec do_decode_type(integet()) :: {:ok, atom()} | :error like defp do_decode_type(1), do: {:ok, :a} or even validate the integer and just do defp do_decode_type(10), do: :a (do_descode_type is just and example name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the one hand yes, but on the other hand I would have to handle this {:ok, class} | :error in decode_type so I am not sure that's worthy

defp decode_type(<<4::16, data::binary>>), do: {:ok, :mf, data}
defp decode_type(<<5::16, data::binary>>), do: {:ok, :cname, data}
defp decode_type(<<6::16, data::binary>>), do: {:ok, :soa, data}
defp decode_type(<<7::16, data::binary>>), do: {:ok, :mb, data}
defp decode_type(<<8::16, data::binary>>), do: {:ok, :mg, data}
defp decode_type(<<9::16, data::binary>>), do: {:ok, :mr, data}
defp decode_type(<<10::16, data::binary>>), do: {:ok, :null, data}
defp decode_type(<<11::16, data::binary>>), do: {:ok, :wks, data}
defp decode_type(<<12::16, data::binary>>), do: {:ok, :ptr, data}
defp decode_type(<<13::16, data::binary>>), do: {:ok, :hinfo, data}
defp decode_type(<<14::16, data::binary>>), do: {:ok, :minfo, data}
defp decode_type(<<15::16, data::binary>>), do: {:ok, :mx, data}
defp decode_type(<<16::16, data::binary>>), do: {:ok, :txt, data}
defp decode_type(<<252::16, data::binary>>), do: {:ok, :afxr, data}
defp decode_type(<<253::16, data::binary>>), do: {:ok, :mailb, data}
defp decode_type(<<254::16, data::binary>>), do: {:ok, :maila, data}
defp decode_type(<<255::16, data::binary>>), do: {:ok, :*, data}
defp decode_type(_), do: :error

# In mDNS, the top bit has special meaning.
# See RFC 6762, sec. 18.12 and 18.13.
defp decode_class(<<top_bit::1, 1::15, data::binary>>), do: {:ok, top_bit == 1, :in, data}
defp decode_class(<<top_bit::1, 2::15, data::binary>>), do: {:ok, top_bit == 1, :cs, data}
defp decode_class(<<top_bit::1, 3::15, data::binary>>), do: {:ok, top_bit == 1, :ch, data}
defp decode_class(<<top_bit::1, 4::15, data::binary>>), do: {:ok, top_bit == 1, :hs, data}
defp decode_class(<<top_bit::1, 255::15, data::binary>>), do: {:ok, top_bit == 1, :*, data}
defp decode_class(_), do: :error

defp decode_ttl(<<ttl::32, data::binary>>), do: {:ok, ttl, data}
defp decode_ttl(_), do: :error

# leave rdata interpretation to the user
defp decode_rdata(<<rdlen::16, rdata::binary-size(rdlen), data::binary>>),
do: {:ok, rdata, data}

defp decode_rdata(_), do: :error

defp encode_header(msg) do
qr = to_int(msg.qr)
aa = to_int(msg.aa)
tc = to_int(msg.tc)
rd = to_int(msg.rd)
ra = to_int(msg.ra)

qdcount = length(msg.question)
ancount = length(msg.answer)
nscount = length(msg.authority)
arcount = length(msg.additional)

<<msg.id::16, qr::1, msg.opcode::4, aa::1, tc::1, rd::1, ra::1, msg.z::3, msg.rcode::4,
qdcount::16, ancount::16, nscount::16, arcount::16>>
end

defp encode_body(msg) do
encode_query_section(msg.question) <>
encode_rr_section(msg.answer) <>
encode_rr_section(msg.authority) <>
encode_rr_section(msg.additional)
end

defp encode_query_section(queries, acc \\ <<>>)
defp encode_query_section([], acc), do: acc

defp encode_query_section([query | queries], acc) do
name = encode_name(query.qname)
type = encode_type(query.qtype)
class = encode_class(query.qclass, query.unicast_response)

acc = acc <> <<name::binary, type::binary, class::binary>>
encode_query_section(queries, acc)
end

defp encode_rr_section(rr, acc \\ <<>>)
defp encode_rr_section([], acc), do: acc

defp encode_rr_section([rr | rrs], acc) do
name = encode_name(rr.name)
type = encode_type(rr.type)
class = encode_class(rr.class, rr.flush_cache)
ttl = <<rr.ttl::32>>
rdlen = <<byte_size(rr.rdata)::16>>

encoded_rr =
<<name::binary, type::binary, class::binary, ttl::binary, rdlen::binary, rr.rdata::binary>>

acc = acc <> encoded_rr
encode_rr_section(rrs, acc)
end

defp encode_name(name) do
for label <- String.split(name, "."), into: <<>> do
size = byte_size(label)
if size > 63, do: raise("Label #{label} too long. Max length: 63.")
<<size, label::binary>>
end <> <<0>>
end

defp encode_type(:a), do: <<1::16>>

defp encode_class(class, top_bit_set) when is_boolean(top_bit_set),
do: encode_class(class, to_int(top_bit_set))

defp encode_class(:in, top_bit), do: <<top_bit::1, 1::15>>

defp to_int(true), do: 1
defp to_int(false), do: 0
end
8 changes: 0 additions & 8 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
%{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
"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"},
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
Expand All @@ -9,16 +8,9 @@
"ex_stun": {:hex, :ex_stun, "0.1.0", "252474bf4c8519fbf4bc0fbfc6a1b846a634b1478c65dbbfb4b6ab4e33c2a95a", [:mix], [], "hexpm", "629fc8be45b624a92522f81d85ba001877b1f0745889a2419bdb678790d7480c"},
"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"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"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"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"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"},
"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"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
}
60 changes: 60 additions & 0 deletions test/dns/message_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule ExICE.DNS.MessageTest do
use ExUnit.Case, async: true

@mdns_query <<0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24,
0x34, 0x35, 0x65, 0x38, 0x62, 0x34, 0x36, 0x39, 0x2D, 0x36, 0x62, 0x32, 0x32,
0x2D, 0x34, 0x66, 0x34, 0x31, 0x2D, 0x61, 0x31, 0x30, 0x32, 0x2D, 0x66, 0x64,
0x39, 0x65, 0x61, 0x38, 0x62, 0x64, 0x36, 0x31, 0x38, 0x32, 0x05, 0x6C, 0x6F,
0x63, 0x61, 0x6C, 0x00, 0x00, 0x01, 0x00, 0x01>>

@mdns_query_response <<0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x24, 0x34, 0x35, 0x65, 0x38, 0x62, 0x34, 0x36, 0x39, 0x2D, 0x36, 0x62,
0x32, 0x32, 0x2D, 0x34, 0x66, 0x34, 0x31, 0x2D, 0x61, 0x31, 0x30, 0x32,
0x2D, 0x66, 0x64, 0x39, 0x65, 0x61, 0x38, 0x62, 0x64, 0x36, 0x31, 0x38,
0x32, 0x05, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x00, 0x00, 0x01, 0x80, 0x01,
0x00, 0x00, 0x00, 0x78, 0x00, 0x04, 0xC0, 0xA8, 0x00, 0x01>>

@addr "45e8b469-6b22-4f41-a102-fd9ea8bd6182.local"

test "invalid message" do
assert ExICE.DNS.Message.decode(<<>>) == :error
# header too short (should be 96)
assert ExICE.DNS.Message.decode(<<0::95>>) == :error
end

test "mdns query" do
query = %ExICE.DNS.Message{
question: [
%{
qname: @addr,
qtype: :a,
qclass: :in,
unicast_response: false
}
]
}

assert {:ok, query} == ExICE.DNS.Message.decode(@mdns_query)
assert @mdns_query == ExICE.DNS.Message.encode(query)
end

test "mdns query response" do
query_response = %ExICE.DNS.Message{
qr: true,
aa: true,
answer: [
%{
name: @addr,
type: :a,
ttl: 120,
flush_cache: true,
class: :in,
rdata: <<192, 168, 0, 1>>
}
]
}

assert {:ok, query_response} == ExICE.DNS.Message.decode(@mdns_query_response)
assert @mdns_query_response == ExICE.DNS.Message.encode(query_response)
end
end