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:
2026-04-20 02:41:50 -04:00
parent 5239751a71
commit bd67764bf4
5 changed files with 209 additions and 23 deletions
+15
View File
@@ -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;
+69 -15
View File
@@ -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">
+1 -1
View File
@@ -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',