diff --git a/docs/BuildFromSource.md b/docs/BuildFromSource.md index 4ec0387..adbd9b9 100644 --- a/docs/BuildFromSource.md +++ b/docs/BuildFromSource.md @@ -48,6 +48,7 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v] | `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (параметр можно указывать несколько раз) | | `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare](./CfProxy.md) | | `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudflare. [Подробнее](./CfProxy.md) | +| `--cfproxy-worker-domain` | | Домен Cloudflare Worker [Подробнее](./CfWorker.md) | | `--cfproxy-priority` | `true` | Пробовать проксировать через Cloudflare перед прямым TCP подключением | | `--fake-tls-domain` | | Включить маскировку Fake TLS (ee-secret) с указанным SNI-доменом | | `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) | diff --git a/docs/CfWorker.md b/docs/CfWorker.md new file mode 100644 index 0000000..0f374ee --- /dev/null +++ b/docs/CfWorker.md @@ -0,0 +1,131 @@ +# Cloudflare Worker + +Альтернативный (полностью бесплатный, не нужно покупать домен в отличии от [CfProxy](./CfProxy.md)) способ проксирования. + +Прокси возвращает доступ к тому, что раньше не загружалось (реакции, некоторые стикеры). Если на аккаунте без Premium с данным способом все еще не загружаются фото/видео, оставьте в блоке `DC → IP` только `4:149.154.167.220` + +## + +1. **Добавьте в [zapret](https://github.com/Flowseal/zapret-discord-youtube/) или в любое другое ПО следующие домены:** +``` +cloudflare.com +cloudflare.dev +workers.dev +``` +2. Создайте аккаунт в [Cloudflare](https://dash.cloudflare.com/) (или войдите в существующий) +3. Слева в панели выберите `Compute` → `Workers & Pages` +4. Нажмите сверху справа кнопку **`Create application`** → `Start with Hello World!` → `Deploy` +5. Сверху справа нажмите кнопку **`Edit code`**, замените код слева на тот, что находится внизу страницы + * Если у вас не загружается код, то вы не выполнили первый пункт +6. Нажмите сверху справа кнопку **`Deploy`** +7. Скопируйте домен из поля справа и укажите его в настройках **Cloudflare Worker** (или через аргумент `--cfproxy-worker-domain`) + * Пример домена: `random-symbols-1234.username.workers.dev` + +### Код Worker'а +```javascript +import { connect } from "cloudflare:sockets"; + +const DC_IPS = { + "1": "149.154.175.50", + "2": "149.154.167.51", + "3": "149.154.175.100", + "4": "149.154.167.91", + "5": "149.154.171.5", + "203": "91.105.192.100", +}; + +function isIpv4(value) { + return /^\d{1,3}(\.\d{1,3}){3}$/.test(value || ""); +} + +function pickDst(url) { + const byDst = url.searchParams.get("dst"); + if (isIpv4(byDst)) { + return byDst; + } + const dc = url.searchParams.get("dc") || "2"; + return DC_IPS[dc] || DC_IPS["2"]; +} + +function toBytes(data) { + if (data instanceof ArrayBuffer) { + return new Uint8Array(data); + } + if (typeof data === "string") { + return new TextEncoder().encode(data); + } + if (data && typeof data.arrayBuffer === "function") { + return data.arrayBuffer().then((ab) => new Uint8Array(ab)); + } + return new Uint8Array(); +} + +export default { + async fetch(request) { + if ((request.headers.get("Upgrade") || "").toLowerCase() !== "websocket") { + return new Response("Expected websocket", { status: 426 }); + } + + const url = new URL(request.url); + if (url.pathname !== "/apiws") { + return new Response("Not found", { status: 404 }); + } + + const dst = pickDst(url); + const pair = new WebSocketPair(); + const client = pair[0]; + const server = pair[1]; + server.accept(); + + const socket = connect({ hostname: dst, port: 443 }); + const tcpReader = socket.readable.getReader(); + const tcpWriter = socket.writable.getWriter(); + + server.addEventListener("message", async (event) => { + try { + await tcpWriter.write(await toBytes(event.data)); + } catch { + try { + server.close(1011, "tcp write failed"); + } catch {} + } + }); + + server.addEventListener("close", async () => { + try { + await tcpWriter.close(); + } catch {} + try { + socket.close(); + } catch {} + }); + + (async () => { + try { + while (true) { + const { value, done } = await tcpReader.read(); + if (done) { + break; + } + if (value) { + server.send(value); + } + } + } catch { + } finally { + try { + server.close(); + } catch {} + try { + tcpReader.releaseLock(); + } catch {} + try { + socket.close(); + } catch {} + } + })(); + + return new Response(null, { status: 101, webSocket: client }); + }, +}; +``` \ No newline at end of file diff --git a/docs/TrayConfig.md b/docs/TrayConfig.md index b8c194f..14822b9 100644 --- a/docs/TrayConfig.md +++ b/docs/TrayConfig.md @@ -23,6 +23,7 @@ Tray-приложение хранит данные в: "cfproxy": true, "cfproxy_priority": true, "cfproxy_user_domain": "", + "cfproxy_worker_domain": "", "appearance": "auto" } ``` diff --git a/proxy/bridge.py b/proxy/bridge.py index 160278e..b4c51cd 100644 --- a/proxy/bridge.py +++ b/proxy/bridge.py @@ -4,6 +4,7 @@ import struct from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from typing import Dict, List, Optional +from urllib.parse import urlencode from .utils import * from .stats import stats @@ -132,14 +133,24 @@ async def do_fallback(reader, writer, relay_init, label, fallback_dst = DC_DEFAULT_IPS.get(dc) use_cf = proxy_config.fallback_cfproxy cf_first = proxy_config.fallback_cfproxy_priority + worker_domain = proxy_config.cfproxy_worker_domain methods: List[str] = ['tcp'] if use_cf: methods.insert(0 if cf_first else 1, 'cf') + if worker_domain: + methods.insert(0, 'cf_worker') for method in methods: - if method == 'cf': + if method == 'cf_worker' and fallback_dst: + ok = await _cfproxy_worker_fallback( + reader, writer, relay_init, label, ctx, + dc=dc, is_media=is_media, fallback_dst=fallback_dst, + splitter=splitter) + if ok: + return True + elif method == 'cf': ok = await _cfproxy_fallback( reader, writer, relay_init, label, ctx, dc=dc, is_media=is_media, @@ -157,6 +168,42 @@ async def do_fallback(reader, writer, relay_init, label, return False +async def _cfproxy_worker_fallback(reader, writer, relay_init, label, + ctx: CryptoCtx, + dc: int, is_media: bool, + fallback_dst: str, + splitter=None): + media_tag = ' media' if is_media else '' + worker_domain = proxy_config.cfproxy_worker_domain + if not worker_domain: + return False + + query = urlencode({ + 'dst': fallback_dst, + 'dc': str(dc), + 'media': '1' if is_media else '0', + }) + path = f'/apiws?{query}' + + log.info("[%s] DC%d%s -> trying CF worker for %s", + label, dc, media_tag, fallback_dst) + + try: + ws = await RawWebSocket.connect(worker_domain, worker_domain, + timeout=10.0, path=path) + except Exception as exc: + log.warning("[%s] DC%d%s CF worker failed: %s", + label, dc, media_tag, repr(exc)) + return False + + stats.connections_cfproxy += 1 + await ws.send(relay_init) + await bridge_ws_reencrypt(reader, writer, ws, label, ctx, + dc=dc, is_media=is_media, + splitter=splitter) + return True + + async def _cfproxy_fallback(reader, writer, relay_init, label, ctx: CryptoCtx, dc: int, is_media: bool, diff --git a/proxy/config.py b/proxy/config.py index 8c2a32f..85a8948 100644 --- a/proxy/config.py +++ b/proxy/config.py @@ -60,6 +60,7 @@ class ProxyConfig: fallback_cfproxy: bool = True fallback_cfproxy_priority: bool = True cfproxy_user_domain: str = '' + cfproxy_worker_domain: str = '' fake_tls_domain: str = '' proxy_protocol: bool = False diff --git a/proxy/raw_websocket.py b/proxy/raw_websocket.py index 7106ddc..30d07e9 100644 --- a/proxy/raw_websocket.py +++ b/proxy/raw_websocket.py @@ -78,7 +78,8 @@ class RawWebSocket: self._closed = False @staticmethod - async def connect(host: str, domain: str, timeout: float = 10.0) -> 'RawWebSocket': + async def connect(host: str, domain: str, timeout: float = 10.0, + path: str = '/apiws') -> 'RawWebSocket': reader, writer = await asyncio.wait_for( asyncio.open_connection(host, 443, ssl=_ssl_ctx, server_hostname=domain), @@ -89,7 +90,7 @@ class RawWebSocket: ws_key = base64.b64encode(os.urandom(16)).decode() req = ( - f'GET /apiws HTTP/1.1\r\n' + f'GET {path} HTTP/1.1\r\n' f'Host: {domain}\r\n' f'Upgrade: websocket\r\n' f'Connection: Upgrade\r\n' diff --git a/proxy/tg_ws_proxy.py b/proxy/tg_ws_proxy.py index 01a2cf3..fa984b5 100644 --- a/proxy/tg_ws_proxy.py +++ b/proxy/tg_ws_proxy.py @@ -590,6 +590,9 @@ async def _run(stop_event: Optional[asyncio.Event] = None): prio = 'CF first' if proxy_config.fallback_cfproxy_priority else 'TCP first' user_domain = "user" if proxy_config.cfproxy_user_domain else "auto" log.info(" CF proxy: enabled (%s | %s)", prio, user_domain) + if proxy_config.cfproxy_worker_domain: + log.info(" CF worker: enabled (%s)", + proxy_config.cfproxy_worker_domain) log.info("=" * 60) log.info(" Connect:") if ftls: @@ -687,6 +690,10 @@ def main(): ap.add_argument('--cfproxy-domain', type=str, default='', metavar='DOMAIN', help='User defined Cloudflare-proxied domain for WS fallback') + ap.add_argument('--cfproxy-worker-domain', type=str, default='', + metavar='DOMAIN', + help='Cloudflare Worker domain for WS fallback ' + '(tried before other fallback methods)') ap.add_argument('--no-cfproxy', action='store_true', help='Disable Cloudflare proxy fallback') ap.add_argument('--cfproxy-priority', type=_parse_bool, default=True, @@ -732,6 +739,7 @@ def main(): proxy_config.fallback_cfproxy = not args.no_cfproxy proxy_config.fallback_cfproxy_priority = args.cfproxy_priority proxy_config.cfproxy_user_domain = args.cfproxy_domain.strip() + proxy_config.cfproxy_worker_domain = args.cfproxy_worker_domain.strip() proxy_config.fake_tls_domain = args.fake_tls_domain.strip() proxy_config.proxy_protocol = args.proxy_protocol diff --git a/utils/default_config.py b/utils/default_config.py index 8e0dc5b..037edd7 100644 --- a/utils/default_config.py +++ b/utils/default_config.py @@ -20,6 +20,7 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = { "cfproxy": True, "cfproxy_priority": True, "cfproxy_user_domain": "", + "cfproxy_worker_domain": "", } diff --git a/utils/tray_common.py b/utils/tray_common.py index 3e3c70f..9d63e5c 100644 --- a/utils/tray_common.py +++ b/utils/tray_common.py @@ -273,6 +273,7 @@ def apply_proxy_config(cfg: dict) -> bool: pc.fallback_cfproxy = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"]) pc.fallback_cfproxy_priority = cfg.get("cfproxy_priority", DEFAULT_CONFIG["cfproxy_priority"]) pc.cfproxy_user_domain = cfg.get("cfproxy_user_domain", DEFAULT_CONFIG["cfproxy_user_domain"]) + pc.cfproxy_worker_domain = cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG["cfproxy_worker_domain"]) return True