Renaming Project and changing license
This commit is contained in:
1 parent
4a1930a896
commit
756c491e28
24 files changed
+173
-555
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,227 @@
|
||||
import logging
|
||||
import threading
|
||||
import dns.query
|
||||
import dns.message
|
||||
import dns.tsigkeyring
|
||||
import dns.name
|
||||
import dns.zone
|
||||
import dns.rdatatype
|
||||
import dns.rdataclass
|
||||
import dns.rdtypes
|
||||
import dns.exception
|
||||
import dns.renderer
|
||||
from netbox_dns.models import Zone
|
||||
from netbox_dns.choices import ZoneStatusChoices
|
||||
from netbox_dns_bridge.models import IntegerKeyValueSetting, CatalogZoneMemberIdentifier
|
||||
from uuid import uuid4
|
||||
from base64 import b32encode
|
||||
|
||||
logger = logging.getLogger("dns-transfer-endpoint")
|
||||
|
||||
_LOCK = threading.Lock()
|
||||
_SERIAL_MAX = 0xFFFFFFFF
|
||||
_SERIAL_OBJ = None
|
||||
_PREVIOUS_LAST_ZONE_UPDATE = None
|
||||
|
||||
def init() -> None:
|
||||
_init_serial()
|
||||
_create_missing_member_identifiers()
|
||||
|
||||
# Following function loads the last serial from the DB. No return value
|
||||
# but it sets the setting "catalog_serial" or terminates the plugin on failure.
|
||||
def _init_serial() -> None:
|
||||
global _SERIAL_OBJ
|
||||
|
||||
try:
|
||||
_SERIAL_OBJ = IntegerKeyValueSetting.objects.get(
|
||||
key="catalog-zone-soa-serial"
|
||||
)
|
||||
logger.debug(
|
||||
f"Catalog zone SOA serial number {_SERIAL_OBJ.value} loaded from database"
|
||||
)
|
||||
except IntegerKeyValueSetting.DoesNotExist:
|
||||
_SERIAL_OBJ = IntegerKeyValueSetting.objects.create(
|
||||
key="catalog-zone-soa-serial", value=1
|
||||
)
|
||||
logger.debug(
|
||||
f"Catalog zone SOA serial number was not set in the database. Set to {_SERIAL_OBJ.value}"
|
||||
)
|
||||
|
||||
def _increment_serial() -> None:
|
||||
global _SERIAL_OBJ
|
||||
|
||||
if 0 < _SERIAL_OBJ.value < _SERIAL_MAX:
|
||||
_SERIAL_OBJ.value += 1
|
||||
_SERIAL_OBJ.save()
|
||||
else:
|
||||
logger.warning(
|
||||
f"Failed to incremenet catalog serial {_SERIAL_OBJ.value}. Will overflow serial back to 1"
|
||||
)
|
||||
_SERIAL_OBJ.value = 1
|
||||
_SERIAL_OBJ.save()
|
||||
logger.debug(
|
||||
f"Catalog zone SOA serial number incremented to {_SERIAL_OBJ.value}"
|
||||
)
|
||||
|
||||
# If a zone has no catz identifier yet, create it:
|
||||
def _create_missing_member_identifiers() -> None:
|
||||
existing_zone_ids = CatalogZoneMemberIdentifier.objects.values_list(
|
||||
"zone_id", flat=True
|
||||
)
|
||||
|
||||
missing_zones = Zone.objects.exclude(id__in=existing_zone_ids)
|
||||
|
||||
new_objects = [
|
||||
CatalogZoneMemberIdentifier(
|
||||
zone=zone,
|
||||
name=_generate_member_identifier(),
|
||||
)
|
||||
for zone in missing_zones
|
||||
]
|
||||
|
||||
for identifier in new_objects:
|
||||
logger.debug(f"Zone {identifier.zone} has no catz member identifier. Creating...")
|
||||
|
||||
CatalogZoneMemberIdentifier.objects.bulk_create(
|
||||
new_objects,
|
||||
ignore_conflicts=False,
|
||||
)
|
||||
|
||||
|
||||
def create_zone(name, view_name) -> dns.zone.Zone:
|
||||
global _PREVIOUS_LAST_ZONE_UPDATE
|
||||
# Synchronize following across threads as TCP and UDP listener both use it.
|
||||
with _LOCK:
|
||||
latest_zone = (
|
||||
Zone.objects.filter(status=ZoneStatusChoices.STATUS_ACTIVE)
|
||||
.order_by("-last_updated")
|
||||
.first()
|
||||
)
|
||||
|
||||
last_zone_update = getattr(latest_zone, "last_updated", None)
|
||||
|
||||
# Check if there was a zone updated since last call
|
||||
# If no zone was found previously then this will be false since (None != None) = False
|
||||
if _PREVIOUS_LAST_ZONE_UPDATE != last_zone_update:
|
||||
if last_zone_update is not None:
|
||||
logger.debug(
|
||||
f"Zone {latest_zone.name} was updated in view {latest_zone.view.name}"
|
||||
)
|
||||
# Setting previous last zone update for next iteration:
|
||||
_PREVIOUS_LAST_ZONE_UPDATE = last_zone_update
|
||||
_increment_serial()
|
||||
|
||||
# Zone origin
|
||||
origin = dns.name.from_text(name, dns.name.root)
|
||||
|
||||
# Create a new empty zone
|
||||
zone = dns.zone.Zone(origin)
|
||||
zone.rdclass = dns.rdataclass.IN
|
||||
|
||||
# get zones from netbox
|
||||
nb_zones = Zone.objects.filter(
|
||||
view__name=view_name, status=ZoneStatusChoices.STATUS_ACTIVE
|
||||
).select_related("catz_identifier")
|
||||
|
||||
ptr_base = dns.name.from_text("zones", origin)
|
||||
|
||||
for nb_zone in nb_zones:
|
||||
ttl = 0
|
||||
qname = dns.name.from_text(nb_zone.name, dns.name.root)
|
||||
|
||||
# Create PTR record
|
||||
#p_name = f"zid-{nb_zone.id:09d}"
|
||||
p_name = nb_zone.catz_identifier.name
|
||||
|
||||
ptr_name = dns.name.from_text(p_name, ptr_base)
|
||||
if not ptr_name.is_subdomain(origin):
|
||||
raise ValueError(
|
||||
"Catalog zone member identifier {ptr_name.to_text()} not a subdomain"
|
||||
)
|
||||
rdata = dns.rdata.from_text(
|
||||
dns.rdataclass.IN, dns.rdatatype.PTR, qname.to_text()
|
||||
)
|
||||
rdataset = zone.find_rdataset(ptr_name, dns.rdatatype.PTR, create=True)
|
||||
rdataset.add(rdata, ttl)
|
||||
|
||||
# Configure DNSSec Policy for member Zone if DNSSec is enabled
|
||||
if nb_zone.dnssec_policy:
|
||||
# Configure policy
|
||||
rid = dns.name.from_text("group", ptr_name)
|
||||
policy_name = nb_zone.dnssec_policy.name.rstrip(" ")
|
||||
group_name = f"dnssec-policy-{policy_name}"
|
||||
rdata = dns.rdata.from_text(
|
||||
dns.rdataclass.IN, dns.rdatatype.TXT, group_name
|
||||
)
|
||||
rdataset = zone.find_rdataset(rid, dns.rdatatype.TXT, create=True)
|
||||
rdataset.add(rdata, ttl)
|
||||
|
||||
## Configure dnssec status for member zone
|
||||
# status = str(1 if nb_zone.dnssec_policy else 0)
|
||||
# rid = dns.name.from_text("enabled.dnssec.ext", ptr_name)
|
||||
# rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, status)
|
||||
# rdataset = zone.find_rdataset(rid, dns.rdatatype.TXT, create=True)
|
||||
# rdataset.add(rdata, ttl)
|
||||
|
||||
# SOA Record components
|
||||
ttl = 0
|
||||
rclass = dns.rdataclass.IN
|
||||
rtype = dns.rdatatype.SOA
|
||||
mname = dns.name.from_text("invalid", dns.name.root)
|
||||
rname = dns.name.from_text("invalid", dns.name.root)
|
||||
serial = _SERIAL_OBJ.value
|
||||
refresh = 60
|
||||
retry = 10
|
||||
expire = 1209600
|
||||
minimum = 0
|
||||
|
||||
# Create SOA rdata object
|
||||
soa_rdata = dns.rdata.from_text(
|
||||
rclass,
|
||||
rtype,
|
||||
f"{mname} {rname} {serial} {refresh} {retry} {expire} {minimum}",
|
||||
)
|
||||
|
||||
# Create Rdataset and add the RDATA to it
|
||||
soa_rdataset = dns.rdataset.Rdataset(rclass, rtype)
|
||||
soa_rdataset.add(soa_rdata, ttl)
|
||||
|
||||
# Add to the origin node in the zone
|
||||
node = zone.find_node(origin, create=True)
|
||||
node.rdatasets.append(soa_rdataset)
|
||||
|
||||
# NS record for catz.
|
||||
ns_name = dns.name.from_text("invalid", dns.name.root)
|
||||
ns_rdata = dns.rdata.from_text(
|
||||
dns.rdataclass.IN, dns.rdatatype.NS, str(ns_name)
|
||||
)
|
||||
ns_rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
|
||||
ns_rdataset.add(ns_rdata, 0)
|
||||
|
||||
# Add to node (catz. is the origin)
|
||||
ns_node = zone.find_node(origin, create=True)
|
||||
ns_node.rdatasets.append(ns_rdataset)
|
||||
|
||||
# TXT record for version.catz.
|
||||
version_name = dns.name.from_text(
|
||||
"version", origin
|
||||
) # relative to origin "catz."
|
||||
txt_rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"2"')
|
||||
|
||||
txt_rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.TXT)
|
||||
txt_rdataset.add(txt_rdata, 0)
|
||||
|
||||
# Add to node version.catz.
|
||||
txt_node = zone.find_node(version_name, create=True)
|
||||
txt_node.rdatasets.append(txt_rdataset)
|
||||
|
||||
return zone
|
||||
|
||||
def _generate_member_identifier() -> None:
|
||||
return b32encode(uuid4().bytes)[0:26].lower().decode('UTF-8')
|
||||
|
||||
def update_member_identifier(zone: Zone) -> None:
|
||||
CatalogZoneMemberIdentifier.objects.update_or_create(
|
||||
zone=zone,
|
||||
defaults={"name": _generate_member_identifier},
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
import logging
|
||||
import socketserver
|
||||
import socket
|
||||
|
||||
logger = logging.getLogger("dns-transfer-endpoint")
|
||||
|
||||
class DNSAddressMixin:
|
||||
def _resolve_address(self, server_address, socktype, proto):
|
||||
host, port = server_address
|
||||
|
||||
infos = socket.getaddrinfo(
|
||||
host,
|
||||
port,
|
||||
socket.AF_UNSPEC,
|
||||
socktype,
|
||||
proto,
|
||||
socket.AI_PASSIVE
|
||||
)
|
||||
|
||||
family, _, _, _, sockaddr = infos[0]
|
||||
self.address_family = family
|
||||
return sockaddr
|
||||
|
||||
|
||||
class TCPDNSServer(DNSAddressMixin, socketserver.TCPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, server_address, handler_class, keyring, tsig_view_map):
|
||||
sockaddr = self._resolve_address(
|
||||
server_address,
|
||||
socket.SOCK_STREAM,
|
||||
socket.IPPROTO_TCP
|
||||
)
|
||||
|
||||
super().__init__(sockaddr, handler_class)
|
||||
|
||||
self.keyring = keyring
|
||||
self.tsig_view_map = tsig_view_map
|
||||
|
||||
|
||||
class UDPDNSServer(DNSAddressMixin, socketserver.UDPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, server_address, handler_class, keyring, tsig_view_map):
|
||||
sockaddr = self._resolve_address(
|
||||
server_address,
|
||||
socket.SOCK_DGRAM,
|
||||
socket.IPPROTO_UDP
|
||||
)
|
||||
|
||||
super().__init__(sockaddr, handler_class)
|
||||
|
||||
self.keyring = keyring
|
||||
self.tsig_view_map = tsig_view_map
|
||||
@@ -0,0 +1,413 @@
|
||||
import logging
|
||||
import socketserver
|
||||
import socket
|
||||
import dns.query
|
||||
import dns.message
|
||||
import dns.tsigkeyring
|
||||
import dns.name
|
||||
import dns.zone
|
||||
import dns.rdatatype
|
||||
import dns.rdataclass
|
||||
import dns.rdtypes
|
||||
import dns.exception
|
||||
import dns.renderer
|
||||
from netbox_dns.models import Zone, Record
|
||||
from netbox_dns.choices import ZoneStatusChoices, RecordStatusChoices
|
||||
from . import catalog_zone_manager as catzm
|
||||
|
||||
logger = logging.getLogger("dns-transfer-endpoint")
|
||||
|
||||
|
||||
class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
||||
def __init__(self, request, client_address, server) -> None:
|
||||
self.MAX_WIRE = 65535
|
||||
self.RESERVED_TSIG = 300
|
||||
super().__init__(request, client_address, server)
|
||||
|
||||
# _getZoneFromNB rewritten
|
||||
def _getZoneFromNB(self, zone_name, view_name) -> dns.zone.Zone:
|
||||
# Find the zone
|
||||
try:
|
||||
nb_zone = Zone.objects.get(
|
||||
name=zone_name,
|
||||
view__name=view_name,
|
||||
status=ZoneStatusChoices.STATUS_ACTIVE,
|
||||
)
|
||||
except Zone.DoesNotExist:
|
||||
return None
|
||||
|
||||
# Build DNS zone
|
||||
zone = dns.zone.Zone(zone_name, dns.name.root)
|
||||
zone.rdclass = dns.rdataclass.IN
|
||||
|
||||
nb_records = Record.objects.filter(
|
||||
zone=nb_zone, status=RecordStatusChoices.STATUS_ACTIVE
|
||||
)
|
||||
|
||||
rdatasets_dict = {}
|
||||
|
||||
for record in nb_records:
|
||||
rdtype = dns.rdatatype.from_text(record.type)
|
||||
if not record.name:
|
||||
name = zone.origin
|
||||
elif record.name.endswith("."):
|
||||
name = dns.name.from_text(record.name)
|
||||
else:
|
||||
name = dns.name.from_text(record.name, origin=zone.origin)
|
||||
|
||||
# If the record has no TTL, use the zone default
|
||||
ttl = record.ttl or nb_zone.default_ttl
|
||||
|
||||
# Apply quoting for TXT records to stop tokanizer
|
||||
# from cutting it up:
|
||||
value = record.value
|
||||
if rdtype == dns.rdatatype.TXT:
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1].replace('" "', "").replace('"', '"')
|
||||
|
||||
if len(value) > 255:
|
||||
# This is a bug fix for netbox. If netbox allowed for
|
||||
# an unquoted value to be larger then 255 characters,
|
||||
# it misunderstood everything behind a ; as a comment.
|
||||
chunks = [
|
||||
'"{}"'.format(value[i : i + 255])
|
||||
for i in range(0, len(value), 255)
|
||||
]
|
||||
value = " ".join(chunks)
|
||||
else:
|
||||
value = f'"{value}"'
|
||||
|
||||
rdata = dns.rdata.from_text(
|
||||
dns.rdataclass.IN,
|
||||
rdtype,
|
||||
value,
|
||||
relativize=False,
|
||||
origin=zone.origin,
|
||||
)
|
||||
|
||||
# Initialize rdataset if it doesn't exist for this name and type
|
||||
if name not in rdatasets_dict:
|
||||
rdatasets_dict[name] = {}
|
||||
if rdtype not in rdatasets_dict[name]:
|
||||
rdatasets_dict[name][rdtype] = dns.rdataset.Rdataset(
|
||||
dns.rdataclass.IN, rdtype
|
||||
)
|
||||
|
||||
# Add the rdata to the appropriate rdataset
|
||||
rdatasets_dict[name][rdtype].add(rdata, ttl)
|
||||
|
||||
# Now, add all rdatasets to the zone
|
||||
for name, rdtypes in rdatasets_dict.items():
|
||||
for rdtype, rdataset in rdtypes.items():
|
||||
# Ensure rdataset has the same rdclass as the zone
|
||||
if rdataset.rdclass != zone.rdclass:
|
||||
raise ValueError(
|
||||
f"rdataset rdclass {rdataset.rdclass} does not match zone rdclass {zone.rdclass}"
|
||||
)
|
||||
|
||||
# Check if the rdataset has any rdata before creating an RRset
|
||||
if not rdataset:
|
||||
logger.debug(f"Skipping empty rdataset for {name} {rdtype}")
|
||||
continue # Skip empty rdataset
|
||||
|
||||
# Replace the rdataset for the given name and type
|
||||
zone.replace_rdataset(name, rdataset)
|
||||
return zone
|
||||
|
||||
def _denyRequestBadTSIG(self, wire, tsig_error: dns.rcode) -> None:
|
||||
# Use empty keyring to parse TSIG without validating
|
||||
query = dns.message.from_wire(
|
||||
wire, keyring={}, ignore_trailing=True, continue_on_error=True
|
||||
)
|
||||
|
||||
# Make a response
|
||||
response = dns.message.make_response(query)
|
||||
response.set_rcode(dns.rcode.REFUSED)
|
||||
|
||||
if query.had_tsig:
|
||||
# Add TSIG with BADKEY, but do not sign (no MAC)
|
||||
response.use_tsig(
|
||||
keyring={}, # empty; we're not signing
|
||||
keyname=query.keyname,
|
||||
tsig_error=tsig_error,
|
||||
)
|
||||
self.denyRequest(query)
|
||||
|
||||
def _send_response(self, data) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def _deny_request(self, query, rcode: dns.rcode = dns.rcode.REFUSED) -> None:
|
||||
response = dns.message.make_response(query)
|
||||
response.set_rcode(rcode)
|
||||
wire = response.to_wire(multi=False)
|
||||
self._send_response(wire)
|
||||
|
||||
def _handle_soa_request(self, query, soa_rrset, zone, peer, nb_view, dname) -> None:
|
||||
# We assume that the SOA rdataset has at least one record (it usually does).
|
||||
soa_rdata = soa_rrset[0] # Get the first SOA record
|
||||
|
||||
# Now, create the rrset from the soa_rdata
|
||||
rrset = dns.rrset.from_rdata(zone.origin, soa_rrset.ttl, soa_rdata)
|
||||
|
||||
response = dns.message.make_response(query)
|
||||
# Set the Authoritative Answer flag
|
||||
response.flags |= dns.flags.AA
|
||||
|
||||
# Append the rrset to the response's answer section
|
||||
response.answer.append(rrset)
|
||||
|
||||
# TSIG response
|
||||
if query.had_tsig:
|
||||
# If key was found in DB
|
||||
if query.keyname in self.server.keyring:
|
||||
response.use_tsig(
|
||||
self.server.keyring, keyname=query.keyname, original_id=query.id
|
||||
)
|
||||
else:
|
||||
# If key is unknown
|
||||
response.set_rcode(dns.rcode.REFUSED)
|
||||
# Generate new key for keyname provided
|
||||
# keyname = query.keyname
|
||||
# b64 = base64.b64encode(urandom(32)).decode("ascii")
|
||||
# newkey = dns.tsigkeyring.from_text({ keyname: b64 })
|
||||
response.use_tsig(
|
||||
self.server.keyring,
|
||||
keyname=query.keyname,
|
||||
tsig_error=dns.rcode.BADKEY,
|
||||
)
|
||||
|
||||
data = response.to_wire(max_size=512)
|
||||
self._send_response(data)
|
||||
logger.debug(f"{peer} SOA {nb_view.name}/{dname}")
|
||||
|
||||
def _handle_axfr_request(self, query, zone, peer, nb_view, dname) -> None:
|
||||
rrsets = []
|
||||
soa_rrset = None
|
||||
for name, rdataset in zone.iterate_rdatasets():
|
||||
if not name.is_absolute():
|
||||
name = name.concatenate(zone.origin)
|
||||
rrset = dns.rrset.from_rdata_list(name, rdataset.ttl, rdataset)
|
||||
if rdataset.rdtype == dns.rdatatype.SOA and soa_rrset is None:
|
||||
soa_rrset = rrset
|
||||
else:
|
||||
rrsets.append(rrset)
|
||||
|
||||
rrsets.insert(0, soa_rrset) # Opening SOA
|
||||
rrsets.append(soa_rrset) # Closing SOA
|
||||
|
||||
# 2. Create a Renderer for the first message
|
||||
r = dns.renderer.Renderer(
|
||||
id=query.id,
|
||||
flags=(dns.flags.QR | dns.flags.AA),
|
||||
max_size=self.MAX_WIRE,
|
||||
origin=None,
|
||||
)
|
||||
r.add_question(
|
||||
query.question[0].name,
|
||||
query.question[0].rdtype,
|
||||
query.question[0].rdclass,
|
||||
)
|
||||
|
||||
# 3. Loop through RRsets
|
||||
tsig_ctx = None
|
||||
for rrset in rrsets:
|
||||
# logger.debug(f"Iterating over rrset {rrset}. tsig_ctx: {tsig_ctx}")
|
||||
try:
|
||||
# logger.debug(f"Adding {rrset} to renderer object")
|
||||
r.add_rrset(dns.renderer.ANSWER, rrset)
|
||||
if r.max_size - len(r.output.getvalue()) < self.RESERVED_TSIG:
|
||||
raise dns.exception.TooBig("TSIG wont fit")
|
||||
except dns.exception.TooBig:
|
||||
# TSIG chain previous message
|
||||
r.write_header()
|
||||
tsig_ctx = r.add_multi_tsig(
|
||||
ctx=tsig_ctx,
|
||||
secret=self.server.keyring[query.keyname],
|
||||
keyname=query.keyname,
|
||||
fudge=300,
|
||||
id=query.id,
|
||||
tsig_error=0,
|
||||
other_data=b"",
|
||||
request_mac=r.mac if tsig_ctx else query.mac,
|
||||
)
|
||||
wire = r.get_wire()
|
||||
self.request.sendall(len(wire).to_bytes(2, "big") + wire)
|
||||
|
||||
# Start new renderer
|
||||
r = dns.renderer.Renderer(
|
||||
id=query.id,
|
||||
flags=(dns.flags.QR | dns.flags.AA),
|
||||
max_size=self.MAX_WIRE,
|
||||
origin=None,
|
||||
)
|
||||
r.add_question(
|
||||
query.question[0].name,
|
||||
query.question[0].rdtype,
|
||||
query.question[0].rdclass,
|
||||
)
|
||||
r.add_rrset(dns.renderer.ANSWER, rrset)
|
||||
|
||||
# 4. Final message with terminating TSIG
|
||||
r.write_header()
|
||||
# logger.debug(f"Final message. tsig_ctx: {tsig_ctx}")
|
||||
tsig_ctx = r.add_multi_tsig(
|
||||
ctx=tsig_ctx,
|
||||
secret=self.server.keyring[query.keyname],
|
||||
keyname=query.keyname,
|
||||
fudge=300,
|
||||
id=query.id,
|
||||
tsig_error=0,
|
||||
other_data=b"",
|
||||
# request_mac=r.mac if tsig_ctx else None,
|
||||
request_mac=r.mac if tsig_ctx else query.mac,
|
||||
)
|
||||
wire = r.get_wire()
|
||||
self._send_response(wire)
|
||||
|
||||
logger.debug(f"{peer} AXFR {nb_view.name}/{dname}")
|
||||
|
||||
def _handle_dns_query(self, wire) -> None:
|
||||
peer = self.client_address[0]
|
||||
try:
|
||||
query = dns.message.from_wire(
|
||||
wire,
|
||||
keyring=self.server.keyring,
|
||||
continue_on_error=False,
|
||||
ignore_trailing=True
|
||||
)
|
||||
|
||||
except dns.tsig.BadSignature as e:
|
||||
logger.warning(
|
||||
f"Request denied from {peer} failed TSIG verification: {e}"
|
||||
)
|
||||
self._denyRequestBadTSIG(wire, dns.rcode.BADSIG)
|
||||
return
|
||||
|
||||
except (dns.message.UnknownTSIGKey, dns.tsig.BadAlgorithm) as e:
|
||||
logger.warning(f"Request denied from {peer} with bad TSIG key: {e}")
|
||||
self._denyRequestBadTSIG(wire, dns.rcode.BADKEY)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error parsing query: ", e)
|
||||
return
|
||||
|
||||
# If there was no question in the query, refuse
|
||||
if len(query.question) != 1:
|
||||
self._deny_request(query)
|
||||
return
|
||||
|
||||
question = query.question[0]
|
||||
qname = question.name
|
||||
qtype = question.rdtype
|
||||
dname = qname.to_text().rstrip(".")
|
||||
|
||||
# Only process AXFR/SOA queris
|
||||
if qtype not in (dns.rdatatype.AXFR, dns.rdatatype.SOA):
|
||||
logger.warning(
|
||||
f"Request denied from {peer}: Request was not AXFR or SOA (Type: {qtype})"
|
||||
)
|
||||
self._deny_request(query)
|
||||
return
|
||||
|
||||
# Identify TSIG key used
|
||||
if not query.had_tsig:
|
||||
logger.warning(f"Request denied from {peer}: No TSIG key used")
|
||||
self._deny_request(query)
|
||||
return
|
||||
|
||||
key_name = query.keyname.canonicalize().to_text()
|
||||
|
||||
# Check if the key matches a view
|
||||
nb_view = self.server.tsig_view_map.get(key_name)
|
||||
if not nb_view:
|
||||
logger.warning(
|
||||
f"Request denied from {peer}: {key_name} does not match a view"
|
||||
)
|
||||
self._deny_request(query)
|
||||
return
|
||||
|
||||
# Check if catalog zone
|
||||
if dname == "catz" or dname == f"{nb_view.name}.catz":
|
||||
zone = catzm.create_zone(dname, nb_view.name)
|
||||
else:
|
||||
zone = self._getZoneFromNB(dname, nb_view.name)
|
||||
# When zone was not found, let client know
|
||||
if not zone:
|
||||
logger.warning(f"Zone {dname} not found in view {nb_view.name}")
|
||||
self._deny_request(query)
|
||||
return
|
||||
|
||||
# Retrieve the existing SOA record from the Zone
|
||||
soa_rrset = zone.get_rdataset(zone.origin, dns.rdatatype.SOA)
|
||||
if soa_rrset is None:
|
||||
logger.error(f"Zone {dname} has no SOA — aborting")
|
||||
return
|
||||
|
||||
if qtype == dns.rdatatype.SOA:
|
||||
self._handle_soa_request(query, soa_rrset, zone, peer, nb_view, dname)
|
||||
elif qtype == dns.rdatatype.AXFR:
|
||||
self._handle_axfr_request(query, zone, peer, nb_view, dname)
|
||||
|
||||
|
||||
class UDPRequestHandler(DNSBaseRequestHandler):
|
||||
def __init__(self, request, client_address, server) -> None:
|
||||
super().__init__(request, client_address, server)
|
||||
|
||||
def _send_response(self, data) -> None:
|
||||
sock = self.request[1]
|
||||
sock.sendto(data, self.client_address)
|
||||
|
||||
def handle(self) -> None:
|
||||
data, sock = self.request
|
||||
peer = self.client_address[0]
|
||||
try:
|
||||
self._handle_dns_query(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling request from {peer}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class TCPRequestHandler(DNSBaseRequestHandler):
|
||||
def __init__(self, request, client_address, server) -> None:
|
||||
super().__init__(request, client_address, server)
|
||||
|
||||
def _send_response(self, data) -> None:
|
||||
length = len(data).to_bytes(2, byteorder="big")
|
||||
self.request.sendall(length + data)
|
||||
|
||||
def handle(self) -> None:
|
||||
peer = self.client_address[0]
|
||||
sock = self.request # TCP socket
|
||||
sock.settimeout(10.0) # Default 10 second timeout for inactivity
|
||||
try:
|
||||
while True:
|
||||
# Read 2-byte length prefix
|
||||
length_data = sock.recv(2)
|
||||
if not length_data:
|
||||
return # connection closed by peer
|
||||
if len(length_data) < 2:
|
||||
# Incomplete length data, wait for more or fail
|
||||
while len(length_data) < 2:
|
||||
chunk = sock.recv(2 - len(length_data))
|
||||
if not chunk:
|
||||
return
|
||||
length_data += chunk
|
||||
|
||||
length = int.from_bytes(length_data, byteorder="big")
|
||||
wire = b""
|
||||
while len(wire) < length:
|
||||
chunk = sock.recv(length - len(wire))
|
||||
if not chunk:
|
||||
return # connection closed
|
||||
wire += chunk
|
||||
self._handle_dns_query(wire)
|
||||
|
||||
except socket.timeout:
|
||||
logger.debug(f"Connection from {peer} timed out")
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling request from {peer}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
Reference in new issue
Block a user