Improve Hysteria for Happ: BBR, password auth, salamander, UDP 443.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,8 +2,8 @@
|
|||||||
Hysteria 2 protocol manager — teddysun/hysteria Docker image.
|
Hysteria 2 protocol manager — teddysun/hysteria Docker image.
|
||||||
|
|
||||||
Installs Hysteria with Let's Encrypt TLS for an admin-provided domain
|
Installs Hysteria with Let's Encrypt TLS for an admin-provided domain
|
||||||
(certbot standalone on port 80), then runs the UDP QUIC server.
|
(certbot standalone on port 80), host networking, BBR, and salamander obfs.
|
||||||
Clients use userpass auth; share links are hysteria2:// URIs.
|
Clients use password auth (Happ/INCY-compatible); share links are hy2:// URIs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -34,7 +34,7 @@ class HysteriaManager:
|
|||||||
IMAGE_NAME = 'teddysun/hysteria:latest'
|
IMAGE_NAME = 'teddysun/hysteria:latest'
|
||||||
CERTBOT_IMAGE = 'certbot/certbot:latest'
|
CERTBOT_IMAGE = 'certbot/certbot:latest'
|
||||||
BASE_DIR = '/opt/amnezia/hysteria'
|
BASE_DIR = '/opt/amnezia/hysteria'
|
||||||
DEFAULT_PORT = 8998
|
DEFAULT_PORT = 443
|
||||||
|
|
||||||
def __init__(self, ssh, protocol='hysteria'):
|
def __init__(self, ssh, protocol='hysteria'):
|
||||||
self.ssh = ssh
|
self.ssh = ssh
|
||||||
@@ -98,7 +98,7 @@ class HysteriaManager:
|
|||||||
meta = self._read_metadata() if exists else {}
|
meta = self._read_metadata() if exists else {}
|
||||||
clients = self._read_clients() if exists else []
|
clients = self._read_clients() if exists else []
|
||||||
|
|
||||||
# Auto-migrate older bridge/-p installs that break QUIC (client EOF while ping works)
|
# Auto-migrate older installs (bridge NAT / userpass / missing BBR+obfs)
|
||||||
if exists:
|
if exists:
|
||||||
try:
|
try:
|
||||||
name = self._container_name(protocol_type)
|
name = self._container_name(protocol_type)
|
||||||
@@ -106,13 +106,20 @@ class HysteriaManager:
|
|||||||
f"docker inspect -f '{{{{.HostConfig.NetworkMode}}}}' {name} 2>/dev/null"
|
f"docker inspect -f '{{{{.HostConfig.NetworkMode}}}}' {name} 2>/dev/null"
|
||||||
)
|
)
|
||||||
mode = (mode or '').strip()
|
mode = (mode or '').strip()
|
||||||
if mode and mode != 'host':
|
cfg = self._read_file(self.config_path)
|
||||||
|
needs_cfg = (
|
||||||
|
'type: userpass' in cfg
|
||||||
|
or 'type: salamander' not in cfg
|
||||||
|
or 'type: password' not in cfg
|
||||||
|
)
|
||||||
|
if (mode and mode != 'host') or needs_cfg:
|
||||||
port = int(meta.get('port') or self.DEFAULT_PORT)
|
port = int(meta.get('port') or self.DEFAULT_PORT)
|
||||||
self._write_config_from_clients(port)
|
self._write_config_from_clients(port)
|
||||||
self._start_container(port)
|
self._start_container(port)
|
||||||
running = self.check_container_running(protocol_type)
|
running = self.check_container_running(protocol_type)
|
||||||
|
meta = self._read_metadata()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('Hysteria host-network migrate failed: %s', e)
|
logger.warning('Hysteria migrate failed: %s', e)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'container_exists': exists,
|
'container_exists': exists,
|
||||||
@@ -177,7 +184,24 @@ class HysteriaManager:
|
|||||||
def _write_clients(self, clients):
|
def _write_clients(self, clients):
|
||||||
self._write_file(self.clients_path, json.dumps(clients, indent=2, ensure_ascii=False))
|
self._write_file(self.clients_path, json.dumps(clients, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
def _build_server_yaml(self, port, clients):
|
def _ensure_secrets(self, meta):
|
||||||
|
"""Password auth (Happ/INCY-friendly) + salamander obfs secrets."""
|
||||||
|
changed = False
|
||||||
|
if not meta.get('password'):
|
||||||
|
meta['password'] = _rand_token(24)
|
||||||
|
changed = True
|
||||||
|
if not meta.get('obfs_password'):
|
||||||
|
meta['obfs_password'] = _rand_token(16)
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
self._write_metadata(meta)
|
||||||
|
return meta
|
||||||
|
|
||||||
|
def _build_server_yaml(self, port, clients, meta=None):
|
||||||
|
meta = self._ensure_secrets(meta if meta is not None else self._read_metadata())
|
||||||
|
password = meta['password']
|
||||||
|
obfs_password = meta['obfs_password']
|
||||||
|
# password auth — Happ / sing-box / INCY expect a single auth string, not userpass
|
||||||
lines = [
|
lines = [
|
||||||
f'listen: :{int(port)}',
|
f'listen: :{int(port)}',
|
||||||
'',
|
'',
|
||||||
@@ -186,24 +210,14 @@ class HysteriaManager:
|
|||||||
' key: /etc/hysteria/private.key',
|
' key: /etc/hysteria/private.key',
|
||||||
'',
|
'',
|
||||||
'auth:',
|
'auth:',
|
||||||
' type: userpass',
|
' type: password',
|
||||||
' userpass:',
|
f' password: "{password}"',
|
||||||
]
|
'',
|
||||||
if clients:
|
'obfs:',
|
||||||
for c in clients:
|
' type: salamander',
|
||||||
if c.get('enabled', True) is False:
|
' salamander:',
|
||||||
continue
|
f' password: "{obfs_password}"',
|
||||||
user = str(c.get('username') or '').strip().lower()
|
|
||||||
pwd = str(c.get('password') or '').strip()
|
|
||||||
if user and pwd:
|
|
||||||
# YAML double-quote; usernames lowercased (viper maps keys to lower)
|
|
||||||
lines.append(f' "{user}": "{pwd}"')
|
|
||||||
else:
|
|
||||||
# Placeholder so Hysteria starts; first real client replaces this
|
|
||||||
lines.append(f' "__bootstrap__": "{_rand_token(24)}"')
|
|
||||||
lines.extend([
|
|
||||||
'',
|
'',
|
||||||
# Larger windows + disable PMTUD: avoids common QUIC EOF behind VPS/Docker NAT
|
|
||||||
'quic:',
|
'quic:',
|
||||||
' initStreamReceiveWindow: 8388608',
|
' initStreamReceiveWindow: 8388608',
|
||||||
' maxStreamReceiveWindow: 8388608',
|
' maxStreamReceiveWindow: 8388608',
|
||||||
@@ -229,18 +243,18 @@ class HysteriaManager:
|
|||||||
' - name: direct',
|
' - name: direct',
|
||||||
' type: direct',
|
' type: direct',
|
||||||
'',
|
'',
|
||||||
])
|
]
|
||||||
return '\n'.join(lines)
|
return '\n'.join(lines)
|
||||||
|
|
||||||
def _write_config_from_clients(self, port=None):
|
def _write_config_from_clients(self, port=None):
|
||||||
meta = self._read_metadata()
|
meta = self._ensure_secrets(self._read_metadata())
|
||||||
port = int(port or meta.get('port') or self.DEFAULT_PORT)
|
port = int(port or meta.get('port') or self.DEFAULT_PORT)
|
||||||
clients = self._read_clients()
|
# clients list is panel-side only (shared password); still rewrite yaml for port/obfs/secrets
|
||||||
self._write_file(self.config_path, self._build_server_yaml(port, clients))
|
self._write_file(self.config_path, self._build_server_yaml(port, self._read_clients(), meta))
|
||||||
return port
|
return port
|
||||||
|
|
||||||
def _tune_host_udp_buffers(self):
|
def _tune_host_network(self):
|
||||||
"""Raise kernel UDP buffers — too-small buffers cause quic-go EOF on clients."""
|
"""UDP buffers + BBR — reduces quic-go EOF and improves throughput."""
|
||||||
script = r"""
|
script = r"""
|
||||||
set +e
|
set +e
|
||||||
mkdir -p /etc/sysctl.d
|
mkdir -p /etc/sysctl.d
|
||||||
@@ -249,12 +263,19 @@ printf '%s\n' \
|
|||||||
'net.core.wmem_max=16777216' \
|
'net.core.wmem_max=16777216' \
|
||||||
'net.core.rmem_default=16777216' \
|
'net.core.rmem_default=16777216' \
|
||||||
'net.core.wmem_default=16777216' \
|
'net.core.wmem_default=16777216' \
|
||||||
|
'net.core.default_qdisc=fq' \
|
||||||
|
'net.ipv4.tcp_congestion_control=bbr' \
|
||||||
|
'net.ipv4.ip_forward=1' \
|
||||||
> /etc/sysctl.d/99-amnezia-hysteria.conf
|
> /etc/sysctl.d/99-amnezia-hysteria.conf
|
||||||
|
modprobe tcp_bbr >/dev/null 2>&1 || true
|
||||||
sysctl -p /etc/sysctl.d/99-amnezia-hysteria.conf >/dev/null 2>&1 || true
|
sysctl -p /etc/sysctl.d/99-amnezia-hysteria.conf >/dev/null 2>&1 || true
|
||||||
sysctl -w net.core.rmem_max=16777216 >/dev/null 2>&1 || true
|
sysctl -w net.core.rmem_max=16777216 >/dev/null 2>&1 || true
|
||||||
sysctl -w net.core.wmem_max=16777216 >/dev/null 2>&1 || true
|
sysctl -w net.core.wmem_max=16777216 >/dev/null 2>&1 || true
|
||||||
sysctl -w net.core.rmem_default=16777216 >/dev/null 2>&1 || true
|
sysctl -w net.core.rmem_default=16777216 >/dev/null 2>&1 || true
|
||||||
sysctl -w net.core.wmem_default=16777216 >/dev/null 2>&1 || true
|
sysctl -w net.core.wmem_default=16777216 >/dev/null 2>&1 || true
|
||||||
|
sysctl -w net.core.default_qdisc=fq >/dev/null 2>&1 || true
|
||||||
|
sysctl -w net.ipv4.tcp_congestion_control=bbr >/dev/null 2>&1 || true
|
||||||
|
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true
|
||||||
"""
|
"""
|
||||||
self.ssh.run_sudo_command(f"sh -c {_q(script)}", timeout=30)
|
self.ssh.run_sudo_command(f"sh -c {_q(script)}", timeout=30)
|
||||||
|
|
||||||
@@ -277,9 +298,8 @@ iptables -C INPUT -p udp --dport {port} -j ACCEPT 2>/dev/null || iptables -I INP
|
|||||||
"""Run with --network host so QUIC/UDP is not broken by Docker NAT (EOF)."""
|
"""Run with --network host so QUIC/UDP is not broken by Docker NAT (EOF)."""
|
||||||
name = self.container_name
|
name = self.container_name
|
||||||
port = int(port)
|
port = int(port)
|
||||||
self._tune_host_udp_buffers()
|
self._tune_host_network()
|
||||||
self._open_udp_port(port)
|
self._open_udp_port(port)
|
||||||
# Cert must be readable inside the container (non-root entrypoint possible)
|
|
||||||
self.ssh.run_sudo_command(
|
self.ssh.run_sudo_command(
|
||||||
f"chmod 644 {_q(self.cert_path)} {_q(self.key_path)} 2>/dev/null || true"
|
f"chmod 644 {_q(self.cert_path)} {_q(self.key_path)} 2>/dev/null || true"
|
||||||
)
|
)
|
||||||
@@ -298,7 +318,6 @@ iptables -C INPUT -p udp --dport {port} -j ACCEPT 2>/dev/null || iptables -I INP
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _reload_container(self):
|
def _reload_container(self):
|
||||||
# Recreate (not restart) so older bridge/-p installs are migrated to host network
|
|
||||||
meta = self._read_metadata()
|
meta = self._read_metadata()
|
||||||
port = int(meta.get('port') or self.DEFAULT_PORT)
|
port = int(meta.get('port') or self.DEFAULT_PORT)
|
||||||
try:
|
try:
|
||||||
@@ -345,15 +364,21 @@ chmod 644 {_q(self.cert_path)} {_q(self.key_path)}
|
|||||||
if code != 0:
|
if code != 0:
|
||||||
raise RuntimeError(f'Failed to install certificate files: {err or out}')
|
raise RuntimeError(f'Failed to install certificate files: {err or out}')
|
||||||
|
|
||||||
def _build_share_uri(self, host, port, domain, username, password, name):
|
def _build_share_uri(self, host, port, domain, name, meta=None):
|
||||||
user = str(username or '').strip().lower()
|
"""Happ/INCY-compatible hy2 link: password auth + salamander + IP host + SNI."""
|
||||||
userinfo = f"{quote(user, safe='')}:{quote(password, safe='')}"
|
meta = self._ensure_secrets(meta if meta is not None else self._read_metadata())
|
||||||
fragment = quote(name or user, safe='')
|
password = meta['password']
|
||||||
|
obfs_password = meta['obfs_password']
|
||||||
sni = domain or host
|
sni = domain or host
|
||||||
conn_host = domain or host
|
# Prefer raw IP in URI — domain may be CDN/DNS filtered for UDP while ICMP still works
|
||||||
|
conn_host = host or domain
|
||||||
|
fragment = quote(name or 'hysteria', safe='')
|
||||||
return (
|
return (
|
||||||
f"hysteria2://{userinfo}@{conn_host}:{int(port)}/"
|
f"hy2://{quote(password, safe='')}@{conn_host}:{int(port)}/"
|
||||||
f"?sni={quote(sni, safe='')}&insecure=0#{fragment}"
|
f"?sni={quote(sni, safe='')}"
|
||||||
|
f"&obfs=salamander"
|
||||||
|
f"&obfs-password={quote(obfs_password, safe='')}"
|
||||||
|
f"&insecure=0#{fragment}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ===================== INSTALL / REMOVE =====================
|
# ===================== INSTALL / REMOVE =====================
|
||||||
@@ -380,13 +405,16 @@ chmod 644 {_q(self.cert_path)} {_q(self.key_path)}
|
|||||||
|
|
||||||
self.ssh.run_sudo_command(f"mkdir -p {_q(self.base_dir)}")
|
self.ssh.run_sudo_command(f"mkdir -p {_q(self.base_dir)}")
|
||||||
self._write_clients([])
|
self._write_clients([])
|
||||||
self._write_metadata({
|
meta = {
|
||||||
'domain': domain,
|
'domain': domain,
|
||||||
'email': email,
|
'email': email,
|
||||||
'port': port,
|
'port': port,
|
||||||
})
|
'password': _rand_token(24),
|
||||||
|
'obfs_password': _rand_token(16),
|
||||||
|
}
|
||||||
|
self._write_metadata(meta)
|
||||||
self._write_config_from_clients(port)
|
self._write_config_from_clients(port)
|
||||||
log.append('Prepared config directory')
|
log.append('Prepared config directory (password auth + salamander + BBR)')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._issue_certificate(domain, email)
|
self._issue_certificate(domain, email)
|
||||||
@@ -399,7 +427,7 @@ chmod 644 {_q(self.cert_path)} {_q(self.key_path)}
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'status': 'error', 'message': str(e), 'log': log}
|
return {'status': 'error', 'message': str(e), 'log': log}
|
||||||
|
|
||||||
log.append(f'Started {self.container_name} on UDP {port} (host network)')
|
log.append(f'Started {self.container_name} on UDP {port} (host network, BBR)')
|
||||||
return {
|
return {
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': 'Hysteria installed',
|
'message': 'Hysteria installed',
|
||||||
@@ -431,60 +459,58 @@ chmod 644 {_q(self.cert_path)} {_q(self.key_path)}
|
|||||||
clients = self._read_clients()
|
clients = self._read_clients()
|
||||||
result = []
|
result = []
|
||||||
for c in clients:
|
for c in clients:
|
||||||
|
cname = c.get('name') or c.get('id')
|
||||||
result.append({
|
result.append({
|
||||||
'clientId': c.get('id'),
|
'clientId': c.get('id'),
|
||||||
'client_id': c.get('id'),
|
'client_id': c.get('id'),
|
||||||
'id': c.get('id'),
|
'id': c.get('id'),
|
||||||
'name': c.get('name') or c.get('username'),
|
'name': cname,
|
||||||
'email': c.get('username'),
|
'email': cname,
|
||||||
'userData': {'clientName': c.get('name') or c.get('username')},
|
'enabled': c.get('enabled', True),
|
||||||
|
'userData': {'clientName': cname, 'enabled': c.get('enabled', True)},
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def add_client(self, protocol_type, name, host, port=None):
|
def add_client(self, protocol_type, name, host, port=None):
|
||||||
meta = self._read_metadata()
|
meta = self._ensure_secrets(self._read_metadata())
|
||||||
domain = meta.get('domain') or host
|
domain = meta.get('domain') or host
|
||||||
port = int(port or meta.get('port') or self.DEFAULT_PORT)
|
port = int(port or meta.get('port') or self.DEFAULT_PORT)
|
||||||
client_id = secrets.token_hex(8)
|
client_id = secrets.token_hex(8)
|
||||||
username = f"u_{client_id}" # lowercase — hysteria userpass keys are case-folded
|
|
||||||
password = _rand_token(20)
|
|
||||||
clients = self._read_clients()
|
clients = self._read_clients()
|
||||||
clients.append({
|
clients.append({
|
||||||
'id': client_id,
|
'id': client_id,
|
||||||
'name': name or username,
|
'name': name or f'client-{client_id[:6]}',
|
||||||
'username': username,
|
|
||||||
'password': password,
|
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
})
|
})
|
||||||
self._write_clients(clients)
|
self._write_clients(clients)
|
||||||
|
# Shared password auth — refresh yaml only if secrets/port changed
|
||||||
self._write_config_from_clients(port)
|
self._write_config_from_clients(port)
|
||||||
self._reload_container()
|
self._reload_container()
|
||||||
config = self._build_share_uri(host, port, domain, username, password, name or username)
|
config = self._build_share_uri(host, port, domain, name or client_id, meta)
|
||||||
return {'client_id': client_id, 'config': config}
|
return {'client_id': client_id, 'config': config}
|
||||||
|
|
||||||
def get_client_config(self, protocol_type, client_id, host, port=None):
|
def get_client_config(self, protocol_type, client_id, host, port=None):
|
||||||
meta = self._read_metadata()
|
meta = self._ensure_secrets(self._read_metadata())
|
||||||
domain = meta.get('domain') or host
|
domain = meta.get('domain') or host
|
||||||
port = int(port or meta.get('port') or self.DEFAULT_PORT)
|
port = int(port or meta.get('port') or self.DEFAULT_PORT)
|
||||||
clients = self._read_clients()
|
clients = self._read_clients()
|
||||||
client = next((c for c in clients if c.get('id') == client_id), None)
|
client = next((c for c in clients if c.get('id') == client_id), None)
|
||||||
if not client:
|
if not client:
|
||||||
raise RuntimeError('Client not found')
|
raise RuntimeError('Client not found')
|
||||||
|
if client.get('enabled', True) is False:
|
||||||
|
raise RuntimeError('Client is disabled')
|
||||||
return self._build_share_uri(
|
return self._build_share_uri(
|
||||||
host, port, domain,
|
host, port, domain,
|
||||||
client.get('username'), client.get('password'),
|
client.get('name') or client_id,
|
||||||
client.get('name') or client.get('username'),
|
meta,
|
||||||
)
|
)
|
||||||
|
|
||||||
def remove_client(self, protocol_type, client_id):
|
def remove_client(self, protocol_type, client_id):
|
||||||
clients = [c for c in self._read_clients() if c.get('id') != client_id]
|
clients = [c for c in self._read_clients() if c.get('id') != client_id]
|
||||||
self._write_clients(clients)
|
self._write_clients(clients)
|
||||||
self._write_config_from_clients()
|
|
||||||
self._reload_container()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def toggle_client(self, protocol_type, client_id, enable=True):
|
def toggle_client(self, protocol_type, client_id, enable=True):
|
||||||
# Soft-disable: remove from yaml but keep in table with enabled=false
|
|
||||||
clients = self._read_clients()
|
clients = self._read_clients()
|
||||||
found = False
|
found = False
|
||||||
for c in clients:
|
for c in clients:
|
||||||
@@ -495,6 +521,4 @@ chmod 644 {_q(self.cert_path)} {_q(self.key_path)}
|
|||||||
if not found:
|
if not found:
|
||||||
raise RuntimeError('Client not found')
|
raise RuntimeError('Client not found')
|
||||||
self._write_clients(clients)
|
self._write_clients(clients)
|
||||||
self._write_config_from_clients()
|
|
||||||
self._reload_container()
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -1801,7 +1801,7 @@
|
|||||||
updateNginxDnsHint();
|
updateNginxDnsHint();
|
||||||
} else if (base === 'hysteria') {
|
} else if (base === 'hysteria') {
|
||||||
portLabel.textContent = _('port') + ' (UDP)';
|
portLabel.textContent = _('port') + ' (UDP)';
|
||||||
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 8998) : '8998';
|
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 443) : '443';
|
||||||
portInput.disabled = false;
|
portInput.disabled = false;
|
||||||
portHint.textContent = currentInstallAnother ? _('port_next_instance_hint') : _('hysteria_port_hint');
|
portHint.textContent = currentInstallAnother ? _('port_next_instance_hint') : _('hysteria_port_hint');
|
||||||
hysteriaOpts.style.display = 'block';
|
hysteriaOpts.style.display = 'block';
|
||||||
|
|||||||
@@ -359,8 +359,8 @@
|
|||||||
"hysteria_desc": "Hysteria 2 (QUIC/UDP) via teddysun/hysteria with Let's Encrypt TLS. Admin sets the domain at install time.",
|
"hysteria_desc": "Hysteria 2 (QUIC/UDP) via teddysun/hysteria with Let's Encrypt TLS. Admin sets the domain at install time.",
|
||||||
"hysteria_domain": "Domain",
|
"hysteria_domain": "Domain",
|
||||||
"hysteria_email": "Let's Encrypt email",
|
"hysteria_email": "Let's Encrypt email",
|
||||||
"hysteria_port_hint": "UDP port for Hysteria (default 8998). Port 80/TCP must be free for certificate issuance.",
|
"hysteria_port_hint": "UDP port for Hysteria (default 443 — better for DPI). Port 80/TCP must be free for certificate issuance.",
|
||||||
"hysteria_install_hint": "Point the domain A-record to this server before install. Port 80/TCP is used temporarily for Let's Encrypt HTTP-01 validation.",
|
"hysteria_install_hint": "Point the domain A-record to this server before install. Port 80/TCP is used temporarily for Let's Encrypt HTTP-01 validation. BBR and salamander obfuscation are enabled automatically.",
|
||||||
"hysteria_dns_hint": "Create this DNS record before installation:",
|
"hysteria_dns_hint": "Create this DNS record before installation:",
|
||||||
"tls_emulation": "TLS Emulation",
|
"tls_emulation": "TLS Emulation",
|
||||||
"tls_domain": "Masking Domain",
|
"tls_domain": "Masking Domain",
|
||||||
|
|||||||
@@ -364,8 +364,8 @@
|
|||||||
"hysteria_desc": "Hysteria 2 (QUIC/UDP) با تصویر teddysun/hysteria و TLS از Let's Encrypt. دامنه را ادمین هنگام نصب مشخص میکند.",
|
"hysteria_desc": "Hysteria 2 (QUIC/UDP) با تصویر teddysun/hysteria و TLS از Let's Encrypt. دامنه را ادمین هنگام نصب مشخص میکند.",
|
||||||
"hysteria_domain": "دامنه",
|
"hysteria_domain": "دامنه",
|
||||||
"hysteria_email": "ایمیل Let's Encrypt",
|
"hysteria_email": "ایمیل Let's Encrypt",
|
||||||
"hysteria_port_hint": "پورت UDP برای Hysteria (پیشفرض 8998). پورت 80/TCP باید برای صدور گواهی آزاد باشد.",
|
"hysteria_port_hint": "پورت UDP برای Hysteria (پیشفرض 443). پورت 80/TCP باید برای صدور گواهی آزاد باشد.",
|
||||||
"hysteria_install_hint": "قبل از نصب، رکورد A دامنه باید به این سرور اشاره کند. پورت 80/TCP موقتاً برای اعتبارسنجی HTTP-01 استفاده میشود.",
|
"hysteria_install_hint": "قبل از نصب، رکورد A دامنه باید به این سرور اشاره کند. BBR و obfuscation salamander بهصورت خودکار فعال میشوند.",
|
||||||
"hysteria_dns_hint": "قبل از نصب این رکورد DNS را بسازید:",
|
"hysteria_dns_hint": "قبل از نصب این رکورد DNS را بسازید:",
|
||||||
"dns_desc": "سرور DNS شخصی برای مسدود کردن تبلیغات و حفظ حریم خصوصی.",
|
"dns_desc": "سرور DNS شخصی برای مسدود کردن تبلیغات و حفظ حریم خصوصی.",
|
||||||
"dns_internal_hint": "The service runs on the internal network (172.29.172.254) without binding to host ports.",
|
"dns_internal_hint": "The service runs on the internal network (172.29.172.254) without binding to host ports.",
|
||||||
|
|||||||
@@ -364,8 +364,8 @@
|
|||||||
"hysteria_desc": "Hysteria 2 (QUIC/UDP) via teddysun/hysteria avec TLS Let's Encrypt. Le domaine est défini par l'admin à l'installation.",
|
"hysteria_desc": "Hysteria 2 (QUIC/UDP) via teddysun/hysteria avec TLS Let's Encrypt. Le domaine est défini par l'admin à l'installation.",
|
||||||
"hysteria_domain": "Domaine",
|
"hysteria_domain": "Domaine",
|
||||||
"hysteria_email": "Email Let's Encrypt",
|
"hysteria_email": "Email Let's Encrypt",
|
||||||
"hysteria_port_hint": "Port UDP pour Hysteria (8998 par défaut). Le port 80/TCP doit être libre pour le certificat.",
|
"hysteria_port_hint": "Port UDP pour Hysteria (443 par défaut). Le port 80/TCP doit être libre pour le certificat.",
|
||||||
"hysteria_install_hint": "L'enregistrement A du domaine doit pointer vers ce serveur. Le port 80/TCP est utilisé temporairement pour la validation HTTP-01 Let's Encrypt.",
|
"hysteria_install_hint": "L'enregistrement A du domaine doit pointer vers ce serveur. BBR et obfuscation salamander sont activés automatiquement.",
|
||||||
"hysteria_dns_hint": "Créez cet enregistrement DNS avant l'installation :",
|
"hysteria_dns_hint": "Créez cet enregistrement DNS avant l'installation :",
|
||||||
"dns_desc": "Serveur DNS personnel pour bloquer les publicités et protéger la vie privée.",
|
"dns_desc": "Serveur DNS personnel pour bloquer les publicités et protéger la vie privée.",
|
||||||
"dns_internal_hint": "The service runs on the internal network (172.29.172.254) without binding to host ports.",
|
"dns_internal_hint": "The service runs on the internal network (172.29.172.254) without binding to host ports.",
|
||||||
|
|||||||
@@ -359,8 +359,8 @@
|
|||||||
"hysteria_desc": "Hysteria 2 (QUIC/UDP) на образе teddysun/hysteria с TLS от Let's Encrypt. Домен указывает админ при установке.",
|
"hysteria_desc": "Hysteria 2 (QUIC/UDP) на образе teddysun/hysteria с TLS от Let's Encrypt. Домен указывает админ при установке.",
|
||||||
"hysteria_domain": "Домен",
|
"hysteria_domain": "Домен",
|
||||||
"hysteria_email": "Email для Let's Encrypt",
|
"hysteria_email": "Email для Let's Encrypt",
|
||||||
"hysteria_port_hint": "UDP-порт Hysteria (по умолчанию 8998). Порт 80/TCP должен быть свободен для выпуска сертификата.",
|
"hysteria_port_hint": "UDP-порт Hysteria (по умолчанию 443 — лучше проходит DPI). Порт 80/TCP должен быть свободен для выпуска сертификата.",
|
||||||
"hysteria_install_hint": "Перед установкой A-запись домена должна указывать на этот сервер. Порт 80/TCP временно используется для HTTP-01 проверки Let's Encrypt.",
|
"hysteria_install_hint": "Перед установкой A-запись домена должна указывать на этот сервер. Порт 80/TCP временно используется для HTTP-01 проверки Let's Encrypt. BBR и obfuscation salamander включаются автоматически.",
|
||||||
"hysteria_dns_hint": "Перед установкой создайте DNS-запись:",
|
"hysteria_dns_hint": "Перед установкой создайте DNS-запись:",
|
||||||
"tls_emulation": "TLS-эмуляция",
|
"tls_emulation": "TLS-эмуляция",
|
||||||
"tls_domain": "Домен маскировки",
|
"tls_domain": "Домен маскировки",
|
||||||
|
|||||||
@@ -364,8 +364,8 @@
|
|||||||
"hysteria_desc": "Hysteria 2(QUIC/UDP,teddysun/hysteria),支持 Let's Encrypt TLS。安装时由管理员指定域名。",
|
"hysteria_desc": "Hysteria 2(QUIC/UDP,teddysun/hysteria),支持 Let's Encrypt TLS。安装时由管理员指定域名。",
|
||||||
"hysteria_domain": "域名",
|
"hysteria_domain": "域名",
|
||||||
"hysteria_email": "Let's Encrypt 邮箱",
|
"hysteria_email": "Let's Encrypt 邮箱",
|
||||||
"hysteria_port_hint": "Hysteria UDP 端口(默认 8998)。签发证书时需要 80/TCP 空闲。",
|
"hysteria_port_hint": "Hysteria UDP 端口(默认 443)。签发证书时需要 80/TCP 空闲。",
|
||||||
"hysteria_install_hint": "安装前请将域名 A 记录指向本服务器。80/TCP 会临时用于 Let's Encrypt HTTP-01 验证。",
|
"hysteria_install_hint": "安装前请将域名 A 记录指向本服务器。将自动启用 BBR 与 salamander 混淆。",
|
||||||
"hysteria_dns_hint": "安装前请创建此 DNS 记录:",
|
"hysteria_dns_hint": "安装前请创建此 DNS 记录:",
|
||||||
"dns_desc": "防止广告和保护隐私的个人 DNS 服务器。",
|
"dns_desc": "防止广告和保护隐私的个人 DNS 服务器。",
|
||||||
"dns_internal_hint": "The service runs on the internal network (172.29.172.254) without binding to host ports.",
|
"dns_internal_hint": "The service runs on the internal network (172.29.172.254) without binding to host ports.",
|
||||||
|
|||||||
Reference in New Issue
Block a user