fix: DNS split-horizon in DDNS mode, service access filter, health check, verbosity persistence
Unit Tests / test (push) Successful in 7m32s

- DNS (critical): add _configured_dns_params() that returns (primary_domain,
  split_horizon_zones) from config_manager so all apply_all_dns_rules() callers
  pass the correct primary zone (e.g. 'pic.ngo') and split-horizon list
  (e.g. ['pic1.pic.ngo']) instead of the FQDN as the primary — fixes
  DNS_PROBE_FINISHED_BAD_CONFIG for all external domains when on VPN

- firewall_manager: add split_horizon_zones param to apply_all_dns_rules()
  and forward it to generate_corefile()

- Peers: filter service_access list to installed services only; peers.py
  derives valid services from config_manager.get_installed_services() with
  the email→mail ID mapping; Peers.jsx fetches from /api/store/installed
  and filters the checkboxes and defaults accordingly

- Health check: fix file_manager→'files' ID mapping so files service health
  is checked when installed (was silently skipped due to 'file' vs 'files')

- Verbosity persistence: move log_levels.json from non-mounted
  /app/api/config/ to CONFIG_DIR (/app/config/) which maps to config/api/
  on the host; both load (managers.py) and save (routes/services.py) updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 13:05:58 -04:00
parent 4ebcb1d077
commit c696ca9ef6
10 changed files with 83 additions and 46 deletions
+19 -4
View File
@@ -298,6 +298,19 @@ def _configured_domain() -> str:
return identity.get('domain_name') or identity.get('domain', 'cell')
def _configured_dns_params():
"""Return (primary_domain, split_horizon_zones) for Corefile generation.
In DDNS mode the primary CoreDNS zone is the parent domain (e.g. 'pic.ngo')
and the cell's FQDN (e.g. 'pic1.pic.ngo') is a separate split-horizon block
so LAN clients resolve *.pic1.pic.ngo to the internal Caddy IP.
In LAN mode both values are the same so split_horizon_zones is empty.
"""
primary = config_manager.get_internal_domain()
effective = config_manager.get_effective_domain()
return primary, ([effective] if effective != primary else [])
def _restore_cell_wg_peers(cell_links):
"""Re-add any cell link [Peer] blocks that are missing from wg0.conf.
@@ -359,8 +372,10 @@ def _apply_startup_enforcement():
# (happens if the container was rebuilt, wg0.conf was reset, etc.)
_restore_cell_wg_peers(cell_links)
wireguard_manager.sync_cell_routes()
firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH, _configured_domain(),
cell_links=cell_links)
_dns_primary, _dns_szones = _configured_dns_params()
firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH, _dns_primary,
cell_links=cell_links,
split_horizon_zones=_dns_szones)
logger.info(f"Applied enforcement rules for {len(peers)} peers, {len(cell_links)} cells on startup")
# Phase 3: reapply policy routing rules for peers whose internet traffic is
# routed through an exit cell (ip rule entries don't survive container restart)
@@ -529,12 +544,12 @@ def perform_health_check():
# email/calendar/files are optional store services — only check them when installed
_installed_store_ids = set(config_manager.get_installed_services())
_OPTIONAL_STORE_MANAGERS = frozenset({'email_manager', 'calendar_manager', 'file_manager'})
_MANAGER_TO_STORE_ID = {'email_manager': 'email', 'calendar_manager': 'calendar', 'file_manager': 'files'}
# Get health from each service
for service_name in service_bus.list_services():
if service_name in _OPTIONAL_STORE_MANAGERS:
# Map manager name to store service id (strip _manager suffix)
store_id = service_name.replace('_manager', '')
store_id = _MANAGER_TO_STORE_ID[service_name]
if store_id not in _installed_store_ids:
continue
try: