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

mikrotik-doh-failover v0.4.3 — обложка обзора версии
Резерв проверяется иначе, чем основной: переключение и проверка по живым ответам

TL;DR: что это и как проверить за десять минут

  1. Проба резерва в resolve-режиме больше не выполняется: монитор переключается и проверяет результат через :resolve уже после переключения.
  2. JSON-режим получил детект подмены для резерва: если в ответе резервного эндпоинта видны адреса-заглушки, проба сообщает substituted, а не ok.
  3. Документация дополнена: как поднять резерв на отдельном хосте и какая ловушка есть в dnsdist 2.x с HTTP/2.
  4. Обновление — один импорт routeros/05-dns-health-monitor.rsc; пороги, состояния и обычный DNS-резерв не меняются.
  5. Честная оговорка автора: само переключение на живом роутере наблюдать не удалось — оно подтверждено разбором кода, а не полевым прогоном.
/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» заметок
mikrotik-doh-failover v0.4.3: одно изменение по сути и что осталось непроверенным

Два числа из таблицы требуют пояснения. Ноль запросов пробы резерва в 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 отклонит его и переключение закончится откатом.
  • Ловушка dnsdist 2.x. Документация предупреждает о поведении HTTP/2 в этой ветке; на моём хосте dnsdist 1.7.3, поэтому проверить предупреждение я не мог — учитывайте это при выборе версии.
  • Сброс кэша — не лечение. Он отделяет закэшированную заглушку от подмены на пути; если пара возвращается снова, проблема не в кэше.

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

  1. Снять /export и /system backup save до импорта.
  2. Забрать 05-dns-health-monitor.rsc по закреплённому тегу или коммиту, а не с main.
  3. Проверить в файле наличие ветки probeMode = "resolve" до импорта.
  4. Импортировать монитор и очистить пять глобалов состояния.
  5. Убедиться, что probeMode соответствует типу эндпоинта: свой — resolve, публичный — json.
  6. Проверить DNSMonitorState, DNSFailoverAttempts, :resolve после сброса кэша и журнал по STUB.
  7. Если 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 для вас критичен, это стоит проверить один раз осознанно, а не надеяться на код.

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

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

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-режиме проба резерва выставляется без сетевого запроса; на Гжатской Debian dnsdist 1.7.3 и addDOHLocal("127.0.0.1:8053", nil, nil, {"/dns-query"}).
  • Обновление: один импорт routeros/05-dns-health-monitor.rsc плюс очистка пяти глобалов; пороги, состояния и обычный DNS-резерв не меняются.
  • Проверка: DNSMonitorState = healthy, DNSFailoverAttempts = 0, честные адреса после сброса кэша, пустой журнал по STUB.
  • Ограничения: переключение на живом роутере не наблюдалось (только разбор кода); ловушка dnsdist 2.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 без запроса резерва.