pounce/backend/app/services/yield_dns.py
Yves Gugger bb7ce97330
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Deploy: referral rewards antifraud + legal contact updates
2025-12-15 13:56:43 +01:00

170 lines
4.7 KiB
Python

"""
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,
)