|
|
|
@@ -22,9 +22,10 @@ from netbox_dns.choices import ZoneStatusChoices, RecordStatusChoices
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from netbox_plugin_bind_provisioner.models import IntegerKeyValueSetting
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger('bind-transfer-endpoint')
|
|
|
|
|
logger = logging.getLogger("bind-transfer-endpoint")
|
|
|
|
|
|
|
|
|
|
class CatalogZone():
|
|
|
|
|
|
|
|
|
|
class CatalogZone:
|
|
|
|
|
lock = threading.Lock()
|
|
|
|
|
_serial_max = 0xFFFFFFFF
|
|
|
|
|
_serial_obj = None
|
|
|
|
@@ -35,12 +36,19 @@ class CatalogZone():
|
|
|
|
|
@classmethod
|
|
|
|
|
def initSerial(cls):
|
|
|
|
|
try:
|
|
|
|
|
cls._serial_obj = IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial")
|
|
|
|
|
logger.debug(f"Catalog zone SOA serial number {cls._serial_obj.value} loaded from database")
|
|
|
|
|
cls._serial_obj = IntegerKeyValueSetting.objects.get(
|
|
|
|
|
key="catalog-zone-soa-serial"
|
|
|
|
|
)
|
|
|
|
|
logger.debug(
|
|
|
|
|
f"Catalog zone SOA serial number {cls._serial_obj.value} loaded from database"
|
|
|
|
|
)
|
|
|
|
|
except IntegerKeyValueSetting.DoesNotExist:
|
|
|
|
|
cls._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 {cls._serial_obj.value}")
|
|
|
|
|
|
|
|
|
|
cls._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 {cls._serial_obj.value}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _incrementSerial(cls):
|
|
|
|
@@ -53,8 +61,9 @@ class CatalogZone():
|
|
|
|
|
)
|
|
|
|
|
cls._serial_obj = 1
|
|
|
|
|
cls._serial_obj.save()
|
|
|
|
|
logger.debug(f"Catalog zone SOA serial number incremented to {_serial_obj.value}")
|
|
|
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
f"Catalog zone SOA serial number incremented to {_serial_obj.value}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def create(cls, name, view_name):
|
|
|
|
@@ -63,9 +72,13 @@ class CatalogZone():
|
|
|
|
|
# cls = self.__class__
|
|
|
|
|
# datestamp = datetime.now().strftime("%y%m%d")
|
|
|
|
|
# latest_zone = Zone.objects.filter().order_by("-last_updated").first()
|
|
|
|
|
latest_zone = Zone.objects.filter(status=ZoneStatusChoices.STATUS_ACTIVE).order_by('-last_updated').first()
|
|
|
|
|
latest_zone = (
|
|
|
|
|
Zone.objects.filter(status=ZoneStatusChoices.STATUS_ACTIVE)
|
|
|
|
|
.order_by("-last_updated")
|
|
|
|
|
.first()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
last_zone_update = getattr(latest_zone, 'last_updated', None)
|
|
|
|
|
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
|
|
|
|
@@ -110,19 +123,21 @@ class CatalogZone():
|
|
|
|
|
# Configure DNSSec Policy for member Zone if DNSSec is enabled
|
|
|
|
|
if nb_zone.dnssec_policy:
|
|
|
|
|
# Configure policy
|
|
|
|
|
rid = dns.name.from_text(f"policy.dnssec", ptr_name)
|
|
|
|
|
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, nb_zone.dnssec_policy.name
|
|
|
|
|
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(f"enabled.dnssec", 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)
|
|
|
|
|
## 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 = 3600
|
|
|
|
@@ -151,7 +166,6 @@ class CatalogZone():
|
|
|
|
|
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(
|
|
|
|
@@ -187,10 +201,14 @@ class CatalogZone():
|
|
|
|
|
serial = int(f.read().strip())
|
|
|
|
|
cls._serial_obj.value = int(serial)
|
|
|
|
|
cls._serial_obj.save()
|
|
|
|
|
logger.info(f"Legacy catalog serial file {file_path} imported into database and file removed. Serial is now {serial}")
|
|
|
|
|
logger.info(
|
|
|
|
|
f"Legacy catalog serial file {file_path} imported into database and file removed. Serial is now {serial}"
|
|
|
|
|
)
|
|
|
|
|
os.remove(file_path)
|
|
|
|
|
except OSError as e:
|
|
|
|
|
logger.error(f"Failed to import catalog zone serial from legacy file {file_path}")
|
|
|
|
|
logger.error(
|
|
|
|
|
f"Failed to import catalog zone serial from legacy file {file_path}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
|
|
|
@@ -199,7 +217,9 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
|
|
|
|
# Find the zone
|
|
|
|
|
try:
|
|
|
|
|
nb_zone = Zone.objects.get(
|
|
|
|
|
name=zone_name, view__name=view_name, status=ZoneStatusChoices.STATUS_ACTIVE
|
|
|
|
|
name=zone_name,
|
|
|
|
|
view__name=view_name,
|
|
|
|
|
status=ZoneStatusChoices.STATUS_ACTIVE,
|
|
|
|
|
)
|
|
|
|
|
except Zone.DoesNotExist:
|
|
|
|
|
return None
|
|
|
|
@@ -226,10 +246,21 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
|
|
|
|
# If the record has no TTL, use the zone default
|
|
|
|
|
ttl = record.ttl or nb_zone.default_ttl
|
|
|
|
|
|
|
|
|
|
# Apply special quoting for TXT records to stop tokanizer
|
|
|
|
|
# from cutting it up:
|
|
|
|
|
if rdtype == dns.rdatatype.TXT:
|
|
|
|
|
if len(record.value) <= 255:
|
|
|
|
|
record_value = f'"{record.value}"'
|
|
|
|
|
else:
|
|
|
|
|
chunks = ['"{}"'.format(record.value[i:i+255]) for i in range(0, len(record.value), 255)]
|
|
|
|
|
record_value = " ".join(chunks)
|
|
|
|
|
else:
|
|
|
|
|
record_value = record.value
|
|
|
|
|
|
|
|
|
|
rdata = dns.rdata.from_text(
|
|
|
|
|
dns.rdataclass.IN,
|
|
|
|
|
rdtype,
|
|
|
|
|
record.value,
|
|
|
|
|
record_value, #asd
|
|
|
|
|
relativize=False,
|
|
|
|
|
origin=zone.origin,
|
|
|
|
|
)
|
|
|
|
@@ -238,7 +269,9 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
|
|
|
|
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)
|
|
|
|
|
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)
|
|
|
|
@@ -248,7 +281,9 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
|
|
|
|
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}")
|
|
|
|
|
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:
|
|
|
|
@@ -262,7 +297,9 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
|
|
|
|
|
|
|
|
|
def denyRequestBadTSIG(self, wire, tsig_error: dns.rcode):
|
|
|
|
|
# Use empty keyring to parse TSIG without validating
|
|
|
|
|
query = dns.message.from_wire(wire, keyring={}, ignore_trailing=True, continue_on_error=True)
|
|
|
|
|
query = dns.message.from_wire(
|
|
|
|
|
wire, keyring={}, ignore_trailing=True, continue_on_error=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Make a response
|
|
|
|
|
response = dns.message.make_response(query)
|
|
|
|
@@ -273,7 +310,7 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
|
|
|
|
response.use_tsig(
|
|
|
|
|
keyring={}, # empty; we're not signing
|
|
|
|
|
keyname=query.keyname,
|
|
|
|
|
tsig_error=tsig_error
|
|
|
|
|
tsig_error=tsig_error,
|
|
|
|
|
)
|
|
|
|
|
self.denyRequest(query)
|
|
|
|
|
|
|
|
|
@@ -296,7 +333,7 @@ class UDPRequestHandler(DNSBaseRequestHandler):
|
|
|
|
|
data,
|
|
|
|
|
keyring=self.server.keyring,
|
|
|
|
|
continue_on_error=False,
|
|
|
|
|
ignore_trailing=True
|
|
|
|
|
ignore_trailing=True,
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error parsing query: ", e)
|
|
|
|
@@ -317,7 +354,9 @@ class UDPRequestHandler(DNSBaseRequestHandler):
|
|
|
|
|
|
|
|
|
|
# Only process SOA queries
|
|
|
|
|
if qtype != dns.rdatatype.SOA:
|
|
|
|
|
logger.warning(f"Request denied from {peer}: Request was not SOA (Type: {qtype})")
|
|
|
|
|
logger.warning(
|
|
|
|
|
f"Request denied from {peer}: Request was not SOA (Type: {qtype})"
|
|
|
|
|
)
|
|
|
|
|
self.denyRequest(query)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
@@ -332,7 +371,9 @@ class UDPRequestHandler(DNSBaseRequestHandler):
|
|
|
|
|
# Check if 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")
|
|
|
|
|
logger.warning(
|
|
|
|
|
f"Request denied from {peer}: {key_name} does not match a view"
|
|
|
|
|
)
|
|
|
|
|
self.denyRequestBadTSIG(wire, dns.rcode.BADKEY)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
@@ -364,7 +405,9 @@ class UDPRequestHandler(DNSBaseRequestHandler):
|
|
|
|
|
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)
|
|
|
|
|
response.use_tsig(
|
|
|
|
|
self.server.keyring, keyname=query.keyname, original_id=query.id
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
# If key is unknown
|
|
|
|
|
response.set_rcode(dns.rcode.REFUSED)
|
|
|
|
@@ -372,7 +415,11 @@ class UDPRequestHandler(DNSBaseRequestHandler):
|
|
|
|
|
# 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)
|
|
|
|
|
response.use_tsig(
|
|
|
|
|
self.server.keyring,
|
|
|
|
|
keyname=query.keyname,
|
|
|
|
|
tsig_error=dns.rcode.BADKEY,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Send back the response
|
|
|
|
|
response_wire = response.to_wire(max_size=512)
|
|
|
|
@@ -416,7 +463,9 @@ class TCPRequestHandler(DNSBaseRequestHandler):
|
|
|
|
|
query = dns.message.from_wire(wire, keyring=self.server.keyring)
|
|
|
|
|
# If TSIG signature doesnt match our key, refuse query:
|
|
|
|
|
except dns.tsig.BadSignature as e:
|
|
|
|
|
logger.warning(f"Request denied from {peer} failed TSIG verification: {e}")
|
|
|
|
|
logger.warning(
|
|
|
|
|
f"Request denied from {peer} failed TSIG verification: {e}"
|
|
|
|
|
)
|
|
|
|
|
self.denyRequestBadTSIG(wire, dns.rcode.BADSIG)
|
|
|
|
|
return
|
|
|
|
|
# If TSIG Key used is not in our list, refuse query:
|
|
|
|
@@ -439,7 +488,9 @@ class TCPRequestHandler(DNSBaseRequestHandler):
|
|
|
|
|
# Only process AXFR queries
|
|
|
|
|
if qtype != dns.rdatatype.AXFR:
|
|
|
|
|
# if qtype != dns.rdatatype.AXFR or qtype != dns.rdatatype.SOA:
|
|
|
|
|
logger.warning(f"Request denied from {peer}: Request was not AXFR (Type: {qtype})")
|
|
|
|
|
logger.warning(
|
|
|
|
|
f"Request denied from {peer}: Request was not AXFR (Type: {qtype})"
|
|
|
|
|
)
|
|
|
|
|
self.denyRequest(query)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
@@ -454,7 +505,9 @@ class TCPRequestHandler(DNSBaseRequestHandler):
|
|
|
|
|
# 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")
|
|
|
|
|
logger.warning(
|
|
|
|
|
f"Request denied from {peer}: {key_name} does not match a view"
|
|
|
|
|
)
|
|
|
|
|
self.denyRequest(query)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
@@ -491,7 +544,10 @@ class TCPRequestHandler(DNSBaseRequestHandler):
|
|
|
|
|
|
|
|
|
|
# 2. Create a Renderer for the first message
|
|
|
|
|
r = dns.renderer.Renderer(
|
|
|
|
|
id=query.id, flags=(dns.flags.QR | dns.flags.AA), max_size=MAX_WIRE, origin=None
|
|
|
|
|
id=query.id,
|
|
|
|
|
flags=(dns.flags.QR | dns.flags.AA),
|
|
|
|
|
max_size=MAX_WIRE,
|
|
|
|
|
origin=None,
|
|
|
|
|
)
|
|
|
|
|
r.add_question(
|
|
|
|
|
query.question[0].name,
|
|
|
|
@@ -526,7 +582,10 @@ class TCPRequestHandler(DNSBaseRequestHandler):
|
|
|
|
|
|
|
|
|
|
# Start new renderer
|
|
|
|
|
r = dns.renderer.Renderer(
|
|
|
|
|
id=query.id, flags=(dns.flags.QR | dns.flags.AA), max_size=MAX_WIRE, origin=None
|
|
|
|
|
id=query.id,
|
|
|
|
|
flags=(dns.flags.QR | dns.flags.AA),
|
|
|
|
|
max_size=MAX_WIRE,
|
|
|
|
|
origin=None,
|
|
|
|
|
)
|
|
|
|
|
r.add_question(
|
|
|
|
|
query.question[0].name,
|
|
|
|
@@ -558,8 +617,8 @@ class TCPRequestHandler(DNSBaseRequestHandler):
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error handling request from {peer}: {e}")
|
|
|
|
|
import traceback
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TCPDNSServer(socketserver.TCPServer):
|
|
|
|
@@ -571,7 +630,6 @@ class TCPDNSServer(socketserver.TCPServer):
|
|
|
|
|
self.tsig_view_map = tsig_view_map
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UDPDNSServer(socketserver.UDPServer):
|
|
|
|
|
allow_reuse_address = True
|
|
|
|
|
|
|
|
|
@@ -581,12 +639,13 @@ class UDPDNSServer(socketserver.UDPServer):
|
|
|
|
|
self.tsig_view_map = tsig_view_map
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Command(BaseCommand):
|
|
|
|
|
help = "Run a minimal AXFR DNS server using NetBox DNS plugin data"
|
|
|
|
|
|
|
|
|
|
def loadSettings(self):
|
|
|
|
|
self.settings = settings.PLUGINS_CONFIG.get("netbox_plugin_bind_provisioner", None)
|
|
|
|
|
self.settings = settings.PLUGINS_CONFIG.get(
|
|
|
|
|
"netbox_plugin_bind_provisioner", None
|
|
|
|
|
)
|
|
|
|
|
if not self.settings:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"Command failed to initialize due to missing settings. Terminating Netbox."
|
|
|
|
@@ -594,9 +653,7 @@ class Command(BaseCommand):
|
|
|
|
|
|
|
|
|
|
self.tsig_keys = self.settings.get("tsig_keys", None)
|
|
|
|
|
if not self.tsig_keys:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"tsig_keys variable not set in plugin settings."
|
|
|
|
|
)
|
|
|
|
|
raise RuntimeError("tsig_keys variable not set in plugin settings.")
|
|
|
|
|
|
|
|
|
|
# Load TSIG keys and map them to views
|
|
|
|
|
def loadTSIGKeySettings(self):
|
|
|
|
@@ -640,13 +697,17 @@ class Command(BaseCommand):
|
|
|
|
|
raise RuntimeError(msg)
|
|
|
|
|
|
|
|
|
|
def add_arguments(self, parser):
|
|
|
|
|
parser.add_argument('--port', type=int, default=5354, help='Port number to listen on')
|
|
|
|
|
parser.add_argument('--address', type=str, default="0.0.0.0", help='IP to bind to')
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--port", type=int, default=5354, help="Port number to listen on"
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--address", type=str, default="0.0.0.0", help="IP to bind to"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def handle(self, *args, **options):
|
|
|
|
|
# Load parameters
|
|
|
|
|
port = options['port']
|
|
|
|
|
address = options['address']
|
|
|
|
|
port = options["port"]
|
|
|
|
|
address = options["address"]
|
|
|
|
|
CatalogZone.initSerial()
|
|
|
|
|
# Initialize settings
|
|
|
|
|
self.loadSettings()
|
|
|
|
@@ -657,16 +718,21 @@ class Command(BaseCommand):
|
|
|
|
|
if serial_file:
|
|
|
|
|
CatalogZone.importLegacySerialFile(serial_file)
|
|
|
|
|
|
|
|
|
|
udp_server = UDPDNSServer((address, port), UDPRequestHandler, self.keyring, self.tsig_view_map)
|
|
|
|
|
tcp_server = TCPDNSServer((address, port), TCPRequestHandler, self.keyring, self.tsig_view_map)
|
|
|
|
|
udp_server = UDPDNSServer(
|
|
|
|
|
(address, port), UDPRequestHandler, self.keyring, self.tsig_view_map
|
|
|
|
|
)
|
|
|
|
|
tcp_server = TCPDNSServer(
|
|
|
|
|
(address, port), TCPRequestHandler, self.keyring, self.tsig_view_map
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def run_udp_server(server):
|
|
|
|
|
logger.debug(f"SOA endpoint listening on {address} udp/{port}")
|
|
|
|
|
server.serve_forever()
|
|
|
|
|
|
|
|
|
|
udp_thread = threading.Thread(target=run_udp_server, args=(udp_server,), daemon=True)
|
|
|
|
|
udp_thread = threading.Thread(
|
|
|
|
|
target=run_udp_server, args=(udp_server,), daemon=True
|
|
|
|
|
)
|
|
|
|
|
udp_thread.start()
|
|
|
|
|
|
|
|
|
|
logger.debug(f"AXFR endpoint listening on {address} tcp/{port}")
|
|
|
|
|
tcp_server.serve_forever()
|
|
|
|
|
|