Версия 0.4.1 — это релиз про надёжность, а не про новые возможности: цепочка отказоустойчивости DoH на MikroTik получила жёсткий таймаут на каждой пробе, симметричную проверку при переключении на третий эндпоинт и честный счётчик попыток на всю цепочку. Но отдельной строки в истории изменений мало: с момента, когда вышла основная статья про этот набор (она описывала состояние 0.2.0), прошли ещё 0.3.1 и 0.4.0 — и там появилось то, ради чего всё делалось: падение в обычный DNS как последняя ступень, проба в памяти без временных файлов и автомат состояний с гистерезисом. Ниже — что именно изменилось, что обновлять на роутере и как убедиться, что изменения прижились.

mikrotik-doh-failover v0.4.1 — обложка обзора версии
Цепочка primary → secondary → tertiary → plain и жёсткий таймаут на каждой пробе

TL;DR: что нового в 0.4.1

  1. Жёсткий таймаут. Каждый /tool fetch в мониторе несёт duration=5s — зависший эндпоинт больше не блокирует прогон дольше интервала планировщика.
  2. Третий эндпоинт проверяется так же, как второй. Переключение на dohUrlC теперь идёт с проверкой после переключения и откатом при неудаче.
  3. Счётчик честный. DNSFailoverAttempts растёт на каждом реальном переключении — на второй, третий и обычный DNS, — а не только на первом.
  4. Возврат из plain DNS не ждёт основной. Если основной эндпоинт лежит, а второй отвечает, монитор поднимается на любой рабочий DoH.
  5. Обновление — один импорт. Меняется только 05-dns-health-monitor.rsc, остальные скрипты трогать не нужно.
/import file-name=05-dns-health-monitor.rsc
:global DNSMonitorState; :set DNSMonitorState ""
:global DNSFailCount; :set DNSFailCount 0
:global DNSRecoverCount; :set DNSRecoverCount 0
:global DNSFailoverAttempts; :set DNSFailoverAttempts 0
:global DNSMonitorRunning; :set DNSMonitorRunning false

Проблема, которую решает версия

Монитор DoH — это скрипт, который сам решает, когда сеть переезжает на резервный резолвер. У такого скрипта три места, где ошибка стоит дорого: проба может зависнуть, переключение может оставить сеть на мёртвом эндпоинте, а возврат может застрять на неполной логике.

Первое место до 0.4.1 было открытым. Если эндпоинт принимает соединение и молчит, /tool fetch без таймаута ждёт столько, сколько даст сеть, и прогон выходит за интервал планировщика. На практике это значит, что следующий запуск накладывается на предыдущий, а в 0.3.1 появился защитный флаг от наложения — то есть сеть в такой ситуации не переключалась бы вообще, пока висит первая проба.

Второе место: третий эндпоинт (dohUrlC) добавлялся как «ещё один резерв», но проверка после переключения у него отсутствовала. Если адрес принимает соединение, а имён не разрешает, монитор оставался на сломанной третьей ступени, хотя основной к тому времени мог уже ожить.

Третье место — счётчик попыток. Логика «не больше трёх попыток на инцидент» полезна только тогда, когда считает все переключения; до 0.4.1 он рос лишь на первом, поэтому длинная цепочка могла прокручиваться дальше, чем задумано.

И четвёртое, самое неприятное по последствиям: возврат из обычного DNS. Пока резолвер работал без шифрования, монитор ждал именно основной эндпоинт. Если основной лежал час, а второй был жив, сеть час жила без DoH — при том что любая шифрованная ступень лучше открытого DNS.

Что нового: четыре изменения по существу

Жёсткий таймаут на каждой пробе

В скрипте на main шесть вызовов /tool fetch, и у всех теперь есть duration=5s (посчитано поиском по duration=5s в routeros/05-dns-health-monitor.rsc). Автор указывает, что поведение проверено на RouterOS 7.23.1.

Что было Что стало Зачем
проба без ограничения времени duration=5s на каждом fetch зависший эндпоинт не выносит прогон за интервал планировщика
наложение прогонов защищено флагом флаг остался, но теперь реже срабатывает меньше «пропущенных» проверок

Симметрия переключения: третий эндпоинт как второй

Ступень Проба до переключения Проверка после Откат при неудаче
secondary (dohUrlB) была была была
tertiary (dohUrlC) была появилась в 0.4.1 появилась в 0.4.1
plain DNS (plainFallback) — по содержимому возврат на DoH

