- PVSM.RU - https://www.pvsm.ru -

DDNS для российских DNS-хостингов: почему не подошли готовые клиенты и что я написал взамен

Сервер стоит дома или в филиале, провайдер время от времени меняет внешний адрес, а домен в зоне .ru обслуживает REG.RU [1], Selectel или Yandex Cloud. Популярные DDNS-клиенты эти DNS-хостинги почти не поддерживают, поэтому запись обновляют самописными скриптами на curl. Ломаются такие скрипты молча. Selectel в 2026 году отключил старый DNS API, и скрипты, которые в него ходили, перестали обновлять записи. Я разобрал API этих хостингов, собрал их подводные камни и написал на Go DDNS-демон dnspatch, который их учитывает.

Начну с того, как эту задачу решают сейчас: почему самый частый совет, Cloudflare, подходит российской аудитории с оговорками, что умеют готовые DDNS-клиенты и где ломаются самописные скрипты. Затем разберу API REG.RU [1], Selectel и Yandex Cloud и их подводные камни. В конце расскажу, как устроен dnspatch, почему он устроен именно так и как запустить его за пару минут.

Почему не Cloudflare

Для 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 не идёт. Но если зона уже обслуживается у регистратора, где куплен домен, например у REG.RU [1], перенос ради DDNS-клиента означает смену NS со всеми новыми формальностями. Проще взять клиент, который работает с DNS-хостингом, где зона уже лежит.

Что уже есть

Я проверил известные 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 его нет

qdm12/ddns-updater [6]

Go

3 218

нет среди 59 провайдеров

TimothyYe/godns [7]

Go

1 779

нет

troglobit/inadyn [8]

C

1 152

Яндекс PDD; репозиторий в архиве с октября 2025 года

mholt/caddy-dynamicdns [9]

Go

370

Selectel, Timeweb и NIC.RU через libdns, но только как модуль Caddy

OpenWrt ddns-scripts [10]

shell

—

Beget

sfadeev/dddns-updater [11]

C#

2

Beget и Timeweb, только A

У dddns-updater есть провайдер REG.RU [1], но на 24 сентября его метод обновления вызывает только service/nop, то есть проверку доступа к API, и запись не меняет [5].

Дальше идут одиночные скрипты. Я нашёл на GitHub 13 репозиториев под REG.RU [1], Selectel, Beget, Timeweb и Yandex Cloud, у каждого от 0 до 5 звёзд. В трекерах крупных клиентов нашёлся только один запрос на такие хостинги, про Timeweb в ddns-updater [6], и тот без реакций. Спрос есть, но он раздроблен, и каждый пишет себе свой скрипт.

Подводные камни API

REG.RU пускает в API только с разрешённых адресов

REG.API 2 отвечает только на запросы с IP-адресов, перечисленных в настройках безопасности аккаунта [7]. А DDNS нужен как раз потому, что адрес меняется. Остаётся ходить в API через небольшой VPS [1] со статическим адресом и разрешить в REG.RU [1] только его. Для этого у провайдеров 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 [1], и в DNS попал бы он. Логин и пароль из URL прокси dnspatch не печатает ни в логах, ни в ошибках. Вот что будет, если ошибиться в схеме прокси:

$ 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.RU [1] он один на всю зону.

Пароль уходит в теле каждого запроса к REG.API. Ответ 307 повторил бы такой запрос вместе с паролем по адресу, который назвал сервер, поэтому провайдеры dnspatch редиректам не следуют.

Selectel отключил старый API

Selectel выводил старый DNS-хостинг из работы по этапам [8]. Создавать в нём домены нельзя с 1 марта 2025 года, редактировать записи нельзя с 1 февраля 2026-го. API domains/v1 отключён 1 мая 2026-го, а NS-серверы старого хостинга [1] выключены 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"                 # номер аккаунта в панели 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 секунд), существующая сохраняет свой.

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 [1] через прокси и корень другой зоны в 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-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 его переопределяют. Так один аккаунт REG.RU [1] обслуживает и 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

Для REG.RU [1] не забудьте разрешить в настройках API адрес, с которого пойдут запросы, или добавить proxy, как выше. Проверить конфиг до запуска можно тем же образом, командой docker compose run --rm dnspatch --check-config.

Что дальше

  • Увеличение количества поддерживаемых провайдеров.

  • Уведомления о смене адреса и сбоях.

  • Health-check и пинги мониторинга

  • Рассматривается вопрос о необходимости UI, но точно буду делать его опциональным

Итоги

Узнать адрес и отправить его в API несложно. Сложнее учесть особенности конкретного API. REG.RU [1] принимает запросы только с разрешённых адресов, Selectel выключил старый API, а Yandex Cloud требует цепочку из ключа, JWT и IAM-токена. Самописный скрипт узнаёт о таких вещах, только когда ломается, и ломается молча.

Код dnspatch открыт под лицензией MIT и лежит на GitHub: https://github.com/KrimsN/dnspatch [12]. Если что-то работает не так или вашего хостинга [1] нет в списке, напишите в issues [13]. А если всё работает, буду рад звёздочке на GitHub.

Если какую-то часть стоит разобрать подробнее, напишите в комментариях, что именно интересно.

Источники

  1. CNews, 7 ноября 2024 года. Роскомнадзор рекомендует заменить инструмент шифрования компании Cloudflare на российские аналоги [14]

  2. Блог Cloudflare, 26 июня 2025 года. Russian Internet users are unable to access the open Internet [15]

  3. vc.ru. Обязательная идентификация доменов через «Госуслуги»: что изменится с 1 сентября [16]

  4. ddclient, issue #954. Migrate ‘yandex’ protocol from LegacyProtocol to Protocol (add AAAA support) [17]

  5. sfadeev/dddns-updater, код провайдера REG.RU [1]. RegRuDnsProvider.cs [18]

  6. qdm12/ddns-updater, issue #1153. Feature request: Add support for Timeweb DNS provider [19]

  7. Справка REG.RU [1]. Какие ограничения есть при работе с Рег.API [20]

  8. Документация Selectel. General information about DNS hosting [21]

  9. 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