From 116856c1d1a9288097e62c325bdf491e996716db Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Thu, 15 Jan 2026 00:34:46 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D1=82=20=D0=B2=D0=B8=D0=B7=D1=83=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20=D1=86=D0=B5=D0=BF=D0=BE=D1=87?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D1=80=D0=BE=D0=BA=D1=81=D0=B8,=20=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8?= =?UTF-8?q?=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D1=80=D0=B5=D0=B7=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF=D1=80=D0=BE=D0=BA=D1=81?= =?UTF-8?q?=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DOCKER.md | 62 +++-- web/__pycache__/server.cpython-311.pyc | Bin 0 -> 45451 bytes web/index.html | 360 ++++++++++++++++++++++++- web/server.py | 254 ++++++++++++++++- 4 files changed, 652 insertions(+), 24 deletions(-) create mode 100644 web/__pycache__/server.cpython-311.pyc diff --git a/docs/DOCKER.md b/docs/DOCKER.md index ad29a07..146aee0 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -11,8 +11,6 @@ - 🔄 **Переключение серверов** — в один клик - 💾 **Сохранение настроек** — URL и выбранный сервер сохраняются - - --- ## 🔧 Требования @@ -80,25 +78,57 @@ docker compose up -d ## 🌐 Порты -| Порт | Назначение | URL | -|------|------------|-----| -| `3456` | Веб-интерфейс | http://localhost:3456 | -| `8080` | HTTP/SOCKS5 прокси | `127.0.0.1:8080` | -| `9090` | API управления (внутренний) | — | +| Порт | Назначение | URL | +| ------ | --------------------------- | --------------------- | +| `3456` | Веб-интерфейс | http://localhost:3456 | +| `8080` | HTTP/SOCKS5 прокси | `127.0.0.1:8080` | +| `9090` | API управления (внутренний) | — | + +### 🔧 Изменение порта прокси + +Если порт `8080` уже занят, можно запустить на другом порту (например, `8082`): + +**Способ 1: Через переменную окружения (Mac/Linux)** + +```bash +PROXY_PORT=8082 docker compose up -d +``` + +**Способ 2: Через переменную окружения (Windows PowerShell)** + +```powershell +$env:PROXY_PORT=8082; docker compose up -d +``` + +**Способ 3: Через .env файл (универсальный)** + +Создайте файл `.env` в корне проекта: + +``` +PROXY_PORT=8082 +``` + +Затем запустите: + +```bash +docker compose up -d +``` + +> 💡 URL подключения изменится на `http://127.0.0.1:8082` и `socks5://127.0.0.1:8082` --- ## 📋 Управление контейнером -| Действие | Команда | -|----------|---------| -| Посмотреть статус | `docker ps` | -| Посмотреть логи | `docker logs --tail 50 sing-proxy` | -| Остановить | `docker compose stop` | -| Запустить снова | `docker compose start` | -| Перезапустить | `docker compose restart` | -| Полностью удалить | `docker compose down` | -| Пересобрать | `docker compose up -d --build` | +| Действие | Команда | +| ----------------- | ---------------------------------- | +| Посмотреть статус | `docker ps` | +| Посмотреть логи | `docker logs --tail 50 sing-proxy` | +| Остановить | `docker compose stop` | +| Запустить снова | `docker compose start` | +| Перезапустить | `docker compose restart` | +| Полностью удалить | `docker compose down` | +| Пересобрать | `docker compose up -d --build` | --- diff --git a/web/__pycache__/server.cpython-311.pyc b/web/__pycache__/server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bddfb3baba7a35491f1469780c993c6b2b591c56 GIT binary patch literal 45451 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�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 ABLDyZ literal 0 HcmV?d00001 diff --git a/web/index.html b/web/index.html index 4e4a2c7..65ce8a6 100644 --- a/web/index.html +++ b/web/index.html @@ -238,9 +238,183 @@ + +

