Implement connectivity store services (wireguard-ext, openvpn-client, tor)
Unit Tests / test (push) Successful in 11m31s
Unit Tests / test (push) Successful in 11m31s
- ConnectivityManager: move config dirs to data_dir/services/<id>/config so Docker can bind-mount them into store-service containers (Docker resolves bind-mount paths on the host, not inside the API container). Add _migrate_legacy_configs to copy existing files from the old config_dir location on first boot. - manifest_validator: add allow_host_network parameter to validate_rendered_compose. When True, waives the external-network requirement, permits network_mode: host, and allows devices: — all needed by VPN/Tor containers that must share the host network namespace to create tun/wg interfaces. Non-host services are unaffected. - service_composer: read requires_host_network from the manifest and pass allow_host_network=True to validate_rendered_compose for connectivity services. - Tests: update file-path assertions to new data_dir layout; add TestMigrateLegacyConfigs, TestValidateRenderedComposeHostNetwork, and two TestWriteCompose cases for the host-network path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+32
-16
@@ -158,7 +158,8 @@ def validate_manifest(manifest: dict) -> tuple:
|
||||
return (len(errors) == 0, errors)
|
||||
|
||||
|
||||
def validate_rendered_compose(yaml_text: str, allowed_data_dir: str = None) -> tuple:
|
||||
def validate_rendered_compose(yaml_text: str, allowed_data_dir: str = None,
|
||||
allow_host_network: bool = False) -> tuple:
|
||||
"""
|
||||
Parse and security-validate a rendered docker-compose YAML string.
|
||||
|
||||
@@ -168,6 +169,12 @@ def validate_rendered_compose(yaml_text: str, allowed_data_dir: str = None) -> t
|
||||
allowed_data_dir: when set, absolute bind mounts under this prefix are
|
||||
permitted — they come from ${PIC_DATA_DIR} substitution and land in the
|
||||
designated service data directory.
|
||||
|
||||
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.
|
||||
"""
|
||||
errors = []
|
||||
|
||||
@@ -179,17 +186,19 @@ def validate_rendered_compose(yaml_text: str, allowed_data_dir: str = None) -> t
|
||||
if not isinstance(doc, dict):
|
||||
return (False, ['compose file must be a YAML mapping'])
|
||||
|
||||
# At least one external network must exist so the container joins the cell network
|
||||
# rather than an isolated bridge that would be invisible to Caddy and CoreDNS.
|
||||
networks = doc.get('networks') or {}
|
||||
has_external = any(
|
||||
isinstance(v, dict) and v.get('external')
|
||||
for v in networks.values()
|
||||
)
|
||||
if not has_external:
|
||||
errors.append(
|
||||
'compose file must declare at least one network with external: true'
|
||||
# Regular (bridged) services must join the cell-network so Caddy and CoreDNS
|
||||
# can reach them. Host-network services share the host namespace directly,
|
||||
# so the external network declaration would be wrong and is omitted.
|
||||
if not allow_host_network:
|
||||
networks = doc.get('networks') or {}
|
||||
has_external = any(
|
||||
isinstance(v, dict) and v.get('external')
|
||||
for v in networks.values()
|
||||
)
|
||||
if not has_external:
|
||||
errors.append(
|
||||
'compose file must declare at least one network with external: true'
|
||||
)
|
||||
|
||||
for svc_name, svc in (doc.get('services') or {}).items():
|
||||
if not isinstance(svc, dict):
|
||||
@@ -204,10 +213,17 @@ def validate_rendered_compose(yaml_text: str, allowed_data_dir: str = None) -> t
|
||||
errors.append(f'{prefix}: privileged: true is not allowed')
|
||||
|
||||
net_mode = svc.get('network_mode')
|
||||
if net_mode is not None and net_mode not in (None, 'bridge'):
|
||||
errors.append(
|
||||
f'{prefix}: network_mode {net_mode!r} is not allowed (only bridge)'
|
||||
)
|
||||
if allow_host_network:
|
||||
if net_mode is not None and net_mode not in ('host',):
|
||||
errors.append(
|
||||
f'{prefix}: network_mode {net_mode!r} is not allowed '
|
||||
'(connectivity services must use host)'
|
||||
)
|
||||
else:
|
||||
if net_mode is not None and net_mode not in (None, 'bridge'):
|
||||
errors.append(
|
||||
f'{prefix}: network_mode {net_mode!r} is not allowed (only bridge)'
|
||||
)
|
||||
|
||||
if svc.get('pid') == 'host':
|
||||
errors.append(f'{prefix}: pid: host is not allowed')
|
||||
@@ -238,7 +254,7 @@ def validate_rendered_compose(yaml_text: str, allowed_data_dir: str = None) -> t
|
||||
f'{prefix}: absolute host bind mount not allowed: {vol_str!r}'
|
||||
)
|
||||
|
||||
if 'devices' in svc:
|
||||
if 'devices' in svc and not allow_host_network:
|
||||
errors.append(f'{prefix}: devices key is not allowed')
|
||||
|
||||
for opt in svc.get('security_opt') or []:
|
||||
|
||||
Reference in New Issue
Block a user