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