Running djbdns in a podman container

Posted on Sat 08 January 2022 in misc

I have been using djbdns for several years now and I still love it. Tiny, secure, maybe not as beginner friendly but I don't mind that. I previously used an RPM file that I first build on Enterprise Linux 6 which cleanly build on EL7 as well, but EL8 is another issue. Now the end of life for EL7 is still two years out when writing this blog, I was still interested in a more long term solution. Enter containers. As a user of EL for servers, both professionally and privately, I switched to podman i.o. Docker, just because the daemonless nature of podman yield a more secure system and it's even possible to run the containers rootless, but in this case that comes at a price.

Rootfull vs rootless containers

Wherever I use containers I try to use them rootless, but in certain scenarios it is an issue with running a DNS. Most importantly, running a djbdns container rootlessly means that it is not possible to distinguish the source of DNS queries. That means that you cannot use a split horizon or limit AXFR access to specific IP addresses. The logging will also show a source query address in the network namespace of the container (usually 10.0.2.0/24). If neither are required, running a container with tinydns and axfrdns rootlessly is a good idea.

Another issue with a rootless container is the fact that dnscache cannot forward queries to another rootless container with tinydns on the same host since they both listen on port 53 and there is no way to distinguish between the networks.

If tinydns and axfrdns are run in a rootless container, both TCP and UDP port 53 must be published on some high host port. Via a NAT firewall rule a query to port 53 of the host can be redirected to the published port.

When using a rootfull container, the container can be defined with its own IP address within a podman network and in this case port 53 does not need to be published on a host port, port 53 on the container IP is reachable and a DNAT firewall rule can be used to send the DNS query to the container. If you use a second rootfull container with dnscache the DNS queries that should be handled by tinydns or axfrdns can be forwarded to the IP addres of the tinydns container.

Creating the container.

If I run a container, I want it small. I started with the Dockerfile that was published by Yasuo Ohgaki to install djbdns, ucspi-tcp and daemontools on Alpine Linux. For this, gcc etc had to be installed to compile the code, but I want it two changes on that: first I wanted to apply some patches and second, I wanted only the compiled result, not the source nor gcc, perl and the like. To achieve that I modified the Dockerfile to a multi stage version where first a container is used to compile the data and then a second container is created where the compiled result is copied to.

The containerfile downloads the software and some patches from the original websites, so to verify that the correct files are actually downloaded I check the SHA256 checksums of the download.

For daemontools 0.76 I applied a single patch to replace "extern int errno;" by "#include <errno.h>". This is needed to be able to compile the software with gcc (this goes for ucspi-tcp and djbdns as well). After compiling and installing, the various binaries stay in the source tree and there were symlinks from the "/command" directory to the compiled binaries in the source tree and there were symlinks from /usr/local/bin to the symlinks in the /command directory. I deleted the symlinks from /usr/local/bin and did a "cp -L" from /command directory to /usr/local/bin.

On ucspi-tcp 0.88 I applied "Fefe's" diff20 patch to enable IPv6 support on tcpserver and tcpclient.

On djbdns 1.05 I applied quite a few more patches. First, I applied Fefe's (test28) patch to enable IPv6 for tinydns. Second, I installed Peter Conrad's DNSSEC patch for tinydns (version 1.8) which requires Fefe's patch. I have yet to enable DNSSEC. A third patch that I applied is Guilherme Balena Versiani's NAPTR extension to Michael Handler's SRV patch which allows easy creation of both resource records. Since Guilherme published a patched version of djbdns-1.05, I re-calculated the patch and applied it to my patched source. This yielded a few rejected hunks that were easily fixable. I also added a second parameter to his "rr_finish()" statements because lacking those generated compile errors. The final patch against my Fefe + DNSSEC version is a collection of various patches that I collected. I applied them all manually to what I had until then and used git diff to generate a big patch with all of them. The patches are named in the top of this collective patch file, but for most of them I can no longer find the original online.

The modified version of Guilherme's patch and the patch collective, along with two scripts and the containerfile are available via this Gitlab snippet.

After compiling all these I copied the content of /usr/local/bin to the destination image. I added two scripts to this directory: a script to start the required djbdns daemon (startdjb.sh) and a second one to build the djbdns databases from within the container (makeall.sh).

Recreating the image yourself is trivial. Clone the snippet repository and build the image from within the cloned directory like so:

git clone https://gitlab.com/snippets/2232485.git
cd 2232485

and then either

docker build -f Containerfile -t yourtagname .

or

buildah bud -f Containerfile -t yourtagname

This should yield an 8MB container. If you want to try a ready-made image, a saved image is available via this link. The image can be recreated via podman load < tarfile or docker load < tarfile.

Using the containers

The containers expose TCP and UDP ports 53 and /usr/local/etc should be mapped to a directory on the host. If you use podman on a system with SELinux enabled (I hope you do), make sure the directory that is mapped to the container's /usr/local/etc has a writable content type (I use container_file_t).

Via the DJBMODE environment variable, you can define which djbdns daemon should run in the container. If no value is provided, the value tinydns is assumed which starts both tinydns and axfrdns. For the dnscache, walldns or rbldns daemons, you should use the values dnscache, walldns or rbldns respectively.

To run the container rootlessly, use a command line like:

podman create --rm --name djbdns -p 5553:53/tcp -p 5553:53/udp \
    -v /var/podman:/usr/local/etc -e DJBMODE=tinydns alpine-djbdns

On the first run, the /var/podman will be populated with a tinydns and axfrdns tree. Make sure that the administrator redirects TCP and UDP queries to the port(s) as specified.

If you run a rootfull container, you can assign an IP address to the container. By default, podman installs a network named "podman" with an IP network of 10.88.0.0/16. To find out, you can use

podman network ls

and

podman network inspect networkname

to see which IP space is available. To create an own network, you use something like

podman network create --subnet 172.25.1.0/24 --gateway 172.25.1.1 djbnet

Creating the container with an IP address goes something like this:

podman create --rm --ip 172.25.1.10 --network djbnet --name djbdns \
    -v /var/podman:/usr/local/etc -e DJBMODE=tinydns alpine-djbdns

If you also need dnscache you can create the container similarly but with a different IP address in the same network. The exposed ports do not need to be published on a host port, port 53 is available on the container's IP address. To reach the container, the routing tables need to know how to reach the container, or the host running the container needs DNAT rules when the server is queried on the DNS port (don't forget to set the DJBMODE variable to dnscache for that to work).

It is advisable to generate systemd files with

podman generate systemd --files --new --name containername

With this systemd can be used to start, stop and monitor the containers.

With rootfull containers, it is possible to create forward files for dnscache to be handled by tinydns or axfrdns. If, e.g., the tinydns container listens on IP address 172.25.1.10 and serves DNS records for example.com, you can create the file *volumemount*/dnscache/root/servers/example.com with 172.25.1.10 as content.

After first running the tinydns container, the tinydns and axfrdns databases are still missing. They can be created externally and copied to *volumemount*/tinydns/root/data.cdb and *volumemount*/axfrdns/tcp.dns respectively, or the source files data and tcp can be edited after which you run "makeall.sh" in the running container with

podman exec -it containername /usr/local/bin/makeall.sh

References