Repository navigation
Verification
Commands with the expected output, then the measured results.
/certificate print where trusted=yes
# expect: "SSL.com TLS ECC Root CA 2022", trusted=yes
/ip dns print
# expect (0.2.0 defaults): servers=1.1.1.1,1.0.0.1;
# use-doh-server=https://cloudflare-dns.com/dns-query; verify-doh-cert=yes
/ip dns static print where comment~"DoH bootstrap"
# expect: two addresses for the primary host, two for dns.google
/system clock print # expect the current date
/system ntp client print # expect status: synchronized
/ip firewall nat print where comment~"force DNS"
/ip firewall filter print where comment~"DNS"
/ip dns cache flush
:put [:resolve example.com] # 104.20.x / 172.66.x = real
# 8.6.112.0 / 8.47.69.0 = stub pair (Troubleshooting)
:put [:resolve pornhub.com] # filtering: 0.0.0.0; standard: a real IP
:local r [/tool fetch url="https://cloudflare-dns.com/dns-query?name=example.com&type=A" \
http-header-field="accept: application/dns-json" mode=https as-value output=user]
:put ($r->"status") # finished
:put ([:find ($r->"data") "\"Status\":0"]) # >= 0, no temp file
/log print where message~"DNS monitor"Reset the state globals before a test so the transition is logged again:
:global DNSMonitorState; :set DNSMonitorState ""
:global DNSFailCount; :set DNSFailCount 0
:global DNSRecoverCount; :set DNSRecoverCount 0
:global DNSFailoverAttempts; :set DNSFailoverAttempts 0
:global DNSMonitorRunning; :set DNSMonitorRunning false| # | Scenario | Setup | Expected |
|---|---|---|---|
| 1 | normal | DoH configured, real answers | state healthy, log HEALTHY: answers look sane … (once) |
| 2 | single blip | break primary, run the monitor once | state suspect, log SUSPECT: primary unreachable (1/2) — no failover (anti-flap) |
| 3 | two bad runs | run the monitor a second time | state fallback, log FALLBACK: switched to fallback DoH …, endpoint = fallback |
| 4 | stub pair | resolver returns 8.6.112.0/8.47.69.0
|
state stub, log STUB …, no failover |
| 5 | both down | primary + fallback both unreachable | state degraded, log DEGRADED: both endpoints unreachable …
|
| 6 | fallback broken CA | fallback host without a trusted CA | log DEGRADED: fallback DoH did not resolve either — rolled back
|
| 7 | recovery, first run | restore primary, run once | state recovering, log RECOVERING: primary answers again (1/2)
|
| 8 | recovery, second run | run again | state healthy, log HEALTHY: primary DoH answers again …
|
| 9 | wrong clock | set the year back, run | log CLOCK_INVALID …, nothing else |
| 10 | device-mode home | run on a home device |
log MONITOR_DISABLED_BY_DEVICE_MODE …, state manual-only
|
| 11 | CAPsMAN clients | rejoin a client | client uses the router and resolves |
| 12 | concurrency guard |
:global DNSMonitorRunning; :set DNSMonitorRunning true, run the monitor |
log DNS monitor: skipped — a previous run is still active, state unchanged |
Restore afterwards:
/ip dns set use-doh-server="https://cloudflare-dns.com/dns-query" verify-doh-cert=yes
/ip dns cache flush| Check | Observed |
|---|---|
| import |
Script file loaded and executed successfully; script + scheduler created |
| scheduler policy | policy=read,write,policy,test |
| JSON probe |
code 200, 251-byte body, "Status":0 located by :find
|
| content check |
answers look sane (N of 3) / answers are the stub pair …
|
| failover | switched to fallback, example.com -> 8.6.112.0, endpoint = fallback |
| rollback | both endpoints dead → configuration untouched |
| clock guard | wrong year → explicit message, no further action |
| Quad9 JSON |
DoH unable to decode BASE64-URL — not probeable |
| Check | Observed |
|---|---|
| CA import | 118 CAs imported; trusted=yes set |
| CA required | without it: failure: dns server failure
|
| builtin store parameter |
builtin-trust-anchors → bad parameter; the real one is builtin-trust-store
|
:resolve server= |
supported |
:resolve timeout= |
bad parameter timeout |
| DoH resolve |
:resolve example.com → 8.6.112.0 / 8.47.69.0, recorded then as "real IP" — it is the stub pair |
Six devices, RouterOS 7.23.1 / 7.24.5:
| Device | device-mode | Result |
|---|---|---|
| office gateway (Chateau PRO ax) | advanced | monitor installed, one stub warning, VPN untouched |
| AP (cAP ax) | advanced | installed, one stub warning |
| test box (hAP ax²) | advanced | installed, failover and rollback exercised |
| branch A | advanced | installed, tunnel to the gateway 3 ms |
| branch B | home |
not installed: fetch and scheduler are blocked by device-mode |
| branch C | advanced | installed |
Per-model device-mode is recorded in the compatibility table of README.md —
that table is the reference, and this one only records what the field run met.
All monitored devices reported the stub pair for the canaries at the time of installation — the reason this release exists.
| Check | How | Expected |
|---|---|---|
| payload check | probe URL whose body contains a stub address |
substituted, not ok
|
STUB_CLEARED |
stub prefixes forced to match a live answer | flush, re-check, state healthy
|
STUB |
stub prefixes forced, answers unchanged after flush |
STUB: logged with per-name detail |
probeMode "resolve" |
own endpoint as primary, probeUrlA empty |
healthy, no switching, honest answers |
| no side effects | three runs with applyChanges=false
|
/ip dns unchanged |
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