Table of Contents

Synopsis

Here we are going to discuss how you can setup a local DNS12 server using docker34 and CoreDNS.56

Introduction

If you are not already familiar with how DNS12 works, then basically it is an address book. In this address book are stored the IP addresses7 and their corresponding Domain Names (i.e. google.com, youtube.com, amazon.com, etc…). When you type google.com in your browser, your browser will first attempt to resolve this Domain Name by querying whatever Name Servers your computer has set in its local configuration (Note: this is most often set automatically by the router on the wired/wireless network when you first join).

The result of this query is the resolution of the Domain Name to its corresponding address (e.g. while google.com has multiple IP addresses, one of the returned addresses is 108.177.122.113). This can be seen below in the following example using the well known tool dig:89

$ dig google.com

; <<>> DiG 9.10.6 <<>> google.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10808
;; flags: qr rd ra; QUERY: 1, ANSWER: 6, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;google.com.                    IN      A

;; ANSWER SECTION:
google.com.             76      IN      A       108.177.122.113
google.com.             76      IN      A       108.177.122.139
google.com.             76      IN      A       108.177.122.138
google.com.             76      IN      A       108.177.122.102
google.com.             76      IN      A       108.177.122.101
google.com.             76      IN      A       108.177.122.100

;; Query time: 7 msec
;; SERVER: 192.168.2.1#53(192.168.2.1)
;; WHEN: Fri Jan 03 10:09:45 EST 2020
;; MSG SIZE  rcvd: 135

We can see from the above example that there are several IP addresses associated for google.com (including the 108.177.122.113 address we noted earlier). Now your browser can actually send the desired HTTP request10 to Google’s servers at 108.177.122.113, which will in turn respond with the desired HTML/CSS/Javascript that comprise its search engine interface. All DNS really did is allow you to “lookup” the address for Google’s server. In a very simplified way, this is how DNS works.

But this example only applies to externally located hosts (e.g. the servers running websites like google.com, youtube.com, and amazon.com). What if you wanted to create Domain Names and the corresponding DNS Records for your local hosts/services running on your local network? This is where you would want to have a locally running DNS server, with entries composed of Domain Names pointing to local IP addresseson your local network.

Getting Started

CoreDNS Config

To start with we need to create the config file for CoreDNS. This file is called a Corefile and an example can be seen below

$ mkdir -p local-dns/root
$ cd local-dns
$ cat <<EOF > root/Corefile
.:53 {
    forward . 8.8.8.8 9.9.9.9
    log
    errors
}

example.com:53 {
    file /root/db.example
    log
    errors
}
EOF

First we start by using mkdir to create the directories to store our config files. Then we use the cat command and a heredoc to write to the Corefile. The first section of this Corefile (i.e. the .:53) corresponds to all DNS queries that do no match any of the other domains listed in the config file (in this instance we mean the example.com domain listed underneath). What we are saying with this first section is “any query that does not directly match any other domains listed in this Corefile, forward that query to the name servers at 8.8.8.8 and 9.9.9.9”. These are Google’s DNS servers (8.8.8.8 and 9.9.9.9 respectively). This works out well because not all of our DNS queries are going to correspond to local domains, we still need to send out queries for external domains out on then WAN11 (i.e. internet).

If you have not already guessed, the last section specifically matches any DNS queries for the domain example.com. Here you are telling the CoreDNS server to specifically reference the file located at /root/db.example for every DNS query that specifies example.com. The domain name here can be whatever you want and as many as you want, but you would need a separate db.* file for each domain you are referencing in this Corefile:

foobarbaz.org:53 {
    file /root/db.foobarbaz
    log
    errors
}

helloworld.com:53 {
    file /root/db.helloworld
    log
    errors
}

In the above example we created rules to add to the Corefile for the domains foobarbaz.org and helloworld.com. Notice we have uniquely named db.* files for foobarbaz.db and helloworld.db respectively.

DNS Zone File

Back to our original example, let us now dive into the db.example12 file we referenced in the Corefile above, and look at how it can be setup to work:

$ cat <<EOF > root/db.example
$ORIGIN example.com.  ; designates the start of this zone file in the namespace
$TTL 1h               ; default expiration time of all resource records without their own TTL value
@                 IN  SOA     ns.example.com. rtiger.example.com. (
                                  2020010510     ; Serial
                                  1d             ; Refresh
                                  2h             ; Retry
                                  4w             ; Expire
                                  1h)            ; Minimum TTL
@                 IN  A       192.168.1.20       ; Local IPv4 address for example.com.
@                 IN  NS      ns.example.com.    ; Name server for example.com.
ns                IN  CNAME   @                  ; Alias for name server (points to example.com.)
webblog           IN  CNAME   @                  ; Alias for webblog.example.com
netprint          IN  CNAME   @                  ; Alias for netprint.example.com

We will leave an extensive explanation of the zone file for further reference,12 and will instead attempt a brief explanation of the above values.

As you can see at the top of the file is the $ORIGIN keyword, which merely sets the global ORIGIN domain name (i.e. in this case example.com). The $ORIGIN value can then be referenced using the @ symbol throughout the zone file. You can see the use of the @ symbol to replace the example.com domain name through various records. Again, please refer to the zone file reference12 for information on the $TTL value.

Next we see several resource records (also shortened to RR) starting directly below the $TTL value:

$ORIGIN example.com.  ; designates the start of this zone file in the namespace
$TTL 1h               ; default expiration time of all resource records without their own TTL value

; =============================== Resource Records ==============================