Смысл симметрии простой: любая ступень, на которую сеть переехала, должна отвечать именами, а не просто открывать TCP. Если dohUrlC достижим, но не разрешает, монитор откатывается на основной, а не остаётся на сломанной третьей ступени.

Счётчик на всю цепочку

DNSFailoverAttempts теперь растёт на каждом реальном переключении: secondary, tertiary и plain. Вместе с maxAttempts = 3 это даёт ровно то поведение, которое обещано в документации: не больше трёх переключений на инцидент, дальше монитор ждёт восстановления основного и не забивает журнал.

Возврат из plain DNS на любой рабочий DoH

Было Стало
из состояния plain выход только на основной эндпоинт выход на любой рабочий DoH: если основной лежит, а второй отвечает — сеть возвращается в DoH
основной лежит час — час открытого DNS открытый DNS только пока не отвечает ни один DoH

Это изменение важнее, чем кажется: plain — это состояние с пониженной приватностью (в журнал оно пишется строкой PRIVACY DEGRADED), и держать его дольше необходимого нельзя.

Ключевые изменения с 0.2.0: что вы пропустили

Основная статья описывала 0.2.0. Между ней и текущей версией вышли две содержательные версии, и без них картина неполная.

flowchart LR
  V020["0.2.0: один DoH и резерв DoH, проба файлом"] --> V031["0.3.1: автомат состояний, гистерезис, строгий режим, Zabbix"]
  V031 --> V040["0.4.0: цепочка primary → secondary → tertiary → plain"]
  V040 --> V041["0.4.1: duration=5s, проверка tertiary, возврат на любой DoH"]
  V041 --> NOW["текущая цепочка отказоустойчивости DoH"]
Версия Что появилось Почему это важно
0.3.1 автомат состояний HEALTHY → SUSPECT → FALLBACK → RECOVERING → HEALTHY плюс STUB, DEGRADED, CLOCK_INVALID, MANUAL-ONLY поведение стало предсказуемым, а не «набором условий в скрипте»
0.3.1 failureThreshold и recoveryThreshold (оба 2) как защита от дребезга сеть не переключается на каждой одиночной неудаче
0.3.1 настраиваемый monitorInterval (60 с вместо жёстких 3 минут) проверка чаще, при этом без ручной правки тела скрипта
0.3.1 enforcement=strict в 02-security-policies.rsc: блокировка DoT (853) и известных публичных DoH закрывает обход принудительного DNS, но с честно описанным остаточным риском
0.3.1 модуль zabbix/: 8 элементов, 5 триггеров, 2 дерева сервисов состояние DoH и VPN видно по парку, а не по одному роутеру
0.4.0 проба через as-value output=user — ответ в памяти, без временных файлов в /file убраны гонки очистки файлов
0.4.0 упорядоченная цепочка primary → secondary → tertiary → plain появилась последняя ступень на случай, когда не отвечает ни один DoH
0.4.0 новые настройки dohUrlC, probeUrlC, plainFallback, plainServers цепочка описывается конфигом, а не правкой тела
0.4.1 таймаут, симметрия tertiary, счётчик, возврат из plain то, о чём эта статья

Отдельная строка про гистерезис: два плохих прогона подряд на переключение и два хороших на возврат — это компромисс между скоростью реакции и дребезгом. При интервале 60 секунд переключение происходит примерно через две минуты после устойчивой деградации, а возврат — через две минуты после устойчивого восстановления.

Замеры и цифры

mikrotik-doh-failover v0.4.1: ключевые числа
mikrotik-doh-failover v0.4.1: ключевые числа релиза и проверенной базы
Что Значение Откуда
Изменено файлов между 0.4.0 и 0.4.1 6 заметки к релизу v0.4.1
Коммитов между 0.4.0 и 0.4.1 3 там же
Вызовов /tool fetch с duration=5s 6 поиск по routeros/05-dns-health-monitor.rsc на main
Значений гистерезиса по умолчанию 2 и 2 config.example.rsc: failureThreshold, recoveryThreshold
Попыток на инцидент по умолчанию 3 maxAttempts
Интервал монитора по умолчанию 60 с monitorInterval
Тиков таймаута пробы 5 с duration=5s
Проверенных устройств 6 docs/verification.md
Проверенных версий RouterOS 7.23.1 и 7.24.5 там же
Объектов Zabbix в модуле 8 элементов, 5 триггеров, 2 дерева zabbix/
Скриптов RouterOS в наборе 7 (около 360 строк) README, раздел «В цифрах»

