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.
This commit is contained in:
Sven Luethi committed 2026-06-11 16:33:47 +02:00
1 parent cb4886f032
commit 73544a4335
4 files changed
+70 -53

No files matched your search

+48 -41
View File
@@ -1,10 +1,10 @@
# Netbox DNS Bridge # Netbox DNS Bridge
(Formerly Netbox Bind Provisioner)
The NetBox DNS Bridge plugin extends [NetBox DNS](https://github.com/sys4/netbox-plugin-dns) by 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 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. 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).
<a href="https://pypi.org/project/netbox-plugin-dns-bridge/"><img src="https://img.shields.io/pypi/v/netbox-plugin-dns-bridge" alt="PyPi"></a> <a href="https://pypi.org/project/netbox-plugin-dns-bridge/"><img src="https://img.shields.io/pypi/v/netbox-plugin-dns-bridge" alt="PyPi"></a>
<a href="https://github.com/suraxius/netbox-plugin-dns-bridge/stargazers"><img src="https://img.shields.io/github/stars/suraxius/netbox-plugin-dns-bridge?style=flat" alt="Stars Badge"></a> <a href="https://github.com/suraxius/netbox-plugin-dns-bridge/stargazers"><img src="https://img.shields.io/github/stars/suraxius/netbox-plugin-dns-bridge?style=flat" alt="Stars Badge"></a>
@@ -18,12 +18,6 @@ These include zone transfers (RFC 5936), catalog zones (RFC 9432), and dynamic u
<a href="https://pepy.tech/project/netbox-plugin-dns-bridge"><img src="https://static.pepy.tech/personalized-badge/netbox-plugin-dns-bridge?period=monthly&left_color=BLACK&right_color=BLUE&left_text=Downloads%2fMonth" alt="Downloads/Month"></a> <a href="https://pepy.tech/project/netbox-plugin-dns-bridge"><img src="https://static.pepy.tech/personalized-badge/netbox-plugin-dns-bridge?period=monthly&left_color=BLACK&right_color=BLUE&left_text=Downloads%2fMonth" alt="Downloads/Month"></a>
<a href="https://pepy.tech/project/netbox-plugin-dns-bridge"><img src="https://static.pepy.tech/personalized-badge/netbox-plugin-dns-bridge?period=weekly&left_color=BLACK&right_color=BLUE&left_text=Downloads%2fWeek" alt="Downloads/Week"></a> <a href="https://pepy.tech/project/netbox-plugin-dns-bridge"><img src="https://static.pepy.tech/personalized-badge/netbox-plugin-dns-bridge?period=weekly&left_color=BLACK&right_color=BLUE&left_text=Downloads%2fWeek" alt="Downloads/Week"></a>
![Architecture Overview](docs/architecture-overview.svg)
DISCLAIMER: The dynamic update endpoint isn't here yet. Its planned - without a deadline.
## Plugin configuration ## Plugin configuration
While providing Zone transfers via AXFR, the Server also exposes specialized catalog zones that BIND 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 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) --address | IP of interface to bind to (defaults to 0.0.0.0)
### Plugin settings ### 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 Setting | Description
--------------------| --------------------------------------------------------- --------------------| ---------------------------------------------------------
tsig_keys | Maps a TSIG Key to be used for each view. 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 ## Plugin compatibility
This plugin is an extension to the netbox-plugin-dns plugin. As such the versioning of this plugin This plugin extends the netbox-plugin-dns plugin. As such the versioning was changed to match the
was changed to match the one of the netbox-plugin-dns plugin closely. To guarantee compatability, one of netbox-plugin-dns more closely. To guarantee compatability, ensure that the major and minor
ensure that the major and minor version match between both plugins. version match between both plugins. For example, when using netbox-plugin-dns `v1.5.5` install
For example, when using netbox-plugin-dns `v1.5.5` install netbox-plugin-dns-bridge `v1.5.x`. 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.
## Installation guide ## Installation guide
This setup provisions a BIND9 server directly with DNS data from NetBox. BIND9 can optionally run on 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 ########## ########## ZONES ##########
view "public" { view "public" {
match-clients { public; }; match-clients { public; };
allow-notify { key "public_view_key"; };
catalog-zones { catalog-zones {
zone "catz" zone "catz"
@@ -240,6 +246,7 @@ This guide assumes:
view "private" { view "private" {
match-clients { private; }; match-clients { private; };
allow-notify { key "private_view_key"; };
catalog-zones { catalog-zones {
zone "catz" zone "catz"
+4 -2
View File
@@ -12,6 +12,7 @@ from .logger import get_logger
from .models import SeenTransferClients from .models import SeenTransferClients
logger = get_logger(__name__) logger = get_logger(__name__)
SETTINGS = settings.PLUGINS_CONFIG["netbox_dns_bridge"]
class SendDNSNotify(JobRunner): class SendDNSNotify(JobRunner):
@@ -19,7 +20,7 @@ class SendDNSNotify(JobRunner):
name = "Send DNS NOTIFY" name = "Send DNS NOTIFY"
def _load_tsig_key(self, view_name: str) -> dns.tsig.Key: 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: if view_name not in tsig_keys:
raise RuntimeError(f"No TSIG key configured for view '{view_name}'") 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) view = View.objects.get(name=view_name)
tsig_key = self._load_tsig_key(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( clients = SeenTransferClients.objects.filter(
view=view, last_seen__gte=seen_cutoff view=view, last_seen__gte=seen_cutoff
) )
+12 -9
View File
@@ -12,7 +12,7 @@ import dns.exception
import dns.renderer import dns.renderer
from .logger import get_logger from .logger import get_logger
from django.utils import timezone 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 netbox_dns_bridge import catalog_zone_manager as catzm
from .models import SeenTransferClients from .models import SeenTransferClients
@@ -103,7 +103,8 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
# Ensure rdataset has the same rdclass as the zone # Ensure rdataset has the same rdclass as the zone
if rdataset.rdclass != zone.rdclass: if rdataset.rdclass != zone.rdclass:
raise ValueError( 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 # 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) wire = response.to_wire(multi=False)
self._send_response(wire) 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: 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). # We assume that the SOA rdataset has at least one record (it usually does).
soa_rdata = soa_rrset[0] # Get the first SOA record soa_rdata = soa_rrset[0] # Get the first SOA record
@@ -274,13 +282,6 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
) )
wire = r.get_wire() wire = r.get_wire()
self._send_response(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}") logger.info(f"{peer} AXFR {nb_view.name}/{dname}")
def _handle_dns_query(self, wire) -> None: def _handle_dns_query(self, wire) -> None:
@@ -361,8 +362,10 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
if qtype == dns.rdatatype.SOA: if qtype == dns.rdatatype.SOA:
self._handle_soa_request(query, soa_rrset, zone, peer, nb_view, dname) self._handle_soa_request(query, soa_rrset, zone, peer, nb_view, dname)
self._track_seen_client(peer, nb_view)
elif qtype == dns.rdatatype.AXFR: elif qtype == dns.rdatatype.AXFR:
self._handle_axfr_request(query, zone, peer, nb_view, dname) self._handle_axfr_request(query, zone, peer, nb_view, dname)
self._track_seen_client(peer, nb_view)
class UDPRequestHandler(DNSBaseRequestHandler): class UDPRequestHandler(DNSBaseRequestHandler):
@@ -1,11 +1,14 @@
from django.db.models.signals import pre_save, post_save from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.db import transaction from django.db import transaction
from django.conf import settings
from netbox.context import current_request from netbox.context import current_request
from netbox_dns.models import Zone from netbox_dns.models import Zone
from netbox_dns_bridge import catalog_zone_manager as catzm from netbox_dns_bridge import catalog_zone_manager as catzm
from netbox_dns_bridge import notify_handler from netbox_dns_bridge import notify_handler
SETTINGS = settings.PLUGINS_CONFIG["netbox_dns_bridge"]
@receiver(pre_save, sender=Zone) @receiver(pre_save, sender=Zone)
def zone_pre_save(sender, instance, **kwargs): def zone_pre_save(sender, instance, **kwargs):
@@ -31,7 +34,9 @@ def zone_post_save(sender, instance, created, **kwargs):
def _on_commit(): def _on_commit():
catzm.increment_soa_serial() catzm.increment_soa_serial()
notify_handler.schedule(zone)
if SETTINGS.get("notify_clients", False):
notify_handler.schedule(zone)
transaction.on_commit(_on_commit) transaction.on_commit(_on_commit)