Authoritative dnsmasq in a MikroTik container

Georg Lukas
Sun, 26 Jul 2026 19:24:04 +0200
dn42, mikrotik, net

This post is about setting up and configuring a dnsmasq container on a MikroTik RouterOS device to be used as an authoritative DNS server and a forwarding cache at the same time.

Introduction

Recently I joined DN42, a world-wide overlay network created for experimenting with IP routing and for connecting communities, hack-spaces, and networking nerds who are not allowed 😜 to break the real Internet.

I wanted to play on hard mode, so I configured the DN42 tunnels in a VRF on my home lab switch, a MikroTik RB4011. And of course I wanted to provide authoritative DNS for my .dn42 zone from the same switch.

MikroTik routers come with a built-in DNS service, but it can't act as an authoritative server, and there is a shared DNS cache for all clients, which would leak my home lab to DN42.

I already run prosody and iperf on my switch, so the "obvious" "solution" was to add another container.

MikroTik Containers

MikroTik routers with ARM and ARM64 CPUs support Docker containers. The manufacturer strongly recommends using external USB / SD card storage to not wear out internal flash. Ignoring that recommendation might cause the flash in your router to fail and turn the router into an expensive paperweight.

The RB4011 doesn't have USB or SD ports, and adding NVMe looks complicated. It is possible to use the Storage package to mount remote disks, but I hate circular dependencies between my devices. Then again, people reports millions of flash writes, so I'm going to YOLO my containers on the internal 512MB flash, using /docker for the images and volumes.

Nevertheless, if you look for a MikroTik router to run containers, maybe pick one with ARM64 and a physical storage port.

Enabling containers requires a walk to the switch to ensure physical presence, and will increase your attack surface. Then we can configure it to fetch from Docker Hub, which gives us access to some 117k ARM images:

/container config
set registry-url=https://registry-1.docker.io tmpdir=/docker

Installing a dnsmasq container

I've settled with the dockurr/dnsmasq image based on Alpine which needs around 12MB of flash space. It accepts DNS1 to DNS4 from the environment and allows to bind-mount /etc/dnsmasq.d for additional config files:

/container envs
add key=DNS1 list=ENV_DNSMASQ value=fd42:d42:d42:54::1
add key=DNS2 list=ENV_DNSMASQ value=fd42:d42:d42:53::1
/container mounts
add dst=/etc/dnsmasq.d list=MOUNT_DNSMASQ_DNSMASQD src=/docker/dnsmasq-dnsmasq.d
/container
add remote-image=dockurr/dnsmasq name=dn42-dnsmasq \
    envlists=ENV_DNSMASQ mountlists=MOUNT_DNSMASQ_DNSMASQD \
    root-dir=/docker/images/dnsmasq interface=veth5 start-on-boot=yes

The two upstream servers are taken from the DN42 anycast DNS.

The veth5 interface is a virtual interface bridged into my DN42 LAN on a dedicated dn42 VRF. I've added two addresses to veth5, ::2 for the authoritative DNS, and ::53 for the forwarder. Initially, I only had one address, but dnsmasq refuses to operate a DNS forwarder on the same IP/interface as an authoritative server.

/inteface bridge
add comment="dn42 bridge with containers" name=dn42-bridge
/ipv6 address
add address=fd16:9939:ecc0::1/64 advertise=no interface=dn42-bridge
/ip vrf
add interfaces=<snip>,dn42-bridge name=dn42
/inteface veth
add address=fd16:9939:ecc0::2/64,fd16:9939:ecc0::53/64 comment="DN42 dnsmasq" \
    gateway6=fd16:9939:ecc0::1 name=veth5
/interface bridge port
add bridge=dn42-bridge interface=veth5

Note: don't add the veth to your VRF, or you'll end up with "port is already slave"!

When you add the container, RouterOS will download and extract the image in the background. Later you can update it with /container/update - both will show the download progress in the UI:

