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:
+54
-13
@@ -161,6 +161,32 @@ class CaddyManager(BaseServiceManager):
|
||||
lines.append("}")
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _build_core_service_routes(domain: str) -> str:
|
||||
"""Return 4-space-indented named-matcher + handle blocks for core services."""
|
||||
return (
|
||||
f" @calendar host calendar.{domain}\n"
|
||||
f" handle @calendar {{\n"
|
||||
f" reverse_proxy cell-radicale:5232\n"
|
||||
f" }}\n"
|
||||
f" @mail host mail.{domain} webmail.{domain}\n"
|
||||
f" handle @mail {{\n"
|
||||
f" reverse_proxy cell-rainloop:8888\n"
|
||||
f" }}\n"
|
||||
f" @files host files.{domain}\n"
|
||||
f" handle @files {{\n"
|
||||
f" reverse_proxy cell-filegator:8080\n"
|
||||
f" }}\n"
|
||||
f" @webdav host webdav.{domain}\n"
|
||||
f" handle @webdav {{\n"
|
||||
f" reverse_proxy cell-webdav:80\n"
|
||||
f" }}\n"
|
||||
f" @api host api.{domain}\n"
|
||||
f" handle @api {{\n"
|
||||
f" reverse_proxy cell-api:3000\n"
|
||||
f" }}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _indent_routes(routes: str, spaces: int = 4) -> str:
|
||||
"""Indent a multi-line route block by ``spaces`` columns."""
|
||||
@@ -203,16 +229,17 @@ class CaddyManager(BaseServiceManager):
|
||||
def _caddyfile_pic_ngo(self, cell_name: str,
|
||||
service_routes: str, core_routes: str) -> str:
|
||||
"""pic_ngo mode: wildcard DNS-01 via the pic_ngo plugin."""
|
||||
body = []
|
||||
domain = f"{cell_name}.pic.ngo"
|
||||
body = [self._build_core_service_routes(domain)]
|
||||
if service_routes:
|
||||
body.append(self._indent_routes(service_routes))
|
||||
body.append(core_routes)
|
||||
inner = "\n".join(body)
|
||||
email = f"admin@{cell_name}.pic.ngo"
|
||||
email = f"admin@{domain}"
|
||||
return (
|
||||
f"{self._global_acme_block(email)}\n"
|
||||
"\n"
|
||||
f"*.{cell_name}.pic.ngo, {cell_name}.pic.ngo {{\n"
|
||||
f"*.{domain}, {domain} {{\n"
|
||||
" tls {\n"
|
||||
" dns pic_ngo {\n"
|
||||
" token {$PIC_NGO_DDNS_TOKEN}\n"
|
||||
@@ -226,7 +253,7 @@ class CaddyManager(BaseServiceManager):
|
||||
def _caddyfile_cloudflare(self, custom_domain: str,
|
||||
service_routes: str, core_routes: str) -> str:
|
||||
"""cloudflare mode: wildcard DNS-01 via the cloudflare plugin."""
|
||||
body = []
|
||||
body = [self._build_core_service_routes(custom_domain)]
|
||||
if service_routes:
|
||||
body.append(self._indent_routes(service_routes))
|
||||
body.append(core_routes)
|
||||
@@ -245,7 +272,8 @@ class CaddyManager(BaseServiceManager):
|
||||
def _caddyfile_duckdns(self, cell_name: str,
|
||||
service_routes: str, core_routes: str) -> str:
|
||||
"""duckdns mode: DNS-01 via the duckdns plugin."""
|
||||
body = []
|
||||
domain = f"{cell_name}.duckdns.org"
|
||||
body = [self._build_core_service_routes(domain)]
|
||||
if service_routes:
|
||||
body.append(self._indent_routes(service_routes))
|
||||
body.append(core_routes)
|
||||
@@ -253,7 +281,7 @@ class CaddyManager(BaseServiceManager):
|
||||
return (
|
||||
f"{self._global_acme_block(None)}\n"
|
||||
"\n"
|
||||
f"*.{cell_name}.duckdns.org {{\n"
|
||||
f"*.{domain} {{\n"
|
||||
" tls {\n"
|
||||
" dns duckdns {$DUCKDNS_TOKEN}\n"
|
||||
" }\n"
|
||||
@@ -265,23 +293,36 @@ class CaddyManager(BaseServiceManager):
|
||||
installed_services: List[Dict[str, Any]],
|
||||
core_routes: str) -> str:
|
||||
"""http01 mode: no wildcard. Each service gets its own block."""
|
||||
# Main host block — only the core routes (api + webui). Service
|
||||
# routes that could otherwise be served as path-prefixes are NOT
|
||||
# placed here because in http01 mode each service is intended to
|
||||
# live on its own subdomain (otherwise it could also use a path
|
||||
# prefix here, but the spec calls for separate blocks).
|
||||
# Main host block — only the core routes (api + webui).
|
||||
out = [self._global_acme_block('{$ACME_EMAIL}'), ""]
|
||||
out.append(f"{host} {{")
|
||||
out.append(core_routes)
|
||||
out.append("}")
|
||||
|
||||
# One block per installed service that has a caddy_route.
|
||||
# One block per core service subdomain.
|
||||
_core_services = [
|
||||
('calendar', 'cell-radicale:5232'),
|
||||
('mail', 'cell-rainloop:8888'),
|
||||
('webmail', 'cell-rainloop:8888'),
|
||||
('files', 'cell-filegator:8080'),
|
||||
('webdav', 'cell-webdav:80'),
|
||||
('api', 'cell-api:3000'),
|
||||
]
|
||||
for subdomain, backend in _core_services:
|
||||
out.append("")
|
||||
out.append(f"{subdomain}.{host} {{")
|
||||
out.append(f" reverse_proxy {backend}")
|
||||
out.append("}")
|
||||
|
||||
# One block per installed (store plugin) service that has a caddy_route,
|
||||
# skipping any name that conflicts with a core service.
|
||||
_core_names = {s for s, _ in _core_services}
|
||||
for svc in installed_services or []:
|
||||
if not svc:
|
||||
continue
|
||||
route = svc.get('caddy_route')
|
||||
name = svc.get('name') or svc.get('subdomain')
|
||||
if not route or not name:
|
||||
if not route or not name or name in _core_names:
|
||||
continue
|
||||
out.append("")
|
||||
out.append(f"{name}.{host} {{")
|
||||
|
||||
Reference in New Issue
Block a user