Версия 0.4.3 — это исправление одного пути, который остался на старой логике после 0.4.2. В resolve-режиме монитор судит о доступности основного эндпоинта по качеству живых ответов, но перед переключением на резерв всё ещё спрашивал резерв по DNS-JSON. Свой DoH-эндпоинт такой запрос не обслуживает: он отдаёт wire-format и отвечает 400. Проба навсегда оставалась bad, и переключение не происходило — то есть проба резерва DoH сводила на нет весь смысл режима: основной лежал, резерв работал, роутер продолжал смотреть в мёртвый апстрим. Ниже — что именно изменилось, что пришло в 0.4.2 и осталось непроверенным, и как убедиться, что обновление прижилось.

TL;DR: что это и как проверить за десять минут
- Проба резерва в resolve-режиме больше не выполняется: монитор переключается и проверяет результат через
:resolveуже после переключения. - JSON-режим получил детект подмены для резерва: если в ответе резервного эндпоинта видны адреса-заглушки, проба сообщает
substituted, а неok. - Документация дополнена: как поднять резерв на отдельном хосте и какая ловушка есть в
dnsdist2.x с HTTP/2. - Обновление — один импорт
routeros/05-dns-health-monitor.rsc; пороги, состояния и обычный DNS-резерв не меняются. - Честная оговорка автора: само переключение на живом роутере наблюдать не удалось — оно подтверждено разбором кода, а не полевым прогоном.
/export file=before-043
/system backup save name=before-043
/import file-name=05-dns-health-monitor.rsc
:put $DNSMonitorState
Почему этот релиз существует
В 0.4.2 появился параметр probeMode с двумя значениями. json — привычная проба: запрос к DNS-JSON API публичного резолвера, проверка поля Status. resolve — новая: доступность судится по качеству ответов, которые резолвер реально отдаёт, потому что свой DoH-эндпоинт DNS-JSON не поддерживает и на такой запрос отвечает 400 Bad Request.
Переключение режима решило половину задачи: основной эндпоинт перестал считаться мёртвым. Вторая половина осталась: ветка переключения на резерв всё ещё начиналась с вопроса «а резерв отвечает по DNS-JSON?». Для своего эндпоинта ответ всегда отрицательный, поэтому probeB был bad, и монитор не переключался.
Автор описывает это замером: при остановленном основном эндпоинте /ip dns продолжал держать мёртвый апстрим, а :resolve возвращал failure на всём окне наблюдения. Это ровно тот класс дефектов, который опаснее явной ошибки: система выглядит работающей, потому что монитор молчит, а сеть в это время стоит без разрешения имён.
Что нового в 0.4.3
flowchart LR A041["0.4.1: таймаут пробы, симметрия tertiary"] --> A042["0.4.2: probeMode, детект подмены, STUB после flush, пакет на PyPI"] A042 --> A043["0.4.3: проба резерва в resolve-режиме, substituted в JSON-режиме"] A043 --> A043B["документация: резерв на отдельном хосте, грабли dnsdist 2.x"]
| Область | Было | Стало |
|---|---|---|
| Проба резерва в resolve-режиме | запрос DNS-JSON, для своего эндпоинта всегда bad |
проба не выполняется; переключение проверяется через :resolve после него |
| Проба резерва в JSON-режиме | проверялось только Status: 0 |
дополнительно распознаются адреса-заглушки в теле, статус substituted |
| Документация | — | резерв на отдельном хосте, ловушка HTTP/2 в dnsdist 2.x |
Ключевая деталь — асимметрия, которая теперь исчезла. Раньше основной и резервный эндпоинты проверялись по-разному: основной мог судиться по ответам, резервный — только по DNS-JSON. В resolve-режиме такая проверка резерва не имела смысла, потому что она отвечала не на вопрос «работает ли резерв», а на вопрос «умеет ли резерв говорить на языке, который мы выбрали для публичных резолверов».
Ключевые изменения с 0.4.1: что пришло в 0.4.2 и остаётся важным
0.4.3 продолжает 0.4.2, поэтому без второго релиза картина неполная.
| Изменение 0.4.2 | Зачем |
|---|---|
probeMode: json или resolve |
свой DoH отдаёт wire-format и не имеет DNS-JSON API |
Детект подмены: Status: 0 больше не считается доказательством |
подменяющая сеть возвращает синтаксически идеальный ответ с адресом-заглушкой |
STUB только после однократного сброса кэша и повторной проверки |
заглушка, закэшированная пока путь был перехвачен, жила до истечения TTL и выглядела постоянной |
Новый текст STUB |
путь перехвачен, смена резолвера сама по себе не поможет |
Пакет на PyPI mikrotik-doh-failover |
инструменты Zabbix ставятся как doh-zabbix-setup и doh-zabbix-collector, учётные данные читаются во время работы |
Эта таблица объясняет, почему в 0.4.3 проба резерва вообще оказалась лишней: если резолвер судится по ответам, то и переключение должно судиться по ответам, а не по доступности чужого API.
Как это устроено внутри
Скрипт монитора остаётся одной программой, которая по расписанию проверяет качество ответов и, если они плохие, идёт по цепочке: основной → резервный → третий (если задан) → обычный DNS (если разрешён). Разница версии — в том, как принимается решение о резерве.
В коде на main видно прямую реализацию нового поведения: при probeMode = "resolve" переменная пробы резерва выставляется в ok без сетевого запроса, то есть решение принимается не по недоступной API, а по факту: переключились — и проверили результат живыми ответами.
flowchart TD
BKP["/export file=before-043 и backup save"] --> IMP["импорт routeros/05-dns-health-monitor.rsc"]
IMP --> CFG{"probeMode задан?"}
CFG -- "json" --> PUB["публичный DoH: проба по DNS-JSON"]
CFG -- "resolve" --> SELF["свой DoH: проба по качеству ответов"]
PUB --> CHK["проверка: состояние, счётчик, журнал"]
SELF --> CHK
CHK --> RES{"резерв переключается?"}
RES -- "нет" --> DOC["смотреть probeB и адрес резерва"]
RES -- да --> DONE["готово: healthy, попыток 0"]| Что проверяется | В режиме json |
В режиме resolve |
|---|---|---|
| Основной эндпоинт | запрос DNS-JSON, Status и адреса |
качество живых ответов |
| Резервный эндпоинт до переключения | запрос DNS-JSON | не проверяется |
| Резервный эндпоинт после переключения | проверка ответов | проверка ответов через :resolve |
| Признак подмены | substituted в теле ответа |
заглушки в живых ответах |
Замеры и цифры
| Что | Значение | Откуда |
|---|---|---|
| Коммитов между 0.4.2 и 0.4.3 | 1 | заметки к релизу |
| Изменённых файлов | 8 | там же |
| Ассет релиза | routeros-scripts-0.4.3.zip |
страница релиза |
Значений probeMode |
2 (json, resolve) |
routeros/05-dns-health-monitor.rsc на main |
| Запросов пробы резерва в resolve-режиме | 0 | ветка :if ($probeMode = "resolve") в скрипте |
Версия dnsdist на проверенном хосте |
1.7.3 | dnsdist --version на Гжатской Debian |
| Вызов DoH в её конфигурации | addDOHLocal("127.0.0.1:8053", nil, nil, {"/dns-query"}) |
/etc/dnsdist/dnsdist.conf |
| Наблюдение переключения на живом роутере | не удалось | раздел «Verification status» заметок |
Два числа из таблицы требуют пояснения. Ноль запросов пробы резерва в resolve-режиме — это не «проверка отключена», а «проверка перенесена на результат»: монитор узнаёт о неудаче по :resolve после переключения и откатывается, если ответов нет. И версия dnsdist 1.7.3 на моём хосте — это ровно та причина, по которой ловушку из документации про dnsdist 2.x я подтвердить не могу: у меня другая мажорная версия.
Почему это круто
- Убран обходной путь, а не симптом. Вместо «разрешить резерву отвечать по DNS-JSON» проверка просто перестала задавать бессмысленный вопрос.
- Решение принимается по результату. В resolve-режиме важно не то, что эндпоинт открывает порт, а то, что он разрешает имена.
- Подмена ловится и на резерве. В JSON-режиме
substitutedтеперь возможен в обеих ветках, а не только в основной. - Один импорт. Обновление не требует трогать пороги, состояния и обычный DNS-резерв.
- Документация про резерв на отдельном хосте. Это единственный способ проверить failover честно, и он описан вместе с ловушкой.
- Автор не выдаёт непроверенное за проверенное. Раздел про статус проверки прямо говорит: переключение подтверждено разбором кода.
- Совместимость сохранена. Кто не включал
probeMode, ничего не заметит: по умолчанию остаётсяjson. - Одна строка диагностики.
:put $DNSMonitorStateи журнал дают ответ быстрее, чем перебор конфигурации.
Установка и обновление
Если набор уже стоит, меняется только монитор.
/export file=before-043
/system backup save name=before-043
:global DNSMonitorState; :set DNSMonitorState ""
:global DNSFailCount; :set DNSFailCount 0
:global DNSRecoverCount; :set DNSRecoverCount 0
:global DNSFailoverAttempts; :set DNSFailoverAttempts 0
:global DNSMonitorRunning; :set DNSMonitorRunning false
/import file-name=05-dns-health-monitor.rsc
Сброс глобалов — не ритуал: старые значения счётчиков и состояния иначе попадут в новую логику, и первый прогон будет выглядеть странно.
| Шаг | Что делать | Зачем |
|---|---|---|
| 1 | /export file=before-043 |
откат без догадок |
| 2 | импорт монитора | единственный изменённый файл |
| 3 | очистка пяти глобалов | чистое начальное состояние |
| 4 | проверка probeMode в конфигурационном блоке |
режим должен соответствовать типу эндпоинта |
| 5 | первый прогон и чтение журнала | убедиться, что проба отработала |
Проверка после обновления
flowchart TD
Q["запрос имени через текущий резолвер"] --> R{"что вернулось?"}
R -- "ошибка resolve" --> OUT["отказ пути: резолвер недоступен"]
R -- "пара 8.6.112.x или 8.47.69.x" --> FLUSH["сброс кэша и повторная проверка"]
FLUSH --> R2{"после сброса та же пара?"}
R2 -- да --> SUBST["подмена на пути: смена резолвера не поможет"]
R2 -- нет --> CACHE["заглушка была из кэша: путь в порядке"]
R -- "честный адрес" --> OK["ответ честный, состояние healthy"]:put $DNSMonitorState # ожидается healthy
:put $DNSFailoverAttempts # 0, пока отвечает основной
/ip dns cache flush
:put [:resolve example.com] # честный адрес, не пара 8.6.112.x / 8.47.69.x
/log print where message~"STUB"
| Проверка | Ожидание | Что означает отклонение |
|---|---|---|
| Состояние | healthy |
stub — подмена на пути, degraded — не осталось рабочих ступеней |
| Счётчик попыток | 0 |
рост без причины — резерв проверяется неправильно |
:resolve после сброса кэша |
честный адрес | та же пара заглушек — перехват на пути, смена резолвера не поможет |
Журнал по STUB |
пусто | строка с «after a cache flush» означает устойчивую подмену |
| Тело скрипта на роутере | ветка resolve без запроса резерва | старая версия: обновление не применилось |
Отдельно стоит понимать, что проба и отказ — разные вещи. Ответ 400 или 405 на запрос к DNS-JSON означает, что TLS-рукопожатие и проверка сертификата прошли: эндпоинт жив, просто не поддерживает этот API. Именно на этом смешении и вырос дефект, который закрывает 0.4.3.
Совместимость и ограничения
| Что | Значение |
|---|---|
| RouterOS | 7.20+ по заявлению, поведение проверялось на 7.23.1 и 7.24.5 |
| Режим по умолчанию | json, то есть публичные резолверы работают как раньше |
probeMode "resolve" |
обязателен для своего DoH-эндпоинта |
| Резерв на отдельном хосте | описан в документации, вместе с ловушкой dnsdist 2.x |
| Проверка переключения полем | не выполнена, подтверждено разбором кода |
| Обычный DNS как последняя ступень | остался как в 0.4.2, с пометкой о понижении приватности |
Подводные камни
probeModeважнее, чем кажется. Если оставитьjsonпри своём эндпоинте, основной будет считаться мёртвым и роутер уйдёт на публичный резерв — туда, где ответы и подменяются.- Свой эндпоинт отвечает
400на DNS-JSON. Это не отказ и не ошибка настройки; по коду ответа нельзя делать вывод о доступности. - Заглушка выглядит как обычный адрес. Пара
8.6.112.x/8.47.69.xсинтаксически безупречна, поэтому проверять надо содержимое, а не наличие ответа. - Кэш держит заглушку до TTL. После смены резолвера клиенты ещё получают старое; поэтому
STUBставится только после сброса кэша и повторной проверки. - Переключение не наблюдалось в поле. Обещанное поведение подтверждено кодом; при внедрении стоит поднять резерв на отдельном хосте и один раз остановить основной осознанно.
- Резерв нужен со своим корнем доверия. Если резервный хост другой организации, его цепочка сертификатов должна быть в хранилище, иначе
verify-doh-cert=yesотклонит его и переключение закончится откатом. - Ловушка
dnsdist2.x. Документация предупреждает о поведении HTTP/2 в этой ветке; на моём хостеdnsdist1.7.3, поэтому проверить предупреждение я не мог — учитывайте это при выборе версии. - Сброс кэша — не лечение. Он отделяет закэшированную заглушку от подмены на пути; если пара возвращается снова, проблема не в кэше.
Чек-лист обновления
- Снять
/exportи/system backup saveдо импорта. - Забрать
05-dns-health-monitor.rscпо закреплённому тегу или коммиту, а не сmain. - Проверить в файле наличие ветки
probeMode = "resolve"до импорта. - Импортировать монитор и очистить пять глобалов состояния.
- Убедиться, что
probeModeсоответствует типу эндпоинта: свой —resolve, публичный —json. - Проверить
DNSMonitorState,DNSFailoverAttempts,:resolveпосле сброса кэша и журнал поSTUB. - Если failover критичен — один раз проверить переключение, остановив основной эндпоинт осознанно, и записать результат.
Критерий «обновление прижилось»: состояние healthy, счётчик попыток 0, разрешение имён даёт честные адреса, в журнале нет STUB, а в теле скрипта на роутере есть ветка resolve без запроса резерва.
FAQ
Что именно было сломано до 0.4.3?
В resolve-режиме монитор перед переключением спрашивал резервный эндпоинт по DNS-JSON. Свой DoH такой запрос не обслуживает, проба всегда была bad, и переключение не происходило: сеть оставалась на мёртвом апстриме.
Нужно ли что-то менять, если у меня публичный резолвер?
Нет. По умолчанию режим json, а в этом режиме поведение не изменилось — кроме того, что в ответе резерва теперь распознаются адреса-заглушки.
Почему ответ 400 не означает, что эндпоинт недоступен?
Потому что 400 возвращается уже после успешного TLS-рукопожатия и проверки сертификата: соединение установлено, сервер жив, но не поддерживает этот API. О доступности судят по ответам, а не по коду ошибки запроса.
Как проверить, что подмена идёт именно на пути, а не в кэше?
Сбросить кэш и повторить запрос. Если вернулись честные адреса — дело было в кэше; если та же пара заглушек — перехватывает сеть, и смена резолвера не поможет.
Обязательно ли поднимать резерв на отдельном хосте?
Для честной проверки — да: только тогда остановка основного не затрагивает резерв, и видно, работает ли переключение. В документации это описано вместе с ловушкой версии dnsdist.
Можно ли считать версию проверенной, если переключение не наблюдали?
Нет, и автор говорит это прямо: подтверждены ответы резерва, работа списка доступа и честные ответы через основной, а переключение — только разбором кода. Планируя внедрение, исходите из этого.
Итог
0.4.3 — маленький релиз с большим смыслом: он убирает последнее место, где решение принималось по API, которого у своего эндпоинта нет. Вместе с 0.4.2 получается связная картина: проба судит ответы, подмена распознаётся по содержимому, заглушка из кэша отделяется от перехвата на пути, а резерв проверяется результатом переключения. Единственное, что осталось за кадром, — полевое наблюдение самого переключения; если failover для вас критичен, это стоит проверить один раз осознанно, а не надеяться на код.
Цитата: Проверка доступности должна задавать вопрос, на который эндпоинт способен ответить, иначе исправная система выглядит сломанной, а сломанная — исправной.
Связанные материалы
- mikrotik-doh-failover: отказоустойчивый DoH на MikroTik — основная статья продукта: установка, настройка, подводные камни.
- mikrotik-doh-failover v0.4.1: цепочка отказоустойчивости DoH — предыдущий разбор версии: таймаут пробы, третья ступень, возврат из открытого DNS.
- Релиз v0.4.3 — заметки о выпуске и статус проверки.
- CHANGELOG.md — полная история от 0.1.0 до 0.4.3.
- docs/troubleshooting.md §6.13 — подмена ответов DoH на пути и минутная методика различения.
LLM Summary
- Тема: mikrotik-doh-failover v0.4.3 — исправление пробы резервного эндпоинта в resolve-режиме и мелкие улучшения распознавания подмены.
- Что было сломано: в resolve-режиме проба резерва шла по DNS-JSON, свой эндпоинт отвечает
400, проба всегдаbad, переключение не происходило. - Что стало: в resolve-режиме резерв до переключения не опрашивается, переключение проверяется через
:resolveпосле него; в JSON-режиме проба резерва распознаёт адреса-заглушки и возвращаетsubstituted. - Контекст 0.4.2: параметр
probeMode(json/resolve), отказ от проверки только поStatus: 0,STUBтолько после сброса кэша, пакет на PyPI с командамиdoh-zabbix-setupиdoh-zabbix-collector. - Мои замеры: в скрипте на
mainдве веткиprobeMode, в resolve-режиме проба резерва выставляется без сетевого запроса; на Гжатской Debiandnsdist1.7.3 иaddDOHLocal("127.0.0.1:8053", nil, nil, {"/dns-query"}). - Обновление: один импорт
routeros/05-dns-health-monitor.rscплюс очистка пяти глобалов; пороги, состояния и обычный DNS-резерв не меняются. - Проверка:
DNSMonitorState = healthy,DNSFailoverAttempts = 0, честные адреса после сброса кэша, пустой журнал поSTUB. - Ограничения: переключение на живом роутере не наблюдалось (только разбор кода); ловушка
dnsdist2.x не проверена — на моём хосте другая мажорная версия. - Ссылки: https://github.com/paulmann/mikrotik-doh-failover/releases/tag/v0.4.3, https://sysadministrator.ru/mikrotik-doh-failover/
Промт для агента (установка и настройка)
Готовый промт для AI-агента: скопируйте блок целиком кнопкой и отдайте своему ассистенту. Шаги, предусловия и таблица проверок взяты из AGENTS.md репозитория, обновление — из раздела «Upgrade» заметок к релизу 0.4.3.
Задача: обновить mikrotik-doh-failover до 0.4.3 на роутере RouterOS и доказать, что проба резерва работает в выбранном режиме.
Материалы:
- репозиторий: https://github.com/paulmann/mikrotik-doh-failover
- релиз: https://github.com/paulmann/mikrotik-doh-failover/releases/tag/v0.4.3
- контракт исполнителя: https://github.com/paulmann/mikrotik-doh-failover/blob/main/AGENTS.md
- подмена ответов: docs/troubleshooting.md §6.12 и §6.13
Предусловия (провал — остановка):
- RouterOS 7.20 или новее: /system resource print
- время синхронизировано: /system ntp client print → status: synchronized
- device-mode известен: /system device-mode print (на home планировщик и fetch запрещены)
- файл 05-dns-health-monitor.rsc загружен в ФАЙЛЫ роутера
- есть окно обслуживания: /export file=before-doh до первого шага
Обновление:
/export file=before-043
/system backup save name=before-043
:global DNSMonitorState; :set DNSMonitorState ""
:global DNSFailCount; :set DNSFailCount 0
:global DNSRecoverCount; :set DNSRecoverCount 0
:global DNSFailoverAttempts; :set DNSFailoverAttempts 0
:global DNSMonitorRunning; :set DNSMonitorRunning false
/import file-name=05-dns-health-monitor.rsc
Настройка режима:
- свой DoH-эндпоинт: probeMode "resolve" (DNS-JSON он не обслуживает и отвечает 400)
- публичный резолвер: probeMode "json"
Проверка:
:put $DNSMonitorState # healthy
:put $DNSFailoverAttempts # 0, пока отвечает основной
/ip dns cache flush
:put [:resolve example.com] # честный адрес, не 8.6.112.x / 8.47.69.x
/log print where message~"STUB" # пусто на здоровой конфигурации
Запрещено:
- считать ответ 400 или 405 доказательством недоступности: TLS и сертификат при этом прошли;
- оставлять probeMode "json" при своём эндпоинте: основной будет признан мёртвым, роутер уйдёт на публичный резерв;
- объявлять STUB без сброса кэша и повторной проверки;
- утверждать, что переключение проверено, если основной эндпоинт не останавливали: в релизе это подтверждено только разбором кода;
- ставить резерв другого оператора без его корневого сертификата в хранилище доверия.
Что считать успехом: состояние healthy, счётчик попыток 0, честные ответы после сброса кэша, журнал без STUB, в теле скрипта на роутере ветка resolve без запроса резерва.