Simple DNS for your basic needs

Published on Wednesday, 03 February, 2021

Intro

Sometimes you don't need (or want) some complex solution to simple problem like DNS. Therefore this one will be the simplified version of previous guide with bind as only element.

For this one you will again need podman. If you are (like me in this case) doing this on centOS or similar machine, getting podman is as simple as:

# dnf install podman

If you are on some other distro, it shouldn't be that complicated.

Building container image

You could probably find one on dockerhub or some other image repo, but building our own is simple and makes sure we know what we are running.

The goal of this will be simple, create a dns server that will provide a custom local domain, and forward everything else to upstream of choice.

All my guides follow same folder structure to keep all the files:

/containers
├── build
│   └── bind
└── run
    └── bind
        └── etc

Run following commands to create it:

# mkdir -p /containers/build/bind
# mkdir -p /containers/run/bind/etc

We'll create a simple Dockerfile in /containers/build/bind with following content:

FROM alpine:latest

LABEL maintainer="Marvin Sinister"

RUN addgroup -S -g 2021 bind && adduser -S -u 2001 -G bind bind; \
    apk add --no-cache ca-certificates bind-tools bind; \
    rm -rf /var/cache/apk/*; \
    mkdir /var/cache/bind;

RUN chown -R bind: /etc/bind; \
    chown -R bind: /var/cache/bind;

HEALTHCHECK --interval=5s --timeout=3s --start-period=5s CMD nslookup ns.domain.tld 127.0.0.1 || exit 1

USER bind

CMD ["/bin/sh", "-c", "/usr/sbin/named -g -4"]

There is really nothing special about it, we are using latest alpine base image, installing bind, fixing some permissions and adding healthcheck. Make sure you use your domain instead of ns.domain.tld in HEALTHCHECK.

We can build the container now:

# podman build . -t bind
STEP 1: FROM alpine:latest
STEP 2: LABEL maintainer="Marvin Sinister"
--> Using cache 573e94441dfdbd7dcfa8e232ce7baedb9860192d749efd679b2bb8ccf64a797d
--> 573e94441df
STEP 3: RUN addgroup -S -g 2021 bind && adduser -S -u 2001 -G bind bind;     apk add --no-cache ca-certificates bind-tools bind;     rm -rf /var/cache/apk/*;     mkdir /var/cache/bind;
fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/community/x86_64/APKINDEX.tar.gz
(1/53) Installing ca-certificates (20191127-r5)

...

(53/53) Installing bind (9.16.11-r0)
Executing bind-9.16.11-r0.pre-install
Executing bind-9.16.11-r0.post-install
wrote key file "/etc/bind/rndc.key"
Executing busybox-1.32.1-r2.trigger
Executing ca-certificates-20191127-r5.trigger
OK: 41 MiB in 67 packages
--> e90abd46aba
STEP 4: RUN chown -R bind: /etc/bind;     chown -R bind: /var/cache/bind;
--> c5b324d0cff
STEP 5: HEALTHCHECK --interval=5s --timeout=3s --start-period=5s CMD nslookup ns.domain.tld 127.0.0.1 || exit 1
--> 3c7e5ae2cf3
STEP 6: USER bind
--> bbb48348aa0
STEP 7: CMD ["/bin/sh", "-c", "/usr/sbin/named -g -4"]
STEP 8: COMMIT bind
--> 1bd34f66b06
1bd34f66b066f714124253631db94d50a21643bf3b8c2fbb6539862d447bee32

If you are running recent enought version of podman you might get warnings about HEALTCHECK not being supported. In that case, just add --format docker to end of build command.

You should now see the image:

# podman image ls
REPOSITORY                  TAG         IMAGE ID      CREATED        SIZE
localhost/bind              latest      1bd34f66b066  3 minutes ago  41.9 MB
docker.io/library/alpine    latest      e50c909a8df2  5 days ago     5.88 MB

Creating configurations

The main config goes into etc/named.conf:

// This is the primary configuration file for the BIND DNS server named.

// If you are just adding zones, please do that in /etc/bind/named.conf.local

include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";

Next, lets do options, etc/named.conf.options:

options {
        directory "/var/cache/bind";

        forwarders {
                1.1.1.1;
                1.0.0.1;
        };

        recursion yes;
        allow-query { lan; };

        dnssec-validation auto;

        auth-nxdomain no;    # conform to RFC1035
        listen-on { any; };
};

Here we define our forwarders, in this case cloudflares 1.1.1.1, allow the recursion and limit the queries to our local network (defined later).

Now, let's define local domains:

acl lan {
        127.0.0.1;
        };

zone "domain.tld" {
        type master;
        file "/etc/bind/db.domain.tld";
};

zone "122.168.192.in-addr.arpa" {
        type master;
        notify no;
        file "/etc/bind/db.122.168.192.in-addr.arpa";
};

In this configuration we define the acl, we are only allowing localhost because bind will run in container and all calls will be from localhost from containers point of view. Next we define our domain domain.tld and our reverse domain 122.168.192.in-addr.arpa. The reverse zone should correspond to your ip range, in this case 192.168.122.0/24.

Let's define the zones themself, first the forward zone domain.tld:

; BIND data file for domain.tld zone
;
$TTL    86400
@       IN      SOA     ns.domain.tld. root.domain.tld. (
                              5         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                          86400 )       ; Negative Cache TTL
;
@       IN      NS      ns.domain.tld.
ns      IN      A       192.168.122.254

; hosts
containers              IN      A       192.168.122.254

This is where we add our dns entries, in this case we have two, ns.domain.tld and containers.domain.tld both pointing to 192.168.122.254, the address of our podman host containers.domain.tld.

And lats, the reverse zone:

;
; BIND reverse data file for lan zone
;
$TTL    604800
@       IN      SOA     ns.domain.tld. root.domain.tld. (
                              5         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
;
@       IN      NS      ns.domain.tld.
254     IN      PTR     ns.domain.tld.

; hosts
254     IN      PTR     containers.domain.tld.

Similar to forward zone, the reverse zone defines the reverse pointers so you can find what hosts are behind some address.

Since bind uses DNSSEC to check the upstream repositories, we need to provide the upstream keys, you can download them from ICS website from bind.keys (at the time of writing this document). Download the file and save it as etc/bind.keys.

Since we specified user with id 2021 in Dockerfile we will make that user owner of those files:

# chown -R 2021 /containers/run/bind

Running the pod

While we could run this as a container, the decision was made to standardize on pods. There is no major reason to go one way or other, but standardization generally provides benefits long term. In that spirit, let's create the pod:

# podman pod create --name dns -p '192.168.122.254:53:53/udp'

And run the bind container within:

# podman run -d --name bind --pod dns -v '/containers/run/bind/etc:/etc/bind:Z' --user 2021 localhost/bind:latest

To allow external machines access to our new dns, we need to open 53/udp on firewall:

# firewall-cmd --add-service=dns --permanent
# firewall-cmd --reload

Once everything starts, we can try running some dns queries to check if everything is okay, install the bind-utils to get dig command:

# dnf install bind-utils

And then check if you can resolve some addresses:

# dig +short @192.168.122.254 google.com
172.217.16.110
# dig +short @192.168.122.254 ns.domain.tld
192.168.122.254

And that's it, now you can point your network to 192.168.122.254 to resolve you local domain.