Zone-o-Matic: как DynDNS-эндпоинт вырос в сторону записи моего DNS
Мой авторитативный DNS проще некуда: CoreDNS на хабах отдаёт обычные
зонные файлы плагином file, который перечитывает их, когда меняется serial
в SOA. Легко читать, легко бэкапить, легко смотреть diff в git.
Zone-o-Matic — небольшой сервис на Go, который пишет в эти файлы. Он не задумывался как большой проект — каждый API в нём появился вместе с новым клиентом, которому нужно было поменять запись.
Исходная задача: роутеры и acme.sh
Август 2025. Часть моих роутеров сидит за динамическими адресами, и мне
хотелось держать их имена в своих зонах, а не у стороннего DynDNS-провайдера.
Эти же роутеры получают TLS-сертификаты через acme.sh, которому нужно
публиковать записи для вызовов dns-01.
Поэтому первая версия говорила на двух уже существующих протоколах:
GET /nic/updateв стиле no-ip, который знают ddns-scripts в OpenWrt и почти любой роутер;- API acme-dns, который
acme.shподдерживает из коробки.
Оба сводятся к «выставь эту запись в том зонном файле».
Продолжение: lego
Ноябрь 2025. Хосты вне OpenWrt — например, S3-шлюз — продлевают
сертификаты через lego. У lego есть
универсальный провайдер httpreq:
два вызова, /present и /cleanup, которые добавляют и удаляют одно значение
вызова. С ними сертификат на имя и его wildcard можно проверять
одновременно.
Продолжение: PowerDNS для Proxmox
Апрель 2026. SDN в Proxmox VE умеет регистрировать виртуалки в DNS, но только через сервер PowerDNS. Вместо того чтобы поднимать PowerDNS рядом с CoreDNS, zoneomatic получил то подмножество API PowerDNS, которое Proxmox реально вызывает.
Продолжение: RFC2136 — из-за cert-manager
Сентябрь 2026. Кластеры Kubernetes выпускают сертификаты через
cert-manager. Встроенного солвера для lego httpreq в cert-manager нет —
пришлось бы ставить отдельный webhook-плагин в каждый кластер. Зато встроен
солвер rfc2136:
обычный DNS UPDATE с аутентификацией через TSIG.
Так zoneomatic научился говорить на DNS UPDATE. Слушателей два: ACME-шный,
который принимает только TXT-записи _acme-challenge, и для полных
обновлений, которым могут пользоваться nsupdate и knsupdate. Теперь
каждому кластеру нужен только ClusterIssuer и TSIG-ключ — никакого
webhook. Примерно тогда же DynDNS-обновления научились синхронизировать
соответствующие PTR-записи в обратных зонах.
Бережно с зонными файлами
При таком количестве писателей зонные файлы всё равно должны выглядеть так, будто их аккуратно правил человек:
- Комментарии сохраняются в любом месте — целые строки, конец записи, внутри скобок SOA или многострочного TXT. Заменённая запись сохраняет свой комментарий, удалённая забирает его с собой.
- Serial в SOA увеличивается — serial в виде Unix-времени становится
текущим временем (и всё равно растёт при нескольких изменениях за секунду),
serial вида
YYYYMMDDnnувеличивается на единицу. - Каждая запись проверяется до того, как попасть на диск. Новый файл заново разбирается независимым парсером (miekg/dns) и должен содержать ровно задуманные записи и все комментарии; иначе обновление отклоняется, а старый файл остаётся. Сама замена атомарна.
- ACME-записи никогда не исчезают. Когда вызова нет, на именах
_acme-challengeстоит заглушка.
Форматтер, pkg/dnsfmt, — отдельный модуль Go, а запись зон покрыта
фаззингом.
Безопасность и тестирование
HTTP-пользователи берутся из htpasswd-файла с bcrypt-хешами; RFC2136 использует TSIG-ключи с закреплённым алгоритмом. TSIG проверяет подлинность запроса, но не шифрует его, поэтому слушатели RFC2136 должны жить на приватном интерфейсе — у меня это VPN mesh — и вдобавок за списком разрешённых адресов. Писать можно только в заранее заданные зонные файлы.
Кроме юнит- и end-to-end-тестов с настоящими nsupdate/knsupdate, цель
make k3d поднимает одноразовый кластер k3d с cert-manager,
Pebble в роли ACME-сервера и CoreDNS
и выпускает настоящие сертификаты через zoneomatic. CI гоняет то же самое.
Для контейнеров есть Dockerfile, а для OpenWrt — пакет в моём фиде — так он и работает рядом с CoreDNS на роутерах.