feat: connectivity — registry-driven peer table, sshuttle/proxy egress, egress UI

The peer table was empty because it was not consulting the peer registry;
now peers are driven by PeerRegistry so the Connectivity page reflects actual
connected cells.

Exit-key handling is unified: all code paths now use the same key derivation
so a store-service exit bridge and a manual WireGuard peer both produce
consistent routing state.

Two new egress exit types are added (sshuttle via SSH tunnel and proxy via
redsocks SOCKS5), wiring through connectivity_manager, egress_manager, and
app.py routes. This lets a cell route its traffic through an SSH host or a
SOCKS5 proxy as an alternative to WireGuard exit nodes.

ServiceStoreManager and ServiceBus updated so the egress lifecycle (install /
uninstall) is cleanly signalled between components.

Connectivity.jsx gains the Service Egress section, letting operators assign
and reassign egress methods from the UI without touching config files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 08:36:15 -04:00
parent cc7a223fdf
commit 6232ef23a9
8 changed files with 1096 additions and 61 deletions
+21 -9
View File
@@ -19,19 +19,26 @@ from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
EXIT_TYPES = ("default", "wireguard_ext", "openvpn", "tor")
EXIT_TYPES = ("default", "wireguard_ext", "openvpn", "tor", "sshuttle", "proxy")
# fwmark values — must not collide with ConnectivityManager (0x10, 0x20, 0x30)
MARKS = {"wireguard_ext": 0x110, "openvpn": 0x120, "tor": 0x130}
# fwmark values — must not collide with ConnectivityManager
# (0x10, 0x20, 0x30, 0x40, 0x50)
MARKS = {"wireguard_ext": 0x110, "openvpn": 0x120, "tor": 0x130,
"sshuttle": 0x140, "proxy": 0x150}
# Policy routing table IDs
TABLES = {"wireguard_ext": 210, "openvpn": 220, "tor": 230}
TABLES = {"wireguard_ext": 210, "openvpn": 220, "tor": 230,
"sshuttle": 240, "proxy": 250}
EGRESS_CHAIN = "PIC_EGRESS"
# Transparent proxy port used by Tor
_TOR_TRANS_PORT = 9040
# Local transparent-proxy ports for redirect-style exits (no exit iface):
# traffic is REDIRECTed to the listener of the corresponding exit container.
_REDIRECT_PORTS = {"tor": _TOR_TRANS_PORT, "sshuttle": 12300, "proxy": 12345}
class EgressManager:
"""Per-service egress enforcement via host iptables fwmark policy-routing."""
@@ -94,8 +101,9 @@ class EgressManager:
self._ensure_chains()
self._ensure_host_ip_rules()
self._add_mark_rule(container_ip, MARKS[exit_via], service_id)
if exit_via == 'tor':
self._add_tor_redirect(container_ip, service_id)
if exit_via in _REDIRECT_PORTS:
self._add_redirect(container_ip, _REDIRECT_PORTS[exit_via],
service_id)
except Exception as exc:
logger.error('apply_service(%s): %s', service_id, exc)
return {'ok': False, 'error': str(exc)}
@@ -266,15 +274,19 @@ class EgressManager:
'-m', 'comment', '--comment', self._tag(service_id),
])
def _add_tor_redirect(self, service_ip: str, service_id: str) -> None:
"""Redirect the service container's TCP traffic to the local Tor TransPort."""
def _add_redirect(self, service_ip: str, port: int, service_id: str) -> None:
"""Redirect the container's TCP traffic to a local transparent-proxy port."""
self._iptables([
'-t', 'nat', '-A', EGRESS_CHAIN,
'-s', service_ip, '-p', 'tcp',
'-j', 'REDIRECT', '--to-ports', str(_TOR_TRANS_PORT),
'-j', 'REDIRECT', '--to-ports', str(port),
'-m', 'comment', '--comment', self._tag(service_id),
])
def _add_tor_redirect(self, service_ip: str, service_id: str) -> None:
"""Redirect the service container's TCP traffic to the local Tor TransPort."""
self._add_redirect(service_ip, _TOR_TRANS_PORT, service_id)
def _clear_egress_rules(self, service_id: str) -> None:
"""Remove all rules tagged pic-egr-<service_id> from mangle and nat."""
import re as _re