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
+45
View File
@@ -323,6 +323,51 @@ PersistentKeepalive = 30
self.assertFalse(success)
class TestWireGuardCellPeer(unittest.TestCase):
"""Test add_cell_peer allows subnet CIDRs for site-to-site connections."""
def setUp(self):
self.test_dir = tempfile.mkdtemp()
self.data_dir = os.path.join(self.test_dir, 'data')
self.config_dir = os.path.join(self.test_dir, 'config')
os.makedirs(self.data_dir, exist_ok=True)
os.makedirs(self.config_dir, exist_ok=True)
patcher = patch.object(WireGuardManager, '_syncconf', return_value=None)
self.mock_sync = patcher.start()
self.addCleanup(patcher.stop)
self.wg = WireGuardManager(self.data_dir, self.config_dir)
def tearDown(self):
shutil.rmtree(self.test_dir)
def test_add_cell_peer_allows_subnet_cidr(self):
ok = self.wg.add_cell_peer('remote', 'remotepubkey=', '5.6.7.8:51821', '10.1.0.0/24')
self.assertTrue(ok)
content = self.wg._read_config()
self.assertIn('10.1.0.0/24', content)
def test_add_cell_peer_writes_full_endpoint(self):
self.wg.add_cell_peer('remote', 'remotepubkey=', '5.6.7.8:51821', '10.1.0.0/24')
content = self.wg._read_config()
self.assertIn('Endpoint = 5.6.7.8:51821', content)
def test_add_cell_peer_comment_has_cell_prefix(self):
self.wg.add_cell_peer('remote', 'remotepubkey=', '5.6.7.8:51821', '10.1.0.0/24')
content = self.wg._read_config()
self.assertIn('# cell:remote', content)
def test_add_cell_peer_invalid_cidr_returns_false(self):
ok = self.wg.add_cell_peer('remote', 'remotepubkey=', '5.6.7.8:51821', 'not-a-cidr')
self.assertFalse(ok)
def test_add_cell_peer_can_coexist_with_regular_peers(self):
self.wg.add_peer('alice', 'alicepubkey=', '', '10.0.0.2/32')
self.wg.add_cell_peer('remote', 'remotepubkey=', '5.6.7.8:51821', '10.1.0.0/24')
content = self.wg._read_config()
self.assertIn('alicepubkey=', content)
self.assertIn('remotepubkey=', content)
class TestWireGuardConfigReads(unittest.TestCase):
"""Test that port/address/network are read from wg0.conf, not hardcoded."""