Archived
115 lines
3.3 KiB
Python
115 lines
3.3 KiB
Python
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))}")
|