From 0b5a5b23e8ad82ec5db02924f2332b985708136a Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Mon, 20 Apr 2026 14:18:43 -0400 Subject: [PATCH] fix: split-tunnel default for peers, port check via wg interface, tunnel mode toggle in UI - check_port_open now checks if wg0 interface is actually listening (via 'wg show wg0') instead of requiring a live peer handshake. This means the port shows 'Open' whenever WireGuard is running, not only when a peer has connected recently. - get_peer_config defaults to split-tunnel AllowedIPs (10.0.0.0/24, 172.20.0.0/16) so VPN clients only route cell service traffic through the tunnel. Local LAN traffic (192.168.x.x etc.) stays direct, fixing the 60-120ms penalty when pinging local hosts while on VPN. - Peer config modal now uses cell DNS (172.20.0.2) so .cell domains resolve correctly with both split and full tunnel. - Added split/full tunnel toggle in the peer config modal so users can download either config variant. Co-Authored-By: Claude Sonnet 4.6 --- api/wireguard_manager.py | 42 +++++++++---------- webui/src/pages/WireGuard.jsx | 77 +++++++++++++++++++---------------- 2 files changed, 63 insertions(+), 56 deletions(-) diff --git a/api/wireguard_manager.py b/api/wireguard_manager.py index 0bf164a..1e9786e 100644 --- a/api/wireguard_manager.py +++ b/api/wireguard_manager.py @@ -237,11 +237,18 @@ 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' + def get_peer_config(self, peer_name: str, peer_ip: str, peer_private_key: str, server_endpoint: str = '', - allowed_ips: str = '0.0.0.0/0, ::/0') -> str: - """Generate a WireGuard client config string (full-tunnel by default).""" + allowed_ips: str = None) -> str: + """Generate a WireGuard client config string (split-tunnel by default).""" + if allowed_ips is None: + allowed_ips = self.SPLIT_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}' @@ -302,11 +309,18 @@ class WireGuardManager(BaseServiceManager): return ip def check_port_open(self, port: int = DEFAULT_PORT) -> bool: - """Check if the WireGuard UDP port is reachable from outside.""" - external_ip = self.get_external_ip() - if not external_ip: - return False - # Check via WireGuard itself: if any peer has a recent handshake the port is open + """Check if WireGuard is running and listening on the UDP port.""" + # Primary: check if wg0 interface is up (means port IS listening) + try: + result = subprocess.run( + ['docker', 'exec', 'cell-wireguard', 'wg', 'show', 'wg0'], + capture_output=True, text=True, timeout=5, + ) + if result.returncode == 0 and 'listen port' in result.stdout.lower(): + return True + except Exception: + pass + # Fallback: recent peer handshake confirms external reachability try: statuses = self.get_all_peer_statuses() for st in statuses.values(): @@ -314,20 +328,6 @@ class WireGuardManager(BaseServiceManager): return True except Exception: pass - # Try UDP port check APIs that support UDP - if _requests is not None: - for url, params in [ - ('https://portchecker.io/api/query', {'host': external_ip, 'port': port, 'type': 'udp'}), - ('https://api.ipquery.io/portcheck', {'ip': external_ip, 'port': port, 'protocol': 'udp'}), - ]: - try: - resp = _requests.get(url, params=params, timeout=6) - if resp.ok: - d = resp.json() - if d.get('open') or d.get('isOpen') or d.get('status') == 'open': - return True - except Exception: - continue return False def get_server_config(self) -> Dict[str, Any]: diff --git a/webui/src/pages/WireGuard.jsx b/webui/src/pages/WireGuard.jsx index d94f97a..5ba1faa 100644 --- a/webui/src/pages/WireGuard.jsx +++ b/webui/src/pages/WireGuard.jsx @@ -16,6 +16,7 @@ function WireGuard() { const [peerConfig, setPeerConfig] = useState(''); const [qrCodeDataUrl, setQrCodeDataUrl] = useState(''); const [peerStatuses, setPeerStatuses] = useState({}); + const [tunnelMode, setTunnelMode] = useState('split'); // 'split' or 'full' useEffect(() => { fetchWireGuardData(); @@ -119,28 +120,12 @@ function WireGuard() { await fetchWireGuardData(); }; - const handleViewPeerConfig = async (peer) => { + const handleViewPeerConfig = async (peer, mode = tunnelMode) => { setSelectedPeer(peer); try { - // Try to get existing config first - const response = await wireguardAPI.getPeerConfig({ name: peer.name }); - let config = response.data.config; - - // If no config exists, generate a complete one with real server config - if (!config || config === 'Configuration not available') { - // Get server configuration first - const serverConfig = await getServerConfig(); - - // Create peer with server config - const peerWithServerConfig = { - ...peer, - server_public_key: serverConfig.public_key, - server_endpoint: serverConfig.endpoint - }; - - config = generateWireGuardConfig(peerWithServerConfig); - } - + const sc = await getServerConfig(); + const peerWithServerConfig = { ...peer, server_public_key: sc.public_key, server_endpoint: sc.endpoint }; + const config = generateWireGuardConfig(peerWithServerConfig, mode); setPeerConfig(config); // Generate QR code for the config @@ -196,25 +181,26 @@ function WireGuard() { return { public_key: '', endpoint: ':51820' }; }; - const generateWireGuardConfig = (peer) => { - // Use real keys from the peer data + const CELL_DNS = '172.20.0.2'; + const SPLIT_TUNNEL_IPS = '10.0.0.0/24, 172.20.0.0/16'; + const FULL_TUNNEL_IPS = '0.0.0.0/0, ::/0'; + + const generateWireGuardConfig = (peer, mode = tunnelMode) => { const serverPublicKey = peer.server_public_key || "SERVER_PUBLIC_KEY_PLACEHOLDER"; const serverEndpoint = peer.server_endpoint || "YOUR_SERVER_IP:51820"; - const serverAllowedIPs = peer.allowed_ips || "0.0.0.0/0"; const privateKey = peer.private_key || 'YOUR_PRIVATE_KEY_HERE'; - - // Check if IP already has a subnet mask, if not add /32 - const peerAddress = peer.ip.includes('/') ? peer.ip : `${peer.ip}/32`; - + const peerAddress = peer.ip?.includes('/') ? peer.ip : `${peer.ip}/32`; + const allowedIPs = mode === 'full' ? FULL_TUNNEL_IPS : SPLIT_TUNNEL_IPS; + return `[Interface] PrivateKey = ${privateKey} Address = ${peerAddress} -DNS = 8.8.8.8, 1.1.1.1 +DNS = ${CELL_DNS} [Peer] PublicKey = ${serverPublicKey} Endpoint = ${serverEndpoint} -AllowedIPs = ${serverAllowedIPs} +AllowedIPs = ${allowedIPs} PersistentKeepalive = ${peer.persistent_keepalive || 25}`; }; @@ -620,13 +606,34 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`; {selectedPeer.name} Configuration - +
+
+ + +
+ +
+

+ {tunnelMode === 'split' + ? 'Split tunnel: only cell services (10.0.0.0/24, 172.20.0.0/16) route through VPN — local network & internet traffic stay direct.' + : 'Full tunnel: all traffic (internet + local) routes through VPN server.'} +