netbox-plugin-dns-bridge 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. build/lib/netbox_dns_bridge/__init__.py +36 -0
  2. build/lib/netbox_dns_bridge/catalog_zone_manager.py +234 -0
  3. build/lib/netbox_dns_bridge/dns_server.py +46 -0
  4. build/lib/netbox_dns_bridge/management/commands/dns-export-zone.py +60 -0
  5. build/lib/netbox_dns_bridge/management/commands/dns-export-zones.py +52 -0
  6. build/lib/netbox_dns_bridge/management/commands/dns-settings.py +51 -0
  7. build/lib/netbox_dns_bridge/management/commands/dns-transfer-endpoint.py +117 -0
  8. build/lib/netbox_dns_bridge/migrations/0001_initial.py +58 -0
  9. build/lib/netbox_dns_bridge/models.py +33 -0
  10. build/lib/netbox_dns_bridge/request_handler.py +412 -0
  11. build/lib/netbox_dns_bridge/signals.py +37 -0
  12. build/lib/netbox_dns_bridge/utils.py +35 -0
  13. netbox_dns_bridge/__init__.py +36 -0
  14. netbox_dns_bridge/catalog_zone_manager.py +234 -0
  15. netbox_dns_bridge/dns_server.py +46 -0
  16. netbox_dns_bridge/management/commands/dns-export-zone.py +60 -0
  17. netbox_dns_bridge/management/commands/dns-export-zones.py +52 -0
  18. netbox_dns_bridge/management/commands/dns-settings.py +51 -0
  19. netbox_dns_bridge/management/commands/dns-transfer-endpoint.py +117 -0
  20. netbox_dns_bridge/migrations/0001_initial.py +58 -0
  21. netbox_dns_bridge/models.py +33 -0
  22. netbox_dns_bridge/request_handler.py +412 -0
  23. netbox_dns_bridge/signals.py +37 -0
  24. netbox_dns_bridge/utils.py +35 -0
  25. netbox_plugin_dns_bridge-1.5.0.dist-info/METADATA +286 -0
  26. netbox_plugin_dns_bridge-1.5.0.dist-info/RECORD +29 -0
  27. netbox_plugin_dns_bridge-1.5.0.dist-info/WHEEL +5 -0
  28. netbox_plugin_dns_bridge-1.5.0.dist-info/licenses/LICENSE.md +18 -0
  29. netbox_plugin_dns_bridge-1.5.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,36 @@
