Add project files
This commit is contained in:
@@ -0,0 +1,449 @@
|
||||
<?php
|
||||
/**
|
||||
* VPN Server Management Class
|
||||
* Handles deployment and management of Amnezia VPN servers
|
||||
* Based on amnezia_deploy_v2.php
|
||||
*/
|
||||
class VpnServer {
|
||||
private $serverId;
|
||||
private $data;
|
||||
|
||||
public function __construct(?int $serverId = null) {
|
||||
$this->serverId = $serverId;
|
||||
if ($serverId) {
|
||||
$this->load();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load server data from database
|
||||
*/
|
||||
private function load(): void {
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('SELECT * FROM vpn_servers WHERE id = ?');
|
||||
$stmt->execute([$this->serverId]);
|
||||
$this->data = $stmt->fetch();
|
||||
if (!$this->data) {
|
||||
throw new Exception('Server not found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new VPN server in database
|
||||
*/
|
||||
public static function create(array $data): int {
|
||||
$pdo = DB::conn();
|
||||
|
||||
// Validate required fields
|
||||
$required = ['user_id', 'name', 'host', 'port', 'username', 'password'];
|
||||
foreach ($required as $field) {
|
||||
if (empty($data[$field])) {
|
||||
throw new Exception("Field {$field} is required");
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO vpn_servers
|
||||
(user_id, name, host, port, username, password, container_name, vpn_subnet, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
$data['user_id'],
|
||||
$data['name'],
|
||||
$data['host'],
|
||||
$data['port'],
|
||||
$data['username'],
|
||||
$data['password'],
|
||||
$data['container_name'] ?? 'amnezia-awg',
|
||||
$data['vpn_subnet'] ?? '10.8.1.0/24',
|
||||
'deploying'
|
||||
]);
|
||||
|
||||
return (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy VPN server using amnezia_deploy_v2.php logic
|
||||
*/
|
||||
public function deploy(): array {
|
||||
if (!$this->data) {
|
||||
throw new Exception('Server not loaded');
|
||||
}
|
||||
|
||||
$pdo = DB::conn();
|
||||
$errors = [];
|
||||
|
||||
try {
|
||||
// Update status to deploying
|
||||
$pdo->prepare('UPDATE vpn_servers SET status = ? WHERE id = ?')
|
||||
->execute(['deploying', $this->serverId]);
|
||||
|
||||
// Test SSH connection
|
||||
if (!$this->testConnection()) {
|
||||
throw new Exception('SSH connection failed');
|
||||
}
|
||||
|
||||
// Install Docker if needed
|
||||
$this->installDocker();
|
||||
|
||||
// Create directories
|
||||
$this->executeCommand('mkdir -p /opt/amnezia/amnezia-awg', true);
|
||||
|
||||
// Find free UDP port
|
||||
$vpnPort = $this->findFreeUdpPort();
|
||||
|
||||
// Create Dockerfile
|
||||
$this->createDockerfile();
|
||||
|
||||
// Create start script
|
||||
$this->createStartScript();
|
||||
|
||||
// Build Docker image
|
||||
$this->buildDockerImage();
|
||||
|
||||
// Run container
|
||||
$this->runContainer($vpnPort);
|
||||
|
||||
// Initialize server config
|
||||
$keys = $this->initializeServerConfig($vpnPort);
|
||||
|
||||
// Update database with deployment info
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE vpn_servers
|
||||
SET vpn_port = ?,
|
||||
server_public_key = ?,
|
||||
preshared_key = ?,
|
||||
awg_params = ?,
|
||||
status = ?,
|
||||
deployed_at = NOW(),
|
||||
error_message = NULL
|
||||
WHERE id = ?
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
$vpnPort,
|
||||
$keys['public_key'],
|
||||
$keys['preshared_key'],
|
||||
json_encode($keys['awg_params']),
|
||||
'active',
|
||||
$this->serverId
|
||||
]);
|
||||
|
||||
// Reload data
|
||||
$this->load();
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'vpn_port' => $vpnPort,
|
||||
'public_key' => $keys['public_key']
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Update status to error
|
||||
$pdo->prepare('UPDATE vpn_servers SET status = ?, error_message = ? WHERE id = ?')
|
||||
->execute(['error', $e->getMessage(), $this->serverId]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SSH connection to server
|
||||
*/
|
||||
private function testConnection(): bool {
|
||||
$testCommand = sprintf(
|
||||
"sshpass -p '%s' ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no -o ConnectTimeout=10 %s@%s 'echo test' 2>/dev/null",
|
||||
$this->data['password'],
|
||||
$this->data['username'],
|
||||
$this->data['host']
|
||||
);
|
||||
|
||||
$result = shell_exec($testCommand);
|
||||
return trim($result) === 'test';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute command on remote server
|
||||
*/
|
||||
private function executeCommand(string $command, bool $sudo = false): string {
|
||||
if ($sudo && strtolower($this->data['username']) !== 'root') {
|
||||
$command = "echo '{$this->data['password']}' | sudo -S " . $command;
|
||||
}
|
||||
|
||||
$escapedCommand = escapeshellarg($command);
|
||||
$sshCommand = sprintf(
|
||||
"sshpass -p '%s' ssh -q -o LogLevel=ERROR -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no %s@%s %s 2>&1",
|
||||
$this->data['password'],
|
||||
$this->data['username'],
|
||||
$this->data['host'],
|
||||
$escapedCommand
|
||||
);
|
||||
|
||||
return shell_exec($sshCommand) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Docker on remote server
|
||||
*/
|
||||
private function installDocker(): void {
|
||||
$dockerVersion = $this->executeCommand('docker --version');
|
||||
if (stripos($dockerVersion, 'version') !== false) {
|
||||
return; // Docker already installed
|
||||
}
|
||||
|
||||
$this->executeCommand('curl -fsSL https://get.docker.com | sh', true);
|
||||
$this->executeCommand('systemctl enable --now docker', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find free UDP port on remote server
|
||||
*/
|
||||
private function findFreeUdpPort(): int {
|
||||
$min = 30000;
|
||||
$max = 65000;
|
||||
|
||||
for ($attempt = 0; $attempt < 30; $attempt++) {
|
||||
$candidate = random_int($min, $max);
|
||||
$cmd = "ss -lun | awk '{print \$4}' | grep -E ':(" . $candidate . ")($| )' || true";
|
||||
$out = $this->executeCommand($cmd, false);
|
||||
if (trim($out) === '') {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception('Could not find free UDP port');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Dockerfile on remote server
|
||||
*/
|
||||
private function createDockerfile(): void {
|
||||
$dockerfile = <<<'DOCKERFILE'
|
||||
FROM amneziavpn/amnezia-wg:latest
|
||||
|
||||
LABEL maintainer="AmneziaVPN"
|
||||
|
||||
RUN apk add --no-cache bash curl dumb-init
|
||||
RUN apk --update upgrade --no-cache
|
||||
|
||||
RUN mkdir -p /opt/amnezia
|
||||
RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh
|
||||
RUN chmod a+x /opt/amnezia/start.sh
|
||||
|
||||
ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
||||
CMD [ "" ]
|
||||
DOCKERFILE;
|
||||
|
||||
$escaped = addslashes(trim($dockerfile));
|
||||
$this->executeCommand("echo \"{$escaped}\" > /opt/amnezia/amnezia-awg/Dockerfile", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create start script on remote server
|
||||
*/
|
||||
private function createStartScript(): void {
|
||||
$script = <<<'BASH'
|
||||
#!/bin/bash
|
||||
|
||||
echo "Container startup"
|
||||
|
||||
# Wait for config if not exists yet
|
||||
for i in {1..30}; do
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Kill daemons in case of restart
|
||||
wg-quick down /opt/amnezia/awg/wg0.conf 2>/dev/null || true
|
||||
|
||||
# Start daemons if configured
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
wg-quick up /opt/amnezia/awg/wg0.conf
|
||||
echo "WireGuard started"
|
||||
else
|
||||
echo "No wg0.conf found, skipping WireGuard startup"
|
||||
fi
|
||||
|
||||
# Allow traffic on the TUN interface
|
||||
iptables -A INPUT -i wg0 -j ACCEPT 2>/dev/null || true
|
||||
iptables -A FORWARD -i wg0 -j ACCEPT 2>/dev/null || true
|
||||
iptables -A OUTPUT -o wg0 -j ACCEPT 2>/dev/null || true
|
||||
|
||||
# Allow forwarding traffic only from the VPN
|
||||
iptables -A FORWARD -i wg0 -o eth0 -s 10.8.1.0/24 -j ACCEPT 2>/dev/null || true
|
||||
iptables -A FORWARD -i wg0 -o eth1 -s 10.8.1.0/24 -j ACCEPT 2>/dev/null || true
|
||||
|
||||
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true
|
||||
|
||||
iptables -t nat -A POSTROUTING -s 10.8.1.0/24 -o eth0 -j MASQUERADE 2>/dev/null || true
|
||||
iptables -t nat -A POSTROUTING -s 10.8.1.0/24 -o eth1 -j MASQUERADE 2>/dev/null || true
|
||||
|
||||
tail -f /dev/null
|
||||
BASH;
|
||||
|
||||
$escaped = addslashes(trim($script));
|
||||
$this->executeCommand("echo \"{$escaped}\" > /opt/amnezia/amnezia-awg/start.sh", true);
|
||||
$this->executeCommand("chmod +x /opt/amnezia/amnezia-awg/start.sh", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Docker image
|
||||
*/
|
||||
private function buildDockerImage(): void {
|
||||
$containerName = $this->data['container_name'];
|
||||
|
||||
// Cleanup old container/image
|
||||
$this->executeCommand("docker stop {$containerName} 2>/dev/null || true", true);
|
||||
$this->executeCommand("docker rm -fv {$containerName} 2>/dev/null || true", true);
|
||||
$this->executeCommand("docker rmi {$containerName} 2>/dev/null || true", true);
|
||||
|
||||
// Build new image
|
||||
$buildCmd = sprintf(
|
||||
'docker build --no-cache --pull -t %s /opt/amnezia/amnezia-awg',
|
||||
$containerName
|
||||
);
|
||||
$this->executeCommand($buildCmd, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Docker container
|
||||
*/
|
||||
private function runContainer(int $vpnPort): void {
|
||||
$containerName = $this->data['container_name'];
|
||||
|
||||
$runCmd = sprintf(
|
||||
'docker run -d --log-driver none --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p %d:%d/udp -v /lib/modules:/lib/modules --name %s %s',
|
||||
$vpnPort,
|
||||
$vpnPort,
|
||||
$containerName,
|
||||
$containerName
|
||||
);
|
||||
|
||||
$this->executeCommand($runCmd, true);
|
||||
sleep(3); // Wait for container to start
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize server configuration with AWG parameters
|
||||
*/
|
||||
private function initializeServerConfig(int $vpnPort): array {
|
||||
$containerName = $this->data['container_name'];
|
||||
|
||||
// Create directory
|
||||
$this->executeCommand("docker exec -i {$containerName} mkdir -p /opt/amnezia/awg", true);
|
||||
|
||||
// Generate keys
|
||||
$this->executeCommand("docker exec -i {$containerName} sh -c 'cd /opt/amnezia/awg && umask 077 && wg genkey | tee server_private.key | wg pubkey > wireguard_server_public_key.key'", true);
|
||||
$this->executeCommand("docker exec -i {$containerName} sh -c 'cd /opt/amnezia/awg && wg genpsk > wireguard_psk.key'", true);
|
||||
$this->executeCommand("docker exec -i {$containerName} chmod 600 /opt/amnezia/awg/server_private.key /opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_server_public_key.key", true);
|
||||
|
||||
// Get keys
|
||||
$privKey = trim($this->executeCommand("docker exec -i {$containerName} cat /opt/amnezia/awg/server_private.key", true));
|
||||
$pubKey = trim($this->executeCommand("docker exec -i {$containerName} cat /opt/amnezia/awg/wireguard_server_public_key.key", true));
|
||||
$psk = trim($this->executeCommand("docker exec -i {$containerName} cat /opt/amnezia/awg/wireguard_psk.key", true));
|
||||
|
||||
// Generate AWG parameters
|
||||
$awgParams = [
|
||||
'Jc' => 3,
|
||||
'Jmin' => 10,
|
||||
'Jmax' => 50,
|
||||
'S1' => rand(50, 250),
|
||||
'S2' => rand(50, 250),
|
||||
'H1' => rand(100000, 2000000000),
|
||||
'H2' => rand(100000, 2000000000),
|
||||
'H3' => rand(100000, 2000000000),
|
||||
'H4' => rand(100000, 2000000000)
|
||||
];
|
||||
|
||||
// Create wg0.conf
|
||||
$wgConfig = "[Interface]\n";
|
||||
$wgConfig .= "PrivateKey = {$privKey}\n";
|
||||
$wgConfig .= "Address = {$this->data['vpn_subnet']}\n";
|
||||
$wgConfig .= "ListenPort = {$vpnPort}\n";
|
||||
foreach ($awgParams as $key => $value) {
|
||||
$wgConfig .= "{$key} = {$value}\n";
|
||||
}
|
||||
$wgConfig .= "\n";
|
||||
|
||||
$escaped = addslashes($wgConfig);
|
||||
$this->executeCommand("docker exec -i {$containerName} sh -c 'echo \"{$escaped}\" > /opt/amnezia/awg/wg0.conf'", true);
|
||||
$this->executeCommand("docker exec -i {$containerName} chmod 600 /opt/amnezia/awg/wg0.conf", true);
|
||||
|
||||
// Create clientsTable
|
||||
$this->executeCommand("docker exec -i {$containerName} sh -c 'echo \"[]\" > /opt/amnezia/awg/clientsTable'", true);
|
||||
|
||||
// Start WireGuard
|
||||
$this->executeCommand("docker exec -i {$containerName} wg-quick up /opt/amnezia/awg/wg0.conf 2>&1", true);
|
||||
|
||||
// Apply firewall rules
|
||||
$this->executeCommand("docker exec -i {$containerName} sh -c 'iptables -A INPUT -i wg0 -j ACCEPT 2>/dev/null || true'", true);
|
||||
$this->executeCommand("docker exec -i {$containerName} sh -c 'iptables -A FORWARD -i wg0 -j ACCEPT 2>/dev/null || true'", true);
|
||||
$this->executeCommand("docker exec -i {$containerName} sh -c 'iptables -A OUTPUT -o wg0 -j ACCEPT 2>/dev/null || true'", true);
|
||||
$this->executeCommand("docker exec -i {$containerName} sh -c 'iptables -A FORWARD -i wg0 -o eth0 -s 10.8.1.0/24 -j ACCEPT 2>/dev/null || true'", true);
|
||||
$this->executeCommand("docker exec -i {$containerName} sh -c 'iptables -t nat -A POSTROUTING -s 10.8.1.0/24 -o eth0 -j MASQUERADE 2>/dev/null || true'", true);
|
||||
|
||||
sleep(2);
|
||||
|
||||
return [
|
||||
'public_key' => $pubKey,
|
||||
'preshared_key' => $psk,
|
||||
'awg_params' => $awgParams
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server status from database
|
||||
*/
|
||||
public function getStatus(): string {
|
||||
return $this->data['status'] ?? 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all servers for a user
|
||||
*/
|
||||
public static function listByUser(int $userId): array {
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('SELECT * FROM vpn_servers WHERE user_id = ? ORDER BY created_at DESC');
|
||||
$stmt->execute([$userId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all servers (admin only)
|
||||
*/
|
||||
public static function listAll(): array {
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->query('SELECT s.*, u.email as user_email FROM vpn_servers s LEFT JOIN users u ON s.user_id = u.id ORDER BY s.created_at DESC');
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete server
|
||||
*/
|
||||
public function delete(): bool {
|
||||
// Stop and remove container
|
||||
try {
|
||||
$containerName = $this->data['container_name'];
|
||||
$this->executeCommand("docker stop {$containerName} 2>/dev/null || true", true);
|
||||
$this->executeCommand("docker rm -fv {$containerName} 2>/dev/null || true", true);
|
||||
$this->executeCommand("rm -rf /opt/amnezia/amnezia-awg", true);
|
||||
} catch (Exception $e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('DELETE FROM vpn_servers WHERE id = ?');
|
||||
return $stmt->execute([$this->serverId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server data
|
||||
*/
|
||||
public function getData(): ?array {
|
||||
return $this->data;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user