Все числа выше — из репозитория: либо из заметок к релизу, либо из самого кода на main. Замеров на собственном роутере в этом прогоне не было, поэтому строки про поведение на устройстве помечены как данные автора, а не как мой прогон.

Почему это круто

  • Таймаут делает прогон предсказуемым. Монитор с планировщиком обязан укладываться в свой интервал; duration=5s превращает это из пожелания в свойство.
  • Симметрия убирает особый случай. Одна и та же логика для второй и третьей ступени — меньше ветвей, которые кто-то забудет обновить.
  • Счётчик перестал врать. Ограничение «три попытки» наконец означает три попытки на всю цепочку.
  • Возврат быстрее. Открытый DNS остаётся ровно столько, сколько нет ни одного рабочего DoH.
  • Проба не оставляет мусора. С 0.4.0 ответ читается в памяти — временные файлы в /file и гонки их очистки больше не существуют.
  • Цепочка описывается конфигом. dohUrlC пустой — ступени нет; plainFallback=false — открытый DNS запрещён полностью.
  • Строгий режим назван честно. enforcement=strict блокирует DoT и публичные DoH, но остаточный риск описан, а не спрятан.
  • Мониторинг парка в комплекте. Модуль Zabbix видит состояние DoH и VPN с шести роутеров по SSH, а не по одному устройству.

Установка и обновление

Если набор уже стоит на 0.3.1 или 0.4.0

Меняется только монитор: якорь доверия, настройка апстрима и политики не тронуты. Порядок из заметок к релизу — импорт скрипта и очистка глобалов состояния, чтобы старые значения не влияли на новый код.

/import file-name=05-dns-health-monitor.rsc
:global DNSMonitorState; :set DNSMonitorState ""
:global DNSFailCount; :set DNSFailCount 0
:global DNSRecoverCount; :set DNSRecoverCount 0
:global DNSFailoverAttempts; :set DNSFailoverAttempts 0
:global DNSMonitorRunning; :set DNSMonitorRunning false

Перед импортом полезно снять состояние до правки — это и есть откат:

/export file=before-041
/system backup save name=before-041

Если набор ставится с нуля

flowchart TD
  BKP["/export file=before-doh и /system backup save"] --> NTP["NTP по IP, без зависимости от DNS"]
  NTP --> CA["00-install-ca.rsc: якорь доверия"]
  CA --> DOH["01-configure-doh.rsc: один апстрим и четыре bootstrap-записи"]
  DOH --> POL["02-security-policies.rsc: принудительный 53 и защита резолвера"]
  POL --> MON{"обновление с 0.4.0?"}
  MON -- да --> ONLY["импорт только 05-dns-health-monitor.rsc плюс очистка глобалов"]
  MON -- "новая установка" --> ALL["05-dns-health-monitor.rsc целиком"]
  ONLY --> VER["проверка по таблице из AGENTS.md §5"]
  ALL --> VER

Порядок важен: якорь доверия до апстрима, апстрим до политик, монитор последним. Каждый шаг проверяется отдельным блоком из AGENTS.md §5, и несовпадение ожидаемого значения — это дефект, а не повод «попробовать ещё раз».

Шаг Файл Что делает
0 — /export file=before-doh и /system backup save name=before-doh
1 пример конфигурации NTP по IP-адресам: время не должно зависеть от DNS, который мы защищаем
2 00-install-ca.rsc импорт корневого CA и отметка trusted=yes
3 01-configure-doh.rsc один апстрим, verify-doh-cert=yes, четыре bootstrap-записи
4 02-security-policies.rsc принудительный 53 для локальной сети, отбрасывание DNS из WAN
5 05-dns-health-monitor.rsc скрипт монитора и планировщик

Отдельная деталь про закрепление ссылок: main уходит вперёд тега, поэтому raw-файлы лучше брать по коммиту или тегу. В AGENTS.md закреплён коммит 38ef7e0 — это правильный подход, но помните, что вместе с ним вы получите и состояние документации на тот момент.

Проверка после обновления

Проверять нужно не «работает ли интернет», а четыре конкретные вещи: таймаут в теле скрипта, состояние монитора, счётчик попыток и отсутствие ошибок в журнале.

# 1. в скрипте на роутере есть duration=5s
/system script print where name="DNS_HealthMonitor_Failover"

# 2. состояние и счётчик
:put $DNSMonitorState
:put $DNSFailoverAttempts

