feat: add EgressManager — per-service egress enforcement via host iptables
Unit Tests / test (push) Successful in 11m20s

Routes outbound traffic from installed service containers through
alternate exits (wireguard_ext, openvpn, tor) using host-side
iptables fwmark policy-routing in a dedicated PIC_EGRESS chain.
Marks 0x110/0x120/0x130 are distinct from ConnectivityManager's
0x10/0x20/0x30. Container IPs discovered at runtime via docker
inspect. Wired into ServiceStoreManager install/remove lifecycle
and managers.py singleton. 22 new tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 00:58:47 -04:00
parent 5cbbfb41d9
commit 03a67ad922
4 changed files with 962 additions and 1 deletions
+14 -1
View File
@@ -75,12 +75,13 @@ class ServiceStoreManager(BaseServiceManager):
def __init__(self, config_manager, caddy_manager, container_manager,
data_dir: str = '', config_dir: str = '',
service_composer=None):
service_composer=None, egress_manager=None):
super().__init__('service_store', data_dir, config_dir)
self.config_manager = config_manager
self.caddy_manager = caddy_manager
self.container_manager = container_manager
self.service_composer = service_composer
self.egress_manager = egress_manager
self.compose_override = os.environ.get(
'COMPOSE_SERVICES_PATH', '/app/docker-compose.services.yml'
)
@@ -345,6 +346,12 @@ class ServiceStoreManager(BaseServiceManager):
except Exception as e:
logger.warning('install: caddy regenerate failed for %s (non-fatal): %s', service_id, e)
if self.egress_manager:
try:
self.egress_manager.apply_service(service_id)
except Exception as exc:
logger.warning('Egress apply failed for %s (non-fatal): %s', service_id, exc)
return {'ok': True}
def remove(self, service_id: str, purge_data: bool = False) -> dict:
@@ -363,6 +370,12 @@ class ServiceStoreManager(BaseServiceManager):
'error': f'Cannot remove {service_id}: required by {", ".join(sorted(dependents))}',
}
if self.egress_manager:
try:
self.egress_manager.clear_service(service_id)
except Exception as exc:
logger.warning('Egress clear failed for %s (non-fatal): %s', service_id, exc)
# Stop and remove containers (best-effort)
if self.service_composer is not None:
try: