Fix .lan NXDOMAIN for custom hostnames (pbx.lan, nas.lan, etc.)
After the Unbound/:53 + ctrld/:5354 architecture change, fix-lan-zone wrote local-lan-zone.conf with only 'local-zone: "lan." static' and no local-data records. Unbound then returned NXDOMAIN for every .lan name not explicitly listed — including pbx.lan and any hostname in local-hostnames.json — because the static zone intercepts all .lan queries before they can reach dnsmasq. Fix: - Add _build_unbound_lan_zone_conf(entries, mgmt_ip) which builds a complete local-lan-zone.conf: the static zone declaration plus local-data A records for every entry in local-hostnames.json and the two built-in management aliases (switch.mgmt.lan, management.lan). - Update fix-lan-zone to use this helper instead of the bare zone-only string. Running fix-lan-zone now also pushes all saved hostnames. - Update save_local_hostnames to push the updated local-lan-zone.conf to Unbound via SSH and reload if OPNsense SSH is configured, so adding/editing hostnames in the DNS tab takes effect immediately without a separate fix-lan-zone call. https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
This commit is contained in:
+52
-3
@@ -2853,6 +2853,29 @@ def _generate_dnsmasq_conf(entries: list, mgmt_ip: str = "192.168.99.50") -> str
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _build_unbound_lan_zone_conf(entries: list, mgmt_ip: str = "192.168.99.50") -> str:
|
||||
"""
|
||||
Build the full local-lan-zone.conf for Unbound.
|
||||
|
||||
Declares 'lan.' as a static zone (so .lan never leaks to ControlD) and
|
||||
adds local-data A records for every entry in local-hostnames.json plus
|
||||
the two built-in management aliases. Without these local-data lines every
|
||||
.lan name that isn't listed gets NXDOMAIN — including pbx.lan and any
|
||||
other custom hostname the user defined.
|
||||
"""
|
||||
lines = ['local-zone: "lan." static', ""]
|
||||
# Management PC aliases — always present
|
||||
for alias in ("switch.mgmt.lan", "management.lan"):
|
||||
lines.append(f'local-data: "{alias}. A {mgmt_ip}"')
|
||||
# User-defined entries from local-hostnames.json
|
||||
for e in entries:
|
||||
name = e.get("name", "").strip().rstrip(".")
|
||||
ip = e.get("ip", "").strip()
|
||||
if name and ip:
|
||||
lines.append(f'local-data: "{name}. A {ip}"')
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _generate_ctrld_split_horizon_block(local_domain: str = "lan",
|
||||
dnsmasq_port: int = 5353) -> str:
|
||||
"""
|
||||
@@ -2955,6 +2978,21 @@ def save_local_hostnames(body: LocalHostnamesUpdate):
|
||||
local_domain=body.local_domain or "lan"
|
||||
)
|
||||
|
||||
# Push local-data records into Unbound on OPNsense if SSH is configured.
|
||||
# Without this, Unbound's static lan. zone returns NXDOMAIN for any
|
||||
# custom .lan hostname (pbx.lan, nas.lan, etc.) that isn't explicitly
|
||||
# listed — even though they exist in dnsmasq.
|
||||
unbound_push = None
|
||||
try:
|
||||
opn_cfg = _load_opnsense_cfg()
|
||||
if opn_cfg.get("ssh_key_path"):
|
||||
lan_zone_conf = _build_unbound_lan_zone_conf(entries, mgmt_ip)
|
||||
_opnsense_sftp_write(opn_cfg, f"{UNBOUND_ETC}/local-lan-zone.conf", lan_zone_conf)
|
||||
_opnsense_ssh_run(opn_cfg, "unbound-control reload 2>&1")
|
||||
unbound_push = f"Pushed {len(entries)} local-data record(s) to Unbound and reloaded"
|
||||
except Exception as _upe:
|
||||
unbound_push = f"Unbound push skipped: {_upe}"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"entries": entries,
|
||||
@@ -2962,6 +3000,7 @@ def save_local_hostnames(body: LocalHostnamesUpdate):
|
||||
"conf_path": str(DNSMASQ_CONF_PATH),
|
||||
"split_horizon": split_horizon,
|
||||
"full_toml": split_horizon_toml,
|
||||
"unbound_push": unbound_push,
|
||||
"docker_compose_snippet": (
|
||||
" dnsmasq:\n"
|
||||
" image: andyshinn/dnsmasq:latest\n"
|
||||
@@ -3541,11 +3580,21 @@ def opnsense_unbound_fix_lan_zone():
|
||||
raise HTTPException(503, "OPNsense SSH not configured")
|
||||
steps = []
|
||||
errors = []
|
||||
# Write the correct local-lan-zone.conf via SFTP
|
||||
lan_zone_conf = 'local-zone: "lan." static\n'
|
||||
# Build local-lan-zone.conf with all local-data records so custom .lan
|
||||
# hostnames (pbx.lan, nas.lan, etc.) resolve correctly from Unbound.
|
||||
import socket as _sock2
|
||||
try:
|
||||
mgmt_ip = _sock2.gethostbyname(_sock2.gethostname())
|
||||
except Exception:
|
||||
mgmt_ip = "192.168.99.50"
|
||||
entries = _load_local_hostnames()
|
||||
lan_zone_conf = _build_unbound_lan_zone_conf(entries, mgmt_ip)
|
||||
try:
|
||||
_opnsense_sftp_write(cfg, f"{UNBOUND_ETC}/local-lan-zone.conf", lan_zone_conf)
|
||||
steps.append("Wrote local-lan-zone.conf: local-zone \"lan.\" static")
|
||||
steps.append(
|
||||
f"Wrote local-lan-zone.conf: local-zone \"lan.\" static + "
|
||||
f"{len(entries)} local-data record(s)"
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(f"Write local-lan-zone.conf: {e}")
|
||||
raise HTTPException(500, "; ".join(errors))
|
||||
|
||||
Reference in New Issue
Block a user