From 853f57bc4085f8998391afe4ab61b23dddc864f4 Mon Sep 17 00:00:00 2001 From: infosave2007 Date: Fri, 30 Jan 2026 22:14:09 +0300 Subject: [PATCH] feat: Enforce single IP per peer for AWG/WireGuard connections --- bin/collect_metrics.php | 5 ++ inc/ServerMonitoring.php | 123 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/bin/collect_metrics.php b/bin/collect_metrics.php index 34909d8..36d827c 100644 --- a/bin/collect_metrics.php +++ b/bin/collect_metrics.php @@ -61,6 +61,11 @@ while (true) { $monitoring->enforceXraySingleIpPerUser(); } + // Enforce single IP per peer for AWG servers + if (strpos($containerName, 'awg') !== false || strpos($containerName, 'wireguard') !== false) { + $monitoring->enforceAwgSingleIpPerPeer(); + } + // Collect server metrics $serverMetrics = $monitoring->collectMetrics(); echo " Server: CPU={$serverMetrics['cpu_percent']}% RAM={$serverMetrics['ram_used_mb']}/{$serverMetrics['ram_total_mb']}MB "; diff --git a/inc/ServerMonitoring.php b/inc/ServerMonitoring.php index 52425c1..640482a 100644 --- a/inc/ServerMonitoring.php +++ b/inc/ServerMonitoring.php @@ -652,6 +652,129 @@ class ServerMonitoring } } + /** + * Enforce single IP per peer for AWG/WireGuard connections. + * If a peer's endpoint changes while session is active, block the new IP. + */ + public function enforceAwgSingleIpPerPeer(): void + { + $containerName = $this->serverData['container_name'] ?? ''; + if (strpos($containerName, 'awg') === false && strpos($containerName, 'wireguard') === false) { + return; // Not an AWG server + } + + // Get current peer states + $cmd = "docker exec $containerName wg show wg0 dump"; + $result = $this->execSSH($cmd); + if (!$result) { + return; + } + + $lines = explode("\n", trim($result)); + if (count($lines) < 2) { + return; // No peers + } + + // Load locked endpoints from file + $lockFile = '/tmp/awg_locked_endpoints_' . $this->serverData['id'] . '.json'; + $lockedEndpoints = []; + $lockFileCmd = "cat $lockFile 2>/dev/null || echo '{}'"; + $lockData = $this->execSSH($lockFileCmd); + if ($lockData) { + $lockedEndpoints = json_decode($lockData, true) ?: []; + } + + $currentPeers = []; + $ipsToBlock = []; + $now = time(); + + // Skip first line (interface info) + for ($i = 1; $i < count($lines); $i++) { + $parts = preg_split('/\s+/', trim($lines[$i])); + if (count($parts) < 8) { + continue; + } + + // Format: interface pubkey psk endpoint allowed-ips latest-handshake rx tx keepalive + $pubkey = $parts[0]; + $endpoint = $parts[2]; // IP:Port or (none) + $latestHandshake = (int)$parts[4]; + + if ($endpoint === '(none)' || $latestHandshake === 0) { + // Peer not connected - clear lock + unset($lockedEndpoints[$pubkey]); + continue; + } + + // Extract just IP from endpoint (IP:Port) + $endpointIp = explode(':', $endpoint)[0]; + $isActive = ($now - $latestHandshake) < 180; // Active if handshake within 3 minutes + + $currentPeers[$pubkey] = $endpointIp; + + if ($isActive) { + if (!isset($lockedEndpoints[$pubkey])) { + // First connection - lock this IP + $lockedEndpoints[$pubkey] = $endpointIp; + } elseif ($lockedEndpoints[$pubkey] !== $endpointIp) { + // Endpoint changed during active session - block new IP + $ipsToBlock[] = $endpointIp; + error_log("[AWG Enforcement] Peer $pubkey changed endpoint from {$lockedEndpoints[$pubkey]} to $endpointIp - blocking"); + } + } else { + // Session expired - update locked endpoint for next connection + $lockedEndpoints[$pubkey] = $endpointIp; + } + } + + // Clean up locks for peers that no longer exist + foreach ($lockedEndpoints as $pubkey => $ip) { + if (!isset($currentPeers[$pubkey])) { + unset($lockedEndpoints[$pubkey]); + } + } + + // Save locked endpoints + $lockJson = json_encode($lockedEndpoints); + $saveLockCmd = "echo " . escapeshellarg($lockJson) . " > $lockFile"; + $this->execSSH($saveLockCmd); + + // Apply iptables rules for blocked IPs + if (!empty($ipsToBlock)) { + foreach ($ipsToBlock as $ip) { + // Block UDP traffic from this IP to WireGuard port + $wgPort = $this->serverData['vpn_port'] ?? 51820; + $blockCmd = "docker exec $containerName iptables -C INPUT -s $ip -p udp --dport $wgPort -j DROP 2>/dev/null || docker exec $containerName iptables -I INPUT -s $ip -p udp --dport $wgPort -j DROP"; + $this->execSSH($blockCmd); + } + } + + // Remove blocks for IPs that are now the locked endpoint (old device disconnected) + $wgPort = $this->serverData['vpn_port'] ?? 51820; + $listRulesCmd = "docker exec $containerName iptables -L INPUT -n --line-numbers | grep 'DROP.*udp dpt:$wgPort' | awk '{print \$1, \$4}'"; + $rulesResult = $this->execSSH($listRulesCmd); + if ($rulesResult) { + $rulesToRemove = []; + foreach (explode("\n", trim($rulesResult)) as $line) { + $parts = preg_split('/\s+/', trim($line)); + if (count($parts) >= 2) { + $ruleNum = $parts[0]; + $blockedIp = $parts[1]; + // If this IP is now the locked endpoint for any peer, remove the block + if (in_array($blockedIp, $lockedEndpoints)) { + $rulesToRemove[] = $ruleNum; + } + } + } + // Remove rules in reverse order (highest number first) + rsort($rulesToRemove); + foreach ($rulesToRemove as $ruleNum) { + $rmCmd = "docker exec $containerName iptables -D INPUT $ruleNum 2>/dev/null || true"; + $this->execSSH($rmCmd); + } + } + } + /** * Count total online clients across all Xray servers * Returns array with 'total' count and 'users' list