fix(dns): report every host override so duplicates can be cleared
CI / test (3.10) (push) Failing after 21s
CI / test (3.11) (push) Failing after 21s
CI / test (3.12) (push) Failing after 26s
CI / test (3.9) (push) Failing after 14s

The inventory collapsed rows that shared a name, domain, address and type,
which was meant to hide the alias records searchhostoverride lists alongside
their parent. It hid genuine duplicates too — and since sync_dns_zone deletes
what the inventory tells it about, it could only ever remove one copy per name
before adding a fresh one. A pile of identical overrides could grow but never
shrink.

An office firewall reached fifteen identical A records for its own name, all
tagged [netork], while netOrk's database showed one.

OPNsense flags alias rows with isAlias, so filter on that and report every
remaining row under its own UUID. Releases that predate the flag give no way to
tell an alias from a copy, so the content collapse stays as a fallback there.
sync_dns_zone now also pushes one override per logical record, because two
netOrk rows for one host is a state its database can legitimately be in.

The auto-PTR flag is read from addptr, which is what OPNsense 26.1 returns;
ptrrecord was absent from every row, so the default made every A record claim
it managed a PTR — and netOrk derives reverse-zone entries from exactly that
flag. addptr now goes out on writes alongside the legacy name.

Refs christianmanivong/netork#103, christianmanivong/netork#107
This commit is contained in:
Christian Manivong
2026-08-20 23:35:04 +07:00
parent 960aaefa13
commit d27c32096d
2 changed files with 156 additions and 10 deletions
+31 -10
View File
@@ -2662,19 +2662,32 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
"/api/unbound/settings/searchhostoverride",
{"current": 1, "rowCount": -1, "searchPhrase": ""},
)
for row in data.get("rows", []):
rows = data.get("rows", []) or []
# searchhostoverride lists alias records alongside the host override
# they hang off, each under its own UUID. OPNsense flags them, so
# skip them by that flag and report every remaining row as it is —
# including two rows that happen to hold the same name and address.
# Those are duplicates on the device, and sync_dns_zone can only
# clear the copies it is told about.
flagged = any("isAlias" in row for row in rows)
for row in rows:
if flagged and row.get("isAlias"):
continue
host = row.get("hostname", "") or ""
domain = row.get("domain", "") or ""
fqdn = f"{host}.{domain}" if host and domain else host or domain
ip = row.get("server", "") or ""
rr = row.get("rr", "A") or "A"
# OPNsense searchhostoverride includes alias records alongside parent
# records; aliases often have identical content but separate UUIDs.
# Deduplicate by logical key to avoid inflating the zone with copies.
key = (host.lower(), domain.lower(), ip, rr)
if key in seen:
continue
seen.add(key)
if not flagged:
# Releases without the flag give nothing to tell an alias
# apart from a copy, so collapsing by content stays the
# safer read there.
key = (host.lower(), domain.lower(), ip, rr)
if key in seen:
continue
seen.add(key)
# The auto-PTR flag is "addptr" since 25.x, "ptrrecord" before.
ptr = row.get("addptr", row.get("ptrrecord", "1"))
result.append({
"uuid": row.get("uuid", ""),
"hostname": host,
@@ -2684,7 +2697,7 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
"record_type": rr,
"description": row.get("description", "") or "",
"enabled": str(row.get("enabled", "1")) == "1",
"ptrrecord": str(row.get("ptrrecord", "1")) == "1",
"ptrrecord": str(ptr) == "1",
"service": "unbound",
})
except Exception as exc:
@@ -2790,14 +2803,21 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
logger.info("Removed %s host override %s.%s (uuid %s)",
service, entry["hostname"], zone_clean, uid)
# Re-add all enabled A/AAAA records
# Re-add all enabled A/AAAA records, one override per logical record.
# Two rows for the same name is a state netOrk's own database can
# legitimately be in; the device must still end up with one.
added = 0
pushed: set[tuple[str, str, str]] = set()
for rec in records:
if not rec.get("enabled", True):
continue
rtype = rec.get("record_type", "A")
if rtype not in ("A", "AAAA"):
continue
key = (str(rec.get("hostname", "")).lower(), rtype, str(rec.get("ip", "")))
if key in pushed:
continue
pushed.add(key)
if service == "unbound":
self._post("/api/unbound/settings/addhostoverride", {
"host": {
@@ -2807,6 +2827,7 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
"rr": rtype,
"server": rec.get("ip", ""),
"description": self._NETORK_TAG,
"addptr": "1",
"ptrrecord": "1",
"mxprio": "",
"mx": "",