diff --git a/switch_backend.py b/switch_backend.py index cad4b82..fce77d4 100644 --- a/switch_backend.py +++ b/switch_backend.py @@ -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))