2020-04-15 00:56:20 +02:00
|
|
|
import logging
|
2020-04-19 21:28:06 +02:00
|
|
|
from typing import List, Optional
|
2020-04-15 00:56:20 +02:00
|
|
|
|
|
|
|
import dns.query
|
|
|
|
import dns.rdatatype
|
|
|
|
import dns.resolver
|
2020-04-19 21:28:06 +02:00
|
|
|
from django.db import IntegrityError
|
2020-04-15 00:56:20 +02:00
|
|
|
from django.db.models.functions import Length
|
|
|
|
from django_rq import job
|
2020-04-15 13:46:10 +02:00
|
|
|
from dns import rcode
|
|
|
|
from netaddr import ip
|
2020-04-15 00:56:20 +02:00
|
|
|
|
2020-04-15 13:46:10 +02:00
|
|
|
from netbox_ddns.models import ACTION_CREATE, ACTION_DELETE, DNSStatus, ReverseZone, Zone
|
2020-04-19 21:28:06 +02:00
|
|
|
from netbox_ddns.utils import normalize_fqdn
|
2020-04-15 00:56:20 +02:00
|
|
|
|
|
|
|
logger = logging.getLogger('netbox_ddns')
|
|
|
|
|
|
|
|
|
|
|
|
def get_zone(dns_name: str) -> Optional[Zone]:
|
|
|
|
# Generate all possible zones
|
|
|
|
zones = []
|
|
|
|
parts = dns_name.lower().split('.')
|
|
|
|
for i in range(len(parts)):
|
|
|
|
zones.append('.'.join(parts[-i - 1:]))
|
|
|
|
|
|
|
|
# Find the zone, if any
|
|
|
|
return Zone.objects.filter(name__in=zones).order_by(Length('name').desc()).first()
|
|
|
|
|
|
|
|
|
|
|
|
def get_soa(dns_name: str) -> str:
|
|
|
|
parts = dns_name.rstrip('.').split('.')
|
|
|
|
for i in range(len(parts)):
|
2020-04-19 21:28:06 +02:00
|
|
|
zone_name = normalize_fqdn('.'.join(parts[i:]))
|
2020-04-15 00:56:20 +02:00
|
|
|
|
|
|
|
try:
|
2020-04-15 13:46:10 +02:00
|
|
|
dns.resolver.query(zone_name, dns.rdatatype.SOA)
|
2020-04-15 00:56:20 +02:00
|
|
|
return zone_name
|
|
|
|
except dns.resolver.NoAnswer:
|
|
|
|
# The name exists, but has no SOA. Continue one level further up
|
|
|
|
continue
|
|
|
|
except dns.resolver.NXDOMAIN as e:
|
|
|
|
# Look for a SOA record in the authority section
|
|
|
|
for query, response in e.responses().items():
|
|
|
|
for rrset in response.authority:
|
|
|
|
if rrset.rdtype == dns.rdatatype.SOA:
|
2020-04-15 13:46:10 +02:00
|
|
|
return rrset.name.to_text()
|
2020-04-15 00:56:20 +02:00
|
|
|
|
|
|
|
|
2020-04-15 13:46:10 +02:00
|
|
|
def get_reverse_zone(address: ip.IPAddress) -> Optional[ReverseZone]:
|
2020-04-15 00:56:20 +02:00
|
|
|
# Find the zone, if any
|
|
|
|
zones = list(ReverseZone.objects.filter(prefix__net_contains=address))
|
|
|
|
if not zones:
|
|
|
|
return None
|
|
|
|
|
|
|
|
zones.sort(key=lambda zone: zone.prefix.prefixlen)
|
|
|
|
return zones[-1]
|
|
|
|
|
|
|
|
|
2020-04-19 21:28:06 +02:00
|
|
|
def status_update(output: List[str], operation: str, response) -> None:
|
2020-04-15 13:46:10 +02:00
|
|
|
code = response.rcode()
|
2020-04-15 00:56:20 +02:00
|
|
|
|
2020-04-15 13:46:10 +02:00
|
|
|
if code == dns.rcode.NOERROR:
|
2020-04-15 00:56:20 +02:00
|
|
|
message = f"{operation} successful"
|
|
|
|
logger.info(message)
|
|
|
|
else:
|
2020-04-15 13:46:10 +02:00
|
|
|
message = f"{operation} failed: {dns.rcode.to_text(code)}"
|
2020-04-15 00:56:20 +02:00
|
|
|
logger.error(message)
|
|
|
|
|
2020-04-15 13:46:10 +02:00
|
|
|
output.append(message)
|
2020-04-15 00:56:20 +02:00
|
|
|
|
|
|
|
|
2020-04-19 21:28:06 +02:00
|
|
|
def create_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
|
|
|
zone = get_zone(dns_name)
|
|
|
|
if zone:
|
|
|
|
logger.debug(f"Found zone {zone.name} for {dns_name}")
|
|
|
|
if status:
|
|
|
|
status.forward_action = ACTION_CREATE
|
|
|
|
|
|
|
|
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
|
|
|
soa = get_soa(dns_name)
|
|
|
|
if soa == zone.name:
|
|
|
|
record_type = 'A' if address.version == 4 else 'AAAA'
|
|
|
|
update = zone.server.create_update(zone.name)
|
|
|
|
update.add(
|
|
|
|
dns_name,
|
|
|
|
zone.ttl,
|
|
|
|
record_type,
|
|
|
|
str(address)
|
|
|
|
)
|
|
|
|
response = dns.query.udp(update, zone.server.address)
|
|
|
|
status_update(output, f'Adding {dns_name} {record_type} {address}', response)
|
|
|
|
if status:
|
|
|
|
status.forward_rcode = response.rcode()
|
|
|
|
else:
|
|
|
|
logger.warning(f"Can't update zone {zone.name} for {dns_name}, "
|
|
|
|
f"it has delegated authority for {soa}")
|
|
|
|
if status:
|
|
|
|
status.forward_rcode = rcode.NOTAUTH
|
|
|
|
else:
|
|
|
|
logger.debug(f"No zone found for {dns_name}")
|
|
|
|
|
|
|
|
|
|
|
|
def delete_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
|
|
|
zone = get_zone(dns_name)
|
|
|
|
if zone:
|
|
|
|
logger.debug(f"Found zone {zone.name} for {dns_name}")
|
|
|
|
if status:
|
|
|
|
status.forward_action = ACTION_DELETE
|
|
|
|
|
|
|
|
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
|
|
|
soa = get_soa(dns_name)
|
|
|
|
if soa == zone.name:
|
|
|
|
record_type = 'A' if address.version == 4 else 'AAAA'
|
|
|
|
update = zone.server.create_update(zone.name)
|
|
|
|
update.delete(
|
|
|
|
dns_name,
|
|
|
|
record_type,
|
|
|
|
str(address)
|
|
|
|
)
|
|
|
|
response = dns.query.udp(update, zone.server.address)
|
|
|
|
status_update(output, f'Deleting {dns_name} {record_type} {address}', response)
|
|
|
|
if status:
|
|
|
|
status.forward_rcode = response.rcode()
|
|
|
|
else:
|
|
|
|
logger.warning(f"Can't update zone {zone.name} {dns_name}, "
|
|
|
|
f"it has delegated authority for {soa}")
|
|
|
|
if status:
|
|
|
|
status.forward_rcode = rcode.NOTAUTH
|
|
|
|
else:
|
|
|
|
logger.debug(f"No zone found for {dns_name}")
|
|
|
|
|
|
|
|
|
|
|
|
def create_reverse(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
|
|
|
zone = get_reverse_zone(address)
|
|
|
|
if zone and dns_name:
|
|
|
|
record_name = zone.record_name(address)
|
|
|
|
logger.debug(f"Found zone {zone.name} for {record_name}")
|
|
|
|
if status:
|
|
|
|
status.reverse_action = ACTION_CREATE
|
|
|
|
|
|
|
|
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
|
|
|
soa = get_soa(record_name)
|
|
|
|
if soa == zone.name:
|
|
|
|
update = zone.server.create_update(zone.name)
|
|
|
|
update.add(
|
|
|
|
record_name,
|
|
|
|
zone.ttl,
|
|
|
|
'ptr',
|
|
|
|
dns_name
|
|
|
|
)
|
|
|
|
response = dns.query.udp(update, zone.server.address)
|
|
|
|
status_update(output, f'Adding {record_name} PTR {dns_name}', response)
|
|
|
|
if status:
|
|
|
|
status.reverse_rcode = response.rcode()
|
|
|
|
else:
|
|
|
|
logger.warning(f"Can't update zone {zone.name} for {record_name}, "
|
|
|
|
f"it has delegated authority for {soa}")
|
|
|
|
if status:
|
|
|
|
status.reverse_rcode = rcode.NOTAUTH
|
|
|
|
else:
|
|
|
|
logger.debug(f"No zone found for {address}")
|
|
|
|
|
|
|
|
|
|
|
|
def delete_reverse(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
|
|
|
zone = get_reverse_zone(address)
|
|
|
|
if zone and dns_name:
|
|
|
|
record_name = zone.record_name(address)
|
|
|
|
logger.debug(f"Found zone {zone.name} for {record_name}")
|
|
|
|
if status:
|
|
|
|
status.reverse_action = ACTION_DELETE
|
|
|
|
|
|
|
|
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
|
|
|
soa = get_soa(record_name)
|
|
|
|
if soa == zone.name:
|
|
|
|
update = zone.server.create_update(zone.name)
|
|
|
|
update.delete(
|
|
|
|
record_name,
|
|
|
|
'ptr',
|
|
|
|
dns_name
|
|
|
|
)
|
|
|
|
response = dns.query.udp(update, zone.server.address)
|
|
|
|
status_update(output, f'Deleting {record_name} PTR {dns_name}', response)
|
|
|
|
if status:
|
|
|
|
status.reverse_rcode = response.rcode()
|
|
|
|
else:
|
|
|
|
logger.warning(f"Can't update zone {zone.name} for {record_name}, "
|
|
|
|
f"it has delegated authority for {soa}")
|
|
|
|
if status:
|
|
|
|
status.reverse_rcode = rcode.NOTAUTH
|
|
|
|
else:
|
|
|
|
logger.debug(f"No zone found for {address}")
|
|
|
|
|
|
|
|
|
2020-04-15 00:56:20 +02:00
|
|
|
@job
|
2020-04-19 21:28:06 +02:00
|
|
|
def dns_create(dns_name: str, address: ip.IPAddress, forward=True, reverse=True, status: DNSStatus = None):
|
|
|
|
output = []
|
2020-04-15 13:46:10 +02:00
|
|
|
|
2020-04-19 21:28:06 +02:00
|
|
|
if forward:
|
|
|
|
create_forward(dns_name, address, status, output)
|
|
|
|
if reverse:
|
|
|
|
create_reverse(dns_name, address, status, output)
|
|
|
|
|
|
|
|
if status:
|
|
|
|
try:
|
|
|
|
status.save()
|
|
|
|
except IntegrityError:
|
|
|
|
# Race condition when creating?
|
|
|
|
status.save(force_update=True)
|
|
|
|
|
|
|
|
return ', '.join(output)
|
|
|
|
|
|
|
|
|
|
|
|
@job
|
|
|
|
def dns_delete(dns_name: str, address: ip.IPAddress, forward=True, reverse=True, status: DNSStatus = None):
|
2020-04-15 13:46:10 +02:00
|
|
|
output = []
|
2020-04-19 21:28:06 +02:00
|
|
|
|
|
|
|
if forward:
|
|
|
|
delete_forward(dns_name, address, status, output)
|
|
|
|
if reverse:
|
|
|
|
delete_reverse(dns_name, address, status, output)
|
|
|
|
|
|
|
|
if status:
|
|
|
|
try:
|
|
|
|
status.save()
|
|
|
|
except IntegrityError:
|
|
|
|
# Race condition when creating?
|
|
|
|
status.save(force_update=True)
|
2020-04-15 13:46:10 +02:00
|
|
|
|
|
|
|
return ', '.join(output)
|