Repository navigation
Monitoring
The scripts in routeros/ keep each router healthy on its own. This page covers the
other half: seeing the state of the whole fleet — DoH and VPN — in one place, with
triggers that fire before users call.
Everything below is shipped in the repository under zabbix/
— note that zabbix/ was committed after the v0.2.0 tag, so it is on main
and in no release yet. Measured on Zabbix 8 against six MikroTik devices
(RouterOS 7.23.1 and 7.24.5 — the two versions this repository records).
A Zabbix agent on the router is not an option: the fleet includes devices where
device-mode is home, so fetch and scheduler are closed. Even on advanced
devices, adding an agent means another item of software to keep alive.
So the direction is reversed. A single collector on the management host
(192.168.31.31 in our deployment) opens one SSH session per router every two
minutes, asks a few questions with RouterOS scripting, and pushes the answers to
Zabbix as trapper items.
management host
doh-zabbix-collector.timer ──▶ doh-zabbix-collector.service
│ ssh chateau-md@<router> (key /root/.ssh/id_mikrotik_agent)
▼
RouterOS: :resolve three canaries
/ip dns get use-doh-server
/interface wireguard peers (count, last-handshake)
│
▼ zabbix_sender 127.0.0.1:10051
Zabbix trapper items ──▶ triggers ──▶ service tree
One command per router, sent as a single SSH argument (no intermediate shell), so there is no quoting to get wrong:
:local r "";
:foreach d in={"example.com";"cdnjs.cloudflare.com";"cdn.tailwindcss.com"} do={
:local ip ""; :do {:set ip [:tostr [:resolve $d]]} on-error={:set ip ""};
:set r ($r . $d . "=" . $ip . ";")};
:put ("EP=" . [/ip dns get use-doh-server]);
:put ("MON=" . [:len [/system scheduler find name="Scheduler_DNS_Health_Check"]]);
:put ("CAN=" . $r);
:put ("CLK=" . [/system clock get date]);
:local n 0; :local m 999999;
:foreach p in=[/interface wireguard peers find] do={
:set n ($n + 1); :local hs [/interface wireguard peers get $p last-handshake];
:if (($hs != "") and ($hs > 0) and ($hs < $m)) do={:set m $hs}};
:put ("PEERS=" . $n); :put ("HS=" . $m);
:put ("GW=" . [:len [/ip route find where routing-table="gw535" and active=yes and dst-address="0.0.0.0/0"]])
All six :put lines matter: the collector parses EP, MON, CAN, CLK,
PEERS, HS and GW, and a line missing from the snippet leaves its item
empty (doh.monitor never builds without MON, for example).
| Key | Type | Meaning |
|---|---|---|
doh.state |
numeric | 0 healthy, 1 stub pair, 2 not answering, 3 no data |
doh.endpoint |
text | current /ip dns use-doh-server
|
doh.canary |
text | answers for the three canaries |
doh.monitor |
numeric | does the Scheduler_DNS_Health_Check entry exist on the device (1/0) |
vpn.state |
numeric | 0 ok, 1 degraded, 2 down, 3 no data |
vpn.peers |
numeric | number of WireGuard peers |
vpn.handshake |
numeric (s) | age of the freshest handshake |
vpn.detail |
text | the same in words |
doh.state is decided by content, exactly like the on-device monitor: any canary
resolving to a real address means healthy; only stub answers mean stub; no answer
at all means dead.
vpn.state thresholds: no peers → down; freshest handshake older than 15 minutes →
down; older than 5 minutes → degraded. Devices that carry no tunnels are pinned to
ok — they are not supposed to have handshakes.
| Name | Expression | Severity |
|---|---|---|
| DoH is down | last(/<host>/doh.state)=2 |
Disaster |
| No DoH data | nodata(/<host>/doh.state,15m)=1 |
Warning |
| VPN tunnel is down | last(/<host>/vpn.state)=2 |
Disaster |
| VPN tunnel is unstable | last(/<host>/vpn.state)=1 |
Warning |
| No VPN data | nodata(/<host>/vpn.state,15m)=1 |
Warning |
There is deliberately no trigger for the stub pair. For the networks this fleet
sits in, 8.6.112.x / 8.47.69.x is the normal Cloudflare answer for legitimate
names — see Troubleshooting §6.10. A trigger on it would be open
forever and would train everyone to ignore the board. The condition stays visible in
doh.state (=1) and in doh.canary, where it can be read when a site misbehaves.
DoH на микротиках («DoH on the MikroTiks»)
└── DoH на микротиках / mt-31-1 (problem tags: service=doh, device=mt-31-1)
└── … one per device
VPN
├── Transport, Tunnel, Routing, … (pre-existing children of the VPN root)
└── Микротики («MikroTiks»)
└── Микротики / mt-31-1 (problem tags: service=vpn, device=mt-31-1)
└── … one per device
The service names are Cyrillic on purpose — they are the strings the script creates, and the Zabbix UI shows them exactly as written. Look for them by name, not by a translated label.
Open it at https://<zabbix>/zabbix.php?action=service.list.
Four Zabbix 8 behaviours worth knowing before building the same thing:
- a service cannot have
problem_tagsand children at the same time — parents aggregate children, leaves take their status from tags; - the script creates every service with
algorithm=1, which means "problem if at least one child has a problem" — the behaviour you want, and the one measured on 2026-10-26, where a single device's problem reached «Микротики» and then «VPN».algorithm=2means "problem only if all children have problems", i.e. one dead site would stay invisible; -
status = -1in the API means OK, not "unknown". Test forstatus >= 1; -
parentidand theauthparameter are gone: the token travels in theAuthorizationheader, and hierarchy is an arrayparents. Value maps belong to a host, not to the system.
# 1. objects in Zabbix (idempotent - safe to re-run after edits)
export ZBX_USER=... ZBX_PASSWORD=... # API login; it is not stored in the file
python3 doh-zabbix-setup.py
# 2. collector and timer on the management host
install -m 755 doh-zabbix-collector.py /usr/local/bin/
cp systemd/doh-zabbix-collector.{service,timer} /etc/systemd/system/
systemctl daemon-reload && systemctl enable --now doh-zabbix-collector.timer
# 3. verify
systemctl list-timers doh-zabbix-collector.timer
journalctl -u doh-zabbix-collector.service -n 10The setup script finds the routers by the addresses of existing hosts, so it
reuses mt-31-1, mt-1-1 and friends instead of creating duplicates. Hosts are
matched on the value in the item's interface, which means renaming a host in Zabbix
does not break the mapping.
Prerequisites: zabbix_sender, an SSH key with chateau-md@ access on every
router, and current host keys in known_hosts. A stale RSA entry in known_hosts
is the most common reason the collector reports unknown for one device.
Sending a value is not the same as a working trigger chain. To check the chain, stop the timer first — otherwise it overwrites the injected value two minutes later and the test proves nothing:
systemctl stop doh-zabbix-collector.timer
printf '"mt-31-1" vpn.state "2"\n' | zabbix_sender -z 127.0.0.1 -i -Expected: the problem "VPN tunnel is down" opens, MikroTiks / mt-31-1 goes to
Disaster, and within a minute MikroTiks and VPN follow. Then restore:
printf '"mt-31-1" vpn.state "0"\n' | zabbix_sender -z 127.0.0.1 -i -
systemctl start doh-zabbix-collector.timerMeasured on 2026-10-26: problem opened at 14:25:23 and closed at 14:25:27;
aggregation child → MikroTiks → VPN confirmed with the timer stopped.
- Resolution is two minutes. A tunnel that drops and recovers inside one interval is invisible here — that is what the on-device monitor and its log are for.
-
One SSH hop per router. If the management host cannot reach a router, that
device reports
no dataand thenodata()trigger fires; it does not mean the router is broken. -
last-handshakeis a duration, not a number. RouterOS returns00:01:00; the collector parsesw/d/h:m:sand plain seconds. Sending the raw string leaves the item at zero. - The collector never changes anything on a router. It only reads and resolves.
The RouterOS side stays copy-by-hand: a router configuration is reviewed, not installed. The Zabbix side is ordinary Python, so it ships as a package:
pip install mikrotik-doh-failover
doh-zabbix-setup # create host, items and triggers
doh-zabbix-collector # send one round of valuesCredentials are read at run time from ZBX_USER / ZBX_PASSWORD; the sender path from
ZBX_SENDER. A missing sender now says what to install instead of raising a traceback.
Installing is optional — the same files run from a checkout.
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