wip: peers
This commit is contained in:
+51
-14
@@ -154,16 +154,16 @@ def clear_log_context(exc):
|
|||||||
request_context.set({})
|
request_context.set({})
|
||||||
|
|
||||||
# Initialize managers with proper directories
|
# Initialize managers with proper directories
|
||||||
network_manager = NetworkManager(data_dir='./data', config_dir='./config')
|
network_manager = NetworkManager(data_dir='/app/data', config_dir='/app/config')
|
||||||
wireguard_manager = WireGuardManager(data_dir='./data', config_dir='./config')
|
wireguard_manager = WireGuardManager(data_dir='/app/data', config_dir='/app/config')
|
||||||
peer_registry = PeerRegistry(data_dir='./data', config_dir='./config')
|
peer_registry = PeerRegistry(data_dir='/app/data', config_dir='/app/config')
|
||||||
email_manager = EmailManager(data_dir='./data', config_dir='./config')
|
email_manager = EmailManager(data_dir='/app/data', config_dir='/app/config')
|
||||||
calendar_manager = CalendarManager(data_dir='./data', config_dir='./config')
|
calendar_manager = CalendarManager(data_dir='/app/data', config_dir='/app/config')
|
||||||
file_manager = FileManager(data_dir='./data', config_dir='./config')
|
file_manager = FileManager(data_dir='/app/data', config_dir='/app/config')
|
||||||
routing_manager = RoutingManager(data_dir='./data', config_dir='./config')
|
routing_manager = RoutingManager(data_dir='/app/data', config_dir='/app/config')
|
||||||
cell_manager = CellManager(data_dir='./data', config_dir='./config')
|
cell_manager = CellManager(data_dir='/app/data', config_dir='/app/config')
|
||||||
app.vault_manager = VaultManager(data_dir='./data', config_dir='./config')
|
app.vault_manager = VaultManager(data_dir='/app/data', config_dir='/app/config')
|
||||||
container_manager = ContainerManager(data_dir='./data', config_dir='./config')
|
container_manager = ContainerManager(data_dir='/app/data', config_dir='/app/config')
|
||||||
|
|
||||||
# Register services with service bus
|
# Register services with service bus
|
||||||
service_bus.register_service('network', network_manager)
|
service_bus.register_service('network', network_manager)
|
||||||
@@ -839,12 +839,43 @@ def get_peer_config():
|
|||||||
if data is None or 'name' not in data:
|
if data is None or 'name' not in data:
|
||||||
return jsonify({"error": "Missing peer name"}), 400
|
return jsonify({"error": "Missing peer name"}), 400
|
||||||
|
|
||||||
# For now, return not implemented - this would need to be implemented
|
peer_name = data['name']
|
||||||
return jsonify({"error": "Not implemented yet"}), 501
|
|
||||||
|
# Get peer from peer registry
|
||||||
|
peer = peer_registry.get_peer(peer_name)
|
||||||
|
if not peer:
|
||||||
|
return jsonify({"config": "Peer not found"})
|
||||||
|
|
||||||
|
# Get server configuration
|
||||||
|
server_config = wireguard_manager.get_server_config()
|
||||||
|
|
||||||
|
# Generate client configuration using peer registry data
|
||||||
|
config = f"""[Interface]
|
||||||
|
PrivateKey = {peer.get('private_key', 'YOUR_PRIVATE_KEY_HERE')}
|
||||||
|
Address = {peer.get('ip', '10.0.0.2')}/32
|
||||||
|
DNS = 8.8.8.8, 1.1.1.1
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = {server_config.get('public_key', 'SERVER_PUBLIC_KEY_PLACEHOLDER')}
|
||||||
|
Endpoint = {server_config.get('endpoint', 'YOUR_SERVER_IP:51820')}
|
||||||
|
AllowedIPs = {peer.get('allowed_ips', '0.0.0.0/0')}
|
||||||
|
PersistentKeepalive = {peer.get('persistent_keepalive', 25)}"""
|
||||||
|
|
||||||
|
return jsonify({"config": config})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting peer config: {e}")
|
logger.error(f"Error getting peer config: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
# Peer Registry API
|
# Peer Registry API
|
||||||
@app.route('/api/peers', methods=['GET'])
|
@app.route('/api/peers', methods=['GET'])
|
||||||
def get_peers():
|
def get_peers():
|
||||||
@@ -870,11 +901,17 @@ def add_peer():
|
|||||||
if field not in data:
|
if field not in data:
|
||||||
return jsonify({"error": f"Missing required field: {field}"}), 400
|
return jsonify({"error": f"Missing required field: {field}"}), 400
|
||||||
|
|
||||||
# Add peer to registry
|
# Add peer to registry with all provided fields
|
||||||
peer_info = {
|
peer_info = {
|
||||||
'peer': data['name'],
|
'peer': data['name'],
|
||||||
'ip': data['ip'],
|
'ip': data['ip'],
|
||||||
'public_key': data['public_key']
|
'public_key': data['public_key'],
|
||||||
|
'private_key': data.get('private_key'),
|
||||||
|
'server_public_key': data.get('server_public_key'),
|
||||||
|
'server_endpoint': data.get('server_endpoint'),
|
||||||
|
'allowed_ips': data.get('allowed_ips'),
|
||||||
|
'persistent_keepalive': data.get('persistent_keepalive'),
|
||||||
|
'description': data.get('description')
|
||||||
}
|
}
|
||||||
|
|
||||||
success = peer_registry.add_peer(peer_info)
|
success = peer_registry.add_peer(peer_info)
|
||||||
|
|||||||
+290
-1
@@ -371,4 +371,293 @@ AllowedIPs = {allowed_ips}
|
|||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to restart WireGuard service: {e}")
|
logger.error(f"Failed to restart WireGuard service: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_peer_config(self, peer_name: str) -> Optional[str]:
|
||||||
|
"""Get WireGuard client configuration for a specific peer"""
|
||||||
|
try:
|
||||||
|
# Get peer information
|
||||||
|
peers = self.get_wireguard_peers()
|
||||||
|
peer_info = None
|
||||||
|
|
||||||
|
for peer in peers:
|
||||||
|
if peer.get('name') == peer_name:
|
||||||
|
peer_info = peer
|
||||||
|
break
|
||||||
|
|
||||||
|
if not peer_info:
|
||||||
|
logger.warning(f"Peer {peer_name} not found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get server configuration
|
||||||
|
server_config = self._get_server_config()
|
||||||
|
|
||||||
|
# Generate client configuration
|
||||||
|
client_config = self._generate_client_config(peer_info, server_config)
|
||||||
|
|
||||||
|
return client_config
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting peer config for {peer_name}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_server_config(self) -> Dict[str, str]:
|
||||||
|
"""Get server configuration details"""
|
||||||
|
try:
|
||||||
|
# Try to read server config file
|
||||||
|
server_config_path = os.path.join(self.wg_config_dir, 'wg_confs', 'wg0.conf')
|
||||||
|
if os.path.exists(server_config_path):
|
||||||
|
with open(server_config_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Parse server configuration
|
||||||
|
lines = content.strip().split('\n')
|
||||||
|
server_public_key = None
|
||||||
|
server_endpoint = None
|
||||||
|
server_private_key = None
|
||||||
|
|
||||||
|
# Look for server private key and endpoint
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith('PrivateKey'):
|
||||||
|
server_private_key = line.split('=', 1)[1].strip()
|
||||||
|
elif line.startswith('ListenPort'):
|
||||||
|
port = line.split('=', 1)[1].strip()
|
||||||
|
# Get server IP from environment or detect it
|
||||||
|
server_ip = os.environ.get('WIREGUARD_SERVER_IP')
|
||||||
|
if not server_ip:
|
||||||
|
# Try to get the actual external IP
|
||||||
|
try:
|
||||||
|
import socket
|
||||||
|
import requests
|
||||||
|
# First try to get external IP from a service
|
||||||
|
try:
|
||||||
|
response = requests.get('https://api.ipify.org', timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
server_ip = response.text.strip()
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to get external IP")
|
||||||
|
except Exception:
|
||||||
|
# Fallback: try to get local IP that's not Docker internal
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
local_ip = s.getsockname()[0]
|
||||||
|
# If it's a Docker internal IP, use localhost for development
|
||||||
|
if local_ip.startswith('172.') or local_ip.startswith('192.168.'):
|
||||||
|
server_ip = "localhost"
|
||||||
|
else:
|
||||||
|
server_ip = local_ip
|
||||||
|
except Exception:
|
||||||
|
# Ultimate fallback to localhost for development
|
||||||
|
server_ip = "localhost"
|
||||||
|
server_endpoint = f"{server_ip}:{port}"
|
||||||
|
|
||||||
|
# Generate public key from private key if we have it
|
||||||
|
if server_private_key:
|
||||||
|
try:
|
||||||
|
# Use wg pubkey command to generate public key from private key
|
||||||
|
import subprocess
|
||||||
|
result = subprocess.run(['wg', 'pubkey'],
|
||||||
|
input=server_private_key,
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
server_public_key = result.stdout.strip()
|
||||||
|
else:
|
||||||
|
# Fallback: try to read from existing public key file
|
||||||
|
pubkey_path = os.path.join(self.wg_config_dir, 'publickey')
|
||||||
|
if os.path.exists(pubkey_path):
|
||||||
|
with open(pubkey_path, 'r') as f:
|
||||||
|
server_public_key = f.read().strip()
|
||||||
|
else:
|
||||||
|
server_public_key = "SERVER_PUBLIC_KEY_PLACEHOLDER"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not generate public key: {e}")
|
||||||
|
server_public_key = "SERVER_PUBLIC_KEY_PLACEHOLDER"
|
||||||
|
else:
|
||||||
|
server_public_key = "SERVER_PUBLIC_KEY_PLACEHOLDER"
|
||||||
|
|
||||||
|
# Set default endpoint if not found
|
||||||
|
if not server_endpoint:
|
||||||
|
# Try to get the actual server IP
|
||||||
|
server_ip = os.environ.get('WIREGUARD_SERVER_IP')
|
||||||
|
if not server_ip:
|
||||||
|
# Try to get the actual external IP
|
||||||
|
try:
|
||||||
|
import socket
|
||||||
|
import requests
|
||||||
|
# First try to get external IP from a service
|
||||||
|
try:
|
||||||
|
response = requests.get('https://api.ipify.org', timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
server_ip = response.text.strip()
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to get external IP")
|
||||||
|
except Exception:
|
||||||
|
# Fallback: try to get local IP that's not Docker internal
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
local_ip = s.getsockname()[0]
|
||||||
|
# If it's a Docker internal IP, use localhost for development
|
||||||
|
if local_ip.startswith('172.') or local_ip.startswith('192.168.'):
|
||||||
|
server_ip = "localhost"
|
||||||
|
else:
|
||||||
|
server_ip = local_ip
|
||||||
|
except Exception:
|
||||||
|
# Ultimate fallback to localhost for development
|
||||||
|
server_ip = "localhost"
|
||||||
|
server_endpoint = f"{server_ip}:51820"
|
||||||
|
|
||||||
|
return {
|
||||||
|
'public_key': server_public_key,
|
||||||
|
'endpoint': server_endpoint,
|
||||||
|
'allowed_ips': '0.0.0.0/0'
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading server config: {e}")
|
||||||
|
|
||||||
|
# Return default values
|
||||||
|
return {
|
||||||
|
'public_key': 'SERVER_PUBLIC_KEY_PLACEHOLDER',
|
||||||
|
'endpoint': 'YOUR_SERVER_IP:51820',
|
||||||
|
'allowed_ips': '0.0.0.0/0'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _generate_client_config(self, peer_info: Dict[str, Any], server_config: Dict[str, str]) -> str:
|
||||||
|
"""Generate WireGuard client configuration"""
|
||||||
|
try:
|
||||||
|
# Get peer private key from peer data
|
||||||
|
peer_private_key = peer_info.get('private_key', 'YOUR_PRIVATE_KEY_HERE')
|
||||||
|
|
||||||
|
config = f"""[Interface]
|
||||||
|
PrivateKey = {peer_private_key}
|
||||||
|
Address = {peer_info.get('ip', '10.0.0.2')}/32
|
||||||
|
DNS = 8.8.8.8, 1.1.1.1
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = {server_config['public_key']}
|
||||||
|
Endpoint = {server_config['endpoint']}
|
||||||
|
AllowedIPs = {server_config['allowed_ips']}
|
||||||
|
PersistentKeepalive = {peer_info.get('persistent_keepalive', 25)}"""
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating client config: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_server_config(self) -> Dict[str, str]:
|
||||||
|
"""Get server configuration details"""
|
||||||
|
try:
|
||||||
|
# Try to read server config file
|
||||||
|
server_config_path = os.path.join(self.wg_config_dir, 'wg_confs', 'wg0.conf')
|
||||||
|
logger.info(f"Looking for server config at: {server_config_path}")
|
||||||
|
logger.info(f"wg_config_dir is: {self.wg_config_dir}")
|
||||||
|
logger.info(f"File exists: {os.path.exists(server_config_path)}")
|
||||||
|
if os.path.exists(server_config_path):
|
||||||
|
with open(server_config_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Parse server configuration
|
||||||
|
lines = content.strip().split('\n')
|
||||||
|
server_public_key = None
|
||||||
|
server_endpoint = None
|
||||||
|
server_private_key = None
|
||||||
|
|
||||||
|
# Look for server private key and endpoint
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith('PrivateKey'):
|
||||||
|
server_private_key = line.split('=', 1)[1].strip()
|
||||||
|
logger.info(f"Found server private key: {server_private_key[:10]}...")
|
||||||
|
elif line.startswith('ListenPort'):
|
||||||
|
port = line.split('=', 1)[1].strip()
|
||||||
|
logger.info(f"Found listen port: {port}")
|
||||||
|
# Get server IP from environment or detect it
|
||||||
|
server_ip = os.environ.get('WIREGUARD_SERVER_IP')
|
||||||
|
if not server_ip:
|
||||||
|
# Try to get the actual external IP
|
||||||
|
try:
|
||||||
|
import socket
|
||||||
|
import requests
|
||||||
|
# First try to get external IP from a service
|
||||||
|
try:
|
||||||
|
response = requests.get('https://api.ipify.org', timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
server_ip = response.text.strip()
|
||||||
|
logger.info(f"Got external IP from service: {server_ip}")
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to get external IP")
|
||||||
|
except Exception:
|
||||||
|
# Fallback: try to get local IP that's not Docker internal
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
local_ip = s.getsockname()[0]
|
||||||
|
# If it's a Docker internal IP, use localhost for development
|
||||||
|
if local_ip.startswith('172.') or local_ip.startswith('192.168.'):
|
||||||
|
server_ip = "localhost"
|
||||||
|
logger.info(f"Using localhost for development (Docker internal IP: {local_ip})")
|
||||||
|
else:
|
||||||
|
server_ip = local_ip
|
||||||
|
logger.info(f"Using local IP: {server_ip}")
|
||||||
|
except Exception:
|
||||||
|
# Ultimate fallback to localhost for development
|
||||||
|
server_ip = "localhost"
|
||||||
|
logger.info("Using localhost as ultimate fallback")
|
||||||
|
server_endpoint = f"{server_ip}:{port}"
|
||||||
|
logger.info(f"Set server endpoint: {server_endpoint}")
|
||||||
|
|
||||||
|
# Generate public key from private key if we have it
|
||||||
|
if server_private_key:
|
||||||
|
try:
|
||||||
|
logger.info("Generating public key from private key...")
|
||||||
|
# Use wg pubkey command to generate public key from private key
|
||||||
|
import subprocess
|
||||||
|
result = subprocess.run(['wg', 'pubkey'],
|
||||||
|
input=server_private_key,
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
server_public_key = result.stdout.strip()
|
||||||
|
logger.info(f"Generated server public key: {server_public_key[:10]}...")
|
||||||
|
else:
|
||||||
|
# Fallback: try to read from existing public key file
|
||||||
|
pubkey_path = os.path.join(self.wg_config_dir, 'publickey')
|
||||||
|
if os.path.exists(pubkey_path):
|
||||||
|
with open(pubkey_path, 'r') as f:
|
||||||
|
server_public_key = f.read().strip()
|
||||||
|
else:
|
||||||
|
server_public_key = "SERVER_PUBLIC_KEY_PLACEHOLDER"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not generate public key: {e}")
|
||||||
|
server_public_key = "SERVER_PUBLIC_KEY_PLACEHOLDER"
|
||||||
|
else:
|
||||||
|
server_public_key = "SERVER_PUBLIC_KEY_PLACEHOLDER"
|
||||||
|
|
||||||
|
# Set default endpoint if not found
|
||||||
|
if not server_endpoint:
|
||||||
|
# Try to get the actual server IP
|
||||||
|
server_ip = os.environ.get('WIREGUARD_SERVER_IP')
|
||||||
|
if not server_ip:
|
||||||
|
# Try to get the host IP from Docker network
|
||||||
|
try:
|
||||||
|
import socket
|
||||||
|
# Connect to a remote address to determine local IP
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
server_ip = s.getsockname()[0]
|
||||||
|
except Exception:
|
||||||
|
# Fallback to localhost
|
||||||
|
server_ip = "localhost"
|
||||||
|
server_endpoint = f"{server_ip}:51820"
|
||||||
|
|
||||||
|
return {
|
||||||
|
'public_key': server_public_key,
|
||||||
|
'endpoint': server_endpoint
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading server config: {e}")
|
||||||
|
|
||||||
|
# Return default values
|
||||||
|
return {
|
||||||
|
'public_key': 'SERVER_PUBLIC_KEY_PLACEHOLDER',
|
||||||
|
'endpoint': 'YOUR_SERVER_IP:51820'
|
||||||
|
}
|
||||||
@@ -133,6 +133,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./data/api:/app/data
|
- ./data/api:/app/data
|
||||||
- ./config/api:/app/config
|
- ./config/api:/app/config
|
||||||
|
- ./config/wireguard:/app/config/wireguard
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
@@ -170,6 +171,7 @@ services:
|
|||||||
- "8082:8080"
|
- "8082:8080"
|
||||||
environment:
|
environment:
|
||||||
- FG_PUBLIC_PATH=/files-ui
|
- FG_PUBLIC_PATH=/files-ui
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cell-network:
|
cell-network:
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Personal Internet Cell</title>
|
<title>Personal Internet Cell</title>
|
||||||
|
<script src="https://unpkg.com/qrcode@1.5.3/build/qrcode.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+911
-43
File diff suppressed because it is too large
Load Diff
+378
-42
@@ -1,11 +1,15 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Shield, Key, Users } from 'lucide-react';
|
import { Shield, Key, Users, Activity, Wifi, Download, Copy, RefreshCw, Play, Pause, AlertCircle } from 'lucide-react';
|
||||||
import { wireguardAPI } from '../services/api';
|
import { wireguardAPI } from '../services/api';
|
||||||
|
|
||||||
function WireGuard() {
|
function WireGuard() {
|
||||||
const [status, setStatus] = useState(null);
|
const [status, setStatus] = useState(null);
|
||||||
const [peers, setPeers] = useState([]);
|
const [peers, setPeers] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const [selectedPeer, setSelectedPeer] = useState(null);
|
||||||
|
const [showPeerConfig, setShowPeerConfig] = useState(false);
|
||||||
|
const [peerConfig, setPeerConfig] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchWireGuardData();
|
fetchWireGuardData();
|
||||||
@@ -19,14 +23,73 @@ function WireGuard() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
setStatus(statusResponse.data);
|
setStatus(statusResponse.data);
|
||||||
setPeers(peersResponse.data);
|
setPeers(peersResponse.data || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch WireGuard data:', error);
|
console.error('Failed to fetch WireGuard data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
setIsRefreshing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
await fetchWireGuardData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewPeerConfig = async (peer) => {
|
||||||
|
setSelectedPeer(peer);
|
||||||
|
try {
|
||||||
|
const response = await wireguardAPI.getPeerConfig({ name: peer.name });
|
||||||
|
setPeerConfig(response.data.config || 'Configuration not available');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get peer config:', error);
|
||||||
|
setPeerConfig('Failed to load configuration');
|
||||||
|
}
|
||||||
|
setShowPeerConfig(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = async (text) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
alert('Configuration copied to clipboard!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy to clipboard:', error);
|
||||||
|
alert('Failed to copy to clipboard. Please copy manually.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadConfig = (peerName, config) => {
|
||||||
|
const blob = new Blob([config], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${peerName}.conf`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatBytes = (bytes) => {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
@@ -38,55 +101,328 @@ function WireGuard() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">WireGuard</h1>
|
<div className="flex justify-between items-center">
|
||||||
<p className="mt-2 text-gray-600">
|
<div>
|
||||||
Manage WireGuard VPN configuration and peers
|
<h1 className="text-2xl font-bold text-gray-900">WireGuard VPN</h1>
|
||||||
</p>
|
<p className="mt-2 text-gray-600">
|
||||||
|
Manage WireGuard VPN configuration and monitor active peers
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={refreshData}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
className="btn btn-secondary flex items-center"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
{/* Status Overview */}
|
||||||
{/* Status */}
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center">
|
||||||
<Shield className="h-6 w-6 text-primary-500 mr-2" />
|
<div className="p-2 bg-primary-100 rounded-lg">
|
||||||
<h3 className="text-lg font-medium text-gray-900">Status</h3>
|
<Shield className="h-6 w-6 text-primary-600" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-500">Service Status</p>
|
||||||
|
<p className={`text-lg font-semibold ${status?.running ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{status?.running ? 'Online' : 'Offline'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{status ? (
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between">
|
<div className="card">
|
||||||
<span className="text-sm text-gray-500">Interface:</span>
|
<div className="flex items-center">
|
||||||
<span className="text-sm font-medium">{status.interface || 'wg0'}</span>
|
<div className="p-2 bg-blue-100 rounded-lg">
|
||||||
|
<Users className="h-6 w-6 text-blue-600" />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-2 bg-green-100 rounded-lg">
|
||||||
|
<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-lg font-semibold text-gray-900">
|
||||||
|
{peers.filter(peer => getPeerStatus(peer).online).length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-2 bg-purple-100 rounded-lg">
|
||||||
|
<Wifi className="h-6 w-6 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-500">Interface</p>
|
||||||
|
<p className="text-lg font-semibold text-gray-900">{status?.interface || 'wg0'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Traffic Stats */}
|
||||||
|
{status?.total_traffic && (
|
||||||
|
<div className="card mb-8">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<Activity className="h-6 w-6 text-primary-500 mr-2" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Traffic Statistics</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 mb-1">Data Received</p>
|
||||||
|
<p className="text-2xl font-bold text-green-600">
|
||||||
|
{formatBytes(status.total_traffic.bytes_received || 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 mb-1">Data Sent</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-600">
|
||||||
|
{formatBytes(status.total_traffic.bytes_sent || 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Peer
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
IP Address
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Last Handshake
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Traffic
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{peers.map((peer, index) => {
|
||||||
|
const peerStatus = getPeerStatus(peer);
|
||||||
|
return (
|
||||||
|
<tr key={index} className="hover:bg-gray-50">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0 h-8 w-8">
|
||||||
|
<div className="h-8 w-8 rounded-full bg-primary-100 flex items-center justify-center">
|
||||||
|
<Shield className="h-4 w-4 text-primary-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<div className="text-sm font-medium text-gray-900">{peer.name}</div>
|
||||||
|
{peer.description && (
|
||||||
|
<div className="text-sm text-gray-500">{peer.description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{peer.ip || peer.AllowedIPs || 'N/A'}
|
||||||
|
</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
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-red-100 text-red-800'
|
||||||
|
}`}>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full mr-1.5 ${
|
||||||
|
peerStatus.online ? 'bg-green-400' : 'bg-red-400'
|
||||||
|
}`} />
|
||||||
|
{peerStatus.online ? 'Online' : 'Offline'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{peerStatus.online
|
||||||
|
? new Date(peerStatus.lastHandshake).toLocaleString()
|
||||||
|
: 'Never'
|
||||||
|
}
|
||||||
|
</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)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-xs">
|
||||||
|
<span className="text-blue-600 mr-1">↑</span>
|
||||||
|
{formatBytes(peerStatus.transferTx)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleViewPeerConfig(peer)}
|
||||||
|
className="text-primary-600 hover:text-primary-900"
|
||||||
|
title="View Configuration"
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => downloadConfig(peer.name, peerConfig)}
|
||||||
|
className="text-primary-600 hover:text-primary-900"
|
||||||
|
title="Download Config"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Peer Configuration Modal */}
|
||||||
|
{showPeerConfig && selectedPeer && (
|
||||||
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||||
|
<div className="relative top-10 mx-auto p-5 border w-full max-w-4xl shadow-lg rounded-md bg-white">
|
||||||
|
<div className="mt-3">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Shield className="h-6 w-6 text-primary-500 mr-2" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
|
{selectedPeer.name} Configuration
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPeerConfig(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm text-gray-500">Status:</span>
|
<div className="space-y-4">
|
||||||
<span className="text-sm font-medium text-success-600">Active</span>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Peer Name
|
||||||
|
</label>
|
||||||
|
<p className="text-sm text-gray-900 bg-gray-50 p-2 rounded">{selectedPeer.name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
IP Address
|
||||||
|
</label>
|
||||||
|
<p className="text-sm text-gray-900 bg-gray-50 p-2 rounded">{selectedPeer.ip || selectedPeer.AllowedIPs}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Public Key
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-gray-900 bg-gray-50 p-2 rounded break-all">{selectedPeer.public_key || selectedPeer.PublicKey}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Allowed IPs
|
||||||
|
</label>
|
||||||
|
<p className="text-sm text-gray-900 bg-gray-50 p-2 rounded">{selectedPeer.allowed_ips || selectedPeer.AllowedIPs}</p>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPeerConfig(false)}
|
||||||
|
className="btn btn-secondary"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<p className="text-gray-500 text-sm">Status unavailable</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Peers */}
|
|
||||||
<div className="card">
|
|
||||||
<div className="flex items-center mb-4">
|
|
||||||
<Users className="h-6 w-6 text-primary-500 mr-2" />
|
|
||||||
<h3 className="text-lg font-medium text-gray-900">Peers</h3>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{peers.length > 0 ? (
|
|
||||||
peers.map((peer, index) => (
|
|
||||||
<div key={index} className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
||||||
<span className="text-sm font-medium">{peer.name}</span>
|
|
||||||
<span className="text-sm text-gray-500">{peer.ip}</span>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-500 text-sm">No peers configured</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user