Repository navigation
Flow
Seven diagrams, in the order that makes them easiest to read:
- Component map — what talks to what
- Installation order
- The DNS query path, step by step
- Health monitor algorithm — exactly as coded
- Monitor state machine
- Failover in time — who does what
- Diagnosing "the site does not open"
Everything below reflects routeros/05-dns-health-monitor.rsc in v0.4.0, verified
on RouterOS 7.23.1.
flowchart LR
subgraph LAN["LAN"]
C1["client 1<br/>DHCP: DNS = router"]
C2["client 2<br/>hardcoded 8.8.8.8"]
end
subgraph R["RouterOS"]
NAT["dstnat redirect<br/>UDP/TCP 53 -> router"]
RES["router resolver<br/>/ip dns"]
CACHE["DNS cache"]
RULES{"use-doh-server<br/>set?"}
MON["health monitor<br/>script + 60s scheduler"]
ST["/ip dns static<br/>bootstrap A records"]
end
subgraph UP["Upstreams"]
A["DoH A — primary<br/>standard Cloudflare"]
B["DoH B — fallback<br/>Google"]
P["plain servers<br/>1.1.1.1, 1.0.0.1<br/>bootstrap only"]
end
C1 -->|"query to router"| RES
C2 -->|"query to 8.8.8.8"| NAT
NAT -->|"rewritten to router"| RES
RES --> CACHE
CACHE -->|"miss"| RULES
RULES -->|"yes"| A
RULES -.->|"after failover"| B
RULES -.->|"never as fallback"| P
MON -->|"/tool fetch probe<br/>does not touch /ip dns"| A
MON -->|"probe before switching"| B
MON -->|"switch / rollback"| RULES
ST -->|"endpoint hostnames<br/>resolve without DNS"| MON
MON -->|"reads answers"| RES
CA["trusted CA<br/>SSL.com ECC 2022"] -->|"TLS validation"| A
CA -->|"TLS validation"| B
NTP["NTP"] -->|"correct clock"| CA
flowchart TD
START["start"] --> PRE{"RouterOS 7.20+?<br/>NTP synchronized?<br/>device-mode known?"}
PRE -->|"no"| STOP1["stop: fix preconditions"]:::bad
PRE -->|"yes"| UPL["upload certs/cloudflare-doh-ca.crt<br/>to the router Files"]
UPL --> S00["00-install-ca.rsc<br/>import CA, set trusted=yes"]
S00 --> S01["01-configure-doh.rsc<br/>servers, use-doh-server,<br/>bootstrap records for A and B"]
S01 --> S02["02-security-policies.rsc<br/>redirect 53, accept LAN, drop WAN"]
S02 --> S05["05-dns-health-monitor.rsc<br/>script + scheduler"]
S05 --> CHK{"/certificate print where trusted=yes<br/>:resolve example.com"}
CHK -->|"real address"| OK["done: monitor runs every interval (60s)"]:::good
CHK -->|"8.6.112.x / 8.47.69.x"| STUB["stub pair — not a DoH failure<br/>see troubleshooting 6.10"]:::warn
CHK -->|"empty / error"| FIX["check CA, clock, bootstrap records"]:::bad
classDef good fill:#d7f5dd,stroke:#2f7d4f,color:#000
classDef warn fill:#fff4cc,stroke:#a67c00,color:#000
classDef bad fill:#ffd6d6,stroke:#a33,color:#000
flowchart TD
Q["client asks for cdn.tailwindcss.com"] --> SRC{"client's resolver<br/>= router?"}
SRC -->|"yes"| LOCAL["answered locally by the router"]
SRC -->|"no, e.g. 8.8.8.8"| REDIR["dstnat redirect rewrites<br/>destination to the router"]
REDIR --> LOCAL
LOCAL --> HIT{"in cache?"}
HIT -->|"yes"| ANS["answer returned<br/>TTL from cache"]
HIT -->|"no"| DOH{"use-doh-server set?"}
DOH -->|"yes"| QUERY["HTTPS query to the active endpoint<br/>certificate verified"]
DOH -->|"no"| PLAIN["plain query to servers=<br/>bootstrap path"]
QUERY --> CACHEIT["answer cached"]
PLAIN --> CACHEIT
CACHEIT --> ANS
ANS --> KIND{"what came back?"}
KIND -->|"104.x / 172.x"| REAL["real address<br/>works everywhere"]:::good
KIND -->|"8.6.112.x / 8.47.69.x"| PAIR["stub pair<br/>works only where reachable"]:::warn
KIND -->|"0.0.0.0"| BLOCK["filtering resolver blocked it"]:::warn
classDef good fill:#d7f5dd,stroke:#2f7d4f,color:#000
classDef warn fill:#fff4cc,stroke:#a67c00,color:#000
Exactly the branches of 05-dns-health-monitor.rsc.
flowchart TD
RUN["scheduler fires every interval (60s)"] --> DEVICE{"device-mode == advanced?"}
DEVICE -->|"no (home)"| MANUAL["state = manual-only<br/>log MONITOR_DISABLED_BY_DEVICE_MODE<br/>STOP"]:::warn
DEVICE -->|"yes"| CLOCK{"year from /system clock is 2025 or later?"}
CLOCK -->|"no"| CLOCKBAD["log error: clock looks wrong<br/>cert validation impossible<br/>STOP"]:::bad
CLOCK -->|"yes"| CANARY["resolve 3 canaries:<br/>example.com, cdnjs.cloudflare.com,<br/>cdn.tailwindcss.com"]
CANARY --> JUDGE["judge CONTENT:<br/>empty / 0.0.0.0 / stub prefix = bad"]
JUDGE --> GOOD{"any real address?"}
GOOD -->|"yes"| RESET["DNSFailoverAttempts = 0"]
RESET --> WASB{"state == fallback or recovering?"}
WASB -->|"yes"| BACK["set use-doh-server = A<br/>flush cache<br/>state = healthy<br/>log warning"]:::good
WASB -->|"no"| HSTATE{"state != healthy?"}
HSTATE -->|"yes"| HLOG["state = healthy<br/>log info once"]:::good
HSTATE -->|"no"| NOTHING["no action, no log"]:::good
GOOD -->|"no"| STUBSEEN{"any answer<br/>was the stub pair?"}
STUBSEEN -->|"yes"| STUBSTATE{"state != stub?"}
STUBSTATE -->|"yes"| STUBLOG["state = stub<br/>log warning once:<br/>pair unreachable here?<br/>switching does not help"]:::warn
STUBSTATE -->|"no"| NOTHING2["no action, no log"]:::warn
STUBSEEN -->|"no"| PROBEA["probe endpoint A<br/>/tool fetch + DNS-JSON"]
PROBEA --> ALIVEA{"probeA == ok?"}
ALIVEA -->|"yes"| FLUSH["flush cache<br/>state = suspect<br/>log SUSPECT once<br/>re-check next interval"]:::warn
ALIVEA -->|"no"| LOGDOWN{"state not suspect/fallback?"}
LOGDOWN -->|"yes"| ERR1["log error:<br/>primary DoH unreachable"]:::bad
LOGDOWN -->|"no"| SKIPLOG["no repeat log"]
ERR1 --> CANDO
SKIPLOG --> CANDO
CANDO{"applyChanges AND attempts below maxAttempts?"}
CANDO -->|"no"| KEEP1["keep current configuration"]:::warn
CANDO -->|"yes"| PROBEB["probe endpoint B<br/>/tool fetch + DNS-JSON"]
PROBEB --> ALIVEB{"probeB == ok?"}
ALIVEB -->|"no"| KEEP2["state = degraded<br/>log DEGRADED once:<br/>both endpoints unreachable"]:::bad
ALIVEB -->|"yes"| SWITCH["attempts + 1<br/>set use-doh-server = B<br/>flush cache, wait 3 s"]
SWITCH --> VERIFY{"resolve example.com<br/>returns anything?"}
VERIFY -->|"yes"| SWOK["state = fallback<br/>log warning with the answer"]:::good
VERIFY -->|"no"| ROLL["set use-doh-server = A<br/>flush cache<br/>state = degraded<br/>log DEGRADED: check CA for B"]:::bad
classDef good fill:#d7f5dd,stroke:#2f7d4f,color:#000
classDef warn fill:#fff4cc,stroke:#a67c00,color:#000
classDef bad fill:#ffd6d6,stroke:#a33,color:#000
stateDiagram-v2
[*] --> healthy: first clean run
[*] --> clock_invalid: wrong date
[*] --> manual_only: device-mode=home
healthy --> suspect: canary fails once
suspect --> healthy: canary passes
suspect --> fallback: failureThreshold bad runs, fallback verified
fallback --> recovering: primary passes once
recovering --> healthy: recoveryThreshold good runs
recovering --> fallback: primary fails again
suspect --> degraded: both endpoints unreachable
fallback --> degraded: fallback stopped resolving
healthy --> stub: answers became the pair
suspect --> stub: answers became the pair
stub --> healthy: real answers again
stub --> degraded: answers empty too
note right of stub
No failover here on purpose:
a second endpoint returns
the same stub pair
end note
note right of fallback
failureThreshold / recoveryThreshold
are the anti-flap hysteresis
end note
sequenceDiagram
autonumber
participant LAN as LAN client
participant R as Router resolver
participant A as DoH A primary
participant B as DoH B fallback
participant M as Monitor
LAN->>R: resolve cdn.tailwindcss.com
R->>A: HTTPS query
A-->>R: 8.47.69.0 / 8.6.112.0
R-->>LAN: answer (the stub pair)
Note over M: one interval later
M->>R: :resolve 3 canaries
R-->>M: stub pair only
M->>M: content check fails, stubSeen = 1
M->>M: state = stub, one warning
Note right of M: no switch — B would answer the same
Note over A: primary goes down
M->>R: :resolve canaries
R-->>M: empty on error
M->>A: /tool fetch probe
A--xM: unreachable
M->>B: /tool fetch probe
B-->>M: Status 0
M->>R: set use-doh-server = B, flush cache
M->>R: wait 3 s, :resolve example.com
R->>B: HTTPS query
B-->>R: 104.20.23.154
R-->>M: address
M->>M: state = fallback, log warning
Note over A: primary recovers
M->>R: :resolve canaries
R->>B: HTTPS query
B-->>R: real addresses
R-->>M: real answers and state = fallback
M->>R: set use-doh-server = A, flush cache
M->>M: state = healthy, attempts = 0, log warning
flowchart TD
S["browser cannot open a CDN name"] --> D1{"monitor log says<br/>which state?"}
D1 -->|"healthy"| D2{"resolve the name<br/>from a client"}
D2 -->|"empty"| D3["client-side problem:<br/>its own DNS/tunnel"]:::warn
D2 -->|"real address"| D4["not DNS: TLS, MTU,<br/>proxy or the site itself"]:::warn
D2 -->|"stub pair"| D5{"curl --resolve name:443:8.6.112.0<br/>from the client"}
D1 -->|"stub"| D5
D5 -->|"302 in ~0.2 s"| D6["pair is reachable here:<br/>DNS is not the cause"]:::good
D5 -->|"timeout"| D7{"do you have a tunnel<br/>to a working vantage?"}
D7 -->|"yes"| F1["route the pair through the tunnel<br/>address-list + routing mark"]:::good
D7 -->|"no"| F2["pin reachable addresses<br/>/ip dns static for the names you need"]:::good
D1 -->|"degraded"| D8{"log says both endpoints<br/>unreachable?"}
D8 -->|"yes"| D9{"bootstrap records<br/>for both hosts present?"}
D9 -->|"no"| F3["add bootstrap A records,<br/>re-run the monitor"]:::good
D9 -->|"yes"| F4["no DNS path at all:<br/>check WAN, device-mode fetch"]:::bad
D8 -->|"no"| F5["cache flushed, wait one cycle;<br/>if still dead check the CA"]:::warn
classDef good fill:#d7f5dd,stroke:#2f7d4f,color:#000
classDef warn fill:#fff4cc,stroke:#a67c00,color:#000
classDef bad fill:#ffd6d6,stroke:#a33,color:#000
| Symbol | Meaning |
|---|---|
| solid arrow | data path that happens on every query |
| dotted arrow | path taken only after a failover |
| green node | healthy outcome |
| yellow node | degraded but explained — no action needed on the router |
| red node | needs a human |
The single most important thing these diagrams encode: stub is not a failure
of DoH. The monitor deliberately does not fail over there, because the second
endpoint returns the same pair. What matters is whether that pair is reachable
from the network — and that question can only be answered from a client, not from
the router.
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