Skip to content

Commit b1341f0

Browse files
authored
Merge pull request #13 from LuPo/feat_ddns
Add Dynamic DNS (DDNS) support for Kea 3.0+
2 parents a0bb401 + c492195 commit b1341f0

35 files changed

Lines changed: 4769 additions & 5 deletions

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
# Changelog
22

3+
## 0.8.0 (2026-04-26)
4+
5+
### Added — Dynamic DNS (DDNS) support for Kea 3.0+
6+
Models the full `kea-dhcp-ddns` (D2) configuration surface and the per-server / per-subnet / per-class `ddns-*` knobs that `kea-dhcp4`/`kea-dhcp6` can render. Gated behind the `enable_ddns` plugin setting (which requires `enable_netbox_dns=True` — TSIG keys are plugin-local but zones and nameservers come from `netbox-plugin-dns`).
7+
8+
- **New models**
9+
- `TSIGKey` — RFC 2845 keys with algorithm choices, optional digest-bits truncation, and a pluggable `secret_backend` registry (v1 ships `plaintext`; `vault` reserved for future). Secret values are auto-generated on save when input is empty or not algorithm-length base64 — paste `tsig-keygen` output verbatim or just save and let the plugin mint one. Plaintext reveal is gated by a dedicated `view_secret_tsigkey` permission.
10+
- `D2Daemon` — one row per logical D2 service. `listener_mode` chooses between `local` (renders `127.0.0.1`; deploy one D2 instance per peer for HA-pair DDNS redundancy) and `remote` (single shared instance pinned to an IPAM record). Includes NCR transport, control-socket path (defaults from `d2_default_control_socket_path` plugin setting), and a per-daemon `/api/.../d2-daemons/<id>/kea-config/` endpoint that emits a complete `kea-dhcp-ddns.conf`.
11+
- `DDNSDomain` — binds a `D2Daemon` to a `netbox_dns.Zone`. Forward/reverse direction is derived from the zone name at emission time. Authoritative nameserver IPs are resolved from the zone's `NameServer` records (A/AAAA/CNAME chain) at config-emit time. Create form is multi-zone — pick any combination of forward and reverse zones in one shot.
12+
- `DDNSPolicy` — reusable `ddns-*` override group (`ddns-send-updates`, `ddns-override-no-update`, `ddns-replace-client-name`, `ddns-generated-prefix`/`-qualifying-suffix`, `hostname-char-set`/`-replacement`, conflict-resolution mode, TTL knobs). All fields nullable — `to_kea_overrides()` emits only the keys the operator set, kebab-cased.
13+
14+
- **Existing model changes** (all nullable FKs, `on_delete=SET_NULL`)
15+
- `DHCPServer.d2_daemon`, `ddns_enable_updates`, `ddns_sender_ip`, `ddns_sender_port`, `ddns_policy`
16+
- `DHCPHARelationship.d2_daemon`, `ddns_enable_updates`, `ddns_policy`
17+
- `Subnet.ddns_policy`
18+
- `ClientClass.ddns_policy`
19+
20+
- **HA-pair precedence**`DHCPServer.effective_d2_daemon` and `effective_ddns_policy` properties: when the server's HA relationship has a value set, that wins (peers must agree on the D2 target and the `ddns-*` policy). Sender IP/port stay per-server (the local source endpoint is always per-host). The DHCPServer detail page shows the effective values with an "inherited from HA relationship" badge.
21+
22+
- **Kea config emission** — no Python-side merging of override hierarchies. Each non-null `DDNSPolicy.to_kea_overrides()` is emitted verbatim at the JSON level the policy is attached to (server-level → `Dhcp4`/`Dhcp6` root, subnet-level → subnet dict, class-level → client-class dict). Kea resolves the precedence chain itself when processing packets.
23+
24+
- **Bulk edit**`DDNSDomain` got a bulk-edit view + form so you can set `d2_daemon` / `tsig_key` on many rows at once.
25+
26+
- **Quick-add**`tsig_key` field on `DDNSDomain` form has the `+` quick-add button so a new TSIG key can be created inline without leaving the form.
27+
28+
### Plugin settings
29+
- `enable_ddns` (default `False`) — master switch; nav, URLs, API routes, form fields, and serializer fields are all gated by it.
30+
- `ddns_secret_backend` (default `"plaintext"`) — backend identifier for `TSIGKey.get_secret()`.
31+
- `d2_default_control_socket_path` (default `"/tmp/kea-dhcp-ddns-ctrl.sock"`) — pre-fills the control socket field on new `D2Daemon` rows.
32+
333
## 0.7.3 (2026-04-07)
434

