fix: full-tunnel default, real host routing table, peer config tunnel mode

- WireGuard default changed to full tunnel (0.0.0.0/0) — all peer traffic
  routes through PIC server so internet latency matches server's clean 41ms
- UI tunnel toggle now defaults to Full tunnel
- API /peers/config accepts allowed_ips param so UI toggle wires through
- Routing page reads real host routes via /proc/1/net/route (pid: host)
  instead of mock data; shows ens18/192.168.31.1 correctly
- Add iproute2 + util-linux to API Dockerfile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 15:20:55 -04:00
parent e7decf6f06
commit 9d7d74f3f4
6 changed files with 59 additions and 34 deletions
+50 -28
View File
@@ -862,37 +862,59 @@ class RoutingManager(BaseServiceManager):
logger.error(f"Failed to apply firewall rule: {e}")
def _get_routing_table(self) -> List[Dict]:
"""Get current routing table"""
"""Get host routing table from /proc/1/net/route (host PID namespace)."""
try:
result = subprocess.run(['ip', 'route', 'show'],
capture_output=True, text=True, timeout=10)
routes = []
for line in result.stdout.strip().split('\n'):
if line.strip():
routes.append({
'route': line.strip(),
'parsed': self._parse_route(line.strip())
})
return routes
except FileNotFoundError:
# System tools not available (development environment)
# Return mock routing table for development
return [
{
'route': 'default via 192.168.1.1 dev en0',
'parsed': {'destination': 'default', 'via': '192.168.1.1', 'dev': 'en0', 'metric': ''}
},
{
'route': '10.0.0.0/24 dev wg0',
'parsed': {'destination': '10.0.0.0/24', 'via': '', 'dev': 'wg0', 'metric': ''}
}
]
return self._parse_proc_net_route('/proc/1/net/route')
except Exception:
pass
# Fallback: WireGuard container routing table
try:
result = subprocess.run(
['docker', 'exec', 'cell-wireguard', 'ip', 'route', 'show'],
capture_output=True, text=True, timeout=10,
)
if result.returncode == 0:
routes = []
for line in result.stdout.strip().split('\n'):
if line.strip():
routes.append({'route': line.strip(), 'parsed': self._parse_route(line.strip())})
return routes
except Exception as e:
logger.error(f"Failed to get routing table: {e}")
return []
return []
def _parse_proc_net_route(self, path: str) -> List[Dict]:
"""Parse /proc/net/route hex table into human-readable routes."""
import socket, struct
routes = []
with open(path) as f:
lines = f.readlines()[1:] # skip header
for line in lines:
parts = line.strip().split()
if len(parts) < 8:
continue
iface, dest_hex, gw_hex, mask_hex = parts[0], parts[1], parts[2], parts[7]
def hex_to_ip(h):
return socket.inet_ntoa(struct.pack('<I', int(h, 16)))
dest = hex_to_ip(dest_hex)
gw = hex_to_ip(gw_hex)
mask = hex_to_ip(mask_hex)
prefix = bin(struct.unpack('>I', socket.inet_aton(mask))[0]).count('1')
if dest == '0.0.0.0' and mask == '0.0.0.0':
dest_str = 'default'
route_str = f'default via {gw} dev {iface}'
else:
dest_str = f'{dest}/{prefix}'
route_str = f'{dest}/{prefix} dev {iface}' + (f' via {gw}' if gw != '0.0.0.0' else '')
routes.append({
'route': route_str,
'parsed': {'destination': dest_str, 'via': gw if gw != '0.0.0.0' else '', 'dev': iface, 'metric': ''},
})
return routes
def _parse_route(self, route_line: str) -> Dict:
"""Parse route line into components"""