- PVSM.RU - https://www.pvsm.ru -
Сервер стоит дома или в филиале, провайдер время от времени меняет внешний адрес, а домен в зоне .ru обслуживает , Selectel или Yandex Cloud. Популярные DDNS-клиенты эти DNS-хостинги почти не поддерживают, поэтому запись обновляют самописными скриптами на curl. Ломаются такие скрипты молча. Selectel в 2026 году отключил старый DNS API, и скрипты, которые в него ходили, перестали обновлять записи. Я разобрал API этих хостингов, собрал их подводные камни и написал на Go DDNS-демон dnspatch, который их учитывает.
Начну с того, как эту задачу решают сейчас: почему самый частый совет, Cloudflare, подходит российской аудитории с оговорками, что умеют готовые DDNS-клиенты и где ломаются самописные скрипты. Затем разберу API
Для self-hosted обычно советуют перенести зону в Cloudflare и поставить favonia/cloudflare-ddns [2] (⭐ 2,9 тыс.) или timothymiller/cloudflare-ddns [3] (⭐ 4,5 тыс.). Оба работают только с Cloudflare. Для сервиса с российской аудиторией у этого совета появились оговорки:
в ноябре 2024 года ЦМУ ССОП (Центр мониторинга и управления сетью связи общего пользования, подведомственный Роскомнадзору) рекомендовал владельцам сайтов отключить TLS ECH или перейти на отечественные CDN [1];
с 9 июня 2025 года российские провайдеры замедляют соединения с ресурсами за Cloudflare, и пользователь получает только первые 16 КБ ответа [2];
с 1 сентября 2026 года по 569-ФЗ делегировать домен в зонах .ru, .рф и .su и менять его NS можно только после идентификации администратора через Госуслуги [3].
Замедление касается соединений с серверами Cloudflare, через которые идёт трафик сайта. Если в Cloudflare лежит только DNS, а запись указывает прямо на ваш адрес, трафик через Cloudflare не идёт. Но если зона уже обслуживается у регистратора, где куплен домен, например у
Я проверил известные DDNS-клиенты на поддержку российских DNS-хостингов. Звёзды указаны на 24 сентября 2026 года.
|
Клиент |
Язык |
⭐ |
Российские DNS-хостинги |
|---|---|---|---|
|
jeessy2/ddns-go [4] |
Go |
17 366 |
нет; около 30 провайдеров, в основном китайские |
|
ddclient [5] |
Perl |
3 553 |
устаревший API Яндекс PDD без AAAA [4]; пример для NIC.RU есть только в ветке main, в релизе 4.0.0 его нет |
|
Go |
3 218 |
нет среди 59 провайдеров |
|
|
TimothyYe/godns [7] |
Go |
1 779 |
нет |
|
troglobit/inadyn [8] |
C |
1 152 |
Яндекс PDD; репозиторий в архиве с октября 2025 года |
|
Go |
370 |
Selectel, Timeweb и NIC.RU через libdns, но только как модуль Caddy |
|
|
OpenWrt ddns-scripts [10] |
shell |
— |
Beget |
|
C# |
2 |
Beget и Timeweb, только A |
У dddns-updater есть провайдер service/nop, то есть проверку доступа к API, и запись не меняет [5].
Дальше идут одиночные скрипты. Я нашёл на GitHub 13 репозиториев под
REG.API 2 отвечает только на запросы с IP-адресов, перечисленных в настройках безопасности аккаунта [7]. А DDNS нужен как раз потому, что адрес меняется. Остаётся ходить в API через небольшой 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 игнорируют. Иначе сервис вернул бы адрес
$ REGRU_PASSWORD=x PROXY_URL='sock5://user:s3cret@203.0.113.5:1080' dnspatch --config dnspatch.toml --check-config
dnspatch: 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.API. Ответ 307 повторил бы такой запрос вместе с паролем по адресу, который назвал сервер, поэтому провайдеры dnspatch редиректам не следуют.
Selectel выводил старый DNS-хостинг из работы по этапам [8]. Создавать в нём домены нельзя с 1 марта 2025 года, редактировать записи нельзя с 1 февраля 2026-го. API domains/v1 отключён 1 мая 2026-го, а NS-серверы старого api.selectel.ru/domains/v1/. Кто поставил такой скрипт в cron и забыл, с февраля остался без обновлений DNS, и cron об этом никому не сообщил.
Новый API называется domains/v2, авторизация в нём идёт через Keystone (OpenStack Identity). Для dnspatch нужен сервисный пользователь с доступом к проекту, в котором лежит зона:
[provider.selectel]
type = "selectel"
account_id = "123456" # номер аккаунта в панели Selectel
username = "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 секунд), существующая сохраняет свой.
Для 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 три сущности:
retriever узнаёт текущий публичный адрес;
provider записывает адрес в DNS;
instance связывает один или несколько retriever’ов с одним или несколькими provider’ами и опрашивает их со своим интервалом.
Instance’ы работают независимо, так что в одном процессе можно вести несколько сайтов и зоны у разных хостингов. Вот типичный конфиг. В нём два источника IPv4, из которых второй запасной, записи home и *.home в
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-config
dnspatch: 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 его переопределяют. Так один аккаунт 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" на Windows
family = "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 required
unknown 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 .env
chmod 644 dnspatch.toml # контейнер работает под uid 65532 и должен прочитать файл
# compose.yml
services:
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 -d
docker compose logs -f
Для proxy, как выше. Проверить конфиг до запуска можно тем же образом, командой docker compose run --rm dnspatch --check-config.
Увеличение количества поддерживаемых провайдеров.
Уведомления о смене адреса и сбоях.
Health-check и пинги мониторинга
Рассматривается вопрос о необходимости UI, но точно буду делать его опциональным
Узнать адрес и отправить его в API несложно. Сложнее учесть особенности конкретного API.
Код dnspatch открыт под лицензией MIT и лежит на GitHub: https://github.com/KrimsN/dnspatch [12]. Если что-то работает не так или вашего
Если какую-то часть стоит разобрать подробнее, напишите в комментариях, что именно интересно.
CNews, 7 ноября 2024 года. Роскомнадзор рекомендует заменить инструмент шифрования компании Cloudflare на российские аналоги [14]
Блог Cloudflare, 26 июня 2025 года. Russian Internet users are unable to access the open Internet [15]
vc.ru. Обязательная идентификация доменов через «Госуслуги»: что изменится с 1 сентября [16]
ddclient, issue #954. Migrate ‘yandex’ protocol from LegacyProtocol to Protocol (add AAAA support) [17]
sfadeev/dddns-updater, код провайдера
qdm12/ddns-updater, issue #1153. Feature request: Add support for Timeweb DNS provider [19]
Справка
Документация Selectel. General information about DNS hosting [21]
tonytkachenko/selectel-ddns, адрес API в примере конфига. .env.sample [22]
Автор: KrimsN
Источник [23]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/dns-2/459273
Ссылки в тексте:
[1] REG.RU: https://www.reg.ru/?rlink=reflink-717
[2] favonia/cloudflare-ddns: https://github.com/favonia/cloudflare-ddns
[3] timothymiller/cloudflare-ddns: https://github.com/timothymiller/cloudflare-ddns
[4] jeessy2/ddns-go: https://github.com/jeessy2/ddns-go
[5] ddclient: https://github.com/ddclient/ddclient
[6] qdm12/ddns-updater: https://github.com/qdm12/ddns-updater
[7] TimothyYe/godns: https://github.com/TimothyYe/godns
[8] troglobit/inadyn: https://github.com/troglobit/inadyn
[9] mholt/caddy-dynamicdns: https://github.com/mholt/caddy-dynamicdns
[10] OpenWrt ddns-scripts: https://github.com/openwrt/packages/tree/master/net/ddns-scripts
[11] sfadeev/dddns-updater: https://github.com/sfadeev/dddns-updater
[12] https://github.com/KrimsN/dnspatch: https://github.com/KrimsN/dnspatch
[13] issues: https://github.com/KrimsN/dnspatch/issues
[14] Роскомнадзор рекомендует заменить инструмент шифрования компании Cloudflare на российские аналоги: https://www.cnews.ru/news/top/2024-11-07_dochka_roskomnadzora_porekomendovala
[15] Russian Internet users are unable to access the open Internet: https://blog.cloudflare.com/russian-internet-users-are-unable-to-access-the-open-internet/
[16] Обязательная идентификация доменов через «Госуслуги»: что изменится с 1 сентября: https://vc.ru/legal/2923008-obyazatelnaya-identifikatsiya-domenov-cherez-gosuslugi
[17] Migrate ‘yandex’ protocol from LegacyProtocol to Protocol (add AAAA support): https://github.com/ddclient/ddclient/issues/954
[18] RegRuDnsProvider.cs: https://github.com/sfadeev/dddns-updater/blob/master/DnsUpdater/Services/DnsProviders/RegRuDnsProvider.cs
[19] Feature request: Add support for Timeweb DNS provider: https://github.com/qdm12/ddns-updater/issues/1153
[20] Какие ограничения есть при работе с Рег.API: https://help.reg.ru/support/partneram/reg-api/kakiye-ogranicheniya-yest-pri-rabote-s-reg-api
[21] General information about DNS hosting: https://docs.selectel.ru/en/dns-hosting/about-dns/
[22] .env.sample: https://github.com/tonytkachenko/selectel-ddns/blob/main/.env.sample
[23] Источник: https://habr.com/ru/articles/1089494/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1089494
Нажмите здесь для печати.