fix: P0/P1 audit fixes — DDNS correctness, peer provisioning gates, honest stubs

CloudflareDDNS.update() was calling the wrong endpoint; fix to use the
correct zone-records API so DDNS updates actually land.

NoIP and FreeDNS providers now return explicit "not implemented" errors
instead of silently claiming success, preventing false-positive health state.

PicNgoDNS ACME dns-challenge now sends the token in the request body (was
missing), so cert issuance no longer silently fails.

add_peer gates builtin-service provisioning on the installed-services list
so a freshly-provisioned peer does not attempt to configure services that
aren't present, eliminating the startup error loop.

Startup Caddyfile regeneration added to routes/config.py so that a stale
on-disk Caddyfile no longer triggers the health-monitor restart loop after
a config change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 08:23:00 -04:00
parent 649378b59b
commit cc7a223fdf
4 changed files with 200 additions and 74 deletions
+8 -10
View File
@@ -9,6 +9,8 @@ import logging
import re
import yaml
from constants import RESERVED_SUBDOMAINS
logger = logging.getLogger('picell')
_SUBDOMAIN_RE = re.compile(r'^[a-z][a-z0-9-]{0,30}$')
@@ -21,12 +23,6 @@ _CAP_DENYLIST = frozenset({
'ALL', 'SYS_ADMIN', 'SYS_MODULE', 'SYS_PTRACE', 'SYS_RAWIO',
'SYS_BOOT', 'MAC_ADMIN', 'MAC_OVERRIDE', 'SYS_TIME', 'SYS_TTY_CONFIG',
})
_RESERVED_SUBDOMAINS = frozenset({
# Core PIC infrastructure — never allow store services to hijack these
'api', 'webui', 'admin', 'www', 'ns1', 'ns2', 'git', 'registry', 'install',
# 'mail', 'calendar', 'files', 'webdav', 'webmail' are intentionally absent:
# they belong to official PIC store services and must be claimable by them.
})
_BACKEND_DENYLIST = frozenset({
'cell-api', 'cell-caddy', 'cell-coredns', 'cell-dnsmasq',
'cell-wireguard', 'cell-vault', 'localhost', '127.0.0.1',
@@ -172,9 +168,11 @@ def validate_rendered_compose(yaml_text: str, allowed_data_dir: str = None,
allow_host_network: when True, the compose file is permitted to use
network_mode: host and devices: — required for connectivity services
(wireguard-ext, openvpn-client, tor) that must share the host network
namespace to create tun/wg interfaces. The external-network requirement
is also waived since host-network containers reach the cell network directly.
(wireguard-ext, openvpn-client, tor, sshuttle [cell-sshuttle],
proxy [cell-redsocks]) that must share the host network namespace to
create tun/wg interfaces or expose local transparent-proxy listeners.
The external-network requirement is also waived since host-network
containers reach the cell network directly.
"""
errors = []
@@ -330,7 +328,7 @@ def _check_subdomain(value, field_name: str, errors: list) -> None:
if not isinstance(value, str):
errors.append(f'{field_name} must be a string')
return
if value in _RESERVED_SUBDOMAINS:
if value in RESERVED_SUBDOMAINS:
errors.append(f'{field_name} is reserved: {value!r}')
elif not _SUBDOMAIN_RE.match(value):
errors.append(