diff --git a/api/app.py b/api/app.py index d64b1db..d007634 100644 --- a/api/app.py +++ b/api/app.py @@ -876,11 +876,28 @@ def get_network_status(): def get_peer_config(): try: data = request.get_json(silent=True) or {} + peer_name = data.get('name', data.get('peer', '')) + + # Look up peer details from registry if not supplied + peer_ip = data.get('ip', '') + peer_private_key = data.get('private_key', '') + if peer_name and (not peer_ip or not peer_private_key): + registered = peer_registry.get_peer(peer_name) + if registered: + peer_ip = peer_ip or registered.get('ip', '') + peer_private_key = peer_private_key or registered.get('private_key', '') + + # Use real external endpoint if not supplied + server_endpoint = data.get('server_endpoint', '') + if not server_endpoint: + srv = wireguard_manager.get_server_config() + server_endpoint = srv.get('endpoint') or '' + result = wireguard_manager.get_peer_config( - peer_name=data.get('name', data.get('peer', '')), - peer_ip=data.get('ip', ''), - peer_private_key=data.get('private_key', ''), - server_endpoint=data.get('server_endpoint', '') + peer_name=peer_name, + peer_ip=peer_ip, + peer_private_key=peer_private_key, + server_endpoint=server_endpoint, ) return jsonify({"config": result}) except Exception as e: @@ -890,13 +907,27 @@ def get_peer_config(): @app.route('/api/wireguard/server-config', methods=['GET']) def get_server_config(): try: - # Get server configuration from WireGuard manager config = wireguard_manager.get_server_config() return jsonify(config) except Exception as e: logger.error(f"Error getting server config: {e}") return jsonify({"error": str(e)}), 500 +@app.route('/api/wireguard/refresh-ip', methods=['POST']) +def refresh_external_ip(): + try: + ip = wireguard_manager.get_external_ip(force_refresh=True) + port_open = wireguard_manager.check_port_open() + return jsonify({ + 'external_ip': ip, + 'port': wireguard_manager.DEFAULT_PORT if hasattr(wireguard_manager, 'DEFAULT_PORT') else 51820, + 'port_open': port_open, + 'endpoint': f'{ip}:{51820}' if ip else None, + }) + except Exception as e: + logger.error(f"Error refreshing external IP: {e}") + return jsonify({"error": str(e)}), 500 + # Peer Registry API @app.route('/api/peers', methods=['GET']) def get_peers(): diff --git a/api/wireguard_manager.py b/api/wireguard_manager.py index e3e6f34..00be677 100644 --- a/api/wireguard_manager.py +++ b/api/wireguard_manager.py @@ -6,13 +6,20 @@ WireGuard Manager for Personal Internet Cell import os import json import base64 +import socket import subprocess import logging +import time from datetime import datetime from typing import Dict, List, Optional, Any from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from base_service_manager import BaseServiceManager +try: + import requests as _requests +except ImportError: + _requests = None + logger = logging.getLogger(__name__) SERVER_ADDRESS = '172.20.0.1/16' @@ -215,16 +222,95 @@ class WireGuardManager(BaseServiceManager): return ( f'[Interface]\n' f'PrivateKey = {peer_private_key}\n' - f'Address = {peer_ip}/32\n' + f'Address = {peer_ip if "/" in peer_ip else f"{peer_ip}/32"}\n' f'DNS = {PEER_DNS}\n' f'\n' f'[Peer]\n' f'PublicKey = {server_keys["public_key"]}\n' f'AllowedIPs = {SERVER_NETWORK}\n' - f'Endpoint = {server_endpoint}:{DEFAULT_PORT}\n' + f'Endpoint = {server_endpoint if ":" in server_endpoint else f"{server_endpoint}:{DEFAULT_PORT}"}\n' f'PersistentKeepalive = 25\n' ) + # ── External IP & port ──────────────────────────────────────────────────── + + def _ip_cache_file(self) -> str: + return os.path.join(self.keys_dir, 'external_ip.json') + + def get_external_ip(self, force_refresh: bool = False) -> Optional[str]: + """Detect external IP, caching result for 1 hour.""" + cache_file = self._ip_cache_file() + if not force_refresh and os.path.exists(cache_file): + try: + with open(cache_file) as f: + data = json.load(f) + if time.time() - data.get('ts', 0) < 3600: + return data.get('ip') + except Exception: + pass + + ip = None + services = [ + 'https://api.ipify.org', + 'https://ifconfig.me/ip', + 'https://icanhazip.com', + ] + if _requests: + for url in services: + try: + resp = _requests.get(url, timeout=5) + candidate = resp.text.strip() + if candidate and len(candidate) < 45: + ip = candidate + break + except Exception: + continue + + if ip: + try: + with open(cache_file, 'w') as f: + json.dump({'ip': ip, 'ts': time.time()}, f) + except (PermissionError, OSError): + pass + 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 or _requests is None: + return False + # Use an external UDP port-check service + try: + resp = _requests.get( + f'https://portchecker.co/api/v1/query', + params={'host': external_ip, 'port': port}, + timeout=8, + ) + if resp.ok: + data = resp.json() + return bool(data.get('isOpen') or data.get('open')) + except Exception: + pass + # Fallback: try TCP (won't work for UDP WireGuard, but gives a network clue) + try: + sock = socket.create_connection((external_ip, port), timeout=3) + sock.close() + return True + except Exception: + return False + + def get_server_config(self) -> Dict[str, Any]: + """Return server public key, external IP, endpoint, and port status.""" + keys = self.get_keys() + external_ip = self.get_external_ip() + endpoint = f'{external_ip}:{DEFAULT_PORT}' if external_ip else None + return { + 'public_key': keys['public_key'], + 'external_ip': external_ip, + 'endpoint': endpoint, + 'port': DEFAULT_PORT, + } + # ── Status & connectivity ───────────────────────────────────────────────── def get_status(self) -> Dict[str, Any]: diff --git a/webui/nginx.conf b/webui/nginx.conf index 76b87a6..8db4e38 100644 --- a/webui/nginx.conf +++ b/webui/nginx.conf @@ -4,6 +4,21 @@ server { root /usr/share/nginx/html; index index.html; + # Proxy API and health calls to the backend container + location /api/ { + proxy_pass http://cell-api:3000/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 60s; + } + + location /health { + proxy_pass http://cell-api:3000/health; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + # Handle client-side routing location / { try_files $uri $uri/ /index.html; diff --git a/webui/src/pages/WireGuard.jsx b/webui/src/pages/WireGuard.jsx index 952c709..8298935 100644 --- a/webui/src/pages/WireGuard.jsx +++ b/webui/src/pages/WireGuard.jsx @@ -1,10 +1,12 @@ import { useState, useEffect } from 'react'; -import { Shield, Key, Users, Activity, Wifi, Download, Copy, RefreshCw, Play, Pause, AlertCircle, Eye } from 'lucide-react'; +import { Shield, Key, Users, Activity, Wifi, Download, Copy, RefreshCw, Play, Pause, AlertCircle, Eye, Globe, CheckCircle, XCircle } from 'lucide-react'; import { wireguardAPI, peerAPI } from '../services/api'; import QRCode from 'qrcode'; function WireGuard() { const [status, setStatus] = useState(null); + const [serverConfig, setServerConfig] = useState(null); + const [isRefreshingIp, setIsRefreshingIp] = useState(false); const [peers, setPeers] = useState([]); const [totalPeers, setTotalPeers] = useState(0); const [isLoading, setIsLoading] = useState(true); @@ -19,15 +21,30 @@ function WireGuard() { fetchWireGuardData(); }, []); + const refreshExternalIp = async () => { + setIsRefreshingIp(true); + try { + const response = await fetch('/api/wireguard/refresh-ip', { method: 'POST' }); + const data = await response.json(); + setServerConfig(prev => ({ ...prev, ...data })); + } catch (e) { + console.error('Failed to refresh IP:', e); + } finally { + setIsRefreshingIp(false); + } + }; + const fetchWireGuardData = async () => { try { - const [statusResponse, peersResponse, wireguardResponse] = await Promise.all([ + const [statusResponse, peersResponse, wireguardResponse, serverConfigResponse] = await Promise.all([ wireguardAPI.getStatus(), peerAPI.getPeers(), - wireguardAPI.getPeers() + wireguardAPI.getPeers(), + fetch('/api/wireguard/server-config').then(r => r.json()).catch(() => null), ]); - + setStatus(statusResponse.data); + if (serverConfigResponse) setServerConfig(serverConfigResponse); // Merge peer registry data with WireGuard data (same as Peers page) const peersData = peersResponse.data || []; @@ -160,25 +177,18 @@ function WireGuard() { }; const getServerConfig = async () => { + if (serverConfig?.public_key) return serverConfig; try { - // Try to get server configuration from API const response = await fetch('/api/wireguard/server-config'); if (response.ok) { const config = await response.json(); - return { - public_key: config.public_key || "SERVER_PUBLIC_KEY_PLACEHOLDER", - endpoint: config.endpoint || "YOUR_SERVER_IP:51820" - }; + setServerConfig(config); + return config; } } catch (error) { console.warn('Could not get server config:', error); } - - // Return default values - return { - public_key: "SERVER_PUBLIC_KEY_PLACEHOLDER", - endpoint: "YOUR_SERVER_IP:51820" - }; + return { public_key: '', endpoint: ':51820' }; }; const generateWireGuardConfig = (peer) => { @@ -354,6 +364,50 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`; + {/* External IP & Port Status */} +
+
+
+ +

Server Endpoint

+
+ +
+
+
+

External IP

+

+ {serverConfig?.external_ip || Detecting…} +

+
+
+

WireGuard Endpoint

+

+ {serverConfig?.endpoint || `:${serverConfig?.port || 51820}`} +

+
+
+

Server Public Key

+

+ {serverConfig?.public_key || '—'} +

+
+
+ {serverConfig && !serverConfig.external_ip && ( +
+ + External IP could not be detected. Check internet connectivity, then click Refresh IP. +
+ )} +
+ {/* Traffic Stats */} {status?.total_traffic && (
diff --git a/webui/src/services/api.js b/webui/src/services/api.js index c2f7496..74b2118 100644 --- a/webui/src/services/api.js +++ b/webui/src/services/api.js @@ -2,7 +2,7 @@ import axios from 'axios'; // Create axios instance with base configuration const api = axios.create({ - baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000', + baseURL: import.meta.env.VITE_API_URL || '', timeout: 10000, headers: { 'Content-Type': 'application/json',