Phase 4: service store — manifest validation, install/remove, Store UI
- ServiceStoreManager: manifest allowlist (git.pic.ngo/roof/*), volume
denylist, ACCEPT-only iptables rules, ${SERVICE_IP}-only dest_ip
- IP allocator: pool 172.20.0.20-254, skips CONTAINER_OFFSETS VIPs
- Compose overlay: docker-compose.services.yml auto-included via DCF
- Flask blueprint at /api/store: list, install, remove, refresh
- Store.jsx: full install/remove UI with spinners and toast notifications
- 95 new unit tests for ServiceStoreManager (all passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -804,3 +804,52 @@ def apply_all_dns_rules(peers: List[Dict[str, Any]], corefile_path: str = COREFI
|
||||
if ok:
|
||||
reload_coredns()
|
||||
return ok
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Service store firewall rules
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _service_tag(service_id: str) -> str:
|
||||
safe = re.sub(r'[^a-z0-9]', '-', service_id.lower())
|
||||
return f'pic-svc-{safe}'
|
||||
|
||||
|
||||
def apply_service_rules(service_id: str, service_ip: str, rules: list) -> bool:
|
||||
"""Apply manifest-declared ACCEPT rules for an installed service."""
|
||||
tag = _service_tag(service_id)
|
||||
clear_service_rules(service_id)
|
||||
for r in rules:
|
||||
if r.get('type') != 'ACCEPT':
|
||||
continue
|
||||
dest_ip = r['dest_ip'].replace('${SERVICE_IP}', service_ip)
|
||||
dport = str(r['dest_port'])
|
||||
proto = r.get('proto', 'tcp')
|
||||
_iptables(['-I', 'FORWARD',
|
||||
'-d', dest_ip, '-p', proto, '--dport', dport,
|
||||
'-m', 'comment', '--comment', tag,
|
||||
'-j', 'ACCEPT'])
|
||||
return True
|
||||
|
||||
|
||||
def clear_service_rules(service_id: str) -> None:
|
||||
"""Remove all iptables rules tagged for this service using save/restore."""
|
||||
tag = _service_tag(service_id)
|
||||
comment_re = re.compile(rf'--comment\s+["\']?{re.escape(tag)}["\']?(\s|$)')
|
||||
try:
|
||||
save = _wg_exec(['iptables-save'])
|
||||
if save.returncode != 0:
|
||||
return
|
||||
lines = save.stdout.splitlines()
|
||||
filtered = [l for l in lines if not comment_re.search(l)]
|
||||
if len(filtered) == len(lines):
|
||||
return
|
||||
restore_input = '\n'.join(filtered) + '\n'
|
||||
restore = subprocess.run(
|
||||
['docker', 'exec', '-i', WIREGUARD_CONTAINER, 'iptables-restore'],
|
||||
input=restore_input, capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if restore.returncode != 0:
|
||||
logger.warning(f'clear_service_rules iptables-restore failed: {restore.stderr.strip()}')
|
||||
except Exception as e:
|
||||
logger.error(f'clear_service_rules({service_id}): {e}')
|
||||
|
||||
Reference in New Issue
Block a user