/dev/null') ?: ''); return $pub; } public static function addClientPeer(string $clientPublicKey, string $clientAddressCidr): bool { if (!self::isEnabled()) { return true; } $pub = trim($clientPublicKey); if ($pub === '' || str_starts_with($pub, 'dev-placeholder-pub-')) { return false; } $iface = self::interfaceName(); $allowed = trim($clientAddressCidr); if ($allowed === '') { $allowed = WireGuardAddressPool::allocateClientAddress('default'); } $code = self::runWg( 'set ' . escapeshellarg($iface) . ' peer ' . escapeshellarg($pub) . ' allowed-ips ' . escapeshellarg($allowed) ); return $code === 0; } public static function removeClientPeer(?string $clientPublicKey): void { if (!self::isEnabled()) { return; } $pub = trim((string) $clientPublicKey); if ($pub === '' || str_starts_with($pub, 'dev-placeholder-pub-')) { return; } $iface = self::interfaceName(); self::runWg( 'set ' . escapeshellarg($iface) . ' peer ' . escapeshellarg($pub) . ' remove' ); } /** @return list */ public static function listPeerDumpRows(): array { if (!self::isEnabled()) { return []; } $iface = self::interfaceName(); $prefix = rtrim((string) cfg('remote_access.wg_set_prefix', ''), " \t"); $cmd = ($prefix !== '' ? $prefix . ' ' : '') . 'wg show ' . escapeshellarg($iface) . ' dump 2>/dev/null'; $raw = @shell_exec($cmd); if (!is_string($raw) || trim($raw) === '') { return []; } $rows = []; foreach (preg_split('/\r\n|\n|\r/', trim($raw)) as $line) { if ($line === '' || str_starts_with($line, 'interface:')) { continue; } $cols = explode("\t", $line); if (count($cols) < 4) { continue; } $pub = trim($cols[0]); if ($pub === '') { continue; } $rows[] = [ 'public_key' => $pub, 'allowed_ips' => trim((string) ($cols[3] ?? '')), ]; } return $rows; } /** * Remove live wg peers not tied to pending/active sessions (or with empty AllowedIPs). * * @param list $keepPublicKeys * @return list */ public static function pruneOrphanPeers(array $keepPublicKeys, bool $dryRun = false): array { if (!self::isEnabled()) { return []; } $keep = []; foreach ($keepPublicKeys as $key) { $pub = trim((string) $key); if ($pub !== '') { $keep[$pub] = true; } } $pruned = []; foreach (self::listPeerDumpRows() as $row) { $pub = $row['public_key']; $allowed = trim($row['allowed_ips']); $broken = ($allowed === '' || $allowed === '(none)'); $orphan = !isset($keep[$pub]); if (!$orphan && !$broken) { continue; } if (!$dryRun) { self::removeClientPeer($pub); } $pruned[] = [ 'public_key' => $pub, 'allowed_ips' => $allowed, 'reason' => $broken ? 'broken_allowed_ips' : 'orphan', ]; } return $pruned; } private static function runWg(string $args): int { $prefix = rtrim((string) cfg('remote_access.wg_set_prefix', ''), " \t"); $cmd = ($prefix !== '' ? $prefix . ' ' : '') . 'wg ' . $args; exec($cmd . ' 2>&1', $out, $code); if ($code !== 0) { error_log('WireGuardPeerProvisioner: ' . $cmd . ' failed: ' . implode(' ', $out)); } return $code; } }