diff --git a/app.py b/app.py index b145e5e..c0f4bb5 100644 --- a/app.py +++ b/app.py @@ -103,7 +103,7 @@ else: application_path = os.path.dirname(__file__) DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export -CURRENT_VERSION = "v2.6.4" +CURRENT_VERSION = "v2.6.5" RELEASES_REPO_URL = repo_url() RELEASES_API_LATEST = api_latest_url() BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin')) @@ -199,12 +199,26 @@ def get_ssh(server): ) -def get_server_connect_host(server: dict) -> str: +WG_AWG_PROTOCOLS = frozenset({'awg', 'awg2', 'awg_legacy', 'wireguard'}) + + +def get_server_connect_host(server: dict, protocol: Optional[str] = None) -> str: """Hostname embedded in client VPN configs (Endpoint, vless://, etc.). SSH still uses server['host']; this value can be a DNS name so clients keep working after the server IP changes — update the A-record only. + Per-protocol connect_domain (AWG / WireGuard) overrides the server default. """ + protocols = server.get('protocols') or {} + if protocol: + base = protocol_base(protocol) + if base in WG_AWG_PROTOCOLS: + for key in (protocol, base): + pinfo = protocols.get(key) if key else None + if isinstance(pinfo, dict): + domain = (pinfo.get('connect_domain') or '').strip() + if domain: + return domain.lower().rstrip('.') info = server.get('server_info') or {} for key in ('connect_domain', 'ssl_domain'): val = (info.get(key) or '').strip() @@ -1446,7 +1460,7 @@ def _create_remote_client(manager, protocol: str, server: dict, name: str, sourc base = protocol_base(protocol) if base == 'telemt': user_data = (source_client or {}).get('userData') or {} - connect_host = get_server_connect_host(server) + connect_host = get_server_connect_host(server, protocol) return manager.add_client( protocol, name, connect_host, port, telemt_quota=user_data.get('quota'), @@ -1455,7 +1469,7 @@ def _create_remote_client(manager, protocol: str, server: dict, name: str, sourc user_ad_tag=user_data.get('user_ad_tag'), max_tcp_conns=user_data.get('max_tcp_conns'), ) - connect_host = get_server_connect_host(server) + connect_host = get_server_connect_host(server, protocol) if base == 'wireguard': return manager.add_client(name, connect_host) return manager.add_client(protocol, name, connect_host, port) @@ -1663,9 +1677,9 @@ async def perform_mass_operations(delete_uids: List[str] = None, toggle_uids: Li manager = get_protocol_manager(ssh, c_req['protocol']) if c_req['protocol'] == 'wireguard': - res = await asyncio.to_thread(manager.add_client, c_req['name'], get_server_connect_host(srv)) + res = await asyncio.to_thread(manager.add_client, c_req['name'], get_server_connect_host(srv, 'wireguard')) else: - res = await asyncio.to_thread(_manager_call, manager, 'add_client', c_req['protocol'], c_req['name'], get_server_connect_host(srv), port) + res = await asyncio.to_thread(_manager_call, manager, 'add_client', c_req['protocol'], c_req['name'], get_server_connect_host(srv, c_req['protocol']), port) if res.get('client_id'): new_conn = { @@ -2129,6 +2143,11 @@ class EditServerRequest(BaseModel): connect_domain: Optional[str] = None +class ConnectDomainRequest(BaseModel): + connect_domain: str = '' + protocol: Optional[str] = None + + class ReorderServersRequest(BaseModel): # `order[i]` is the *old* server index now at position `i` in the new layout. order: List[int] @@ -3049,6 +3068,75 @@ async def api_edit_server(request: Request, server_id: int, req: EditServerReque return JSONResponse({'error': str(e)}, status_code=500) +@app.get('/api/servers/{server_id}/connect-domain', tags=["Servers"]) +async def api_get_connect_domain(request: Request, server_id: int, protocol: Optional[str] = None): + if not _check_admin(request): + return JSONResponse({'error': 'Forbidden'}, status_code=403) + data = load_data() + if server_id >= len(data['servers']): + return JSONResponse({'error': 'Server not found'}, status_code=404) + server = data['servers'][server_id] + proto = (protocol or '').strip() + info = server.get('server_info') or {} + proto_domain = '' + if proto: + pinfo = (server.get('protocols') or {}).get(proto) or {} + proto_domain = (pinfo.get('connect_domain') or '').strip() + return { + 'connect_domain': (info.get('connect_domain') or '').strip(), + 'protocol_connect_domain': proto_domain, + 'protocol': proto, + 'effective_host': get_server_connect_host(server, proto or None), + 'ssh_host': server.get('host') or '', + } + + +@app.post('/api/servers/{server_id}/connect-domain', tags=["Servers"]) +async def api_set_connect_domain(request: Request, server_id: int, req: ConnectDomainRequest): + """Set client Endpoint hostname for AWG / WireGuard without re-verifying SSH.""" + 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) + server = data['servers'][server_id] + domain = (req.connect_domain or '').strip().lower().rstrip('.') + proto = (req.protocol or '').strip() + + if proto: + base = protocol_base(proto) + if base not in WG_AWG_PROTOCOLS: + return JSONResponse( + {'error': 'Per-protocol domain is only supported for AmneziaWG / WireGuard'}, + status_code=400, + ) + protocols = server.setdefault('protocols', {}) + proto_info = protocols.setdefault(proto, {}) + if domain: + proto_info['connect_domain'] = domain + else: + proto_info.pop('connect_domain', None) + else: + info = dict(server.get('server_info') or {}) + if domain: + info['connect_domain'] = domain + else: + info.pop('connect_domain', None) + server['server_info'] = info + + save_data(data) + return { + 'status': 'success', + 'connect_domain': domain, + 'protocol': proto, + 'effective_host': get_server_connect_host(server, proto or None), + } + except Exception as e: + logger.exception('Error saving connect domain') + return JSONResponse({'error': str(e)}, status_code=500) + + @app.get('/api/servers/{server_id}/ping', tags=["Servers"]) async def api_server_ping(request: Request, server_id: int): """Cheap reachability check: opens a TCP connection to the SSH port, @@ -3983,7 +4071,7 @@ async def api_protocol_export_clients(request: Request, server_id: int, req: Pro continue try: config = _manager_call( - manager, 'get_client_config', req.protocol, client_id, get_server_connect_host(server), port + manager, 'get_client_config', req.protocol, client_id, get_server_connect_host(server, req.protocol), port ) except Exception as exc: skipped.append(f'{client_name}: {exc}') @@ -4467,7 +4555,7 @@ async def api_add_connection(request: Request, server_id: int, req: AddConnectio if protocol_base(req.protocol) == 'telemt': result = manager.add_client( - req.protocol, req.name, get_server_connect_host(server), port, + req.protocol, req.name, get_server_connect_host(server, req.protocol), port, telemt_quota=req.telemt_quota, telemt_max_ips=req.telemt_max_ips, telemt_expiry=req.telemt_expiry, @@ -4476,9 +4564,9 @@ async def api_add_connection(request: Request, server_id: int, req: AddConnectio max_tcp_conns=req.telemt_max_conns ) elif protocol_base(req.protocol) == 'wireguard': - result = manager.add_client(req.name, get_server_connect_host(server)) + result = manager.add_client(req.name, get_server_connect_host(server, req.protocol)) else: - result = manager.add_client(req.protocol, req.name, get_server_connect_host(server), port) + result = manager.add_client(req.protocol, req.name, get_server_connect_host(server, req.protocol), port) ssh.disconnect() if result.get('config'): @@ -4601,7 +4689,7 @@ async def api_get_connection_config(request: Request, server_id: int, req: Conne ssh = get_ssh(server) ssh.connect() manager = get_protocol_manager(ssh, req.protocol) - config = _manager_call(manager, 'get_client_config', req.protocol, req.client_id, get_server_connect_host(server), port) + config = _manager_call(manager, 'get_client_config', req.protocol, req.client_id, get_server_connect_host(server, req.protocol), port) ssh.disconnect() vpn_link = generate_vpn_link(config) if config else '' return {'config': config, 'vpn_link': vpn_link} @@ -4784,7 +4872,7 @@ async def api_add_user(request: Request, req: AddUserRequest): manager = get_protocol_manager(ssh, req.protocol) if protocol_base(req.protocol) == 'telemt': conn_result = manager.add_client( - req.protocol, conn_name, get_server_connect_host(server), port, + req.protocol, conn_name, get_server_connect_host(server, req.protocol), port, telemt_quota=req.telemt_quota, telemt_max_ips=req.telemt_max_ips, telemt_expiry=req.telemt_expiry, @@ -4793,7 +4881,7 @@ async def api_add_user(request: Request, req: AddUserRequest): max_tcp_conns=req.telemt_max_conns ) else: - conn_result = manager.add_client(req.protocol, conn_name, get_server_connect_host(server), port) + conn_result = manager.add_client(req.protocol, conn_name, get_server_connect_host(server, req.protocol), port) ssh.disconnect() if conn_result.get('client_id'): @@ -5001,12 +5089,12 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo # Link existing client config = await asyncio.to_thread( _manager_call, manager, 'get_client_config', - req.protocol, req.client_id, get_server_connect_host(server), port, + req.protocol, req.client_id, get_server_connect_host(server, req.protocol), port, ) result = {'client_id': req.client_id, 'config': config} elif protocol_base(req.protocol) == 'telemt': result = await asyncio.to_thread( - manager.add_client, req.protocol, req.name, get_server_connect_host(server), port, + manager.add_client, req.protocol, req.name, get_server_connect_host(server, req.protocol), port, telemt_quota=req.telemt_quota, telemt_max_ips=req.telemt_max_ips, telemt_expiry=req.telemt_expiry, @@ -5015,11 +5103,11 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo max_tcp_conns=req.telemt_max_conns, ) elif protocol_base(req.protocol) == 'wireguard': - result = await asyncio.to_thread(manager.add_client, req.name, get_server_connect_host(server)) + result = await asyncio.to_thread(manager.add_client, req.name, get_server_connect_host(server, req.protocol)) else: result = await asyncio.to_thread( _manager_call, manager, 'add_client', - req.protocol, req.name, get_server_connect_host(server), port, + req.protocol, req.name, get_server_connect_host(server, req.protocol), port, ) finally: await asyncio.to_thread(ssh.disconnect) @@ -5210,7 +5298,7 @@ async def api_share_config(token: str, connection_id: str, request: Request): ssh.connect() # Use appropriate manager for the protocol manager = get_protocol_manager(ssh, conn['protocol']) - config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server), port) + config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server, conn['protocol']), port) ssh.disconnect() vpn_link = generate_vpn_link(config) if config else '' return {'config': config, 'vpn_link': vpn_link, 'expires_at': user.get('expiration_date')} @@ -5370,7 +5458,7 @@ async def api_guest_config(token: str, connection_id: str, request: Request): ssh = get_ssh(server) ssh.connect() manager = get_protocol_manager(ssh, conn['protocol']) - config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server), port) + config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server, conn['protocol']), port) ssh.disconnect() vpn_link = generate_vpn_link(config) if config else '' return {'config': config, 'vpn_link': vpn_link, 'expires_at': holder.get('expiration_date')} @@ -5434,11 +5522,11 @@ async def api_guest_create(token: str, req: GuestCreateRequest, request: Request try: manager = get_protocol_manager(ssh, protocol) if protocol_base(protocol) == 'wireguard': - result = await asyncio.to_thread(manager.add_client, name, get_server_connect_host(server)) + result = await asyncio.to_thread(manager.add_client, name, get_server_connect_host(server, protocol)) else: result = await asyncio.to_thread( _manager_call, manager, 'add_client', - protocol, name, get_server_connect_host(server), port, + protocol, name, get_server_connect_host(server, protocol), port, ) finally: await asyncio.to_thread(ssh.disconnect) @@ -5596,11 +5684,11 @@ async def _create_config_for_protocol( try: manager = get_protocol_manager(ssh, protocol) if protocol_base(protocol) == 'wireguard': - result = await asyncio.to_thread(manager.add_client, name, get_server_connect_host(server)) + result = await asyncio.to_thread(manager.add_client, name, get_server_connect_host(server, protocol)) else: result = await asyncio.to_thread( _manager_call, manager, 'add_client', - protocol, name, get_server_connect_host(server), port, + protocol, name, get_server_connect_host(server, protocol), port, ) finally: await asyncio.to_thread(ssh.disconnect) @@ -5971,7 +6059,7 @@ async def api_my_connection_config(request: Request, connection_id: str): ssh.connect() # Use appropriate manager for the protocol (fixes Telemt/Xray not working for users) manager = get_protocol_manager(ssh, conn['protocol']) - config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server), port) + config = _manager_call(manager, 'get_client_config', conn['protocol'], conn['client_id'], get_server_connect_host(server, conn['protocol']), port) ssh.disconnect() vpn_link = generate_vpn_link(config) if config else '' expires_at = None diff --git a/docker-compose.yml b/docker-compose.yml index 15fb7c8..fb1a596 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,7 @@ services: labels: - traefik.enable=true - traefik.docker.network=dokploy-network + - traefik.http.services.amnezia_panel.loadbalancer.server.port=5000 expose: - "5000" healthcheck: diff --git a/templates/server.html b/templates/server.html index feb8833..a7b1387 100644 --- a/templates/server.html +++ b/templates/server.html @@ -155,6 +155,25 @@ +