feat(cells): fix PIC-to-PIC connection + add service-sharing permissions
Phase 1 — connection fixes:
- routing_manager.stop(): remove iptables -F / -t nat -F nuclear flush that
would wipe WireGuard MASQUERADE and all peer rules on any UI stop action
- wireguard_manager.add_cell_peer(): reject vpn_subnet that overlaps the local
WG network (routing blackhole — was the root cause of no handshake)
- wireguard_manager._syncconf(): pass Endpoint to 'wg set' so cell peers with
static endpoints are synced to the kernel (not just AllowedIPs)
Phase 2 — service-sharing permissions backend:
- firewall_manager: add _cell_tag(), clear_cell_rules(), apply_cell_rules(),
apply_all_cell_rules() — iptables FORWARD rules for cell-to-cell traffic
using 'pic-cell-<name>' comment tags, distinct from 'pic-peer-*'
- app.py startup enforcement: call apply_all_cell_rules(cell_links) so rules
survive API restarts
- cell_link_manager: permissions schema {inbound, outbound} per service;
lazy migration for existing entries; update_permissions(), get_permissions();
apply_cell_rules wired into add_connection/remove_connection
- routes/cells.py: GET /api/cells/services, GET+PUT /api/cells/<n>/permissions;
RuntimeError now returns 400 (not 500) from add_connection
Removed broken 'test' cell (subnet 10.0.0.0/24 collided with local WG network).
Second PIC must use a distinct subnet (e.g. 10.0.1.0/24) before reconnecting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -365,6 +365,8 @@ class WireGuardManager(BaseServiceManager):
|
||||
current_peer['ips'] = line.split('=', 1)[1].strip()
|
||||
elif line.startswith('PersistentKeepalive'):
|
||||
current_peer['ka'] = line.split('=', 1)[1].strip()
|
||||
elif line.startswith('Endpoint'):
|
||||
current_peer['endpoint'] = line.split('=', 1)[1].strip()
|
||||
elif line == '' and 'pub' in current_peer:
|
||||
desired[current_peer['pub']] = current_peer
|
||||
current_peer = None
|
||||
@@ -397,6 +399,8 @@ class WireGuardManager(BaseServiceManager):
|
||||
'peer', pub,
|
||||
'allowed-ips', p.get('ips', ''),
|
||||
'persistent-keepalive', p.get('ka', '25')]
|
||||
if p.get('endpoint'):
|
||||
args += ['endpoint', p['endpoint']]
|
||||
subprocess.run(args, capture_output=True, timeout=5)
|
||||
|
||||
logger.info(f'wg set applied: {len(desired)} peers')
|
||||
@@ -483,7 +487,7 @@ class WireGuardManager(BaseServiceManager):
|
||||
logger.error(f'add_cell_peer: invalid endpoint port: {endpoint!r}')
|
||||
return False
|
||||
try:
|
||||
ipaddress.ip_network(vpn_subnet, strict=False)
|
||||
remote_net = ipaddress.ip_network(vpn_subnet, strict=False)
|
||||
except ValueError as e:
|
||||
logger.error(f'add_cell_peer: invalid vpn_subnet {vpn_subnet!r}: {e}')
|
||||
return False
|
||||
@@ -491,6 +495,17 @@ class WireGuardManager(BaseServiceManager):
|
||||
if any(c.isspace() for c in vpn_subnet):
|
||||
logger.error(f'add_cell_peer: vpn_subnet contains whitespace: {vpn_subnet!r}')
|
||||
return False
|
||||
# Reject subnets that overlap the local WG network — would create a routing blackhole
|
||||
try:
|
||||
local_net = ipaddress.ip_network(self._get_configured_network(), strict=False)
|
||||
if local_net.overlaps(remote_net):
|
||||
logger.error(
|
||||
f'add_cell_peer: vpn_subnet {vpn_subnet!r} overlaps local WG network '
|
||||
f'{str(local_net)!r} — use a distinct subnet on the remote cell'
|
||||
)
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
content = self._read_config()
|
||||
peer_block = (
|
||||
|
||||
Reference in New Issue
Block a user