+
+ Proxy_Chain +
+ +
+ +
+
+ +
+ You +
+ + +
+
+ +
+ + +
+ + + + +
+
+
+ + + +
+ VPN +
+
+ + +
+ +
+
+ + +
+
+ +
+ Net +
+
+ + +
+ No proxy configured +
+
+ + +
+
+ Connection_Settings +
+ +
+ +
+ HTTP + + +
+ + +
+ SOCKS5 + + +
+
+ +
+ + Use these URLs in browser/app proxy settings +
+
+ + +
+
+ + Fallback_Proxy + +
+ + + +
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+ + URLTest auto-selects fastest proxy. Re-apply subscription after changes. +
+ + + +
+
+
+ class="flex-grow flex flex-col bg-black border border-[#00ff41]/20 overflow-hidden font-mono h-[200px] lg:h-[250px]">
{ + const originalText = btn.textContent; + btn.textContent = 'Copied!'; + btn.classList.add('text-blue-400'); + setTimeout(() => { + btn.textContent = originalText; + btn.classList.remove('text-blue-400'); + }, 1500); + addLog(`COPIED: ${input.value}`, 'success'); + }); + } + els.clearLogs.addEventListener('click', () => { const lastChild = els.logsContainer.lastElementChild; els.logsContainer.innerHTML = ''; @@ -624,6 +832,12 @@ const res = await fetch('/status'); const data = await res.json(); + // Update proxy URLs with actual port + if (data.proxyPort) { + els.httpProxyUrl.value = `http://127.0.0.1:${data.proxyPort}`; + els.socks5ProxyUrl.value = `socks5://127.0.0.1:${data.proxyPort}`; + } + if (data.active && data.tag) { const currentTag = state.activeNode ? state.activeNode.tag : null; @@ -689,9 +903,151 @@ checkConnectionSpeed(true); }); + // --- Fallback Proxy Functions --- + + async function loadFallbackConfig() { + try { + const res = await fetch('/fallback-config'); + const data = await res.json(); + + els.fallbackToggle.checked = data.enabled || false; + els.fallbackHost.value = data.host || '192.168.50.111'; + els.fallbackPort.value = data.port || 8080; + + updateFallbackUI(data.enabled || false); + addLog('FALLBACK_CONFIG_LOADED', 'info'); + } catch (e) { + addLog('FALLBACK_CONFIG_LOAD_FAILED', 'error'); + } + } + + function updateFallbackUI(enabled) { + els.fallbackToggleLabel.textContent = enabled ? 'ON' : 'OFF'; + els.fallbackToggleLabel.classList.toggle('opacity-100', enabled); + els.fallbackToggleLabel.classList.toggle('opacity-50', !enabled); + } + + async function saveFallbackConfig() { + const enabled = els.fallbackToggle.checked; + const host = els.fallbackHost.value.trim(); + const port = parseInt(els.fallbackPort.value) || 8080; + + if (enabled && !host) { + addLog('FALLBACK_HOST_REQUIRED', 'error'); + showFallbackStatus('Host is required', 'error'); + return; + } + + try { + const res = await fetch('/fallback-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ enabled, host, port }) + }); + const data = await res.json(); + + if (data.success) { + addLog(enabled ? 'FALLBACK_ENABLED' : 'FALLBACK_DISABLED', 'success'); + const msg = data.regenerated ? 'Applied!' : 'Saved!'; + showFallbackStatus(msg, 'success'); + updateFallbackUI(enabled); + // Refresh proxy chain visualization + updateProxyChain(); + } else { + throw new Error(data.error); + } + } catch (e) { + addLog(`FALLBACK_SAVE_FAILED: ${e.message}`, 'error'); + showFallbackStatus('Save failed', 'error'); + } + } + + function showFallbackStatus(msg, type = 'info') { + els.fallbackStatus.textContent = msg; + els.fallbackStatus.classList.remove('hidden', 'text-[#00ff41]/50', 'text-red-500', 'text-blue-400'); + + if (type === 'success') els.fallbackStatus.classList.add('text-blue-400'); + else if (type === 'error') els.fallbackStatus.classList.add('text-red-500'); + else els.fallbackStatus.classList.add('text-[#00ff41]/50'); + + setTimeout(() => { + els.fallbackStatus.classList.add('hidden'); + }, 3000); + } + + // --- Proxy Chain Visualization --- + async function updateProxyChain() { + try { + const res = await fetch('/active-proxy'); + const data = await res.json(); + + // Reset all states + els.chainFallbackBox.classList.remove('border-[#00ff41]', 'bg-[#00ff41]/20'); + els.chainFallbackBox.classList.add('border-[#00ff41]/30'); + els.chainVPNBox.classList.remove('border-[#00ff41]', 'bg-[#00ff41]/20'); + els.chainVPNBox.classList.add('border-[#00ff41]/30'); + els.chainFallbackX.classList.add('hidden'); + els.chainFallbackX.classList.remove('flex'); + els.chainVPNX.classList.add('hidden'); + els.chainVPNX.classList.remove('flex'); + + if (!data.configured) { + els.chainStatus.textContent = 'No proxy configured'; + els.chainFallbackRow.classList.add('hidden'); + els.chainVPNLabel.textContent = 'VPN'; + return; + } + + // Update VPN label + els.chainVPNLabel.textContent = data.vpnTag || 'VPN'; + + if (data.fallbackEnabled) { + // Show fallback branch + els.chainFallbackRow.classList.remove('hidden'); + els.chainFallbackLabel.textContent = data.fallbackHost || 'Fallback'; + + if (data.fallbackReachable) { + // Fallback is active (green border, no X) + els.chainFallbackBox.classList.remove('border-[#00ff41]/30'); + els.chainFallbackBox.classList.add('border-[#00ff41]', 'bg-[#00ff41]/20'); + els.chainStatus.innerHTML = ` Fallback active (${data.fallbackLatency}ms)`; + } else { + // Fallback unreachable - show X, VPN is active + els.chainFallbackX.classList.remove('hidden'); + els.chainFallbackX.classList.add('flex'); + els.chainVPNBox.classList.remove('border-[#00ff41]/30'); + els.chainVPNBox.classList.add('border-[#00ff41]', 'bg-[#00ff41]/20'); + els.chainStatus.innerHTML = ` VPN active (fallback down)`; + } + } else { + // No fallback - hide fallback row, VPN is active + els.chainFallbackRow.classList.add('hidden'); + els.chainVPNBox.classList.remove('border-[#00ff41]/30'); + els.chainVPNBox.classList.add('border-[#00ff41]', 'bg-[#00ff41]/20'); + els.chainStatus.innerHTML = ` VPN direct`; + } + + // Reinitialize lucide icons for new X elements + lucide.createIcons(); + + } catch (e) { + els.chainStatus.textContent = 'Failed to load'; + } + } + + // Fallback Event Listeners + els.saveFallbackBtn.addEventListener('click', saveFallbackConfig); + + els.fallbackToggle.addEventListener('change', saveFallbackConfig); + // --- Init --- addLog('TERMINAL_READY', 'info'); loadSaved(); + loadFallbackConfig(); + updateProxyChain(); + + // Periodically update proxy chain + setInterval(updateProxyChain, 10000); // Blink animation style const style = document.createElement('style'); diff --git a/web/server.py b/web/server.py index 47d9da8..560e3bc 100644 --- a/web/server.py +++ b/web/server.py @@ -27,6 +27,14 @@ DATA_DIR = BASE_DIR / "data" CONFIG_FILE = DATA_DIR / "client.json" HWID_FILE = DATA_DIR / "hwid" SUBSCRIPTION_FILE = DATA_DIR / "subscription.json" +FALLBACK_FILE = DATA_DIR / "fallback.json" + +# Default fallback proxy settings +DEFAULT_FALLBACK = { + "enabled": False, + "host": "192.168.50.111", + "port": 8080 +} def get_hwid() -> str: @@ -74,6 +82,27 @@ def load_subscription() -> dict: return None +def save_fallback_config(enabled: bool, host: str, port: int): + """Save fallback proxy configuration to file""" + DATA_DIR.mkdir(parents=True, exist_ok=True) + data = { + "enabled": enabled, + "host": host, + "port": port + } + FALLBACK_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2)) + + +def load_fallback_config() -> dict: + """Load fallback proxy configuration from file""" + if FALLBACK_FILE.exists(): + try: + return json.loads(FALLBACK_FILE.read_text()) + except json.JSONDecodeError: + pass + return DEFAULT_FALLBACK.copy() + + def measure_tcp_latency(host: str, port: int, timeout: float = 2.0) -> int: """Measure TCP latency to a host:port in milliseconds""" start_time = time.time() @@ -339,6 +368,10 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler): self.test_connection() elif self.path.startswith("/static/"): self.serve_static() + elif self.path == "/fallback-config": + self.get_fallback_config() + elif self.path == "/active-proxy": + self.get_active_proxy() else: self.send_error(404) @@ -352,6 +385,8 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler): self.apply_subscription() elif self.path == "/ping-target": self.ping_target() + elif self.path == "/fallback-config": + self.save_fallback_config_endpoint() else: self.send_error(404) @@ -375,6 +410,175 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler): else: self.send_error(404) + def get_fallback_config(self): + """Get fallback proxy configuration""" + fallback = load_fallback_config() + self.send_json({ + "enabled": fallback.get("enabled", False), + "host": fallback.get("host", "192.168.50.111"), + "port": fallback.get("port", 8080) + }) + + def get_active_proxy(self): + """Get information about current active proxy chain""" + result = { + "configured": False, + "fallbackEnabled": False, + "fallbackHost": None, + "vpnTag": None, + "vpnServer": None, + "activeOutbound": None + } + + if not CONFIG_FILE.exists(): + self.send_json(result) + return + + try: + config = json.loads(CONFIG_FILE.read_text()) + outbounds = config.get("outbounds", []) + route_final = config.get("route", {}).get("final") + + result["configured"] = True + + for outbound in outbounds: + out_type = outbound.get("type") + + if out_type == "urltest": + result["fallbackEnabled"] = True + elif out_type == "http" and outbound.get("tag") == "fallback-proxy": + result["fallbackHost"] = f"{outbound.get('server')}:{outbound.get('server_port')}" + elif out_type in ["vless", "vmess", "trojan", "shadowsocks", "hysteria2"]: + result["vpnTag"] = outbound.get("tag") + result["vpnServer"] = outbound.get("server") + + # Determine which is actually active + # For now, we show the configured route + result["activeOutbound"] = route_final + + # Check fallback proxy reachability (quick TCP check) + if result["fallbackEnabled"] and result["fallbackHost"]: + try: + host, port = result["fallbackHost"].split(":") + latency = measure_tcp_latency(host, int(port), timeout=1.0) + result["fallbackReachable"] = latency > 0 + result["fallbackLatency"] = latency if latency > 0 else None + except Exception: + result["fallbackReachable"] = False + result["fallbackLatency"] = None + + except Exception as e: + result["error"] = str(e) + + self.send_json(result) + + def save_fallback_config_endpoint(self): + """Save fallback proxy configuration and regenerate config""" + try: + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length).decode("utf-8") + data = json.loads(body) + + enabled = data.get("enabled", False) + host = data.get("host", "").strip() + port = int(data.get("port", 8080)) + + if enabled and not host: + self.send_json({"success": False, "error": "Host is required"}, 400) + return + + save_fallback_config(enabled, host, port) + + # Regenerate current config if it exists + regenerated = self.regenerate_current_config() + + self.send_json({ + "success": True, + "message": "Fallback config saved", + "regenerated": regenerated + }) + + except json.JSONDecodeError: + self.send_json({"success": False, "error": "Invalid JSON"}, 400) + except Exception as e: + self.send_json({"success": False, "error": str(e)}, 500) + + def regenerate_current_config(self) -> bool: + """Regenerate current config with updated fallback settings""" + if not CONFIG_FILE.exists(): + return False + + try: + config = json.loads(CONFIG_FILE.read_text()) + outbounds = config.get("outbounds", []) + + # Find the VPN outbound (vless, vmess, etc.) + vpn_outbound = None + utility_outbounds = [] + + for outbound in outbounds: + if outbound.get("type") in ["vless", "vmess", "trojan", "shadowsocks", "hysteria2"]: + vpn_outbound = outbound + elif outbound.get("type") in ["direct", "block", "dns"]: + utility_outbounds.append(outbound) + + if not vpn_outbound: + return False + + selected_tag = vpn_outbound.get("tag") + + # Load fallback config + fallback = load_fallback_config() + fallback_enabled = fallback.get("enabled", False) + fallback_host = fallback.get("host", "") + fallback_port = fallback.get("port", 8080) + + # Build new outbounds + final_outbounds = [] + final_tag = selected_tag + + if fallback_enabled and fallback_host: + urltest_outbound = { + "type": "urltest", + "tag": "auto-select", + "outbounds": ["fallback-proxy", selected_tag], + "url": "http://www.gstatic.com/generate_204", + "interval": "30s", + "tolerance": 9999 + } + + fallback_outbound = { + "type": "http", + "tag": "fallback-proxy", + "server": fallback_host, + "server_port": fallback_port + } + + final_outbounds.append(urltest_outbound) + final_outbounds.append(fallback_outbound) + final_tag = "auto-select" + + final_outbounds.append(vpn_outbound) + final_outbounds.extend(utility_outbounds) + + config["outbounds"] = final_outbounds + config["route"]["final"] = final_tag + + # Write config + CONFIG_FILE.write_text(json.dumps(config, indent=2, ensure_ascii=False)) + + # Reload sing-box + try: + urllib.request.urlopen("http://127.0.0.1:9090/reload", timeout=3) + except Exception: + pass + + return True + + except Exception as e: + print(f"[WebUI] Failed to regenerate config: {e}") + return False + def get_status(self): """Get current proxy status""" config_exists = CONFIG_FILE.exists() @@ -395,7 +599,8 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler): self.send_json({ "active": config_exists, "tag": current_tag, - "server": current_server + "server": current_server, + "proxyPort": PROXY_PORT }) def get_subscription(self): @@ -672,12 +877,49 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler): self.send_json({"success": False, "error": f"Сервер '{selected_tag}' не найден"}, 400) return - # Add selected server as main outbound - new_outbounds.insert(0, selected_outbound) + # Load fallback configuration + fallback = load_fallback_config() + fallback_enabled = fallback.get("enabled", False) + fallback_host = fallback.get("host", "") + fallback_port = fallback.get("port", 8080) - # Update route - remove incompatible fields and set only final + # Build outbounds list + final_outbounds = [] + final_tag = selected_tag + + if fallback_enabled and fallback_host: + # Add URLTest for automatic fallback selection + # High tolerance (9999ms) ensures first working proxy is preferred + urltest_outbound = { + "type": "urltest", + "tag": "auto-select", + "outbounds": ["fallback-proxy", selected_tag], + "url": "http://www.gstatic.com/generate_204", + "interval": "30s", + "tolerance": 9999 # Use first working proxy, not fastest + } + + # Add HTTP fallback proxy + fallback_outbound = { + "type": "http", + "tag": "fallback-proxy", + "server": fallback_host, + "server_port": fallback_port + } + + final_outbounds.append(urltest_outbound) + final_outbounds.append(fallback_outbound) + final_tag = "auto-select" + + # Add selected VPN server + final_outbounds.append(selected_outbound) + + # Add utility outbounds (direct, block, dns) + final_outbounds.extend(new_outbounds) + + # Update route routes = { - "final": selected_tag, + "final": final_tag, "auto_detect_interface": True } @@ -703,7 +945,7 @@ class ProxyControlHandler(http.server.BaseHTTPRequestHandler): } ] - config["outbounds"] = new_outbounds + config["outbounds"] = final_outbounds config["route"] = routes # Ensure data directory exists