1
+ import logging
2
+ from netbox.plugins import PluginConfig
3
+ from django.conf import settings
4
+
5
+ __version__ = "1.5.0"
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ logging.basicConfig(
10
+ level=logging.DEBUG, # Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
11
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", # Log format
12
+ datefmt="%Y-%m-%d %H:%M:%S", # Date format for timestamps
13
+ )
14
+
15
+
16
+ class DNSBridgeConfig(PluginConfig):
17
+ name = "netbox_dns_bridge"
18
+ verbose_name = "Netbox DNS Bridge"
19
+ description = ""
20
+ version = __version__
21
+ author = "Sven Luethi"
22
+ author_email = "dev@sven.luethi.co"
23
+ base_url = "dns-bridge"
24
+
25
+ def ready(self):
26
+ self.settings = settings.PLUGINS_CONFIG.get(self.name, None)
27
+ if not self.settings:
28
+ raise RuntimeError(
29
+ f"{self.name}: Plugin {self.verbose_name} failed to initialize due to missing settings. Terminating Netbox."
30
+ )
31
+
32
+ from . import signals as signals
33
+
34
+
35
+ config = DNSBridgeConfig
36
+ default_app_config = ".apps.NetboxDNSBridgeConfig"
@@ -0,0 +1,234 @@
1
+ import logging
2
+ import threading
3
+ import dns.query
4
+ import dns.message
5
+ import dns.tsigkeyring
6
+ import dns.name
7
+ import dns.zone
8
+ import dns.rdatatype
9
+ import dns.rdataclass
10
+ import dns.rdtypes
11
+ import dns.exception
12
+ import dns.renderer
13
+ from netbox_dns.models import Zone
14
+ from netbox_dns.choices import ZoneStatusChoices
15
+ from netbox_dns_bridge.models import IntegerKeyValueSetting, CatalogZoneMemberIdentifier
16
+ from uuid import uuid4
17
+ from base64 import b32encode
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ _LOCK = threading.Lock()
22
+ _SERIAL_MAX = 0xFFFFFFFF
23
+ _SERIAL_OBJ = None
24
+ _PREVIOUS_LAST_ZONE_UPDATE = None
25
+
26
+
27
+ def init() -> None:
28
+ _init_serial()
29
+ _create_missing_member_identifiers()
30
+
31
+
32
+ # Following function loads the last serial from the DB. No return value
33
+ # but it sets the setting "catalog_serial" or terminates the plugin on failure.
34
+ def _init_serial() -> None:
35
+ global _SERIAL_OBJ
36
+
37
+ try:
38
+ _SERIAL_OBJ = IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial")
39
+ logger.debug(
40
+ f"Catalog zone SOA serial number {_SERIAL_OBJ.value} loaded from database"
41
+ )
42
+ except IntegerKeyValueSetting.DoesNotExist:
43
+ _SERIAL_OBJ = IntegerKeyValueSetting.objects.create(
44
+ key="catalog-zone-soa-serial", value=1
45
+ )
46
+ logger.debug(
47
+ f"Catalog zone SOA serial number was not set in the database. Set to {_SERIAL_OBJ.value}"
48
+ )
49
+
50
+
51
+ def _set_serial(new_serial: int) -> bool:
52
+ global _SERIAL_OBJ
53
+
54
+ if 0 < new_serial < _SERIAL_MAX:
55
+ _SERIAL_OBJ.value = new_serial
56
+ _SERIAL_OBJ.save()
57
+ return True
58
+ else:
59
+ return False
60
+
61
+
62
+ def _increment_serial() -> None:
63
+ if not _set_serial(_SERIAL_OBJ.value + 1):
64
+ logger.warning(
65
+ f"Failed to incremenet catalog serial {_SERIAL_OBJ.value}. Will overflow serial back to 1"
66
+ )
67
+ _set_serial(1)
68
+ logger.debug(
69
+ f"Catalog zone SOA serial number incremented to {_SERIAL_OBJ.value}"
70
+ )
71
+
72
+
73
+ # If a zone has no catz identifier yet, create it:
74
+ def _create_missing_member_identifiers() -> None:
75
+ existing_zone_ids = CatalogZoneMemberIdentifier.objects.values_list(
76
+ "zone_id", flat=True
77
+ )
78
+
79
+ missing_zones = Zone.objects.exclude(id__in=existing_zone_ids)
80
+
81
+ new_objects = [
82
+ CatalogZoneMemberIdentifier(
83
+ zone=zone,
84
+ name=_generate_member_identifier(),
85
+ )
86
+ for zone in missing_zones
87
+ ]
88
+
89
+ for identifier in new_objects:
90
+ logger.debug(
91
+ f"Zone {identifier.zone} has no catz member identifier. Creating..."
92
+ )
93
+
94
+ CatalogZoneMemberIdentifier.objects.bulk_create(
95
+ new_objects,
96
+ ignore_conflicts=False,
97
+ )
98
+
99
+
100
+ def create_zone(name, view_name) -> dns.zone.Zone:
101
+ global _PREVIOUS_LAST_ZONE_UPDATE
102
+ # Synchronize following across threads as TCP and UDP listener both use it.
103
+ with _LOCK:
104
+ latest_zone = (
105
+ Zone.objects.filter(status=ZoneStatusChoices.STATUS_ACTIVE)
106
+ .order_by("-last_updated")
107
+ .first()
108
+ )
109
+
110
+ last_zone_update = getattr(latest_zone, "last_updated", None)
111
+
112
+ # Check if there was a zone updated since last call
113
+ # If no zone was found previously then this will be false since (None != None) = False
114
+ if _PREVIOUS_LAST_ZONE_UPDATE != last_zone_update:
115
+ if last_zone_update is not None:
116
+ logger.debug(
117
+ f"Zone {latest_zone.name} was updated in view {latest_zone.view.name}"
118
+ )
119
+ # Setting previous last zone update for next iteration:
120
+ _PREVIOUS_LAST_ZONE_UPDATE = last_zone_update
121
+ _increment_serial()
122
+
123
+ # Zone origin
124
+ origin = dns.name.from_text(name, dns.name.root)
125
+
126
+ # Create a new empty zone
127
+ zone = dns.zone.Zone(origin)
128
+ zone.rdclass = dns.rdataclass.IN
129
+
130
+ # get zones from netbox
131
+ nb_zones = Zone.objects.filter(
132
+ view__name=view_name, status=ZoneStatusChoices.STATUS_ACTIVE
133
+ ).select_related("catz_identifier")
134
+
135
+ ptr_base = dns.name.from_text("zones", origin)
136
+
137
+ for nb_zone in nb_zones:
138
+ ttl = 0
139
+ qname = dns.name.from_text(nb_zone.name, dns.name.root)
140
+
141
+ # Create PTR record
142
+ # p_name = f"zid-{nb_zone.id:09d}"
143
+ p_name = nb_zone.catz_identifier.name
144
+
145
+ ptr_name = dns.name.from_text(p_name, ptr_base)
146
+ if not ptr_name.is_subdomain(origin):
147
+ raise ValueError(
148
+ "Catalog zone member identifier {ptr_name.to_text()} not a subdomain"
149
+ )
150
+ rdata = dns.rdata.from_text(
151
+ dns.rdataclass.IN, dns.rdatatype.PTR, qname.to_text()
152
+ )
153
+ rdataset = zone.find_rdataset(ptr_name, dns.rdatatype.PTR, create=True)
154
+ rdataset.add(rdata, ttl)
155
+
156
+ # Configure DNSSec Policy for member Zone if DNSSec is enabled
157
+ if nb_zone.dnssec_policy:
158
+ # Configure policy
159
+ rid = dns.name.from_text("group", ptr_name)
160
+ policy_name = nb_zone.dnssec_policy.name.rstrip(" ")
161
+ group_name = f"dnssec-policy-{policy_name}"
162
+ rdata = dns.rdata.from_text(
163
+ dns.rdataclass.IN, dns.rdatatype.TXT, group_name
164
+ )
165
+ rdataset = zone.find_rdataset(rid, dns.rdatatype.TXT, create=True)
166
+ rdataset.add(rdata, ttl)
167
+
168
+ ## Configure dnssec status for member zone
169
+ # status = str(1 if nb_zone.dnssec_policy else 0)
170
+ # rid = dns.name.from_text("enabled.dnssec.ext", ptr_name)
171
+ # rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, status)
172
+ # rdataset = zone.find_rdataset(rid, dns.rdatatype.TXT, create=True)
173
+ # rdataset.add(rdata, ttl)
174
+
175
+ # SOA Record components
176
+ ttl = 0
177
+ rclass = dns.rdataclass.IN
178
+ rtype = dns.rdatatype.SOA
179
+ mname = dns.name.from_text("invalid", dns.name.root)
180
+ rname = dns.name.from_text("invalid", dns.name.root)
181
+ serial = _SERIAL_OBJ.value
182
+ refresh = 60
183
+ retry = 10
184
+ expire = 1209600
185
+ minimum = 0
186
+
187
+ # Create SOA rdata object
188
+ soa_rdata = dns.rdata.from_text(
189
+ rclass,
190
+ rtype,
191
+ f"{mname} {rname} {serial} {refresh} {retry} {expire} {minimum}",
192
+ )
193
+
194
+ # Create Rdataset and add the RDATA to it
195
+ soa_rdataset = dns.rdataset.Rdataset(rclass, rtype)
196
+ soa_rdataset.add(soa_rdata, ttl)
197
+
198
+ # Add to the origin node in the zone
199
+ node = zone.find_node(origin, create=True)
200
+ node.rdatasets.append(soa_rdataset)
201
+
202
+ # NS record for catz.
203
+ ns_name = dns.name.from_text("invalid", dns.name.root)
204
+ ns_rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, str(ns_name))
205
+ ns_rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
206
+ ns_rdataset.add(ns_rdata, 0)
207
+
208
+ # Add to node (catz. is the origin)
209
+ ns_node = zone.find_node(origin, create=True)
210
+ ns_node.rdatasets.append(ns_rdataset)
211
+
212
+ # TXT record for version.catz.
213
+ version_name = dns.name.from_text("version", origin) # relative to origin "catz."
214
+ txt_rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"2"')
215
+
216
+ txt_rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.TXT)
217
+ txt_rdataset.add(txt_rdata, 0)
218
+
219
+ # Add to node version.catz.
220
+ txt_node = zone.find_node(version_name, create=True)
221
+ txt_node.rdatasets.append(txt_rdataset)
222
+
223
+ return zone
224
+
225
+
226
+ def _generate_member_identifier() -> None:
227
+ return b32encode(uuid4().bytes)[0:26].lower().decode("UTF-8")
228
+
229
+
230
+ def update_member_identifier(zone: Zone) -> None:
231
+ CatalogZoneMemberIdentifier.objects.update_or_create(
232
+ zone=zone,
233
+ defaults={"name": _generate_member_identifier},
234
+ )
@@ -0,0 +1,46 @@
1
+ import logging
2
+ import socketserver
3
+ import socket
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ class DNSAddressMixin:
9
+ def _resolve_address(self, server_address, socktype, proto):
10
+ host, port = server_address
11
+
12
+ infos = socket.getaddrinfo(
13
+ host, port, socket.AF_UNSPEC, socktype, proto, socket.AI_PASSIVE
14
+ )
15
+
16
+ family, _, _, _, sockaddr = infos[0]
17
+ self.address_family = family
18
+ return sockaddr
19
+
20
+
21
+ class TCPDNSServer(DNSAddressMixin, socketserver.TCPServer):
22
+ allow_reuse_address = True
23
+
24
+ def __init__(self, server_address, handler_class, keyring, tsig_view_map):
25
+ sockaddr = self._resolve_address(
26
+ server_address, socket.SOCK_STREAM, socket.IPPROTO_TCP
27
+ )
28
+
29
+ super().__init__(sockaddr, handler_class)
30
+
31
+ self.keyring = keyring
32
+ self.tsig_view_map = tsig_view_map
33
+
34
+
35
+ class UDPDNSServer(DNSAddressMixin, socketserver.UDPServer):
36
+ allow_reuse_address = True
37
+
38
+ def __init__(self, server_address, handler_class, keyring, tsig_view_map):
39
+ sockaddr = self._resolve_address(
40
+ server_address, socket.SOCK_DGRAM, socket.IPPROTO_UDP
41
+ )
42
+
43
+ super().__init__(sockaddr, handler_class)
44
+
45
+ self.keyring = keyring
46
+ self.tsig_view_map = tsig_view_map
@@ -0,0 +1,60 @@
1
+ import os
2
+ import dns.zone
3
+ import dns.rdatatype
4
+ import dns.rdataclass
5
+ import dns.exception
6
+ import netbox_dns.models
7
+ from netbox_dns_bridge.utils import export_bind_zone_file
8
+ from django.core.management.base import BaseCommand, CommandError
9
+
10
+
11
+ class Command(BaseCommand):
12
+
13
+ def add_arguments(self, parser):
14
+ parser.add_argument(
15
+ "--view",
16
+ type=str,
17
+ help="The name of the view the Zone to be exported is in",
18
+ )
19
+ parser.add_argument(
20
+ "--zone", type=str, help="The FQDN of the Zone to be exported"
21
+ )
22
+ parser.add_argument(
23
+ "--file", type=str, help="Path of the zone file to be written"
24
+ )
25
+
26
+ def handle(self, *args, **options):
27
+ # if len(options) < 2:
28
+ # print("export-zone <zone name> <file path>")
29
+ # sys.exit(1)
30
+
31
+ # Load parameters
32
+ view_name = options["view"]
33
+ zone_name = options["zone"]
34
+ file_path = options["file"]
35
+
36
+ if not view_name:
37
+ raise CommandError("No --view parameter given")
38
+ elif not zone_name:
39
+ raise CommandError("No --zone parameter given")
40
+ elif not file_path:
41
+ raise CommandError("No --file parameter given")
42
+
43
+ try:
44
+ # Load the zone from NetBox DNS
45
+ nb_zone = netbox_dns.models.Zone.objects.get(
46
+ view__name=view_name, name=zone_name
47
+ )
48
+
49
+ if nb_zone:
50
+ export_bind_zone_file(nb_zone, file_path=file_path)
51
+ else:
52
+ print("Zone not found in Netbox. Aborting")
53
+ sys.exit(1)
54
+
55
+ except Exception as e:
56
+ raise CommandError(f"Failed to export zone: {e}")
57
+
58
+ self.stdout.write(
59
+ self.style.SUCCESS(f"Zone '{zone_name}' exported to '{file_path}'")
60
+ )
@@ -0,0 +1,52 @@
1
+ import os
2
+ import dns.zone
3
+ import dns.rdatatype
4
+ import dns.rdataclass
5
+ import dns.exception
6
+ import netbox_dns.models
7
+ from netbox_dns_bridge.utils import export_bind_zone_file
8
+ from django.core.management.base import BaseCommand, CommandError
9
+
10
+
11
+ class Command(BaseCommand):
12
+
13
+ def add_arguments(self, parser):
14
+ parser.add_argument(
15
+ "--path",
16
+ type=str,
17
+ help="Path to the directory where zone files are to be written",
18
+ )
19
+
20
+ def handle(self, *args, **options):
21
+ # Load parameters
22
+ base_path = options["path"]
23
+
24
+ if not base_path:
25
+ raise CommandError("No --path parameter given")
26
+
27
+ # Ensure base_path exists
28
+ os.makedirs(base_path, exist_ok=True)
29
+
30
+ # Iterate through all zones
31
+ nb_zones = netbox_dns.models.Zone.objects.prefetch_related("view").all()
32
+ if not nb_zones.exists():
33
+ self.stdout.write(self.style.WARNING("No zones found in NetBox DNS."))
34
+ return
35
+
36
+ for zone in nb_zones:
37
+ # Determine view directory
38
+ view_name = zone.view.name
39
+ view_dir = os.path.join(base_path, view_name)
40
+ os.makedirs(view_dir, exist_ok=True)
41
+
42
+ # Zone file path
43
+ file_path = os.path.join(view_dir, zone.name)
44
+
45
+ try:
46
+ # Call existing export function
47
+ export_bind_zone_file(zone, file_path=file_path)
48
+ self.stdout.write(
49
+ self.style.SUCCESS(f"Exported zone '{zone.name}' to '{file_path}'")
50
+ )
51
+ except Exception as e:
52
+ self.stderr.write(f"Failed to export zone '{zone.name}': {e}")
@@ -0,0 +1,51 @@
1
+ from django.core.management.base import BaseCommand, CommandError
2
+ from netbox_dns_bridge.models import IntegerKeyValueSetting
3
+
4
+
5
+ class Command(BaseCommand):
6
+ help = "Get or set integer key-value settings"
7
+
8
+ def add_arguments(self, parser):
9
+ parser.add_argument(
10
+ "action", choices=["get", "set", "list"], help="Action to perform"
11
+ )
12
+ parser.add_argument("key", nargs="?", help="Setting key")
13
+ parser.add_argument(
14
+ "value", nargs="?", type=int, help="Value to set (required for 'set')"
15
+ )
16
+
17
+ def handle(self, *args, **options):
18
+ action = options["action"]
19
+ key = options.get("key")
20
+ value = options.get("value")
21
+
22
+ if action == "list":
23
+ settings = IntegerKeyValueSetting.objects.all()
24
+ if not settings.exists():
25
+ self.stdout.write("No settings found.")
26
+ return
27
+ for setting in settings:
28
+ self.stdout.write(str(setting))
29
+
30
+ elif action == "get":
31
+ if not key:
32
+ raise CommandError("A key is required for GET")
33
+ try:
34
+ setting = IntegerKeyValueSetting.objects.get(key=key)
35
+ self.stdout.write(f"{setting.key}: {setting.value}")
36
+ except IntegerKeyValueSetting.DoesNotExist:
37
+ raise CommandError(f"No setting was found for {key}")
38
+
39
+ elif action == "set":
40
+ if not key:
41
+ raise CommandError("A setting is required for SET command")
42
+ if value is None:
43
+ raise CommandError("A value is required for SET command")
44
+ try:
45
+ setting = IntegerKeyValueSetting.objects.get(key=key)
46
+ old_value = setting.value
47
+ setting.value = value
48
+ setting.save()
49
+ self.stdout.write(f"Updated {key} from {old_value} to {value}")
50
+ except IntegerKeyValueSetting.DoesNotExist:
51
+ raise CommandError(f"No setting found for {key}")
@@ -0,0 +1,117 @@
1
+ import logging
2
+ import threading
3
+ import dns.query
4
+ import dns.message
5
+ import dns.tsigkeyring
6
+ import dns.name
7
+ import dns.zone
8
+ import dns.rdatatype
9
+ import dns.rdataclass
10
+ import dns.rdtypes
11
+ import dns.exception
12
+ import dns.renderer
13
+
14
+ from django.core.management.base import BaseCommand
15
+ from django.conf import settings
16
+ from netbox_dns.models import View
17
+ from netbox_dns_bridge.request_handler import UDPRequestHandler, TCPRequestHandler
18
+ from netbox_dns_bridge.dns_server import UDPDNSServer, TCPDNSServer
19
+ from netbox_dns_bridge import catalog_zone_manager as catzm
20
+ from netbox_dns_bridge.models import IntegerKeyValueSetting
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class Command(BaseCommand):
26
+ help = "Run a minimal AXFR DNS server using data from NetBox DNS plugin"
27
+
28
+ def load_settings(self):
29
+ self.settings = settings.PLUGINS_CONFIG.get("netbox_dns_bridge", None)
30
+ if not self.settings:
31
+ raise RuntimeError(
32
+ "Command failed to initialize due to missing settings. Terminating Netbox."
33
+ )
34
+
35
+ self.tsig_keys = self.settings.get("tsig_keys", None)
36
+ if not self.tsig_keys:
37
+ raise RuntimeError("tsig_keys variable not set in plugin settings.")
38
+
39
+ # Load TSIG keys and map them to views
40
+ def load_tsig_key_settings(self):
41
+ self.keyring = {}
42
+ self.tsig_view_map = {}
43
+
44
+ for view_name, data in self.tsig_keys.items():
45
+ raw_key_name = data.get("keyname")
46
+ secret = data.get("secret")
47
+ algorithm_str = data.get("algorithm", "hmac-sha256")
48
+
49
+ if not raw_key_name or not secret:
50
+ logger.error(
51
+ f"Skipping TSIG key for view {view_name}: missing keyname or secret."
52
+ )
53
+ continue
54
+
55
+ try:
56
+ nb_view = View.objects.get(name=view_name)
57
+ except View.DoesNotExist:
58
+ logger.error(
59
+ f"Skipping TSIG key {raw_key_name}: view '{view_name}' not found."
60
+ )
61
+ continue
62
+
63
+ # Normalize key name to absolute DNS name
64
+ key_name_obj = dns.name.from_text(raw_key_name, origin=None).canonicalize()
65
+ if not key_name_obj.is_absolute():
66
+ key_name_obj = key_name_obj.concatenate(dns.name.root)
67
+ key_name_str = key_name_obj.to_text() # Will always include trailing do
68
+
69
+ self.keyring[key_name_obj] = dns.tsig.Key(
70
+ name=key_name_obj, secret=secret, algorithm=algorithm_str
71
+ )
72
+ self.tsig_view_map[key_name_str] = nb_view
73
+ logger.debug(f"Loaded TSIG key: {key_name_str} view: {nb_view.name}")
74
+
75
+ if not self.keyring:
76
+ msg = "No TSIG keys found in database."
77
+ logger.critical(msg)
78
+ raise RuntimeError(msg)
79
+
80
+ def add_arguments(self, parser):
81
+ parser.add_argument(
82
+ "--port", type=int, default=5354, help="Port number to listen on"
83
+ )
84
+ parser.add_argument(
85
+ "--address", type=str, default="0.0.0.0", help="IP to bind to"
86
+ )
87
+
88
+ def handle(self, *args, **options):
89
+ # Load parameters
90
+ port = options["port"]
91
+ address = options["address"]
92
+ catzm.init()
93
+
94
+ # Initialize settings
95
+ self.load_settings()
96
+ self.load_tsig_key_settings()
97
+
98
+ udp_server = UDPDNSServer(
99
+ (address, port), UDPRequestHandler, self.keyring, self.tsig_view_map
100
+ )
101
+
102
+ tcp_server = TCPDNSServer(
103
+ (address, port), TCPRequestHandler, self.keyring, self.tsig_view_map
104
+ )
105
+
106
+ def run_udp_server(server):
107
+ logger.debug(f"Query endpoint listening on {address} udp/{port}")
108
+ server.serve_forever()
109
+
110
+ udp_thread = threading.Thread(
111
+ target=run_udp_server, args=(udp_server,), daemon=True
112
+ )
113
+
114
+ udp_thread.start()
115
+
116
+ logger.debug(f"Query endpoint listening on {address} tcp/{port}")
117
+ tcp_server.serve_forever()
@@ -0,0 +1,58 @@
1
+ # Generated by Django 5.2.9 on 2026-04-08 11:29
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ (
13
+ "netbox_dns",
14
+ "0030_dnsseckeytemplate_comments_dnsseckeytemplate_owner_and_more",
15
+ ),
16
+ ]
17
+
18
+ operations = [
19
+ migrations.CreateModel(
20
+ name="IntegerKeyValueSetting",
21
+ fields=[
22
+ (
23
+ "id",
24
+ models.BigAutoField(
25
+ auto_created=True, primary_key=True, serialize=False
26
+ ),
27
+ ),
28
+ ("key", models.CharField(max_length=64)),
29
+ ("value", models.IntegerField()),
30
+ ],
31
+ options={
32
+ "default_permissions": (),
33
+ },
34
+ ),
35
+ migrations.CreateModel(
36
+ name="CatalogZoneMemberIdentifier",
37
+ fields=[
38
+ (
39
+ "id",
40
+ models.BigAutoField(
41
+ auto_created=True, primary_key=True, serialize=False
42
+ ),
43
+ ),
44
+ ("name", models.CharField(max_length=26, unique=True)),
45
+ (
46
+ "zone",
47
+ models.OneToOneField(
48
+ on_delete=django.db.models.deletion.CASCADE,
49
+ related_name="catz_identifier",
50
+ to="netbox_dns.zone",
51
+ ),
52
+ ),
53
+ ],
54
+ options={
55
+ "ordering": ("name",),
56
+ },
57
+ ),
58
+ ]
@@ -0,0 +1,33 @@
1
+ from django.db import models
2
+ import netbox.models
3
+ import netbox_dns.models
4
+
5
+
6
+ class IntegerKeyValueSetting(models.Model):
7
+ key = models.CharField(max_length=64)
8
+ value = models.IntegerField()
9
+
10
+ def __str__(self):
11
+ return f"{self.key}: {str(self.value)}"
12
+
13
+ class Meta:
14
+ default_permissions = ()
15
+
16
+
17
+ class CatalogZoneMemberIdentifier(models.Model):
18
+ name = models.CharField(
19
+ max_length=26,
20
+ unique=True,
21
+ )
22
+
23
+ zone = models.OneToOneField(
24
+ to=netbox_dns.models.Zone,
25
+ on_delete=models.CASCADE,
26
+ related_name="catz_identifier",
27
+ )
28
+
29
+ class Meta:
30
+ ordering = ("name",)
31
+
32
+ def __str__(self):
33
+ return self.name