diff --git a/README.md b/README.md index 1f45524..bb8b160 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Amnezia Web Panel -A modern, high-performance web interface for managing **AmneziaWG**, **Classic WireGuard**, **Xray (XTLS-Reality)**, **Hysteria 2**, **Telemt (Telegram MTProxy)**, **Cloudflare WARP**, **AmneziaDNS**, **AdGuard Home**, **SOCKS5**, and **NGINX + Let's Encrypt** services on remote Ubuntu servers — from a single dashboard. Designed to provide a premium user experience with robust administrative capabilities. +A modern, high-performance web interface for managing **AmneziaWG**, **Classic WireGuard**, **Xray (XTLS-Reality)**, **Hysteria 2**, **NaiveProxy**, **Telemt (Telegram MTProxy)**, **Cloudflare WARP**, **AmneziaDNS**, **AdGuard Home**, **SOCKS5**, and **NGINX + Let's Encrypt** services on remote Ubuntu servers — from a single dashboard. Designed to provide a premium user experience with robust administrative capabilities. > ### 🔄 Compatibility with Official Amnezia Client > @@ -62,7 +62,8 @@ Configuration panel for system parameters and preferences: * **AmneziaWG (AWG / AWG 2.0 / AWG Legacy)**: Advanced WireGuard-based protocol with S3/S4 obfuscation to bypass deep packet inspection (DPI). Three coexisting variants — modern AWG 2.0 with full junk-packet masking, and a legacy variant for older clients. * **Classic WireGuard**: Standard, high-performance WireGuard protocol for unmatched speed and broad device compatibility with traffic monitoring support. * **Xray (XTLS-Reality)**: Stealthy protocol that masks VPN traffic as standard HTTPS browsing. Pinned to **Xray-core v26.x**; transparently reads both the **panel layout** (`meta.json` + `clientsTable.json`) and the **native Amnezia client layout** (`xray_*.key` files + `clientsTable`), so a node first installed via the official mobile/desktop app can be attached to the panel without re-installation. - * **Hysteria 2**: QUIC/HTTP3 proxy from [apernet/hysteria](https://github.com/apernet/hysteria) — Let's Encrypt TLS, salamander obfuscation, password auth, `hy2://` share links. + * **Hysteria 2**: QUIC/HTTP3 proxy from [apernet/hysteria](https://github.com/apernet/hysteria) via `tobyxdd/hysteria:v2` — Let's Encrypt TLS, salamander obfuscation, password auth, `hy2://` share links. Per-server SSL domain/email defaults, renew from settings; install requires free TCP **80** and **443**. + * **NaiveProxy**: HTTPS/HTTP2 camouflage proxy via [klzgrad/naiveproxy](https://github.com/klzgrad/naiveproxy) (Caddy + [klzgrad/forwardproxy](https://github.com/klzgrad/forwardproxy) naïve fork). ACME TLS on the domain, per-client basic auth, `naive+https://` share links. Requires free TCP **80** and **443**. * **Telemt (Telegram MTProxy)**: High-performance Telegram MTProxy with TLS emulation and comprehensive management (quotas, IP limits, real-time session tracking). Robust install path that auto-configures Docker's official apt/yum repository when needed. * **Cloudflare WARP**: Add and manage WARP-powered connectivity from the panel for routing and network flexibility. * **🛠 Services**: @@ -72,13 +73,14 @@ Configuration panel for system parameters and preferences: * **NGINX + Let's Encrypt**: Reverse-proxy and HTTPS automation with certificate management for secure public endpoints. * **⚙️ Core Server Management**: * **Add / Edit / Delete / Reorder** server entries — drag-and-drop reorder updates `server_id` references in saved connections automatically. + * **Per-server SSL defaults**: optional domain + Let's Encrypt email on each server, pre-filled when installing Hysteria or NGINX. * **Live ping indicator** next to each server name — non-blocking TCP-connect probe to the SSH port, runs on the asyncio loop in parallel for all servers. * **Clear server** wipes every Amnezia-related container, image and `/opt/amnezia` directory in a single sudo script — works for any current or future `amnezia-*` protocol. * **Reboot** the server directly from the UI. * Strictly concurrent protocol status polling — all supported protocols/services checked in parallel for immediate feedback. * **Asynchronous Processing**: Resilient, non-blocking background architecture prevents the UI panel from freezing, even if remote endpoints hang. * **🧩 Marketplace & Templates**: - * Market templates provide quick presets for installing and configuring supported protocols and services. + * Market templates provide quick presets for installing and configuring supported protocols and services (including **Hysteria 2**). * Multi-protocol management lets you run and control multiple protocol instances on the same server. * **🌐 Internationalization (i18n)**: * Full support for **English**, **Russian**, **French**, **Chinese**, and **Persian**. @@ -220,11 +222,25 @@ GitHub Actions workflows in `.github/workflows/`: ## 📋 Fix / changelog (this fork) +### v2.0.0-beta +* **NaiveProxy** (Beta): install [klzgrad/naiveproxy](https://github.com/klzgrad/naiveproxy) server via Caddy + naïve [forwardproxy](https://github.com/klzgrad/forwardproxy) — domain TLS (ACME), per-client basic auth, `naive+https://` share links. +* Marketplace / server page / users / invites / Telegram / backups include NaiveProxy. +* Install warns that free TCP **80** and **443** are required; uses per-server SSL domain/email defaults when set. +* Official prebuilt Caddy on amd64; xcaddy build path for other arches. +* Version line bumped to **2.0 Beta** (`v2.0.0-beta`). + +### v1.8.0 +* **Hysteria 2** fully wired in marketplace, server page, users/invites, Telegram bot, and backups. +* **Per-server SSL defaults**: set domain + Let's Encrypt email when adding/editing a server; values pre-fill Hysteria (and NGINX) install forms. +* **SSL renew**: change domain or use “Renew SSL” in Hysteria settings to re-issue the certificate (needs free TCP 80). +* **Install warning**: free TCP ports **80** and **443** required for stable Let's Encrypt / HTTPS-QUIC operation. +* Official image `tobyxdd/hysteria:v2`, salamander obfs, password auth, `hy2://` share links, selectable UDP listen port. + ### v1.7.0 * **Clearer create flow**: pick **server first**, then **protocol** (WireGuard, AmneziaWG 2.0, …) — invites, guest access, and user connections no longer dump everything into one messy list. -* Protocol titles are human-readable and ordered (AWG 2.0 → AWG → Legacy → WireGuard → Xray → Telemt). +* Protocol titles are human-readable and ordered (AWG 2.0 → AWG → Legacy → WireGuard → Xray → Telemt → Hysteria). * 3x-ui is a separate “server” choice; VLESS inbounds still load from that panel’s API. -* **Hysteria 2** restored ([apernet/hysteria](https://github.com/apernet/hysteria)) via official `tobyxdd/hysteria:v2` image, Let’s Encrypt domain TLS, selectable UDP port, `hy2://` client links. +* **Hysteria 2** manager restored ([apernet/hysteria](https://github.com/apernet/hysteria)) — see **v1.8.0** for full panel integration. ### v1.6.0 * **3x-ui multi-panel**: register several 3x-ui servers in Settings; pick a panel and load VLESS inbounds over its API when creating users/invites (share link comes from 3x-ui). @@ -297,6 +313,7 @@ web-panel/ │ ├── wireguard_manager.py # Classic WireGuard │ ├── xray_manager.py # Xray-core (VLESS-Reality) │ ├── hysteria_manager.py # Hysteria 2 (apernet/hysteria) +│ ├── naiveproxy_manager.py # NaiveProxy (Caddy + klzgrad/forwardproxy) │ ├── telemt_manager.py # Telegram MTProxy │ ├── dns_manager.py # AmneziaDNS (Unbound) │ ├── adguard_manager.py # AdGuard Home diff --git a/app.py b/app.py index e03762c..f66b242 100644 --- a/app.py +++ b/app.py @@ -101,7 +101,7 @@ else: application_path = os.path.dirname(__file__) DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export -CURRENT_VERSION = "v1.7.0" +CURRENT_VERSION = "v2.0.0-beta" RELEASES_REPO_URL = "https://git.evilfox.cc/test2/Amnezia-Web-Panel-main" RELEASES_API_LATEST = "https://git.evilfox.cc/api/v1/repos/test2/Amnezia-Web-Panel-main/releases/latest" BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin')) @@ -843,7 +843,7 @@ async def wait_for_tunnel_url(provider: str, seconds: int = 20): return get_tunnel_status(provider) -BASE_PROTOCOLS = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'dns', 'wireguard', 'socks5', 'adguard', 'nginx'] +BASE_PROTOCOLS = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'naiveproxy', 'dns', 'wireguard', 'socks5', 'adguard', 'nginx'] MULTI_INSTANCE_PROTOCOLS = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'socks5'} @@ -885,6 +885,7 @@ def protocol_display_name(protocol: str) -> str: 'xray': 'Xray', 'telemt': 'Telemt', 'hysteria': 'Hysteria 2', + 'naiveproxy': 'NaiveProxy', 'dns': 'AmneziaDNS', 'wireguard': 'WireGuard', 'socks5': 'SOCKS5', @@ -905,6 +906,7 @@ def protocol_container_name(protocol: str) -> Optional[str]: 'xray': 'amnezia-xray', 'telemt': 'telemt', 'hysteria': 'amnezia-hysteria', + 'naiveproxy': 'amnezia-naiveproxy', 'dns': 'amnezia-dns', 'wireguard': 'amnezia-wireguard', 'socks5': 'amnezia-socks5proxy', @@ -947,6 +949,9 @@ def get_protocol_manager(ssh, protocol: str): elif base == 'hysteria': from managers.hysteria_manager import HysteriaManager return HysteriaManager(ssh, protocol) + elif base == 'naiveproxy': + from managers.naiveproxy_manager import NaiveProxyManager + return NaiveProxyManager(ssh, protocol) from managers.awg_manager import AWGManager return AWGManager(ssh) @@ -1008,7 +1013,7 @@ def _manager_call(manager, method, protocol, *args, **kwargs): # Protocols that own VPN clients (can list/link connections) -CLIENT_VPN_BASES = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard', 'hysteria', 'xui'} +CLIENT_VPN_BASES = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard', 'hysteria', 'naiveproxy', 'xui'} def generate_vpn_link(config_text): @@ -1718,6 +1723,9 @@ class InstallProtocolRequest(BaseModel): # Hysteria hysteria_domain: Optional[str] = None hysteria_email: Optional[str] = None + # NaiveProxy + naiveproxy_domain: Optional[str] = None + naiveproxy_email: Optional[str] = None class Socks5SettingsRequest(BaseModel): @@ -2852,6 +2860,12 @@ async def api_check_server(request: Request, server_id: int): merged[key] = db_proto[key] if db_proto.get('port') and not merged.get('port'): merged['port'] = db_proto['port'] + if protocol_base(proto) == 'naiveproxy': + for key in ('domain', 'email'): + if db_proto.get(key) not in (None, '') and not merged.get(key): + merged[key] = db_proto[key] + if db_proto.get('port') and not merged.get('port'): + merged['port'] = db_proto['port'] return merged def should_preserve_saved_protocol(proto, result=None, err=None): @@ -3010,6 +3024,13 @@ async def api_install_protocol(request: Request, server_id: int, req: InstallPro domain=req.hysteria_domain, email=req.hysteria_email, ) + elif install_base == 'naiveproxy': + result = manager.install_protocol( + protocol_type=install_protocol, + port=req.port, + domain=req.naiveproxy_domain, + email=req.naiveproxy_email, + ) else: result = manager.install_protocol(install_protocol, port=req.port) @@ -3048,6 +3069,17 @@ async def api_install_protocol(request: Request, server_id: int, req: InstallPro proto_record['email'] = result.get('email') if result.get('port'): proto_record['port'] = str(result['port']) + if install_base == 'naiveproxy': + info = server.setdefault('server_info', {}) + if req.naiveproxy_domain: + info['ssl_domain'] = (req.naiveproxy_domain or '').strip().lower() + if req.naiveproxy_email: + info['ssl_email'] = (req.naiveproxy_email or '').strip() + save_data(data) + proto_record['domain'] = result.get('domain') + proto_record['email'] = result.get('email') + if result.get('port'): + proto_record['port'] = str(result['port']) proto_record['base_protocol'] = install_base proto_record['instance'] = protocol_instance(install_protocol) proto_record['display_name'] = protocol_display_name(install_protocol) @@ -3233,6 +3265,7 @@ CONTAINER_NAMES = { 'xray': 'amnezia-xray', 'telemt': 'telemt', 'hysteria': 'amnezia-hysteria', + 'naiveproxy': 'amnezia-naiveproxy', 'dns': 'amnezia-dns', 'wireguard': 'amnezia-wireguard', 'socks5': 'amnezia-socks5proxy', @@ -3733,6 +3766,10 @@ async def api_server_config(request: Request, server_id: int, req: ProtocolReque from managers.hysteria_manager import HysteriaManager mgr = HysteriaManager(ssh, req.protocol) config = mgr.get_server_config(req.protocol) + elif protocol_base(req.protocol) == 'naiveproxy': + from managers.naiveproxy_manager import NaiveProxyManager + mgr = NaiveProxyManager(ssh, req.protocol) + config = mgr.get_server_config(req.protocol) else: mgr = AWGManager(ssh) config = mgr._get_server_config(req.protocol) @@ -3781,6 +3818,10 @@ async def api_server_config_save(request: Request, server_id: int, req: ServerCo from managers.hysteria_manager import HysteriaManager mgr = HysteriaManager(ssh, req.protocol) mgr.save_server_config(req.protocol, req.config) + elif protocol_base(req.protocol) == 'naiveproxy': + from managers.naiveproxy_manager import NaiveProxyManager + mgr = NaiveProxyManager(ssh, req.protocol) + mgr.save_server_config(req.protocol, req.config) else: mgr = AWGManager(ssh) mgr.save_server_config(req.protocol, req.config) diff --git a/managers/backup_manager.py b/managers/backup_manager.py index 7511ddc..f2c0959 100644 --- a/managers/backup_manager.py +++ b/managers/backup_manager.py @@ -66,6 +66,10 @@ class BackupManager: remote_dir = inst_path('/opt/amnezia/hysteria') paths['host'] = [remote_dir] paths['container'] = ['/etc/hysteria'] + elif base == 'naiveproxy': + remote_dir = inst_path('/opt/amnezia/naiveproxy') + paths['host'] = [remote_dir] + paths['container'] = ['/etc/caddy'] elif base == 'dns': paths['host'] = ['/opt/amnezia/dns'] paths['container'] = ['/opt/amnezia/dns'] diff --git a/managers/naiveproxy_manager.py b/managers/naiveproxy_manager.py new file mode 100644 index 0000000..41a1518 --- /dev/null +++ b/managers/naiveproxy_manager.py @@ -0,0 +1,594 @@ +""" +NaiveProxy manager — Caddy + klzgrad/forwardproxy (naive fork). + +Server side is the naïve Caddy forward_proxy from: + https://github.com/klzgrad/naiveproxy + https://github.com/klzgrad/forwardproxy + +Uses the official prebuilt Caddy binary on amd64, or builds via xcaddy on other +arches. Clients authenticate with HTTP basic auth; share links are +naive+https://user:pass@domain URIs (NekoRay / sing-box compatible). +Requires free TCP 80 (ACME) and 443 (HTTPS / HTTP2 proxy). +""" + +from __future__ import annotations + +import json +import logging +import re +import secrets +import shlex +import string +import time +from urllib.parse import quote + +logger = logging.getLogger(__name__) + + +def _q(value): + return shlex.quote(str(value)) + + +def _rand_token(length=16): + alphabet = string.ascii_lowercase + string.digits + return ''.join(secrets.choice(alphabet) for _ in range(length)) + + +class NaiveProxyManager: + PROTOCOL = 'naiveproxy' + CONTAINER_NAME = 'amnezia-naiveproxy' + IMAGE_NAME = 'amnezia-naiveproxy:local' + BASE_DIR = '/opt/amnezia/naiveproxy' + DEFAULT_PORT = 443 + # Official Caddy+forwardproxy naive build (linux amd64) + CADDY_RELEASE = 'v2.11.2-naive' + CADDY_URL = ( + 'https://github.com/klzgrad/forwardproxy/releases/download/' + f'{CADDY_RELEASE}/caddy-forwardproxy-naive.tar.xz' + ) + + def __init__(self, ssh, protocol='naiveproxy'): + self.ssh = ssh + self.protocol = protocol or self.PROTOCOL + self.container_name = self.CONTAINER_NAME + self.base_dir = self.BASE_DIR + self.caddyfile_path = f'{self.base_dir}/Caddyfile' + self.clients_path = f'{self.base_dir}/clients.json' + self.meta_path = f'{self.base_dir}/metadata.json' + self.html_dir = f'{self.base_dir}/html' + self.bin_dir = f'{self.base_dir}/bin' + self.data_dir = f'{self.base_dir}/caddy-data' + self.config_dir = f'{self.base_dir}/caddy-config' + + # ===================== 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=None): + 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, protocol_type=None): + out, _, _ = self.ssh.run_sudo_command( + f"docker ps --filter name=^{self.CONTAINER_NAME}$ --format '{{{{.Status}}}}'" + ) + return 'Up' in out + + def get_logs(self, protocol_type=None, tail=200): + tail = max(20, min(int(tail or 200), 2000)) + out, err, code = self.ssh.run_sudo_command( + f"docker logs --tail {tail} --timestamps {self.CONTAINER_NAME} 2>&1", + timeout=30, + ) + text = (out or err or '').strip() + if code != 0 and not text: + text = f'(no logs: exit {code})' + return text + + def get_container_diagnostics(self, protocol_type=None): + fmt = ( + '{{.State.Status}}|{{.State.Running}}|{{.State.ExitCode}}|' + '{{.State.OOMKilled}}|{{.State.Error}}|{{.State.FinishedAt}}|' + '{{.State.StartedAt}}' + ) + out, _, code = self.ssh.run_sudo_command( + f"docker inspect -f '{fmt}' {self.CONTAINER_NAME} 2>/dev/null" + ) + diag = { + 'status': 'unknown', + 'running': False, + 'exit_code': None, + 'error_summary': '', + 'recent_logs': '', + } + if code == 0 and out.strip(): + parts = out.strip().split('|') + while len(parts) < 7: + parts.append('') + diag['status'] = parts[0] + diag['running'] = parts[1].lower() == 'true' + try: + diag['exit_code'] = int(parts[2]) + except ValueError: + diag['exit_code'] = parts[2] + if parts[3].lower() == 'true': + diag['error_summary'] = 'OOM killed' + elif parts[4]: + diag['error_summary'] = parts[4] + elif not diag['running'] and diag['exit_code'] not in (0, '0', None, ''): + diag['error_summary'] = f'Exited with code {diag["exit_code"]}' + diag['recent_logs'] = self.get_logs(protocol_type, tail=80) + if not diag['error_summary'] and not diag['running']: + logs = diag['recent_logs'].lower() + if 'address already in use' in logs or 'bind:' in logs: + diag['error_summary'] = ( + 'TCP 80/443 already in use — free them or stop conflicting services' + ) + return diag + + def get_server_status(self, protocol_type=None): + protocol_type = protocol_type or self.protocol + exists = self.check_protocol_installed(protocol_type) + running = self.check_container_running(protocol_type) if exists else False + meta = self._read_metadata() if exists else {} + clients = self._read_clients() if exists else [] + port = int(meta.get('port') or self.DEFAULT_PORT) + return { + 'container_exists': exists, + 'container_running': running, + 'port': port, + 'domain': meta.get('domain') or '', + 'email': meta.get('email') or '', + 'clients_count': len(clients), + 'protocol': protocol_type, + 'base_protocol': self.PROTOCOL, + 'instance': 1, + 'container_name': self.CONTAINER_NAME, + } + + # ===================== IO HELPERS ===================== + + def _validate_domain(self, domain): + domain = (domain or '').strip().lower() + if not domain or len(domain) > 253: + raise ValueError('Domain is required') + if not re.match(r'^(?!-)[a-z0-9.-]+(?/dev/null") + return out if code == 0 else '' + + def _write_file(self, path, content): + # Write via base64 to avoid shell escaping issues + import base64 + b64 = base64.b64encode((content or '').encode('utf-8')).decode('ascii') + script = ( + f"mkdir -p $(dirname {_q(path)}) && " + f"echo {_q(b64)} | base64 -d > {_q(path)}" + ) + out, err, code = self.ssh.run_sudo_command(f"sh -c {_q(script)}", timeout=30) + if code != 0: + raise RuntimeError(f'Failed to write {path}: {err or out}') + + def _read_metadata(self): + raw = self._read_file(self.meta_path).strip() + if not raw: + return {} + try: + data = json.loads(raw) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + def _write_metadata(self, meta): + self._write_file(self.meta_path, json.dumps(meta, indent=2, ensure_ascii=False)) + + def _read_clients(self): + raw = self._read_file(self.clients_path).strip() + if not raw: + return [] + try: + data = json.loads(raw) + return data if isinstance(data, list) else [] + except Exception: + return [] + + def _write_clients(self, clients): + self._write_file(self.clients_path, json.dumps(clients, indent=2, ensure_ascii=False)) + + def _caddy_auth_lines(self, clients): + lines = [] + for c in clients: + if c.get('enabled', True) is False: + continue + user = (c.get('username') or '').strip() + password = (c.get('password') or '').strip() + if not user or not password: + continue + # Credentials are alphanumeric from our generator — safe in Caddyfile + lines.append(f' basic_auth {user} {password}') + return lines + + def _build_caddyfile(self, domain, email, clients, probe_path): + auth_lines = self._caddy_auth_lines(clients) + if not auth_lines: + # Keep a non-shared bootstrap user so the proxy is never open + auth_lines = [f' basic_auth bootstrap {_rand_token(24)}'] + auth_block = '\n'.join(auth_lines) + probe = (probe_path or _rand_token(12)).strip('/') + return f"""{{ + order forward_proxy before file_server + log {{ + exclude http.log.error + }} +}} +:443, {domain} {{ + tls {email} + encode + route {{ + forward_proxy {{ +{auth_block} + hide_ip + hide_via + probe_resistance {probe} + }} + file_server {{ + root /var/www/html + }} + }} +}} +""" + + def _write_config_from_clients(self): + meta = self._read_metadata() + domain = meta.get('domain') or '' + email = meta.get('email') or '' + if not domain or not email: + raise RuntimeError('Domain and email are required in metadata') + clients = self._read_clients() + probe = meta.get('probe_resistance') or _rand_token(12) + if not meta.get('probe_resistance'): + meta['probe_resistance'] = probe + self._write_metadata(meta) + text = self._build_caddyfile(domain, email, clients, probe) + self._write_file(self.caddyfile_path, text) + + def _ensure_placeholder_site(self): + index = f'{self.html_dir}/index.html' + existing = self._read_file(index) + if existing.strip(): + return + html = ( + '' + 'Welcome

