Compare commits

...
8 Commits
Author SHA1 Message Date
Sven Luthi 6418ed56e6 modified: README.md 2026-04-09 19:10:39 +02:00
Sven Luthi fd16f7e701 modified: netbox_dns_bridge/__init__.py
Bump version to 1.5.2
2026-04-09 18:51:54 +02:00
Sven Luthi f926795d1b modified: netbox_dns_bridge/request_handler.py
Change query logging from debug to info. Seeing queries is very useful :)
2026-04-09 18:36:14 +02:00
Sven Luthi eedc01303b modified: netbox_dns_bridge/catalog_zone_manager.py
modified:   netbox_dns_bridge/request_handler.py
modified:   netbox_dns_bridge/utils.py
Various little bug fixes. Analysed by AI but fixed by hand to ensure integrity.
2026-04-09 18:28:05 +02:00
Sven Luthi 646059bb07 modified: netbox_dns_bridge/signals.py
Fix import path
2026-04-09 17:27:00 +02:00
Sven Luthi f41b4cea56 modified: netbox_dns_bridge/request_handler.py
Closes issue #15
2026-04-09 15:04:39 +02:00
Sven Luthi 3dec22b9f3 modified: README.md 2026-04-09 14:32:50 +02:00
Sven Luthi b23c773bee modified: README.md 2026-04-09 14:27:46 +02:00
6 changed files with 42 additions and 33 deletions

No files matched your search

