diff --git a/lib/ex_ice/dns/message.ex b/lib/ex_ice/dns/message.ex new file mode 100644 index 0000000..b8ccf32 --- /dev/null +++ b/lib/ex_ice/dns/message.ex @@ -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( + <> + ) 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} + 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(<>), do: {:ok, top_bit == 1, :in, data} + defp decode_class(<>), do: {:ok, top_bit == 1, :cs, data} + defp decode_class(<>), do: {:ok, top_bit == 1, :ch, data} + defp decode_class(<>), do: {:ok, top_bit == 1, :hs, data} + defp decode_class(<>), do: {:ok, top_bit == 1, :*, data} + defp decode_class(_), do: :error + + defp decode_ttl(<>), do: {:ok, ttl, data} + defp decode_ttl(_), do: :error + + # leave rdata interpretation to the user + defp decode_rdata(<>), + 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) + + <> + 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 <> <> + 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 = <> + rdlen = <> + + encoded_rr = + <> + + 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.") + <> + 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: <> + + defp to_int(true), do: 1 + defp to_int(false), do: 0 +end diff --git a/mix.lock b/mix.lock index 6eef951..dbba9ab 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, @@ -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"}, } diff --git a/test/dns/message_test.exs b/test/dns/message_test.exs new file mode 100644 index 0000000..d9d4ce6 --- /dev/null +++ b/test/dns/message_test.exs @@ -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