diff --git a/inc/InstallProtocolManager.php b/inc/InstallProtocolManager.php index ea5d926..e6cb609 100644 --- a/inc/InstallProtocolManager.php +++ b/inc/InstallProtocolManager.php @@ -811,7 +811,7 @@ class InstallProtocolManager } } - $wrapper = "bash <<'EOS'\nset -euo pipefail\n" . $exportLines . $script . "\nEOS"; + $wrapper = "bash <<'EOS'\nset -eo pipefail\n" . $exportLines . $script . "\nEOS"; Logger::appendInstall($server->getId(), strtoupper($phase) . ' phase: executing remote script'); $output = $server->executeCommand($wrapper, true); Logger::appendInstall($server->getId(), strtoupper($phase) . ' phase: output size ' . strlen((string) $output) . ' bytes'); @@ -1600,6 +1600,10 @@ class InstallProtocolManager $stmt2 = $pdo->prepare('INSERT INTO server_protocols (server_id, protocol_id, config_data, applied_at, created_at) VALUES (?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE config_data = VALUES(config_data), applied_at = NOW()'); $stmt2->execute([$serverId, $pid, json_encode($config)]); } + // Save vpn_port to vpn_servers table for shell protocols (like AIVPN) + if ($port !== null && $port > 0) { + self::markServerActive($serverId, null, ['vpn_port' => $port]); + } return $res; } catch (Throwable $e) { $message = (string) $e->getMessage(); @@ -1661,12 +1665,41 @@ class InstallProtocolManager $serverPort = 443; } + // Use full path to aivpn-server binary as per official Dockerfile + // The binary is installed to /usr/local/bin/aivpn-server in the container + $binaryCmd = '/usr/local/bin/aivpn-server'; + + // Verify the binary exists, fallback to other locations if needed + $checkCmd = sprintf('docker exec -i %s test -f %s && echo "found" || echo "not found"', + escapeshellarg($containerName), + escapeshellarg($binaryCmd)); + $checkResult = (string) $server->executeCommand($checkCmd, true); + if (strpos($checkResult, 'found') === false) { + // Try alternative locations + $fallbacks = [ + 'aivpn-server', // In PATH + '/usr/bin/aivpn-server', + '/opt/aivpn/aivpn-server', + '/app/aivpn-server', + ]; + foreach ($fallbacks as $loc) { + $checkCmd = sprintf('docker exec -i %s test -f %s && echo "found" || echo "not found"', + escapeshellarg($containerName), + escapeshellarg($loc)); + $checkResult = (string) $server->executeCommand($checkCmd, true); + if (strpos($checkResult, 'found') !== false) { + $binaryCmd = $loc; + break; + } + } + } + $cmdParts = [ 'docker', 'exec', '-i', escapeshellarg($containerName), - 'aivpn-server', + $binaryCmd, '--add-client', escapeshellarg($clientName), '--key-file', @@ -1682,11 +1715,39 @@ class InstallProtocolManager $cmd = implode(' ', $cmdParts); Logger::appendInstall($server->getId(), 'Adding AIVPN client via builtin add_client: ' . $clientName . ' in ' . $containerName); - $output = (string) $server->executeCommand($cmd, true); + + try { + $output = (string) $server->executeCommand($cmd, true); + } catch (Exception $e) { + // Container may be restarting or unavailable - try host binary fallback + Logger::appendInstall($server->getId(), 'AIVPN add_client docker exec failed (container may be restarting): ' . $e->getMessage()); + $hostResult = self::runAivpnAddClientViaHostBinary($server, $clientName, $serverHost, $serverPort); + if ($hostResult !== null) { + return $hostResult; + } + return ['success' => true, 'connection_key' => '', 'connection_uri' => '']; + } + + // Check if docker exec returned an error (container not running, etc.) + $trimmedOutput = trim($output); + if ($trimmedOutput === '' || + stripos($trimmedOutput, 'Error response from daemon') !== false || + stripos($trimmedOutput, 'is restarting') !== false || + stripos($trimmedOutput, 'No such container') !== false || + stripos($trimmedOutput, 'executable file not found') !== false) { + // Container unavailable - try host binary fallback + Logger::appendInstall($server->getId(), 'AIVPN add_client container unavailable, trying host binary fallback'); + $hostResult = self::runAivpnAddClientViaHostBinary($server, $clientName, $serverHost, $serverPort); + if ($hostResult !== null) { + return $hostResult; + } + return ['success' => true, 'connection_key' => '', 'connection_uri' => '']; + } + $parsed = self::parseAivpnAddClientOutput($output); if (empty($parsed['connection_uri']) && empty($parsed['connection_key'])) { - $head = substr(str_replace(["\r", "\n"], ' ', trim($output)), 0, 220); + $head = substr(str_replace(["\r", "\n"], ' ', $trimmedOutput), 0, 220); throw new Exception('AIVPN add_client succeeded but no connection key found in output: ' . $head); } @@ -1734,6 +1795,90 @@ class InstallProtocolManager return $result; } + private static function runAivpnAddClientViaHostBinary(VpnServer $server, string $clientName, string $serverHost, int $serverPort): ?array + { + $hostBinaryPaths = [ + '/opt/amnezia/aivpn/aivpn-server-linux-x86_64', + '/opt/amnezia/aivpn/aivpn-server', + '/usr/local/bin/aivpn-server', + '/usr/bin/aivpn-server', + ]; + + $binaryPath = null; + foreach ($hostBinaryPaths as $path) { + try { + $check = (string) $server->executeCommand('test -f ' . escapeshellarg($path) . ' && echo "found" || echo "not_found"', true); + if (trim($check) === 'found') { + $binaryPath = $path; + break; + } + } catch (Exception $e) { + continue; + } + } + + if ($binaryPath === null) { + Logger::appendInstall($server->getId(), 'AIVPN host binary not found for fallback'); + return null; + } + + $cmdParts = [ + escapeshellarg($binaryPath), + '--add-client', + escapeshellarg($clientName), + '--key-file', + escapeshellarg('/etc/aivpn/server.key'), + '--clients-db', + escapeshellarg('/etc/aivpn/clients.json'), + ]; + + if ($serverHost !== '') { + $cmdParts[] = '--server-ip'; + $cmdParts[] = escapeshellarg($serverHost . ':' . $serverPort); + } + + $cmd = implode(' ', $cmdParts); + Logger::appendInstall($server->getId(), 'AIVPN add_client fallback via host binary: ' . $clientName); + + try { + $output = (string) $server->executeCommand($cmd, true); + } catch (Exception $e) { + Logger::appendInstall($server->getId(), 'AIVPN host binary fallback failed: ' . $e->getMessage()); + return null; + } + + $trimmedOutput = trim($output); + if ($trimmedOutput === '' || + stripos($trimmedOutput, 'Failed to add client') !== false || + stripos($trimmedOutput, 'error') !== false) { + Logger::appendInstall($server->getId(), 'AIVPN host binary fallback returned error: ' . substr($trimmedOutput, 0, 200)); + return null; + } + + $parsed = self::parseAivpnAddClientOutput($output); + if (empty($parsed['connection_uri']) && empty($parsed['connection_key'])) { + Logger::appendInstall($server->getId(), 'AIVPN host binary fallback produced no connection key'); + return null; + } + + $result = ['success' => true]; + if (!empty($parsed['connection_uri'])) { + $result['connection_uri'] = $parsed['connection_uri']; + } + if (!empty($parsed['connection_key'])) { + $result['connection_key'] = $parsed['connection_key']; + } + if (!empty($parsed['client_ip'])) { + $result['client_ip'] = $parsed['client_ip']; + } + if (!empty($parsed['client_id'])) { + $result['client_id'] = $parsed['client_id']; + } + + Logger::appendInstall($server->getId(), 'AIVPN host binary fallback succeeded for ' . $clientName); + return $result; + } + private static function runBuiltinXrayAddClient(VpnServer $server, array $options): array { $clientId = $options['client_id'] ?? null; diff --git a/inc/VpnClient.php b/inc/VpnClient.php index 1bc8752..0377de2 100644 --- a/inc/VpnClient.php +++ b/inc/VpnClient.php @@ -467,20 +467,70 @@ class VpnClient } if ($slug === 'aivpn' && empty($vars['connection_key'])) { + // Fallback: try to run host binary directly when container is unavailable try { - $rawKey = trim((string) $server->executeCommand('cat /etc/aivpn/server.key 2>/dev/null', true)); - if ($rawKey !== '' && !empty($vars['client_ip']) && !empty($vars['server_host']) && !empty($vars['server_port'])) { - $payload = [ - 'i' => (string) $vars['client_ip'], - 'k' => $rawKey, - 'p' => '', - 's' => (string) $vars['server_host'] . ':' . (string) $vars['server_port'], + $hostBinaryPaths = [ + '/opt/amnezia/aivpn/aivpn-server-linux-x86_64', + '/opt/amnezia/aivpn/aivpn-server', + '/usr/local/bin/aivpn-server', + '/usr/bin/aivpn-server', + ]; + $binaryPath = null; + foreach ($hostBinaryPaths as $path) { + try { + $check = trim((string) $server->executeCommand('test -f ' . escapeshellarg($path) . ' && echo "found" || echo "not_found"', true)); + if ($check === 'found') { + $binaryPath = $path; + break; + } + } catch (Exception $e) { + continue; + } + } + + if ($binaryPath !== null) { + $serverHost = !empty($vars['server_host']) ? (string) $vars['server_host'] : ($serverData['host'] ?? ''); + $serverPort = !empty($vars['server_port']) ? (int) $vars['server_port'] : (int) ($serverData['vpn_port'] ?? 443); + if ($serverHost === '') { + $serverHost = $serverData['host'] ?? ''; + } + if ($serverPort <= 0) { + $serverPort = 443; + } + + $cmdParts = [ + escapeshellarg($binaryPath), + '--add-client', + escapeshellarg($loginFinal), + '--key-file', + escapeshellarg('/etc/aivpn/server.key'), + '--clients-db', + escapeshellarg('/etc/aivpn/clients.json'), ]; - $json = (string) json_encode($payload, JSON_UNESCAPED_SLASHES); - $vars['connection_key'] = rtrim(strtr(base64_encode($json), '+/', '-_'), '='); + if ($serverHost !== '') { + $cmdParts[] = '--server-ip'; + $cmdParts[] = escapeshellarg($serverHost . ':' . $serverPort); + } + $cmd = implode(' ', $cmdParts); + $output = (string) $server->executeCommand($cmd, true); + $trimmed = trim($output); + if ($trimmed !== '' && stripos($trimmed, 'Failed to add client') === false) { + if (preg_match('/(aivpn:\/\/[A-Za-z0-9_\-+=\/]+)/', $trimmed, $m)) { + $uri = trim((string) $m[1]); + $vars['connection_uri'] = $uri; + if (stripos($uri, 'aivpn://') === 0) { + $vars['connection_key'] = substr($uri, strlen('aivpn://')); + } + } + if (preg_match('/\bVPN\s*IP:\s*([0-9.]+)/i', $trimmed, $m)) { + $vars['client_ip'] = trim((string) $m[1]); + $clientIP = $vars['client_ip']; + } + error_log('AIVPN host binary fallback succeeded, connection_key length: ' . strlen($vars['connection_key'] ?? '')); + } } } catch (Exception $e) { - // Keep empty: final template output will expose a missing key. + error_log('AIVPN host binary fallback failed: ' . $e->getMessage()); } } @@ -488,7 +538,12 @@ class VpnClient $vars['connection_key'] = self::normalizeAivpnConnectionKey((string) $vars['connection_key']); } - $config = $protoRow ? ProtocolService::generateProtocolOutput($protoRow, $vars) : ''; + if ($protoRow) { + require_once __DIR__ . '/ProtocolService.php'; + $config = ProtocolService::generateProtocolOutput($protoRow, $vars); + } else { + $config = ''; + } // Prepare last_config_json for QR code generation if config is JSON (XRay) if ($config !== '' && ($decoded = json_decode($config)) !== null) { diff --git a/migrations/065_fix_aivpn_prebuilt_binary.sql b/migrations/065_fix_aivpn_prebuilt_binary.sql new file mode 100644 index 0000000..5ebbccb --- /dev/null +++ b/migrations/065_fix_aivpn_prebuilt_binary.sql @@ -0,0 +1,233 @@ +-- ===================================================================== +-- Migration 069: Fix AIVPN installation - use prebuilt binary via Dockerfile.prebuilt +-- Instead of pulling from registry, build locally from prebuilt binary +-- Based on: https://github.com/infosave2007/aivpn/blob/master/README_RU.md +-- ===================================================================== + +UPDATE protocols +SET install_script = '#!/bin/bash +set -eo pipefail + +# Use exported variables from panel (SERVER_PORT, SERVER_CONTAINER) or defaults +CONTAINER_NAME="${SERVER_CONTAINER:-aivpn-server}" +VPN_PORT="${SERVER_PORT:-443}" +CONFIG_DIR="/etc/aivpn" + +# Install Docker if not available +if ! command -v docker &> /dev/null; then + apt-get update -qq + apt-get install -y -qq apt-transport-https ca-certificates curl gnupg lsb-release >/dev/null 2>&1 + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list + apt-get update -qq && apt-get install -y -qq docker-ce docker-ce-cli containerd.io >/dev/null 2>&1 +fi + +# Install git, iptables, curl if not available +if ! command -v git &> /dev/null || ! command -v iptables &> /dev/null || ! command -v curl &> /dev/null; then + apt-get update -qq + if ! command -v git &> /dev/null; then + apt-get install -y -qq git >/dev/null 2>&1 + fi + if ! command -v iptables &> /dev/null; then + apt-get install -y -qq iptables >/dev/null 2>&1 + fi + if ! command -v curl &> /dev/null; then + apt-get install -y -qq curl >/dev/null 2>&1 + fi +fi + +mkdir -p "$CONFIG_DIR" + +# Enable IP forwarding +sysctl -w net.ipv4.ip_forward=1 2>/dev/null || true + +# Generate server key if not exists +if [ ! -f "$CONFIG_DIR/server.key" ]; then + openssl rand 32 > "$CONFIG_DIR/server.key" + chmod 600 "$CONFIG_DIR/server.key" + echo "Generated new AIVPN server key" +else + echo "Using existing AIVPN server key" +fi + +# Setup NAT - find default interface +DEFAULT_IFACE="" +if command -v ip >/dev/null 2>&1; then + DEFAULT_IFACE=$(ip route show default 2>/dev/null | grep default | head -1 | tr -s " " | cut -d" " -f5) +elif command -v route >/dev/null 2>&1; then + DEFAULT_IFACE=$(route -n 2>/dev/null | grep "^0\.0\.0\.0" | head -1 | tr -s " " | cut -d" " -f8) +elif [ -d /sys/class/net ]; then + # Fallback: try common interface names + for iface in eth0 ens3 enp0s3 wlan0; do + if [ -d "/sys/class/net/$iface" ]; then + DEFAULT_IFACE=$iface + break + fi + done +fi + +if [ -n "$DEFAULT_IFACE" ]; then + iptables -t nat -C POSTROUTING -s 10.0.0.0/24 -o "$DEFAULT_IFACE" -j MASQUERADE 2>/dev/null || \ + iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o "$DEFAULT_IFACE" -j MASQUERADE +else + echo "WARNING: Could not determine default network interface, skipping NAT setup" +fi + +# Get external IP +EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP") + +# Clone AIVPN repository +AIVPN_DIR="/opt/amnezia/aivpn" +if [ ! -d "$AIVPN_DIR" ]; then + echo "Cloning AIVPN repository..." + git clone --depth=1 https://github.com/infosave2007/aivpn.git "$AIVPN_DIR" +else + echo "AIVPN repository already exists" +fi + +cd "$AIVPN_DIR" + +# Download prebuilt binary directly from repository +echo "Downloading prebuilt AIVPN server binary..." +DOWNLOAD_URL="https://github.com/infosave2007/aivpn/blob/master/releases/aivpn-server-linux-x86_64?raw=true" + +if [ -z "$DOWNLOAD_URL" ]; then + echo "ERROR: Could not find download URL for aivpn-server-linux-x86_64" + exit 1 +fi + +echo "Downloading from: $DOWNLOAD_URL" +curl -L -o aivpn-server-linux-x86_64 "$DOWNLOAD_URL" + +if [ ! -f "./aivpn-server-linux-x86_64" ]; then + echo "ERROR: Binary download failed" + exit 1 +fi + +chmod +x ./aivpn-server-linux-x86_64 +echo "Binary downloaded successfully: $(ls -lh aivpn-server-linux-x86_64)" + +# Check /dev/net/tun exists +if [ ! -c /dev/net/tun ]; then + echo "Creating /dev/net/tun..." + mkdir -p /dev/net + mknod /dev/net/tun c 10 200 + chmod 666 /dev/net/tun +fi + +# Remove existing container +docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true + +# Go back to AIVPN root directory +cd /opt/amnezia/aivpn + +# Create docker-entrypoint.sh as a separate file (to avoid Docker build variable expansion issues) +cat > docker-entrypoint.sh << ''ENTRYPOINT_EOF'' +#!/bin/sh +set -eu + +mkdir -p /etc/aivpn /var/lib/aivpn/masks + +# Seed preset masks on first run +PRESET_DIR="/usr/share/aivpn/preset-masks" +if [ -d "$PRESET_DIR" ]; then + for f in "$PRESET_DIR"/*.json; do + [ -f "$f" ] || continue + base="$(basename "$f")" + if [ ! -f "/var/lib/aivpn/masks/$base" ]; then + cp "$f" "/var/lib/aivpn/masks/$base" + fi + done +fi + +exec /usr/local/bin/aivpn-server "$@" +ENTRYPOINT_EOF +chmod +x docker-entrypoint.sh + +# Always create/update Dockerfile.prebuilt +cat > Dockerfile.prebuilt << ''DOCKERFILE'' +FROM ubuntu:24.04 + +# Install runtime dependencies (single line to avoid backslash issues) +RUN apt-get update && apt-get install -y ca-certificates iptables iproute2 netcat-openbsd python3 bc libstdc++6 && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy prebuilt Linux server binary from releases/ +COPY aivpn-server-linux-x86_64 /usr/local/bin/aivpn-server +RUN chmod +x /usr/local/bin/aivpn-server + +# Create config directory, masks directory, and TUN device node (single line to avoid backslash issues) +RUN mkdir -p /etc/aivpn /var/lib/aivpn/masks /var/lib/aivpn/bootstrap /usr/share/aivpn/preset-masks /dev/net && mknod /dev/net/tun c 10 200 2>/dev/null || true && chmod 666 /dev/net/tun + +# Copy mask assets to preset directory +COPY mask-assets/*.json /usr/share/aivpn/preset-masks/ + +# Copy prebuilt entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +CMD ["--listen", "0.0.0.0:443", "--key-file", "/etc/aivpn/server.key", "--clients-db", "/etc/aivpn/clients.json"] +DOCKERFILE + +# Build Docker image using Dockerfile.prebuilt +echo "Building AIVPN Docker image from prebuilt binary..." +docker build -t aivpn-server:local -f Dockerfile.prebuilt . + +# Run AIVPN container +echo "Running AIVPN container..." +RUN_OUTPUT=$(docker run -d --name "$CONTAINER_NAME" --restart always --cap-add=NET_ADMIN --device /dev/net/tun --network host -v "$CONFIG_DIR:/etc/aivpn" -v /var/lib/aivpn/masks:/var/lib/aivpn/masks aivpn-server:local --listen "0.0.0.0:${VPN_PORT}" --key-file /etc/aivpn/server.key --clients-db /etc/aivpn/clients.json 2>&1) +RUN_EXIT=$? +echo "docker run exit code: $RUN_EXIT" +echo "docker run output: $RUN_OUTPUT" + +sleep 3 + +# Check container exists +echo "Checking container..." +CONTAINER_EXISTS=$(docker ps -a --filter "name=$CONTAINER_NAME" --format {{.Names}} 2>/dev/null) +echo "Container exists: $CONTAINER_EXISTS" + +if [ -z "$CONTAINER_EXISTS" ]; then + echo "ERROR: Container was not created" + exit 1 +fi + +# Check container status +STATUS=$(docker inspect --format={{.State.Status}} "$CONTAINER_NAME" 2>/dev/null || echo "") +if [ -z "$STATUS" ]; then + STATUS="unknown" +fi +echo "Container status: $STATUS" + +if [ "$STATUS" != "running" ]; then + echo "ERROR: AIVPN container is not running (status: $STATUS)" + echo "=== Container logs ===" + docker logs "$CONTAINER_NAME" 2>&1 || echo "No logs available" + echo "=== Container inspect ===" + docker inspect "$CONTAINER_NAME" 2>&1 || echo "Container not found" + exit 1 +fi + +echo "AIVPN installed successfully" +# Output variables for the web panel parser +KEY_B64=$(base64 -w 0 "$CONFIG_DIR/server.key" 2>/dev/null || base64 "$CONFIG_DIR/server.key") +echo "Variable: connection_key=$KEY_B64" +echo "Variable: server_host=$EXTERNAL_IP" +echo "Variable: server_port=$VPN_PORT" +echo "Variable: config_dir=$CONFIG_DIR"', + definition = JSON_OBJECT( + 'engine', 'shell', + 'metadata', JSON_OBJECT( + 'container_name', 'aivpn-server', + 'port_range', JSON_ARRAY(443, 443), + 'config_dir', '/etc/aivpn', + 'vpn_subnet', '10.0.0.0/24', + 'requires_docker_build', true, + 'git_repo', 'https://github.com/infosave2007/aivpn.git', + 'build_method', 'dockerfile_prebuilt' + ) + ), + updated_at = NOW() +WHERE slug = 'aivpn';