Template
Switch Xray to VLESS+XHTTP+TLS with Cloudflare DNS ACME (v3.1.1).
Issue Let's Encrypt via Cloudflare API token so install no longer needs port 80; keep HTTP-01 as fallback. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -103,7 +103,7 @@ else:
|
|||||||
application_path = os.path.dirname(__file__)
|
application_path = os.path.dirname(__file__)
|
||||||
|
|
||||||
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
|
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
|
||||||
CURRENT_VERSION = "v3.0.1"
|
CURRENT_VERSION = "v3.1.1"
|
||||||
RELEASES_REPO_URL = repo_url()
|
RELEASES_REPO_URL = repo_url()
|
||||||
RELEASES_API_LATEST = api_latest_url()
|
RELEASES_API_LATEST = api_latest_url()
|
||||||
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
|
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
|
||||||
@@ -2225,6 +2225,11 @@ class InstallProtocolRequest(BaseModel):
|
|||||||
# NaiveProxy
|
# NaiveProxy
|
||||||
naiveproxy_domain: Optional[str] = None
|
naiveproxy_domain: Optional[str] = None
|
||||||
naiveproxy_email: Optional[str] = None
|
naiveproxy_email: Optional[str] = None
|
||||||
|
# Xray (VLESS + XHTTP + TLS)
|
||||||
|
xray_domain: Optional[str] = None
|
||||||
|
xray_email: Optional[str] = None
|
||||||
|
xray_acme_method: Optional[str] = 'cloudflare' # cloudflare | http
|
||||||
|
xray_cf_token: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class Socks5SettingsRequest(BaseModel):
|
class Socks5SettingsRequest(BaseModel):
|
||||||
@@ -3708,7 +3713,13 @@ async def api_install_protocol(request: Request, server_id: int, req: InstallPro
|
|||||||
max_connections=req.max_connections if req.max_connections is not None else 0
|
max_connections=req.max_connections if req.max_connections is not None else 0
|
||||||
)
|
)
|
||||||
elif install_base == 'xray':
|
elif install_base == 'xray':
|
||||||
result = manager.install_protocol(port=req.port)
|
result = manager.install_protocol(
|
||||||
|
port=req.port,
|
||||||
|
domain=req.xray_domain,
|
||||||
|
email=req.xray_email,
|
||||||
|
acme_method=req.xray_acme_method or 'cloudflare',
|
||||||
|
cloudflare_token=req.xray_cf_token,
|
||||||
|
)
|
||||||
elif install_base == 'wireguard':
|
elif install_base == 'wireguard':
|
||||||
result = manager.install_protocol(port=req.port)
|
result = manager.install_protocol(port=req.port)
|
||||||
elif install_base == 'socks5':
|
elif install_base == 'socks5':
|
||||||
@@ -3795,6 +3806,20 @@ async def api_install_protocol(request: Request, server_id: int, req: InstallPro
|
|||||||
proto_record['email'] = result.get('email')
|
proto_record['email'] = result.get('email')
|
||||||
if result.get('port'):
|
if result.get('port'):
|
||||||
proto_record['port'] = str(result['port'])
|
proto_record['port'] = str(result['port'])
|
||||||
|
if install_base == 'xray':
|
||||||
|
info = server.setdefault('server_info', {})
|
||||||
|
if req.xray_domain:
|
||||||
|
info['ssl_domain'] = (req.xray_domain or '').strip().lower()
|
||||||
|
if req.xray_email:
|
||||||
|
info['ssl_email'] = (req.xray_email or '').strip()
|
||||||
|
save_data(data)
|
||||||
|
proto_record['domain'] = result.get('domain')
|
||||||
|
proto_record['path'] = result.get('path')
|
||||||
|
proto_record['transport'] = 'xhttp'
|
||||||
|
proto_record['security'] = 'tls'
|
||||||
|
proto_record['acme_method'] = result.get('acme_method') or req.xray_acme_method or 'cloudflare'
|
||||||
|
if result.get('port'):
|
||||||
|
proto_record['port'] = str(result['port'])
|
||||||
if install_base == 'naiveproxy':
|
if install_base == 'naiveproxy':
|
||||||
info = server.setdefault('server_info', {})
|
info = server.setdefault('server_info', {})
|
||||||
if req.naiveproxy_domain:
|
if req.naiveproxy_domain:
|
||||||
|
|||||||
+354
-136
@@ -1,5 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import secrets
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
import base64
|
import base64
|
||||||
@@ -9,13 +11,23 @@ import urllib.parse
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CERTBOT_IMAGE = 'certbot/certbot:latest'
|
||||||
|
CERTBOT_CF_IMAGE = 'certbot/dns-cloudflare:latest'
|
||||||
|
XRAY_RELEASE = 'v26.3.27'
|
||||||
|
|
||||||
|
|
||||||
|
def _q(value):
|
||||||
|
return shlex.quote(str(value))
|
||||||
|
|
||||||
|
|
||||||
class XrayManager:
|
class XrayManager:
|
||||||
"""Manages Xray (VLESS-Reality) protocol installation and client management."""
|
"""Manages Xray VLESS + XHTTP + TLS (Let's Encrypt) for DPI-resistant installs."""
|
||||||
|
|
||||||
PROTOCOL = 'xray'
|
PROTOCOL = 'xray'
|
||||||
CONTAINER_NAME = 'amnezia-xray'
|
CONTAINER_NAME = 'amnezia-xray'
|
||||||
IMAGE_NAME = 'amneziavpn/amnezia-xray' # or we can build it
|
IMAGE_NAME = 'amneziavpn/amnezia-xray'
|
||||||
|
DEFAULT_PORT = 443
|
||||||
|
|
||||||
def __init__(self, ssh_manager, protocol='xray'):
|
def __init__(self, ssh_manager, protocol='xray'):
|
||||||
self.ssh = ssh_manager
|
self.ssh = ssh_manager
|
||||||
self.protocol = protocol or self.PROTOCOL
|
self.protocol = protocol or self.PROTOCOL
|
||||||
@@ -155,35 +167,182 @@ class XrayManager:
|
|||||||
'container_exists': exists,
|
'container_exists': exists,
|
||||||
'container_running': running,
|
'container_running': running,
|
||||||
'clients_count': len(clients),
|
'clients_count': len(clients),
|
||||||
'port': meta.get('port')
|
'port': meta.get('port'),
|
||||||
|
'domain': meta.get('domain') or meta.get('site_name'),
|
||||||
|
'transport': meta.get('transport') or 'xhttp',
|
||||||
|
'security': meta.get('security') or 'tls',
|
||||||
}
|
}
|
||||||
|
|
||||||
def install_protocol(self, port=443, site_name='yahoo.com'):
|
def _validate_domain(self, domain):
|
||||||
"""Full installation of Xray."""
|
domain = (domain or '').strip().lower().rstrip('.')
|
||||||
|
if not domain or not re.match(r'^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$', domain):
|
||||||
|
raise ValueError('Valid domain is required for Xray XHTTP+TLS (e.g. vpn.example.com)')
|
||||||
|
return domain
|
||||||
|
|
||||||
|
def _validate_email(self, email):
|
||||||
|
email = (email or '').strip()
|
||||||
|
if not email or '@' not in email or '.' not in email.split('@')[-1]:
|
||||||
|
raise ValueError('Valid email is required for Let\'s Encrypt')
|
||||||
|
return email
|
||||||
|
|
||||||
|
def _certs_dir(self):
|
||||||
|
return f'{self._config_dir()}/certs'
|
||||||
|
|
||||||
|
def _letsencrypt_dir(self):
|
||||||
|
return f'{self._config_dir()}/letsencrypt'
|
||||||
|
|
||||||
|
def _cert_path(self):
|
||||||
|
return f'{self._certs_dir()}/fullchain.pem'
|
||||||
|
|
||||||
|
def _key_path(self):
|
||||||
|
return f'{self._certs_dir()}/privkey.pem'
|
||||||
|
|
||||||
|
def _random_xhttp_path(self):
|
||||||
|
# Looks like a static/CDN asset path — harder for simple DPI signatures.
|
||||||
|
return f'/assets/{secrets.token_hex(8)}/{secrets.token_hex(4)}.js'
|
||||||
|
|
||||||
|
def _cf_creds_path(self):
|
||||||
|
return f'{self._config_dir()}/cloudflare.ini'
|
||||||
|
|
||||||
|
def _write_cloudflare_ini(self, token):
|
||||||
|
token = (token or '').strip()
|
||||||
|
if not token:
|
||||||
|
raise ValueError('Cloudflare API token is required for DNS validation')
|
||||||
|
# Restrictive file for certbot dns plugin
|
||||||
|
content = f"dns_cloudflare_api_token = {token}\n"
|
||||||
|
path = self._cf_creds_path()
|
||||||
|
self.ssh.run_sudo_command(f"mkdir -p {_q(self._config_dir())}")
|
||||||
|
self.ssh.upload_file_sudo(content, path)
|
||||||
|
self.ssh.run_sudo_command(f"chmod 600 {_q(path)}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
def _install_issued_certs(self, domain, le_dir):
|
||||||
|
copy_script = f"""
|
||||||
|
set -e
|
||||||
|
LIVE={_q(le_dir)}/live/{_q(domain)}
|
||||||
|
test -s "$LIVE/fullchain.pem"
|
||||||
|
test -s "$LIVE/privkey.pem"
|
||||||
|
mkdir -p {_q(self._certs_dir())}
|
||||||
|
cp -f "$LIVE/fullchain.pem" {_q(self._cert_path())}
|
||||||
|
cp -f "$LIVE/privkey.pem" {_q(self._key_path())}
|
||||||
|
chmod 644 {_q(self._cert_path())} {_q(self._key_path())}
|
||||||
|
"""
|
||||||
|
out, err, code = self.ssh.run_sudo_command(f"sh -c {_q(copy_script)}", timeout=30)
|
||||||
|
if code != 0:
|
||||||
|
raise RuntimeError(f'Failed to install certificate files: {err or out}')
|
||||||
|
|
||||||
|
def _issue_certificate(self, domain, email, *, acme_method='cloudflare', cloudflare_token=None):
|
||||||
|
"""Issue Let's Encrypt cert via Cloudflare DNS-01 (preferred) or HTTP-01 standalone."""
|
||||||
|
method = (acme_method or 'cloudflare').strip().lower()
|
||||||
|
if method in ('cf', 'dns', 'dns-01', 'cloudflare_dns'):
|
||||||
|
method = 'cloudflare'
|
||||||
|
if method in ('http', 'http-01', 'standalone', 'port80'):
|
||||||
|
method = 'http'
|
||||||
|
|
||||||
|
le_dir = self._letsencrypt_dir()
|
||||||
|
certs_dir = self._certs_dir()
|
||||||
|
self.ssh.run_sudo_command(f"mkdir -p {_q(le_dir)} {_q(certs_dir)}")
|
||||||
|
self.ssh.run_sudo_command("docker rm -fv amnezia-xray-certbot 2>/dev/null || true")
|
||||||
|
|
||||||
|
if method == 'cloudflare':
|
||||||
|
creds = self._write_cloudflare_ini(cloudflare_token)
|
||||||
|
self.ssh.run_sudo_command(f"docker pull {CERTBOT_CF_IMAGE}", timeout=180)
|
||||||
|
# DNS-01 — no TCP 80 needed. Token needs Zone:DNS:Edit on the domain zone.
|
||||||
|
cmd = (
|
||||||
|
f"docker run --rm --name amnezia-xray-certbot "
|
||||||
|
f"-v {_q(le_dir)}:/etc/letsencrypt "
|
||||||
|
f"-v {_q(creds)}:/cloudflare.ini:ro "
|
||||||
|
f"{CERTBOT_CF_IMAGE} certonly "
|
||||||
|
f"--dns-cloudflare "
|
||||||
|
f"--dns-cloudflare-credentials /cloudflare.ini "
|
||||||
|
f"--dns-cloudflare-propagation-seconds 30 "
|
||||||
|
f"--non-interactive --agree-tos --no-eff-email "
|
||||||
|
f"--email {_q(email)} -d {_q(domain)}"
|
||||||
|
)
|
||||||
|
out, err, code = self.ssh.run_sudo_command(cmd, timeout=420)
|
||||||
|
if code != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Let's Encrypt (Cloudflare DNS) failed for {domain}. "
|
||||||
|
f"Check API token (Zone:DNS:Edit) and that the domain is on Cloudflare. {err or out}"
|
||||||
|
)
|
||||||
|
self._install_issued_certs(domain, le_dir)
|
||||||
|
return 'cloudflare'
|
||||||
|
|
||||||
|
# HTTP-01 standalone — needs free TCP 80
|
||||||
|
self.ssh.run_sudo_command(f"docker pull {CERTBOT_IMAGE}", timeout=180)
|
||||||
|
cmd = (
|
||||||
|
f"docker run --rm --name amnezia-xray-certbot "
|
||||||
|
f"-p 80:80 "
|
||||||
|
f"-v {_q(le_dir)}:/etc/letsencrypt "
|
||||||
|
f"{CERTBOT_IMAGE} certonly --standalone "
|
||||||
|
f"--non-interactive --agree-tos --no-eff-email "
|
||||||
|
f"--email {_q(email)} -d {_q(domain)}"
|
||||||
|
)
|
||||||
|
out, err, code = self.ssh.run_sudo_command(cmd, timeout=300)
|
||||||
|
if code != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Let's Encrypt (HTTP-01) failed for {domain}. "
|
||||||
|
f"Point an A-record to this server and free TCP port 80. {err or out}"
|
||||||
|
)
|
||||||
|
self._install_issued_certs(domain, le_dir)
|
||||||
|
return 'http'
|
||||||
|
|
||||||
|
def _is_xhttp_tls_inbound(self, inbound):
|
||||||
|
stream = (inbound or {}).get('streamSettings') or {}
|
||||||
|
return (
|
||||||
|
str(stream.get('network') or '').lower() in ('xhttp', 'splithttp')
|
||||||
|
and str(stream.get('security') or '').lower() == 'tls'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _is_reality_inbound(self, inbound):
|
||||||
|
stream = (inbound or {}).get('streamSettings') or {}
|
||||||
|
return str(stream.get('security') or '').lower() == 'reality'
|
||||||
|
|
||||||
|
def install_protocol(self, port=443, domain=None, email=None, site_name=None,
|
||||||
|
acme_method='cloudflare', cloudflare_token=None):
|
||||||
|
"""Install VLESS + XHTTP + TLS (Let's Encrypt via Cloudflare DNS or HTTP-01)."""
|
||||||
results = []
|
results = []
|
||||||
|
domain = self._validate_domain(domain or site_name)
|
||||||
|
email = self._validate_email(email)
|
||||||
|
port = int(port or self.DEFAULT_PORT)
|
||||||
|
method = (acme_method or 'cloudflare').strip().lower()
|
||||||
|
if method in ('cf', 'dns', 'dns-01', 'cloudflare_dns'):
|
||||||
|
method = 'cloudflare'
|
||||||
|
if method in ('http', 'http-01', 'standalone', 'port80'):
|
||||||
|
method = 'http'
|
||||||
|
if method == 'http' and port == 80:
|
||||||
|
raise RuntimeError('Port 80 is reserved for Let\'s Encrypt HTTP-01 validation')
|
||||||
|
if method == 'cloudflare' and not (cloudflare_token or '').strip():
|
||||||
|
raise ValueError('Cloudflare API token is required')
|
||||||
|
|
||||||
if not self.check_docker_installed():
|
if not self.check_docker_installed():
|
||||||
results.append("Installing Docker...")
|
results.append("Docker not detected — install may fail")
|
||||||
# Using same install method as AWGManager or assume it's installed
|
|
||||||
pass
|
|
||||||
|
|
||||||
results.append("Removing old container if exists...")
|
results.append("Removing old container if exists...")
|
||||||
if self.check_protocol_installed():
|
if self.check_protocol_installed():
|
||||||
self.remove_container()
|
self.remove_container()
|
||||||
|
|
||||||
|
if method == 'cloudflare':
|
||||||
|
results.append(f"Issuing Let's Encrypt cert via Cloudflare DNS for {domain}...")
|
||||||
|
else:
|
||||||
|
results.append(f"Issuing Let's Encrypt cert via HTTP-01 (port 80) for {domain}...")
|
||||||
|
used = self._issue_certificate(
|
||||||
|
domain, email, acme_method=method, cloudflare_token=cloudflare_token,
|
||||||
|
)
|
||||||
|
results.append(f"TLS certificate ready ({used})")
|
||||||
|
|
||||||
results.append("Building Docker image...")
|
results.append("Building Docker image...")
|
||||||
config_dir = self._config_dir()
|
config_dir = self._config_dir()
|
||||||
dockerfile_folder = f"/opt/amnezia/{self.container_name}"
|
dockerfile_folder = f"/opt/amnezia/{self.container_name}"
|
||||||
dockerfile_content = f"""FROM alpine:3.15
|
dockerfile_content = f"""FROM alpine:3.15
|
||||||
RUN apk add --no-cache curl unzip bash openssl netcat-openbsd dumb-init rng-tools xz iptables ip6tables
|
RUN apk add --no-cache curl unzip bash openssl netcat-openbsd dumb-init rng-tools xz iptables ip6tables ca-certificates
|
||||||
RUN apk --update upgrade --no-cache
|
RUN apk --update upgrade --no-cache
|
||||||
RUN mkdir -p {config_dir}
|
RUN mkdir -p {config_dir}
|
||||||
RUN curl -L -H "Cache-Control: no-cache" -o /root/xray.zip "https://github.com/XTLS/Xray-core/releases/download/v26.3.27/Xray-linux-64.zip" && \\
|
RUN curl -L -H "Cache-Control: no-cache" -o /root/xray.zip "https://github.com/XTLS/Xray-core/releases/download/{XRAY_RELEASE}/Xray-linux-64.zip" && \\
|
||||||
unzip /root/xray.zip -d /usr/bin/ && \\
|
unzip /root/xray.zip -d /usr/bin/ && \\
|
||||||
chmod a+x /usr/bin/xray && \\
|
chmod a+x /usr/bin/xray && \\
|
||||||
rm /root/xray.zip
|
rm /root/xray.zip
|
||||||
|
|
||||||
# Tune network
|
|
||||||
RUN echo "fs.file-max = 51200" >> /etc/sysctl.conf && \\
|
RUN echo "fs.file-max = 51200" >> /etc/sysctl.conf && \\
|
||||||
echo "net.core.rmem_max = 67108864" >> /etc/sysctl.conf && \\
|
echo "net.core.rmem_max = 67108864" >> /etc/sysctl.conf && \\
|
||||||
echo "net.core.wmem_max = 67108864" >> /etc/sysctl.conf && \\
|
echo "net.core.wmem_max = 67108864" >> /etc/sysctl.conf && \\
|
||||||
@@ -191,18 +350,12 @@ RUN echo "fs.file-max = 51200" >> /etc/sysctl.conf && \\
|
|||||||
echo "net.core.somaxconn = 4096" >> /etc/sysctl.conf && \\
|
echo "net.core.somaxconn = 4096" >> /etc/sysctl.conf && \\
|
||||||
echo "net.ipv4.tcp_syncookies = 1" >> /etc/sysctl.conf && \\
|
echo "net.ipv4.tcp_syncookies = 1" >> /etc/sysctl.conf && \\
|
||||||
echo "net.ipv4.tcp_tw_reuse = 1" >> /etc/sysctl.conf && \\
|
echo "net.ipv4.tcp_tw_reuse = 1" >> /etc/sysctl.conf && \\
|
||||||
echo "net.ipv4.tcp_tw_recycle = 0" >> /etc/sysctl.conf && \\
|
|
||||||
echo "net.ipv4.tcp_fin_timeout = 30" >> /etc/sysctl.conf && \\
|
echo "net.ipv4.tcp_fin_timeout = 30" >> /etc/sysctl.conf && \\
|
||||||
echo "net.ipv4.tcp_keepalive_time = 1200" >> /etc/sysctl.conf && \\
|
echo "net.ipv4.tcp_keepalive_time = 1200" >> /etc/sysctl.conf && \\
|
||||||
echo "net.ipv4.ip_local_port_range = 10000 65000" >> /etc/sysctl.conf && \\
|
echo "net.ipv4.ip_local_port_range = 10000 65000" >> /etc/sysctl.conf && \\
|
||||||
echo "net.ipv4.tcp_max_syn_backlog = 8192" >> /etc/sysctl.conf && \\
|
echo "net.ipv4.tcp_max_syn_backlog = 8192" >> /etc/sysctl.conf && \\
|
||||||
echo "net.ipv4.tcp_max_tw_buckets = 5000" >> /etc/sysctl.conf && \\
|
|
||||||
echo "net.ipv4.tcp_fastopen = 3" >> /etc/sysctl.conf && \\
|
echo "net.ipv4.tcp_fastopen = 3" >> /etc/sysctl.conf && \\
|
||||||
echo "net.ipv4.tcp_mem = 25600 51200 102400" >> /etc/sysctl.conf && \\
|
echo "net.ipv4.tcp_congestion_control = bbr" >> /etc/sysctl.conf
|
||||||
echo "net.ipv4.tcp_rmem = 4096 87380 67108864" >> /etc/sysctl.conf && \\
|
|
||||||
echo "net.ipv4.tcp_wmem = 4096 65536 67108864" >> /etc/sysctl.conf && \\
|
|
||||||
echo "net.ipv4.tcp_mtu_probing = 1" >> /etc/sysctl.conf && \\
|
|
||||||
echo "net.ipv4.tcp_congestion_control = hybla" >> /etc/sysctl.conf
|
|
||||||
|
|
||||||
RUN mkdir -p /etc/security && \\
|
RUN mkdir -p /etc/security && \\
|
||||||
echo "* soft nofile 51200" >> /etc/security/limits.conf && \\
|
echo "* soft nofile 51200" >> /etc/security/limits.conf && \\
|
||||||
@@ -217,33 +370,20 @@ ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
|||||||
"""
|
"""
|
||||||
self.ssh.run_sudo_command(f"mkdir -p {dockerfile_folder}")
|
self.ssh.run_sudo_command(f"mkdir -p {dockerfile_folder}")
|
||||||
self.ssh.upload_file_sudo(dockerfile_content, f"{dockerfile_folder}/Dockerfile")
|
self.ssh.upload_file_sudo(dockerfile_content, f"{dockerfile_folder}/Dockerfile")
|
||||||
|
|
||||||
_, err, code = self.ssh.run_sudo_command(
|
_, err, code = self.ssh.run_sudo_command(
|
||||||
f"docker build --no-cache -t {self.image_name} {dockerfile_folder}", timeout=300
|
f"docker build --no-cache -t {self.image_name} {dockerfile_folder}", timeout=300
|
||||||
)
|
)
|
||||||
if code != 0: raise RuntimeError(f"Failed to build container: {err}")
|
if code != 0:
|
||||||
|
raise RuntimeError(f"Failed to build container: {err}")
|
||||||
|
|
||||||
results.append("Generating keys and config...")
|
results.append("Generating XHTTP+TLS config...")
|
||||||
# We generate a base config using a temp container or directly if host has openssl
|
xhttp_path = self._random_xhttp_path()
|
||||||
|
# auto → packet-up under TLS (CDN/middlebox friendly); works direct too.
|
||||||
# Xray keypair generation using a temporary run overriding the entrypoint
|
xhttp_mode = 'auto'
|
||||||
keypair_cmd = f"docker run --rm --entrypoint=\"\" {self.image_name} /usr/bin/xray x25519"
|
|
||||||
out_kp, err_kp, code_kp = self.ssh.run_sudo_command(keypair_cmd)
|
|
||||||
if code_kp != 0: raise RuntimeError(f"Failed to generate x25519 keys: {err_kp}")
|
|
||||||
|
|
||||||
priv_key = ""
|
|
||||||
pub_key = ""
|
|
||||||
for line in out_kp.split('\n'):
|
|
||||||
if "Private" in line: priv_key = line.split(":", 1)[1].strip()
|
|
||||||
if "Public" in line: pub_key = line.split(":", 1)[1].strip()
|
|
||||||
|
|
||||||
short_id_cmd = f"docker run --rm --entrypoint=\"\" {self.image_name} openssl rand -hex 8"
|
|
||||||
out_sid, _, _ = self.ssh.run_sudo_command(short_id_cmd)
|
|
||||||
short_id = out_sid.strip()
|
|
||||||
|
|
||||||
# Generate initial server.json with Stats and API enabled
|
|
||||||
server_json = {
|
server_json = {
|
||||||
"log": {"loglevel": "error"},
|
"log": {"loglevel": "warning"},
|
||||||
"stats": {},
|
"stats": {},
|
||||||
"api": {
|
"api": {
|
||||||
"services": ["StatsService", "LoggerService", "HandlerService"],
|
"services": ["StatsService", "LoggerService", "HandlerService"],
|
||||||
@@ -260,6 +400,7 @@ ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
|||||||
},
|
},
|
||||||
"inbounds": [
|
"inbounds": [
|
||||||
{
|
{
|
||||||
|
"listen": "0.0.0.0",
|
||||||
"port": int(port),
|
"port": int(port),
|
||||||
"protocol": "vless",
|
"protocol": "vless",
|
||||||
"tag": "proxy",
|
"tag": "proxy",
|
||||||
@@ -268,14 +409,37 @@ ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
|||||||
"decryption": "none"
|
"decryption": "none"
|
||||||
},
|
},
|
||||||
"streamSettings": {
|
"streamSettings": {
|
||||||
"network": "tcp",
|
"network": "xhttp",
|
||||||
"security": "reality",
|
"security": "tls",
|
||||||
"realitySettings": {
|
"tlsSettings": {
|
||||||
"dest": f"{site_name}:443",
|
"serverName": domain,
|
||||||
"serverNames": [site_name],
|
"minVersion": "1.3",
|
||||||
"privateKey": priv_key,
|
"alpn": ["h2", "http/1.1"],
|
||||||
"shortIds": [short_id]
|
"certificates": [{
|
||||||
|
"certificateFile": self._cert_path(),
|
||||||
|
"keyFile": self._key_path()
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"xhttpSettings": {
|
||||||
|
"path": xhttp_path,
|
||||||
|
"host": domain,
|
||||||
|
"mode": xhttp_mode,
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": [
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||||
|
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sockopt": {
|
||||||
|
"tcpFastOpen": True,
|
||||||
|
"tcpNoDelay": True
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"sniffing": {
|
||||||
|
"enabled": True,
|
||||||
|
"destOverride": ["http", "tls", "quic"],
|
||||||
|
"routeOnly": True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -286,29 +450,48 @@ ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
|||||||
"tag": "api"
|
"tag": "api"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outbounds": [{"protocol": "freedom"}],
|
"outbounds": [
|
||||||
|
{"protocol": "freedom", "tag": "direct"},
|
||||||
|
{"protocol": "blackhole", "tag": "block"}
|
||||||
|
],
|
||||||
"routing": {
|
"routing": {
|
||||||
|
"domainStrategy": "AsIs",
|
||||||
"rules": [
|
"rules": [
|
||||||
{
|
{
|
||||||
|
"type": "field",
|
||||||
"inboundTag": ["api"],
|
"inboundTag": ["api"],
|
||||||
"outboundTag": "api",
|
"outboundTag": "api"
|
||||||
"type": "field"
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"protocol": ["bittorrent"],
|
||||||
|
"outboundTag": "block"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ssh.run_sudo_command(f"mkdir -p {config_dir}")
|
self.ssh.run_sudo_command(f"mkdir -p {config_dir}")
|
||||||
self.ssh.upload_file_sudo(json.dumps(server_json, indent=2), f"{config_dir}/server.json")
|
self.ssh.upload_file_sudo(json.dumps(server_json, indent=2), f"{config_dir}/server.json")
|
||||||
# Native layout — separate key files matching the official Amnezia client install.
|
|
||||||
# See client/server_scripts/xray/configure_container.sh for the canonical layout.
|
meta = {
|
||||||
self.ssh.upload_file_sudo(priv_key + '\n', "/opt/amnezia/xray/xray_private.key")
|
'transport': 'xhttp',
|
||||||
self.ssh.upload_file_sudo(pub_key + '\n', "/opt/amnezia/xray/xray_public.key")
|
'security': 'tls',
|
||||||
self.ssh.upload_file_sudo(short_id + '\n', "/opt/amnezia/xray/xray_short_id.key")
|
'domain': domain,
|
||||||
# xray_uuid.key marks the install-time "default" client whose ID gets skipped on
|
'email': email,
|
||||||
# auto-import. Panel installs do not reserve such a client, so we leave it empty.
|
'path': xhttp_path,
|
||||||
self.ssh.upload_file_sudo('\n', "/opt/amnezia/xray/xray_uuid.key")
|
'mode': xhttp_mode,
|
||||||
self.ssh.upload_file_sudo("[]", "/opt/amnezia/xray/clientsTable")
|
'port': int(port),
|
||||||
|
'fingerprint': 'chrome',
|
||||||
|
'alpn': 'h2',
|
||||||
|
'site_name': domain,
|
||||||
|
'acme_method': used,
|
||||||
|
}
|
||||||
|
self.ssh.upload_file_sudo(json.dumps(meta, indent=2), f"{config_dir}/meta.json")
|
||||||
|
self.ssh.upload_file_sudo("[]", f"{config_dir}/clientsTable.json")
|
||||||
|
# Clear cached layout so we pick panel (meta.json) next.
|
||||||
|
if hasattr(self, '_cached_layout'):
|
||||||
|
delattr(self, '_cached_layout')
|
||||||
|
|
||||||
results.append("Starting container...")
|
results.append("Starting container...")
|
||||||
run_cmd = f"""docker run -d \\
|
run_cmd = f"""docker run -d \\
|
||||||
@@ -316,19 +499,26 @@ ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
|||||||
--privileged \\
|
--privileged \\
|
||||||
--cap-add=NET_ADMIN \\
|
--cap-add=NET_ADMIN \\
|
||||||
-p {port}:{port}/tcp \\
|
-p {port}:{port}/tcp \\
|
||||||
-p {port}:{port}/udp \\
|
|
||||||
-v {config_dir}:{config_dir} \\
|
-v {config_dir}:{config_dir} \\
|
||||||
--name {self.container_name} \\
|
--name {self.container_name} \\
|
||||||
{self.image_name}"""
|
{self.image_name}"""
|
||||||
|
|
||||||
_, err, code = self.ssh.run_sudo_command(run_cmd)
|
_, err, code = self.ssh.run_sudo_command(run_cmd)
|
||||||
if code != 0: raise RuntimeError(f"Failed to run container: {err}")
|
if code != 0:
|
||||||
|
raise RuntimeError(f"Failed to run container: {err}")
|
||||||
|
|
||||||
# Try to connect to network if needed
|
|
||||||
self.ssh.run_sudo_command(f"docker network connect amnezia-dns-net {self.container_name} || true")
|
self.ssh.run_sudo_command(f"docker network connect amnezia-dns-net {self.container_name} || true")
|
||||||
|
|
||||||
results.append("Xray configured and running")
|
results.append("Xray VLESS+XHTTP+TLS configured and running")
|
||||||
return {'status': 'success', 'protocol': self.protocol, 'port': port, 'log': results}
|
return {
|
||||||
|
'status': 'success',
|
||||||
|
'protocol': self.protocol,
|
||||||
|
'port': port,
|
||||||
|
'domain': domain,
|
||||||
|
'path': xhttp_path,
|
||||||
|
'acme_method': used,
|
||||||
|
'log': results,
|
||||||
|
}
|
||||||
|
|
||||||
def remove_container(self):
|
def remove_container(self):
|
||||||
self.ssh.run_sudo_command(f"docker stop {self.container_name}")
|
self.ssh.run_sudo_command(f"docker stop {self.container_name}")
|
||||||
@@ -431,28 +621,61 @@ ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _client_object(self, client_id, inbound=None):
|
||||||
|
client = {'id': client_id, 'email': client_id}
|
||||||
|
# Vision flow only for classic TCP/Reality; XHTTP must not set flow.
|
||||||
|
if inbound and self._is_reality_inbound(inbound):
|
||||||
|
network = str((inbound.get('streamSettings') or {}).get('network') or 'tcp').lower()
|
||||||
|
if network in ('tcp', 'raw', ''):
|
||||||
|
client['flow'] = 'xtls-rprx-vision'
|
||||||
|
return client
|
||||||
|
|
||||||
def _get_meta_json(self):
|
def _get_meta_json(self):
|
||||||
"""Read protocol metadata. Supports both layouts.
|
"""Read protocol metadata from meta.json and/or server.json (XHTTP+TLS or legacy Reality)."""
|
||||||
|
|
||||||
Native layout pulls keys from xray_*.key files. Panel layout reads
|
|
||||||
meta.json. Either way, port and site_name come from server.json since
|
|
||||||
that is the authoritative runtime config — meta.json may go stale if
|
|
||||||
the user edits server.json directly via the panel.
|
|
||||||
"""
|
|
||||||
config = self._get_server_json() or {}
|
config = self._get_server_json() or {}
|
||||||
|
inbound = self._get_vless_inbound(config) or {}
|
||||||
|
stream = inbound.get('streamSettings') or {}
|
||||||
|
port = inbound.get('port')
|
||||||
|
|
||||||
port = None
|
meta = {}
|
||||||
site_name = None
|
out = self._read_remote_file(f"{self._config_dir()}/meta.json")
|
||||||
rs = {}
|
if out:
|
||||||
try:
|
try:
|
||||||
ib = next(b for b in config.get('inbounds', []) if b.get('protocol') == 'vless')
|
meta = json.loads(out)
|
||||||
port = ib.get('port')
|
except Exception:
|
||||||
rs = ib.get('streamSettings', {}).get('realitySettings', {}) or {}
|
meta = {}
|
||||||
names = rs.get('serverNames') or []
|
|
||||||
if names:
|
if port is not None:
|
||||||
site_name = names[0]
|
meta['port'] = port
|
||||||
except StopIteration:
|
|
||||||
pass
|
network = str(stream.get('network') or '').lower()
|
||||||
|
security = str(stream.get('security') or '').lower()
|
||||||
|
|
||||||
|
if network in ('xhttp', 'splithttp') and security == 'tls':
|
||||||
|
xs = stream.get('xhttpSettings') or stream.get('splithttpSettings') or {}
|
||||||
|
tls = stream.get('tlsSettings') or {}
|
||||||
|
meta['transport'] = 'xhttp'
|
||||||
|
meta['security'] = 'tls'
|
||||||
|
meta['path'] = xs.get('path') or meta.get('path') or '/'
|
||||||
|
meta['mode'] = xs.get('mode') or meta.get('mode') or 'auto'
|
||||||
|
meta['domain'] = (
|
||||||
|
meta.get('domain')
|
||||||
|
or xs.get('host')
|
||||||
|
or tls.get('serverName')
|
||||||
|
or meta.get('site_name')
|
||||||
|
)
|
||||||
|
meta['site_name'] = meta.get('domain') or meta.get('site_name')
|
||||||
|
meta['fingerprint'] = meta.get('fingerprint') or 'chrome'
|
||||||
|
meta['alpn'] = meta.get('alpn') or 'h2'
|
||||||
|
return meta
|
||||||
|
|
||||||
|
# Legacy Reality
|
||||||
|
rs = stream.get('realitySettings') or {}
|
||||||
|
names = rs.get('serverNames') or []
|
||||||
|
site_name = names[0] if names else meta.get('site_name') or 'yahoo.com'
|
||||||
|
meta['transport'] = 'tcp'
|
||||||
|
meta['security'] = 'reality'
|
||||||
|
meta['site_name'] = site_name
|
||||||
|
|
||||||
if self._detect_layout() == 'native':
|
if self._detect_layout() == 'native':
|
||||||
priv = (self._read_remote_file(f"{self._config_dir()}/xray_private.key") or '').strip()
|
priv = (self._read_remote_file(f"{self._config_dir()}/xray_private.key") or '').strip()
|
||||||
@@ -465,26 +688,15 @@ ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
|||||||
sid = sids[0] if sids else ''
|
sid = sids[0] if sids else ''
|
||||||
if not pub:
|
if not pub:
|
||||||
pub = self._derive_pubkey_from_priv(priv)
|
pub = self._derive_pubkey_from_priv(priv)
|
||||||
return {
|
meta.update({
|
||||||
'private_key': priv,
|
'private_key': priv,
|
||||||
'public_key': pub,
|
'public_key': pub,
|
||||||
'short_id': sid,
|
'short_id': sid,
|
||||||
'port': port,
|
'port': port,
|
||||||
'site_name': site_name or 'yahoo.com',
|
'site_name': site_name,
|
||||||
}
|
})
|
||||||
|
return meta
|
||||||
|
|
||||||
# Panel (legacy) layout
|
|
||||||
out = self._read_remote_file(f"{self._config_dir()}/meta.json")
|
|
||||||
meta = {}
|
|
||||||
if out:
|
|
||||||
try:
|
|
||||||
meta = json.loads(out)
|
|
||||||
except Exception:
|
|
||||||
meta = {}
|
|
||||||
if port is not None:
|
|
||||||
meta['port'] = port
|
|
||||||
if site_name:
|
|
||||||
meta['site_name'] = site_name
|
|
||||||
if not meta.get('private_key'):
|
if not meta.get('private_key'):
|
||||||
meta['private_key'] = rs.get('privateKey', '')
|
meta['private_key'] = rs.get('privateKey', '')
|
||||||
if not meta.get('short_id'):
|
if not meta.get('short_id'):
|
||||||
@@ -678,38 +890,55 @@ ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
|||||||
def get_client_config(self, protocol, client_id, server_host, port):
|
def get_client_config(self, protocol, client_id, server_host, port):
|
||||||
clients = self._get_clients_table()
|
clients = self._get_clients_table()
|
||||||
client = next((c for c in clients if c['clientId'] == client_id), None)
|
client = next((c for c in clients if c['clientId'] == client_id), None)
|
||||||
if not client: return None
|
if not client:
|
||||||
|
return None
|
||||||
|
|
||||||
meta = self._get_meta_json()
|
meta = self._get_meta_json() or {}
|
||||||
if not meta: return None
|
config = self._get_server_json() or {}
|
||||||
|
inbound = self._get_vless_inbound(config) or {}
|
||||||
config = self._get_server_json()
|
|
||||||
sni = meta.get('site_name', 'yahoo.com')
|
|
||||||
if config:
|
|
||||||
try:
|
|
||||||
sni = config['inbounds'][0]['streamSettings']['realitySettings']['serverNames'][0]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Format URL
|
|
||||||
# vless://{id}@{host}:{port}?type=tcp&security=reality&pbk={public_key}&sni={sni}&fp=chrome&sid={short_id}&spx=%2F&flow=xtls-rprx-vision#{name}
|
|
||||||
|
|
||||||
name = client.get('userData', {}).get('clientName', 'vpn')
|
name = client.get('userData', {}).get('clientName', 'vpn')
|
||||||
encoded_name = urllib.parse.quote(name)
|
encoded_name = urllib.parse.quote(name)
|
||||||
|
listen_port = meta.get('port', port)
|
||||||
url = (
|
|
||||||
f"vless://{client_id}@{server_host}:{meta.get('port', port)}"
|
if self._is_xhttp_tls_inbound(inbound) or (meta.get('transport') == 'xhttp' and meta.get('security') == 'tls'):
|
||||||
f"?type=tcp&security=reality&pbk={meta['public_key']}"
|
domain = meta.get('domain') or meta.get('site_name') or server_host
|
||||||
f"&sni={sni}&fp=chrome&sid={meta['short_id']}"
|
path = meta.get('path') or '/'
|
||||||
|
mode = meta.get('mode') or 'auto'
|
||||||
|
fp = meta.get('fingerprint') or 'chrome'
|
||||||
|
alpn = meta.get('alpn') or 'h2'
|
||||||
|
# Prefer domain for TLS SNI / cert match; fall back to connect host.
|
||||||
|
host = domain or server_host
|
||||||
|
return (
|
||||||
|
f"vless://{client_id}@{host}:{listen_port}"
|
||||||
|
f"?encryption=none&security=tls&type=xhttp"
|
||||||
|
f"&path={urllib.parse.quote(path, safe='')}"
|
||||||
|
f"&mode={urllib.parse.quote(str(mode), safe='')}"
|
||||||
|
f"&host={urllib.parse.quote(domain, safe='')}"
|
||||||
|
f"&sni={urllib.parse.quote(domain, safe='')}"
|
||||||
|
f"&fp={urllib.parse.quote(fp, safe='')}"
|
||||||
|
f"&alpn={urllib.parse.quote(alpn, safe='')}"
|
||||||
|
f"#{encoded_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Legacy Reality share link
|
||||||
|
sni = meta.get('site_name', 'yahoo.com')
|
||||||
|
try:
|
||||||
|
sni = inbound['streamSettings']['realitySettings']['serverNames'][0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return (
|
||||||
|
f"vless://{client_id}@{server_host}:{listen_port}"
|
||||||
|
f"?type=tcp&security=reality&pbk={meta.get('public_key', '')}"
|
||||||
|
f"&sni={sni}&fp=chrome&sid={meta.get('short_id', '')}"
|
||||||
f"&spx=%2F&flow=xtls-rprx-vision#{encoded_name}"
|
f"&spx=%2F&flow=xtls-rprx-vision#{encoded_name}"
|
||||||
)
|
)
|
||||||
return url
|
|
||||||
|
|
||||||
def add_client(self, protocol, client_name, server_host, port):
|
def add_client(self, protocol, client_name, server_host, port):
|
||||||
client_id = str(uuid.uuid4())
|
client_id = str(uuid.uuid4())
|
||||||
|
|
||||||
config = self._get_server_json()
|
config = self._get_server_json()
|
||||||
if not config: raise RuntimeError("Xray server config not found.")
|
if not config:
|
||||||
|
raise RuntimeError("Xray server config not found.")
|
||||||
|
|
||||||
self._upgrade_config_for_stats(config, restart=False)
|
self._upgrade_config_for_stats(config, restart=False)
|
||||||
|
|
||||||
@@ -717,13 +946,8 @@ ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
|||||||
if not inbound:
|
if not inbound:
|
||||||
raise RuntimeError("Xray VLESS inbound not found.")
|
raise RuntimeError("Xray VLESS inbound not found.")
|
||||||
|
|
||||||
# Ensure clients structure exists
|
|
||||||
clients_node = inbound.setdefault('settings', {}).setdefault('clients', [])
|
clients_node = inbound.setdefault('settings', {}).setdefault('clients', [])
|
||||||
client = {
|
client = self._client_object(client_id, inbound)
|
||||||
"id": client_id,
|
|
||||||
"flow": "xtls-rprx-vision",
|
|
||||||
"email": client_id
|
|
||||||
}
|
|
||||||
if not self._xray_api_add_user(config, client):
|
if not self._xray_api_add_user(config, client):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Xray runtime API is not available for hot user updates. "
|
"Xray runtime API is not available for hot user updates. "
|
||||||
@@ -733,7 +957,6 @@ ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
|||||||
clients_node.append(client)
|
clients_node.append(client)
|
||||||
self._write_server_json(config, restart=False)
|
self._write_server_json(config, restart=False)
|
||||||
|
|
||||||
# Update table
|
|
||||||
clients_table = self._get_clients_table()
|
clients_table = self._get_clients_table()
|
||||||
clients_table.append({
|
clients_table.append({
|
||||||
'clientId': client_id,
|
'clientId': client_id,
|
||||||
@@ -758,14 +981,9 @@ ENTRYPOINT [ "dumb-init", "/opt/amnezia/start.sh" ]
|
|||||||
raise RuntimeError("Xray VLESS inbound not found.")
|
raise RuntimeError("Xray VLESS inbound not found.")
|
||||||
clients_node = inbound.setdefault('settings', {}).setdefault('clients', [])
|
clients_node = inbound.setdefault('settings', {}).setdefault('clients', [])
|
||||||
|
|
||||||
# If toggling on and not present, we can re-add it from clientsTable
|
|
||||||
if enable:
|
if enable:
|
||||||
if not any(c['id'] == client_id for c in clients_node):
|
if not any(c['id'] == client_id for c in clients_node):
|
||||||
client = {
|
client = self._client_object(client_id, inbound)
|
||||||
"id": client_id,
|
|
||||||
"flow": "xtls-rprx-vision",
|
|
||||||
"email": client_id
|
|
||||||
}
|
|
||||||
if not self._xray_api_add_user(config, client):
|
if not self._xray_api_add_user(config, client):
|
||||||
raise RuntimeError("Xray runtime API failed to enable the client without restarting the container.")
|
raise RuntimeError("Xray runtime API failed to enable the client without restarting the container.")
|
||||||
clients_node.append(client)
|
clients_node.append(client)
|
||||||
|
|||||||
@@ -210,7 +210,7 @@
|
|||||||
awg: 'AmneziaWG',
|
awg: 'AmneziaWG',
|
||||||
awg_legacy: 'AWG Legacy',
|
awg_legacy: 'AWG Legacy',
|
||||||
wireguard: 'WireGuard',
|
wireguard: 'WireGuard',
|
||||||
xray: 'Xray (VLESS-Reality)',
|
xray: 'Xray (VLESS-XHTTP-TLS)',
|
||||||
telemt: 'Telemt',
|
telemt: 'Telemt',
|
||||||
hysteria: 'Hysteria 2',
|
hysteria: 'Hysteria 2',
|
||||||
naiveproxy: 'NaiveProxy',
|
naiveproxy: 'NaiveProxy',
|
||||||
|
|||||||
+104
-2
@@ -272,7 +272,7 @@
|
|||||||
<div class="protocol-icon">{{ icon('zap') }}</div>
|
<div class="protocol-icon">{{ icon('zap') }}</div>
|
||||||
<div class="flex gap-sm" id="xray-ctrl" style="display:none!important;"></div>
|
<div class="flex gap-sm" id="xray-ctrl" style="display:none!important;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="protocol-name">Xray (VLESS-Reality)</div>
|
<div class="protocol-name">Xray (VLESS-XHTTP-TLS)</div>
|
||||||
<div class="protocol-desc">
|
<div class="protocol-desc">
|
||||||
{{ _('xray_desc') }}
|
{{ _('xray_desc') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -796,6 +796,41 @@
|
|||||||
<div class="form-hint">{{ _('nginx_install_hint') }}</div>
|
<div class="form-hint">{{ _('nginx_install_hint') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="xrayOptions"
|
||||||
|
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" id="xrayPortsWarning" 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);">
|
||||||
|
{{ _('xray_ports_warning_cf') }}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{{ _('xray_domain') }} *</label>
|
||||||
|
<input class="form-input" type="text" id="installXrayDomain" placeholder="vpn.example.com" oninput="updateXrayDnsHint()">
|
||||||
|
<div class="form-hint" id="xrayDnsHint"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{{ _('xray_email') }} *</label>
|
||||||
|
<input class="form-input" type="email" id="installXrayEmail" placeholder="admin@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{{ _('xray_acme_method') }}</label>
|
||||||
|
<div style="display:flex; flex-direction:column; gap:8px;">
|
||||||
|
<label style="display:flex; align-items:flex-start; gap:8px; cursor:pointer;">
|
||||||
|
<input type="radio" name="xrayAcmeMethod" value="cloudflare" checked onchange="updateXrayAcmeUi()">
|
||||||
|
<span>{{ _('xray_acme_cloudflare') }}</span>
|
||||||
|
</label>
|
||||||
|
<label style="display:flex; align-items:flex-start; gap:8px; cursor:pointer;">
|
||||||
|
<input type="radio" name="xrayAcmeMethod" value="http" onchange="updateXrayAcmeUi()">
|
||||||
|
<span>{{ _('xray_acme_http') }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="xrayCfTokenGroup">
|
||||||
|
<label class="form-label">{{ _('xray_cf_token') }} *</label>
|
||||||
|
<input class="form-input" type="password" id="installXrayCfToken" autocomplete="off" placeholder="Cloudflare API Token">
|
||||||
|
<div class="form-hint">{{ _('xray_cf_token_hint') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-hint">{{ _('xray_install_hint') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="hysteriaOptions"
|
<div id="hysteriaOptions"
|
||||||
style="display:none; padding: var(--space-md); background: rgba(0,0,0,0.03); border-radius: var(--radius-md); margin-bottom: var(--space-md);">
|
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);">
|
<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);">
|
||||||
@@ -1233,7 +1268,7 @@
|
|||||||
{ proto: 'awg2', category: 'protocols', icon: 'sparkles', title: 'AmneziaWG 2.0', descKey: 'awg_desc', badge: 'NEW' },
|
{ proto: 'awg2', category: 'protocols', icon: 'sparkles', title: 'AmneziaWG 2.0', descKey: 'awg_desc', badge: 'NEW' },
|
||||||
{ proto: 'awg', category: 'protocols', icon: 'shield', title: 'AmneziaWG', descKey: 'awg_desc' },
|
{ proto: 'awg', category: 'protocols', icon: 'shield', title: 'AmneziaWG', descKey: 'awg_desc' },
|
||||||
{ proto: 'awg_legacy', category: 'protocols', icon: 'radio', title: 'AmneziaWG Legacy', descKey: 'awg_legacy_desc' },
|
{ proto: 'awg_legacy', category: 'protocols', icon: 'radio', title: 'AmneziaWG Legacy', descKey: 'awg_legacy_desc' },
|
||||||
{ proto: 'xray', category: 'protocols', icon: 'zap', title: 'Xray (VLESS-Reality)', descKey: 'xray_desc' },
|
{ proto: 'xray', category: 'protocols', icon: 'zap', title: 'Xray (VLESS-XHTTP-TLS)', descKey: 'xray_desc' },
|
||||||
{ proto: 'telemt', category: 'protocols', icon: 'plane', title: 'Telemt (Telegram Proxy)', descKey: 'telemt_desc' },
|
{ proto: 'telemt', category: 'protocols', icon: 'plane', title: 'Telemt (Telegram Proxy)', descKey: 'telemt_desc' },
|
||||||
{ proto: 'mieru', category: 'protocols', icon: 'zap', title: 'Mieru', descKey: 'mieru_desc', badge: 'v3.28.0' },
|
{ proto: 'mieru', category: 'protocols', icon: 'zap', title: 'Mieru', descKey: 'mieru_desc', badge: 'v3.28.0' },
|
||||||
{ proto: 'hysteria', category: 'protocols', icon: 'refresh', title: 'Hysteria 2', descKey: 'hysteria_desc' },
|
{ proto: 'hysteria', category: 'protocols', icon: 'refresh', title: 'Hysteria 2', descKey: 'hysteria_desc' },
|
||||||
@@ -1946,6 +1981,16 @@
|
|||||||
if (protoBase(proto) === 'hysteria' && info.domain) {
|
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>`;
|
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) === 'xray' && info.domain) {
|
||||||
|
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('xray_domain')}</span><span class="protocol-info-value">${info.domain}</span></div>`;
|
||||||
|
}
|
||||||
|
if (protoBase(proto) === 'xray' && info.acme_method) {
|
||||||
|
const acmeLabel = info.acme_method === 'cloudflare' ? _('xray_acme_cloudflare_short') : _('xray_acme_http_short');
|
||||||
|
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('xray_acme_method')}</span><span class="protocol-info-value">${acmeLabel}</span></div>`;
|
||||||
|
}
|
||||||
|
if (protoBase(proto) === 'xray' && (info.transport || info.security)) {
|
||||||
|
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('protocol_label')}</span><span class="protocol-info-value">VLESS · ${(info.transport || 'xhttp').toUpperCase()}+${(info.security || 'tls').toUpperCase()}</span></div>`;
|
||||||
|
}
|
||||||
if (protoBase(proto) === 'naiveproxy' && info.domain) {
|
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>`;
|
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('naiveproxy_domain')}</span><span class="protocol-info-value">${info.domain}</span></div>`;
|
||||||
}
|
}
|
||||||
@@ -2394,6 +2439,34 @@
|
|||||||
hint.innerHTML = `${_('naiveproxy_dns_hint')} <code>A ${domain} ${SERVER_HOST}</code>`;
|
hint.innerHTML = `${_('naiveproxy_dns_hint')} <code>A ${domain} ${SERVER_HOST}</code>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getXrayAcmeMethod() {
|
||||||
|
const el = document.querySelector('input[name="xrayAcmeMethod"]:checked');
|
||||||
|
return el ? el.value : 'cloudflare';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateXrayAcmeUi() {
|
||||||
|
const method = getXrayAcmeMethod();
|
||||||
|
const cfGroup = document.getElementById('xrayCfTokenGroup');
|
||||||
|
const warn = document.getElementById('xrayPortsWarning');
|
||||||
|
if (cfGroup) cfGroup.style.display = method === 'cloudflare' ? '' : 'none';
|
||||||
|
if (warn) warn.textContent = method === 'cloudflare' ? _('xray_ports_warning_cf') : _('xray_ports_warning');
|
||||||
|
updateXrayDnsHint();
|
||||||
|
const portHint = document.getElementById('installPortHint');
|
||||||
|
if (portHint && protoBase(currentInstallProto) === 'xray' && !currentInstallAnother) {
|
||||||
|
portHint.textContent = method === 'cloudflare' ? _('port_xray_hint_cf') : _('port_xray_hint');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateXrayDnsHint() {
|
||||||
|
const input = document.getElementById('installXrayDomain');
|
||||||
|
const hint = document.getElementById('xrayDnsHint');
|
||||||
|
if (!input || !hint) return;
|
||||||
|
const domain = (input.value || '').trim() || 'vpn.example.com';
|
||||||
|
const method = getXrayAcmeMethod();
|
||||||
|
const prefix = method === 'cloudflare' ? _('xray_dns_hint_cf') : _('xray_dns_hint');
|
||||||
|
hint.innerHTML = `${prefix} <code>A ${domain} ${SERVER_HOST}</code>`;
|
||||||
|
}
|
||||||
|
|
||||||
function openInstallModal(proto, installAnother = false) {
|
function openInstallModal(proto, installAnother = false) {
|
||||||
const base = protoBase(proto);
|
const base = protoBase(proto);
|
||||||
currentInstallProto = installAnother ? base : proto;
|
currentInstallProto = installAnother ? base : proto;
|
||||||
@@ -2412,6 +2485,7 @@
|
|||||||
const hysteriaOpts = document.getElementById('hysteriaOptions');
|
const hysteriaOpts = document.getElementById('hysteriaOptions');
|
||||||
const naiveproxyOpts = document.getElementById('naiveproxyOptions');
|
const naiveproxyOpts = document.getElementById('naiveproxyOptions');
|
||||||
const mieruOpts = document.getElementById('mieruOptions');
|
const mieruOpts = document.getElementById('mieruOptions');
|
||||||
|
const xrayOpts = document.getElementById('xrayOptions');
|
||||||
|
|
||||||
telemtOpts.style.display = 'none';
|
telemtOpts.style.display = 'none';
|
||||||
socks5Opts.style.display = 'none';
|
socks5Opts.style.display = 'none';
|
||||||
@@ -2420,6 +2494,7 @@
|
|||||||
hysteriaOpts.style.display = 'none';
|
hysteriaOpts.style.display = 'none';
|
||||||
if (naiveproxyOpts) naiveproxyOpts.style.display = 'none';
|
if (naiveproxyOpts) naiveproxyOpts.style.display = 'none';
|
||||||
if (mieruOpts) mieruOpts.style.display = 'none';
|
if (mieruOpts) mieruOpts.style.display = 'none';
|
||||||
|
if (xrayOpts) xrayOpts.style.display = 'none';
|
||||||
if (portGroup) portGroup.style.display = '';
|
if (portGroup) portGroup.style.display = '';
|
||||||
|
|
||||||
if (base === 'dns') {
|
if (base === 'dns') {
|
||||||
@@ -2432,6 +2507,12 @@
|
|||||||
portInput.disabled = false;
|
portInput.disabled = false;
|
||||||
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 443) : '443';
|
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 443) : '443';
|
||||||
portHint.textContent = currentInstallAnother ? _('port_next_instance_hint') : _('port_xray_hint');
|
portHint.textContent = currentInstallAnother ? _('port_next_instance_hint') : _('port_xray_hint');
|
||||||
|
if (xrayOpts) xrayOpts.style.display = 'block';
|
||||||
|
const xrDomain = document.getElementById('installXrayDomain');
|
||||||
|
const xrEmail = document.getElementById('installXrayEmail');
|
||||||
|
if (xrDomain && !xrDomain.value && SERVER_SSL_DOMAIN) xrDomain.value = SERVER_SSL_DOMAIN;
|
||||||
|
if (xrEmail && !xrEmail.value && SERVER_SSL_EMAIL) xrEmail.value = SERVER_SSL_EMAIL;
|
||||||
|
updateXrayAcmeUi();
|
||||||
} else if (base === 'telemt') {
|
} else if (base === 'telemt') {
|
||||||
portLabel.textContent = _('port') + ' (TCP)';
|
portLabel.textContent = _('port') + ' (TCP)';
|
||||||
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 443) : '443';
|
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 443) : '443';
|
||||||
@@ -2513,6 +2594,20 @@
|
|||||||
|
|
||||||
async function installProtocol() {
|
async function installProtocol() {
|
||||||
const port = document.getElementById('installPort').value;
|
const port = document.getElementById('installPort').value;
|
||||||
|
if (protoBase(currentInstallProto) === 'xray') {
|
||||||
|
const xrDomain = (document.getElementById('installXrayDomain')?.value || '').trim();
|
||||||
|
const xrEmail = (document.getElementById('installXrayEmail')?.value || '').trim();
|
||||||
|
const acme = getXrayAcmeMethod();
|
||||||
|
const cfToken = (document.getElementById('installXrayCfToken')?.value || '').trim();
|
||||||
|
if (!xrDomain || !xrEmail) {
|
||||||
|
showToast(_('xray_domain') + ' / ' + _('xray_email'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (acme === 'cloudflare' && !cfToken) {
|
||||||
|
showToast(_('xray_cf_token'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const btn = document.getElementById('installBtn');
|
const btn = document.getElementById('installBtn');
|
||||||
const text = document.getElementById('installBtnText');
|
const text = document.getElementById('installBtnText');
|
||||||
const spinner = document.getElementById('installSpinner');
|
const spinner = document.getElementById('installSpinner');
|
||||||
@@ -2571,6 +2666,13 @@
|
|||||||
}
|
}
|
||||||
params.hysteria_domain = document.getElementById('installHysteriaDomain').value.trim();
|
params.hysteria_domain = document.getElementById('installHysteriaDomain').value.trim();
|
||||||
params.hysteria_email = document.getElementById('installHysteriaEmail').value.trim();
|
params.hysteria_email = document.getElementById('installHysteriaEmail').value.trim();
|
||||||
|
} else if (protoBase(currentInstallProto) === 'xray') {
|
||||||
|
params.xray_domain = (document.getElementById('installXrayDomain')?.value || '').trim();
|
||||||
|
params.xray_email = (document.getElementById('installXrayEmail')?.value || '').trim();
|
||||||
|
params.xray_acme_method = getXrayAcmeMethod();
|
||||||
|
if (params.xray_acme_method === 'cloudflare') {
|
||||||
|
params.xray_cf_token = (document.getElementById('installXrayCfToken')?.value || '').trim();
|
||||||
|
}
|
||||||
} else if (protoBase(currentInstallProto) === 'naiveproxy') {
|
} else if (protoBase(currentInstallProto) === 'naiveproxy') {
|
||||||
params.port = '443';
|
params.port = '443';
|
||||||
params.naiveproxy_domain = document.getElementById('installNaiveproxyDomain').value.trim();
|
params.naiveproxy_domain = document.getElementById('installNaiveproxyDomain').value.trim();
|
||||||
|
|||||||
@@ -1563,7 +1563,7 @@
|
|||||||
awg: 'AmneziaWG',
|
awg: 'AmneziaWG',
|
||||||
awg_legacy: 'AWG Legacy',
|
awg_legacy: 'AWG Legacy',
|
||||||
wireguard: 'WireGuard',
|
wireguard: 'WireGuard',
|
||||||
xray: 'Xray (VLESS-Reality)',
|
xray: 'Xray (VLESS-XHTTP-TLS)',
|
||||||
telemt: 'Telemt',
|
telemt: 'Telemt',
|
||||||
hysteria: 'Hysteria 2',
|
hysteria: 'Hysteria 2',
|
||||||
naiveproxy: 'NaiveProxy',
|
naiveproxy: 'NaiveProxy',
|
||||||
|
|||||||
@@ -637,7 +637,7 @@
|
|||||||
awg: 'AmneziaWG',
|
awg: 'AmneziaWG',
|
||||||
awg_legacy: 'AWG Legacy',
|
awg_legacy: 'AWG Legacy',
|
||||||
wireguard: 'WireGuard',
|
wireguard: 'WireGuard',
|
||||||
xray: 'Xray (VLESS-Reality)',
|
xray: 'Xray (VLESS-XHTTP-TLS)',
|
||||||
telemt: 'Telemt',
|
telemt: 'Telemt',
|
||||||
hysteria: 'Hysteria 2',
|
hysteria: 'Hysteria 2',
|
||||||
naiveproxy: 'NaiveProxy',
|
naiveproxy: 'NaiveProxy',
|
||||||
|
|||||||
+17
-2
@@ -72,7 +72,7 @@
|
|||||||
"docker_not_installed": "Docker not installed",
|
"docker_not_installed": "Docker not installed",
|
||||||
"awg_desc": "A newer version of the protocol based on awg-go. Supports advanced obfuscation with S3, S4 parameters.",
|
"awg_desc": "A newer version of the protocol based on awg-go. Supports advanced obfuscation with S3, S4 parameters.",
|
||||||
"awg_legacy_desc": "Original AWG version based on WireGuard kernel. Compatible with older client versions.",
|
"awg_legacy_desc": "Original AWG version based on WireGuard kernel. Compatible with older client versions.",
|
||||||
"xray_desc": "Modern protocol that masks traffic as regular web traffic (XTLS-Reality). Resistant to deep packet analysis.",
|
"xray_desc": "VLESS over XHTTP+TLS — traffic looks like normal HTTPS/HTTP2. Tuned for DPI resistance (no Vision flow). Needs a domain; SSL via Cloudflare DNS or HTTP-01.",
|
||||||
"wireguard_desc": "Standard and fastest VPN protocol. Supported natively on all modern OS, but easily detected by DPI.",
|
"wireguard_desc": "Standard and fastest VPN protocol. Supported natively on all modern OS, but easily detected by DPI.",
|
||||||
"not_checked": "Not checked",
|
"not_checked": "Not checked",
|
||||||
"connections": "Connections",
|
"connections": "Connections",
|
||||||
@@ -82,7 +82,22 @@
|
|||||||
"no_connections_desc": "Add your first connection to generate a VPN configuration",
|
"no_connections_desc": "Add your first connection to generate a VPN configuration",
|
||||||
"install_protocol": "Install protocol",
|
"install_protocol": "Install protocol",
|
||||||
"port_default_hint": "Default port: 55424. Make sure it\u0027s not busy",
|
"port_default_hint": "Default port: 55424. Make sure it\u0027s not busy",
|
||||||
"port_xray_hint": "Default port: 443 (recommended for Xray). Make sure it\u0027s not taken by another web server.",
|
"port_xray_hint": "Default 443/TCP. Prefer Cloudflare DNS ACME — port 80 is not needed. Point domain A-record (DNS only / grey cloud) to this server.",
|
||||||
|
"port_xray_hint_cf": "Default 443/TCP. Certificate via Cloudflare DNS — port 80 is not used. Keep Cloudflare proxy off (grey cloud).",
|
||||||
|
"xray_domain": "Domain",
|
||||||
|
"xray_email": "Let\u0027s Encrypt email",
|
||||||
|
"xray_dns_hint": "Create DNS record:",
|
||||||
|
"xray_dns_hint_cf": "Domain must be on Cloudflare. A-record (DNS only):",
|
||||||
|
"xray_install_hint": "Installs VLESS + XHTTP + TLS (Xray-core). Prefer Cloudflare API token (DNS-01) so TCP 80 stays free.",
|
||||||
|
"xray_ports_warning": "TCP 80 must be free during install (Let\u0027s Encrypt HTTP-01). Reinstall replaces the previous Xray stack.",
|
||||||
|
"xray_ports_warning_cf": "Cloudflare DNS ACME does not use port 80. Token needs Zone → DNS → Edit. Keep proxy off (grey cloud). Reinstall replaces the previous Xray stack.",
|
||||||
|
"xray_acme_method": "Certificate method",
|
||||||
|
"xray_acme_cloudflare": "Cloudflare DNS (API token) — recommended, no port 80",
|
||||||
|
"xray_acme_http": "HTTP-01 — needs free TCP 80",
|
||||||
|
"xray_acme_cloudflare_short": "Cloudflare DNS",
|
||||||
|
"xray_acme_http_short": "HTTP-01",
|
||||||
|
"xray_cf_token": "Cloudflare API token",
|
||||||
|
"xray_cf_token_hint": "Create a token with Zone:DNS:Edit for this domain zone. Token is used only to issue the cert and is not shown again.",
|
||||||
"reinstall": "Reinstall",
|
"reinstall": "Reinstall",
|
||||||
"uninstall_confirm": "Uninstall {}? All connections and configurations will be lost.",
|
"uninstall_confirm": "Uninstall {}? All connections and configurations will be lost.",
|
||||||
"stop_container_confirm": "Stop container {}?",
|
"stop_container_confirm": "Stop container {}?",
|
||||||
|
|||||||
+17
-2
@@ -71,7 +71,7 @@
|
|||||||
"docker_not_installed": "داکر نصب نیست",
|
"docker_not_installed": "داکر نصب نیست",
|
||||||
"awg_desc": "نسخه جدید پروتکل بر پایه awg-go. پشتیبانی از مبهمسازی پیشرفته (S3, S4).",
|
"awg_desc": "نسخه جدید پروتکل بر پایه awg-go. پشتیبانی از مبهمسازی پیشرفته (S3, S4).",
|
||||||
"awg_legacy_desc": "نسخه اصلی AWG. سازگار با نسخههای قدیمی کلاینت.",
|
"awg_legacy_desc": "نسخه اصلی AWG. سازگار با نسخههای قدیمی کلاینت.",
|
||||||
"xray_desc": "تغییر ظاهر ترافیک به ترافیک معمولی وب (XTLS-Reality). مقاوم در برابر فیلترینگ شدید.",
|
"xray_desc": "VLESS over XHTTP+TLS — looks like normal HTTPS/HTTP2. Needs a domain + Let\u0027s Encrypt.",
|
||||||
"not_checked": "بررسی نشده",
|
"not_checked": "بررسی نشده",
|
||||||
"connections": "اتصالها",
|
"connections": "اتصالها",
|
||||||
"add": "افزودن",
|
"add": "افزودن",
|
||||||
@@ -80,7 +80,22 @@
|
|||||||
"no_connections_desc": "اولین اتصال خود را برای ایجاد پیکربندی VPN اضافه کنید",
|
"no_connections_desc": "اولین اتصال خود را برای ایجاد پیکربندی VPN اضافه کنید",
|
||||||
"install_protocol": "نصب پروتکل",
|
"install_protocol": "نصب پروتکل",
|
||||||
"port_default_hint": "پورت پیشفرض: 55424. مطمئن شوید این پورت آزاد است.",
|
"port_default_hint": "پورت پیشفرض: 55424. مطمئن شوید این پورت آزاد است.",
|
||||||
"port_xray_hint": "پورت پیشنهادی: 443. مطمئن شوید توسط وبسرور دیگری اشغال نشده باشد.",
|
"port_xray_hint": "Default 443/TCP. Prefer Cloudflare DNS ACME — port 80 is not needed.",
|
||||||
|
"port_xray_hint_cf": "Default 443/TCP. Certificate via Cloudflare DNS — port 80 is not used.",
|
||||||
|
"xray_domain": "Domain",
|
||||||
|
"xray_email": "Let\u0027s Encrypt email",
|
||||||
|
"xray_dns_hint": "Create DNS record:",
|
||||||
|
"xray_dns_hint_cf": "Domain must be on Cloudflare. A-record (DNS only):",
|
||||||
|
"xray_install_hint": "Installs VLESS + XHTTP + TLS. Prefer Cloudflare API token (DNS-01) so TCP 80 stays free.",
|
||||||
|
"xray_ports_warning": "TCP 80 must be free during install (Let\u0027s Encrypt HTTP-01).",
|
||||||
|
"xray_ports_warning_cf": "Cloudflare DNS ACME does not use port 80. Token needs Zone → DNS → Edit. Keep proxy off (grey cloud).",
|
||||||
|
"xray_acme_method": "Certificate method",
|
||||||
|
"xray_acme_cloudflare": "Cloudflare DNS (API token) — recommended, no port 80",
|
||||||
|
"xray_acme_http": "HTTP-01 — needs free TCP 80",
|
||||||
|
"xray_acme_cloudflare_short": "Cloudflare DNS",
|
||||||
|
"xray_acme_http_short": "HTTP-01",
|
||||||
|
"xray_cf_token": "Cloudflare API token",
|
||||||
|
"xray_cf_token_hint": "Token with Zone:DNS:Edit for this domain zone. Used only to issue the cert.",
|
||||||
"reinstall": "نصب مجدد",
|
"reinstall": "نصب مجدد",
|
||||||
"uninstall_confirm": "حذف نصب {}؟ تمام اتصالها و پیکربندیها از بین خواهند رفت.",
|
"uninstall_confirm": "حذف نصب {}؟ تمام اتصالها و پیکربندیها از بین خواهند رفت.",
|
||||||
"stop_container_confirm": "توقف کانتینر {}؟",
|
"stop_container_confirm": "توقف کانتینر {}؟",
|
||||||
|
|||||||
+17
-2
@@ -71,7 +71,7 @@
|
|||||||
"docker_not_installed": "Docker non installé",
|
"docker_not_installed": "Docker non installé",
|
||||||
"awg_desc": "Version moderne basée sur awg-go. Obfuscation avancée (S3, S4).",
|
"awg_desc": "Version moderne basée sur awg-go. Obfuscation avancée (S3, S4).",
|
||||||
"awg_legacy_desc": "Version AWG originale. Compatible avec les anciens clients.",
|
"awg_legacy_desc": "Version AWG originale. Compatible avec les anciens clients.",
|
||||||
"xray_desc": "Masque le trafic en trafic web normal (XTLS-Reality). Résiste au DPI.",
|
"xray_desc": "VLESS over XHTTP+TLS — looks like normal HTTPS/HTTP2. Needs a domain + Let\u0027s Encrypt.",
|
||||||
"not_checked": "Non vérifié",
|
"not_checked": "Non vérifié",
|
||||||
"connections": "Connexions",
|
"connections": "Connexions",
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
@@ -80,7 +80,22 @@
|
|||||||
"no_connections_desc": "Ajoutez votre première connexion pour générer un fichier VPN",
|
"no_connections_desc": "Ajoutez votre première connexion pour générer un fichier VPN",
|
||||||
"install_protocol": "Installer le protocole",
|
"install_protocol": "Installer le protocole",
|
||||||
"port_default_hint": "Port par défaut : 55424. Assurez-vous qu\u0027il est libre.",
|
"port_default_hint": "Port par défaut : 55424. Assurez-vous qu\u0027il est libre.",
|
||||||
"port_xray_hint": "Port recommandé : 443. Assurez-vous qu\u0027il n\u0027est pas utilisé par un serveur web.",
|
"port_xray_hint": "Default 443/TCP. Prefer Cloudflare DNS ACME — port 80 is not needed.",
|
||||||
|
"port_xray_hint_cf": "Default 443/TCP. Certificate via Cloudflare DNS — port 80 is not used.",
|
||||||
|
"xray_domain": "Domain",
|
||||||
|
"xray_email": "Let\u0027s Encrypt email",
|
||||||
|
"xray_dns_hint": "Create DNS record:",
|
||||||
|
"xray_dns_hint_cf": "Domain must be on Cloudflare. A-record (DNS only):",
|
||||||
|
"xray_install_hint": "Installs VLESS + XHTTP + TLS. Prefer Cloudflare API token (DNS-01) so TCP 80 stays free.",
|
||||||
|
"xray_ports_warning": "TCP 80 must be free during install (Let\u0027s Encrypt HTTP-01).",
|
||||||
|
"xray_ports_warning_cf": "Cloudflare DNS ACME does not use port 80. Token needs Zone → DNS → Edit. Keep proxy off (grey cloud).",
|
||||||
|
"xray_acme_method": "Certificate method",
|
||||||
|
"xray_acme_cloudflare": "Cloudflare DNS (API token) — recommended, no port 80",
|
||||||
|
"xray_acme_http": "HTTP-01 — needs free TCP 80",
|
||||||
|
"xray_acme_cloudflare_short": "Cloudflare DNS",
|
||||||
|
"xray_acme_http_short": "HTTP-01",
|
||||||
|
"xray_cf_token": "Cloudflare API token",
|
||||||
|
"xray_cf_token_hint": "Token with Zone:DNS:Edit for this domain zone. Used only to issue the cert.",
|
||||||
"reinstall": "Réinstaller",
|
"reinstall": "Réinstaller",
|
||||||
"uninstall_confirm": "Désinstaller {} ? Toutes les données seront perdues.",
|
"uninstall_confirm": "Désinstaller {} ? Toutes les données seront perdues.",
|
||||||
"stop_container_confirm": "Arrêter le conteneur {} ?",
|
"stop_container_confirm": "Arrêter le conteneur {} ?",
|
||||||
|
|||||||
+17
-2
@@ -72,7 +72,7 @@
|
|||||||
"docker_not_installed": "Docker не установлен",
|
"docker_not_installed": "Docker не установлен",
|
||||||
"awg_desc": "Новая версия протокола на основе awg-go. Поддерживает расширенную обфускацию с параметрами S3, S4.",
|
"awg_desc": "Новая версия протокола на основе awg-go. Поддерживает расширенную обфускацию с параметрами S3, S4.",
|
||||||
"awg_legacy_desc": "Оригинальная версия AWG на базе ядра WireGuard. Совместима с клиентами старых версий.",
|
"awg_legacy_desc": "Оригинальная версия AWG на базе ядра WireGuard. Совместима с клиентами старых версий.",
|
||||||
"xray_desc": "Современный протокол с маскировкой под обычный веб-трафик (XTLS-Reality). Устойчив к глубокому анализу пакетов.",
|
"xray_desc": "VLESS поверх XHTTP+TLS — трафик как обычный HTTPS/HTTP2. Заточено под DPI (без Vision flow). Нужен домен; SSL через Cloudflare DNS или HTTP-01.",
|
||||||
"wireguard_desc": "Стандартный и самый быстрый VPN-протокол. Встроен во все современные ОС, но легко блокируется DPI.",
|
"wireguard_desc": "Стандартный и самый быстрый VPN-протокол. Встроен во все современные ОС, но легко блокируется DPI.",
|
||||||
"not_checked": "Не проверено",
|
"not_checked": "Не проверено",
|
||||||
"connections": "Подключения",
|
"connections": "Подключения",
|
||||||
@@ -82,7 +82,22 @@
|
|||||||
"no_connections_desc": "Добавьте первое подключение для генерации VPN конфигурации",
|
"no_connections_desc": "Добавьте первое подключение для генерации VPN конфигурации",
|
||||||
"install_protocol": "Установить протокол",
|
"install_protocol": "Установить протокол",
|
||||||
"port_default_hint": "Порт по умолчанию: 55424. Убедитесь, что он не занят",
|
"port_default_hint": "Порт по умолчанию: 55424. Убедитесь, что он не занят",
|
||||||
"port_xray_hint": "Порт по умолчанию: 443 (рекомендуется для Xray). Убедитесь, что он не занят другим веб-сервером.",
|
"port_xray_hint": "По умолчанию 443/TCP. Лучше Cloudflare DNS ACME — порт 80 не нужен. A-запись домена (только DNS / серое облако) на этот сервер.",
|
||||||
|
"port_xray_hint_cf": "По умолчанию 443/TCP. Сертификат через Cloudflare DNS — порт 80 не используется. Прокси Cloudflare выключите (серое облако).",
|
||||||
|
"xray_domain": "Домен",
|
||||||
|
"xray_email": "Email для Let\u0027s Encrypt",
|
||||||
|
"xray_dns_hint": "Создайте DNS-запись:",
|
||||||
|
"xray_dns_hint_cf": "Домен должен быть в Cloudflare. A-запись (только DNS):",
|
||||||
|
"xray_install_hint": "Ставит VLESS + XHTTP + TLS (Xray-core). Рекомендуется токен Cloudflare (DNS-01) — TCP 80 не занимается.",
|
||||||
|
"xray_ports_warning": "На время установки TCP 80 должен быть свободен (Let\u0027s Encrypt HTTP-01). Переустановка заменяет предыдущий Xray.",
|
||||||
|
"xray_ports_warning_cf": "Cloudflare DNS ACME не использует порт 80. Токену нужно Zone → DNS → Edit. Прокси выключите (серое облако). Переустановка заменяет предыдущий Xray.",
|
||||||
|
"xray_acme_method": "Способ выпуска SSL",
|
||||||
|
"xray_acme_cloudflare": "Cloudflare DNS (API-токен) — рекомендуется, без порта 80",
|
||||||
|
"xray_acme_http": "HTTP-01 — нужен свободный TCP 80",
|
||||||
|
"xray_acme_cloudflare_short": "Cloudflare DNS",
|
||||||
|
"xray_acme_http_short": "HTTP-01",
|
||||||
|
"xray_cf_token": "API-токен Cloudflare",
|
||||||
|
"xray_cf_token_hint": "Создайте токен с правом Zone:DNS:Edit для зоны домена. Токен нужен только для выпуска сертификата и больше не показывается.",
|
||||||
"reinstall": "Переустановить",
|
"reinstall": "Переустановить",
|
||||||
"uninstall_confirm": "Удалить {}? Все подключения и конфигурации будут потеряны.",
|
"uninstall_confirm": "Удалить {}? Все подключения и конфигурации будут потеряны.",
|
||||||
"stop_container_confirm": "Остановить контейнер {}?",
|
"stop_container_confirm": "Остановить контейнер {}?",
|
||||||
|
|||||||
+17
-2
@@ -71,7 +71,7 @@
|
|||||||
"docker_not_installed": "Docker 未安装",
|
"docker_not_installed": "Docker 未安装",
|
||||||
"awg_desc": "基于 awg-go 的新版协议。支持 S3, S4 高级混淆。",
|
"awg_desc": "基于 awg-go 的新版协议。支持 S3, S4 高级混淆。",
|
||||||
"awg_legacy_desc": "原始 AWG 版本。兼容旧版客户端。",
|
"awg_legacy_desc": "原始 AWG 版本。兼容旧版客户端。",
|
||||||
"xray_desc": "将流量伪装成普通网页流量 (XTLS-Reality),抗封锁能力强。",
|
"xray_desc": "VLESS over XHTTP+TLS — looks like normal HTTPS/HTTP2. Needs a domain + Let\u0027s Encrypt.",
|
||||||
"not_checked": "未检查",
|
"not_checked": "未检查",
|
||||||
"connections": "连接",
|
"connections": "连接",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
@@ -80,7 +80,22 @@
|
|||||||
"no_connections_desc": "添加首个连接以生成 VPN 配置文件",
|
"no_connections_desc": "添加首个连接以生成 VPN 配置文件",
|
||||||
"install_protocol": "安装协议",
|
"install_protocol": "安装协议",
|
||||||
"port_default_hint": "默认端口: 55424。请确保端口未被占用。",
|
"port_default_hint": "默认端口: 55424。请确保端口未被占用。",
|
||||||
"port_xray_hint": "推荐端口: 443。请确保未被其他 Web 服务器使用。",
|
"port_xray_hint": "Default 443/TCP. Prefer Cloudflare DNS ACME — port 80 is not needed.",
|
||||||
|
"port_xray_hint_cf": "Default 443/TCP. Certificate via Cloudflare DNS — port 80 is not used.",
|
||||||
|
"xray_domain": "Domain",
|
||||||
|
"xray_email": "Let\u0027s Encrypt email",
|
||||||
|
"xray_dns_hint": "Create DNS record:",
|
||||||
|
"xray_dns_hint_cf": "Domain must be on Cloudflare. A-record (DNS only):",
|
||||||
|
"xray_install_hint": "Installs VLESS + XHTTP + TLS. Prefer Cloudflare API token (DNS-01) so TCP 80 stays free.",
|
||||||
|
"xray_ports_warning": "TCP 80 must be free during install (Let\u0027s Encrypt HTTP-01).",
|
||||||
|
"xray_ports_warning_cf": "Cloudflare DNS ACME does not use port 80. Token needs Zone → DNS → Edit. Keep proxy off (grey cloud).",
|
||||||
|
"xray_acme_method": "Certificate method",
|
||||||
|
"xray_acme_cloudflare": "Cloudflare DNS (API token) — recommended, no port 80",
|
||||||
|
"xray_acme_http": "HTTP-01 — needs free TCP 80",
|
||||||
|
"xray_acme_cloudflare_short": "Cloudflare DNS",
|
||||||
|
"xray_acme_http_short": "HTTP-01",
|
||||||
|
"xray_cf_token": "Cloudflare API token",
|
||||||
|
"xray_cf_token_hint": "Token with Zone:DNS:Edit for this domain zone. Used only to issue the cert.",
|
||||||
"reinstall": "重新安装",
|
"reinstall": "重新安装",
|
||||||
"uninstall_confirm": "确定卸载 {}?所有连接和配置都将丢失。",
|
"uninstall_confirm": "确定卸载 {}?所有连接和配置都将丢失。",
|
||||||
"stop_container_confirm": "停止容器 {}?",
|
"stop_container_confirm": "停止容器 {}?",
|
||||||
|
|||||||
Reference in New Issue
Block a user