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 <noreply@anthropic.com>
This commit is contained in:
+21
-21
@@ -237,11 +237,18 @@ class WireGuardManager(BaseServiceManager):
|
|||||||
self._write_config('\n'.join(new_lines))
|
self._write_config('\n'.join(new_lines))
|
||||||
return True
|
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,
|
def get_peer_config(self, peer_name: str, peer_ip: str,
|
||||||
peer_private_key: str,
|
peer_private_key: str,
|
||||||
server_endpoint: str = '<SERVER_IP>',
|
server_endpoint: str = '<SERVER_IP>',
|
||||||
allowed_ips: str = '0.0.0.0/0, ::/0') -> str:
|
allowed_ips: str = None) -> str:
|
||||||
"""Generate a WireGuard client config string (full-tunnel by default)."""
|
"""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()
|
server_keys = self.get_keys()
|
||||||
peer_dns = _resolve_peer_dns()
|
peer_dns = _resolve_peer_dns()
|
||||||
endpoint = server_endpoint if ':' in server_endpoint else f'{server_endpoint}:{DEFAULT_PORT}'
|
endpoint = server_endpoint if ':' in server_endpoint else f'{server_endpoint}:{DEFAULT_PORT}'
|
||||||
@@ -302,11 +309,18 @@ class WireGuardManager(BaseServiceManager):
|
|||||||
return ip
|
return ip
|
||||||
|
|
||||||
def check_port_open(self, port: int = DEFAULT_PORT) -> bool:
|
def check_port_open(self, port: int = DEFAULT_PORT) -> bool:
|
||||||
"""Check if the WireGuard UDP port is reachable from outside."""
|
"""Check if WireGuard is running and listening on the UDP port."""
|
||||||
external_ip = self.get_external_ip()
|
# Primary: check if wg0 interface is up (means port IS listening)
|
||||||
if not external_ip:
|
try:
|
||||||
return False
|
result = subprocess.run(
|
||||||
# Check via WireGuard itself: if any peer has a recent handshake the port is open
|
['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:
|
try:
|
||||||
statuses = self.get_all_peer_statuses()
|
statuses = self.get_all_peer_statuses()
|
||||||
for st in statuses.values():
|
for st in statuses.values():
|
||||||
@@ -314,20 +328,6 @@ class WireGuardManager(BaseServiceManager):
|
|||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
return False
|
||||||
|
|
||||||
def get_server_config(self) -> Dict[str, Any]:
|
def get_server_config(self) -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function WireGuard() {
|
|||||||
const [peerConfig, setPeerConfig] = useState('');
|
const [peerConfig, setPeerConfig] = useState('');
|
||||||
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
|
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
|
||||||
const [peerStatuses, setPeerStatuses] = useState({});
|
const [peerStatuses, setPeerStatuses] = useState({});
|
||||||
|
const [tunnelMode, setTunnelMode] = useState('split'); // 'split' or 'full'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchWireGuardData();
|
fetchWireGuardData();
|
||||||
@@ -119,28 +120,12 @@ function WireGuard() {
|
|||||||
await fetchWireGuardData();
|
await fetchWireGuardData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewPeerConfig = async (peer) => {
|
const handleViewPeerConfig = async (peer, mode = tunnelMode) => {
|
||||||
setSelectedPeer(peer);
|
setSelectedPeer(peer);
|
||||||
try {
|
try {
|
||||||
// Try to get existing config first
|
const sc = await getServerConfig();
|
||||||
const response = await wireguardAPI.getPeerConfig({ name: peer.name });
|
const peerWithServerConfig = { ...peer, server_public_key: sc.public_key, server_endpoint: sc.endpoint };
|
||||||
let config = response.data.config;
|
const config = generateWireGuardConfig(peerWithServerConfig, mode);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPeerConfig(config);
|
setPeerConfig(config);
|
||||||
|
|
||||||
// Generate QR code for the config
|
// Generate QR code for the config
|
||||||
@@ -196,25 +181,26 @@ function WireGuard() {
|
|||||||
return { public_key: '', endpoint: '<SERVER_IP>:51820' };
|
return { public_key: '', endpoint: '<SERVER_IP>:51820' };
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateWireGuardConfig = (peer) => {
|
const CELL_DNS = '172.20.0.2';
|
||||||
// Use real keys from the peer data
|
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 serverPublicKey = peer.server_public_key || "SERVER_PUBLIC_KEY_PLACEHOLDER";
|
||||||
const serverEndpoint = peer.server_endpoint || "YOUR_SERVER_IP:51820";
|
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';
|
const privateKey = peer.private_key || 'YOUR_PRIVATE_KEY_HERE';
|
||||||
|
const peerAddress = peer.ip?.includes('/') ? peer.ip : `${peer.ip}/32`;
|
||||||
// Check if IP already has a subnet mask, if not add /32
|
const allowedIPs = mode === 'full' ? FULL_TUNNEL_IPS : SPLIT_TUNNEL_IPS;
|
||||||
const peerAddress = peer.ip.includes('/') ? peer.ip : `${peer.ip}/32`;
|
|
||||||
|
|
||||||
return `[Interface]
|
return `[Interface]
|
||||||
PrivateKey = ${privateKey}
|
PrivateKey = ${privateKey}
|
||||||
Address = ${peerAddress}
|
Address = ${peerAddress}
|
||||||
DNS = 8.8.8.8, 1.1.1.1
|
DNS = ${CELL_DNS}
|
||||||
|
|
||||||
[Peer]
|
[Peer]
|
||||||
PublicKey = ${serverPublicKey}
|
PublicKey = ${serverPublicKey}
|
||||||
Endpoint = ${serverEndpoint}
|
Endpoint = ${serverEndpoint}
|
||||||
AllowedIPs = ${serverAllowedIPs}
|
AllowedIPs = ${allowedIPs}
|
||||||
PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
|
PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -620,6 +606,21 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
|
|||||||
{selectedPeer.name} Configuration
|
{selectedPeer.name} Configuration
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="flex items-center bg-gray-100 rounded-lg p-1 text-xs">
|
||||||
|
<button
|
||||||
|
onClick={() => { setTunnelMode('split'); handleViewPeerConfig(selectedPeer, 'split'); }}
|
||||||
|
className={`px-2 py-1 rounded ${tunnelMode === 'split' ? 'bg-white shadow text-primary-700 font-medium' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
Split tunnel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setTunnelMode('full'); handleViewPeerConfig(selectedPeer, 'full'); }}
|
||||||
|
className={`px-2 py-1 rounded ${tunnelMode === 'full' ? 'bg-white shadow text-primary-700 font-medium' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
Full tunnel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowPeerConfig(false)}
|
onClick={() => setShowPeerConfig(false)}
|
||||||
className="text-gray-400 hover:text-gray-600"
|
className="text-gray-400 hover:text-gray-600"
|
||||||
@@ -627,6 +628,12 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
|
|||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mb-3">
|
||||||
|
{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.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user