# 3. разрешение имён идёт через DoH, а не через заглушку
/ip dns cache flush
:put [:resolve example.com]

# 4. в журнале нет ошибок соединения
/log print where message~"connection error"
flowchart TD
  P["/tool fetch к DNS-JSON API, duration=5s"] --> R{"status пробы"}
  R -- finished --> BODY{"в теле есть Status:0?"}
  R -- "timeout или connection error" --> DOWN["endpoint недоступен: считать отказом"]
  R -- "400 или 405" --> TLS["TLS и сертификат прошли: endpoint жив"]
  BODY -- да --> OK["canary пройдена"]
  BODY -- нет --> BAD["считать неудачей"]
  TLS --> OK
  OK --> COUNT{"failureThreshold плохих прогонов подряд?"}
  BAD --> COUNT
  DOWN --> COUNT
  COUNT -- да --> SW["переключение по цепочке с проверкой и откатом"]
  COUNT -- нет --> WAIT["ждать следующего прогона"]
Проверка Ожидание
Состояние после чистого прогона healthy
Счётчик попыток, пока основной отвечает 0
Разрешение example.com реальный адрес, не пара 8.6.112.x / 8.47.69.x
/log print where message~"connection error" пусто на здоровой конфигурации
Форма пробы as-value output=user, без временных файлов

Отдельно про диагностику: нельзя делать вывод «DoH мёртв» по ошибке /tool fetch. Ответ 400 или 405 означает, что TLS-рукопожатие и проверка сертификата прошли, — то есть эндпоинт жив, а запрос просто не того типа. Монитор ищет в ответе "Status":0, а не отсутствие ошибки.

Совместимость и ограничения

Что Значение
RouterOS 7.20+ по заявлению, поведение проверено на 7.23.1 и 7.24.5
device-mode advanced для планировщика; на home монитор запускается только вручную
dedicated механизм блокировки клиентского DoH enforcement=strict: DoT 853 и список публичных DoH
Провайдеры в цепочке только те, что отвечают на DNS-JSON; Quad9 в резерв не годится
Полная реприоритизация нет: подъём «вверх» по цепочке, но не переход «вбок» между резервами

Ограничение про «вбок» стоит прочитать внимательно: монитор поднимается с открытого DNS на любой DoH и с резерва возвращается на основной, но не различает «сижу на втором» и «сижу на третьем», чтобы перескочить между ними. Строгий приоритет сохранён: основной всегда предпочтительнее.

Подводные камни

  • 01 и 02 не идемпотентны. Повторный импорт добавит вторую запись в /ip dns static, второе правило NAT и второе правило фильтра. Перед повторным импортом смотрят, что уже есть, по комментариям DoH bootstrap, force DNS, DNS.
  • Резервному хосту нужен свой корневой CA. Для dns.google это не SSL.com: без корня Google проверка сертификата отклонит резерв, переключение не даст разрешения имён, и монитор откатится — то есть вы останетесь на мёртвом основном.
  • duration не отменяет таймаутов планировщика. Пять секунд на пробу — это потолок на одну операцию; полный прогон из шести проб теоретически может занять больше интервала, если все эндпоинты висят.
  • Quad9 не может быть звеном цепочки. На JSON-пробу он отвечает DoH unable to decode BASE64-URL, и выглядит недоступным.
  • device-mode=home блокирует и планировщик, и fetch. На таком устройстве монитор сохраняет текущую конфигурацию и работает только вручную.
  • Документация отстаёт от тега. AGENTS.md в заголовке называет версию 0.4.0, а README в блоке состояния документации пишет «покрытая версия 0.4.0, последний тег v0.3.1», хотя тег 0.4.1 уже выпущен. Читая инструкцию, сверяйтесь с датой и с самим кодом на main — при расхождении верить коду.
  • Флаг filtering ничего не делает. В мониторе он объявлен и нигде не читается: пометка для читателя, а не переключатель поведения.

Чек-лист обновления

  1. Снять /export file=before-041 и /system backup save name=before-041.
  2. Забрать 05-dns-health-monitor.rsc по закреплённому коммиту или тегу, не с main.
  3. Проверить в скачанном файле наличие duration=5s и as-value output=user — до импорта, а не после.
  4. Импортировать монитор и очистить пять глобалов состояния.
  5. Проверить DNSMonitorState и DNSFailoverAttempts после первого чистого прогона.
  6. Убедиться, что разрешение имён даёт реальный адрес, а журнал не содержит connection error.
  7. Если планируется третья ступень — задать dohUrlC и probeUrlC и проверить, что провайдер отвечает на DNS-JSON.

