5.3 KiB
CoreDNS resolver
Goal
Replace the RouterOS built-in DNS forwarder with a CoreDNS container for configurability, and suppress IPv6 (AAAA) resolution by default to keep traffic on IPv4.
Background
The ISP provides no native IPv6 — only a Hurricane Electric (HE) tunnel
(2001:470:61a3::/48). HE addresses fall in ranges some sites flag as
datacenter/bot traffic, producing endless CAPTCHAs. The goal is to prefer IPv4
egress while keeping IPv6 available for our own services and any domain
explicitly trusted over IPv6.
What this is NOT (and why)
An earlier iteration used DNS64 + NAT64 (Tayga) to force traffic through IPv4. It was removed:
- Performance: Tayga is a userspace translator with no hardware offload. Every translated packet crossed RouterOS twice (v6 in, v4 out) plus a userspace hop, capping throughput at ~250 Mbps against a 1 Gbps line.
- SPOF: two containers (CoreDNS + Tayga) in the datapath of nearly all traffic on a router whose native forwarder had been rock-solid.
- Architectural inversion: NAT64 exists to let IPv6-only clients reach IPv4. We don't want IPv6 egress at all — using NAT64 to avoid IPv6 was solving the problem backwards.
Plain AAAA suppression in CoreDNS achieves the same IPv4-preferred outcome with zero datapath overhead — DNS is the only thing touched, packet forwarding stays on the RouterOS fastpath at line rate.
The full account of the NAT64/IPv6-mostly attempt and why it was abandoned is in nat64-dns64-postmortem.md.
How it works
CoreDNS runs as a single container (172.20.0.3), reachable from RouterOS DNS
which forwards client queries to it. The Corefile
has three server blocks:
lumpiasty.xyz— our own zone. Forwards normally, keeps AAAA, so internal services reachable over the HE prefix resolve to their real IPv6 addresses..(default) — forwards everything else, but atemplate IN AAAAblock returns empty NOERROR for all AAAA queries, so clients fall back to IPv4 and avoid the HE tunnel's flagged egress. A records and all other types pass through untouched.
The whitelist is implemented as a reusable (aaaa_allowed) snippet imported by
zones that should keep AAAA. To trust another domain over IPv6, add a server
block for it that imports aaaa_allowed.
Why suppression, not NXDOMAIN
The AAAA template returns NOERROR with an empty answer (NODATA), not NXDOMAIN. This is correct: the name exists, it just has no (advertised) AAAA. Clients treat it as "no IPv6 address" and use the A record. Returning NXDOMAIN would wrongly imply the name doesn't exist and break the A lookup.
Future improvement
The current global-suppress-plus-whitelist is coarse: a domain that is genuinely IPv6-only (no A record) and not whitelisted becomes unreachable. The intended end state is a plugin that suppresses AAAA only when the domain also has an A record, so IPv6-only destinations keep working without manual whitelisting. No in-tree CoreDNS plugin does this today.
Custom image
Built from source with a minimal plugin set (errors, log, health,
template, cache, forward, reload) instead of the default ~40, producing
a ~6-8 MB image. The dns64 plugin is no longer compiled in.
Source: mikrotik/coredns/. Built by Woodpecker
(.woodpecker/coredns-build.yaml) on pushes
touching mikrotik/coredns/**, pushed to gitea.lumpiasty.xyz/lumpiasty/coredns-mikrotik.
RouterOS integration
/ip/dns servers=172.20.0.3— RouterOS forwards client queries to CoreDNS- RDNSS in RA (
/ipv6/nd dns=...on vlan2/vlan5) advertises an IPv6 resolver (the router's per-VLAN address) to dual-stack clients; RouterOS DNS relays to CoreDNS - No DHCP option 108, no PREF64 — those belonged to the removed IPv6-mostly setup
Pitfalls learned (kept for reference)
These were hit during the NAT64 era and the migration; some still apply:
- RouterOS static FWD entries corrupt NXDOMAIN. A
type=FWD match-subdomain=yesentry returns NOERROR/empty instead of relaying NXDOMAIN. Combined withndots:5and kubernetes pod search domains,getaddrinfostops at the first search-suffixed NODATA candidate and never tries the absolute name — apps fail withENOTFOUNDfor valid hostnames whilenslookup(absolute query) works. Our own zone is therefore handled in the Corefile, not via a RouterOS FWD entry. RouterOS DNS does plain forwarding only (plus the Tailscalets.netFWD, which is acceptable as its subdomains genuinely don't exist publicly). advertise-dns=noon new ND entries. RouterOS creates per-interfaceipv6 ndentries withadvertise-dns=no, suppressing the RDNSS option even when a staticdns=list is set. Must be enabled explicitly.- Per-interface ND entries must be created, not modified. Only the
interface=alldefault ships out of the box;api_find_and_modifymatching a specific interface silently matches nothing. Useapi_modify.
Verification: rdisc6 (NixOS package ndisc6) dumps RA contents. The CoreDNS
log plugin output is visible via /log print on the router (container
logging=yes) and shows the rcode CoreDNS returned — comparing it to what the
client received isolates which hop corrupts a response.