resolved
resolved
(pronounced "resolved", not "resolved") is a simple DNS server, and
associated tools, for home networks. To that end, it supports:
- Three modes of operation: as a recursive or forwarding nameserver (with caching) or as an authoritative nameserver for your specified domains only.
- Defining custom records in hosts files (to make existing DNS blacklists each to use) and in zone files.
- Listening on either IPv4 or IPv6, and communicating with upstream nameservers over both.
Usage
Install rustup
, and then install the default toolchain:
rustup show
Then, compile in release mode;
cargo build --release
The DNS Server
resolved
hasn't had any sort of security review, so be wary of exposing it on a public network.
Since resolved
binds to port 53 (both UDP and TCP), it needs to be
run as root or to have the CAP_NET_BIND_SERVICE
capability.
sudo ./target/release/resolved -Z config/zones
The config/zones
directory contains standard configuration which you'll
usually want to have (such as the "root hints" file), so you would typically
either put your zone files in config/zones
, or put them somewhere else and
pass a second -Z
option like so:
sudo ./target/release/resolved -Z config/zones -Z /path/to/your/zone/files
See the CLI documentation for more.
The DNS Client
There is also a dnsq
utility to resolve names based on the server
configuration directly. The main purpose of it is to test configuration
changes.
$ ./target/release/dnsq www.barrucadu.co.uk. AAAA -Z config/zones
;; QUESTION
www.barrucadu.co.uk. IN AAAA
;; ANSWER
www.barrucadu.co.uk. 300 IN CNAME barrucadu.co.uk.
barrucadu.co.uk. 300 IN AAAA 2a01:4f8:c0c:bfc1::
See the --help
text for all options.
Other Tools
There are also four utility programs (htoh
, htoz
, ztoh
, and ztoz
) to
convert between hosts files and zone files.
They accept any syntactically valid file as input, and output it in a consistent
format regardless of how the input is structured, so htoh
and ztoz
can be
used to normalise existing files.
Development
Rust sources are in the crates/
directory. There are two shared libraries:
dns-types
- basic types used in other packages (crate documentation)dns-resolver
- the DNS resolvers (crate documentation)
And six binaries:
dnsq
- utility to resolve DNS queries (crate documentation)resolved
- the DNS server (crate documentation)htoh
- utility to normalise hosts files (crate documentation)htoz
- utility to convert hosts files to zone files (crate documentation)ztoh
- utility to convert zone files to hosts files (crate documentation)ztoz
- utility to normalise zone files (crate documentation)
Developing with nix
Open a development shell:
nix develop
And run cargo commands in there.
Testing
Run the unit tests with:
cargo test
There are also fuzz tests in the fuzz/
directory, using
cargo-fuzz
:
cargo install cargo-fuzz
# list targets
cargo fuzz list
# run a target until it panics or is killed with ctrl-c
cargo fuzz run <target>
Supported standards
-
RFC 1034: Domain Names - Concepts and Facilities
Gives the basic semantics of DNS and the algorithms for recursive and non-recursive resolution.
-
RFC 1035: Domain Names - Implementation and Specification
Defines the wire format and discusses implementation concerns of the algorithms from RFC 1034.
-
RFC 2782: A DNS RR for specifying the location of services (DNS SRV)
Defines the
SRV
record and query types. -
RFC 3596: DNS Extensions to Support IP Version 6
Defines the
AAAA
record and query types. -
RFC 4343: Domain Name System (DNS) Case Insensitivity Clarification
Clarifies that domain names are not ASCII, and yet for case insensitivity purposes are case-folded as ASCII is. And also that "case preservation", as required by other RFCs, is more or less meaningless.
-
RFC 6761: Special-Use Domain Names
Defines several zones with special behaviour. This is RFC implemented as configuration distributed with the DNS server (in
config/zones
) not code. -
Defines the Linux hosts file format.
Introduction to DNS
The Domain Name System is a huge distributed eventually-consistent database
mapping names, like www.barrucadu.co.uk
, to numbers, like 116.203.34.201
.
It's federated, with trusted entities delegating control of segments of the DNS
namespace to others. It holds hundreds of millions of records, and updates to
this database are typically visible in minutes to hours.
And the protocol behind it is not massively different to when it was standardised in the 1980s in RFC 1034: Domain Names - Concepts and Facilities and RFC 1035: Domain Names - Implementation and Specification.
This page gives an overview of how all this works, covering:
- The DNS protocol
- How your browser gets from
www.barrucadu.co.uk
to an IP address - What a "zone" is
- The difference between authoritative, recursive, and forwarding nameservers
- What happens when you update a DNS record (there's no such thing as "propagation")
The DNS protocol
Let's start with an example (the +noedns
flag turns off some extensions to the
basic DNS protocol):
$ dig +noedns www.barrucadu.co.uk
; <<>> DiG 9.18.19 <<>> +noedns www.barrucadu.co.uk
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42090
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;www.barrucadu.co.uk. IN A
;; ANSWER SECTION:
www.barrucadu.co.uk. 300 IN CNAME barrucadu.co.uk.
barrucadu.co.uk. 300 IN A 116.203.34.201
;; Query time: 28 msec
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
;; WHEN: Sat Oct 14 15:22:49 BST 2023
;; MSG SIZE rcvd: 67
I've used dig
a lot so I'm fairly used to reading this output, but I've since
realised I wasn't really reading it.
What does flags: qr rd ra
mean? What about AUTHORITY
and ADDITIONAL
?
Also, all the domain names there have a trailing dot. What's that about?
Time to dig into the protocol. RFC 1035 is our guide here.
Format of a DNS Message
DNS has two types of messages, queries and responses, and uses port 53. It prefers UDP but, if a message is too long to send in a single UDP datagram, it falls back to TCP.
A DNS message has five parts. These are:
-
A header, which specifies what sort of message this is and how many entries are in the other parts. This also has those flags we saw in the
dig
output. -
The "question section", which specifies what sort of records the client is interested in. In principle you can ask multiple questions in a single query, but in practice this isn't widely supported.
-
The "answer section", a collection of records directly answering the questions.
-
The "authority section", a series of
NS
records pointing to an authoritative source which can answer the questions. -
The "additional section", a series of records which may be useful when using records from the answer and authority sections. For example, the
A
records for any nameservers given in the authority section. This is often omitted to save space.
The answer, authority, and additional sections won't be present in a query. But the question section will be present in a response: it's copied over from the query.
The Header
The header is 12 bytes long and has a few different fields packed in there. RFC 1035 has some nice ASCII art illustrations:
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Where,
-
ID
is a 16-bit random identifier set by the client and copied into the response by the server. Since UDP is connectionless, the ID is essential for the client to know which response goes with which query. -
QR
indicates whether this is a query (0) or a response (1). -
OPCODE
is a four-bit field, set by the client and copied into the response by the server, indicating what type of query this message is. The most common opcode is 0, which is a "standard query". -
AA
("Authoritative Answer") is set by the server and means that this response is authoritative.More on authority in zones.
-
TC
("Truncation") is set by the server and means that the full response couldn't fit in a single UDP datagram, and so the client should try again using TCP. -
RD
("Recursion Desired") is set by the client, and copied into the response by the server, and means that they would like the server to answer the question recursively, if they can.More on recursive and non-recursive resolution in how resolution happens.
-
RA
("Recursion Available") is set by the server and means that it can perform recursive resolution, if requested. -
Z
is reserved for future use, and so should be set to zero if you don't implement those future standards. -
RCODE
is a four-bit field, set by the server, indicating what type of response this message is. There are a few common ones:- 0 means no error
- 1 means the server couldn't understand the query
- 2 means the server encountered an error processing the query
- 3 means the domain name in the query doesn't exist
- 4 means the server doesn't support this sort of query
- 5 means the server refused to answer the query
-
QDCOUNT
,ANCOUNT
,NSCOUNT
, andARCOUNT
are 16-bit integers specifying the number of entries in the question, answer, authority, and additional sections (respectively) of the message.
All the multi-byte fields in a DNS message are unsigned and big endian.
Domain Names
Before diving into the other sections, let's have a look at how domain names are encoded. They show up a lot, after all.
Let's take the domain name www.barrucadu.co.uk.
, and separate it by dots.
This gives us a sequence of labels:
www
barrucadu
co
uk
- (the empty label)
How you actually interpret those labels is a bit confused, unfortunately.
RFC 1035 says that they are sequences of arbitrary octets and that you can't assume any particular character encoding, but it also says that labels are to be compared case-insensitively.
RFC 4343 clarifies that that means octets in the range 0x41
to 0x5a
(the
upper case ASCII letters) are considered equal to corresponding octets in the
range 0x61
to 0x7a
(the lower case ASCII letters), and vice versa, but that
that still doesn't mean that labels are ASCII, as they can also contain
arbitrary non-ASCII octets.
But there's also RFC 3492, which defines the punycode standard for encoding internationalised, i.e. unicode, domain names into ASCII. So maybe domain names are ASCII after all?
There may well be a later RFC which resolves this ambiguity and says that labels are definitely ASCII, but I haven't seen it yet.
Anyway, back to the topic of encoding.
A label is encoded as a one-octet length field followed by the octets of the
label. And an encoded domain name is a sequence of encoded labels. This means
that a domain name ends with 0x00
, the length of the empty label (and also
makes encoded domain names work as null-terinated C strings, what a handy
coincidence).
So www.barrucadu.co.uk
is encoded as:
0x03 w w w 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00
There are two restrictions on domain names:
-
A single label may be no more than 63 octets long (not including the length octet)
-
An entire encoded domain name may be no more than 255 octets long (including the label length octets)
Compression
Domain names get repeated a lot in DNS messages, and the 512 bytes of a UDP datagram can start to feel pretty limiting. So DNS also has a compression mechanism, where some suffix of a domain name can be replaced with a pointer to an earlier occurrence of that name.
So if the name www.barrucadu.co.uk.
appears in a message twice, the
second occurrence could be represented as:
www.barrucadu.co.uk.
www.barrucadu.co.[pointer to 'uk.']
www.barrucadu.[pointer to 'co.uk.']
www.[pointer to 'barrucadu.co.uk.']
[pointer to 'www.barrucadu.co.uk.']
But how do you distinguish between a regular label and a pointer? Well, remember that a label can't be longer than 63 octets. And what's 63 as an 8-bit binary number?
It's 00111111
.
There's two whole bits there at the front which are completely wasted!
So pointers are encoded as the two-octet sequence 11[14-bit index into message]
.
Pretty clever.
Questions
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ QNAME /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QTYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QCLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Where,
-
QNAME
is the domain name, which can be any length (so long as it's properly encoded), it's not padded to any specific size. -
QTYPE
is a 16-bit integer specifying the type of records the client is interested in. Which will usually be a record type (see the next subsection) or 255, meaning "all records". There are a few otherQTYPE
s but those are less common. -
QCLASS
is a 16-bit integer specifying which network class the client is interested in. These days this will always be 1, orIN
, for "internet".
As an aside, it feels kind of wasteful that we effectively throw away 16 whole bits for each question and record on the historical "class" artefact. UDP messages are short, so we compress domain names to squeeze out a little extra space, but then we waste a bunch like this! Even worse, there never were very many network classes: RFC 1035 only defines four. Did the IETF really expect there to be so many non-internet networks in the future?
We can now understand the question section of our dig
example!
;; QUESTION SECTION:
;www.barrucadu.co.uk. IN A
Means that it's looking for an internet address record for
www.barrucadu.co.uk.
(yes, it shows the type and class the other
way around). That question is encoded as:
0x03 w w w 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 ; qname: www.barrucadu.co.uk.
0x00 0x01 ; qtype: A
0x00 0x01 ; qclass: IN
Resource Records
The answer, authority, and additional sections are all a sequence of resource records:
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ /
/ NAME /
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/ RDATA /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Where,
-
NAME
is the domain name, which is variable-length like theQNAME
of a question. -
TYPE
is a 16-bit integer specifying what sort of record this is. There are a fair few of these, but some common ones are:- 1, an
A
record - 2, a
NS
record - 5, a
CNAME
record - 28, a
AAAA
record (from RFC 3596) - and plenty others
- 1, an
-
CLASS
is a 16-bit integer specifying what network class this record applies to. Like theQCLASS
, these days this will always be 1. Unless you're specifically running some sort of old non-IP-based network for fun. -
TTL
is a 32-bit integer specifying the number of seconds that this record is valid for. This is important for caching purposes. Zero has a special meaning: it means that you can use the record to do whatever it is you're doing right now, but that you can't cache it at all. -
RDLENGTH
is a 16-bit integer specifying the length of theRDATA
section. -
RDATA
is the record data, which is type- and class-specific. For example:IN A
records hold an IPv4 address, as a 32-bit numberIN NS
andIN CNAME
records hold a domain nameIN AAAA
records hold an IPv6 address, as a 128-bit number
Returning to our dig
example, we had two different resource records in the
response:
;; ANSWER SECTION:
www.barrucadu.co.uk. 300 IN CNAME barrucadu.co.uk.
barrucadu.co.uk. 300 IN A 116.203.34.201
We have one IN CNAME
record for www.barrucadu.co.uk.
and one IN A
record
for barrucadu.co.uk.
. This is because, upon encountering a CNAME
,
resolution starts again with whatever name the CNAME
refers to (unless the
query was for, say IN CNAME www.barrucadu.co.uk
- more on this in how
resolution happens).
Leaving out the name compression for simplicity, those records are encoded as:
0x03 w w w 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 ; name: www.barrucadu.co.uk.
0x00 0x05 ; type: CNAME
0x00 0x01 ; class: IN
0x00 0x00 0x01 0x2c ; ttl: 300
0x00 0x11 ; rdlength: 17
0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 ; rdata: barrucadu.co.uk.
0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 ; name: barrucadu.co.uk.
0x00 0x01 ; type: A
0x00 0x01 ; class: IN
0x00 0x00 0x01 0x2c ; ttl: 300
0x00 0x04 ; rdlength: 4
0x74 0xcb 0x22 0xc9 ; rdata: 116.203.34.201
Example DNS query & response
Returning to our dig +noedns www.barrucadu.co.uk
example from the beginning,
we can now see the whole encoded query and response. I've included comments and
linebreaks to make it clear what's what.
Here's the query:
;; header
0xa4 0x6a ; ID: 46676
0x01 0x00 ; flags: RD
0x00 0x01 ; QDCOUNT: 1
0x00 0x00 ; ANCOUNT: 0
0x00 0x00 ; NSCOUNT: 0
0x00 0x00 ; ARCOUNT: 0
;; question section
; www.barrucadu.co.uk. A IN
0x03 w w w 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x01 0x00 0x01
And here's the response (omitting compression):
;; header
0xa4 0x6a ; ID: 46676
0x81 0x80 ; flags: QR, RD, RA
0x00 0x01 ; QDCOUNT: 1
0x00 0x02 ; ANCOUNT: 2
0x00 0x00 ; NSCOUNT: 0
0x00 0x00 ; ARCOUNT: 0
;; question section
; www.barrucadu.co.uk. A IN
0x03 w w w 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x01 0x00 0x01
;; answer section
; www.barrucadu.co.uk. CNAME IN 300 barrucadu.co.uk.
0x03 w w w 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x05 0x00 0x01 0x00 0x00 0x01 0x2c 0x00 0x11 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00
; barrucadu.co.uk. A IN 300 116.203.34.201
0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x01 0x00 0x01 0x00 0x00 0x01 0x2c 0x00 0x04 0x74 0xcb 0x22 0xc9
And that's that!
The DNS protocol isn't very complicated. But it is somewhat fiddly, what with
each record type having its own RDATA
format, and the domain name compression.
How resolution happens
When we ran dig +noedns www.barrucadu.co.uk
in the previous section, we got an
answer. We found the IP address which www.barrucadu.co.uk.
refers to.
But how?
Well, dig
tells us that it talked to some server at 1.1.1.1. But how did
that server know? Does it have a copy of the entire DNS? Unlikely, since
there are hundreds of millions of records in use.
The answer is that the server followed a process called recursive resolution. This is described in section 5.3.3 of RFC 1034:
- See if we already know the answer (e.g. the relevant records are already cached), and return it to the client if so
- Figure out the best nameservers to ask
- Send them queries until one responds
- Analyse the response:
- If the response answers the question, cache it and return it to the client
- If the response gives some better nameservers to use, cache them and go back to step 2
- If the response gives a CNAME, and this is not the answer, cache the CNAME record and start again with the new name
- If the response is an error or doesn't make sense, go back to step 3
On the face of it this looks pretty straightforward, but on closer inspection that step 2 is doing a lot of work: how exactly do we "figure out the best nameservers to ask"? (That step 1 is also doing a surprising amount of work if your nameserver supports authoritative zones (see next section) - for the full details, see section 4.3.2 of RFC 1034).
Well, step 4.b gives us a clue here: if the response gives some better nameservers to use, cache them and go back to step 2. So we don't need to pick the correct nameservers at the very beginning. We only need to know about a nameserver which will be able to point us to a nameserver which knows that (or is closer to knowing that).
There are thirteen nameservers which, transitively, know about every domain name. These are the root nameservers, and they're where recursive resolution starts.
You can find them at a.root-servers.net.
through m.root-servers.net.
So you just point your recursive resolver at, say, j.root-servers.net.
and... oh wait, we have a chicken-and-egg problem. Ultimately, you need to know
their IP addresses. IANA, the Internet Assigned Numbers Authority, provides the
"root hints" file, which has the IPv4 and IPv6 addresses of these root
nameservers.
How do you download that file if you don't have DNS working to resolve
www.iana.org.
? Look, you just need IP addresses to get DNS and DNS to get IP
addresses. Use 1.1.1.1 or something while you get your fancy recursive resolver
working.
Alright, let's resolve www.barrucadu.co.uk.
recursively! Starting with:
$ dig www.barrucadu.co.uk @j.root-servers.net
; <<>> DiG 9.18.19 <<>> www.barrucadu.co.uk @j.root-servers.net
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 52538
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 8, ADDITIONAL: 17
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1472
;; QUESTION SECTION:
;www.barrucadu.co.uk. IN A
;; AUTHORITY SECTION:
uk. 172800 IN NS nsa.nic.uk.
uk. 172800 IN NS nsb.nic.uk.
uk. 172800 IN NS nsc.nic.uk.
uk. 172800 IN NS nsd.nic.uk.
uk. 172800 IN NS dns1.nic.uk.
uk. 172800 IN NS dns2.nic.uk.
uk. 172800 IN NS dns3.nic.uk.
uk. 172800 IN NS dns4.nic.uk.
;; ADDITIONAL SECTION:
nsa.nic.uk. 172800 IN A 156.154.100.3
nsb.nic.uk. 172800 IN A 156.154.101.3
nsc.nic.uk. 172800 IN A 156.154.102.3
nsd.nic.uk. 172800 IN A 156.154.103.3
dns1.nic.uk. 172800 IN A 213.248.216.1
dns2.nic.uk. 172800 IN A 103.49.80.1
dns3.nic.uk. 172800 IN A 213.248.220.1
dns4.nic.uk. 172800 IN A 43.230.48.1
nsa.nic.uk. 172800 IN AAAA 2001:502:ad09::3
nsb.nic.uk. 172800 IN AAAA 2001:502:2eda::3
nsc.nic.uk. 172800 IN AAAA 2610:a1:1009::3
nsd.nic.uk. 172800 IN AAAA 2610:a1:1010::3
dns1.nic.uk. 172800 IN AAAA 2a01:618:400::1
dns2.nic.uk. 172800 IN AAAA 2401:fd80:400::1
dns3.nic.uk. 172800 IN AAAA 2a01:618:404::1
dns4.nic.uk. 172800 IN AAAA 2401:fd80:404::1
;; Query time: 12 msec
;; SERVER: 192.58.128.30#53(j.root-servers.net) (UDP)
;; WHEN: Sat Oct 14 15:26:42 BST 2023
;; MSG SIZE rcvd: 552
Alright, we now know the names and IP addresses of the uk.
nameservers.
Thanks, additional section!
On we go:
$ dig www.barrucadu.co.uk @156.154.100.3
; <<>> DiG 9.18.19 <<>> www.barrucadu.co.uk @156.154.100.3
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32107
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: e393a7276308963401000000652aa5442e48e9fbe6ee0c99 (good)
;; QUESTION SECTION:
;www.barrucadu.co.uk. IN A
;; AUTHORITY SECTION:
barrucadu.co.uk. 172800 IN NS ns-1828.awsdns-36.co.uk.
barrucadu.co.uk. 172800 IN NS ns-1520.awsdns-62.org.
barrucadu.co.uk. 172800 IN NS ns-98.awsdns-12.com.
barrucadu.co.uk. 172800 IN NS ns-763.awsdns-31.net.
;; Query time: 12 msec
;; SERVER: 156.154.100.3#53(156.154.100.3) (UDP)
;; WHEN: Sat Oct 14 15:27:16 BST 2023
;; MSG SIZE rcvd: 215
No additional section here, so we'll need to resolve one of those nameservers.
However, if we pick ns-1828.awsdns-36.co.uk.
we can just ask the uk.
nameservers again rather than going back to the root:
$ dig ns-1828.awsdns-36.co.uk. @156.154.101.3
; <<>> DiG 9.18.19 <<>> ns-1828.awsdns-36.co.uk. @156.154.101.3
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 1223
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 9
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: 3976ec588720dc3001000000652aa5b0bad98873c996aaa1 (good)
;; QUESTION SECTION:
;ns-1828.awsdns-36.co.uk. IN A
;; AUTHORITY SECTION:
awsdns-36.co.uk. 172800 IN NS g-ns-356.awsdns-36.co.uk.
awsdns-36.co.uk. 172800 IN NS g-ns-1511.awsdns-36.co.uk.
awsdns-36.co.uk. 172800 IN NS g-ns-932.awsdns-36.co.uk.
awsdns-36.co.uk. 172800 IN NS g-ns-1832.awsdns-36.co.uk.
;; ADDITIONAL SECTION:
g-ns-1832.awsdns-36.co.uk. 172800 IN A 205.251.199.40
g-ns-1511.awsdns-36.co.uk. 172800 IN A 205.251.197.231
g-ns-932.awsdns-36.co.uk. 172800 IN A 205.251.195.164
g-ns-356.awsdns-36.co.uk. 172800 IN A 205.251.193.100
g-ns-1832.awsdns-36.co.uk. 172800 IN AAAA 2600:9000:5307:2800::1
g-ns-1511.awsdns-36.co.uk. 172800 IN AAAA 2600:9000:5305:e700::1
g-ns-932.awsdns-36.co.uk. 172800 IN AAAA 2600:9000:5303:a400::1
g-ns-356.awsdns-36.co.uk. 172800 IN AAAA 2600:9000:5301:6400::1
;; Query time: 10 msec
;; SERVER: 156.154.101.3#53(156.154.101.3) (UDP)
;; WHEN: Sat Oct 14 15:29:04 BST 2023
;; MSG SIZE rcvd: 350
Getting there, we've now got down to the AWS DNS nameservers. Next!
$ dig ns-1828.awsdns-36.co.uk. @205.251.199.40
; <<>> DiG 9.18.19 <<>> ns-1828.awsdns-36.co.uk. @205.251.199.40
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 8253
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 9
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;ns-1828.awsdns-36.co.uk. IN A
;; ANSWER SECTION:
ns-1828.awsdns-36.co.uk. 172800 IN A 205.251.199.36
;; AUTHORITY SECTION:
awsdns-36.co.uk. 172800 IN NS g-ns-1511.awsdns-36.co.uk.
awsdns-36.co.uk. 172800 IN NS g-ns-1832.awsdns-36.co.uk.
awsdns-36.co.uk. 172800 IN NS g-ns-356.awsdns-36.co.uk.
awsdns-36.co.uk. 172800 IN NS g-ns-932.awsdns-36.co.uk.
;; ADDITIONAL SECTION:
g-ns-1511.awsdns-36.co.uk. 172800 IN A 205.251.197.231
g-ns-1511.awsdns-36.co.uk. 172800 IN AAAA 2600:9000:5305:e700::1
g-ns-1832.awsdns-36.co.uk. 172800 IN A 205.251.199.40
g-ns-1832.awsdns-36.co.uk. 172800 IN AAAA 2600:9000:5307:2800::1
g-ns-356.awsdns-36.co.uk. 172800 IN A 205.251.193.100
g-ns-356.awsdns-36.co.uk. 172800 IN AAAA 2600:9000:5301:6400::1
g-ns-932.awsdns-36.co.uk. 172800 IN A 205.251.195.164
g-ns-932.awsdns-36.co.uk. 172800 IN AAAA 2600:9000:5303:a400::1
;; Query time: 12 msec
;; SERVER: 205.251.199.40#53(205.251.199.40) (UDP)
;; WHEN: Sat Oct 14 15:30:26 BST 2023
;; MSG SIZE rcvd: 338
We've got an IP address for ns-1828.awsdns-36.co.uk.
! Now we can answer our
original question:
$ dig www.barrucadu.co.uk @205.251.199.36
; <<>> DiG 9.18.19 <<>> www.barrucadu.co.uk @205.251.199.36
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62416
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 4, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;www.barrucadu.co.uk. IN A
;; ANSWER SECTION:
www.barrucadu.co.uk. 300 IN CNAME barrucadu.co.uk.
barrucadu.co.uk. 300 IN A 116.203.34.201
;; AUTHORITY SECTION:
barrucadu.co.uk. 172800 IN NS ns-1520.awsdns-62.org.
barrucadu.co.uk. 172800 IN NS ns-1828.awsdns-36.co.uk.
barrucadu.co.uk. 172800 IN NS ns-763.awsdns-31.net.
barrucadu.co.uk. 172800 IN NS ns-98.awsdns-12.com.
;; Query time: 12 msec
;; SERVER: 205.251.199.36#53(205.251.199.36) (UDP)
;; WHEN: Sat Oct 14 15:31:20 BST 2023
;; MSG SIZE rcvd: 212
And we're done, after 5 requests to other nameservers. But in practice, a recursive resolver would likely have some of those already cached and wouldn't need to fetch them again.
Zones
In the previous section, it looked very much like the DNS was broken up into
subtrees (or "zones", if you will) based on the label structure: the .
nameservers knew about the uk.
nameservers, but couldn't answer queries about
subdomains of those directly; and similarly, the uk.
nameservers knew about
the nameservers for barrucadu.co.uk.
, but not any of its other records.
This makes sense. Imagine if the root nameservers knew every DNS record! Their databases would be huge! It would be infeasible to run a handful of servers which know hundreds of millions of records and which the whole world uses.
So .
is a zone. And uk.
is a zone. And barrucadu.co.uk.
is a zone. All
of the TLDs are zones, and every domain you can buy creates a new zone. A zone
can be bigger than a single label, e.g. foo.bar.baz.barrucadu.co.uk.
is in
the barrucadu.co.uk.
zone unless I delegate it to someone else, by creating
some NS
records for, say, baz.barrucadu.co.uk.
That's exactly how registering a domain name works, by the way. The registrars
have privileged access to the TLD nameservers, and you pay them some money for
them to send a message to the nameservers saying "please delegate barrucadu
to
these other nameservers".
Zones are traditionally represented in a textual format defined in RFC 1035.
You've seen this format before: it's the format dig
gives its responses in and
it's the format of the root hints file.
Here's the zone file I use for my LAN DNS (which is served by resolved
):
$ORIGIN lan.
@ 300 IN SOA @ @ 6 300 300 300 300
router 300 IN A 10.0.0.1
nyarlathotep 300 IN A 10.0.0.3
*.nyarlathotep 300 IN CNAME nyarlathotep
help 300 IN CNAME nyarlathotep
*.help 300 IN CNAME help
nas 300 IN CNAME nyarlathotep
bedroom.awair 300 IN A 10.0.20.187
living-room.awair 300 IN A 10.0.20.117
It's a list of records, but note that they all use relative domain names (no dot
at the end). I could write them as absolute domain names, but that would be
repetitive, and who doesn't want to golf their zone files? The $ORIGIN
line
at the top is used to complete any relative names, and the @
is an alias for
the origin, so this zone file could also be written as:
lan. 300 IN SOA lan. lan. 6 300 300 300 300
router.lan. 300 IN A 10.0.0.1
nyarlathotep.lan. 300 IN A 10.0.0.3
*.nyarlathotep.lan. 300 IN CNAME nyarlathotep.lan.
help.lan. 300 IN CNAME nyarlathotep.lan.
*.help.lan. 300 IN CNAME help.lan.
nas.lan. 300 IN CNAME nyarlathotep.lan.
bedroom.awair.lan. 300 IN A 10.0.20.187
living-room.awair.lan. 300 IN A 10.0.20.117
Zones come in two types: authoritative (also just called a zone, or a master zone) and non-authoritative (also called hints). An authoritative zone has a SOA record, and causes the nameserver to give authoritative responses to questions which fall into that zone (see the next section).
Non-authoritative zones are primarily useful as a sort of permanent cache. Take
the root hints file for example: all recursive resolvers need to know the NS
records for .
. But they should not act as if they're authoritative for .
,
they just know a little bit about it.
Since any nameserver could claim to be authoritative for any zone it wants, and
I'm sure malicious nameservers often do try to claim ownership of big sites like
google.com.
, how does the DNS work?
It works on trust.
You trust that the root nameservers will give you the correct nameservers for all the TLDs. You then, in turn, trust that the TLD nameservers will give the correct nameservers for the domains registered under those TLDs. And so on, all the way down to the domain you actually want to resolve.
Not every nameserver operator will be equally trustworthy or competent, so that trust does erode somewhat as you move further and further away from the root, but if you do some basic validation of DNS responses (e.g. validating that the answers you get are for domains you know to be delegated to this nameserver), you can do pretty well.
DNS doesn't "propagate"
When I first got into web development, the common wisdom was that DNS changes took 24 to 48 hours to "propagate". But having seen some details of the DNS protocol and how recursive resolution works, does that really make sense? Shouldn't changes be visible as soon as the TTL of the old record expires? And shouldn't new records be visible immediately? Why do changes need to propagate? Where do they propagate to?
Propagation implies a push model, where you make your changes and then they get sent to the resolvers which need them. But that's not what happens at all: instead, caches expire.
Ok, there are two cases in which DNS does propagate:
- If you update your domain's NS records, your registrar needs to push those changes to the TLD nameservers. Apparently this used to be kind of slow, like, 20+ years ago. These days it's very fast.
- If you run a very high traffic authoritative nameserver, you'll operate multiple instances of it around the world to improve reliability and latency. So if you change a record, that change needs to be pushed out to all your servers. But this should take under a minute unless something is very wrong.
My hunch is that this 24 to 48 hour window came from:
- Registrars being slow to update the TLD nameservers once upon a time
- ISPs running notoriously poorly-behaving nameservers
Ah, ISP DNS. Almost the first thing any self-respecting nerd changes when setting up a new home network. They often do nefarious things like redirect misspelled domain names to ad-covered search pages, trying to profit off your typos. And, as it turns out, a lot of them ignore TTLs, and will cache something for a long period if they feel like it.
How long? Well, I've seen reports of 24 hours...
Well, no matter what the cause of the occasional slow DNS update is (though I can't say I've experienced slow DNS updates in a very long time, and updates are evidently fast enough for changing an A record to be considered a viable failover mechanism for big sites) "propagation" is the wrong mental model.
DNS is pull, not push.
Command line interface
resolved
consists of a DNS nameserver and client, and some conversion
utilities for hosts and zone files:
-
resolved - DNS server. Listens on port 53 to respond to DNS queries. Read this in conjunction with the configuration documentation and guides.
-
dnsq - DNS client. Command-line tool to resolve a single query in the same way as
resolved
does, useful for testing configuration changes. -
Conversion utilities. Convert between hosts files and zone files, validating the contents and normalising the formatting.
resolved - DNS server
resolved
hasn't had any sort of security review, so be wary of exposing it on a public network.
A typical usage of resolved
will look like:
sudo /path/to/resolved --cache-size 1000000 \
-Z /path/to/config/zones \
-A /path/to/your/hosts \
-Z /path/to/your/zones
See --help
for a full listing of command-line options (most of which can also
be specified via environment variables), and also the configuration
documentation and guides.
Monitoring
Prometheus metrics are exposed at http://127.0.0.1:9420/metrics
by default.
Logs are emitted to stdout. Control the log level with the RUST_LOG
environment variable:
RUST_LOG=trace
- verbose messages useful for development, like "entered function X"RUST_LOG=debug
- warns about strange but recoverable situations, like "socket read error"RUST_LOG=info
- gives top-level information, like "new connection" or "reloading configuration"RUST_LOG=warn
- warns about recoverable internal errors and invalid configuration, like "could not serialise message" or "invalid record in cache"RUST_LOG=error
- warns about fatal errors and then terminates the process, like "could not bind socket"
You can also set the log level per component. A good default RUST_LOG
definition is dns_resolver=info,resolved=info
.
Set the log format with the RUST_LOG_FORMAT
environment variable, which is a
sequence of comma-separated values:
- One of
full
(default),compact
,pretty
, orjson
- see the tracing_subscriber crate - One of
ansi
(default),no-ansi
- One of
time
(default),no-time
If running under systemd (or some other processor supervisor which automatically
adds timestamps), a good default RUST_LOG_FORMAT
definition is json,no-time
.
Permissions
DNS uses port 53 (both UDP and TCP). So resolved
must be run as root or with
the CAP_NET_BIND_SERVICE
capability.
Signals
SIGUSR1
- reload the configuration
dnsq - DNS client
Answers questions in exactly the same way as resolved
, providing a way to test
configuration changes before deploying them.
For example:
$ /path/to/dnsq www.barrucadu.co.uk. AAAA -Z /path/to/config/zones
;; QUESTION
www.barrucadu.co.uk. IN AAAA
;; ANSWER
www.barrucadu.co.uk. 300 IN CNAME barrucadu.co.uk.
barrucadu.co.uk. 300 IN AAAA 2a01:4f8:c0c:bfc1::
See --help
for a full listing of command-line options (which are a subset of
the resolved
options), and also the configuration documentation and
guides.
Conversion utilities
This is a collection of four programs for converting between hosts files and zones files:
htoh
- Read a hosts file from stdin, output it in a normalised form to stdout.htoz
- Read a hosts file from stdin, convert it to a zone file, and output it in a normalised form to stdout.ztoh
- Read a zone file from stdin, convert it to a hosts file, and output it in a normalised form to stdout.ztoz
- Read a zone file from stdin, output it in a normalised form to stdout.
ztoh
Hosts files can only contain non-wildcard A and AAAA records, so this conversion is lossy.
--strict
- Return an error if the zone file contains any records which cannot be represented in a hosts file
Configuration
This section provides a reference to the resolved
configuration:
-
Hosts and zone files. The file formats themselves and how they affect resolution.
-
Standard zones. The zone files distributed with
resolved
and what they're for.
Hosts and zone files
resolved
supports two venerable formats for specifying DNS information: hosts
files and zone files.
Hosts files can specify A
and AAAA
records, zone files can specify any types
of record. Additionally, zone files can be "authoritative" (treated as fully
defining all the records under a given domain) or "non-authoritative" (treated
as providing a kind of permanent cache).
Non-authoritative zone files are also called "hints" files.
Hosts files
Hosts files follow a simple format, given in the hosts(5) manual page. It is a line-based format where each entry is of the form:
<ip-address> <hostname> [<hostname>...]
The IP address can be an IPv4 address (in which case the entry defines one or
more A
records) or an IPv6 address (in which case the entry defines one or
more AAAA
records).
For example, the following hosts file assigns A
records to example.com
and
example.net
, and AAAA
records to example.com
, example.net
, and
example.org
:
127.0.0.1 example.com example.net
::1 example.com example.net example.org
Hostnames in hosts files do not need the trailing .
, they're interpreted
relative to the root domain.
Zone files
Zone files follow a complicated format, given in section 5 of RFC 1035. It is mostly a line-based format (other than parentheses, see below) where each entry is one of:
$ORIGIN <domain-name>
$INCLUDE <file-name> [<domain-name>]
[<domain-name>] [<ttl>] [<class>] <type> <rdata>
[<domain-name>] [<class>] [<ttl>] <type> <rdata>
Where,
$ORIGIN
sets the base domain for relative domains (i.e. domains without the trailing.
)$INCLUDE
includes another file, optionally setting its$ORIGIN
to a given domain name.- The last two lines define resource records: if the domain name, TTL, or class are omitted the corresponding value from the previous resource record is used.
resolved
doesn't support $INCLUDE
directives, and only supports the IN
record class.
The format of the <rdata>
depends on the <type>
:
A
: an IPv4 address in standard formAAAA
: an IPv6 address in standard formCNAME
: a domain nameHINFO
: a sequence of escaped octetsMB
: a domain nameMD
: a domain nameMF
: a domain nameMG
: a domain nameMR
: a domain nameMX
: a decimal integer (the preference) and a domain name (the exchange)MINFO
: two domain names (the rmailbx and emailbx)NS
: a domain nameNULL
: a sequence of escaped octetsPTR
: a domain nameSOA
: two domain names (the mname and rname) and four decimal integers (the serial, refresh, retry, expire, and minimum)SRV
: three decimal integers (the priority, weight, and port) and a domain name (the target)TXT
: a sequence of escaped octetsWKS
: a sequence of escaped octets
There are some special characters:
@
by itself denotes the current$ORIGIN
\X
whereX
is some non-digit character escapesX
\DDD
whereDDD
is a decimal number is the octet corresponding to that number(
...)
group data that crosses a line boundary"
..."
quote a sequence of octets, allowing spaces within
For example, the following zone file assigns A
, MX
, and SOA
records to
example.com
and CNAME
records to www.example.com
and blog.example.com
:
$ORIGIN example.com.
@ 300 IN SOA @ @ 1 300 300 300 300
@ 300 IN A 127.0.0.1
@ 300 IN MX 10 mail.example.net
www 300 IN CNAME @
blog 300 IN CNAME @
Behaviour
resolved
prefers using records from hosts and zone files to answer queries, so
it is possible to block a domain (or point it elsewhere) with a hosts file and
to arbitrarily override records with zone files.
Internally, hosts files are converted into non-authoritative zones (i.e. a
zone without a SOA
record).
When deciding which zone to use to answer a query, resolved
uses the SOA
records to decide which zones are relevant. This has some consequences:
More specific authoritative zones override less specific ones
resolved
uses the most specific zone it can to answer a query about a domain,
which means if there are records for the same domain across multiple zones, only
one of the zones will be used.
For example, if we have these two zone files:
; authoritative for example.com, defines A record for foo.www.example.com
$ORIGIN example.com.
@ 300 IN SOA @ @ 1 300 300 300 300
foo.www.example.com. 300 IN A 127.0.0.1
and
; authoritative for www.example.com, defines no other records
$ORIGIN www.example.com.
@ 300 IN SOA @ @ 1 300 300 300 300
Then a query for foo.www.example.com
will be routed to the authoritative zone
for www.example.com
and return no result, even though there is a result in the
zone for example.com
.
Authoritative zones override non-authoritative zones
Perhaps a non-obvious consequence of more specific zones overriding less specific ones is that authoritative zones override non-authoritative ones. This is because non-authoritative zones are merged into the root zone, which is the least specific zones.
For example, if we have this zone file:
; authoritative for example.com
$ORIGIN example.com.
@ 300 IN SOA @ @ 1 300 300 300 300
And then either a non-authoritative zone file or a hosts file overriding a domain in that zone:
; a different zone file, with no SOA record
www.example.com. 300 IN A 127.0.0.1
or
# a hosts file
127.0.0.1 www.example.com
Then the override for www.example.com
is ignored, and it will in fact resolve
to nothing, since there is no record for www.example.com
in its authoritative
zone.
The same authoritative zone can be defined across multiple files
If two or more zone files define the same authoritative zone, they are merged, with records from the second file overriding records from the first file where there is a clash.
Both zone files need to specify the SOA
record.
For example, if we have these two zone files:
; authoritative for example.com
$ORIGIN example.com.
@ 300 IN SOA @ @ 1 300 300 300 300
www 300 IN A 127.0.0.1
blog 300 IN CNAME www
and
; authoritative for example.com
$ORIGIN example.com.
@ 300 IN SOA @ @ 2 300 300 300 300
@ 300 IN MX mail
www 300 IN A 127.0.0.2
mail 300 IN A 127.0.0.3
Then resolved -z file1 -z file2
would use the zone:
; authoritative for example.com
$ORIGIN example.com.
@ 300 IN SOA @ @ 2 300 300 300 300
@ 300 IN MX mail
www 300 IN A 127.0.0.1
www 300 IN A 127.0.0.2
blog 300 IN CNAME www
mail 300 IN A 127.0.0.3
This is potentially confusing if misused, but allows adding records to the standard zones without editing those files.
Standard zones
resolved
comes with the "root hints" file, from IANA, and authoritative
zones for the RFC 6761: Special-Use Domain Names. These zone files are
stored in the config/zones
directory.
Root hints
This is a non-authoritative zone file (a "hints" file) giving the NS
records
for .
(the root domain) and the A
and AAAA
records for those nameservers.
This file (or an equivalent if you want to use an alternative DNS root) is
required when operating resolved
as a recursive resolver.
RFC 6761 Private address reverse-mapping domains
- 10.in-addr.arpa.zone
- 16.172.in-addr.arpa.zone
- 168.192.in-addr.arpa.zone
- 17.172.in-addr.arpa.zone
- 18.172.in-addr.arpa.zone
- 19.172.in-addr.arpa.zone
- 20.172.in-addr.arpa.zone
- 21.172.in-addr.arpa.zone
- 22.172.in-addr.arpa.zone
- 23.172.in-addr.arpa.zone
- 24.172.in-addr.arpa.zone
- 25.172.in-addr.arpa.zone
- 26.172.in-addr.arpa.zone
- 27.172.in-addr.arpa.zone
- 28.172.in-addr.arpa.zone
- 29.172.in-addr.arpa.zone
- 30.172.in-addr.arpa.zone
- 31.172.in-addr.arpa.zone
These are authoritative zone files, defining no records by default, for implementing reverse lookups in the private address ranges.
For example, if the name of the host 10.0.0.1
is example.lan
, to support
reverse lookups you should add the following record to 10.in-addr.arpa.zone
:
1.0.0 IN PTR example.lan.
RFC 6761 "invalid." domain
This is an authoritative zone file defining no records, so that all queries for domains in this zone fail.
No records should be added to it.
RFC 6761 "localhost." domain
This is an authoritative zone file defining A
and AAAA
records for
localhost.
and *.localhost.
such that all queries respond positively with
the loopback address.
No other records should be added to it.
RFC 6761 "test." domain
This is an authoritative zone file, defining no records by default. You may add any records you like to this zone.
Guides
This section provides how-to guides to do what you want with resolved
:
-
Use a DNS blocklist. Using and creating your own blocklists, to do things like block advertising domains or prevent certain websites from being available.
-
Override a DNS record. A more powerful version of DNS blocklisting, allowing you to totally change how existing domains behave.
-
Set up LAN DNS. So you can provide DNS blocklisting, overrides, and local name resolution to all machines on your network.
Use a DNS blocklist
This is a special case of overriding DNS records.
A DNS blocklist is a hosts file mapping domains you want to block to
0.0.0.0
(to block it over IPv4), ::0
(to block it over IPv6), or both.
For example, a hosts file containing the following two lines would block
example.com
over both IPv4 and IPv6:
0.0.0.0 example.com
::0 example.com
For a much larger example, see Steven Black's hosts file which begins with:
#=====================================
# Title: Hosts contributed by Steven Black
# http://stevenblack.com
0.0.0.0 ck.getcookiestxt.com
0.0.0.0 eu1.clevertap-prod.com
0.0.0.0 wizhumpgyros.com
0.0.0.0 coccyxwickimp.com
0.0.0.0 webmail-who-int.000webhostapp.com
And then has many more entries.
If you have a DNS blocklist in some other format (for example, just a list of domains to block) you'll need to convert it into a hosts file (or a zone file) first.
Once you have your hosts file, configure resolved
to use it with the -a
or
-A
options:
resolved -a /path/to/directory/file
resolved -A /path/to/directory
I recommend the -A
form, as you can then add or remove hosts files to the
directory and send SIGUSR1
to resolved
to reload the hosts files without
restarting the process.
Override a DNS record
If resolved
can resolve a record locally it won't query an upstream
nameserver, even if it's not authoritative for that domain.
This means you can use hosts and zone files to override any DNS records you want.
Override an A or AAAA record
A hosts file allows you to override the A
or AAAA
records for a
domain.
For example, a hosts file containing the following two lines would make
example.com
resolve to your local machine, over both IPv4 and IPv6:
127.0.0.1 example.com
::1 example.com
Once you have your hosts file, configure resolved
to use it with the -a
or
-A
options:
resolved -a /path/to/directory/file
resolved -A /path/to/directory
I recommend the -A
form, as you can then add or remove hosts files to the
directory and send SIGUSR1
to resolved
to reload the hosts files without
restarting the process.
Hosts files have some limitations: you can only override A
and AAAA
records,
and you can't define wildcard records. For those, you need to use a zone file.
Override any type of record
This is how the "root hints" file works: it overrides the NS
records for the root domain, which resolved
would otherwise have no way to figure out.
A zone file allows you to override any type of record, including wildcard records.
For example, a zone file containing the following three lines would make
*.example.com
resolve to example.com
, and example.com
to your local
machine, over both IPv4 and IPv6.
*.example.com. 300 IN CNAME example.com.
example.com. 300 IN A 127.0.0.1
example.com. 300 IN AAAA ::1
If you want to totally override all records for a domain, rather than just
some of them, make your zone file authoritative for that domain by adding a
SOA
record.
For example, example.com
has an MX record, which resolved
will fetch from
the upstream nameservers if all you do is override CNAME
, A
, and AAAA
records. You can "delete" the MX
record by making your zone authoritative for
example.com
:
example.com. 300 IN SOA example.com. example.com. 6 300 300 300 300
*.example.com. 300 IN CNAME example.com.
example.com. 300 IN A 127.0.0.1
example.com. 300 IN AAAA ::1
Once you have your zone file, configure resolved
to use it with the -z
or
-Z
options:
resolved -z /path/to/directory/file
resolved -Z /path/to/directory
I recommend the -Z
form, as you can then add or remove zone files to the
directory and send SIGUSR1
to resolved
to reload the zone files without
restarting the process.
Set up LAN DNS
If you use NixOS, you may find it useful to look at how I run resolved
on my home server: package, module, configuration.
You will need to keep your computer switched on all the time, otherwise other machines on your network will no longer be able to resolve DNS.
This guide will walk you through:
- Installing
resolved
- Configuring it with the records that you want for your LAN
- Writing a systemd unit to run it
- Getting other hosts on your network to use it
At the end, you will be able to assign names to machines on your LAN (and any other records you want), and have everything on your LAN use them.
Installing resolved
First, install rustup
through your package manager.
Build resolved
in release mode:
rustup show
cargo build --release
Copy over resolved
to /opt
:
sudo mkdir -p /opt/resolved/bin
sudo cp target/release/resolved /opt/resolved/bin
sudo cp -r config /opt/resolved
sudo mkdir /opt/resolved/config/hosts
We now have the following locations:
/opt/resolved/bin/resolved
- the DNS server executable/opt/resolved/config/hosts
- directory to store your custom hosts files (e.g. DNS blocklists)/opt/resolved/config/zones
- directory to storeresolved
's standard zone files and your custom zone files (e.g. records for LAN hosts)
As everything is under /opt/resolved
, you can just make a copy of that
directory to fully back up your configuration.
Writing your configuration
Now set up whatever configuration you want in /opt/resolved/config/
.
-
Want a DNS blocklist or to override a domain's
A
orAAAA
records? Put the hosts files in/opt/resolved/config/hosts
. -
Want to override some other DNS records? Put the zone files in
/opt/resolved/config/zones
. -
Want to set up custom records for your local machines? Decide on a zone to put them in (such as
lan.
) and put an authoritative zone file for it in/opt/resolved/config/zones
.
For example, here is the authoritative zone file I use for my LAN:
$ORIGIN lan.
@ 300 IN SOA @ @ 6 300 300 300 300
router 300 IN A 10.0.0.1
nyarlathotep 300 IN A 10.0.0.3
*.nyarlathotep 300 IN CNAME nyarlathotep
help 300 IN CNAME nyarlathotep
*.help 300 IN CNAME help
nas 300 IN CNAME nyarlathotep
bedroom.awair 300 IN A 10.0.20.187
living-room.awair 300 IN A 10.0.20.117
Let's break that down:
- My router is at
10.0.0.1
, and can be reached at the namerouter.lan
. - I have a home server called
nyarlathotep
at10.0.0.3
which is reachable by a few different hostnames:nyarlathotep.lan
,*.nyarlathotep.lan
,help.lan
,*.help.lan
, andnas.lan
. - I have a couple of air quality monitoring devices, with the names
bedroom.awair.lan
andliving-room.awair.lan
.
If you also want reverse DNS to work (going from an IP address to a hostname),
then add PTR
records to the appropriate standard zone file.
For example, for my LAN I modify the 10.in-addr.arpa.zone
file:
$ORIGIN 10.in-addr.arpa.
@ IN SOA . . 3 3600 3600 3600 3600
1.0.0 IN PTR router.lan.
3.0.0 IN PTR nyarlathotep.lan.
187.20.0 IN PTR bedroom.awair.lan.
117.20.0 IN PTR living-room.awair.lan.
Running it with systemd
Here's a simplified version of the systemd unit file that I use:
[Unit]
After=network-online.target
Description=barrucadu/resolved nameserver
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
DynamicUser=true
Environment="RUST_LOG=dns_resolver=info,resolved=info"
Environment="RUST_LOG_FORMAT=json,no-time"
ExecReload=/bin/kill -USR1 $MAINPID
ExecStart=/opt/resolved/bin/resolved --cache-size 1000000 -A /opt/resolved/config/hosts -Z /opt/resolved/config/zones
Restart=on-failure
[Install]
WantedBy=multi-user.target
This will run resolved
under a non-root user, as a recursive resolver with a
maximum cache size of 1 million records (though in practice it'll be much
smaller than that, as records will expire - my cache hovers at around 4000
records).
See /opt/resolved/bin/resolved --help
for other options you can set.
Save this to /etc/systemd/system/resolved.service
and then enable it:
sudo systemctl enable --now resolved.service
Here are some helpful commands:
sudo systemctl stop resolved.service
- stop the serversudo systemctl reload resolved.service
- reload the hosts and zonesjournalctl -fu resolved.service
- follow the logs
Configuring other machines on your LAN
Open your router's control panel and:
- Give the machine which will be running
resolved
a static IP address - Change the DNS server assigned by DHCP to that IP address
The other machines on your LAN will gradually switch over to resolving DNS via
resolved
on your server. You can force this by reconnecting a machine to the
network.
Now you're running DNS for your LAN!