Критерий «обновление прижилось»: состояние healthy, счётчик попыток 0, разрешение имён работает, в журнале нет ошибок, а в теле скрипта на роутере есть duration=5s.

FAQ

Нужно ли обновлять остальные скрипты, кроме монитора?

Нет. В 0.4.1 меняется только 05-dns-health-monitor.rsc; 00, 01 и 02 остались прежними, и повторный их импорт вреден из-за отсутствия проверки существования записей.

Почему нельзя просто поставить duration=10s и забыть?

Можно, но тогда прогон из шести проб в худшем случае растянется до минуты, а интервал по умолчанию — 60 секунд. Пять секунд выбраны как компромисс между «дождаться медленного эндпоинта» и «уложиться в интервал».

Что будет, если третий эндпоинт настроен, но не разрешает имена?

Монитор проверит его после переключения и откатится на основной. Именно этого не хватало до 0.4.1: раньше сеть могла остаться на достижимом, но бесполезном эндпоинте.

Зачем нужен plainFallback, если он ухудшает приватность?

Как последняя ступень на случай, когда не отвечает ни один DoH. По умолчанию он выключен; если включён — в журнал пишется PRIVACY DEGRADED, и версия 0.4.1 сокращает время пребывания в этом состоянии до минимума.

Монитор может «перескочить» со второго эндпоинта на третий?

Нет. Подъём идёт «вверх» — с открытого DNS на DoH и с резерва на основной; различать «на втором» и «на третьем» для перехода вбок он не умеет. Это записано в известных ограничениях релиза.

Откуда берутся числа про шесть проверенных устройств?

Из docs/verification.md репозитория: три устройства на RouterOS 7.24.5 с беспроводным стеком, два без него и одно на 7.23.1. Собственных замеров на оборудовании в этой статье нет.

Стоит ли сразу включать enforcement=strict?

Сначала посмотрите, чем пользуются клиенты: строгий режим блокирует DoT и известные публичные DoH, и приложения, которые их используют, могут перестать разрешать имена. Остаточный риск при standard описан в документации проекта.

Итог

0.4.1 — маленький релиз, который закрывает три конкретные дыры: проба без таймаута, непроверенное переключение на третью ступень и счётчик, считавший только первое переключение. Вместе с 0.3.1 и 0.4.0 это превращает набор из «скрипта, который переключает DoH» в цепочку отказоустойчивости с предсказуемым поведением: автомат состояний, гистерезис против дребезга, последняя ступень на открытом DNS с честной пометкой в журнале и возврат на любой рабочий DoH. Если у вас стоит версия 0.2.0 — обновление стоит одного импорта и одной проверки.

Цитата: Отказоустойчивость — это не «есть второй адрес», а проверка каждой ступени до и после переключения и честный счётчик того, сколько раз сеть уже переезжала.

Связанные материалы

LLM Summary

  • Тема: mikrotik-doh-failover v0.4.1 — релиз надёжности для цепочки отказоустойчивости DoH на MikroTik RouterOS; до него в блоге была только основная статья по состоянию 0.2.0.
  • Что изменилось в 0.4.1: duration=5s на каждом /tool fetch, проверка третьего эндпоинта после переключения с откатом, DNSFailoverAttempts на всю цепочку, возврат из открытого DNS на любой рабочий DoH.
  • Что было пропущено с 0.2.0: 0.3.1 — автомат состояний, гистерезис failureThreshold/recoveryThreshold по 2, monitorInterval 60 с, enforcement=strict, модуль Zabbix; 0.4.0 — проба as-value output=user без временных файлов, цепочка primary → secondary → tertiary → plain, настройки dohUrlC, probeUrlC, plainFallback, plainServers.
  • Ключевые команды: /import file-name=05-dns-health-monitor.rsc, очистка пяти глобалов состояния, :put $DNSMonitorState, :put $DNSFailoverAttempts, /log print where message~"connection error".
  • Замеры: 6 вызовов /tool fetch с таймаутом (поиск по скрипту на main), 3 коммита и 6 изменённых файлов между 0.4.0 и 0.4.1, 6 проверенных устройств, две версии RouterOS (7.23.1 и 7.24.5), 8 элементов и 5 триггеров Zabbix.
  • Ограничения: нельзя ставить Quad9 в цепочку (не отвечает на DNS-JSON), device-mode=home блокирует планировщик и fetch, перехода «вбок» между резервами нет, 01 и 02 не идемпотентны, документация в репозитории отстаёт от тега.
  • Проверка после обновления: состояние healthy, счётчик попыток 0, реальный адрес при разрешении имён, пустой журнал ошибок соединения, duration=5s в теле скрипта на роутере.
  • Ссылки: https://github.com/paulmann/mikrotik-doh-failover/releases/tag/v0.4.1, https://sysadministrator.ru/mikrotik-doh-failover/

