wip: wireguard

This commit is contained in:
Cloud
2025-09-14 03:31:14 -05:00
parent 5bd7443681
commit bb6ccfe023
8 changed files with 1468 additions and 91 deletions
+323 -80
View File
@@ -1,15 +1,19 @@
import { useState, useEffect } from 'react';
import { Shield, Key, Users, Activity, Wifi, Download, Copy, RefreshCw, Play, Pause, AlertCircle } from 'lucide-react';
import { wireguardAPI } from '../services/api';
import { Shield, Key, Users, Activity, Wifi, Download, Copy, RefreshCw, Play, Pause, AlertCircle, Eye } from 'lucide-react';
import { wireguardAPI, peerAPI } from '../services/api';
import QRCode from 'qrcode';
function WireGuard() {
const [status, setStatus] = useState(null);
const [peers, setPeers] = useState([]);
const [totalPeers, setTotalPeers] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [selectedPeer, setSelectedPeer] = useState(null);
const [showPeerConfig, setShowPeerConfig] = useState(false);
const [peerConfig, setPeerConfig] = useState('');
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
const [peerStatuses, setPeerStatuses] = useState({});
useEffect(() => {
fetchWireGuardData();
@@ -17,13 +21,69 @@ function WireGuard() {
const fetchWireGuardData = async () => {
try {
const [statusResponse, peersResponse] = await Promise.all([
const [statusResponse, peersResponse, wireguardResponse] = await Promise.all([
wireguardAPI.getStatus(),
peerAPI.getPeers(),
wireguardAPI.getPeers()
]);
setStatus(statusResponse.data);
setPeers(peersResponse.data || []);
// Merge peer registry data with WireGuard data (same as Peers page)
const peersData = peersResponse.data || [];
const wireguardPeers = wireguardResponse.data || [];
// Create a map of WireGuard peers by name for quick lookup
const wireguardMap = {};
wireguardPeers.forEach(peer => {
wireguardMap[peer.name] = peer;
});
// Merge the data
const mergedPeers = peersData.map(peer => ({
...peer,
...wireguardMap[peer.peer || peer.name],
name: peer.peer || peer.name,
status: 'Online', // For now, assume all peers are online
type: 'WireGuard',
// Preserve important fields that might be overwritten
private_key: peer.private_key,
server_public_key: peer.server_public_key,
server_endpoint: peer.server_endpoint,
allowed_ips: peer.allowed_ips || wireguardMap[peer.peer || peer.name]?.AllowedIPs || '0.0.0.0/0',
persistent_keepalive: peer.persistent_keepalive || wireguardMap[peer.peer || peer.name]?.PersistentKeepalive || 25
}));
// Load peer statuses first
const statusPromises = mergedPeers.map(async (peer) => {
if (peer.public_key) {
const status = await getPeerStatus(peer);
return { peerId: peer.name, status };
}
return { peerId: peer.name, status: { online: null, lastHandshake: null, transferRx: 0, transferTx: 0 } };
});
const statusResults = await Promise.all(statusPromises);
const statusMap = {};
statusResults.forEach(({ peerId, status }) => {
statusMap[peerId] = status;
});
setPeerStatuses(statusMap);
// Set total peers count
setTotalPeers(mergedPeers.length);
// Filter to only show live connected peers
const livePeers = mergedPeers.filter(peer => {
const peerStatus = statusMap[peer.name];
return peerStatus && (
peerStatus.online === true ||
(peerStatus.lastHandshake && peerStatus.lastHandshake !== null) ||
(peerStatus.transferRx > 0 || peerStatus.transferTx > 0)
);
});
setPeers(livePeers);
} catch (error) {
console.error('Failed to fetch WireGuard data:', error);
} finally {
@@ -40,11 +100,39 @@ function WireGuard() {
const handleViewPeerConfig = async (peer) => {
setSelectedPeer(peer);
try {
// Try to get existing config first
const response = await wireguardAPI.getPeerConfig({ name: peer.name });
setPeerConfig(response.data.config || 'Configuration not available');
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);
}
setPeerConfig(config);
// Generate QR code for the config
try {
const qrDataUrl = await generateQRCode(config);
setQrCodeDataUrl(qrDataUrl);
} catch (qrError) {
console.error('Failed to generate QR code:', qrError);
setQrCodeDataUrl('');
}
} catch (error) {
console.error('Failed to get peer config:', error);
setPeerConfig('Failed to load configuration');
setQrCodeDataUrl('');
}
setShowPeerConfig(true);
};
@@ -71,6 +159,68 @@ function WireGuard() {
URL.revokeObjectURL(url);
};
const getServerConfig = async () => {
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"
};
}
} 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"
};
};
const generateWireGuardConfig = (peer) => {
// Use real keys from the peer data
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`;
return `[Interface]
PrivateKey = ${privateKey}
Address = ${peerAddress}
DNS = 8.8.8.8, 1.1.1.1
[Peer]
PublicKey = ${serverPublicKey}
Endpoint = ${serverEndpoint}
AllowedIPs = ${serverAllowedIPs}
PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
};
const generateQRCode = async (text) => {
try {
const qrDataUrl = await QRCode.toDataURL(text, {
width: 256,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
},
errorCorrectionLevel: 'M'
});
return qrDataUrl;
} catch (error) {
console.error('QR Code generation error:', error);
throw error;
}
};
const formatBytes = (bytes) => {
if (bytes === 0) return '0 B';
const k = 1024;
@@ -79,15 +229,45 @@ function WireGuard() {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const getPeerStatus = (peer) => {
// This would need to be implemented based on actual peer status
// For now, we'll show a mock status
return {
online: Math.random() > 0.3, // Random for demo
lastHandshake: new Date(Date.now() - Math.random() * 3600000).toISOString(),
transferRx: Math.floor(Math.random() * 1000000),
transferTx: Math.floor(Math.random() * 1000000)
};
const getPeerStatus = async (peer) => {
try {
// Get real peer status from the API
const response = await fetch('http://localhost:3000/api/wireguard/peers/status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
public_key: peer.public_key
})
});
if (response.ok) {
const status = await response.json();
return {
online: status.online,
lastHandshake: status.latest_handshake,
transferRx: status.transfer_rx || 0,
transferTx: status.transfer_tx || 0
};
} else {
console.error('Failed to get peer status:', response.status);
return {
online: null,
lastHandshake: null,
transferRx: 0,
transferTx: 0
};
}
} catch (error) {
console.error('Error getting peer status:', error);
return {
online: null,
lastHandshake: null,
transferRx: 0,
transferTx: 0
};
}
};
if (isLoading) {
@@ -142,7 +322,7 @@ function WireGuard() {
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Total Peers</p>
<p className="text-lg font-semibold text-gray-900">{peers.length}</p>
<p className="text-lg font-semibold text-gray-900">{totalPeers}</p>
</div>
</div>
</div>
@@ -153,9 +333,9 @@ function WireGuard() {
<Activity className="h-6 w-6 text-green-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Active Peers</p>
<p className="text-sm font-medium text-gray-500">Live Connections</p>
<p className="text-lg font-semibold text-gray-900">
{peers.filter(peer => getPeerStatus(peer).online).length}
{peers.length}
</p>
</div>
</div>
@@ -199,28 +379,31 @@ function WireGuard() {
)}
{/* Peers Table */}
<div className="card">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center">
<Users className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Connected Peers</h3>
<div className="card">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center">
<Users className="h-6 w-6 text-primary-500 mr-2" />
<h3 className="text-lg font-medium text-gray-900">Live Connected Peers</h3>
</div>
<div className="text-sm text-gray-500">
{peers.length} peer{peers.length !== 1 ? 's' : ''} currently connected
</div>
</div>
<div className="text-sm text-gray-500">
{peers.length} peer{peers.length !== 1 ? 's' : ''} configured
</div>
</div>
{peers.length === 0 ? (
<div className="text-center py-12">
<Shield className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No peers configured</h3>
<p className="text-gray-500 mb-4">Add your first peer to start using WireGuard VPN</p>
<button
onClick={() => window.location.href = '/peers'}
className="btn btn-primary"
>
Add Peer
</button>
<Wifi className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No active connections</h3>
<p className="text-gray-500 mb-4">No peers are currently connected to the WireGuard VPN</p>
<div className="space-y-2">
<p className="text-sm text-gray-400">Configured peers will appear here when they connect</p>
<button
onClick={() => window.location.href = '/peers'}
className="btn btn-secondary"
>
Manage Peers
</button>
</div>
</div>
) : (
<div className="overflow-x-auto">
@@ -249,7 +432,7 @@ function WireGuard() {
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{peers.map((peer, index) => {
const peerStatus = getPeerStatus(peer);
const peerStatus = peerStatuses[peer.name] || { online: null, lastHandshake: null, transferRx: 0, transferTx: 0 };
return (
<tr key={index} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
@@ -272,31 +455,35 @@ function WireGuard() {
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
peerStatus.online
peerStatus.online === true
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
: peerStatus.online === false
? 'bg-red-100 text-red-800'
: 'bg-gray-100 text-gray-800'
}`}>
<div className={`w-1.5 h-1.5 rounded-full mr-1.5 ${
peerStatus.online ? 'bg-green-400' : 'bg-red-400'
peerStatus.online === true ? 'bg-green-400' :
peerStatus.online === false ? 'bg-red-400' : 'bg-gray-400'
}`} />
{peerStatus.online ? 'Online' : 'Offline'}
{peerStatus.online === true ? 'Online' :
peerStatus.online === false ? 'Offline' : 'Unknown'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{peerStatus.online
{peerStatus.lastHandshake
? new Date(peerStatus.lastHandshake).toLocaleString()
: 'Never'
: 'Unknown'
}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div className="space-y-1">
<div className="flex items-center text-xs">
<span className="text-green-600 mr-1"></span>
{formatBytes(peerStatus.transferRx)}
{peerStatus.transferRx > 0 ? formatBytes(peerStatus.transferRx) : 'No data'}
</div>
<div className="flex items-center text-xs">
<span className="text-blue-600 mr-1"></span>
{formatBytes(peerStatus.transferTx)}
{peerStatus.transferTx > 0 ? formatBytes(peerStatus.transferTx) : 'No data'}
</div>
</div>
</td>
@@ -307,14 +494,16 @@ function WireGuard() {
className="text-primary-600 hover:text-primary-900"
title="View Configuration"
>
<Copy className="h-4 w-4" />
<Eye className="h-4 w-4" />
</button>
<button
onClick={() => downloadConfig(peer.name, peerConfig)}
className="text-primary-600 hover:text-primary-900"
title="Download Config"
onClick={() => handleViewPeerConfig(peer)}
className="text-green-600 hover:text-green-900"
title="Show QR Code"
>
<Download className="h-4 w-4" />
<div className="h-4 w-4 border-2 border-current rounded-sm flex items-center justify-center">
<div className="w-2 h-2 bg-current rounded-sm"></div>
</div>
</button>
</div>
</td>
@@ -383,39 +572,93 @@ function WireGuard() {
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
WireGuard Configuration
</label>
<div className="relative">
<textarea
value={peerConfig}
readOnly
className="w-full h-64 p-3 border border-gray-300 rounded-md font-mono text-sm bg-gray-50"
placeholder="Loading configuration..."
/>
<div className="absolute top-2 right-2 flex space-x-2">
<button
onClick={() => copyToClipboard(peerConfig)}
className="btn btn-secondary btn-sm flex items-center"
title="Copy to clipboard"
>
<Copy className="h-4 w-4 mr-1" />
Copy
</button>
<button
onClick={() => downloadConfig(selectedPeer.name, peerConfig)}
className="btn btn-secondary btn-sm flex items-center"
title="Download config file"
>
<Download className="h-4 w-4 mr-1" />
Download
</button>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
WireGuard Configuration
</label>
<div className="relative">
<textarea
value={peerConfig}
readOnly
className="w-full h-64 p-3 border border-gray-300 rounded-md font-mono text-sm bg-gray-50"
placeholder="Loading configuration..."
/>
<div className="absolute top-2 right-2 flex space-x-2">
<button
onClick={() => copyToClipboard(peerConfig)}
className="btn btn-secondary btn-sm flex items-center"
title="Copy to clipboard"
>
<Copy className="h-4 w-4 mr-1" />
Copy
</button>
<button
onClick={() => downloadConfig(selectedPeer.name, peerConfig)}
className="btn btn-secondary btn-sm flex items-center"
title="Download config file"
>
<Download className="h-4 w-4 mr-1" />
Download
</button>
</div>
</div>
<p className="text-xs text-gray-500 mt-2">
Use this configuration on your mobile device or computer to connect to this WireGuard network.
</p>
</div>
{/* QR Code Section */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2 flex items-center">
<div className="h-5 w-5 mr-2 border-2 border-gray-400 rounded-sm flex items-center justify-center">
<div className="w-2 h-2 bg-gray-400 rounded-sm"></div>
</div>
QR Code
</label>
<div className="text-center">
{qrCodeDataUrl ? (
<div className="space-y-4">
<div className="inline-block p-4 bg-white border-2 border-gray-200 rounded-lg">
<img
src={qrCodeDataUrl}
alt="WireGuard QR Code"
className="w-48 h-48 mx-auto"
/>
</div>
<div className="space-y-2">
<p className="text-sm text-gray-600">
Scan this QR code with your WireGuard app to connect automatically
</p>
<div className="flex justify-center space-x-2">
<button
onClick={() => {
const link = document.createElement('a');
link.href = qrCodeDataUrl;
link.download = `${selectedPeer.name}-qrcode.png`;
link.click();
}}
className="btn btn-sm btn-secondary flex items-center"
title="Download QR Code"
>
<Download className="h-4 w-4 mr-1" />
Download QR
</button>
</div>
</div>
</div>
) : (
<div className="p-8 bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg">
<div className="h-12 w-12 border-2 border-gray-400 rounded-sm flex items-center justify-center mx-auto mb-2">
<div className="w-6 h-6 bg-gray-400 rounded-sm"></div>
</div>
<p className="text-sm text-gray-500">
QR Code will appear here
</p>
</div>
)}
</div>
</div>
<p className="text-xs text-gray-500 mt-2">
Use this configuration on your mobile device or computer to connect to this WireGuard network.
</p>
</div>
</div>