feat: cell-to-cell (PIC mesh) connection feature

Site-to-site WireGuard tunnels between PIC cells with automatic DNS forwarding.
Each cell generates an invite JSON (public key, endpoint, VPN subnet, DNS IP,
domain); the remote cell imports it to establish a bidirectional tunnel and
CoreDNS forwarding block so each cell's domain resolves across the mesh.

Backend:
- CellLinkManager: invite generation, add/remove connections, live WireGuard
  handshake status; stores links in data/cell_links.json
- WireGuardManager: add_cell_peer() accepts subnet CIDRs (not /32) and an
  optional endpoint for site-to-site peers; _read_iface_field() reads port,
  address, and network directly from wg0.conf at runtime instead of constants
- NetworkManager: add/remove CoreDNS forwarding blocks per remote cell domain
- app.py: /api/cells/* routes; _next_peer_ip() derives VPN range from
  configured address so peer allocation follows any address change

Frontend:
- CellNetwork page: invite panel (JSON + QR), connect form (paste JSON),
  connected cells list (green/red status, disconnect button)
- App.jsx: Cell Network nav entry and route

Tests: 25 new tests across test_wireguard_manager, test_network_manager,
test_cell_link_manager (263 total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 08:34:21 -04:00
parent 3912452fd6
commit 848f8cfc7c
10 changed files with 872 additions and 1 deletions
+29
View File
@@ -354,6 +354,35 @@ class WireGuardManager(BaseServiceManager):
logger.error(f'add_peer failed: {e}')
return False
def add_cell_peer(self, name: str, public_key: str, endpoint: str, vpn_subnet: str) -> bool:
"""Add a site-to-site [Peer] block for another PIC cell.
Unlike add_peer(), allows a subnet CIDR as AllowedIPs (whole remote VPN range).
The endpoint is expected to already include the port (e.g. '1.2.3.4:51820').
"""
import ipaddress
try:
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
try:
content = self._read_config()
peer_block = (
f'\n[Peer]\n'
f'# cell:{name}\n'
f'PublicKey = {public_key}\n'
f'AllowedIPs = {vpn_subnet}\n'
f'PersistentKeepalive = 25\n'
)
if endpoint:
peer_block += f'Endpoint = {endpoint}\n'
self._write_config(content + peer_block)
return True
except Exception as e:
logger.error(f'add_cell_peer failed: {e}')
return False
def remove_peer(self, public_key: str) -> bool:
"""Remove the [Peer] block matching public_key from wg0.conf."""
try: