Сервер стоит дома или в филиале, провайдер время от времени меняет внешний адрес, а домен в зоне .ru обслуживает REG.RU, Selectel или Yandex Cloud. Популярные DDNS-клиенты эти DNS-хостинги почти не поддерживают, поэтому запись обновляют самописными скриптами на curl. Ломаются такие скрипты молча. Selectel в 2026 году отключил старый DNS API, и скрипты, которые в него ходили, перестали обновлять записи. Я разобрал API этих хостингов, собрал их подводные камни и написал на Go DDNS-демон dnspatch, который их учитывает.
Начну с того, как эту задачу решают сейчас: почему самый частый совет, Cloudflare, подходит российской аудитории с оговорками, что умеют готовые DDNS-клиенты и где ломаются самописные скрипты. Затем разберу API REG.RU, Selectel и Yandex Cloud и их подводные камни. В конце расскажу, как устроен dnspatch, почему он устроен именно так и как запустить его за пару минут.
Почему не Cloudflare
Для self-hosted обычно советуют перенести зону в Cloudflare и поставить favonia/cloudflare-ddns (⭐ 2,9 тыс.) или timothymiller/cloudflare-ddns (⭐ 4,5 тыс.). Оба работают только с Cloudflare. Для сервиса с российской аудиторией у этого совета появились оговорки:
-
в ноябре 2024 года ЦМУ ССОП (Центр мониторинга и управления сетью связи общего пользования, подведомственный Роскомнадзору) рекомендовал владельцам сайтов отключить TLS ECH или перейти на отечественные CDN [1];
-
с 9 июня 2025 года российские провайдеры замедляют соединения с ресурсами за Cloudflare, и пользователь получает только первые 16 КБ ответа [2];
-
с 1 сентября 2026 года по 569-ФЗ делегировать домен в зонах .ru, .рф и .su и менять его NS можно только после идентификации администратора через Госуслуги [3].
Замедление касается соединений с серверами Cloudflare, через которые идёт трафик сайта. Если в Cloudflare лежит только DNS, а запись указывает прямо на ваш адрес, трафик через Cloudflare не идёт. Но если зона уже обслуживается у регистратора, где куплен домен, например у REG.RU, перенос ради DDNS-клиента означает смену NS со всеми новыми формальностями. Проще взять клиент, который работает с DNS-хостингом, где зона уже лежит.
Что уже есть
Я проверил известные DDNS-клиенты на поддержку российских DNS-хостингов. Звёзды указаны на 24 сентября 2026 года.
|
Клиент |
Язык |
⭐ |
Российские DNS-хостинги |
|---|---|---|---|
|
Go |
17 366 |
нет; около 30 провайдеров, в основном китайские |
|
|
Perl |
3 553 |
устаревший API Яндекс PDD без AAAA [4]; пример для NIC.RU есть только в ветке main, в релизе 4.0.0 его нет |
|
|
Go |
3 218 |
нет среди 59 провайдеров |
|
|
Go |
1 779 |
нет |
|
|
C |
1 152 |
Яндекс PDD; репозиторий в архиве с октября 2025 года |
|
|
Go |
370 |
Selectel, Timeweb и NIC.RU через libdns, но только как модуль Caddy |
|
|
shell |
— |
Beget |
|
|
C# |
2 |
Beget и Timeweb, только A |
У dddns-updater есть провайдер REG.RU, но на 24 сентября его метод обновления вызывает только service/nop, то есть проверку доступа к API, и запись не меняет [5].
Дальше идут одиночные скрипты. Я нашёл на GitHub 13 репозиториев под REG.RU, Selectel, Beget, Timeweb и Yandex Cloud, у каждого от 0 до 5 звёзд. В трекерах крупных клиентов нашёлся только один запрос на такие хостинги, про Timeweb в ddns-updater [6], и тот без реакций. Спрос есть, но он раздроблен, и каждый пишет себе свой скрипт.
Подводные камни API
REG.RU пускает в API только с разрешённых адресов
REG.API 2 отвечает только на запросы с IP-адресов, перечисленных в настройках безопасности аккаунта [7]. А DDNS нужен как раз потому, что адрес меняется. Остаётся ходить в API через небольшой VPS со статическим адресом и разрешить в REG.RU только его. Для этого у провайдеров dnspatch есть параметр proxy:
[[instance.provider]]type = "regru"username = "my-login"password = "${REGRU_PASSWORD}"zone = "example.ru"rr_name = "home"proxy = "${PROXY_URL}" # например, socks5://user:pass@203.0.113.5:1080
Поддерживаются схемы socks5, socks5h, http и https. Retriever’ы, которые узнают адрес, по умолчанию ходят напрямую и переменные HTTP_PROXY/HTTPS_PROXY игнорируют. Иначе сервис вернул бы адрес VPS, и в DNS попал бы он. Логин и пароль из URL прокси dnspatch не печатает ни в логах, ни в ошибках. Вот что будет, если ошибиться в схеме прокси:
$ REGRU_PASSWORD=x PROXY_URL='sock5://user:s3cret@203.0.113.5:1080' dnspatch --config dnspatch.toml --check-configdnspatch: error: instance "home": provider "regru": proxy: scheme must be one of socks5, socks5h, http or https, or the word "direct" for no proxy
Второй камень в том, что в API нет метода «изменить запись». dnspatch читает записи зоны (zone/get_resource_records), добавляет новую (zone/add_alias для A, zone/add_aaaa для AAAA) и только потом удаляет старую (zone/remove_record). Порядок выбран так, чтобы имя ни в какой момент не осталось без адреса. Если процесс упадёт между добавлением и удалением, в зоне окажутся две записи. На следующем цикле dnspatch найдёт среди них нужную и удалит остальные. Если же записей несколько и ни одна не совпадает с текущим адресом, dnspatch ничего не трогает и пишет ошибку. Такое состояние создал не он, и угадывать, какую запись заменить, он не станет. TTL через API не задаётся, в REG.RU он один на всю зону.
Пароль уходит в теле каждого запроса к REG.API. Ответ 307 повторил бы такой запрос вместе с паролем по адресу, который назвал сервер, поэтому провайдеры dnspatch редиректам не следуют.
Selectel отключил старый API
Selectel выводил старый DNS-хостинг из работы по этапам [8]. Создавать в нём домены нельзя с 1 марта 2025 года, редактировать записи нельзя с 1 февраля 2026-го. API domains/v1 отключён 1 мая 2026-го, а NS-серверы старого хостинга выключены 1 августа 2026 года. Скрипт tonytkachenko/selectel-ddns [9] ходит как раз в api.selectel.ru/domains/v1/. Кто поставил такой скрипт в cron и забыл, с февраля остался без обновлений DNS, и cron об этом никому не сообщил.
Новый API называется domains/v2, авторизация в нём идёт через Keystone (OpenStack Identity). Для dnspatch нужен сервисный пользователь с доступом к проекту, в котором лежит зона:
[provider.selectel]type = "selectel"account_id = "123456" # номер аккаунта в панели Selectelusername = "dnspatch" # сервисный пользовательpassword = "${SELECTEL_PASSWORD}"project_name = "my-project"zone = "example.ru"rr_name = "home"
dnspatch получает токен запросом POST /identity/v3/auth/tokens (сам токен приходит в заголовке X-Subject-Token), кеширует и перевыпускает за минуту до истечения. На 401 он получает новый токен и повторяет запрос один раз. Все записи одного имени и типа в Selectel хранятся одним объектом rrset, и его содержимое заменяется одним запросом. Поэтому промежуточного состояния, когда в DNS два адреса или ни одного, здесь нет. TTL задаётся для новой записи (по умолчанию 60 секунд), существующая сохраняет свой.
Yandex Cloud и цепочка из ключа, JWT и IAM-токена
Для Cloud DNS нужен сервисный аккаунт с ролью dns.editor на каталог зоны и его авторизованный ключ в виде JSON-файла, который создаёт yc iam key create. Из ключа dnspatch собирает JWT, подписанный PS256, со сроком жизни в час (больше IAM не принимает), обменивает его на IAM-токен в iam.api.cloud.yandex.net/iam/v1/tokens и кеширует токен до истечения. SDK для этого не нужен, хватает стандартной библиотеки:
signing := enc.EncodeToString(header) + "." + enc.EncodeToString(claims)sum := sha256.Sum256([]byte(signing))sig, err := rsa.SignPSS(rand.Reader, p.key, crypto.SHA256, sum[:],&rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash})if err != nil {return "", fmt.Errorf("signing JWT: %w", err)}return signing + "." + enc.EncodeToString(sig), nil
У dnspatch всего две прямые зависимости, BurntSushi/toml для конфига и miekg/dns для работы с DNS. Сам ключ удобно передать файлом через key = "${file:/run/secrets/yc_key.json}".
В самом DNS API нашлись две мелочи. TTL в ответе приходит строкой, потому что 64-битные числа API кодирует в JSON как строки, и поле типа int на таком ответе падает с ошибкой разбора. dnspatch читает TTL как json.Number, который принимает обе формы. Запись меняется методом upsertRecordSets, а он возвращает не результат, а long-running operation. Если операция сразу пришла с ошибкой, dnspatch это заметит, а операцию, которая ещё выполняется, он считает принятой.
Как устроен dnspatch
В dnspatch три сущности:
-
retriever узнаёт текущий публичный адрес;
-
provider записывает адрес в DNS;
-
instance связывает один или несколько retriever’ов с одним или несколькими provider’ами и опрашивает их со своим интервалом.
Instance’ы работают независимо, так что в одном процессе можно вести несколько сайтов и зоны у разных хостингов. Вот типичный конфиг. В нём два источника IPv4, из которых второй запасной, записи home и *.home в REG.RU через прокси и корень другой зоны в Yandex Cloud:
interval = "5m"[retriever.v4]type = "ipify"family = "ipv4"[retriever.v4-reserve]type = "icanhazip"family = "ipv4"[provider.regru]type = "regru"username = "my-login"password = "${REGRU_PASSWORD}"zone = "example.ru"rr_name = "home"proxy = "${PROXY_URL}"[provider.yandexcloud]type = "yandexcloud"key = "${file:./secrets/yc_key.json}"zone_id = "dns1234567890abcdef"rr_name = "@"[[instance]]name = "home"[[instance.retriever]]ref = "v4"[[instance.retriever]]ref = "v4-reserve"[[instance.provider]]ref = "regru"[[instance.provider]]ref = "regru"rr_name = "*.home"[[instance.provider]]ref = "yandexcloud"
$ REGRU_PASSWORD=x PROXY_URL='socks5://user:pass@203.0.113.5:1080' dnspatch --config dnspatch.toml --check-configdnspatch: config OK: dnspatch.toml (1 instance(s)) home: interval=5m0s retrievers=[v4(ipv4), v4-reserve(ipv4)] providers=[regru(rr_name=home), regru(rr_name=*.home), yandexcloud]
ref ссылается на определение, а параметры рядом с ref его переопределяют. Так один аккаунт REG.RU обслуживает и home, и *.home. icanhazip вызывается, только если ipify не ответил.
Два стека и запасные источники
Retriever’ы instance’а опрашиваются по порядку, пока не заполнится каждое семейство адресов. Какое семейство вернул retriever, dnspatch определяет по самому адресу, а не по настройкам. Для A и AAAA в одном instance’е хватит одного retriever’а с family = "dual" (его умеют icanhazip, identme, ifconfigco, ipify и interface) или двух, по одному на IPv4 и IPv6.
Retriever interface берёт адрес с сетевого интерфейса, без внешнего сервиса. Он нужен в первую очередь для IPv6. Когда провайдер делегирует префикс (DHCPv6-PD), адрес из него уже висит на интерфейсе, и спрашивать о нём внешний сервис незачем. Учитываются только публичные адреса, а loopback, link-local, частные сети, ULA (fc00::/7) и CGNAT (100.64.0.0/10) пропускаются. Если публичных несколько, берётся наименьший, чтобы выбор не прыгал между циклами. Сузить выбор можно префиксом:
[retriever.lan]type = "interface"name = "eth0" # "wan" или "br-lan" на OpenWrt, "Ethernet" на Windowsfamily = "ipv6"network = "2001:db8:1234::/64" # только адреса из этого префикса
Ответ внешнего сервиса dnspatch разбирает через netip.ParseAddr и проверяет семейство. HTML-страница ошибки, пустой ответ, loopback или адрес не того семейства становятся ошибкой retriever’а и в DNS не попадают.
Строгий конфиг
Ошибки в параметрах выводятся все сразу, а к неизвестному параметру dnspatch подсказывает ближайшее имя. Возьмём конфиг из быстрого старта (он в конце статьи) и сделаем в нём две опечатки, family = "ip4" у retriever’а и pasword вместо password:
$ REGRU_PASSWORD=x dnspatch --config typo.toml --check-config; echo "exit=$?"dnspatch: error: instance "home": retriever "ipify": family: must be "ipv4", "ipv6" or "dual", got "ip4"instance "home": provider "regru": parameter "password" is requiredunknown parameter "pasword" (did you mean "password"?)exit=2
Незаданную переменную окружения dnspatch тоже считает ошибкой и не подставляет вместо неё пустую строку. Иначе демон с пустым паролем стартовал бы и на каждом цикле получал отказ авторизации. О такой ошибке dnspatch сообщает первой, а остальные покажет, когда переменная появится. --check-config проверяет конфиг и выходит с кодом 0 или 2, не запуская демон. Его удобно поставить в ExecStartPre у systemd или в CI. В репозитории CI так проверяет все примеры из examples/.
Секреты
${NAME} подставляет переменную окружения, а ${file:/run/secrets/...} подставляет содержимое файла без последнего перевода строки. Второй вариант нужен потому, что значения из .env становятся переменными окружения контейнера, а их покажет docker inspect любому, у кого есть доступ к Docker-демону. Secrets в Docker и Kubernetes монтируются файлами и в окружение не попадают.
Сбои
-
Provider’ы одного instance’а обновляются независимо, и упавший не мешает остальным. На каждый запрос адреса и каждую запись отводится 30 секунд.
-
Provider получает запрос, только если адрес отличается от последнего, который он принял.
-
Упавший provider повторяется с экспоненциальным backoff и jitter. Первая пауза длится от половины интервала до целого интервала, дальше потолок удваивается с каждой ошибкой до 30 минут (или до интервала, если он длиннее). Jitter разводит во времени instance’ы, которые стартовали вместе.
Последний записанный адрес хранится только в памяти. После перезапуска первый цикл записывает текущий адрес во все provider’ы, даже если он там уже есть. Это один лишний вызов каждого provider’а за перезапуск. Файл состояния я делать не стал. В контейнере без volume он терялся бы при каждом перезапуске, то есть как раз там, где был бы нужен, а ещё потребовал бы думать о пути, правах и устаревших данных. Запись у provider’ов идемпотентна, и если адрес уже верный, в зоне ничего не меняется.
Быстрый старт
# dnspatch.toml[[instance]]name = "home"[[instance.retriever]]type = "ipify"[[instance.provider]]type = "regru"username = "my-login"password = "${REGRU_PASSWORD}"zone = "example.ru"rr_name = "home"
echo 'REGRU_PASSWORD=...' > .env && chmod 600 .envchmod 644 dnspatch.toml # контейнер работает под uid 65532 и должен прочитать файл
# compose.ymlservices: dnspatch: image: krimsn/dnspatch:0.3.0 restart: unless-stopped env_file: .env volumes: - ./dnspatch.toml:/etc/dnspatch/config.toml:ro logging: driver: json-file options: {max-size: "10m", max-file: "3"}
docker compose up -ddocker compose logs -f
Для REG.RU не забудьте разрешить в настройках API адрес, с которого пойдут запросы, или добавить proxy, как выше. Проверить конфиг до запуска можно тем же образом, командой docker compose run --rm dnspatch --check-config.
Что дальше
-
Увеличение количества поддерживаемых провайдеров.
-
Уведомления о смене адреса и сбоях.
-
Health-check и пинги мониторинга
-
Рассматривается вопрос о необходимости UI, но точно буду делать его опциональным
Итоги
Узнать адрес и отправить его в API несложно. Сложнее учесть особенности конкретного API. REG.RU принимает запросы только с разрешённых адресов, Selectel выключил старый API, а Yandex Cloud требует цепочку из ключа, JWT и IAM-токена. Самописный скрипт узнаёт о таких вещах, только когда ломается, и ломается молча.
Код dnspatch открыт под лицензией MIT и лежит на GitHub: https://github.com/KrimsN/dnspatch. Если что-то работает не так или вашего хостинга нет в списке, напишите в issues. А если всё работает, буду рад звёздочке на GitHub.
Если какую-то часть стоит разобрать подробнее, напишите в комментариях, что именно интересно.
Источники
-
CNews, 7 ноября 2024 года. Роскомнадзор рекомендует заменить инструмент шифрования компании Cloudflare на российские аналоги
-
Блог Cloudflare, 26 июня 2025 года. Russian Internet users are unable to access the open Internet
-
vc.ru. Обязательная идентификация доменов через «Госуслуги»: что изменится с 1 сентября
-
ddclient, issue #954. Migrate ‘yandex’ protocol from LegacyProtocol to Protocol (add AAAA support)
-
sfadeev/dddns-updater, код провайдера REG.RU. RegRuDnsProvider.cs
-
qdm12/ddns-updater, issue #1153. Feature request: Add support for Timeweb DNS provider
-
Справка REG.RU. Какие ограничения есть при работе с Рег.API
-
Документация Selectel. General information about DNS hosting
-
tonytkachenko/selectel-ddns, адрес API в примере конфига. .env.sample
ссылка на оригинал статьи https://habr.com/ru/articles/1089494/