wip: wireguard
This commit is contained in:
+323
-80
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user