modified: netbox_dns_bridge/__init__.py
deleted: netbox_dns_bridge/logger.py modified: netbox_dns_bridge/management/commands/dns-transfer-endpoint.py new file: netbox_dns_bridge/management/commands/dns_transfer_endpoint/__init__.py renamed: netbox_dns_bridge/catalog_zone_manager.py -> netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py renamed: netbox_dns_bridge/dns_server.py -> netbox_dns_bridge/management/commands/dns_transfer_endpoint/dns_server.py renamed: netbox_dns_bridge/request_handler.py -> netbox_dns_bridge/management/commands/dns_transfer_endpoint/request_handler.py modified: netbox_dns_bridge/signals/netbox_dns_zones.py renamed: netbox_dns_bridge/notify_handler.py -> netbox_dns_bridge/signals/notify_handler.py modified: netbox_dns_bridge/utils.py
This commit is contained in:
1 parent
0ac23ff3a0
commit
ac34afc46c
10 files changed
+25
-37
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,182 @@
|
||||
import dns.name
|
||||
import dns.zone
|
||||
import dns.rdatatype
|
||||
import dns.rdataclass
|
||||
from netbox_dns_bridge.utils import get_logger
|
||||
from netbox_dns.models import Zone as NBZone
|
||||
from netbox_dns_bridge.models import IntegerKeyValueSetting, CatalogZoneMemberIdentifier
|
||||
from uuid import uuid4
|
||||
from base64 import b32encode
|
||||
from django.db import transaction, close_old_connections, OperationalError
|
||||
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
def increment_soa_serial() -> int:
|
||||
with transaction.atomic():
|
||||
try:
|
||||
(
|
||||
soa_serial_obj,
|
||||
_,
|
||||
) = IntegerKeyValueSetting.objects.select_for_update().get_or_create(
|
||||
key="catalog-zone-soa-serial",
|
||||
defaults={"value": 1},
|
||||
)
|
||||
|
||||
new_soa_serial = soa_serial_obj.value + 1
|
||||
if new_soa_serial > 0xFFFFFFFF:
|
||||
LOGGER.warning(
|
||||
f"Catalog serial {soa_serial_obj.value} reached max — wrapping back to 1"
|
||||
)
|
||||
new_soa_serial = 1
|
||||
soa_serial_obj.value = new_soa_serial
|
||||
soa_serial_obj.save()
|
||||
soa_serial_obj.refresh_from_db()
|
||||
LOGGER.debug(
|
||||
f"Catalog zone SOA serial number is now {soa_serial_obj.value}"
|
||||
)
|
||||
|
||||
return soa_serial_obj.value
|
||||
except OperationalError as e:
|
||||
LOGGER.error(
|
||||
f"ERROR: Failed to increment the Catalog Zone's SOA serial: {e}"
|
||||
)
|
||||
close_old_connections()
|
||||
|
||||
|
||||
def create_zone(name, view_name) -> dns.zone.Zone:
|
||||
# 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
|
||||
|
||||
try:
|
||||
# get zones from netbox
|
||||
nb_zones = NBZone.objects.filter(
|
||||
view__name=view_name, active=True
|
||||
).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
|
||||
try:
|
||||
catz_identifier = nb_zone.catz_identifier
|
||||
except NBZone.catz_identifier.RelatedObjectDoesNotExist:
|
||||
catz_identifier = CatalogZoneMemberIdentifier.objects.create(
|
||||
zone=nb_zone, name=_generate_member_identifier()
|
||||
)
|
||||
|
||||
p_name = catz_identifier.name
|
||||
|
||||
ptr_name = dns.name.from_text(p_name, ptr_base)
|
||||
if not ptr_name.is_subdomain(origin):
|
||||
raise ValueError(
|
||||
f"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:
|
||||
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)
|
||||
|
||||
zone_root_node = zone.find_node(origin, create=True)
|
||||
|
||||
# SOA record
|
||||
soa_rdataset = _create_soa_rdataset()
|
||||
zone_root_node.rdatasets.append(soa_rdataset)
|
||||
|
||||
# NS record
|
||||
ns_rdataset = _create_ns_rdataset()
|
||||
zone_root_node.rdatasets.append(ns_rdataset)
|
||||
|
||||
# version.catz. record
|
||||
version_rdataset = _create_version_rdataset()
|
||||
zone_version_node = zone.find_node(
|
||||
dns.name.from_text("version", origin), create=True
|
||||
)
|
||||
zone_version_node.rdatasets.append(version_rdataset)
|
||||
|
||||
return zone
|
||||
|
||||
except OperationalError as e:
|
||||
LOGGER.error(f"ERROR: Failed to create Catalog Zone: {e}")
|
||||
close_old_connections()
|
||||
return None
|
||||
|
||||
|
||||
def _generate_member_identifier() -> str:
|
||||
return b32encode(uuid4().bytes)[0:26].lower().decode("UTF-8")
|
||||
|
||||
|
||||
def update_member_identifier(zone: NBZone) -> None:
|
||||
try:
|
||||
CatalogZoneMemberIdentifier.objects.update_or_create(
|
||||
zone=zone,
|
||||
defaults={"name": _generate_member_identifier()},
|
||||
)
|
||||
except OperationalError as e:
|
||||
LOGGER.error(f"ERROR: Failed to update Catalog Zone member identifier: {e}")
|
||||
close_old_connections()
|
||||
|
||||
|
||||
def _create_soa_rdataset() -> dns.rdataset:
|
||||
try:
|
||||
serial_obj, _ = IntegerKeyValueSetting.objects.get_or_create(
|
||||
key="catalog-zone-soa-serial", defaults={"value": 1}
|
||||
)
|
||||
serial = serial_obj.value
|
||||
|
||||
# SOA Record components
|
||||
ttl = 0
|
||||
rclass = dns.rdataclass.IN
|
||||
rtype = dns.rdatatype.SOA
|
||||
|
||||
# Create SOA rdata object
|
||||
rdata = dns.rdata.from_text(
|
||||
rclass,
|
||||
rtype,
|
||||
f"invalid. invalid. {serial} 60 10 1209600 0",
|
||||
)
|
||||
|
||||
# Create Rdataset and add the RDATA to it
|
||||
rdataset = dns.rdataset.Rdataset(rclass, rtype)
|
||||
rdataset.add(rdata, ttl)
|
||||
return rdataset
|
||||
|
||||
except OperationalError as e:
|
||||
LOGGER.error(f"ERROR: Failed to access catz SOA record in database: {e}")
|
||||
close_old_connections()
|
||||
|
||||
|
||||
def _create_ns_rdataset() -> dns.rdataset:
|
||||
# NS record for catz.
|
||||
name = dns.name.from_text("invalid", dns.name.root)
|
||||
rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, str(name))
|
||||
rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
|
||||
rdataset.add(rdata, 0)
|
||||
return rdataset
|
||||
|
||||
|
||||
def _create_version_rdataset() -> dns.rdataset:
|
||||
rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"2"')
|
||||
rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.TXT)
|
||||
rdataset.add(rdata, 0)
|
||||
return rdataset
|
||||
@@ -0,0 +1,46 @@
|
||||
import socketserver
|
||||
import socket
|
||||
from netbox_dns_bridge.utils import get_logger
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
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,382 @@
|
||||
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 django.utils import timezone
|
||||
from django.db import close_old_connections, OperationalError
|
||||
from django.conf import settings
|
||||
from netbox_dns.models import Zone, View
|
||||
from . import catalog_zone_manager as catzm
|
||||
from netbox_dns_bridge.models import SeenTransferClients
|
||||
from netbox_dns_bridge.utils import build_dns_zone, get_logger
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
SETTINGS = settings.PLUGINS_CONFIG.get("netbox_dns_bridge", {})
|
||||
|
||||
|
||||
class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
||||
transport = None
|
||||
|
||||
def __init__(self, request, client_address, server) -> None:
|
||||
self.MAX_WIRE = 65535
|
||||
self.RESERVED_TSIG = 300
|
||||
super().__init__(request, client_address, server)
|
||||
|
||||
# _get_zone_from_nb rewritten
|
||||
def _get_zone_from_nb(self, zone_name, view_name) -> dns.zone.Zone:
|
||||
# Find the zone
|
||||
try:
|
||||
nb_zone = Zone.objects.get(
|
||||
name=zone_name,
|
||||
view__name=view_name,
|
||||
active=True,
|
||||
)
|
||||
return build_dns_zone(nb_zone)
|
||||
except Zone.DoesNotExist:
|
||||
return None
|
||||
except OperationalError as e:
|
||||
LOGGER.error(f"DB ERROR: {e}")
|
||||
close_old_connections()
|
||||
|
||||
def _deny_request_bad_tsig(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 error code, but do not sign (no MAC, empty keyring)
|
||||
response.use_tsig(
|
||||
keyring={}, # empty; we're not signing
|
||||
keyname=query.keyname,
|
||||
tsig_error=tsig_error,
|
||||
)
|
||||
self._send_response(response.to_wire())
|
||||
|
||||
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 _track_seen_client(self, source_ip: str, view: View) -> None:
|
||||
if SETTINGS.get("notify_clients", False):
|
||||
try:
|
||||
SeenTransferClients.objects.update_or_create(
|
||||
source_ip=source_ip,
|
||||
view=view,
|
||||
defaults={"last_seen": timezone.now()},
|
||||
)
|
||||
|
||||
except OperationalError as e:
|
||||
LOGGER.error(f"DB ERROR: {e}")
|
||||
close_old_connections()
|
||||
|
||||
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:
|
||||
# unknown key — use an empty keyring so use_tsig() does not
|
||||
# raise KeyError trying to look up a key that isn't present.
|
||||
response.set_rcode(dns.rcode.REFUSED)
|
||||
response.use_tsig(
|
||||
{}, # empty keyring; no signing
|
||||
keyname=query.keyname,
|
||||
tsig_error=dns.rcode.BADKEY,
|
||||
)
|
||||
|
||||
data = response.to_wire(max_size=512)
|
||||
self._send_response(data)
|
||||
LOGGER.info(f"{peer} SOA {nb_view.name}/{dname}")
|
||||
|
||||
def _handle_axfr_request(self, query, zone, peer, nb_view, dname) -> None:
|
||||
if query.keyname not in self.server.keyring:
|
||||
LOGGER.error(f"AXFR aborted: keyname {query.keyname} not in keyring")
|
||||
self._deny_request(query)
|
||||
return
|
||||
|
||||
tsig_key = self.server.keyring[query.keyname]
|
||||
|
||||
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)
|
||||
|
||||
if soa_rrset is None:
|
||||
LOGGER.error(f"Zone {dname} has no SOA — aborting")
|
||||
return
|
||||
|
||||
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:
|
||||
try:
|
||||
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=tsig_key.secret,
|
||||
algorithm=tsig_key.algorithm,
|
||||
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,
|
||||
)
|
||||
|
||||
try:
|
||||
r.add_rrset(dns.renderer.ANSWER, rrset)
|
||||
except dns.exception.TooBig:
|
||||
LOGGER.error(
|
||||
f"RRset {rrset.name}/{dns.rdatatype.to_text(rrset.rdtype)} "
|
||||
f"exceeds MAX_WIRE ({self.MAX_WIRE}) and cannot be sent; "
|
||||
f"skipping."
|
||||
)
|
||||
|
||||
# 4. Final message with terminating TSIG
|
||||
r.write_header()
|
||||
tsig_ctx = r.add_multi_tsig(
|
||||
ctx=tsig_ctx,
|
||||
secret=tsig_key.secret,
|
||||
algorithm=tsig_key.algorithm,
|
||||
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._send_response(wire)
|
||||
LOGGER.info(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._deny_request_bad_tsig(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._deny_request_bad_tsig(wire, dns.rcode.BADKEY)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
LOGGER.error(f"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
|
||||
|
||||
if qtype == dns.rdatatype.AXFR and self.transport != "tcp":
|
||||
LOGGER.warning(f"Request denied from {peer}: AXFR requires TCP")
|
||||
self._deny_request(query, dns.rcode.REFUSED)
|
||||
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
|
||||
dname_lower = dname.lower()
|
||||
if dname_lower == "catz" or dname_lower == f"{nb_view.name.lower()}.catz":
|
||||
zone = catzm.create_zone(dname_lower, nb_view.name)
|
||||
else:
|
||||
zone = self._get_zone_from_nb(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)
|
||||
self._track_seen_client(peer, nb_view)
|
||||
elif qtype == dns.rdatatype.AXFR:
|
||||
self._handle_axfr_request(query, zone, peer, nb_view, dname)
|
||||
self._track_seen_client(peer, nb_view)
|
||||
|
||||
|
||||
class UDPRequestHandler(DNSBaseRequestHandler):
|
||||
transport = "udp"
|
||||
|
||||
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):
|
||||
transport = "tcp"
|
||||
|
||||
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