Repository navigation
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.
: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=yesrejects 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".
:local probeGood {"example.com";"cdnjs.cloudflare.com";"cdn.tailwindcss.com"}
:local stubPrefixes {"8.6.112.";"8.47.69."}
:local filtering falseThree 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.
: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.
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.
: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 outagefailureThreshold 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.
/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.
: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.
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.
Wiki for mikrotik-doh-failover 0.4.1 (2026-10-28, RouterOS 7.23.1 / 7.24.5).
The repository is the source of truth: when a page here disagrees with
README.md or
docs/, the
repository wins. Found a wrong statement? Open an issue with the command you ran
and its output — a claim without a measurement is not accepted.
mikrotik-doh-failover — wiki for version 0.4.3
- Getting started
- Configuration
- How it works — full flowchart
- Health monitor
- Verification
- Troubleshooting
- Changelog
- Upgrading from 0.1.0
- Zabbix monitoring
Package: PyPI — mikrotik-doh-failover · Repository: paulmann/mikrotik-doh-failover · README · Releases · Changelog