@                 IN  SOA     ns.example.com. rtiger.example.com. (
                                  2020010510     ; Serial
                                  1d             ; Refresh
                                  2h             ; Retry
                                  4w             ; Expire
                                  1h)            ; Minimum TTL
@                 IN  A       192.168.1.20       ; Local IPv4 address for example.com.
@                 IN  NS      ns.example.com.    ; Name server for example.com.
ns                IN  CNAME   @                  ; Alias for ns.example.com
webblog           IN  CNAME   @                  ; Alias for webblog.example.com
netprint          IN  CNAME   @                  ; Alias for netprint.example.com

More information on the resource records can be found in the references13 located in the reference section. What is important to understand here is that we have an address record with the label A that connects the domain name example.com to the IP address 192.168.1.20. This is the actual address for example.com. The other resource records for CNAME are what are known as canonical name records. Here we use some shorthand to alias the domain ns, weblog, and netprint (which correspond to ns.example.com, webblog.example.com, netprint.example.com respectively) to example.com (again we used the @ symbol here as a reference). This is simply because all of the subdomain names are all pointing to the same IP address as example.com so we can simply point them to the example.com domain. This makes management much easier, because should we need to change the IP address associated with example.com, we will not have excessive multiple entries to change.

Finally we have the NS and SOA resource records. The NS records simply stand for name server and allow us to declare a name server (in this case we use the ns.example.com domain which is actually an alias or CNAME for example.com). The SOA record is the only record specifically required by the zone file and is short for Start of Authority. This DNS record has a few different features compared to previously described A, CNAME, and NS records. It requires a name server address (in this case the ns.example.com domain name) that hosts the A record for the $ORIGIN domain (in this case example.com). In this specific example the name server is the same as the server hosting this zone file, that is why the name server is this example is merely an alias to example.com. It also requires an administrator contact (in this case the rtiger.example.com) and various time-related data (seen above with comments to the right). Please see the references on the resource records13 and zone file12 for more info.

Docker Deploy

Finally we are ready to deploy our local DNS server. We can deploy it simply as shown below:

$ docker run -d \
             --name coredns \
             -v ~/local-dns/root/:/root \
             -p 53:53/udp \
             coredns/coredns -conf /root/Corefile

To clarify, we are mounting the directories we created earlier ~/local-dns/root for CoreDNS to find the config files, so be aware that if your path is different you will need to change this value to the location of your Corefile/db.* files. Now we need to test it to make sure it is working

Using Local DNS Server

Testing

To begin using our server, we need to first test whether our DNS request will work. This will involve our good friend, once again, dig.89 Let us start by querying the server using its local IP address and the example.com domain:

$ dig @192.168.1.20 example.com

; <<>> DiG 9.10.6 <<>> @192.168.1.20 example.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50252
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;example.com.                   IN      A

;; ANSWER SECTION:
example.com.            3600    IN      A       192.168.1.20

;; AUTHORITY SECTION:
example.com.            3600    IN      NS      ns.example.com.

;; Query time: 41 msec
;; SERVER: 192.168.1.20#53(192.168.1.20)
;; WHEN: Sun Jan 05 18:52:10 EST 2020
;; MSG SIZE  rcvd: 106

We can see in the ANSWER SECTION of the dig report that the A record for example.com points to 192.168.1.20 just like we defined in the zone file. We can also see that the AUTHORITY SECTION reports an NS record with ns.example.com as the name server, just like we defined in the zone file. What about our other records for webblog.example.com and netprint.example.com:

$ dig @192.168.1.20 webblog.example.com netprint.example.com
; <<>> DiG 9.10.6 <<>> @192.168.1.20 webblog.example.com netprint.example.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 37312
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 1, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;webblog.example.com.           IN      A

;; ANSWER SECTION:
webblog.example.com.    3600    IN      CNAME   example.com.
example.com.            3600    IN      A       192.168.1.20

;; AUTHORITY SECTION:
example.com.            3600    IN      NS      ns.example.com.

;; Query time: 552 msec
;; SERVER: 192.168.1.20#53(192.168.1.20)
;; WHEN: Sun Jan 05 18:56:02 EST 2020
;; MSG SIZE  rcvd: 158

;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50787
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 1, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;netprint.example.com.          IN      A

;; ANSWER SECTION:
netprint.example.com.   3600    IN      CNAME   example.com.
example.com.            3600    IN      A       192.168.1.20

;; AUTHORITY SECTION:
example.com.            3600    IN      NS      ns.example.com.

;; Query time: 10 msec
;; SERVER: 192.168.1.20#53(192.168.1.20)
;; WHEN: Sun Jan 05 18:56:02 EST 2020
;; MSG SIZE  rcvd: 160

We see two reports generated, both containing CNAME records for webblog.example.com and netprint.example.com respectively. Our local DNS server is working!!!

macOS DNS Resolver

In this section we actually cover how to setup your macOS computer to use the local DNS server we have just successfully deployed and tested. We will be using a macOS-specific command line tool called networksetup. With this tool we will be setting the name servers used for a specific interface (in this case Wi-Fi):

$ networksetup -setdnsservers Wi-Fi 192.168.1.20

This will immediately point all your DNS traffic to the CoreDNS server we deployed on host 192.168.1.20. To reset the name servers used to the default:

$ networksetup -setdnsservers Wi-Fi empty

Now we are back the the original DNS servers being used. Confirm this with the following:

$ networksetup -getdnsservers Wi-Fi
There aren't any DNS Servers set on Wi-Fi.

The above response is the default response for macOS (i.e. the state before we changed it to use our local DNS servers). Note: These commands were performed in macOS Mojave.

References