Merge feature/ssh-auth-draft into master — release v2.0.0

This commit is contained in:
infosave2007
2026-04-04 18:13:12 +03:00
103 changed files with 21459 additions and 1157 deletions
+32
View File
@@ -32,6 +32,21 @@ test_qr.svg
*.swo
*~
# Local tooling / scratch
.trae/
scripts/_cycle_out/
# Local exported client configs (do not commit)
oleg*.conf
adminnew*.conf
________*.conf
*fixed*.conf
test_client_simple.conf
# Local QR exports / screenshots
*_QR.png
photo_*.jpeg
# Build artifacts
dist/
build/
@@ -42,3 +57,20 @@ backup/
backups/
TEST_RESULTS.md
LDAP_FEATURE.md
# Documentation and Tests
tests/
docs/
amnezia-web-panel.code-workspace
restore_local.php
test_protocols.php
scripts/regen_qr.php
scripts/test_xray_install.sh
scripts/test_online.php
API_AWG_DOCS.md
log.txt
scripts/bootstrap_awg_container.sh
scripts/fix_server_visibility.sh
scripts/remote_fix_client_create.sh
scripts/retest_client_api.sh
scripts/awg2_retest_final.sh
+28
View File
@@ -18,6 +18,34 @@ Response:
}
```
## Protocols
### List Active Protocols (for JWT API clients)
```bash
curl -X GET http://localhost:8082/api/protocols/active \
-H "Authorization: Bearer $TOKEN"
```
Example response:
```json
{
"success": true,
"protocols": [
{"id": 11, "slug": "awg2", "name": "AmneziaWG 2.0"},
{"id": 13, "slug": "aivpn", "name": "AIVPN"},
{"id": 12, "slug": "mtproxy", "name": "MTProxy (Telegram)"}
]
}
```
### Install Protocol on Server
```bash
curl -X POST http://localhost:8082/api/servers/1/protocols/install \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"protocol_id":11}'
```
## Clients
### Create Client with QR Code
+9
View File
@@ -14,6 +14,7 @@ RUN apt-get update && apt-get install -y \
qrencode \
cron \
libldap2-dev \
docker.io \
&& docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd ldap \
&& a2enmod rewrite \
@@ -56,6 +57,14 @@ RUN chmod +x /var/www/html/bin/monitor_metrics.sh
# Create startup script
RUN echo '#!/bin/bash\n\
service cron start\n\
# Ensure www-data can talk to host docker socket if mounted\n\
if [ -S /var/run/docker.sock ]; then\n\
SOCK_GID=$(stat -c %g /var/run/docker.sock)\n\
if ! getent group docker >/dev/null; then\n\
groupadd -g "$SOCK_GID" docker || true\n\
fi\n\
usermod -aG docker www-data || true\n\
fi\n\
# Start metrics collector on container startup\n\
/bin/bash /var/www/html/bin/monitor_metrics.sh\n\
apache2-foreground' > /start.sh \
+95 -5
View File
@@ -4,11 +4,15 @@ Web-based management panel for Amnezia AWG (WireGuard) VPN servers.
## Features
- VPN server deployment via SSH
- VPN server deployment via SSH (Password or **SSH Key**)
- **Import from existing VPN panels** (wg-easy, 3x-ui)
- **Advanced Protocol Management** (WireGuard, AmneziaWG, OpenVPN, Shadowsocks, etc.)
- **AI-powered Protocol Configuration** using OpenRouter (optional)
- Client configuration management with **expiration dates**
- **Traffic limits** for clients with automatic enforcement
- **Server backup and restore** functionality
- **Scenario Testing**: Define and test different VPN connection scenarios across protocols
- **Advanced Log Management**: View, search, and manage system and container logs
- Traffic statistics monitoring
- QR code generation for mobile apps
- Multi-language interface (English, Russian, Spanish, German, French, Chinese)
@@ -16,6 +20,19 @@ Web-based management panel for Amnezia AWG (WireGuard) VPN servers.
- User authentication and access control
- **Automatic client expiration and traffic limit checks** via cron
## Available Protocols
- AmneziaWG Advanced (`amnezia-wg-advanced`)
- AmneziaWG 2.0 (`awg2`)
- WireGuard Standard (`wireguard-standard`)
- OpenVPN (`openvpn`)
- Shadowsocks (`shadowsocks`)
- XRay VLESS (`xray-vless`)
- MTProxy (Telegram) (`mtproxy`)
- SMB Server (`smb`)
- AIVPN (`aivpn`) - https://github.com/infosave2007/aivpn
## Requirements
- Docker
@@ -32,15 +49,50 @@ cp .env.example .env
docker compose up -d
docker compose exec web composer install
# Apply migrations (fresh install + updates)
# 1) bootstrap base schema
docker compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < migrations/001_init.sql
# 2) apply the rest (safe to run repeatedly)
for f in migrations/*.sql; do
[ "$(basename "$f")" = "001_init.sql" ] && continue
docker compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < "$f" || true
done
# Or for older Docker Compose V1
docker-compose up -d
docker-compose exec web composer install
docker-compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < migrations/001_init.sql
for f in migrations/*.sql; do
[ "$(basename "$f")" = "001_init.sql" ] && continue
docker-compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < "$f" || true
done
```
Access: http://localhost:8082
Default login: admin@amnez.ia / admin123
### Remote Server Prerequisite
For protocol deployment on a clean remote host, Docker Engine must be available on that host.
If Docker is missing, install it first (Ubuntu example):
```bash
apt-get update -y
apt-get install -y ca-certificates curl gnupg lsb-release
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
apt-get update -y
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
```
## Configuration
Edit `.env`:
@@ -50,7 +102,7 @@ DB_HOST=db
DB_PORT=3306
DB_DATABASE=amnezia_panel
DB_USERNAME=amnezia
DB_PASSWORD=amnezia123
DB_PASSWORD=amnezia
ADMIN_EMAIL=admin@amnez.ia
ADMIN_PASSWORD=admin123
@@ -63,7 +115,10 @@ JWT_SECRET=your-secret-key-change-this
### Add VPN Server
1. Servers → Add Server
2. Enter: name, host IP, SSH port, username, password
1. Servers → Add Server
2. Enter: name, host IP, SSH port, username
3. Choose authentication method: **Password** or **SSH Key**
- For SSH Key: Paste your private key (PEM/OpenSSH format)
3. **(Optional) Enable import from existing panel:**
- Check "Import from existing panel"
- Select panel type (wg-easy or 3x-ui)
@@ -141,6 +196,30 @@ curl -X POST http://localhost:8082/api/servers/1/restore \
-d '{"backup_id": 123}'
```
### Protocol Management
Manage VPN protocols via **Settings → Protocols**:
- Install/Uninstall protocols (WireGuard, AmneziaWG, OpenVPN, etc.)
- Configure protocol settings (ports, transport, obfuscation)
- **AI Assistant**: Use "Ask AI" to generate complex protocol configurations tailored to your needs (requires OpenRouter API key).
### Scenario Testing & Logs
**Scenario Testing**:
- Create test scenarios to verify connectivity across different protocols and network conditions.
- Run automated tests to ensure your VPN infrastructure is reliable.
**Log Management**:
- Centralized view of all system, container, and application logs.
- Search and filter capabilities to quickly diagnose issues.
### AI Assistant
Configure OpenRouter API key in **Settings** to enable:
- Auto-translation of the interface
- AI-assisted protocol configuration
- Intelligent troubleshooting suggestions
### Automatic Monitoring and Metrics Collection
**Metrics collector runs automatically** on container startup and is monitored by cron every 3 minutes. If the process crashes, it will be automatically restarted.
@@ -222,16 +301,25 @@ DELETE /api/servers/{id}/delete - Delete server by ID
GET /api/servers/{id}/clients - List clients on server
```
### Protocols
```
GET /api/protocols/active - List all available protocols (JWT-friendly, includes protocol IDs)
GET /api/protocols - Protocol management endpoint (requires session admin auth, not JWT)
GET /api/servers/{id}/protocols - List installed protocols on server
POST /api/servers/{id}/protocols/install - Install protocol
```
### Clients
```
GET /api/clients - List all clients
GET /api/clients/{id}/details - Get client details with stats, config and QR code
GET /api/clients/{id}/qr - Get client QR code
POST /api/clients/create - Create new client (returns config and QR code)
Parameters: server_id, name, expires_in_days (optional)
Parameters: server_id, name, protocol_id (optional, default: installed), expires_in_days (optional)
POST /api/clients/{id}/revoke - Revoke client access
POST /api/clients/{id}/restore - Restore client access
DELETE /api/clients/{id}/delete - Delete client by ID
DELETE /api/clients/{id}/delete - Delete client by ID (removes from DB and server)
POST /api/clients/{id}/set-expiration - Set client expiration date
POST /api/clients/{id}/set-expiration - Set client expiration date
Parameters: expires_at (Y-m-d H:i:s or null)
POST /api/clients/{id}/extend - Extend client expiration
@@ -284,6 +372,8 @@ inc/ - Core classes
JWT.php - Token auth
QrUtil.php - QR code generation
PanelImporter.php - Import from wg-easy/3x-ui
InstallProtocolManager.php - Protocol management core
OpenRouterService.php - AI integration
templates/ - Twig templates
migrations/ - SQL migrations (executed in alphabetical order)
```
+12
View File
@@ -10,6 +10,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../inc/Config.php';
Config::load(__DIR__ . '/../.env');
require_once __DIR__ . '/../inc/DB.php';
require_once __DIR__ . '/../inc/VpnServer.php';
require_once __DIR__ . '/../inc/VpnClient.php';
@@ -55,6 +56,17 @@ while (true) {
$monitoring = new ServerMonitoring($server['id']);
// Enforce single IP per user for Xray servers
$containerName = $server['container_name'] ?? '';
if (strpos($containerName, 'xray') !== false) {
$monitoring->enforceXraySingleIpPerUser();
}
// Enforce single IP per peer for AWG servers
if (strpos($containerName, 'awg') !== false || strpos($containerName, 'wireguard') !== false) {
$monitoring->enforceAwgSingleIpPerPeer();
}
// Collect server metrics
$serverMetrics = $monitoring->collectMetrics();
echo " Server: CPU={$serverMetrics['cpu_percent']}% RAM={$serverMetrics['ram_used_mb']}/{$serverMetrics['ram_total_mb']}MB ";
+378
View File
@@ -0,0 +1,378 @@
<?php
class AIController
{
private $openRouterService;
public function __construct()
{
$this->openRouterService = new OpenRouterService();
}
/**
* AI Assistant endpoint for generating installation scripts
*/
public function assist(): void
{
requireAdmin();
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Invalid JSON input');
}
$prompt = trim($input['prompt'] ?? '');
$model = trim($input['model'] ?? 'openai/gpt-3.5-turbo');
$protocolType = trim($input['protocol_type'] ?? '');
$protocolId = isset($input['protocol_id']) ? (int) $input['protocol_id'] : null;
$target = trim($input['target'] ?? 'install'); // install, uninstall, template
if (empty($prompt)) {
throw new Exception('Prompt is required');
}
// Generate enhanced prompt based on protocol type and target
$enhancedPrompt = $this->enhancePrompt($prompt, $protocolType, $target);
// Call OpenRouter API
$response = $this->openRouterService->generateScript($enhancedPrompt, $model);
// Save AI generation to database
$generationId = $this->saveAIGeneration([
'protocol_id' => $protocolId,
'model_used' => $model,
'prompt' => "[$target] " . $prompt,
'generated_script' => $response['script'] ?? '',
'suggestions' => json_encode($response['suggestions'] ?? []),
'ubuntu_compatible' => $response['ubuntu_compatible'] ?? null,
'created_at' => date('Y-m-d H:i:s')
]);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'data' => [
'script' => $response['script'] ?? '',
'suggestions' => $response['suggestions'] ?? [],
'ubuntu_compatible' => $response['ubuntu_compatible'] ?? false,
'estimated_time' => $response['estimated_time'] ?? '5 minutes',
'model_used' => $model,
'generation_id' => $generationId,
'target' => $target
]
]);
} catch (Exception $e) {
error_log("Error in AIController::assist: " . $e->getMessage());
header('Content-Type: application/json');
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
/**
* Get available AI models
*/
public function getModels(): void
{
requireAdmin();
try {
$models = $this->openRouterService->getAvailableModels();
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'data' => $models
]);
} catch (Exception $e) {
error_log("Error in AIController::getModels: " . $e->getMessage());
header('Content-Type: application/json');
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
public function testModel(): void
{
requireAdmin();
header('Content-Type: application/json');
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Invalid JSON input');
}
$model = trim($input['model'] ?? '');
if ($model === '') {
throw new Exception('Model id is required');
}
$result = $this->openRouterService->testModelAvailability($model);
echo json_encode([
'success' => $result['success'] ?? false,
'message' => $result['message'] ?? null,
'http_code' => $result['http_code'] ?? null
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
/**
* Get AI generation history for a protocol
*/
public function getGenerationHistory(int $protocolId): void
{
requireAdmin();
try {
$generations = $this->getAIGenerationsByProtocol($protocolId);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'data' => $generations
]);
} catch (Exception $e) {
error_log("Error in AIController::getGenerationHistory: " . $e->getMessage());
header('Content-Type: application/json');
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
/**
* Apply AI-generated script to protocol
*/
public function applyGeneration(int $generationId): void
{
requireAdmin();
try {
$generation = $this->getAIGeneration($generationId);
if (!$generation) {
throw new Exception('AI generation not found');
}
if (!$generation['protocol_id']) {
throw new Exception('This generation is not associated with any protocol');
}
// Determine target from prompt
$target = 'install';
if (preg_match('/^\[(uninstall|template)\]/', $generation['prompt'], $matches)) {
$target = $matches[1];
}
// Update protocol with generated script
$pdo = DB::conn();
if ($target === 'uninstall') {
$stmt = $pdo->prepare('
UPDATE protocols
SET uninstall_script = ?, updated_at = ?
WHERE id = ?
');
$stmt->execute([
$generation['generated_script'],
date('Y-m-d H:i:s'),
$generation['protocol_id']
]);
} elseif ($target === 'template') {
$stmt = $pdo->prepare('
UPDATE protocols
SET output_template = ?, updated_at = ?
WHERE id = ?
');
$stmt->execute([
$generation['generated_script'],
date('Y-m-d H:i:s'),
$generation['protocol_id']
]);
} else {
$stmt = $pdo->prepare('
UPDATE protocols
SET install_script = ?, ubuntu_compatible = ?, updated_at = ?
WHERE id = ?
');
$stmt->execute([
$generation['generated_script'],
$generation['ubuntu_compatible'],
date('Y-m-d H:i:s'),
$generation['protocol_id']
]);
}
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'message' => 'AI-generated script applied to protocol successfully'
]);
} catch (Exception $e) {
error_log("Error in AIController::applyGeneration: " . $e->getMessage());
header('Content-Type: application/json');
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
/**
* Preview AI-generated script with syntax highlighting
*/
public function previewGeneration(int $generationId): void
{
requireAdmin();
try {
$generation = $this->getAIGeneration($generationId);
if (!$generation) {
throw new Exception('AI generation not found');
}
View::render('ai/preview_generation.twig', [
'generation' => $generation,
'script' => $generation['generated_script'],
'suggestions' => json_decode($generation['suggestions'], true) ?? []
]);
} catch (Exception $e) {
$_SESSION['protocol_error'] = $e->getMessage();
redirect('/settings/protocols');
}
}
/**
* Enhance user prompt with context and requirements
*/
private function enhancePrompt(string $prompt, string $protocolType, string $target = 'install'): string
{
$context = "";
if ($target === 'uninstall') {
$context = "Create a bash uninstallation script for a VPN protocol. ";
} elseif ($target === 'template') {
$context = "Create a WireGuard-compatible client config template. ";
} else {
$context = "Create a bash installation script for a VPN protocol. ";
}
if ($protocolType) {
$context .= "This is for a $protocolType protocol. ";
}
$context .= "Requirements:\n";
if ($target === 'template') {
$context .= "- Use Mustache-style placeholders like {{private_key}}, {{client_ip}}, {{server_host}}, {{server_port}}\n";
$context .= "- The template should be valid configuration format (e.g. .conf for WireGuard)\n";
} elseif ($target === 'uninstall') {
$context .= "- The script should be compatible with Ubuntu 22.04 and 24.04\n";
$context .= "- Remove all docker containers, images, and networks created by the installation\n";
$context .= "- Remove configuration files and directories\n";
$context .= "- Clean up firewall rules if necessary\n";
} else {
$context .= "- The script should be compatible with Ubuntu 22.04 and 24.04\n";
$context .= "- Use Docker containers where possible for isolation\n";
$context .= "- Generate necessary keys and certificates automatically\n";
$context .= "- Configure appropriate firewall rules\n";
$context .= "- Provide clear output about installation progress\n";
$context .= "- Handle errors gracefully\n";
$context .= "- Include configuration validation\n";
}
$context .= "\nUser requirements: $prompt\n";
$context .= "\nReturn the response in this JSON format:\n";
$context .= "{\n";
if ($target === 'template') {
$context .= ' "script": "[Interface]\\nPrivateKey = {{private_key}}...",' . "\n";
} else {
$context .= ' "script": "#!/bin/bash\\n# Complete script",' . "\n";
}
$context .= ' "suggestions": ["suggestion 1", "suggestion 2"],' . "\n";
$context .= ' "ubuntu_compatible": true,' . "\n";
$context .= ' "estimated_time": "5 minutes"' . "\n";
$context .= "}\n";
return $context;
}
/**
* Database methods
*/
private function saveAIGeneration(array $data): int
{
$pdo = DB::conn();
$stmt = $pdo->prepare('
INSERT INTO ai_generations (protocol_id, model_used, prompt, generated_script, suggestions, ubuntu_compatible, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
');
$stmt->execute([
$data['protocol_id'],
$data['model_used'],
$data['prompt'],
$data['generated_script'],
$data['suggestions'],
$data['ubuntu_compatible'],
$data['created_at']
]);
return (int) $pdo->lastInsertId();
}
private function getAIGeneration(int $id): ?array
{
$pdo = DB::conn();
$stmt = $pdo->prepare('
SELECT ag.*, p.name as protocol_name, p.slug as protocol_slug
FROM ai_generations ag
LEFT JOIN protocols p ON ag.protocol_id = p.id
WHERE ag.id = ?
');
$stmt->execute([$id]);
$generation = $stmt->fetch(PDO::FETCH_ASSOC);
return $generation ?: null;
}
private function getAIGenerationsByProtocol(int $protocolId): array
{
$pdo = DB::conn();
$stmt = $pdo->prepare('
SELECT ag.*, p.name as protocol_name
FROM ai_generations ag
LEFT JOIN protocols p ON ag.protocol_id = p.id
WHERE ag.protocol_id = ?
ORDER BY ag.created_at DESC
LIMIT 50
');
$stmt->execute([$protocolId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
+408
View File
@@ -0,0 +1,408 @@
<?php
/**
* LogsController
* Manages application logs viewing and management
* Admin-only access to application log files
*/
class LogsController {
private const LOGS_DIR = __DIR__ . '/../logs';
private const MAX_FILE_SIZE = 10485760; // 10 MB
private const ALLOWED_EXTENSIONS = ['log', 'txt'];
/**
* List and view application logs
* GET /admin/logs
*/
public function index() {
requireAdmin();
$logFiles = $this->getLogFiles();
$selectedFile = $_GET['file'] ?? null;
$logContent = '';
$logLines = [];
$fileSize = 0;
$lineCount = 0;
if ($selectedFile && $this->isValidLogFile($selectedFile)) {
$filePath = self::LOGS_DIR . '/' . basename($selectedFile);
if (file_exists($filePath)) {
$fileSize = filesize($filePath);
// Read log file (last 1000 lines or complete if small)
$logContent = $this->readLogFile($filePath);
$logLines = array_filter(explode("\n", $logContent));
$lineCount = count($logLines);
}
}
View::render('settings/logs.twig', [
'log_files' => $logFiles,
'selected_file' => $selectedFile,
'log_content' => $logContent,
'log_lines' => $logLines,
'line_count' => $lineCount,
'file_size' => $fileSize,
'section' => 'logs'
]);
}
/**
* Get list of available log files
*/
private function getLogFiles(): array {
$files = [];
if (!is_dir(self::LOGS_DIR)) {
return $files;
}
$dirContents = @scandir(self::LOGS_DIR, SCANDIR_SORT_DESCENDING);
if ($dirContents === false) {
return $files;
}
foreach ($dirContents as $filename) {
// Skip . and ..
if ($filename === '.' || $filename === '..') {
continue;
}
$filePath = self::LOGS_DIR . '/' . $filename;
// Only regular files
if (!is_file($filePath)) {
continue;
}
// Check extension
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
continue;
}
$size = filesize($filePath);
$modified = filemtime($filePath);
$files[] = [
'name' => $filename,
'path' => $filename,
'size' => $size,
'size_formatted' => $this->formatBytes($size),
'modified' => $modified,
'modified_formatted' => date('Y-m-d H:i:s', $modified),
'readable' => is_readable($filePath)
];
}
return $files;
}
/**
* Read log file content (last N lines)
*/
private function readLogFile(string $filePath, int $maxLines = 1000): string {
if (!is_readable($filePath)) {
return '';
}
$fileSize = filesize($filePath);
// If file is small enough, read completely
if ($fileSize <= self::MAX_FILE_SIZE) {
return file_get_contents($filePath) ?: '';
}
// For large files, read last N lines
$handle = fopen($filePath, 'r');
if ($handle === false) {
return '';
}
// Seek to end and read backwards
fseek($handle, 0, SEEK_END);
$lines = [];
$lineCount = 0;
while ($lineCount < $maxLines && ftell($handle) > 0) {
$chunk = '';
$pos = ftell($handle);
// Read backwards in chunks
$chunkSize = min(4096, $pos);
fseek($handle, -$chunkSize, SEEK_CUR);
$chunk = fread($handle, $chunkSize);
fseek($handle, -$chunkSize, SEEK_CUR);
$parts = explode("\n", $chunk);
$lines = array_merge($parts, $lines);
$lineCount = count($lines);
if ($pos <= $chunkSize) {
break;
}
}
fclose($handle);
// Take last N lines and rejoin
$lines = array_slice($lines, -$maxLines);
return implode("\n", $lines);
}
/**
* Download log file
* GET /admin/logs/download?file=filename
*/
public function download() {
requireAdmin();
$file = $_GET['file'] ?? null;
if (!$file || !$this->isValidLogFile($file)) {
http_response_code(400);
echo 'Invalid file';
return;
}
$filePath = self::LOGS_DIR . '/' . basename($file);
if (!file_exists($filePath) || !is_readable($filePath)) {
http_response_code(404);
echo 'File not found';
return;
}
header('Content-Type: text/plain; charset=utf-8');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Content-Length: ' . filesize($filePath));
readfile($filePath);
exit;
}
/**
* Delete log file
* POST /admin/logs/delete
*/
public function delete() {
requireAdmin();
header('Content-Type: application/json');
$file = $_POST['file'] ?? null;
if (!$file || !$this->isValidLogFile($file)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Invalid file']);
return;
}
$filePath = self::LOGS_DIR . '/' . basename($file);
if (!file_exists($filePath)) {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'File not found']);
return;
}
if (@unlink($filePath)) {
echo json_encode([
'success' => true,
'message' => 'Log file deleted successfully',
'redirect' => '/admin/logs'
]);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to delete file']);
}
}
/**
* Clear all log files
* POST /admin/logs/clear-all
*/
public function clearAll() {
requireAdmin();
header('Content-Type: application/json');
$logFiles = $this->getLogFiles();
$deleted = 0;
$failed = 0;
foreach ($logFiles as $file) {
$filePath = self::LOGS_DIR . '/' . $file['path'];
if (@unlink($filePath)) {
$deleted++;
} else {
$failed++;
}
}
echo json_encode([
'success' => true,
'deleted' => $deleted,
'failed' => $failed,
'message' => "Deleted $deleted log files" . ($failed > 0 ? ", failed to delete $failed" : ''),
'redirect' => '/admin/logs'
]);
}
/**
* Search logs
* POST /admin/logs/search
*/
public function search() {
requireAdmin();
header('Content-Type: application/json');
$query = $_POST['query'] ?? '';
$file = $_POST['file'] ?? null;
$caseSensitive = isset($_POST['case_sensitive']) && $_POST['case_sensitive'] === 'on';
if (empty($query) || strlen($query) < 2) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Search query too short']);
return;
}
if (!$file || !$this->isValidLogFile($file)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Invalid file']);
return;
}
$filePath = self::LOGS_DIR . '/' . basename($file);
if (!file_exists($filePath) || !is_readable($filePath)) {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'File not found']);
return;
}
$content = file_get_contents($filePath);
if ($content === false) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to read file']);
return;
}
$lines = explode("\n", $content);
$results = [];
$flags = $caseSensitive ? 0 : PREG_GREP_INVERT;
foreach ($lines as $lineNum => $line) {
if (empty($line)) {
continue;
}
// Case-sensitive or case-insensitive search
$matches = $caseSensitive
? (strpos($line, $query) !== false)
: (stripos($line, $query) !== false);
if ($matches) {
$results[] = [
'line' => $lineNum + 1,
'content' => $line
];
}
}
echo json_encode([
'success' => true,
'query' => $query,
'results_count' => count($results),
'results' => array_slice($results, 0, 100) // Limit to 100 results
]);
}
/**
* Get log statistics
* POST /admin/logs/stats
*/
public function stats() {
requireAdmin();
header('Content-Type: application/json');
$file = $_POST['file'] ?? null;
if (!$file || !$this->isValidLogFile($file)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Invalid file']);
return;
}
$filePath = self::LOGS_DIR . '/' . basename($file);
if (!file_exists($filePath) || !is_readable($filePath)) {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'File not found']);
return;
}
$content = file_get_contents($filePath);
if ($content === false) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to read file']);
return;
}
$lines = array_filter(explode("\n", $content));
$errorCount = count(preg_grep('/error|exception|fatal|fail/i', $lines));
$warningCount = count(preg_grep('/warning|warn/i', $lines));
$successCount = count(preg_grep('/success|completed|ok/i', $lines));
echo json_encode([
'success' => true,
'total_lines' => count($lines),
'errors' => $errorCount,
'warnings' => $warningCount,
'success' => $successCount,
'file_size' => filesize($filePath),
'file_size_formatted' => $this->formatBytes(filesize($filePath)),
'last_modified' => date('Y-m-d H:i:s', filemtime($filePath))
]);
}
/**
* Validate log file name
*/
private function isValidLogFile(string $filename): bool {
// Prevent directory traversal
if (strpos($filename, '/') !== false || strpos($filename, '\\') !== false) {
return false;
}
if (strpos($filename, '..') !== false) {
return false;
}
// Check extension
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
return false;
}
return true;
}
/**
* Format bytes to human readable
*/
private function formatBytes(int $bytes, int $precision = 2): string {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
}
@@ -0,0 +1,955 @@
<?php
class ProtocolManagementController
{
/**
* Display protocols list and management interface
*/
public function index(): void
{
requireAdmin();
try {
$protocols = $this->getAllProtocols();
$selectedId = isset($_GET['id']) ? (int) $_GET['id'] : null;
$isNew = isset($_GET['new']);
$isTemplate = isset($_GET['template']);
$editing = null;
if (!$isNew && $selectedId) {
$editing = $this->getProtocolById($selectedId);
}
$pdo = DB::conn();
$stmt = $pdo->prepare("SELECT api_key FROM api_keys WHERE service_name = 'openrouter' AND is_active = 1 LIMIT 1");
$stmt->execute();
$openrouterKey = $stmt->fetchColumn() ?: '';
if ($isTemplate && $editing) {
View::render('settings/protocol_template_editor.twig', [
'protocol' => $editing,
'success' => $_SESSION['protocol_success'] ?? null,
'error' => $_SESSION['protocol_error'] ?? null,
'openrouter_key' => $openrouterKey,
]);
} elseif ($editing || $isNew) {
View::render('settings/protocol_form.twig', [
'editing' => $editing,
'success' => $_SESSION['protocol_success'] ?? null,
'error' => $_SESSION['protocol_error'] ?? null,
'openrouter_key' => $openrouterKey,
]);
} else {
View::render('settings/protocols_management.twig', [
'protocols' => $protocols,
'success' => $_SESSION['protocol_success'] ?? null,
'error' => $_SESSION['protocol_error'] ?? null,
'openrouter_key' => $openrouterKey,
]);
}
unset($_SESSION['protocol_success'], $_SESSION['protocol_error']);
} catch (Exception $e) {
error_log("Error in ProtocolManagementController::index: " . $e->getMessage());
$_SESSION['protocol_error'] = 'Failed to load protocols: ' . $e->getMessage();
redirect('/settings/protocols-management');
}
}
/**
* Create or update protocol
*/
public function save(): void
{
requireAdmin();
try {
$id = isset($_POST['id']) && $_POST['id'] !== '' ? (int) $_POST['id'] : null;
$name = trim($_POST['name'] ?? '');
$slug = trim($_POST['slug'] ?? '');
$description = trim($_POST['description'] ?? '');
$installScript = trim($_POST['install_script'] ?? '');
$passwordCommand = trim($_POST['password_command'] ?? '');
$uninstallScript = trim($_POST['uninstall_script'] ?? '');
$outputTemplate = trim($_POST['output_template'] ?? '');
$qrCodeTemplate = trim($_POST['qr_code_template'] ?? '');
$qrCodeFormat = trim($_POST['qr_code_format'] ?? 'amnezia_compressed');
$ubuntuCompatible = isset($_POST['ubuntu_compatible']) ? 1 : 0;
$showTextContent = isset($_POST['show_text_content']) ? 1 : 0;
$isActive = isset($_POST['is_active']) ? 1 : 0;
// Validation
if ($name === '' || $slug === '') {
throw new Exception('Name and slug are required');
}
if (!preg_match('/^[a-z0-9_-]+$/i', $slug)) {
throw new Exception('Slug may contain only letters, numbers, dashes, and underscores');
}
// Check if slug is unique (for new protocols or when updating slug)
if ($this->isSlugExists($slug, $id)) {
throw new Exception('Protocol with this slug already exists');
}
$protocolData = [
'name' => $name,
'slug' => $slug,
'description' => $description,
'install_script' => $installScript,
'output_template' => $outputTemplate,
'qr_code_template' => $qrCodeTemplate,
'qr_code_format' => $qrCodeFormat,
'password_command' => $passwordCommand,
'uninstall_script' => $uninstallScript,
'ubuntu_compatible' => $ubuntuCompatible,
'show_text_content' => $showTextContent,
'is_active' => $isActive,
'updated_at' => date('Y-m-d H:i:s')
];
if ($id) {
// Update existing protocol
$this->updateProtocol($id, $protocolData);
$savedId = $id;
$_SESSION['protocol_success'] = 'Protocol updated successfully';
} else {
// Create new protocol
$protocolData['created_at'] = date('Y-m-d H:i:s');
$savedId = $this->createProtocol($protocolData);
$_SESSION['protocol_success'] = 'Protocol created successfully';
}
redirect('/settings/protocols-management?id=' . $savedId);
} catch (Exception $e) {
$_SESSION['protocol_error'] = $e->getMessage();
$id = isset($_POST['id']) ? (int) $_POST['id'] : null;
redirect('/settings/protocols-management' . ($id ? '?id=' . $id : '?new=1'));
}
}
/**
* Delete protocol
*/
public function delete(int $id): void
{
requireAdmin();
try {
$protocol = $this->getProtocolById($id);
if (!$protocol) {
throw new Exception('Protocol not found');
}
// Check if protocol is used by any servers
if ($this->isProtocolUsed($id)) {
throw new Exception('Cannot delete protocol that is currently used by servers');
}
$this->deleteProtocol($id);
$_SESSION['protocol_success'] = 'Protocol deleted successfully';
} catch (Exception $e) {
$_SESSION['protocol_error'] = $e->getMessage();
}
redirect('/settings/protocols-management');
}
/**
* API endpoint: Get all protocols (JSON)
*/
public function apiGetProtocols(): void
{
requireAdmin();
try {
$protocols = $this->getAllProtocols();
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'data' => $protocols
]);
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
/**
* API endpoint: Get single protocol (JSON)
*/
public function apiGetProtocol(int $id): void
{
requireAdmin();
try {
$protocol = $this->getProtocolById($id);
if (!$protocol) {
throw new Exception('Protocol not found');
}
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'data' => $protocol
]);
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(404);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
/**
* API endpoint: Create protocol (JSON)
*/
public function apiCreateProtocol(): void
{
requireAdmin();
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Invalid JSON input');
}
// Validate required fields
$requiredFields = ['name', 'slug'];
foreach ($requiredFields as $field) {
if (empty($input[$field])) {
throw new Exception("Field '$field' is required");
}
}
// Validate slug format
if (!preg_match('/^[a-z0-9_-]+$/i', $input['slug'])) {
throw new Exception('Slug may contain only letters, numbers, dashes, and underscores');
}
// Check if slug exists
if ($this->isSlugExists($input['slug'])) {
throw new Exception('Protocol with this slug already exists');
}
$protocolData = [
'name' => trim($input['name']),
'slug' => trim($input['slug']),
'description' => trim($input['description'] ?? ''),
'install_script' => trim($input['install_script'] ?? ''),
'output_template' => trim($input['output_template'] ?? ''),
'qr_code_template' => trim($input['qr_code_template'] ?? ''),
'qr_code_format' => trim($input['qr_code_format'] ?? 'amnezia_compressed'),
'ubuntu_compatible' => (bool) ($input['ubuntu_compatible'] ?? false),
'show_text_content' => (bool) ($input['show_text_content'] ?? false),
'is_active' => (bool) ($input['is_active'] ?? true),
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
$id = $this->createProtocol($protocolData);
$protocol = $this->getProtocolById($id);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'data' => $protocol,
'message' => 'Protocol created successfully'
]);
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
/**
* API endpoint: Update protocol (JSON)
*/
public function apiUpdateProtocol(int $id): void
{
requireAdmin();
try {
$protocol = $this->getProtocolById($id);
if (!$protocol) {
throw new Exception('Protocol not found');
}
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Invalid JSON input');
}
$protocolData = [];
$allowedFields = ['name', 'slug', 'description', 'install_script', 'output_template', 'qr_code_template', 'qr_code_format', 'ubuntu_compatible', 'show_text_content', 'is_active'];
foreach ($allowedFields as $field) {
if (isset($input[$field])) {
if ($field === 'ubuntu_compatible' || $field === 'is_active') {
$protocolData[$field] = (bool) $input[$field];
} elseif ($field === 'slug') {
$slug = trim($input[$field]);
if (!preg_match('/^[a-z0-9_-]+$/i', $slug)) {
throw new Exception('Slug may contain only letters, numbers, dashes, and underscores');
}
if ($this->isSlugExists($slug, $id)) {
throw new Exception('Protocol with this slug already exists');
}
$protocolData[$field] = $slug;
} else {
$protocolData[$field] = trim($input[$field]);
}
}
}
if (!empty($protocolData)) {
$protocolData['updated_at'] = date('Y-m-d H:i:s');
$this->updateProtocol($id, $protocolData);
$protocol = $this->getProtocolById($id);
}
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'data' => $protocol,
'message' => 'Protocol updated successfully'
]);
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
/**
* API endpoint: Delete protocol (JSON)
*/
public function apiDeleteProtocol(int $id): void
{
requireAdmin();
try {
$protocol = $this->getProtocolById($id);
if (!$protocol) {
throw new Exception('Protocol not found');
}
if ($this->isProtocolUsed($id)) {
throw new Exception('Cannot delete protocol that is currently used by servers');
}
$this->deleteProtocol($id);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'message' => 'Protocol deleted successfully'
]);
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
public function apiTestInstallProtocol(int $id): void
{
requireAdmin();
// Suppress all errors and warnings to prevent HTML output before JSON
@ini_set('display_errors', '0');
error_reporting(0);
// Clean any previous output
if (ob_get_level())
ob_end_clean();
ob_start();
header('Content-Type: application/json');
try {
$protocol = $this->getProtocolById($id);
if (!$protocol) {
throw new Exception('Protocol not found');
}
$script = trim($protocol['install_script'] ?? '');
if ($script === '') {
throw new Exception('Install script is empty');
}
$container = 'proto-test-' . $id;
$this->runHostCommand('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
$run = $this->runHostCommandChecked('docker run --privileged -d -v /var/run/docker.sock:/var/run/docker.sock --name ' . escapeshellarg($container) . ' ubuntu:22.04 sleep infinity');
if ($run['rc'] !== 0) {
throw new Exception('Docker not accessible: ' . trim($run['out']));
}
$cliPath = '/usr/local/bin/docker';
$try1 = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '"');
if ($try1['rc'] !== 0 || $try1['out'] === '') {
$cliPath = '/usr/bin/docker';
$try2 = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '"');
if ($try2['rc'] !== 0 || $try2['out'] === '') {
throw new Exception('Failed to read docker CLI from docker:24-dind image');
}
}
$cp = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '" | docker exec -i ' . escapeshellarg($container) . ' sh -lc "cat > /usr/local/bin/docker && chmod +x /usr/local/bin/docker"');
if ($cp['rc'] !== 0) {
throw new Exception('Failed to provide docker CLI to test container: ' . trim($cp['out']));
}
$this->execInContainerChecked($container, 'chmod +x /usr/local/bin/docker');
$ver = $this->execInContainerChecked($container, 'docker --version');
if ($ver['rc'] !== 0) {
throw new Exception('Docker CLI not available in test container');
}
$prelude = <<<'SH'
set -euo pipefail
set -x
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
wg() {
if docker ps --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then
docker exec -i "$CONTAINER_NAME" wg "$@"
else
docker pull -q amneziavpn/amnezia-wg:latest >/dev/null 2>&1 || true
docker run --rm -i --privileged --cap-add=NET_ADMIN amneziavpn/amnezia-wg:latest wg "$@"
fi
}
SH;
$wrapped = $prelude . "\n" . $script;
$runScript = $this->execInContainerChecked($container, $wrapped);
if ($runScript['rc'] !== 0) {
throw new Exception("Install script failed: " . trim($runScript['out']));
}
$output = $runScript['out'];
$extracted = $this->extractValuesFromOutput($output);
$variables = $this->getProtocolVariables($id);
foreach ($extracted as $k => $v) {
if (array_key_exists($k, $variables)) {
$variables[$k] = $v;
}
}
$preview = ProtocolService::generateProtocolOutput($protocol, $variables);
// Cleanup test containers: proto-test and AWG if created
$this->runHostCommand('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
$this->runHostCommand('docker rm -f amnezia-awg >/dev/null 2>&1 || true');
// Clean buffer and output JSON
if (ob_get_level())
ob_end_clean();
echo json_encode([
'success' => true,
'logs' => $output,
'extracted' => $extracted,
'preview' => $preview
]);
} catch (Exception $e) {
// Clean buffer and output error JSON
if (ob_get_level())
ob_end_clean();
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
}
public function apiTestInstallProtocolStream(int $id): void
{
requireAdmin();
// Suppress all errors and warnings to prevent HTML output
@ini_set('display_errors', '0');
error_reporting(0);
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
@ini_set('output_buffering', 'off');
@ini_set('zlib.output_compression', '0');
@ob_implicit_flush(true);
// CRITICAL: Use ob_end_clean() instead of ob_end_flush() to DISCARD any
// buffered warnings/errors that would corrupt the SSE stream
while (ob_get_level()) {
@ob_end_clean();
}
$send = function (array $data) {
echo 'data: ' . json_encode($data) . "\n\n";
@flush();
};
try {
$protocol = $this->getProtocolById($id);
if (!$protocol) {
$send(['type' => 'error', 'error' => 'Protocol not found']);
return;
}
$script = trim($protocol['install_script'] ?? '');
if ($script === '') {
$send(['type' => 'error', 'error' => 'Install script is empty']);
return;
}
$container = 'proto-test-' . $id;
$send(['type' => 'start']);
$send(['type' => 'cmd', 'cmd' => 'docker rm -f ' . $container]);
$rm = $this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
$send(['type' => 'cmd_done', 'rc' => $rm['rc']]);
$cmdRun = 'docker run --network host --privileged -d --name ' . $container . ' ubuntu:22.04 sleep infinity';
$send(['type' => 'cmd', 'cmd' => $cmdRun]);
$run = $this->runHostCommandChecked('docker run --network host --privileged -d -v /var/run/docker.sock:/var/run/docker.sock -v /opt/amnezia:/opt/amnezia -v /etc/aivpn:/etc/aivpn -v /etc/amnezia:/etc/amnezia --name ' . escapeshellarg($container) . ' ubuntu:22.04 sleep infinity');
if ($run['rc'] !== 0) {
$send(['type' => 'error', 'error' => 'Docker not accessible: ' . trim($run['out'])]);
return;
}
$send(['type' => 'cmd_done', 'rc' => 0]);
$send(['type' => 'cmd', 'cmd' => 'provide docker cli']);
$cliPath = '/usr/local/bin/docker';
$send(['type' => 'cmd', 'cmd' => 'provide docker cli from docker:24-dind image']);
$try1 = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '"');
if ($try1['rc'] !== 0 || $try1['out'] === '') {
$cliPath = '/usr/bin/docker';
}
$cp = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '" | docker exec -i ' . escapeshellarg($container) . ' sh -lc "cat > /usr/local/bin/docker && chmod +x /usr/local/bin/docker"');
if ($cp['rc'] !== 0) {
$send(['type' => 'error', 'error' => 'Failed to provide docker CLI to test container: ' . trim($cp['out'])]);
return;
}
$this->execInContainerChecked($container, 'chmod +x /usr/local/bin/docker');
$ver = $this->execInContainerChecked($container, 'docker --version');
$send(['type' => 'out', 'line' => $ver['out']]);
if ($ver['rc'] !== 0) {
$send(['type' => 'error', 'error' => 'Docker CLI not available in test container']);
return;
}
$prelude = <<<'SH'
set -euo pipefail
set -x
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
wg() {
if docker ps --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then
docker exec -i "$CONTAINER_NAME" wg "$@"
else
docker pull -q amneziavpn/amnezia-wg:latest >/dev/null 2>&1 || true
docker run --rm -i --privileged --cap-add=NET_ADMIN amneziavpn/amnezia-wg:latest wg "$@"
fi
}
SH;
$wrapped = $prelude . "\n" . $script;
$send(['type' => 'cmd', 'cmd' => 'install_script']);
$runScript = $this->execInContainerChecked($container, $wrapped);
$outLines = explode("\n", trim($runScript['out']));
foreach ($outLines as $line) {
if ($line !== '')
$send(['type' => 'out', 'line' => $line]);
}
if ($runScript['rc'] !== 0) {
$send(['type' => 'error', 'error' => 'Install script failed: ' . trim($runScript['out'])]);
$this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
return;
}
$send(['type' => 'cmd_done', 'rc' => 0]);
$extracted = $this->extractValuesFromOutput($runScript['out']);
$variables = $this->getProtocolVariables($id);
// Merge all extracted variables (not just existing ones)
$variables = array_merge($variables, $extracted);
$preview = ProtocolService::generateProtocolOutput($protocol, $variables);
$send(['type' => 'preview', 'preview' => $preview]);
$this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
$this->runHostCommandChecked('docker rm -f amnezia-awg >/dev/null 2>&1 || true');
$send(['type' => 'done']);
} catch (Exception $e) {
echo 'data: ' . json_encode(['type' => 'error', 'error' => $e->getMessage()]) . "\n\n";
@flush();
}
}
public function apiTestUninstallProtocolStream(int $id): void
{
requireAdmin();
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
@ini_set('output_buffering', 'off');
@ini_set('zlib.output_compression', '0');
@ob_implicit_flush(true);
@ob_end_flush();
$send = function (array $data) {
echo 'data: ' . json_encode($data) . "\n\n";
@flush();
};
try {
$protocol = $this->getProtocolById($id);
if (!$protocol) {
$send(['type' => 'error', 'error' => 'Protocol not found']);
return;
}
$installScript = trim($protocol['install_script'] ?? '');
$uninstallScript = trim($protocol['uninstall_script'] ?? '');
if ($installScript === '') {
$send(['type' => 'error', 'error' => 'Install script is empty (required for setup)']);
return;
}
if ($uninstallScript === '') {
$send(['type' => 'error', 'error' => 'Uninstall script is empty']);
return;
}
// Normalize uninstall script if needed? For now assume it's fine.
$container = 'proto-test-uninstall-' . $id;
$send(['type' => 'start']);
// 1. Setup container
$send(['type' => 'cmd', 'cmd' => 'Setting up test environment...']);
$this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
$run = $this->runHostCommandChecked('docker run --privileged -d -v /var/run/docker.sock:/var/run/docker.sock --name ' . escapeshellarg($container) . ' ubuntu:22.04 sleep infinity');
if ($run['rc'] !== 0) {
$send(['type' => 'error', 'error' => 'Docker not accessible: ' . trim($run['out'])]);
return;
}
// Provide docker CLI
$cliPath = '/usr/local/bin/docker';
$try1 = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '"');
if ($try1['rc'] !== 0 || $try1['out'] === '') {
$cliPath = '/usr/bin/docker';
}
$cp = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '" | docker exec -i ' . escapeshellarg($container) . ' sh -lc "cat > /usr/local/bin/docker && chmod +x /usr/local/bin/docker"');
if ($cp['rc'] !== 0) {
$send(['type' => 'error', 'error' => 'Failed to provide docker CLI']);
return;
}
$this->execInContainerChecked($container, 'chmod +x /usr/local/bin/docker');
$prelude = <<<'SH'
set -euo pipefail
set -x
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
wg() {
if docker ps --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then
docker exec -i "$CONTAINER_NAME" wg "$@"
else
docker pull -q amneziavpn/amnezia-wg:latest >/dev/null 2>&1 || true
docker run --rm -i --privileged --cap-add=NET_ADMIN amneziavpn/amnezia-wg:latest wg "$@"
fi
}
SH;
// 2. Run Install Script
$send(['type' => 'cmd', 'cmd' => 'Running installation script...']);
$wrappedInstall = $prelude . "\n" . $installScript;
$runInstall = $this->execInContainerChecked($container, $wrappedInstall);
if ($runInstall['rc'] !== 0) {
$send(['type' => 'error', 'error' => 'Setup (install) failed: ' . trim($runInstall['out'])]);
$this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
return;
}
$send(['type' => 'out', 'line' => 'Installation successful. Now running uninstall...']);
// 3. Run Uninstall Script
$send(['type' => 'cmd', 'cmd' => 'Running uninstallation script...']);
$wrappedUninstall = $prelude . "\n" . $uninstallScript;
$runUninstall = $this->execInContainerChecked($container, $wrappedUninstall);
$outLines = explode("\n", trim($runUninstall['out']));
foreach ($outLines as $line) {
if ($line !== '')
$send(['type' => 'out', 'line' => $line]);
}
if ($runUninstall['rc'] !== 0) {
$send(['type' => 'error', 'error' => 'Uninstall script failed: ' . trim($runUninstall['out'])]);
} else {
$send(['type' => 'cmd_done', 'rc' => 0]);
$send(['type' => 'out', 'line' => 'Uninstallation completed successfully.']);
}
// Cleanup
$this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
$this->runHostCommandChecked('docker rm -f amnezia-awg >/dev/null 2>&1 || true'); // Cleanup potential leftover
$send(['type' => 'done']);
} catch (Exception $e) {
echo 'data: ' . json_encode(['type' => 'error', 'error' => $e->getMessage()]) . "\n\n";
@flush();
}
}
private function runHostCommand(string $cmd): void
{
$out = shell_exec($cmd);
}
private function runHostCommandChecked(string $cmd): array
{
$lines = [];
$rc = 0;
exec($cmd . ' 2>&1', $lines, $rc);
return ['out' => implode("\n", $lines), 'rc' => $rc];
}
private function execInContainer(string $container, string $cmd): string
{
$full = 'docker exec ' . escapeshellarg($container) . ' bash -lc ' . escapeshellarg($cmd);
$out = shell_exec($full . ' 2>&1');
return $out ?? '';
}
private function execInContainerChecked(string $container, string $cmd): array
{
$lines = [];
$rc = 0;
$full = 'docker exec ' . escapeshellarg($container) . ' bash -lc ' . escapeshellarg($cmd);
exec($full . ' 2>&1', $lines, $rc);
return ['out' => implode("\n", $lines), 'rc' => $rc];
}
private function normalizeAwgInstallScript(string $script): string
{
// Script in DB already has #!/bin/bash and set -euo pipefail
// Just return it as-is since variables are already defined in the script
return $script;
}
private function extractValuesFromOutput(string $output): array
{
$res = [];
// Extract port
if (preg_match('/Port:\s*(\d+)/i', $output, $m)) {
$res['server_port'] = $m[1];
}
// Extract server public key
if (preg_match('/Server Public Key:\s*([A-Za-z0-9+\/=]+)/i', $output, $m)) {
$res['server_public_key'] = $m[1];
}
// Extract preshared key
if (preg_match('/PresharedKey\s*=\s*([A-Za-z0-9+\/=]+)/i', $output, $m)) {
$res['preshared_key'] = $m[1];
}
// Extract subnet (format: "Subnet: 10.8.1.1/24")
if (preg_match('/Subnet:\s*([0-9.]+)\/(\d+)/i', $output, $m)) {
$res['subnet_ip'] = $m[1];
$res['subnet_cidr'] = $m[2];
}
// Extract password (for non-WireGuard protocols)
if (preg_match('/Password:\s*(\S+)/i', $output, $m)) {
$res['password'] = $m[1];
}
// Extract method (for protocols like Shadowsocks)
if (preg_match('/Method:\s*(\S+)/i', $output, $m)) {
$res['method'] = $m[1];
}
// Extract client ID (for protocols that use it)
if (preg_match('/ClientID\s*:\s*([0-9a-fA-F-]+)/i', $output, $m)) {
$res['client_id'] = $m[1];
}
// Extract secret (for MTProxy and similar protocols)
if (preg_match('/Secret:\s*([a-fA-F0-9]+)/i', $output, $m)) {
$res['secret'] = $m[1];
}
// Extract server host/IP
if (preg_match('/Server\s*Host:\s*(\S+)/i', $output, $m)) {
$res['server_host'] = trim($m[1], "'\"");
}
// Generic variable extraction (Variable: KEY=VALUE)
if (preg_match_all('/Variable:\s*([a-zA-Z0-9_]+)=(.*)/', $output, $matches, PREG_SET_ORDER)) {
foreach ($matches as $m) {
$key = trim($m[1]);
$val = trim($m[2]);
// Remove surrounding quotes if present
$val = trim($val, "'\"");
$res[$key] = $val;
}
}
// Fallback: parse any remaining "Key: Value" lines not yet captured
// This catches protocol-specific variables like custom fields
$lines = preg_split('/\r?\n/', $output);
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') continue;
// Skip set -x trace lines (start with +)
if (preg_match('/^\+/', $line)) continue;
if (preg_match('/^([A-Za-z][A-Za-z0-9 _-]*?)\s*:\s*(.+)$/', $line, $m)) {
$rawKey = trim($m[1]);
$rawVal = trim($m[2], " \t'\"");
$normalized = strtolower(preg_replace('/\s+/', '_', $rawKey));
// Don't overwrite already extracted keys
if (!array_key_exists($normalized, $res)) {
$res[$normalized] = $rawVal;
}
}
}
return $res;
}
private function getProtocolVariables(int $protocolId): array
{
$pdo = DB::conn();
$stmt = $pdo->prepare('SELECT variable_name, COALESCE(default_value, "") as val FROM protocol_variables WHERE protocol_id = ?');
$stmt->execute([$protocolId]);
$vars = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$vars[$row['variable_name']] = $row['val'] ?? '';
}
return $vars;
}
/**
* Database methods
*/
private function getAllProtocols(): array
{
return ProtocolService::getAllProtocolsWithStats();
}
private function getProtocolById(int $id): ?array
{
$pdo = DB::conn();
$stmt = $pdo->prepare('
SELECT p.*,
COUNT(DISTINCT sp.server_id) as server_count
FROM protocols p
LEFT JOIN server_protocols sp ON p.id = sp.protocol_id
WHERE p.id = ?
GROUP BY p.id
');
$stmt->execute([$id]);
$protocol = $stmt->fetch(PDO::FETCH_ASSOC);
return $protocol ?: null;
}
private function createProtocol(array $data): int
{
$pdo = DB::conn();
$stmt = $pdo->prepare('
INSERT INTO protocols (name, slug, description, install_script, uninstall_script, password_command, output_template, ubuntu_compatible, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
');
$stmt->execute([
$data['name'],
$data['slug'],
$data['description'],
$data['install_script'],
$data['uninstall_script'],
$data['password_command'] ?? '',
$data['output_template'],
$data['ubuntu_compatible'],
$data['is_active'],
$data['created_at'],
$data['updated_at']
]);
return (int) $pdo->lastInsertId();
}
private function updateProtocol(int $id, array $data): void
{
$setParts = [];
$values = [];
foreach ($data as $key => $value) {
$setParts[] = "$key = ?";
$values[] = $value;
}
$values[] = $id;
$sql = 'UPDATE protocols SET ' . implode(', ', $setParts) . ' WHERE id = ?';
$pdo = DB::conn();
$stmt = $pdo->prepare($sql);
$stmt->execute($values);
}
private function deleteProtocol(int $id): void
{
$pdo = DB::conn();
$stmt = $pdo->prepare('DELETE FROM protocols WHERE id = ?');
$stmt->execute([$id]);
}
private function isSlugExists(string $slug, ?int $excludeId = null): bool
{
$pdo = DB::conn();
if ($excludeId) {
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ? AND id != ?');
$stmt->execute([$slug, $excludeId]);
} else {
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ?');
$stmt->execute([$slug]);
}
return (bool) $stmt->fetchColumn();
}
private function isProtocolUsed(int $id): bool
{
$pdo = DB::conn();
$stmt = $pdo->prepare('SELECT COUNT(*) FROM server_protocols WHERE protocol_id = ?');
$stmt->execute([$id]);
return (bool) $stmt->fetchColumn();
}
}
+375
View File
@@ -0,0 +1,375 @@
<?php
/**
* ScenarioController
* Manages protocol installation scenarios (CRUD operations)
* Allows administrators to view, create, edit, and delete VPN protocol deployment scenarios
*/
class ScenarioController {
/**
* List all protocol scenarios
* GET /admin/scenarios
*/
public function listScenarios() {
requireAdmin();
$scenarios = InstallProtocolManager::getAll();
View::render('settings/scenarios.twig', [
'scenarios' => $scenarios,
'section' => 'scenarios'
]);
}
/**
* View single scenario details
* GET /admin/scenario/:id
*/
public function viewScenario($id) {
requireAdmin();
$scenario = InstallProtocolManager::getById((int)$id);
if (!$scenario) {
http_response_code(404);
View::render('404.twig');
return;
}
$definition = $scenario['definition'] ?? [];
View::render('settings/scenario_view.twig', [
'scenario' => $scenario,
'definition' => $definition,
'section' => 'scenarios'
]);
}
/**
* Show form to create new scenario
* GET /admin/scenario/create
*/
public function createScenarioForm() {
requireAdmin();
$templateDefinition = [
'engine' => 'shell',
'metadata' => [
'container_name' => 'custom-container',
'config_path' => '/opt/amnezia/custom'
],
'scripts' => [
'detect' => 'echo \'{"status":"absent","message":"Custom protocol not found"}\'',
'install' => 'echo \'{"success":true,"message":"Custom protocol installed"}\'',
'restore' => 'echo \'{"success":true,"message":"Custom protocol restored"}\''
]
];
View::render('settings/scenario_form.twig', [
'scenario' => null,
'templateDefinition' => json_encode($templateDefinition, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
'section' => 'scenarios'
]);
}
/**
* Show form to edit existing scenario
* GET /admin/scenario/:id/edit
*/
public function editScenarioForm($id) {
requireAdmin();
$scenario = InstallProtocolManager::getById((int)$id);
if (!$scenario) {
http_response_code(404);
View::render('404.twig');
return;
}
$definitionJson = json_encode($scenario['definition'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
View::render('settings/scenario_form.twig', [
'scenario' => $scenario,
'templateDefinition' => $definitionJson,
'section' => 'scenarios'
]);
}
/**
* Save scenario (create or update)
* POST /admin/scenario
*/
public function saveScenario() {
requireAdmin();
$data = $_POST;
// Validate required fields
if (empty($data['slug']) || empty($data['name'])) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Slug and name are required'
]);
return;
}
// Parse definition JSON
if (!empty($data['definition'])) {
if (is_string($data['definition'])) {
$definition = json_decode($data['definition'], true);
if (json_last_error() !== JSON_ERROR_NONE) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Invalid JSON in definition: ' . json_last_error_msg()
]);
return;
}
} else {
$definition = $data['definition'];
}
} else {
$definition = [];
}
// Validate definition structure
if (empty($definition['engine'])) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Definition must have "engine" field'
]);
return;
}
$saveData = [
'slug' => $data['slug'],
'name' => $data['name'],
'description' => $data['description'] ?? null,
'definition' => $definition,
'is_active' => isset($data['is_active']) ? (int)$data['is_active'] : 1
];
if (!empty($data['id'])) {
$saveData['id'] = (int)$data['id'];
}
try {
$id = InstallProtocolManager::save($saveData);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'id' => $id,
'message' => 'Scenario saved successfully',
'redirect' => '/admin/scenarios'
]);
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Error saving scenario: ' . $e->getMessage()
]);
}
}
/**
* Delete scenario
* DELETE /admin/scenario/:id or POST /admin/scenario/:id/delete
*/
public function deleteScenario($id) {
requireAdmin();
$id = (int)$id;
// Prevent deletion of default scenario
$scenario = InstallProtocolManager::getById($id);
if (!$scenario) {
header('Content-Type: application/json');
http_response_code(404);
echo json_encode([
'success' => false,
'message' => 'Scenario not found'
]);
return;
}
if ($scenario['slug'] === InstallProtocolManager::getDefaultSlug()) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Cannot delete default scenario'
]);
return;
}
try {
InstallProtocolManager::delete($id);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'message' => 'Scenario deleted successfully',
'redirect' => '/admin/scenarios'
]);
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Error deleting scenario: ' . $e->getMessage()
]);
}
}
/**
* Test scenario script (detection)
* POST /admin/scenario/:id/test
*/
public function testScenario($id) {
requireAdmin();
$scenario = InstallProtocolManager::getById((int)$id);
if (!$scenario) {
header('Content-Type: application/json');
http_response_code(404);
echo json_encode([
'success' => false,
'message' => 'Scenario not found'
]);
return;
}
$serverId = (int)($_POST['server_id'] ?? 0);
if (!$serverId) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Server ID required'
]);
return;
}
$server = new VpnServer($serverId);
if (!$server->getData()) {
header('Content-Type: application/json');
http_response_code(404);
echo json_encode([
'success' => false,
'message' => 'Server not found'
]);
return;
}
try {
// Test SSH connection first
if (!$server->testConnection()) {
throw new Exception('SSH connection to server failed');
}
// Run detection script
$result = InstallProtocolManager::runDetection($server, $scenario);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'result' => $result
]);
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Error testing scenario: ' . $e->getMessage()
]);
}
}
/**
* Export scenario as JSON
* GET /admin/scenario/:id/export
*/
public function exportScenario($id) {
requireAdmin();
$scenario = InstallProtocolManager::getById((int)$id);
if (!$scenario) {
http_response_code(404);
return;
}
header('Content-Type: application/json');
header('Content-Disposition: attachment; filename="scenario-' . $scenario['slug'] . '-' . date('Y-m-d') . '.json"');
// Remove database IDs from export
unset($scenario['id']);
echo json_encode($scenario, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
/**
* Import scenario from JSON
* POST /admin/scenario/import
*/
public function importScenario() {
requireAdmin();
$file = $_FILES['file'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'No file provided'
]);
return;
}
$contents = file_get_contents($file['tmp_name']);
$scenario = json_decode($contents, true);
if (!is_array($scenario)) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Invalid JSON file'
]);
return;
}
try {
// Remove ID so it creates a new entry
unset($scenario['id']);
// Ensure required fields exist
if (empty($scenario['slug']) || empty($scenario['name'])) {
throw new Exception('Imported scenario must have slug and name');
}
$id = InstallProtocolManager::save($scenario);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'id' => $id,
'message' => 'Scenario imported successfully',
'redirect' => '/admin/scenarios'
]);
} catch (Exception $e) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Error importing scenario: ' . $e->getMessage()
]);
}
}
}
+72 -3
View File
@@ -13,11 +13,48 @@ class SettingsController {
$stats = $this->getTranslationStats();
$users = $this->getAllUsers();
$apiKey = $this->getApiKey('openrouter');
// LDAP data for embedded tab
$stmt = $this->pdo->query("SELECT * FROM ldap_configs WHERE id = 1");
$config = $stmt->fetch() ?: [];
$stmt = $this->pdo->query("SELECT * FROM ldap_group_mappings ORDER BY ldap_group");
$mappings = $stmt->fetchAll();
// Protocols data for embedded tab (new management)
$protocols = ProtocolService::getAllProtocolsWithStats();
$selectedId = isset($_GET['id']) ? (int)$_GET['id'] : null;
$isNew = isset($_GET['new']);
$editing = null;
if (!$isNew) {
if ($selectedId) {
try {
$editing = ProtocolService::getProtocolWithDetails($selectedId);
} catch (Exception $e) {
$editing = null;
}
}
if (!$editing && !empty($protocols)) {
$firstId = (int)($protocols[0]['id'] ?? 0);
if ($firstId) {
try { $editing = ProtocolService::getProtocolWithDetails($firstId); } catch (Exception $e) { $editing = null; }
}
}
}
$definitionPretty = json_encode([], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$data = [
'translation_stats' => $stats,
'users' => $users,
'openrouter_key' => $apiKey
'openrouter_key' => $apiKey,
// LDAP
'config' => $config,
'mappings' => $mappings,
// Protocols
'protocols' => $protocols,
'editing' => $editing,
'definition_json' => $definitionPretty,
'is_new' => $isNew,
'default_slug' => isset($editing['slug']) ? $editing['slug'] : (isset($protocols[0]['slug']) ? $protocols[0]['slug'] : 'amnezia-wg'),
];
// Check for session messages
@@ -29,6 +66,15 @@ class SettingsController {
$data['error'] = $_SESSION['settings_error'];
unset($_SESSION['settings_error']);
}
// Also pick up protocol messages if present
if (isset($_SESSION['protocol_success']) && !isset($data['success'])) {
$data['success'] = $_SESSION['protocol_success'];
unset($_SESSION['protocol_success']);
}
if (isset($_SESSION['protocol_error']) && !isset($data['error'])) {
$data['error'] = $_SESSION['protocol_error'];
unset($_SESSION['protocol_error']);
}
View::render('settings.twig', $data);
}
@@ -79,6 +125,29 @@ class SettingsController {
exit;
}
public function updateProfile() {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /settings');
exit;
}
$user = Auth::user();
$displayName = trim($_POST['display_name'] ?? '');
if ($displayName === '') {
$_SESSION['settings_error'] = 'Display name cannot be empty';
header('Location: /settings#profile');
exit;
}
$stmt = $this->pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?");
$stmt->execute([$displayName, $user['id']]);
$_SESSION['settings_success'] = 'Profile updated';
header('Location: /settings#profile');
exit;
}
public function addUser() {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /settings');
@@ -422,7 +491,7 @@ class SettingsController {
]);
$_SESSION['settings_success'] = 'LDAP settings saved successfully';
header('Location: /settings/ldap');
header('Location: /settings#ldap');
exit;
}
+17 -2
View File
@@ -12,13 +12,13 @@ services:
MYSQL_USER: ${DB_USERNAME:-amnezia}
MYSQL_PASSWORD: ${DB_PASSWORD:-amnezia}
ports:
- "3307:3306"
- "3309:3306"
volumes:
- db_data:/var/lib/mysql
- ./migrations:/docker-entrypoint-initdb.d
- ./my.cnf:/etc/mysql/conf.d/my.cnf:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
interval: 10s
timeout: 5s
retries: 10
@@ -32,6 +32,8 @@ services:
depends_on:
db:
condition: service_healthy
dind:
condition: service_started
env_file:
- .env
environment:
@@ -42,10 +44,23 @@ services:
DB_DATABASE: ${DB_DATABASE:-amnezia_panel}
DB_USERNAME: ${DB_USERNAME:-amnezia}
DB_PASSWORD: ${DB_PASSWORD:-amnezia}
DOCKER_HOST: tcp://dind:2375
volumes:
- ./:/var/www/html
ports:
- "8082:80"
dind:
image: docker:24-dind
container_name: amnezia-panel-dind
privileged: true
restart: unless-stopped
environment:
DOCKER_TLS_CERTDIR: ""
command: ["dockerd", "--host=tcp://0.0.0.0:2375", "--host=unix:///var/run/docker.sock", "--tls=false", "--dns=8.8.8.8", "--dns=1.1.1.1", "--mtu=1200"]
volumes:
- dind_data:/var/lib/docker
volumes:
db_data:
dind_data:
+477
View File
@@ -0,0 +1,477 @@
<?php
/**
* Backup library utilities for importing servers from backup files.
*/
class BackupLibrary {
/**
* Discover available backup files.
*
* @param bool $registerTokens Whether to register tokens in the session
* @return array<int, array<string, mixed>>
*/
public static function listAvailable(bool $registerTokens = false): array {
if (!isset($_SESSION['backup_library']) || !is_array($_SESSION['backup_library'])) {
$_SESSION['backup_library'] = [];
}
if (!isset($_SESSION['backup_uploads']) || !is_array($_SESSION['backup_uploads'])) {
$_SESSION['backup_uploads'] = [];
}
$results = [];
foreach (self::getDirectories() as $directory) {
$files = glob($directory . DIRECTORY_SEPARATOR . '*.{backup,json}', GLOB_BRACE) ?: [];
foreach ($files as $filePath) {
if (!is_file($filePath) || !is_readable($filePath)) {
continue;
}
try {
$parsed = BackupParser::parseMetadata($filePath);
} catch (Throwable $e) {
// Skip invalid backup file but log for debugging
error_log('Backup parse failed for ' . $filePath . ': ' . $e->getMessage());
continue;
}
if (empty($parsed['servers'])) {
continue;
}
$token = hash('sha256', $filePath);
if ($registerTokens || !isset($_SESSION['backup_library'][$token])) {
$_SESSION['backup_library'][$token] = $filePath;
}
$results[] = [
'token' => $token,
'file_name' => basename($filePath),
'type' => $parsed['type'],
'origin' => 'filesystem',
'servers' => self::mapServerMetadata($parsed['servers'] ?? []),
];
}
}
foreach ($_SESSION['backup_uploads'] as $token => $upload) {
$results[] = [
'token' => $token,
'file_name' => $upload['file_name'],
'type' => $upload['type'],
'origin' => 'upload',
'servers' => self::mapServerMetadata($upload['data']['servers'] ?? []),
];
}
usort($results, function ($a, $b) {
return strcmp($a['file_name'], $b['file_name']);
});
return $results;
}
/**
* Load full server data from backup using the session token and server index.
*
* @param string $token Backup token
* @param int $serverIndex Index of the server inside backup file
* @return array<string, mixed>
* @throws Exception When token or server not found
*/
public static function loadServer(string $token, int $serverIndex): array {
$path = $_SESSION['backup_library'][$token] ?? null;
if ($path) {
if (!is_file($path) || !is_readable($path)) {
throw new Exception('Selected backup is not available');
}
$parsed = BackupParser::parse($path);
if (!isset($parsed['servers'][$serverIndex])) {
throw new Exception('Requested server not found in backup');
}
$server = $parsed['servers'][$serverIndex];
$server['source_file'] = $path;
$server['type'] = $parsed['type'];
return $server;
}
$upload = $_SESSION['backup_uploads'][$token] ?? null;
if ($upload) {
$parsed = $upload['data'];
if (!isset($parsed['servers'][$serverIndex])) {
throw new Exception('Requested server not found in uploaded backup');
}
$server = $parsed['servers'][$serverIndex];
$server['source_file'] = $upload['path'];
$server['type'] = $parsed['type'];
return $server;
}
throw new Exception('Selected backup is not available');
}
/**
* Get list of directories that may contain backup files.
*
* @return array<int, string>
*/
private static function getDirectories(): array {
$directories = [];
$default = realpath(__DIR__ . '/../backups');
if ($default) {
$directories[] = $default;
}
$envDirs = Config::get('BACKUP_LIBRARY_DIRS');
if (!empty($envDirs)) {
foreach (preg_split('/[;,]+/', $envDirs) as $rawDir) {
$normalized = trim($rawDir);
if ($normalized === '') {
continue;
}
if (is_dir($normalized)) {
$real = realpath($normalized);
if ($real) {
$directories[] = $real;
}
}
}
}
$home = getenv('HOME');
if ($home) {
$candidate = realpath($home . DIRECTORY_SEPARATOR . 'Downloads' . DIRECTORY_SEPARATOR . 'infosave');
if ($candidate) {
$directories[] = $candidate;
}
}
// Remove duplicates
$directories = array_values(array_unique($directories));
return $directories;
}
/**
* Register uploaded backup file and return metadata for UI.
*/
public static function registerUploaded(string $fileName, string $storedPath, array $parsed): array {
if (!isset($_SESSION['backup_uploads']) || !is_array($_SESSION['backup_uploads'])) {
$_SESSION['backup_uploads'] = [];
}
$token = 'upload_' . bin2hex(random_bytes(16));
$_SESSION['backup_uploads'][$token] = [
'file_name' => $fileName,
'path' => $storedPath,
'type' => $parsed['type'],
'data' => $parsed,
];
return [
'token' => $token,
'file_name' => $fileName,
'type' => $parsed['type'],
'origin' => 'upload',
'servers' => self::mapServerMetadata($parsed['servers'] ?? []),
];
}
/**
* Check whether provided token belongs to uploaded backup.
*/
public static function isUploadToken(string $token): bool {
return isset($_SESSION['backup_uploads'][$token]);
}
/**
* Retrieve stored upload metadata for a token.
*/
public static function getUploadRecord(string $token): ?array {
if (!isset($_SESSION['backup_uploads'][$token])) {
return null;
}
return $_SESSION['backup_uploads'][$token];
}
/**
* Get lightweight server metadata for an uploaded backup token.
*/
public static function getUploadServers(string $token): array {
$upload = self::getUploadRecord($token);
if (!$upload) {
return [];
}
return self::mapServerMetadata($upload['data']['servers'] ?? []);
}
/**
* Forget uploaded backup token and remove temporary file.
*/
public static function forgetUpload(string $token): void {
$upload = $_SESSION['backup_uploads'][$token] ?? null;
if (!$upload) {
return;
}
unset($_SESSION['backup_uploads'][$token]);
$path = $upload['path'] ?? null;
if ($path && is_file($path)) {
@unlink($path);
}
}
/**
* Map server metadata for front-end lists.
*/
public static function mapServerMetadata($servers): array {
if (!is_array($servers)) {
return [];
}
return array_map(function ($server, $index) {
return [
'index' => $index,
'label' => $server['label'] ?? ('Server #' . ($index + 1)),
'host' => $server['host'] ?? null,
'vpn_port' => $server['vpn_port'] ?? null,
'client_count' => isset($server['clients']) && is_array($server['clients'])
? count($server['clients'])
: 0
];
}, $servers, array_keys($servers));
}
}
/**
* Parse backup files and normalize into a single representation.
*/
class BackupParser {
/**
* Parse backup file metadata without storing heavy payloads.
*
* @param string $path
* @return array<string, mixed>
*/
public static function parseMetadata(string $path): array {
$parsed = self::parse($path);
// Strip client details to keep metadata light
$parsed['servers'] = array_map(function ($server) {
$server['clients'] = $server['clients'] ?? [];
return $server;
}, $parsed['servers']);
return $parsed;
}
/**
* Parse backup file fully.
*
* @param string $path
* @return array<string, mixed>
* @throws Exception On parse errors
*/
public static function parse(string $path): array {
$contents = file_get_contents($path);
if ($contents === false) {
throw new Exception('Unable to read backup file');
}
$decoded = json_decode($contents, true);
if (!is_array($decoded)) {
throw new Exception('Backup file is not valid JSON');
}
if (isset($decoded['server']) && isset($decoded['clients'])) {
return self::parsePanelBackup($decoded);
}
if (isset($decoded['Servers/serversList'])) {
return self::parseAmneziaBackup($decoded);
}
throw new Exception('Unsupported backup format');
}
/**
* Parse backup produced by the Amnezia mobile/desktop application (.backup files).
*/
private static function parseAmneziaBackup(array $decoded): array {
$serversRaw = json_decode($decoded['Servers/serversList'] ?? '[]', true);
if (!is_array($serversRaw)) {
throw new Exception('Invalid Amnezia backup payload');
}
$servers = [];
foreach ($serversRaw as $serverIndex => $serverEntry) {
$containers = $serverEntry['containers'] ?? [];
foreach ($containers as $container) {
if (($container['container'] ?? '') !== 'amnezia-awg') {
continue;
}
$awg = $container['awg'] ?? [];
if (empty($awg)) {
continue;
}
$host = $serverEntry['hostName'] ?? ($awg['hostName'] ?? null);
if (!$host) {
continue;
}
$awgParams = [];
foreach (['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4'] as $key) {
if (isset($awg[$key])) {
$awgParams[$key] = is_numeric($awg[$key]) ? (int)$awg[$key] : $awg[$key];
}
}
$vpnPort = isset($awg['port']) ? (int)$awg['port'] : null;
$sshPort = isset($serverEntry['port']) ? (int)$serverEntry['port'] : 22;
$sshUser = $serverEntry['userName'] ?? 'root';
$sshPass = $serverEntry['password'] ?? '';
if ($sshPass === '') {
// Skip records without SSH credentials; these are likely client snapshots.
continue;
}
$name = trim($serverEntry['description'] ?? '') ?: $host;
$subnet = $container['awg']['subnet_address'] ?? null;
$clients = [];
if (!empty($awg['last_config'])) {
$lastConfig = json_decode($awg['last_config'], true);
if (is_array($lastConfig)) {
$clientIp = $lastConfig['client_ip'] ?? null;
if (!$subnet && $clientIp) {
$subnet = self::inferSubnet($clientIp);
}
$clients[] = [
'name' => $lastConfig['client_ip'] ?? ($lastConfig['clientId'] ?? $host . '_client'),
'client_ip' => $clientIp,
'public_key' => $lastConfig['client_pub_key'] ?? '',
'private_key' => $lastConfig['client_priv_key'] ?? '',
'preshared_key' => $lastConfig['psk_key'] ?? ($awg['psk_key'] ?? ''),
'config' => $lastConfig['config'] ?? '',
'status' => 'active',
'expires_at' => null,
];
}
}
if (!$subnet) {
$subnet = '10.8.1.0/24';
} elseif (!str_contains($subnet, '/')) {
$subnet .= '/24';
}
$servers[] = [
'label' => $name . ' (' . $host . ')',
'name' => $name,
'host' => $host,
'ssh_port' => $sshPort,
'ssh_username' => $sshUser ?: 'root',
'ssh_password' => $sshPass,
'vpn_port' => $vpnPort,
'container_name' => $container['container'] ?? 'amnezia-awg',
'vpn_subnet' => $subnet,
'server_public_key' => $awg['server_pub_key'] ?? null,
'preshared_key' => $awg['psk_key'] ?? null,
'awg_params' => $awgParams,
'clients' => $clients,
];
}
}
return [
'type' => 'amnezia_app',
'servers' => $servers,
];
}
/**
* Parse backup generated by this panel (backups/backup_*.json).
*/
private static function parsePanelBackup(array $decoded): array {
$server = $decoded['server'];
$awgParams = $server['awg_params'] ?? [];
if (is_string($awgParams)) {
$decodedParams = json_decode($awgParams, true);
if (is_array($decodedParams)) {
$awgParams = $decodedParams;
}
}
$vpnPort = isset($server['vpn_port']) ? (int)$server['vpn_port'] : null;
$sshPort = isset($server['port']) ? (int)$server['port'] : 22;
$sshUser = $server['username'] ?? 'root';
$sshPass = $server['password'] ?? '';
$host = $server['host']
?? $server['host_name']
?? $server['host_ip']
?? null;
if (!$host) {
throw new Exception('Panel backup is missing server host/SSH details. Create the server manually and import its clients via the panel importer.');
}
$clients = [];
foreach ($decoded['clients'] as $client) {
$clients[] = [
'name' => $client['name'] ?? ($client['client_ip'] ?? 'client'),
'client_ip' => $client['client_ip'] ?? null,
'public_key' => $client['public_key'] ?? '',
'private_key' => $client['private_key'] ?? '',
'preshared_key' => $client['preshared_key'] ?? ($server['preshared_key'] ?? ''),
'config' => $client['config'] ?? '',
'status' => $client['status'] ?? 'active',
'expires_at' => $client['expires_at'] ?? null,
'created_at' => $client['created_at'] ?? null,
];
}
return [
'type' => 'panel_backup',
'servers' => [
[
'label' => ($server['name'] ?? 'Server') . ' (' . $host . ')',
'name' => $server['name'] ?? 'Server',
'host' => $host,
'ssh_port' => $sshPort,
'ssh_username' => $sshUser,
'ssh_password' => $sshPass,
'vpn_port' => $vpnPort,
'container_name' => $server['container_name'] ?? 'amnezia-awg',
'vpn_subnet' => $server['vpn_subnet'] ?? '10.8.1.0/24',
'server_public_key' => $server['server_public_key'] ?? null,
'preshared_key' => $server['preshared_key'] ?? null,
'awg_params' => $awgParams,
'clients' => $clients,
]
],
];
}
/**
* Infer /24 subnet from client IP.
*/
private static function inferSubnet(string $ip): string {
$parts = explode('.', $ip);
if (count($parts) === 4) {
return $parts[0] . '.' . $parts[1] . '.' . $parts[2] . '.0/24';
}
return '10.8.1.0/24';
}
}
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
<?php
class Logger {
private const DEFAULT_LOGS_DIR = __DIR__ . '/../logs';
private static function ensureDir(string $dir): void {
if (!is_dir($dir)) {
@mkdir($dir, 0777, true);
}
}
private static function getLogsDir(): string {
// Fallback to project logs directory next to inc/
$dir = self::DEFAULT_LOGS_DIR;
self::ensureDir($dir);
return $dir;
}
public static function appendInstall(int $serverId, string $message): void {
$dir = self::getLogsDir();
$file = $dir . '/install_server_' . $serverId . '.log';
$line = '[' . date('Y-m-d H:i:s') . '] ' . $message . "\n";
@file_put_contents($file, $line, FILE_APPEND);
}
}
+314
View File
@@ -0,0 +1,314 @@
<?php
class OpenRouterService {
private $apiKey;
private $apiUrl = 'https://openrouter.ai/api/v1';
private $timeout = 60; // 60 seconds timeout for AI generation
public function __construct() {
$this->apiKey = $_ENV['OPENROUTER_API_KEY'] ?? null;
if (!$this->apiKey) {
throw new Exception('OpenRouter API key not configured');
}
}
/**
* Generate installation script using OpenRouter API
*/
public function generateScript(string $prompt, string $model = 'openai/gpt-3.5-turbo'): array {
try {
$messages = [
[
'role' => 'system',
'content' => 'You are a helpful assistant that creates bash installation scripts for VPN protocols. Always respond with valid JSON containing the script, suggestions, ubuntu compatibility, and estimated installation time.'
],
[
'role' => 'user',
'content' => $prompt
]
];
$response = $this->makeAPICall('/chat/completions', [
'model' => $model,
'messages' => $messages,
'temperature' => 0.3, // Lower temperature for more consistent results
'max_tokens' => 4000, // Sufficient for detailed scripts
'response_format' => ['type' => 'json_object']
]);
if (!isset($response['choices'][0]['message']['content'])) {
throw new Exception('Invalid response from OpenRouter API');
}
$content = $response['choices'][0]['message']['content'];
$parsed = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
// If JSON parsing fails, try to extract script from plain text
return $this->parsePlainTextResponse($content);
}
return $this->validateAndEnhanceResponse($parsed);
} catch (Exception $e) {
error_log("Error in OpenRouterService::generateScript: " . $e->getMessage());
throw new Exception('Failed to generate script: ' . $e->getMessage());
}
}
/**
* Get available AI models from OpenRouter
*/
public function getAvailableModels(): array {
try {
$response = $this->makeAPICall('/models', [], 'GET');
if (!isset($response['data'])) {
throw new Exception('Invalid response from OpenRouter API');
}
// Filter models suitable for code generation
$codeModels = array_filter($response['data'], function($model) {
$codeModelIds = [
'openai/gpt-3.5-turbo',
'openai/gpt-4',
'openai/gpt-4-turbo',
'anthropic/claude-3-haiku',
'anthropic/claude-3-sonnet',
'anthropic/claude-3-opus',
'google/gemini-pro',
'meta-llama/llama-2-70b-chat',
'meta-llama/llama-3-70b-instruct'
];
return in_array($model['id'], $codeModelIds) && $model['top_provider'] === true;
});
return array_values(array_map(function($model) {
return [
'id' => $model['id'],
'name' => $model['name'] ?? $model['id'],
'description' => $model['description'] ?? '',
'pricing' => $model['pricing'] ?? null
];
}, $codeModels));
} catch (Exception $e) {
error_log("Error in OpenRouterService::getAvailableModels: " . $e->getMessage());
// Return default models if API call fails
return $this->getDefaultModels();
}
}
/**
* Make API call to OpenRouter
*/
private function makeAPICall(string $endpoint, array $data = [], string $method = 'POST'): array {
$ch = curl_init();
$url = $this->apiUrl . $endpoint;
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->apiKey,
'Content-Type: application/json',
'HTTP-Referer: ' . ($_ENV['APP_URL'] ?? 'https://localhost'),
'X-Title: Amnezia VPN Panel'
],
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2
]);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new Exception('CURL error: ' . $error);
}
if ($httpCode >= 400) {
$errorData = json_decode($response, true);
$errorMessage = $errorData['error']['message'] ?? "HTTP $httpCode error";
throw new Exception($errorMessage);
}
$decoded = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Invalid JSON response from OpenRouter API');
}
return $decoded;
}
/**
* Parse plain text response when JSON parsing fails
*/
private function parsePlainTextResponse(string $content): array {
// Try to extract bash script from plain text
if (preg_match('/```bash\n(.*?)\n```/s', $content, $matches)) {
$script = trim($matches[1]);
} elseif (preg_match('/```(.*?)```/s', $content, $matches)) {
$script = trim($matches[1]);
} else {
// If no code blocks found, treat the entire content as script
$script = trim($content);
}
// Add bash shebang if not present
if (!str_starts_with($script, '#!')) {
$script = "#!/bin/bash\n\n" . $script;
}
return [
'script' => $script,
'suggestions' => [
'Check the script for syntax errors',
'Test the script in a safe environment',
'Review security implications'
],
'ubuntu_compatible' => true,
'estimated_time' => '5 minutes'
];
}
/**
* Validate and enhance AI response
*/
private function validateAndEnhanceResponse(array $response): array {
$defaults = [
'script' => '#!/bin/bash\n# Default installation script\necho "Installation script placeholder"',
'suggestions' => [],
'ubuntu_compatible' => true,
'estimated_time' => '5 minutes'
];
// Ensure all required fields are present
foreach ($defaults as $key => $defaultValue) {
if (!isset($response[$key])) {
$response[$key] = $defaultValue;
}
}
// Validate script format
if (!str_starts_with(trim($response['script']), '#!')) {
$response['script'] = "#!/bin/bash\n\n" . $response['script'];
}
// Ensure suggestions is an array
if (!is_array($response['suggestions'])) {
$response['suggestions'] = [];
}
// Add default suggestions if none provided
if (empty($response['suggestions'])) {
$response['suggestions'] = [
'Review the generated script for security implications',
'Test the script in a development environment first',
'Ensure all dependencies are available on your system',
'Backup your system before running the script'
];
}
// Validate ubuntu_compatible is boolean
if (!is_bool($response['ubuntu_compatible'])) {
$response['ubuntu_compatible'] = true;
}
return $response;
}
/**
* Get default models when API is unavailable
*/
private function getDefaultModels(): array {
return [
[
'id' => 'openai/gpt-3.5-turbo',
'name' => 'GPT-3.5 Turbo',
'description' => 'Fast and cost-effective model for general purpose tasks',
'pricing' => ['prompt' => '0.001', 'completion' => '0.002']
],
[
'id' => 'openai/gpt-4',
'name' => 'GPT-4',
'description' => 'Most capable model for complex tasks',
'pricing' => ['prompt' => '0.03', 'completion' => '0.06']
],
[
'id' => 'anthropic/claude-3-haiku',
'name' => 'Claude 3 Haiku',
'description' => 'Fast and cost-effective model from Anthropic',
'pricing' => ['prompt' => '0.00025', 'completion' => '0.00125']
],
[
'id' => 'anthropic/claude-3-sonnet',
'name' => 'Claude 3 Sonnet',
'description' => 'Balanced performance and cost from Anthropic',
'pricing' => ['prompt' => '0.003', 'completion' => '0.015']
]
];
}
public function testModelAvailability(string $modelId): array {
if (!$this->apiKey) {
return [
'success' => false,
'http_code' => 401,
'message' => 'OpenRouter API key not configured'
];
}
$payload = [
'model' => $modelId,
'messages' => [
['role' => 'user', 'content' => 'Reply with: OK']
],
'max_tokens' => 5,
'temperature' => 0
];
$ch = curl_init($this->apiUrl . '/chat/completions');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiKey,
'HTTP-Referer: https://amnez.ia',
'X-Title: Amnezia VPN Panel'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
return [
'success' => false,
'http_code' => null,
'message' => 'Network error: ' . $curlError
];
}
$json = json_decode($response, true);
$ok = $httpCode === 200 && isset($json['choices'][0]['message']['content']);
return [
'success' => $ok,
'http_code' => $httpCode,
'message' => $ok ? 'Model is available' : ($json['error']['message'] ?? 'Model test failed')
];
}
}
+407
View File
@@ -0,0 +1,407 @@
<?php
class ProtocolService
{
/**
* Get all protocols with additional metadata
*/
public static function getAllProtocolsWithStats(): array
{
try {
$pdo = DB::conn();
$stmt = $pdo->query('
SELECT p.*,
COUNT(DISTINCT sp.server_id) as server_count,
COUNT(DISTINCT pt.id) as template_count,
COUNT(DISTINCT pv.id) as variable_count,
COUNT(DISTINCT ag.id) as ai_generation_count,
MAX(ag.created_at) as last_ai_generation
FROM protocols p
LEFT JOIN server_protocols sp ON p.id = sp.protocol_id
LEFT JOIN protocol_templates pt ON p.id = pt.protocol_id
LEFT JOIN protocol_variables pv ON p.id = pv.protocol_id
LEFT JOIN ai_generations ag ON p.id = ag.protocol_id
GROUP BY p.id
ORDER BY p.name ASC
');
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log("Error in ProtocolService::getAllProtocolsWithStats: " . $e->getMessage());
throw new Exception('Failed to get protocols with stats');
}
}
/**
* Get protocol with all related data (templates, variables, AI history)
*/
public static function getProtocolWithDetails(int $protocolId): array
{
try {
$pdo = DB::conn();
// Get protocol
$stmt = $pdo->prepare('SELECT * FROM protocols WHERE id = ?');
$stmt->execute([$protocolId]);
$protocol = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$protocol) {
throw new Exception('Protocol not found');
}
// Get templates
$stmt = $pdo->prepare('SELECT * FROM protocol_templates WHERE protocol_id = ? ORDER BY is_default DESC, template_name ASC');
$stmt->execute([$protocolId]);
$protocol['templates'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get variables
$stmt = $pdo->prepare('SELECT * FROM protocol_variables WHERE protocol_id = ? ORDER BY variable_name ASC');
$stmt->execute([$protocolId]);
$protocol['variables'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get AI generation history (last 10)
$stmt = $pdo->prepare('
SELECT ag.*, p.name as protocol_name
FROM ai_generations ag
LEFT JOIN protocols p ON ag.protocol_id = p.id
WHERE ag.protocol_id = ?
ORDER BY ag.created_at DESC
LIMIT 10
');
$stmt->execute([$protocolId]);
$protocol['ai_history'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get server usage
$stmt = $pdo->prepare('
SELECT sp.*, vs.name as server_name, vs.host as server_host
FROM server_protocols sp
JOIN vpn_servers vs ON sp.server_id = vs.id
WHERE sp.protocol_id = ?
ORDER BY sp.applied_at DESC
');
$stmt->execute([$protocolId]);
$protocol['server_usage'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $protocol;
} catch (Exception $e) {
error_log("Error in ProtocolService::getProtocolWithDetails: " . $e->getMessage());
throw new Exception('Failed to get protocol details');
}
}
/**
* Validate protocol data before saving
*/
public static function validateProtocolData(array $data): array
{
$errors = [];
// Validate name
if (empty($data['name'])) {
$errors[] = 'Protocol name is required';
} elseif (strlen($data['name']) > 255) {
$errors[] = 'Protocol name must be less than 255 characters';
}
// Validate slug
if (empty($data['slug'])) {
$errors[] = 'Protocol slug is required';
} elseif (!preg_match('/^[a-z0-9_-]+$/i', $data['slug'])) {
$errors[] = 'Slug may contain only letters, numbers, dashes, and underscores';
} elseif (strlen($data['slug']) > 100) {
$errors[] = 'Protocol slug must be less than 100 characters';
}
// Validate description length
if (isset($data['description']) && strlen($data['description']) > 65535) {
$errors[] = 'Description is too long';
}
// Validate install script
if (isset($data['install_script']) && strlen($data['install_script']) > 16777215) { // MEDIUMTEXT limit
$errors[] = 'Installation script is too long';
}
// Validate output template
if (isset($data['output_template']) && strlen($data['output_template']) > 16777215) { // MEDIUMTEXT limit
$errors[] = 'Output template is too long';
}
// Validate ubuntu_compatible
if (isset($data['ubuntu_compatible']) && !is_bool($data['ubuntu_compatible']) && !in_array($data['ubuntu_compatible'], [0, 1, '0', '1'])) {
$errors[] = 'Ubuntu compatible must be a boolean value';
}
// Validate is_active
if (isset($data['is_active']) && !is_bool($data['is_active']) && !in_array($data['is_active'], [0, 1, '0', '1'])) {
$errors[] = 'Active status must be a boolean value';
}
// Validate QR code template
if (isset($data['qr_code_template']) && strlen($data['qr_code_template']) > 16777215) {
$errors[] = 'QR code template is too long';
}
// Validate QR code format
if (isset($data['qr_code_format']) && !in_array($data['qr_code_format'], ['raw', 'amnezia_compressed'])) {
$errors[] = 'Invalid QR code format';
}
return $errors;
}
/**
* Check if slug is unique
*/
public static function isSlugUnique(string $slug, ?int $excludeId = null): bool
{
try {
$pdo = DB::conn();
if ($excludeId) {
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ? AND id != ?');
$stmt->execute([$slug, $excludeId]);
} else {
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ?');
$stmt->execute([$slug]);
}
return (int) $stmt->fetchColumn() === 0;
} catch (Exception $e) {
error_log("Error in ProtocolService::isSlugUnique: " . $e->getMessage());
return false;
}
}
/**
* Check if protocol can be deleted
*/
public static function canDeleteProtocol(int $protocolId): array
{
try {
$pdo = DB::conn();
// Check if protocol is used by any servers
$stmt = $pdo->prepare('SELECT COUNT(*) FROM server_protocols WHERE protocol_id = ?');
$stmt->execute([$protocolId]);
$serverCount = (int) $stmt->fetchColumn();
$canDelete = $serverCount === 0;
$reason = '';
if (!$canDelete) {
$reason = "Protocol is currently used by $serverCount server(s)";
}
return [
'can_delete' => $canDelete,
'reason' => $reason,
'server_count' => $serverCount
];
} catch (Exception $e) {
error_log("Error in ProtocolService::canDeleteProtocol: " . $e->getMessage());
return [
'can_delete' => false,
'reason' => 'Database error occurred',
'server_count' => 0
];
}
}
/**
* Generate protocol template with variables
*/
public static function generateProtocolOutput(array $protocol, array $variables): string
{
try {
$template = $protocol['output_template'] ?? '';
if (empty($template)) {
return '';
}
foreach ($variables as $key => $value) {
$template = str_replace('{{' . $key . '}}', $value ?? '', $template);
}
$template = preg_replace('/(\w+:\/\/[^\/:]+):(?=\/|\?|$)/', '$1', $template);
$template = preg_replace('/(@[^\/:]+):(?=\/|\?|$)/', '$1', $template);
$template = preg_replace('/(\w+:\/\/)@(?=[^\/]{1})/', '$1', $template);
$template = preg_replace('/\{\{[^}]+\}\}/', '', $template);
// Check for unreplaced variables
if (preg_match('/\{\{([^}]+)\}\}/', $template, $matches)) {
error_log("Unreplaced variables in protocol template: " . implode(', ', $matches));
}
return $template;
} catch (Exception $e) {
error_log("Error in ProtocolService::generateProtocolOutput: " . $e->getMessage());
return '';
}
}
/**
* Generate QR code payload from template
*/
public static function generateQrCodePayload(array $protocol, array $variables): string
{
try {
$template = $protocol['qr_code_template'] ?? '';
$format = $protocol['qr_code_format'] ?? 'amnezia_compressed';
if (empty($template)) {
return '';
}
// Render template using the same logic as output template
// We temporarily wrap it to use the existing method
$rendered = self::generateProtocolOutput(['output_template' => $template], $variables);
if ($format === 'amnezia_compressed') {
require_once __DIR__ . '/QrUtil.php';
return QrUtil::encodeOldPayloadFromJson($rendered);
}
// For 'raw' and 'text' formats, return rendered template directly
return $rendered;
} catch (Exception $e) {
error_log("Error in ProtocolService::generateQrCodePayload: " . $e->getMessage());
return '';
}
}
/**
* Get protocol statistics for dashboard
*/
public static function getProtocolStatistics(): array
{
try {
$pdo = DB::conn();
// Total protocols
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols');
$totalProtocols = (int) $stmt->fetchColumn();
// Active protocols
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols WHERE is_active = 1');
$activeProtocols = (int) $stmt->fetchColumn();
// Ubuntu compatible protocols
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols WHERE ubuntu_compatible = 1');
$ubuntuCompatibleProtocols = (int) $stmt->fetchColumn();
// Protocols with AI generations
$stmt = $pdo->query('
SELECT COUNT(DISTINCT protocol_id)
FROM ai_generations
WHERE protocol_id IS NOT NULL
');
$protocolsWithAI = (int) $stmt->fetchColumn();
// Recent AI generations
$stmt = $pdo->query('
SELECT COUNT(*)
FROM ai_generations
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
');
$recentAIGenerations = (int) $stmt->fetchColumn();
// Server usage by protocol
$stmt = $pdo->query('
SELECT p.name, COUNT(sp.server_id) as server_count
FROM protocols p
LEFT JOIN server_protocols sp ON p.id = sp.protocol_id
GROUP BY p.id, p.name
ORDER BY server_count DESC
LIMIT 10
');
$serverUsageByProtocol = $stmt->fetchAll(PDO::FETCH_ASSOC);
return [
'total_protocols' => $totalProtocols,
'active_protocols' => $activeProtocols,
'ubuntu_compatible_protocols' => $ubuntuCompatibleProtocols,
'protocols_with_ai' => $protocolsWithAI,
'recent_ai_generations' => $recentAIGenerations,
'server_usage_by_protocol' => $serverUsageByProtocol,
'ai_usage_percentage' => $totalProtocols > 0 ? round(($protocolsWithAI / $totalProtocols) * 100, 2) : 0
];
} catch (Exception $e) {
error_log("Error in ProtocolService::getProtocolStatistics: " . $e->getMessage());
return [
'total_protocols' => 0,
'active_protocols' => 0,
'ubuntu_compatible_protocols' => 0,
'protocols_with_ai' => 0,
'recent_ai_generations' => 0,
'server_usage_by_protocol' => [],
'ai_usage_percentage' => 0
];
}
}
/**
* Get AI generation statistics
*/
public static function getAIGenerationStatistics(): array
{
try {
$pdo = DB::conn();
// Total AI generations
$stmt = $pdo->query('SELECT COUNT(*) FROM ai_generations');
$totalGenerations = (int) $stmt->fetchColumn();
// AI generations this month
$stmt = $pdo->query('
SELECT COUNT(*)
FROM ai_generations
WHERE MONTH(created_at) = MONTH(NOW()) AND YEAR(created_at) = YEAR(NOW())
');
$thisMonthGenerations = (int) $stmt->fetchColumn();
// AI generations by model
$stmt = $pdo->query('
SELECT model_used, COUNT(*) as count
FROM ai_generations
GROUP BY model_used
ORDER BY count DESC
');
$generationsByModel = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Ubuntu compatible generations
$stmt = $pdo->query('
SELECT COUNT(*)
FROM ai_generations
WHERE ubuntu_compatible = 1
');
$ubuntuCompatibleGenerations = (int) $stmt->fetchColumn();
return [
'total_generations' => $totalGenerations,
'this_month_generations' => $thisMonthGenerations,
'generations_by_model' => $generationsByModel,
'ubuntu_compatible_generations' => $ubuntuCompatibleGenerations,
'ubuntu_compatible_percentage' => $totalGenerations > 0 ? round(($ubuntuCompatibleGenerations / $totalGenerations) * 100, 2) : 0
];
} catch (Exception $e) {
error_log("Error in ProtocolService::getAIGenerationStatistics: " . $e->getMessage());
return [
'total_generations' => 0,
'this_month_generations' => 0,
'generations_by_model' => [],
'ubuntu_compatible_generations' => 0,
'ubuntu_compatible_percentage' => 0
];
}
}
}
+303 -51
View File
@@ -7,8 +7,10 @@ use Endroid\QrCode\Label\Label;
use Endroid\QrCode\Label\LabelAlignment;
use Endroid\QrCode\Encoding\Encoding;
class QrUtil {
public static function pngBase64(string $text, int $size = 300, int $margin = 1, string $label = 'Amnezia QR (old)') : string {
class QrUtil
{
public static function pngBase64(string $text, int $size = 300, int $margin = 1, string $label = 'Amnezia QR (old)'): string
{
// Try to load Composer autoload if not yet loaded
if (!class_exists(QrCode::class)) {
$autoload = __DIR__ . '/vendor/autoload.php';
@@ -53,31 +55,34 @@ class QrUtil {
throw new RuntimeException('QR library not available');
}
private static function urlsafe_b64_encode(string $bytes): string {
private static function urlsafe_b64_encode(string $bytes): string
{
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}
public static function encodeOldPayloadFromJson(string $jsonText): string {
public static function encodeOldPayloadFromJson(string $jsonText): string
{
$json = self::normalizeJson($jsonText);
// Old format uses zlib (gzcompress) with header [version, compressed_len, uncompressed_len]
$compressed = gzcompress($json, 9);
if ($compressed === false) {
throw new RuntimeException('gzcompress failed');
}
$uncompressedLen = strlen($json);
$compressedLen = strlen($compressed) + 4;
$version = 0x07C00100; // align with working payload header (big-endian)
$compressedLen = strlen($compressed) + 4; // +4 for the uncompressed length field
$version = 0x07C00100; // Amnezia magic version number
$header = pack('N3', $version, $compressedLen, $uncompressedLen);
return self::urlsafe_b64_encode($header . $compressed);
}
public static function encodeOldPayloadFromConf(string $confText): string {
public static function encodeOldPayloadFromConf(string $confText): string
{
$payload = self::buildOldEnvelopeFromConf($confText);
return self::encodeOldPayloadFromJson(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
}
private static function resolveServerDescription(?string $endpointHost): string {
$desc = (string)($endpointHost ?? '');
private static function resolveServerDescription(?string $endpointHost): string
{
$desc = (string) ($endpointHost ?? '');
try {
$cfgPath = __DIR__ . '/config.php';
$dbPath = __DIR__ . '/Database.php';
@@ -98,21 +103,32 @@ class QrUtil {
return $desc;
}
private static function buildOldEnvelopeFromConf(string $conf): array {
$endpointHost = null; $endpointPort = null; $mtu = null; $dns = []; $keepAlive = null;
$privKey = null; $pubKeyServer = null; $psk = null; $address = null; $allowedIps = [];
public static function parseWireGuardConfig(string $conf): array
{
$endpointHost = null;
$endpointPort = null;
$mtu = null;
$dns = [];
$keepAlive = null;
$privKey = null;
$pubKeyServer = null;
$psk = null;
$address = null;
$allowedIps = [];
foreach (explode("\n", $conf) as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') { continue; }
if ($line === '' || $line[0] === '#') {
continue;
}
if (stripos($line, 'Endpoint') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
if (preg_match('/^\[?([^\]]+)\]?:([0-9]{2,5})$/', $v, $m)) {
$endpointHost = $m[1];
$endpointPort = (int)$m[2];
$endpointPort = (int) $m[2];
}
} elseif (stripos($line, 'MTU') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$mtu = (int)$v;
$mtu = (int) $v;
} elseif (stripos($line, 'DNS') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$dns = array_map('trim', preg_split('/[,\s]+/', $v));
@@ -133,13 +149,19 @@ class QrUtil {
$allowedIps = array_map('trim', preg_split('/[,\s]+/', $v));
} elseif (stripos($line, 'PersistentKeepalive') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$keepAlive = (int)$v;
$keepAlive = (int) $v;
}
}
if (!$endpointPort) { $endpointPort = 51820; }
if (!$mtu) { $mtu = 1280; }
if (!$keepAlive) { $keepAlive = 25; }
if (!$endpointPort) {
$endpointPort = 51820;
}
if (!$mtu) {
$mtu = 1280;
}
if (!$keepAlive) {
$keepAlive = 25;
}
$dns1 = $dns[0] ?? '1.1.1.1';
$dns2 = $dns[1] ?? '1.0.0.1';
@@ -155,9 +177,15 @@ class QrUtil {
// Collect obfuscation params from conf if present
$params = [
'H1' => null, 'H2' => null, 'H3' => null, 'H4' => null,
'Jc' => null, 'Jmin' => null, 'Jmax' => null,
'S1' => null, 'S2' => null,
'H1' => null,
'H2' => null,
'H3' => null,
'H4' => null,
'Jc' => null,
'Jmin' => null,
'Jmax' => null,
'S1' => null,
'S2' => null,
];
foreach (explode("\n", $conf) as $line) {
$line = trim($line);
@@ -171,27 +199,173 @@ class QrUtil {
// Build last_config JSON object (stringified, pretty-printed)
$lastConfigObj = [
'H1' => (string)($params['H1'] ?? ''),
'H2' => (string)($params['H2'] ?? ''),
'H3' => (string)($params['H3'] ?? ''),
'H4' => (string)($params['H4'] ?? ''),
'Jc' => (string)($params['Jc'] ?? ''),
'Jmax' => (string)($params['Jmax'] ?? ''),
'Jmin' => (string)($params['Jmin'] ?? ''),
'S1' => (string)($params['S1'] ?? ''),
'S2' => (string)($params['S2'] ?? ''),
'H1' => (string) ($params['H1'] ?? ''),
'H2' => (string) ($params['H2'] ?? ''),
'H3' => (string) ($params['H3'] ?? ''),
'H4' => (string) ($params['H4'] ?? ''),
'Jc' => (string) ($params['Jc'] ?? ''),
'Jmax' => (string) ($params['Jmax'] ?? ''),
'Jmin' => (string) ($params['Jmin'] ?? ''),
'S1' => (string) ($params['S1'] ?? ''),
'S2' => (string) ($params['S2'] ?? ''),
'allowed_ips' => $allowedIps ?: ['0.0.0.0/0', '::/0'],
'clientId' => $clientPubKey ?: '',
'client_ip' => preg_replace('/\/(\d{1,2})$/', '', (string)($address ?? '')),
'client_priv_key' => (string)($privKey ?? ''),
'client_ip' => preg_replace('/\/(\d{1,2})$/', '', (string) ($address ?? '')),
'client_priv_key' => (string) ($privKey ?? ''),
'client_pub_key' => $clientPubKey ?: '',
'config' => $conf,
'hostName' => (string)($endpointHost ?? ''),
'mtu' => (string)$mtu,
'persistent_keep_alive' => (string)$keepAlive,
'hostName' => (string) ($endpointHost ?? ''),
'mtu' => (string) $mtu,
'persistent_keep_alive' => (string) $keepAlive,
'port' => $endpointPort,
'psk_key' => (string)($psk ?? ''),
'server_pub_key' => (string)($pubKeyServer ?? ''),
'psk_key' => (string) ($psk ?? ''),
'server_pub_key' => (string) ($pubKeyServer ?? ''),
];
$serverDesc = self::resolveServerDescription($endpointHost);
$vars = [
'last_config_json' => json_encode($lastConfigObj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT),
'port' => (string) $endpointPort,
'description' => $serverDesc,
'dns1' => $dns1,
'dns2' => $dns2,
'hostName' => $endpointHost,
'client_pub_key' => $clientPubKey,
'client_priv_key' => $privKey,
'client_ip' => preg_replace('/\/(\d{1,2})$/', '', (string) ($address ?? '')),
'psk_key' => $psk,
'server_pub_key' => $pubKeyServer,
'mtu' => $mtu,
'persistent_keep_alive' => $keepAlive,
'config' => $conf,
];
// Add params to vars
foreach ($params as $k => $v) {
$vars[$k] = (string) ($v ?? '');
}
return $vars;
}
private static function buildOldEnvelopeFromConf(string $conf): array
{
$endpointHost = null;
$endpointPort = null;
$mtu = null;
$dns = [];
$keepAlive = null;
$privKey = null;
$pubKeyServer = null;
$psk = null;
$address = null;
$allowedIps = [];
foreach (explode("\n", $conf) as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (stripos($line, 'Endpoint') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
if (preg_match('/^\[?([^\]]+)\]?:([0-9]{2,5})$/', $v, $m)) {
$endpointHost = $m[1];
$endpointPort = (int) $m[2];
}
} elseif (stripos($line, 'MTU') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$mtu = (int) $v;
} elseif (stripos($line, 'DNS') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$dns = array_map('trim', preg_split('/[,\s]+/', $v));
} elseif (stripos($line, 'PrivateKey') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$privKey = $v;
} elseif (stripos($line, 'PublicKey') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$pubKeyServer = $v;
} elseif (stripos($line, 'PresharedKey') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$psk = $v;
} elseif (stripos($line, 'Address') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$address = $v;
} elseif (stripos($line, 'AllowedIPs') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$allowedIps = array_map('trim', preg_split('/[,\s]+/', $v));
} elseif (stripos($line, 'PersistentKeepalive') === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$keepAlive = (int) $v;
}
}
if (!$endpointPort) {
$endpointPort = 51820;
}
if (!$mtu) {
$mtu = 1280;
}
if (!$keepAlive) {
$keepAlive = 25;
}
$dns1 = $dns[0] ?? '1.1.1.1';
$dns2 = $dns[1] ?? '1.0.0.1';
// Derive client public key if sodium available
$clientPubKey = '';
if ($privKey && function_exists('sodium_crypto_scalarmult_base')) {
$bin = base64_decode($privKey, true);
if ($bin !== false && strlen($bin) === 32) {
$pub = sodium_crypto_scalarmult_base($bin);
$clientPubKey = base64_encode($pub);
}
}
// Collect obfuscation params from conf if present
$params = [
'H1' => null,
'H2' => null,
'H3' => null,
'H4' => null,
'Jc' => null,
'Jmin' => null,
'Jmax' => null,
'S1' => null,
'S2' => null,
];
foreach (explode("\n", $conf) as $line) {
$line = trim($line);
foreach (array_keys($params) as $k) {
if (stripos($line, $k) === 0 && strpos($line, '=') !== false) {
[, $v] = array_map('trim', explode('=', $line, 2));
$params[$k] = $v;
}
}
}
// Build last_config JSON object (stringified, pretty-printed)
$lastConfigObj = [
'H1' => (string) ($params['H1'] ?? ''),
'H2' => (string) ($params['H2'] ?? ''),
'H3' => (string) ($params['H3'] ?? ''),
'H4' => (string) ($params['H4'] ?? ''),
'Jc' => (string) ($params['Jc'] ?? ''),
'Jmax' => (string) ($params['Jmax'] ?? ''),
'Jmin' => (string) ($params['Jmin'] ?? ''),
'S1' => (string) ($params['S1'] ?? ''),
'S2' => (string) ($params['S2'] ?? ''),
'allowed_ips' => $allowedIps ?: ['0.0.0.0/0', '::/0'],
'clientId' => $clientPubKey ?: '',
'client_ip' => preg_replace('/\/(\d{1,2})$/', '', (string) ($address ?? '')),
'client_priv_key' => (string) ($privKey ?? ''),
'client_pub_key' => $clientPubKey ?: '',
'config' => $conf,
'hostName' => (string) ($endpointHost ?? ''),
'mtu' => (string) $mtu,
'persistent_keep_alive' => (string) $keepAlive,
'port' => $endpointPort,
'psk_key' => (string) ($psk ?? ''),
'server_pub_key' => (string) ($pubKeyServer ?? ''),
];
$serverDesc = self::resolveServerDescription($endpointHost);
@@ -202,17 +376,17 @@ class QrUtil {
[
// awg first, then container (as in the working QR)
'awg' => [
'H1' => (string)($params['H1'] ?? ''),
'H2' => (string)($params['H2'] ?? ''),
'H3' => (string)($params['H3'] ?? ''),
'H4' => (string)($params['H4'] ?? ''),
'Jc' => (string)($params['Jc'] ?? ''),
'Jmax' => (string)($params['Jmax'] ?? ''),
'Jmin' => (string)($params['Jmin'] ?? ''),
'S1' => (string)($params['S1'] ?? ''),
'S2' => (string)($params['S2'] ?? ''),
'H1' => (string) ($params['H1'] ?? ''),
'H2' => (string) ($params['H2'] ?? ''),
'H3' => (string) ($params['H3'] ?? ''),
'H4' => (string) ($params['H4'] ?? ''),
'Jc' => (string) ($params['Jc'] ?? ''),
'Jmax' => (string) ($params['Jmax'] ?? ''),
'Jmin' => (string) ($params['Jmin'] ?? ''),
'S1' => (string) ($params['S1'] ?? ''),
'S2' => (string) ($params['S2'] ?? ''),
'last_config' => json_encode($lastConfigObj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT),
'port' => (string)$endpointPort,
'port' => (string) $endpointPort,
'transport_proto' => 'udp',
],
'container' => 'amnezia-awg',
@@ -227,9 +401,87 @@ class QrUtil {
return $envelope;
}
private static function normalizeJson(string $text): string {
private static function normalizeJson(string $text): string
{
$decoded = json_decode($text, true);
if (!is_array($decoded)) throw new InvalidArgumentException('Invalid JSON');
if (!is_array($decoded))
throw new InvalidArgumentException('Invalid JSON');
return json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
}
public static function encodeXrayPayload(string $host, int $port, string $clientId, string $description = '', ?array $reality = null, string $rawConfig = '', string $flow = ''): string
{
$desc = $description !== '' ? $description : self::resolveServerDescription($host);
// Construct full Client XRay config (Amnezia native format expects this structure)
$outbound = [
'protocol' => 'vless',
'settings' => [
'vnext' => [
[
'address' => $host,
'port' => $port,
'users' => [
[
'id' => $clientId,
'encryption' => 'none'
]
]
]
]
],
'streamSettings' => [
'network' => 'tcp',
'security' => ($reality ? 'reality' : 'none')
]
];
if ($flow !== '') {
$outbound['settings']['vnext'][0]['users'][0]['flow'] = $flow;
}
if ($reality) {
$outbound['streamSettings']['realitySettings'] = [
'fingerprint' => $reality['fingerprint'] ?? 'chrome',
'serverName' => $reality['serverName'] ?? '',
'publicKey' => $reality['publicKey'] ?? '',
'shortId' => $reality['shortId'] ?? '',
'spiderX' => ''
];
}
$fullConfig = [
'log' => ['loglevel' => 'warning'],
'inbounds' => [
[
'listen' => '127.0.0.1',
'port' => 10808,
'protocol' => 'socks',
'settings' => ['udp' => true]
]
],
'outbounds' => [$outbound]
];
$envelope = [
'containers' => [
[
'xray' => [
// No isThirdPartyConfig flag - treats as native container
'last_config' => json_encode($fullConfig, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT),
'port' => (string) $port,
'transport_proto' => 'tcp'
],
'container' => 'amnezia-xray'
]
],
'defaultContainer' => 'amnezia-xray',
'description' => $desc,
'dns1' => '1.1.1.1',
'dns2' => '1.0.0.1',
'hostName' => $host,
];
return self::encodeOldPayloadFromJson(json_encode($envelope, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
}
}
+1
View File
@@ -11,6 +11,7 @@ class Router {
}
public static function get(string $pattern, callable $handler): void { self::add('GET', $pattern, $handler); }
public static function post(string $pattern, callable $handler): void { self::add('POST', $pattern, $handler); }
public static function put(string $pattern, callable $handler): void { self::add('PUT', $pattern, $handler); }
public static function delete(string $pattern, callable $handler): void { self::add('DELETE', $pattern, $handler); }
private static function normalizePattern(string $pattern): string {
+967 -97
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -2,6 +2,7 @@
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use Twig\TwigFunction;
use Twig\TwigFilter;
class View {
private static ?Environment $twig = null;
@@ -36,6 +37,22 @@ class View {
});
self::$twig->addFunction($flagFunc);
// Add bytes format filter
$bytesFilter = new TwigFilter('bytes_format', function (int $bytes, int $precision = 2): string {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
});
self::$twig->addFilter($bytesFilter);
// Add translation filter (alias: trans)
$transFilter = new TwigFilter('trans', function (string $key, array $params = []) {
return Translator::t($key, $params);
});
self::$twig->addFilter($transFilter);
// Add globals
foreach ($globals as $k => $v) self::$twig->addGlobal($k, $v);
}
+1727 -292
View File
File diff suppressed because it is too large Load Diff
+502 -151
View File
File diff suppressed because it is too large Load Diff
+788
View File
@@ -0,0 +1,788 @@
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en','servers','backup_upload_type','Backup type'),
('en','servers','backup_type_auto','Auto detect'),
('en','servers','backup_type_amnezia','Amnezia app (.backup)'),
('en','servers','backup_type_panel','Panel export (.json)'),
('en','servers','backup_upload_hint','Upload a .backup or .json file. After upload, pick a server entry above.'),
('ru','servers','backup_upload_type','Тип бэкапа'),
('ru','servers','backup_type_auto','Определить автоматически'),
('ru','servers','backup_type_amnezia','Приложение Amnezia (.backup)'),
('ru','servers','backup_type_panel','Экспорт панели (.json)'),
('ru','servers','backup_upload_hint','Загрузите файл .backup или .json. После загрузки выберите сервер выше.'),
('es','servers','backup_upload_type','Tipo de copia de seguridad'),
('es','servers','backup_type_auto','Detectar automáticamente'),
('es','servers','backup_type_amnezia','Aplicación Amnezia (.backup)'),
('es','servers','backup_type_panel','Exportación del panel (.json)'),
('es','servers','backup_upload_hint','Suba un archivo .backup o .json. Después seleccione el servidor arriba.'),
('de','servers','backup_upload_type','Backup-Typ'),
('de','servers','backup_type_auto','Automatisch erkennen'),
('de','servers','backup_type_amnezia','Amnezia-App (.backup)'),
('de','servers','backup_type_panel','Panel-Export (.json)'),
('de','servers','backup_upload_hint','Laden Sie eine .backup- oder .json-Datei hoch. Wählen Sie anschließend oben einen Server aus.'),
('fr','servers','backup_upload_type','Type de sauvegarde'),
('fr','servers','backup_type_auto','Détection automatique'),
('fr','servers','backup_type_amnezia','Application Amnezia (.backup)'),
('fr','servers','backup_type_panel','Export du panneau (.json)'),
('fr','servers','backup_upload_hint','Téléversez un fichier .backup ou .json, puis sélectionnez un serveur ci-dessus.'),
('zh','servers','backup_upload_type','备份类型'),
('zh','servers','backup_type_auto','自动检测'),
('zh','servers','backup_type_amnezia','Amnezia 应用 (.backup)'),
('zh','servers','backup_type_panel','面板导出 (.json)'),
('zh','servers','backup_upload_hint','上传 .backup 或 .json 文件,随后在上方选择服务器。')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'servers', 'config_import_title', 'Import configuration'),
('en', 'servers', 'config_import_hint', 'Upload a configuration backup to update this server and its clients.'),
('en', 'servers', 'config_import_type_label', 'Backup type'),
('en', 'servers', 'config_import_type_panel', 'Panel backup (.json)'),
('en', 'servers', 'config_import_type_amnezia', 'Amnezia app backup (.backup)'),
('en', 'servers', 'config_import_file_label', 'Configuration file'),
('en', 'servers', 'config_import_file_hint', 'Our panel uses .json files. The Amnezia app uses .backup files.'),
('en', 'servers', 'config_import_submit', 'Import configuration'),
('ru', 'servers', 'config_import_title', 'Импорт конфигурации'),
('ru', 'servers', 'config_import_hint', 'Загрузите файл бэкапа, чтобы обновить настройки сервера и список клиентов.'),
('ru', 'servers', 'config_import_type_label', 'Источник бэкапа'),
('ru', 'servers', 'config_import_type_panel', 'Бэкап панели (.json)'),
('ru', 'servers', 'config_import_type_amnezia', 'Бэкап приложения Amnezia (.backup)'),
('ru', 'servers', 'config_import_file_label', 'Файл конфигурации'),
('ru', 'servers', 'config_import_file_hint', 'Панель использует файлы .json. Приложение Amnezia — файлы .backup.'),
('ru', 'servers', 'config_import_submit', 'Импортировать конфигурацию'),
('es', 'servers', 'config_import_title', 'Importar configuración'),
('es', 'servers', 'config_import_hint', 'Cargue un respaldo para actualizar este servidor y sus clientes.'),
('es', 'servers', 'config_import_type_label', 'Tipo de backup'),
('es', 'servers', 'config_import_type_panel', 'Backup del panel (.json)'),
('es', 'servers', 'config_import_type_amnezia', 'Backup de la app Amnezia (.backup)'),
('es', 'servers', 'config_import_file_label', 'Archivo de configuración'),
('es', 'servers', 'config_import_file_hint', 'El panel usa archivos .json. La app Amnezia usa archivos .backup.'),
('es', 'servers', 'config_import_submit', 'Importar configuración'),
('de', 'servers', 'config_import_title', 'Konfiguration importieren'),
('de', 'servers', 'config_import_hint', 'Laden Sie eine Sicherung hoch, um diesen Server und seine Clients zu aktualisieren.'),
('de', 'servers', 'config_import_type_label', 'Backup-Typ'),
('de', 'servers', 'config_import_type_panel', 'Panel-Backup (.json)'),
('de', 'servers', 'config_import_type_amnezia', 'Amnezia-App-Backup (.backup)'),
('de', 'servers', 'config_import_file_label', 'Konfigurationsfile'),
('de', 'servers', 'config_import_file_hint', 'Die Panel-Backups sind .json. Die Amnezia-App nutzt .backup-Dateien.'),
('de', 'servers', 'config_import_submit', 'Konfiguration importieren'),
('fr', 'servers', 'config_import_title', 'Importer la configuration'),
('fr', 'servers', 'config_import_hint', 'Téléversez un fichier de sauvegarde pour mettre à jour ce serveur et ses clients.'),
('fr', 'servers', 'config_import_type_label', 'Type de sauvegarde'),
('fr', 'servers', 'config_import_type_panel', 'Sauvegarde du panneau (.json)'),
('fr', 'servers', 'config_import_type_amnezia', 'Sauvegarde de lapplication Amnezia (.backup)'),
('fr', 'servers', 'config_import_file_label', 'Fichier de configuration'),
('fr', 'servers', 'config_import_file_hint', 'Notre panneau utilise des fichiers .json. Lapplication Amnezia utilise des fichiers .backup.'),
('fr', 'servers', 'config_import_submit', 'Importer la configuration'),
('zh', 'servers', 'config_import_title', '导入配置'),
('zh', 'servers', 'config_import_hint', '上传备份文件以更新此服务器及其客户端。'),
('zh', 'servers', 'config_import_type_label', '备份类型'),
('zh', 'servers', 'config_import_type_panel', '面板备份 (.json)'),
('zh', 'servers', 'config_import_type_amnezia', 'Amnezia 应用备份 (.backup)'),
('zh', 'servers', 'config_import_file_label', '配置文件'),
('zh', 'servers', 'config_import_file_hint', '面板使用 .json 文件,Amnezia 应用使用 .backup 文件。'),
('zh', 'servers', 'config_import_submit', '导入配置')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en','servers','creation_mode','Creation mode'),
('en','servers','creation_mode_manual','Manual setup'),
('en','servers','creation_mode_backup','Import from backup'),
('en','servers','upload_backup_file','Upload backup file'),
('en','servers','backup_upload_hint','Supported formats: panel JSON export or Amnezia application .backup'),
('en','servers','backup_server_entry','Select server entry'),
('en','servers','backup_summary_host','Host'),
('en','servers','backup_summary_clients','Clients'),
('en','servers','config_import_title','Restore configuration from backup'),
('en','servers','config_import_hint','Import server configuration (and optional clients) from a panel export or Amnezia application backup.'),
('en','servers','config_import_type_label','Backup type'),
('en','servers','config_import_type_panel','Panel export (.json)'),
('en','servers','config_import_type_amnezia','Amnezia app backup (.backup)'),
('en','servers','config_import_file_label','Configuration file'),
('en','servers','config_import_file_hint','The file remains on the server only during import and is deleted afterwards.'),
('en','servers','config_import_submit','Import configuration'),
('ru','servers','creation_mode','Режим создания'),
('ru','servers','creation_mode_manual','Ручная настройка'),
('ru','servers','creation_mode_backup','Импорт из бэкапа'),
('ru','servers','upload_backup_file','Загрузите файл бэкапа'),
('ru','servers','backup_upload_hint','Поддерживаются форматы: экспорт панели JSON или бэкап приложения Amnezia (.backup)'),
('ru','servers','backup_server_entry','Выберите запись сервера'),
('ru','servers','backup_summary_host','Хост'),
('ru','servers','backup_summary_clients','Клиенты'),
('ru','servers','config_import_title','Восстановление конфигурации из бэкапа'),
('ru','servers','config_import_hint','Импортируйте конфигурацию сервера (и при необходимости клиентов) из экспорта панели или бэкапа приложения Amnezia.'),
('ru','servers','config_import_type_label','Тип бэкапа'),
('ru','servers','config_import_type_panel','Экспорт панели (.json)'),
('ru','servers','config_import_type_amnezia','Бэкап приложения Amnezia (.backup)'),
('ru','servers','config_import_file_label','Файл конфигурации'),
('ru','servers','config_import_file_hint','Файл хранится на сервере только во время импорта и удаляется сразу после завершения.'),
('ru','servers','config_import_submit','Импортировать конфигурацию')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
CREATE TABLE IF NOT EXISTS protocols (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
install_script TEXT,
output_template TEXT,
ubuntu_compatible BOOLEAN DEFAULT false,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_protocols_slug (slug),
INDEX idx_protocols_active (is_active),
INDEX idx_protocols_ubuntu_compatible (ubuntu_compatible)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS protocol_templates (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
protocol_id INT UNSIGNED NOT NULL,
template_name VARCHAR(255) NOT NULL,
template_content TEXT NOT NULL,
is_default BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_protocol_templates_protocol (protocol_id),
INDEX idx_protocol_templates_default (is_default),
FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS protocol_variables (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
protocol_id INT UNSIGNED NOT NULL,
variable_name VARCHAR(100) NOT NULL,
variable_type VARCHAR(50) NOT NULL DEFAULT 'string',
default_value TEXT,
description TEXT,
required BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_protocol_variables_protocol (protocol_id),
INDEX idx_protocol_variables_name (variable_name),
FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS server_protocols (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
server_id INT UNSIGNED NOT NULL,
protocol_id INT UNSIGNED NOT NULL,
config_data JSON,
applied_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_server_protocols_server (server_id),
INDEX idx_server_protocols_protocol (protocol_id),
INDEX idx_server_protocols_applied (applied_at),
UNIQUE KEY unique_server_protocol (server_id, protocol_id),
FOREIGN KEY (server_id) REFERENCES vpn_servers(id) ON DELETE CASCADE,
FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS ai_generations (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
protocol_id INT UNSIGNED NULL,
model_used VARCHAR(100) NOT NULL,
prompt TEXT NOT NULL,
generated_script TEXT,
suggestions JSON,
ubuntu_compatible BOOLEAN,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ai_generations_protocol (protocol_id),
INDEX idx_ai_generations_model (model_used),
INDEX idx_ai_generations_created (created_at DESC),
FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active)
SELECT 'AmneziaWG Advanced', 'amnezia-wg-advanced', 'AmneziaWG protocol with advanced junk packet obfuscation parameters', '#!/bin/bash
echo "AmneziaWG Advanced installed"
', '[Interface]
PrivateKey = {{private_key}}
Address = {{client_ip}}/32
DNS = 8.8.8.8, 8.8.4.4
[Peer]
PublicKey = {{server_public_key}}
PresharedKey = {{preshared_key}}
Endpoint = {{server_host}}:{{server_port}}
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
Jc = {{jc}}
Jmin = {{jmin}}
Jmax = {{jmax}}
S1 = {{s1}}
S2 = {{s2}}
H1 = {{h1}}
H2 = {{h2}}
H3 = {{h3}}
H4 = {{h4}}', true, true
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='amnezia-wg-advanced');
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active)
SELECT 'WireGuard Standard', 'wireguard-standard', 'Standard WireGuard VPN protocol', '#!/bin/bash
CONTAINER_NAME="wireguard"
VPN_SUBNET="10.8.2.0/24"
PRIVATE_KEY=$(wg genkey)
PUBLIC_KEY=$(echo $PRIVATE_KEY | wg pubkey)
PRESHARED_KEY=$(wg genpsk)
docker run -d \
--name $CONTAINER_NAME \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
-v /opt/wireguard:/etc/wireguard \
linuxserver/wireguard
cat > /opt/wireguard/wg0.conf << EOF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.2.1/24
ListenPort = 51820
[Peer]
PublicKey =
PresharedKey = $PRESHARED_KEY
AllowedIPs = 10.8.2.2/32
EOF
echo "WireGuard Standard installed successfully"
echo "Server Public Key: $PUBLIC_KEY"', '[Interface]
PrivateKey = {{private_key}}
Address = {{client_ip}}/32
DNS = 8.8.8.8, 8.8.4.4
[Peer]
PublicKey = {{server_public_key}}
PresharedKey = {{preshared_key}}
Endpoint = {{server_host}}:{{server_port}}
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25', true, true
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='wireguard-standard');
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active)
SELECT 'OpenVPN', 'openvpn', 'OpenVPN protocol with TCP/UDP support', '#!/bin/bash
CONTAINER_NAME="openvpn"
VPN_SUBNET="10.8.3.0/24"
docker run -d \
--name $CONTAINER_NAME \
--cap-add=NET_ADMIN \
-p 1194:1194/udp \
-p 1194:1194/tcp \
-v /opt/openvpn:/etc/openvpn \
kylemanna/openvpn
docker exec -it $CONTAINER_NAME ovpn_genconfig -u udp://{{server_host}}:1194
docker exec -it $CONTAINER_NAME ovpn_initpki
echo "OpenVPN installed successfully"
echo "Available on ports: 1194/udp, 1194/tcp"', 'client
dev tun
proto {{protocol}}
remote {{server_host}} {{server_port}}
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
cert client.crt
key client.key
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
verb 3', true, true
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='openvpn');
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active)
SELECT 'Shadowsocks', 'shadowsocks', 'Shadowsocks proxy protocol', '#!/bin/bash
CONTAINER_NAME="shadowsocks"
PASSWORD=$(openssl rand -base64 12)
docker run -d \
--name $CONTAINER_NAME \
-p 8388:8388 \
-e METHOD=aes-256-gcm \
-e PASSWORD=$PASSWORD \
shadowsocks/shadowsocks-libev
echo "Shadowsocks installed successfully"
echo "Port: 8388"
echo "Method: aes-256-gcm"
echo "Password: $PASSWORD"', '{
"server": "{{server_host}}",
"server_port": {{server_port}},
"password": "{{password}}",
"method": "{{method}}"
}', true, true
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='shadowsocks');
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
SELECT p.id, 'Default AmneziaWG', '[Interface]
PrivateKey = {{private_key}}
Address = {{client_ip}}/32
DNS = 8.8.8.8, 8.8.4.4
[Peer]
PublicKey = {{server_public_key}}
PresharedKey = {{preshared_key}}
Endpoint = {{server_host}}:{{server_port}}
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
Jc = {{jc}}
Jmin = {{jmin}}
Jmax = {{jmax}}
S1 = {{s1}}
S2 = {{s2}}
H1 = {{h1}}
H2 = {{h2}}
H3 = {{h3}}
H4 = {{h4}}', true
FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id=p.id AND template_name='Default AmneziaWG');
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
SELECT p.id, 'Default WireGuard', '[Interface]
PrivateKey = {{private_key}}
Address = {{client_ip}}/32
DNS = 8.8.8.8, 8.8.4.4
[Peer]
PublicKey = {{server_public_key}}
PresharedKey = {{preshared_key}}
Endpoint = {{server_host}}:{{server_port}}
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25', true
FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id=p.id AND template_name='Default WireGuard');
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
SELECT p.id, 'Default OpenVPN', 'client
dev tun
proto {{protocol}}
remote {{server_host}} {{server_port}}
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
cert client.crt
key client.key
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
verb 3', true
FROM protocols p WHERE p.slug='openvpn' AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id=p.id AND template_name='Default OpenVPN');
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
SELECT p.id, 'Default Shadowsocks', '{
"server": "{{server_host}}",
"server_port": {{server_port}},
"password": "{{password}}",
"method": "{{method}}"
}', true
FROM protocols p WHERE p.slug='shadowsocks' AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id=p.id AND template_name='Default Shadowsocks');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'private_key', 'string', '', 'Client private key', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='private_key');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'client_ip', 'string', '10.8.1.2', 'Client IP address', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='client_ip');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_public_key', 'string', '', 'Server public key', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_public_key');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'preshared_key', 'string', '', 'Pre-shared key for additional security', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='preshared_key');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_host');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_port', 'number', '51820', 'Server port', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_port');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'jc', 'number', '4', 'Junk packet count', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='jc');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'jmin', 'number', '50', 'Minimum junk packet size', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='jmin');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'jmax', 'number', '1000', 'Maximum junk packet size', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='jmax');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 's1', 'number', '148', 'Junk packet size 1', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='s1');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 's2', 'number', '450', 'Junk packet size 2', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='s2');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'h1', 'number', '320121696', 'Junk packet header 1', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='h1');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'h2', 'number', '51525354', 'Junk packet header 2', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='h2');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'h3', 'number', '13141516', 'Junk packet header 3', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='h3');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'h4', 'number', '92435495', 'Junk packet header 4', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='h4');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'private_key', 'string', '', 'Client private key', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='private_key');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'client_ip', 'string', '10.8.2.2', 'Client IP address', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='client_ip');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_public_key', 'string', '', 'Server public key', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_public_key');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'preshared_key', 'string', '', 'Pre-shared key for additional security', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='preshared_key');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_host');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_port', 'number', '51820', 'Server port', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_port');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'protocol', 'string', 'udp', 'Connection protocol (udp/tcp)', true FROM protocols p WHERE p.slug='openvpn' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='protocol');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true FROM protocols p WHERE p.slug='openvpn' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_host');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_port', 'number', '1194', 'Server port', true FROM protocols p WHERE p.slug='openvpn' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_port');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true FROM protocols p WHERE p.slug='shadowsocks' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_host');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_port', 'number', '8388', 'Server port', true FROM protocols p WHERE p.slug='shadowsocks' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_port');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'password', 'string', '', 'Connection password', true FROM protocols p WHERE p.slug='shadowsocks' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='password');
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en','common','cancel','Cancel'),
('ru','common','cancel','Отмена'),
('es','common','cancel','Cancelar'),
('de','common','cancel','Abbrechen'),
('fr','common','cancel','Annuler'),
('zh','common','cancel','取消'),
('en','common','format','Format'),
('ru','common','format','Форматировать'),
('es','common','format','Formatear'),
('de','common','format','Formatieren'),
('fr','common','format','Formater'),
('zh','common','format','格式化'),
('en','common','clear','Clear'),
('ru','common','clear','Очистить'),
('es','common','clear','Borrar'),
('de','common','clear','Leeren'),
('fr','common','clear','Effacer'),
('zh','common','clear','清空'),
('en','protocols','template_editor_help','Use placeholders like {{variable}} and preview client output'),
('ru','protocols','template_editor_help','Используйте плейсхолдеры вида {{variable}} и просматривайте вывод клиента'),
('es','protocols','template_editor_help','Usa marcadores como {{variable}} y previsualiza la salida del cliente'),
('de','protocols','template_editor_help','Verwenden Sie Platzhalter wie {{variable}} und sehen Sie die ClientAusgabe in der Vorschau'),
('fr','protocols','template_editor_help','Utilisez des placeholders comme {{variable}} et prévisualisez la sortie client'),
('zh','protocols','template_editor_help','使用如 {{variable}} 的占位符并预览客户端输出'),
('en','protocols','variable_private_key_help','Client private key'),
('ru','protocols','variable_private_key_help','Приватный ключ клиента'),
('es','protocols','variable_private_key_help','Clave privada del cliente'),
('de','protocols','variable_private_key_help','Privater Schlüssel des Clients'),
('fr','protocols','variable_private_key_help','Clé privée du client'),
('zh','protocols','variable_private_key_help','客户端私钥'),
('en','protocols','variable_public_key_help','Server public key'),
('ru','protocols','variable_public_key_help','Публичный ключ сервера'),
('es','protocols','variable_public_key_help','Clave pública del servidor'),
('de','protocols','variable_public_key_help','Öffentlicher Schlüssel des Servers'),
('fr','protocols','variable_public_key_help','Clé publique du serveur'),
('zh','protocols','variable_public_key_help','服务器公钥'),
('en','protocols','variable_client_ip_help','Client IP address'),
('ru','protocols','variable_client_ip_help','IP‑адрес клиента'),
('es','protocols','variable_client_ip_help','Dirección IP del cliente'),
('de','protocols','variable_client_ip_help','IPAdresse des Clients'),
('fr','protocols','variable_client_ip_help','Adresse IP du client'),
('zh','protocols','variable_client_ip_help','客户端 IP 地址'),
('en','protocols','variable_server_host_help','VPN server host'),
('ru','protocols','variable_server_host_help','Хост VPN‑сервера'),
('es','protocols','variable_server_host_help','Host del servidor VPN'),
('de','protocols','variable_server_host_help','VPNServerHost'),
('fr','protocols','variable_server_host_help','Hôte du serveur VPN'),
('zh','protocols','variable_server_host_help','VPN 服务器主机'),
('en','protocols','variable_server_port_help','VPN server port'),
('ru','protocols','variable_server_port_help','Порт VPN‑сервера'),
('es','protocols','variable_server_port_help','Puerto del servidor VPN'),
('de','protocols','variable_server_port_help','VPNServerPort'),
('fr','protocols','variable_server_port_help','Port du serveur VPN'),
('zh','protocols','variable_server_port_help','VPN 服务器端口'),
('en','protocols','variable_preshared_key_help','WireGuard preshared key'),
('ru','protocols','variable_preshared_key_help','Предварительно общий ключ WireGuard'),
('es','protocols','variable_preshared_key_help','Clave precompartida de WireGuard'),
('de','protocols','variable_preshared_key_help','WireGuardvorausgeteilter Schlüssel'),
('fr','protocols','variable_preshared_key_help','Clé prépartagée WireGuard'),
('zh','protocols','variable_preshared_key_help','WireGuard 预共享密钥')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en','ai','enter_protocol_id_to_apply','Enter protocol ID to apply'),
('ru','ai','enter_protocol_id_to_apply','Введите ID протокола для применения'),
('es','ai','enter_protocol_id_to_apply','Introduce el ID de protocolo para aplicar'),
('de','ai','enter_protocol_id_to_apply','ProtokollID zum Anwenden eingeben'),
('fr','ai','enter_protocol_id_to_apply','Saisissez lID du protocole à appliquer'),
('zh','ai','enter_protocol_id_to_apply','输入要应用的协议 ID'),
('en','ai','improve_protocol','Improve protocol script for'),
('ru','ai','improve_protocol','Улучшить скрипт протокола для'),
('es','ai','improve_protocol','Mejorar script del protocolo для'),
('de','ai','improve_protocol','Protokollskript verbessern für'),
('fr','ai','improve_protocol','Améliorer le script du protocole pour'),
('zh','ai','improve_protocol','改进协议脚本:'),
('en','protocols','enter_protocol_name','Enter protocol name'),
('ru','protocols','enter_protocol_name','Введите имя протокола'),
('es','protocols','enter_protocol_name','Introduce el nombre del protocolo'),
('de','protocols','enter_protocol_name','Protokollnamen eingeben'),
('fr','protocols','enter_protocol_name','Saisissez le nom du protocole'),
('zh','protocols','enter_protocol_name','输入协议名称'),
('en','protocols','enter_protocol_slug','Enter protocol slug'),
('ru','protocols','enter_protocol_slug','Введите slug протокола'),
('es','protocols','enter_protocol_slug','Introduce el slug del protocolo'),
('de','protocols','enter_protocol_slug','ProtokollSlug eingeben'),
('fr','protocols','enter_protocol_slug','Saisissez le slug du protocole'),
('zh','protocols','enter_protocol_slug','输入协议 slug'),
('en','protocols','protocol_created_successfully','Protocol created successfully'),
('ru','protocols','protocol_created_successfully','Протокол успешно создан'),
('es','protocols','protocol_created_successfully','Protocolo creado correctamente'),
('de','protocols','protocol_created_successfully','Protokoll erfolgreich erstellt'),
('fr','protocols','protocol_created_successfully','Protocole créé avec succès'),
('zh','protocols','protocol_created_successfully','协议创建成功'),
('en','protocols','error_creating_protocol','Error creating protocol'),
('ru','protocols','error_creating_protocol','Ошибка создания протокола'),
('es','protocols','error_creating_protocol','Error al crear el protocolo'),
('de','protocols','error_creating_protocol','Fehler beim Erstellen des Protokolls'),
('fr','protocols','error_creating_protocol','Erreur lors de la création du protocole'),
('zh','protocols','error_creating_protocol','创建协议时出错')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en','settings','protocols','Protocols'),
('ru','settings','protocols','Протоколы'),
('es','settings','protocols','Protocolos'),
('de','settings','protocols','Protokolle'),
('fr','settings','protocols','Protocoles'),
('zh','settings','protocols','协议'),
('en','settings','protocol_management','Protocol Management'),
('ru','settings','protocol_management','Управление протоколами'),
('es','settings','protocol_management','Gestión de protocolos'),
('de','settings','protocol_management','Protokollverwaltung'),
('fr','settings','protocol_management','Gestion des protocoles'),
('zh','settings','protocol_management','协议管理')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en','protocols','test_install','Test install'),
('ru','protocols','test_install','Протестировать установку'),
('es','protocols','test_install','Probar instalación'),
('de','protocols','test_install','Installation testen'),
('fr','protocols','test_install','Tester linstallation'),
('zh','protocols','test_install','测试安装'),
('en','protocols','testing_on_ubuntu22','Testing on Ubuntu 22.04 in isolated Docker'),
('ru','protocols','testing_on_ubuntu22','Тест на Ubuntu 22.04 в изолированном Docker'),
('es','protocols','testing_on_ubuntu22','Prueba en Ubuntu 22.04 en Docker aislado'),
('de','protocols','testing_on_ubuntu22','Test auf Ubuntu 22.04 in isoliertem Docker'),
('fr','protocols','testing_on_ubuntu22','Test sur Ubuntu 22.04 dans Docker isolé'),
('zh','protocols','testing_on_ubuntu22','在隔离的 Docker 中于 Ubuntu 22.04 测试'),
('en','protocols','test_result','Test result'),
('ru','protocols','test_result','Результат теста'),
('es','protocols','test_result','Resultado de la prueba'),
('de','protocols','test_result','Testergebnis'),
('fr','protocols','test_result','Résultat du test'),
('zh','protocols','test_result','测试结果'),
('en','protocols','client_output_preview','Client output preview'),
('ru','protocols','client_output_preview','Предпросмотр ответа клиенту'),
('es','protocols','client_output_preview','Vista previa de salida del cliente'),
('de','protocols','client_output_preview','ClientAusgabevorschau'),
('fr','protocols','client_output_preview','Aperçu de la sortie client'),
('zh','protocols','client_output_preview','客户端输出预览'),
('en','protocols','test_failed','Test failed'),
('ru','protocols','test_failed','Ошибка теста'),
('es','protocols','test_failed','La prueba falló'),
('de','protocols','test_failed','Test fehlgeschlagen'),
('fr','protocols','test_failed','Échec du test'),
('zh','protocols','test_failed','测试失败')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active, created_at, updated_at)
SELECT 'SMB Server', 'smb', 'Samba SMB file share inside Docker with random host port', '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-smb}"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nSMB_PORT=$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))\n\n docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/smb/share\n docker run -d \\\n --name "$CONTAINER_NAME" \\\n -p "${SMB_PORT}:445" \\\n -v /opt/amnezia/smb/share:/share \\\n dperson/samba -p -u "amnezia;amnezia" -s "share;/share;yes;no;no;amnezia"\n echo "Port: ${SMB_PORT}"\n echo "Password: amnezia"\n', 'smb://{{server_host}}:{{server_port}}/share\nUsername: amnezia\nPassword: {{password}}', true, true, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='smb');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT id, 'server_host', 'string', '127.0.0.1', 'Server hostname or IP', true FROM protocols WHERE slug = 'smb' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='smb') AND variable_name='server_host');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT id, 'server_port', 'number', '445', 'Server port', true FROM protocols WHERE slug = 'smb' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='smb') AND variable_name='server_port');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT id, 'password', 'string', '', 'Connection password', true FROM protocols WHERE slug = 'smb' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='smb') AND variable_name='password');
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active, created_at, updated_at)
SELECT 'XRay VLESS', 'xray-vless', 'XRay VLESS server inside Docker with generated client UUID', '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nXRAY_PORT=$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n cat > /opt/amnezia/xray/config.json << EOF\n{\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": 443,\n "protocol": "vless",\n "settings": {\n "clients": [\n { "id": "${CLIENT_ID}" }\n ],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "none"\n }\n }\n ],\n "outbounds": [\n { "protocol": "freedom" }\n ]\n}\nEOF\n docker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:443" \\\n -v /opt/amnezia/xray:/etc/xray \\\n teddysun/xray\n echo "Port: ${XRAY_PORT}"\n echo "ClientID: ${CLIENT_ID}"\n', 'vless://{{client_id}}@{{server_host}}:{{server_port}}?security=none&type=tcp', true, true, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='xray-vless');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT id, 'server_host', 'string', '127.0.0.1', 'Server hostname or IP', true FROM protocols WHERE slug = 'xray-vless' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='xray-vless') AND variable_name='server_host');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT id, 'server_port', 'number', '443', 'Server port', true FROM protocols WHERE slug = 'xray-vless' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='xray-vless') AND variable_name='server_port');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT id, 'client_id', 'string', '', 'VLESS client ID (UUID)', true FROM protocols WHERE slug = 'xray-vless' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='xray-vless') AND variable_name='client_id');
DELIMITER $$
CREATE PROCEDURE add_protocol_column_and_constraints()
BEGIN
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='vpn_clients' AND COLUMN_NAME='protocol_id') = 0 THEN
ALTER TABLE vpn_clients ADD COLUMN protocol_id INT UNSIGNED NULL AFTER user_id;
END IF;
IF (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='vpn_clients' AND INDEX_NAME='idx_protocol_id') = 0 THEN
ALTER TABLE vpn_clients ADD INDEX idx_protocol_id (protocol_id);
END IF;
IF (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='vpn_clients' AND CONSTRAINT_NAME='fk_vpn_clients_protocol') = 0 THEN
ALTER TABLE vpn_clients ADD CONSTRAINT fk_vpn_clients_protocol FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE SET NULL;
END IF;
END$$
DELIMITER ;
CALL add_protocol_column_and_constraints();
DROP PROCEDURE add_protocol_column_and_constraints;
DELIMITER $$
CREATE PROCEDURE ensure_users_role_column_and_index()
BEGIN
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='users' AND COLUMN_NAME='role') = 0 THEN
ALTER TABLE users ADD COLUMN role ENUM('admin','user') DEFAULT 'user' AFTER name;
END IF;
IF (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='users' AND INDEX_NAME='idx_role') = 0 THEN
ALTER TABLE users ADD INDEX idx_role (role);
END IF;
END$$
DELIMITER ;
CALL ensure_users_role_column_and_index();
DROP PROCEDURE ensure_users_role_column_and_index;
DELIMITER $$
CREATE PROCEDURE add_protocols_optional_columns()
BEGIN
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='protocols' AND COLUMN_NAME='uninstall_script') = 0 THEN
ALTER TABLE protocols ADD COLUMN uninstall_script MEDIUMTEXT NULL AFTER install_script;
END IF;
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='protocols' AND COLUMN_NAME='password_command') = 0 THEN
ALTER TABLE protocols ADD COLUMN password_command TEXT NULL AFTER uninstall_script;
END IF;
END$$
DELIMITER ;
CALL add_protocols_optional_columns();
DROP PROCEDURE add_protocols_optional_columns;
DELIMITER $$
CREATE PROCEDURE ensure_users_display_name_column()
BEGIN
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='users' AND COLUMN_NAME='display_name') = 0 THEN
ALTER TABLE users ADD COLUMN display_name VARCHAR(255) NULL AFTER name;
END IF;
END$$
DELIMITER ;
CALL ensure_users_display_name_column();
DROP PROCEDURE ensure_users_display_name_column;
UPDATE users SET display_name = name WHERE (display_name IS NULL OR display_name = '') AND name IS NOT NULL;
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
EXISTING=$(docker ps -aq -f "name=$CONTAINER_NAME" 2>/dev/null | head -1)
if [ -z "$EXISTING" ]; then
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules amneziavpn/amnezia-wg:latest
sleep 2
else
STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "")
if [ "$STATUS" != "running" ]; then
docker start "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
fi
docker exec -i "$CONTAINER_NAME" sh -lc "mkdir -p /opt/amnezia/awg"
HAS_CONF=$(docker exec "$CONTAINER_NAME" sh -lc "[ -f /opt/amnezia/awg/wg0.conf ] && echo yes || echo no")
if [ "$HAS_CONF" = "yes" ]; then
PORT=$(docker exec "$CONTAINER_NAME" sh -lc "grep -E \"^ListenPort\" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d \"[:space:]\"")
PSK=$(docker exec "$CONTAINER_NAME" cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(docker exec "$CONTAINER_NAME" sh -lc "grep -E \"^PresharedKey\" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d \"[:space:]\"")
fi
PUBKEY=$(docker exec "$CONTAINER_NAME" cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(docker exec "$CONTAINER_NAME" cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
cat > /opt/amnezia/awg/wg0.conf << EOF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
S3 = 20
S4 = 10
H1 = 0xDEADBEEF
H2 = 0xCAFEBABE
H3 = 0x12345678
H4 = 0x9ABCDEF0
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey =
PresharedKey = $PRESHARED_KEY
AllowedIPs = 10.8.1.2/32
EOF
docker exec "$CONTAINER_NAME" sh -lc "echo $PRIVATE_KEY > /opt/amnezia/awg/wireguard_server_private_key.key"
docker exec "$CONTAINER_NAME" sh -lc "echo $PUBLIC_KEY > /opt/amnezia/awg/wireguard_server_public_key.key"
docker exec "$CONTAINER_NAME" sh -lc "echo $PRESHARED_KEY > /opt/amnezia/awg/wireguard_psk.key"
docker exec "$CONTAINER_NAME" sh -lc "echo [] > /opt/amnezia/awg/clientsTable"
docker exec "$CONTAINER_NAME" wg-quick up /opt/amnezia/awg/wg0.conf || true
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey = $PRESHARED_KEY'
WHERE slug = 'amnezia-wg-advanced';
UPDATE protocols SET
uninstall_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm -fv "$CONTAINER_NAME" 2>/dev/null || true
docker image rm amneziavpn/amnezia-wg:latest 2>/dev/null || true
docker network rm amnezia-dns-net 2>/dev/null || true
rm -rf /opt/amnezia/amnezia-awg 2>/dev/null || true
rm -rf /opt/amnezia/awg 2>/dev/null || true
echo "{\"success\":true,\"message\":\"AmneziaWG uninstalled\"}"'
WHERE slug = 'amnezia-wg-advanced';
DELIMITER $$
CREATE PROCEDURE ensure_vpn_servers_install_columns()
BEGIN
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='vpn_servers' AND COLUMN_NAME='install_protocol') = 0 THEN
ALTER TABLE vpn_servers ADD COLUMN install_protocol VARCHAR(100) NULL AFTER container_name;
END IF;
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='vpn_servers' AND COLUMN_NAME='install_options') = 0 THEN
ALTER TABLE vpn_servers ADD COLUMN install_options JSON NULL AFTER install_protocol;
END IF;
END$$
DELIMITER ;
CALL ensure_vpn_servers_install_columns();
DROP PROCEDURE ensure_vpn_servers_install_columns;
+108
View File
@@ -0,0 +1,108 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
EXISTING=$(docker ps -aq -f "name=$CONTAINER_NAME" 2>/dev/null | head -1)
if [ -z "$EXISTING" ]; then
# Run container with volume mount and keepalive command
# Waits for config file to appear before starting WireGuard
docker run -d --name "$CONTAINER_NAME" \
--restart always \
--privileged \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
-p "${VPN_PORT}:${VPN_PORT}/udp" \
-v /lib/modules:/lib/modules \
-v /opt/amnezia/awg:/opt/amnezia/awg \
amneziavpn/amnezia-wg:latest \
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
else
STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "")
if [ "$STATUS" != "running" ]; then
docker start "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
fi
# Check for existing config
if [ -f /opt/amnezia/awg/wg0.conf ]; then
# Extract existing configuration
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# Generate keys using the container
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
# Write config to HOST file (mounted to container)
cat > /opt/amnezia/awg/wg0.conf << EOF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
S3 = 20
S4 = 10
H1 = 0xDEADBEEF
H2 = 0xCAFEBABE
H3 = 0x12345678
H4 = 0x9ABCDEF0
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey =
PresharedKey = $PRESHARED_KEY
AllowedIPs = 10.8.1.2/32
EOF
# Save keys to files on host
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
# Container is already waiting for config (loop), so it should pick it up automatically.
# But we can also force it if needed, or just wait a moment.
# The loop is: while [ ! -f ... ]; do sleep 1; done; wg-quick up ...
# Since we just wrote the file, the loop will exit and run wg-quick up.
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey = $PRESHARED_KEY"
'
WHERE slug = 'amnezia-wg-advanced';
+220
View File
@@ -0,0 +1,220 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
# Function to check if container is healthy
check_container() {
local status
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [ "$status" = "running" ]; then
return 0
elif [ "$status" = "restarting" ]; then
return 2 # Restarting loop
else
return 1 # Stopped or missing
fi
}
# Check for existing configuration on HOST first (preferred persistence)
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration on host."
# Ensure container is running correctly
check_container
STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is in restart loop. Recreating..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
elif [ $STATUS -eq 1 ]; then
# If stopped but exists, remove to recreate with correct flags (just in case)
# Or just start it? Better to recreate to ensure volume mounts are correct.
# But if we recreate, we must ensure we mount the volume.
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
# If container is missing (or we just removed it), create it
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
docker run -d --name "$CONTAINER_NAME" \
--restart always \
--privileged \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
-p "${VPN_PORT}:${VPN_PORT}/udp" \
-v /lib/modules:/lib/modules \
-v /opt/amnezia/awg:/opt/amnezia/awg \
amneziavpn/amnezia-wg:latest \
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
# Wait a moment for it to start
sleep 2
fi
# Extract config from HOST file
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# If no host config, check if container exists and try to rescue config
check_container
STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is restarting and no host config found. Attempting to rescue config..."
# Try to copy from container even if restarting (might fail if container is crashing too fast)
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
echo "Rescued config from broken container."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
# Now recreate container with rescue config
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
# Re-run script to pick up host config (recursive call or just fall through? Fall through requires logic jump)
# Easier to just exit and let user run again? No, let''s proceed.
# We have config on host now, so the logic below for "new install" needs to be skipped or we need to jump back.
# Let''s just restart the script logic by execing self? No, complex.
# Let''s just set a flag.
HAS_RESCUED=1
else
echo "Could not rescue config. Removing broken container."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
HAS_RESCUED=0
fi
elif [ $STATUS -eq 0 ]; then
# Running. Check if it has config inside but not on host (old version)
if docker exec "$CONTAINER_NAME" [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Container running with internal config. Migrating to host..."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
# Recreate to add volume mount
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
HAS_RESCUED=1
else
# Running but no config? Weird. Treat as fresh.
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
HAS_RESCUED=0
fi
else
HAS_RESCUED=0
fi
# If we rescued config, we need to start the container with mounts
if [ "$HAS_RESCUED" = "1" ]; then
docker run -d --name "$CONTAINER_NAME" \
--restart always \
--privileged \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
-p "${VPN_PORT}:${VPN_PORT}/udp" \
-v /lib/modules:/lib/modules \
-v /opt/amnezia/awg:/opt/amnezia/awg \
amneziavpn/amnezia-wg:latest \
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
# Extract and exit (same logic as top)
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# FRESH INSTALL
docker run -d --name "$CONTAINER_NAME" \
--restart always \
--privileged \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
-p "${VPN_PORT}:${VPN_PORT}/udp" \
-v /lib/modules:/lib/modules \
-v /opt/amnezia/awg:/opt/amnezia/awg \
amneziavpn/amnezia-wg:latest \
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
cat > /opt/amnezia/awg/wg0.conf << EOF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
S3 = 20
S4 = 10
H1 = 0xDEADBEEF
H2 = 0xCAFEBABE
H3 = 0x12345678
H4 = 0x9ABCDEF0
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey =
PresharedKey = $PRESHARED_KEY
AllowedIPs = 10.8.1.2/32
EOF
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey = $PRESHARED_KEY"
'
WHERE slug = 'amnezia-wg-advanced';
+213
View File
@@ -0,0 +1,213 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
# Function to check if container is healthy
check_container() {
local status
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [ "$status" = "running" ]; then
return 0
elif [ "$status" = "restarting" ]; then
return 2 # Restarting loop
else
return 1 # Stopped or missing
fi
}
# Check for existing configuration on HOST first (preferred persistence)
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration on host."
# Ensure container is running correctly
STATUS=0
check_container || STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is in restart loop. Recreating..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
elif [ $STATUS -eq 1 ]; then
# If stopped but exists, remove to recreate with correct flags
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
# If container is missing (or we just removed it), create it
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
docker run -d --name "$CONTAINER_NAME" \
--restart always \
--privileged \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
-p "${VPN_PORT}:${VPN_PORT}/udp" \
-v /lib/modules:/lib/modules \
-v /opt/amnezia/awg:/opt/amnezia/awg \
amneziavpn/amnezia-wg:latest \
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
# Wait a moment for it to start
sleep 2
fi
# Extract config from HOST file
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# If no host config, check if container exists and try to rescue config
STATUS=0
check_container || STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is restarting and no host config found. Attempting to rescue config..."
# Try to copy from container even if restarting (might fail if container is crashing too fast)
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
echo "Rescued config from broken container."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
# Now recreate container with rescue config
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
HAS_RESCUED=1
else
echo "Could not rescue config. Removing broken container."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
HAS_RESCUED=0
fi
elif [ $STATUS -eq 0 ]; then
# Running. Check if it has config inside but not on host (old version)
if docker exec "$CONTAINER_NAME" [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Container running with internal config. Migrating to host..."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
# Recreate to add volume mount
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
HAS_RESCUED=1
else
# Running but no config? Weird. Treat as fresh.
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
HAS_RESCUED=0
fi
else
HAS_RESCUED=0
fi
# If we rescued config, we need to start the container with mounts
if [ "$HAS_RESCUED" = "1" ]; then
docker run -d --name "$CONTAINER_NAME" \
--restart always \
--privileged \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
-p "${VPN_PORT}:${VPN_PORT}/udp" \
-v /lib/modules:/lib/modules \
-v /opt/amnezia/awg:/opt/amnezia/awg \
amneziavpn/amnezia-wg:latest \
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
# Extract and exit (same logic as top)
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# FRESH INSTALL
docker run -d --name "$CONTAINER_NAME" \
--restart always \
--privileged \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
-p "${VPN_PORT}:${VPN_PORT}/udp" \
-v /lib/modules:/lib/modules \
-v /opt/amnezia/awg:/opt/amnezia/awg \
amneziavpn/amnezia-wg:latest \
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
cat > /opt/amnezia/awg/wg0.conf << EOF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
S3 = 20
S4 = 10
H1 = 0xDEADBEEF
H2 = 0xCAFEBABE
H3 = 0x12345678
H4 = 0x9ABCDEF0
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey =
PresharedKey = $PRESHARED_KEY
AllowedIPs = 10.8.1.2/32
EOF
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey = $PRESHARED_KEY"
'
WHERE slug = 'amnezia-wg-advanced';
+156
View File
@@ -0,0 +1,156 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
# Function to check if container is healthy
check_container() {
local status
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [ "$status" = "running" ]; then
return 0
elif [ "$status" = "restarting" ]; then
return 2 # Restarting loop
else
return 1 # Stopped or missing
fi
}
# Check for existing configuration on HOST first (preferred persistence)
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration on host."
STATUS=0
check_container || STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is in restart loop. Recreating..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
elif [ $STATUS -eq 1 ]; then
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
# Run container with volume mount - SINGLE LINE to avoid syntax issues
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
fi
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# Rescue logic
STATUS=0
check_container || STATUS=$?
HAS_RESCUED=0
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
echo "Checking for config in existing container..."
# Stop container to ensure stable copy
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
echo "Rescued config from container."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
HAS_RESCUED=1
fi
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
# Start container (Fresh or Rescued)
# SINGLE LINE command
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
if [ "$HAS_RESCUED" = "1" ]; then
# Extract and exit
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# Generate new config
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
cat > /opt/amnezia/awg/wg0.conf << EOF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
S3 = 20
S4 = 10
H1 = 0xDEADBEEF
H2 = 0xCAFEBABE
H3 = 0x12345678
H4 = 0x9ABCDEF0
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey =
PresharedKey = $PRESHARED_KEY
AllowedIPs = 10.8.1.2/32
EOF
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey = $PRESHARED_KEY"
'
WHERE slug = 'amnezia-wg-advanced';
+172
View File
@@ -0,0 +1,172 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
# Function to check if container is healthy
check_container() {
local status
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [ "$status" = "running" ]; then
return 0
elif [ "$status" = "restarting" ]; then
return 2 # Restarting loop
else
return 1 # Stopped or missing
fi
}
# Validate existing config
if [ -f /opt/amnezia/awg/wg0.conf ]; then
if grep -Fq "\$PRIVATE_KEY" /opt/amnezia/awg/wg0.conf; then
echo "Detected broken configuration (unexpanded variables). Removing..."
rm -f /opt/amnezia/awg/wg0.conf
rm -f /opt/amnezia/awg/wireguard_psk.key
rm -f /opt/amnezia/awg/wireguard_server_public_key.key
rm -f /opt/amnezia/awg/wireguard_server_private_key.key
fi
fi
# Check for existing configuration on HOST first (preferred persistence)
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration on host."
STATUS=0
check_container || STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is in restart loop. Recreating..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
elif [ $STATUS -eq 1 ]; then
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
# Run container with volume mount - SINGLE LINE
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
fi
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# Rescue logic
STATUS=0
check_container || STATUS=$?
HAS_RESCUED=0
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
echo "Checking for config in existing container..."
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
# Validate rescued config
if grep -Fq "\$PRIVATE_KEY" /opt/amnezia/awg/wg0.conf; then
echo "Rescued config is broken. Discarding."
rm -f /opt/amnezia/awg/wg0.conf
else
echo "Rescued config from container."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
HAS_RESCUED=1
fi
fi
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
# Start container (Fresh or Rescued)
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
if [ "$HAS_RESCUED" = "1" ]; then
# Extract and exit
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# Generate new config
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
# Use WG_CONF delimiter to avoid EOF replacement in PHP
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
S3 = 20
S4 = 10
H1 = 0xDEADBEEF
H2 = 0xCAFEBABE
H3 = 0x12345678
H4 = 0x9ABCDEF0
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey =
PresharedKey = $PRESHARED_KEY
AllowedIPs = 10.8.1.2/32
WG_CONF
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey = $PRESHARED_KEY"
'
WHERE slug = 'amnezia-wg-advanced';
+177
View File
@@ -0,0 +1,177 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
# Function to check if container is healthy
check_container() {
local status
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [ "$status" = "running" ]; then
return 0
elif [ "$status" = "restarting" ]; then
return 2 # Restarting loop
else
return 1 # Stopped or missing
fi
}
# Validate existing config
if [ -f /opt/amnezia/awg/wg0.conf ]; then
# Check for unexpanded variables
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
echo "Detected broken configuration (unexpanded variables). Removing..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for invalid parameters S3/S4
if grep -Eiq "^S3\s*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4\s*=" /opt/amnezia/awg/wg0.conf; then
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
fi
# Check for existing configuration on HOST first (preferred persistence)
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration on host."
STATUS=0
check_container || STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is in restart loop. Recreating..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
elif [ $STATUS -eq 1 ]; then
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
# Run container with volume mount - SINGLE LINE
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
fi
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# Rescue logic
STATUS=0
check_container || STATUS=$?
HAS_RESCUED=0
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
echo "Checking for config in existing container..."
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
# Validate rescued config
IS_BROKEN=0
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^S3\s*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if [ "$IS_BROKEN" = "1" ]; then
echo "Rescued config is broken. Discarding."
rm -f /opt/amnezia/awg/wg0.conf
else
echo "Rescued config from container."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
HAS_RESCUED=1
fi
fi
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
# Start container (Fresh or Rescued)
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
if [ "$HAS_RESCUED" = "1" ]; then
# Extract and exit
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# Generate new config
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
# Use WG_CONF delimiter to avoid EOF replacement in PHP
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
H1 = 0xDEADBEEF
H2 = 0xCAFEBABE
H3 = 0x12345678
H4 = 0x9ABCDEF0
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey =
PresharedKey = $PRESHARED_KEY
AllowedIPs = 10.8.1.2/32
WG_CONF
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey = $PRESHARED_KEY"
'
WHERE slug = 'amnezia-wg-advanced';
+183
View File
@@ -0,0 +1,183 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
# Function to check if container is healthy
check_container() {
local status
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [ "$status" = "running" ]; then
return 0
elif [ "$status" = "restarting" ]; then
return 2 # Restarting loop
else
return 1 # Stopped or missing
fi
}
# Validate existing config
if [ -f /opt/amnezia/awg/wg0.conf ]; then
# Check for unexpanded variables
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
echo "Detected broken configuration (unexpanded variables). Removing..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for invalid parameters S3/S4
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for hex H-params
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
echo "Detected invalid hex parameters (H1-H4). Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
fi
# Check for existing configuration on HOST first (preferred persistence)
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration on host."
STATUS=0
check_container || STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is in restart loop. Recreating..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
elif [ $STATUS -eq 1 ]; then
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
# Run container with volume mount - SINGLE LINE
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
fi
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# Rescue logic
STATUS=0
check_container || STATUS=$?
HAS_RESCUED=0
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
echo "Checking for config in existing container..."
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
# Validate rescued config
IS_BROKEN=0
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if [ "$IS_BROKEN" = "1" ]; then
echo "Rescued config is broken. Discarding."
rm -f /opt/amnezia/awg/wg0.conf
else
echo "Rescued config from container."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
HAS_RESCUED=1
fi
fi
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
# Start container (Fresh or Rescued)
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
if [ "$HAS_RESCUED" = "1" ]; then
# Extract and exit
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# Generate new config
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
# Use WG_CONF delimiter to avoid EOF replacement in PHP
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
H1 = 1
H2 = 2
H3 = 3
H4 = 4
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey =
PresharedKey = $PRESHARED_KEY
AllowedIPs = 10.8.1.2/32
WG_CONF
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey = $PRESHARED_KEY"
'
WHERE slug = 'amnezia-wg-advanced';
+184
View File
@@ -0,0 +1,184 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
# Function to check if container is healthy
check_container() {
local status
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [ "$status" = "running" ]; then
return 0
elif [ "$status" = "restarting" ]; then
return 2 # Restarting loop
else
return 1 # Stopped or missing
fi
}
# Validate existing config
if [ -f /opt/amnezia/awg/wg0.conf ]; then
# Check for unexpanded variables
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
echo "Detected broken configuration (unexpanded variables). Removing..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for invalid parameters S3/S4
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for hex H-params
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
echo "Detected invalid hex parameters (H1-H4). Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for empty PublicKey
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then
echo "Detected empty PublicKey. Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
fi
# Check for existing configuration on HOST first (preferred persistence)
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration on host."
STATUS=0
check_container || STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is in restart loop. Recreating..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
elif [ $STATUS -eq 1 ]; then
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
# Run container with volume mount - SINGLE LINE
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
fi
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# Rescue logic
STATUS=0
check_container || STATUS=$?
HAS_RESCUED=0
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
echo "Checking for config in existing container..."
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
# Validate rescued config
IS_BROKEN=0
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if [ "$IS_BROKEN" = "1" ]; then
echo "Rescued config is broken. Discarding."
rm -f /opt/amnezia/awg/wg0.conf
else
echo "Rescued config from container."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
HAS_RESCUED=1
fi
fi
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
# Start container (Fresh or Rescued)
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
if [ "$HAS_RESCUED" = "1" ]; then
# Extract and exit
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
exit 0
fi
# Generate new config
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
# Use WG_CONF delimiter to avoid EOF replacement in PHP
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
H1 = 1
H2 = 2
H3 = 3
H4 = 4
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
WG_CONF
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey = $PRESHARED_KEY"
'
WHERE slug = 'amnezia-wg-advanced';
+188
View File
@@ -0,0 +1,188 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
# Function to check if container is healthy
check_container() {
local status
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [ "$status" = "running" ]; then
return 0
elif [ "$status" = "restarting" ]; then
return 2 # Restarting loop
else
return 1 # Stopped or missing
fi
}
# Validate existing config
if [ -f /opt/amnezia/awg/wg0.conf ]; then
# Check for unexpanded variables
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
echo "Detected broken configuration (unexpanded variables). Removing..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for invalid parameters S3/S4
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for hex H-params
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
echo "Detected invalid hex parameters (H1-H4). Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for empty PublicKey
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then
echo "Detected empty PublicKey. Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
fi
# Check for existing configuration on HOST first (preferred persistence)
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration on host."
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
VPN_PORT=${PORT:-$VPN_PORT}
STATUS=0
check_container || STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is in restart loop. Recreating..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
STATUS=1
elif [ $STATUS -eq 0 ]; then
echo "Container is running."
fi
# Ensure container is running
if [ $STATUS -ne 0 ]; then
echo "Starting container..."
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
fi
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: $VPN_PORT"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
exit 0
fi
# Rescue logic
STATUS=0
check_container || STATUS=$?
HAS_RESCUED=0
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
echo "Checking for config in existing container..."
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
# Validate rescued config
IS_BROKEN=0
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if [ "$IS_BROKEN" = "1" ]; then
echo "Rescued config is broken. Discarding."
rm -f /opt/amnezia/awg/wg0.conf
else
echo "Rescued config from container."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
HAS_RESCUED=1
fi
fi
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
# Start container (Fresh or Rescued)
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
if [ "$HAS_RESCUED" = "1" ]; then
# Extract and exit
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
exit 0
fi
# Generate new config
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
# Use WG_CONF delimiter to avoid EOF replacement in PHP
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
H1 = 1
H2 = 2
H3 = 3
H4 = 4
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
WG_CONF
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey: $PRESHARED_KEY"
'
WHERE slug = 'amnezia-wg-advanced';
+22
View File
@@ -0,0 +1,22 @@
-- Backfill XRay server_port from extras.result keys produced by set -x
UPDATE server_protocols sp
JOIN protocols p ON p.id = sp.protocol_id
SET sp.config_data = JSON_SET(sp.config_data, '$.server_port', JSON_EXTRACT(sp.config_data, '$.extras.result."+_xray_port"')),
sp.applied_at = NOW()
WHERE p.slug = 'xray-vless'
AND (
JSON_EXTRACT(sp.config_data, '$.server_port') IS NULL OR
JSON_UNQUOTE(JSON_EXTRACT(sp.config_data, '$.server_port')) = ''
)
AND JSON_EXTRACT(sp.config_data, '$.extras.result."+_xray_port"') IS NOT NULL;
UPDATE server_protocols sp
JOIN protocols p ON p.id = sp.protocol_id
SET sp.config_data = JSON_SET(sp.config_data, '$.server_port', JSON_EXTRACT(sp.config_data, '$.extras.result.xray_port')),
sp.applied_at = NOW()
WHERE p.slug = 'xray-vless'
AND (
JSON_EXTRACT(sp.config_data, '$.server_port') IS NULL OR
JSON_UNQUOTE(JSON_EXTRACT(sp.config_data, '$.server_port')) = ''
)
AND JSON_EXTRACT(sp.config_data, '$.extras.result.xray_port') IS NOT NULL;
+18
View File
@@ -0,0 +1,18 @@
-- Update XRay VLESS protocol to Reality/Vision setup
UPDATE protocols SET install_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nXRAY_PORT=$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))\n\nPRIVATE_KEY=$(docker run --rm teddysun/xray xray x25519 | grep "Private key:" | awk ''{print $3}'')\nPUBLIC_KEY=$(docker run --rm teddysun/xray xray x25519 -i "$PRIVATE_KEY" | grep "Public key:" | awk ''{print $3}'')\nSHORT_ID=$(openssl rand -hex 8)\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n\nSERVER_NAME="www.googletagmanager.com"\nFINGERPRINT="chrome"\nSPIDER_X="/"\n\ndocker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n\ncat > /opt/amnezia/xray/server.json << EOF\n{\n "log": { "loglevel": "warning" },\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": ${XRAY_PORT},\n "protocol": "vless",\n "settings": {\n "clients": [ { "id": "${CLIENT_ID}" } ],\n "decryption": "none",\n "fallbacks": [ { "dest": 80 } ]\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": {\n "show": false,\n "dest": "${SERVER_NAME}:443",\n "xver": 0,\n "serverNames": [ "${SERVER_NAME}" ],\n "privateKey": "${PRIVATE_KEY}",\n "shortIds": [ "${SHORT_ID}" ],\n "fingerprint": "${FINGERPRINT}",\n "spiderX": "${SPIDER_X}"\n }\n }\n }\n ],\n "outbounds": [ { "protocol": "freedom", "tag": "direct" } ]\n}\nEOF\n\n# start container\ndocker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:${XRAY_PORT}" \\\n -v /opt/amnezia/xray:/opt/amnezia/xray \\\n teddysun/xray xray run -c /opt/amnezia/xray/server.json\n\nsleep 2\n\n# panel output\necho "Port: ${XRAY_PORT}"\necho "ClientID: ${CLIENT_ID}"\necho "PublicKey: ${PUBLIC_KEY}"\necho "PrivateKey: ${PRIVATE_KEY}"\necho "ShortID: ${SHORT_ID}"\necho "ServerName: ${SERVER_NAME}"',
output_template = 'vless://{{client_id}}@{{server_host}}:{{server_port}}?encryption=none&flow=xtls-rprx-vision&security=reality&sni={{reality_server_name}}&fp=chrome&pbk={{reality_public_key}}&sid={{reality_short_id}}&type=tcp'
WHERE slug = 'xray-vless';
-- Ensure protocol variables exist
SET @pid = (SELECT id FROM protocols WHERE slug = 'xray-vless');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, description, required)
SELECT @pid, 'reality_public_key', 'string', 'Reality public key (base64url)', true
WHERE NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=@pid AND variable_name='reality_public_key');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, description, required)
SELECT @pid, 'reality_short_id', 'string', 'Reality shortId', true
WHERE NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=@pid AND variable_name='reality_short_id');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, description, required)
SELECT @pid, 'reality_server_name', 'string', 'SNI server name for Reality', true
WHERE NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=@pid AND variable_name='reality_server_name');
+4
View File
@@ -0,0 +1,4 @@
-- Set uninstall script for XRay VLESS protocol
UPDATE protocols
SET uninstall_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${SERVER_CONTAINER:-${CONTAINER_NAME:-amnezia-xray}}"\n\ndocker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true\nrm -rf /opt/amnezia/xray || true\n\necho "Uninstalled: ${CONTAINER_NAME}"\n'
WHERE slug = 'xray-vless';
@@ -0,0 +1,4 @@
-- Make XRay Reality install script robust (keys generation and container name)
UPDATE protocols
SET install_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="amnezia-xray"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nXRAY_PORT=$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))\n\n# ensure image exists to avoid pull logs mixing with key output\ndocker pull teddysun/xray >/dev/null 2>&1 || true\n\nGEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)\nPRIVATE_KEY=$(printf "%s\n" "$GEN" | awk -F": " "/[Pp]rivate/ {print \\$2}" | tr -d " \\t\\r\\n")\nPUBLIC_KEY=$(printf "%s\n" "$GEN" | awk -F": " "/[Pp]ublic/ {print \\$2}" | tr -d " \\t\\r\\n")\n\nif [ -z "$PRIVATE_KEY" ]; then\n PRIVATE_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null | awk -F": " "/[Pp]rivate/ {print \\$2}" | tr -d " \\t\\r\\n" || true)\nfi\nif [ -z "$PUBLIC_KEY" ]; then\n PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | awk -F": " "/[Pp]ublic/ {print \\$2}" | tr -d " \\t\\r\\n" || true)\nfi\n\nSHORT_ID=$(openssl rand -hex 8)\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n\nSERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"\nFINGERPRINT="${FINGERPRINT:-chrome}"\nSPIDER_X="${SPIDER_X:-/}"\n\ndocker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n\ncat > /opt/amnezia/xray/server.json << EOF\n{\n "log": { "loglevel": "warning" },\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": ${XRAY_PORT},\n "protocol": "vless",\n "settings": {\n "clients": [ { "id": "${CLIENT_ID}" } ],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": {\n "show": false,\n "dest": "${SERVER_NAME}:443",\n "xver": 0,\n "serverNames": [ "${SERVER_NAME}" ],\n "privateKey": "${PRIVATE_KEY}",\n "shortIds": [ "${SHORT_ID}" ],\n "fingerprint": "${FINGERPRINT}",\n "spiderX": "${SPIDER_X}"\n }\n }\n }\n ],\n "outbounds": [ { "protocol": "freedom", "tag": "direct" } ]\n}\nEOF\n\n# start container\ndocker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:${XRAY_PORT}" \\\n -v /opt/amnezia/xray:/opt/amnezia/xray \\\n teddysun/xray xray run -c /opt/amnezia/xray/server.json\n\nsleep 2\n\n# panel output\necho "Port: ${XRAY_PORT}"\necho "ClientID: ${CLIENT_ID}"\necho "PublicKey: ${PUBLIC_KEY}"\necho "PrivateKey: ${PRIVATE_KEY}"\necho "ShortID: ${SHORT_ID}"\necho "ServerName: ${SERVER_NAME}"\necho "ContainerName: ${CONTAINER_NAME}"'
WHERE slug = 'xray-vless';
+4
View File
@@ -0,0 +1,4 @@
-- Fix PublicKey extraction and ShortID generation in XRay install script
UPDATE protocols
SET install_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nXRAY_PORT=$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))\n\n# Ensure image present to avoid noisy pull output\ndocker pull teddysun/xray >/dev/null 2>&1 || true\n\n# Generate keys\nGEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)\nPRIVATE_KEY=$(printf "%s\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \t\r\n")\nPUBLIC_KEY=$(printf "%s\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \t\r\n")\n\nif [ -z "$PUBLIC_KEY" ]; then\n PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \t\r\n" || true)\nfi\n\n# Generate shortId without openssl\nSHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \n")\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n\nSERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"\nFINGERPRINT="${FINGERPRINT:-chrome}"\nSPIDER_X="${SPIDER_X:-/}"\n\ndocker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n\ncat > /opt/amnezia/xray/server.json << EOF\n{\n "log": { "loglevel": "warning" },\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": ${XRAY_PORT},\n "protocol": "vless",\n "settings": {\n "clients": [ { "id": "${CLIENT_ID}" } ],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": {\n "show": false,\n "dest": "${SERVER_NAME}:443",\n "xver": 0,\n "serverNames": [ "${SERVER_NAME}" ],\n "privateKey": "${PRIVATE_KEY}",\n "shortIds": [ "${SHORT_ID}" ],\n "fingerprint": "${FINGERPRINT}",\n "spiderX": "${SPIDER_X}"\n }\n }\n }\n ],\n "outbounds": [ { "protocol": "freedom", "tag": "direct" } ]\n}\nEOF\n\n# Start container\ndocker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:${XRAY_PORT}" \\\n -v /opt/amnezia/xray:/opt/amnezia/xray \\\n teddysun/xray xray run -c /opt/amnezia/xray/server.json\n\nsleep 2\n\n# Output configuration\necho "Port: ${XRAY_PORT}"\necho "ClientID: ${CLIENT_ID}"\necho "PublicKey: ${PUBLIC_KEY}"\necho "PrivateKey: ${PRIVATE_KEY}"\necho "ShortID: ${SHORT_ID}"\necho "ServerName: ${SERVER_NAME}"\necho "ContainerName: ${CONTAINER_NAME}"'
WHERE slug = 'xray-vless';
@@ -0,0 +1,3 @@
UPDATE protocols
SET install_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nXRAY_PORT=${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}\n\n# Ensure image present to avoid noisy pull output\ndocker pull teddysun/xray >/dev/null 2>&1 || true\n\n# Generate keys\nGEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)\nPRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")\nPUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")\n\nif [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then\n PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)\nfi\n\nSHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n\nSERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"\nFINGERPRINT="${FINGERPRINT:-chrome}"\nSPIDER_X="${SPIDER_X:-/}"\n\ndocker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n\ncat > /opt/amnezia/xray/server.json << EOF\n{\n "log": { "loglevel": "warning" },\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": ${XRAY_PORT},\n "protocol": "vless",\n "settings": {\n "clients": [ { "id": "${CLIENT_ID}" } ],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": {\n "show": false,\n "dest": "${SERVER_NAME}:443",\n "xver": 0,\n "serverNames": [ "${SERVER_NAME}" ],\n "privateKey": "${PRIVATE_KEY}",\n "shortIds": [ "${SHORT_ID}" ],\n "fingerprint": "${FINGERPRINT}",\n "spiderX": "${SPIDER_X}"\n }\n }\n }\n ],\n "outbounds": [ { "protocol": "freedom", "tag": "direct" } ]\n}\nEOF\n\n# Start container\ndocker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:${XRAY_PORT}" \\\n -v /opt/amnezia/xray:/opt/amnezia/xray \\\n teddysun/xray xray run -c /opt/amnezia/xray/server.json\n\nsleep 2\n\n# Output configuration\necho "XrayPort: ${XRAY_PORT}"\necho "Port: ${XRAY_PORT}"\necho "ClientID: ${CLIENT_ID}"\necho "PublicKey: ${PUBLIC_KEY}"\necho "PrivateKey: ${PRIVATE_KEY}"\necho "ShortID: ${SHORT_ID}"\necho "ServerName: ${SERVER_NAME}"\necho "ContainerName: ${CONTAINER_NAME}"'
WHERE slug = 'xray-vless';
+3
View File
@@ -0,0 +1,3 @@
UPDATE protocols
SET install_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"\nXRAY_PORT=${SERVER_PORT:-443}\n\n# Ensure image present to avoid noisy pull output\ndocker pull teddysun/xray >/dev/null 2>&1 || true\n\n# Generate keys\nGEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)\nPRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")\nPUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")\n\nif [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then\n PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)\nfi\n\nSHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n\nSERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"\nFINGERPRINT="${FINGERPRINT:-chrome}"\nSPIDER_X="${SPIDER_X:-/}"\n\ndocker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n\ncat > /opt/amnezia/xray/server.json << EOF\n{\n "log": { "loglevel": "warning" },\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": ${XRAY_PORT},\n "protocol": "vless",\n "settings": {\n "clients": [ { "id": "${CLIENT_ID}" } ],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": {\n "show": false,\n "dest": "${SERVER_NAME}:443",\n "xver": 0,\n "serverNames": [ "${SERVER_NAME}" ],\n "privateKey": "${PRIVATE_KEY}",\n "shortIds": [ "${SHORT_ID}" ],\n "fingerprint": "${FINGERPRINT}",\n "spiderX": "${SPIDER_X}"\n }\n }\n }\n ],\n "outbounds": [ { "protocol": "freedom", "tag": "direct" } ]\n}\nEOF\n\n# Start container\ndocker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:${XRAY_PORT}" \\\n -v /opt/amnezia/xray:/opt/amnezia/xray \\\n teddysun/xray xray run -c /opt/amnezia/xray/server.json\n\nsleep 2\n\n# Output configuration\necho "XrayPort: ${XRAY_PORT}"\necho "Port: ${XRAY_PORT}"\necho "ClientID: ${CLIENT_ID}"\necho "PublicKey: ${PUBLIC_KEY}"\necho "PrivateKey: ${PRIVATE_KEY}"\necho "ShortID: ${SHORT_ID}"\necho "ServerName: ${SERVER_NAME}"\necho "ContainerName: ${CONTAINER_NAME}"'
WHERE slug = 'xray-vless';
+49
View File
@@ -0,0 +1,49 @@
ALTER TABLE protocols ADD COLUMN qr_code_template MEDIUMTEXT DEFAULT NULL;
ALTER TABLE protocols ADD COLUMN qr_code_format VARCHAR(50) DEFAULT 'amnezia_compressed';
-- Update AmneziaWG and WireGuard
UPDATE protocols SET qr_code_template = '{
"containers": [
{
"awg": {
"H1": "{{H1}}",
"H2": "{{H2}}",
"H3": "{{H3}}",
"H4": "{{H4}}",
"Jc": "{{Jc}}",
"Jmax": "{{Jmax}}",
"Jmin": "{{Jmin}}",
"S1": "{{S1}}",
"S2": "{{S2}}",
"last_config": {{last_config_json}},
"port": "{{port}}",
"transport_proto": "udp"
},
"container": "amnezia-awg"
}
],
"defaultContainer": "amnezia-awg",
"description": "{{description}}",
"dns1": "{{dns1}}",
"dns2": "{{dns2}}",
"hostName": "{{hostName}}"
}' WHERE slug IN ('amnezia-wg', 'wireguard', 'amnezia-wg-advanced');
-- Update XRay
UPDATE protocols SET qr_code_template = '{
"containers": [
{
"container": "amnezia-xray",
"xray": {
"last_config": {{last_config_json}},
"port": "{{port}}",
"transport_proto": "tcp"
}
}
],
"defaultContainer": "amnezia-xray",
"description": "{{description}}",
"dns1": "1.1.1.1",
"dns2": "1.0.0.1",
"hostName": "{{hostName}}"
}' WHERE slug LIKE '%xray%';
@@ -0,0 +1,10 @@
-- Add translations for QR code template UI
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'protocols', 'qr_code_template', 'QR Code Template'),
('en', 'protocols', 'qr_code_format', 'QR Code Format'),
('en', 'protocols', 'qr_code_format_help', 'Select the format for the QR code payload. "Amnezia Compressed" uses the legacy Qt/QDataStream format. "Raw Content" uses the template output directly.'),
('en', 'protocols', 'qr_code_template_help', 'Template for the QR code payload. Use {{last_config_json}} to include the full configuration as a JSON object.'),
('en', 'protocols', 'variable_last_config_json_help', 'Full configuration as a JSON object (required for Amnezia format)'),
('en', 'protocols', 'plus_all_output_variables', 'Plus all variables from the Output Template section'),
('en', 'ai', 'prompt_placeholder_qr_template', 'Describe how the QR code payload should be structured (e.g., "Standard WireGuard config format" or "JSON with specific fields")')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
@@ -0,0 +1,70 @@
-- Add Russian translations for Protocol Editor
INSERT INTO translations (locale, category, key_name, translation) VALUES
('ru', 'protocols', 'edit_protocol', 'Редактирование протокола'),
('ru', 'protocols', 'create_protocol', 'Создание протокола'),
('ru', 'protocols', 'edit_protocol_description', 'Изменение настроек и скриптов протокола'),
('ru', 'protocols', 'create_protocol_description', 'Добавление нового протокола в систему'),
('ru', 'protocols', 'back_to_protocols', 'К списку протоколов'),
('ru', 'protocols', 'basic_information', 'Основная информация'),
('ru', 'protocols', 'name_label', 'Название'),
('ru', 'protocols', 'name_help', 'Отображаемое имя протокола'),
('ru', 'protocols', 'slug_label', 'Слаг (ID)'),
('ru', 'protocols', 'slug_help', 'Уникальный идентификатор (латиница, цифры, дефис)'),
('ru', 'protocols', 'description_help', 'Краткое описание протокола'),
('ru', 'protocols', 'installation_script', 'Скрипт установки'),
('ru', 'protocols', 'install_script_help', 'Bash скрипт, который будет выполнен при установке протокола'),
('ru', 'protocols', 'uninstallation_script', 'Скрипт удаления'),
('ru', 'protocols', 'uninstall_script_help', 'Bash скрипт, который будет выполнен при удалении протокола'),
('ru', 'protocols', 'test_install', 'Тест установки'),
('ru', 'protocols', 'test_uninstall', 'Тест удаления'),
('ru', 'protocols', 'testing_on_ubuntu22', 'Тестирование на Ubuntu 22.04 (Docker)'),
('ru', 'protocols', 'test_result', 'Результат выполнения'),
('ru', 'protocols', 'client_output_preview', 'Предпросмотр конфига клиента'),
('ru', 'protocols', 'output_template', 'Шаблон конфигурации'),
('ru', 'protocols', 'output_template_help', 'Шаблон для генерации файла конфигурации клиента. Используйте переменные {{variable}}'),
('ru', 'protocols', 'available_variables', 'Доступные переменные'),
('ru', 'protocols', 'variable_private_key_help', 'Приватный ключ клиента'),
('ru', 'protocols', 'variable_public_key_help', 'Публичный ключ сервера'),
('ru', 'protocols', 'variable_client_ip_help', 'IP-адрес клиента'),
('ru', 'protocols', 'variable_server_host_help', 'Хост сервера (IP или домен)'),
('ru', 'protocols', 'variable_server_port_help', 'Порт сервера'),
('ru', 'protocols', 'variable_preshared_key_help', 'Дополнительный ключ шифрования (PSK)'),
('ru', 'protocols', 'variable_last_config_json_help', 'Полная конфигурация в формате JSON (для Amnezia)'),
('ru', 'protocols', 'plus_all_output_variables', 'Плюс все переменные из шаблона конфигурации'),
('ru', 'protocols', 'qr_code_template', 'Шаблон QR-кода'),
('ru', 'protocols', 'qr_code_template_help', 'Шаблон для формирования содержимого QR-кода'),
('ru', 'protocols', 'qr_code_format', 'Формат QR-кода'),
('ru', 'protocols', 'qr_code_format_help', 'Выберите формат данных в QR-коде'),
('ru', 'protocols', 'password_generation', 'Генерация пароля'),
('ru', 'protocols', 'password_command_help', 'Команда для генерации пароля/ключа (выполняется перед установкой)'),
('ru', 'protocols', 'ubuntu_compatible', 'Совместим с Ubuntu'),
('ru', 'protocols', 'active_label', 'Активен'),
('ru', 'protocols', 'update_protocol', 'Обновить протокол'),
('ru', 'protocols', 'save_protocol', 'Сохранить протокол'),
('ru', 'protocols', 'please_fill_required_fields', 'Пожалуйста, заполните обязательные поля'),
('ru', 'protocols', 'invalid_slug_format', 'Неверный формат слага'),
('ru', 'ai', 'get_ai_help', 'Помощь AI'),
('ru', 'ai', 'assistant', 'AI Ассистент'),
('ru', 'ai', 'select_model', 'Выберите модель'),
('ru', 'ai', 'model_gpt35_turbo', 'GPT-3.5 Turbo'),
('ru', 'ai', 'model_gpt4', 'GPT-4'),
('ru', 'ai', 'model_claude3_haiku', 'Claude 3 Haiku'),
('ru', 'ai', 'model_claude3_sonnet', 'Claude 3 Sonnet'),
('ru', 'ai', 'custom_model_placeholder', 'Или введите имя модели вручную'),
('ru', 'ai', 'check_availability', 'Проверить'),
('ru', 'ai', 'protocol_type', 'Тип протокола'),
('ru', 'ai', 'general_vpn', 'Общий VPN'),
('ru', 'ai', 'describe_requirements', 'Опишите требования'),
('ru', 'ai', 'prompt_placeholder', 'Например: Скрипт для установки Shadowsocks на порт 8388...'),
('ru', 'ai', 'prompt_placeholder_template', 'Например: Конфиг в формате JSON с полями server, port, password...'),
('ru', 'ai', 'prompt_placeholder_qr_template', 'Например: Ссылка вида vless://uuid@host:port...'),
('ru', 'ai', 'prompt_placeholder_uninstall', 'Например: Остановить docker контейнер и удалить файлы...'),
('ru', 'ai', 'generate_script', 'Сгенерировать'),
('ru', 'ai', 'generating_script', 'Генерация...'),
('ru', 'ai', 'generated_script', 'Результат'),
('ru', 'ai', 'suggestions', 'Предложения'),
('ru', 'ai', 'apply_to_current_protocol', 'Применить'),
('ru', 'ai', 'confirm_apply_script', 'Это заменит текущее содержимое поля. Продолжить?'),
('ru', 'ai', 'please_enter_requirements', 'Пожалуйста, введите требования'),
('ru', 'ai', 'error_generating_script', 'Ошибка генерации')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
+10
View File
@@ -0,0 +1,10 @@
-- Add show_text_content column to protocols
ALTER TABLE protocols ADD COLUMN show_text_content TINYINT(1) NOT NULL DEFAULT 0;
-- Add translations
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'protocols', 'show_text_content', 'Show text content on client page'),
('en', 'protocols', 'qr_code_format_text', 'Simple Text'),
('ru', 'protocols', 'show_text_content', 'Показывать текстовое содержимое на странице клиента'),
('ru', 'protocols', 'qr_code_format_text', 'Простой текст')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
+188
View File
@@ -0,0 +1,188 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
# Function to check if container is healthy
check_container() {
local status
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [ "$status" = "running" ]; then
return 0
elif [ "$status" = "restarting" ]; then
return 2 # Restarting loop
else
return 1 # Stopped or missing
fi
}
# Validate existing config
if [ -f /opt/amnezia/awg/wg0.conf ]; then
# Check for unexpanded variables
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
echo "Detected broken configuration (unexpanded variables). Removing..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for invalid parameters S3/S4
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for hex H-params
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
echo "Detected invalid hex parameters (H1-H4). Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for empty PublicKey
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then
echo "Detected empty PublicKey. Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
fi
# Check for existing configuration on HOST first (preferred persistence)
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration on host."
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
VPN_PORT=${PORT:-$VPN_PORT}
STATUS=0
check_container || STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is in restart loop. Recreating..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
STATUS=1
elif [ $STATUS -eq 0 ]; then
echo "Container is running."
fi
# Ensure container is running
if [ $STATUS -ne 0 ]; then
echo "Starting container..."
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
fi
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: $VPN_PORT"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
exit 0
fi
# Rescue logic
STATUS=0
check_container || STATUS=$?
HAS_RESCUED=0
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
echo "Checking for config in existing container..."
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
# Validate rescued config
IS_BROKEN=0
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if [ "$IS_BROKEN" = "1" ]; then
echo "Rescued config is broken. Discarding."
rm -f /opt/amnezia/awg/wg0.conf
else
echo "Rescued config from container."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
HAS_RESCUED=1
fi
fi
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
# Start container (Fresh or Rescued)
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
if [ "$HAS_RESCUED" = "1" ]; then
# Extract and exit
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
exit 0
fi
# Generate new config
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
# Use WG_CONF delimiter to avoid EOF replacement in PHP
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
H1 = 1
H2 = 2
H3 = 3
H4 = 4
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
WG_CONF
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey: $PRESHARED_KEY"
'
WHERE slug = 'amnezia-wg-advanced';
+288
View File
@@ -0,0 +1,288 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
# Function to check if container is healthy
check_container() {
local status
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [ "$status" = "running" ]; then
return 0
elif [ "$status" = "restarting" ]; then
return 2 # Restarting loop
else
return 1 # Stopped or missing
fi
}
# Validate existing config
if [ -f /opt/amnezia/awg/wg0.conf ]; then
# Check for unexpanded variables
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
echo "Detected broken configuration (unexpanded variables). Removing..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for invalid parameters S3/S4
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for hex H-params
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
echo "Detected invalid hex parameters (H1-H4). Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for empty PublicKey
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then
echo "Detected empty PublicKey. Removing config to regenerate..."
rm -f /opt/amnezia/awg/wg0.conf
fi
fi
# Check for existing configuration on HOST first (preferred persistence)
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration on host."
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
VPN_PORT=${PORT:-$VPN_PORT}
STATUS=0
check_container || STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Container is in restart loop. Recreating..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
STATUS=1
elif [ $STATUS -eq 0 ]; then
echo "Container is running."
fi
# Ensure container is running
if [ $STATUS -ne 0 ]; then
echo "Starting container..."
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
fi
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: $VPN_PORT"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
# Output variables for preview
echo "Variable: server_port=$VPN_PORT"
echo "Variable: server_public_key=$PUBKEY"
echo "Variable: preshared_key=$PSK"
echo "Variable: server_host=YOUR_IP"
# Dummy client vars for preview
CLIENT_PRIV_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
echo "Variable: private_key=$CLIENT_PRIV_KEY"
echo "Variable: client_ip=10.8.1.2"
echo "Variable: dns_servers=1.1.1.1"
# Obfuscation params (extract from config if possible, else defaults)
JC=$(grep -E "^Jc" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
JMIN=$(grep -E "^Jmin" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
JMAX=$(grep -E "^Jmax" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
S1=$(grep -E "^S1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
S2=$(grep -E "^S2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
H1=$(grep -E "^H1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
H2=$(grep -E "^H2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
H3=$(grep -E "^H3" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
H4=$(grep -E "^H4" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
echo "Variable: Jc=${JC:-5}"
echo "Variable: JC=${JC:-5}"
echo "Variable: Jmin=${JMIN:-100}"
echo "Variable: JMIN=${JMIN:-100}"
echo "Variable: Jmax=${JMAX:-200}"
echo "Variable: JMAX=${JMAX:-200}"
echo "Variable: S1=${S1:-50}"
echo "Variable: S2=${S2:-100}"
echo "Variable: H1=${H1:-1}"
echo "Variable: H2=${H2:-2}"
echo "Variable: H3=${H3:-3}"
echo "Variable: H4=${H4:-4}"
exit 0
fi
# Rescue logic
STATUS=0
check_container || STATUS=$?
HAS_RESCUED=0
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
echo "Checking for config in existing container..."
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
# Validate rescued config
IS_BROKEN=0
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
if [ "$IS_BROKEN" = "1" ]; then
echo "Rescued config is broken. Discarding."
rm -f /opt/amnezia/awg/wg0.conf
else
echo "Rescued config from container."
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
HAS_RESCUED=1
fi
fi
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
# Start container (Fresh or Rescued)
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
if [ "$HAS_RESCUED" = "1" ]; then
# Extract and exit
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
# Output variables for preview
echo "Variable: server_port=$VPN_PORT"
echo "Variable: server_public_key=$PUBKEY"
echo "Variable: preshared_key=$PSK"
echo "Variable: server_host=YOUR_IP"
# Dummy client vars for preview
CLIENT_PRIV_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
echo "Variable: private_key=$CLIENT_PRIV_KEY"
echo "Variable: client_ip=10.8.1.2"
echo "Variable: dns_servers=1.1.1.1"
# Obfuscation params (extract from config if possible, else defaults)
JC=$(grep -E "^Jc" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
JMIN=$(grep -E "^Jmin" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
JMAX=$(grep -E "^Jmax" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
S1=$(grep -E "^S1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
S2=$(grep -E "^S2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
H1=$(grep -E "^H1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
H2=$(grep -E "^H2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
H3=$(grep -E "^H3" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
H4=$(grep -E "^H4" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
echo "Variable: Jc=${JC:-5}"
echo "Variable: JC=${JC:-5}"
echo "Variable: JMIN=${JMIN:-100}"
echo "Variable: Jmin=${JMIN:-100}"
echo "Variable: JMAX=${JMAX:-200}"
echo "Variable: Jmax=${JMAX:-200}"
echo "Variable: S1=${S1:-50}"
echo "Variable: S2=${S2:-100}"
echo "Variable: H1=${H1:-1}"
echo "Variable: H2=${H2:-2}"
echo "Variable: H3=${H3:-3}"
echo "Variable: H4=${H4:-4}"
exit 0
fi
# Generate new config
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
# Use WG_CONF delimiter to avoid EOF replacement in PHP
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = 5
Jmin = 100
Jmax = 200
S1 = 50
S2 = 100
H1 = 1
H2 = 2
H3 = 3
H4 = 4
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
WG_CONF
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey: $PRESHARED_KEY"
# Output variables for preview
echo "Variable: server_port=$VPN_PORT"
echo "Variable: server_public_key=$PUBLIC_KEY"
echo "Variable: preshared_key=$PRESHARED_KEY"
echo "Variable: server_host=YOUR_IP"
# Dummy client vars for preview
CLIENT_PRIV_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
echo "Variable: private_key=$CLIENT_PRIV_KEY"
echo "Variable: client_ip=10.8.1.2"
echo "Variable: dns_servers=1.1.1.1"
# Obfuscation params (hardcoded in new config)
echo "Variable: Jc=5"
echo "Variable: JC=5"
echo "Variable: Jmin=100"
echo "Variable: JMIN=100"
echo "Variable: Jmax=200"
echo "Variable: JMAX=200"
echo "Variable: S1=50"
echo "Variable: S2=100"
echo "Variable: H1=1"
echo "Variable: H2=2"
echo "Variable: H3=3"
echo "Variable: H4=4"
'
WHERE slug = 'amnezia-wg-advanced';
+5
View File
@@ -0,0 +1,5 @@
-- Fix AmneziaWG MTU to 1280 for better compatibility
-- This resolves connection issues with PPPoE, mobile networks, and tunnels
UPDATE protocols SET
install_script = REPLACE(install_script, 'MTU=${MTU:-1420}', 'MTU=${MTU:-1280}')
WHERE slug = 'amnezia-wg-advanced';
@@ -0,0 +1,52 @@
-- Fix AmneziaWG client config template: add MTU and use capital letter variables
UPDATE protocols SET
output_template = '[Interface]
PrivateKey = {{private_key}}
Address = {{client_ip}}/32
DNS = {{dns_servers}}
MTU = 1280
Jc = {{Jc}}
Jmin = {{Jmin}}
Jmax = {{Jmax}}
S1 = {{S1}}
S2 = {{S2}}
H1 = {{H1}}
H2 = {{H2}}
H3 = {{H3}}
H4 = {{H4}}
[Peer]
PublicKey = {{server_public_key}}
PresharedKey = {{preshared_key}}
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = {{server_host}}:{{server_port}}
PersistentKeepalive = 25'
WHERE slug = 'amnezia-wg-advanced';
-- Create capital letter variables that map to lowercase ones
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'Jc', 'number', '5', 'Junk packet count', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='Jc');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'Jmin', 'number', '100', 'Minimum junk packet size', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='Jmin');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'Jmax', 'number', '200', 'Maximum junk packet size', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='Jmax');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'S1', 'number', '50', 'Junk packet size 1', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='S1');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'S2', 'number', '100', 'Junk packet size 2', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='S2');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'H1', 'number', '1', 'Obfuscation header 1', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='H1');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'H2', 'number', '2', 'Obfuscation header 2', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='H2');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'H3', 'number', '3', 'Obfuscation header 3', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='H3');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'H4', 'number', '4', 'Obfuscation header 4', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='H4');
@@ -0,0 +1,17 @@
-- Remove fully uppercase variable outputs from AmneziaWG install script
-- Keep only Jc, Jmin, Jmax format (first letter uppercase)
UPDATE protocols SET
install_script = REPLACE(
REPLACE(
REPLACE(
install_script,
'echo "Variable: JC=${JC:-5}"',
''
),
'echo "Variable: JMIN=${JMIN:-100}"',
''
),
'echo "Variable: JMAX=${JMAX:-200}"',
''
)
WHERE slug = 'amnezia-wg-advanced';
+1
View File
@@ -0,0 +1 @@
ALTER TABLE vpn_servers ADD COLUMN ssh_key TEXT NULL;
@@ -0,0 +1,85 @@
-- Fix X-Ray install script variable substitution
-- The heredoc was preserving ${VAR} as literals instead of expanding them
UPDATE protocols
SET install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
XRAY_PORT=${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
# Ensure image present
docker pull teddysun/xray >/dev/null 2>&1 || true
# Generate keys
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
PUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
if [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
fi
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
FINGERPRINT="${FINGERPRINT:-chrome}"
SPIDER_X="${SPIDER_X:-/}"
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
mkdir -p /opt/amnezia/xray
# Write config using printf to ensure variable expansion
printf ''%s\\n'' "{
\"log\": { \"loglevel\": \"warning\" },
\"inbounds\": [
{
\"listen\": \"0.0.0.0\",
\"port\": $XRAY_PORT,
\"protocol\": \"vless\",
\"settings\": {
\"clients\": [ { \"id\": \"$CLIENT_ID\" } ],
\"decryption\": \"none\"
},
\"streamSettings\": {
\"network\": \"tcp\",
\"security\": \"reality\",
\"realitySettings\": {
\"show\": false,
\"dest\": \"$SERVER_NAME:443\",
\"xver\": 0,
\"serverNames\": [ \"$SERVER_NAME\" ],
\"privateKey\": \"$PRIVATE_KEY\",
\"shortIds\": [ \"$SHORT_ID\" ],
\"fingerprint\": \"$FINGERPRINT\",
\"spiderX\": \"$SPIDER_X\"
}
}
}
],
\"outbounds\": [ { \"protocol\": \"freedom\", \"tag\": \"direct\" } ]
}" > /opt/amnezia/xray/server.json
# Start container
docker run -d \
--name "$CONTAINER_NAME" \
--restart always \
-p "${XRAY_PORT}:${XRAY_PORT}" \
-v /opt/amnezia/xray:/opt/amnezia/xray \
teddysun/xray xray run -c /opt/amnezia/xray/server.json
sleep 2
# Output configuration
echo "XrayPort: ${XRAY_PORT}"
echo "Port: ${XRAY_PORT}"
echo "ClientID: ${CLIENT_ID}"
echo "PublicKey: ${PUBLIC_KEY}"
echo "PrivateKey: ${PRIVATE_KEY}"
echo "ShortID: ${SHORT_ID}"
echo "ServerName: ${SERVER_NAME}"
echo "ContainerName: ${CONTAINER_NAME}"'
WHERE slug = 'xray-vless';
+83
View File
@@ -0,0 +1,83 @@
-- Fix X-Ray install script JSON quotes
-- Previous migration caused missing quotes in JSON because MySQL consumed one level of escaping
-- We need \\\" in SQL to get \" in Bash, which echo outputs as " in the file
UPDATE protocols
SET install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
XRAY_PORT=${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
# Ensure image present
docker pull teddysun/xray >/dev/null 2>&1 || true
# Generate keys
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
PUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
if [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
fi
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
FINGERPRINT="${FINGERPRINT:-chrome}"
SPIDER_X="${SPIDER_X:-/}"
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
mkdir -p /opt/amnezia/xray
C="/opt/amnezia/xray/server.json"
echo "{" > "$C"
echo " \\\"log\\\": { \\\"loglevel\\\": \\\"warning\\\" }," >> "$C"
echo " \\\"inbounds\\\": [" >> "$C"
echo " {" >> "$C"
echo " \\\"listen\\\": \\\"0.0.0.0\\\"," >> "$C"
echo " \\\"port\\\": $XRAY_PORT," >> "$C"
echo " \\\"protocol\\\": \\\"vless\\\"," >> "$C"
echo " \\\"settings\\\": {" >> "$C"
echo " \\\"clients\\\": [ { \\\"id\\\": \\\"$CLIENT_ID\\\" } ]," >> "$C"
echo " \\\"decryption\\\": \\\"none\\\"" >> "$C"
echo " }," >> "$C"
echo " \\\"streamSettings\\\": {" >> "$C"
echo " \\\"network\\\": \\\"tcp\\\"," >> "$C"
echo " \\\"security\\\": \\\"reality\\\"," >> "$C"
echo " \\\"realitySettings\\\": {" >> "$C"
echo " \\\"show\\\": false," >> "$C"
echo " \\\"dest\\\": \\\"$SERVER_NAME:443\\\"," >> "$C"
echo " \\\"xver\\\": 0," >> "$C"
echo " \\\"serverNames\\\": [ \\\"$SERVER_NAME\\\" ]," >> "$C"
echo " \\\"privateKey\\\": \\\"$PRIVATE_KEY\\\"," >> "$C"
echo " \\\"shortIds\\\": [ \\\"$SHORT_ID\\\" ]," >> "$C"
echo " \\\"fingerprint\\\": \\\"$FINGERPRINT\\\"," >> "$C"
echo " \\\"spiderX\\\": \\\"$SPIDER_X\\\"" >> "$C"
echo " }" >> "$C"
echo " }" >> "$C"
echo " }" >> "$C"
echo " ]," >> "$C"
echo " \\\"outbounds\\\": [ { \\\"protocol\\\": \\\"freedom\\\", \\\"tag\\\": \\\"direct\\\" } ]" >> "$C"
echo "}" >> "$C"
docker run -d \
--name "$CONTAINER_NAME" \
--restart always \
-p "${XRAY_PORT}:${XRAY_PORT}" \
-v /opt/amnezia/xray:/opt/amnezia/xray \
teddysun/xray xray run -c /opt/amnezia/xray/server.json
sleep 2
echo "XrayPort: ${XRAY_PORT}"
echo "Port: ${XRAY_PORT}"
echo "ClientID: ${CLIENT_ID}"
echo "PublicKey: ${PUBLIC_KEY}"
echo "PrivateKey: ${PRIVATE_KEY}"
echo "ShortID: ${SHORT_ID}"
echo "ServerName: ${SERVER_NAME}"
echo "ContainerName: ${CONTAINER_NAME}"'
WHERE slug = 'xray-vless';
+83
View File
@@ -0,0 +1,83 @@
-- Fix X-Ray server config to include flow "xtls-rprx-vision"
-- This parameter is present in the client config template but was missing in the server config
-- causing connection failures with newer clients and X-Ray versions.
UPDATE protocols
SET install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
XRAY_PORT=${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
# Ensure image present
docker pull teddysun/xray >/dev/null 2>&1 || true
# Generate keys
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
PUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
if [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
fi
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
FINGERPRINT="${FINGERPRINT:-chrome}"
SPIDER_X="${SPIDER_X:-/}"
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
mkdir -p /opt/amnezia/xray
C="/opt/amnezia/xray/server.json"
echo "{" > "$C"
echo " \\\"log\\\": { \\\"loglevel\\\": \\\"warning\\\" }," >> "$C"
echo " \\\"inbounds\\\": [" >> "$C"
echo " {" >> "$C"
echo " \\\"listen\\\": \\\"0.0.0.0\\\"," >> "$C"
echo " \\\"port\\\": $XRAY_PORT," >> "$C"
echo " \\\"protocol\\\": \\\"vless\\\"," >> "$C"
echo " \\\"settings\\\": {" >> "$C"
echo " \\\"clients\\\": [ { \\\"id\\\": \\\"$CLIENT_ID\\\", \\\"flow\\\": \\\"xtls-rprx-vision\\\" } ]," >> "$C"
echo " \\\"decryption\\\": \\\"none\\\"" >> "$C"
echo " }," >> "$C"
echo " \\\"streamSettings\\\": {" >> "$C"
echo " \\\"network\\\": \\\"tcp\\\"," >> "$C"
echo " \\\"security\\\": \\\"reality\\\"," >> "$C"
echo " \\\"realitySettings\\\": {" >> "$C"
echo " \\\"show\\\": false," >> "$C"
echo " \\\"dest\\\": \\\"$SERVER_NAME:443\\\"," >> "$C"
echo " \\\"xver\\\": 0," >> "$C"
echo " \\\"serverNames\\\": [ \\\"$SERVER_NAME\\\" ]," >> "$C"
echo " \\\"privateKey\\\": \\\"$PRIVATE_KEY\\\"," >> "$C"
echo " \\\"shortIds\\\": [ \\\"$SHORT_ID\\\" ]," >> "$C"
echo " \\\"fingerprint\\\": \\\"$FINGERPRINT\\\"," >> "$C"
echo " \\\"spiderX\\\": \\\"$SPIDER_X\\\"" >> "$C"
echo " }" >> "$C"
echo " }" >> "$C"
echo " }" >> "$C"
echo " ]," >> "$C"
echo " \\\"outbounds\\\": [ { \\\"protocol\\\": \\\"freedom\\\", \\\"tag\\\": \\\"direct\\\" } ]" >> "$C"
echo "}" >> "$C"
docker run -d \
--name "$CONTAINER_NAME" \
--restart always \
-p "${XRAY_PORT}:${XRAY_PORT}" \
-v /opt/amnezia/xray:/opt/amnezia/xray \
teddysun/xray xray run -c /opt/amnezia/xray/server.json
sleep 2
echo "XrayPort: ${XRAY_PORT}"
echo "Port: ${XRAY_PORT}"
echo "ClientID: ${CLIENT_ID}"
echo "PublicKey: ${PUBLIC_KEY}"
echo "PrivateKey: ${PRIVATE_KEY}"
echo "ShortID: ${SHORT_ID}"
echo "ServerName: ${SERVER_NAME}"
echo "ContainerName: ${CONTAINER_NAME}"'
WHERE slug = 'xray-vless';
+81
View File
@@ -0,0 +1,81 @@
-- Fix X-Ray port to 443 to match Android client and avoid firewall issues.
-- Previous usage of random ports caused connection failures on restricted networks.
UPDATE protocols
SET install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
# Default to port 443 if SERVER_PORT is not provided
XRAY_PORT=${SERVER_PORT:-443}
# Ensure image present
docker pull teddysun/xray >/dev/null 2>&1 || true
# Generate keys
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
PUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
if [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
fi
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
FINGERPRINT="${FINGERPRINT:-chrome}"
SPIDER_X="${SPIDER_X:-/}"
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
mkdir -p /opt/amnezia/xray
C="/opt/amnezia/xray/server.json"
echo "{" > "$C"
echo " \\\"log\\\": { \\\"loglevel\\\": \\\"warning\\\" }," >> "$C"
echo " \\\"inbounds\\\": [" >> "$C"
echo " {" >> "$C"
echo " \\\"listen\\\": \\\"0.0.0.0\\\"," >> "$C"
echo " \\\"port\\\": $XRAY_PORT," >> "$C"
echo " \\\"protocol\\\": \\\"vless\\\"," >> "$C"
echo " \\\"settings\\\": {" >> "$C"
echo " \\\"clients\\\": [ { \\\"id\\\": \\\"$CLIENT_ID\\\", \\\"flow\\\": \\\"xtls-rprx-vision\\\" } ]," >> "$C"
echo " \\\"decryption\\\": \\\"none\\\"" >> "$C"
echo " }," >> "$C"
echo " \\\"streamSettings\\\": {" >> "$C"
echo " \\\"network\\\": \\\"tcp\\\"," >> "$C"
echo " \\\"security\\\": \\\"reality\\\"," >> "$C"
echo " \\\"realitySettings\\\": {" >> "$C"
echo " \\\"show\\\": false," >> "$C"
echo " \\\"dest\\\": \\\"$SERVER_NAME:443\\\"," >> "$C"
echo " \\\"xver\\\": 0," >> "$C"
echo " \\\"serverNames\\\": [ \\\"$SERVER_NAME\\\" ]," >> "$C"
echo " \\\"privateKey\\\": \\\"$PRIVATE_KEY\\\"," >> "$C"
echo " \\\"shortIds\\\": [ \\\"$SHORT_ID\\\" ]," >> "$C"
echo " \\\"fingerprint\\\": \\\"$FINGERPRINT\\\"," >> "$C"
echo " \\\"spiderX\\\": \\\"$SPIDER_X\\\"" >> "$C"
echo " }" >> "$C"
echo " }" >> "$C"
echo " }" >> "$C"
echo " ]," >> "$C"
echo " \\\"outbounds\\\": [ { \\\"protocol\\\": \\\"freedom\\\", \\\"tag\\\": \\\"direct\\\" } ]" >> "$C"
echo "}" >> "$C"
docker run -d \
--name "$CONTAINER_NAME" \
--restart always \
-p "${XRAY_PORT}:${XRAY_PORT}" \
-v /opt/amnezia/xray:/opt/amnezia/xray \
teddysun/xray xray run -c /opt/amnezia/xray/server.json
sleep 2
echo "XrayPort: ${XRAY_PORT}"
echo "Port: ${XRAY_PORT}"
echo "ClientID: ${CLIENT_ID}"
echo "PublicKey: ${PUBLIC_KEY}"
echo "PrivateKey: ${PRIVATE_KEY}"
echo "ShortID: ${SHORT_ID}"
echo "ServerName: ${SERVER_NAME}"
echo "ContainerName: ${CONTAINER_NAME}"'
WHERE slug = 'xray-vless';
+77
View File
@@ -0,0 +1,77 @@
-- Fix X-Ray install script:
-- 1) Use single-line docker run (backslash continuations break in MySQL)
-- 2) Handle new xray x25519 output format (Password instead of Public key)
UPDATE protocols
SET install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
XRAY_PORT=${SERVER_PORT:-443}
docker pull teddysun/xray >/dev/null 2>&1 || true
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
PRIVATE_KEY=$(printf "%s\n" "$GEN" | sed -n -E "s/^[Pp]rivate[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
if [ -z "$PRIVATE_KEY" ]; then
PRIVATE_KEY=$(printf "%s\n" "$GEN" | grep -i "private" | head -1 | sed "s/.*:[[:space:]]*//" | tr -d " \\t\\r\\n")
fi
PUBLIC_KEY=$(printf "%s\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
if [ -z "$PUBLIC_KEY" ]; then
PUBLIC_KEY=$(printf "%s\n" "$GEN" | sed -n -E "s/^[Pp]assword:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
fi
if [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]assword:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
fi
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
FINGERPRINT="${FINGERPRINT:-chrome}"
SPIDER_X="${SPIDER_X:-/}"
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
mkdir -p /opt/amnezia/xray
cat > /opt/amnezia/xray/server.json <<EOJSON
{
"log": { "loglevel": "warning" },
"inbounds": [{
"listen": "0.0.0.0",
"port": ${XRAY_PORT},
"protocol": "vless",
"settings": {
"clients": [{ "id": "${CLIENT_ID}", "flow": "xtls-rprx-vision" }],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"show": false,
"dest": "${SERVER_NAME}:443",
"xver": 0,
"serverNames": ["${SERVER_NAME}"],
"privateKey": "${PRIVATE_KEY}",
"shortIds": ["${SHORT_ID}"],
"fingerprint": "${FINGERPRINT}",
"spiderX": "${SPIDER_X}"
}
}
}],
"outbounds": [{ "protocol": "freedom", "tag": "direct" }]
}
EOJSON
docker run -d --name "$CONTAINER_NAME" --restart always -p "${XRAY_PORT}:${XRAY_PORT}" -v /opt/amnezia/xray:/opt/amnezia/xray teddysun/xray xray run -c /opt/amnezia/xray/server.json
sleep 2
echo "XrayPort: ${XRAY_PORT}"
echo "Port: ${XRAY_PORT}"
echo "ClientID: ${CLIENT_ID}"
echo "PublicKey: ${PUBLIC_KEY}"
echo "PrivateKey: ${PRIVATE_KEY}"
echo "ShortID: ${SHORT_ID}"
echo "ServerName: ${SERVER_NAME}"
echo "ContainerName: ${CONTAINER_NAME}"'
WHERE slug = 'xray-vless';
+64
View File
@@ -0,0 +1,64 @@
-- Safely update protocols table schema and data
-- 1. Ensure columns exist
SET @dbname = DATABASE();
SET @tablename = "protocols";
SET @columnname = "definition";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (column_name = @columnname)
) > 0,
"SELECT 1",
"ALTER TABLE protocols ADD COLUMN definition JSON NULL AFTER description"
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
SET @columnname = "show_text_content";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (column_name = @columnname)
) > 0,
"SELECT 1",
"ALTER TABLE protocols ADD COLUMN show_text_content TINYINT(1) DEFAULT 0 AFTER definition"
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
-- 2. Insert Data (amnezia-wg removed - use amnezia-wg-advanced instead)
INSERT IGNORE INTO protocols (slug, name, description, definition, show_text_content, is_active) VALUES
('wireguard', 'WireGuard', 'Standard WireGuard', '{}', 0, 1),
('openvpn', 'OpenVPN', 'Standard OpenVPN', '{}', 0, 1),
('shadowsocks', 'Shadowsocks', 'Shadowsocks proxy', '{}', 0, 1),
('cloak', 'Cloak', 'Cloak obfuscation', '{}', 0, 1);
-- 3. Update vpn_clients structure (original logic from migration)
SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='vpn_clients' AND COLUMN_NAME='protocol_id');
SET @sql := IF(@exist=0, 'ALTER TABLE vpn_clients ADD COLUMN protocol_id INT UNSIGNED NULL AFTER server_id, ADD INDEX idx_protocol_id (protocol_id), ADD CONSTRAINT fk_clients_protocol FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE SET NULL', 'SELECT "Column protocol_id exists"');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 4. Create server_protocols if not exists
CREATE TABLE IF NOT EXISTS server_protocols (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
server_id INT UNSIGNED NOT NULL,
protocol_id INT UNSIGNED NOT NULL,
config_data JSON,
container_id VARCHAR(255) NULL,
applied_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_server_proto (server_id, protocol_id),
FOREIGN KEY (server_id) REFERENCES vpn_servers(id) ON DELETE CASCADE,
FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+123
View File
@@ -0,0 +1,123 @@
-- Enable Stats and API for XRay VLESS protocol
-- This allows collecting traffic usage per user
-- Supports restoration of existing keys via environment variables
UPDATE protocols SET install_script = '#!/bin/bash
set -eu
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
XRAY_PORT=${SERVER_PORT:-443}
docker pull teddysun/xray >/dev/null 2>&1 || true
# Use existing keys if provided, otherwise generate new ones
if [ -z "${PRIVATE_KEY:-}" ]; then
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
if [ -z "$PRIVATE_KEY" ]; then
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | grep -i "private" | head -1 | sed "s/.*:[[:space:]]*//" | tr -d " \\t\\r\\n")
fi
fi
# Derive public key from private key
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
if [ -z "$PUBLIC_KEY" ]; then
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]assword:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
fi
# Use existing short_id or generate new one
if [ -z "${SHORT_ID:-}" ]; then
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
fi
# Use existing client_id or generate new one
if [ -z "${CLIENT_ID:-}" ]; then
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
fi
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
FINGERPRINT="${FINGERPRINT:-chrome}"
SPIDER_X="${SPIDER_X:-/}"
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
mkdir -p /opt/amnezia/xray
cat > /opt/amnezia/xray/server.json <<EOJSON
{
"log": { "loglevel": "warning" },
"stats": {},
"api": {
"tag": "api",
"services": [ "StatsService" ]
},
"policy": {
"levels": {
"0": {
"statsUserUplink": true,
"statsUserDownlink": true
}
},
"system": {
"statsInboundUplink": true,
"statsInboundDownlink": true
}
},
"inbounds": [{
"listen": "0.0.0.0",
"port": ${XRAY_PORT},
"protocol": "vless",
"settings": {
"clients": [{ "id": "${CLIENT_ID}", "flow": "xtls-rprx-vision", "email": "${CLIENT_ID}" }],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"show": false,
"dest": "${SERVER_NAME}:443",
"xver": 0,
"serverNames": ["${SERVER_NAME}"],
"privateKey": "${PRIVATE_KEY}",
"shortIds": ["${SHORT_ID}"],
"fingerprint": "${FINGERPRINT}",
"spiderX": "${SPIDER_X}"
}
}
},
{
"listen": "127.0.0.1",
"port": 10085,
"protocol": "dokodemo-door",
"tag": "api",
"settings": {
"address": "127.0.0.1"
}
}],
"outbounds": [{ "protocol": "freedom", "tag": "direct" }],
"routing": {
"rules": [
{
"inboundTag": [ "api" ],
"outboundTag": "api",
"type": "field"
}
]
}
}
EOJSON
docker run -d --name "$CONTAINER_NAME" --restart always -p "${XRAY_PORT}:${XRAY_PORT}" -v /opt/amnezia/xray:/opt/amnezia/xray teddysun/xray xray run -c /opt/amnezia/xray/server.json
sleep 2
echo "XrayPort: ${XRAY_PORT}"
echo "Port: ${XRAY_PORT}"
echo "ClientID: ${CLIENT_ID}"
echo "PublicKey: ${PUBLIC_KEY}"
echo "PrivateKey: ${PRIVATE_KEY}"
echo "ShortID: ${SHORT_ID}"
echo "ServerName: ${SERVER_NAME}"
echo "ContainerName: ${CONTAINER_NAME}"
'
WHERE slug = 'xray-vless';
+20
View File
@@ -0,0 +1,20 @@
-- Add dns_servers column to vpn_servers table if missing
-- Needed for correct configuration regeneration
SET @dbname = DATABASE();
SET @tablename = "vpn_servers";
SET @columnname = "dns_servers";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (column_name = @columnname)
) > 0,
"SELECT 1",
"ALTER TABLE vpn_servers ADD COLUMN dns_servers VARCHAR(255) DEFAULT '1.1.1.1, 1.0.0.1'"
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
+150
View File
@@ -0,0 +1,150 @@
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
# Ensure host directory exists for persistence
mkdir -p /opt/amnezia/awg
# Function to check if container is healthy
check_container() {
local status
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
if [ "$status" = "running" ]; then
return 0
elif [ "$status" = "restarting" ]; then
return 2 # Restarting loop
else
return 1 # Stopped or missing
fi
}
# Validate existing config
if [ -f /opt/amnezia/awg/wg0.conf ]; then
# Check for broken config
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
rm -f /opt/amnezia/awg/wg0.conf
fi
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for invalid hex parameters H
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
rm -f /opt/amnezia/awg/wg0.conf
fi
# Check for insecure defaults (1, 2, 3, 4)
if grep -Eiq "^H1[[:space:]]*=[[:space:]]*1$" /opt/amnezia/awg/wg0.conf; then
# Only remove if H2=2 etc also match? Or just safe to regenerate if H1=1 (insecure)
rm -f /opt/amnezia/awg/wg0.conf
fi
fi
# Check for existing configuration on HOST first (preferred persistence)
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration on host."
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
VPN_PORT=${PORT:-$VPN_PORT}
STATUS=0
check_container || STATUS=$?
if [ $STATUS -ne 0 ]; then
echo "Starting container..."
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
fi
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG configuration"
echo "Port: $VPN_PORT"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
# Output variables for preview
echo "Variable: server_port=$VPN_PORT"
echo "Variable: server_public_key=$PUBKEY"
echo "Variable: preshared_key=$PSK"
# Extract actual params
JC=$(grep -E "^Jc" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
H1=$(grep -E "^H1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
echo "Variable: Jc=${JC:-5}"
echo "Variable: H1=${H1:-$((RANDOM * 1000 + RANDOM))}"
exit 0
fi
# Generate new config
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
# Generate Random Obfuscation Params
JC=$(( (RANDOM % 8) + 3 ))
JMIN=50
JMAX=$(( (RANDOM % 500) + 500 ))
S1=$(( (RANDOM % 150) + 50 ))
S2=$(( (RANDOM % 150) + 50 ))
# Using od for larger range 32-bit ints
H1=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d "[:space:]")
H2=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d "[:space:]")
H3=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d "[:space:]")
H4=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d "[:space:]")
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = $JC
Jmin = $JMIN
Jmax = $JMAX
S1 = $S1
S2 = $S2
H1 = $H1
H2 = $H2
H3 = $H3
H4 = $H4
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
WG_CONF
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
echo "AmneziaWG Advanced installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey: $PRESHARED_KEY"
echo "Variable: server_port=$VPN_PORT"
echo "Variable: server_public_key=$PUBLIC_KEY"
echo "Variable: preshared_key=$PRESHARED_KEY"
echo "Variable: Jc=$JC"
echo "Variable: Jmin=$JMIN"
echo "Variable: H1=$H1"
echo "Variable: H2=$H2"
echo "Variable: H3=$H3"
echo "Variable: H4=$H4"
'
WHERE slug = 'amnezia-wg-advanced';
+155
View File
@@ -0,0 +1,155 @@
-- Fix AWG Advanced install script: create container BEFORE generating keys
-- The issue was that the script tried to call docker exec wg genkey before the container existed
UPDATE protocols SET
install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
MTU=${MTU:-1420}
mkdir -p /opt/amnezia/awg
# Check if container exists and is running
container_running() {
docker inspect --format="{{.State.Running}}" "$CONTAINER_NAME" 2>/dev/null | grep -q true
}
# Clean up broken configs
if [ -f /opt/amnezia/awg/wg0.conf ]; then
if grep -q "PRIVATE_KEY" /opt/amnezia/awg/wg0.conf 2>/dev/null; then
rm -f /opt/amnezia/awg/wg0.conf
fi
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf 2>/dev/null; then
rm -f /opt/amnezia/awg/wg0.conf
fi
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf 2>/dev/null; then
rm -f /opt/amnezia/awg/wg0.conf
fi
if grep -Eiq "^H1[[:space:]]*=[[:space:]]*1$" /opt/amnezia/awg/wg0.conf 2>/dev/null; then
rm -f /opt/amnezia/awg/wg0.conf
fi
fi
# If valid config exists, just ensure container is running
if [ -f /opt/amnezia/awg/wg0.conf ]; then
echo "Found existing configuration"
VPN_PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " " || echo "$VPN_PORT")
if ! container_running; then
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 3
# Force reload interface to apply AWG params
docker exec "$CONTAINER_NAME" ip link del wg0 2>/dev/null || true
docker exec "$CONTAINER_NAME" wg-quick up /opt/amnezia/awg/wg0.conf
fi
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || echo "")
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || echo "")
JC=$(grep -E "^Jc" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
JMIN=$(grep -E "^Jmin" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
JMAX=$(grep -E "^Jmax" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
S1=$(grep -E "^S1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
S2=$(grep -E "^S2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
H1=$(grep -E "^H1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
H2=$(grep -E "^H2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
H3=$(grep -E "^H3" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
H4=$(grep -E "^H4" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
echo "Using existing AmneziaWG configuration"
echo "Variable: server_port=$VPN_PORT"
echo "Variable: server_public_key=$PUBKEY"
echo "Variable: preshared_key=$PSK"
echo "Variable: container_name=$CONTAINER_NAME"
echo "Variable: Jc=$JC"
echo "Variable: Jmin=$JMIN"
echo "Variable: Jmax=$JMAX"
echo "Variable: S1=$S1"
echo "Variable: S2=$S2"
echo "Variable: H1=$H1"
echo "Variable: H2=$H2"
echo "Variable: H3=$H3"
echo "Variable: H4=$H4"
exit 0
fi
# FRESH INSTALL
echo "Starting fresh AmneziaWG Advanced installation..."
# Remove old container if exists
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
# Start container FIRST so we can use wg tools inside it
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 3
# Generate keys using the container
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
# Generate random obfuscation parameters
JC=$((RANDOM % 8 + 3))
JMIN=50
JMAX=$((RANDOM % 500 + 500))
S1=$((RANDOM % 150 + 50))
S2=$((RANDOM % 150 + 50))
H1=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d " ")
H2=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d " ")
H3=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d " ")
H4=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d " ")
# Create config file
cat > /opt/amnezia/awg/wg0.conf << WGCONF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = $JC
Jmin = $JMIN
Jmax = $JMAX
S1 = $S1
S2 = $S2
H1 = $H1
H2 = $H2
H3 = $H3
H4 = $H4
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
WGCONF
# Save keys
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
echo "[]" > /opt/amnezia/awg/clientsTable
# Restart container and explicitly reload WG interface to apply AWG params
docker restart "$CONTAINER_NAME"
sleep 2
# CRITICAL: Force reload interface to apply AWG obfuscation parameters
docker exec "$CONTAINER_NAME" ip link del wg0 2>/dev/null || true
docker exec "$CONTAINER_NAME" wg-quick up /opt/amnezia/awg/wg0.conf
sleep 1
echo "AmneziaWG Advanced installed successfully"
echo "Variable: server_port=$VPN_PORT"
echo "Variable: server_public_key=$PUBLIC_KEY"
echo "Variable: preshared_key=$PRESHARED_KEY"
echo "Variable: container_name=$CONTAINER_NAME"
echo "Variable: Jc=$JC"
echo "Variable: Jmin=$JMIN"
echo "Variable: Jmax=$JMAX"
echo "Variable: S1=$S1"
echo "Variable: S2=$S2"
echo "Variable: H1=$H1"
echo "Variable: H2=$H2"
echo "Variable: H3=$H3"
echo "Variable: H4=$H4"
'
WHERE slug = 'amnezia-wg-advanced';
@@ -0,0 +1 @@
ALTER TABLE vpn_clients ADD COLUMN current_speed BIGINT DEFAULT 0 AFTER traffic_limit;
+3
View File
@@ -0,0 +1,3 @@
ALTER TABLE vpn_clients ADD COLUMN speed_up BIGINT DEFAULT 0 AFTER current_speed;
ALTER TABLE vpn_clients ADD COLUMN speed_down BIGINT DEFAULT 0 AFTER speed_up;
-- We can drop current_speed later or keep it as total
@@ -0,0 +1,131 @@
-- Enable single IP enforcement for XRay VLESS protocol
-- Adds:
-- 1. statsUserOnline for tracking online connections
-- 2. RoutingService for dynamic IP blocking
-- 3. blocked outbound (blackhole) for dropping unwanted traffic
-- 4. vless-in tag on main inbound for targeting rules
UPDATE protocols SET install_script = '#!/bin/bash
set -eu
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
XRAY_PORT=${SERVER_PORT:-443}
docker pull teddysun/xray >/dev/null 2>&1 || true
# Use existing keys if provided, otherwise generate new ones
if [ -z "${PRIVATE_KEY:-}" ]; then
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
if [ -z "$PRIVATE_KEY" ]; then
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | grep -i "private" | head -1 | sed "s/.*:[[:space:]]*//" | tr -d " \\t\\r\\n")
fi
fi
# Derive public key from private key
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
if [ -z "$PUBLIC_KEY" ]; then
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]assword:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
fi
# Use existing short_id or generate new one
if [ -z "${SHORT_ID:-}" ]; then
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
fi
# Use existing client_id or generate new one
if [ -z "${CLIENT_ID:-}" ]; then
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
fi
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
FINGERPRINT="${FINGERPRINT:-chrome}"
SPIDER_X="${SPIDER_X:-/}"
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
mkdir -p /opt/amnezia/xray
cat > /opt/amnezia/xray/server.json <<EOJSON
{
"log": { "loglevel": "warning" },
"stats": {},
"api": {
"tag": "api",
"services": [ "StatsService", "RoutingService" ]
},
"policy": {
"levels": {
"0": {
"statsUserUplink": true,
"statsUserDownlink": true,
"statsUserOnline": true
}
},
"system": {
"statsInboundUplink": true,
"statsInboundDownlink": true
}
},
"inbounds": [{
"listen": "0.0.0.0",
"port": ${XRAY_PORT},
"protocol": "vless",
"tag": "vless-in",
"settings": {
"clients": [{ "id": "${CLIENT_ID}", "flow": "xtls-rprx-vision", "email": "${CLIENT_ID}", "level": 0 }],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"show": false,
"dest": "${SERVER_NAME}:443",
"xver": 0,
"serverNames": ["${SERVER_NAME}"],
"privateKey": "${PRIVATE_KEY}",
"shortIds": ["${SHORT_ID}"],
"fingerprint": "${FINGERPRINT}",
"spiderX": "${SPIDER_X}"
}
}
},
{
"listen": "127.0.0.1",
"port": 10085,
"protocol": "dokodemo-door",
"tag": "api",
"settings": {
"address": "127.0.0.1"
}
}],
"outbounds": [
{ "protocol": "freedom", "tag": "direct" },
{ "protocol": "blackhole", "tag": "blocked" }
],
"routing": {
"rules": [
{
"inboundTag": [ "api" ],
"outboundTag": "api",
"type": "field"
}
]
}
}
EOJSON
docker run -d --name "$CONTAINER_NAME" --restart always -p "${XRAY_PORT}:${XRAY_PORT}" -v /opt/amnezia/xray:/opt/amnezia/xray teddysun/xray xray run -c /opt/amnezia/xray/server.json
sleep 2
echo "XrayPort: ${XRAY_PORT}"
echo "Port: ${XRAY_PORT}"
echo "ClientID: ${CLIENT_ID}"
echo "PublicKey: ${PUBLIC_KEY}"
echo "PrivateKey: ${PRIVATE_KEY}"
echo "ShortID: ${SHORT_ID}"
echo "ServerName: ${SERVER_NAME}"
echo "ContainerName: ${CONTAINER_NAME}"
'
WHERE slug = 'xray-vless';
@@ -0,0 +1,9 @@
-- Add translation for dashboard.online_now
INSERT INTO translations (`locale`, `category`, `key_name`, `translation`) VALUES
('en', 'dashboard', 'online_now', 'Online Now'),
('ru', 'dashboard', 'online_now', 'Сейчас онлайн'),
('es', 'dashboard', 'online_now', 'En línea ahora'),
('de', 'dashboard', 'online_now', 'Jetzt online'),
('fr', 'dashboard', 'online_now', 'En ligne maintenant'),
('zh', 'dashboard', 'online_now', '当前在线')
ON DUPLICATE KEY UPDATE `translation` = VALUES(`translation`);
@@ -0,0 +1,5 @@
-- Enable text content display by default on client page for XRay VLESS
UPDATE protocols
SET show_text_content = 1
WHERE slug = 'xray-vless'
AND COALESCE(show_text_content, 0) <> 1;
@@ -0,0 +1,31 @@
-- Add missing translations for protocol management UI (EN/RU)
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'protocols', 'management', 'Protocol Management'),
('ru', 'protocols', 'management', 'Управление протоколами'),
('en', 'protocols', 'management_description', 'Configure and manage VPN protocols'),
('ru', 'protocols', 'management_description', 'Настройка и управление VPN-протоколами'),
('en', 'common', 'active', 'Active'),
('ru', 'common', 'active', 'Активный'),
('en', 'common', 'inactive', 'Inactive'),
('ru', 'common', 'inactive', 'Неактивный'),
('en', 'protocols', 'add_protocol', 'Add Protocol'),
('ru', 'protocols', 'add_protocol', 'Добавить протокол'),
('en', 'common', 'settings', 'Settings'),
('ru', 'common', 'settings', 'Настройки'),
('en', 'protocols', 'available_protocols', 'Available Protocols'),
('ru', 'protocols', 'available_protocols', 'Доступные протоколы'),
('en', 'protocols', 'search_protocols', 'Search protocols'),
('ru', 'protocols', 'search_protocols', 'Поиск протоколов'),
('en', 'protocols', 'all_protocols', 'All Protocols'),
('ru', 'protocols', 'all_protocols', 'Все протоколы'),
('en', 'protocols', 'active_only', 'Active only'),
('ru', 'protocols', 'active_only', 'Только активные'),
('en', 'protocols', 'with_ai_generations', 'With AI generations'),
('ru', 'protocols', 'with_ai_generations', 'С AI-генерациями')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
-- Hide protocols that should not be published
UPDATE protocols
SET is_active = 0
WHERE slug IN ('cloak', 'openvpn', 'shadowsocks', 'wireguard', 'wireguard-standard')
OR name IN ('Cloak', 'OpenVPN', 'Shadowsocks', 'WireGuard', 'WireGuard Standard');
+371
View File
@@ -0,0 +1,371 @@
-- =====================================================================
-- Migration 058: Add AmneziaWG 2.0 protocol (amneziawg-go userspace)
-- Uses amneziawg-go (Go userspace) instead of kernel module
-- https://github.com/amnezia-vpn/amneziawg-go
-- =====================================================================
-- 1. Insert the protocol entry (clone output_template from amnezia-wg-advanced)
INSERT INTO protocols (name, slug, description, install_script, uninstall_script, output_template, ubuntu_compatible, is_active, definition, created_at, updated_at)
SELECT
'AmneziaWG 2.0',
'awg2',
'AmneziaWG 2.0 — userspace Go implementation (amneziawg-go). No kernel module required.',
'#!/bin/bash
set -euo pipefail
# Use exported variables from panel (SERVER_PORT, SERVER_CONTAINER) or defaults
CONTAINER_NAME="${SERVER_CONTAINER:-amnezia-awg2}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT="${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}"
MTU=${MTU:-1420}
# Install git if not available
if ! command -v git &> /dev/null; then
apt-get update -qq && apt-get install -y -qq git >/dev/null 2>&1
fi
mkdir -p /opt/amnezia/awg2
# Clone amneziawg-go source for Docker build
if [ ! -d /opt/amnezia/awg2/src ]; then
git clone --depth=1 https://github.com/amnezia-vpn/amneziawg-go.git /opt/amnezia/awg2/src
fi
# Build Docker image using the repo Dockerfile (multi-stage: Go compile + tools)
docker build --no-cache -t amnezia-awg2 /opt/amnezia/awg2/src
# Run container (userspace: no SYS_MODULE, no /lib/modules)
EXISTING=$(docker ps -aq -f "name=$CONTAINER_NAME" 2>/dev/null | head -1)
if [ -z "$EXISTING" ]; then
docker run -d --name "$CONTAINER_NAME" --restart always --cap-add=NET_ADMIN --device /dev/net/tun -p "${VPN_PORT}:${VPN_PORT}/udp" -v /opt/amnezia/awg2:/opt/amnezia/awg amnezia-awg2 sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; WG_QUICK_USERSPACE_IMPLEMENTATION=amneziawg-go awg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
else
STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "")
if [ \"$STATUS\" != \"running\" ]; then
docker start \"$CONTAINER_NAME\" >/dev/null 2>&1 || true
fi
fi
# Check for existing config
if [ -f /opt/amnezia/awg2/wg0.conf ]; then
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg2/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg2/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg2/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG 2.0 configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
echo "Server Host: $EXTERNAL_IP"
# Output AWG params from existing config
for P in Jc Jmin Jmax S1 S2 S3 S4 H1 H2 H3 H4; do
VAL=$(grep -E "^$P " /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
if [ -n "$VAL" ]; then echo "Variable: $P=$VAL"; fi
done
echo "Variable: dns_servers=1.1.1.1, 1.0.0.1"
exit 0
fi
# Generate keys
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
# AWG obfuscation parameters
JC=5
JMIN=50
JMAX=1000
S1_VAL=50
S2_VAL=100
S3_VAL=20
S4_VAL=10
# H1-H4: keep numeric values for broad awg-tools compatibility.
H1_VAL=123456789
H2_VAL=223456789
H3_VAL=323456789
H4_VAL=423456789
# Write config
cat > /opt/amnezia/awg2/wg0.conf << EOF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = $JC
Jmin = $JMIN
Jmax = $JMAX
S1 = $S1_VAL
S2 = $S2_VAL
S3 = $S3_VAL
S4 = $S4_VAL
H1 = $H1_VAL
H2 = $H2_VAL
H3 = $H3_VAL
H4 = $H4_VAL
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
EOF
echo "$PRIVATE_KEY" > /opt/amnezia/awg2/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg2/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg2/wireguard_psk.key
echo "[]" > /opt/amnezia/awg2/clientsTable
# 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")
echo "AmneziaWG 2.0 installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey = $PRESHARED_KEY"
echo "Server Host: $EXTERNAL_IP"
echo "Variable: Jc=$JC"
echo "Variable: Jmin=$JMIN"
echo "Variable: Jmax=$JMAX"
echo "Variable: S1=$S1_VAL"
echo "Variable: S2=$S2_VAL"
echo "Variable: S3=$S3_VAL"
echo "Variable: S4=$S4_VAL"
echo "Variable: H1=$H1_VAL"
echo "Variable: H2=$H2_VAL"
echo "Variable: H3=$H3_VAL"
echo "Variable: H4=$H4_VAL"
echo "Variable: dns_servers=1.1.1.1, 1.0.0.1"',
'#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg2}"
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm -fv "$CONTAINER_NAME" 2>/dev/null || true
docker image rm amnezia-awg2 2>/dev/null || true
rm -rf /opt/amnezia/awg2 2>/dev/null || true
echo "{\"success\":true,\"message\":\"AmneziaWG 2.0 uninstalled\"}"',
p.output_template,
1,
1,
JSON_OBJECT(
'engine', 'shell',
'metadata', JSON_OBJECT(
'container_name', 'amnezia-awg2',
'vpn_subnet', '10.8.1.0/24',
'port_range', JSON_ARRAY(30000, 65000),
'config_dir', '/opt/amnezia/awg2'
)
),
NOW(),
NOW()
FROM protocols p
WHERE p.slug = 'amnezia-wg-advanced'
AND NOT EXISTS (SELECT 1 FROM protocols WHERE slug = 'awg2');
-- 2. Clone protocol variables from amnezia-wg-advanced to awg2
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT
(SELECT id FROM protocols WHERE slug = 'awg2' LIMIT 1),
src.variable_name,
src.variable_type,
src.default_value,
src.description,
src.required
FROM protocol_variables src
WHERE src.protocol_id = (SELECT id FROM protocols WHERE slug = 'amnezia-wg-advanced' LIMIT 1)
AND NOT EXISTS (
SELECT 1 FROM protocol_variables ev
WHERE ev.protocol_id = (SELECT id FROM protocols WHERE slug = 'awg2' LIMIT 1)
AND ev.variable_name = src.variable_name
);
-- 3. Clone protocol templates from amnezia-wg-advanced to awg2
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
SELECT
(SELECT id FROM protocols WHERE slug = 'awg2' LIMIT 1),
src.template_name,
src.template_content,
src.is_default
FROM protocol_templates src
WHERE src.protocol_id = (SELECT id FROM protocols WHERE slug = 'amnezia-wg-advanced' LIMIT 1)
AND NOT EXISTS (
SELECT 1 FROM protocol_templates et
WHERE et.protocol_id = (SELECT id FROM protocols WHERE slug = 'awg2' LIMIT 1)
AND et.template_name = src.template_name
);
-- 4. Update install_script for existing awg2 protocol (in case migration was already run)
UPDATE protocols SET install_script = '#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${SERVER_CONTAINER:-amnezia-awg2}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
VPN_PORT="${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}"
MTU=${MTU:-1420}
if ! command -v git &> /dev/null; then
apt-get update -qq && apt-get install -y -qq git >/dev/null 2>&1
fi
mkdir -p /opt/amnezia/awg2
if [ ! -d /opt/amnezia/awg2/src ]; then
git clone --depth=1 https://github.com/amnezia-vpn/amneziawg-go.git /opt/amnezia/awg2/src
fi
docker build --no-cache -t amnezia-awg2 /opt/amnezia/awg2/src
EXISTING=$(docker ps -aq -f "name=$CONTAINER_NAME" 2>/dev/null | head -1)
if [ -z "$EXISTING" ]; then
docker run -d --name "$CONTAINER_NAME" --restart always --cap-add=NET_ADMIN --device /dev/net/tun -p "${VPN_PORT}:${VPN_PORT}/udp" -v /opt/amnezia/awg2:/opt/amnezia/awg amnezia-awg2 sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; WG_QUICK_USERSPACE_IMPLEMENTATION=amneziawg-go awg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
sleep 2
else
STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "")
if [ \"$STATUS\" != \"running\" ]; then
docker start \"$CONTAINER_NAME\" >/dev/null 2>&1 || true
fi
fi
if [ -f /opt/amnezia/awg2/wg0.conf ]; then
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg2/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
fi
PUBKEY=$(cat /opt/amnezia/awg2/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then
PRIVKEY=$(cat /opt/amnezia/awg2/wireguard_server_private_key.key 2>/dev/null || true)
if [ -n "$PRIVKEY" ]; then
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
fi
fi
echo "Using existing AmneziaWG 2.0 configuration"
echo "Port: ${PORT:-$VPN_PORT}"
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
echo "Server Host: $EXTERNAL_IP"
for P in Jc Jmin Jmax S1 S2 S3 S4 H1 H2 H3 H4; do
VAL=$(grep -E "^$P " /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
if [ -n "$VAL" ]; then echo "Variable: $P=$VAL"; fi
done
echo "Variable: dns_servers=1.1.1.1, 1.0.0.1"
exit 0
fi
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
JC=5
JMIN=50
JMAX=1000
S1_VAL=50
S2_VAL=100
S3_VAL=20
S4_VAL=10
H1_VAL=123456789
H2_VAL=223456789
H3_VAL=323456789
H4_VAL=423456789
cat > /opt/amnezia/awg2/wg0.conf << EOF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = 10.8.1.1/24
ListenPort = $VPN_PORT
MTU = $MTU
Jc = $JC
Jmin = $JMIN
Jmax = $JMAX
S1 = $S1_VAL
S2 = $S2_VAL
S3 = $S3_VAL
S4 = $S4_VAL
H1 = $H1_VAL
H2 = $H2_VAL
H3 = $H3_VAL
H4 = $H4_VAL
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
EOF
echo "$PRIVATE_KEY" > /opt/amnezia/awg2/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg2/wireguard_server_public_key.key
echo "$PRESHARED_KEY" > /opt/amnezia/awg2/wireguard_psk.key
echo "[]" > /opt/amnezia/awg2/clientsTable
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
echo "AmneziaWG 2.0 installed successfully"
echo "Port: $VPN_PORT"
echo "Server Public Key: $PUBLIC_KEY"
echo "PresharedKey = $PRESHARED_KEY"
echo "Server Host: $EXTERNAL_IP"
echo "Variable: Jc=$JC"
echo "Variable: Jmin=$JMIN"
echo "Variable: Jmax=$JMAX"
echo "Variable: S1=$S1_VAL"
echo "Variable: S2=$S2_VAL"
echo "Variable: S3=$S3_VAL"
echo "Variable: S4=$S4_VAL"
echo "Variable: H1=$H1_VAL"
echo "Variable: H2=$H2_VAL"
echo "Variable: H3=$H3_VAL"
echo "Variable: H4=$H4_VAL"
echo "Variable: dns_servers=1.1.1.1, 1.0.0.1"'
WHERE slug = 'awg2';
-- 5. Update output_template for AWG2 (add S3/S4 padding params)
UPDATE protocols SET output_template = '[Interface]
PrivateKey = {{private_key}}
Address = {{client_ip}}/32
DNS = {{dns_servers}}
MTU = 1280
Jc = {{Jc}}
Jmin = {{Jmin}}
Jmax = {{Jmax}}
S1 = {{S1}}
S2 = {{S2}}
S3 = {{S3}}
S4 = {{S4}}
H1 = {{H1}}
H2 = {{H2}}
H3 = {{H3}}
H4 = {{H4}}
[Peer]
PublicKey = {{server_public_key}}
PresharedKey = {{preshared_key}}
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = {{server_host}}:{{server_port}}
PersistentKeepalive = 25'
WHERE slug = 'awg2';
-- 6. Add S3/S4 protocol variables for awg2
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'S3', 'number', '20', 'Padding of handshake cookie message', false
FROM protocols p WHERE p.slug = 'awg2'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'S3');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'S4', 'number', '10', 'Padding of transport messages', false
FROM protocols p WHERE p.slug = 'awg2'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'S4');
+113
View File
@@ -0,0 +1,113 @@
-- =====================================================================
-- Migration 059: Add MTProxy (Telegram) protocol
-- https://hub.docker.com/r/telegrammessenger/proxy/
-- Zero-configuration Telegram MTProto proxy server
-- =====================================================================
-- 1. Insert the MTProxy protocol
INSERT INTO protocols (name, slug, description, install_script, uninstall_script, output_template, show_text_content, ubuntu_compatible, is_active, definition, created_at, updated_at)
SELECT
'MTProxy (Telegram)',
'mtproxy',
'Telegram MTProto proxy — zero-configuration proxy server for Telegram messenger.',
'#!/bin/bash
set -euo pipefail
# Use exported variables from panel (SERVER_PORT, SERVER_CONTAINER) or defaults
CONTAINER_NAME="${SERVER_CONTAINER:-amnezia-mtproxy}"
PORT_RANGE_START=${PORT_RANGE_START:-30000}
PORT_RANGE_END=${PORT_RANGE_END:-65000}
MTPROXY_PORT="${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}"
mkdir -p /opt/amnezia/mtproxy
# Generate secret if not exists
if [ -f /opt/amnezia/mtproxy/secret ]; then
SECRET=$(cat /opt/amnezia/mtproxy/secret)
echo "Using existing MTProxy secret"
else
SECRET=$(cat /dev/urandom | tr -dc a-f0-9 | head -c 32 || true)
echo "$SECRET" > /opt/amnezia/mtproxy/secret
fi
# Store port
echo "$MTPROXY_PORT" > /opt/amnezia/mtproxy/port
# Remove existing container
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
# Run MTProxy container (single line for heredoc compatibility)
docker run -d --name "$CONTAINER_NAME" --restart always -p "${MTPROXY_PORT}:443" -v /opt/amnezia/mtproxy:/data -e SECRET="$SECRET" telegrammessenger/proxy:latest
sleep 3
# 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")
echo "MTProxy installed successfully"
echo "Port: $MTPROXY_PORT"
echo "Secret: $SECRET"
echo "Server Host: $EXTERNAL_IP"',
'#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-mtproxy}"
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm -fv "$CONTAINER_NAME" 2>/dev/null || true
docker image rm telegrammessenger/proxy:latest 2>/dev/null || true
rm -rf /opt/amnezia/mtproxy 2>/dev/null || true
echo "{\"success\":true,\"message\":\"MTProxy uninstalled\"}"',
'tg://proxy?server={{server_host}}&port={{server_port}}&secret={{secret}}',
1,
1,
1,
JSON_OBJECT(
'engine', 'shell',
'metadata', JSON_OBJECT(
'container_name', 'amnezia-mtproxy',
'port_range', JSON_ARRAY(30000, 65000),
'config_dir', '/opt/amnezia/mtproxy'
)
),
NOW(),
NOW()
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug = 'mtproxy');
-- 2. Add protocol variables for MTProxy
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'secret', 'string', '', 'MTProxy secret (32 hex chars)', true
FROM protocols p WHERE p.slug = 'mtproxy'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'secret');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true
FROM protocols p WHERE p.slug = 'mtproxy'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'server_host');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_port', 'number', '443', 'MTProxy external port', true
FROM protocols p WHERE p.slug = 'mtproxy'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'server_port');
-- 3. Add default template for MTProxy
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
SELECT p.id, 'Default MTProxy', 'tg://proxy?server={{server_host}}&port={{server_port}}&secret={{secret}}', true
FROM protocols p WHERE p.slug = 'mtproxy'
AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id = p.id AND template_name = 'Default MTProxy');
-- 4. Add QR code template (same as output)
UPDATE protocols SET
qr_code_template = 'tg://proxy?server={{server_host}}&port={{server_port}}&secret={{secret}}',
qr_code_format = 'raw'
WHERE slug = 'mtproxy';
-- 5. Add translations for MTProxy
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'protocols', 'protocol_mtproxy', 'MTProxy (Telegram)')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('ru', 'protocols', 'protocol_mtproxy', 'MTProxy (Telegram)')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
+156
View File
@@ -0,0 +1,156 @@
-- =====================================================================
-- Migration 060: Add AIVPN protocol (AI-powered VPN with traffic disguise)
-- https://github.com/infosave2007/aivpn
-- Neural Resonance AI for DPI bypass, Zero-RTT, PFS
-- =====================================================================
-- 1. Insert the AIVPN protocol
INSERT INTO protocols (name, slug, description, install_script, uninstall_script, output_template, show_text_content, ubuntu_compatible, is_active, definition, created_at, updated_at)
SELECT
'AIVPN',
'aivpn',
'AIVPN — AI-powered VPN с маскировкой трафика под реальные приложения (Zoom, TikTok, DNS). Neural Resonance для обхода DPI.',
'#!/bin/bash
set -euo 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 git and iptables if not available
if ! command -v git &> /dev/null || ! command -v iptables &> /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
fi
# 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
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
iptables -t nat -C POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
# 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 source for Docker build
if [ ! -d /opt/amnezia/aivpn ]; then
git clone --depth=1 https://github.com/infosave2007/aivpn.git /opt/amnezia/aivpn
fi
# Build Docker image
cd /opt/amnezia/aivpn
docker build --no-cache -t aivpn-server -f Dockerfile .
# Remove existing container
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
# Run AIVPN container
docker run -d --name "$CONTAINER_NAME" --restart always --cap-add=NET_ADMIN --device /dev/net/tun --network host -v "$CONFIG_DIR:/etc/aivpn" aivpn-server --listen "0.0.0.0:${VPN_PORT}" --key-file /etc/aivpn/server.key
sleep 3
# Check container status
STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "unknown")
if [ "$STATUS" != "running" ]; then
echo "ERROR: AIVPN container is not running"
docker logs "$CONTAINER_NAME" 2>&1
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"',
'#!/bin/bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-aivpn-server}"
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm -fv "$CONTAINER_NAME" 2>/dev/null || true
docker image rm aivpn-server 2>/dev/null || true
rm -rf /opt/amnezia/aivpn 2>/dev/null || true
# Remove NAT rules
iptables -t nat -D POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE 2>/dev/null || true
echo "{\"success\":true,\"message\":\"AIVPN uninstalled\"}"',
'aivpn://{{connection_key}}',
1,
1,
1,
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'
)
),
NOW(),
NOW()
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug = 'aivpn');
-- 2. Add protocol variables for AIVPN
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'connection_key', 'string', '', 'AIVPN connection key (generated by server)', true
FROM protocols p WHERE p.slug = 'aivpn'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'connection_key');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true
FROM protocols p WHERE p.slug = 'aivpn'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'server_host');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_port', 'number', '443', 'AIVPN server port', true
FROM protocols p WHERE p.slug = 'aivpn'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'server_port');
-- 3. Add default template for AIVPN
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
SELECT p.id, 'Default AIVPN', 'aivpn://{{connection_key}}', true
FROM protocols p WHERE p.slug = 'aivpn'
AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id = p.id AND template_name = 'Default AIVPN');
-- 4. Add translations for AIVPN
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'protocols', 'protocol_aivpn', 'AIVPN (AI-Powered)')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('ru', 'protocols', 'protocol_aivpn', 'AIVPN (ИИ-протокол)')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
@@ -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;
@@ -0,0 +1,14 @@
-- Remove invalid empty peer block from AWG2 install script.
-- The old script generated wg0.conf with:
-- [Peer]
-- PublicKey =
-- which causes awg setconf parse errors and restart loops.
UPDATE protocols
SET install_script = REPLACE(
install_script,
'\n[Peer]\nPublicKey = \nPresharedKey = $PRESHARED_KEY\nAllowedIPs = 10.8.1.2/32\n',
'\n'
)
WHERE slug = 'awg2'
AND install_script LIKE '%[Peer]%PublicKey = %PresharedKey = $PRESHARED_KEY%AllowedIPs = 10.8.1.2/32%';
+2
View File
@@ -10,3 +10,5 @@ collation-server = utf8mb4_unicode_ci
init_connect = 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci'
init_connect = 'SET collation_connection = utf8mb4_unicode_ci'
skip-character-set-client-handshake
binlog_expire_logs_seconds = 259200
max_binlog_size = 100M
+2570 -436
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
<?php
require_once __DIR__ . '/inc/Config.php';
Config::load(__DIR__ . '/.env');
require_once __DIR__ . '/inc/DB.php';
try {
$pdo = DB::conn();
$sql = file_get_contents(__DIR__ . '/migrations/053_split_speed.sql');
$pdo->exec($sql);
echo "Migration 053 applied successfully.\n";
} catch (Exception $e) {
echo "Migration failed: " . $e->getMessage() . "\n";
}
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
set -euo pipefail
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
EMAIL="${EMAIL:-admin@amnez.ia}"
PASSWORD="${PASSWORD:-admin123}"
SERVER_HOST="${SERVER_HOST:-217.26.25.6}"
PROTOCOL_SLUG="${PROTOCOL_SLUG:-amnezia-wg-advanced}"
CLIENT_NAME="${CLIENT_NAME:-api-selfcheck}"
CLIENT_LOGIN="${CLIENT_LOGIN:-api-selfcheck}"
OUT_DIR="${OUT_DIR:-scripts/_cycle_out}"
mkdir -p "$OUT_DIR"
AUTH_RESP=$(curl -sS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD" || true)
TOKEN=$(printf '%s' "$AUTH_RESP" | python3 -c 'import sys,json; raw=sys.stdin.read().strip();
import sys
if not raw: sys.exit(2)
j=json.loads(raw)
print(j.get("token",""))
') || {
echo "ERROR: failed to parse /api/auth/token response" >&2
echo "PANEL_URL=$PANEL_URL" >&2
echo "Response (first 500 chars):" >&2
printf '%s' "$AUTH_RESP" | head -c 500 >&2
echo >&2
exit 1
}
if [[ -z "${TOKEN:-}" ]]; then
echo "ERROR: token is empty" >&2
printf '%s' "$AUTH_RESP" | head -c 500 >&2
echo >&2
exit 1
fi
echo "TOKEN_OK"
SERVER_JSON=$(curl -fsS "$PANEL_URL/api/servers" -H "Authorization: Bearer $TOKEN")
SERVER_ID=$(printf '%s' "$SERVER_JSON" | python3 -c 'import sys,json; j=json.load(sys.stdin); host=sys.argv[1];
out="";
for s in j.get("servers",[]):
if str(s.get("host","" )).strip()==host:
out=str(s.get("id",""))
break
print(out)
' "$SERVER_HOST")
if [[ -z "${SERVER_ID:-}" ]]; then
echo "ERROR: server with host $SERVER_HOST not found" >&2
printf '%s' "$SERVER_JSON" | python3 -m json.tool | head -200
exit 1
fi
echo "SERVER_ID=$SERVER_ID"
PROTO_JSON=$(curl -fsS "$PANEL_URL/api/protocols/active" -H "Authorization: Bearer $TOKEN")
PROTOCOL_ID=$(printf '%s' "$PROTO_JSON" | python3 -c 'import sys,json; j=json.load(sys.stdin); slug=sys.argv[1];
out="";
for p in j.get("protocols",[]):
if p.get("slug")==slug:
out=str(p.get("id",""))
break
print(out)
' "$PROTOCOL_SLUG")
if [[ -z "${PROTOCOL_ID:-}" ]]; then
echo "ERROR: protocol $PROTOCOL_SLUG not found" >&2
printf '%s' "$PROTO_JSON" | python3 -m json.tool | head -200
exit 1
fi
echo "PROTOCOL_ID=$PROTOCOL_ID"
pretty_print() {
# Reads JSON from stdin and pretty-prints it. If it's not JSON, prints raw.
local data
data=$(cat)
if python3 -m json.tool >/dev/null 2>&1 <<<"$data"; then
python3 -m json.tool <<<"$data"
else
printf '%s' "$data"
fi
}
echo "--- UNINSTALL $PROTOCOL_SLUG (ignore errors)"
set +e
UNINSTALL_RESP=$(curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/$PROTOCOL_SLUG/uninstall" \
-H "Authorization: Bearer $TOKEN" || true)
printf '%s' "$UNINSTALL_RESP" >"$OUT_DIR/uninstall_${PROTOCOL_SLUG}.txt"
printf '%s' "$UNINSTALL_RESP" | pretty_print | head -200
set -e
echo "--- INSTALL protocol_id=$PROTOCOL_ID"
INSTALL_RESP=$(curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/install" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"protocol_id\":$PROTOCOL_ID}" || true)
printf '%s' "$INSTALL_RESP" >"$OUT_DIR/install_${PROTOCOL_ID}.txt"
printf '%s' "$INSTALL_RESP" | pretty_print | head -200
echo "--- CREATE client"
CLIENT_RESP=$(curl -fsS -X POST "$PANEL_URL/api/clients/create" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"server_id\":$SERVER_ID,\"protocol_id\":$PROTOCOL_ID,\"name\":\"$CLIENT_NAME\",\"login\":\"$CLIENT_LOGIN\"}")
printf '%s' "$CLIENT_RESP" >"$OUT_DIR/client_create_${PROTOCOL_ID}.txt"
printf '%s' "$CLIENT_RESP" | pretty_print | head -200
CLIENT_ID=$(printf '%s' "$CLIENT_RESP" | python3 -c 'import sys,json; j=json.load(sys.stdin); print(j.get("client",{}).get("id",""))')
if [[ -z "${CLIENT_ID:-}" ]]; then
echo "ERROR: client_id missing" >&2
exit 1
fi
echo "CLIENT_ID=$CLIENT_ID"
echo "--- SELFTEST"
SELFTEST_RESP=$(curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/selftest" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"client_id\":$CLIENT_ID,\"create_client\":false,\"install\":false,\"protocol_id\":$PROTOCOL_ID}")
printf '%s' "$SELFTEST_RESP" >"$OUT_DIR/selftest_${CLIENT_ID}.txt"
printf '%s' "$SELFTEST_RESP" | pretty_print | head -260
NEED_DIAG=$(printf '%s' "$SELFTEST_RESP" | python3 -c 'import sys,json; j=json.load(sys.stdin); peer=(j.get("wg") or {}).get("peer") or {}; hs=int(peer.get("latest_handshake") or 0); ep=str(peer.get("endpoint") or ""); print("1" if (ep=="(none)" or hs==0) else "0")')
if [[ "$NEED_DIAG" == "1" ]]; then
echo "--- DIAGNOSE HANDSHAKE"
DIAG_RESP=$(curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/diagnose-handshake" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"client_id\":$CLIENT_ID,\"duration_seconds\":5}" || true)
printf '%s' "$DIAG_RESP" >"$OUT_DIR/diagnose_${CLIENT_ID}.txt"
printf '%s' "$DIAG_RESP" | pretty_print | head -260
fi
echo "DONE (responses saved in $OUT_DIR)"
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
EMAIL="${EMAIL:-admin@amnez.ia}"
PASSWORD="${PASSWORD:-admin123}"
SERVER_ID="${SERVER_ID:-5}"
CLIENT_ID="${CLIENT_ID:-}"
DURATION="${DURATION:-10}"
OUT_FILE="${OUT_FILE:-}"
if [[ -z "${CLIENT_ID}" ]]; then
echo "ERROR: CLIENT_ID is required" >&2
exit 2
fi
if [[ -z "${OUT_FILE}" ]]; then
OUT_FILE="scripts/_cycle_out/diagnose_client_${CLIENT_ID}.json"
fi
mkdir -p "$(dirname "$OUT_FILE")"
TOKEN_JSON=$(curl -sS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD")
TOKEN=$(printf '%s' "$TOKEN_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("token",""))')
if [[ -z "${TOKEN}" ]]; then
echo "ERROR: token empty" >&2
printf '%s' "$TOKEN_JSON" | head -c 300 >&2
echo >&2
exit 3
fi
RESP_WITH_STATUS=$(curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/diagnose-handshake" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"client_id\":$CLIENT_ID,\"duration_seconds\":$DURATION}" \
-w "\n__HTTP_STATUS__%{http_code}")
HTTP_STATUS=$(printf '%s' "$RESP_WITH_STATUS" | awk -F'__HTTP_STATUS__' 'END{print $2}')
RESP=$(printf '%s' "$RESP_WITH_STATUS" | awk -F'__HTTP_STATUS__' '{print $1}')
if [[ -z "${RESP:-}" ]]; then
echo "ERROR: empty response (http_status=${HTTP_STATUS:-unknown})" >&2
exit 4
fi
TMP_FILE="${OUT_FILE}.tmp"
printf '%s' "$RESP" >"$TMP_FILE"
mv "$TMP_FILE" "$OUT_FILE"
echo "saved:$OUT_FILE (http_status=${HTTP_STATUS:-unknown})"
if [[ "${HTTP_STATUS:-}" =~ ^[0-9]+$ ]] && [[ "${HTTP_STATUS}" -ge 400 ]]; then
exit 5
fi
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
EMAIL="${EMAIL:-admin@amnez.ia}"
PASSWORD="${PASSWORD:-admin123}"
SERVER_ID="${SERVER_ID:-5}"
OUT_FILE="${OUT_FILE:-scripts/_cycle_out/diagnose_no_client.json}"
DURATION="${DURATION:-2}"
mkdir -p "$(dirname "$OUT_FILE")"
TOKEN_JSON=$(curl -sS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD")
TOKEN=$(printf '%s' "$TOKEN_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin)["token"])')
TMP_FILE="${OUT_FILE}.tmp"
RESP=$(curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/diagnose-handshake" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"duration_seconds\":$DURATION}" || true)
if [[ -z "${RESP:-}" ]]; then
echo "ERROR: empty response from diagnose-handshake" >&2
echo "PANEL_URL=$PANEL_URL SERVER_ID=$SERVER_ID" >&2
echo "Token JSON (first 200):" >&2
printf '%s' "$TOKEN_JSON" | head -c 200 >&2
echo >&2
exit 2
fi
printf '%s' "$RESP" > "$TMP_FILE"
mv "$TMP_FILE" "$OUT_FILE"
echo "saved:$OUT_FILE"
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
EMAIL="${EMAIL:-admin@amnez.ia}"
PASSWORD="${PASSWORD:-admin123}"
TOKEN_JSON=$(curl -sS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD")
TOKEN=$(printf '%s' "$TOKEN_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("token",""))')
if [[ -z "${TOKEN:-}" ]]; then
echo "ERROR: token empty" >&2
printf '%s' "$TOKEN_JSON" | head -c 200 >&2
echo >&2
exit 3
fi
curl -fsS "$PANEL_URL/api/clients" -H "Authorization: Bearer $TOKEN" | \
python3 -c 'import sys,json; j=json.load(sys.stdin); cs=j.get("clients",[]);
print("id\tname\tprotocol\tserver_id")
for c in cs:
print(f"{c.get("id")}\t{c.get("name")}\t{c.get("protocol")}\t{c.get("server_id")}")'
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
EMAIL="${EMAIL:-}"
PASSWORD="${PASSWORD:-}"
TOKEN="${TOKEN:-}"
SERVER_ID="${SERVER_ID:-1}"
PROTOCOL_ID="${PROTOCOL_ID:-}"
UNINSTALL_SLUG="${UNINSTALL_SLUG:-}"
CLIENT_NAME="${CLIENT_NAME:-smoke-client}"
CLIENT_LOGIN="${CLIENT_LOGIN:-smoke-client}"
SELFTEST="${SELFTEST:-1}"
DIAGNOSE="${DIAGNOSE:-1}"
if [[ -z "$TOKEN" ]]; then
if [[ -z "$EMAIL" || -z "$PASSWORD" ]]; then
echo "ERROR: set TOKEN or (EMAIL and PASSWORD)" >&2
exit 1
fi
echo "[1/6] Getting JWT token..." >&2
TOKEN="$(curl -fsS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD" | php -r '$j=json_decode(stream_get_contents(STDIN),true); echo $j["token"] ?? "";')"
fi
if [[ -z "$TOKEN" ]]; then
echo "ERROR: failed to obtain token" >&2
exit 1
fi
auth=(-H "Authorization: Bearer $TOKEN")
echo "[2/6] Listing active protocols..." >&2
curl -fsS "$PANEL_URL/api/protocols/active" "${auth[@]}" | cat
if [[ -n "$UNINSTALL_SLUG" ]]; then
echo "[3/6] Uninstalling protocol slug=$UNINSTALL_SLUG on server=$SERVER_ID ..." >&2
curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/$UNINSTALL_SLUG/uninstall" "${auth[@]}" | cat
else
echo "[3/6] Skipping uninstall (set UNINSTALL_SLUG to run)." >&2
fi
if [[ -n "$PROTOCOL_ID" ]]; then
echo "[4/6] Installing protocol_id=$PROTOCOL_ID on server=$SERVER_ID ..." >&2
curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/install" \
"${auth[@]}" \
-H "Content-Type: application/json" \
-d "{\"protocol_id\": $PROTOCOL_ID}" | cat
else
echo "[4/6] Skipping install (set PROTOCOL_ID to run)." >&2
fi
echo "[5/6] Creating client on server=$SERVER_ID (protocol_id=${PROTOCOL_ID:-auto})..." >&2
CREATE_PAYLOAD=$(php -r '$d=["server_id"=>(int)getenv("SERVER_ID"),"name"=>getenv("CLIENT_NAME"),"login"=>getenv("CLIENT_LOGIN")]; $pid=getenv("PROTOCOL_ID"); if($pid!==false && $pid!==""){$d["protocol_id"]= (int)$pid;} echo json_encode($d, JSON_UNESCAPED_SLASHES);')
RESP="$(curl -fsS -X POST "$PANEL_URL/api/clients/create" "${auth[@]}" -H "Content-Type: application/json" -d "$CREATE_PAYLOAD")"
echo "$RESP" | cat
CLIENT_ID=$(echo "$RESP" | php -r '$j=json_decode(stream_get_contents(STDIN),true); echo $j["client"]["id"] ?? "";')
if [[ -n "$CLIENT_ID" ]]; then
echo "[6/6] Fetching client details (includes stats sync)..." >&2
curl -fsS "$PANEL_URL/api/clients/$CLIENT_ID/details" "${auth[@]}" | cat
if [[ "$SELFTEST" == "1" ]]; then
echo >&2
echo "[selftest] Verifying generated config vs server wg0..." >&2
SELFTEST_PAYLOAD=$(php -r '$d=["protocol_id"=>getenv("PROTOCOL_ID")!==false && getenv("PROTOCOL_ID")!=="" ? (int)getenv("PROTOCOL_ID") : 0, "install"=>false, "create_client"=>false, "client_id"=>(int)getenv("CLIENT_ID")]; echo json_encode($d, JSON_UNESCAPED_SLASHES);')
SELFTEST_RESP=$(curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/selftest" \
"${auth[@]}" \
-H "Content-Type: application/json" \
-d "$SELFTEST_PAYLOAD")
echo "$SELFTEST_RESP" | cat
if [[ "$DIAGNOSE" == "1" ]]; then
# If peer endpoint is none OR latest_handshake=0, run server-side diagnostics
NEED_DIAG=$(echo "$SELFTEST_RESP" | php -r '$j=json_decode(stream_get_contents(STDIN),true); $hs=$j["wg"]["peer"]["latest_handshake"] ?? null; $ep=$j["wg"]["peer"]["endpoint"] ?? null; echo ((string)$ep==="(none)" || (int)$hs===0) ? "1" : "0";')
if [[ "$NEED_DIAG" == "1" ]]; then
echo >&2
echo "[diagnose] Collecting server-side evidence (wg/ports/firewall/tcpdump)..." >&2
DIAG_PAYLOAD=$(php -r '$d=["client_id"=>(int)getenv("CLIENT_ID"),"duration_seconds"=>5]; echo json_encode($d, JSON_UNESCAPED_SLASHES);')
curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/diagnose-handshake" \
"${auth[@]}" \
-H "Content-Type: application/json" \
-d "$DIAG_PAYLOAD" | cat
fi
fi
fi
else
echo "[6/6] No client id returned; skipping details." >&2
fi
echo >&2
echo "Done." >&2
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
EMAIL="${EMAIL:-admin@amnez.ia}"
PASSWORD="${PASSWORD:-admin123}"
CLIENT_NAME="${CLIENT_NAME:-}"
CLIENT_ID="${CLIENT_ID:-}"
OUT_DIR="${OUT_DIR:-scripts/_cycle_out}"
mkdir -p "$OUT_DIR"
if [[ -z "$CLIENT_ID" && -z "$CLIENT_NAME" ]]; then
echo "ERROR: set CLIENT_ID or CLIENT_NAME" >&2
exit 2
fi
TOKEN_JSON=$(curl -sS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD")
TOKEN=$(printf '%s' "$TOKEN_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("token",""))')
if [[ -z "${TOKEN:-}" ]]; then
echo "ERROR: token empty" >&2
printf '%s' "$TOKEN_JSON" | head -c 200 >&2
echo >&2
exit 3
fi
if [[ -z "$CLIENT_ID" ]]; then
CLIENTS_JSON=$(curl -fsS "$PANEL_URL/api/clients" -H "Authorization: Bearer $TOKEN")
CLIENT_ID=$(printf '%s' "$CLIENTS_JSON" | python3 -c 'import sys,json; j=json.load(sys.stdin); needle=sys.argv[1];
for c in j.get("clients",[]):
if str(c.get("name",""))==needle:
print(c.get("id",""));
raise SystemExit
print("")' "$CLIENT_NAME")
fi
if [[ -z "${CLIENT_ID:-}" ]]; then
echo "ERROR: client not found" >&2
exit 4
fi
RESP=$(curl -fsS -X POST "$PANEL_URL/api/clients/$CLIENT_ID/regenerate-config" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}' )
JSON_OUT="$OUT_DIR/regenerate_${CLIENT_ID}.json"
CONF_OUT="$OUT_DIR/${CLIENT_NAME:-client_${CLIENT_ID}}_regenerated.conf"
printf '%s' "$RESP" >"$JSON_OUT"
# Extract config field
printf '%s' "$RESP" | python3 -c 'import sys,json; j=json.load(sys.stdin); c=(j.get("client") or {}).get("config") or ""; sys.stdout.write(c)' >"$CONF_OUT"
echo "saved_json:$JSON_OUT"
echo "saved_conf:$CONF_OUT"
+69
View File
@@ -0,0 +1,69 @@
#!/bin/bash
# Universal cleanup script for all Amnezia containers
# Based on remove_all_containers.sh from amnezia-client
# Usage: ./cleanup_amnezia.sh
set -euo pipefail
echo "========================================="
echo "Amnezia VPN - Complete Cleanup Script"
echo "========================================="
echo ""
echo "WARNING: This will remove ALL Amnezia containers, images, and data!"
echo "Press Ctrl+C to cancel, or Enter to continue..."
read -r
echo ""
echo "Step 1: Stopping all Amnezia containers..."
CONTAINERS=$(docker ps -a | grep amnezia | awk '{print $1}' || true)
if [ -n "$CONTAINERS" ]; then
echo "$CONTAINERS" | xargs docker stop || true
echo "✓ Containers stopped"
else
echo "✓ No running containers found"
fi
echo ""
echo "Step 2: Removing all Amnezia containers..."
CONTAINERS=$(docker ps -a | grep amnezia | awk '{print $1}' || true)
if [ -n "$CONTAINERS" ]; then
echo "$CONTAINERS" | xargs docker rm -fv || true
echo "✓ Containers removed"
else
echo "✓ No containers to remove"
fi
echo ""
echo "Step 3: Removing all Amnezia images..."
IMAGES=$(docker images -a | grep amnezia | awk '{print $3}' || true)
if [ -n "$IMAGES" ]; then
echo "$IMAGES" | xargs docker rmi -f || true
echo "✓ Images removed"
else
echo "✓ No images to remove"
fi
echo ""
echo "Step 4: Removing Amnezia DNS network..."
docker network rm amnezia-dns-net 2>/dev/null && echo "✓ Network removed" || echo "✓ Network not found"
echo ""
echo "Step 5: Removing Amnezia data directory..."
if [ -d "/opt/amnezia" ]; then
rm -rf /opt/amnezia
echo "✓ Data directory removed"
else
echo "✓ Data directory not found"
fi
echo ""
echo "========================================="
echo "Cleanup completed successfully!"
echo "========================================="
echo ""
echo "Summary:"
echo "- All Amnezia containers stopped and removed"
echo "- All Amnezia Docker images removed"
echo "- Amnezia DNS network removed"
echo "- All configuration data removed from /opt/amnezia"
echo ""
+35
View File
@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/../inc/Config.php';
require_once __DIR__ . '/../inc/DB.php';
require_once __DIR__ . '/../inc/VpnClient.php';
require_once __DIR__ . '/../inc/VpnServer.php';
$pdo = DB::conn();
$clientId = 4;
echo "Loading client $clientId...\n";
$client = new VpnClient($clientId);
$data = $client->getData();
if (!$data) {
die("Client not found\n");
}
echo "Client Name: " . $data['name'] . "\n";
echo "Config: " . substr($data['config'], 0, 50) . "...\n";
echo "Running syncStats()...\n";
try {
$res = $client->syncStats();
echo "Sync Result: " . ($res ? 'TRUE' : 'FALSE') . "\n";
// Check DB
$fresh = new VpnClient($clientId);
$d = $fresh->getData();
echo "Bytes Sent: " . $d['bytes_sent'] . "\n";
echo "Bytes Recv: " . $d['bytes_received'] . "\n";
echo "Last Handshake: " . $d['last_handshake'] . "\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
+19
View File
@@ -0,0 +1,19 @@
<?php
$priv = getenv('WG_PRIV_B64') ?: '';
$priv = trim($priv);
$raw = base64_decode($priv, true);
if ($raw === false) {
fwrite(STDERR, "invalid_base64\n");
exit(2);
}
echo "raw_len=" . strlen($raw) . "\n";
if (strlen($raw) !== 32) {
fwrite(STDERR, "invalid_length\n");
exit(3);
}
if (!function_exists('sodium_crypto_scalarmult_base')) {
fwrite(STDERR, "libsodium_missing\n");
exit(4);
}
$pub = sodium_crypto_scalarmult_base($raw);
echo "pub=" . base64_encode($pub) . "\n";
+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"
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# Remove single Amnezia container
# Based on remove_container.sh from amnezia-client
# Usage: ./remove_container.sh <container_name>
set -euo pipefail
if [ $# -eq 0 ]; then
echo "Usage: $0 <container_name>"
echo "Example: $0 amnezia-awg"
exit 1
fi
CONTAINER_NAME="$1"
echo "Removing Amnezia container: $CONTAINER_NAME"
echo ""
# Stop the container
echo "Stopping container..."
docker stop "$CONTAINER_NAME" 2>/dev/null && echo "✓ Container stopped" || echo "✓ Container not running"
# Remove the container with volumes
echo "Removing container..."
docker rm -fv "$CONTAINER_NAME" 2>/dev/null && echo "✓ Container removed" || echo "✓ Container not found"
# Remove the image
echo "Removing image..."
docker rmi "$CONTAINER_NAME" 2>/dev/null && echo "✓ Image removed" || echo "✓ Image not found"
echo ""
echo "Container $CONTAINER_NAME has been removed successfully!"
+136
View File
@@ -0,0 +1,136 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
if (php_sapi_name() !== 'cli') {
die("CLI only");
}
require_once __DIR__ . '/../inc/Config.php';
require_once __DIR__ . '/../inc/DB.php';
require_once __DIR__ . '/../inc/VpnServer.php';
echo "Starting AmneziaWG Sync (DB -> Server)...\n";
try {
// Assuming Server ID 1 for now (or pass as arg)
$serverId = 1;
$server = new VpnServer($serverId);
$data = $server->getData();
if (!$data) {
die("Server not found\n");
}
$containerName = $data['container_name'] ?? 'amnezia-awg';
// 1. Get Server Params
$awgParams = json_decode($data['awg_params'] ?? '[]', true);
if (empty($awgParams)) {
// Safe Fallback if DB empty? Or error?
// Better error out to avoid breakage, but user wants FIX.
// If empty, generate new randoms?
// Let's assume params exist or fetch from current wg0 check.
// For now, fail if missing.
echo "Warning: AWG Params missing in DB. Fetching defaults/randoms...\n";
$awgParams = [
'Jc' => 5,
'Jmin' => 50,
'Jmax' => 1000,
'S1' => 100,
'S2' => 200,
'H1' => 18274619,
'H2' => 2938471,
'H3' => 918273,
'H4' => 1928374
];
}
// 2. Get Keys (Interface)
// Server Private Key should be in DB?
// vpn_servers table has server_public_key... but usually NOT private key?
// Start script puts keys in /opt/amnezia/awg/....key
// We should READ them from file to be safe.
// Read directly from HOST file to avoid container dependency (deadlock if stuck in restart loop)
$privKey = trim($server->executeCommand("cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null", true));
if (empty($privKey)) {
// Fallback: try container exec (only if host file missing)
$privKey = trim($server->executeCommand("docker exec -i $containerName cat /opt/amnezia/awg/server_private.key", true));
}
if (!$privKey || strpos($privKey, 'Error response') !== false) {
// If still missing or error message
die("Fatal: Could not retrieve Server Private Key. Check /opt/amnezia/awg/ directory.\n");
}
$vpnPort = $data['vpn_port'] ?? 51820;
// 3. Build Interface Block
$conf = "[Interface]\n";
$conf .= "PrivateKey = $privKey\n";
$conf .= "Address = 10.8.1.1/24\n"; // Hardcoded or from DB? vpn_subnet usually.
$conf .= "ListenPort = $vpnPort\n";
// Normalize params
$cleanParams = [];
foreach ($awgParams as $k => $v)
$cleanParams[strtoupper($k)] = $v;
$conf .= "Jc = " . ($cleanParams['JC'] ?? 5) . "\n";
$conf .= "Jmin = " . ($cleanParams['JMIN'] ?? 50) . "\n";
$conf .= "Jmax = " . ($cleanParams['JMAX'] ?? 1000) . "\n";
$conf .= "S1 = " . ($cleanParams['S1'] ?? 50) . "\n";
$conf .= "S2 = " . ($cleanParams['S2'] ?? 100) . "\n";
$conf .= "H1 = " . ($cleanParams['H1'] ?? 1) . "\n";
$conf .= "H2 = " . ($cleanParams['H2'] ?? 2) . "\n";
$conf .= "H3 = " . ($cleanParams['H3'] ?? 3) . "\n";
$conf .= "H4 = " . ($cleanParams['H4'] ?? 4) . "\n";
$conf .= "PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE\n";
$conf .= "PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE\n\n";
// 4. Load Clients
$pdo = DB::conn();
$stmt = $pdo->prepare("SELECT * FROM vpn_clients WHERE server_id = ? AND status = 'active'");
$stmt->execute([$serverId]);
$clients = $stmt->fetchAll();
echo "Found " . count($clients) . " clients in DB.\n";
foreach ($clients as $client) {
$pub = $client['public_key'];
if (empty($pub)) {
echo "Skipping client {$client['id']} (Empty Public Key)\n";
continue;
}
$psk = $client['preshared_key'];
$ip = $client['client_ip'];
$allowed = $client['allowed_ips'] ?? "$ip/32"; // Fallback to IP/32
$conf .= "[Peer]\n";
$conf .= "PublicKey = $pub\n";
if ($psk)
$conf .= "PresharedKey = $psk\n";
$conf .= "AllowedIPs = $allowed\n\n";
}
// 5. Write Config
// Use host path that matches container volume (-v /opt/amnezia/awg:/opt/amnezia/awg)
$hostConfPath = '/opt/amnezia/awg/wg0.conf';
$escaped = addslashes($conf);
$server->executeCommand("echo \"$escaped\" > $hostConfPath", true);
// Also copy to container path if mounted (usually same file via bind mount)
// 6. Restart Interface
echo "Restarting WireGuard interface...\n";
$server->executeCommand("docker exec -i $containerName wg-quick down wg0 || true", true);
$server->executeCommand("docker exec -i $containerName wg-quick up wg0", true);
echo "Sync Complete.\n";
} catch (Throwable $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
+244
View File
@@ -0,0 +1,244 @@
{% extends "layout.twig" %}
{% block title %}{{ t('ai.generation_preview') }} - {{ parent() }}{% endblock %}
{% block content %}
<div class="min-h-screen bg-gray-50">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-900">{{ t('ai.generation_preview') }}</h1>
<p class="mt-2 text-gray-600">{{ t('ai.generation_preview_description') }}</p>
</div>
<div class="flex space-x-3">
<a href="/settings/protocols" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
{{ t('protocols.back_to_protocols') }}
</a>
<button id="apply-script-btn" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
{{ t('ai.apply_to_protocol') }}
</button>
</div>
</div>
</div>
<!-- Generation Info -->
<div class="bg-white shadow rounded-lg mb-6">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">{{ t('ai.generation_details') }}</h2>
</div>
<div class="px-6 py-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700">{{ t('ai.model_used') }}</label>
<p class="mt-1 text-sm text-gray-900">{{ generation.model_used }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">{{ t('ai.generated_at') }}</label>
<p class="mt-1 text-sm text-gray-900">{{ generation.created_at|date('Y-m-d H:i:s') }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">{{ t('protocols.ubuntu_compatible') }}</label>
<div class="mt-1">
{% if generation.ubuntu_compatible %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
{{ t('common.compatible') }}
</span>
{% else %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
{{ t('common.not_compatible') }}
</span>
{% endif %}
</div>
</div>
{% if generation.protocol_name %}
<div>
<label class="block text-sm font-medium text-gray-700">{{ t('ai.associated_protocol') }}</label>
<p class="mt-1 text-sm text-gray-900">{{ generation.protocol_name }}</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Prompt -->
<div class="bg-white shadow rounded-lg mb-6">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">{{ t('ai.user_prompt') }}</h2>
</div>
<div class="px-6 py-4">
<p class="text-sm text-gray-900 whitespace-pre-wrap">{{ generation.prompt }}</p>
</div>
</div>
<!-- Generated Script -->
<div class="bg-white shadow rounded-lg mb-6">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex justify-between items-center">
<h2 class="text-lg font-medium text-gray-900">{{ t('ai.generated_installation_script') }}</h2>
<button id="copy-script-btn" class="inline-flex items-center px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
{{ t('ai.copy_script') }}
</button>
</div>
</div>
<div class="px-6 py-4">
<div class="bg-gray-900 text-green-400 p-4 rounded-md overflow-x-auto">
<pre id="script-content" class="text-sm whitespace-pre-wrap">{{ script }}</pre>
</div>
</div>
</div>
<!-- AI Suggestions -->
{% if suggestions %}
<div class="bg-white shadow rounded-lg mb-6">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">{{ t('ai.suggestions') }}</h2>
</div>
<div class="px-6 py-4">
<ul class="space-y-2">
{% for suggestion in suggestions %}
<li class="flex items-start">
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>
<span class="text-sm text-gray-900">{{ suggestion }}</span>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<!-- Actions -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">{{ t('common.actions') }}</h2>
</div>
<div class="px-6 py-4">
<div class="flex flex-wrap gap-3">
<button id="download-script-btn" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
{{ t('ai.download_script') }}
</button>
{% if generation.protocol_id %}
<button id="view-protocol-btn" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
{{ t('protocols.view_protocol') }}
</button>
{% endif %}
<button id="regenerate-btn" class="inline-flex items-center px-4 py-2 border border-purple-300 rounded-md shadow-sm text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
{{ t('ai.regenerate') }}
</button>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const scriptContent = document.getElementById('script-content').textContent;
const generationId = {{ generation.id }};
const protocolId = {{ generation.protocol_id ?: 'null' }};
// Copy script
document.getElementById('copy-script-btn').addEventListener('click', function() {
const textarea = document.createElement('textarea');
textarea.value = scriptContent;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
// Show feedback
const originalText = this.innerHTML;
this.innerHTML = '<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>{{ t('common.copied') }}';
setTimeout(() => {
this.innerHTML = originalText;
}, 2000);
});
// Download script
document.getElementById('download-script-btn').addEventListener('click', function() {
const blob = new Blob([scriptContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `install-${generationId}.sh`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// Apply to protocol
document.getElementById('apply-script-btn').addEventListener('click', function() {
if (!protocolId) {
alert('{{ t('ai.no_associated_protocol') }}');
return;
}
if (confirm('{{ t('ai.confirm_apply_script_to_protocol') }}')) {
fetch(`/api/ai/generations/${generationId}/apply`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('{{ t('ai.script_applied_successfully') }}');
window.location.href = `/settings/protocols/${protocolId}/edit`;
} else {
alert('{{ t('ai.error_applying_script') }}: ' + result.error);
}
})
.catch(error => {
alert('{{ t('ai.error_applying_script') }}: ' + error.message);
});
}
});
// View protocol
document.getElementById('view-protocol-btn').addEventListener('click', function() {
if (protocolId) {
window.location.href = `/settings/protocols/${protocolId}/edit`;
}
});
// Regenerate
document.getElementById('regenerate-btn').addEventListener('click', function() {
if (confirm('{{ t('ai.confirm_regenerate_script') }}')) {
// Go back to protocols page with AI assistant open
window.location.href = '/settings/protocols';
}
});
});
</script>
{% endblock %}
+8
View File
@@ -18,6 +18,7 @@
{% endif %}
</dd>
</div>
<div><dt class="text-sm text-gray-600">Логин</dt><dd>{{ client.name|default('') }}</dd></div>
<div><dt class="text-sm text-gray-600">{{ t('common.created') }}</dt><dd>{{ client.created_at }}</dd></div>
</dl>
<div class="flex gap-2">
@@ -150,6 +151,13 @@
<p class="text-sm text-gray-600 mt-2">Scan with Amnezia VPN app</p>
</div>
{% endif %}
{% 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; overflow-wrap: anywhere; word-break: break-word;">{{ protocol_output }}</pre>
</div>
{% endif %}
</div>
<script>
+4 -4
View File
@@ -40,13 +40,13 @@
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
<i class="fas fa-check-circle text-2xl"></i>
<div class="p-3 rounded-full bg-green-100 text-green-600">
<i class="fas fa-wifi text-2xl"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">{{ t('dashboard.active_clients') }}</p>
<p class="text-sm font-medium text-gray-600">{{ t('dashboard.online_now') }}</p>
<p class="text-2xl font-bold text-gray-900">
{{ servers|filter(s => s.status == 'active')|length }}
{{ online_count|default(0) }}
</p>
</div>
</div>
+74
View File
@@ -135,6 +135,28 @@
<main class="{% if user %}py-10{% endif %}">
{% block content %}{% endblock %}
</main>
<!-- Custom Confirmation Modal -->
<div id="confirmModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-[9999]" style="display:none;">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div class="mt-3 text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
</div>
<h3 class="text-lg leading-6 font-medium text-gray-900 mt-4" id="confirmModalTitle">Подтверждение</h3>
<div class="mt-2 px-7 py-3">
<p class="text-sm text-gray-500" id="confirmModalMessage">Вы уверены?</p>
</div>
<div class="items-center px-4 py-3 flex justify-center gap-4">
<button id="confirmModalCancel" class="px-4 py-2 bg-gray-200 text-gray-800 text-base font-medium rounded-md shadow-sm hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300">
Отмена
</button>
<button id="confirmModalOk" class="px-4 py-2 bg-red-600 text-white text-base font-medium rounded-md shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500">
Удалить
</button>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="bg-white border-t border-gray-200 mt-auto">
@@ -174,6 +196,58 @@
// Form will submit normally, dropdown will close on page reload
}
});
// Custom confirmation modal function (replaces native confirm)
window.showConfirmModal = function(message, title) {
return new Promise((resolve) => {
const modal = document.getElementById('confirmModal');
const titleEl = document.getElementById('confirmModalTitle');
const msgEl = document.getElementById('confirmModalMessage');
const okBtn = document.getElementById('confirmModalOk');
const cancelBtn = document.getElementById('confirmModalCancel');
if (!modal) {
// Fallback to native confirm if modal not found
resolve(confirm(message));
return;
}
titleEl.textContent = title || 'Подтверждение';
msgEl.textContent = message || 'Вы уверены?';
modal.style.display = 'flex';
modal.classList.remove('hidden');
function cleanup() {
modal.style.display = 'none';
modal.classList.add('hidden');
okBtn.removeEventListener('click', onOk);
cancelBtn.removeEventListener('click', onCancel);
modal.removeEventListener('click', onBackdrop);
}
function onOk() {
cleanup();
resolve(true);
}
function onCancel() {
cleanup();
resolve(false);
}
function onBackdrop(e) {
if (e.target === modal) {
cleanup();
resolve(false);
}
}
okBtn.addEventListener('click', onOk);
cancelBtn.addEventListener('click', onCancel);
modal.addEventListener('click', onBackdrop);
});
};
</script>
{% block scripts %}{% endblock %}
+182 -33
View File
@@ -3,43 +3,113 @@
{% block content %}
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold mb-8"><i class="fas fa-plus-circle text-purple-600"></i> Add New Server</h1>
{% if error %}<div class="mb-4 bg-red-50 border border-red-400 text-red-700 px-4 py-3 rounded">{{ error }}</div>{% endif %}
{% if error %}
<div class="mb-4 bg-red-50 border border-red-400 text-red-700 px-4 py-3 rounded">{{ error }}</div>
{% endif %}
<form method="POST" enctype="multipart/form-data" class="bg-white shadow rounded-lg p-6 space-y-6">
<div><label class="block text-sm font-medium text-gray-700">Server Name</label><input name="name" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="US Server 1"></div>
<div><label class="block text-sm font-medium text-gray-700">Host IP/Domain</label><input name="host" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="123.456.789.0"></div>
<div><label class="block text-sm font-medium text-gray-700">SSH Port</label><input name="port" type="number" value="22" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
<div><label class="block text-sm font-medium text-gray-700">SSH Username</label><input name="username" value="root" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
<div><label class="block text-sm font-medium text-gray-700">SSH Password</label><input name="password" type="password" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
<!-- Import from existing panel -->
<div class="border-t pt-6">
<div class="flex items-center mb-4">
<input type="checkbox" id="enableImport" name="enable_import" class="h-4 w-4 text-purple-600 rounded" onchange="toggleImportFields()">
<label for="enableImport" class="ml-2 text-sm font-medium text-gray-700">
{{ t('servers.import_from_panel') }}
<div>
<label class="block text-sm font-medium text-gray-700">{{ t('servers.creation_mode') }}</label>
<div class="mt-2 flex items-center gap-6">
<label class="inline-flex items-center">
<input type="radio" name="creation_mode" value="manual" class="text-purple-600" {% if selected_mode|default('manual') == 'manual' %}checked{% endif %}>
<span class="ml-2 text-sm text-gray-700">{{ t('servers.creation_mode_manual') }}</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="creation_mode" value="backup" class="text-purple-600" {% if selected_mode == 'backup' %}checked{% endif %}>
<span class="ml-2 text-sm text-gray-700">{{ t('servers.creation_mode_backup') }}</span>
</label>
</div>
<div id="importFields" style="display: none;" class="space-y-4 pl-6 border-l-2 border-purple-200">
</div>
<div id="manualSection" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Server Name</label>
<input name="name" data-field-manual required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="US Server 1" value="{{ form_data.name ?? '' }}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Installation Protocol</label>
{% set selectedProtocol = form_data.install_protocol ?? default_protocol %}
<select name="install_protocol" data-field-manual class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
{% for protocol in protocols %}
<option value="{{ protocol.slug }}" {% if protocol.slug == selectedProtocol %}selected{% endif %}>{{ protocol.name }}</option>
{% endfor %}
</select>
<p id="protocolDescription" class="mt-1 text-xs text-gray-500"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Host IP/Domain</label>
<input name="host" data-field-manual required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="123.456.789.0" value="{{ form_data.host ?? '' }}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">SSH Port</label>
<input name="port" data-field-manual type="number" value="{{ form_data.port ?? 22 }}" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">SSH Username</label>
<input name="username" data-field-manual value="{{ form_data.username ?? 'root' }}" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">{{ t('servers.select_panel_type') }}</label>
<select name="panel_type" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
<option value="">-- {{ t('servers.select_panel_type') }} --</option>
<option value="wg-easy">{{ t('servers.panel_type_wgeasy') }}</option>
<option value="3x-ui">{{ t('servers.panel_type_3xui') }}</option>
</select>
<label class="block text-sm font-medium text-gray-700">Authentication Method</label>
<div class="mt-2 flex items-center gap-6">
<label class="inline-flex items-center">
<input type="radio" name="auth_method" value="password" class="text-purple-600" checked onchange="toggleAuthMethod()">
<span class="ml-2 text-sm text-gray-700">Password</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="auth_method" value="ssh_key" class="text-purple-600" onchange="toggleAuthMethod()">
<span class="ml-2 text-sm text-gray-700">SSH Key</span>
</label>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">{{ t('servers.upload_backup_file') }}</label>
<input type="file" name="backup_file" accept=".json" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
<p class="mt-1 text-xs text-gray-500">
wg-easy: db.json | 3x-ui: export.json
</p>
<div id="authPassword">
<label class="block text-sm font-medium text-gray-700">SSH Password</label>
<input name="password" data-field-manual type="password" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div id="authSshKey" style="display: none;">
<label class="block text-sm font-medium text-gray-700">SSH Private Key</label>
<textarea name="ssh_key" data-field-manual rows="6" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-xs" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----..."></textarea>
<p class="mt-1 text-xs text-gray-500">Paste your private key (PEM or OpenSSH format)</p>
</div>
</div>
</div>
<div id="backupSection" class="space-y-6" style="display: none;">
<input type="hidden" name="backup_token" value="{{ form_data.backup_token ?? '' }}">
<div>
<label class="block text-sm font-medium text-gray-700">{{ t('servers.backup_upload_type') }}</label>
<select name="backup_upload_type" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" data-field-backup>
<option value="auto" {% if form_data.backup_upload_type|default('auto') == 'auto' %}selected{% endif %}>{{ t('servers.backup_type_auto') }}</option>
<option value="amnezia_app" {% if form_data.backup_upload_type|default('auto') == 'amnezia_app' %}selected{% endif %}>{{ t('servers.backup_type_amnezia') }}</option>
<option value="panel_backup" {% if form_data.backup_upload_type|default('auto') == 'panel_backup' %}selected{% endif %}>{{ t('servers.backup_type_panel') }}</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">{{ t('servers.upload_backup_file') }}</label>
<input type="file" name="backup_upload" accept=".backup,.json" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" data-field-backup>
<p class="mt-1 text-xs text-gray-500">{{ t('servers.backup_upload_hint') }}</p>
</div>
{% if form_data.uploaded_servers is defined and form_data.uploaded_servers %}
<div class="border-t pt-6 space-y-2">
<label class="block text-sm font-medium text-gray-700">{{ t('servers.backup_server_entry') }}</label>
<select name="backup_server_index" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" data-field-backup>
{% for server in form_data.uploaded_servers %}
{% set option_host = server.host is defined and server.host is not empty ? server.host : t('common.na') %}
{% set option_clients = server.client_count is defined ? server.client_count : t('common.na') %}
<option value="{{ server.index }}" {% if form_data.backup_server_index|default('') == (server.index ~ '') %}selected{% endif %}>
{{ server.label }}{{ t('servers.backup_summary_host') }}: {{ option_host }}, {{ t('servers.backup_summary_clients') }}: {{ option_clients }}
</option>
{% endfor %}
</select>
</div>
{% endif %}
</div>
<button type="submit" class="w-full gradient-bg text-white py-2 px-4 rounded-md hover:opacity-90">
<i class="fas fa-save mr-2"></i>Create Server
</button>
@@ -47,11 +117,90 @@
</div>
<script>
function toggleImportFields() {
const checkbox = document.getElementById('enableImport');
const fields = document.getElementById('importFields');
fields.style.display = checkbox.checked ? 'block' : 'none';
const modeRadios = Array.prototype.slice.call(document.querySelectorAll('input[name="creation_mode"]'));
const manualSection = document.getElementById('manualSection');
const backupSection = document.getElementById('backupSection');
const manualFields = Array.prototype.slice.call(document.querySelectorAll('[data-field-manual]'));
const backupFields = Array.prototype.slice.call(document.querySelectorAll('[data-field-backup]'));
const initialMode = {{ selected_mode|default('manual')|json_encode|raw }};
function setFieldsState(mode) {
manualFields.forEach(el => {
if (!el) return;
el.disabled = mode !== 'manual';
});
backupFields.forEach(el => {
if (!el) return;
el.disabled = mode !== 'backup';
});
}
function switchMode() {
const selectedRadio = document.querySelector('input[name="creation_mode"]:checked');
const mode = selectedRadio ? selectedRadio.value : 'manual';
if (manualSection) {
manualSection.style.display = mode === 'manual' ? 'block' : 'none';
}
if (backupSection) {
backupSection.style.display = mode === 'backup' ? 'block' : 'none';
}
setFieldsState(mode);
}
modeRadios.forEach(function (radio) {
radio.addEventListener('change', switchMode);
});
const initialModeRadio = document.querySelector(`input[name="creation_mode"][value="${initialMode}"]`);
if (initialModeRadio) {
initialModeRadio.checked = true;
}
switchMode();
{% set protocol_map = {} %}
{% for protocol in protocols %}
{% set protocol_map = protocol_map | merge({ (protocol.slug): (protocol.description ?? '') }) %}
{% endfor %}
const protocolDescriptions = {{ protocol_map | json_encode | raw }};
const protocolDescriptionEl = document.getElementById('protocolDescription');
const protocolSelect = document.querySelector('select[name="install_protocol"]');
function updateProtocolDescription() {
if (!protocolSelect || !protocolDescriptionEl) return;
const description = protocolDescriptions[protocolSelect.value] || '';
protocolDescriptionEl.textContent = description;
protocolDescriptionEl.style.display = description ? 'block' : 'none';
}
function toggleAuthMethod() {
const method = document.querySelector('input[name="auth_method"]:checked').value;
const passwordSection = document.getElementById('authPassword');
const keySection = document.getElementById('authSshKey');
const passwordInput = passwordSection.querySelector('input');
const keyInput = keySection.querySelector('textarea');
if (method === 'password') {
passwordSection.style.display = 'block';
keySection.style.display = 'none';
passwordInput.required = true;
keyInput.required = false;
keyInput.value = ''; // Clear key if switching back to password to avoid ambiguity
} else {
passwordSection.style.display = 'none';
keySection.style.display = 'block';
passwordInput.required = false;
passwordInput.value = ''; // Clear password
keyInput.required = true;
}
}
if (protocolSelect) {
protocolSelect.addEventListener('change', updateProtocolDescription);
updateProtocolDescription();
}
// Initialize auth method state
toggleAuthMethod();
</script>
{% endblock %}
+113 -32
View File
@@ -2,55 +2,136 @@
{% block title %}Deploy {{ server.name }}{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6">Deploying: {{ server.name }}</h1>
<h1 class="text-2xl font-bold mb-1">Deploying: {{ server.name }}</h1>
<p class="text-sm text-gray-400 mb-6">Protocol: {{ server.install_protocol ?? 'amnezia-wg' }}</p>
<div id="deployLog" class="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm h-96 overflow-y-auto mb-4">
<div>Ready to deploy...</div>
</div>
<div id="deployActions" class="flex gap-3 mb-4 hidden"></div>
<button id="deployBtn" onclick="deploy()" class="gradient-bg text-white px-6 py-2 rounded hover:opacity-90 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
<span id="btnText">Start Deployment</span>
<i id="btnSpinner" class="fas fa-spinner fa-spin ml-2 hidden"></i>
</button>
</div>
<script>
function deploy() {
let pendingDecisionToken = null;
function setButtonState(isProcessing, label) {
const btn = document.getElementById('deployBtn');
const btnText = document.getElementById('btnText');
const btnSpinner = document.getElementById('btnSpinner');
btn.disabled = isProcessing;
btnText.textContent = label;
if (isProcessing) {
btnSpinner.classList.remove('hidden');
} else {
btnSpinner.classList.add('hidden');
}
}
function appendLog(message, cssClass) {
const log = document.getElementById('deployLog');
// Disable button and show spinner
btn.disabled = true;
btnText.textContent = 'Deploying...';
btnSpinner.classList.remove('hidden');
log.innerHTML = '<div>📡 Connecting to server...</div>';
log.innerHTML += '<div>🔧 Installing Docker...</div>';
log.innerHTML += '<div>📦 Building container...</div>';
log.innerHTML += '<div>🔐 Generating keys...</div>';
log.innerHTML += '<div>⚙️ Configuring WireGuard...</div>';
fetch('/servers/{{ server.id }}/deploy', {method: 'POST'})
.then(r => r.json())
.then(d => {
if (d.success) {
log.innerHTML += '<div class="text-green-500 font-bold">✅ Deployment successful!</div>';
log.innerHTML += '<div class="text-yellow-300">🔌 VPN Port: ' + d.vpn_port + '</div>';
log.innerHTML += '<div class="text-yellow-300">🔑 Public Key: ' + d.public_key.substring(0, 40) + '...</div>';
btnText.textContent = 'Redirecting...';
btnSpinner.classList.add('hidden');
const line = document.createElement('div');
if (cssClass) {
line.className = cssClass;
}
line.innerHTML = message;
log.appendChild(line);
log.scrollTop = log.scrollHeight;
}
function hideActions() {
const actions = document.getElementById('deployActions');
actions.classList.add('hidden');
actions.innerHTML = '';
}
function showActions(options) {
const actions = document.getElementById('deployActions');
actions.innerHTML = '';
Object.keys(options || {}).forEach(key => {
const option = options[key];
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = option.label || key;
btn.className = 'px-4 py-2 rounded bg-white text-gray-800 border border-gray-200 shadow-sm hover:bg-gray-50 transition';
btn.onclick = function () {
hideActions();
deploy(option.mode || key);
};
actions.appendChild(btn);
});
if (actions.childElementCount > 0) {
actions.classList.remove('hidden');
}
}
function deploy(mode) {
const payload = {};
if (mode) {
payload.install_mode = mode;
}
if (pendingDecisionToken) {
payload.decision_token = pendingDecisionToken;
}
if (!mode) {
pendingDecisionToken = null;
document.getElementById('deployLog').innerHTML = '';
appendLog('📡 Connecting to server...');
appendLog('🔧 Preparing environment...');
}
hideActions();
setButtonState(true, mode ? 'Processing...' : 'Deploying...');
fetch('/servers/{{ server.id }}/deploy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(async response => {
const data = await response.json().catch(() => ({ success: false, error: 'Invalid server response' }));
if (!response.ok && !data.requires_action) {
throw new Error(data.error || ('HTTP ' + response.status));
}
return data;
})
.then(data => {
if (data.requires_action) {
pendingDecisionToken = data.decision_token || null;
const details = data.details || {};
appendLog('⚠️ ' + (details.message || 'Existing configuration detected'), 'text-yellow-300');
if (details.details && details.details.summary) {
appendLog(details.details.summary, 'text-yellow-200');
} else if (details.details) {
appendLog(JSON.stringify(details.details), 'text-yellow-200 text-xs');
}
showActions(data.options);
setButtonState(false, 'Select action');
return;
}
if (data.success) {
pendingDecisionToken = null;
hideActions();
appendLog('✅ Deployment successful!', 'text-green-500 font-bold');
if (data.vpn_port) {
appendLog('🔌 VPN Port: ' + data.vpn_port, 'text-yellow-300');
}
if (data.public_key) {
appendLog('🔑 Public Key: ' + data.public_key.substring(0, 40) + '...', 'text-yellow-300');
}
setButtonState(true, 'Redirecting...');
setTimeout(() => window.location.href = '/servers/{{ server.id }}', 2000);
} else {
log.innerHTML += '<div class="text-red-500 font-bold">❌ Error: ' + (d.error || 'Unknown error') + '</div>';
btn.disabled = false;
btnText.textContent = 'Retry Deployment';
btnSpinner.classList.add('hidden');
appendLog('❌ ' + (data.error || 'Unknown error'), 'text-red-500 font-bold');
setButtonState(false, 'Retry Deployment');
}
})
.catch(e => {
log.innerHTML += '<div class="text-red-500 font-bold">❌ Network error: ' + e.message + '</div>';
btn.disabled = false;
btnText.textContent = 'Retry Deployment';
btnSpinner.classList.add('hidden');
.catch(error => {
appendLog('❌ Network error: ' + error.message, 'text-red-500 font-bold');
setButtonState(false, 'Retry Deployment');
});
}
</script>
+2 -3
View File
@@ -98,9 +98,8 @@
<a href="/servers/{{ server.id }}" class="text-purple-600 hover:text-purple-900">
<i class="fas fa-eye mr-1"></i>{{ t('servers.view') }}
</a>
<form method="POST" action="/servers/{{ server.id }}/delete" class="inline"
onsubmit="return confirm('{{ t('message.confirm') }} Delete server {{ server.name }}?');">
<button type="submit" class="text-red-600 hover:text-red-900">
<form method="POST" action="/servers/{{ server.id }}/delete" class="inline" id="delete-form-{{ server.id }}">
<button type="button" class="text-red-600 hover:text-red-900" onclick="(async()=>{ event.stopPropagation(); if(await showConfirmModal('Удалить сервер {{ server.name }}?', 'Удаление сервера')) { document.getElementById('delete-form-{{ server.id }}').submit(); } })()">
<i class="fas fa-trash mr-1"></i>{{ t('servers.delete') }}
</button>
</form>
+389 -46
View File
@@ -60,8 +60,50 @@
<div><dt class="text-sm text-gray-600">{{ t('common.status') }}</dt><dd><span class="px-2 py-1 bg-green-100 text-green-800 rounded text-sm">{{ server.status }}</span></dd></div>
<div><dt class="text-sm text-gray-600">VPN Port</dt><dd>{{ server.vpn_port }}</dd></div>
<div><dt class="text-sm text-gray-600">Subnet</dt><dd>{{ server.vpn_subnet }}</dd></div>
</dl>
<div class="mt-4 flex items-center gap-2">
<button type="button" id="uninstallAllBtn" class="px-3 py-1 bg-gray-600 text-white rounded text-sm">Удалить все протоколы</button>
<span id="uninstallMsg" class="ml-3 text-sm text-gray-600"></span>
</div>
<div class="mt-4">
<label class="block text-sm text-gray-600 mb-1">Добавить протокол</label>
<div class="flex items-center gap-2">
<select id="availableProtocolSelect" class="px-3 py-2 border rounded">
{% for p in available_protocols %}
<option value="{{ p.id }}">{{ p.name }}</option>
{% endfor %}
</select>
<button id="activateProtocolBtn" class="px-3 py-1 bg-green-600 text-white rounded text-sm">Установить</button>
<span id="activateMsg" class="ml-3 text-sm text-gray-600"></span>
</div>
</div>
<!-- Установка протоколов выполняется только через Настройки -->
<div class="mt-4">
<label class="block text-sm text-gray-600 mb-1">Установленные протоколы</label>
<div class="space-y-2">
{% for sp in server_protocols %}
<div class="border rounded px-3 py-2">
<div class="flex items-center justify-between">
<div class="text-sm font-medium">{{ sp.name }} <span class="text-gray-500">({{ sp.slug }})</span></div>
<button type="button" class="px-3 py-1 bg-red-600 text-white rounded text-sm btn-uninstall-sp" data-slug="{{ sp.slug }}">Удалить</button>
</div>
<div class="mt-1 text-xs text-gray-600">
{% if sp.server_host %}<span>Host: {{ sp.server_host }}</span>{% endif %}
{% if sp.server_port %}<span class="ml-2">Port: {{ sp.server_port }}</span>{% endif %}
</div>
</div>
{% else %}
<div class="text-sm text-gray-500">Нет установленных протоколов</div>
{% endfor %}
</div>
<div id="uninstallSpMsg" class="mt-2 text-sm text-gray-600"></div>
</div>
{% if server.status == 'active' %}
<div class="metric-mini" id="serverMetrics">
<div class="metric-row">
@@ -96,7 +138,17 @@
<form method="POST" action="/servers/{{ server.id }}/clients/create" class="space-y-3" id="createClientForm">
<div>
<input name="name" placeholder="{{ t('clients.name') }}" required class="w-full px-3 py-2 border rounded" id="clientName">
<p class="text-xs text-gray-500 mt-1">Spaces will be replaced with underscore. All characters allowed including Cyrillic.</p>
</div>
<div>
<input name="login" placeholder="Логин (уникально на сервере, пусто — из имени)" class="w-full px-3 py-2 border rounded" id="clientLogin">
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ t('ai.protocol_type') }}</label>
<select name="protocol_id" class="w-full px-3 py-2 border rounded">
{% for sp in server_protocols %}
<option value="{{ sp.protocol_id }}">{{ sp.name }}</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ t('clients.expiration') }}</label>
@@ -136,6 +188,31 @@
</form>
</div>
</div>
<div class="bg-white rounded shadow p-6 mb-8">
<h3 class="font-bold mb-4 flex items-center gap-2">
<i class="fas fa-file-import text-purple-500"></i>
{{ t('servers.config_import_title') }}
</h3>
<p class="text-sm text-gray-600 mb-4">{{ t('servers.config_import_hint') }}</p>
<form method="POST" action="/servers/{{ server.id }}/config/import" enctype="multipart/form-data" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('servers.config_import_type_label') }}</label>
<select name="import_type" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
<option value="panel_backup">{{ t('servers.config_import_type_panel') }}</option>
<option value="amnezia_app">{{ t('servers.config_import_type_amnezia') }}</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('servers.config_import_file_label') }}</label>
<input type="file" name="config_file" accept=".json,.backup" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
<p class="text-xs text-gray-500 mt-1">{{ t('servers.config_import_file_hint') }}</p>
</div>
<button type="submit" class="gradient-bg text-white px-4 py-2 rounded">
<i class="fas fa-upload mr-2"></i>{{ t('servers.config_import_submit') }}
</button>
</form>
</div>
<!-- Backup Section -->
<div class="bg-white rounded shadow mb-8">
@@ -154,15 +231,29 @@
<div class="bg-white rounded shadow">
<div class="px-6 py-4 border-b flex justify-between items-center">
<h3 class="font-bold">{{ t('clients.title') }} ({{ clients|length }})</h3>
<button onclick="syncAllStats({{ server.id }})" class="text-purple-600 hover:text-purple-800 text-sm">
<i class="fas fa-sync-alt"></i> {{ t('clients.sync_stats') }}
</button>
<div class="flex items-center gap-3">
<form method="GET" action="/servers/{{ server.id }}" class="flex items-center gap-2">
<label class="text-sm text-gray-600">{{ t('ai.protocol_type') }}</label>
<select name="protocol_id" class="px-3 py-2 border rounded" onchange="this.form.submit()">
<option value="">Все</option>
{% for sp in server_protocols %}
<option value="{{ sp.protocol_id }}" {% if selected_protocol_id == sp.protocol_id %}selected{% endif %}>{{ sp.name }}</option>
{% endfor %}
</select>
</form>
<button onclick="syncAllStats({{ server.id }})" class="text-purple-600 hover:text-purple-800 text-sm">
<i class="fas fa-sync-alt"></i> {{ t('clients.sync_stats') }}
</button>
</div>
</div>
{% if clients|length > 0 %}
<table class="w-full">
<div class="w-full overflow-x-auto">
<table class="w-full" style="min-width: 1120px;">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.name') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Логин</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ai.protocol_type') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.ip') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.status') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.expiration') }}</th>
@@ -177,12 +268,23 @@
{% for client in clients %}
<tr class="border-t">
<td class="px-6 py-4">{{ client.name }}</td>
<td class="px-6 py-4">{{ client.client_ip }}</td>
<td class="px-6 py-4">{{ client.name }}</td>
<td class="px-6 py-4">
{% if client.status == 'active' %}
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">{{ t('status.active') }}</span>
{% if client.protocol_name %}
<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{{ client.protocol_name }}</span>
{% else %}
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t('status.disabled') }}</span>
<span class="text-gray-400">-</span>
{% endif %}
</td>
<td class="px-6 py-4">{{ client.client_ip }}</td>
<td class="px-6 py-4" data-client-name="{{ client.name }}" data-client-status="{{ client.status }}" data-last-handshake="{{ client.last_handshake }}">
{% set is_online_by_handshake = client.last_handshake and (("now"|date('U') - client.last_handshake|date('U')) < 300) %}
{% if client.name in online_logins or is_online_by_handshake %}
<span class="online-badge px-2 py-1 bg-green-100 text-green-800 rounded text-xs"><i class="fas fa-wifi mr-1"></i>Online</span>
{% elseif client.status == 'active' %}
<span class="status-badge px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">{{ t('status.active') }}</span>
{% else %}
<span class="status-badge px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t('status.disabled') }}</span>
{% endif %}
</td>
<td class="px-6 py-4 text-sm">
@@ -203,18 +305,18 @@
<span class="text-gray-600">{{ client.expires_at|date('Y-m-d') }}</span>
{% endif %}
{% else %}
<span class="text-gray-400">{{ t('clients.never_expires') }}</span>
<span class="text-green-500 text-xl" title="{{ t('clients.never_expires') }}">&infin;</span>
{% endif %}
</td>
<td class="px-6 py-4 text-sm">
<div class="text-gray-600">
{{ (client.bytes_sent|default(0) / 1024 / 1024)|number_format(2) }} MB
<td class="px-2 py-2 text-xs">
<div class="text-gray-600 font-mono">
{{ (client.bytes_sent|default(0) / 1024 / 1024)|number_format(2) }} MB
</div>
<div class="text-gray-600">
{{ (client.bytes_received|default(0) / 1024 / 1024)|number_format(2) }} MB
<div class="text-gray-600 font-mono">
{{ (client.bytes_received|default(0) / 1024 / 1024)|number_format(2) }} MB
</div>
</td>
<td class="px-6 py-4 text-sm">
<td class="px-2 py-2 text-xs text-center">
{% if client.traffic_limit %}
{% set total_traffic = (client.bytes_sent|default(0) + client.bytes_received|default(0)) %}
{% set limit_gb = (client.traffic_limit / 1073741824)|number_format(2) %}
@@ -222,26 +324,35 @@
{% set percentage = ((total_traffic / client.traffic_limit) * 100)|round %}
{% if percentage >= 100 %}
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">
<span class="px-2 py-1 bg-red-100 text-red-800 rounded">
<i class="fas fa-exclamation-circle"></i> {{ t('clients.overlimit') }}
</span>
{% elseif percentage >= 80 %}
<span class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs">
<span class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded">
{{ used_gb }} / {{ limit_gb }} GB ({{ percentage }}%)
</span>
{% else %}
<span class="text-gray-600">{{ used_gb }} / {{ limit_gb }} GB</span>
{% endif %}
{% else %}
<span class="text-gray-400">{{ t('clients.unlimited') }}</span>
<span class="text-green-500 text-lg" title="{{ t('clients.unlimited') }}">&infin;</span>
{% endif %}
</td>
<td class="px-6 py-4 text-sm">
<div id="client-speed-{{ client.id }}" class="text-gray-400">-</div>
<td class="px-2 py-2 text-xs">
<div class="flex flex-col items-center" style="width: 120px; max-width: 120px;">
<div style="height: 30px; width: 100%;">
<canvas id="clientSparkline-{{ client.id }}"></canvas>
</div>
<div id="client-speed-{{ client.id }}" class="text-gray-600 text-[10px] mt-1 font-mono text-center leading-tight">
<div class="text-green-600 whitespace-nowrap">↑{{ ((client.speed_up|default(0) * 8) / 1000000)|number_format(2) }} Mbit</div>
<div class="text-blue-600 whitespace-nowrap">↓{{ ((client.speed_down|default(0) * 8) / 1000000)|number_format(2) }} Mbit</div>
</div>
</div>
</td>
<td class="px-6 py-4 text-sm">
<td class="px-2 py-2 text-xs whitespace-nowrap text-right">
{% if client.last_handshake %}
<span class="text-gray-600">{{ client.last_handshake }}</span>
<span class="text-gray-600 block">{{ client.last_handshake|split(' ')|first }}</span>
<span class="text-gray-400 block">{{ client.last_handshake|split(' ')|last }}</span>
{% else %}
<span class="text-gray-400">{{ t('clients.never') }}</span>
{% endif %}
@@ -250,7 +361,7 @@
<a href="/clients/{{ client.id }}" class="text-purple-600 hover:text-purple-800 mr-2">{{ t('servers.view') }}</a>
{% if client.status == 'active' %}
<form method="POST" action="/clients/{{ client.id }}/revoke" style="display:inline;">
<button type="submit" class="text-orange-600 hover:text-orange-800 mr-2" onclick="return confirm('{{ t('clients.revoke_confirm') }}')">{{ t('clients.revoke') }}</button>
<button type="button" class="text-orange-600 hover:text-orange-800 mr-2" onclick="confirmAction(this, '{{ t('clients.revoke_confirm') }}')">{{ t('clients.revoke') }}</button>
</form>
{% else %}
<form method="POST" action="/clients/{{ client.id }}/restore" style="display:inline;">
@@ -258,13 +369,14 @@
</form>
{% endif %}
<form method="POST" action="/clients/{{ client.id }}/delete" style="display:inline;">
<button type="submit" class="text-red-600 hover:text-red-800" onclick="return confirm('{{ t('clients.delete_confirm') }}')">{{ t('clients.delete') }}</button>
<button type="button" class="text-red-600 hover:text-red-800" onclick="confirmAction(this, '{{ t('clients.delete_confirm') }}')">{{ t('clients.delete') }}</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="p-12 text-center text-gray-500">{{ t('clients.no_clients') }}</div>
{% endif %}
@@ -272,6 +384,12 @@
</div>
<script>
async function confirmAction(btn, message) {
if (await showConfirmModal(message)) {
btn.closest('form').submit();
}
}
function toggleExpirationInput() {
const select = document.getElementById('expirationSelect');
const input = document.getElementById('expirationSeconds');
@@ -286,6 +404,105 @@ function toggleExpirationInput() {
}
}
document.addEventListener('DOMContentLoaded', function() {
const uninstallAllBtn = document.getElementById('uninstallAllBtn');
const msg = document.getElementById('uninstallMsg');
if (uninstallAllBtn) {
uninstallAllBtn.addEventListener('click', async function(e) {
console.log('uninstallAllBtn clicked');
e.preventDefault();
e.stopPropagation();
if (!confirm('Удалить все Amnezia-контейнеры на сервере?')) {
console.log('User canceled');
return;
}
console.log('Starting uninstall all...');
uninstallAllBtn.disabled = true;
msg.innerHTML = '<i class="fas fa-circle-notch fa-spin text-red-600 mr-2"></i><span class="text-gray-700">Удаление всех контейнеров...</span>';
try {
const res = await fetch(`/servers/{{ server.id }}/protocols/uninstall-all`, { method: 'POST', credentials: 'same-origin' });
const data = await res.json();
console.log('Response:', data);
if (data.success) {
msg.textContent = data.message || 'Успешно';
setTimeout(() => location.reload(), 1200);
} else {
msg.textContent = data.error || 'Ошибка';
}
} catch (e) {
console.error('Error:', e);
msg.textContent = e.message;
}
uninstallAllBtn.disabled = false;
});
}
const activateBtn = document.getElementById('activateProtocolBtn');
if (activateBtn) {
activateBtn.addEventListener('click', async function() {
const select = document.getElementById('availableProtocolSelect');
const msg2 = document.getElementById('activateMsg');
msg2.textContent = '';
const pid = select ? select.value : '';
if (!pid) { msg2.textContent = 'Нет доступных протоколов'; return; }
activateBtn.disabled = true;
msg2.innerHTML = '<i class="fas fa-circle-notch fa-spin text-blue-600 mr-2"></i><span class="text-gray-700">Установка протокола...</span>';
try {
const res = await fetch('/servers/{{ server.id }}/protocols/activate', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'protocol_id=' + encodeURIComponent(pid)
});
let data;
const ct = res.headers.get('content-type') || '';
if (ct.includes('application/json')) { data = await res.json(); } else { data = { error: await res.text() }; }
if (res.ok && data && data.success !== false && !data.error) {
msg2.textContent = 'Готово';
setTimeout(() => location.reload(), 1000);
} else {
msg2.textContent = (data && data.error) ? data.error : ('Ошибка установки (' + res.status + ')');
}
} catch (e) {
msg2.textContent = e.message || 'Ошибка связи';
}
activateBtn.disabled = false;
});
}
document.querySelectorAll('.btn-uninstall-sp').forEach(btn => {
btn.addEventListener('click', async function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const confirmed = await showConfirmModal('Удалить протокол и всех его клиентов?', 'Удаление протокола');
if (!confirmed) return;
const slug = btn.getAttribute('data-slug');
const m = document.getElementById('uninstallSpMsg');
m.textContent = '';
btn.disabled = true;
m.innerHTML = '<i class="fas fa-circle-notch fa-spin text-red-600 mr-2"></i><span class="text-gray-700">Удаление протокола...</span>';
try {
const resp = await fetch('/servers/{{ server.id }}/protocols/' + encodeURIComponent(slug) + '/uninstall', { method: 'POST', credentials: 'same-origin' });
let data;
const ct = resp.headers.get('content-type') || '';
if (ct.includes('application/json')) { data = await resp.json(); } else { data = { error: await resp.text() }; }
if (resp.ok && data && !data.error) {
m.textContent = 'Удалено. Клиенты: ' + (data.clients_removed || 0);
setTimeout(() => location.reload(), 800);
} else {
m.textContent = (data && data.error) ? data.error : ('Ошибка удаления (' + resp.status + ')');
}
} catch (e) {
m.textContent = e.message || 'Ошибка связи';
}
btn.disabled = false;
});
});
});
function toggleTrafficInput() {
const select = document.getElementById('trafficSelect');
const input = document.getElementById('trafficMegabytes');
@@ -304,12 +521,11 @@ document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('createClientForm');
const clientNameInput = document.getElementById('clientName');
// Auto-replace spaces with underscores
if (clientNameInput) {
clientNameInput.addEventListener('input', function(e) {
// Replace only spaces with underscore, allow all other characters including Cyrillic
const clientLoginInput = document.getElementById('clientLogin');
if (clientLoginInput) {
clientLoginInput.addEventListener('input', function(e) {
let value = e.target.value;
let sanitized = value.replace(/ /g, '_');
let sanitized = value.replace(/\s+/g, '_');
if (value !== sanitized) {
e.target.value = sanitized;
}
@@ -662,39 +878,134 @@ if (document.getElementById('cpuSparkline')) {
}
// Update client speeds
let clientCharts = {};
function prepareSparklineSeries(values) {
const cleaned = values.map(v => {
const n = Number(v);
return Number.isFinite(n) && n > 0 ? n : 0;
});
const nonZero = cleaned.filter(v => v > 0).sort((a, b) => a - b);
let capped = cleaned;
// Suppress single extreme spikes that make the mini-chart unreadable.
if (nonZero.length >= 5) {
const p95Index = Math.floor((nonZero.length - 1) * 0.95);
const p95 = nonZero[p95Index] || 0;
const cap = p95 > 0 ? p95 * 2 : 0;
if (cap > 0) {
capped = cleaned.map(v => Math.min(v, cap));
}
}
// Small moving average to reduce jitter on tiny sparklines.
return capped.map((_, i, arr) => {
const from = Math.max(0, i - 1);
const to = Math.min(arr.length - 1, i + 1);
let sum = 0;
let count = 0;
for (let j = from; j <= to; j++) {
sum += arr[j];
count++;
}
return count > 0 ? sum / count : 0;
});
}
async function updateClientSpeeds() {
const clientRows = document.querySelectorAll('[id^="client-speed-"]');
console.log('Found client speed rows:', clientRows.length);
for (const row of clientRows) {
const clientId = row.id.replace('client-speed-', '');
console.log(`Fetching metrics for client ${clientId}`);
const canvasId = `clientSparkline-${clientId}`;
const canvas = document.getElementById(canvasId);
try {
const response = await fetch(`/api/clients/${clientId}/metrics?hours=1`, {
const response = await fetch(`/api/clients/${clientId}/metrics?hours=24`, { // Fetch 24h for sparkline
credentials: 'same-origin'
});
const data = await response.json();
console.log(`Client ${clientId} metrics:`, data);
if (data.success && data.metrics && data.metrics.length > 0) {
const latest = data.metrics[data.metrics.length - 1];
const speedUp = parseFloat(latest.speed_up_kbps).toFixed(1);
const speedDown = parseFloat(latest.speed_down_kbps).toFixed(1);
if (data.success && data.metrics) {
const metrics = data.metrics; // Use all points for chart
// Format as compact badge
row.innerHTML = `<span class="text-xs text-gray-700">↑${speedUp} ↓${speedDown} KB/s</span>`;
// 1. Render/Update Chart
if (canvas) {
const labels = metrics.map((_, i) => i);
const rawUp = metrics.map(m => (parseFloat(m.speed_up_kbps) / 1000)); // Mbps
const rawDown = metrics.map(m => (parseFloat(m.speed_down_kbps) / 1000)); // Mbps
const dataUp = prepareSparklineSeries(rawUp);
const dataDown = prepareSparklineSeries(rawDown);
if (clientCharts[clientId]) {
// Update existing chart
clientCharts[clientId].data.labels = labels;
clientCharts[clientId].data.datasets[0].data = dataUp;
clientCharts[clientId].data.datasets[1].data = dataDown;
clientCharts[clientId].update('none');
} else {
// Create new chart
clientCharts[clientId] = new Chart(canvas, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Up',
data: dataUp,
borderColor: '#16a34a', // green-600
borderWidth: 1.5,
pointRadius: 0,
fill: false,
tension: 0.4
},
{
label: 'Down',
data: dataDown,
borderColor: '#2563eb', // blue-600
borderWidth: 1.5,
pointRadius: 0,
fill: false,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false }, tooltip: { enabled: false } },
scales: {
x: { display: false },
y: { display: false, beginAtZero: true }
},
animation: false
}
});
}
}
// 2. Update Text Badge (Last Known Speed)
if (metrics.length > 0) {
const latest = metrics[metrics.length - 1];
const speedUp = (parseFloat(latest.speed_up_kbps) / 1000).toFixed(2);
const speedDown = (parseFloat(latest.speed_down_kbps) / 1000).toFixed(2);
row.innerHTML = `
<div class="text-green-600 whitespace-nowrap">↑${speedUp} Mbit</div>
<div class="text-blue-600 whitespace-nowrap">↓${speedDown} Mbit</div>
`;
} else {
row.innerHTML = '<span class="text-gray-400">-</span>';
}
} else {
console.log(`No metrics for client ${clientId}`);
row.innerHTML = '<span class="text-xs text-gray-400">-</span>';
}
} catch (error) {
console.error(`Failed to fetch metrics for client ${clientId}:`, error);
row.innerHTML = '<span class="text-xs text-gray-400">-</span>';
row.innerHTML = '<span class="text-xs text-gray-400">Error</span>';
}
}
}
@@ -704,6 +1015,38 @@ if (document.querySelector('[id^="client-speed-"]')) {
updateClientSpeeds();
setInterval(updateClientSpeeds, 30000);
}
// Real-time online status updates
async function updateOnlineStatus() {
const serverId = {{ server.id }};
try {
const response = await fetch(`/api/servers/${serverId}/online`, {
credentials: 'same-origin'
});
if (!response.ok) return;
const data = await response.json();
if (!data.success) return;
const onlineSet = new Set(data.online);
document.querySelectorAll('td[data-client-name]').forEach(cell => {
const clientName = cell.dataset.clientName;
const clientStatus = cell.dataset.clientStatus;
if (onlineSet.has(clientName)) {
cell.innerHTML = '<span class="online-badge px-2 py-1 bg-green-100 text-green-800 rounded text-xs"><i class="fas fa-wifi mr-1"></i>Online</span>';
} else if (clientStatus === 'active') {
cell.innerHTML = '<span class="status-badge px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">{{ t("status.active") }}</span>';
} else {
cell.innerHTML = '<span class="status-badge px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t("status.disabled") }}</span>';
}
});
} catch (e) {
console.error('Failed to update online status:', e);
}
}
// Poll every 5 seconds
setInterval(updateOnlineStatus, 5000);
{% endif %}
</script>
{% endblock %}
+222 -2
View File
@@ -42,21 +42,54 @@
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-language mr-2"></i>{{ t('settings.translations') }}
</a>
<a href="/tools/qr-decode"
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-qrcode mr-2"></i>QR Декодер
</a>
{% if user.role == 'admin' %}
<a href="#" onclick="showTab('users'); return false;" id="tab-users"
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-users mr-2"></i>{{ t('settings.users') }}
</a>
<a href="/settings/ldap"
<a href="#" onclick="showTab('ldap'); return false;" id="tab-ldap"
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-network-wired mr-2"></i>LDAP
</a>
<a href="#" onclick="showTab('protocols'); return false;" id="tab-protocols"
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-cubes mr-2"></i>{{ t('settings.protocol_management') }}
</a>
<a href="/admin/logs"
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-file-alt mr-2"></i>{{ 'Логи' | trans }}
</a>
{% endif %}
</nav>
</div>
<!-- Profile Tab -->
<div id="content-profile" class="tab-content">
<div class="bg-white shadow rounded-lg mb-6">
<div class="px-6 py-5 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">
<i class="fas fa-user mr-2 text-purple-600"></i>Профиль
</h2>
</div>
<div class="px-6 py-5">
<form method="POST" action="/settings/profile">
<div class="space-y-4 max-w-md">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Отображаемое имя</label>
<input type="text" name="display_name" value="{{ user.display_name|default(user.name) }}"
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
</div>
<button type="submit" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
<i class="fas fa-save mr-2"></i>{{ t('form.save') }}
</button>
</div>
</form>
</div>
</div>
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-5 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">
@@ -278,6 +311,11 @@
<i class="fas fa-trash"></i> {{ t('clients.delete') }}
</button>
</form>
{% else %}
<!-- For the current user show a Change Password action in Actions column -->
<a href="#" id="self-change-password" onclick="showTab('profile'); return false;" class="text-purple-600 hover:text-purple-900">
<i class="fas fa-key mr-1"></i> {{ t('settings.change_password') }}
</a>
{% endif %}
</td>
</tr>
@@ -286,6 +324,163 @@
</table>
</div>
</div>
<!-- LDAP Tab -->
<div id="content-ldap" class="tab-content hidden">
<div class="bg-white rounded-lg shadow-md p-6 mb-6 max-w-5xl">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-gray-900">{{ t('ldap.settings') }}</h2>
<button id="testConnection" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
{{ t('ldap.test_connection') }}
</button>
</div>
<form id="ldapForm" method="POST" action="/settings/ldap/save">
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
<label class="flex items-center cursor-pointer">
<input type="checkbox" name="enabled" value="1"
{% if config.enabled %}checked{% endif %}
class="w-5 h-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
<span class="ml-3 text-lg font-medium text-gray-900">{{ t('ldap.enable_ldap_auth') }}</span>
</label>
<p class="mt-2 ml-8 text-sm text-gray-600">{{ t('ldap.enable_description') }}</p>
</div>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ t('ldap.host') }} <span class="text-red-500">*</span>
</label>
<input type="text" name="host" value="{{ config.host }}" required
placeholder="ldap.example.com"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ t('ldap.port') }}
</label>
<input type="number" name="port" value="{{ config.port }}"
placeholder="389"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
<div>
<label class="flex items-center cursor-pointer">
<input type="checkbox" name="use_tls" value="1"
{% if config.use_tls %}checked{% endif %}
class="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">{{ t('ldap.use_tls') }} (LDAPS)</span>
</label>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ t('ldap.base_dn') }} <span class="text-red-500">*</span>
</label>
<input type="text" name="base_dn" value="{{ config.base_dn }}" required
placeholder="dc=example,dc=com"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.base_dn_description') }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ t('ldap.bind_dn') }} <span class="text-red-500">*</span>
</label>
<input type="text" name="bind_dn" value="{{ config.bind_dn }}" required
placeholder="cn=admin,dc=example,dc=com"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.bind_dn_description') }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ t('ldap.bind_password') }} <span class="text-red-500">*</span>
</label>
<input type="password" name="bind_password" value="{{ config.bind_password }}" required
placeholder="••••••••"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ t('ldap.user_search_filter') }}
</label>
<input type="text" name="user_search_filter" value="{{ config.user_search_filter }}"
placeholder="(uid=%s)"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.user_search_filter_description') }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ t('ldap.group_search_filter') }}
</label>
<input type="text" name="group_search_filter" value="{{ config.group_search_filter }}"
placeholder="(memberUid=%s)"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ t('ldap.sync_interval') }}
</label>
<input type="number" name="sync_interval" value="{{ config.sync_interval }}"
placeholder="30"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.sync_interval_description') }}</p>
</div>
</div>
<div class="mt-6 flex gap-4">
<button type="submit" class="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700">
{{ t('common.save') }}
</button>
<a href="/settings" class="px-6 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400">
{{ t('common.cancel') }}
</a>
</div>
</form>
</div>
<div class="bg-white rounded-lg shadow-md p-6 max-w-5xl">
<h3 class="text-xl font-bold text-gray-800 mb-4">{{ t('ldap.group_mappings') }}</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ldap.group') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ldap.role') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ldap.description') }}</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for mapping in mappings %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ mapping.ldap_group }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs font-semibold rounded-full
{% if mapping.role_name == 'admin' %}bg-red-100 text-red-800
{% elseif mapping.role_name == 'manager' %}bg-blue-100 text-blue-800
{% else %}bg-gray-100 text-gray-800{% endif %}">
{{ mapping.role_name }}
</span>
</td>
<td class="px-6 py-4 text-sm text-gray-500">{{ mapping.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Protocols Tab -->
<div id="content-protocols" class="tab-content hidden">
{% include 'settings/protocols_management.twig' %}
</div>
{% endif %}
</div>
@@ -341,8 +536,33 @@ function translateLanguage(lang) {
.catch(err => {
alert('{{ t('message.error') }}: ' + err.message);
button.disabled = false;
button.innerHTML = originalText;
button.innerHTML = originalText;
});
}
// LDAP test button inside settings page
document.addEventListener('click', function(e) {
if (e.target && e.target.id === 'testConnection') {
const btn = e.target;
btn.disabled = true;
btn.textContent = '{{ t('ldap.testing') }}...';
fetch('/settings/ldap/test', { method: 'POST' })
.then(r => r.json())
.then(result => {
if (result.success) {
alert('✓ ' + result.message);
} else {
alert('✗ ' + result.message);
}
})
.catch(err => {
alert('{{ t('ldap.connection_test_failed') }}: ' + err.message);
})
.finally(() => {
btn.disabled = false;
btn.textContent = '{{ t('ldap.test_connection') }}';
});
}
});
</script>
{% endblock %}
+343
View File
@@ -0,0 +1,343 @@
{% extends "layout.twig" %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900">{{ 'Логи приложения' | trans }}</h1>
<p class="mt-2 text-sm text-gray-600">{{ 'Просмотр, поиск и управление файлами логов' | trans }}</p>
</div>
{% if log_files | length > 0 %}
<div class="flex space-x-2">
<button class="px-4 py-2 bg-yellow-500 text-white rounded-md hover:bg-yellow-600" id="btnClearAll" title="{{ 'Удалить все логи' | trans }}">
<i class="fas fa-trash mr-2"></i>{{ 'Очистить все' | trans }}
</button>
<a href="/admin/logs/download?file={{ selected_file }}"
class="px-4 py-2 rounded-md border {{ selected_file ? 'bg-white text-gray-700 hover:bg-gray-50' : 'bg-gray-100 text-gray-400 cursor-not-allowed' }}">
<i class="fas fa-download mr-2"></i>{{ 'Скачать' | trans }}
</a>
</div>
{% endif %}
</div>
{% if user and user.role == 'admin' %}
<div class="mb-6 border-b border-gray-200">
<nav class="-mb-px flex space-x-8">
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-user mr-2"></i>{{ t('settings.profile') }}
</a>
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-key mr-2"></i>{{ t('settings.api_keys') }}
</a>
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-language mr-2"></i>{{ t('settings.translations') }}
</a>
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-users mr-2"></i>{{ t('settings.users') }}
</a>
<a href="/settings/ldap" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-network-wired mr-2"></i>LDAP
</a>
<a href="/settings/protocols" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-cubes mr-2"></i>Protocols
</a>
<a href="/admin/logs" class="tab-link border-purple-500 text-purple-600 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-file-alt mr-2"></i>{{ 'Логи' | trans }}
</a>
</nav>
</div>
{% endif %}
<div class="grid grid-cols-12 gap-6">
<!-- Sidebar with file list -->
<div class="col-span-12 md:col-span-3">
<div class="bg-white shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">{{ 'Файлы логов' | trans }}</h2>
</div>
<div class="max-h-[600px] overflow-y-auto">
{% if log_files | length > 0 %}
{% for file in log_files %}
<a href="/admin/logs?file={{ file.path }}" class="block px-6 py-4 border-b border-gray-100 hover:bg-gray-50 {{ selected_file == file.path ? 'bg-purple-50' : '' }}">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="text-sm font-medium" style="word-break: break-all;">{{ file.name }}</div>
<div class="text-xs text-gray-500">{{ file.size_formatted }}</div>
</div>
<button type="button" class="text-red-600 hover:text-red-800 text-sm delete-log" data-file="{{ file.path }}" title="{{ 'Удалить' | trans }}">
<i class="fas fa-trash-alt"></i>
</button>
</div>
<div class="text-xs text-gray-500 mt-2">{{ file.modified_formatted }}</div>
</a>
{% endfor %}
{% else %}
<div class="px-6 py-10 text-center text-gray-500">
{{ 'Логи не найдены' | trans }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Main content area -->
<div class="col-span-12 md:col-span-9">
{% if selected_file %}
<!-- File info -->
<div class="bg-white shadow rounded-lg mb-4">
<div class="px-6 py-4 border-b border-gray-200">
<div class="md:flex md:items-center md:justify-between">
<div class="mb-2 md:mb-0">
<h3 class="text-sm text-gray-700">{{ 'Файл:' | trans }} <code class="text-gray-900">{{ selected_file }}</code></h3>
</div>
<div class="text-xs text-gray-500">
<strong>{{ 'Размер:' | trans }}</strong> {{ file_size | default(0) | bytes_format }}
<span class="mx-2">•</span>
<strong>{{ 'Строк:' | trans }}</strong> {{ line_count | number_format(0, '.', ' ') }}
</div>
</div>
</div>
</div>
<!-- Search form -->
<div class="bg-white shadow rounded-lg mb-4">
<div class="px-6 py-4">
<form id="searchForm" class="md:flex md:items-center md:space-x-3">
<input type="hidden" name="file" value="{{ selected_file }}">
<div class="flex-1 mb-3 md:mb-0">
<input type="text" id="searchQuery" name="query" required
placeholder="{{ 'Поиск в логе...' | trans }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
</div>
<label class="inline-flex items-center mb-3 md:mb-0">
<input type="checkbox" id="caseSensitive" name="case_sensitive" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500">
<span class="ml-2 text-sm text-gray-600">{{ 'Учитывать регистр' | trans }}</span>
</label>
<button type="submit" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
<i class="fas fa-search mr-2"></i>{{ 'Найти' | trans }}
</button>
<button type="button" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200" id="statsBtn">
<i class="fas fa-chart-bar mr-2"></i>{{ 'Статистика' | trans }}
</button>
</form>
</div>
</div>
<!-- Search results -->
<div id="searchResults" class="hidden bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded mb-4">
<strong>{{ 'Результаты поиска:' | trans }}</strong>
<div id="resultsContent" class="mt-2 text-sm"></div>
</div>
<!-- Statistics -->
<div id="statsPanel" class="hidden bg-white shadow rounded-lg mb-4">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-sm font-medium text-gray-900">{{ 'Статистика логов' | trans }}</h3>
</div>
<div class="px-6 py-4">
<div class="grid grid-cols-4 gap-4">
<div class="text-center p-3 border-r">
<div class="text-2xl text-purple-600" id="totalLines">0</div>
<div class="text-xs text-gray-500">{{ 'Всего строк' | trans }}</div>
</div>
<div class="text-center p-3 border-r">
<div class="text-2xl text-red-600" id="errorCount">0</div>
<div class="text-xs text-gray-500">{{ 'Ошибок' | trans }}</div>
</div>
<div class="text-center p-3 border-r">
<div class="text-2xl text-yellow-600" id="warningCount">0</div>
<div class="text-xs text-gray-500">{{ 'Предупреждений' | trans }}</div>
</div>
<div class="text-center p-3">
<div class="text-2xl text-green-600" id="successCount">0</div>
<div class="text-xs text-gray-500">{{ 'Успехов' | trans }}</div>
</div>
</div>
<div class="text-xs text-gray-500 mt-2" id="lastModified"></div>
</div>
</div>
<!-- Log content -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-3 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-900">{{ 'Содержание логов' | trans }}</h3>
<button type="button" class="px-2 py-1 text-sm border rounded hover:bg-gray-50" id="toggleLineNumbers">
<i class="fas fa-list-ol"></i>
</button>
</div>
<div class="px-6 py-4">
<pre class="mb-0 max-h-[600px] overflow-auto"><code id="logContent" class="show-line-numbers">{{ log_content }}</code></pre>
</div>
</div>
{% else %}
<div class="bg-blue-50 border border-blue-200 text-blue-800 px-6 py-10 rounded text-center">
<i class="fas fa-info-circle text-2xl mb-3"></i>
<div class="text-lg">{{ 'Выберите файл логов для просмотра' | trans }}</div>
</div>
{% endif %}
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const selectedFile = '{{ selected_file }}';
// Delete log file
document.querySelectorAll('.delete-log').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const file = this.dataset.file;
if (confirm('{{ "Удалить этот файл логов?" | trans }}')) {
fetch('/admin/logs/delete', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'file=' + encodeURIComponent(file)
})
.then(r => r.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect;
} else {
alert('{{ "Ошибка:" | trans }} ' + data.message);
}
});
}
});
});
// Clear all logs
const clearAllBtn = document.getElementById('btnClearAll');
if (clearAllBtn) {
clearAllBtn.addEventListener('click', function() {
if (confirm('{{ "Удалить ВСЕ файлы логов? Это действие необратимо." | trans }}')) {
fetch('/admin/logs/clear-all', {method: 'POST'})
.then(r => r.json())
.then(data => {
alert(data.message);
window.location.href = data.redirect;
});
}
});
}
// Search logs
document.getElementById('searchForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('/admin/logs/search', {
method: 'POST',
body: new URLSearchParams(formData)
})
.then(r => r.json())
.then(data => {
if (data.success) {
showSearchResults(data);
} else {
alert('{{ "Ошибка:" | trans }} ' + data.message);
}
});
});
// Statistics
document.getElementById('statsBtn').addEventListener('click', function() {
if (!selectedFile) return;
fetch('/admin/logs/stats', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'file=' + encodeURIComponent(selectedFile)
})
.then(r => r.json())
.then(data => {
if (data.success) {
showStatistics(data);
}
});
});
// Toggle line numbers
document.getElementById('toggleLineNumbers').addEventListener('click', function() {
const content = document.getElementById('logContent');
content.classList.toggle('show-line-numbers');
});
function showSearchResults(data) {
const resultsDiv = document.getElementById('searchResults');
const resultsContent = document.getElementById('resultsContent');
if (data.results_count === 0) {
resultsContent.innerHTML = '<p class="mb-0">{{ "Результатов не найдено" | trans }}</p>';
} else {
let html = '<p class="mb-2">{{ "Найдено совпадений:" | trans }} <strong>' + data.results_count + '</strong></p>';
html += '<div class="results-list" style="max-height: 300px; overflow-y: auto;">';
data.results.forEach((result, idx) => {
html += '<div class="border-bottom pb-2 mb-2">';
html += '<small class="text-muted">{{ "Строка" | trans }} ' + result.line + ':</small><br>';
html += '<code>' + escapeHtml(result.content.substring(0, 200)) + (result.content.length > 200 ? '...' : '') + '</code>';
html += '</div>';
});
html += '</div>';
resultsContent.innerHTML = html;
}
resultsDiv.style.display = 'block';
}
function showStatistics(data) {
document.getElementById('totalLines').textContent = data.total_lines;
document.getElementById('errorCount').textContent = data.errors;
document.getElementById('warningCount').textContent = data.warnings;
document.getElementById('successCount').textContent = data.success;
document.getElementById('lastModified').textContent = '{{ "Последнее обновление:" | trans }} ' + data.last_modified;
document.getElementById('statsPanel').style.display = 'block';
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
});
</script>
<style>
#logContent {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.4;
}
#logContent.show-line-numbers {
counter-reset: line;
}
#logContent.show-line-numbers::before {
content: '';
}
.results-list {
border: 1px solid #ddd;
border-radius: 3px;
padding: 10px;
background: #fafafa;
}
.list-group-item.active {
background-color: #007bff;
border-color: #007bff;
}
</style>
{% endblock %}
+707
View File
@@ -0,0 +1,707 @@
{% extends "layout.twig" %}
{% block title %}{{ editing ? t('protocols.edit_protocol') : t('protocols.create_protocol') }} - {{ parent() }}{% endblock %}
{% block content %}
<div class="min-h-screen bg-gray-50">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-900">{{ editing ? t('protocols.edit_protocol') : t('protocols.create_protocol') }}</h1>
<p class="mt-2 text-gray-600">{{ editing ? t('protocols.edit_protocol_description') : t('protocols.create_protocol_description') }}</p>
</div>
<a href="/settings/protocols" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
{{ t('protocols.back_to_protocols') }}
</a>
</div>
</div>
<!-- Success/Error Messages -->
{% if success %}
<div class="mb-4 bg-green-50 border border-green-200 rounded-md p-4">
<div class="flex">
<svg class="w-5 h-5 text-green-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<p class="text-green-800">{{ success }}</p>
</div>
</div>
{% endif %}
{% if error %}
<div class="mb-4 bg-red-50 border border-red-200 rounded-md p-4">
<div class="flex">
<svg class="w-5 h-5 text-red-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
<p class="text-red-800">{{ error }}</p>
</div>
</div>
{% endif %}
<!-- Protocol Form -->
<form id="protocol-form" method="POST" action="/settings/protocols/save" class="space-y-6">
{% if editing %}
<input type="hidden" name="id" value="{{ editing.id }}">
{% endif %}
<!-- Basic Information -->
<div class="bg-white shadow rounded-lg p-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">{{ t('protocols.basic_information') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.name_label') }} *</label>
<input type="text" id="name" name="name" value="{{ editing.name ?? '' }}" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.name_help') }}</p>
</div>
<div>
<label for="slug" class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.slug_label') }} *</label>
<input type="text" id="slug" name="slug" value="{{ editing.slug ?? '' }}" required pattern="[a-z0-9_-]+" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.slug_help') }}</p>
</div>
</div>
<div class="mt-6">
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">{{ t('common.description') }}</label>
<textarea id="description" name="description" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">{{ editing.description ?? '' }}</textarea>
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.description_help') }}</p>
</div>
</div>
<!-- Installation Script -->
<div class="bg-white shadow rounded-lg p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.installation_script') }}</h2>
<button type="button" id="ai-help-btn" class="inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
{{ t('ai.get_ai_help') }}
</button>
</div>
<div>
<textarea id="install_script" name="install_script" rows="15" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="#!/bin/bash&#10;# Installation script here">{{ editing.install_script ?? '' }}</textarea>
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.install_script_help') }}</p>
<div class="mt-3 flex items-center space-x-2">
<button id="test-install-btn" type="button" class="inline-flex items-center px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4"/></svg>
{{ t('protocols.test_install') }}
</button>
<span class="text-xs text-gray-500">{{ t('protocols.testing_on_ubuntu22') }}</span>
</div>
<div id="test-install-result" class="mt-3 hidden">
<h3 class="text-sm font-medium text-gray-900">{{ t('protocols.test_result') }}</h3>
<pre id="test-install-output" class="mt-2 p-3 bg-gray-50 border border-gray-200 rounded text-xs whitespace-pre-wrap"></pre>
<h3 class="mt-3 text-sm font-medium text-gray-900">{{ t('protocols.client_output_preview') }}</h3>
<pre id="test-client-preview" class="mt-2 p-3 bg-gray-50 border border-gray-200 rounded text-xs whitespace-pre-wrap"></pre>
</div>
</div>
</div>
<!-- Uninstallation Script -->
<div class="bg-white shadow rounded-lg p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.uninstallation_script') }}</h2>
<button type="button" class="ai-help-btn inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" data-target="uninstall">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
{{ t('ai.get_ai_help') }}
</button>
</div>
<div>
<textarea id="uninstall_script" name="uninstall_script" rows="12" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="#!/bin/bash&#10;# Uninstallation script here">{{ editing.uninstall_script ?? '' }}</textarea>
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.uninstall_script_help') }}</p>
<div class="mt-3 flex items-center space-x-2">
<button id="test-uninstall-btn" type="button" class="inline-flex items-center px-3 py-2 bg-red-600 text-white rounded hover:bg-red-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
{{ t('protocols.test_uninstall') }}
</button>
<span class="text-xs text-gray-500">{{ t('protocols.testing_on_ubuntu22') }}</span>
</div>
<div id="test-uninstall-result" class="mt-3 hidden">
<h3 class="text-sm font-medium text-gray-900">{{ t('protocols.test_result') }}</h3>
<pre id="test-uninstall-output" class="mt-2 p-3 bg-gray-50 border border-gray-200 rounded text-xs whitespace-pre-wrap"></pre>
</div>
</div>
</div>
<!-- Output Template -->
<div class="bg-white shadow rounded-lg p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.output_template') }}</h2>
<button type="button" class="ai-help-btn inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" data-target="template">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
{{ t('ai.get_ai_help') }}
</button>
</div>
<div>
<textarea id="output_template" name="output_template" rows="10" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="[Interface]&#10;PrivateKey = {{private_key}}&#10;Address = {{client_ip}}/32">{{ editing.output_template ?? '' }}</textarea>
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.output_template_help') }}</p>
</div>
<div class="mt-4 p-4 bg-blue-50 rounded-md">
<h3 class="text-sm font-medium text-blue-900 mb-2">{{ t('protocols.available_variables') }}</h3>
<div class="text-sm text-blue-800 space-y-1">
<p><code>{{private_key}}</code> - {{ t('protocols.variable_private_key_help') }}</p>
<p><code>{{public_key}}</code> - {{ t('protocols.variable_public_key_help') }}</p>
<p><code>{{client_ip}}</code> - {{ t('protocols.variable_client_ip_help') }}</p>
<p><code>{{server_host}}</code> - {{ t('protocols.variable_server_host_help') }}</p>
<p><code>{{server_port}}</code> - {{ t('protocols.variable_server_port_help') }}</p>
<p><code>{{preshared_key}}</code> - {{ t('protocols.variable_preshared_key_help') }}</p>
</div>
</div>
</div>
<!-- QR Code Template -->
<div class="bg-white shadow rounded-lg p-6">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center">
<input type="checkbox" id="qr_section_toggle" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-3" checked>
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.qr_code_template') }}</h2>
</div>
<button type="button" class="ai-help-btn inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" data-target="qr_template">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
{{ t('ai.get_ai_help') }}
</button>
</div>
<div id="qr_section_content">
<div class="mb-4">
<label for="qr_code_format" class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.qr_code_format') }}</label>
<select id="qr_code_format" name="qr_code_format" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="amnezia_compressed" {% if editing.qr_code_format == 'amnezia_compressed' %}selected{% endif %}>Amnezia Compressed (Default)</option>
<option value="raw" {% if editing.qr_code_format == 'raw' %}selected{% endif %}>Raw Content</option>
<option value="text" {% if editing.qr_code_format == 'text' %}selected{% endif %}>{{ t('protocols.qr_code_format_text') }}</option>
</select>
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.qr_code_format_help') }}</p>
</div>
<div>
<textarea id="qr_code_template" name="qr_code_template" rows="10" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="{&quot;last_config&quot;:{{last_config_json}}}">{{ editing.qr_code_template ?? '' }}</textarea>
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.qr_code_template_help') }}</p>
</div>
<div class="mt-4 p-4 bg-blue-50 rounded-md">
<h3 class="text-sm font-medium text-blue-900 mb-2">{{ t('protocols.available_variables') }}</h3>
<div class="text-sm text-blue-800 space-y-1">
<p><code>{{last_config_json}}</code> - {{ t('protocols.variable_last_config_json_help') }}</p>
<p>{{ t('protocols.plus_all_output_variables') }}</p>
</div>
</div>
</div>
</div>
<!-- Password Generation -->
<div class="bg-white shadow rounded-lg p-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">{{ t('protocols.password_generation') }}</h2>
<div>
<textarea id="password_command" name="password_command" rows="6" class="w-full px-3 py-2 border rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="echo \$(openssl rand -base64 12)">{{ editing.password_command ?? '' }}</textarea>
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.password_command_help') }}</p>
</div>
</div>
<!-- Settings -->
<div class="bg-white shadow rounded-lg p-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">{{ t('common.settings') }}</h2>
<div class="space-y-4">
<div class="flex items-center">
<input type="checkbox" id="ubuntu_compatible" name="ubuntu_compatible" value="1" {% if editing.ubuntu_compatible %}checked{% endif %} class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="ubuntu_compatible" class="ml-2 block text-sm text-gray-900">{{ t('protocols.ubuntu_compatible') }}</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="show_text_content" name="show_text_content" value="1" {% if editing.show_text_content %}checked{% endif %} class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="show_text_content" class="ml-2 block text-sm text-gray-900">{{ t('protocols.show_text_content') }}</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="is_active" name="is_active" value="1" {% if editing.is_active is not defined or editing.is_active %}checked{% endif %} class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="is_active" class="ml-2 block text-sm text-gray-900">{{ t('protocols.active_label') }}</label>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="flex justify-end space-x-3">
<a href="/settings/protocols" class="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{{ t('common.cancel') }}
</a>
<button type="submit" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{{ editing ? t('protocols.update_protocol') : t('protocols.create_protocol') }}
</button>
</div>
</form>
</div>
</div>
<!-- AI Assistant Modal -->
<div id="ai-assistant-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900">{{ t('ai.assistant') }}</h3>
<button id="close-ai-modal" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.select_model') }}</label>
<select id="ai-model-select" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="openai/gpt-3.5-turbo">{{ t('ai.model_gpt35_turbo') }}</option>
<option value="openai/gpt-4">{{ t('ai.model_gpt4') }}</option>
<option value="anthropic/claude-3-haiku">{{ t('ai.model_claude3_haiku') }}</option>
<option value="anthropic/claude-3-sonnet">{{ t('ai.model_claude3_sonnet') }}</option>
</select>
<div class="mt-2 flex items-center space-x-2">
<input id="ai-model-custom" type="text" placeholder="{{ t('ai.custom_model_placeholder') }}" class="flex-1 px-3 py-2 border border-gray-300 rounded-md">
<button id="ai-model-test-btn" type="button" class="px-3 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50">{{ t('ai.check_availability') }}</button>
</div>
<p class="mt-2 text-xs text-gray-500">
<i class="fas fa-external-link-alt"></i>
<a href="https://openrouter.ai/models" target="_blank" class="text-purple-600">openrouter.ai/models</a>
</p>
{% if not openrouter_key %}
<div class="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded">
<div class="flex items-center justify-between">
<span class="text-xs text-yellow-800">{{ t('settings.no_api_key') }}</span>
<a href="/settings#api" class="px-2 py-1 text-xs bg-purple-600 text-white rounded">{{ t('settings.enter_api_key') }}</a>
</div>
</div>
{% endif %}
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.protocol_type') }}</label>
<select id="ai-protocol-type" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">{{ t('ai.general_vpn') }}</option>
<option value="wireguard">WireGuard</option>
<option value="openvpn">OpenVPN</option>
<option value="shadowsocks">Shadowsocks</option>
<option value="cloak">Cloak</option>
<option value="ikev2">IKEv2</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.describe_requirements') }}</label>
<textarea id="ai-prompt" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="{{ t('ai.prompt_placeholder') }}"></textarea>
</div>
<div class="mb-4">
<button id="generate-script-btn" class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
{{ t('ai.generate_script') }}
</button>
</div>
<div id="ai-loading" class="hidden text-center py-4">
<div class="inline-flex items-center">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-purple-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>{{ t('ai.generating_script') }}</span>
</div>
</div>
<div id="ai-result" class="hidden">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.generated_script') }}</label>
<div class="bg-gray-900 text-green-400 p-4 rounded-md overflow-x-auto">
<pre id="generated-script" class="text-sm whitespace-pre-wrap"></pre>
</div>
</div>
<div id="ai-suggestions" class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.suggestions') }}</label>
<ul id="suggestions-list" class="list-disc list-inside space-y-1 text-sm text-gray-600"></ul>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('common.compatibility') }}</label>
<div id="ubuntu-compatibility" class="flex items-center"></div>
</div>
<div class="flex space-x-3">
<button id="apply-to-current-btn" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
{{ t('ai.apply_to_current_protocol') }}
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// AI Assistant Modal
const aiModal = document.getElementById('ai-assistant-modal');
const aiHelpBtns = document.querySelectorAll('.ai-help-btn');
const closeAIModal = document.getElementById('close-ai-modal');
const generateScriptBtn = document.getElementById('generate-script-btn');
const aiLoading = document.getElementById('ai-loading');
const aiResult = document.getElementById('ai-result');
const applyToCurrentBtn = document.getElementById('apply-to-current-btn');
const installScriptTextarea = document.getElementById('install_script');
const uninstallScriptTextarea = document.getElementById('uninstall_script');
const outputTemplateTextarea = document.getElementById('output_template');
// QR Template Section Toggle
const qrSectionToggle = document.getElementById('qr_section_toggle');
const qrSectionContent = document.getElementById('qr_section_content');
if (qrSectionToggle && qrSectionContent) {
qrSectionToggle.addEventListener('change', function() {
qrSectionContent.style.display = this.checked ? 'block' : 'none';
});
}
let currentAiTarget = 'install'; // install, uninstall, template
function showAIModal(target) {
currentAiTarget = target || 'install';
aiModal.classList.remove('hidden');
aiResult.classList.add('hidden');
aiLoading.classList.add('hidden');
// Update modal title or prompt placeholder based on target if needed
const promptArea = document.getElementById('ai-prompt');
if (currentAiTarget === 'template') {
promptArea.placeholder = "{{ t('ai.prompt_placeholder_template') }}";
} else if (currentAiTarget === 'qr_template') {
promptArea.placeholder = "{{ t('ai.prompt_placeholder_qr_template') }}";
} else if (currentAiTarget === 'uninstall') {
promptArea.placeholder = "{{ t('ai.prompt_placeholder_uninstall') }}";
} else {
promptArea.placeholder = "{{ t('ai.prompt_placeholder') }}";
}
}
function hideAIModal() {
aiModal.classList.add('hidden');
}
// Attach event listeners to all AI help buttons
aiHelpBtns.forEach(btn => {
btn.addEventListener('click', function() {
const target = this.getAttribute('data-target') || 'install';
showAIModal(target);
});
});
// Also attach to the original ID if it exists (for backward compatibility or if I missed updating one)
const originalAiBtn = document.getElementById('ai-help-btn');
if (originalAiBtn) {
originalAiBtn.addEventListener('click', function() {
showAIModal('install');
});
}
closeAIModal.addEventListener('click', hideAIModal);
// Close modal when clicking outside
aiModal.addEventListener('click', function(e) {
if (e.target === aiModal) {
hideAIModal();
}
});
// Generate script with AI
generateScriptBtn.addEventListener('click', async function() {
const model = document.getElementById('ai-model-select').value;
const customModel = document.getElementById('ai-model-custom').value.trim();
const effectiveModel = customModel !== '' ? customModel : model;
const protocolType = document.getElementById('ai-protocol-type').value;
const prompt = document.getElementById('ai-prompt').value;
if (!prompt.trim()) {
alert('{{ t('ai.please_enter_requirements') }}');
return;
}
aiLoading.classList.remove('hidden');
generateScriptBtn.disabled = true;
try {
const response = await fetch('/api/ai/assist', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: prompt,
model: effectiveModel,
protocol_type: protocolType,
target: currentAiTarget
})
});
const result = await response.json();
if (result.success) {
displayAIResult(result.data);
} else {
alert('{{ t('ai.error_generating_script') }}: ' + result.error);
}
} catch (error) {
alert('{{ t('ai.error_generating_script') }}: ' + error.message);
} finally {
aiLoading.classList.add('hidden');
generateScriptBtn.disabled = false;
}
});
function displayAIResult(data) {
document.getElementById('generated-script').textContent = data.script;
const suggestionsList = document.getElementById('suggestions-list');
suggestionsList.innerHTML = '';
data.suggestions.forEach(suggestion => {
const li = document.createElement('li');
li.textContent = suggestion;
suggestionsList.appendChild(li);
});
const compatibilityDiv = document.getElementById('ubuntu-compatibility');
if (data.ubuntu_compatible) {
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg><span class="text-green-700">Compatible with Ubuntu 22.04-24.04</span>';
} else {
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg><span class="text-red-700">May not be compatible with Ubuntu 22.04-24.04</span>';
}
aiResult.classList.remove('hidden');
}
// Apply to current protocol
applyToCurrentBtn.addEventListener('click', function() {
const generatedScript = document.getElementById('generated-script').textContent;
if (generatedScript && confirm('{{ t('ai.confirm_apply_script') }}')) {
if (currentAiTarget === 'uninstall') {
uninstallScriptTextarea.value = generatedScript;
} else if (currentAiTarget === 'template') {
outputTemplateTextarea.value = generatedScript;
} else if (currentAiTarget === 'qr_template') {
document.getElementById('qr_code_template').value = generatedScript;
} else {
installScriptTextarea.value = generatedScript;
}
hideAIModal();
}
});
// Form validation
document.getElementById('protocol-form').addEventListener('submit', function(e) {
const name = document.getElementById('name').value.trim();
const slug = document.getElementById('slug').value.trim();
if (!name || !slug) {
e.preventDefault();
alert('{{ t('protocols.please_fill_required_fields') }}');
return;
}
if (!/^[a-z0-9_-]+$/i.test(slug)) {
e.preventDefault();
alert('{{ t('protocols.invalid_slug_format') }}');
return;
}
});
// Auto-generate slug from name
document.getElementById('name').addEventListener('blur', function() {
const name = this.value.trim();
const slugField = document.getElementById('slug');
if (name && !slugField.value.trim()) {
slugField.value = name.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
});
// Test Install Script
const testBtn = document.getElementById('test-install-btn');
const testBox = document.getElementById('test-install-result');
const testOut = document.getElementById('test-install-output');
const clientPrev = document.getElementById('test-client-preview');
if (testBtn) {
testBtn.addEventListener('click', function() {
const protocolId = {{ editing ? editing.id : 'null' }};
if (!protocolId) {
return;
}
testBtn.disabled = true;
testBtn.classList.add('opacity-50');
testOut.textContent = '';
clientPrev.textContent = '';
testBox.classList.remove('hidden');
const appendCmd = (cmd) => {
const line = document.createElement('div');
line.className = 'text-xs text-gray-800';
line.innerHTML = `<span class="text-blue-600">$</span> <code>${cmd}</code>`;
testOut.appendChild(line);
};
const appendOut = (text) => {
const pre = document.createElement('pre');
pre.className = 'mt-1 p-2 bg-gray-100 border border-gray-200 rounded text-xs whitespace-pre-wrap';
pre.textContent = text;
testOut.appendChild(pre);
};
const setError = (msg) => {
const err = document.createElement('div');
err.className = 'mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700';
err.textContent = msg;
testOut.appendChild(err);
};
let es;
try {
es = new EventSource(`/api/protocols/${protocolId}/test-install/stream`);
} catch (e) {
es = null;
}
if (es) {
es.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
if (data.type === 'start') {
appendOut('{{ t('protocols.testing_on_ubuntu22') }}');
} else if (data.type === 'cmd') {
appendCmd(data.cmd);
} else if (data.type === 'out') {
appendOut(data.line);
} else if (data.type === 'cmd_done') {
if (data.rc !== 0) {
setError('Command failed');
}
} else if (data.type === 'preview') {
clientPrev.textContent = data.preview || '';
} else if (data.type === 'done') {
es.close();
testBtn.disabled = false;
testBtn.classList.remove('opacity-50');
} else if (data.type === 'error') {
setError(data.error || 'Unknown error');
es.close();
testBtn.disabled = false;
testBtn.classList.remove('opacity-50');
}
} catch (_) {}
};
es.onerror = () => {
es.close();
testBtn.disabled = false;
testBtn.classList.remove('opacity-50');
setError('Connection failed');
};
} else {
// Fallback to non-stream if needed, but we implemented stream
testBtn.disabled = false;
testBtn.classList.remove('opacity-50');
}
});
}
// Test Uninstall Script
const testUninstallBtn = document.getElementById('test-uninstall-btn');
const testUninstallBox = document.getElementById('test-uninstall-result');
const testUninstallOut = document.getElementById('test-uninstall-output');
if (testUninstallBtn) {
testUninstallBtn.addEventListener('click', function() {
const protocolId = {{ editing ? editing.id : 'null' }};
if (!protocolId) {
return;
}
testUninstallBtn.disabled = true;
testUninstallBtn.classList.add('opacity-50');
testUninstallOut.textContent = '';
testUninstallBox.classList.remove('hidden');
const appendCmd = (cmd) => {
const line = document.createElement('div');
line.className = 'text-xs text-gray-800';
line.innerHTML = `<span class="text-blue-600">$</span> <code>${cmd}</code>`;
testUninstallOut.appendChild(line);
};
const appendOut = (text) => {
const pre = document.createElement('pre');
pre.className = 'mt-1 p-2 bg-gray-100 border border-gray-200 rounded text-xs whitespace-pre-wrap';
pre.textContent = text;
testUninstallOut.appendChild(pre);
};
const setError = (msg) => {
const err = document.createElement('div');
err.className = 'mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700';
err.textContent = msg;
testUninstallOut.appendChild(err);
};
let es;
try {
es = new EventSource(`/api/protocols/${protocolId}/test-uninstall/stream`);
} catch (e) {
es = null;
}
if (es) {
es.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
if (data.type === 'start') {
appendOut('{{ t('protocols.testing_on_ubuntu22') }}');
} else if (data.type === 'cmd') {
appendCmd(data.cmd);
} else if (data.type === 'out') {
appendOut(data.line);
} else if (data.type === 'cmd_done') {
if (data.rc !== 0) {
setError('Command failed');
}
} else if (data.type === 'done') {
es.close();
testUninstallBtn.disabled = false;
testUninstallBtn.classList.remove('opacity-50');
} else if (data.type === 'error') {
setError(data.error || 'Unknown error');
es.close();
testUninstallBtn.disabled = false;
testUninstallBtn.classList.remove('opacity-50');
}
} catch (_) {}
};
es.onerror = () => {
es.close();
testUninstallBtn.disabled = false;
testUninstallBtn.classList.remove('opacity-50');
setError('Connection failed');
};
}
});
}
});
</script>
{% endblock %}
@@ -0,0 +1,272 @@
{% extends "layout.twig" %}
{% block title %}{{ t('protocols.template_editor') }} - {{ parent() }}{% endblock %}
{% block content %}
<div class="min-h-screen bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-900">{{ t('protocols.template_editor') }}</h1>
<p class="mt-2 text-gray-600">{{ t('protocols.template_editor_description') }}</p>
</div>
<a href="/settings/protocols" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
{{ t('protocols.back_to_protocols') }}
</a>
</div>
</div>
<!-- Template Editor -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">{{ protocol.name }} - {{ t('protocols.output_template') }}</h2>
<p class="mt-1 text-sm text-gray-600">{{ t('protocols.template_editor_help') }}</p>
</div>
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Template Editor -->
<div>
<div class="flex justify-between items-center mb-3">
<label class="block text-sm font-medium text-gray-700">{{ t('protocols.template_content') }}</label>
<div class="flex space-x-2">
<button id="format-template" class="px-3 py-1 text-xs bg-gray-100 text-gray-700 rounded hover:bg-gray-200">{{ t('common.format') }}</button>
<button id="clear-template" class="px-3 py-1 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200">{{ t('common.clear') }}</button>
</div>
</div>
<textarea id="template-editor" rows="20" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">{{ protocol.output_template }}</textarea>
<div class="mt-3 flex space-x-2">
<button id="save-template" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/>
</svg>
{{ t('protocols.save_template') }}
</button>
<button id="preview-template" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
{{ t('common.preview') }}
</button>
</div>
</div>
<!-- Preview Panel -->
<div>
<div class="flex justify-between items-center mb-3">
<label class="block text-sm font-medium text-gray-700">{{ t('common.preview') }}</label>
<button id="refresh-preview" class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded hover:bg-green-200">{{ t('common.refresh') }}</button>
</div>
<div class="bg-gray-900 text-green-400 p-4 rounded-md h-96 overflow-auto">
<pre id="template-preview" class="text-sm whitespace-pre-wrap">{{ t('protocols.click_preview_to_see_output') }}</pre>
</div>
<div class="mt-3">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.test_variables') }}</label>
<div class="space-y-2">
<input type="text" id="test-private-key" placeholder="{{ t('protocols.private_key') }}" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" value="test_private_key_example_1234567890abcdef">
<input type="text" id="test-client-ip" placeholder="{{ t('protocols.client_ip') }}" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" value="10.8.1.2">
<input type="text" id="test-server-host" placeholder="{{ t('protocols.server_host') }}" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" value="vpn.example.com">
<input type="text" id="test-server-port" placeholder="{{ t('protocols.server_port') }}" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" value="51820">
<input type="text" id="test-preshared-key" placeholder="{{ t('protocols.preshared_key') }}" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" value="test_preshared_key_example">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Template Variables -->
<div class="mt-6 bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.template_variables') }}</h2>
<p class="mt-1 text-sm text-gray-600">{{ t('protocols.template_variables_help') }}</p>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="variable-card p-3 border border-gray-200 rounded-md">
<div class="flex justify-between items-center">
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{private_key}}</code>
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{private_key}}">{{ t('common.copy') }}</button>
</div>
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_private_key_desc') }}</p>
</div>
<div class="variable-card p-3 border border-gray-200 rounded-md">
<div class="flex justify-between items-center">
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{public_key}}</code>
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{public_key}}">{{ t('common.copy') }}</button>
</div>
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_public_key_desc') }}</p>
</div>
<div class="variable-card p-3 border border-gray-200 rounded-md">
<div class="flex justify-between items-center">
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{client_ip}}</code>
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{client_ip}}">{{ t('common.copy') }}</button>
</div>
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_client_ip_desc') }}</p>
</div>
<div class="variable-card p-3 border border-gray-200 rounded-md">
<div class="flex justify-between items-center">
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{server_host}}</code>
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{server_host}}">{{ t('common.copy') }}</button>
</div>
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_server_host_desc') }}</p>
</div>
<div class="variable-card p-3 border border-gray-200 rounded-md">
<div class="flex justify-between items-center">
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{server_port}}</code>
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{server_port}}">{{ t('common.copy') }}</button>
</div>
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_server_port_desc') }}</p>
</div>
<div class="variable-card p-3 border border-gray-200 rounded-md">
<div class="flex justify-between items-center">
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{preshared_key}}</code>
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{preshared_key}}">{{ t('common.copy') }}</button>
</div>
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_preshared_key_desc') }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const templateEditor = document.getElementById('template-editor');
const templatePreview = document.getElementById('template-preview');
const saveTemplateBtn = document.getElementById('save-template');
const previewBtn = document.getElementById('preview-template');
const refreshBtn = document.getElementById('refresh-preview');
const formatBtn = document.getElementById('format-template');
const clearBtn = document.getElementById('clear-template');
const testPrivateKey = document.getElementById('test-private-key');
const testClientIp = document.getElementById('test-client-ip');
const testServerHost = document.getElementById('test-server-host');
const testServerPort = document.getElementById('test-server-port');
const testPresharedKey = document.getElementById('test-preshared-key');
// Preview template
function previewTemplate() {
let template = templateEditor.value;
// Replace variables with test values
template = template.replace(/\{\{private_key\}\}/g, testPrivateKey.value);
template = template.replace(/\{\{public_key\}\}/g, 'test_public_key_example');
template = template.replace(/\{\{client_ip\}\}/g, testClientIp.value);
template = template.replace(/\{\{server_host\}\}/g, testServerHost.value);
template = template.replace(/\{\{server_port\}\}/g, testServerPort.value);
template = template.replace(/\{\{preshared_key\}\}/g, testPresharedKey.value);
templatePreview.textContent = template;
}
previewBtn.addEventListener('click', previewTemplate);
refreshBtn.addEventListener('click', previewTemplate);
// Auto-preview on input change
[testPrivateKey, testClientIp, testServerHost, testServerPort, testPresharedKey].forEach(input => {
input.addEventListener('input', function() {
if (templatePreview.textContent !== '{{ t('protocols.click_preview_to_see_output') }}') {
previewTemplate();
}
});
});
// Save template
saveTemplateBtn.addEventListener('click', function() {
const protocolId = {{ protocol.id }};
const template = templateEditor.value;
fetch(`/api/protocols/${protocolId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
output_template: template
})
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('{{ t('protocols.template_saved_successfully') }}');
} else {
alert('{{ t('protocols.error_saving_template') }}: ' + result.error);
}
})
.catch(error => {
alert('{{ t('protocols.error_saving_template') }}: ' + error.message);
});
});
// Format template (basic formatting)
formatBtn.addEventListener('click', function() {
let template = templateEditor.value;
// Basic formatting for WireGuard configs
if (template.includes('[Interface]') || template.includes('[Peer]')) {
template = template.replace(/\n\s*/g, '\n');
template = template.replace(/\[/g, '\n[');
template = template.trim();
}
templateEditor.value = template;
alert('{{ t('protocols.template_formatted') }}');
});
// Clear template
clearBtn.addEventListener('click', function() {
if (confirm('{{ t('protocols.confirm_clear_template') }}')) {
templateEditor.value = '';
templatePreview.textContent = '{{ t('protocols.click_preview_to_see_output') }}';
}
});
// Copy variables
document.querySelectorAll('.copy-variable').forEach(btn => {
btn.addEventListener('click', function() {
const variable = this.dataset.variable;
const textarea = document.createElement('textarea');
textarea.value = variable;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
// Show feedback
const originalText = this.textContent;
this.textContent = '{{ t('common.copied') }}';
setTimeout(() => {
this.textContent = originalText;
}, 1000);
});
});
// Auto-save functionality (optional)
let autoSaveTimeout;
templateEditor.addEventListener('input', function() {
clearTimeout(autoSaveTimeout);
autoSaveTimeout = setTimeout(function() {
// Could implement auto-save here
console.log('Template changed, could auto-save...');
}, 2000);
});
});
</script>
{% endblock %}
@@ -0,0 +1,522 @@
<div class="max-w-6xl mx-auto px-1 py-2">
<!-- Header -->
<div class="mb-8">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-900">{{ t('protocols.management') }}</h1>
<p class="mt-2 text-gray-600">{{ t('protocols.management_description') }}</p>
</div>
<div class="flex space-x-3">
<button id="ai-assistant-btn" class="inline-flex items-center px-4 py-2 border border-purple-300 rounded-md shadow-sm text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
{{ t('ai.assistant') }}
</button>
<a href="/settings/protocols/new" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
{{ t('protocols.add_protocol') }}
</a>
</div>
</div>
</div>
<!-- Success/Error Messages -->
{% if success %}
<div class="mb-4 bg-green-50 border border-green-200 rounded-md p-4">
<div class="flex">
<svg class="w-5 h-5 text-green-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<p class="text-green-800">{{ success }}</p>
</div>
</div>
{% endif %}
{% if error %}
<div class="mb-4 bg-red-50 border border-red-200 rounded-md p-4">
<div class="flex">
<svg class="w-5 h-5 text-red-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
<p class="text-red-800">{{ error }}</p>
</div>
</div>
{% endif %}
<!-- Protocols Grid -->
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<div class="px-4 py-5 sm:p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.available_protocols') }}</h2>
<div class="flex space-x-2">
<input type="text" id="protocol-search" placeholder="{{ t('protocols.search_protocols') }}" class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<select id="protocol-filter" class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">{{ t('protocols.all_protocols') }}</option>
<option value="active">{{ t('protocols.active_only') }}</option>
<option value="ubuntu">{{ t('protocols.ubuntu_compatible') }}</option>
<option value="with-ai">{{ t('protocols.with_ai_generations') }}</option>
</select>
</div>
</div>
<div id="protocols-list" class="space-y-4">
{% for protocol in protocols %}
<div class="protocol-card border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow" data-protocol-id="{{ protocol.id }}" data-protocol-name="{{ protocol.name }}" data-protocol-slug="{{ protocol.slug }}" data-active="{{ protocol.is_active }}" data-ubuntu="{{ protocol.ubuntu_compatible }}" data-ai-generations="{{ protocol.ai_generation_count }}">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center space-x-3">
<h3 class="text-lg font-semibold text-gray-900">{{ protocol.name }}</h3>
{% if protocol.is_active %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{{ t('common.active') }}
</span>
{% else %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{{ t('common.inactive') }}
</span>
{% endif %}
{% if protocol.ubuntu_compatible %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
Ubuntu 22-24
</span>
{% endif %}
{% if protocol.ai_generation_count > 0 %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
AI {{ protocol.ai_generation_count }}
</span>
{% endif %}
</div>
<p class="mt-1 text-sm text-gray-600">{{ protocol.description }}</p>
<div class="mt-2 flex items-center space-x-4 text-xs text-gray-500">
<span>{{ t('common.slug') }}: <code class="bg-gray-100 px-1 rounded">{{ protocol.slug }}</code></span>
<span>{{ t('common.servers') }}: {{ protocol.server_count }}</span>
<span>{{ t('common.templates') }}: {{ protocol.template_count }}</span>
<span>{{ t('common.variables') }}: {{ protocol.variable_count }}</span>
</div>
</div>
<div class="flex space-x-2">
<button class="ai-generate-btn text-purple-600 hover:text-purple-900" data-protocol-id="{{ protocol.id }}" title="{{ t('ai.generate_with_ai') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
</button>
<a href="/settings/protocols/{{ protocol.id }}/edit" class="text-blue-600 hover:text-blue-900" title="{{ t('common.edit') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</a>
<a href="/settings/protocols/{{ protocol.id }}/template" class="text-green-600 hover:text-green-900" title="{{ t('protocols.edit_template') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
</a>
{% if protocol.server_count == 0 %}
<button class="delete-protocol-btn text-red-600 hover:text-red-900" data-protocol-id="{{ protocol.id }}" data-protocol-name="{{ protocol.name }}" title="{{ t('common.delete') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="text-center py-8">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">{{ t('protocols.no_protocols') }}</h3>
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.no_protocols_description') }}</p>
<div class="mt-6">
<a href="/settings/protocols/new" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
{{ t('protocols.create_first_protocol') }}
</a>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- AI Assistant Modal -->
<div id="ai-assistant-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900">{{ t('ai.assistant') }}</h3>
<button id="close-ai-modal" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.select_model') }}</label>
<select id="ai-model-select" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="openai/gpt-3.5-turbo">{{ t('ai.model_gpt35_turbo') }}</option>
<option value="openai/gpt-4">{{ t('ai.model_gpt4') }}</option>
<option value="anthropic/claude-3-haiku">{{ t('ai.model_claude3_haiku') }}</option>
<option value="anthropic/claude-3-sonnet">{{ t('ai.model_claude3_sonnet') }}</option>
</select>
<div class="mt-2 flex items-center space-x-2">
<input id="ai-model-custom" type="text" placeholder="{{ t('ai.custom_model_placeholder') }}" class="flex-1 px-3 py-2 border border-gray-300 rounded-md">
<button id="ai-model-test-btn" type="button" class="px-3 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50">{{ t('ai.check_availability') }}</button>
</div>
<p class="mt-2 text-xs text-gray-500">
<i class="fas fa-external-link-alt"></i>
<a href="https://openrouter.ai/models" target="_blank" class="text-purple-600">openrouter.ai/models</a>
</p>
{% if not openrouter_key %}
<div class="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded">
<div class="flex items-center justify-between">
<span class="text-xs text-yellow-800">{{ t('settings.no_api_key') }}</span>
<a href="/settings#api" class="px-2 py-1 text-xs bg-purple-600 text-white rounded">{{ t('settings.enter_api_key') }}</a>
</div>
</div>
{% endif %}
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.protocol_type') }}</label>
<select id="ai-protocol-type" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">{{ t('ai.general_vpn') }}</option>
<option value="wireguard">WireGuard</option>
<option value="openvpn">OpenVPN</option>
<option value="shadowsocks">Shadowsocks</option>
<option value="cloak">Cloak</option>
<option value="ikev2">IKEv2</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.describe_requirements') }}</label>
<textarea id="ai-prompt" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="{{ t('ai.prompt_placeholder') }}"></textarea>
</div>
<div class="mb-4">
<button id="generate-script-btn" class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
{{ t('ai.generate_script') }}
</button>
</div>
<div id="ai-loading" class="hidden text-center py-4">
<div class="inline-flex items-center">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-purple-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>{{ t('ai.generating_script') }}</span>
</div>
</div>
<div id="ai-result" class="hidden">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.generated_script') }}</label>
<div class="bg-gray-900 text-green-400 p-4 rounded-md overflow-x-auto">
<pre id="generated-script" class="text-sm whitespace-pre-wrap"></pre>
</div>
</div>
<div id="ai-suggestions" class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.suggestions') }}</label>
<ul id="suggestions-list" class="list-disc list-inside space-y-1 text-sm text-gray-600"></ul>
</div>
<div class="mb-4 grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.name') }}</label>
<input id="ai-protocol-name" type="text" placeholder="Protocol Name" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.slug') }}</label>
<input id="ai-protocol-slug" type="text" placeholder="protocol-slug" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('common.compatibility') }}</label>
<div id="ubuntu-compatibility" class="flex items-center"></div>
</div>
<div class="flex space-x-3">
<button id="apply-to-protocol-btn" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
{{ t('ai.apply_to_protocol') }}
</button>
<button id="create-new-protocol-btn" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{{ t('ai.create_new_protocol') }}
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Protocol search and filter
const searchInput = document.getElementById('protocol-search');
const filterSelect = document.getElementById('protocol-filter');
const protocolCards = document.querySelectorAll('.protocol-card');
function filterProtocols() {
const searchTerm = searchInput.value.toLowerCase();
const filterValue = filterSelect.value;
protocolCards.forEach(card => {
const name = card.dataset.protocolName.toLowerCase();
const slug = card.dataset.protocolSlug.toLowerCase();
const isActive = card.dataset.active === '1';
const isUbuntu = card.dataset.ubuntu === '1';
const hasAI = parseInt(card.dataset.aiGenerations) > 0;
let show = true;
// Search filter
if (searchTerm && !name.includes(searchTerm) && !slug.includes(searchTerm)) {
show = false;
}
// Filter by type
if (filterValue === 'active' && !isActive) show = false;
if (filterValue === 'ubuntu' && !isUbuntu) show = false;
if (filterValue === 'with-ai' && !hasAI) show = false;
card.style.display = show ? 'block' : 'none';
});
}
searchInput.addEventListener('input', filterProtocols);
filterSelect.addEventListener('change', filterProtocols);
// AI Assistant Modal
const aiModal = document.getElementById('ai-assistant-modal');
const aiAssistantBtn = document.getElementById('ai-assistant-btn');
const closeAIModal = document.getElementById('close-ai-modal');
const generateScriptBtn = document.getElementById('generate-script-btn');
const aiLoading = document.getElementById('ai-loading');
const aiResult = document.getElementById('ai-result');
const applyToProtocolBtn = document.getElementById('apply-to-protocol-btn');
const createNewProtocolBtn = document.getElementById('create-new-protocol-btn');
let currentGeneration = null;
let currentProtocolId = null;
function showAIModal() {
aiModal.classList.remove('hidden');
aiResult.classList.add('hidden');
aiLoading.classList.add('hidden');
}
function hideAIModal() {
aiModal.classList.add('hidden');
currentGeneration = null;
currentProtocolId = null;
}
aiAssistantBtn.addEventListener('click', showAIModal);
closeAIModal.addEventListener('click', hideAIModal);
// Close modal when clicking outside
aiModal.addEventListener('click', function(e) {
if (e.target === aiModal) {
hideAIModal();
}
});
// Generate script with AI
generateScriptBtn.addEventListener('click', async function() {
const model = document.getElementById('ai-model-select').value;
const customModel = document.getElementById('ai-model-custom').value.trim();
const effectiveModel = customModel !== '' ? customModel : model;
const protocolType = document.getElementById('ai-protocol-type').value;
const prompt = document.getElementById('ai-prompt').value;
if (!prompt.trim()) {
alert('{{ t('ai.please_enter_requirements') }}');
return;
}
aiLoading.classList.remove('hidden');
generateScriptBtn.disabled = true;
try {
const response = await fetch('/api/ai/assist', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: prompt,
model: effectiveModel,
protocol_type: protocolType,
protocol_id: currentProtocolId
})
});
const result = await response.json();
if (result.success) {
currentGeneration = result.data;
displayAIResult(result.data);
} else {
alert('{{ t('ai.error_generating_script') }}: ' + result.error);
}
} catch (error) {
alert('{{ t('ai.error_generating_script') }}: ' + error.message);
} finally {
aiLoading.classList.add('hidden');
generateScriptBtn.disabled = false;
}
});
function displayAIResult(data) {
document.getElementById('generated-script').textContent = data.script;
const suggestionsList = document.getElementById('suggestions-list');
suggestionsList.innerHTML = '';
data.suggestions.forEach(suggestion => {
const li = document.createElement('li');
li.textContent = suggestion;
suggestionsList.appendChild(li);
});
const compatibilityDiv = document.getElementById('ubuntu-compatibility');
if (data.ubuntu_compatible) {
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg><span class="text-green-700">Compatible with Ubuntu 22.04-24.04</span>';
} else {
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg><span class="text-red-700">May not be compatible with Ubuntu 22.04-24.04</span>';
}
aiResult.classList.remove('hidden');
}
// Apply to existing protocol
applyToProtocolBtn.addEventListener('click', function() {
if (!currentGeneration) return;
const protocolId = prompt('{{ t('ai.enter_protocol_id_to_apply') }}:');
if (!protocolId) return;
fetch(`/api/protocols/${protocolId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
install_script: currentGeneration.script,
ubuntu_compatible: currentGeneration.ubuntu_compatible
})
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('{{ t('ai.script_applied_successfully') }}');
location.reload();
} else {
alert('{{ t('ai.error_applying_script') }}: ' + result.error);
}
})
.catch(error => {
alert('{{ t('ai.error_applying_script') }}: ' + error.message);
});
});
// Create new protocol with generated script
createNewProtocolBtn.addEventListener('click', function() {
if (!currentGeneration) return;
const nameInput = document.getElementById('ai-protocol-name');
const slugInput = document.getElementById('ai-protocol-slug');
const name = (nameInput.value || '').trim();
let slug = (slugInput.value || '').trim();
if (!name) { alert('{{ t('protocols.enter_protocol_name') }}'); return; }
if (!slug) { slug = name.toLowerCase().replace(/\s+/g, '-'); }
fetch('/api/protocols', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
slug: slug,
install_script: currentGeneration.script,
ubuntu_compatible: currentGeneration.ubuntu_compatible,
description: `Generated with AI using ${document.getElementById('ai-model-select').value}`
})
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('{{ t('protocols.protocol_created_successfully') }}');
location.reload();
} else {
alert('{{ t('protocols.error_creating_protocol') }}: ' + result.error);
}
})
.catch(error => {
alert('{{ t('protocols.error_creating_protocol') }}: ' + error.message);
});
});
// AI generate for specific protocol
document.querySelectorAll('.ai-generate-btn').forEach(btn => {
btn.addEventListener('click', function() {
currentProtocolId = this.dataset.protocolId;
showAIModal();
document.getElementById('ai-prompt').value = `{{ t('ai.improve_protocol') }} ${this.closest('.protocol-card').dataset.protocolName}`;
});
});
// Test custom model availability
document.getElementById('ai-model-test-btn').addEventListener('click', async function() {
const customModel = document.getElementById('ai-model-custom').value.trim();
if (!customModel) { alert('Введите идентификатор модели'); return; }
try {
const resp = await fetch('/api/ai/test-model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: customModel })
});
const result = await resp.json();
if (result.success) {
alert('Модель доступна');
} else {
alert('Модель недоступна: ' + (result.error || result.message || ''));
}
} catch (e) {
alert('Ошибка проверки модели: ' + e.message);
}
});
// Delete protocol
document.querySelectorAll('.delete-protocol-btn').forEach(btn => {
btn.addEventListener('click', function() {
const protocolId = this.dataset.protocolId;
const protocolName = this.dataset.protocolName;
if (confirm(`{{ t('protocols.confirm_delete_protocol') }} '${protocolName}'?`)) {
fetch(`/api/protocols/${protocolId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(result => {
if (result.success) {
location.reload();
} else {
alert('{{ t('protocols.error_deleting_protocol') }}: ' + result.error);
}
})
.catch(error => {
alert('{{ t('protocols.error_deleting_protocol') }}: ' + error.message);
});
}
});
});
});
</script>
+250
View File
@@ -0,0 +1,250 @@
{% extends "layout.twig" %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-10 offset-md-1">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
{% if scenario %}
{{ 'Редактирование сценария:' | trans }} {{ scenario.name }}
{% else %}
{{ 'Новый сценарий' | trans }}
{% endif %}
</h5>
</div>
<div class="card-body">
<form id="scenarioForm">
<input type="hidden" name="id" value="{{ scenario.id | default('') }}">
<div class="mb-3">
<label for="slug" class="form-label">{{ 'Уникальный идентификатор' | trans }} *</label>
<input type="text" class="form-control" id="slug" name="slug"
value="{{ scenario.slug | default('') }}" required
pattern="^[a-z0-9\-]+$" title="{{ 'Только строчные буквы, цифры и дефисы' | trans }}">
<small class="form-text text-muted">{{ 'например: xray-vless, openvpn-tls' | trans }}</small>
</div>
<div class="mb-3">
<label for="name" class="form-label">{{ 'Название протокола' | trans }} *</label>
<input type="text" class="form-control" id="name" name="name"
value="{{ scenario.name | default('') }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">{{ 'Описание' | trans }}</label>
<textarea class="form-control" id="description" name="description" rows="2">{{ scenario.description | default('') }}</textarea>
</div>
<div class="mb-3">
<label for="definition" class="form-label">{{ 'Определение сценария (JSON)' | trans }} *</label>
<textarea class="form-control font-monospace" id="definition" name="definition"
rows="20" required>{{ templateDefinition }}</textarea>
<small class="form-text text-muted d-block mt-2">
<strong>{{ 'Структура JSON:' | trans }}</strong><br>
<code>{ "engine": "shell|builtin_awg", "metadata": {...}, "scripts": { "detect": "...", "install": "...", "restore": "..." } }</code>
</small>
<small class="form-text text-muted d-block mt-2">
<strong>{{ 'Доступные переменные в скриптах:' | trans }}</strong><br>
<code>{{ "{{server.host}}, {{server.username}}, {{server.container_name}}, {{metadata.*}}" | trans }}</code>
</small>
<div id="jsonError" class="alert alert-danger mt-2" style="display: none;"></div>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="is_active" name="is_active" value="1"
{% if scenario.is_active ?? true %}checked{% endif %}>
<label class="form-check-label" for="is_active">
{{ 'Активный сценарий' | trans }}
</label>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-success">
<i class="fas fa-save"></i> {{ 'Сохранить' | trans }}
</button>
<a href="/admin/scenarios" class="btn btn-secondary">
{{ 'Отмена' | trans }}
</a>
{% if scenario %}
<button type="button" class="btn btn-info ms-auto" id="testBtn">
<i class="fas fa-flask"></i> {{ 'Тест на сервере' | trans }}
</button>
{% endif %}
</div>
</form>
</div>
</div>
<!-- JSON Validation Helper -->
<div class="card mt-3">
<div class="card-header bg-light">
<h6 class="mb-0">{{ 'Справка по формату' | trans }}</h6>
</div>
<div class="card-body">
<h6>{{ 'Поля сценария:' | trans }}</h6>
<ul>
<li><strong>engine:</strong> Тип движка ("shell" или "builtin_awg")</li>
<li><strong>metadata:</strong> Объект с параметрами протокола (container_name, config_path и т.д.)</li>
<li><strong>scripts:</strong> Объект со скриптами (detect, install, restore)</li>
</ul>
<h6 class="mt-3">{{ 'Поля скриптов:' | trans }}</h6>
<ul>
<li><strong>detect:</strong> Bash скрипт для определения установленной конфигурации. Должен вывести JSON с полями "status" (absent/partial/existing) и "details"</li>
<li><strong>install:</strong> Bash скрипт для установки протокола. Должен вывести JSON с "success": true/false</li>
<li><strong>restore:</strong> Bash скрипт для восстановления конфигурации из detection результата</li>
</ul>
<h6 class="mt-3">{{ 'Переменные окружения в скриптах:' | trans }}</h6>
<ul>
<li><code>SERVER_HOST</code> - IP/домен сервера</li>
<li><code>SERVER_USER</code> - SSH пользователь</li>
<li><code>SERVER_CONTAINER</code> - имя контейнера</li>
<li><code>PROTOCOL_*</code> - все поля из metadata (например, PROTOCOL_CONTAINER_NAME, PROTOCOL_CONFIG_PATH)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Test Modal -->
<div class="modal fade" id="testModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ 'Тест сценария' | trans }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="testServer" class="form-label">{{ 'Выбрать сервер' | trans }}</label>
<select class="form-control" id="testServer">
<option value="">{{ 'Загружаю...' | trans }}</option>
</select>
</div>
<div id="testResult" class="mt-3" style="display: none;">
<strong>{{ 'Результат:' | trans }}</strong>
<pre id="testResultContent" class="bg-light p-3 rounded" style="max-height: 300px; overflow-y: auto;"></pre>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Закрыть' | trans }}</button>
<button type="button" class="btn btn-primary" id="runTestBtn">{{ 'Запустить тест' | trans }}</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('scenarioForm');
const definitionTextarea = document.getElementById('definition');
const jsonError = document.getElementById('jsonError');
const testBtn = document.getElementById('testBtn');
// Validate JSON on change
definitionTextarea.addEventListener('change', validateJson);
definitionTextarea.addEventListener('blur', validateJson);
function validateJson() {
jsonError.style.display = 'none';
try {
JSON.parse(definitionTextarea.value);
} catch (e) {
jsonError.textContent = `{{ "Ошибка JSON:" | trans }} ${e.message}`;
jsonError.style.display = 'block';
}
}
// Form submission
form.addEventListener('submit', async function(e) {
e.preventDefault();
if (!validateJson()) return;
const formData = new FormData(this);
try {
const response = await fetch('/admin/scenario', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
alert('{{ "Сценарий успешно сохранен" | trans }}');
window.location.href = data.redirect;
} else {
alert(`{{ "Ошибка:" | trans }} ${data.message}`);
}
} catch (err) {
alert(`{{ "Ошибка отправки:" | trans }} ${err}`);
}
});
// Test button
if (testBtn) {
testBtn.addEventListener('click', function() {
loadServers();
new bootstrap.Modal(document.getElementById('testModal')).show();
});
}
// Load available servers
async function loadServers() {
try {
const response = await fetch('/api/servers?limit=50');
const data = await response.json();
const select = document.getElementById('testServer');
select.innerHTML = '';
if (data.servers && data.servers.length > 0) {
data.servers.forEach(server => {
const option = document.createElement('option');
option.value = server.id;
option.textContent = `${server.name} (${server.host})`;
select.appendChild(option);
});
} else {
select.innerHTML = '<option value="">{{ "Сервера не найдены" | trans }}</option>';
}
} catch (err) {
console.error('Error loading servers:', err);
}
}
// Run test
document.getElementById('runTestBtn').addEventListener('click', async function() {
const serverId = document.getElementById('testServer').value;
if (!serverId) {
alert('{{ "Выберите сервер" | trans }}');
return;
}
const scenarioId = document.querySelector('input[name="id"]').value;
try {
const response = await fetch(`/admin/scenario/${scenarioId}/test`, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `server_id=${serverId}`
});
const data = await response.json();
const resultDiv = document.getElementById('testResult');
const resultContent = document.getElementById('testResultContent');
resultContent.textContent = JSON.stringify(data.result, null, 2);
resultDiv.style.display = 'block';
} catch (err) {
alert(`{{ "Ошибка теста:" | trans }} ${err}`);
}
});
});
</script>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More