feat: route PIC services as subdomains of the cell's effective domain
Unit Tests / test (push) Successful in 11m33s
Unit Tests / test (push) Successful in 11m33s
In DDNS modes (pic_ngo, cloudflare, duckdns, http01), all built-in services are now reachable as subdomains of the cell domain, e.g. calendar.pic1.pic.ngo instead of pic1.pic.ngo/calendar. Key changes: - CaddyManager._build_core_service_routes(): new helper generates Caddy named-matcher host blocks for calendar, mail/webmail, files, webdav, and api subdomains within the wildcard TLS server block. - All ACME modes (pic_ngo, cloudflare, duckdns) use the new subdomain matchers; http01 emits a dedicated server block per service. - http01: installed store-plugin services whose name clashes with a core service are skipped to prevent duplicate server blocks. - routes/config.py: ip_utils.write_caddyfile() is skipped in non-LAN modes so LAN Caddy config never overwrites the ACME config. - firewall_manager.generate_corefile(): new split_horizon_zones param adds local authoritative file zones so LAN clients resolve *.pic1.pic.ngo to the internal Caddy IP without hairpin NAT. - NetworkManager.update_split_horizon_zone(): writes the wildcard zone file and regenerates the Corefile with the split-horizon block; called automatically after every identity change in non-LAN mode. - Added @ to allowed record-name chars in update_dns_zone validation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+29
-1
@@ -45,7 +45,7 @@ class NetworkManager(BaseServiceManager):
|
||||
for rec in records:
|
||||
rname = rec.get('name', '')
|
||||
rvalue = rec.get('value', '')
|
||||
if rname and not re.match(r'^[a-zA-Z0-9_.*-]{1,253}$', str(rname)):
|
||||
if rname and not re.match(r'^[a-zA-Z0-9_@.*-]{1,253}$', str(rname)):
|
||||
logger.error(f"update_dns_zone: invalid record name {rname!r}")
|
||||
return False
|
||||
if rvalue and not re.match(r'^[a-zA-Z0-9._: -]{1,512}$', str(rvalue)):
|
||||
@@ -165,6 +165,34 @@ class NetworkManager(BaseServiceManager):
|
||||
self.update_dns_zone(domain, records)
|
||||
logger.info(f"Created {len(records)} default DNS records for zone '{domain}'")
|
||||
|
||||
def update_split_horizon_zone(self, effective_domain: str, caddy_ip: str,
|
||||
primary_domain: str = 'cell',
|
||||
peers: Optional[List[Dict]] = None,
|
||||
cell_links: Optional[List[Dict]] = None) -> bool:
|
||||
"""Write a local authoritative zone for effective_domain pointing all
|
||||
hosts (wildcard) to caddy_ip so LAN clients resolve service subdomains
|
||||
without hairpin NAT. Regenerates the Corefile and reloads CoreDNS."""
|
||||
import firewall_manager as _fm
|
||||
# SOA/NS are generated by _generate_zone_content; just pass the A records.
|
||||
records = [
|
||||
{'name': '@', 'type': 'A', 'value': caddy_ip},
|
||||
{'name': '*', 'type': 'A', 'value': caddy_ip},
|
||||
]
|
||||
ok = self.update_dns_zone(effective_domain, records)
|
||||
if not ok:
|
||||
logger.warning('update_split_horizon_zone: zone file write failed for %s', effective_domain)
|
||||
|
||||
corefile = os.path.join(self.config_dir, 'dns', 'Corefile')
|
||||
peers_data = peers or []
|
||||
ok_cf = _fm.generate_corefile(
|
||||
peers_data, corefile, primary_domain,
|
||||
cell_links=cell_links,
|
||||
split_horizon_zones=[effective_domain],
|
||||
)
|
||||
if ok_cf:
|
||||
_fm.reload_coredns()
|
||||
return ok and ok_cf
|
||||
|
||||
def apply_ip_range(self, ip_range: str, cell_name: str, domain: str) -> Dict[str, Any]:
|
||||
"""Rewrite the primary DNS zone file with IPs derived from the new subnet."""
|
||||
restarted: List[str] = []
|
||||
|
||||
Reference in New Issue
Block a user