├── .formatter.exs ├── .gitignore ├── README.org ├── lib ├── dns.ex └── dns │ ├── packet.ex │ ├── packet │ ├── header.ex │ ├── question.ex │ └── resource_record.ex │ └── server.ex ├── mix.exs ├── mix.lock └── test ├── dns ├── packet_test.exs └── server_test.exs ├── dns_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | dns-*.tar 24 | 25 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Write a DNS server from scratch in Elixir 2 | 3 | Inspired by [[https://github.com/EmilHernvall/dnsguide][EmilHernvall/dnsguide: A guide to writing a DNS Server from scratch in Rust]], this project is my attempt to write a DNS server in Elixir. 4 | 5 | ** How to use this server? 6 | For now, this server only supports querying an ~A~ record for one domain. 7 | 8 | You can open ~iex -S mix~, and spin up a server like this: 9 | 10 | #+begin_src elixir 11 | {:ok, server} = DNS.Server.start 12 | # => 13 | # {:ok, #PID<0.191.0>} 14 | 15 | DNS.Server.recursive_lookup(server, "baidu.com") 16 | # => 17 | # {:ok, 18 | # %DNS.Packet{ 19 | # additionals: [ 20 | # %{addr: {202, 108, 22, 220}, domain: "dns.baidu.com", ttl: 86400, type: :A}, 21 | # %{addr: {220, 181, 33, 31}, domain: "ns2.baidu.com", ttl: 86400, type: :A}, 22 | # %{addr: {112, 80, 248, 64}, domain: "ns3.baidu.com", ttl: 86400, type: :A}, 23 | # %{addr: {14, 215, 178, 80}, domain: "ns4.baidu.com", ttl: 86400, type: :A}, 24 | # %{addr: {180, 76, 76, 92}, domain: "ns7.baidu.com", ttl: 86400, type: :A} 25 | # ], 26 | # answers: [ 27 | # %{addr: {39, 156, 69, 79}, domain: "baidu.com", ttl: 600, type: :A}, 28 | # %{addr: {220, 181, 38, 148}, domain: "baidu.com", ttl: 600, type: :A} 29 | # ], 30 | # authorities: [ 31 | # %{domain: "baidu.com", host: "ns4.baidu.com", ttl: 86400, type: :NS}, 32 | # %{domain: "baidu.com", host: "dns.baidu.com", ttl: 86400, type: :NS}, 33 | # %{domain: "baidu.com", host: "ns2.baidu.com", ttl: 86400, type: :NS}, 34 | # %{domain: "baidu.com", host: "ns7.baidu.com", ttl: 86400, type: :NS}, 35 | # %{domain: "baidu.com", host: "ns3.baidu.com", ttl: 86400, type: :NS} 36 | # ], 37 | # header: %DNS.Packet.Header{ 38 | # additional_count: 5, 39 | # answer_count: 2, 40 | # authoritative_answer: true, 41 | # authority_count: 5, 42 | # id: 15943, 43 | # operation_code: 0, 44 | # query_response: true, 45 | # question_count: 1, 46 | # recursion_available: false, 47 | # recursion_desired: false, 48 | # reserved: 0, 49 | # response_code: 0, 50 | # truncated_message: false 51 | # }, 52 | # questions: [%DNS.Packet.Question{name: "baidu.com", type: :A}] 53 | # }} 54 | #+end_src 55 | ** Takeaways 56 | The server implement is still in a simple and early stage. 57 | But I've already learned a ton: 58 | - How to parse a DNS Packet 59 | 60 | Parsing a DNS Packet correctly is one of the key foundations for a workable DNS Server. 61 | Below are the steps I took to understand the packet format, implement it in Elixir, and improve the implementation: 62 | 63 | + Understanding the Format 64 | 65 | To understand the format, the best place is the original document that described the DNS implementation and specification: 66 | [[https://tools.ietf.org/html/rfc1035][RFC 1035 - Domain names - implementation and specification]]. 67 | 68 | Several fun facts I learned from this documentation: 69 | 1. What's the syntax of a domain. 70 | 2. Why domain names are case-insensitive. 71 | + Basic Pattern Matching 72 | + Parser Combinator 73 | * Readability 74 | * Benchmark 75 | - How to work with Dialyzer 76 | - How to build a UDP Server in Elixir 77 | - Adding concurrency to an Elixir program is like a breeze 78 | 79 | ** Future Improvements 80 | 1. Parse domains as FQDN 81 | 2. Add support to more resource record types (:TXT, :SOA, :ALIAS, etc.) 82 | 3. Caching 83 | 4. Extract functional core from ~DNS.Server~ 84 | -------------------------------------------------------------------------------- /lib/dns.ex: -------------------------------------------------------------------------------- 1 | defmodule DNS do 2 | @moduledoc """ 3 | Documentation for `DNS`. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> DNS.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/dns/packet.ex: -------------------------------------------------------------------------------- 1 | defmodule DNS.Packet do 2 | alias DNS.Packet.Header 3 | alias DNS.Packet.Question 4 | alias DNS.Packet.ResourceRecord 5 | 6 | defstruct header: %Header{}, questions: [], answers: [], authorities: [], additionals: [] 7 | 8 | @type t :: %__MODULE__{ 9 | header: Header.t(), 10 | questions: list(Question.t()), 11 | answers: list(), 12 | authorities: list(), 13 | additionals: list() 14 | } 15 | 16 | @spec new_query(binary()) :: t() 17 | def new_query(domain) do 18 | %__MODULE__{ 19 | header: %Header{ 20 | # TODO: pass id as an argument 21 | id: Enum.random(0..65535), 22 | query_response: false, 23 | operation_code: 0, 24 | question_count: 1, 25 | recursion_desired: false, 26 | authoritative_answer: false, 27 | truncated_message: false, 28 | recursion_available: false, 29 | reserved: 0, 30 | response_code: 0, 31 | answer_count: 0, 32 | authority_count: 0, 33 | additional_count: 0 34 | }, 35 | questions: [%Question{name: domain, type: :A}] 36 | } 37 | end 38 | 39 | @spec to_binary(t()) :: binary() 40 | def to_binary(%__MODULE__{} = packet) do 41 | Header.to_binary(packet.header) <> 42 | Question.to_binary(packet.questions) <> 43 | ResourceRecord.to_binary(packet.answers) <> 44 | ResourceRecord.to_binary(packet.authorities) <> 45 | ResourceRecord.to_binary(packet.additionals) 46 | end 47 | 48 | @spec parse(binary()) :: t() 49 | def parse(binary) do 50 | {header, rest} = parse_header(binary) 51 | 52 | {:ok, [questions, answers, authorities, additionals], ""} = 53 | sequence([ 54 | repeat(question_parser(binary), header.question_count), 55 | repeat(resource_record_parser(binary), header.answer_count), 56 | repeat(resource_record_parser(binary), header.authority_count), 57 | repeat(resource_record_parser(binary), header.additional_count) 58 | ]).(rest) 59 | 60 | %__MODULE__{ 61 | header: header, 62 | questions: questions, 63 | answers: answers, 64 | authorities: authorities, 65 | additionals: additionals 66 | } 67 | end 68 | 69 | defp parse_header(<< 70 | id::size(16), 71 | qr::size(1), 72 | opcode::size(4), 73 | aa::size(1), 74 | tc::size(1), 75 | rd::size(1), 76 | ra::size(1), 77 | z::size(3), 78 | rcode::size(4), 79 | qdcount::size(16), 80 | ancount::size(16), 81 | nscount::size(16), 82 | arcount::size(16), 83 | rest::binary 84 | >>) do 85 | { 86 | %Header{ 87 | id: id, 88 | query_response: qr == 1, 89 | operation_code: opcode, 90 | authoritative_answer: aa == 1, 91 | truncated_message: tc == 1, 92 | recursion_desired: rd == 1, 93 | recursion_available: ra == 1, 94 | reserved: z, 95 | response_code: rcode, 96 | question_count: qdcount, 97 | answer_count: ancount, 98 | authority_count: nscount, 99 | additional_count: arcount 100 | }, 101 | rest 102 | } 103 | end 104 | 105 | defp sequence(parsers) do 106 | fn input -> 107 | case parsers do 108 | [] -> 109 | {:ok, [], input} 110 | 111 | [first_parser | other_parsers] -> 112 | with {:ok, first_term, rest} <- first_parser.(input), 113 | {:ok, other_terms, rest} <- sequence(other_parsers).(rest), 114 | do: {:ok, [first_term | other_terms], rest} 115 | end 116 | end 117 | end 118 | 119 | defp repeat(parser, count) do 120 | parser 121 | |> List.duplicate(count) 122 | |> sequence() 123 | end 124 | 125 | defp question_parser(binary) do 126 | fn input -> 127 | {label, rest} = extract_label(input, binary) 128 | <> = rest 129 | {:ok, build_question(label, type_enum), rest} 130 | end 131 | end 132 | 133 | defp build_question(name, type_enum) when is_number(type_enum) do 134 | build_question(name, resolve_type(type_enum)) 135 | end 136 | 137 | defp build_question(name, type) when is_atom(type) do 138 | %Question{name: name, type: type} 139 | end 140 | 141 | defp resource_record_parser(binary) do 142 | fn input -> 143 | {label, rest} = extract_label(input, binary, []) 144 | 145 | <> = rest 147 | 148 | resource_record = build_resource_record(label, type_enum, ttl, rdata, binary) 149 | 150 | {:ok, resource_record, rest} 151 | end 152 | end 153 | 154 | defp build_resource_record(domain, type_enum, ttl, rdata, binary) when is_number(type_enum) do 155 | build_resource_record(domain, resolve_type(type_enum), ttl, rdata, binary) 156 | end 157 | 158 | defp build_resource_record(domain, :A, ttl, rdata, _binary) do 159 | %{ 160 | type: :A, 161 | ttl: ttl, 162 | domain: domain, 163 | addr: rdata |> :binary.bin_to_list() |> List.to_tuple() 164 | } 165 | end 166 | 167 | defp build_resource_record(domain, :NS, ttl, rdata, binary) do 168 | {host, ""} = extract_label(rdata, binary) 169 | 170 | %{ 171 | type: :NS, 172 | ttl: ttl, 173 | domain: domain, 174 | host: host 175 | } 176 | end 177 | 178 | defp build_resource_record(domain, :CNAME, ttl, rdata, binary) do 179 | {host, ""} = extract_label(rdata, binary) 180 | 181 | %{ 182 | type: :CNAME, 183 | ttl: ttl, 184 | domain: domain, 185 | host: host 186 | } 187 | end 188 | 189 | defp build_resource_record(domain, :MX, ttl, rdata, binary) do 190 | <> = rdata 191 | {exchange, ""} = extract_label(rest, binary) 192 | 193 | %{ 194 | type: :MX, 195 | ttl: ttl, 196 | domain: domain, 197 | preference: preference, 198 | exchange: exchange 199 | } 200 | end 201 | 202 | defp build_resource_record(domain, :AAAA, ttl, rdata, _binary) do 203 | ipv6 = 204 | for(<>, do: part) 205 | |> List.to_tuple() 206 | 207 | %{ 208 | type: :AAAA, 209 | ttl: ttl, 210 | domain: domain, 211 | addr: ipv6 212 | } 213 | end 214 | 215 | defp build_resource_record(domain, _, ttl, rdata, _binary) do 216 | %{ 217 | type: :UNKNOWN, 218 | ttl: ttl, 219 | domain: domain, 220 | rdata: rdata 221 | } 222 | end 223 | 224 | defp extract_label(rest, binary, label_parts \\ []) 225 | 226 | defp extract_label(<<1::size(1), 1::size(1), pos::size(14), rest::binary>>, binary, label_parts) do 227 | <<_::bytes-size(pos), jump::bytes>> = binary 228 | {label, _} = extract_label(jump, binary) 229 | 230 | label_parts = [label | label_parts] 231 | {label_parts |> Enum.reverse() |> Enum.join("."), rest} 232 | end 233 | 234 | defp extract_label(<<0, rest::binary>>, _binary, label_parts) do 235 | {label_parts |> Enum.reverse() |> Enum.join("."), rest} 236 | end 237 | 238 | defp extract_label( 239 | <>, 240 | binary, 241 | label_parts 242 | ) do 243 | extract_label(rest, binary, [label_part | label_parts]) 244 | end 245 | 246 | defp resolve_type(1), do: :A 247 | defp resolve_type(2), do: :NS 248 | defp resolve_type(5), do: :CNAME 249 | defp resolve_type(15), do: :MX 250 | defp resolve_type(28), do: :AAAA 251 | defp resolve_type(_), do: :UNKNOWN 252 | end 253 | -------------------------------------------------------------------------------- /lib/dns/packet/header.ex: -------------------------------------------------------------------------------- 1 | defmodule DNS.Packet.Header do 2 | defstruct id: nil, 3 | query_response: false, 4 | operation_code: 0, 5 | authoritative_answer: false, 6 | truncated_message: false, 7 | recursion_desired: false, 8 | recursion_available: false, 9 | reserved: 0, 10 | response_code: 0, 11 | question_count: 0, 12 | answer_count: 0, 13 | authority_count: 0, 14 | additional_count: 0 15 | 16 | @type t :: %__MODULE__{ 17 | id: 0..65535, 18 | query_response: boolean(), 19 | operation_code: 0..15, 20 | authoritative_answer: boolean(), 21 | truncated_message: boolean(), 22 | recursion_desired: boolean(), 23 | recursion_available: boolean(), 24 | reserved: 0..7, 25 | response_code: 0..15, 26 | question_count: 0..65535, 27 | answer_count: 0..65535, 28 | authority_count: 0..65535, 29 | additional_count: 0..65535 30 | } 31 | 32 | def to_binary(%__MODULE__{} = header) do 33 | << 34 | header.id::16, 35 | bool_to_int(header.query_response)::1, 36 | header.operation_code::4, 37 | bool_to_int(header.authoritative_answer)::1, 38 | bool_to_int(header.truncated_message)::1, 39 | bool_to_int(header.recursion_desired)::1, 40 | bool_to_int(header.recursion_available)::1, 41 | header.reserved::3, 42 | header.response_code::4, 43 | header.question_count::16, 44 | header.answer_count::16, 45 | header.authority_count::16, 46 | header.additional_count::16 47 | >> 48 | end 49 | 50 | defp bool_to_int(true), do: 1 51 | defp bool_to_int(false), do: 0 52 | end 53 | -------------------------------------------------------------------------------- /lib/dns/packet/question.ex: -------------------------------------------------------------------------------- 1 | defmodule DNS.Packet.Question do 2 | defstruct name: nil, type: nil 3 | @type t :: %__MODULE__{name: binary(), type: atom()} 4 | 5 | def to_binary(questions) when is_list(questions) do 6 | questions 7 | |> Enum.map(&to_binary/1) 8 | |> Enum.join() 9 | end 10 | 11 | def to_binary(%__MODULE__{} = question) do 12 | to_label(question.name) <> 13 | to_type(question.type) <> 14 | <<1::16>> 15 | end 16 | 17 | defp to_label(binary) do 18 | (binary 19 | |> String.split(".") 20 | |> Enum.map(&[String.length(&1), &1]) 21 | |> IO.iodata_to_binary()) <> 22 | <<0>> 23 | end 24 | 25 | defp to_type(:A), do: <<1::16>> 26 | end 27 | -------------------------------------------------------------------------------- /lib/dns/packet/resource_record.ex: -------------------------------------------------------------------------------- 1 | defmodule DNS.Packet.ResourceRecord do 2 | def to_binary(records) when is_list(records) do 3 | records 4 | |> Enum.map(&to_binary/1) 5 | |> Enum.join() 6 | end 7 | 8 | def to_binary(record) do 9 | rdata = extract_rdata(record) 10 | rdlen = String.length(rdata) 11 | 12 | << 13 | to_label(record.domain)::binary, 14 | to_enum(record.type)::16, 15 | 0::16, 16 | record.ttl::16, 17 | rdlen::16, 18 | rdata::binary 19 | >> 20 | end 21 | 22 | defp to_label(binary) do 23 | (binary 24 | |> String.split(".") 25 | |> Enum.map(&[String.length(&1), &1]) 26 | |> IO.iodata_to_binary()) <> 27 | <<0>> 28 | end 29 | 30 | defp to_enum(:A), do: 1 31 | defp to_enum(:NS), do: 2 32 | defp to_enum(:CNAME), do: 5 33 | defp to_enum(:MX), do: 15 34 | defp to_enum(:AAAA), do: 28 35 | 36 | defp extract_rdata(%{type: :A, addr: addr}), 37 | do: addr |> Tuple.to_list() |> IO.iodata_to_binary() 38 | 39 | defp extract_rdata(%{type: :NS, host: host}) do 40 | to_label(host) 41 | end 42 | 43 | defp extract_rdata(%{type: :CNAME, host: host}) do 44 | to_label(host) 45 | end 46 | 47 | defp extract_rdata(%{type: :MX, preference: preference, exchange: exchange}) do 48 | <> 49 | end 50 | 51 | defp extract_rdata(%{type: :AAAA, addr: addr}), 52 | do: addr |> Tuple.to_list() |> Enum.map(&<<&1::16>>) |> IO.iodata_to_binary() 53 | end 54 | -------------------------------------------------------------------------------- /lib/dns/server.ex: -------------------------------------------------------------------------------- 1 | defmodule DNS.Server do 2 | @root_dns {198, 41, 0, 4} 3 | @default_dns_port 53 4 | 5 | use GenServer 6 | 7 | def start do 8 | GenServer.start(__MODULE__, []) 9 | end 10 | 11 | def stop(server) do 12 | GenServer.stop(server) 13 | end 14 | 15 | def recursive_lookup(server, domain) do 16 | GenServer.call(server, {:recursive_lookup, domain}) 17 | end 18 | 19 | @impl true 20 | def init(_) do 21 | {:ok, udp_server} = Socket.UDP.open(2053, mode: :active) 22 | 23 | {:ok, %{udp_server: udp_server, callees: %{}}} 24 | end 25 | 26 | @impl true 27 | def handle_call({:recursive_lookup, domain}, from, %{udp_server: udp_server} = state) do 28 | query = 29 | domain 30 | |> DNS.Packet.new_query() 31 | 32 | binary = 33 | query 34 | |> DNS.Packet.to_binary() 35 | 36 | :ok = Socket.Datagram.send(udp_server, binary, {@root_dns, @default_dns_port}) 37 | 38 | {:noreply, put_in(state.callees[query.header.id], %{from: from, query: query})} 39 | end 40 | 41 | @impl true 42 | def handle_info( 43 | {:udp, udp_server, _, @default_dns_port, response}, 44 | %{udp_server: udp_server} = state 45 | ) do 46 | response = DNS.Packet.parse(response) 47 | 48 | {callee, new_state} = pop_in(state.callees[response.header.id]) 49 | 50 | if response.header.answer_count > 0 do 51 | GenServer.reply(callee.from, {:ok, response}) 52 | 53 | {:noreply, new_state} 54 | else 55 | [%{host: next_dns_server_domain} | _] = response.authorities 56 | 57 | %{addr: next_dns_server_ip} = 58 | response.additionals 59 | |> Enum.find(&match?(%{domain: ^next_dns_server_domain, type: :A}, &1)) 60 | 61 | :ok = 62 | Socket.Datagram.send( 63 | udp_server, 64 | DNS.Packet.to_binary(callee.query), 65 | {next_dns_server_ip, @default_dns_port} 66 | ) 67 | 68 | {:noreply, put_in(new_state.callees[response.header.id], callee)} 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DNS.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dns, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | {:socket, "~> 0.3.0"} 25 | # {:dep_from_hexpm, "~> 0.3.0"}, 26 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 27 | ] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm", "f82ea9833ef49dde272e6568ab8aac657a636acb4cf44a7de8a935acb8957c2e"}, 3 | } 4 | -------------------------------------------------------------------------------- /test/dns/packet_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DNS.PacketTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias DNS.Packet 5 | 6 | describe "parse/1 header section" do 7 | test "parses header" do 8 | query = 9 | <<0x86, 0x2A, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x06, 0x67, 10 | 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 11 | 0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x25, 0x00, 0x04, 0xD8, 0x3A, 12 | 0xD3, 0x8E>> 13 | 14 | assert match?( 15 | %{ 16 | header: %{ 17 | id: 0x862A, 18 | recursion_desired: true, 19 | truncated_message: false, 20 | authoritative_answer: false, 21 | operation_code: 0, 22 | query_response: true, 23 | reserved: 0, 24 | response_code: 0, 25 | recursion_available: true, 26 | question_count: 1, 27 | answer_count: 1, 28 | authority_count: 0, 29 | additional_count: 0 30 | } 31 | }, 32 | Packet.parse(query) 33 | ) 34 | end 35 | end 36 | 37 | describe "parse/1 question section" do 38 | test "parses one question" do 39 | query = 40 | <<0x86, 0x2A, 0x81, 0x80, 0, 1, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x06, 0x67, 0x6F, 41 | 0x6F, 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, 42 | 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x25, 0x00, 0x04, 0xD8, 0x3A, 0xD3, 43 | 0x8E>> 44 | 45 | assert match?( 46 | %{ 47 | questions: [ 48 | %{name: "google.com", type: :A} 49 | ] 50 | }, 51 | Packet.parse(query) 52 | ) 53 | end 54 | 55 | test "parses multiple questions" do 56 | query = 57 | <<0x86, 0x2A, 0x81, 0x80, 0, 2, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x06, 0x67, 0x6F, 58 | 0x6F, 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0x06, 59 | 0x67, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 60 | 0x01, 0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x25, 0x00, 0x04, 0xD8, 61 | 0x3A, 0xD3, 0x8E>> 62 | 63 | assert match?( 64 | %{ 65 | questions: [ 66 | %{name: "google.com", type: :A}, 67 | %{name: "google.com", type: :A} 68 | ] 69 | }, 70 | Packet.parse(query) 71 | ) 72 | end 73 | end 74 | 75 | describe "parse/1 answer section" do 76 | test "parses one answer" do 77 | query = 78 | <<0x86, 0x2A, 0x81, 0x80, 0x00, 0x01, 0, 1, 0x00, 0x00, 0x00, 0x00, 0x06, 0x67, 0x6F, 79 | 0x6F, 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, 80 | 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x25, 0x00, 0x04, 0xD8, 0x3A, 0xD3, 81 | 0x8E>> 82 | 83 | assert match?( 84 | %{ 85 | answers: [ 86 | %{domain: "google.com", addr: {216, 58, 211, 142}, ttl: 293} 87 | ] 88 | }, 89 | Packet.parse(query) 90 | ) 91 | end 92 | 93 | test "parses multiple answers" do 94 | query = 95 | <<0x86, 0x2A, 0x81, 0x80, 0x00, 0x02, 0, 2, 0x00, 0x00, 0x00, 0x00, 0x06, 0x67, 0x6F, 96 | 0x6F, 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0x06, 97 | 0x67, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 98 | 0x01, 0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x25, 0x00, 0x04, 0xD8, 99 | 0x3A, 0xD3, 0x8E, 0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x25, 0x00, 100 | 0x04, 0xD8, 0x3A, 0xD3, 0x8E>> 101 | 102 | assert match?( 103 | %{ 104 | answers: [ 105 | %{domain: "google.com", addr: {216, 58, 211, 142}, ttl: 293}, 106 | %{domain: "google.com", addr: {216, 58, 211, 142}, ttl: 293} 107 | ] 108 | }, 109 | Packet.parse(query) 110 | ) 111 | end 112 | 113 | test "includes answer's type info" do 114 | answer = 115 | <<0x1F, 0x00, 0x81, 0x80, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 116 | 0x77, 0x77, 0x05, 0x62, 0x61, 0x69, 0x64, 0x75, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 117 | 0x01, 0x00, 0x01, 0xC0, 0x0C, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x01, 0xE3, 0x00, 118 | 0x0F, 0x03, 0x77, 0x77, 0x77, 0x01, 0x61, 0x06, 0x73, 0x68, 0x69, 0x66, 0x65, 0x6E, 119 | 0xC0, 0x16, 0xC0, 0x2B, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x0E, 120 | 0x03, 0x77, 0x77, 0x77, 0x07, 0x77, 0x73, 0x68, 0x69, 0x66, 0x65, 0x6E, 0xC0, 0x16, 121 | 0xC0, 0x46, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x76, 0x00, 0x04, 0x67, 0xEB, 122 | 0x2E, 0x27>> 123 | 124 | assert match?( 125 | %{ 126 | answers: [ 127 | %{type: :CNAME}, 128 | %{type: :CNAME}, 129 | %{type: :A} 130 | ] 131 | }, 132 | Packet.parse(answer) 133 | ) 134 | end 135 | end 136 | 137 | describe "parse/1 authority section" do 138 | test "multiple authorities" do 139 | answer = 140 | <<0xB6, 0xDA, 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x0C, 0x05, 0x7A, 141 | 0x68, 0x69, 0x68, 0x75, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, 142 | 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x14, 0x01, 0x65, 0x0C, 143 | 0x67, 0x74, 0x6C, 0x64, 0x2D, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x03, 0x6E, 144 | 0x65, 0x74, 0x00, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 145 | 0x04, 0x01, 0x62, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 146 | 0x00, 0x00, 0x04, 0x01, 0x6A, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 147 | 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x6D, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 148 | 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x69, 0xC0, 0x29, 0xC0, 0x12, 0x00, 149 | 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x66, 0xC0, 0x29, 0xC0, 150 | 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x61, 0xC0, 151 | 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 152 | 0x67, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 153 | 0x04, 0x01, 0x68, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 154 | 0x00, 0x00, 0x04, 0x01, 0x6C, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 155 | 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x6B, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 156 | 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x63, 0xC0, 0x29, 0xC0, 0x12, 0x00, 157 | 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x64, 0xC0, 0x29, 0xC0, 158 | 0x27, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0xC0, 0x0C, 0x5E, 159 | 0x1E, 0xC0, 0x27, 0x00, 0x1C, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x10, 0x20, 160 | 0x01, 0x05, 0x02, 0x1C, 0xA1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 161 | 0x30, 0xC0, 0x47, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0xC0, 162 | 0x21, 0x0E, 0x1E, 0xC0, 0x47, 0x00, 0x1C, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 163 | 0x10, 0x20, 0x01, 0x05, 0x03, 0x23, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 164 | 0x02, 0x00, 0x30, 0xC0, 0x57, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 165 | 0x04, 0xC0, 0x30, 0x4F, 0x1E, 0xC0, 0x57, 0x00, 0x1C, 0x00, 0x01, 0x00, 0x02, 0xA3, 166 | 0x00, 0x00, 0x10, 0x20, 0x01, 0x05, 0x02, 0x70, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 167 | 0x00, 0x00, 0x00, 0x00, 0x30, 0xC0, 0x67, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0xA3, 168 | 0x00, 0x00, 0x04, 0xC0, 0x37, 0x53, 0x1E, 0xC0, 0x67, 0x00, 0x1C, 0x00, 0x01, 0x00, 169 | 0x02, 0xA3, 0x00, 0x00, 0x10, 0x20, 0x01, 0x05, 0x01, 0xB1, 0xF9, 0x00, 0x00, 0x00, 170 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xC0, 0x77, 0x00, 0x01, 0x00, 0x01, 0x00, 171 | 0x02, 0xA3, 0x00, 0x00, 0x04, 0xC0, 0x2B, 0xAC, 0x1E, 0xC0, 0x77, 0x00, 0x1C, 0x00, 172 | 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x10, 0x20, 0x01, 0x05, 0x03, 0x39, 0xC1, 0x00, 173 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xC0, 0x87, 0x00, 0x01, 0x00, 174 | 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0xC0, 0x23, 0x33, 0x1E, 0xC0, 0x97, 0x00, 175 | 0x01, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0xC0, 0x05, 0x06, 0x1E>> 176 | 177 | assert match?( 178 | %{ 179 | authorities: [ 180 | %{ 181 | domain: "com", 182 | type: :NS, 183 | ttl: 172_800, 184 | host: "e.gtld-servers.net" 185 | }, 186 | %{ 187 | domain: "com", 188 | type: :NS, 189 | ttl: 172_800, 190 | host: "b.gtld-servers.net" 191 | } 192 | | _ 193 | ] 194 | }, 195 | Packet.parse(answer) 196 | ) 197 | end 198 | end 199 | 200 | describe "parse/1 additional section" do 201 | test "multiple additionals" do 202 | answer = 203 | <<0xB6, 0xDA, 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x0C, 0x05, 0x7A, 204 | 0x68, 0x69, 0x68, 0x75, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, 205 | 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x14, 0x01, 0x65, 0x0C, 206 | 0x67, 0x74, 0x6C, 0x64, 0x2D, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x03, 0x6E, 207 | 0x65, 0x74, 0x00, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 208 | 0x04, 0x01, 0x62, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 209 | 0x00, 0x00, 0x04, 0x01, 0x6A, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 210 | 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x6D, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 211 | 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x69, 0xC0, 0x29, 0xC0, 0x12, 0x00, 212 | 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x66, 0xC0, 0x29, 0xC0, 213 | 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x61, 0xC0, 214 | 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 215 | 0x67, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 216 | 0x04, 0x01, 0x68, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 217 | 0x00, 0x00, 0x04, 0x01, 0x6C, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 0x01, 0x00, 218 | 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x6B, 0xC0, 0x29, 0xC0, 0x12, 0x00, 0x02, 0x00, 219 | 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x63, 0xC0, 0x29, 0xC0, 0x12, 0x00, 220 | 0x02, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0x01, 0x64, 0xC0, 0x29, 0xC0, 221 | 0x27, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0xC0, 0x0C, 0x5E, 222 | 0x1E, 0xC0, 0x27, 0x00, 0x1C, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x10, 0x20, 223 | 0x01, 0x05, 0x02, 0x1C, 0xA1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 224 | 0x30, 0xC0, 0x47, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0xC0, 225 | 0x21, 0x0E, 0x1E, 0xC0, 0x47, 0x00, 0x1C, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 226 | 0x10, 0x20, 0x01, 0x05, 0x03, 0x23, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 227 | 0x02, 0x00, 0x30, 0xC0, 0x57, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 228 | 0x04, 0xC0, 0x30, 0x4F, 0x1E, 0xC0, 0x57, 0x00, 0x1C, 0x00, 0x01, 0x00, 0x02, 0xA3, 229 | 0x00, 0x00, 0x10, 0x20, 0x01, 0x05, 0x02, 0x70, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 230 | 0x00, 0x00, 0x00, 0x00, 0x30, 0xC0, 0x67, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0xA3, 231 | 0x00, 0x00, 0x04, 0xC0, 0x37, 0x53, 0x1E, 0xC0, 0x67, 0x00, 0x1C, 0x00, 0x01, 0x00, 232 | 0x02, 0xA3, 0x00, 0x00, 0x10, 0x20, 0x01, 0x05, 0x01, 0xB1, 0xF9, 0x00, 0x00, 0x00, 233 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xC0, 0x77, 0x00, 0x01, 0x00, 0x01, 0x00, 234 | 0x02, 0xA3, 0x00, 0x00, 0x04, 0xC0, 0x2B, 0xAC, 0x1E, 0xC0, 0x77, 0x00, 0x1C, 0x00, 235 | 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x10, 0x20, 0x01, 0x05, 0x03, 0x39, 0xC1, 0x00, 236 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xC0, 0x87, 0x00, 0x01, 0x00, 237 | 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0xC0, 0x23, 0x33, 0x1E, 0xC0, 0x97, 0x00, 238 | 0x01, 0x00, 0x01, 0x00, 0x02, 0xA3, 0x00, 0x00, 0x04, 0xC0, 0x05, 0x06, 0x1E>> 239 | 240 | assert match?( 241 | %{ 242 | additionals: [ 243 | %{ 244 | domain: "e.gtld-servers.net", 245 | type: :A, 246 | ttl: 172_800, 247 | addr: {192, 12, 94, 30} 248 | }, 249 | %{ 250 | domain: "e.gtld-servers.net", 251 | type: :AAAA, 252 | ttl: 172_800, 253 | addr: {0x2001, 0x502, 0x1CA1, 0, 0, 0, 0, 0x30} 254 | } 255 | | _ 256 | ] 257 | }, 258 | Packet.parse(answer) 259 | ) 260 | end 261 | end 262 | 263 | describe "parse/1 integration" do 264 | test "query packet" do 265 | query = 266 | <<0x2A, 0xD0, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x67, 267 | 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01>> 268 | 269 | assert match?( 270 | %{ 271 | answers: [], 272 | header: %{ 273 | additional_count: 0, 274 | answer_count: 0, 275 | authoritative_answer: false, 276 | authority_count: 0, 277 | id: 10960, 278 | operation_code: 0, 279 | query_response: false, 280 | question_count: 1, 281 | recursion_available: false, 282 | recursion_desired: true, 283 | reserved: 2, 284 | response_code: 0, 285 | truncated_message: false 286 | }, 287 | questions: [%{name: "google.com", type: :A}] 288 | }, 289 | Packet.parse(query) 290 | ) 291 | end 292 | 293 | test "query baidu.com" do 294 | query = 295 | <<0x52, 0x99, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x62, 296 | 0x61, 0x69, 0x64, 0x75, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01>> 297 | 298 | assert match?( 299 | %{ 300 | answers: [], 301 | header: %{ 302 | additional_count: 0, 303 | answer_count: 0, 304 | authoritative_answer: false, 305 | authority_count: 0, 306 | id: 21145, 307 | operation_code: 0, 308 | query_response: false, 309 | question_count: 1, 310 | recursion_available: false, 311 | recursion_desired: true, 312 | reserved: 2, 313 | response_code: 0, 314 | truncated_message: false 315 | }, 316 | questions: [%{name: "baidu.com", type: :A}] 317 | }, 318 | Packet.parse(query) 319 | ) 320 | 321 | answer = 322 | <<0x52, 0x99, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x05, 0x62, 323 | 0x61, 0x69, 0x64, 0x75, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, 324 | 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x34, 0x00, 0x04, 0x27, 0x9C, 0x45, 325 | 0x4F, 0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x34, 0x00, 0x04, 0xDC, 326 | 0xB5, 0x26, 0x94>> 327 | 328 | assert match?( 329 | %{ 330 | answers: [ 331 | %{addr: {39, 156, 69, 79}, domain: "baidu.com", ttl: 308}, 332 | %{addr: {220, 181, 38, 148}, domain: "baidu.com", ttl: 308} 333 | ], 334 | header: %{ 335 | additional_count: 0, 336 | answer_count: 2, 337 | authoritative_answer: false, 338 | authority_count: 0, 339 | id: 21145, 340 | operation_code: 0, 341 | query_response: true, 342 | question_count: 1, 343 | recursion_available: true, 344 | recursion_desired: true, 345 | reserved: 0, 346 | response_code: 0, 347 | truncated_message: false 348 | }, 349 | questions: [%{name: "baidu.com", type: :A}] 350 | }, 351 | Packet.parse(answer) 352 | ) 353 | end 354 | 355 | test "support message compression when a domain name is represented as a sequence of labels ending with a pointer" do 356 | answer = 357 | <<0x52, 0x99, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x05, 0x62, 358 | 0x61, 0x69, 0x64, 0x75, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0x05, 359 | 0x62, 0x61, 0x69, 0x64, 0x75, 0xC0, 18, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x34, 360 | 0x00, 0x04, 0x27, 0x9C, 0x45, 0x4F, 0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 361 | 0x01, 0x34, 0x00, 0x04, 0xDC, 0xB5, 0x26, 0x94>> 362 | 363 | assert match?( 364 | %{ 365 | answers: [ 366 | %{addr: {39, 156, 69, 79}, domain: "baidu.com", ttl: 308}, 367 | %{addr: {220, 181, 38, 148}, domain: "baidu.com", ttl: 308} 368 | ], 369 | header: %{ 370 | additional_count: 0, 371 | answer_count: 2, 372 | authoritative_answer: false, 373 | authority_count: 0, 374 | id: 21145, 375 | operation_code: 0, 376 | query_response: true, 377 | question_count: 1, 378 | recursion_available: true, 379 | recursion_desired: true, 380 | reserved: 0, 381 | response_code: 0, 382 | truncated_message: false 383 | }, 384 | questions: [%{name: "baidu.com", type: :A}] 385 | }, 386 | Packet.parse(answer) 387 | ) 388 | end 389 | 390 | test "support CNAME" do 391 | query = 392 | <<0x1F, 0x00, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 393 | 0x77, 0x77, 0x05, 0x62, 0x61, 0x69, 0x64, 0x75, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 394 | 0x01, 0x00, 0x01>> 395 | 396 | assert match?( 397 | %{ 398 | answers: [], 399 | header: %{ 400 | additional_count: 0, 401 | answer_count: 0, 402 | authoritative_answer: false, 403 | authority_count: 0, 404 | id: 7936, 405 | operation_code: 0, 406 | query_response: false, 407 | question_count: 1, 408 | recursion_available: false, 409 | recursion_desired: true, 410 | reserved: 2, 411 | response_code: 0, 412 | truncated_message: false 413 | }, 414 | questions: [%{name: "www.baidu.com", type: :A}] 415 | }, 416 | Packet.parse(query) 417 | ) 418 | 419 | answer = 420 | <<0x1F, 0x00, 0x81, 0x80, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 421 | 0x77, 0x77, 0x05, 0x62, 0x61, 0x69, 0x64, 0x75, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 422 | 0x01, 0x00, 0x01, 0xC0, 0x0C, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x01, 0xE3, 0x00, 423 | 0x0F, 0x03, 0x77, 0x77, 0x77, 0x01, 0x61, 0x06, 0x73, 0x68, 0x69, 0x66, 0x65, 0x6E, 424 | 0xC0, 0x16, 0xC0, 0x2B, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x0E, 425 | 0x03, 0x77, 0x77, 0x77, 0x07, 0x77, 0x73, 0x68, 0x69, 0x66, 0x65, 0x6E, 0xC0, 0x16, 426 | 0xC0, 0x46, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x76, 0x00, 0x04, 0x67, 0xEB, 427 | 0x2E, 0x27>> 428 | 429 | assert match?( 430 | %{ 431 | answers: [ 432 | %{domain: "www.baidu.com", host: "www.a.shifen.com", ttl: 483}, 433 | %{domain: "www.a.shifen.com", host: "www.wshifen.com", ttl: 62}, 434 | %{addr: {103, 235, 46, 39}, domain: "www.wshifen.com", ttl: 118} 435 | ], 436 | header: %{ 437 | additional_count: 0, 438 | answer_count: 3, 439 | authoritative_answer: false, 440 | authority_count: 0, 441 | id: 7936, 442 | operation_code: 0, 443 | query_response: true, 444 | question_count: 1, 445 | recursion_available: true, 446 | recursion_desired: true, 447 | reserved: 0, 448 | response_code: 0, 449 | truncated_message: false 450 | }, 451 | questions: [%{name: "www.baidu.com", type: :A}] 452 | }, 453 | Packet.parse(answer) 454 | ) 455 | end 456 | end 457 | 458 | describe "new_query/1 for one domain" do 459 | test "generates a random id" do 460 | %{header: %{id: id}} = Packet.new_query("example.com") 461 | 462 | assert id > 0 463 | assert id < 65536 464 | end 465 | 466 | test "sets query_response to false" do 467 | assert %{header: %{query_response: false}} = Packet.new_query("example.com") 468 | end 469 | 470 | test "sets operation_code to 0" do 471 | assert %{header: %{operation_code: 0}} = Packet.new_query("example.com") 472 | end 473 | 474 | test "sets recursion_desired to false" do 475 | assert %{header: %{recursion_desired: false}} = Packet.new_query("example.com") 476 | end 477 | 478 | test "sets question_count to 1" do 479 | assert %{header: %{question_count: 1}} = Packet.new_query("example.com") 480 | end 481 | 482 | test "pushes the domain into questions" do 483 | assert %{questions: [%{name: "example.com", type: :A}]} = Packet.new_query("example.com") 484 | end 485 | 486 | test "sets other fields to false or 0 or []" do 487 | assert %{ 488 | header: %{ 489 | authoritative_answer: false, 490 | truncated_message: false, 491 | recursion_available: false, 492 | reserved: 0, 493 | response_code: 0, 494 | answer_count: 0, 495 | authority_count: 0, 496 | additional_count: 0 497 | }, 498 | answers: [], 499 | authorities: [], 500 | additionals: [] 501 | } = Packet.new_query("example.com") 502 | end 503 | end 504 | 505 | describe "to_binary/1 header section" do 506 | test "sets ID correctly" do 507 | packet = %Packet{header: %Packet.Header{id: 65531}} 508 | 509 | assert <<65531::16, _::bits>> = Packet.to_binary(packet) 510 | end 511 | 512 | test "sets QR correctly" do 513 | packet = %Packet{header: %Packet.Header{id: 65531, query_response: false}} 514 | assert <<_ID::16, 0::1, _::bits>> = Packet.to_binary(packet) 515 | 516 | packet = %Packet{header: %Packet.Header{id: 65531, query_response: true}} 517 | assert <<_ID::16, 1::1, _::bits>> = Packet.to_binary(packet) 518 | end 519 | 520 | test "sets Opcode correctly" do 521 | packet = %Packet{header: %Packet.Header{id: 65531, operation_code: 15}} 522 | assert <<_ID::16, _QR::1, 15::4, _::bits>> = Packet.to_binary(packet) 523 | end 524 | 525 | test "sets AA correctly" do 526 | packet = %Packet{header: %Packet.Header{id: 65531, authoritative_answer: false}} 527 | assert <<_ID::16, _QR::1, _Opcode::4, 0::1, _::bits>> = Packet.to_binary(packet) 528 | 529 | packet = %Packet{header: %Packet.Header{id: 65531, authoritative_answer: true}} 530 | assert <<_ID::16, _QR::1, _Opcode::4, 1::1, _::bits>> = Packet.to_binary(packet) 531 | end 532 | 533 | test "sets TC correctly" do 534 | packet = %Packet{header: %Packet.Header{id: 65531, truncated_message: false}} 535 | assert <<_ID::16, _QR::1, _Opcode::4, _AA::1, 0::1, _::bits>> = Packet.to_binary(packet) 536 | 537 | packet = %Packet{header: %Packet.Header{id: 65531, truncated_message: true}} 538 | assert <<_ID::16, _QR::1, _Opcode::4, _AA::1, 1::1, _::bits>> = Packet.to_binary(packet) 539 | end 540 | 541 | test "sets RD correctly" do 542 | packet = %Packet{header: %Packet.Header{id: 65531, recursion_desired: false}} 543 | 544 | assert <<_ID::16, _QR::1, _Opcode::4, _AA::1, _TC::1, 0::1, _::bits>> = 545 | Packet.to_binary(packet) 546 | 547 | packet = %Packet{header: %Packet.Header{id: 65531, recursion_desired: true}} 548 | 549 | assert <<_ID::16, _QR::1, _Opcode::4, _AA::1, _TC::1, 1::1, _::bits>> = 550 | Packet.to_binary(packet) 551 | end 552 | 553 | test "sets RA correctly" do 554 | packet = %Packet{header: %Packet.Header{id: 65531, recursion_available: false}} 555 | 556 | assert <<_ID::16, _QR::1, _Opcode::4, _AA::1, _TC::1, _RD::1, 0::1, _::bits>> = 557 | Packet.to_binary(packet) 558 | 559 | packet = %Packet{header: %Packet.Header{id: 65531, recursion_available: true}} 560 | 561 | assert <<_ID::16, _QR::1, _Opcode::4, _AA::1, _TC::1, _RD::1, 1::1, _::bits>> = 562 | Packet.to_binary(packet) 563 | end 564 | 565 | test "sets Z correctly" do 566 | packet = %Packet{header: %Packet.Header{id: 65531, reserved: 7}} 567 | 568 | assert <<_ID::16, _QR::1, _Opcode::4, _AA::1, _TC::1, _RD::1, _RA::1, 7::3, _::bits>> = 569 | Packet.to_binary(packet) 570 | end 571 | 572 | test "sets RCODE correctly" do 573 | packet = %Packet{header: %Packet.Header{id: 65531, response_code: 15}} 574 | 575 | assert <<_ID::16, _QR::1, _Opcode::4, _AA::1, _TC::1, _RD::1, _RA::1, _Z::3, 15::4, 576 | _::bits>> = Packet.to_binary(packet) 577 | end 578 | 579 | test "sets QDCOUNT correctly" do 580 | packet = %Packet{header: %Packet.Header{id: 65531, question_count: 60000}} 581 | assert <<_::32, 60000::16, _::bits>> = Packet.to_binary(packet) 582 | end 583 | 584 | test "sets ANCOUNT correctly" do 585 | packet = %Packet{header: %Packet.Header{id: 65531, answer_count: 60001}} 586 | assert <<_::32, _QDCOUNT::16, 60001::16, _::bits>> = Packet.to_binary(packet) 587 | end 588 | 589 | test "sets NSCOUNT correctly" do 590 | packet = %Packet{header: %Packet.Header{id: 65531, authority_count: 60002}} 591 | assert <<_::32, _QDCOUNT::16, _ANCOUNT::16, 60002::16, _::bits>> = Packet.to_binary(packet) 592 | end 593 | 594 | test "sets ARCOUNT correctly" do 595 | packet = %Packet{header: %Packet.Header{id: 65531, additional_count: 60003}} 596 | 597 | assert <<_::32, _QDCOUNT::16, _ANCOUNT::16, _NSCOUNT::16, 60003::16, _::bits>> = 598 | Packet.to_binary(packet) 599 | end 600 | end 601 | 602 | describe "to_binary/1 question section" do 603 | test "one question" do 604 | packet = %Packet{ 605 | header: %Packet.Header{id: 65531}, 606 | questions: [%Packet.Question{name: "example.com", type: :A}] 607 | } 608 | 609 | assert <<_HEADER::96, 7, "example", 3, "com", 0, 1::16, 1::16>> = Packet.to_binary(packet) 610 | end 611 | 612 | test "multiple questions" do 613 | packet = %Packet{ 614 | header: %Packet.Header{id: 65531}, 615 | questions: [ 616 | %Packet.Question{name: "example.com", type: :A}, 617 | %Packet.Question{name: "test.example.com", type: :A} 618 | ] 619 | } 620 | 621 | assert <<_HEADER::96, 7, "example", 3, "com", 0, 1::16, 1::16, 4, "test", 7, "example", 3, 622 | "com", 0, 1::16, 1::16>> = Packet.to_binary(packet) 623 | end 624 | end 625 | 626 | describe "to_binary/1 answer section" do 627 | test "one A answer" do 628 | packet = %Packet{ 629 | header: %Packet.Header{id: 65531}, 630 | answers: [ 631 | %{type: :A, ttl: 64321, domain: "example.com", addr: {192, 168, 1, 1}} 632 | ] 633 | } 634 | 635 | assert <<_HEADER::96, 7, "example", 3, "com", 0, 1::16, 0::16, 64321::16, 4::16, 192, 168, 636 | 1, 1>> = Packet.to_binary(packet) 637 | end 638 | 639 | test "multiple various answers" do 640 | packet = %Packet{ 641 | header: %Packet.Header{id: 65531}, 642 | answers: [ 643 | %{type: :A, ttl: 64321, domain: "example.com", addr: {192, 168, 1, 1}}, 644 | %{type: :NS, ttl: 64321, domain: "example.com", host: "ns.example.com"}, 645 | %{type: :CNAME, ttl: 64321, domain: "example.com", host: "cname.example.com"}, 646 | %{ 647 | type: :MX, 648 | ttl: 64321, 649 | domain: "example.com", 650 | preference: 5, 651 | exchange: "mail1.example.com" 652 | }, 653 | %{ 654 | type: :AAAA, 655 | ttl: 64321, 656 | domain: "example.com", 657 | addr: {0x2001, 0x502, 0x1CA1, 0, 0, 0, 0, 0x30} 658 | } 659 | ] 660 | } 661 | 662 | assert << 663 | _HEADER::96, 664 | # A 665 | 7, 666 | "example", 667 | 3, 668 | "com", 669 | 0, 670 | 1::16, 671 | 0::16, 672 | 64321::16, 673 | 4::16, 674 | 192, 675 | 168, 676 | 1, 677 | 1, 678 | # NS 679 | 7, 680 | "example", 681 | 3, 682 | "com", 683 | 0, 684 | 2::16, 685 | 0::16, 686 | 64321::16, 687 | 16::16, 688 | 2, 689 | "ns", 690 | 7, 691 | "example", 692 | 3, 693 | "com", 694 | 0, 695 | # CNAME 696 | 7, 697 | "example", 698 | 3, 699 | "com", 700 | 0, 701 | 5::16, 702 | 0::16, 703 | 64321::16, 704 | 19::16, 705 | 5, 706 | "cname", 707 | 7, 708 | "example", 709 | 3, 710 | "com", 711 | 0, 712 | # MX 713 | 7, 714 | "example", 715 | 3, 716 | "com", 717 | 0, 718 | 15::16, 719 | 0::16, 720 | 64321::16, 721 | 21::16, 722 | 5::16, 723 | 5, 724 | "mail1", 725 | 7, 726 | "example", 727 | 3, 728 | "com", 729 | 0, 730 | # AAAA 731 | 7, 732 | "example", 733 | 3, 734 | "com", 735 | 0, 736 | 28::16, 737 | 0::16, 738 | 64321::16, 739 | 16::16, 740 | 0x2001::16, 741 | 0x0502::16, 742 | 0x1CA1::16, 743 | 0::16, 744 | 0::16, 745 | 0::16, 746 | 0::16, 747 | 0x30::16 748 | >> = Packet.to_binary(packet) 749 | end 750 | end 751 | 752 | describe "binary |> parse |> to_binary" do 753 | test "simple question" do 754 | binary = 755 | <<0xB6, 0xDA, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x7A, 756 | 0x68, 0x69, 0x68, 0x75, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01>> 757 | 758 | assert binary == binary |> Packet.parse() |> Packet.to_binary() 759 | end 760 | end 761 | end 762 | -------------------------------------------------------------------------------- /test/dns/server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DNS.ServerTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias DNS.Server 5 | 6 | defmodule GoogleDNS do 7 | @google_dns {{8, 8, 8, 8}, 53} 8 | 9 | def query(domain) do 10 | binary = domain |> DNS.Packet.new_query() |> DNS.Packet.to_binary() 11 | 12 | server = Socket.UDP.open!(2053) 13 | Socket.Datagram.send!(server, binary, @google_dns) 14 | {response, @google_dns} = Socket.Datagram.recv!(server) 15 | Socket.close!(server) 16 | 17 | DNS.Packet.parse(response) 18 | end 19 | end 20 | 21 | describe "returns the same result as GoogleDNS" do 22 | test "zhihu.com" do 23 | %{answers: [%{addr: expected}]} = GoogleDNS.query("zhihu.com") 24 | 25 | {:ok, server} = Server.start() 26 | {:ok, %{answers: [%{addr: actual}]}} = Server.recursive_lookup(server, "zhihu.com") 27 | Server.stop(server) 28 | 29 | assert expected == actual 30 | end 31 | end 32 | 33 | describe "recursive_lookup/2" do 34 | test "zhihu.com" do 35 | {:ok, server} = Server.start() 36 | 37 | {:ok, response} = Server.recursive_lookup(server, "zhihu.com") 38 | 39 | assert %{answers: [%{addr: {103, 41, 167, 234}}]} = response 40 | 41 | Server.stop(server) 42 | end 43 | 44 | test "handle queries concurrently" do 45 | {:ok, server} = Server.start() 46 | 47 | assert [ 48 | {:ok, {:ok, %{answers: [%{domain: "zhihu.com"} | _]}}}, 49 | {:ok, {:ok, %{answers: [%{domain: "yahoo.com"} | _]}}}, 50 | {:ok, {:ok, %{answers: [%{domain: "baidu.com"} | _]}}} | _ 51 | ] = 52 | ["zhihu.com", "yahoo.com", "baidu.com"] 53 | |> Task.async_stream(&Server.recursive_lookup(server, &1)) 54 | |> Enum.to_list() 55 | 56 | Server.stop(server) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/dns_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DNSTest do 2 | use ExUnit.Case 3 | doctest DNS 4 | 5 | test "greets the world" do 6 | assert DNS.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------