DNS у Beget: намерение и факт — разные вещи
Четыре грабли, найденные 13.09.2026 на живой задаче (перевод поддомена с
shared-хостинга на VPS). Общее у них одно: панель и dns/getData показывают, что
собирались сделать, а не то, что отдаётся миру.
Поддомена нет — это лечится domain_add_subdomain, а не записью зоны
У Beget поддомен — отдельная сущность, а не запись в зоне родителя. Пока её нет,
dns/getData по FQDN отвечает METHOD_FAILED, и любая запись в это имя обязана
остановиться: read-merge-write без прочитанной зоны превратился бы в
changeRecords по пустому набору, то есть в стирание.
Прежний текст ошибки (NO_ZONE_DATA) советовал единственный обход —
dns_set_records с replace_all=True. Для самой частой причины совет неверен и
опасен: сущность так не создаётся, а слепая запись зоны — ровно то, от чего
защита и стоит.
Теперь _get_result() различает два случая:
- родительская зона читается →
SUBDOMAIN_NOT_CREATEDс готовой командойdomain_add_subdomain(subdomain='bsapp', domain_id=7684388);domain_idберётся изdomain/getListсопоставлением поfqdn, и вdetails.fixлежит машиночитаемая версия того же; - зона не читается по другой причине → прежний
NO_ZONE_DATAс прежним текстом.
Детект работает на всех путях записи: dns_set_* и dns_patch_record ходят
через один _get_result().
Создание поддомена НЕ создаёт пустую сущность
domain/addSubdomainVirtual возвращает только id, и по ответу кажется, что
записей нет. На деле Beget тут же провижинит свой набор:
A -> 87.236.16.28 (shared-хостинг Beget)
TXT -> v=spf1 redirect=beget.com
MX -> mx1.beget.com (10), mx2.beget.com (20)
Два следствия:
- имя начинает резолвиться на shared-хостинг до вашей
dns_set_a. На домене, который уже кому-то отдан, это видимая подмена, а не пауза; MXиSPFпереживаютdns_set_a(тот мерджит зону, а не заменяет) и живут дальше молча — для поддомена под API это мусор, а для почты работающий приём писем на адрес, о котором владелец не знает.
domain_add_subdomain после создания читает зону и отдаёт фактические записи в
поле records, плюс note с этим предупреждением. Провал чтения не делает вызов
неуспешным: поддомен уже создан, причина уходит в records_error.
Catch-all зоны невидим для dns/getData
dns_get('itrobot.ru') отдаёт A -> 185.137.235.2 и никакого wildcard. При этом
любое несуществующее имя в зоне отвечает, и не адресом корня:
nslookup zzz-not-exist-test.itrobot.ru -> 87.236.16.28
nslookup itrobot.ru -> 185.137.235.2
Прочитав зону, агент делает вывод «записи нет, имя свободно». Вывод неверен: имя отвечает чужим сервером. Цена ошибки конкретна — запуск certbot на имя, которое на самом деле смотрит на stage-хостинг: webroot-проверка уходит не на тот сервер, попытка сгорает и приближает лимит Let’s Encrypt на домен.
Поэтому dns_get рядом с записями зоны отдаёт поле effective — фактический
ответ авторитативного NS по запрошенному FQDN, спрошенный напрямую по UDP
(src/mcp_beget/dnsprobe.py). Отключается параметром check_effective=False.
success от dns_set_* означает «принято панелью»
dns_set_a вернул success, dns_get подтвердил новое значение — и это ничего не
говорит о мире: dns_get читает тот же источник, куда писал. Авторитативный NS
в тот же момент отдавал старое значение, совпало через несколько минут.
- все
dns_set_*теперь всегда возвращают предупреждение о задержке распространения; dns_verify(fqdn, expect, type, timeout)опрашивает авторитативные NS зоны до совпадения или до дедлайна и возвращает факт.
Вручную сверять надо nslookup, а НЕ Resolve-DnsName -Server. Флаг
-Server не обходит клиентский кеш Windows: в том же сеансе Resolve-DnsName
-Server ns1.beget.com отдавал старый адрес одновременно с тем, как nslookup на
тот же сервер отдавал новый. Правка выглядела непримененной уже после того, как
применилась.