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:
@@ -272,5 +272,56 @@ test2 1800 IN CNAME test1
|
||||
self.assertIn('192.168.1.10', content)
|
||||
self.assertIn('192.168.1.11', content)
|
||||
|
||||
class TestCellDnsForwarding(unittest.TestCase):
|
||||
"""Test add/remove cell DNS forwarding in Corefile."""
|
||||
|
||||
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(os.path.join(self.config_dir, 'dns'), exist_ok=True)
|
||||
self.nm = NetworkManager(self.data_dir, self.config_dir)
|
||||
self.corefile = os.path.join(self.config_dir, 'dns', 'Corefile')
|
||||
with open(self.corefile, 'w') as f:
|
||||
f.write('home.cell {\n file /data/home.cell.zone\n log\n}\n\n. {\n forward . 8.8.8.8\n log\n}\n')
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.test_dir)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_add_cell_dns_forward_appends_block(self, _mock):
|
||||
self.nm.add_cell_dns_forward('remote.cell', '10.1.0.1')
|
||||
with open(self.corefile) as f:
|
||||
content = f.read()
|
||||
self.assertIn('remote.cell', content)
|
||||
self.assertIn('10.1.0.1', content)
|
||||
self.assertIn('forward . 10.1.0.1', content)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_add_cell_dns_forward_idempotent(self, _mock):
|
||||
self.nm.add_cell_dns_forward('remote.cell', '10.1.0.1')
|
||||
self.nm.add_cell_dns_forward('remote.cell', '10.1.0.1')
|
||||
with open(self.corefile) as f:
|
||||
content = f.read()
|
||||
self.assertEqual(content.count('forward . 10.1.0.1'), 1)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_remove_cell_dns_forward_cleans_block(self, _mock):
|
||||
self.nm.add_cell_dns_forward('remote.cell', '10.1.0.1')
|
||||
self.nm.remove_cell_dns_forward('remote.cell')
|
||||
with open(self.corefile) as f:
|
||||
content = f.read()
|
||||
self.assertNotIn('remote.cell', content)
|
||||
self.assertNotIn('10.1.0.1', content)
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_remove_nonexistent_forward_is_noop(self, _mock):
|
||||
before = open(self.corefile).read()
|
||||
self.nm.remove_cell_dns_forward('nonexistent.cell')
|
||||
after = open(self.corefile).read()
|
||||
self.assertEqual(before, after)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user