Skip to content

Configuration

Mikhail Deynekin edited this page Oct 28, 2026 · 4 revisions

Configuration

All tunables live in the CONFIG block of each script (routeros/config.example.rsc is the copy-paste reference). RouterOS scripts cannot source a shared file, so each script keeps its own block.

DoH endpoints

:local dohUrlA   "https://cloudflare-dns.com/dns-query"   # primary
:local probeUrlA "https://cloudflare-dns.com/dns-query?name=example.com&type=A"
:local dohUrlB   "https://dns.google/dns-query"           # fallback
:local probeUrlB "https://dns.google/resolve?name=example.com&type=A"
  • The probe URL is not always the same as the DoH URL: Cloudflare and Google both speak the DNS-JSON API, Quad9 does not (DoH unable to decode BASE64-URL).
  • The fallback needs its own root CA in the trust store. Without it verify-doh-cert=yes rejects the endpoint, the monitor sees no resolution after switching, and rolls back — with a log line telling you to check the CA.
  • Only one endpoint is active at a time; RouterOS has no native "backup DoH".

The content check

:local probeGood    {"example.com";"cdnjs.cloudflare.com";"cdn.tailwindcss.com"}
:local stubPrefixes {"8.6.112.";"8.47.69."}
:local filtering    false

Three canaries, deliberately different: a plain site, a CDN, and a JS CDN. A single name cannot distinguish "one domain broke" from "the resolver is off". The stub prefixes are the pair Cloudflare hands out in some networks — see Troubleshooting.

filtering is a marker, not a switch. It is declared in the CONFIG block of 05-dns-health-monitor.rsc and is not read anywhere in the script body (measured 2026-10-26: one occurrence in the file, the declaration). It records the intent "this endpoint filters" for a human reader; the log line and the stub-pair detection are the same either way, because the pair was observed from filtering and standard endpoints. If you change the endpoint, the behaviour follows the URL, the servers list and the CA — not this flag.

The failover chain (0.4.0)

:local dohUrlC      ""       # optional tertiary DoH endpoint; empty disables it
:local probeUrlC    ""       # must speak DNS-JSON (Cloudflare/Google, not Quad9)
:local plainFallback false   # last resort plain DNS (off by default)
:local plainServers  "1.1.1.1,1.0.0.1,8.8.8.8,8.8.4.4"

The chain is primary → secondary → tertiary → plain. plainFallback is off by default; enabling it is a deliberate privacy downgrade and the switch is logged as PRIVACY DEGRADED. The probe itself uses /tool fetch ... as-value output=user, so it writes no temporary files to /file.

Bootstrap records

The monitor probes endpoints by hostname, so the hostnames must resolve even when the resolver is dead:

/ip dns static add name=cloudflare-dns.com address=1.1.1.1 ttl=1d comment="DoH bootstrap"
/ip dns static add name=cloudflare-dns.com address=1.0.0.1 ttl=1d comment="DoH bootstrap"
/ip dns static add name=dns.google address=8.8.8.8 ttl=1d comment="DoH bootstrap (fallback)"
/ip dns static add name=dns.google address=8.8.4.4 ttl=1d comment="DoH bootstrap (fallback)"

Four records — two per host, primary and fallback: 01-configure-doh.rsc pins both hosts, because the monitor probes both. 8.8.4.4 is the second Google address and is easy to leave out when copying by hand; the earlier version of this page did exactly that.

If you change endpoints, change these too — otherwise the probe fails, the monitor reports "both endpoints unreachable" and (correctly) refuses to switch.

Monitor tuning

:local monitorInterval   "60s"   # scheduler interval; 45-60s office, 3m home
:local failureThreshold  2       # consecutive bad runs before a failover (anti-flap)
:local recoveryThreshold 2       # consecutive good runs before coming home (anti-flap)
:local applyChanges true         # false = report only, never touch /ip dns
:local maxAttempts  3            # failover attempts per outage

failureThreshold and recoveryThreshold are the anti-flap hysteresis: with a 60 s interval and a threshold of 2, a switch needs ~2 minutes in either direction. applyChanges=false is the way to run the monitor on a device where you only want telemetry.

Scheduler

/system scheduler add name="Scheduler_DNS_Health_Check" interval=$monitorInterval ...
    policy=read,write,policy,test ...

The script itself runs with read,write,policy,test; the scheduler is created with the same narrow set (v1 left it at the default full set, including ftp,reboot,password,sniff,sensitive,romon). monitorInterval defaults to 60s for an office; set 3m on a home device.

Security policies

:local bridge  "bridge"
:local lanList "LAN"
:local wanList "WAN"
:local enforcement "standard"    # "standard" | "strict"

Two NAT rules (action=redirect, UDP and TCP 53) pull classic DNS to the router; four filter rules accept DNS from the LAN and drop it from the WAN so the router is not an open resolver.

enforcement=strict adds: block DoT (TCP 853) and the well-known public DoH endpoints (address-list public-doh). Even strict does not close arbitrary client DoH over HTTPS/443 — that needs a proxy/DPI; the residual risk is documented, not hidden.

Known limit: redirect does not touch a client that already uses the router as its resolver — that query is answered locally and never enters dstnat. So a per-client resolver built from dst-nat rules silently does nothing for such clients. Hand the resolver out over DHCP, or run a separate resolver.

CAPsMAN

Targets one configuration by name (not [find], which would change every configuration) and sets the DHCP dns-server by exact CIDR. Only for the legacy /caps-man stack; wifi-qcom devices configure this under /interface wifi.

Clone this wiki locally