Release v2.0.0-beta: add NaiveProxy (Caddy + klzgrad/forwardproxy).
Installable marketplace protocol with ACME TLS, per-client basic auth, naive+https share links; bump panel version to 2.0 Beta. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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.-]+(?<!-)$', domain) or '.' not in domain:
|
||||
raise ValueError('Invalid domain name')
|
||||
return domain
|
||||
|
||||
def _validate_email(self, email):
|
||||
email = (email or '').strip()
|
||||
if not email or '@' not in email:
|
||||
raise ValueError('Valid Let\'s Encrypt email is required')
|
||||
return email
|
||||
|
||||
def _read_file(self, path):
|
||||
out, _, code = self.ssh.run_sudo_command(f"cat {_q(path)} 2>/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 = (
|
||||
'<!DOCTYPE html><html><head><meta charset="utf-8">'
|
||||
'<title>Welcome</title></head><body><h1>It works</h1></body></html>\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
|
||||
+10
-3
@@ -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"✅ <b>{_e(conn_name)}</b>\n🌐 Server: <b>{_e(server_name)}</b>\n🔌 Protocol: <b>{_e(proto.upper())}</b>")
|
||||
|
||||
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"🔗 <b>Connection link</b> (tap to copy):\n<code>{_e(config)}</code>")
|
||||
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"✅ <b>{_e(conn_name)}</b>\n🌐 Server: <b>{_e(server.get('name') or server.get('host'))}</b>\n🔌 Protocol: <b>{_e(proto.upper())}</b>")
|
||||
if _proto_base(proto) in ("xray", "telemt", "hysteria"):
|
||||
if _proto_base(proto) in ("xray", "telemt", "hysteria", "naiveproxy"):
|
||||
await api.send_message(chat_id, f"🔗 <b>Connection link</b>:\n<code>{_e(config)}</code>")
|
||||
else:
|
||||
await api.send_message(chat_id, f"<b>📄 Configuration:</b>\n<pre>{_e(config)}</pre>")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
+86
-8
@@ -319,6 +319,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NaiveProxy Card -->
|
||||
<div class="card card-hover protocol-card protocol-naiveproxy" id="proto-naiveproxy">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||
<div class="protocol-icon">{{ icon('link') }}</div>
|
||||
<div class="flex gap-sm" id="naiveproxy-ctrl" style="display:none!important;"></div>
|
||||
</div>
|
||||
<div class="protocol-name">NaiveProxy</div>
|
||||
<div class="protocol-desc">
|
||||
{{ _('naiveproxy_desc') }}
|
||||
</div>
|
||||
<div class="protocol-status" id="naiveproxy-status">
|
||||
<span class="badge badge-warn"><span class="badge-dot"></span> {{ _('not_checked') }}</span>
|
||||
</div>
|
||||
<div id="naiveproxy-info" class="hidden">
|
||||
<div class="protocol-info" id="naiveproxy-info-grid"></div>
|
||||
</div>
|
||||
<div class="flex gap-sm" id="naiveproxy-actions">
|
||||
<button class="btn btn-primary btn-sm" onclick="openInstallModal('naiveproxy')" id="naiveproxy-install-btn"
|
||||
style="flex:1">
|
||||
{{ _('install') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WireGuard Card -->
|
||||
<div class="card card-hover protocol-card protocol-wireguard" id="proto-wireguard">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||
@@ -703,6 +727,24 @@
|
||||
<div class="form-hint">{{ _('hysteria_install_hint') }}</div>
|
||||
</div>
|
||||
|
||||
<div id="naiveproxyOptions"
|
||||
style="display:none; padding: var(--space-md); background: rgba(0,0,0,0.03); border-radius: var(--radius-md); margin-bottom: var(--space-md);">
|
||||
<div class="form-hint" style="margin-bottom: var(--space-md); padding: var(--space-sm) var(--space-md); border-radius: var(--radius-sm); background: rgba(234,179,8,0.12); border: 1px solid rgba(234,179,8,0.35); color: var(--text);">
|
||||
{{ _('naiveproxy_ports_warning') }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('naiveproxy_domain') }}</label>
|
||||
<input class="form-input" type="text" id="installNaiveproxyDomain" placeholder="vpn.example.com" oninput="updateNaiveproxyDnsHint()">
|
||||
<div class="form-hint" id="naiveproxyDnsHint"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('naiveproxy_email') }}</label>
|
||||
<input class="form-input" type="email" id="installNaiveproxyEmail" placeholder="admin@example.com">
|
||||
</div>
|
||||
<div class="form-hint">{{ _('naiveproxy_install_hint') }}</div>
|
||||
<div class="form-hint" style="margin-top: var(--space-sm);">{{ _('naiveproxy_port_hint') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('installModal')">{{ _('cancel') }}</button>
|
||||
<button class="btn btn-primary" onclick="installProtocol()" id="installBtn">
|
||||
@@ -1055,6 +1097,7 @@
|
||||
{ proto: 'xray', category: 'protocols', icon: 'zap', title: 'Xray (VLESS-Reality)', descKey: 'xray_desc' },
|
||||
{ proto: 'telemt', category: 'protocols', icon: 'plane', title: 'Telemt (Telegram Proxy)', descKey: 'telemt_desc' },
|
||||
{ proto: 'hysteria', category: 'protocols', icon: 'refresh', title: 'Hysteria 2', descKey: 'hysteria_desc' },
|
||||
{ proto: 'naiveproxy', category: 'protocols', icon: 'link', title: 'NaiveProxy', descKey: 'naiveproxy_desc' },
|
||||
{ proto: 'wireguard', category: 'protocols', icon: 'lock', title: 'WireGuard', descKey: 'wireguard_desc' },
|
||||
{ proto: 'dns', category: 'services', icon: 'search', title: 'AmneziaDNS', descKey: 'dns_desc' },
|
||||
{ proto: 'adguard', category: 'services', icon: 'shield-check', title: 'AdGuard Home', descKey: 'adguard_desc' },
|
||||
@@ -1296,6 +1339,7 @@
|
||||
case 'xray': title = 'Xray'; break;
|
||||
case 'telemt': title = 'Telemt'; break;
|
||||
case 'hysteria': title = 'Hysteria 2'; break;
|
||||
case 'naiveproxy': title = 'NaiveProxy'; break;
|
||||
case 'wireguard': title = 'WireGuard'; break;
|
||||
case 'dns': title = 'AmneziaDNS'; break;
|
||||
case 'socks5': title = 'SOCKS5 Proxy'; break;
|
||||
@@ -1355,7 +1399,7 @@
|
||||
|
||||
for (const [proto, info] of orderedProtocolEntries(data.protocols)) {
|
||||
updateProtocolCard(proto, info);
|
||||
const isVPN = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'wireguard'].includes(protoBase(proto));
|
||||
const isVPN = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'naiveproxy', 'wireguard'].includes(protoBase(proto));
|
||||
if (info.container_running && isVPN) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = proto;
|
||||
@@ -1583,10 +1627,13 @@
|
||||
if (isService) {
|
||||
grid = buildServiceInfoGrid(proto, info);
|
||||
} else if (info.port) {
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('port')}</span><span class="protocol-info-value">${info.port}/${(['xray', 'telemt'].includes(protoBase(proto))) ? 'TCP' : 'UDP'}</span></div>`;
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('port')}</span><span class="protocol-info-value">${info.port}/${(['xray', 'telemt', 'naiveproxy'].includes(protoBase(proto))) ? 'TCP' : 'UDP'}</span></div>`;
|
||||
if (protoBase(proto) === 'hysteria' && info.domain) {
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('hysteria_domain')}</span><span class="protocol-info-value">${info.domain}</span></div>`;
|
||||
}
|
||||
if (protoBase(proto) === 'naiveproxy' && info.domain) {
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('naiveproxy_domain')}</span><span class="protocol-info-value">${info.domain}</span></div>`;
|
||||
}
|
||||
}
|
||||
if (!isService && info.clients_count !== undefined) {
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('connections')}</span><span class="protocol-info-value">${info.clients_count}</span></div>`;
|
||||
@@ -1629,6 +1676,13 @@
|
||||
<button class="btn btn-danger btn-sm" onclick="uninstallProtocol('${proto}')">${_('uninstall')}</button>
|
||||
`;
|
||||
showConnectionsSection();
|
||||
} else if (protoBase(proto) === 'naiveproxy') {
|
||||
actionsEl.innerHTML = `
|
||||
<button class="btn btn-secondary btn-sm" onclick="selectProtocolForConns('${proto}')" style="flex:1">${_('connections')}</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="showProtocolBackups('${proto}')">${_('backup')}</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="uninstallProtocol('${proto}')">${_('uninstall')}</button>
|
||||
`;
|
||||
showConnectionsSection();
|
||||
} else {
|
||||
actionsEl.innerHTML = `
|
||||
<button class="btn btn-secondary btn-sm" onclick="selectProtocolForConns('${proto}')" style="flex:1">${_('connections')}</button>
|
||||
@@ -2007,6 +2061,14 @@
|
||||
hint.innerHTML = `${_('hysteria_dns_hint')} <code>A ${domain} ${SERVER_HOST}</code>`;
|
||||
}
|
||||
|
||||
function updateNaiveproxyDnsHint() {
|
||||
const input = document.getElementById('installNaiveproxyDomain');
|
||||
const hint = document.getElementById('naiveproxyDnsHint');
|
||||
if (!input || !hint) return;
|
||||
const domain = (input.value || '').trim() || 'vpn.example.com';
|
||||
hint.innerHTML = `${_('naiveproxy_dns_hint')} <code>A ${domain} ${SERVER_HOST}</code>`;
|
||||
}
|
||||
|
||||
function openInstallModal(proto, installAnother = false) {
|
||||
const base = protoBase(proto);
|
||||
currentInstallProto = installAnother ? base : proto;
|
||||
@@ -2023,12 +2085,14 @@
|
||||
const adguardOpts = document.getElementById('adguardOptions');
|
||||
const nginxOpts = document.getElementById('nginxOptions');
|
||||
const hysteriaOpts = document.getElementById('hysteriaOptions');
|
||||
const naiveproxyOpts = document.getElementById('naiveproxyOptions');
|
||||
|
||||
telemtOpts.style.display = 'none';
|
||||
socks5Opts.style.display = 'none';
|
||||
adguardOpts.style.display = 'none';
|
||||
nginxOpts.style.display = 'none';
|
||||
hysteriaOpts.style.display = 'none';
|
||||
if (naiveproxyOpts) naiveproxyOpts.style.display = 'none';
|
||||
if (portGroup) portGroup.style.display = '';
|
||||
|
||||
if (base === 'dns') {
|
||||
@@ -2086,6 +2150,16 @@
|
||||
if (hyDomain && !hyDomain.value && SERVER_SSL_DOMAIN) hyDomain.value = SERVER_SSL_DOMAIN;
|
||||
if (hyEmail && !hyEmail.value && SERVER_SSL_EMAIL) hyEmail.value = SERVER_SSL_EMAIL;
|
||||
updateHysteriaDnsHint();
|
||||
} else if (base === 'naiveproxy') {
|
||||
if (portGroup) portGroup.style.display = 'none';
|
||||
portInput.value = '443';
|
||||
portInput.disabled = false;
|
||||
if (naiveproxyOpts) naiveproxyOpts.style.display = 'block';
|
||||
const npDomain = document.getElementById('installNaiveproxyDomain');
|
||||
const npEmail = document.getElementById('installNaiveproxyEmail');
|
||||
if (npDomain && !npDomain.value && SERVER_SSL_DOMAIN) npDomain.value = SERVER_SSL_DOMAIN;
|
||||
if (npEmail && !npEmail.value && SERVER_SSL_EMAIL) npEmail.value = SERVER_SSL_EMAIL;
|
||||
updateNaiveproxyDnsHint();
|
||||
} else {
|
||||
portLabel.textContent = _('port') + ' (UDP)';
|
||||
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 55424) : '55424';
|
||||
@@ -2159,6 +2233,10 @@
|
||||
}
|
||||
params.hysteria_domain = document.getElementById('installHysteriaDomain').value.trim();
|
||||
params.hysteria_email = document.getElementById('installHysteriaEmail').value.trim();
|
||||
} else if (protoBase(currentInstallProto) === 'naiveproxy') {
|
||||
params.port = '443';
|
||||
params.naiveproxy_domain = document.getElementById('installNaiveproxyDomain').value.trim();
|
||||
params.naiveproxy_email = document.getElementById('installNaiveproxyEmail').value.trim();
|
||||
}
|
||||
const result = await apiCall(`/api/servers/${SERVER_ID}/install`, 'POST', params);
|
||||
clearInterval(progressInterval);
|
||||
@@ -2559,7 +2637,7 @@
|
||||
const sent = userData.dataSent || '';
|
||||
const initial = name.charAt(0).toUpperCase();
|
||||
const enabled = (client.enabled !== undefined) ? client.enabled : (userData.enabled !== false);
|
||||
const hasPrivKey = !!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt' || protoBase(proto) === 'hysteria';
|
||||
const hasPrivKey = !!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt' || protoBase(proto) === 'hysteria' || protoBase(proto) === 'naiveproxy';
|
||||
const assignedUser = client.assigned_user || '';
|
||||
|
||||
let metaHtml = '';
|
||||
@@ -2597,7 +2675,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="client-actions">
|
||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="showConnectionConfig('${escapeJs(client.clientId)}', '${escapeJs(name)}', ${!!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt' || protoBase(proto) === 'hysteria'})" title="${_('config')}">${uiIcon('file')}</button>
|
||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="showConnectionConfig('${escapeJs(client.clientId)}', '${escapeJs(name)}', ${!!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt' || protoBase(proto) === 'hysteria' || protoBase(proto) === 'naiveproxy'})" title="${_('config')}">${uiIcon('file')}</button>
|
||||
${proto === 'telemt' ? `<button class="btn btn-secondary btn-sm btn-icon" onclick="editConnection('${escapeJs(client.clientId)}')" title="${_('edit')}">${uiIcon('pencil')}</button>` : ''}
|
||||
<button class="btn btn-secondary btn-sm btn-icon" onclick="toggleConnection('${escapeJs(client.clientId)}', ${!enabled})" title="${toggleTitle}">${toggleIcon}</button>
|
||||
<button class="btn btn-danger btn-sm btn-icon" onclick="removeConnection('${escapeJs(client.clientId)}')" title="${_('delete')}">${uiIcon('trash')}</button>
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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).",
|
||||
|
||||
@@ -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).",
|
||||
|
||||
@@ -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).",
|
||||
|
||||
@@ -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).",
|
||||
|
||||
@@ -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)。",
|
||||
|
||||
Reference in New Issue
Block a user