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
+57
View File
@@ -453,6 +453,63 @@ class NetworkManager(BaseServiceManager):
warnings.append(f"cell_name DNS update failed: {e}")
return {'restarted': restarted, 'warnings': warnings}
def add_cell_dns_forward(self, domain: str, dns_ip: str) -> Dict[str, Any]:
"""Append a CoreDNS forwarding block for a remote cell's domain."""
restarted = []
warnings = []
try:
corefile = os.path.join(self.config_dir, 'dns', 'Corefile')
if not os.path.exists(corefile):
warnings.append('Corefile not found')
return {'restarted': restarted, 'warnings': warnings}
with open(corefile) as f:
content = f.read()
marker = f'# cell:{domain}'
if marker in content:
return {'restarted': restarted, 'warnings': warnings} # already present
forward_block = (
f'\n{marker}\n'
f'{domain} {{\n'
f' forward . {dns_ip}\n'
f' log\n'
f'}}\n'
)
with open(corefile, 'a') as f:
f.write(forward_block)
self._reload_dns_service()
restarted.append('cell-dns (reloaded)')
except Exception as e:
warnings.append(f'add_cell_dns_forward failed: {e}')
return {'restarted': restarted, 'warnings': warnings}
def remove_cell_dns_forward(self, domain: str) -> Dict[str, Any]:
"""Remove a CoreDNS forwarding block for a remote cell's domain."""
import re
restarted = []
warnings = []
try:
corefile = os.path.join(self.config_dir, 'dns', 'Corefile')
if not os.path.exists(corefile):
return {'restarted': restarted, 'warnings': warnings}
with open(corefile) as f:
content = f.read()
marker = f'# cell:{domain}'
if marker not in content:
return {'restarted': restarted, 'warnings': warnings}
new_content = re.sub(
rf'\n# cell:{re.escape(domain)}\n{re.escape(domain)}\s*\{{[^}}]*\}}\n',
'',
content,
flags=re.DOTALL,
)
with open(corefile, 'w') as f:
f.write(new_content)
self._reload_dns_service()
restarted.append('cell-dns (reloaded)')
except Exception as e:
warnings.append(f'remove_cell_dns_forward failed: {e}')
return {'restarted': restarted, 'warnings': warnings}
def test_dns_resolution(self, domain: str) -> Dict:
"""Test DNS resolution for a domain using Python socket."""
import socket