""" Yield DNS verification helpers. Production-grade DNS checks for the Yield Connect flow: - Option A: A-record pointing directly to our server IP (simplest!) - Option B: CNAME/ALIAS to yield.pounce.ch - Option C: Nameserver delegation to our nameservers (requires DNS server) """ from __future__ import annotations from dataclasses import dataclass from typing import Optional import dns.resolver # Our server IP - domains with A-record pointing here are verified YIELD_SERVER_IP = "46.235.147.194" @dataclass(frozen=True) class YieldDNSCheckResult: verified: bool method: Optional[str] # "a_record" | "cname" | "nameserver" | None actual_ns: list[str] actual_a: list[str] cname_ok: bool error: Optional[str] def _resolver(nameserver: str | None = None) -> dns.resolver.Resolver: """Create a DNS resolver, optionally using a specific nameserver.""" r = dns.resolver.Resolver() if nameserver: r.nameservers = [nameserver] r.timeout = 3 r.lifetime = 5 return r # Multiple public DNS servers to check (for propagation consistency) PUBLIC_DNS_SERVERS = ['8.8.8.8', '8.8.4.4', '9.9.9.9', '1.1.1.1'] 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") 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 _resolve_a_from_multiple_dns(host: str) -> set[str]: """ Resolve A records from multiple public DNS servers. Returns the union of all IPs found (handles propagation delays). """ all_ips: set[str] = set() for ns in PUBLIC_DNS_SERVERS: try: r = _resolver(ns) answers = r.resolve(host, "A") for rr in answers: all_ips.add(str(rr)) except Exception: continue return all_ips def verify_yield_dns(domain: str, expected_nameservers: list[str], cname_target: str) -> YieldDNSCheckResult: """ Verify that a domain is connected for Yield. We accept (in order of simplicity): 1. A-record pointing directly to our server IP (46.235.147.194) 2. CNAME/ALIAS to `cname_target` (yield.pounce.ch) 3. Nameserver delegation (NS contains all expected nameservers) """ domain = _normalize_host(domain) expected_ns = sorted({_normalize_host(ns) for ns in expected_nameservers if ns}) target = _normalize_host(cname_target) actual_ns: list[str] = [] actual_a: list[str] = [] if not domain: return YieldDNSCheckResult( verified=False, method=None, actual_ns=[], actual_a=[], cname_ok=False, error="Domain is empty", ) # Option A: Direct A-record to our server IP (SIMPLEST!) # Check multiple DNS servers to handle propagation delays try: all_ips = _resolve_a_from_multiple_dns(domain) actual_a = sorted(all_ips) if YIELD_SERVER_IP in all_ips: return YieldDNSCheckResult( verified=True, method="a_record", actual_ns=[], actual_a=actual_a, cname_ok=False, error=None, ) except Exception as e: return YieldDNSCheckResult( verified=False, method=None, actual_ns=[], actual_a=[], cname_ok=False, error=str(e), ) # Option B: CNAME to yield.pounce.ch if target: try: cnames = _resolve_cname(domain) if any(c == target for c in cnames): return YieldDNSCheckResult( verified=True, method="cname", actual_ns=[], actual_a=actual_a, cname_ok=True, error=None, ) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): pass except Exception: pass # Also check if A-records match target's A-records (ALIAS/ANAME flattening) try: target_as = set(_resolve_a(target)) if target_as and actual_a and set(actual_a).issubset(target_as): return YieldDNSCheckResult( verified=True, method="cname", actual_ns=[], actual_a=actual_a, cname_ok=True, error=None, ) except Exception: pass # Option C: NS delegation (requires DNS server on Port 53) if expected_ns: try: actual_ns = _resolve_ns(domain) if set(expected_ns).issubset(set(actual_ns)): return YieldDNSCheckResult( verified=True, method="nameserver", actual_ns=actual_ns, actual_a=actual_a, cname_ok=False, error=None, ) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): pass except Exception: pass # Not verified return YieldDNSCheckResult( verified=False, method=None, actual_ns=actual_ns, actual_a=actual_a, cname_ok=False, error=None, )