Allow changing Hysteria UDP port when 443 is already in use.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1701,6 +1701,11 @@ class Socks5SettingsRequest(BaseModel):
|
||||
password: Optional[str] = None
|
||||
|
||||
|
||||
class HysteriaSettingsRequest(BaseModel):
|
||||
protocol: str = 'hysteria'
|
||||
port: Optional[int] = None
|
||||
|
||||
|
||||
class ProtocolRequest(BaseModel):
|
||||
protocol: str = 'awg'
|
||||
|
||||
@@ -2963,6 +2968,70 @@ async def api_socks5_update_credentials(request: Request, server_id: int, req: S
|
||||
return JSONResponse({'error': str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.get('/api/servers/{server_id}/hysteria/settings', tags=["Protocols"])
|
||||
async def api_hysteria_get_settings(request: Request, server_id: int, protocol: str = 'hysteria'):
|
||||
"""Return current Hysteria domain/port for the settings modal."""
|
||||
if not _check_admin(request):
|
||||
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
||||
try:
|
||||
data = load_data()
|
||||
if server_id >= len(data['servers']):
|
||||
return JSONResponse({'error': 'Server not found'}, status_code=404)
|
||||
if not is_valid_protocol(protocol) or protocol_base(protocol) != 'hysteria':
|
||||
return JSONResponse({'error': 'Invalid protocol'}, status_code=400)
|
||||
server = data['servers'][server_id]
|
||||
ssh = get_ssh(server)
|
||||
ssh.connect()
|
||||
manager = get_protocol_manager(ssh, protocol)
|
||||
settings = manager.get_settings()
|
||||
ssh.disconnect()
|
||||
saved = (server.get('protocols') or {}).get(protocol) or {}
|
||||
if not settings.get('port') and saved.get('port'):
|
||||
settings['port'] = int(saved['port'])
|
||||
return {'status': 'success', **settings}
|
||||
except Exception as e:
|
||||
logger.exception("Error reading Hysteria settings")
|
||||
return JSONResponse({'error': str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.post('/api/servers/{server_id}/hysteria/settings', tags=["Protocols"])
|
||||
async def api_hysteria_update_settings(request: Request, server_id: int, req: HysteriaSettingsRequest):
|
||||
"""Change Hysteria UDP listen port and recreate the container."""
|
||||
if not _check_admin(request):
|
||||
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
||||
try:
|
||||
data = load_data()
|
||||
if server_id >= len(data['servers']):
|
||||
return JSONResponse({'error': 'Server not found'}, status_code=404)
|
||||
protocol = req.protocol if is_valid_protocol(req.protocol) and protocol_base(req.protocol) == 'hysteria' else 'hysteria'
|
||||
server = data['servers'][server_id]
|
||||
ssh = get_ssh(server)
|
||||
ssh.connect()
|
||||
manager = get_protocol_manager(ssh, protocol)
|
||||
result = manager.update_settings(port=req.port)
|
||||
ssh.disconnect()
|
||||
if result.get('status') == 'error':
|
||||
return JSONResponse(
|
||||
{'error': result.get('message') or 'Failed to update settings', **{k: v for k, v in result.items() if k != 'status'}},
|
||||
status_code=400,
|
||||
)
|
||||
if result.get('port'):
|
||||
srv_proto = server.setdefault('protocols', {}).setdefault(protocol, {})
|
||||
srv_proto['port'] = str(result['port'])
|
||||
srv_proto['installed'] = True
|
||||
srv_proto['base_protocol'] = protocol_base(protocol)
|
||||
srv_proto['instance'] = protocol_instance(protocol)
|
||||
srv_proto['display_name'] = protocol_display_name(protocol)
|
||||
srv_proto['container_name'] = protocol_container_name(protocol)
|
||||
if result.get('domain'):
|
||||
srv_proto['domain'] = result['domain']
|
||||
save_data(data)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.exception("Error updating Hysteria settings")
|
||||
return JSONResponse({'error': str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.post('/api/servers/{server_id}/uninstall', tags=["Protocols"])
|
||||
async def api_uninstall_protocol(request: Request, server_id: int, req: ProtocolRequest):
|
||||
if not _check_admin(request):
|
||||
|
||||
@@ -14,6 +14,7 @@ import re
|
||||
import secrets
|
||||
import shlex
|
||||
import string
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -34,7 +35,7 @@ class HysteriaManager:
|
||||
IMAGE_NAME = 'teddysun/hysteria:latest'
|
||||
CERTBOT_IMAGE = 'certbot/certbot:latest'
|
||||
BASE_DIR = '/opt/amnezia/hysteria'
|
||||
DEFAULT_PORT = 443
|
||||
DEFAULT_PORT = 8998
|
||||
|
||||
def __init__(self, ssh, protocol='hysteria'):
|
||||
self.ssh = ssh
|
||||
@@ -158,15 +159,30 @@ class HysteriaManager:
|
||||
# Pull last meaningful log line
|
||||
last = ''
|
||||
for line in reversed((diag['recent_logs'] or '').splitlines()):
|
||||
line = line.strip()
|
||||
if line:
|
||||
line = self._strip_ansi(line).strip()
|
||||
if not line:
|
||||
continue
|
||||
# Prefer the FATAL / bind error line
|
||||
if 'address already in use' in line.lower() or 'FATAL' in line or 'failed to load' in line.lower():
|
||||
last = line
|
||||
break
|
||||
if not last:
|
||||
last = line
|
||||
if last:
|
||||
# strip docker timestamp prefix if present
|
||||
if ' ' in last and last[0].isdigit():
|
||||
if ' ' in last and (last[0].isdigit() or last[0] == '2'):
|
||||
last = last.split(' ', 1)[-1]
|
||||
diag['error_summary'] = f"exit {diag['exit_code']}: {last[:180]}"
|
||||
last = self._strip_ansi(last)
|
||||
if 'address already in use' in last.lower():
|
||||
m = re.search(r'listen udp :(\d+)', last, re.I) or re.search(r':(\d+):\s*bind', last)
|
||||
p = m.group(1) if m else '?'
|
||||
diag['error_summary'] = (
|
||||
f'UDP port {p} already in use — open Settings and choose another port'
|
||||
)
|
||||
else:
|
||||
# keep message short for badge
|
||||
short = re.sub(r'\s+', ' ', last)[:160]
|
||||
diag['error_summary'] = f"exit {diag['exit_code']}: {short}"
|
||||
else:
|
||||
diag['error_summary'] = f"exited with code {diag['exit_code']}"
|
||||
elif diag['status'] and diag['status'] != 'running':
|
||||
@@ -375,6 +391,19 @@ sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true
|
||||
"""
|
||||
self.ssh.run_sudo_command(f"sh -c {_q(script)}", timeout=30)
|
||||
|
||||
def _strip_ansi(self, text):
|
||||
return re.sub(r'\x1b\[[0-9;]*m', '', text or '')
|
||||
|
||||
def _udp_port_busy(self, port):
|
||||
"""Return (busy: bool, detail: str) for host UDP port (after our container is removed)."""
|
||||
port = int(port)
|
||||
out, _, _ = self.ssh.run_sudo_command(
|
||||
f"ss -ulnp 'sport = :{port}' 2>/dev/null || "
|
||||
f"ss -uln | grep -E ':{port}([[:space:]]|$)' || true"
|
||||
)
|
||||
detail = (out or '').strip()
|
||||
return bool(detail), detail[:240]
|
||||
|
||||
def _open_udp_port(self, port):
|
||||
port = int(port)
|
||||
script = f"""
|
||||
@@ -394,12 +423,20 @@ 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)."""
|
||||
name = self.container_name
|
||||
port = int(port)
|
||||
if port == 80:
|
||||
raise RuntimeError('Port 80 is reserved for Let\'s Encrypt validation')
|
||||
self._tune_host_network()
|
||||
self._open_udp_port(port)
|
||||
self.ssh.run_sudo_command(
|
||||
f"chmod 644 {_q(self.cert_path)} {_q(self.key_path)} 2>/dev/null || true"
|
||||
)
|
||||
self.ssh.run_sudo_command(f"docker rm -fv {name} 2>/dev/null || true")
|
||||
busy, detail = self._udp_port_busy(port)
|
||||
if busy:
|
||||
raise RuntimeError(
|
||||
f'UDP port {port} is already in use — choose another port in Hysteria settings. '
|
||||
f'({self._strip_ansi(detail)})'
|
||||
)
|
||||
run_cmd = (
|
||||
f"docker run -d --restart always "
|
||||
f"--name {name} "
|
||||
@@ -411,8 +448,56 @@ iptables -C INPUT -p udp --dport {port} -j ACCEPT 2>/dev/null || iptables -I INP
|
||||
_, err, code = self.ssh.run_sudo_command(run_cmd, timeout=60)
|
||||
if code != 0:
|
||||
raise RuntimeError(f'Failed to start Hysteria: {err}')
|
||||
# Confirm it stayed up (catch immediate bind failures)
|
||||
time.sleep(1.2)
|
||||
if not self.check_container_running(self.protocol):
|
||||
diag = self.get_container_diagnostics(self.protocol)
|
||||
raise RuntimeError(
|
||||
diag.get('error_summary')
|
||||
or 'Hysteria container exited immediately — check logs / port'
|
||||
)
|
||||
return True
|
||||
|
||||
def get_settings(self):
|
||||
meta = self._ensure_secrets(self._read_metadata())
|
||||
return {
|
||||
'port': int(meta.get('port') or self.DEFAULT_PORT),
|
||||
'domain': meta.get('domain') or '',
|
||||
'email': meta.get('email') or '',
|
||||
'protocol': self.protocol,
|
||||
}
|
||||
|
||||
def update_settings(self, port=None):
|
||||
"""Change listen UDP port and recreate the container."""
|
||||
meta = self._ensure_secrets(self._read_metadata())
|
||||
if port is None:
|
||||
port = meta.get('port') or self.DEFAULT_PORT
|
||||
port = int(port)
|
||||
if port < 1 or port > 65535:
|
||||
return {'status': 'error', 'message': 'Invalid port'}
|
||||
if port == 80:
|
||||
return {'status': 'error', 'message': 'Port 80 is reserved for Let\'s Encrypt validation'}
|
||||
|
||||
meta['port'] = port
|
||||
self._write_metadata(meta)
|
||||
self._write_config_from_clients(port)
|
||||
try:
|
||||
self._start_container(port)
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': str(e),
|
||||
'port': str(port),
|
||||
'domain': meta.get('domain'),
|
||||
}
|
||||
return {
|
||||
'status': 'success',
|
||||
'port': str(port),
|
||||
'domain': meta.get('domain'),
|
||||
'email': meta.get('email'),
|
||||
'message': f'Hysteria listening on UDP {port}',
|
||||
}
|
||||
|
||||
def _reload_container(self):
|
||||
meta = self._read_metadata()
|
||||
port = int(meta.get('port') or self.DEFAULT_PORT)
|
||||
|
||||
+93
-1
@@ -747,6 +747,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Hysteria Settings Modal ===== -->
|
||||
<div class="modal-backdrop" id="hysteriaSettingsModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="hysteriaSettingsTitle">{{ _('hysteria_settings_title') }}</h2>
|
||||
<button class="modal-close" onclick="closeModal('hysteriaSettingsModal')">×</button>
|
||||
</div>
|
||||
<input type="hidden" id="hysteriaSetProto" value="hysteria" />
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('hysteria_domain') }}</label>
|
||||
<input class="form-input" type="text" id="hysteriaSetDomain" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('port') }} (UDP)</label>
|
||||
<input class="form-input" type="number" id="hysteriaSetPort" min="1" max="65535" value="8998">
|
||||
<div class="form-hint">{{ _('hysteria_port_change_hint') }}</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('hysteriaSettingsModal')">{{ _('cancel') }}</button>
|
||||
<button class="btn btn-primary" onclick="saveHysteriaSettings()" id="hysteriaSaveBtn">
|
||||
<span id="hysteriaSaveBtnText">{{ _('save') }}</span>
|
||||
<div class="spinner hidden" id="hysteriaSaveSpinner"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== NGINX Site Modal ===== -->
|
||||
<div class="modal-backdrop" id="siteConfigModal">
|
||||
<div class="modal" style="max-width:680px;">
|
||||
@@ -1562,6 +1589,14 @@
|
||||
<button class="btn btn-secondary btn-sm" onclick="showProtocolBackups('${proto}')" style="flex:1">${_('backup')}</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="uninstallProtocol('${proto}')">${_('uninstall')}</button>
|
||||
`;
|
||||
} else if (protoBase(proto) === 'hysteria') {
|
||||
actionsEl.innerHTML = `
|
||||
<button class="btn btn-secondary btn-sm" onclick="selectProtocolForConns('${proto}')" style="flex:1">${_('connections')}</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="openHysteriaSettings('${proto}')">${_('hysteria_change_port')}</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="showProtocolBackups('${proto}')">${_('backup')}</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="uninstallProtocol('${proto}')">${_('uninstall')}</button>
|
||||
`;
|
||||
showConnectionsSection();
|
||||
} else {
|
||||
actionsEl.innerHTML = `
|
||||
<button class="btn btn-secondary btn-sm" onclick="selectProtocolForConns('${proto}')" style="flex:1">${_('connections')}</button>
|
||||
@@ -1625,6 +1660,13 @@
|
||||
<button class="btn btn-secondary btn-sm" onclick="showProtocolBackups('${proto}')">${_('backup')}</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="uninstallProtocol('${proto}')">${_('uninstall')}</button>
|
||||
`;
|
||||
} else if (protoBase(proto) === 'hysteria') {
|
||||
actionsEl.innerHTML = `
|
||||
<button class="btn btn-primary btn-sm" onclick="openHysteriaSettings('${proto}')" style="flex:1">${_('hysteria_change_port')}</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="showContainerLogs('${proto}')">${_('logs_btn')}</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="openInstallModal('${proto}')">${_('reinstall')}</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="uninstallProtocol('${proto}')">${_('uninstall')}</button>
|
||||
`;
|
||||
} else {
|
||||
actionsEl.innerHTML = `
|
||||
<button class="btn btn-primary btn-sm" onclick="openInstallModal('${proto}')" style="flex:1">${_('reinstall')}</button>
|
||||
@@ -1972,7 +2014,7 @@
|
||||
updateNginxDnsHint();
|
||||
} else if (base === 'hysteria') {
|
||||
portLabel.textContent = _('port') + ' (UDP)';
|
||||
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 443) : '443';
|
||||
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 8998) : '8998';
|
||||
portInput.disabled = false;
|
||||
portHint.textContent = currentInstallAnother ? _('port_next_instance_hint') : _('hysteria_port_hint');
|
||||
hysteriaOpts.style.display = 'block';
|
||||
@@ -2219,6 +2261,56 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Hysteria Settings (port) ==========
|
||||
async function openHysteriaSettings(proto = 'hysteria') {
|
||||
try {
|
||||
const data = await apiCall(`/api/servers/${SERVER_ID}/hysteria/settings?protocol=${encodeURIComponent(proto)}`);
|
||||
document.getElementById('hysteriaSetProto').value = proto;
|
||||
document.getElementById('hysteriaSettingsTitle').textContent =
|
||||
`${_('hysteria_settings_title')} — ${getProtoTitle(proto)}`;
|
||||
document.getElementById('hysteriaSetDomain').value = data.domain || '';
|
||||
// Prefer live/status port, then settings, then 8998
|
||||
const livePort = (currentProtocolStatus[proto] || {}).port;
|
||||
document.getElementById('hysteriaSetPort').value = livePort || data.port || 8998;
|
||||
openModal('hysteriaSettingsModal');
|
||||
} catch (err) {
|
||||
showToast(_('error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHysteriaSettings() {
|
||||
const btn = document.getElementById('hysteriaSaveBtn');
|
||||
const text = document.getElementById('hysteriaSaveBtnText');
|
||||
const spinner = document.getElementById('hysteriaSaveSpinner');
|
||||
const proto = document.getElementById('hysteriaSetProto').value || 'hysteria';
|
||||
const port = parseInt(document.getElementById('hysteriaSetPort').value, 10);
|
||||
if (!port || port < 1 || port > 65535) {
|
||||
showToast(_('error') + ': invalid port', 'error');
|
||||
return;
|
||||
}
|
||||
if (port === 80) {
|
||||
showToast(_('error') + ': port 80 reserved', 'error');
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
text.textContent = _('saving') || 'Saving...';
|
||||
spinner.classList.remove('hidden');
|
||||
try {
|
||||
const res = await apiCall(`/api/servers/${SERVER_ID}/hysteria/settings`, 'POST', {
|
||||
protocol: proto, port,
|
||||
});
|
||||
showToast(res.message || _('hysteria_settings_saved'), 'success');
|
||||
closeModal('hysteriaSettingsModal');
|
||||
setTimeout(() => checkServer(), 800);
|
||||
} catch (err) {
|
||||
showToast(_('error') + ': ' + err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
text.textContent = _('save');
|
||||
spinner.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Connection Management ==========
|
||||
function selectProtocolForConns(proto) {
|
||||
document.getElementById('connectionsSection').style.display = '';
|
||||
|
||||
@@ -359,9 +359,13 @@
|
||||
"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_email": "Let's Encrypt email",
|
||||
"hysteria_port_hint": "UDP port for Hysteria (default 443 — better for DPI). Port 80/TCP must be free for certificate issuance.",
|
||||
"hysteria_port_hint": "UDP port for Hysteria (default 8998). Avoid 443 if nginx/xray already use UDP/TCP 443. 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. BBR and salamander obfuscation are enabled automatically.",
|
||||
"hysteria_dns_hint": "Create this DNS record before installation:",
|
||||
"hysteria_settings_title": "Hysteria settings",
|
||||
"hysteria_change_port": "Change port",
|
||||
"hysteria_port_change_hint": "UDP listen port. If you see “address already in use”, pick a free port (e.g. 8443 or 8998), save, then re-import the client link.",
|
||||
"hysteria_settings_saved": "Hysteria port updated",
|
||||
"container_logs": "Container logs",
|
||||
"logs_btn": "📋 Logs",
|
||||
"logs_live": "Live",
|
||||
|
||||
@@ -364,9 +364,13 @@
|
||||
"hysteria_desc": "Hysteria 2 (QUIC/UDP) با تصویر teddysun/hysteria و TLS از Let's Encrypt. دامنه را ادمین هنگام نصب مشخص میکند.",
|
||||
"hysteria_domain": "دامنه",
|
||||
"hysteria_email": "ایمیل Let's Encrypt",
|
||||
"hysteria_port_hint": "پورت UDP برای Hysteria (پیشفرض 443). پورت 80/TCP باید برای صدور گواهی آزاد باشد.",
|
||||
"hysteria_port_hint": "پورت UDP برای Hysteria (پیشفرض 8998). اگر 443 اشغال است پورت دیگری بگذارید.",
|
||||
"hysteria_install_hint": "قبل از نصب، رکورد A دامنه باید به این سرور اشاره کند. BBR و obfuscation salamander بهصورت خودکار فعال میشوند.",
|
||||
"hysteria_dns_hint": "قبل از نصب این رکورد DNS را بسازید:",
|
||||
"hysteria_settings_title": "تنظیمات Hysteria",
|
||||
"hysteria_change_port": "تغییر پورت",
|
||||
"hysteria_port_change_hint": "پورت UDP. اگر address already in use دیدید، پورت آزاد بگذارید (8443 یا 8998).",
|
||||
"hysteria_settings_saved": "پورت Hysteria بهروز شد",
|
||||
"container_logs": "لاگهای کانتینر",
|
||||
"logs_btn": "📋 Logs",
|
||||
"logs_live": "زنده",
|
||||
|
||||
@@ -364,9 +364,13 @@
|
||||
"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_email": "Email Let's Encrypt",
|
||||
"hysteria_port_hint": "Port UDP pour Hysteria (443 par défaut). Le port 80/TCP doit être libre pour le certificat.",
|
||||
"hysteria_port_hint": "Port UDP pour Hysteria (8998 par défaut). Évitez 443 s'il est déjà pris.",
|
||||
"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_settings_title": "Paramètres Hysteria",
|
||||
"hysteria_change_port": "Changer le port",
|
||||
"hysteria_port_change_hint": "Port UDP. Si « address already in use », choisissez un port libre (8443 ou 8998).",
|
||||
"hysteria_settings_saved": "Port Hysteria mis à jour",
|
||||
"container_logs": "Logs du conteneur",
|
||||
"logs_btn": "📋 Logs",
|
||||
"logs_live": "Temps réel",
|
||||
|
||||
@@ -359,9 +359,13 @@
|
||||
"hysteria_desc": "Hysteria 2 (QUIC/UDP) на образе teddysun/hysteria с TLS от Let's Encrypt. Домен указывает админ при установке.",
|
||||
"hysteria_domain": "Домен",
|
||||
"hysteria_email": "Email для Let's Encrypt",
|
||||
"hysteria_port_hint": "UDP-порт Hysteria (по умолчанию 443 — лучше проходит DPI). Порт 80/TCP должен быть свободен для выпуска сертификата.",
|
||||
"hysteria_port_hint": "UDP-порт Hysteria (по умолчанию 8998). Не ставьте 443, если он уже занят nginx/xray. Порт 80/TCP должен быть свободен для выпуска сертификата.",
|
||||
"hysteria_install_hint": "Перед установкой A-запись домена должна указывать на этот сервер. Порт 80/TCP временно используется для HTTP-01 проверки Let's Encrypt. BBR и obfuscation salamander включаются автоматически.",
|
||||
"hysteria_dns_hint": "Перед установкой создайте DNS-запись:",
|
||||
"hysteria_settings_title": "Настройки Hysteria",
|
||||
"hysteria_change_port": "Сменить порт",
|
||||
"hysteria_port_change_hint": "UDP-порт прослушивания. Если ошибка «address already in use» — выберите свободный порт (8443 или 8998), сохраните и заново импортируйте ссылку в клиент.",
|
||||
"hysteria_settings_saved": "Порт Hysteria обновлён",
|
||||
"container_logs": "Логи контейнера",
|
||||
"logs_btn": "📋 Логи",
|
||||
"logs_live": "В реальном времени",
|
||||
|
||||
@@ -364,9 +364,13 @@
|
||||
"hysteria_desc": "Hysteria 2(QUIC/UDP,teddysun/hysteria),支持 Let's Encrypt TLS。安装时由管理员指定域名。",
|
||||
"hysteria_domain": "域名",
|
||||
"hysteria_email": "Let's Encrypt 邮箱",
|
||||
"hysteria_port_hint": "Hysteria UDP 端口(默认 443)。签发证书时需要 80/TCP 空闲。",
|
||||
"hysteria_port_hint": "Hysteria UDP 端口(默认 8998)。若 443 已被占用请换端口。",
|
||||
"hysteria_install_hint": "安装前请将域名 A 记录指向本服务器。将自动启用 BBR 与 salamander 混淆。",
|
||||
"hysteria_dns_hint": "安装前请创建此 DNS 记录:",
|
||||
"hysteria_settings_title": "Hysteria 设置",
|
||||
"hysteria_change_port": "更改端口",
|
||||
"hysteria_port_change_hint": "UDP 监听端口。若提示 address already in use,请改用空闲端口(如 8443 或 8998)。",
|
||||
"hysteria_settings_saved": "Hysteria 端口已更新",
|
||||
"container_logs": "容器日志",
|
||||
"logs_btn": "📋 日志",
|
||||
"logs_live": "实时",
|
||||
|
||||
Reference in New Issue
Block a user