mirror of
https://github.com/PRVTPRO/Amnezia-Web-Panel.git
synced 2026-09-18 01:44:10 +02:00
29def95df8
Add SOCKS5 support Add AdGuard Home integration Add node status and latency tracking Add drag-and-drop server management Upgrade Xray from v1.8 to v26.3.27 with backward compatibility Fix Telemt dependency installation Extend Telegram bot to support all protocols Improve server cleanup process Enhance API structure (system routes moved to a separate group) Add JWT token generation for external integrations
230 lines
9.2 KiB
Python
230 lines
9.2 KiB
Python
"""
|
|
AdGuard Home Manager — runs adguard/adguardhome in a Docker container,
|
|
joined to the same internal `amnezia-dns-net` network the rest of the panel
|
|
uses. Two install modes:
|
|
|
|
* 'replace' — removes the AmneziaDNS container (if present) and takes
|
|
its static IP (172.29.172.254) so VPN clients keep using
|
|
the same upstream.
|
|
* 'sidebyside' — runs alongside AmneziaDNS on a different static IP
|
|
(172.29.172.253). VPN users can hit it on demand
|
|
(e.g. via the web admin UI from inside the tunnel).
|
|
|
|
Initial setup of AdGuard itself runs through its built-in wizard on the web
|
|
UI port — we do not try to script it (the JSON setup API is unstable across
|
|
versions and trying to drive it programmatically tends to break on upgrade).
|
|
"""
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AdguardManager:
|
|
PROTOCOL = 'adguard'
|
|
CONTAINER_NAME = 'amnezia-adguard'
|
|
IMAGE_NAME = 'adguard/adguardhome:latest'
|
|
|
|
NETWORK_NAME = 'amnezia-dns-net'
|
|
NETWORK_SUBNET = '172.29.172.0/24'
|
|
REPLACE_IP = '172.29.172.254' # AmneziaDNS's slot
|
|
SIDEBYSIDE_IP = '172.29.172.253' # parallel to AmneziaDNS
|
|
|
|
HOST_DIR = '/opt/amnezia/adguard'
|
|
DEFAULT_DNS_PORT = 53
|
|
DEFAULT_WEB_PORT = 3000
|
|
DEFAULT_DOT_PORT = 853
|
|
DEFAULT_DOH_PORT = 443
|
|
|
|
def __init__(self, ssh):
|
|
self.ssh = ssh
|
|
|
|
# ===================== STATUS =====================
|
|
|
|
def check_docker_installed(self):
|
|
out, _, code = self.ssh.run_command("docker --version 2>/dev/null")
|
|
if code != 0:
|
|
return False
|
|
out2, _, _ = self.ssh.run_command(
|
|
"systemctl is-active docker 2>/dev/null || service docker status 2>/dev/null"
|
|
)
|
|
return 'active' in out2 or 'running' in out2.lower()
|
|
|
|
def check_protocol_installed(self, protocol_type='adguard'):
|
|
out, _, _ = self.ssh.run_sudo_command(
|
|
f"docker ps -a --filter name=^{self.CONTAINER_NAME}$ --format '{{{{.Names}}}}'"
|
|
)
|
|
return self.CONTAINER_NAME in out.strip().split('\n')
|
|
|
|
def check_container_running(self):
|
|
out, _, _ = self.ssh.run_sudo_command(
|
|
f"docker ps --filter name=^{self.CONTAINER_NAME}$ --format '{{{{.Status}}}}'"
|
|
)
|
|
return 'Up' in out
|
|
|
|
def _container_ip(self):
|
|
out, _, _ = self.ssh.run_sudo_command(
|
|
f"docker inspect -f '{{{{range .NetworkSettings.Networks}}}}{{{{.IPAddress}}}} {{{{end}}}}' {self.CONTAINER_NAME} 2>/dev/null"
|
|
)
|
|
ip = out.strip().split()[0] if out.strip() else ''
|
|
return ip
|
|
|
|
def _detect_mode(self):
|
|
"""Return 'replace' or 'sidebyside' based on the running container's IP.
|
|
Returns None if not detectable (container not running)."""
|
|
ip = self._container_ip()
|
|
if ip == self.REPLACE_IP:
|
|
return 'replace'
|
|
if ip == self.SIDEBYSIDE_IP:
|
|
return 'sidebyside'
|
|
return None
|
|
|
|
def _exposed_web_port(self):
|
|
"""Reads back the host->container port mapping for the web UI port,
|
|
so the panel can show the correct admin URL after install."""
|
|
out, _, _ = self.ssh.run_sudo_command(
|
|
f"docker port {self.CONTAINER_NAME} 3000/tcp 2>/dev/null"
|
|
)
|
|
if not out.strip():
|
|
return None
|
|
# output like "0.0.0.0:3000" — take the last colon-separated chunk
|
|
last = out.strip().split('\n')[0].split(':')[-1].strip()
|
|
try:
|
|
return int(last)
|
|
except ValueError:
|
|
return None
|
|
|
|
def get_server_status(self, protocol_type='adguard'):
|
|
exists = self.check_protocol_installed()
|
|
running = self.check_container_running()
|
|
mode = self._detect_mode() if running else None
|
|
ip = self._container_ip() if running else ''
|
|
exposed_port = self._exposed_web_port() if running else None
|
|
# When the web UI is not bound to the host the user still needs an
|
|
# admin URL (reachable via VPN) — fall back to the default :3000 the
|
|
# container listens on inside the docker network.
|
|
return {
|
|
'container_exists': exists,
|
|
'container_running': running,
|
|
'mode': mode,
|
|
'internal_ip': ip,
|
|
'web_port': exposed_port or self.DEFAULT_WEB_PORT,
|
|
'web_exposed': exposed_port is not None,
|
|
'port': self.DEFAULT_DNS_PORT,
|
|
'protocol': protocol_type,
|
|
}
|
|
|
|
# ===================== INSTALL / REMOVE =====================
|
|
|
|
def _ensure_network(self):
|
|
self.ssh.run_sudo_command(
|
|
f"docker network ls | grep -q {self.NETWORK_NAME} || "
|
|
f"docker network create --subnet {self.NETWORK_SUBNET} {self.NETWORK_NAME}"
|
|
)
|
|
|
|
def install_protocol(
|
|
self,
|
|
protocol_type='adguard',
|
|
mode='sidebyside',
|
|
web_port=None,
|
|
expose_web=False,
|
|
dns_port=None,
|
|
dot_port=None,
|
|
doh_port=None,
|
|
expose_dns=False,
|
|
expose_dot=False,
|
|
expose_doh=False,
|
|
):
|
|
if not self.check_docker_installed():
|
|
return {'status': 'error', 'message': 'Docker not installed'}
|
|
if mode not in ('replace', 'sidebyside'):
|
|
return {'status': 'error', 'message': f"Invalid mode '{mode}'"}
|
|
|
|
web_port = int(web_port or self.DEFAULT_WEB_PORT)
|
|
dns_port = int(dns_port or self.DEFAULT_DNS_PORT)
|
|
dot_port = int(dot_port or self.DEFAULT_DOT_PORT)
|
|
doh_port = int(doh_port or self.DEFAULT_DOH_PORT)
|
|
|
|
# Persistent volumes — without these the AdGuard setup wizard would have
|
|
# to re-run on every container recreate.
|
|
self.ssh.run_sudo_command(f"mkdir -p {self.HOST_DIR}/work {self.HOST_DIR}/conf")
|
|
|
|
self._ensure_network()
|
|
|
|
# Replace mode: detach + remove AmneziaDNS so we can claim its IP.
|
|
if mode == 'replace':
|
|
self.ssh.run_sudo_command(
|
|
f"docker network disconnect {self.NETWORK_NAME} amnezia-dns 2>/dev/null || true"
|
|
)
|
|
self.ssh.run_sudo_command("docker stop amnezia-dns 2>/dev/null || true")
|
|
self.ssh.run_sudo_command("docker rm -fv amnezia-dns 2>/dev/null || true")
|
|
target_ip = self.REPLACE_IP
|
|
else:
|
|
target_ip = self.SIDEBYSIDE_IP
|
|
|
|
if self.check_protocol_installed():
|
|
self.ssh.run_sudo_command(f"docker stop {self.CONTAINER_NAME} 2>/dev/null || true")
|
|
self.ssh.run_sudo_command(f"docker rm -fv {self.CONTAINER_NAME} 2>/dev/null || true")
|
|
|
|
self.ssh.run_sudo_command(f"docker pull {self.IMAGE_NAME}")
|
|
|
|
# Build port mapping. By default ports are reachable only inside
|
|
# `amnezia-dns-net` (so VPN clients hit them via the static IP).
|
|
# Optional `expose_*` flags add host port mappings for direct access.
|
|
ports = []
|
|
if expose_web:
|
|
ports.append(f"-p {web_port}:3000/tcp")
|
|
if expose_dns:
|
|
ports.append(f"-p {dns_port}:53/tcp")
|
|
ports.append(f"-p {dns_port}:53/udp")
|
|
if expose_dot:
|
|
ports.append(f"-p {dot_port}:853/tcp")
|
|
if expose_doh:
|
|
ports.append(f"-p {doh_port}:443/tcp")
|
|
ports_str = ' '.join(ports)
|
|
|
|
run_cmd = (
|
|
f"docker run -d --name {self.CONTAINER_NAME} --restart always "
|
|
f"--network {self.NETWORK_NAME} --ip {target_ip} "
|
|
f"-v {self.HOST_DIR}/work:/opt/adguardhome/work "
|
|
f"-v {self.HOST_DIR}/conf:/opt/adguardhome/conf "
|
|
f"{ports_str} "
|
|
f"{self.IMAGE_NAME}"
|
|
)
|
|
_, err, code = self.ssh.run_sudo_command(run_cmd)
|
|
if code != 0:
|
|
return {'status': 'error', 'message': f'Failed to start container: {err}'}
|
|
|
|
# Re-attach known VPN containers to the DNS network so they can reach
|
|
# AdGuard at target_ip (mirrors what dns_manager.py does on install).
|
|
for c in ('amnezia-awg', 'amnezia-awg2', 'amnezia-awg-legacy', 'amnezia-xray', 'amnezia-wireguard', 'telemt'):
|
|
self.ssh.run_sudo_command(
|
|
f"docker ps --format '{{{{.Names}}}}' | grep -q '^{c}$' && "
|
|
f"docker network connect {self.NETWORK_NAME} {c} 2>/dev/null || true"
|
|
)
|
|
|
|
url_host = self.ssh.host if expose_web else target_ip
|
|
admin_url = f"http://{url_host}:{web_port}"
|
|
return {
|
|
'status': 'success',
|
|
'protocol': 'adguard',
|
|
'mode': mode,
|
|
'internal_ip': target_ip,
|
|
'web_port': web_port,
|
|
'expose_web': bool(expose_web),
|
|
'admin_url': admin_url,
|
|
'message': 'AdGuard Home installed. Complete the setup wizard via the web UI.',
|
|
'log': [
|
|
f"AdGuard Home installed in '{mode}' mode",
|
|
f"Internal IP: {target_ip}",
|
|
f"Admin UI: {admin_url}" + ("" if expose_web else " (VPN-only — connect via VPN to reach it)"),
|
|
'Open the URL above to run the AdGuard setup wizard.',
|
|
],
|
|
}
|
|
|
|
def remove_container(self, protocol_type='adguard'):
|
|
self.ssh.run_sudo_command(f"docker stop {self.CONTAINER_NAME} || true")
|
|
self.ssh.run_sudo_command(f"docker rm -fv {self.CONTAINER_NAME} || true")
|
|
self.ssh.run_sudo_command(f"rm -rf {self.HOST_DIR}")
|
|
return True
|