diff --git a/README.md b/README.md index 36e428c..ebca89f 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ - **Аккаунт**: информация о регистрации, применение лицензионного ключа WARP+ - **Установка и удаление** пакета `cloudflare-warp` прямо из браузера (репозиторий Cloudflare для Debian/Ubuntu) - **SOCKS-прокси**: смена порта `warp-cli proxy` (часто `40000` или `1024`) -- **Маршрутизация WARP**: списки доменов/IP, сохранение, включение/выключение правил Amnezia/Xray, тест внешнего IP напрямую и через WARP -- **WARP Relay beta**: UDP relay через `nftables`/`iptables` на выбранный WARP/WireGuard endpoint, single-port или Cloudflare multiport +- **Маршрутизация WARP**: списки доменов/IP, JSON-подсказка для Xray, включение/выключение правил Amnezia/Xray, тест внешнего IP напрямую и через WARP +- **WARP Relay beta**: UDP relay через `nftables`/`iptables` на выбранный WARP/WireGuard endpoint, single-port или Cloudflare multiport, DNS/ping probe endpoint-ов - **Пресет 3x-ui**: outbound `warp-socks` → `127.0.0.1:ПОРТ` и правило маршрутизации `geosite:google` - **Пресет Amnezia**: мост Docker `172.17.0.1:11025` → SOCKS на хосте, маршрутизация WARP для выбранных клиентов с понятными именами @@ -117,6 +117,7 @@ install.sh / uninstall.sh # Установка и снятие «в одн - **Сохранить список** — записывает настройки в `/etc/warp-webui/warp-routes.json`, но не трогает Xray. - **Включить в Amnezia/Xray** — добавляет outbound `warp-socks`, создаёт backup `server.json`, добавляет глобальное routing-правило и перезапускает контейнер Amnezia. - **Выключить правила** — убирает глобальные правила `warp-socks` из Amnezia/Xray, список остаётся сохранённым. +- **JSON для Xray** — показывает готовые куски `outbound` и `routing_rule` для ручной настройки 3x-ui/Amnezia. - **Тест IP** — показывает внешний IP сервера напрямую и внешний IP через `127.0.0.1:WARP_PROXY_PORT`. Важно: обычный Cloudflare WARP не даёт выбрать конкретную страну или конкретный exit IP. Эта функция выбирает, **какие домены/IP пойдут через WARP**, а не страну выхода. Если нужен гарантированный регион, нужен отдельный VPN/proxy с выбранной страной или корпоративный Cloudflare egress. @@ -159,9 +160,10 @@ install.sh / uninstall.sh # Установка и снятие «в одн - выбрать source IP сервера или оставить автоопределение; - включить режим одного UDP-порта, обычно `4500 -> 4500`; - включить Cloudflare multiport mode; +- проверить endpoint через DNS/ping probe и подставить лучший IP в поле endpoint; - удалить только managed relay rules. -Важно: relay не гарантирует конкретный Cloudflare exit IP или страну. Он меняет WARP/WireGuard endpoint, через который клиент подключается. Для "желаемой точки" сейчас используйте ручной endpoint/IP; автоматический список точек и latency probe запланированы. +Важно: relay не гарантирует конкретный Cloudflare exit IP или страну. Он меняет WARP/WireGuard endpoint, через который клиент подключается. Для "желаемой точки" сейчас используйте ручной endpoint/IP или кнопку **Найти endpoint**. ## Безопасность @@ -183,8 +185,8 @@ install.sh / uninstall.sh # Установка и снятие «в одн | POST | `/connect`, `/disconnect`, `/restart` | Управление WARP | | POST | `/warp-install`, `/warp-uninstall` | Установка/удаление пакета | | POST | `/auth-config` | Смена логина/пароля веб-панели | -| POST | `/relay-apply`, `/relay-remove` | Применить/удалить WARP Relay rules | -| POST | `/warp-routes-save`, `/warp-routes-enable`, `/warp-routes-disable`, `/warp-routes-test` | Сохранить/применить/выключить маршруты WARP и проверить внешний IP | +| POST | `/relay-apply`, `/relay-remove`, `/relay-probe` | Применить/удалить WARP Relay rules, проверить endpoint | +| POST | `/warp-routes-save`, `/warp-routes-enable`, `/warp-routes-disable`, `/warp-routes-test`, `/warp-routes-preview` | Сохранить/применить/выключить маршруты WARP, проверить внешний IP, показать JSON для Xray | | POST | `/proxy-port`, `/license` | Порт SOCKS, ключ WARP+ | | POST | `/xui-preset`, `/amnezia-preset`, `/amnezia-routing` | Пресеты интеграций | diff --git a/app.py b/app.py index 8109c51..e1d9e54 100755 --- a/app.py +++ b/app.py @@ -934,6 +934,35 @@ def test_warp_route_ips(): save_warp_routes(state.get("domains"), state.get("ips"), enabled=state.get("enabled"), last_test=result) return 200, result + +def warp_routes_preview(domains=None, ips=None): + domain_items, ip_items = split_domain_ip_routes(domains=domains, ips=ips) + if not domain_items and not ip_items: + domain_items = ["geosite:google"] + proxy_port = get_proxy_port().get("port") or 1024 + rule = {"type": "field", "outboundTag": SOCKS_OUTBOUND_TAG} + if domain_items: + rule["domain"] = domain_items + if ip_items: + rule["ip"] = ip_items + return { + "domains": domain_items, + "ips": ip_items, + "xray_local_outbound": { + "tag": SOCKS_OUTBOUND_TAG, + "protocol": "socks", + "settings": {"servers": [{"address": "127.0.0.1", "port": int(proxy_port)}]}, + }, + "xray_docker_outbound": { + "tag": SOCKS_OUTBOUND_TAG, + "protocol": "socks", + "settings": {"servers": [{"address": BRIDGE_HOST, "port": BRIDGE_PORT}]}, + }, + "routing_rule": rule, + "curl_test": f"curl -4fsS --socks5-hostname 127.0.0.1:{proxy_port} https://api.ipify.org", + "note_ru": "Для 3x-ui используйте xray_local_outbound. Для Amnezia Docker используйте xray_docker_outbound после bridge.", + } + def client_presets(socks_port: int): host_public = os.environ.get("WARP_PUBLIC_HOST", "") bridge = f"{BRIDGE_HOST}:{BRIDGE_PORT}" @@ -993,6 +1022,64 @@ def resolve_relay_endpoint(endpoint: str) -> dict: return {"endpoint": endpoint, "ip": infos[0][4][0], "source": "dns"} +def resolve_relay_endpoint_all(endpoint: str) -> dict: + endpoint = (endpoint or RELAY_DEFAULT_ENDPOINT).strip() + try: + ip = _valid_ipv4(endpoint) + return {"endpoint": endpoint, "ips": [ip], "source": "ip"} + except ValueError: + pass + if not re.fullmatch(r"[A-Za-z0-9.-]{1,253}", endpoint) or ".." in endpoint: + raise ValueError("invalid endpoint") + infos = socket.getaddrinfo(endpoint, None, socket.AF_INET, socket.SOCK_DGRAM) + ips = [] + seen = set() + for info in infos: + ip = info[4][0] + if ip not in seen: + seen.add(ip) + ips.append(ip) + if not ips: + raise ValueError("endpoint has no IPv4 address") + return {"endpoint": endpoint, "ips": ips, "source": "dns"} + + +def _ping_ipv4(ip: str) -> dict: + code, out, err = run_cmd(["ping", "-c", "1", "-W", "1", ip], timeout=3) + text = f"{out}\n{err}" + m = re.search(r"time[=<]([0-9.]+)\s*ms", text) + latency = float(m.group(1)) if m else None + return { + "ip": ip, + "ok": code == 0, + "latency_ms": latency, + "code": code, + "stderr": err, + } + + +def probe_relay_endpoints(endpoint: str) -> tuple[int, dict]: + try: + resolved = resolve_relay_endpoint_all(endpoint or RELAY_DEFAULT_ENDPOINT) + except Exception as e: + return 400, {"error": str(e)} + probes = [_ping_ipv4(ip) for ip in resolved["ips"][:12]] + best = None + ok = [p for p in probes if p.get("ok") and p.get("latency_ms") is not None] + if ok: + best = sorted(ok, key=lambda p: p["latency_ms"])[0] + elif probes: + best = probes[0] + return 200, { + "endpoint": resolved["endpoint"], + "source": resolved["source"], + "count": len(resolved["ips"]), + "probes": probes, + "best": best, + "note_ru": "Это DNS/ping probe endpoint-а, не выбор страны выхода WARP.", + } + + def get_public_ipv4() -> str: for url in ("https://api.ipify.org", "https://icanhazip.com", "https://ifconfig.me"): code, out, _ = run_cmd(["curl", "-4fsS", "--max-time", "3", url], timeout=5) @@ -1417,9 +1504,11 @@ INDEX_HTML = r""" +
+