From 73544a43356829e041646a5eabc6ea02e810d8a8 Mon Sep 17 00:00:00 2001 From: Sven Luethi Date: Thu, 11 Jun 2026 16:12:09 +0200 Subject: [PATCH] modified: README.md modified: netbox_dns_bridge/notify_handler.py modified: netbox_dns_bridge/request_handler.py modified: netbox_dns_bridge/signals/netbox_dns_zones.py Disable NOTIFY feature by default and add setting to enable it. --- README.md | 89 ++++++++++--------- netbox_dns_bridge/notify_handler.py | 6 +- netbox_dns_bridge/request_handler.py | 21 +++-- netbox_dns_bridge/signals/netbox_dns_zones.py | 7 +- 4 files changed, 70 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 26c66ce..ef8a1fe 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Netbox DNS Bridge -(Formerly Netbox Bind Provisioner) The NetBox DNS Bridge plugin extends [NetBox DNS](https://github.com/sys4/netbox-plugin-dns) by embedding a lightweight DNS server directly within NetBox. It acts as a bridge between NetBox DNS and your existing DNS infrastructure, leveraging native DNS mechanisms for seamless integration. -These include zone transfers (RFC 5936), catalog zones (RFC 9432), and dynamic updates (RFC 2136). +These include zone transfers (RFC 5936), catalog zones (RFC 9432), and notifying clients about zone +changes (RFC 1996). PyPi Stars Badge @@ -18,12 +18,6 @@ These include zone transfers (RFC 5936), catalog zones (RFC 9432), and dynamic u Downloads/Month Downloads/Week - - -![Architecture Overview](docs/architecture-overview.svg) - -DISCLAIMER: The dynamic update endpoint isn't here yet. Its planned - without a deadline. - ## Plugin configuration While providing Zone transfers via AXFR, the Server also exposes specialized catalog zones that BIND and other RFC9432 compliant DNS Servers use to automatically discover newly created zones and remove @@ -52,44 +46,55 @@ Parameter | Description --address | IP of interface to bind to (defaults to 0.0.0.0) ### Plugin settings +Plugin settings should be configured under `PLUGINS_CONFIG` in `netbox_dns_bridge`: +```json +PLUGINS_CONFIG = { + 'netbox_dns_bridge': { + ... + ... + } +} +``` + +#### TSIG Authentication +Following sets the TSIG key that allows clients to query the transfer endpoint and also they key to +be used to sign NOTIFY messages to clients on zone changes. Each view should have its own unique +key to allow the plugin to identify the view the client is trying to access. Re-using the key for +multiple views yields unpredicted behavior. +```json +'tsig_keys': { + 'the-view-name-here': { + 'keyname': 'the-tsig-key-name-here', + 'algorithm': 'hmac-sha256', + 'secret': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' + } +} +``` + +#### Client notification on zone change +To enable the NOTIFY feature, set following: +``` +'notify_clients': True +``` +This causes the plugin to schedule background jobs which send out NOTIFY messages to all clients +that have previously successfullly requested a zone transfer from the transfer endpoint. + +After how many hours that are client last checked in on a zone with a SOA or transfer request, +should the client be considered dead and no longer be notified. Default is 24 hours. +``` +notify_client_dead_after_hours: 24 +``` + Setting | Description --------------------| --------------------------------------------------------- tsig_keys | Maps a TSIG Key to be used for each view. +notify_clients | When set to true, turns on the notify mechanism to inform downsteams ## Plugin compatibility -This plugin is an extension to the netbox-plugin-dns plugin. As such the versioning of this plugin -was changed to match the one of the netbox-plugin-dns plugin closely. To guarantee compatability, -ensure that the major and minor version match between both plugins. -For example, when using netbox-plugin-dns `v1.5.5` install netbox-plugin-dns-bridge `v1.5.x`. - -## Post 1.0.7 Upgrade Guide -Applies only if you have a <= 1.0.7 installation. If you are freshly installing this plugin, -go on to [Installation Guide](#installation-guide). - -After version 1.0.7, the project was restructured and renamed. Until that point version updates -happened more or less automatically. However, this shift is more of a move from one plugin to -another as the package name has changed. - -This should not be an issue since the slave DNS Servers connected to Netbox can operate -independently while this plugin is being upgraded. - -Make sure to note down the catalog zone serial number before going further. - -1. Remove Netbox Bind Provisioner package - - Remove `netbox-plugin-bind-provisioner` from **local_dependencies.txt** - - Uninstall the package: `pip uninstall netbox-plugin-bind-provisioner` -2. Install Netbox DNS Bridge package - - Install the package `pip install netbox-plugin-dns-bridge` - - Put `netbox-plugin-dns-bridge` in your **local_dependencies.txt** so it will be installed on - next upgrade. -3. Adjust your `configuration.py` - - Change the plugin name from `netbox-plugin-bind-provisioner` to `netbox-plugin-dns-bridge` - - Change the key in `PLUGIN_CONFIG` from `netbox_plugin_bind_provisioner` to `netbox_dns_bridge` -4. Run migrations: `manage.py migrate` -5. Restore the catalog zone serial you noted down previously so that your slave dns servers continue - to pull the changes: `manage.py dns-settings set catalog-zone-soa-serial yourserialnumber` -6. Start the `dns-transfer-endpoint` service. -7. Mission accomplished. +This plugin extends the netbox-plugin-dns plugin. As such the versioning was changed to match the +one of netbox-plugin-dns more closely. To guarantee compatability, ensure that the major and minor +version match between both plugins. For example, when using netbox-plugin-dns `v1.5.5` install +netbox-plugin-dns-bridge `v1.5.x`. ## Installation guide This setup provisions a BIND9 server directly with DNS data from NetBox. BIND9 can optionally run on @@ -222,6 +227,7 @@ This guide assumes: ########## ZONES ########## view "public" { match-clients { public; }; + allow-notify { key "public_view_key"; }; catalog-zones { zone "catz" @@ -240,6 +246,7 @@ This guide assumes: view "private" { match-clients { private; }; + allow-notify { key "private_view_key"; }; catalog-zones { zone "catz" diff --git a/netbox_dns_bridge/notify_handler.py b/netbox_dns_bridge/notify_handler.py index 7e57123..f1f27a5 100644 --- a/netbox_dns_bridge/notify_handler.py +++ b/netbox_dns_bridge/notify_handler.py @@ -12,6 +12,7 @@ from .logger import get_logger from .models import SeenTransferClients logger = get_logger(__name__) +SETTINGS = settings.PLUGINS_CONFIG["netbox_dns_bridge"] class SendDNSNotify(JobRunner): @@ -19,7 +20,7 @@ class SendDNSNotify(JobRunner): name = "Send DNS NOTIFY" def _load_tsig_key(self, view_name: str) -> dns.tsig.Key: - tsig_keys = settings.PLUGINS_CONFIG["netbox_dns_bridge"].get("tsig_keys", {}) + tsig_keys = SETTINGS.get("tsig_keys", {}) if view_name not in tsig_keys: raise RuntimeError(f"No TSIG key configured for view '{view_name}'") @@ -50,7 +51,8 @@ class SendDNSNotify(JobRunner): view = View.objects.get(name=view_name) tsig_key = self._load_tsig_key(view_name) - seen_cutoff = timezone.now() - timezone.timedelta(hours=1) + cutoff_hours = SETTINGS.get("notify_client_dead_after_hours", 24) + seen_cutoff = timezone.now() - timezone.timedelta(hours=cutoff_hours) clients = SeenTransferClients.objects.filter( view=view, last_seen__gte=seen_cutoff ) diff --git a/netbox_dns_bridge/request_handler.py b/netbox_dns_bridge/request_handler.py index 9995b52..2b098ba 100644 --- a/netbox_dns_bridge/request_handler.py +++ b/netbox_dns_bridge/request_handler.py @@ -12,7 +12,7 @@ import dns.exception import dns.renderer from .logger import get_logger from django.utils import timezone -from netbox_dns.models import Zone, Record +from netbox_dns.models import Zone, Record, View from netbox_dns_bridge import catalog_zone_manager as catzm from .models import SeenTransferClients @@ -103,7 +103,8 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler): # 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}" + f"rdataset rdclass {rdataset.rdclass} does not match " + f"zone rdclass {zone.rdclass}" ) # Check if the rdataset has any rdata before creating an RRset @@ -143,6 +144,13 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler): wire = response.to_wire(multi=False) self._send_response(wire) + def _track_seen_client(source_ip: str, view: View) -> None: + SeenTransferClients.objects.update_or_create( + source_ip=source_ip, + view=view, + defaults={"last_seen": timezone.now()}, + ) + 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 @@ -274,13 +282,6 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler): ) wire = r.get_wire() self._send_response(wire) - - SeenTransferClients.objects.update_or_create( - source_ip=peer, - view=nb_view, - defaults={"last_seen": timezone.now()}, - ) - logger.info(f"{peer} AXFR {nb_view.name}/{dname}") def _handle_dns_query(self, wire) -> None: @@ -361,8 +362,10 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler): 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): diff --git a/netbox_dns_bridge/signals/netbox_dns_zones.py b/netbox_dns_bridge/signals/netbox_dns_zones.py index e23c21b..ccc5326 100644 --- a/netbox_dns_bridge/signals/netbox_dns_zones.py +++ b/netbox_dns_bridge/signals/netbox_dns_zones.py @@ -1,11 +1,14 @@ from django.db.models.signals import pre_save, post_save from django.dispatch import receiver from django.db import transaction +from django.conf import settings from netbox.context import current_request from netbox_dns.models import Zone from netbox_dns_bridge import catalog_zone_manager as catzm from netbox_dns_bridge import notify_handler +SETTINGS = settings.PLUGINS_CONFIG["netbox_dns_bridge"] + @receiver(pre_save, sender=Zone) def zone_pre_save(sender, instance, **kwargs): @@ -31,7 +34,9 @@ def zone_post_save(sender, instance, created, **kwargs): def _on_commit(): catzm.increment_soa_serial() - notify_handler.schedule(zone) + + if SETTINGS.get("notify_clients", False): + notify_handler.schedule(zone) transaction.on_commit(_on_commit)