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
209 lines
8.2 KiB
Python
209 lines
8.2 KiB
Python
"""
|
|
SOCKS5 Proxy Manager — runs 3proxy in a Docker container, modelled after the
|
|
official Amnezia client install (client/server_scripts/socks5_proxy/). Holds a
|
|
single user (port + username + password); credentials can be edited later from
|
|
the panel via update_credentials().
|
|
"""
|
|
|
|
import logging
|
|
import secrets
|
|
import string
|
|
import re
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _generate_password(length=16):
|
|
alphabet = string.ascii_letters + string.digits
|
|
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
|
|
|
|
|
class Socks5Manager:
|
|
PROTOCOL = 'socks5'
|
|
CONTAINER_NAME = 'amnezia-socks5proxy'
|
|
IMAGE_NAME = '3proxy/3proxy:0.9.5'
|
|
CONFIG_DIR = '/opt/amnezia/socks5proxy'
|
|
CONFIG_PATH = '/usr/local/3proxy/conf/3proxy.cfg'
|
|
|
|
DEFAULT_PORT = 38080
|
|
DEFAULT_USERNAME = 'proxy_user'
|
|
|
|
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='socks5'):
|
|
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 get_server_status(self, protocol_type='socks5'):
|
|
exists = self.check_protocol_installed()
|
|
running = self.check_container_running()
|
|
creds = self.get_credentials() if exists else {}
|
|
return {
|
|
'container_exists': exists,
|
|
'container_running': running,
|
|
'port': creds.get('port'),
|
|
'username': creds.get('username'),
|
|
'protocol': protocol_type,
|
|
}
|
|
|
|
# ===================== CONFIG I/O =====================
|
|
|
|
def _build_config(self, username, password, port):
|
|
# Mirrors client/server_scripts/socks5_proxy/configure_container.sh.
|
|
# 'auth strong' enforces username/password on every connection;
|
|
# 'allow {user}' restricts the ACL to our single user only.
|
|
return (
|
|
"#!/bin/3proxy\n"
|
|
f"config {self.CONFIG_PATH}\n"
|
|
"timeouts 1 5 30 60 180 1800 15 60\n"
|
|
f"users {username}:CL:{password}\n"
|
|
"log /usr/local/3proxy/logs/3proxy.log\n"
|
|
"auth strong\n"
|
|
f"allow {username}\n"
|
|
f"socks -p{int(port)}\n"
|
|
)
|
|
|
|
def _read_config(self):
|
|
out, _, code = self.ssh.run_sudo_command(
|
|
f"docker exec {self.CONTAINER_NAME} cat {self.CONFIG_PATH} 2>/dev/null"
|
|
)
|
|
if code != 0 or not out.strip():
|
|
out, _, code = self.ssh.run_sudo_command(
|
|
f"cat {self.CONFIG_DIR}/3proxy.cfg 2>/dev/null"
|
|
)
|
|
if code != 0 or not out.strip():
|
|
return ''
|
|
return out
|
|
|
|
def _write_config(self, config_text):
|
|
# Write to host first (so we have a stable copy outside the container),
|
|
# then docker cp into the running container at the path 3proxy expects.
|
|
self.ssh.run_sudo_command(f"mkdir -p {self.CONFIG_DIR}")
|
|
self.ssh.upload_file_sudo(config_text, f"{self.CONFIG_DIR}/3proxy.cfg")
|
|
self.ssh.run_sudo_command(
|
|
f"docker cp {self.CONFIG_DIR}/3proxy.cfg {self.CONTAINER_NAME}:{self.CONFIG_PATH} 2>/dev/null || true"
|
|
)
|
|
|
|
def _parse_credentials(self, config_text):
|
|
creds = {'port': None, 'username': None, 'password': None}
|
|
if not config_text:
|
|
return creds
|
|
m_user = re.search(r'^\s*users\s+([^:\s]+):CL:(\S+)', config_text, re.MULTILINE)
|
|
if m_user:
|
|
creds['username'] = m_user.group(1)
|
|
creds['password'] = m_user.group(2)
|
|
m_port = re.search(r'^\s*socks\s+-p(\d+)', config_text, re.MULTILINE)
|
|
if m_port:
|
|
creds['port'] = int(m_port.group(1))
|
|
return creds
|
|
|
|
def get_credentials(self):
|
|
return self._parse_credentials(self._read_config())
|
|
|
|
# ===================== INSTALL / UPDATE / REMOVE =====================
|
|
|
|
def install_protocol(self, protocol_type='socks5', port=None, username=None, password=None):
|
|
if not self.check_docker_installed():
|
|
return {'status': 'error', 'message': 'Docker not installed'}
|
|
|
|
port = int(port or self.DEFAULT_PORT)
|
|
username = (username or self.DEFAULT_USERNAME).strip() or self.DEFAULT_USERNAME
|
|
password = (password or _generate_password()).strip() or _generate_password()
|
|
|
|
# Pull image (idempotent — fast no-op if cached)
|
|
self.ssh.run_sudo_command(f"docker pull {self.IMAGE_NAME}")
|
|
|
|
# Wipe any prior install, including the bind-mounted config dir, before
|
|
# writing a fresh config — leftover state would leak old credentials.
|
|
if self.check_protocol_installed():
|
|
self.remove_container()
|
|
|
|
config_text = self._build_config(username, password, port)
|
|
self.ssh.run_sudo_command(f"mkdir -p {self.CONFIG_DIR}")
|
|
self.ssh.upload_file_sudo(config_text, f"{self.CONFIG_DIR}/3proxy.cfg")
|
|
|
|
# Bind-mount our config in place of the image's default. 3proxy reads
|
|
# /usr/local/3proxy/conf/3proxy.cfg by convention.
|
|
run_cmd = (
|
|
f"docker run -d --restart always "
|
|
f"--name {self.CONTAINER_NAME} "
|
|
f"-p {port}:{port}/tcp "
|
|
f"-v {self.CONFIG_DIR}/3proxy.cfg:{self.CONFIG_PATH}:ro "
|
|
f"{self.IMAGE_NAME} {self.CONFIG_PATH}"
|
|
)
|
|
_, err, code = self.ssh.run_sudo_command(run_cmd)
|
|
if code != 0:
|
|
return {'status': 'error', 'message': f'Failed to start container: {err}'}
|
|
|
|
return {
|
|
'status': 'success',
|
|
'protocol': 'socks5',
|
|
'port': port,
|
|
'username': username,
|
|
'password': password,
|
|
'message': 'SOCKS5 proxy installed',
|
|
'log': [
|
|
f'SOCKS5 proxy listening on port {port}/TCP',
|
|
f'Username: {username}',
|
|
f'Password: {password}',
|
|
'Save these credentials — the password can also be viewed later via "Change settings".',
|
|
],
|
|
}
|
|
|
|
def update_credentials(self, port=None, username=None, password=None):
|
|
"""Apply new connection settings: regenerates the config file and
|
|
restarts the container so the new port mapping takes effect."""
|
|
if not self.check_protocol_installed():
|
|
return {'status': 'error', 'message': 'SOCKS5 not installed'}
|
|
|
|
current = self.get_credentials()
|
|
new_port = int(port if port is not None else (current.get('port') or self.DEFAULT_PORT))
|
|
new_user = (username or current.get('username') or self.DEFAULT_USERNAME).strip()
|
|
new_pass = (password or current.get('password') or _generate_password()).strip()
|
|
|
|
old_port = current.get('port')
|
|
|
|
# If the port changed we must recreate the container — `docker run -p`
|
|
# mappings are immutable on existing containers.
|
|
if old_port and new_port != old_port:
|
|
return self.install_protocol(
|
|
port=new_port, username=new_user, password=new_pass
|
|
)
|
|
|
|
config_text = self._build_config(new_user, new_pass, new_port)
|
|
self._write_config(config_text)
|
|
self.ssh.run_sudo_command(f"docker restart {self.CONTAINER_NAME}")
|
|
|
|
return {
|
|
'status': 'success',
|
|
'port': new_port,
|
|
'username': new_user,
|
|
'password': new_pass,
|
|
}
|
|
|
|
def remove_container(self, protocol_type='socks5'):
|
|
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.CONFIG_DIR}")
|
|
return True
|