feat: external IP detection, port status, fix peer config generation
- WireGuardManager: get_external_ip() (cached 1h), check_port_open(), get_server_config() returning public_key + detected endpoint - API: /api/wireguard/server-config returns real external IP; /api/wireguard/refresh-ip forces re-detection; /api/wireguard/peers/config now looks up peer IP + private key from registry and uses real server endpoint automatically - Fix doubled port in Endpoint (178.x:51820:51820 → 178.x:51820) - Fix Address=/32 when peer_ip already has mask - WebUI nginx: proxy /api/ and /health to cell-api (fixes localhost:3000 hardcode — UI now works from any machine) - api.js: baseURL='' so all calls go through nginx proxy - WireGuard page: show Server Endpoint card with external IP, endpoint, public key, and Refresh IP button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+36
-5
@@ -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 '<SERVER_IP>'
|
||||
|
||||
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', '<SERVER_IP>')
|
||||
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():
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: '<SERVER_IP>:51820' };
|
||||
};
|
||||
|
||||
const generateWireGuardConfig = (peer) => {
|
||||
@@ -354,6 +364,50 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* External IP & Port Status */}
|
||||
<div className="card mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<Globe className="h-5 w-5 text-gray-500 mr-2" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Server Endpoint</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={refreshExternalIp}
|
||||
disabled={isRefreshingIp}
|
||||
className="btn btn-secondary flex items-center text-sm"
|
||||
>
|
||||
<RefreshCw className={`h-3 w-3 mr-1 ${isRefreshingIp ? 'animate-spin' : ''}`} />
|
||||
Refresh IP
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">External IP</p>
|
||||
<p className="font-mono font-semibold text-gray-900">
|
||||
{serverConfig?.external_ip || <span className="text-yellow-600">Detecting…</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">WireGuard Endpoint</p>
|
||||
<p className="font-mono font-semibold text-gray-900">
|
||||
{serverConfig?.endpoint || `<SERVER_IP>:${serverConfig?.port || 51820}`}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 mb-1">Server Public Key</p>
|
||||
<p className="font-mono text-xs text-gray-700 break-all">
|
||||
{serverConfig?.public_key || '—'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{serverConfig && !serverConfig.external_ip && (
|
||||
<div className="mt-3 flex items-center text-yellow-700 bg-yellow-50 rounded p-2 text-sm">
|
||||
<AlertCircle className="h-4 w-4 mr-2 flex-shrink-0" />
|
||||
External IP could not be detected. Check internet connectivity, then click Refresh IP.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Traffic Stats */}
|
||||
{status?.total_traffic && (
|
||||
<div className="card mb-8">
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user