From 8e415689649c45be096c00753caade6a77492b78 Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Mon, 20 Apr 2026 15:40:19 -0400 Subject: [PATCH] feat: peer access config, DNS fix, real routing table, reinstall notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Peer creation/edit form now configures: - Tunnel mode: full (0.0.0.0/0) or split (PIC only) - Per-service access toggles (calendar, files, mail, webdav) - Peer-to-peer communication toggle - Optional calendar account creation - Access capability badges in peer list Bug fixes: - DNS in client configs was 8.8.8.8 / 172.20.0.2 — now 172.20.0.3 (CoreDNS) This was why .cell domains didn't resolve on connected VPN peers - get_peer_config API uses stored internet_access to set AllowedIPs - New PUT /api/peers/ endpoint with config_changed detection - POST /api/peers//clear-reinstall clears reinstall flag after download - Routing page reads real host routes via /proc/1/net/route (pid: host) Co-Authored-By: Claude Sonnet 4.6 --- api/app.py | 59 +- api/peer_registry.py | 21 + webui/src/pages/Peers.jsx | 1747 ++++++++++++++------------------- webui/src/pages/WireGuard.jsx | 2 +- 4 files changed, 800 insertions(+), 1029 deletions(-) diff --git a/api/app.py b/api/app.py index 5a76616..7c189b1 100644 --- a/api/app.py +++ b/api/app.py @@ -890,8 +890,8 @@ def get_peer_config(): # Look up peer details from registry if not supplied peer_ip = data.get('ip', '') peer_private_key = data.get('private_key', '') + registered = peer_registry.get_peer(peer_name) if peer_name else {} 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', '') @@ -902,7 +902,12 @@ def get_peer_config(): srv = wireguard_manager.get_server_config() server_endpoint = srv.get('endpoint') or '' + # Determine AllowedIPs: explicit > peer's stored internet_access > default full tunnel allowed_ips = data.get('allowed_ips') or None + if not allowed_ips and registered: + internet_access = registered.get('internet_access', True) + allowed_ips = wireguard_manager.FULL_TUNNEL_IPS if internet_access else wireguard_manager.SPLIT_TUNNEL_IPS + result = wireguard_manager.get_peer_config( peer_name=peer_name, peer_ip=peer_ip, @@ -980,19 +985,65 @@ def add_peer(): 'server_endpoint': data.get('server_endpoint'), 'allowed_ips': data.get('allowed_ips'), 'persistent_keepalive': data.get('persistent_keepalive'), - 'description': data.get('description') + 'description': data.get('description'), + 'internet_access': data.get('internet_access', True), + 'service_access': data.get('service_access', ['calendar', 'files', 'mail', 'webdav']), + 'peer_access': data.get('peer_access', True), + 'config_needs_reinstall': False, } - + success = peer_registry.add_peer(peer_info) if success: return jsonify({"message": f"Peer {data['name']} added successfully"}), 201 else: return jsonify({"error": f"Peer {data['name']} already exists"}), 400 - + except Exception as e: logger.error(f"Error adding peer: {e}") return jsonify({"error": str(e)}), 500 + +@app.route('/api/peers/', methods=['PUT']) +def update_peer(peer_name): + """Update peer settings. Marks config_needs_reinstall if VPN config changed.""" + try: + data = request.get_json(silent=True) or {} + existing = peer_registry.get_peer(peer_name) + if not existing: + return jsonify({"error": "Peer not found"}), 404 + + # Detect changes that require client to reinstall tunnel config + config_changed = ( + ('internet_access' in data and data['internet_access'] != existing.get('internet_access', True)) or + ('ip' in data and data['ip'] != existing.get('ip')) or + ('persistent_keepalive' in data and data['persistent_keepalive'] != existing.get('persistent_keepalive')) + ) + + updates = {k: v for k, v in data.items()} + if config_changed: + updates['config_needs_reinstall'] = True + + success = peer_registry.update_peer(peer_name, updates) + if success: + result = {"message": f"Peer {peer_name} updated", "config_changed": config_changed} + return jsonify(result) + else: + return jsonify({"error": "Update failed"}), 500 + except Exception as e: + logger.error(f"Error updating peer {peer_name}: {e}") + return jsonify({"error": str(e)}), 500 + + +@app.route('/api/peers//clear-reinstall', methods=['POST']) +def clear_peer_reinstall(peer_name): + """Clear the config_needs_reinstall flag once user has downloaded new config.""" + try: + peer_registry.clear_reinstall_flag(peer_name) + return jsonify({"message": "Reinstall flag cleared"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route('/api/peers/', methods=['DELETE']) def remove_peer(peer_name): """Remove a peer.""" diff --git a/api/peer_registry.py b/api/peer_registry.py index 4af2340..941c484 100644 --- a/api/peer_registry.py +++ b/api/peer_registry.py @@ -266,6 +266,27 @@ class PeerRegistry(BaseServiceManager): self.logger.error(f"Error removing peer {name}: {e}") return False + def update_peer(self, name: str, fields: Dict[str, Any]) -> bool: + """Update arbitrary fields on a peer.""" + try: + with self.lock: + for peer in self.peers: + if peer.get('peer') == name: + peer.update(fields) + peer['updated_at'] = datetime.utcnow().isoformat() + self._save_peers() + self.logger.info(f"Updated peer {name}: {list(fields.keys())}") + return True + self.logger.warning(f"Peer {name} not found for update") + return False + except Exception as e: + self.logger.error(f"Error updating peer {name}: {e}") + return False + + def clear_reinstall_flag(self, name: str) -> bool: + """Clear the config_needs_reinstall flag after user downloads new config.""" + return self.update_peer(name, {'config_needs_reinstall': False}) + def update_peer_ip(self, name: str, new_ip: str) -> bool: """Update peer IP address""" try: diff --git a/webui/src/pages/Peers.jsx b/webui/src/pages/Peers.jsx index 59265f1..b7a42a5 100644 --- a/webui/src/pages/Peers.jsx +++ b/webui/src/pages/Peers.jsx @@ -1,1024 +1,723 @@ -import { useState, useEffect } from 'react'; -import { Plus, Trash2, Edit, Eye, Wifi, Shield, Copy, Download, Key, Smartphone } from 'lucide-react'; -import { peerAPI, wireguardAPI } from '../services/api'; -import QRCode from 'qrcode'; - -function Peers() { - const [peers, setPeers] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [showAddModal, setShowAddModal] = useState(false); - const [newPeer, setNewPeer] = useState({ - name: '', - ip: '', - public_key: '', - allowed_ips: '0.0.0.0/0', - description: '', - endpoint: '', - persistent_keepalive: 25 - }); - const [showViewModal, setShowViewModal] = useState(false); - const [showEditModal, setShowEditModal] = useState(false); - const [selectedPeer, setSelectedPeer] = useState(null); - const [peerConfig, setPeerConfig] = useState(''); - const [isGeneratingKeys, setIsGeneratingKeys] = useState(false); - const [generatedKeys, setGeneratedKeys] = useState(null); - const [showAdvanced, setShowAdvanced] = useState(false); - const [showSuccessMessage, setShowSuccessMessage] = useState(false); - const [qrCodeDataUrl, setQrCodeDataUrl] = useState(''); - - useEffect(() => { - fetchPeers(); - }, []); - - const fetchPeers = async () => { - try { - const [peersResponse, wireguardResponse] = await Promise.all([ - peerAPI.getPeers(), - wireguardAPI.getPeers() - ]); - - // Merge peer registry data with WireGuard data - 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 - })); - - setPeers(mergedPeers); - } catch (error) { - console.error('Failed to fetch peers:', error); - } finally { - setIsLoading(false); - } - }; - - const generateKeys = async () => { - if (!newPeer.name) { - alert('Please enter a peer name first'); - return; - } - - setIsGeneratingKeys(true); - try { - const response = await wireguardAPI.generatePeerKeys({ peer_name: newPeer.name }); - setGeneratedKeys(response.data); - setNewPeer(prev => ({ - ...prev, - public_key: response.data.public_key - })); - } catch (error) { - console.error('Failed to generate keys:', error); - alert('Failed to generate keys. Please try again.'); - } finally { - setIsGeneratingKeys(false); - } - }; - - const handleAddPeer = async (e) => { - e.preventDefault(); - try { - // Generate keys automatically if not provided - let publicKey = newPeer.public_key; - let privateKey = null; - - if (!publicKey) { - const keyResponse = await wireguardAPI.generatePeerKeys({ peer_name: newPeer.name }); - publicKey = keyResponse.data.public_key; - privateKey = keyResponse.data.private_key; - } - - // Get server configuration - const serverConfig = await getServerConfig(); - - // First add to peer registry with all the data - const peerData = { - name: newPeer.name, - ip: newPeer.ip, - public_key: publicKey, - private_key: privateKey, - server_public_key: serverConfig.public_key, - server_endpoint: serverConfig.endpoint, - allowed_ips: newPeer.allowed_ips, - persistent_keepalive: newPeer.persistent_keepalive, - description: newPeer.description - }; - await peerAPI.addPeer(peerData); - - // Then add to WireGuard - const wireguardData = { - name: newPeer.name, - public_key: publicKey, - allowed_ips: newPeer.allowed_ips, - endpoint: newPeer.endpoint, - persistent_keepalive: newPeer.persistent_keepalive - }; - await wireguardAPI.addPeer(wireguardData); - - setShowAddModal(false); - setNewPeer({ - name: '', - ip: '', - public_key: '', - allowed_ips: '0.0.0.0/0', - description: '', - endpoint: '', - persistent_keepalive: 25 - }); - setGeneratedKeys(null); - setShowSuccessMessage(true); - fetchPeers(); - - // Hide success message after 3 seconds - setTimeout(() => setShowSuccessMessage(false), 3000); - } catch (error) { - console.error('Failed to add peer:', error); - alert('Failed to add peer. Please try again.'); - } - }; - - const getServerConfig = async () => { - try { - // Try to get server configuration from API - console.log('Fetching server config from:', '/api/wireguard/server-config'); - const response = await fetch('/api/wireguard/server-config'); - console.log('Server config response status:', response.status); - console.log('Server config response ok:', response.ok); - - if (response.ok) { - const config = await response.json(); - console.log('Server config from API:', config); - return { - public_key: config.public_key || "SERVER_PUBLIC_KEY_PLACEHOLDER", - endpoint: config.endpoint || "YOUR_SERVER_IP:51820" - }; - } else { - console.error('Failed to get server config, status:', response.status); - const errorText = await response.text(); - console.error('Error response:', errorText); - } - } catch (error) { - console.warn('Could not get server config:', error); - } - - // Return default values - console.log('Using fallback server config'); - return { - public_key: "SERVER_PUBLIC_KEY_PLACEHOLDER", - endpoint: "YOUR_SERVER_IP:51820" - }; - }; - - const handleRemovePeer = async (peerName) => { - if (window.confirm(`Are you sure you want to remove peer "${peerName}"?`)) { - try { - await Promise.all([ - peerAPI.removePeer(peerName), - wireguardAPI.removePeer({ name: peerName }) - ]); - fetchPeers(); - } catch (error) { - console.error('Failed to remove peer:', error); - alert('Failed to remove peer. Please try again.'); - } - } - }; - - const handleViewPeer = async (peer) => { - setSelectedPeer(peer); - try { - // Get server configuration first - console.log('Getting server config for peer:', peer.name); - const serverConfig = await getServerConfig(); - console.log('Server config received:', serverConfig); - - // Create peer with server config - const peerWithServerConfig = { - ...peer, - server_public_key: serverConfig.public_key, - server_endpoint: serverConfig.endpoint - }; - console.log('Peer with server config:', peerWithServerConfig); - - // Try to get existing config first - const response = await wireguardAPI.getPeerConfig({ name: peer.name }); - let config = response.data.config; - - // If no config exists, generate a complete one with real server config - if (!config || config === 'Configuration not available') { - config = generateWireGuardConfig(peerWithServerConfig); - } - - setPeerConfig(config); - - // Generate QR code for the config using QR-specific format - try { - console.log('Generating QR config for peer:', peer.name); - console.log('Using config from API for QR code:', config); - - // Use the same config string that works for the text area - // It already has the correct server data from the API - 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); - // Generate a basic config as fallback with server config - try { - const serverConfig = await getServerConfig(); - const peerWithServerConfig = { - ...peer, - server_public_key: serverConfig.public_key, - server_endpoint: serverConfig.endpoint - }; - const config = generateWireGuardConfig(peerWithServerConfig); - setPeerConfig(config); - - // Generate QR code for the fallback config using QR-specific format - try { - const qrConfig = generateQRConfig(peerWithServerConfig); - console.log('QR Config for peer (fallback):', peer.name); - console.log('QR Config content (fallback):', qrConfig); - const qrDataUrl = await generateQRCode(qrConfig); - setQrCodeDataUrl(qrDataUrl); - } catch (qrError) { - console.error('Failed to generate QR code:', qrError); - setQrCodeDataUrl(''); - } - } catch (serverError) { - console.error('Failed to get server config:', serverError); - // Ultimate fallback with placeholders - const config = generateWireGuardConfig(peer); - setPeerConfig(config); - setQrCodeDataUrl(''); - } - } - setShowViewModal(true); - }; - - 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 generateQRConfig = (peer) => { - // Generate a config for QR code that mobile apps can scan - const serverPublicKey = peer.server_public_key || "SERVER_PUBLIC_KEY_PLACEHOLDER"; - const serverEndpoint = peer.server_endpoint || "YOUR_SERVER_IP:51820"; - 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`; - - // Create a config that's compatible with qrencode and mobile apps - // Use proper spacing and format that WireGuard apps expect - return `[Interface] -PrivateKey = ${privateKey} -Address = ${peerAddress} -DNS = 8.8.8.8, 1.1.1.1 - -[Peer] -PublicKey = ${serverPublicKey} -Endpoint = ${serverEndpoint} -AllowedIPs = 0.0.0.0/0 -PersistentKeepalive = 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 handleEditPeer = (peer) => { - setSelectedPeer(peer); - setNewPeer({ - name: peer.name, - ip: peer.ip || '', - public_key: peer.public_key || '', - allowed_ips: peer.allowed_ips || '0.0.0.0/0', - description: peer.description || '', - endpoint: peer.endpoint || '', - persistent_keepalive: peer.persistent_keepalive || 25 - }); - setShowEditModal(true); - }; - - const handleUpdatePeer = async (e) => { - e.preventDefault(); - try { - // Update peer registry - const peerData = { - peer: newPeer.name, - ip: newPeer.ip, - public_key: newPeer.public_key, - description: newPeer.description - }; - await peerAPI.addPeer(peerData); // This will update if exists - - // Update WireGuard - const wireguardData = { - name: newPeer.name, - public_key: newPeer.public_key, - allowed_ips: newPeer.allowed_ips, - endpoint: newPeer.endpoint, - persistent_keepalive: newPeer.persistent_keepalive - }; - await wireguardAPI.addPeer(wireguardData); - - setShowEditModal(false); - setSelectedPeer(null); - fetchPeers(); - } catch (error) { - console.error('Failed to update peer:', error); - alert('Failed to update peer. Please try again.'); - } - }; - - 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); - }; - - if (isLoading) { - return ( -
-
-
- ); - } - - return ( -
- {/* Success Message */} - {showSuccessMessage && ( -
-
-
- -
-
-

- Peer Added Successfully! -

-
-

WireGuard keys were generated automatically. Click the eye icon to view and copy the configuration for your device.

-
-
-
-
- )} - -
-
-
-

Peers

-

- Manage peer connections and WireGuard configurations -

-
- -
-
- - {/* Peers List */} -
-
- - - - - - - - - - - - {peers.length === 0 ? ( - - - - ) : ( - peers.map((peer) => ( - - - - - - - - )) - )} - -
- Name - - IP Address - - Status - - Type - - Actions -
- No peers configured. Add your first peer to get started. -
-
-
{peer.name}
- {peer.description && ( -
{peer.description}
- )} -
-
- {peer.ip} - - - Online - - -
- - WireGuard -
-
-
- - - - -
-
-
-
- - {/* Add Peer Modal */} - {showAddModal && ( -
{ - // Close modal when clicking on backdrop - if (e.target === e.currentTarget) { - setShowAddModal(false); - setGeneratedKeys(null); - setShowAdvanced(false); - } - }} - > -
-
-
- -

Add New Peer

-
-
-
-
- - setNewPeer({ ...newPeer, name: e.target.value })} - className="input" - placeholder="e.g., mobile-phone, laptop" - required - /> -
- -
- - setNewPeer({ ...newPeer, ip: e.target.value })} - className="input" - placeholder="10.0.0.2" - required - /> -
- -
- - setNewPeer({ ...newPeer, description: e.target.value })} - className="input" - placeholder="Optional description" - /> -
- - {/* Advanced Options Toggle */} -
- -
- - {/* Advanced Options */} - {showAdvanced && ( -
-
- -