535
### Added

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ This plugin bridges the gap between NetBox IPAM and ISC KEA DHCP server configur
3131
- **HA Relationships**: Configure High Availability relationships between DHCP servers
3232
- **Stork Servers**: Manage ISC Stork monitoring server instances
3333
- **Stork Agent Groups**: Configure Stork agent groups that link DHCP servers to a Stork monitoring server
34+
- **TSIG Keys, D2 Daemons, DDNS Domains, DDNS Policies**: Optional Kea 3.0+ Dynamic DNS modelling (see [Dynamic DNS (DDNS)](#dynamic-dns-ddns))
3435

3536
### Stork Monitoring Integration
3637

@@ -42,6 +43,26 @@ This plugin bridges the gap between NetBox IPAM and ISC KEA DHCP server configur
4243
- Stork features are fully optional — disable with `enable_stork: False` in plugin settings
4344
- API endpoints support `Accept: text/plain` for easy integration with Ansible `uri` module
4445

46+
### Dynamic DNS (DDNS)
47+
48+
Optional Kea 3.0+ Dynamic DNS support — model `kea-dhcp-ddns` (D2) instances, TSIG keys, forward/reverse zones, and the `ddns-*` policy knobs that `kea-dhcp4`/`kea-dhcp6` render at server / subnet / client-class scope.
49+
50+
DDNS support is **disabled by default** and requires `enable_netbox_dns: True` (zones and nameservers are sourced from [`netbox-plugin-dns`](https://github.com/peteeckel/netbox-plugin-dns); TSIG keys are plugin-local). Enable with `enable_ddns: True` in `PLUGINS_CONFIG` (see [Configuration](#configuration)) — when off, the navigation entries, URLs, API routes, and form fields are all hidden.
51+
52+
- **Models**
53+
- `TSIGKey` — RFC 2845 keys with algorithm choices (`HMAC-MD5`/`SHA1`/`SHA224`/`SHA256`/`SHA384`/`SHA512`), optional digest-bits truncation, and a pluggable `secret_backend` registry (v1 ships `plaintext`; `vault` reserved). Secret values are auto-generated on save when input is empty or not algorithm-length base64 — you can paste `tsig-keygen` output verbatim or just save and let the plugin mint a fresh one. Plaintext reveal is gated by a dedicated `view_secret_tsigkey` permission.
54+
- `D2Daemon` — one row per logical D2 service. `listener_mode` chooses between **local** (renders `127.0.0.1`; deploy one D2 instance per peer for HA-pair DDNS redundancy) and **remote** (single shared instance pinned to an IPAM record). Each daemon exposes a per-instance `/api/plugins/netbox_dhcp_kea_plugin/d2-daemons/<id>/kea-config/` endpoint that emits a complete `kea-dhcp-ddns.conf`.
55+
- `DDNSDomain` — binds a `D2Daemon` to a `netbox_dns.Zone`. Forward/reverse direction is derived from the zone name at emission time. Authoritative nameserver IPs are resolved from the zone's `NameServer` records (A/AAAA/CNAME chain) when the config is rendered. The create form is multi-zone — pick any combination of forward and reverse zones in one shot.
56+
- `DDNSPolicy` — reusable `ddns-*` override group attachable at server / subnet / client-class scope (`ddns-send-updates`, `ddns-override-no-update`, `ddns-replace-client-name`, `ddns-generated-prefix`/`-qualifying-suffix`, `hostname-char-set`/`-replacement`, conflict-resolution mode, TTL knobs). All fields nullable — only the keys you set are emitted, kebab-cased.
57+
58+
- **HA-pair precedence** — when a `DHCPServer` is in an HA relationship and the relationship has its own `d2_daemon` or `ddns_policy` set, the relationship's value wins (peers must agree on the D2 target and the policy knobs). Sender IP/port stay per-server because the local source endpoint is per-host. The DHCPServer detail page shows the effective values with an "inherited from HA relationship" badge.
59+
60+
- **Local-D2 redundancy pattern (recommended for HA)** — set `listener_mode: local` on the `D2Daemon` shared by an HA pair, then deploy `kea-dhcp-ddns` on every peer host with the same rendered config. Each peer's `kea-dhcp4`/`6` posts NCRs to its own `127.0.0.1:53001`. If the active peer reboots, the standby's local D2 takes over with no DDNS gap.
61+
62+
- **Kea config emission** — no Python-side merging of override hierarchies. Each non-null `DDNSPolicy.to_kea_overrides()` is emitted verbatim at the JSON level the policy is attached to (server-level → `Dhcp4`/`Dhcp6` root, subnet-level → subnet dict, class-level → client-class dict). Kea resolves the precedence chain itself when processing packets.
63+
64+
- **Bulk edit + quick-add**`DDNSDomain` has a bulk-edit form (set `d2_daemon` / `tsig_key` on many rows at once), and the `tsig_key` field on the DDNS Domain form has a `+` quick-add button so a new TSIG key can be created inline without leaving the form.
65+
4566
### High Availability (HA) Support
4667

4768
- Configure HA relationships with multiple modes: hot-standby, load-balancing, passive-backup

netbox_dhcp_kea_plugin/__init__.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
__author__ = """Łukasz Polański"""
44
__email__ = "wookasz@gmail.com"
5-
__version__ = "0.7.3"
5+
__version__ = "0.8.0"
66

77

88
from netbox.plugins import PluginConfig
@@ -22,6 +22,9 @@ class DHCPKEAConfig(PluginConfig):
2222
"menu_name": "DHCP KEA",
2323
"enable_stork": True,
2424
"enable_netbox_dns": False,
25+
"enable_ddns": False,
26+
"ddns_secret_backend": "plaintext",
27+
"d2_default_control_socket_path": "/tmp/kea-dhcp-ddns-ctrl.sock",
2528
"model_defaults": {
2629
"Subnet": {
2730
"valid_lifetime": 3600,
@@ -99,6 +102,35 @@ def get_dhcp_config(self, obj):
99102
# Protect IP sources from deletion — GenericFK has no DB-level constraint
100103
self._register_ip_source_protection()
101104

105+
# DDNS depends on netbox-dns for Zone and NameServer objects
106+
self._validate_ddns_dependencies()
107+
108+
@staticmethod
109+
def _validate_ddns_dependencies():
110+
"""Raise if enable_ddns=True but netbox-dns isn't available."""
111+
from django.conf import settings
112+
from django.core.exceptions import ImproperlyConfigured
113+
114+
cfg = settings.PLUGINS_CONFIG.get("netbox_dhcp_kea_plugin", {})
115+
if not cfg.get("enable_ddns"):
116+
return
117+
118+
if not cfg.get("enable_netbox_dns"):
119+
raise ImproperlyConfigured(
120+
"netbox_dhcp_kea_plugin: enable_ddns=True requires "
121+
"enable_netbox_dns=True — DDNS zones and nameservers are "
122+
"pulled from the netbox-plugin-dns integration."
123+
)
124+
125+
try:
126+
import netbox_dns # noqa: F401
127+
except ImportError as exc:
128+
raise ImproperlyConfigured(
129+
"netbox_dhcp_kea_plugin: enable_ddns=True but "
130+
"netbox_dns could not be imported. Install netbox-plugin-dns "
131+
"and add it to PLUGINS."
132+
) from exc
133+
102134
@staticmethod
103135
def _register_ip_source_protection():
104136
"""Register pre_delete signal to prevent deletion of objects linked as IP sources."""

0 commit comments

Comments
 (0)