Build log · MikroTik RB5009 · DoH + stable RDNSS

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 in the RB5009 CGNAT series. 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

PlaceholderMeaning
<ULA_PREFIX>Your RFC 4193 ULA /48, e.g. fd7a:1b2c:3d4e. Generate one randomly; do not reuse the example.
bridgeThe interface your LAN clients are on (defconf bridge here).
192.168.88.0/24The 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# Apply this AFTER NTP has set the clock — `/tool/fetch` validates TLS 2# against the CA bundle below, which fails on a fresh defconf box with a 3# bad RTC. `/system/ntp/client set enabled=yes servers=time.cloudflare.com` 4# is enough; verify with `/system/clock/print` before continuing. 5/tool/fetch url=https://curl.se/ca/cacert.pem dst-path=cacert.pem 6/certificate/import file-name=cacert.pem passphrase="" 7 8/ip/dns set allow-remote-requests=yes max-concurrent-queries=200 \ 9 use-doh-server=https://cloudflare-dns.com/dns-query verify-doh-cert=yes 10 11/ip/dns/static 12# A-only on purpose — see rationale below. 13add address=104.16.248.249 name=cloudflare-dns.com comment="DoH bootstrap" 14add address=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 add interface=bridge address=<ULA_PREFIX>:1::1/64 \ 2 advertise=yes comment="LAN ULA" 3 4/ip/dns/static 5# Reachable as the FQDN `router.lan` from any client whose resolver is the 6# router (the default once §5/§6 are applied). No search-domain magic; type 7# the dot-lan suffix. 8add address=<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.

RA RDNSS — advertise self

bash

1/ipv6/nd add interface=bridge advertise-dns=self \ 2 managed-address-configuration=no other-configuration=no

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 present 3/log/print where message~"doh" # DoH server in use 4 5# 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 ULA 8nslookup router.lan # resolves to the ULA 9 10# 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.

References

Next in the series

Standards

Share

Comments

Comments are powered by GitHub Discussions and require a free GitHub account to post.