diff --git a/README.md b/README.md index 7d522c5..74d227a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Amnezia Web Panel -A modern, high-performance web interface for managing **AmneziaWG**, **Classic WireGuard**, **Xray (XTLS-Reality)**, **Hysteria 2**, **NaiveProxy**, **Telemt (Telegram MTProxy)**, **Cloudflare WARP**, **AmneziaDNS**, **AdGuard Home**, **SOCKS5**, and **NGINX + Let's Encrypt** services on remote Ubuntu servers — from a single dashboard. Designed to provide a premium user experience with robust administrative capabilities. +A modern, high-performance web interface for managing **AmneziaWG**, **Classic WireGuard**, **Xray (XTLS-Reality)**, **Hysteria 2**, **NaiveProxy**, **Telemt (Telegram MTProxy)**, **Cloudflare WARP**, **NordVPN**, **AmneziaDNS**, **AdGuard Home**, **SOCKS5**, and **NGINX + Let's Encrypt** services on remote Ubuntu servers — from a single dashboard. Designed to provide a premium user experience with robust administrative capabilities. > ### 🔄 Compatibility with Official Amnezia Client > @@ -66,6 +66,7 @@ Configuration panel for system parameters and preferences: * **NaiveProxy** (stable): HTTPS/HTTP2 camouflage proxy via [klzgrad/naiveproxy](https://github.com/klzgrad/naiveproxy) (Caddy + [klzgrad/forwardproxy](https://github.com/klzgrad/forwardproxy) naïve fork). ACME TLS on the domain, per-client basic auth, `naive+https://` share links. Requires free TCP **80** and **443**. **Use Karing** as the client — do **not** use v2rayN; other clients are untested. * **Telemt (Telegram MTProxy)**: High-performance Telegram MTProxy with TLS emulation and comprehensive management (quotas, IP limits, real-time session tracking). Robust install path that auto-configures Docker's official apt/yum repository when needed. * **Cloudflare WARP**: Add and manage WARP-powered connectivity from the panel for routing and network flexibility. + * **NordVPN**: Connect/disconnect outbound NordVPN from Settings via the official CLI (optional country/city). * **🛠 Services**: * **AmneziaDNS**: Internal DNS resolver on a private docker network (`amnezia-dns-net`, IP `172.29.172.254`) to prevent DNS leaks and blockings. * **AdGuard Home**: DNS-based ad blocker with a web admin UI. Two install modes: **Replace AmneziaDNS** (takes its IP, all VPN clients use AdGuard immediately) or **Side-by-side** (parallel deployment on `172.29.172.253`, web UI accessible only over the VPN by default). Optional opt-in checkboxes to expose the web UI / DoT / DoH on the host. @@ -87,6 +88,9 @@ Configuration panel for system parameters and preferences: * Native **RTL (Right-to-Left)** support for Persian language. * **🔄 Auto-update**: * Settings → About checks releases on [git.evilfox.cc](https://git.evilfox.cc/test2/Amnezia-Web-Panel-main) and can install the latest tag via git + restart (when the panel runs from a git checkout). +* **🌍 Tunnels & outbound VPN**: + * **Cloudflare Quick Tunnel** and **ngrok** expose the local panel to the internet. + * **Cloudflare WARP** and **NordVPN** route the host outbound (no public panel URL). * **👥 Advanced User Management**: * Role-based access (Admin, Support, Regular User). * Traffic limits, status monitoring, and account expiration. @@ -224,6 +228,9 @@ GitHub Actions workflows in `.github/workflows/`: ## 📋 Fix / changelog (this fork) +### v2.5.0 +* **NordVPN in Tunnels** — connect/disconnect outbound NordVPN from Settings (official `nordvpn` CLI; optional country/city). + ### v2.4.0 * **Auto-update from Gitea**: Settings → About checks https://git.evilfox.cc/test2/Amnezia-Web-Panel-main releases and can install the latest tag via `git fetch` + checkout, then restart (git install only). diff --git a/app.py b/app.py index 03ca7cb..85f04df 100644 --- a/app.py +++ b/app.py @@ -102,7 +102,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.4.0" +CURRENT_VERSION = "v2.5.0" 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')) @@ -138,6 +138,7 @@ TUNNEL_RUNTIMES = { TUNNEL_LOCK = threading.Lock() TUNNEL_URL_RE = re.compile(r'https://[^\s"\']+') WARP_CLI_COMMAND = 'warp-cli.exe' if os.name == 'nt' else 'warp-cli' +NORDVPN_CLI_COMMAND = 'nordvpn.exe' if os.name == 'nt' else 'nordvpn' @@ -373,6 +374,152 @@ def disable_warp(): return get_warp_status() +def get_nordvpn_cli_binary(): + found = shutil.which(NORDVPN_CLI_COMMAND) + if found: + return found + if os.name == 'nt': + for base in (os.environ.get('ProgramFiles'), os.environ.get('ProgramFiles(x86)')): + if not base: + continue + for sub in ('NordVPN', 'NordVPN NordSec'): + candidate = os.path.join(base, sub, 'nordvpn.exe') + if os.path.exists(candidate): + return candidate + return None + + +def is_nordvpn_cli_installed(): + return bool(get_nordvpn_cli_binary()) + + +def _nordvpn_install_hint(): + system = platform.system().lower() + if system == 'windows': + return ( + 'Install NordVPN from https://nordvpn.com/download/ or Microsoft Store, ' + 'then restart this panel.' + ) + if system == 'darwin': + return 'Install NordVPN for macOS from https://nordvpn.com/download/, then restart this panel.' + return ( + 'Install NordVPN CLI: sh <(curl -sSf https://downloads.nordcdn.com/apps/linux/install.sh), ' + 'add your user to the nordvpn group, run nordvpn login, then restart this panel.' + ) + + +def run_nordvpn_cli(*args, timeout: int = 30): + binary = get_nordvpn_cli_binary() + if not binary: + raise RuntimeError(_nordvpn_install_hint()) + creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 + result = subprocess.run( + [binary, *args], + capture_output=True, + text=True, + encoding='utf-8', + errors='replace', + timeout=timeout, + creationflags=creationflags, + ) + output = '\n'.join(part.strip() for part in (result.stdout, result.stderr) if part and part.strip()) + if result.returncode != 0: + raise RuntimeError(output or f'nordvpn exited with code {result.returncode}') + return output + + +def _parse_nordvpn_status(output: str): + lowered = (output or '').lower() + if 'not logged in' in lowered or 'login required' in lowered or 'please log in' in lowered: + return 'not_logged_in' + if 'disconnected' in lowered: + return 'disconnected' + if 'connecting' in lowered: + return 'connecting' + if 'connected' in lowered: + return 'connected' + if 'daemon' in lowered or 'nordvpnd' in lowered: + return 'service_unavailable' + return 'unknown' + + +def _nordvpn_server_line(output: str) -> str: + for line in (output or '').splitlines(): + stripped = line.strip() + if not stripped: + continue + low = stripped.lower() + if low.startswith('server:') or low.startswith('country:') or low.startswith('your new ip:'): + return stripped + return '' + + +def get_nordvpn_status(): + installed = is_nordvpn_cli_installed() + status = { + 'installed': installed, + 'running': False, + 'connected': False, + 'status': 'not_installed' if not installed else 'unknown', + 'server': '', + 'raw': '', + 'last_error': '' if installed else _nordvpn_install_hint(), + 'install_hint': _nordvpn_install_hint(), + } + if not installed: + return status + try: + output = run_nordvpn_cli('status', timeout=15) + parsed = _parse_nordvpn_status(output) + status.update({ + 'running': parsed in ('connected', 'connecting'), + 'connected': parsed == 'connected', + 'status': parsed, + 'server': _nordvpn_server_line(output), + 'raw': output, + 'last_error': '', + }) + except Exception as e: + status['last_error'] = str(e) + lowered = str(e).lower() + if 'not logged in' in lowered or 'login' in lowered: + status['status'] = 'not_logged_in' + elif 'daemon' in lowered or 'nordvpnd' in lowered or 'service' in lowered or 'permission' in lowered: + status['status'] = 'service_unavailable' + else: + status['status'] = 'error' + return status + + +def enable_nordvpn(country: str = '', city: str = ''): + current = get_nordvpn_status() + if not current.get('installed'): + raise RuntimeError(current.get('install_hint') or _nordvpn_install_hint()) + if current.get('status') == 'not_logged_in': + raise RuntimeError( + 'NordVPN is not logged in. Run `nordvpn login` on this host (browser flow), then retry.' + ) + args = ['connect'] + country = (country or '').strip() + city = (city or '').strip() + if country and city: + args.extend([country, city]) + elif country: + args.append(country) + run_nordvpn_cli(*args, timeout=45) + time.sleep(1) + return get_nordvpn_status() + + +def disable_nordvpn(): + current = get_nordvpn_status() + if not current.get('installed'): + raise RuntimeError(current.get('install_hint') or _nordvpn_install_hint()) + run_nordvpn_cli('disconnect', timeout=30) + time.sleep(0.5) + return get_nordvpn_status() + + def get_tunnel_command_name(provider: str): if provider == 'cloudflare': return 'cloudflared.exe' if os.name == 'nt' else 'cloudflared' @@ -1998,6 +2145,11 @@ class ApplyUpdateRequest(BaseModel): allow_dirty: bool = False +class NordvpnConnectRequest(BaseModel): + country: Optional[str] = '' + city: Optional[str] = '' + + # ======================== Startup ======================== @app.on_event("startup") @@ -5583,6 +5735,7 @@ async def api_tunnels_status(request: Request): 'cloudflare': get_tunnel_status('cloudflare'), 'ngrok': get_tunnel_status('ngrok'), 'warp': get_warp_status(), + 'nordvpn': get_nordvpn_status(), } @@ -5668,6 +5821,28 @@ async def api_warp_disconnect(request: Request): return JSONResponse({'error': str(e)}, status_code=500) +@app.post('/api/settings/nordvpn/connect', tags=["Settings"]) +async def api_nordvpn_connect(request: Request, req: NordvpnConnectRequest = NordvpnConnectRequest()): + if not _check_admin(request): + return JSONResponse({'error': 'Forbidden'}, status_code=403) + try: + return await asyncio.to_thread(enable_nordvpn, req.country or '', req.city or '') + except Exception as e: + logger.exception("Error connecting NordVPN") + return JSONResponse({'error': str(e)}, status_code=500) + + +@app.post('/api/settings/nordvpn/disconnect', tags=["Settings"]) +async def api_nordvpn_disconnect(request: Request): + if not _check_admin(request): + return JSONResponse({'error': 'Forbidden'}, status_code=403) + try: + return await asyncio.to_thread(disable_nordvpn) + except Exception as e: + logger.exception("Error disconnecting NordVPN") + return JSONResponse({'error': str(e)}, status_code=500) + + # @app.post('/api/settings/save') # async def api_save_settings(request: Request, body: SaveSettingsRequest): # _check_admin(request) diff --git a/templates/settings.html b/templates/settings.html index aa8e2ee..6b67476 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -284,6 +284,30 @@ + +
{{ _('tunnels_hint') }}
@@ -956,6 +980,7 @@ updateTunnelProvider('cloudflare', data.cloudflare || {}); updateTunnelProvider('ngrok', data.ngrok || {}); updateWarpStatus(data.warp || {}); + updateNordvpnStatus(data.nordvpn || {}); } catch (err) { showToast(`${_('error')}: ` + err.message, 'error'); } @@ -1025,6 +1050,81 @@ } } + function updateNordvpnStatus(status) { + const badge = document.getElementById('nordvpnInstallBadge'); + const statusText = document.getElementById('nordvpnStatusText'); + const serverText = document.getElementById('nordvpnServerText'); + const hintText = document.getElementById('nordvpnHintText'); + const connectBtn = document.getElementById('nordvpnConnectBtn'); + const disconnectBtn = document.getElementById('nordvpnDisconnectBtn'); + const connectText = document.getElementById('nordvpnConnectBtnText'); + if (!badge || !statusText || !connectBtn || !disconnectBtn || !connectText) return; + + const installed = !!status.installed; + const connected = !!status.connected || status.status === 'connected'; + badge.className = connected ? 'badge badge-success' : (installed ? 'badge badge-info' : 'badge badge-secondary'); + badge.textContent = connected ? _('nordvpn_connected') : (installed ? _('installed') : _('not_installed')); + + const statusKey = `nordvpn_status_${status.status || 'unknown'}`; + statusText.textContent = _(statusKey) || status.status || _('nordvpn_status_unknown'); + if (serverText) { + const serverLine = status.server || ''; + serverText.textContent = serverLine; + serverText.classList.toggle('hidden', !serverLine); + } + hintText.textContent = status.last_error || (installed ? _('nordvpn_hint') : (status.install_hint || _('nordvpn_install_hint'))); + connectText.textContent = installed ? _('nordvpn_connect') : _('nordvpn_install_required'); + connectBtn.disabled = connected; + disconnectBtn.classList.toggle('hidden', !connected); + } + + async function connectNordvpn() { + const btn = document.getElementById('nordvpnConnectBtn'); + const spinner = document.getElementById('nordvpnConnectSpinner'); + const text = document.getElementById('nordvpnConnectBtnText'); + btn.disabled = true; + spinner.classList.remove('hidden'); + text.textContent = _('loading'); + + try { + const country = (document.getElementById('nordvpnCountry') || {}).value || ''; + const city = (document.getElementById('nordvpnCity') || {}).value || ''; + const status = await apiCall('/api/settings/nordvpn/connect', 'POST', { + country: country.trim(), + city: city.trim(), + }); + updateNordvpnStatus(status); + showToast(_('nordvpn_connected'), 'success'); + } catch (err) { + showToast(`${_('error')}: ` + err.message, 'error'); + await loadTunnelStatus(); + } finally { + spinner.classList.add('hidden'); + } + } + + async function disconnectNordvpn() { + const btn = document.getElementById('nordvpnDisconnectBtn'); + const spinner = document.getElementById('nordvpnDisconnectSpinner'); + const text = document.getElementById('nordvpnDisconnectBtnText'); + btn.disabled = true; + spinner.classList.remove('hidden'); + text.textContent = _('loading'); + + try { + const status = await apiCall('/api/settings/nordvpn/disconnect', 'POST'); + updateNordvpnStatus(status); + showToast(_('nordvpn_disconnected'), 'success'); + } catch (err) { + showToast(`${_('error')}: ` + err.message, 'error'); + await loadTunnelStatus(); + } finally { + btn.disabled = false; + text.textContent = _('nordvpn_disconnect'); + spinner.classList.add('hidden'); + } + } + async function enableTunnel(provider) { const btn = document.getElementById(`${provider}TunnelBtn`); const spinner = document.getElementById(`${provider}TunnelSpinner`); diff --git a/translations/en.json b/translations/en.json index 6e36570..7de3617 100644 --- a/translations/en.json +++ b/translations/en.json @@ -505,6 +505,23 @@ "warp_status_not_registered": "Cloudflare WARP is installed but not registered yet. Connect will try to register automatically.", "warp_status_service_unavailable": "Cloudflare WARP service is not available. Make sure warp-svc is running.", "warp_status_error": "Cloudflare WARP returned an error.", + "nordvpn_hint": "NordVPN routes this host outbound through NordVPN servers. It does not create a public panel URL like Quick Tunnel or ngrok. Run nordvpn login on the host once before connecting.", + "nordvpn_install_hint": "Install the official NordVPN client/CLI on this host, run nordvpn login, then restart the panel.", + "nordvpn_install_required": "Install NordVPN first", + "nordvpn_connect": "Connect NordVPN", + "nordvpn_disconnect": "Disconnect", + "nordvpn_connected": "NordVPN connected", + "nordvpn_disconnected": "NordVPN disconnected", + "nordvpn_status_unknown": "NordVPN status is unknown.", + "nordvpn_status_not_installed": "NordVPN CLI is not installed on this host.", + "nordvpn_status_connected": "NordVPN is connected.", + "nordvpn_status_connecting": "NordVPN is connecting.", + "nordvpn_status_disconnected": "NordVPN is disconnected.", + "nordvpn_status_not_logged_in": "NordVPN is installed but not logged in. Run nordvpn login on this host.", + "nordvpn_status_service_unavailable": "NordVPN daemon is not available. Make sure nordvpnd is running.", + "nordvpn_status_error": "NordVPN returned an error.", + "nordvpn_country_placeholder": "Country (optional, e.g. Germany or de)", + "nordvpn_city_placeholder": "City (optional, e.g. Berlin)", "install_starting": "Starting installation...", "server_check_complete": "Check completed", "server_connection_error": "Connection error", diff --git a/translations/fa.json b/translations/fa.json index dedf37e..9804e38 100644 --- a/translations/fa.json +++ b/translations/fa.json @@ -448,6 +448,23 @@ "warp_status_not_registered": "Cloudflare WARP is installed but not registered yet. Connect will try to register automatically.", "warp_status_service_unavailable": "Cloudflare WARP service is not available. Make sure warp-svc is running.", "warp_status_error": "Cloudflare WARP returned an error.", + "nordvpn_hint": "NordVPN routes this host outbound through NordVPN servers. It does not create a public panel URL like Quick Tunnel or ngrok. Run nordvpn login on the host once before connecting.", + "nordvpn_install_hint": "Install the official NordVPN client/CLI on this host, run nordvpn login, then restart the panel.", + "nordvpn_install_required": "Install NordVPN first", + "nordvpn_connect": "Connect NordVPN", + "nordvpn_disconnect": "Disconnect", + "nordvpn_connected": "NordVPN connected", + "nordvpn_disconnected": "NordVPN disconnected", + "nordvpn_status_unknown": "NordVPN status is unknown.", + "nordvpn_status_not_installed": "NordVPN CLI is not installed on this host.", + "nordvpn_status_connected": "NordVPN is connected.", + "nordvpn_status_connecting": "NordVPN is connecting.", + "nordvpn_status_disconnected": "NordVPN is disconnected.", + "nordvpn_status_not_logged_in": "NordVPN is installed but not logged in. Run nordvpn login on this host.", + "nordvpn_status_service_unavailable": "NordVPN daemon is not available. Make sure nordvpnd is running.", + "nordvpn_status_error": "NordVPN returned an error.", + "nordvpn_country_placeholder": "Country (optional, e.g. Germany or de)", + "nordvpn_city_placeholder": "City (optional, e.g. Berlin)", "install_starting": "Starting installation...", "server_check_complete": "Check completed", "server_connection_error": "Connection error", diff --git a/translations/fr.json b/translations/fr.json index 248ae03..b4a82c9 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -448,6 +448,23 @@ "warp_status_not_registered": "Cloudflare WARP is installed but not registered yet. Connect will try to register automatically.", "warp_status_service_unavailable": "Cloudflare WARP service is not available. Make sure warp-svc is running.", "warp_status_error": "Cloudflare WARP returned an error.", + "nordvpn_hint": "NordVPN routes this host outbound through NordVPN servers. It does not create a public panel URL like Quick Tunnel or ngrok. Run nordvpn login on the host once before connecting.", + "nordvpn_install_hint": "Install the official NordVPN client/CLI on this host, run nordvpn login, then restart the panel.", + "nordvpn_install_required": "Install NordVPN first", + "nordvpn_connect": "Connect NordVPN", + "nordvpn_disconnect": "Disconnect", + "nordvpn_connected": "NordVPN connected", + "nordvpn_disconnected": "NordVPN disconnected", + "nordvpn_status_unknown": "NordVPN status is unknown.", + "nordvpn_status_not_installed": "NordVPN CLI is not installed on this host.", + "nordvpn_status_connected": "NordVPN is connected.", + "nordvpn_status_connecting": "NordVPN is connecting.", + "nordvpn_status_disconnected": "NordVPN is disconnected.", + "nordvpn_status_not_logged_in": "NordVPN is installed but not logged in. Run nordvpn login on this host.", + "nordvpn_status_service_unavailable": "NordVPN daemon is not available. Make sure nordvpnd is running.", + "nordvpn_status_error": "NordVPN returned an error.", + "nordvpn_country_placeholder": "Country (optional, e.g. Germany or de)", + "nordvpn_city_placeholder": "City (optional, e.g. Berlin)", "install_starting": "Starting installation...", "server_check_complete": "Check completed", "server_connection_error": "Connection error", diff --git a/translations/ru.json b/translations/ru.json index 0cab006..c0bcb49 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -505,6 +505,23 @@ "warp_status_not_registered": "Cloudflare WARP установлен, но ещё не зарегистрирован. Подключение попробует зарегистрировать его автоматически.", "warp_status_service_unavailable": "Сервис Cloudflare WARP недоступен. Убедитесь, что warp-svc запущен.", "warp_status_error": "Cloudflare WARP вернул ошибку.", + "nordvpn_hint": "NordVPN направляет исходящий трафик этого хоста через серверы NordVPN. Не создаёт публичный URL панели как Quick Tunnel или ngrok. Перед подключением один раз выполните nordvpn login на хосте.", + "nordvpn_install_hint": "Установите официальный клиент/CLI NordVPN на этом хосте, выполните nordvpn login и перезапустите панель.", + "nordvpn_install_required": "Сначала установите NordVPN", + "nordvpn_connect": "Подключить NordVPN", + "nordvpn_disconnect": "Отключить", + "nordvpn_connected": "NordVPN подключён", + "nordvpn_disconnected": "NordVPN отключён", + "nordvpn_status_unknown": "Статус NordVPN неизвестен.", + "nordvpn_status_not_installed": "CLI NordVPN не установлен на этом хосте.", + "nordvpn_status_connected": "NordVPN подключён.", + "nordvpn_status_connecting": "NordVPN подключается.", + "nordvpn_status_disconnected": "NordVPN отключён.", + "nordvpn_status_not_logged_in": "NordVPN установлен, но не выполнен вход. Запустите nordvpn login на хосте.", + "nordvpn_status_service_unavailable": "Демон NordVPN недоступен. Убедитесь, что nordvpnd запущен.", + "nordvpn_status_error": "NordVPN вернул ошибку.", + "nordvpn_country_placeholder": "Страна (необяз., напр. Germany или de)", + "nordvpn_city_placeholder": "Город (необяз., напр. Berlin)", "install_starting": "Начинаем установку...", "server_check_complete": "Проверка завершена", "server_connection_error": "Ошибка подключения", diff --git a/translations/zh.json b/translations/zh.json index 51003e2..c8d2e3a 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -448,6 +448,23 @@ "warp_status_not_registered": "Cloudflare WARP is installed but not registered yet. Connect will try to register automatically.", "warp_status_service_unavailable": "Cloudflare WARP service is not available. Make sure warp-svc is running.", "warp_status_error": "Cloudflare WARP returned an error.", + "nordvpn_hint": "NordVPN routes this host outbound through NordVPN servers. It does not create a public panel URL like Quick Tunnel or ngrok. Run nordvpn login on the host once before connecting.", + "nordvpn_install_hint": "Install the official NordVPN client/CLI on this host, run nordvpn login, then restart the panel.", + "nordvpn_install_required": "Install NordVPN first", + "nordvpn_connect": "Connect NordVPN", + "nordvpn_disconnect": "Disconnect", + "nordvpn_connected": "NordVPN connected", + "nordvpn_disconnected": "NordVPN disconnected", + "nordvpn_status_unknown": "NordVPN status is unknown.", + "nordvpn_status_not_installed": "NordVPN CLI is not installed on this host.", + "nordvpn_status_connected": "NordVPN is connected.", + "nordvpn_status_connecting": "NordVPN is connecting.", + "nordvpn_status_disconnected": "NordVPN is disconnected.", + "nordvpn_status_not_logged_in": "NordVPN is installed but not logged in. Run nordvpn login on this host.", + "nordvpn_status_service_unavailable": "NordVPN daemon is not available. Make sure nordvpnd is running.", + "nordvpn_status_error": "NordVPN returned an error.", + "nordvpn_country_placeholder": "Country (optional, e.g. Germany or de)", + "nordvpn_city_placeholder": "City (optional, e.g. Berlin)", "install_starting": "Starting installation...", "server_check_complete": "Check completed", "server_connection_error": "Connection error",