diff --git a/app.py b/app.py
index 85f4674..a5b6616 100644
--- a/app.py
+++ b/app.py
@@ -804,8 +804,8 @@ async def wait_for_tunnel_url(provider: str, seconds: int = 20):
return get_tunnel_status(provider)
-BASE_PROTOCOLS = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'dns', 'wireguard', 'socks5', 'adguard', 'nginx']
-MULTI_INSTANCE_PROTOCOLS = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'socks5'}
+BASE_PROTOCOLS = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'dns', 'wireguard', 'socks5', 'adguard', 'nginx']
+MULTI_INSTANCE_PROTOCOLS = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'socks5'}
def protocol_base(protocol: str) -> str:
@@ -845,7 +845,6 @@ def protocol_display_name(protocol: str) -> str:
'awg_legacy': 'AmneziaWG Legacy',
'xray': 'Xray',
'telemt': 'Telemt',
- 'hysteria': 'Hysteria 2',
'dns': 'AmneziaDNS',
'wireguard': 'WireGuard',
'socks5': 'SOCKS5',
@@ -865,7 +864,6 @@ def protocol_container_name(protocol: str) -> Optional[str]:
'awg_legacy': 'amnezia-awg-legacy',
'xray': 'amnezia-xray',
'telemt': 'telemt',
- 'hysteria': 'amnezia-hysteria',
'dns': 'amnezia-dns',
'wireguard': 'amnezia-wireguard',
'socks5': 'amnezia-socks5proxy',
@@ -905,9 +903,6 @@ def get_protocol_manager(ssh, protocol: str):
elif base == 'nginx':
from managers.nginx_manager import NginxManager
return NginxManager(ssh, protocol)
- elif base == 'hysteria':
- from managers.hysteria_manager import HysteriaManager
- return HysteriaManager(ssh, protocol)
from managers.awg_manager import AWGManager
return AWGManager(ssh)
@@ -969,7 +964,7 @@ def _manager_call(manager, method, protocol, *args, **kwargs):
# Protocols that own VPN clients (can list/link connections)
-CLIENT_VPN_BASES = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard', 'hysteria', 'xui'}
+CLIENT_VPN_BASES = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard', 'xui'}
def generate_vpn_link(config_text):
@@ -1689,9 +1684,6 @@ class InstallProtocolRequest(BaseModel):
# NGINX
nginx_domain: Optional[str] = None
nginx_email: Optional[str] = None
- # Hysteria
- hysteria_domain: Optional[str] = None
- hysteria_email: Optional[str] = None
class Socks5SettingsRequest(BaseModel):
@@ -1701,17 +1693,12 @@ class Socks5SettingsRequest(BaseModel):
password: Optional[str] = None
-class HysteriaSettingsRequest(BaseModel):
- protocol: str = 'hysteria'
- port: Optional[int] = None
-
-
class ProtocolRequest(BaseModel):
protocol: str = 'awg'
class ContainerLogsRequest(BaseModel):
- protocol: str = 'hysteria'
+ protocol: str = 'awg'
tail: Optional[int] = 200
@@ -2708,13 +2695,6 @@ async def api_check_server(request: Request, server_id: int):
for key in ('domain', 'email', 'site_url'):
if db_proto.get(key) not in (None, ''):
merged[key] = db_proto[key]
- if protocol_base(proto) == 'hysteria':
- for key in ('domain', 'email'):
- if db_proto.get(key) not in (None, '') and not merged.get(key):
- merged[key] = db_proto[key]
- # Keep a useful error even when merging preserved records
- if db_proto.get('port') and not merged.get('port'):
- merged['port'] = db_proto['port']
return merged
def should_preserve_saved_protocol(proto, result=None, err=None):
@@ -2864,13 +2844,6 @@ async def api_install_protocol(request: Request, server_id: int, req: InstallPro
domain=req.nginx_domain,
email=req.nginx_email,
)
- elif install_base == 'hysteria':
- result = manager.install_protocol(
- protocol_type=install_protocol,
- port=req.port,
- domain=req.hysteria_domain,
- email=req.hysteria_email,
- )
else:
result = manager.install_protocol(install_protocol, port=req.port)
@@ -2897,9 +2870,6 @@ async def api_install_protocol(request: Request, server_id: int, req: InstallPro
proto_record['domain'] = result.get('domain')
proto_record['email'] = result.get('email')
proto_record['site_url'] = result.get('site_url')
- if install_base == 'hysteria':
- proto_record['domain'] = result.get('domain')
- proto_record['email'] = result.get('email')
proto_record['base_protocol'] = install_base
proto_record['instance'] = protocol_instance(install_protocol)
proto_record['display_name'] = protocol_display_name(install_protocol)
@@ -2975,70 +2945,6 @@ 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):
@@ -3072,7 +2978,6 @@ CONTAINER_NAMES = {
'awg_legacy': 'amnezia-awg-legacy',
'xray': 'amnezia-xray',
'telemt': 'telemt',
- 'hysteria': 'amnezia-hysteria',
'dns': 'amnezia-dns',
'wireguard': 'amnezia-wireguard',
'socks5': 'amnezia-socks5proxy',
@@ -3222,41 +3127,13 @@ async def api_container_toggle(request: Request, server_id: int, req: ProtocolRe
ssh.run_sudo_command(f"docker stop {container}")
action = 'stopped'
else:
- # Hysteria: recreate with host network / current config instead of plain start
- if protocol_base(req.protocol) == 'hysteria':
- from managers.hysteria_manager import HysteriaManager
- mgr = HysteriaManager(ssh, req.protocol)
- meta = mgr._read_metadata()
- port = int(meta.get('port') or mgr.DEFAULT_PORT)
- mgr._write_config_from_clients(port)
- mgr._start_container(port)
- else:
- ssh.run_sudo_command(f"docker start {container}")
+ ssh.run_sudo_command(f"docker start {container}")
action = 'started'
- # After stop/start, return diagnostics for UI
- error = ''
- recent_logs = ''
- if protocol_base(req.protocol) == 'hysteria':
- from managers.hysteria_manager import HysteriaManager
- mgr = HysteriaManager(ssh, req.protocol)
- diag = mgr.get_container_diagnostics(req.protocol)
- error = diag.get('error_summary') or ''
- recent_logs = diag.get('recent_logs') or ''
- if action == 'started' and not diag.get('running'):
- ssh.disconnect()
- return JSONResponse({
- 'error': error or 'Hysteria failed to start',
- 'action': action,
- 'container': container,
- 'recent_logs': recent_logs,
- }, status_code=400)
ssh.disconnect()
return {
'status': 'success',
'action': action,
'container': container,
- 'error': error,
- 'recent_logs': recent_logs,
}
except Exception as e:
logger.exception("Error toggling container")
@@ -3278,23 +3155,7 @@ async def api_container_logs(request: Request, server_id: int, req: ContainerLog
container = protocol_container_name(req.protocol)
ssh = get_ssh(server)
ssh.connect()
- if protocol_base(req.protocol) == 'hysteria':
- from managers.hysteria_manager import HysteriaManager
- mgr = HysteriaManager(ssh, req.protocol)
- logs = mgr.get_logs(req.protocol, tail=req.tail or 200)
- diag = mgr.get_container_diagnostics(req.protocol)
- ssh.disconnect()
- return {
- 'status': 'success',
- 'protocol': req.protocol,
- 'container': container,
- 'logs': logs,
- 'running': bool(diag.get('running')),
- 'error': diag.get('error_summary') or '',
- 'exit_code': diag.get('exit_code'),
- 'container_status': diag.get('status') or '',
- }
- # Generic fallback for other protocols
+ # Generic docker logs for all protocols
tail = max(20, min(int(req.tail or 200), 2000))
out, err, _ = ssh.run_sudo_command(
f"docker logs --tail {tail} --timestamps {shlex.quote(container)} 2>&1",
@@ -3318,7 +3179,7 @@ async def api_container_logs(request: Request, server_id: int, req: ContainerLog
@app.get('/api/servers/{server_id}/container/logs/stream', tags=["Protocols"])
-async def api_container_logs_stream(request: Request, server_id: int, protocol: str = 'hysteria', tail: int = 100):
+async def api_container_logs_stream(request: Request, server_id: int, protocol: str = 'awg', tail: int = 100):
"""SSE stream of docker logs (polls every ~1.5s for new lines)."""
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
@@ -3415,10 +3276,6 @@ async def api_server_config(request: Request, server_id: int, req: ProtocolReque
from managers.nginx_manager import NginxManager
mgr = NginxManager(ssh, req.protocol)
config = mgr._get_server_config(req.protocol)
- elif protocol_base(req.protocol) == 'hysteria':
- from managers.hysteria_manager import HysteriaManager
- mgr = HysteriaManager(ssh, req.protocol)
- config = mgr.get_server_config(req.protocol)
else:
mgr = AWGManager(ssh)
config = mgr._get_server_config(req.protocol)
@@ -3463,10 +3320,6 @@ async def api_server_config_save(request: Request, server_id: int, req: ServerCo
from managers.nginx_manager import NginxManager
mgr = NginxManager(ssh, req.protocol)
mgr.save_server_config(req.protocol, req.config)
- elif protocol_base(req.protocol) == 'hysteria':
- from managers.hysteria_manager import HysteriaManager
- mgr = HysteriaManager(ssh, req.protocol)
- mgr.save_server_config(req.protocol, req.config)
else:
mgr = AWGManager(ssh)
mgr.save_server_config(req.protocol, req.config)
diff --git a/managers/backup_manager.py b/managers/backup_manager.py
index 1cde323..35305ed 100644
--- a/managers/backup_manager.py
+++ b/managers/backup_manager.py
@@ -62,10 +62,6 @@ class BackupManager:
remote_dir = inst_path('/opt/amnezia/telemt')
paths['host'] = [remote_dir]
paths['container'] = [remote_dir]
- elif base == 'hysteria':
- remote_dir = inst_path('/opt/amnezia/hysteria')
- paths['host'] = [remote_dir]
- paths['container'] = ['/etc/hysteria']
elif base == 'dns':
paths['host'] = ['/opt/amnezia/dns']
paths['container'] = ['/opt/amnezia/dns']
diff --git a/managers/hysteria_manager.py b/managers/hysteria_manager.py
deleted file mode 100644
index 57ed12d..0000000
--- a/managers/hysteria_manager.py
+++ /dev/null
@@ -1,713 +0,0 @@
-"""
-Hysteria 2 protocol manager — teddysun/hysteria Docker image.
-
-Installs Hysteria with Let's Encrypt TLS for an admin-provided domain
-(certbot standalone on port 80), host networking, BBR, and salamander obfs.
-Clients use password auth (Happ/INCY-compatible); share links are hy2:// URIs.
-"""
-
-from __future__ import annotations
-
-import json
-import logging
-import re
-import secrets
-import shlex
-import string
-import time
-from urllib.parse import quote
-
-logger = logging.getLogger(__name__)
-
-
-def _q(value):
- return shlex.quote(str(value))
-
-
-def _rand_token(length=16):
- alphabet = string.ascii_lowercase + string.digits
- return ''.join(secrets.choice(alphabet) for _ in range(length))
-
-
-class HysteriaManager:
- PROTOCOL = 'hysteria'
- CONTAINER_NAME = 'amnezia-hysteria'
- IMAGE_NAME = 'teddysun/hysteria:latest'
- CERTBOT_IMAGE = 'certbot/certbot:latest'
- BASE_DIR = '/opt/amnezia/hysteria'
- DEFAULT_PORT = 8998
-
- def __init__(self, ssh, protocol='hysteria'):
- self.ssh = ssh
- self.protocol = protocol or self.PROTOCOL
- self.instance = self._instance_index(self.protocol)
- self.container_name = self._container_name(self.protocol)
- self.base_dir = self._base_dir(self.protocol)
- self.config_path = f'{self.base_dir}/server.yaml'
- self.clients_path = f'{self.base_dir}/clients.json'
- self.meta_path = f'{self.base_dir}/metadata.json'
- self.cert_path = f'{self.base_dir}/cert.crt'
- self.key_path = f'{self.base_dir}/private.key'
- self.letsencrypt_dir = f'{self.base_dir}/letsencrypt'
-
- def _instance_index(self, protocol=None):
- parts = str(protocol or self.protocol or '').split('__', 1)
- if len(parts) == 2:
- try:
- return max(1, int(parts[1]))
- except ValueError:
- return 1
- return 1
-
- def _container_name(self, protocol=None):
- idx = self._instance_index(protocol or self.protocol)
- return self.CONTAINER_NAME if idx <= 1 else f'{self.CONTAINER_NAME}-{idx}'
-
- def _base_dir(self, protocol=None):
- idx = self._instance_index(protocol or self.protocol)
- return self.BASE_DIR if idx <= 1 else f'{self.BASE_DIR}-{idx}'
-
- # ===================== STATUS =====================
-
- def check_docker_installed(self):
- out, _, code = self.ssh.run_command("docker --version 2>/dev/null")
- if code != 0:
- return False
- out2, _, _ = self.ssh.run_command(
- "systemctl is-active docker 2>/dev/null || service docker status 2>/dev/null"
- )
- return 'active' in out2 or 'running' in out2.lower()
-
- def check_protocol_installed(self, protocol_type=None):
- name = self._container_name(protocol_type or self.protocol)
- out, _, _ = self.ssh.run_sudo_command(
- f"docker ps -a --filter name=^{name}$ --format '{{{{.Names}}}}'"
- )
- return name in out.strip().split('\n')
-
- def check_container_running(self, protocol_type=None):
- name = self._container_name(protocol_type or self.protocol)
- out, _, _ = self.ssh.run_sudo_command(
- f"docker ps --filter name=^{name}$ --format '{{{{.Status}}}}'"
- )
- return 'Up' in out
-
- def get_logs(self, protocol_type=None, tail=200):
- """Return recent docker logs for the Hysteria container."""
- name = self._container_name(protocol_type or self.protocol)
- tail = max(20, min(int(tail or 200), 2000))
- out, err, code = self.ssh.run_sudo_command(
- f"docker logs --tail {tail} --timestamps {name} 2>&1",
- timeout=30,
- )
- text = (out or err or '').strip()
- if code != 0 and not text:
- text = f'(no logs: exit {code})'
- return text
-
- def get_container_diagnostics(self, protocol_type=None):
- """Inspect why the container is stopped / unhealthy."""
- name = self._container_name(protocol_type or self.protocol)
- fmt = (
- '{{.State.Status}}|{{.State.Running}}|{{.State.ExitCode}}|'
- '{{.State.OOMKilled}}|{{.State.Error}}|{{.State.FinishedAt}}|'
- '{{.State.StartedAt}}'
- )
- out, _, code = self.ssh.run_sudo_command(
- f"docker inspect -f '{fmt}' {name} 2>/dev/null"
- )
- diag = {
- 'status': 'unknown',
- 'running': False,
- 'exit_code': None,
- 'oom_killed': False,
- 'error': '',
- 'finished_at': '',
- 'started_at': '',
- 'recent_logs': '',
- 'error_summary': '',
- }
- if code != 0 or not (out or '').strip():
- diag['status'] = 'missing'
- diag['error_summary'] = 'Container not found'
- return diag
-
- parts = (out or '').strip().split('|')
- while len(parts) < 7:
- parts.append('')
- status, running, exit_code, oom, err, finished, started = parts[:7]
- diag['status'] = status or 'unknown'
- diag['running'] = str(running).lower() == 'true'
- try:
- diag['exit_code'] = int(exit_code)
- except Exception:
- diag['exit_code'] = None
- diag['oom_killed'] = str(oom).lower() == 'true'
- diag['error'] = (err or '').strip()
- diag['finished_at'] = (finished or '').strip()
- diag['started_at'] = (started or '').strip()
- diag['recent_logs'] = self.get_logs(protocol_type, tail=40)
-
- # Build human-readable summary for the UI badge
- if diag['running']:
- diag['error_summary'] = ''
- elif diag['oom_killed']:
- diag['error_summary'] = 'OOM killed (out of memory)'
- elif diag['error']:
- diag['error_summary'] = diag['error']
- elif diag['exit_code'] not in (None, 0):
- # Prefer bind/port conflicts over generic FATAL lines
- bind_line = ''
- last = ''
- for raw in reversed((diag['recent_logs'] or '').splitlines()):
- line = self._strip_ansi(raw).strip()
- if not line:
- continue
- # strip docker --timestamps prefix
- m_ts = re.match(r'^\d{4}-\d{2}-\d{2}T\S+\s+(.*)$', line)
- if m_ts:
- line = m_ts.group(1).strip()
- low = line.lower()
- if 'address already in use' in low and not bind_line:
- bind_line = line
- if ('FATAL' in line or 'failed to load' in low) and not last:
- last = line
- if not last:
- last = line
- pick = bind_line or last
- if pick and 'address already in use' in pick.lower():
- m = re.search(r'listen udp :(\d+)', pick, re.I) or re.search(r':(\d+):\s*bind', pick)
- p = m.group(1) if m else '?'
- if p == '?':
- m2 = re.search(r'udp\s*:(\d+)', pick, re.I)
- p = m2.group(1) if m2 else '?'
- diag['error_summary'] = (
- f'UDP port {p} already in use — change port below (try 8998 or 8443)'
- )
- diag['port_busy'] = True
- elif pick:
- short = re.sub(r'\s+', ' ', pick)[: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':
- diag['error_summary'] = f"status: {diag['status']}"
- return diag
-
- def get_server_status(self, protocol_type=None):
- protocol_type = protocol_type or self.protocol
- exists = self.check_protocol_installed(protocol_type)
- running = self.check_container_running(protocol_type)
- meta = self._read_metadata() if exists else {}
- clients = self._read_clients() if exists else []
- diag = {}
-
- # Auto-migrate older installs (bridge NAT / userpass / missing BBR+obfs)
- if exists:
- try:
- name = self._container_name(protocol_type)
- mode, _, _ = self.ssh.run_sudo_command(
- f"docker inspect -f '{{{{.HostConfig.NetworkMode}}}}' {name} 2>/dev/null"
- )
- mode = (mode or '').strip()
- 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)
- self._write_config_from_clients(port)
- self._start_container(port)
- running = self.check_container_running(protocol_type)
- meta = self._read_metadata()
- except Exception as e:
- logger.warning('Hysteria migrate failed: %s', e)
- diag = {'error_summary': f'migrate failed: {e}'}
-
- if exists:
- try:
- diag = self.get_container_diagnostics(protocol_type)
- running = bool(diag.get('running'))
- except Exception as e:
- logger.warning('Hysteria diagnostics failed: %s', e)
- diag = {'error_summary': str(e)}
-
- return {
- 'container_exists': exists,
- 'container_running': running,
- 'port': int(meta.get('port') or self.DEFAULT_PORT),
- 'domain': meta.get('domain'),
- 'email': meta.get('email'),
- 'clients_count': len(clients),
- 'protocol': protocol_type,
- 'base_protocol': self.PROTOCOL,
- 'instance': self._instance_index(protocol_type),
- 'container_name': self._container_name(protocol_type),
- 'error': diag.get('error_summary') or '',
- 'exit_code': diag.get('exit_code'),
- 'container_status': diag.get('status') or '',
- 'recent_logs': diag.get('recent_logs') or '',
- 'port_busy': bool(diag.get('port_busy')),
- }
-
- # ===================== HELPERS =====================
-
- def _validate_domain(self, domain):
- domain = (domain or '').strip().lower()
- if not domain or len(domain) > 253:
- raise ValueError('Domain is required for Hysteria SSL')
- if not re.match(r'^(?!-)[a-z0-9.-]+(?/dev/null")
- return out if code == 0 else ''
-
- def _write_file(self, path, content):
- parent = path.rsplit('/', 1)[0]
- self.ssh.run_sudo_command(f"mkdir -p {_q(parent)}")
- self.ssh.upload_file_sudo(content, path)
-
- def _read_metadata(self):
- raw = self._read_file(self.meta_path)
- if not raw.strip():
- return {}
- try:
- return json.loads(raw)
- except Exception:
- return {}
-
- def _write_metadata(self, meta):
- self._write_file(self.meta_path, json.dumps(meta, indent=2, ensure_ascii=False))
-
- def _read_clients(self):
- raw = self._read_file(self.clients_path)
- if not raw.strip():
- return []
- try:
- data = json.loads(raw)
- return data if isinstance(data, list) else []
- except Exception:
- return []
-
- def _write_clients(self, clients):
- self._write_file(self.clients_path, json.dumps(clients, indent=2, ensure_ascii=False))
-
- def _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 = [
- f'listen: :{int(port)}',
- '',
- 'tls:',
- ' cert: /etc/hysteria/cert.crt',
- ' key: /etc/hysteria/private.key',
- '',
- 'auth:',
- ' type: password',
- f' password: "{password}"',
- '',
- 'obfs:',
- ' type: salamander',
- ' salamander:',
- f' password: "{obfs_password}"',
- '',
- 'quic:',
- ' initStreamReceiveWindow: 8388608',
- ' maxStreamReceiveWindow: 8388608',
- ' initConnReceiveWindow: 20971520',
- ' maxConnReceiveWindow: 20971520',
- ' maxIdleTimeout: 60s',
- ' maxIncomingStreams: 1024',
- ' disablePathMTUDiscovery: true',
- '',
- 'ignoreClientBandwidth: true',
- '',
- 'bandwidth:',
- ' up: 1 gbps',
- ' down: 1 gbps',
- '',
- 'masquerade:',
- ' type: proxy',
- ' proxy:',
- ' url: https://www.bing.com',
- ' rewriteHost: true',
- '',
- 'outbounds:',
- ' - name: direct',
- ' type: direct',
- '',
- ]
- return '\n'.join(lines)
-
- def _write_config_from_clients(self, port=None):
- meta = self._ensure_secrets(self._read_metadata())
- port = int(port or meta.get('port') or self.DEFAULT_PORT)
- # 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, self._read_clients(), meta))
- return port
-
- def _tune_host_network(self):
- """UDP buffers + BBR — reduces quic-go EOF and improves throughput."""
- script = r"""
-set +e
-mkdir -p /etc/sysctl.d
-printf '%s\n' \
- 'net.core.rmem_max=16777216' \
- 'net.core.wmem_max=16777216' \
- 'net.core.rmem_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
-modprobe tcp_bbr >/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.wmem_max=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.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)
-
- 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"""
-set +e
-if command -v ufw >/dev/null 2>&1; then
- ufw allow {port}/udp >/dev/null 2>&1 || true
-fi
-if command -v firewall-cmd >/dev/null 2>&1; then
- firewall-cmd --permanent --add-port={port}/udp >/dev/null 2>&1 || true
- firewall-cmd --reload >/dev/null 2>&1 || true
-fi
-iptables -C INPUT -p udp --dport {port} -j ACCEPT 2>/dev/null || iptables -I INPUT -p udp --dport {port} -j ACCEPT 2>/dev/null || true
-"""
- self.ssh.run_sudo_command(f"sh -c {_q(script)}", timeout=30)
-
- def _start_container(self, port):
- """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} "
- f"--network host "
- f"--cap-add=NET_ADMIN "
- f"-v {_q(self.base_dir)}:/etc/hysteria "
- f"{self.IMAGE_NAME}"
- )
- _, 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())
- port = int(meta.get('port') or self.DEFAULT_PORT)
- return {
- 'port': port,
- 'suggested_port': 8998 if port in (443, 80) else 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)
- try:
- self._start_container(port)
- except Exception as e:
- logger.warning('Hysteria container reload failed: %s', e)
-
- def _issue_certificate(self, domain, email):
- """Issue Let's Encrypt cert via certbot standalone (needs free TCP 80)."""
- self.ssh.run_sudo_command(f"mkdir -p {_q(self.letsencrypt_dir)}")
- self.ssh.run_sudo_command(f"docker pull {self.CERTBOT_IMAGE}", timeout=180)
-
- # Stop anything briefly occupying 80 if it's our leftover certbot
- self.ssh.run_sudo_command(
- "docker rm -fv amnezia-hysteria-certbot 2>/dev/null || true"
- )
-
- cmd = (
- f"docker run --rm --name amnezia-hysteria-certbot "
- f"-p 80:80 "
- f"-v {_q(self.letsencrypt_dir)}:/etc/letsencrypt "
- f"{self.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 failed for {domain}. "
- f"Point A-record to this server and free TCP port 80. {err or out}"
- )
-
- # Copy live certs into paths expected by teddysun image
- copy_script = f"""
-set -e
-LIVE={_q(self.letsencrypt_dir)}/live/{_q(domain)}
-test -s "$LIVE/fullchain.pem"
-test -s "$LIVE/privkey.pem"
-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 _build_share_uri(self, host, port, domain, name, meta=None):
- """Happ/INCY-compatible hy2 link: password auth + salamander + IP host + SNI."""
- meta = self._ensure_secrets(meta if meta is not None else self._read_metadata())
- password = meta['password']
- obfs_password = meta['obfs_password']
- sni = 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 (
- f"hy2://{quote(password, safe='')}@{conn_host}:{int(port)}/"
- f"?sni={quote(sni, safe='')}"
- f"&obfs=salamander"
- f"&obfs-password={quote(obfs_password, safe='')}"
- f"&insecure=0#{fragment}"
- )
-
- # ===================== INSTALL / REMOVE =====================
-
- def install_protocol(self, protocol_type=None, port=None, domain=None, email=None):
- protocol_type = protocol_type or self.protocol
- if not self.check_docker_installed():
- return {'status': 'error', 'message': 'Docker not installed'}
-
- domain = self._validate_domain(domain)
- email = self._validate_email(email)
- port = int(port or self.DEFAULT_PORT)
- if port == 80:
- return {'status': 'error', 'message': 'Port 80 is reserved for Let\'s Encrypt validation'}
-
- log = []
- self.ssh.run_sudo_command(f"docker pull {self.IMAGE_NAME}", timeout=180)
- log.append(f'Pulled {self.IMAGE_NAME}')
-
- if self.check_protocol_installed(protocol_type):
- name = self._container_name(protocol_type)
- self.ssh.run_sudo_command(f"docker rm -fv {name} 2>/dev/null || true")
- log.append('Removed previous container')
-
- self.ssh.run_sudo_command(f"mkdir -p {_q(self.base_dir)}")
- self._write_clients([])
- meta = {
- 'domain': domain,
- 'email': email,
- 'port': port,
- 'password': _rand_token(24),
- 'obfs_password': _rand_token(16),
- }
- self._write_metadata(meta)
- self._write_config_from_clients(port)
- log.append('Prepared config directory (password auth + salamander + BBR)')
-
- try:
- self._issue_certificate(domain, email)
- log.append(f"Issued Let's Encrypt certificate for {domain}")
- except Exception as e:
- return {'status': 'error', 'message': str(e), 'log': log}
-
- try:
- self._start_container(port)
- except Exception as e:
- return {'status': 'error', 'message': str(e), 'log': log}
-
- log.append(f'Started {self.container_name} on UDP {port} (host network, BBR)')
- return {
- 'status': 'success',
- 'message': 'Hysteria installed',
- 'log': log,
- 'port': str(port),
- 'domain': domain,
- 'email': email,
- }
-
- def remove_container(self, protocol_type=None):
- protocol_type = protocol_type or self.protocol
- name = self._container_name(protocol_type)
- base = self._base_dir(protocol_type)
- self.ssh.run_sudo_command(f"docker rm -fv {name} 2>/dev/null || true")
- self.ssh.run_sudo_command(f"rm -rf {_q(base)}")
- return True
-
- def get_server_config(self, protocol_type=None):
- return self._read_file(self.config_path)
-
- def save_server_config(self, protocol_type=None, config_text=''):
- self._write_file(self.config_path, config_text or '')
- self._reload_container()
- return True
-
- # ===================== CLIENTS =====================
-
- def get_clients(self, protocol_type=None):
- clients = self._read_clients()
- result = []
- for c in clients:
- cname = c.get('name') or c.get('id')
- result.append({
- 'clientId': c.get('id'),
- 'client_id': c.get('id'),
- 'id': c.get('id'),
- 'name': cname,
- 'email': cname,
- 'enabled': c.get('enabled', True),
- 'userData': {'clientName': cname, 'enabled': c.get('enabled', True)},
- })
- return result
-
- def add_client(self, protocol_type, name, host, port=None):
- meta = self._ensure_secrets(self._read_metadata())
- domain = meta.get('domain') or host
- port = int(port or meta.get('port') or self.DEFAULT_PORT)
- client_id = secrets.token_hex(8)
- clients = self._read_clients()
- clients.append({
- 'id': client_id,
- 'name': name or f'client-{client_id[:6]}',
- 'enabled': True,
- })
- self._write_clients(clients)
- # Shared password auth — refresh yaml only if secrets/port changed
- self._write_config_from_clients(port)
- self._reload_container()
- config = self._build_share_uri(host, port, domain, name or client_id, meta)
- return {'client_id': client_id, 'config': config}
-
- def get_client_config(self, protocol_type, client_id, host, port=None):
- meta = self._ensure_secrets(self._read_metadata())
- domain = meta.get('domain') or host
- port = int(port or meta.get('port') or self.DEFAULT_PORT)
- clients = self._read_clients()
- client = next((c for c in clients if c.get('id') == client_id), None)
- if not client:
- raise RuntimeError('Client not found')
- if client.get('enabled', True) is False:
- raise RuntimeError('Client is disabled')
- return self._build_share_uri(
- host, port, domain,
- client.get('name') or client_id,
- meta,
- )
-
- def remove_client(self, protocol_type, client_id):
- clients = [c for c in self._read_clients() if c.get('id') != client_id]
- self._write_clients(clients)
- return True
-
- def toggle_client(self, protocol_type, client_id, enable=True):
- clients = self._read_clients()
- found = False
- for c in clients:
- if c.get('id') == client_id:
- c['enabled'] = bool(enable)
- found = True
- break
- if not found:
- raise RuntimeError('Client not found')
- self._write_clients(clients)
- return True
diff --git a/static/css/style.css b/static/css/style.css
index 70fede5..850cde9 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -823,11 +823,6 @@ a:hover {
color: #38bdf8;
}
-.protocol-hysteria .protocol-icon {
- background: linear-gradient(135deg, rgba(14, 165, 233, 0.2), rgba(99, 102, 241, 0.1));
- color: #38bdf8;
-}
-
.protocol-dns .protocol-icon {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.1));
color: var(--success);
diff --git a/telegram_bot.py b/telegram_bot.py
index cb4d9c0..48fe59b 100644
--- a/telegram_bot.py
+++ b/telegram_bot.py
@@ -24,7 +24,7 @@ _bot_task: Optional[asyncio.Task] = None
_callback_refs = {}
_pending_inputs = {}
-CLIENT_PROTOCOLS = {"awg", "awg2", "awg_legacy", "xray", "telemt", "hysteria", "wireguard"}
+CLIENT_PROTOCOLS = {"awg", "awg2", "awg_legacy", "xray", "telemt", "wireguard"}
SERVICE_PROTOCOLS = {"dns", "adguard", "socks5", "nginx"}
@@ -137,7 +137,6 @@ def _protocol_display_name(protocol: str) -> str:
"awg_legacy": "AmneziaWG Legacy",
"xray": "Xray",
"telemt": "Telemt",
- "hysteria": "Hysteria 2",
"dns": "AmneziaDNS",
"wireguard": "WireGuard",
"socks5": "SOCKS5",
@@ -550,7 +549,7 @@ async def _send_config_by_client(api: TelegramAPI, chat_id: int, server: dict, p
server_name = server.get("name") or server.get("host", "Unknown")
await api.send_message(chat_id, f"✅ {_e(conn_name)}\n🌐 Server: {_e(server_name)}\n🔌 Protocol: {_e(proto.upper())}")
- is_link_proto = _proto_base(proto) in ("xray", "telemt", "hysteria")
+ is_link_proto = _proto_base(proto) in ("xray", "telemt")
if is_link_proto:
await api.send_message(chat_id, f"🔗 Connection link (tap to copy):\n{_e(config)}")
else:
@@ -907,7 +906,7 @@ async def _admin_create_client(api: TelegramAPI, chat_id: int, message_id: int,
async def _send_config_text(api: TelegramAPI, chat_id: int, server: dict, proto: str, conn_name: str, config: str, generate_vpn_link_fn: Callable):
await api.send_message(chat_id, f"✅ {_e(conn_name)}\n🌐 Server: {_e(server.get('name') or server.get('host'))}\n🔌 Protocol: {_e(proto.upper())}")
- if _proto_base(proto) in ("xray", "telemt", "hysteria"):
+ if _proto_base(proto) in ("xray", "telemt"):
await api.send_message(chat_id, f"🔗 Connection link:\n{_e(config)}")
else:
await api.send_message(chat_id, f"📄 Configuration:\n
{_e(config)}")
diff --git a/templates/invites.html b/templates/invites.html
index 405c0c4..56367b7 100644
--- a/templates/invites.html
+++ b/templates/invites.html
@@ -128,7 +128,7 @@
{% for s in servers %}
{% set server_idx = loop.index0 %}
{% for key, info in (s.protocols or {}).items() %}
- {% if info.installed and key.split('__')[0] in ['awg','awg2','awg_legacy','xray','wireguard','telemt','hysteria'] %}
+ {% if info.installed and key.split('__')[0] in ['awg','awg2','awg_legacy','xray','wireguard','telemt'] %}
{% endif %}
{% endfor %}
diff --git a/templates/my_connections.html b/templates/my_connections.html
index 706e53e..0bd34d8 100644
--- a/templates/my_connections.html
+++ b/templates/my_connections.html
@@ -117,10 +117,10 @@
document.getElementById('configText').textContent = result.config;
document.getElementById('vpnLinkText').textContent = currentVpnLink;
- // Telemt / Hysteria use share links, not vpn:// Amnezia format
+ // Telemt uses share links, not vpn:// Amnezia format
const vpnTab = document.querySelectorAll('.config-tab')[1];
const vpnPanel = document.getElementById('panel-vpn');
- if (proto === 'telemt' || String(proto || '').split('__')[0] === 'hysteria') {
+ if (proto === 'telemt') {
vpnTab.style.display = 'none';
} else {
vpnTab.style.display = '';
diff --git a/templates/server.html b/templates/server.html
index e0c2b45..c82dabb 100644
--- a/templates/server.html
+++ b/templates/server.html
@@ -294,30 +294,6 @@
-
- A ${domain} ${SERVER_HOST}`;
}
- function updateHysteriaDnsHint() {
- const input = document.getElementById('installHysteriaDomain');
- const hint = document.getElementById('hysteriaDnsHint');
- if (!input || !hint) return;
- const domain = (input.value || '').trim() || 'vpn.example.com';
- hint.innerHTML = `${_('hysteria_dns_hint')} A ${domain} ${SERVER_HOST}`;
- }
-
function openInstallModal(proto, installAnother = false) {
const base = protoBase(proto);
currentInstallProto = installAnother ? base : proto;
@@ -1996,13 +1877,11 @@
const socks5Opts = document.getElementById('socks5Options');
const adguardOpts = document.getElementById('adguardOptions');
const nginxOpts = document.getElementById('nginxOptions');
- const hysteriaOpts = document.getElementById('hysteriaOptions');
telemtOpts.style.display = 'none';
socks5Opts.style.display = 'none';
adguardOpts.style.display = 'none';
nginxOpts.style.display = 'none';
- hysteriaOpts.style.display = 'none';
if (portGroup) portGroup.style.display = '';
if (base === 'dns') {
@@ -2040,19 +1919,6 @@
portHint.textContent = _('nginx_port_hint');
nginxOpts.style.display = 'block';
updateNginxDnsHint();
- } else if (base === 'hysteria') {
- // Port is chosen inside hysteriaOptions (dedicated UDP field)
- if (portGroup) portGroup.style.display = 'none';
- const suggested = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 8998) : '8998';
- portInput.value = suggested;
- portInput.disabled = false;
- hysteriaOpts.style.display = 'block';
- const hyPort = document.getElementById('installHysteriaPort');
- if (hyPort) {
- hyPort.value = suggested;
- hyPort.oninput = () => { portInput.value = hyPort.value; };
- }
- updateHysteriaDnsHint();
} else {
portLabel.textContent = _('port') + ' (UDP)';
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 55424) : '55424';
@@ -2119,13 +1985,6 @@
} else if (protoBase(currentInstallProto) === 'nginx') {
params.nginx_domain = document.getElementById('installNginxDomain').value.trim();
params.nginx_email = document.getElementById('installNginxEmail').value.trim();
- } else if (protoBase(currentInstallProto) === 'hysteria') {
- const hyPortEl = document.getElementById('installHysteriaPort');
- if (hyPortEl && hyPortEl.value) {
- params.port = hyPortEl.value;
- }
- params.hysteria_domain = document.getElementById('installHysteriaDomain').value.trim();
- params.hysteria_email = document.getElementById('installHysteriaEmail').value.trim();
}
const result = await apiCall(`/api/servers/${SERVER_ID}/install`, 'POST', params);
clearInterval(progressInterval);
@@ -2299,84 +2158,6 @@
}
}
- // ========== Hysteria Settings (port) ==========
- async function applyHysteriaPort(proto, portValue) {
- const port = parseInt(portValue, 10);
- if (!port || port < 1 || port > 65535) {
- showToast(_('error') + ': invalid port', 'error');
- return;
- }
- if (port === 80) {
- showToast(_('error') + ': port 80 reserved', 'error');
- return;
- }
- try {
- showToast(_('saving') || 'Saving...', 'info');
- const res = await apiCall(`/api/servers/${SERVER_ID}/hysteria/settings`, 'POST', {
- protocol: proto || 'hysteria', port,
- });
- showToast(res.message || _('hysteria_settings_saved'), 'success');
- setTimeout(() => checkServer(), 800);
- } catch (err) {
- showToast(_('error') + ': ' + err.message, 'error');
- // Still open settings so admin can try another port
- openHysteriaSettings(proto);
- }
- }
-
- async function openHysteriaSettings(proto = 'hysteria') {
- const live = currentProtocolStatus[proto] || {};
- const livePort = live.port || 443;
- const suggest = (parseInt(livePort, 10) === 443) ? 8998 : (livePort || 8998);
- document.getElementById('hysteriaSetProto').value = proto;
- document.getElementById('hysteriaSettingsTitle').textContent =
- `${_('hysteria_settings_title')} — ${getProtoTitle(proto)}`;
- document.getElementById('hysteriaSetDomain').value = live.domain || '';
- document.getElementById('hysteriaSetPort').value = suggest;
- openModal('hysteriaSettingsModal');
- // Enrich from server when possible (don't block UI if SSH is slow/fails)
- try {
- const data = await apiCall(`/api/servers/${SERVER_ID}/hysteria/settings?protocol=${encodeURIComponent(proto)}`);
- if (data.domain) document.getElementById('hysteriaSetDomain').value = data.domain;
- const p = parseInt(data.port, 10);
- if (p && p !== 443) document.getElementById('hysteriaSetPort').value = p;
- else if (p === 443) document.getElementById('hysteriaSetPort').value = 8998;
- } catch (_) { /* modal already open with local values */ }
- }
-
- 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 = '';
@@ -2417,7 +2198,7 @@
const sent = userData.dataSent || '';
const initial = name.charAt(0).toUpperCase();
const enabled = (client.enabled !== undefined) ? client.enabled : (userData.enabled !== false);
- const hasPrivKey = !!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt' || protoBase(proto) === 'hysteria';
+ const hasPrivKey = !!userData.clientPrivateKey || proto === 'xray' || proto === 'telemt';
const assignedUser = client.assigned_user || '';
let metaHtml = '';
@@ -2455,7 +2236,7 @@