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"
|
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",
|
def _generate_ctrld_split_horizon_block(local_domain: str = "lan",
|
||||||
dnsmasq_port: int = 5353) -> str:
|
dnsmasq_port: int = 5353) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -2955,6 +2978,21 @@ def save_local_hostnames(body: LocalHostnamesUpdate):
|
|||||||
local_domain=body.local_domain or "lan"
|
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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
@@ -2962,6 +3000,7 @@ def save_local_hostnames(body: LocalHostnamesUpdate):
|
|||||||
"conf_path": str(DNSMASQ_CONF_PATH),
|
"conf_path": str(DNSMASQ_CONF_PATH),
|
||||||
"split_horizon": split_horizon,
|
"split_horizon": split_horizon,
|
||||||
"full_toml": split_horizon_toml,
|
"full_toml": split_horizon_toml,
|
||||||
|
"unbound_push": unbound_push,
|
||||||
"docker_compose_snippet": (
|
"docker_compose_snippet": (
|
||||||
" dnsmasq:\n"
|
" dnsmasq:\n"
|
||||||
" image: andyshinn/dnsmasq:latest\n"
|
" image: andyshinn/dnsmasq:latest\n"
|
||||||
@@ -3541,11 +3580,21 @@ def opnsense_unbound_fix_lan_zone():
|
|||||||
raise HTTPException(503, "OPNsense SSH not configured")
|
raise HTTPException(503, "OPNsense SSH not configured")
|
||||||
steps = []
|
steps = []
|
||||||
errors = []
|
errors = []
|
||||||
# Write the correct local-lan-zone.conf via SFTP
|
# Build local-lan-zone.conf with all local-data records so custom .lan
|
||||||
lan_zone_conf = 'local-zone: "lan." static\n'
|
# 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:
|
try:
|
||||||
_opnsense_sftp_write(cfg, f"{UNBOUND_ETC}/local-lan-zone.conf", lan_zone_conf)
|
_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:
|
except Exception as e:
|
||||||
errors.append(f"Write local-lan-zone.conf: {e}")
|
errors.append(f"Write local-lan-zone.conf: {e}")
|
||||||
raise HTTPException(500, "; ".join(errors))
|
raise HTTPException(500, "; ".join(errors))
|
||||||
|
|||||||
Reference in New Issue
Block a user