from __future__ import annotations from dataclasses import dataclass from typing import Any, Protocol from ..models import UpdateResult from ..settings import DdnsSettings from .base import DnsProviderError class _RcodeResponse(Protocol): def rcode(self) -> int: ... @dataclass(frozen=True, slots=True) class _RecordUpdate: ttl: int record_type: str value: str | None enabled: bool def publish_rfc2136(result: UpdateResult, settings: DdnsSettings) -> None: if not settings.rfc2136_server: return update, relative = _rfc2136_update(result.hostname, settings) _apply_update_records(update, relative, result, settings) _send_rfc2136_update(update, settings.rfc2136_server, "update") def delete_rfc2136_records(hostname: str, settings: DdnsSettings) -> None: if not settings.rfc2136_server: return update, relative = _rfc2136_update(hostname, settings) update.delete(relative, "A") update.delete(relative, "AAAA") _send_rfc2136_update(update, settings.rfc2136_server, "delete") def _rfc2136_update(hostname: str, settings: DdnsSettings) -> tuple[Any, str]: import dns.update zone = _rfc2136_zone(hostname, settings) update = dns.update.Update(zone, keyring=_rfc2136_keyring(settings)) return update, _rfc2136_relative_name(hostname, zone) def _rfc2136_zone(hostname: str, settings: DdnsSettings) -> str: return settings.rfc2136_zone or ".".join(hostname.split(".")[1:]) def _rfc2136_relative_name(hostname: str, zone: str) -> str: hostname = hostname.rstrip(".") zone = zone.rstrip(".") if hostname == zone: return "@" return hostname.removesuffix("." + zone).rstrip(".") or "@" def _rfc2136_keyring(settings: DdnsSettings) -> Any | None: if not settings.rfc2136_key_name or not settings.rfc2136_key_secret: return None import dns.tsigkeyring return dns.tsigkeyring.from_text({settings.rfc2136_key_name: settings.rfc2136_key_secret}) def _apply_update_records(update: Any, relative: str, result: UpdateResult, settings: DdnsSettings) -> None: _apply_record_update( update, relative, _RecordUpdate(ttl=settings.ttl, record_type="A", value=result.ipv4, enabled=result.update_ipv4), ) _apply_record_update( update, relative, _RecordUpdate(ttl=settings.ttl, record_type="AAAA", value=result.ipv6, enabled=result.update_ipv6), ) def _apply_record_update( update: Any, relative: str, record: _RecordUpdate, ) -> None: if not record.enabled: return if record.value is None: update.delete(relative, record.record_type) return update.replace(relative, record.ttl, record.record_type, record.value) def _send_rfc2136_update(update: Any, server: str, operation: str) -> None: import dns.exception import dns.query import dns.rcode try: response = dns.query.tcp(update, server, timeout=5) except dns.exception.DNSException as exc: raise DnsProviderError(f"RFC2136 DNS {operation} failed") from exc _raise_for_rfc2136_response(response, operation) def _raise_for_rfc2136_response(response: _RcodeResponse, operation: str) -> None: import dns.rcode rcode = response.rcode() if rcode != 0: raise DnsProviderError(f"RFC2136 {operation} failed: {dns.rcode.to_text(dns.rcode.Rcode(rcode))}")