/container update dn42-dnsmasq
/container print
*snip*
#   NAME          ROOT-DIR                INTERFACE  CONTAINER-SIZE  TAG
;;; downloading layer 0/6 0%/7.7M
3 E dn42-dnsmasq  /docker/images/dnsmasq  veth5      7.7MiB          registry-1.docker.io/dockurr/dnsmasq:latest
*snip*

Dual-use dnsmasq

dnsmasq strictly separates authoritative DNS (being the main DNS server for a specific domain) and forwarding DNS (providing DNS responses for whatever a client is asking for). Normally, when you run dnsmasq on a router, you can configure it to be authoritative on the WAN interface and forwarding on the LAN interface.

If you misconfigure it, it will return Host not found: 5(REFUSED) for remote domains, and/or will fail to return a SOA for your domain.

When we run it in a container, we could either add two different virtual interfaces, or we set up two different addresses for the two tasks.

Now, we can create a ge0rg.conf file either directly in the container (/container/shell dn42-dnsmasq and it helpfully comes with busybox' vi), or on our computer, and scp it to /docker/dnsmasq-dnsmasq.d/ge0rg.conf on the RB4011:

# Be the authoritative server on the ::2 address
auth-server=ns1.ge0rg.dn42,fd16:9939:ecc0::2

# Serve two zones: ge0rg.dn42 and reverse lookups for the ULA IPv6 range
auth-zone=ge0rg.dn42,fd16:9939:ecc0::/48

# Define the Start of Authority resource record parameters:
# <serial>,<email>,<refresh>,<retry>,<expire>,<min_ttl>
auth-soa=2026072403, ge0rg.ge0rg.dn42, 1200, 120, 1209600, 86400

# Create the actual AAAA and PTR records, one line per host:
# <primary hostname>,<hostnames>,<IPv6>
host-record=gw.ge0rg.dn42,gw,fd16:9939:ecc0::1
host-record=dnsmasq.ge0rg.dn42,dnsmasq,ns1.ge0rg.dn42,fd16:9939:ecc0::2

host-record=lan.gw.ge0rg.dn42,lan.gw,fd16:9939:ecc0:42::1

host-record=wg.gw.ge0rg.dn42,wg.gw,fd16:9939:ecc0:7390::1

You need to restart dnsmasq after each change to a config file (it will only reload /etc/hosts on SIGHUP):

/container/restart dn42-dnsmasq

DN42 comes with a helper script to verify the correct working of your DNS zone:

$ ./validate-my-dns.py GE0RG-MNT
NOTE:  ge0rg.dn42 doesn't have any ds-rdata specified
NOTE:  0.c.c.e.9.3.9.9.6.1.d.f.ip6.arpa doesn't have any ds-rdata specified

Summary:

domain name                       | success | dnssec fail | wrong NS | wrong SOA | NXDOMAIN | REFUSED | SERVFAIL | timeout
----------------------------------|---------|-------------|----------|-----------|----------|---------| -------- | -------
 ge0rg.dn42                       |       1 |           0 |        0 |         0 |        0 |       0 |        0 |       0
 0.c.c.e.9.3.9.9.6.1.d.f.ip6.arpa |       1 |           0 |        0 |         0 |        0 |       0 |        0 |       0

The missing ds-rdata is about DNSSEC. If you want to sign your zone, use a "grown-up" DNS implementation 😉

The final result looks like this, taken from my peer:

$ mtr --report darkwing.ge0rg.dn42
Start: 2026-07-24T15:49:32+0200
HOST: peerhost                    Loss%   Snt   Last   Avg  Best  Wrst StDev
  1.|-- dn42-peerhost.mypeer.dn42  0.0%    10  124.0  82.4  31.8 132.8  36.0
  2.|-- gw.ge0rg.dn42              0.0%    10   96.6 100.8  48.8 153.6  34.6
  3.|-- darkwing.ge0rg.dn42        0.0%    10   55.3 108.7  55.3 159.5  35.8

Discuss on Mastodon