Skip to content
Mikhail Deynekin edited this page Nov 1, 2026 · 13 revisions

mikrotik-doh-failover — wiki

One verified DoH upstream for RouterOS, a second DoH endpoint as failover, and a health check that judges answers by content — not by "did an IP come back".

Current: v0.4.3 (2026-11-02); previous tag v0.4.2. Verified on RouterOS 7.23.1 and 7.24.5 — those are the only two versions recorded in the repository's verification notes. This wiki is a second copy of README.md and docs/: when they disagree, the repository wins.

Start here

Page What it answers
How it works — full flowchart seven Mermaid diagrams: components, install order, query path, monitor algorithm, states, failover in time, diagnosis
Getting started what to upload, in which order, and what will break if you skip a step
Configuration every tunable: endpoints, probes, filtering, bootstrap, CAPsMAN
Health monitor states, failover, rollback, how to test it in five minutes
Troubleshooting the failures seen in the field, including "sites do not open but DNS is fine"
Verification commands with expected output, and the measured results
Upgrading from 0.1.0 what changed and what to re-import
Monitoring the fleet in Zabbix DoH and VPN state from every router, triggers, service tree
Changelog what changed in each release and why it mattered

Repository: https://github.com/paulmann/mikrotik-doh-failover · Release: https://github.com/paulmann/mikrotik-doh-failover/releases/tag/v0.4.3

What 0.2.0 changed, in one screen

  1. Default upstream is the standard resolver (cloudflare-dns.com, 1.1.1.1/1.0.0.1), not Family. Filtering is an explicit opt-in.
  2. Failover is DoH → DoH. Plain DNS is bootstrap only, never a working fallback — measured plain answers are substituted on the path.
  3. The check judges answers. Empty, 0.0.0.0 and the stub pair 8.6.112.0/8.47.69.0 are unusable; v1 accepted any valid IP.
  4. The check does not disturb the resolver. Both endpoints are probed with /tool fetch against their DNS-JSON API.
  5. Failover is verified and reversible — the fallback is probed before the switch, verified after it, and rolled back if it does not resolve.
  6. Clock guard — a wrong date makes verify-doh-cert impossible and is now reported as such.
  7. Narrow scheduler policy (read,write,policy,test).

What 0.3.0 changed

  1. Explicit state machine — HEALTHY → SUSPECT → FALLBACK → RECOVERING → HEALTHY, plus STUB, DEGRADED, CLOCK_INVALID and MANUAL-ONLY, logged with UPPERCASE tags for alerting.
  2. Hysteresis instead of one-shot flapping — failureThreshold (2) bad runs to leave the primary, recoveryThreshold (2) good runs to come home.
  3. Configurable interval — 60 s default (was a hardcoded 3 m).
  4. enforcement=strict — blocks DoT (853) and known public DoH endpoints; the residual risk of client DoH over 443 is documented.
  5. device-mode=home is diagnosed — MONITOR_DISABLED_BY_DEVICE_MODE + manual-only instead of "both endpoints unreachable".
  6. "Managed failover" wording — the fallback is a different operator, so answers, filtering and geo may change on a switch.

What 0.3.1 changed

  1. Endpoint-driven failover — a failover now triggers on endpoint health as well as on the content check, so a primary that dies while the cache still has answers is detected before the cache TTL expires.
  2. Concurrency guard — an overlapping scheduler run is skipped, and the flag is cleared in on-error (a crashed run cannot lock the monitor).
  3. Recovery gated on the primary — the monitor returns to the primary only when probeA == "ok", so it cannot flap A↔B.
  4. Clock range — dates outside 2025..2035 are rejected (past and future).

What 0.4.0 changed

  1. In-memory probe — the endpoint probe uses /tool fetch ... as-value output=user, so it writes no temporary files to /file and cannot leave stale ones behind. The JSON check accepts both "Status":0 and "Status": 0.
  2. Ordered chain — primary → secondary → tertiary (dohUrlC, optional) → plain DNS (plainFallback, optional, off by default, logged PRIVACY DEGRADED).
  3. New plain state — on the plain-DNS last resort; recovers back to the primary once it answers.

What 0.4.1 changed

  1. Hard timeout — every probe fetch carries duration=5s, so a hung endpoint cannot block the run past the scheduler interval.
  2. Symmetric tertiary — the tertiary switch now verifies after the switch and rolls back (like the secondary), and every switch increments DNSFailoverAttempts.
  3. Plain→DoH climb — recovery from plain now climbs back to any DoH endpoint (not only the primary) when the secondary answers.

The finding behind the release

A branch site stopped opening CDN pages. The router was healthy: DoH answered Status:0, a valid A record came back. The address was 8.6.112.0 / 8.47.69.0 — a pair Cloudflare really serves, but with no path from some providers. Where the path is missing, every name that resolves to the pair stops working, whichever Cloudflare endpoint you choose.

Details, measurements and the three fixes that actually work: Troubleshooting → "Sites do not open while the resolver looks healthy".

Quick start

# 1. Upload certs/cloudflare-doh-ca.crt to the router (Files), then:
/import file-name=00-install-ca.rsc
/import file-name=01-configure-doh.rsc
/import file-name=02-security-policies.rsc
/import file-name=05-dns-health-monitor.rsc

# 2. Check
/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 (see Troubleshooting)
/log print where message~"DNS monitor"

Clone this wiki locally