It works

\n' + ) + self._write_file(index, html) + + def _host_arch(self): + out, _, _ = self.ssh.run_command("uname -m") + return (out or '').strip().lower() + + def _prepare_caddy_binary(self, log): + """Download official amd64 build or compile with xcaddy for other arches.""" + self.ssh.run_sudo_command(f"mkdir -p {_q(self.bin_dir)}") + arch = self._host_arch() + caddy_path = f'{self.bin_dir}/caddy' + + if arch in ('x86_64', 'amd64'): + script = f""" +set -e +cd {_q(self.bin_dir)} +curl -fsSL -o caddy-forwardproxy-naive.tar.xz {_q(self.CADDY_URL)} +tar -xf caddy-forwardproxy-naive.tar.xz +if [ -f caddy-forwardproxy-naive/caddy ]; then + cp -f caddy-forwardproxy-naive/caddy ./caddy +elif [ -f caddy ]; then + true +else + echo "caddy binary not found in archive" >&2 + exit 1 +fi +chmod +x ./caddy +./caddy version +""" + out, err, code = self.ssh.run_sudo_command(f"bash -lc {_q(script)}", timeout=300) + if code != 0: + raise RuntimeError(f'Failed to download NaiveProxy Caddy: {err or out}') + log.append(f'Downloaded official Caddy+forwardproxy ({self.CADDY_RELEASE})') + return caddy_path + + # arm64 / other: build with xcaddy inside golang image + build_script = f""" +set -e +docker pull golang:1.22-bookworm +docker run --rm -v {_q(self.bin_dir)}:/out -w /tmp golang:1.22-bookworm bash -lc ' +set -e +go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest +/go/bin/xcaddy build --with github.com/caddyserver/forwardproxy=github.com/klzgrad/forwardproxy@naive +cp -f caddy /out/caddy +chmod +x /out/caddy +' +{_q(caddy_path)} version +""" + out, err, code = self.ssh.run_sudo_command(f"bash -lc {_q(build_script)}", timeout=900) + if code != 0: + raise RuntimeError(f'Failed to build NaiveProxy Caddy for {arch}: {err or out}') + log.append(f'Built Caddy+forwardproxy via xcaddy for {arch}') + return caddy_path + + def _build_docker_image(self, log): + dockerfile = f"""FROM debian:bookworm-slim +RUN apt-get update \\ + && apt-get install -y --no-install-recommends ca-certificates \\ + && rm -rf /var/lib/apt/lists/* +COPY caddy /usr/bin/caddy +RUN chmod +x /usr/bin/caddy +WORKDIR /etc/caddy +EXPOSE 80 443 +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] +""" + self._write_file(f'{self.bin_dir}/Dockerfile', dockerfile) + out, err, code = self.ssh.run_sudo_command( + f"docker build -t {self.IMAGE_NAME} {_q(self.bin_dir)}", + timeout=300, + ) + if code != 0: + raise RuntimeError(f'Failed to build Docker image: {err or out}') + log.append(f'Built image {self.IMAGE_NAME}') + + def _start_container(self): + self.ssh.run_sudo_command( + f"docker rm -fv {self.CONTAINER_NAME} 2>/dev/null || true" + ) + self.ssh.run_sudo_command(f"mkdir -p {_q(self.data_dir)} {_q(self.config_dir)} {_q(self.html_dir)}") + run_cmd = ( + f"docker run -d --name {self.CONTAINER_NAME} --restart unless-stopped " + f"--network host " + f"-v {_q(self.base_dir)}:/etc/caddy " + f"-v {_q(self.html_dir)}:/var/www/html " + f"-v {_q(self.data_dir)}:/data " + f"-v {_q(self.config_dir)}:/config " + f"-e XDG_DATA_HOME=/data " + f"-e XDG_CONFIG_HOME=/config " + f"{self.IMAGE_NAME}" + ) + _, err, code = self.ssh.run_sudo_command(run_cmd, timeout=60) + if code != 0: + raise RuntimeError(f'Failed to start NaiveProxy: {err}') + time.sleep(2.0) + if not self.check_container_running(): + diag = self.get_container_diagnostics() + raise RuntimeError( + diag.get('error_summary') + or 'NaiveProxy container exited — free TCP 80/443 and check logs' + ) + return True + + def _reload_container(self): + try: + self._write_config_from_clients() + # Prefer graceful reload; fall back to recreate + out, _, code = self.ssh.run_sudo_command( + f"docker exec {self.CONTAINER_NAME} caddy reload " + f"--config /etc/caddy/Caddyfile --adapter caddyfile 2>&1", + timeout=30, + ) + if code != 0: + logger.warning('Caddy reload failed (%s), recreating container', out) + self._start_container() + except Exception as e: + logger.warning('NaiveProxy reload failed: %s', e) + self._start_container() + + def _build_share_uri(self, domain, username, password, name, port=443): + user = quote(username or '', safe='') + pw = quote(password or '', safe='') + host = domain or '' + port = int(port or 443) + authority = f'{user}:{pw}@{host}' if port == 443 else f'{user}:{pw}@{host}:{port}' + fragment = quote(name or 'naiveproxy', safe='') + return f'naive+https://{authority}#{fragment}' + + # ===================== INSTALL / REMOVE ===================== + + def install_protocol(self, protocol_type=None, port=None, domain=None, email=None): + protocol_type = protocol_type or self.protocol + if not self.check_docker_installed(): + return {'status': 'error', 'message': 'Docker not installed'} + + try: + domain = self._validate_domain(domain) + email = self._validate_email(email) + except ValueError as e: + return {'status': 'error', 'message': str(e)} + + port = int(port or self.DEFAULT_PORT) + if port not in (443,): + # Caddy ACME + naive typically bind :443; keep simple for beta + port = 443 + + log = [] + self.ssh.run_sudo_command(f"mkdir -p {_q(self.base_dir)} {_q(self.html_dir)} {_q(self.bin_dir)}") + + if self.check_protocol_installed(protocol_type): + self.ssh.run_sudo_command(f"docker rm -fv {self.CONTAINER_NAME} 2>/dev/null || true") + log.append('Removed previous container') + + try: + self._prepare_caddy_binary(log) + self._build_docker_image(log) + except Exception as e: + return {'status': 'error', 'message': str(e), 'log': log} + + self._write_clients([]) + meta = { + 'domain': domain, + 'email': email, + 'port': port, + 'probe_resistance': _rand_token(12), + 'caddy_release': self.CADDY_RELEASE, + } + self._write_metadata(meta) + self._ensure_placeholder_site() + try: + self._write_config_from_clients() + except Exception as e: + return {'status': 'error', 'message': str(e), 'log': log} + log.append('Wrote Caddyfile (forward_proxy + ACME TLS)') + + try: + self._start_container() + except Exception as e: + return {'status': 'error', 'message': str(e), 'log': log} + + log.append( + f'Started {self.CONTAINER_NAME} on TCP 443 (host network). ' + 'Ports 80 and 443 must stay free for ACME and HTTPS.' + ) + return { + 'status': 'success', + 'message': 'NaiveProxy installed', + 'log': log, + 'port': str(port), + 'domain': domain, + 'email': email, + } + + def remove_container(self, protocol_type=None): + self.ssh.run_sudo_command(f"docker rm -fv {self.CONTAINER_NAME} 2>/dev/null || true") + self.ssh.run_sudo_command(f"rm -rf {_q(self.base_dir)}") + return True + + def get_server_config(self, protocol_type=None): + return self._read_file(self.caddyfile_path) + + def save_server_config(self, protocol_type=None, config_text=''): + self._write_file(self.caddyfile_path, config_text or '') + self._reload_container() + return True + + def get_settings(self): + meta = self._read_metadata() + return { + 'port': int(meta.get('port') or self.DEFAULT_PORT), + 'domain': meta.get('domain') or '', + 'email': meta.get('email') or '', + 'protocol': self.protocol, + } + + def update_settings(self, domain=None, email=None): + meta = self._read_metadata() + try: + if domain is not None and str(domain).strip(): + meta['domain'] = self._validate_domain(domain) + if email is not None and str(email).strip(): + meta['email'] = self._validate_email(email) + except ValueError as e: + return {'status': 'error', 'message': str(e)} + if not meta.get('domain') or not meta.get('email'): + return {'status': 'error', 'message': 'Domain and email are required'} + meta['port'] = int(meta.get('port') or self.DEFAULT_PORT) + self._write_metadata(meta) + try: + self._write_config_from_clients() + self._start_container() + except Exception as e: + return {'status': 'error', 'message': str(e), **self.get_settings()} + return {'status': 'success', 'message': 'NaiveProxy settings updated', **self.get_settings()} + + # ===================== CLIENTS ===================== + + def get_clients(self, protocol_type=None): + clients = self._read_clients() + result = [] + for c in clients: + cname = c.get('name') or c.get('username') or c.get('id') + result.append({ + 'clientId': c.get('id'), + 'client_id': c.get('id'), + 'id': c.get('id'), + 'name': cname, + 'email': cname, + 'enabled': c.get('enabled', True), + 'userData': { + 'clientName': cname, + 'enabled': c.get('enabled', True), + 'username': c.get('username'), + }, + }) + return result + + def add_client(self, protocol_type, name, host, port=None): + meta = self._read_metadata() + domain = meta.get('domain') or host + port = int(port or meta.get('port') or self.DEFAULT_PORT) + client_id = secrets.token_hex(8) + username = f'u{client_id[:8]}' + password = _rand_token(20) + clients = self._read_clients() + clients.append({ + 'id': client_id, + 'name': name or f'client-{client_id[:6]}', + 'username': username, + 'password': password, + 'enabled': True, + }) + self._write_clients(clients) + self._reload_container() + config = self._build_share_uri(domain, username, password, name or client_id, port) + return {'client_id': client_id, 'config': config} + + def get_client_config(self, protocol_type, client_id, host, port=None): + meta = self._read_metadata() + domain = meta.get('domain') or host + port = int(port or meta.get('port') or self.DEFAULT_PORT) + clients = self._read_clients() + client = next((c for c in clients if c.get('id') == client_id), None) + if not client: + raise RuntimeError('Client not found') + if client.get('enabled', True) is False: + raise RuntimeError('Client is disabled') + return self._build_share_uri( + domain, + client.get('username'), + client.get('password'), + client.get('name') or client_id, + port, + ) + + def remove_client(self, protocol_type, client_id): + clients = [c for c in self._read_clients() if c.get('id') != client_id] + self._write_clients(clients) + self._reload_container() + return True + + def toggle_client(self, protocol_type, client_id, enable=True): + clients = self._read_clients() + found = False + for c in clients: + if c.get('id') == client_id: + c['enabled'] = bool(enable) + found = True + break + if not found: + raise RuntimeError('Client not found') + self._write_clients(clients) + self._reload_container() + return True diff --git a/telegram_bot.py b/telegram_bot.py index fdc9969..af66add 100644 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -24,7 +24,7 @@ _bot_task: Optional[asyncio.Task] = None _callback_refs = {} _pending_inputs = {} -CLIENT_PROTOCOLS = {"awg", "awg2", "awg_legacy", "xray", "telemt", "hysteria", "wireguard"} +CLIENT_PROTOCOLS = {"awg", "awg2", "awg_legacy", "xray", "telemt", "hysteria", "naiveproxy", "wireguard"} SERVICE_PROTOCOLS = {"dns", "adguard", "socks5", "nginx"} @@ -138,6 +138,7 @@ def _protocol_display_name(protocol: str) -> str: "xray": "Xray", "telemt": "Telemt", "hysteria": "Hysteria 2", + "naiveproxy": "NaiveProxy", "dns": "AmneziaDNS", "wireguard": "WireGuard", "socks5": "SOCKS5", @@ -339,6 +340,8 @@ def _get_ssh_and_manager(server: dict, proto: str): from managers.awg_manager import AWGManager from managers.xray_manager import XrayManager from managers.telemt_manager import TelemtManager + from managers.hysteria_manager import HysteriaManager + from managers.naiveproxy_manager import NaiveProxyManager from managers.wireguard_manager import WireGuardManager from managers.dns_manager import DNSManager from managers.socks5_manager import Socks5Manager @@ -357,6 +360,10 @@ def _get_ssh_and_manager(server: dict, proto: str): manager = XrayManager(ssh, proto) elif base == "telemt": manager = TelemtManager(ssh, proto) + elif base == "hysteria": + manager = HysteriaManager(ssh, proto) + elif base == "naiveproxy": + manager = NaiveProxyManager(ssh, proto) elif base == "wireguard": manager = WireGuardManager(ssh) elif base == "dns": @@ -562,7 +569,7 @@ async def _send_config_by_client(api: TelegramAPI, chat_id: int, server: dict, p server_name = server.get("name") or server.get("host", "Unknown") await api.send_message(chat_id, f"✅ {_e(conn_name)}\n🌐 Server: {_e(server_name)}\n🔌 Protocol: {_e(proto.upper())}") - is_link_proto = _proto_base(proto) in ("xray", "telemt", "hysteria") + is_link_proto = _proto_base(proto) in ("xray", "telemt", "hysteria", "naiveproxy") if is_link_proto: await api.send_message(chat_id, f"🔗 Connection link (tap to copy):\n{_e(config)}") else: @@ -919,7 +926,7 @@ async def _admin_create_client(api: TelegramAPI, chat_id: int, message_id: int, async def _send_config_text(api: TelegramAPI, chat_id: int, server: dict, proto: str, conn_name: str, config: str, generate_vpn_link_fn: Callable): await api.send_message(chat_id, f"✅ {_e(conn_name)}\n🌐 Server: {_e(server.get('name') or server.get('host'))}\n🔌 Protocol: {_e(proto.upper())}") - if _proto_base(proto) in ("xray", "telemt", "hysteria"): + if _proto_base(proto) in ("xray", "telemt", "hysteria", "naiveproxy"): await api.send_message(chat_id, f"🔗 Connection link:\n{_e(config)}") else: await api.send_message(chat_id, f"📄 Configuration:\n
{_e(config)}
") diff --git a/templates/guest.html b/templates/guest.html index 750b1c0..ed7d1ef 100644 --- a/templates/guest.html +++ b/templates/guest.html @@ -105,6 +105,9 @@ if (p === 'awg_legacy') return 'AWG Legacy'; if (p === 'xray') return 'Xray'; if (p === 'xui') return '3x-ui VLESS'; + if (p === 'hysteria') return 'Hysteria 2'; + if (p === 'naiveproxy') return 'NaiveProxy'; + if (p === 'telemt') return 'Telemt'; return (p || '').toUpperCase(); } diff --git a/templates/invites.html b/templates/invites.html index de42053..367d5f1 100644 --- a/templates/invites.html +++ b/templates/invites.html @@ -196,7 +196,7 @@ const xuiConfigured = {{ 'true' if xui_configured else 'false' }}; const xuiDefaultInbound = {{ xui_default_inbound | int }}; const xuiDefaultPanelId = {{ (xui_default_panel_id or '') | tojson }}; - const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria']; + const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria', 'naiveproxy']; const PROTO_TITLES = { awg2: 'AmneziaWG 2.0', awg: 'AmneziaWG', @@ -205,6 +205,7 @@ xray: 'Xray (VLESS-Reality)', telemt: 'Telemt', hysteria: 'Hysteria 2', + naiveproxy: 'NaiveProxy', xui: '3x-ui VLESS', }; diff --git a/templates/my_connections.html b/templates/my_connections.html index fa287b9..0d65968 100644 --- a/templates/my_connections.html +++ b/templates/my_connections.html @@ -118,10 +118,11 @@ document.getElementById('configText').textContent = result.config; document.getElementById('vpnLinkText').textContent = currentVpnLink; - // Telemt / Hysteria use share links, not vpn:// Amnezia format + // Telemt / Hysteria / NaiveProxy use share links, not vpn:// Amnezia format const vpnTab = document.querySelectorAll('.config-tab')[1]; const vpnPanel = document.getElementById('panel-vpn'); - if (proto === 'telemt' || String(proto || '').split('__')[0] === 'hysteria') { + const base = String(proto || '').split('__')[0]; + if (proto === 'telemt' || base === 'hysteria' || base === 'naiveproxy') { vpnTab.style.display = 'none'; } else { vpnTab.style.display = ''; diff --git a/templates/server.html b/templates/server.html index b12dc2f..db008a1 100644 --- a/templates/server.html +++ b/templates/server.html @@ -319,6 +319,30 @@ + +
+
+
{{ icon('link') }}
+
+
+
NaiveProxy
+
+ {{ _('naiveproxy_desc') }} +
+
+ {{ _('not_checked') }} +
+ +
+ +
+
+
@@ -703,6 +727,24 @@
{{ _('hysteria_install_hint') }}
+ +
- + ${proto === 'telemt' ? `` : ''} @@ -2649,8 +2727,8 @@ document.getElementById('configModalTitle').textContent = `${_('config')} — ${connName}`; document.getElementById('configText').textContent = result.config; document.getElementById('vpnLinkText').textContent = currentVpnLink; - document.getElementById('panel-vpn').style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : ''); - document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : ''); + document.getElementById('panel-vpn').style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' || protoBase(proto) === 'naiveproxy' ? 'none' : ''); + document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' || protoBase(proto) === 'naiveproxy' ? 'none' : ''); switchConfigTab('conf'); openModal('configModal'); generateQR(result.config); @@ -2728,9 +2806,9 @@ const proto = document.getElementById('connProtoSelect').value; // Restore tabs visibility first - document.getElementById('panel-vpn').style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : ''); + document.getElementById('panel-vpn').style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' || protoBase(proto) === 'naiveproxy' ? 'none' : ''); document.getElementById('panel-qr').style.display = ''; - document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' ? 'none' : ''); + document.querySelectorAll('.config-tab')[1].style.display = (proto === 'telemt' || protoBase(proto) === 'hysteria' || protoBase(proto) === 'naiveproxy' ? 'none' : ''); document.querySelectorAll('.config-tab')[2].style.display = ''; // Client was created via native Amnezia app — private key is not stored server-side diff --git a/templates/settings.html b/templates/settings.html index 0feb1cb..909d926 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -1321,7 +1321,7 @@ } const guestServersData = {{ servers | default([]) | tojson }}; - const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria']; + const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria', 'naiveproxy']; const PROTO_TITLES = { awg2: 'AmneziaWG 2.0', awg: 'AmneziaWG', @@ -1330,6 +1330,7 @@ xray: 'Xray (VLESS-Reality)', telemt: 'Telemt', hysteria: 'Hysteria 2', + naiveproxy: 'NaiveProxy', xui: '3x-ui VLESS', }; function protoTitle(key) { diff --git a/templates/users.html b/templates/users.html index 4eecb09..7540017 100644 --- a/templates/users.html +++ b/templates/users.html @@ -631,7 +631,7 @@ const select = document.getElementById(selectId); const group = groupId ? document.getElementById(groupId) : null; select.innerHTML = ''; - const vpnOrder = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria']; + const vpnOrder = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria', 'naiveproxy']; const titles = { awg2: 'AmneziaWG 2.0', awg: 'AmneziaWG', @@ -640,6 +640,7 @@ xray: 'Xray (VLESS-Reality)', telemt: 'Telemt', hysteria: 'Hysteria 2', + naiveproxy: 'NaiveProxy', xui: '3x-ui VLESS', }; const protoTitle = (key) => { diff --git a/translations/en.json b/translations/en.json index b297826..9264df9 100644 --- a/translations/en.json +++ b/translations/en.json @@ -364,6 +364,13 @@ "hysteria_renew_ssl": "Renew SSL", "hysteria_ssl_change_hint": "Changing the domain re-issues the Let's Encrypt certificate (port 80 must be free).", "hysteria_ssl_required": "Domain and email are required for SSL", + "naiveproxy_desc": "NaiveProxy (Caddy + forwardproxy) — HTTPS/HTTP2 proxy with Let's Encrypt TLS. Admin sets the domain at install time.", + "naiveproxy_domain": "Domain", + "naiveproxy_email": "Let's Encrypt email", + "naiveproxy_port_hint": "TCP port 443 is fixed for NaiveProxy. Port 80/TCP must be free for certificate issuance.", + "naiveproxy_install_hint": "Point the domain A-record to this server before install. Free TCP ports 80 and 443 are required (Let's Encrypt and HTTPS proxy).", + "naiveproxy_dns_hint": "Create this DNS record before installation:", + "naiveproxy_ports_warning": "Warning: free TCP ports 80 and 443 are required for stable operation (Let's Encrypt on 80, HTTPS proxy on 443).", "server_ssl_domain": "SSL domain (default)", "server_ssl_email": "SSL email (default)", "server_ssl_hint": "Used when installing Hysteria / NGINX on this server (Let's Encrypt).", diff --git a/translations/fa.json b/translations/fa.json index ee0159e..ac7ec98 100644 --- a/translations/fa.json +++ b/translations/fa.json @@ -354,6 +354,13 @@ "hysteria_renew_ssl": "تمدید SSL", "hysteria_ssl_change_hint": "تغییر دامنه گواهی Let's Encrypt را دوباره صادر می‌کند (پورت ۸۰ باید آزاد باشد).", "hysteria_ssl_required": "برای SSL دامنه و ایمیل لازم است", + "naiveproxy_desc": "NaiveProxy (Caddy + forwardproxy) — پروکسی HTTPS/HTTP2 با TLS از Let's Encrypt. دامنه را ادمین هنگام نصب مشخص می‌کند.", + "naiveproxy_domain": "دامنه", + "naiveproxy_email": "ایمیل Let's Encrypt", + "naiveproxy_port_hint": "پورت TCP 443 برای NaiveProxy ثابت است. پورت 80/TCP باید برای صدور گواهی آزاد باشد.", + "naiveproxy_install_hint": "قبل از نصب، رکورد A دامنه باید به این سرور اشاره کند. پورت‌های TCP آزاد ۸۰ و ۴۴۳ لازم است (Let's Encrypt و پروکسی HTTPS).", + "naiveproxy_dns_hint": "قبل از نصب این رکورد DNS را بسازید:", + "naiveproxy_ports_warning": "هشدار: برای کار پایدار پورت‌های TCP آزاد ۸۰ و ۴۴۳ لازم است (Let's Encrypt روی ۸۰، پروکسی HTTPS روی ۴۴۳).", "server_ssl_domain": "دامنه SSL (پیش‌فرض)", "server_ssl_email": "ایمیل SSL (پیش‌فرض)", "server_ssl_hint": "هنگام نصب Hysteria / NGINX روی این سرور استفاده می‌شود (Let's Encrypt).", diff --git a/translations/fr.json b/translations/fr.json index 0442358..d257a0e 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -354,6 +354,13 @@ "hysteria_renew_ssl": "Renouveler SSL", "hysteria_ssl_change_hint": "Changer le domaine réémet le certificat Let's Encrypt (le port 80 doit être libre).", "hysteria_ssl_required": "Domaine et email requis pour SSL", + "naiveproxy_desc": "NaiveProxy (Caddy + forwardproxy) — proxy HTTPS/HTTP2 avec TLS Let's Encrypt. Le domaine est défini par l'admin à l'installation.", + "naiveproxy_domain": "Domaine", + "naiveproxy_email": "Email Let's Encrypt", + "naiveproxy_port_hint": "Le port TCP 443 est fixe pour NaiveProxy. Le port 80/TCP doit être libre pour l'émission du certificat.", + "naiveproxy_install_hint": "L'enregistrement A du domaine doit pointer vers ce serveur. Les ports TCP 80 et 443 doivent être libres (Let's Encrypt et proxy HTTPS).", + "naiveproxy_dns_hint": "Créez cet enregistrement DNS avant l'installation :", + "naiveproxy_ports_warning": "Attention : les ports TCP 80 et 443 doivent être libres pour un fonctionnement stable (Let's Encrypt sur 80, proxy HTTPS sur 443).", "server_ssl_domain": "Domaine SSL (par défaut)", "server_ssl_email": "Email SSL (par défaut)", "server_ssl_hint": "Utilisé lors de l'installation de Hysteria / NGINX sur ce serveur (Let's Encrypt).", diff --git a/translations/ru.json b/translations/ru.json index 78a2b2a..388ddbe 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -364,6 +364,13 @@ "hysteria_renew_ssl": "Обновить SSL", "hysteria_ssl_change_hint": "Смена домена заново выпускает сертификат Let's Encrypt (порт 80 должен быть свободен).", "hysteria_ssl_required": "Для SSL нужны домен и email", + "naiveproxy_desc": "NaiveProxy (Caddy + forwardproxy) — HTTPS/HTTP2 прокси с TLS от Let's Encrypt. Домен указывает админ при установке.", + "naiveproxy_domain": "Домен", + "naiveproxy_email": "Email для Let's Encrypt", + "naiveproxy_port_hint": "TCP-порт 443 фиксирован для NaiveProxy. Порт 80/TCP должен быть свободен для выпуска сертификата.", + "naiveproxy_install_hint": "Перед установкой A-запись домена должна указывать на этот сервер. Нужны свободные TCP-порты 80 и 443 (Let's Encrypt и HTTPS-прокси).", + "naiveproxy_dns_hint": "Перед установкой создайте DNS-запись:", + "naiveproxy_ports_warning": "Внимание: для стабильной работы нужны свободные TCP-порты 80 и 443 (Let's Encrypt на 80, HTTPS-прокси на 443).", "server_ssl_domain": "SSL-домен (по умолчанию)", "server_ssl_email": "SSL-email (по умолчанию)", "server_ssl_hint": "Подставляется при установке Hysteria / NGINX на этом сервере (Let's Encrypt).", diff --git a/translations/zh.json b/translations/zh.json index 85136ab..76bdb2c 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -354,6 +354,13 @@ "hysteria_renew_ssl": "续签 SSL", "hysteria_ssl_change_hint": "更改域名会重新签发 Let's Encrypt 证书(需空闲端口 80)。", "hysteria_ssl_required": "签发 SSL 需要域名和邮箱", + "naiveproxy_desc": "NaiveProxy(Caddy + forwardproxy)— 带 Let's Encrypt TLS 的 HTTPS/HTTP2 代理。安装时由管理员指定域名。", + "naiveproxy_domain": "域名", + "naiveproxy_email": "Let's Encrypt 邮箱", + "naiveproxy_port_hint": "NaiveProxy 固定使用 TCP 443。签发证书需空闲 TCP 80。", + "naiveproxy_install_hint": "安装前请将域名 A 记录指向本服务器。需空闲 TCP 端口 80 和 443(Let's Encrypt 与 HTTPS 代理)。", + "naiveproxy_dns_hint": "安装前请创建此 DNS 记录:", + "naiveproxy_ports_warning": "注意:稳定运行需要空闲的 TCP 端口 80 和 443(Let's Encrypt 使用 80,HTTPS 代理使用 443)。", "server_ssl_domain": "SSL 域名(默认)", "server_ssl_email": "SSL 邮箱(默认)", "server_ssl_hint": "安装 Hysteria / NGINX 时自动填入(Let's Encrypt)。",