Encrypted DNS with a stable resolver address on RouterOS
Cloudflare DoH upstream and a resolver address clients never have to relearn — a locally assigned ULA over RA RDNSS. No VLANs, no IPv6 uplink. The DNS companion to the CGNAT build log.
Overview
This is a small, self-contained companion to the
CGNAT build log. It does one thing: make a
RouterOS v7 box resolve upstream over encrypted DNS while handing
clients a resolver address that never changes.
It deliberately depends on almost nothing. There is no VLAN segmentation
here, no WireGuard, and — the part worth stating plainly — no IPv6 uplink
required. The stable resolver address is a Unique Local Address
(RFC 4193): it is generated by you, on the LAN, and exists whether or not
the ISP delegates a single bit of IPv6. The encrypted upstream is DoH over
port 443, which leaves the house on whatever default route exists —
plain IPv4 is fine. The whole thing works on a flat, single-subnet,
IPv4-only-internet LAN; it simply also survives the prefix churn you get once
real IPv6 shows up.
Every numbered section is paste-ready against a defconf RouterOS v7 box.
The italic notes are the rationale — the trade-off being made and why.
1. What you need first
A RouterOS v7 box with a working WAN and a LAN interface clients sit on.
That is the whole list. Concretely it does not require:
VLAN segmentation. One flat LAN bridge is fine. If you do have VLANs,
the same three steps apply per RA interface.
An IPv6 uplink. The ULA is internal and needs no delegation; DoH
egresses over IPv4. A house with zero IPv6 internet still gets encrypted
upstream resolution and a stable resolver address.
WireGuard, a VPS, or the rest of the CGNAT build. Those recover global
IPv6; none of them are involved in resolving names or in advertising a
local resolver.
The snippets below assume the defconf bridge on 192.168.88.0/24. Rename
the interface and subnet to match your box; nothing else changes.
2. Conventions and placeholders
Placeholder
Meaning
<ULA_PREFIX>
Your RFC 4193 ULA /48, e.g. fd7a:1b2c:3d4e. Generate one randomly; do not reuse the example.
bridge
The interface your LAN clients are on (defconf bridge here).
192.168.88.0/24
The LAN's IPv4 subnet (defconf here).
A ULA is fd00::/8 plus 40 random bits. Pick the 40 bits randomly once
and keep them — the whole value of a ULA is that it is stable and unique to
your network. fd7a:1b2c:3d4e::/48 is an illustrative value, not one to copy.
3. Encrypted upstream — DoH with bootstrap pins
The router becomes the LAN resolver and forwards every query upstream over
Cloudflare DoH. The static records pin cloudflare-dns.com so the very first
query has somewhere to go before DoH itself can resolve anything.
DoH, not DoT or plain 53: DNS-over-HTTPS is indistinguishable from any other
port-443 HTTPS flow, so no carrier NAT44 or captive middlebox can single it
out and it traverses residential CGNAT with no special handling — DoT (853)
and plain 53 are both trivially blockable and observable. Endpoints stay
simple: they keep a local resolver and never speak DoH themselves; all the
encryption is one hop upstream, on the router.
DoH resolver + bootstrap pins
bash
1/tool/fetch url=https://curl.se/ca/cacert.pem dst-path=cacert.pem
2/certificate/import file-name=cacert.pem passphrase=""34/ip/dns set allow-remote-requests=yes max-concurrent-queries=200\5 use-doh-server=https://cloudflare-dns.com/dns-query verify-doh-cert=yes
67/ip/dns/static
8# A-only on purpose — see rationale below.9addaddress=104.16.248.249 name=cloudflare-dns.com comment="DoH bootstrap"10addaddress=104.16.249.249 name=cloudflare-dns.com comment="DoH bootstrap"
The DoH bootstrap is A-only on purpose. IPv4 is up the moment the WAN is up;
an IPv6 path, if it exists at all, comes up later. Pinning AAAA records here
would push the first DoH query onto a half-warm — or entirely absent — IPv6
path while the IPv4 route is direct to a nearby Cloudflare PoP. Once DoH is
up, regular client queries still resolve and use AAAA records normally.
4. A ULA on the LAN — the stable resolver address
Add a ULA /64 to the LAN interface and publish the router itself as a name on
it. This address is reachable on-link with no IPv6 internet whatsoever — it is
a locally assigned RFC 4193 prefix, not anything the ISP hands you.
LAN ULA + router.lan record
bash
1/ipv6/address addinterface=bridge address=<ULA_PREFIX>:1::1/64 \2advertise=yes comment="LAN ULA"34/ip/dns/static
5# Reachable as the FQDN `router.lan` from any client whose resolver is the6# router (the default once §5/§6 are applied). No search-domain magic; type7# the dot-lan suffix.8addaddress=<ULA_PREFIX>:1::1 name=router.lan type=AAAA comment="LAN ULA"
The resolver identity is on the ULA, so SLAAC prefix churn — or never having
a global prefix at all — never changes the DNS server the OS has memorized.
A GUA would track the ISP delegation and move out from under every client
that cached it.
5. Advertise the resolver — RA RDNSS
Router Advertisements carry the resolver to clients (RFC 8106). This
needs IPv6 enabled on the LAN — it does not need IPv6 to the internet.
advertise-dns=self advertises whatever address the router currently holds
on the interface rather than a literal dns=<ULA_PREFIX>:1::1. The RDNSS can
never point at a stale or wrong address, and it survives a renumber with
nothing to keep in sync. Set it on every RA interface, not just one.
6. Stop handing out a DHCPv4 resolver
With the resolver advertised over RDNSS, the DHCPv4 server should stop
handing out a DNS server at all, so resolution is uniformly the ND RDNSS path.
dns-none on every scope
bash
1# Defconf scope; repeat for every DHCPv4 network you serve.2/ip/dhcp-server/network set[find address=192.168.88.0/24] dns-none=yes
No scope hands out a DHCPv4 resolver anymore, so DNS is uniformly the ND
RDNSS and the router resolves upstream over DoH. The trade-off: a client with
no RFC 8106 RDNSS support gets no DNS at all — acceptable when every LAN
carries a ULA and the device population supports it, but it makes IPv6 a hard
dependency for name resolution. This is the same posture the CGNAT build's
trusted LAN runs; that post's ULA-only VLAN section points back here for the
why.
7. Verification
Smoke tests
bash
1# On the router: upstream really is DoH, not plain 53.2/ip/dns/cache/print # entries present3/log/print where message~"doh"# DoH server in use45# From a client: resolver is the ULA, and it answers.6dig @<ULA_PREFIX>:1::1 cloudflare.com
7scutil --dns|grep-i'nameserver\['# macOS: expect the ULA8nslookup router.lan # resolves to the ULA910# No DHCPv4 resolver leaked.11ipconfig /all # Windows: no IPv4 DNS server
A client with the ULA as its only nameserver, resolving names while the WAN
shows IPv4-only, is the whole proof: upstream is sealed and the resolver
address is one that prefix churn can never move.