diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b74c94..0fc6ab5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ ## Unreleased -- Add `custom_field_force_notify` setting. When set to the name of a boolean custom field on the - NetBox DNS Zone object, the plugin sends a NOTIFY directly to a zone's own nameservers whenever - the zone has that custom field set to `True` and its SOA serial number changes. Nameserver - addresses are resolved from NetBox DNS A/AAAA records within the zone's view and the NOTIFY is - signed with the per-view TSIG key. A companion `force_notify_port` setting (default 53) controls - the destination port. +- Add the option to send a NOTIFY directly to a zone's own nameservers when its SOA serial number + changes. Enable it per zone via a boolean custom field named by `notify_ns_custom_field_name`, + globally for all zones via `notify_ns_all_zones` (default `False`), or both. Nameserver addresses + are resolved from NetBox DNS A/AAAA records within the zone's view and the NOTIFY is signed with + the per-view TSIG key. A companion `notify_ns_port` setting (default 53) controls the destination + port. ## 1.0.7 - 2026-03-02 README Change - Moving private keys to global scope since Bind 9.20 view scoped keys have become unreliable and sometimes wouldnt match. diff --git a/README.md b/README.md index 1effa9b..26b36d8 100644 --- a/README.md +++ b/README.md @@ -98,22 +98,31 @@ list of clients to be notified on zone changes. Default is 24 hours. ``` Switch to using TCP instead of UDP. Defaults to False -#### Forced NOTIFY to a zone's nameservers +#### NOTIFY to a zone's own nameservers In addition to notifying clients that queried the transfer endpoint, the plugin can send a NOTIFY directly to a zone's own nameservers whenever that zone's SOA serial number changes. This is useful -when you want specific zones to have their nameservers pull updates immediately, independent of the -`notify_clients` mechanism. - -The feature is driven by a boolean custom field on the NetBox DNS Zone object. Create a boolean -custom field (e.g. `dns_bridge_force_notify_ns`) that applies to `netbox_dns > zone`, then point the -plugin at it: -``` -'custom_field_force_notify': 'dns_bridge_force_notify_ns' -``` -When this setting is present, every time a zone whose custom field is set to `True` changes its SOA -serial number, the plugin schedules a background NOTIFY to each of the zone's configured nameservers. +when you want nameservers to pull updates immediately, independent of the `notify_clients` mechanism. Only a change of the SOA serial triggers this — other zone edits that leave the serial untouched do -not. The setting is unset by default, which disables the feature entirely. +not. + +This can be enabled per zone, globally, or both. If neither of the settings below is configured, the +feature is disabled entirely. + +**Per zone (custom field).** Create a boolean custom field (e.g. `dns_bridge_notify_ns`) that applies +to `netbox_dns > zone`, then point the plugin at it: +``` +'notify_ns_custom_field_name': 'dns_bridge_notify_ns' +``` +A NOTIFY is then sent for any zone whose custom field is set to `True`. This lets you toggle the +behavior per zone straight from the NetBox UI, without changing the plugin config. + +**Globally (all zones).** +``` +'notify_ns_all_zones': True +``` +When enabled, a NOTIFY is sent for every zone on serial change, regardless of the custom field. +Defaults to `False`. The two options coexist: a NOTIFY is sent if the global switch is on **or** the +zone's custom field is set. The nameserver addresses are resolved from NetBox DNS itself by looking up active A/AAAA records (within the zone's view) matching each nameserver's hostname. The NOTIFY messages are signed with the @@ -121,9 +130,9 @@ same per-view TSIG key configured under `tsig_keys`, so the receiving nameserver signed with that key (e.g. BIND's `allow-notify { key "..."; };`). ``` -'force_notify_port': 53 +'notify_ns_port': 53 ``` -Destination port used for these forced NOTIFY messages. Defaults to 53. +Destination port used for these NOTIFY messages. Defaults to 53. As with the client NOTIFY feature, this uses NetBox's background job system, so at least one `rq-worker` service must be running. diff --git a/netbox_dns_bridge/notify_handler.py b/netbox_dns_bridge/notify_handler.py index 54b6b77..229b2ef 100644 --- a/netbox_dns_bridge/notify_handler.py +++ b/netbox_dns_bridge/notify_handler.py @@ -111,7 +111,7 @@ class SendDNSNotify(JobRunner): close_old_connections() -class SendForcedNSNotify(JobRunner): +class SendZoneNSNotify(JobRunner): """Send a NOTIFY straight to a zone's own nameservers. Unlike ``SendDNSNotify`` (which notifies clients that previously queried the @@ -121,11 +121,11 @@ class SendForcedNSNotify(JobRunner): """ class Meta: - name = "Send forced DNS NOTIFY to zone nameservers" + name = "Send DNS NOTIFY to zone nameservers" def run(self, *args, **kwargs): notify_over_tcp = SETTINGS.get("notify_over_tcp", False) - port = SETTINGS.get("force_notify_port", 53) + port = SETTINGS.get("notify_ns_port", 53) view_name = kwargs["view_name"] zone_name = kwargs["zone_name"] soa_serial = kwargs["soa_serial"] @@ -133,17 +133,17 @@ class SendForcedNSNotify(JobRunner): try: zone = Zone.objects.get(pk=kwargs["zone_id"]) except Zone.DoesNotExist: - LOGGER.error(f"Forced NOTIFY: zone id {kwargs['zone_id']} no longer exists") + LOGGER.error(f"NS NOTIFY: zone id {kwargs['zone_id']} no longer exists") return except OperationalError as e: - LOGGER.error(f"Forced NOTIFY failed due to unexpected error: {e}") + LOGGER.error(f"NS NOTIFY failed due to unexpected error: {e}") close_old_connections() return ns_targets = _resolve_zone_nameserver_ips(zone) if not ns_targets: LOGGER.warning( - f"Forced NOTIFY: no nameserver addresses found in NetBox DNS for " + f"NS NOTIFY: no nameserver addresses found in NetBox DNS for " f"{view_name}/{zone_name} — skipping" ) return @@ -152,7 +152,7 @@ class SendForcedNSNotify(JobRunner): tsig_key = _load_tsig_key(view_name) except RuntimeError as e: LOGGER.error( - f"Forced NOTIFY aborted for {view_name}/{zone_name}: {e}" + f"NS NOTIFY aborted for {view_name}/{zone_name}: {e}" ) return @@ -172,18 +172,18 @@ class SendForcedNSNotify(JobRunner): for ip in ns_targets: try: if notify_over_tcp: - LOGGER.debug("Sending forced NOTIFY over TCP") + LOGGER.debug("Sending NS NOTIFY over TCP") dns.query.tcp(msg, ip, port=port, timeout=2) else: - LOGGER.debug("Sending forced NOTIFY over UDP") + LOGGER.debug("Sending NS NOTIFY over UDP") dns.query.udp(msg, ip, port=port, timeout=2) LOGGER.info( - f"Forced NOTIFY sent: {ip} {view_name}/{fqdn} {soa_serial}" + f"NS NOTIFY sent: {ip} {view_name}/{fqdn} {soa_serial}" ) except Exception as e: LOGGER.error( - f"Forced NOTIFY failed: {ip} {view_name}/{fqdn} {soa_serial}: {e}" + f"NS NOTIFY failed: {ip} {view_name}/{fqdn} {soa_serial}: {e}" ) @@ -253,12 +253,12 @@ def schedule(zone: Zone): ) -def schedule_force_ns_notify(zone: Zone): +def schedule_notify_ns(zone: Zone): soa_serial = _get_soa_serial(zone) if soa_serial is None: return - SendForcedNSNotify.enqueue( + SendZoneNSNotify.enqueue( zone_id=zone.pk, zone_name=zone.name, view_name=zone.view.name, diff --git a/netbox_dns_bridge/signals/netbox_dns_zones.py b/netbox_dns_bridge/signals/netbox_dns_zones.py index 8a6656a..8d617e8 100644 --- a/netbox_dns_bridge/signals/netbox_dns_zones.py +++ b/netbox_dns_bridge/signals/netbox_dns_zones.py @@ -54,11 +54,10 @@ def zone_post_save(sender, instance, created, **kwargs): if SETTINGS.get("notify_clients", False): notify_handler.schedule(zone) - # Force a NOTIFY to the zone's own nameservers when the zone carries the - # configured boolean custom field set to True and its SOA serial changed. - cf_name = SETTINGS.get("custom_field_force_notify") - if cf_name and soa_serial_changed and _force_notify_enabled(zone, cf_name): - notify_handler.schedule_force_ns_notify(zone) + # Send a NOTIFY to the zone's own nameservers when its SOA serial changed + # and the feature is enabled — globally or per zone via a custom field. + if soa_serial_changed and _notify_ns_enabled(zone): + notify_handler.schedule_notify_ns(zone) transaction.on_commit(_on_commit) @@ -77,7 +76,19 @@ def zone_post_delete(sender, instance, **kwargs): catzm.increment_soa_serial() -def _force_notify_enabled(zone, cf_name: str) -> bool: - """Return True only if the zone's boolean custom field ``cf_name`` is True.""" +def _notify_ns_enabled(zone) -> bool: + """Return True if a NOTIFY to the zone's nameservers should be sent. + + Enabled either globally for every zone via ``notify_ns_all_zones`` or per + zone when the boolean custom field named by ``notify_ns_custom_field_name`` + is set to True on the zone. + """ + if SETTINGS.get("notify_ns_all_zones", False): + return True + + cf_name = SETTINGS.get("notify_ns_custom_field_name") + if not cf_name: + return False + custom_fields = getattr(zone, "custom_field_data", None) or {} return custom_fields.get(cf_name) is True