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:
infosave2007
2026-04-04 15:27:40 +03:00
parent da5cdc6ae8
commit c38c3d1c83
8 changed files with 741 additions and 29 deletions
+137 -5
View File
@@ -319,6 +319,9 @@ class InstallProtocolManager
'server_host' => $result['server_host'] ?? null, 'server_host' => $result['server_host'] ?? null,
'container_name' => $result['container_name'] ?? ($metadata['container_name'] ?? 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') { 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) { 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)) { if (array_key_exists($k, $result)) {
@@ -575,6 +578,9 @@ class InstallProtocolManager
]; ];
} }
if ($phase === 'add_client') { 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) // 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 // 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']; return ['success' => true, 'message' => 'No add_client script defined'];
@@ -775,10 +781,15 @@ class InstallProtocolManager
$pairs = [ $pairs = [
'SERVER_HOST' => $serverData['host'] ?? '', 'SERVER_HOST' => $serverData['host'] ?? '',
'SERVER_USER' => $serverData['username'] ?? '', 'SERVER_USER' => $serverData['username'] ?? '',
'SERVER_CONTAINER' => $serverData['container_name'] ?? ($metadata['container_name'] ?? ''), // Prefer protocol-specific settings for scripted installs to avoid
'SERVER_PORT' => isset($serverData['vpn_port']) && (int) $serverData['vpn_port'] > 0 // reusing a container name/port from another protocol on same server.
? (int) $serverData['vpn_port'] 'SERVER_CONTAINER' => $options['container_name']
: (isset($options['server_port']) ? (int) $options['server_port'] : ''), ?? ($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']
: ''),
]; ];
// Check for saved Reality keys in server_protocols table // Check for saved Reality keys in server_protocols table
@@ -876,7 +887,7 @@ class InstallProtocolManager
private static function parseWireGuardConfig(string $config): array private static function parseWireGuardConfig(string $config): array
{ {
$lines = preg_split('/\r?\n/', $config); $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 = []; $awgParams = [];
$listenPort = null; $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 private static function runBuiltinXrayAddClient(VpnServer $server, array $options): array
{ {
$clientId = $options['client_id'] ?? null; $clientId = $options['client_id'] ?? null;
+226 -6
View File
@@ -16,6 +16,8 @@ class ServerMonitoring
private array $serverData; private array $serverData;
private array $xrayStatsCache = []; private array $xrayStatsCache = [];
private bool $xrayStatsFetched = false; 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 * 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.
if (!$this->fetchXrayStats()) { // Otherwise we block AWG/WireGuard stats collection with irrelevant Xray errors.
error_log("Failed to fetch X-ray stats, preventing DB overwrite"); if ($this->isXrayServer()) {
return []; // Abort if stats collection failed if (!$this->fetchXrayStats()) {
error_log("Failed to fetch X-ray stats, preventing DB overwrite");
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']); $clients = VpnClient::listByServer($this->serverData['id']);
@@ -271,12 +283,16 @@ class ServerMonitoring
// Determine if this client is XRay based on protocol_id // Determine if this client is XRay based on protocol_id
$isXrayClient = false; $isXrayClient = false;
$protocolSlug = '';
if (!empty($client['protocol_id'])) { if (!empty($client['protocol_id'])) {
$stmtProto = $db->prepare('SELECT slug FROM protocols WHERE id = ?'); $stmtProto = $db->prepare('SELECT slug FROM protocols WHERE id = ?');
$stmtProto->execute([$client['protocol_id']]); $stmtProto->execute([$client['protocol_id']]);
$protoData = $stmtProto->fetch(); $protoData = $stmtProto->fetch();
if ($protoData && stripos($protoData['slug'], 'xray') !== false) { if ($protoData) {
$isXrayClient = true; $protocolSlug = (string) ($protoData['slug'] ?? '');
if (stripos($protocolSlug, 'xray') !== false) {
$isXrayClient = true;
}
} }
} }
@@ -285,6 +301,11 @@ class ServerMonitoring
$isXrayClient = true; $isXrayClient = true;
} }
$isAivpnClient = (
stripos($protocolSlug, 'aivpn') !== false ||
(!empty($client['config']) && strpos((string) $client['config'], 'aivpn://') === 0)
);
if ($isXrayClient) { if ($isXrayClient) {
// Retrieve DELTA from cache // Retrieve DELTA from cache
if ($this->xrayStatsFetched) { if ($this->xrayStatsFetched) {
@@ -330,6 +351,70 @@ class ServerMonitoring
} else { } else {
// WireGuard Logic - get bytes and handshake timestamp // WireGuard Logic - get bytes and handshake timestamp
$publicKey = $client['public_key']; $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): // 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 // $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) // rx-bytes = bytes received by server = client's upload (bytes_sent)
@@ -352,6 +437,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? // If we couldn't get stats (and they are 0), check if we have previous stats to avoid zeroing out if API fails?
@@ -590,6 +676,140 @@ class ServerMonitoring
return $this->getXrayContainerName() !== null; 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 * Enforce single IP per user for Xray connections
* If a user is connected from multiple IPs, block all but the first one * If a user is connected from multiple IPs, block all but the first one
+277 -17
View File
@@ -126,7 +126,7 @@ class VpnClient
} }
// Add AWG parameters (use UPPERCASE keys internal logic) // 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])) { if (isset($cleanAwgParams[$key])) {
$vars[$key] = $cleanAwgParams[$key]; $vars[$key] = $cleanAwgParams[$key];
} else { } else {
@@ -137,6 +137,8 @@ class VpnClient
'JMAX' => 200, 'JMAX' => 200,
'S1' => 50, 'S1' => 50,
'S2' => 100, 'S2' => 100,
'S3' => 20,
'S4' => 10,
'H1' => 1, 'H1' => 1,
'H2' => 2, 'H2' => 2,
'H3' => 3, 'H3' => 3,
@@ -213,7 +215,7 @@ class VpnClient
foreach ($extras as $k => $v) { foreach ($extras as $k => $v) {
if (is_scalar($v)) { if (is_scalar($v)) {
// Preserve uppercase for AWG obfuscation parameters // 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; $vars[$k] = (string) $v;
} else { } else {
$vars[strtolower($k)] = (string) $v; $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; $pass = null;
$pwdCmd = isset($protoRow['password_command']) ? trim((string) $protoRow['password_command']) : ''; $pwdCmd = isset($protoRow['password_command']) ? trim((string) $protoRow['password_command']) : '';
if ($pwdCmd !== '') { if ($pwdCmd !== '') {
@@ -418,13 +424,59 @@ class VpnClient
// For xray-vless it uses builtin fallback in runScript. // For xray-vless it uses builtin fallback in runScript.
try { try {
require_once __DIR__ . '/InstallProtocolManager.php'; 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) { } catch (Exception $e) {
error_log("Failed to add client to server: " . $e->getMessage()); error_log("Failed to add client to server: " . $e->getMessage());
throw $e; 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) : ''; $config = $protoRow ? ProtocolService::generateProtocolOutput($protoRow, $vars) : '';
// Prepare last_config_json for QR code generation if config is JSON (XRay) // Prepare last_config_json for QR code generation if config is JSON (XRay)
@@ -467,6 +519,46 @@ class VpnClient
return (int) $pdo->lastInsertId(); 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 public static function listByServerAndProtocol(int $serverId, int $protocolId): array
{ {
$pdo = DB::conn(); $pdo = DB::conn();
@@ -681,7 +773,7 @@ class VpnClient
$awgParams = []; $awgParams = [];
$awgLinesCmd = sprintf( $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($containerName),
escapeshellarg($confPath) escapeshellarg($confPath)
); );
@@ -692,7 +784,7 @@ class VpnClient
if ($line === '') { if ($line === '') {
continue; 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]); $k = strtoupper($m[1]);
$awgParams[$k] = (int) $m[2]; $awgParams[$k] = (int) $m[2];
} }
@@ -803,7 +895,7 @@ class VpnClient
// Legacy attempt: some builds print jc/jmin/... in `wg show` output. // Legacy attempt: some builds print jc/jmin/... in `wg show` output.
$wgShowCmd = "docker exec $containerName wg show wg0 2>/dev/null"; $wgShowCmd = "docker exec $containerName wg show wg0 2>/dev/null";
$wgOutput = (string) $server->executeCommand($wgShowCmd, true); $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) { foreach ($paramNames as $param) {
if (preg_match('/^\s*' . preg_quote($param, '/') . ':\s*(\d+)/mi', $wgOutput, $matches)) { if (preg_match('/^\s*' . preg_quote($param, '/') . ':\s*(\d+)/mi', $wgOutput, $matches)) {
$awgParams[strtoupper($param)] = (int) $matches[1]; $awgParams[strtoupper($param)] = (int) $matches[1];
@@ -862,7 +954,7 @@ class VpnClient
$config .= "DNS = 1.1.1.1, 1.0.0.1\n"; $config .= "DNS = 1.1.1.1, 1.0.0.1\n";
// Add AWG parameters // 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])) { if (isset($awgParams[$key])) {
$config .= "{$key} = {$awgParams[$key]}\n"; $config .= "{$key} = {$awgParams[$key]}\n";
continue; continue;
@@ -1370,7 +1462,7 @@ class VpnClient
// If AWG params are missing (common after reinstall), fetch them directly from wg0.conf // 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. // to avoid falling back to template defaults that will not match the server.
if (in_array($slug, ['amnezia-wg-advanced', 'awg2'], true)) { 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; $missing = false;
foreach ($needKeys as $k) { foreach ($needKeys as $k) {
if (!isset($awgParams[$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'), '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])) { if (isset($awgParams[$key])) {
$vars[$key] = $awgParams[$key]; $vars[$key] = $awgParams[$key];
} }
@@ -1574,7 +1666,7 @@ class VpnClient
try { try {
// Get previous stats for speed calculation // Get previous stats for speed calculation
$pdo = DB::conn(); $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]); $stmtPrev->execute([$this->clientId]);
$prev = $stmtPrev->fetch(); $prev = $stmtPrev->fetch();
@@ -1582,20 +1674,31 @@ class VpnClient
$prevReceived = (int) ($prev['bytes_received'] ?? 0); $prevReceived = (int) ($prev['bytes_received'] ?? 0);
$prevSyncAt = $prev['last_sync_at'] ? strtotime($prev['last_sync_at']) : 0; $prevSyncAt = $prev['last_sync_at'] ? strtotime($prev['last_sync_at']) : 0;
$prevHandshake = $prev['last_handshake'] ? strtotime($prev['last_handshake']) : 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 // XRay stats logic
$stats = []; $stats = [];
// Determine protocol by client's protocol_id // Determine protocol by client's protocol_id
$isXray = false; $isXray = false;
$isAivpn = false;
$xrayContainerName = 'amnezia-xray'; // Default XRay container name $xrayContainerName = 'amnezia-xray'; // Default XRay container name
if (!empty($this->data['protocol_id'])) { if (!empty($this->data['protocol_id'])) {
$stmtProto = $pdo->prepare('SELECT slug FROM protocols WHERE id = ?'); $stmtProto = $pdo->prepare('SELECT slug FROM protocols WHERE id = ?');
$stmtProto->execute([$this->data['protocol_id']]); $stmtProto->execute([$this->data['protocol_id']]);
$protoData = $stmtProto->fetch(); $protoData = $stmtProto->fetch();
if ($protoData && stripos($protoData['slug'], 'xray') !== false) { if ($protoData) {
$isXray = true; $slug = (string) ($protoData['slug'] ?? '');
if (stripos($slug, 'xray') !== false) {
$isXray = true;
}
if (stripos($slug, 'aivpn') !== false) {
$isAivpn = true;
}
} }
} }
@@ -1605,8 +1708,12 @@ class VpnClient
if (strpos($containerName, 'xray') !== false) { if (strpos($containerName, 'xray') !== false) {
$isXray = true; $isXray = true;
$xrayContainerName = $containerName; $xrayContainerName = $containerName;
} elseif (strpos($containerName, 'aivpn') !== false) {
$isAivpn = true;
} elseif (!empty($this->data['config']) && strpos($this->data['config'], 'vless://') !== false) { } elseif (!empty($this->data['config']) && strpos($this->data['config'], 'vless://') !== false) {
$isXray = true; $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)) { if (empty($stats)) {
@@ -1681,16 +1810,43 @@ class VpnClient
} }
} }
$stmt = $pdo->prepare(' $isAivpnPersist = $isAivpn && !empty($stats);
UPDATE vpn_clients if ($isAivpnPersist) {
SET bytes_sent = ?, bytes_received = ?, last_handshake = ?, current_speed = ?, speed_up = ?, speed_down = ?, last_sync_at = NOW() $stmt = $pdo->prepare('
WHERE id = ? 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 $lastHandshake = $stats['last_handshake'] > 0
? date('Y-m-d H:i:s', $stats['last_handshake']) ? date('Y-m-d H:i:s', $stats['last_handshake'])
: null; : 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([ return $stmt->execute([
$stats['bytes_sent'], $stats['bytes_sent'],
$stats['bytes_received'], $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 * Get client statistics from server
*/ */
+13
View File
@@ -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 -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); $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); sleep(2);
return [ 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
View File
@@ -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"
+1 -1
View File
@@ -155,7 +155,7 @@
{% if protocol_output and client.show_text_content %} {% if protocol_output and client.show_text_content %}
<div class="bg-white rounded shadow p-6 mt-6"> <div class="bg-white rounded shadow p-6 mt-6">
<h3 class="font-bold mb-4">{{ t('clients.connection_instructions') }}</h3> <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> </div>
{% endif %} {% endif %}
</div> </div>