+8 -7
View File
@@ -1,9 +1,10 @@
# Netbox DNS Bridge # Netbox DNS Bridge
(Formerly Netbox Bind Provisioner) (Formerly Netbox Bind Provisioner)
The Netbox DNS Bridge plugin implements a lightweight DNS server inside Netbox and builds a bridge The NetBox DNS Bridge plugin extends [NetBox DNS](https://github.com/sys4/netbox-plugin-dns) by
for BIND and other DNS Servers implementing RFC9432 to retrieve DNS Zones directly from Netbox embedding a lightweight DNS server directly within NetBox. It acts as a bridge between NetBox DNS
using DNS native mechanisms. 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).
<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-bind-provisioner/stargazers"><img src="https://img.shields.io/github/stars/suraxius/netbox-plugin-bind-provisioner?style=flat" alt="Stars Badge"></a> <a href="https://github.com/suraxius/netbox-plugin-bind-provisioner/stargazers"><img src="https://img.shields.io/github/stars/suraxius/netbox-plugin-bind-provisioner?style=flat" alt="Stars Badge"></a>
@@ -63,7 +64,7 @@ For example, when using netbox-plugin-dns `v1.5.5` install netbox-plugin-dns-bri
## Post 1.0.7 Upgrade Guide ## Post 1.0.7 Upgrade Guide
Applies only if you have a <= 1.0.7 installation. If you are freshly installing this plugin, Applies only if you have a <= 1.0.7 installation. If you are freshly installing this plugin,
go on to [Installation guide](installation-guide). go on to [Installation Guide](#installation-guide).
After version 1.0.7, the project was restructured and renamed. Until that point version updates 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 happened more or less automatically. However, this shift is more of a move from one plugin to
@@ -84,7 +85,7 @@ Make sure to note down the catalog zone serial number before going further.
3. Adjust your `configuration.py` 3. Adjust your `configuration.py`
- Change the plugin name from `netbox-plugin-bind-provisioner` to `netbox-plugin-dns-bridge` - 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` - Change the key in `PLUGIN_CONFIG` from `netbox_plugin_bind_provisioner` to `netbox_dns_bridge`
4. Ensure the `dns-transfer-endpoint` service is not running before the next step. 4. Run migrations: `manage.py migrate`
5. Restore the catalog zone serial you noted down previously so that your slave dns servers continue 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` to pull the changes: `manage.py dns-settings set catalog-zone-soa-serial yourserialnumber`
6. Start the `dns-transfer-endpoint` service. 6. Start the `dns-transfer-endpoint` service.
@@ -160,7 +161,7 @@ This guide assumes:
4. Run migrations 4. Run migrations
``` ```
python3 netbox/manage.py migrate manage.py migrate
``` ```
5. Start listener 5. Start listener
@@ -172,7 +173,7 @@ This guide assumes:
Note that `--port 5354` is optional. The listener will bind this port by default. Note that `--port 5354` is optional. The listener will bind this port by default.
``` ```
python3 netbox/manage.py dns-transfer-endpoint --port 5354 manage.py dns-transfer-endpoint --port 5354
``` ```
6. Configuring a Bind9 to interact with Netbox via the dns-transfer-endpoint endpoint. Note that its 6. Configuring a Bind9 to interact with Netbox via the dns-transfer-endpoint endpoint. Note that its
+1 -1
View File
@@ -2,7 +2,7 @@ import logging
from netbox.plugins import PluginConfig from netbox.plugins import PluginConfig
from django.conf import settings from django.conf import settings
__version__ = "1.5.1" __version__ = "1.5.2"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+3 -5
View File
@@ -62,12 +62,10 @@ def _set_serial(new_serial: int) -> bool:
def _increment_serial() -> None: def _increment_serial() -> None:
if not _set_serial(_SERIAL_OBJ.value + 1): if not _set_serial(_SERIAL_OBJ.value + 1):
logger.warning( logger.warning(
f"Failed to incremenet catalog serial {_SERIAL_OBJ.value}. Will overflow serial back to 1" f"Catalog serial {_SERIAL_OBJ.value} reached max — wrapping back to 1"
) )
_set_serial(1) _set_serial(1)
logger.debug( logger.debug(f"Catalog zone SOA serial number is now {_SERIAL_OBJ.value}")
f"Catalog zone SOA serial number incremented to {_SERIAL_OBJ.value}"
)
# If a zone has no catz identifier yet, create it: # If a zone has no catz identifier yet, create it:
@@ -230,5 +228,5 @@ def _generate_member_identifier() -> None:
def update_member_identifier(zone: Zone) -> None: def update_member_identifier(zone: Zone) -> None:
CatalogZoneMemberIdentifier.objects.update_or_create( CatalogZoneMemberIdentifier.objects.update_or_create(
zone=zone, zone=zone,
defaults={"name": _generate_member_identifier}, defaults={"name": _generate_member_identifier()},
) )
+27 -17
View File
@@ -125,13 +125,13 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
response.set_rcode(dns.rcode.REFUSED) response.set_rcode(dns.rcode.REFUSED)
if query.had_tsig: if query.had_tsig:
# Add TSIG with BADKEY, but do not sign (no MAC) # Add TSIG with error code, but do not sign (no MAC, empty keyring)
response.use_tsig( response.use_tsig(
keyring={}, # empty; we're not signing keyring={}, # empty; we're not signing
keyname=query.keyname, keyname=query.keyname,
tsig_error=tsig_error, tsig_error=tsig_error,
) )
self._deny_request(query) self._send_response(response.to_wire())
def _send_response(self, data) -> None: def _send_response(self, data) -> None:
raise NotImplementedError raise NotImplementedError
@@ -164,23 +164,27 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
self.server.keyring, keyname=query.keyname, original_id=query.id self.server.keyring, keyname=query.keyname, original_id=query.id
) )
else: else:
# If key is unknown # unknown key — use an empty keyring so use_tsig() does not
# raise KeyError trying to look up a key that isn't present.
response.set_rcode(dns.rcode.REFUSED) response.set_rcode(dns.rcode.REFUSED)
# Generate new key for keyname provided
# keyname = query.keyname
# b64 = base64.b64encode(urandom(32)).decode("ascii")
# newkey = dns.tsigkeyring.from_text({ keyname: b64 })
response.use_tsig( response.use_tsig(
self.server.keyring, {}, # empty keyring; no signing
keyname=query.keyname, keyname=query.keyname,
tsig_error=dns.rcode.BADKEY, tsig_error=dns.rcode.BADKEY,
) )
data = response.to_wire(max_size=512) data = response.to_wire(max_size=512)
self._send_response(data) self._send_response(data)
logger.debug(f"{peer} SOA {nb_view.name}/{dname}") logger.info(f"{peer} SOA {nb_view.name}/{dname}")
def _handle_axfr_request(self, query, zone, peer, nb_view, dname) -> None: def _handle_axfr_request(self, query, zone, peer, nb_view, dname) -> None:
if query.keyname not in self.server.keyring:
logger.error(f"AXFR aborted: keyname {query.keyname} not in keyring")
self._deny_request(query)
return
tsig_key = self.server.keyring[query.keyname]
rrsets = [] rrsets = []
soa_rrset = None soa_rrset = None
for name, rdataset in zone.iterate_rdatasets(): for name, rdataset in zone.iterate_rdatasets():
@@ -211,9 +215,7 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
# 3. Loop through RRsets # 3. Loop through RRsets
tsig_ctx = None tsig_ctx = None
for rrset in rrsets: for rrset in rrsets:
# logger.debug(f"Iterating over rrset {rrset}. tsig_ctx: {tsig_ctx}")
try: try:
# logger.debug(f"Adding {rrset} to renderer object")
r.add_rrset(dns.renderer.ANSWER, rrset) r.add_rrset(dns.renderer.ANSWER, rrset)
if r.max_size - len(r.output.getvalue()) < self.RESERVED_TSIG: if r.max_size - len(r.output.getvalue()) < self.RESERVED_TSIG:
raise dns.exception.TooBig("TSIG wont fit") raise dns.exception.TooBig("TSIG wont fit")
@@ -222,7 +224,8 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
r.write_header() r.write_header()
tsig_ctx = r.add_multi_tsig( tsig_ctx = r.add_multi_tsig(
ctx=tsig_ctx, ctx=tsig_ctx,
secret=self.server.keyring[query.keyname], secret=tsig_key.secret,
algorithm=tsig_key.algorithm,
keyname=query.keyname, keyname=query.keyname,
fudge=300, fudge=300,
id=query.id, id=query.id,
@@ -245,26 +248,33 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
query.question[0].rdtype, query.question[0].rdtype,
query.question[0].rdclass, query.question[0].rdclass,
) )
r.add_rrset(dns.renderer.ANSWER, rrset)
try:
r.add_rrset(dns.renderer.ANSWER, rrset)
except dns.exception.TooBig:
logger.error(
f"RRset {rrset.name}/{dns.rdatatype.to_text(rrset.rdtype)} "
f"exceeds MAX_WIRE ({self.MAX_WIRE}) and cannot be sent; "
f"skipping."
)
# 4. Final message with terminating TSIG # 4. Final message with terminating TSIG
r.write_header() r.write_header()
# logger.debug(f"Final message. tsig_ctx: {tsig_ctx}")
tsig_ctx = r.add_multi_tsig( tsig_ctx = r.add_multi_tsig(
ctx=tsig_ctx, ctx=tsig_ctx,
secret=self.server.keyring[query.keyname], secret=tsig_key.secret,
algorithm=tsig_key.algorithm,
keyname=query.keyname, keyname=query.keyname,
fudge=300, fudge=300,
id=query.id, id=query.id,
tsig_error=0, tsig_error=0,
other_data=b"", other_data=b"",
# request_mac=r.mac if tsig_ctx else None,
request_mac=r.mac if tsig_ctx else query.mac, request_mac=r.mac if tsig_ctx else query.mac,
) )
wire = r.get_wire() wire = r.get_wire()
self._send_response(wire) self._send_response(wire)
logger.debug(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:
peer = self.client_address[0] peer = self.client_address[0]
+1 -1
View File
@@ -1,7 +1,7 @@
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 netbox_dns.models import Zone from netbox_dns.models import Zone
from .service.endpoint import catalog_zone_manager as catzm from . import catalog_zone_manager as catzm
@receiver(pre_save, sender=Zone) @receiver(pre_save, sender=Zone)
+2 -2
View File
@@ -20,8 +20,8 @@ def export_bind_zone_file(nb_zone: netbox_dns.models.Zone, file_path: str):
rdata = dns.rdata.from_text(rdclass, rdatatype, record.value) rdata = dns.rdata.from_text(rdclass, rdatatype, record.value)
# Create rdataset # Create rdataset
rdataset = dns.rdataset.Rdataset(rdclass, rdatatype, record.ttl) rdataset = dns.rdataset.Rdataset(rdclass, rdatatype)
rdataset.add(rdata) rdataset.add(rdata, record.ttl)
# Add to zone # Add to zone
node = dp_zone.find_node(rname, create=True) node = dp_zone.find_node(rname, create=True)