From 9d7d74f3f4d0acf90ca05af92f7cad29d9705c8d Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Mon, 20 Apr 2026 15:20:55 -0400 Subject: [PATCH] fix: full-tunnel default, real host routing table, peer config tunnel mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- api/Dockerfile | 2 + api/app.py | 2 + api/routing_manager.py | 78 ++++++++++++++++++++++------------- api/wireguard_manager.py | 8 ++-- docker-compose.yml | 1 + webui/src/pages/WireGuard.jsx | 2 +- 6 files changed, 59 insertions(+), 34 deletions(-) diff --git a/api/Dockerfile b/api/Dockerfile index b5faa9e..83d8ac7 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -6,6 +6,8 @@ WORKDIR /app/api RUN apt-get update && apt-get install -y \ wireguard-tools \ iptables \ + iproute2 \ + util-linux \ curl \ ca-certificates \ gnupg \ diff --git a/api/app.py b/api/app.py index 0a43f78..5a76616 100644 --- a/api/app.py +++ b/api/app.py @@ -902,11 +902,13 @@ def get_peer_config(): srv = wireguard_manager.get_server_config() server_endpoint = srv.get('endpoint') or '' + allowed_ips = data.get('allowed_ips') or None result = wireguard_manager.get_peer_config( peer_name=peer_name, peer_ip=peer_ip, peer_private_key=peer_private_key, server_endpoint=server_endpoint, + allowed_ips=allowed_ips, ) return jsonify({"config": result}) except Exception as e: diff --git a/api/routing_manager.py b/api/routing_manager.py index 5555720..f117c88 100644 --- a/api/routing_manager.py +++ b/api/routing_manager.py @@ -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', 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""" diff --git a/api/wireguard_manager.py b/api/wireguard_manager.py index dabb512..a91929d 100644 --- a/api/wireguard_manager.py +++ b/api/wireguard_manager.py @@ -237,18 +237,16 @@ class WireGuardManager(BaseServiceManager): self._write_config('\n'.join(new_lines)) return True - # Split-tunnel: only route cell VPN + Docker subnets through WireGuard. - # This keeps the client's local LAN traffic (e.g. 192.168.x.x) off the tunnel, - # avoiding the internet RTT penalty when pinging local devices. SPLIT_TUNNEL_IPS = '10.0.0.0/24, 172.20.0.0/16' + FULL_TUNNEL_IPS = '0.0.0.0/0, ::/0' def get_peer_config(self, peer_name: str, peer_ip: str, peer_private_key: str, server_endpoint: str = '', allowed_ips: str = None) -> str: - """Generate a WireGuard client config string (split-tunnel by default).""" + """Generate a WireGuard client config string (full-tunnel by default).""" if allowed_ips is None: - allowed_ips = self.SPLIT_TUNNEL_IPS + allowed_ips = self.FULL_TUNNEL_IPS server_keys = self.get_keys() peer_dns = _resolve_peer_dns() endpoint = server_endpoint if ':' in server_endpoint else f'{server_endpoint}:{DEFAULT_PORT}' diff --git a/docker-compose.yml b/docker-compose.yml index 767b155..501309f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -157,6 +157,7 @@ services: - ./config/api:/app/config - ./config/wireguard:/app/config/wireguard - /var/run/docker.sock:/var/run/docker.sock + pid: host restart: unless-stopped networks: cell-network: diff --git a/webui/src/pages/WireGuard.jsx b/webui/src/pages/WireGuard.jsx index 5ba1faa..d9fed90 100644 --- a/webui/src/pages/WireGuard.jsx +++ b/webui/src/pages/WireGuard.jsx @@ -16,7 +16,7 @@ function WireGuard() { const [peerConfig, setPeerConfig] = useState(''); const [qrCodeDataUrl, setQrCodeDataUrl] = useState(''); const [peerStatuses, setPeerStatuses] = useState({}); - const [tunnelMode, setTunnelMode] = useState('split'); // 'split' or 'full' + const [tunnelMode, setTunnelMode] = useState('full'); // 'split' or 'full' useEffect(() => { fetchWireGuardData();