feat: Phase 3 - per-peer internet routing via exit cell
Adds the ability to route a specific peer's internet traffic through a
connected cell acting as an exit relay.
Cell A side:
- PUT /api/peers/<peer>/route-via {"via_cell": "cellB"} sets route_via
- Updates WG AllowedIPs to include 0.0.0.0/0 for the exit cell peer
- Adds ip rule + ip route in policy table inside cell-wireguard so the
specific peer's traffic egresses via cellB's WG IP
- Sets exit_relay_active on the cell link and pushes use_as_exit_relay=True
to cellB via peer-sync
Cell B side:
- Receives use_as_exit_relay in the peer-sync payload
- Calls apply_cell_rules(..., exit_relay=True) to add FORWARD -o eth0 ACCEPT
- Stores remote_exit_relay_active flag for startup recovery
Startup recovery:
- apply_all_cell_rules passes exit_relay=remote_exit_relay_active (cellB)
- _apply_startup_enforcement reapplies ip rule for each peer with route_via (cellA)
since policy routing rules don't survive container restart
peer_registry gets route_via field with lazy migration.
22 new tests across test_cell_link_manager, test_peer_registry, test_peer_route_via.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -193,6 +193,14 @@ class PeerRegistry(BaseServiceManager):
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading peers: {e}")
|
||||
self.peers = []
|
||||
# Phase 3 migration: per-peer internet routing
|
||||
changed = False
|
||||
for peer in self.peers:
|
||||
if 'route_via' not in peer:
|
||||
peer['route_via'] = None
|
||||
changed = True
|
||||
if changed:
|
||||
self._save_peers()
|
||||
else:
|
||||
self.peers = []
|
||||
self.logger.info("No peers file found, starting with empty registry")
|
||||
@@ -326,6 +334,18 @@ class PeerRegistry(BaseServiceManager):
|
||||
self.logger.error(f"Error updating peer {name} IP: {e}")
|
||||
return False
|
||||
|
||||
def set_route_via(self, peer_name: str, via_cell: Optional[str]) -> Dict[str, Any]:
|
||||
"""Set or clear the route_via field on a peer. Returns the updated peer dict."""
|
||||
with self.lock:
|
||||
for peer in self.peers:
|
||||
if peer.get('peer') == peer_name:
|
||||
peer['route_via'] = via_cell
|
||||
peer['updated_at'] = datetime.utcnow().isoformat()
|
||||
self._save_peers()
|
||||
self.logger.info(f"Set route_via for {peer_name}: {via_cell!r}")
|
||||
return dict(peer)
|
||||
raise ValueError(f"Peer '{peer_name}' not found")
|
||||
|
||||
def get_peer_stats(self) -> Dict[str, Any]:
|
||||
"""Get peer registry statistics"""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user