Промт для агента (установка и настройка)

Готовый промт для AI-агента: скопируйте блок целиком кнопкой и отдайте своему ассистенту. Шаги, порядок применения и таблица проверок взяты из AGENTS.md репозитория — это инструкция автора для машинного исполнителя, а не пересказ.

Задача: обновить mikrotik-doh-failover до 0.4.1 на роутере RouterOS и доказать, что обновление прижилось.

Материалы:
- репозиторий: https://github.com/paulmann/mikrotik-doh-failover
- релиз: https://github.com/paulmann/mikrotik-doh-failover/releases/tag/v0.4.1
- контракт исполнителя (прочитать целиком до работы): https://github.com/paulmann/mikrotik-doh-failover/blob/main/AGENTS.md
- история изменений: https://github.com/paulmann/mikrotik-doh-failover/blob/main/CHANGELOG.md

Предусловия из AGENTS.md §2 — каждая строка проверяется, провал означает остановку, а не предупреждение:
- RouterOS 7.20 или новее: /system resource print
- время синхронизировано: /system ntp client print, status: synchronized
- device-mode известен: /system device-mode print (на home планировщик и fetch запрещены)
- административная сессия на роутере: импорт пишет в хранилище сертификатов
- файлы CA и .rsc загружены в ФАЙЛЫ роутера, а не в рабочий каталог: import file-name= читает файловую систему роутера
- имена интерфейсов совпадают с реальностью: /interface bridge print, /interface list print
- есть окно обслуживания: /export file=before-doh до первого шага

Порядок применения из AGENTS.md §4:
0. /export file=before-doh и /system backup save name=before-doh
1. NTP по IP-адресам: /system ntp client set enabled=yes servers=162.159.200.1,216.239.35.0
2. /import file-name=00-install-ca.rsc
3. /import file-name=01-configure-doh.rsc
4. /import file-name=02-security-policies.rsc
5. обновление до 0.4.1: /import file-name=05-dns-health-monitor.rsc, затем очистить глобалы DNSMonitorState, DNSFailCount, DNSRecoverCount, DNSFailoverAttempts, DNSMonitorRunning

Проверка из AGENTS.md §5 — ожидаемые значения, несовпадение является отчётом о дефекте:
- /certificate print where trusted=yes — доверенный SSL.com TLS ECC Root CA 2022
- /ip dns print — 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" — четыре записи: две для cloudflare-dns.com, две для dns.google
- /ip firewall nat print where comment~"force DNS" — два правила redirect, UDP и TCP, порт 53
- /ip firewall filter print where comment~"DNS" — accept из LAN, drop из WAN, UDP и TCP
- /ip dns cache flush, затем :put [:resolve example.com] — реальный адрес, НЕ пара 8.6.112.x / 8.47.69.x
- :put $DNSMonitorState — healthy после чистого прогона
- :put $DNSFailoverAttempts — 0, пока отвечает основной
- проба эндпоинта: /tool fetch с as-value output=user и duration=5s — status finished и неотрицательная позиция :find по "Status":0
- /log print where message~"connection error" — пусто на здоровой конфигурации

Запрещено:
- повторно импортировать 01 и 02 без проверки: они не идемпотентны и создадут дубли (/ip dns static print where comment~"DoH bootstrap", /ip firewall nat print where comment~"force DNS", /ip firewall filter print where comment~"DNS")
- считать ошибку /tool fetch доказательством «DoH мёртв»: 400 или 405 означают, что TLS и сертификат прошли
- ставить Quad9 звеном цепочки: он не отвечает на DNS-JSON
- рассчитывать на автоматический планировщик при device-mode=home: там только ручной запуск /system script run DNS_HealthMonitor_Failover

Что считать успехом: состояние healthy, счётчик попыток 0, разрешение имён даёт реальный адрес, журнал без ошибок соединения, в теле скрипта на роутере присутствует duration=5s.