Files
vpn/app/services/remote.py
T

241 lines
8.4 KiB
Python

"""Deploy real WireGuard / AmneziaWG configs to remote VPS over SSH."""
from __future__ import annotations
import logging
import time
from app.models import Protocol, VpnServer
from app.services.ssh import SSHManager
logger = logging.getLogger(__name__)
DOCKER_IMAGE = "amneziavpn/amnezia-wg:latest"
def ssh_from_server(server: VpnServer) -> SSHManager:
if not server.ssh_host or not server.ssh_username:
raise ValueError("SSH данные сервера не заданы")
if not server.ssh_password and not server.ssh_private_key:
raise ValueError("Нужен пароль или ключ SSH")
return SSHManager(
host=server.ssh_host,
port=server.ssh_port or 22,
username=server.ssh_username,
password=server.ssh_password,
private_key=server.ssh_private_key,
)
def container_name(server: VpnServer) -> str:
safe = "".join(c if c.isalnum() else "-" for c in server.interface_name)
return f"vpn-panel-{safe}"
def remote_dir(server: VpnServer) -> str:
return f"/opt/vpn-panel/{server.interface_name}"
def remote_conf_path(server: VpnServer) -> str:
return f"{remote_dir(server)}/{server.interface_name}.conf"
def wg_binary(protocol: str) -> str:
return "awg" if protocol == Protocol.AWG2.value else "wg"
def ensure_docker(ssh: SSHManager) -> None:
out, _, code = ssh.run_command("docker --version 2>/dev/null")
if code == 0 and out:
return
script = r"""
set -e
export DEBIAN_FRONTEND=noninteractive
if command -v apt-get >/dev/null 2>&1; then
apt-get update -yq
apt-get install -yq docker.io curl
systemctl enable --now docker
elif command -v dnf >/dev/null 2>&1; then
dnf install -y docker
systemctl enable --now docker
elif command -v yum >/dev/null 2>&1; then
yum install -y docker
systemctl enable --now docker
else
echo "No supported package manager"; exit 1
fi
docker --version
"""
out, err, code = ssh.run_sudo_script(script, timeout=300)
if code != 0:
raise RuntimeError(f"Не удалось установить Docker: {err or out}")
def install_vpn(server: VpnServer, server_config: str) -> list[str]:
"""Install Docker container and apply initial server config on VPS."""
logs: list[str] = []
ssh = ssh_from_server(server)
ssh.connect()
try:
logs.append("Проверка Docker...")
ensure_docker(ssh)
logs.append("Docker готов")
name = container_name(server)
conf_dir = remote_dir(server)
conf_path = remote_conf_path(server)
port = server.public_port
binary = wg_binary(server.protocol)
ssh.run_sudo_command(f"mkdir -p {conf_dir}")
ssh.upload_file_sudo(server_config.replace("\r\n", "\n"), conf_path)
logs.append(f"Конфиг записан: {conf_path}")
# Stop old container if exists
ssh.run_sudo_command(f"docker rm -f {name} 2>/dev/null || true")
logs.append(f"Pull образа {DOCKER_IMAGE}...")
out, err, code = ssh.run_sudo_command(f"docker pull {DOCKER_IMAGE}", timeout=300)
if code != 0:
raise RuntimeError(f"docker pull failed: {err or out}")
# start.sh brings interface up and keeps container alive
start_script = f"""#!/bin/bash
set -e
CONF="{conf_path}"
IFACE="{server.interface_name}"
BIN="{binary}"
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true
# Normalize config path inside container (mounted at /config)
if [ -f /config/$IFACE.conf ]; then
CONF=/config/$IFACE.conf
fi
mkdir -p /etc/amnezia/$BIN 2>/dev/null || mkdir -p /etc/wireguard 2>/dev/null || true
cp -f "$CONF" /etc/amnezia/$BIN/$IFACE.conf 2>/dev/null || cp -f "$CONF" /etc/wireguard/$IFACE.conf 2>/dev/null || true
$BIN-quick down "$CONF" 2>/dev/null || true
$BIN-quick up "$CONF"
# NAT
iptables -t nat -C POSTROUTING -s {server.subnet} -o eth0 -j MASQUERADE 2>/dev/null \\
|| iptables -t nat -A POSTROUTING -s {server.subnet} -o eth0 -j MASQUERADE
iptables -C FORWARD -i $IFACE -j ACCEPT 2>/dev/null || iptables -A FORWARD -i $IFACE -j ACCEPT
iptables -C FORWARD -o $IFACE -j ACCEPT 2>/dev/null || iptables -A FORWARD -o $IFACE -j ACCEPT
tail -f /dev/null
"""
ssh.upload_file_sudo(start_script, f"{conf_dir}/start.sh")
ssh.run_sudo_command(f"chmod +x {conf_dir}/start.sh")
run_cmd = (
f"docker run -d --name {name} --restart always --privileged "
f"--cap-add=NET_ADMIN --cap-add=SYS_MODULE "
f"-p {port}:{port}/udp "
f"-v {conf_dir}:/config "
f"-v /lib/modules:/lib/modules "
f'--sysctl="net.ipv4.conf.all.src_valid_mark=1" '
f'--sysctl="net.ipv4.ip_forward=1" '
f"--entrypoint /bin/bash {DOCKER_IMAGE} "
f"/config/start.sh"
)
logs.append("Запуск контейнера...")
out, err, code = ssh.run_sudo_command(run_cmd, timeout=120)
if code != 0:
raise RuntimeError(f"Не удалось запустить контейнер: {err or out}")
# wait running
for _ in range(15):
status, _, _ = ssh.run_sudo_command(
f"docker inspect --format='{{{{.State.Status}}}}' {name}"
)
if "running" in status:
break
time.sleep(2)
else:
logs_out, _, _ = ssh.run_sudo_command(f"docker logs --tail 40 {name} 2>&1")
raise RuntimeError(f"Контейнер не запустился:\n{logs_out}")
time.sleep(3)
iface_check, _, _ = ssh.run_sudo_command(
f"docker exec {name} {binary} show {server.interface_name} 2>&1 || true"
)
logs.append("Контейнер запущен")
if iface_check.strip():
logs.append(f"Интерфейс: OK ({server.interface_name})")
else:
logs.append("Предупреждение: интерфейс ещё поднимается, конфиг на диске готов")
# open firewall on host if ufw
ssh.run_sudo_command(f"ufw allow {port}/udp 2>/dev/null || true")
ssh.run_sudo_command(
"sysctl -w net.ipv4.ip_forward=1; "
"grep -q '^net.ipv4.ip_forward=1' /etc/sysctl.conf || "
"echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf"
)
logs.append("Готово: VPN установлен, клиентские .conf можно выдавать")
return logs
finally:
ssh.disconnect()
def deploy_config(server: VpnServer, server_config: str) -> None:
"""Push updated server.conf to VPS and sync peers without full reinstall."""
ssh = ssh_from_server(server)
ssh.connect()
try:
name = container_name(server)
conf_path = remote_conf_path(server)
conf_dir = remote_dir(server)
binary = wg_binary(server.protocol)
ssh.run_sudo_command(f"mkdir -p {conf_dir}")
ssh.upload_file_sudo(server_config.replace("\r\n", "\n"), conf_path)
# container exists?
names, _, _ = ssh.run_sudo_command(
f"docker ps -a --filter name=^{name}$ --format '{{{{.Names}}}}'"
)
if name not in names:
raise RuntimeError(
"VPN контейнер не установлен. Нажмите «Установить VPN на сервер»."
)
# copy into running paths and sync
sync_script = f"""
set -e
NAME="{name}"
CONF="{conf_path}"
IFACE="{server.interface_name}"
BIN="{binary}"
docker cp "$CONF" "$NAME:/config/$IFACE.conf"
docker exec "$NAME" bash -c "cp -f /config/$IFACE.conf /etc/amnezia/$BIN/$IFACE.conf 2>/dev/null || cp -f /config/$IFACE.conf /etc/wireguard/$IFACE.conf 2>/dev/null || true"
# try syncconf; fallback restart
if docker exec "$NAME" bash -c "$BIN syncconf $IFACE <($BIN-quick strip /config/$IFACE.conf)" 2>/dev/null; then
echo synced
else
docker restart "$NAME"
echo restarted
fi
"""
out, err, code = ssh.run_sudo_script(sync_script, timeout=90)
if code != 0:
raise RuntimeError(f"Не удалось применить конфиг: {err or out}")
finally:
ssh.disconnect()
def container_running(server: VpnServer) -> bool:
ssh = ssh_from_server(server)
ssh.connect()
try:
name = container_name(server)
out, _, _ = ssh.run_sudo_command(
f"docker ps --filter name=^{name}$ --format '{{{{.Status}}}}'"
)
return "Up" in out
finally:
ssh.disconnect()