fix: wait for public DNS resolvers before returning from verify_dns

Previously verify_dns_record_external returned True as soon as the TXT
record was found on authoritative NS (ns1/ns2.reg.ru). But Let's Encrypt
validates through recursive resolvers (like 8.8.8.8) which may have
cached the old DNS response. For wildcard+base domain certs, the second
challenge token was invisible to LE due to stale cache on recursive DNS.

Now the function requires the record to be visible on at least one
public recursive resolver (1.1.1.1/8.8.8.8/9.9.9.9) before returning
success, ensuring Let's Encrypt can verify the challenge.
This commit is contained in:
Dmitriy Fofanov
2026-02-25 13:02:28 +03:00
parent 3cc3a9876e
commit 8c31201b51
+29 -17
View File
@@ -1569,20 +1569,28 @@ class LetsEncryptManager:
self.logger.warning(" Не удалось определить authoritative NS, используем публичные DNS")
public_resolvers = ["1.1.1.1", "8.8.8.8", "9.9.9.9"]
found_on_auth = False
for attempt in range(attempts):
try:
# 1) Проверка через authoritative NS (самый точный источник)
for ns in auth_ns:
ns_output = self._query_txt_nslookup(full_domain, ns)
if expected_value in ns_output:
self.logger.info(f" ✅ Попытка {attempt + 1}/{attempts}: TXT найден на authoritative NS {ns}")
for line in ns_output.split('\n'):
if 'text =' in line.lower() or expected_value[:20] in line:
self.logger.info(f" {line.strip()}")
return True
if not found_on_auth:
for ns in auth_ns:
ns_output = self._query_txt_nslookup(full_domain, ns)
if expected_value in ns_output:
self.logger.info(f" ✅ Попытка {attempt + 1}/{attempts}: TXT найден на authoritative NS {ns}")
for line in ns_output.split('\n'):
if 'text =' in line.lower() or expected_value[:20] in line:
self.logger.info(f" {line.strip()}")
found_on_auth = True
break
# 2) Проверка через публичные резолверы
# ВАЖНО: Let's Encrypt валидирует через рекурсивные резолверы, а не
# authoritative NS напрямую. Поэтому ОБЯЗАТЕЛЬНО ждём, пока запись
# станет видна через публичные DNS (8.8.8.8 и т.д.), иначе при
# wildcard + base domain сертификатах кэш рекурсивных резолверов
# может содержать старую версию без нового токена.
found_public = False
for resolver in public_resolvers:
res_output = self._query_txt_nslookup(full_domain, resolver)
@@ -1597,16 +1605,20 @@ class LetsEncryptManager:
if found_public:
return True
# 3) Fallback на системный resolver
default_output = self._query_txt_nslookup(full_domain)
if expected_value in default_output:
self.logger.info(f" ✅ Попытка {attempt + 1}/{attempts}: TXT найден через системный DNS")
for line in default_output.split('\n'):
if 'text =' in line.lower() or expected_value[:20] in line:
self.logger.info(f" {line.strip()}")
return True
# 3) Fallback: если нет authoritative NS, проверяем системный resolver
if not auth_ns:
default_output = self._query_txt_nslookup(full_domain)
if expected_value in default_output:
self.logger.info(f" ✅ Попытка {attempt + 1}/{attempts}: TXT найден через системный DNS")
for line in default_output.split('\n'):
if 'text =' in line.lower() or expected_value[:20] in line:
self.logger.info(f" {line.strip()}")
return True
self.logger.info(f" ⏳ Попытка {attempt + 1}/{attempts}: DNS запись не найдена, ждём...")
if found_on_auth:
self.logger.info(f" ⏳ Попытка {attempt + 1}/{attempts}: TXT есть на authoritative NS, ждём распространения на публичные DNS...")
else:
self.logger.info(f" ⏳ Попытка {attempt + 1}/{attempts}: DNS запись не найдена, ждём...")
except Exception as e:
self.logger.info(f" ⚠️ Попытка {attempt + 1}/{attempts}: Ошибка nslookup - {e}")