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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user