""" Yield DNS verification helpers. Production-grade DNS checks for the Yield Connect flow: - Option A (recommended): Nameserver delegation to our nameservers - Option B (simpler): CNAME/ALIAS to a shared target """ from __future__ import annotations from dataclasses import dataclass from typing import Optional import dns.resolver @dataclass(frozen=True) class YieldDNSCheckResult: verified: bool method: Optional[str] # "nameserver" | "cname" | None actual_ns: list[str] cname_ok: bool error: Optional[str] def _resolver() -> dns.resolver.Resolver: r = dns.resolver.Resolver() r.timeout = 3 r.lifetime = 5 return r def _normalize_host(host: str) -> str: return host.rstrip(".").lower().strip() def _resolve_ns(domain: str) -> list[str]: r = _resolver() answers = r.resolve(domain, "NS") # NS answers are RRset with .target return sorted({_normalize_host(str(rr.target)) for rr in answers}) def _resolve_cname(domain: str) -> list[str]: r = _resolver() answers = r.resolve(domain, "CNAME") return sorted({_normalize_host(str(rr.target)) for rr in answers}) def _resolve_a(host: str) -> list[str]: r = _resolver() answers = r.resolve(host, "A") return sorted({str(rr) for rr in answers}) def verify_yield_dns(domain: str, expected_nameservers: list[str], cname_target: str) -> YieldDNSCheckResult: """ Verify that a domain is connected for Yield. We accept: - Nameserver delegation (NS contains all expected nameservers), OR - CNAME/ALIAS to `cname_target` (either CNAME matches, or A records match target A records) """ domain = _normalize_host(domain) expected_ns = sorted({_normalize_host(ns) for ns in expected_nameservers if ns}) target = _normalize_host(cname_target) if not domain: return YieldDNSCheckResult( verified=False, method=None, actual_ns=[], cname_ok=False, error="Domain is empty", ) if not expected_ns and not target: return YieldDNSCheckResult( verified=False, method=None, actual_ns=[], cname_ok=False, error="Yield DNS is not configured on server", ) # Option A: NS delegation try: actual_ns = _resolve_ns(domain) if expected_ns and set(expected_ns).issubset(set(actual_ns)): return YieldDNSCheckResult( verified=True, method="nameserver", actual_ns=actual_ns, cname_ok=False, error=None, ) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): actual_ns = [] except Exception as e: return YieldDNSCheckResult( verified=False, method=None, actual_ns=[], cname_ok=False, error=str(e), ) # Option B: CNAME / ALIAS if not target: return YieldDNSCheckResult( verified=False, method=None, actual_ns=actual_ns, cname_ok=False, error="Yield CNAME target is not configured on server", ) # 1) Direct CNAME check (works for subdomain CNAME setups) try: cnames = _resolve_cname(domain) if any(c == target for c in cnames): return YieldDNSCheckResult( verified=True, method="cname", actual_ns=actual_ns, cname_ok=True, error=None, ) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): pass except Exception as e: return YieldDNSCheckResult( verified=False, method=None, actual_ns=actual_ns, cname_ok=False, error=str(e), ) # 2) ALIAS/ANAME flattening: compare A records against target A records try: target_as = set(_resolve_a(target)) domain_as = set(_resolve_a(domain)) if target_as and domain_as and domain_as.issubset(target_as): return YieldDNSCheckResult( verified=True, method="cname", actual_ns=actual_ns, cname_ok=True, error=None, ) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): pass except Exception as e: return YieldDNSCheckResult( verified=False, method=None, actual_ns=actual_ns, cname_ok=False, error=str(e), ) return YieldDNSCheckResult( verified=False, method=None, actual_ns=actual_ns, cname_ok=False, error=None, )