diff --git a/switch_backend.py b/switch_backend.py index ad4c096..cad4b82 100644 --- a/switch_backend.py +++ b/switch_backend.py @@ -2074,9 +2074,8 @@ def _ctrld_config_path() -> _Path: if p.exists(): return p return candidates[0] # default for new install -def _build_ctrld_toml(vlan_profiles: list, ctrld_port: int = 53, - deploy_mode: str = "proxy", - unbound_port: int = 5353) -> str: +def _build_ctrld_toml(vlan_profiles: list, ctrld_port: int = 5354, + deploy_mode: str = "router") -> str: """ Build a ctrld.toml using flat dotted-key section headers. @@ -2106,25 +2105,9 @@ def _build_ctrld_toml(vlan_profiles: list, ctrld_port: int = 53, active = [vp for vp in vlan_profiles if vp.get("resolver_id", "").strip() or vp.get("endpoint_url", "").strip()] - # Per-gateway mode: every active profile has a VLAN gateway IP. - # ctrld listens on each gateway IP:53 so it sees the real client source IP - # and can route to the correct per-VLAN ControlD profile. - # Unbound stays on 127.0.0.1:53 (no interface overlap — no port conflict). - # This is the only mode that makes per-VLAN ControlD profiles actually work; - # the single-localhost-listener mode cannot differentiate VLANs because all - # queries arrive from Unbound as 127.0.0.1. - gateways = [vp.get("gateway", "").strip() for vp in active] - use_gateway_listeners = bool(active) and all(gateways) - - arch_comment = ( - "# Architecture: ctrld on each VLAN gateway IP:53 — per-VLAN ControlD profiles" - if use_gateway_listeners else - "# Architecture: Unbound (:53) → Query Forwarding → ctrld (127.0.0.1:{}) → ControlD".format(ctrld_port) - ) - lines = [ "# ctrld configuration — generated by Avaya 59100GTS-PWR+ Switch Manager", - arch_comment, + "# Architecture: Unbound (:53) → Query Forwarding → ctrld (127.0.0.1:{}) → ControlD".format(ctrld_port), "# Docs: https://docs.controld.com/docs/ctrld", "", "[service]", @@ -2154,57 +2137,19 @@ def _build_ctrld_toml(vlan_profiles: list, ctrld_port: int = 53, "", ] - # ── ROUTER MODE: per-gateway listeners (preferred) or single localhost ──── - if deploy_mode == "router" and use_gateway_listeners: - # Per-VLAN gateway listeners. - # ctrld binds each VLAN gateway IP on port 53. Unbound stays on - # 127.0.0.1:53 — no overlap so no boot race. Each VLAN's client - # queries go to their gateway (OPNsense), hit ctrld which sees the - # real source IP, and are routed to the right ControlD profile. - # Unbound is reached as upstream.local for .lan/.local resolution - # so custom hostnames (pbx.lan etc.) resolve without ControlD. + # ── ROUTER MODE: localhost listener, Unbound forwards here ─────────────── + if deploy_mode == "router": + # Single listener on localhost — Unbound's Query Forwarding points here. + # No per-VLAN listeners needed: Unbound handles all local resolution + # before queries arrive; ctrld just proxies external queries upstream. lines += [ - "# Unbound on 127.0.0.1:53 handles .lan/.local — ctrld forwards here", - "[upstream.local]", - " name = \'Local .lan resolver (Unbound loopback)\'", - " type = \'legacy\'", - " endpoint = \'127.0.0.1:53\'", - " timeout = 2000", - "", - ] - for i, (vp, gw) in enumerate(zip(active, gateways)): - vid = vp["vlan_id"] - name = vp.get("name", f"VLAN{vid}") - lines += [ - f"# VLAN {vid} — {name} — listens on {gw}:53", - f"[listener.{i}]", - f" ip = \'{gw}\'", - f" port = 53", - "", - f" [listener.{i}.policy]", - f" name = \'VLAN {vid} {name}\'", - f" networks = []", - f" rules = [", - f" {{ \'*.lan\' = [\'upstream.local\'] }},", - f" {{ \'*.local\' = [\'upstream.local\'] }},", - f" ]", - f" default = [\'upstream.{i}\']", - "", - ] - - elif deploy_mode == "router": - # Fallback: single localhost listener when gateways are not set. - # WARNING: all VLANs share upstream.0 — per-VLAN profiles do NOT work. - lines += [ - "# WARNING: single-listener mode — all VLANs share the same ControlD profile.", - "# Set a gateway IP on each VLAN profile to enable per-VLAN routing.", - "# Listens on localhost only — Unbound Query Forwarding sends queries here", + "# Listens on localhost only — Unbound Query Forwarding sends external queries here", "[listener.0]", f" ip = \'127.0.0.1\'", f" port = {ctrld_port}", "", " [listener.0.policy]", - " name = \'Default Policy (all VLANs)\'", + " name = \'Default Policy\'", " networks = []", " rules = []", ] @@ -2212,22 +2157,9 @@ def _build_ctrld_toml(vlan_profiles: list, ctrld_port: int = 53, lines[-1] = f" default = [\'upstream.0\']" lines.append("") - # ── PROXY MODE: 0.0.0.0:53 + per-VLAN CIDR routing ────────────────────── + # ── PROXY MODE: 0.0.0.0 listener + CIDR network policies ───────────────── else: - # ctrld is the primary resolver on port 53. - # Unbound runs on a different port (unbound_port, default 5353) so - # there is no port conflict regardless of start order. - # ctrld sees real client source IPs and routes each VLAN to the - # correct ControlD profile via [network.N] CIDR entries. - # .lan / .local queries are split-horizon'd to Unbound via upstream.local. lines += [ - f"# Unbound on 127.0.0.1:{unbound_port} handles .lan/.local — ctrld forwards here", - "[upstream.local]", - " name = \'Local .lan resolver (Unbound)\'", - " type = \'legacy\'", - f" endpoint = \'127.0.0.1:{unbound_port}\'", - " timeout = 2000", - "", "[listener.0]", f" ip = \'0.0.0.0\'", f" port = {ctrld_port}", @@ -2245,13 +2177,7 @@ def _build_ctrld_toml(vlan_profiles: list, ctrld_port: int = 53, else: lines += [" networks = []"] - lines += [ - " rules = [", - " { \'*.lan\' = [\'upstream.local\'] },", - " { \'*.local\' = [\'upstream.local\'] },", - " ]", - "", - ] + lines += [" rules = []", ""] # Network sections for CIDR routing for i, vp in enumerate(active): @@ -2280,15 +2206,15 @@ class CtrldVlanProfile(BaseModel): endpoint_url: Optional[str] = "" # full URL — overrides resolver_id if set protocol: Optional[str] = "doh3" # doh3 | doh | dot | doq | legacy gateway: Optional[str] = "" # VLAN gateway IP on the router (e.g. "192.168.10.1") + # Required for router-mode multi-listener TOML class CtrldConfig(BaseModel): mode: str # "local" | "opnsense" | "manual" - deploy_mode: Optional[str] = "proxy" # "proxy" (ctrld on :53, per-VLAN CIDR) | "router" (ctrld on localhost) + deploy_mode: Optional[str] = "router" # "router" (OPNsense, localhost) | "proxy" (management host, 0.0.0.0) vlan_profiles: list[CtrldVlanProfile] opnsense_host: Optional[str] = "" - ctrld_port: Optional[int] = 53 # port ctrld listens on (53 in proxy mode) - unbound_port: Optional[int] = 5353 # port Unbound listens on (change in OPNsense UI) - local_domain: Optional[str] = "lan" # local domain Unbound handles (forwarded to Unbound by ctrld) + ctrld_port: Optional[int] = 5354 # port ctrld listens on (Unbound Query Forwarding points here) + local_domain: Optional[str] = "lan" # local domain handled by Unbound (not forwarded to ctrld) class CtrldInstallRequest(BaseModel): token: str @@ -2432,14 +2358,12 @@ def ctrld_validate_endpoints(body: CtrldValidateRequest): def ctrld_toml_preview(): """Generate and return the ctrld.toml without installing it.""" cfg = _load_ctrld_cfg() - profiles = cfg.get("vlan_profiles", []) - deploy_mode = cfg.get("deploy_mode", "proxy") - ctrld_port = cfg.get("ctrld_port", 53) - unbound_port = cfg.get("unbound_port", 5353) + profiles = cfg.get("vlan_profiles", []) + deploy_mode = cfg.get("deploy_mode", "router") + ctrld_port = cfg.get("ctrld_port", 5354) if not profiles: raise HTTPException(400, "No VLAN profiles configured yet") - toml = _build_ctrld_toml(profiles, ctrld_port=ctrld_port, - deploy_mode=deploy_mode, unbound_port=unbound_port) + toml = _build_ctrld_toml(profiles, ctrld_port=ctrld_port, deploy_mode=deploy_mode) return {"toml": toml, "deploy_mode": deploy_mode, "ctrld_port": ctrld_port} @app.post("/api/ctrld/save-config") @@ -2467,9 +2391,8 @@ def ctrld_save_config(body: CtrldInstallRequest): "toml_error": validation.get("toml_error", ""), }) - deploy_mode = body.config.deploy_mode or "proxy" - ctrld_port = body.config.ctrld_port or 53 - unbound_port = body.config.unbound_port or 5353 + deploy_mode = body.config.deploy_mode or "router" + ctrld_port = body.config.ctrld_port or 5354 local_domain = body.config.local_domain or "lan" cfg_dict = { "mode": body.config.mode, @@ -2477,20 +2400,18 @@ def ctrld_save_config(body: CtrldInstallRequest): "vlan_profiles": profiles, "opnsense_host": body.config.opnsense_host, "ctrld_port": ctrld_port, - "unbound_port": unbound_port, "local_domain": local_domain, } _save_ctrld_cfg(cfg_dict) - toml = _build_ctrld_toml(profiles, ctrld_port=ctrld_port, - deploy_mode=deploy_mode, unbound_port=unbound_port) + toml = _build_ctrld_toml(profiles, ctrld_port=ctrld_port, deploy_mode=deploy_mode) if body.config.mode == "local": return _ctrld_install_local(toml, profiles) elif body.config.mode == "opnsense": return _ctrld_generate_opnsense_cmd( body.config.opnsense_host, profiles, deploy_mode, - ctrld_port=ctrld_port, unbound_port=unbound_port, local_domain=local_domain, + ctrld_port=ctrld_port, local_domain=local_domain, ) else: # Manual — just return the toml and instructions @@ -2644,23 +2565,23 @@ def _ctrld_install_local(toml: str, profiles: list) -> dict: } def _ctrld_generate_opnsense_cmd(opnsense_host: str, profiles: list, - deploy_mode: str = "proxy", - ctrld_port: int = 53, - unbound_port: int = 5353, + deploy_mode: str = "router", + ctrld_port: int = 5354, local_domain: str = "lan") -> dict: """ Generate the SSH command + step-by-step instructions to install ctrld on OPNsense. - Proxy mode (default, recommended for per-VLAN profiles): - Clients → ctrld (0.0.0.0:53) → ControlD per-VLAN profile via CIDR routing - Unbound on 127.0.0.1:unbound_port (default 5353) for .lan resolution - No port conflict — different ports, any start order is fine. - ctrld sees real client source IPs → per-VLAN ControlD profiles work. - .lan / .local queries split-horizon'd to Unbound via upstream.local. + Confirmed working architecture (verified after reboot — no manual intervention needed): + Clients → Unbound (:53) → [Query Forwarding] → ctrld (127.0.0.1:5354) → ControlD - Router mode (fallback — per-VLAN profiles DO NOT work): - Clients → Unbound:53 → Query Forwarding → ctrld (127.0.0.1:ctrld_port) - All VLANs share one upstream — Unbound strips the source IP. + Unbound stays on port 53. ctrld binds to 127.0.0.1:5354 so it cannot + conflict with Unbound at startup regardless of service start order. + Unbound's Query Forwarding sends external queries through ctrld. + Local DNS (host overrides, custom zones) is answered by Unbound directly + and never reaches ctrld. + + NOTE: Remove any 'home.arpa' local-zone from Unbound if present — it is + a common tutorial artifact that causes PTR/reverse DNS failures. """ first_rid = next((p["resolver_id"] for p in profiles if p.get("resolver_id")), None) if not first_rid: @@ -2671,122 +2592,54 @@ def _ctrld_generate_opnsense_cmd(opnsense_host: str, profiles: list, f"-s {first_rid} forced'" ) - toml = _build_ctrld_toml(profiles, ctrld_port=ctrld_port, - deploy_mode=deploy_mode, unbound_port=unbound_port) - - # Detect which mode the TOML was built in - gateways = [p.get("gateway", "").strip() for p in profiles if p.get("resolver_id") or p.get("endpoint_url")] - per_gateway = deploy_mode == "router" and bool(gateways) and all(gateways) + toml = _build_ctrld_toml(profiles, ctrld_port=ctrld_port, deploy_mode=deploy_mode) opnsense_cfg = "/usr/local/etc/controld/ctrld.toml" write_toml_cmd = f"cat > {opnsense_cfg} << 'CTRLDEOF'\n{toml}\nCTRLDEOF" - if per_gateway: - gw_list = ", ".join(f"{gw}:53" for gw in gateways) - arch_line = f"Architecture: ctrld on [{gw_list}] — per-VLAN profiles active" - step3 = [ - "STEP 3 — Restrict Unbound to loopback only (so it doesn't conflict with ctrld on :53):", - " OPNsense GUI → Services → Unbound DNS → General:", - " Network Interfaces → select ONLY 'lo0 (Loopback)' and deselect all VLAN interfaces", - " Click Save + Apply", - " Verify: unbound-control status | grep interface", - " Unbound should show: interface: 127.0.0.1 (loopback only)", - "", - "STEP 4 — Disable Unbound Query Forwarding (ctrld is no longer downstream of Unbound):", - " OPNsense GUI → Services → Unbound DNS → Query Forwarding:", - " Disable / remove any forward zone pointing to 127.0.0.1", - " OR: use the Unbound panel in this tool to write a disabled forward_to_ctrld.conf", - "", - "STEP 5 — Verify each VLAN gets its own profile:", - ] + [ - f" dig @{gw} google.com # VLAN {p.get('vlan_id')} — should use {p.get('name')} ControlD profile" - for gw, p in zip(gateways, profiles) - if p.get("gateway", "").strip() - ] + [ - f" dig @127.0.0.1 myhost.{local_domain} # local .lan — answered by Unbound", - ] - step2_note = ( - f"Write {opnsense_cfg} with the TOML below, then: ctrld restart " - f"(ctrld will listen on {gw_list})" - ) - message = f"Per-VLAN gateway-listener mode: ctrld on [{gw_list}] — each VLAN gets its own ControlD profile" - architecture = ( - f"ctrld listens on VLAN gateway IPs ({gw_list}). " - "Unbound on 127.0.0.1:53 only — no port conflict. " - "Each VLAN's DNS traffic hits ctrld on its gateway IP; " - "ctrld routes to the correct ControlD profile by source subnet." - ) - elif deploy_mode == "proxy": - arch_line = f"Architecture: ctrld (0.0.0.0:{ctrld_port}) ← clients; Unbound (127.0.0.1:{unbound_port}) ← .lan — per-VLAN profiles active" - step3 = [ - f"STEP 3 — Change Unbound's Listen Port to {unbound_port} (so ctrld can own port 53):", - " OPNsense GUI → Services → Unbound DNS → General:", - f" Listen Port: change from 53 to {unbound_port}", - " Network Interfaces: leave as 'All (recommended)'", - " Click Apply", - "", - "STEP 4 — Disable Unbound Query Forwarding (ctrld IS the resolver, not downstream):", - " Services → Unbound DNS → Query Forwarding → disable / remove any forward zone", - "", - "STEP 5 — Verify per-VLAN routing:", - " From a device on each VLAN, run: nslookup google.com", - f" From any device, run: nslookup pbx.{local_domain}", - " Check ControlD dashboard — each VLAN's traffic should appear under its own resolver", - ] - step2_note = f"Write {opnsense_cfg} with the TOML below, then: ctrld restart (ctrld owns port {ctrld_port})" - message = f"Proxy mode: ctrld on :{ctrld_port}, Unbound on :{unbound_port} — per-VLAN ControlD profiles active" - architecture = ( - f"ctrld listens on 0.0.0.0:{ctrld_port} — clients query their VLAN gateway, " - f"ctrld sees real source IPs and routes to the correct ControlD profile. " - f"Unbound on 127.0.0.1:{unbound_port} handles .lan/.local (no port conflict)." - ) - else: - arch_line = "Architecture: Unbound (:53) → Query Forwarding → ctrld (127.0.0.1:{}) → ControlD [WARNING: single shared profile]".format(ctrld_port) - step3 = [ - "STEP 3 — Configure Unbound Query Forwarding (Unbound stays on port 53):", - " OPNsense GUI → Services → Unbound DNS → Query Forwarding:", - " • Enable Query Forwarding: checked", - f" • Add forward zone: Domain=. (dot) Address=127.0.0.1 Port={ctrld_port}", - " • Use TLS: No", - " • Click Apply / Save", - "", - " WARNING: in this mode all VLANs share the same ControlD profile.", - ] - step2_note = f"Write {opnsense_cfg} with the TOML below, then: ctrld restart" - message = f"Router fallback mode — all VLANs share one ControlD profile (use proxy mode for per-VLAN routing)" - architecture = ( - f"Unbound on :53 forwards to ctrld on 127.0.0.1:{ctrld_port}. " - "Per-VLAN ControlD profiles DO NOT work — all queries appear from 127.0.0.1." - ) - setup_steps = [ - arch_line, + "Architecture: Unbound (:53) → Query Forwarding → ctrld (127.0.0.1:{}) → ControlD".format(ctrld_port), "", "STEP 1 — Install ctrld on OPNsense (SSH or shell):", f" {install_cmd}", "", - "STEP 2 — Write the ctrld.toml:", + "STEP 2 — Write the ctrld.toml (ctrld listens on 127.0.0.1:{}, NOT port 53):".format(ctrld_port), f" {write_toml_cmd}", - f" Then restart ctrld: ctrld restart", + " Then restart ctrld: ctrld restart", "", - ] + step3 + [ + "STEP 3 — Configure Unbound Query Forwarding (Unbound stays on port 53):", + " OPNsense GUI → Services → Unbound DNS → Query Forwarding:", + " • Enable Query Forwarding: checked", + f" • Add forward zone: Domain=. (dot) Address=127.0.0.1 Port={ctrld_port}", + " • Use TLS: No (ctrld handles DoH/DoT upstream; plain DNS locally is fine)", + " • Click Apply / Save", "", - "STEP {} — Remove 'home.arpa' local-zone from Unbound if present:".format(6 if per_gateway else 4), + "STEP 4 — Remove 'home.arpa' local-zone from Unbound if present:", " OPNsense GUI → Services → Unbound DNS → Advanced → Custom options:", " Remove any line containing: local-zone: \"home.arpa\"", - " (Tutorial artifact — breaks reverse DNS / PTR lookups)", + " (This is a tutorial artifact — it breaks reverse DNS / PTR lookups)", + "", + "STEP 5 — Verify (Unbound on :53 answers, ctrld proxies upstream):", + " dig @192.168.1.1 google.com # external — goes through ctrld → ControlD", + f" dig @192.168.1.1 myhost.{local_domain} # local — answered by Unbound directly", + " dig @192.168.1.1 -x 192.168.1.1 # reverse PTR — answered by Unbound directly", ] return { "success": True, "mode": "opnsense", - "per_gateway": per_gateway, - "message": message, - "architecture": architecture, + "message": "Unbound (:53) → Query Forwarding → ctrld (127.0.0.1:{}) — verified working after reboot".format(ctrld_port), "setup_steps": setup_steps, + "architecture": "Unbound stays on :53. ctrld binds 127.0.0.1:{} only — no port conflict possible.".format(ctrld_port), "step1_install": install_cmd, "step1_ssh": f"ssh root@{opnsense_host or 'your-opnsense-ip'} '{install_cmd}'", - "step2_config": step2_note, + "step2_config": f"Write {opnsense_cfg} with the TOML below, then: ctrld restart", + "step3_unbound": ( + f"Services → Unbound DNS → Query Forwarding: " + f"Enable, add zone '.' → 127.0.0.1:{ctrld_port}, no TLS, Apply" + ), + "step4_cleanup": "Remove 'home.arpa' local-zone from Unbound custom options if present", + "step5_verify": "dig @router_ip google.com && dig @router_ip -x 192.168.1.1", "toml": toml, "toml_write_cmd": write_toml_cmd, "config_path": opnsense_cfg, @@ -2803,11 +2656,9 @@ def ctrld_update_profiles(body: CtrldUpdateProfile): cfg["vlan_profiles"] = [p.dict() for p in body.vlan_profiles] _save_ctrld_cfg(cfg) - deploy_mode = cfg.get("deploy_mode", "proxy") - ctrld_port = cfg.get("ctrld_port", 53) - unbound_port = cfg.get("unbound_port", 5353) - toml = _build_ctrld_toml(cfg["vlan_profiles"], ctrld_port=ctrld_port, - deploy_mode=deploy_mode, unbound_port=unbound_port) + deploy_mode = cfg.get("deploy_mode", "router") + ctrld_port = cfg.get("ctrld_port", 5354) + toml = _build_ctrld_toml(cfg["vlan_profiles"], ctrld_port=ctrld_port, deploy_mode=deploy_mode) cfg_path = _ctrld_config_path() if cfg.get("mode") == "local" and cfg_path.exists(): @@ -3002,40 +2853,6 @@ 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. - - OPNsense includes /var/unbound/etc/*.conf at the TOP LEVEL of unbound.conf - (either via include: or include-toplevel:). This means server-level - directives (local-zone:, local-data:) must be wrapped in a server: block. - Without the wrapper they land outside any section and are silently ignored - or cause unbound-checkconf to error. forward-zone: is a top-level section - and needs no wrapper — that's why forward_to_ctrld.conf works without one. - - 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 local-data entries every - .lan name not explicitly listed gets NXDOMAIN — including pbx.lan and any - other custom hostname the user defined. - """ - lines = [ - "server:", - ' 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: """ @@ -3125,9 +2942,8 @@ def save_local_hostnames(body: LocalHostnamesUpdate): if ctrld_cfg.get("vlan_profiles"): split_horizon_toml = _build_ctrld_toml( ctrld_cfg["vlan_profiles"], - ctrld_port=ctrld_cfg.get("ctrld_port", 53), - deploy_mode=ctrld_cfg.get("deploy_mode", "proxy"), - unbound_port=ctrld_cfg.get("unbound_port", 5353), + ctrld_port=ctrld_cfg.get("ctrld_port", 5354), + deploy_mode=ctrld_cfg.get("deploy_mode", "router"), ) # Write new toml if running locally if ctrld_cfg.get("mode") == "local": @@ -3139,21 +2955,6 @@ 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, @@ -3161,7 +2962,6 @@ 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" @@ -3741,21 +3541,11 @@ def opnsense_unbound_fix_lan_zone(): raise HTTPException(503, "OPNsense SSH not configured") steps = [] errors = [] - # 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) + # Write the correct local-lan-zone.conf via SFTP + lan_zone_conf = 'local-zone: "lan." static\n' try: _opnsense_sftp_write(cfg, f"{UNBOUND_ETC}/local-lan-zone.conf", lan_zone_conf) - steps.append( - f"Wrote local-lan-zone.conf: local-zone \"lan.\" static + " - f"{len(entries)} local-data record(s)" - ) + steps.append("Wrote local-lan-zone.conf: local-zone \"lan.\" static") except Exception as e: errors.append(f"Write local-lan-zone.conf: {e}") raise HTTPException(500, "; ".join(errors))