feat: Add AIVPN support and enhance client statistics tracking
- Introduced AIVPN server detection and statistics fetching in ServerMonitoring. - Implemented AIVPN client statistics handling in VpnClient, including raw and offset counters for traffic. - Enhanced AWG parameters to include S3 and S4. - Updated database schema to accommodate new AIVPN statistics fields. - Added a script for remote reset and reinstallation of protocols. - Improved client view template to ensure proper display of connection instructions. - Added translations for connection instructions in multiple languages. - Ensured host-level NAT for AWG subnet in VpnServer.
This commit is contained in:
@@ -319,6 +319,9 @@ class InstallProtocolManager
|
||||
'server_host' => $result['server_host'] ?? null,
|
||||
'container_name' => $result['container_name'] ?? ($metadata['container_name'] ?? null),
|
||||
];
|
||||
if (($protocol['slug'] ?? '') === 'aivpn' && array_key_exists('connection_key', $result)) {
|
||||
$extras['connection_key'] = $result['connection_key'];
|
||||
}
|
||||
if (($protocol['slug'] ?? '') === 'xray-vless') {
|
||||
foreach (['client_id', 'container_name', 'server_port', 'xray_port', 'reality_public_key', 'reality_private_key', 'reality_short_id', 'reality_server_name'] as $k) {
|
||||
if (array_key_exists($k, $result)) {
|
||||
@@ -575,6 +578,9 @@ class InstallProtocolManager
|
||||
];
|
||||
}
|
||||
if ($phase === 'add_client') {
|
||||
if (($protocol['slug'] ?? '') === 'aivpn') {
|
||||
return self::runBuiltinAivpnAddClient($server, $options);
|
||||
}
|
||||
// If no script and no builtin handler, we just skip it (assume not needed or manual)
|
||||
// Or throw generic error? Better return success to not break flow if not implemented for other protocols
|
||||
return ['success' => true, 'message' => 'No add_client script defined'];
|
||||
@@ -775,10 +781,15 @@ class InstallProtocolManager
|
||||
$pairs = [
|
||||
'SERVER_HOST' => $serverData['host'] ?? '',
|
||||
'SERVER_USER' => $serverData['username'] ?? '',
|
||||
'SERVER_CONTAINER' => $serverData['container_name'] ?? ($metadata['container_name'] ?? ''),
|
||||
'SERVER_PORT' => isset($serverData['vpn_port']) && (int) $serverData['vpn_port'] > 0
|
||||
// Prefer protocol-specific settings for scripted installs to avoid
|
||||
// reusing a container name/port from another protocol on same server.
|
||||
'SERVER_CONTAINER' => $options['container_name']
|
||||
?? ($metadata['container_name'] ?? ($serverData['container_name'] ?? '')),
|
||||
'SERVER_PORT' => isset($options['server_port']) && (int) $options['server_port'] > 0
|
||||
? (int) $options['server_port']
|
||||
: (isset($serverData['vpn_port']) && (int) $serverData['vpn_port'] > 0
|
||||
? (int) $serverData['vpn_port']
|
||||
: (isset($options['server_port']) ? (int) $options['server_port'] : ''),
|
||||
: ''),
|
||||
];
|
||||
|
||||
// Check for saved Reality keys in server_protocols table
|
||||
@@ -876,7 +887,7 @@ class InstallProtocolManager
|
||||
private static function parseWireGuardConfig(string $config): array
|
||||
{
|
||||
$lines = preg_split('/\r?\n/', $config);
|
||||
$awgKeys = ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4'];
|
||||
$awgKeys = ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4'];
|
||||
$awgParams = [];
|
||||
$listenPort = null;
|
||||
|
||||
@@ -1292,6 +1303,127 @@ class InstallProtocolManager
|
||||
}
|
||||
}
|
||||
|
||||
private static function runBuiltinAivpnAddClient(VpnServer $server, array $options): array
|
||||
{
|
||||
$serverData = $server->getData();
|
||||
$containerName = trim((string) ($options['container_name'] ?? ($serverData['container_name'] ?? '')));
|
||||
if ($containerName === '' || stripos($containerName, 'aivpn') === false) {
|
||||
$containerName = 'aivpn-server';
|
||||
}
|
||||
|
||||
$clientName = trim((string) ($options['login'] ?? ($options['name'] ?? '')));
|
||||
if ($clientName === '') {
|
||||
$clientName = 'client-' . date('YmdHis');
|
||||
}
|
||||
|
||||
$serverHostRaw = trim((string) ($options['server_host'] ?? ($serverData['host'] ?? '')));
|
||||
$serverHostSanitized = preg_replace('#^https?://#i', '', $serverHostRaw);
|
||||
$serverHostSanitized = preg_replace('#/.*$#', '', $serverHostSanitized ?? '');
|
||||
$serverHost = $serverHostSanitized;
|
||||
$embeddedPort = null;
|
||||
if ($serverHostSanitized !== '' && preg_match('/^(.+?)(?::\d+)+$/', $serverHostSanitized, $m)) {
|
||||
$serverHost = trim((string) $m[1]);
|
||||
if (preg_match('/:(\d+)$/', $serverHostSanitized, $pm)) {
|
||||
$embeddedPort = (int) $pm[1];
|
||||
}
|
||||
}
|
||||
|
||||
$defaultPort = 443;
|
||||
if (stripos((string) ($serverData['install_protocol'] ?? ''), 'aivpn') !== false && (int) ($serverData['vpn_port'] ?? 0) > 0) {
|
||||
$defaultPort = (int) $serverData['vpn_port'];
|
||||
}
|
||||
$serverPort = isset($options['server_port']) ? (int) $options['server_port'] : 0;
|
||||
if ($serverPort <= 0 && $embeddedPort !== null && $embeddedPort > 0) {
|
||||
$serverPort = $embeddedPort;
|
||||
}
|
||||
if ($serverPort <= 0) {
|
||||
$serverPort = $defaultPort;
|
||||
}
|
||||
if (
|
||||
stripos((string) ($serverData['install_protocol'] ?? ''), 'aivpn') === false &&
|
||||
$embeddedPort === null &&
|
||||
(int) ($serverData['vpn_port'] ?? 0) > 0 &&
|
||||
$serverPort === (int) $serverData['vpn_port']
|
||||
) {
|
||||
$serverPort = 443;
|
||||
}
|
||||
if ($serverPort <= 0) {
|
||||
$serverPort = 443;
|
||||
}
|
||||
|
||||
$cmdParts = [
|
||||
'docker',
|
||||
'exec',
|
||||
'-i',
|
||||
escapeshellarg($containerName),
|
||||
'aivpn-server',
|
||||
'--add-client',
|
||||
escapeshellarg($clientName),
|
||||
'--key-file',
|
||||
'/etc/aivpn/server.key',
|
||||
'--clients-db',
|
||||
'/etc/aivpn/clients.json',
|
||||
];
|
||||
|
||||
if ($serverHost !== '') {
|
||||
$cmdParts[] = '--server-ip';
|
||||
$cmdParts[] = escapeshellarg($serverHost . ':' . $serverPort);
|
||||
}
|
||||
|
||||
$cmd = implode(' ', $cmdParts);
|
||||
Logger::appendInstall($server->getId(), 'Adding AIVPN client via builtin add_client: ' . $clientName . ' in ' . $containerName);
|
||||
$output = (string) $server->executeCommand($cmd, true);
|
||||
$parsed = self::parseAivpnAddClientOutput($output);
|
||||
|
||||
if (empty($parsed['connection_uri']) && empty($parsed['connection_key'])) {
|
||||
$head = substr(str_replace(["\r", "\n"], ' ', trim($output)), 0, 220);
|
||||
throw new Exception('AIVPN add_client succeeded but no connection key found in output: ' . $head);
|
||||
}
|
||||
|
||||
$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'];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function parseAivpnAddClientOutput(string $output): array
|
||||
{
|
||||
$result = [];
|
||||
$trimmed = trim($output);
|
||||
if ($trimmed === '') {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (preg_match('/(aivpn:\/\/[A-Za-z0-9_\-+=\/]+)/', $trimmed, $m)) {
|
||||
$uri = trim((string) $m[1]);
|
||||
$result['connection_uri'] = $uri;
|
||||
if (stripos($uri, 'aivpn://') === 0) {
|
||||
$result['connection_key'] = substr($uri, strlen('aivpn://'));
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/\bID:\s*([a-zA-Z0-9]+)/', $trimmed, $m)) {
|
||||
$result['client_id'] = trim((string) $m[1]);
|
||||
}
|
||||
|
||||
if (preg_match('/\bVPN\s*IP:\s*([0-9.]+)/i', $trimmed, $m)) {
|
||||
$result['client_ip'] = trim((string) $m[1]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function runBuiltinXrayAddClient(VpnServer $server, array $options): array
|
||||
{
|
||||
$clientId = $options['client_id'] ?? null;
|
||||
|
||||
+223
-3
@@ -16,6 +16,8 @@ class ServerMonitoring
|
||||
private array $serverData;
|
||||
private array $xrayStatsCache = [];
|
||||
private bool $xrayStatsFetched = false;
|
||||
private array $aivpnStatsCache = ['by_name' => [], 'by_id' => [], 'by_ip' => []];
|
||||
private bool $aivpnStatsFetched = false;
|
||||
|
||||
/**
|
||||
* Fetch all X-ray user stats in one batch
|
||||
@@ -113,10 +115,20 @@ class ServerMonitoring
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-fetch X-ray stats
|
||||
// Pre-fetch X-ray stats only for Xray servers.
|
||||
// Otherwise we block AWG/WireGuard stats collection with irrelevant Xray errors.
|
||||
if ($this->isXrayServer()) {
|
||||
if (!$this->fetchXrayStats()) {
|
||||
error_log("Failed to fetch X-ray stats, preventing DB overwrite");
|
||||
return []; // Abort if stats collection failed
|
||||
return []; // Abort only for Xray servers
|
||||
}
|
||||
}
|
||||
|
||||
// For AIVPN we best-effort fetch client stats once per cycle.
|
||||
if ($this->isAivpnServer()) {
|
||||
if (!$this->fetchAivpnStats()) {
|
||||
error_log("Failed to fetch AIVPN stats, using DB fallback values");
|
||||
}
|
||||
}
|
||||
|
||||
$clients = VpnClient::listByServer($this->serverData['id']);
|
||||
@@ -271,20 +283,29 @@ class ServerMonitoring
|
||||
|
||||
// Determine if this client is XRay based on protocol_id
|
||||
$isXrayClient = false;
|
||||
$protocolSlug = '';
|
||||
if (!empty($client['protocol_id'])) {
|
||||
$stmtProto = $db->prepare('SELECT slug FROM protocols WHERE id = ?');
|
||||
$stmtProto->execute([$client['protocol_id']]);
|
||||
$protoData = $stmtProto->fetch();
|
||||
if ($protoData && stripos($protoData['slug'], 'xray') !== false) {
|
||||
if ($protoData) {
|
||||
$protocolSlug = (string) ($protoData['slug'] ?? '');
|
||||
if (stripos($protocolSlug, 'xray') !== false) {
|
||||
$isXrayClient = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check config for vless URI
|
||||
if (!$isXrayClient && !empty($client['config']) && strpos($client['config'], 'vless://') !== false) {
|
||||
$isXrayClient = true;
|
||||
}
|
||||
|
||||
$isAivpnClient = (
|
||||
stripos($protocolSlug, 'aivpn') !== false ||
|
||||
(!empty($client['config']) && strpos((string) $client['config'], 'aivpn://') === 0)
|
||||
);
|
||||
|
||||
if ($isXrayClient) {
|
||||
// Retrieve DELTA from cache
|
||||
if ($this->xrayStatsFetched) {
|
||||
@@ -330,6 +351,70 @@ class ServerMonitoring
|
||||
} else {
|
||||
// WireGuard Logic - get bytes and handshake timestamp
|
||||
$publicKey = $client['public_key'];
|
||||
$isWireguardClient = (
|
||||
stripos($protocolSlug, 'awg') !== false ||
|
||||
stripos($protocolSlug, 'wireguard') !== false
|
||||
);
|
||||
|
||||
if ($isAivpnClient) {
|
||||
$aivpn = $this->getAivpnClientStats($client);
|
||||
if (is_array($aivpn)) {
|
||||
$stmt = $db->prepare("SELECT bytes_sent, bytes_received, aivpn_raw_bytes_in, aivpn_raw_bytes_out, aivpn_offset_bytes_in, aivpn_offset_bytes_out FROM vpn_clients WHERE id = ?");
|
||||
$stmt->execute([$client['id']]);
|
||||
$currentDbStats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$prevSent = (int) ($currentDbStats['bytes_sent'] ?? 0);
|
||||
$prevReceived = (int) ($currentDbStats['bytes_received'] ?? 0);
|
||||
$rawInPrev = (int) ($currentDbStats['aivpn_raw_bytes_in'] ?? 0);
|
||||
$rawOutPrev = (int) ($currentDbStats['aivpn_raw_bytes_out'] ?? 0);
|
||||
$offsetIn = (int) ($currentDbStats['aivpn_offset_bytes_in'] ?? 0);
|
||||
$offsetOut = (int) ($currentDbStats['aivpn_offset_bytes_out'] ?? 0);
|
||||
|
||||
$rawInNow = (int) ($aivpn['bytes_in'] ?? 0);
|
||||
$rawOutNow = (int) ($aivpn['bytes_out'] ?? 0);
|
||||
|
||||
// Detect counter rollover/reset in AIVPN source and preserve cumulative totals.
|
||||
if ($rawInNow < $rawInPrev) {
|
||||
$offsetIn = max($offsetIn + $rawInPrev, $prevSent);
|
||||
}
|
||||
if ($rawOutNow < $rawOutPrev) {
|
||||
$offsetOut = max($offsetOut + $rawOutPrev, $prevReceived);
|
||||
}
|
||||
|
||||
$candidateSent = $offsetIn + $rawInNow;
|
||||
$candidateReceived = $offsetOut + $rawOutNow;
|
||||
|
||||
// AIVPN stores per-client counters as bytes_in/bytes_out.
|
||||
// Map to panel semantics: sent=client upload, received=client download.
|
||||
$bytesSent = max($prevSent, $candidateSent);
|
||||
$bytesReceived = max($prevReceived, $candidateReceived);
|
||||
|
||||
$stmtAivpn = $db->prepare("UPDATE vpn_clients SET aivpn_raw_bytes_in = ?, aivpn_raw_bytes_out = ?, aivpn_offset_bytes_in = ?, aivpn_offset_bytes_out = ? WHERE id = ?");
|
||||
$stmtAivpn->execute([$rawInNow, $rawOutNow, $offsetIn, $offsetOut, $client['id']]);
|
||||
|
||||
$lastHandshake = $aivpn['last_handshake'] ?? null;
|
||||
if (is_string($lastHandshake) && $lastHandshake !== '') {
|
||||
$ts = strtotime($lastHandshake);
|
||||
if ($ts) {
|
||||
$stmtHs = $db->prepare("UPDATE vpn_clients SET last_handshake = ? WHERE id = ?");
|
||||
$stmtHs->execute([date('Y-m-d H:i:s', $ts), $client['id']]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$stmt = $db->prepare("SELECT bytes_sent, bytes_received FROM vpn_clients WHERE id = ?");
|
||||
$stmt->execute([$client['id']]);
|
||||
$currentDbStats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$bytesSent = (int) ($currentDbStats['bytes_sent'] ?? 0);
|
||||
$bytesReceived = (int) ($currentDbStats['bytes_received'] ?? 0);
|
||||
}
|
||||
} elseif (empty($publicKey) || !$isWireguardClient) {
|
||||
// Non-WireGuard protocols without dedicated collectors keep DB values.
|
||||
$stmt = $db->prepare("SELECT bytes_sent, bytes_received FROM vpn_clients WHERE id = ?");
|
||||
$stmt->execute([$client['id']]);
|
||||
$currentDbStats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$bytesSent = (int) ($currentDbStats['bytes_sent'] ?? 0);
|
||||
$bytesReceived = (int) ($currentDbStats['bytes_received'] ?? 0);
|
||||
} else {
|
||||
// wg show all dump format (tab-separated):
|
||||
// $1=interface $2=pubkey $3=psk $4=endpoint $5=allowed-ips $6=latest-handshake $7=rx-bytes $8=tx-bytes $9=keepalive
|
||||
// rx-bytes = bytes received by server = client's upload (bytes_sent)
|
||||
@@ -353,6 +438,7 @@ class ServerMonitoring
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't get stats (and they are 0), check if we have previous stats to avoid zeroing out if API fails?
|
||||
// But for speed calc we need current values.
|
||||
@@ -590,6 +676,140 @@ class ServerMonitoring
|
||||
return $this->getXrayContainerName() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this server is an AIVPN server.
|
||||
*/
|
||||
private function isAivpnServer(): bool
|
||||
{
|
||||
$containerName = (string) ($this->serverData['container_name'] ?? '');
|
||||
$protocol = (string) ($this->serverData['install_protocol'] ?? '');
|
||||
return stripos($containerName, 'aivpn') !== false || stripos($protocol, 'aivpn') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch AIVPN clients and their stats once per collection cycle.
|
||||
*/
|
||||
private function fetchAivpnStats(): bool
|
||||
{
|
||||
if ($this->aivpnStatsFetched) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->aivpnStatsFetched = true;
|
||||
$this->aivpnStatsCache = ['by_name' => [], 'by_id' => [], 'by_ip' => []];
|
||||
|
||||
$containerName = trim((string) ($this->serverData['container_name'] ?? ''));
|
||||
if ($containerName === '' || stripos($containerName, 'aivpn') === false) {
|
||||
$containerName = 'aivpn-server';
|
||||
}
|
||||
|
||||
$jsonRaw = $this->execSSH(
|
||||
'docker exec -i ' . escapeshellarg($containerName) . ' cat /etc/aivpn/clients.json 2>/dev/null'
|
||||
);
|
||||
|
||||
if (!$jsonRaw || trim($jsonRaw) === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = json_decode($jsonRaw, true);
|
||||
if (!is_array($data) || !isset($data['clients']) || !is_array($data['clients'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($data['clients'] as $entry) {
|
||||
if (!is_array($entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stats = is_array($entry['stats'] ?? null) ? $entry['stats'] : [];
|
||||
$record = [
|
||||
'id' => (string) ($entry['id'] ?? ''),
|
||||
'name' => (string) ($entry['name'] ?? ''),
|
||||
'vpn_ip' => (string) ($entry['vpn_ip'] ?? ''),
|
||||
'bytes_in' => (int) ($stats['bytes_in'] ?? 0),
|
||||
'bytes_out' => (int) ($stats['bytes_out'] ?? 0),
|
||||
'last_handshake' => isset($stats['last_handshake']) ? (string) $stats['last_handshake'] : null,
|
||||
];
|
||||
|
||||
if ($record['name'] !== '') {
|
||||
$this->aivpnStatsCache['by_name'][strtolower($record['name'])] = $record;
|
||||
}
|
||||
if ($record['id'] !== '') {
|
||||
$this->aivpnStatsCache['by_id'][$record['id']] = $record;
|
||||
}
|
||||
if ($record['vpn_ip'] !== '') {
|
||||
$this->aivpnStatsCache['by_ip'][$record['vpn_ip']] = $record;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getAivpnClientStats(array $client): ?array
|
||||
{
|
||||
if (!$this->aivpnStatsFetched && !$this->fetchAivpnStats()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = trim((string) ($client['name'] ?? ''));
|
||||
if ($name !== '') {
|
||||
$nameKey = strtolower($name);
|
||||
if (isset($this->aivpnStatsCache['by_name'][$nameKey])) {
|
||||
return $this->aivpnStatsCache['by_name'][$nameKey];
|
||||
}
|
||||
}
|
||||
|
||||
$clientIp = trim((string) ($client['client_ip'] ?? ''));
|
||||
if ($clientIp !== '' && isset($this->aivpnStatsCache['by_ip'][$clientIp])) {
|
||||
return $this->aivpnStatsCache['by_ip'][$clientIp];
|
||||
}
|
||||
|
||||
$cfgIp = $this->extractAivpnIpFromConfig((string) ($client['config'] ?? ''));
|
||||
if ($cfgIp !== '' && isset($this->aivpnStatsCache['by_ip'][$cfgIp])) {
|
||||
return $this->aivpnStatsCache['by_ip'][$cfgIp];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function extractAivpnIpFromConfig(string $config): string
|
||||
{
|
||||
if (stripos($config, 'aivpn://') !== 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$payload = substr($config, strlen('aivpn://'));
|
||||
if ($payload === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$decoded = base64_decode(strtr($payload, '-_', '+/'), true);
|
||||
if ($decoded === false) {
|
||||
$padLen = strlen($payload) % 4;
|
||||
$normalized = $payload;
|
||||
if ($padLen > 0) {
|
||||
$normalized .= str_repeat('=', 4 - $padLen);
|
||||
}
|
||||
$decoded = base64_decode(strtr($normalized, '-_', '+/'), true);
|
||||
}
|
||||
|
||||
if ($decoded === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = json_decode($decoded, true);
|
||||
if (!is_array($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$ip = trim((string) ($data['i'] ?? ''));
|
||||
if ($ip !== '' && preg_match('/^\d{1,3}(?:\.\d{1,3}){3}$/', $ip)) {
|
||||
return $ip;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce single IP per user for Xray connections
|
||||
* If a user is connected from multiple IPs, block all but the first one
|
||||
|
||||
+271
-11
@@ -126,7 +126,7 @@ class VpnClient
|
||||
}
|
||||
|
||||
// Add AWG parameters (use UPPERCASE keys internal logic)
|
||||
foreach (['JC', 'JMIN', 'JMAX', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4'] as $key) {
|
||||
foreach (['JC', 'JMIN', 'JMAX', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4'] as $key) {
|
||||
if (isset($cleanAwgParams[$key])) {
|
||||
$vars[$key] = $cleanAwgParams[$key];
|
||||
} else {
|
||||
@@ -137,6 +137,8 @@ class VpnClient
|
||||
'JMAX' => 200,
|
||||
'S1' => 50,
|
||||
'S2' => 100,
|
||||
'S3' => 20,
|
||||
'S4' => 10,
|
||||
'H1' => 1,
|
||||
'H2' => 2,
|
||||
'H3' => 3,
|
||||
@@ -213,7 +215,7 @@ class VpnClient
|
||||
foreach ($extras as $k => $v) {
|
||||
if (is_scalar($v)) {
|
||||
// Preserve uppercase for AWG obfuscation parameters
|
||||
if (in_array($k, ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4'], true)) {
|
||||
if (in_array($k, ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4'], true)) {
|
||||
$vars[$k] = (string) $v;
|
||||
} else {
|
||||
$vars[strtolower($k)] = (string) $v;
|
||||
@@ -379,6 +381,10 @@ class VpnClient
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($slug === 'aivpn') {
|
||||
// Canonical connection key should come from AIVPN --add-client output.
|
||||
// We keep fallback generation later only if add_client flow didn't provide a key.
|
||||
}
|
||||
$pass = null;
|
||||
$pwdCmd = isset($protoRow['password_command']) ? trim((string) $protoRow['password_command']) : '';
|
||||
if ($pwdCmd !== '') {
|
||||
@@ -418,13 +424,59 @@ class VpnClient
|
||||
// For xray-vless it uses builtin fallback in runScript.
|
||||
try {
|
||||
require_once __DIR__ . '/InstallProtocolManager.php';
|
||||
InstallProtocolManager::addClient($server, $protoRow, $vars);
|
||||
$addClientResult = InstallProtocolManager::addClient($server, $protoRow, $vars);
|
||||
if (is_array($addClientResult)) {
|
||||
foreach ($addClientResult as $rk => $rv) {
|
||||
if (!is_scalar($rv)) {
|
||||
continue;
|
||||
}
|
||||
$key = (string) $rk;
|
||||
$value = trim((string) $rv);
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
$vars[$key] = $value;
|
||||
$vars[strtolower($key)] = $value;
|
||||
}
|
||||
|
||||
if ($slug === 'aivpn') {
|
||||
if (empty($vars['connection_key']) && !empty($vars['connection_uri']) && stripos((string) $vars['connection_uri'], 'aivpn://') === 0) {
|
||||
$vars['connection_key'] = substr((string) $vars['connection_uri'], strlen('aivpn://'));
|
||||
}
|
||||
if (!empty($vars['client_ip']) && preg_match('/^\d{1,3}(?:\.\d{1,3}){3}$/', (string) $vars['client_ip'])) {
|
||||
$clientIP = (string) $vars['client_ip'];
|
||||
$vars['client_ip'] = $clientIP;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("Failed to add client to server: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
if ($slug === 'aivpn' && empty($vars['connection_key'])) {
|
||||
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'],
|
||||
];
|
||||
$json = (string) json_encode($payload, JSON_UNESCAPED_SLASHES);
|
||||
$vars['connection_key'] = rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Keep empty: final template output will expose a missing key.
|
||||
}
|
||||
}
|
||||
|
||||
if ($slug === 'aivpn' && !empty($vars['connection_key'])) {
|
||||
$vars['connection_key'] = self::normalizeAivpnConnectionKey((string) $vars['connection_key']);
|
||||
}
|
||||
|
||||
$config = $protoRow ? ProtocolService::generateProtocolOutput($protoRow, $vars) : '';
|
||||
|
||||
// Prepare last_config_json for QR code generation if config is JSON (XRay)
|
||||
@@ -467,6 +519,46 @@ class VpnClient
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
private static function normalizeAivpnConnectionKey(string $key): string
|
||||
{
|
||||
$key = trim($key);
|
||||
if ($key === '') {
|
||||
return $key;
|
||||
}
|
||||
|
||||
$decoded = base64_decode(strtr($key, '-_', '+/'), true);
|
||||
if ($decoded === false) {
|
||||
$padLen = strlen($key) % 4;
|
||||
$normalized = $key;
|
||||
if ($padLen > 0) {
|
||||
$normalized .= str_repeat('=', 4 - $padLen);
|
||||
}
|
||||
$decoded = base64_decode(strtr($normalized, '-_', '+/'), true);
|
||||
}
|
||||
|
||||
if ($decoded === false) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
$data = json_decode($decoded, true);
|
||||
if (!is_array($data) || empty($data['s']) || !is_string($data['s'])) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
$endpoint = trim($data['s']);
|
||||
$endpoint = preg_replace('#^https?://#i', '', $endpoint);
|
||||
$endpoint = preg_replace('#/.*$#', '', $endpoint ?? '');
|
||||
|
||||
if ($endpoint !== '' && preg_match('/^(.+?)(?::\d+){2,}$/', $endpoint, $m) && preg_match('/:(\d+)$/', $endpoint, $pm)) {
|
||||
$endpoint = trim((string) $m[1]) . ':' . (string) $pm[1];
|
||||
$data['s'] = $endpoint;
|
||||
$json = (string) json_encode($data, JSON_UNESCAPED_SLASHES);
|
||||
return rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
public static function listByServerAndProtocol(int $serverId, int $protocolId): array
|
||||
{
|
||||
$pdo = DB::conn();
|
||||
@@ -681,7 +773,7 @@ class VpnClient
|
||||
$awgParams = [];
|
||||
|
||||
$awgLinesCmd = sprintf(
|
||||
"docker exec %s sh -c \"grep -E '^[[:space:]]*(Jc|Jmin|Jmax|S1|S2|H1|H2|H3|H4)[[:space:]]*=' %s 2>/dev/null || true\"",
|
||||
"docker exec %s sh -c \"grep -E '^[[:space:]]*(Jc|Jmin|Jmax|S1|S2|S3|S4|H1|H2|H3|H4)[[:space:]]*=' %s 2>/dev/null || true\"",
|
||||
escapeshellarg($containerName),
|
||||
escapeshellarg($confPath)
|
||||
);
|
||||
@@ -692,7 +784,7 @@ class VpnClient
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^(Jc|Jmin|Jmax|S1|S2|H1|H2|H3|H4)\s*=\s*(\d+)\s*$/i', $line, $m)) {
|
||||
if (preg_match('/^(Jc|Jmin|Jmax|S1|S2|S3|S4|H1|H2|H3|H4)\s*=\s*(\d+)\s*$/i', $line, $m)) {
|
||||
$k = strtoupper($m[1]);
|
||||
$awgParams[$k] = (int) $m[2];
|
||||
}
|
||||
@@ -803,7 +895,7 @@ class VpnClient
|
||||
// Legacy attempt: some builds print jc/jmin/... in `wg show` output.
|
||||
$wgShowCmd = "docker exec $containerName wg show wg0 2>/dev/null";
|
||||
$wgOutput = (string) $server->executeCommand($wgShowCmd, true);
|
||||
$paramNames = ['jc', 'jmin', 'jmax', 's1', 's2', 'h1', 'h2', 'h3', 'h4'];
|
||||
$paramNames = ['jc', 'jmin', 'jmax', 's1', 's2', 's3', 's4', 'h1', 'h2', 'h3', 'h4'];
|
||||
foreach ($paramNames as $param) {
|
||||
if (preg_match('/^\s*' . preg_quote($param, '/') . ':\s*(\d+)/mi', $wgOutput, $matches)) {
|
||||
$awgParams[strtoupper($param)] = (int) $matches[1];
|
||||
@@ -862,7 +954,7 @@ class VpnClient
|
||||
$config .= "DNS = 1.1.1.1, 1.0.0.1\n";
|
||||
|
||||
// Add AWG parameters
|
||||
foreach (['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4'] as $key) {
|
||||
foreach (['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4'] as $key) {
|
||||
if (isset($awgParams[$key])) {
|
||||
$config .= "{$key} = {$awgParams[$key]}\n";
|
||||
continue;
|
||||
@@ -1370,7 +1462,7 @@ class VpnClient
|
||||
// If AWG params are missing (common after reinstall), fetch them directly from wg0.conf
|
||||
// to avoid falling back to template defaults that will not match the server.
|
||||
if (in_array($slug, ['amnezia-wg-advanced', 'awg2'], true)) {
|
||||
$needKeys = ['JC', 'JMIN', 'JMAX', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4'];
|
||||
$needKeys = ['JC', 'JMIN', 'JMAX', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4'];
|
||||
$missing = false;
|
||||
foreach ($needKeys as $k) {
|
||||
if (!isset($awgParams[$k])) {
|
||||
@@ -1438,7 +1530,7 @@ class VpnClient
|
||||
'dns_servers' => (string) ($serverData['dns_servers'] ?? '1.1.1.1, 1.0.0.1'),
|
||||
];
|
||||
|
||||
foreach (['JC', 'JMIN', 'JMAX', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4'] as $key) {
|
||||
foreach (['JC', 'JMIN', 'JMAX', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4'] as $key) {
|
||||
if (isset($awgParams[$key])) {
|
||||
$vars[$key] = $awgParams[$key];
|
||||
}
|
||||
@@ -1574,7 +1666,7 @@ class VpnClient
|
||||
try {
|
||||
// Get previous stats for speed calculation
|
||||
$pdo = DB::conn();
|
||||
$stmtPrev = $pdo->prepare('SELECT bytes_sent, bytes_received, last_sync_at, last_handshake FROM vpn_clients WHERE id = ?');
|
||||
$stmtPrev = $pdo->prepare('SELECT bytes_sent, bytes_received, last_sync_at, last_handshake, aivpn_raw_bytes_in, aivpn_raw_bytes_out, aivpn_offset_bytes_in, aivpn_offset_bytes_out FROM vpn_clients WHERE id = ?');
|
||||
$stmtPrev->execute([$this->clientId]);
|
||||
$prev = $stmtPrev->fetch();
|
||||
|
||||
@@ -1582,21 +1674,32 @@ class VpnClient
|
||||
$prevReceived = (int) ($prev['bytes_received'] ?? 0);
|
||||
$prevSyncAt = $prev['last_sync_at'] ? strtotime($prev['last_sync_at']) : 0;
|
||||
$prevHandshake = $prev['last_handshake'] ? strtotime($prev['last_handshake']) : 0;
|
||||
$aivpnRawInPrev = (int) ($prev['aivpn_raw_bytes_in'] ?? 0);
|
||||
$aivpnRawOutPrev = (int) ($prev['aivpn_raw_bytes_out'] ?? 0);
|
||||
$aivpnOffsetIn = (int) ($prev['aivpn_offset_bytes_in'] ?? 0);
|
||||
$aivpnOffsetOut = (int) ($prev['aivpn_offset_bytes_out'] ?? 0);
|
||||
|
||||
// XRay stats logic
|
||||
$stats = [];
|
||||
|
||||
// Determine protocol by client's protocol_id
|
||||
$isXray = false;
|
||||
$isAivpn = false;
|
||||
$xrayContainerName = 'amnezia-xray'; // Default XRay container name
|
||||
|
||||
if (!empty($this->data['protocol_id'])) {
|
||||
$stmtProto = $pdo->prepare('SELECT slug FROM protocols WHERE id = ?');
|
||||
$stmtProto->execute([$this->data['protocol_id']]);
|
||||
$protoData = $stmtProto->fetch();
|
||||
if ($protoData && stripos($protoData['slug'], 'xray') !== false) {
|
||||
if ($protoData) {
|
||||
$slug = (string) ($protoData['slug'] ?? '');
|
||||
if (stripos($slug, 'xray') !== false) {
|
||||
$isXray = true;
|
||||
}
|
||||
if (stripos($slug, 'aivpn') !== false) {
|
||||
$isAivpn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check container_name or config for xray indicators
|
||||
@@ -1605,8 +1708,12 @@ class VpnClient
|
||||
if (strpos($containerName, 'xray') !== false) {
|
||||
$isXray = true;
|
||||
$xrayContainerName = $containerName;
|
||||
} elseif (strpos($containerName, 'aivpn') !== false) {
|
||||
$isAivpn = true;
|
||||
} elseif (!empty($this->data['config']) && strpos($this->data['config'], 'vless://') !== false) {
|
||||
$isXray = true;
|
||||
} elseif (!empty($this->data['config']) && strpos($this->data['config'], 'aivpn://') === 0) {
|
||||
$isAivpn = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1648,6 +1755,28 @@ class VpnClient
|
||||
}
|
||||
}
|
||||
|
||||
} elseif ($isAivpn) {
|
||||
$stats = self::getAivpnStatsFromServer($serverData, $this->data);
|
||||
if (!empty($stats)) {
|
||||
$rawInNow = (int) ($stats['bytes_sent'] ?? 0);
|
||||
$rawOutNow = (int) ($stats['bytes_received'] ?? 0);
|
||||
|
||||
if ($rawInNow < $aivpnRawInPrev) {
|
||||
$aivpnOffsetIn = max($aivpnOffsetIn + $aivpnRawInPrev, $prevSent);
|
||||
}
|
||||
if ($rawOutNow < $aivpnRawOutPrev) {
|
||||
$aivpnOffsetOut = max($aivpnOffsetOut + $aivpnRawOutPrev, $prevReceived);
|
||||
}
|
||||
|
||||
$candidateSent = $aivpnOffsetIn + $rawInNow;
|
||||
$candidateReceived = $aivpnOffsetOut + $rawOutNow;
|
||||
$stats['bytes_sent'] = max($prevSent, $candidateSent);
|
||||
$stats['bytes_received'] = max($prevReceived, $candidateReceived);
|
||||
|
||||
if (empty($stats['last_handshake']) || (int) $stats['last_handshake'] <= 0) {
|
||||
$stats['last_handshake'] = $prevHandshake;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($stats)) {
|
||||
@@ -1681,16 +1810,43 @@ class VpnClient
|
||||
}
|
||||
}
|
||||
|
||||
$isAivpnPersist = $isAivpn && !empty($stats);
|
||||
if ($isAivpnPersist) {
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE vpn_clients
|
||||
SET bytes_sent = ?, bytes_received = ?, last_handshake = ?, current_speed = ?, speed_up = ?, speed_down = ?,
|
||||
aivpn_raw_bytes_in = ?, aivpn_raw_bytes_out = ?, aivpn_offset_bytes_in = ?, aivpn_offset_bytes_out = ?,
|
||||
last_sync_at = NOW()
|
||||
WHERE id = ?
|
||||
');
|
||||
} else {
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE vpn_clients
|
||||
SET bytes_sent = ?, bytes_received = ?, last_handshake = ?, current_speed = ?, speed_up = ?, speed_down = ?, last_sync_at = NOW()
|
||||
WHERE id = ?
|
||||
');
|
||||
}
|
||||
|
||||
$lastHandshake = $stats['last_handshake'] > 0
|
||||
? date('Y-m-d H:i:s', $stats['last_handshake'])
|
||||
: null;
|
||||
|
||||
if ($isAivpnPersist) {
|
||||
return $stmt->execute([
|
||||
$stats['bytes_sent'],
|
||||
$stats['bytes_received'],
|
||||
$lastHandshake,
|
||||
$currentSpeed,
|
||||
$speedUp,
|
||||
$speedDown,
|
||||
(int) ($stats['bytes_sent_raw'] ?? 0),
|
||||
(int) ($stats['bytes_received_raw'] ?? 0),
|
||||
$aivpnOffsetIn,
|
||||
$aivpnOffsetOut,
|
||||
$this->clientId
|
||||
]);
|
||||
}
|
||||
|
||||
return $stmt->execute([
|
||||
$stats['bytes_sent'],
|
||||
$stats['bytes_received'],
|
||||
@@ -1706,6 +1862,110 @@ class VpnClient
|
||||
}
|
||||
}
|
||||
|
||||
private static function getAivpnStatsFromServer(array $serverData, array $clientData): array
|
||||
{
|
||||
$stats = [
|
||||
'bytes_sent' => 0,
|
||||
'bytes_received' => 0,
|
||||
'bytes_sent_raw' => 0,
|
||||
'bytes_received_raw' => 0,
|
||||
'last_handshake' => 0,
|
||||
];
|
||||
|
||||
$containerName = (string) ($serverData['container_name'] ?? '');
|
||||
if ($containerName === '' || stripos($containerName, 'aivpn') === false) {
|
||||
$containerName = 'aivpn-server';
|
||||
}
|
||||
|
||||
$cmd = sprintf('docker exec -i %s cat /etc/aivpn/clients.json 2>/dev/null', escapeshellarg($containerName));
|
||||
$output = self::executeServerCommand($serverData, $cmd, true);
|
||||
if (trim((string) $output) === '') {
|
||||
return $stats;
|
||||
}
|
||||
|
||||
$data = json_decode((string) $output, true);
|
||||
if (!is_array($data) || !isset($data['clients']) || !is_array($data['clients'])) {
|
||||
return $stats;
|
||||
}
|
||||
|
||||
$name = strtolower(trim((string) ($clientData['name'] ?? '')));
|
||||
$clientIp = trim((string) ($clientData['client_ip'] ?? ''));
|
||||
$cfgIp = self::extractAivpnIpFromConfig((string) ($clientData['config'] ?? ''));
|
||||
|
||||
$match = null;
|
||||
foreach ($data['clients'] as $entry) {
|
||||
if (!is_array($entry)) {
|
||||
continue;
|
||||
}
|
||||
$entryName = strtolower(trim((string) ($entry['name'] ?? '')));
|
||||
$entryIp = trim((string) ($entry['vpn_ip'] ?? ''));
|
||||
if ($name !== '' && $entryName === $name) {
|
||||
$match = $entry;
|
||||
break;
|
||||
}
|
||||
if ($clientIp !== '' && $entryIp === $clientIp) {
|
||||
$match = $entry;
|
||||
break;
|
||||
}
|
||||
if ($cfgIp !== '' && $entryIp === $cfgIp) {
|
||||
$match = $entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($match)) {
|
||||
return $stats;
|
||||
}
|
||||
|
||||
$s = is_array($match['stats'] ?? null) ? $match['stats'] : [];
|
||||
$rawIn = (int) ($s['bytes_in'] ?? 0);
|
||||
$rawOut = (int) ($s['bytes_out'] ?? 0);
|
||||
$stats['bytes_sent_raw'] = $rawIn;
|
||||
$stats['bytes_received_raw'] = $rawOut;
|
||||
$stats['bytes_sent'] = $rawIn;
|
||||
$stats['bytes_received'] = $rawOut;
|
||||
|
||||
if (!empty($s['last_handshake']) && is_string($s['last_handshake'])) {
|
||||
$ts = strtotime($s['last_handshake']);
|
||||
if ($ts !== false) {
|
||||
$stats['last_handshake'] = (int) $ts;
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private static function extractAivpnIpFromConfig(string $config): string
|
||||
{
|
||||
if (stripos($config, 'aivpn://') !== 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$payload = substr($config, strlen('aivpn://'));
|
||||
if ($payload === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$b64 = strtr($payload, '-_', '+/');
|
||||
$padLen = strlen($b64) % 4;
|
||||
if ($padLen > 0) {
|
||||
$b64 .= str_repeat('=', 4 - $padLen);
|
||||
}
|
||||
|
||||
$decoded = base64_decode($b64, true);
|
||||
if ($decoded === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = json_decode($decoded, true);
|
||||
if (!is_array($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$ip = trim((string) ($data['i'] ?? ''));
|
||||
return preg_match('/^\d{1,3}(?:\.\d{1,3}){3}$/', $ip) ? $ip : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client statistics from server
|
||||
*/
|
||||
|
||||
@@ -706,6 +706,19 @@ BASH;
|
||||
$this->executeCommand("docker exec -i {$containerName} sh -c 'iptables -A FORWARD -i wg0 -o eth0 -s 10.8.1.0/24 -j ACCEPT 2>/dev/null || true'", true);
|
||||
$this->executeCommand("docker exec -i {$containerName} sh -c 'iptables -t nat -A POSTROUTING -s 10.8.1.0/24 -o eth0 -j MASQUERADE 2>/dev/null || true'", true);
|
||||
|
||||
// Ensure host-level forwarding/NAT for AWG subnet as well (required on some Docker host setups).
|
||||
$vpnSubnet = (string) ($this->data['vpn_subnet'] ?? '10.8.1.0/24');
|
||||
$vpnSubnetEsc = escapeshellarg($vpnSubnet);
|
||||
$hostNatCmd = "bash -lc 'IFACE=\\$(ip route | awk \"{if (\\$1==\\\"default\\\") {print \\$5; exit}}\"); " .
|
||||
"iptables -t nat -C POSTROUTING -s " . $vpnSubnetEsc . " -o \\\"\\$IFACE\\\" -j MASQUERADE 2>/dev/null || " .
|
||||
"iptables -t nat -I POSTROUTING 1 -s " . $vpnSubnetEsc . " -o \\\"\\$IFACE\\\" -j MASQUERADE; " .
|
||||
"iptables -C FORWARD -s " . $vpnSubnetEsc . " -o \\\"\\$IFACE\\\" -j ACCEPT 2>/dev/null || " .
|
||||
"iptables -I FORWARD 1 -s " . $vpnSubnetEsc . " -o \\\"\\$IFACE\\\" -j ACCEPT; " .
|
||||
"iptables -C FORWARD -d " . $vpnSubnetEsc . " -m conntrack --ctstate RELATED,ESTABLISHED -i \\\"\\$IFACE\\\" -j ACCEPT 2>/dev/null || " .
|
||||
"iptables -I FORWARD 1 -d " . $vpnSubnetEsc . " -m conntrack --ctstate RELATED,ESTABLISHED -i \\\"\\$IFACE\\\" -j ACCEPT; " .
|
||||
"sysctl -w net.ipv4.ip_forward=1 >/dev/null'";
|
||||
$this->executeCommand($hostNatCmd, true);
|
||||
|
||||
sleep(2);
|
||||
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Ensure clients.connection_instructions exists in all locales used by UI.
|
||||
-- Without this key, client view heading may be missing or fallback text can appear inconsistent.
|
||||
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en', 'clients', 'connection_instructions', 'Connection Instructions'),
|
||||
('ru', 'clients', 'connection_instructions', 'Инструкции по подключению'),
|
||||
('es', 'clients', 'connection_instructions', 'Instrucciones de conexión'),
|
||||
('de', 'clients', 'connection_instructions', 'Verbindungsanweisungen'),
|
||||
('fr', 'clients', 'connection_instructions', 'Instructions de connexion'),
|
||||
('zh', 'clients', 'connection_instructions', '连接说明')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Add persistent AIVPN raw/offset counters for monotonic traffic totals across server restarts.
|
||||
|
||||
ALTER TABLE vpn_clients
|
||||
ADD COLUMN aivpn_raw_bytes_in BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER bytes_received,
|
||||
ADD COLUMN aivpn_raw_bytes_out BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER aivpn_raw_bytes_in,
|
||||
ADD COLUMN aivpn_offset_bytes_in BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER aivpn_raw_bytes_out,
|
||||
ADD COLUMN aivpn_offset_bytes_out BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER aivpn_offset_bytes_in;
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PANEL_URL="http://localhost:8082"
|
||||
EMAIL="admin@amnez.ia"
|
||||
PASSWORD="admin123"
|
||||
SERVER_ID="1"
|
||||
REMOTE_HOST="217.26.25.6"
|
||||
REMOTE_USER="root"
|
||||
REMOTE_PASS='1Fr045jZbtF!'
|
||||
|
||||
# protocol IDs in this workspace
|
||||
AWG2_ID="11"
|
||||
AIVPN_ID="13"
|
||||
MTPROXY_ID="12"
|
||||
|
||||
echo "== auth =="
|
||||
TOKEN=$(curl -sS -X POST "$PANEL_URL/api/auth/token" \
|
||||
-d "email=$EMAIL&password=$PASSWORD" | python3 -c 'import sys,json; print(json.load(sys.stdin)["token"])')
|
||||
|
||||
echo "== remote full docker cleanup =="
|
||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" 'bash -s' <<'EOSSH'
|
||||
set -euo pipefail
|
||||
|
||||
# Stop and remove all containers if any
|
||||
if [ -n "$(docker ps -aq 2>/dev/null || true)" ]; then
|
||||
docker rm -f $(docker ps -aq) >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Full cleanup of images/volumes/networks/build cache
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
docker system prune -af --volumes >/dev/null 2>&1 || true
|
||||
docker builder prune -af >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Remove protocol dirs to force fresh bootstrap
|
||||
rm -rf /opt/amnezia /etc/aivpn /etc/amnezia /etc/mtproxy 2>/dev/null || true
|
||||
mkdir -p /opt/amnezia /etc/aivpn /etc/amnezia /etc/mtproxy
|
||||
|
||||
echo "remote cleanup done"
|
||||
EOSSH
|
||||
|
||||
echo "== install awg2 =="
|
||||
curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/install" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"protocol_id\":$AWG2_ID}" | tee /tmp/install_awg2_after_remote_reset.json
|
||||
|
||||
echo
|
||||
echo "== install aivpn =="
|
||||
curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/install" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"protocol_id\":$AIVPN_ID}" | tee /tmp/install_aivpn_after_remote_reset.json
|
||||
|
||||
echo
|
||||
echo "== install mtproxy =="
|
||||
curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/install" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"protocol_id\":$MTPROXY_ID}" | tee /tmp/install_mtproxy_after_remote_reset.json
|
||||
|
||||
echo
|
||||
echo "== verify containers on remote =="
|
||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" \
|
||||
"docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
|
||||
|
||||
echo
|
||||
echo "done"
|
||||
@@ -155,7 +155,7 @@
|
||||
{% if protocol_output and client.show_text_content %}
|
||||
<div class="bg-white rounded shadow p-6 mt-6">
|
||||
<h3 class="font-bold mb-4">{{ t('clients.connection_instructions') }}</h3>
|
||||
<pre class="mb-0" style="white-space: pre-wrap;">{{ protocol_output }}</pre>
|
||||
<pre class="mb-0" style="white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;">{{ protocol_output }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user