Compare commits

..
5 Commits
Author SHA1 Message Date
Sven Luthi 41d48f2807 modified: netbox_dns_bridge/__init__.py
modified:   netbox_dns_bridge/catalog_zone_manager.py
Fix wrong import in previous version and slight change to
meachanism to create catz member identifier.
2026-07-15 17:48:49 +02:00
Sven Luthi e50cfd7cb6 modified: netbox_dns_bridge/__init__.py
Bump version
2026-07-14 17:51:37 +02:00
Sven Luthi 55f6622997 modified: README.md 2026-07-14 14:53:12 +02:00
Sven Luethi fd8a5a0d9d modified: netbox_dns_bridge/catalog_zone_manager.py
modified:   netbox_dns_bridge/management/commands/dns-transfer-endpoint.py
modified:   netbox_dns_bridge/request_handler.py
modified:   netbox_dns_bridge/signals/netbox_dns_zones.py
Range of bug fixes and improvements
2026-07-13 13:41:29 +02:00
Sven Luethi b72fa9b664 modified: README.md
modified:   netbox_dns_bridge/notify_handler.py
2026-07-11 02:46:17 +02:00
7 changed files with 55 additions and 90 deletions

No files matched your search

+9 -7
View File
@@ -74,27 +74,29 @@ multiple views yields unpredicted behavior.
#### NOTIFY
Clients may be notified of zone changes using the NOTIFY mechanism defined in RFC 1996.
When enabled, the zone transfer endpoint keeps track of any client that successfully queried the
endpoint and when a zone changes. Once a zone has changed, NetBox informs each client of change.
endpoint. Once a zone has changed, NetBox informs each client about the zone changed so that the
client can request a new zone transfer.
Note that this feature uses NetBox's background job system to schedule the messages asynchronously.
In order to work, you need at least one `rq-worker` service running in the background to handle
the queried jobs.
the queued jobs.
To enable the NOTIFY feature, set following:
##### NOTIFY Settings
```
'notify_clients': True
```
##### NOTIFY Settings
This enables the NOTIFY system. Defaults to False
```
notify_client_alive_threshold_hours: 24
'notify_client_alive_threshold_hours': 24
```
This sets how long a client is considred "alive" after it last queried the transfer endpoint.
Once the client has failed to check in for this amout of time, it is automatically removed from the
list of clients to be notified on zone changes. Default is 24 hours.
```
notify_over_tcp: True
'notify_over_tcp': True
```
Switch to using TCP instead of UDP. Default: False
Switch to using TCP instead of UDP. Defaults to False
## Plugin compatibility
This plugin extends the netbox-plugin-dns plugin. As such the versioning was changed to match the
+1 -1
View File
@@ -2,7 +2,7 @@ from netbox.plugins import PluginConfig
from django.conf import settings
from .logger import get_logger
__version__ = "1.5.4"
__version__ = "1.5.6"
LOGGER = get_logger(__name__)
+28 -64
View File
@@ -3,7 +3,7 @@ import dns.zone
import dns.rdatatype
import dns.rdataclass
from .logger import get_logger
from netbox_dns.models import Zone
from netbox_dns.models import Zone as NBZone
from netbox_dns_bridge.models import IntegerKeyValueSetting, CatalogZoneMemberIdentifier
from uuid import uuid4
from base64 import b32encode
@@ -12,30 +12,30 @@ from django.db import transaction, close_old_connections, OperationalError
LOGGER = get_logger(__name__)
def init() -> None:
_init_soa_serial()
_create_missing_member_identifiers()
def increment_soa_serial() -> int:
with transaction.atomic():
try:
soa_serial = IntegerKeyValueSetting.objects.select_for_update().get(
key="catalog-zone-soa-serial"
soa_serial_obj = (
IntegerKeyValueSetting.objects.select_for_update().get_or_create(
key="catalog-zone-soa-serial",
defaults={"value": 1},
)
)
new_soa_serial = soa_serial.value + 1
new_soa_serial = soa_serial_obj.value + 1
if new_soa_serial > 0xFFFFFFFF:
LOGGER.warning(
f"Catalog serial {soa_serial.value} reached max — wrapping back to 1"
f"Catalog serial {soa_serial_obj.value} reached max — wrapping back to 1"
)
new_soa_serial = 1
soa_serial.value = new_soa_serial
soa_serial.save()
soa_serial.refresh_from_db()
LOGGER.debug(f"Catalog zone SOA serial number is now {soa_serial.value}")
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.value
return soa_serial_obj.value
except OperationalError as e:
LOGGER.error(
f"ERROR: Failed to increment the Catalog Zone's SOA serial: {e}"
@@ -53,7 +53,7 @@ def create_zone(name, view_name) -> dns.zone.Zone:
try:
# get zones from netbox
nb_zones = Zone.objects.filter(
nb_zones = NBZone.objects.filter(
view__name=view_name, active=True
).select_related("catz_identifier")
@@ -64,7 +64,14 @@ def create_zone(name, view_name) -> dns.zone.Zone:
qname = dns.name.from_text(nb_zone.name, dns.name.root)
# Create PTR record
p_name = nb_zone.catz_identifier.name
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):
@@ -113,25 +120,11 @@ def create_zone(name, view_name) -> dns.zone.Zone:
return None
def _init_soa_serial() -> None:
try:
IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial")
LOGGER.info("Catalog zone SOA serial number loaded from database")
except IntegerKeyValueSetting.DoesNotExist:
IntegerKeyValueSetting.objects.create(key="catalog-zone-soa-serial", value=1)
LOGGER.debug(
"Catalog zone SOA serial number was not set in the database. Set to 1"
)
except OperationalError as e:
LOGGER.error(f"ERROR: Failed to initialize the Catalog Zone's SOA serial: {e}")
close_old_connections()
def _generate_member_identifier() -> str:
return b32encode(uuid4().bytes)[0:26].lower().decode("UTF-8")
def update_member_identifier(zone: Zone) -> None:
def update_member_identifier(zone: NBZone) -> None:
try:
CatalogZoneMemberIdentifier.objects.update_or_create(
zone=zone,
@@ -142,40 +135,11 @@ def update_member_identifier(zone: Zone) -> None:
close_old_connections()
def _create_missing_member_identifiers() -> None:
try:
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} missing catz member identifier. Creating..."
)
CatalogZoneMemberIdentifier.objects.bulk_create(
new_objects,
ignore_conflicts=False,
)
except OperationalError as e:
LOGGER.error(f"ERROR: Could not create catz member identifier: {e}")
close_old_connections()
def _create_soa_rdataset() -> dns.rdataset:
try:
serial = IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial").value
serial = IntegerKeyValueSetting.objects.get_or_create(
key="catalog-zone-soa-serial", defaults={"value": 1}
).value
# SOA Record components
ttl = 0
@@ -14,7 +14,6 @@ from django.core.management.base import BaseCommand
from django.conf import settings
from django.db import close_old_connections, OperationalError
from netbox_dns.models import View
from netbox_dns_bridge import catalog_zone_manager as catzm
from netbox_dns_bridge.request_handler import UDPRequestHandler, TCPRequestHandler
from netbox_dns_bridge.dns_server import UDPDNSServer, TCPDNSServer
from netbox_dns_bridge.logger import get_logger
@@ -80,7 +79,6 @@ class Command(BaseCommand):
# Load parameters
port = options["port"]
address = options["address"]
catzm.init()
self.tsig_keys = SETTINGS.get("tsig_keys", None)
if not self.tsig_keys:
+9 -15
View File
@@ -52,12 +52,14 @@ class SendDNSNotify(JobRunner):
view_name = kwargs["view_name"]
try:
catz_serial = IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial").value
catz_serial = IntegerKeyValueSetting.objects.get(
key="catalog-zone-soa-serial"
).value
zones = [
(kwargs["zone_name"], kwargs["soa_serial"]),
("catz", catz_serial),
(f"{view_name}.catz", catz_serial)
(f"{view_name}.catz", catz_serial),
]
view = View.objects.get(name=view_name)
@@ -72,7 +74,7 @@ class SendDNSNotify(JobRunner):
for zone_name, soa_serial in zones:
zone_name = dns.name.from_text(zone_name, dns.name.root).to_text()
msg = dns.message.make_query(zone_name, dns.rdatatype.SOA)
msg.flags = 0
msg.flags |= dns.flags.AA # = 0
msg.set_opcode(dns.opcode.NOTIFY)
soa_rdata = dns.rdata.from_text(
@@ -80,9 +82,7 @@ class SendDNSNotify(JobRunner):
dns.rdatatype.SOA,
f"invalid. invalid. {soa_serial} 60 10 1209600 0",
)
soa_rrset = dns.rrset.from_rdata(
zone_name, 0, soa_rdata
)
soa_rrset = dns.rrset.from_rdata(zone_name, 0, soa_rdata)
msg.authority.append(soa_rrset)
msg.use_tsig(keyring={tsig_key.name: tsig_key}, keyname=tsig_key.name)
@@ -114,13 +114,9 @@ class SendDNSNotify(JobRunner):
def schedule(zone: Zone):
try:
soa_record = Record.objects.filter(
zone=zone, type="SOA", active=True
).first()
soa_record = Record.objects.filter(zone=zone, type="SOA", active=True).first()
if soa_record is None:
LOGGER.error(
f"Zone {zone.name} has no active SOA record — skipping NOTIFY"
)
LOGGER.error(f"Zone {zone.name} has no active SOA record — skipping NOTIFY")
return
soa_serial = int(soa_record.value.split()[2])
@@ -130,9 +126,7 @@ def schedule(zone: Zone):
close_old_connections()
return
except (ValueError, IndexError) as e:
LOGGER.error(
f"Failed to parse SOA serial for zone {zone.name}: {e}"
)
LOGGER.error(f"Failed to parse SOA serial for zone {zone.name}: {e}")
return
SendDNSNotify.enqueue(
+4
View File
@@ -215,6 +215,10 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
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
@@ -27,13 +27,15 @@ def zone_post_save(sender, instance, created, **kwargs):
zone = instance
# Quit if this is not the first signal in this session for this zone.
try:
request = current_request.get()
if request is not None:
handled_zones = getattr(request, "_handled_zones", set())
if zone.pk in handled_zones:
return
handled_zones.add(zone.pk)
request._handled_zones = handled_zones
except LookupError:
pass
# On commit, increment soa serial and if notify is enabled, schedule bg job.
def _on_commit():
@@ -53,6 +55,7 @@ def zone_post_save(sender, instance, created, **kwargs):
if old_name and zone.name != old_name:
catzm.update_member_identifier(zone)
@receiver(post_delete, sender=Zone)
def zone_post_delete(sender, instance, **kwargs):
catzm.increment_soa_serial()