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 41 additions and 32 deletions

No files matched your search

+8 -7
View File
@@ -1,9 +1,10 @@
# Netbox DNS Bridge
(Formerly Netbox Bind Provisioner)
The Netbox DNS Bridge plugin implements a lightweight DNS server inside Netbox and builds a bridge
for BIND and other DNS Servers implementing RFC9432 to retrieve DNS Zones directly from Netbox
using DNS native mechanisms.
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).
<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>
@@ -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
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
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`
- 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. 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
to pull the changes: `manage.py dns-settings set catalog-zone-soa-serial yourserialnumber`
6. Start the `dns-transfer-endpoint` service.
@@ -160,7 +161,7 @@ This guide assumes:
4. Run migrations
```
python3 netbox/manage.py migrate
manage.py migrate
```
5. Start listener
@@ -172,7 +173,7 @@ This guide assumes:
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
+1 -1
View File
@@ -2,7 +2,7 @@ import logging
from netbox.plugins import PluginConfig
from django.conf import settings
__version__ = "1.5.1"
__version__ = "1.5.2"
logger = logging.getLogger(__name__)
+3 -5
View File
@@ -62,12 +62,10 @@ def _set_serial(new_serial: int) -> bool:
def _increment_serial() -> None:
if not _set_serial(_SERIAL_OBJ.value + 1):
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)
logger.debug(
f"Catalog zone SOA serial number incremented to {_SERIAL_OBJ.value}"
)
logger.debug(f"Catalog zone SOA serial number is now {_SERIAL_OBJ.value}")
# 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:
CatalogZoneMemberIdentifier.objects.update_or_create(
zone=zone,
defaults={"name": _generate_member_identifier},
defaults={"name": _generate_member_identifier()},
)
+26 -16
View File
@@ -125,13 +125,13 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
response.set_rcode(dns.rcode.REFUSED)
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(
keyring={}, # empty; we're not signing
keyname=query.keyname,
tsig_error=tsig_error,
)
self._deny_request(query)
self._send_response(response.to_wire())
def _send_response(self, data) -> None:
raise NotImplementedError
@@ -164,23 +164,27 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
self.server.keyring, keyname=query.keyname, original_id=query.id
)
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)
# 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(
self.server.keyring,
{}, # empty keyring; no signing
keyname=query.keyname,
tsig_error=dns.rcode.BADKEY,
)
data = response.to_wire(max_size=512)
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:
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 = []
soa_rrset = None
for name, rdataset in zone.iterate_rdatasets():
@@ -211,9 +215,7 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
# 3. Loop through RRsets
tsig_ctx = None
for rrset in rrsets:
# logger.debug(f"Iterating over rrset {rrset}. tsig_ctx: {tsig_ctx}")
try:
# logger.debug(f"Adding {rrset} to renderer object")
r.add_rrset(dns.renderer.ANSWER, rrset)
if r.max_size - len(r.output.getvalue()) < self.RESERVED_TSIG:
raise dns.exception.TooBig("TSIG wont fit")
@@ -222,7 +224,8 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
r.write_header()
tsig_ctx = r.add_multi_tsig(
ctx=tsig_ctx,
secret=self.server.keyring[query.keyname],
secret=tsig_key.secret,
algorithm=tsig_key.algorithm,
keyname=query.keyname,
fudge=300,
id=query.id,
@@ -245,26 +248,33 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
query.question[0].rdtype,
query.question[0].rdclass,
)
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
r.write_header()
# logger.debug(f"Final message. tsig_ctx: {tsig_ctx}")
tsig_ctx = r.add_multi_tsig(
ctx=tsig_ctx,
secret=self.server.keyring[query.keyname],
secret=tsig_key.secret,
algorithm=tsig_key.algorithm,
keyname=query.keyname,
fudge=300,
id=query.id,
tsig_error=0,
other_data=b"",
# request_mac=r.mac if tsig_ctx else None,
request_mac=r.mac if tsig_ctx else query.mac,
)
wire = r.get_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:
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.dispatch import receiver
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)
+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)
# Create rdataset
rdataset = dns.rdataset.Rdataset(rdclass, rdatatype, record.ttl)
rdataset.add(rdata)
rdataset = dns.rdataset.Rdataset(rdclass, rdatatype)
rdataset.add(rdata, record.ttl)
# Add to zone
node = dp_zone.find_node(rname, create=True)