From 986133974135c9a3ce63293bbdca737e9fa4e494 Mon Sep 17 00:00:00 2001 From: Daniel Lavrushin Date: Sun, 31 May 2026 20:36:55 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20implement=20WebSocket=20connection=20po?= =?UTF-8?q?oling=20and=20improve=20MTProto=20tran=E2=80=A6=20(#229)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog.md | 5 + docs/docs/mtproto.md | 253 ++++++----- .../current/mtproto.md | 226 ++++++---- docs/static/img/mtproto/20260531200322.png | Bin 0 -> 196288 bytes src/config/config.go | 2 + src/config/migration.go | 15 + src/config/types.go | 8 + src/config/validation.go | 2 +- src/http/handler/common.go | 33 +- src/http/handler/config.go | 13 +- src/http/handler/mtproto.go | 22 +- .../sets/routing/TrafficRouting.tsx | 41 +- .../ui/src/components/settings/MTProto.tsx | 115 +++-- src/http/ui/src/components/settings/Page.tsx | 32 +- src/http/ui/src/i18n/en.json | 27 +- src/http/ui/src/i18n/ru.json | 27 +- src/http/ui/src/models/config.ts | 5 +- src/http/ui/src/utils/clipboard.ts | 33 +- src/main.go | 19 + src/mtproto/cfproxy.go | 384 +++++++++++++++++ src/mtproto/cfproxy_cooldown_test.go | 57 +++ src/mtproto/cfproxy_test.go | 104 +++++ src/mtproto/dc.go | 86 ++++ src/mtproto/dc_test.go | 46 ++ src/mtproto/faketls.go | 3 +- src/mtproto/obfuscated2.go | 292 +++++++++++-- src/mtproto/plantransports_test.go | 116 ++++- src/mtproto/server.go | 204 +++++++-- src/mtproto/transparent.go | 171 ++++++++ src/mtproto/transparent_test.go | 189 +++++++++ src/mtproto/wsdial.go | 123 +++++- src/mtproto/wspool.go | 397 ++++++++++++++++++ src/nfq/handler.go | 2 +- src/tables/routing.go | 21 +- src/tables/routing_proxy.go | 2 +- src/tproxy/listener.go | 42 +- src/tproxy/manager.go | 26 +- 37 files changed, 2752 insertions(+), 391 deletions(-) create mode 100644 docs/static/img/mtproto/20260531200322.png create mode 100644 src/mtproto/cfproxy.go create mode 100644 src/mtproto/cfproxy_cooldown_test.go create mode 100644 src/mtproto/cfproxy_test.go create mode 100644 src/mtproto/dc_test.go create mode 100644 src/mtproto/transparent.go create mode 100644 src/mtproto/transparent_test.go create mode 100644 src/mtproto/wspool.go diff --git a/changelog.md b/changelog.md index 3bceb961..07427354 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,10 @@ # B4 - Bye Bye Big Bro +## [1.64.0] - 2026-05-31 + +- IMPROVED: **MTProto Telegram routing is more reliable, especially for media** - the WebSocket routing (bridge mode) now works on phones as well as desktop, loads media more reliably, and always prefers WebSocket regardless of the proxy server's transport setting. Data centers without a WebSocket edge no longer stall when the shared fallback domains get rate-limited. New "Cloudflare Worker" support adds a free, per-user relay (no domain to buy) for accounts where media, reactions, or stickers still fail to load - see Settings -> MTProto Proxy. +- FIXED: **Copy buttons did nothing when the Web UI was opened over plain HTTP** - copying commands, set exports, or system info silently failed on installs reached by LAN IP (for example `http://192.168.1.1`), because browsers only allow the modern clipboard API over HTTPS or localhost. The fallback copy path now works in those cases too. + ## [1.63.0] - 2026-05-22 - FIXED: **Geo databases sometimes saved to a broken `b4` folder after install** - in rare cases the installer would record `b4/geosite.dat` instead of a full path like `/etc/b4/geosite.dat`, and the Web UI then refused to download new files. The installer now refuses non-absolute paths, b4 fixes any broken path it finds on startup, and the UI falls back to a safe default if the stored path looks wrong. diff --git a/docs/docs/mtproto.md b/docs/docs/mtproto.md index 5230f4d8..f44e4a0b 100644 --- a/docs/docs/mtproto.md +++ b/docs/docs/mtproto.md @@ -1,32 +1,143 @@ --- sidebar_position: 12 -title: MTProto Proxy +title: MTProto / Telegram --- -# MTProto Proxy for Telegram +# Telegram with B4 -B4 includes a built-in Telegram MTProto proxy that disguises traffic as a regular HTTPS connection to a popular website. +B4 can keep Telegram working on a censored network in two ways. They are independent and can run at the same time: -## Two deployment scenarios +1. **MTProto proxy server** - clients add B4 as an MTProto proxy inside the Telegram app (with a secret). Use this when a device should connect *to* B4, for example a phone on a cellular network reaching your home B4. +2. **Telegram over WebSocket (transparent bridge)** - a per-set routing mode that intercepts Telegram traffic from LAN devices and relays it for them. No in-app proxy and no secret. Use this to fix Telegram for every device behind B4 at once. -![mtproto](/img/mtproto/20260322135051.png) +Both rely on the same **Telegram upstream** settings (how B4 itself reaches Telegram's data centers), so that section is described once and applies to either mode. -### Scenario 1: B4 on a VPS abroad (simple) +| Aspect | Proxy server | WebSocket bridge | +| ----------------- | ----------------------------------------- | ------------------------- | +| Where configured | Settings → MTProto | A set's routing mode | +| Per-device setup | Add proxy + secret in Telegram | None (transparent) | +| Good for | One device reaching B4 (incl. remote/home)| All LAN devices at once | +| Needs MTProto on | Yes | No (independent) | -B4 runs on a server outside the censored zone. Users from inside Russia connect Telegram directly to the VPS. +--- -```text -Phone (Russia) ──────▶ B4 on VPS ──────▶ Telegram - TSPU sees - "HTTPS to google.com" - (not blocked) +## Telegram upstream (shared) + +Settings → **MTProto Proxy** → **Telegram upstream**. This controls how B4 reaches Telegram's data centers. It is used by the proxy server *and* by the WebSocket bridge mode, so it applies even when the proxy server is turned off. + +### Transport mode + +- **Direct TCP** - fastest. Use when the B4 host can reach `149.154.0.0/16` directly (e.g. a VPS abroad). +- **Auto (WebSocket → TCP)** - try WebSocket first via `kws*.web.telegram.org`, fall back to direct TCP. Recommended on censored networks. +- **WebSocket only** - strict WebSocket transport, no TCP fallback. + +:::info +The transport-mode dropdown applies to the **proxy server** only. The **WebSocket bridge** routing mode always uses Auto (WebSocket-first with TCP fallback). +::: + +### Cloudflare Worker domain (recommended fallback) + +If media, reactions, or stickers fail to load, set a **Cloudflare Worker domain**. It is a free per-user WebSocket relay you host on your own Cloudflare account (`*.workers.dev`). B4 can reach any data center through it, so it rescues DCs with no native WebSocket edge (1, 3, 5) and connections throttled on the shared CF pool. The worker is tried after Telegram's own edge (so the fast native path still wins for DC 2/4) and before the shared CF pool. + +Setup, in short: + +1. Create a free Cloudflare account. +2. In **Compute → Workers & Pages**, create a Worker from the default template and deploy it. +3. Replace the worker code with the proxy script, then redeploy. +4. Copy the worker's `name-1234.username.workers.dev` domain into the **Cloudflare Worker domain** field. Comma-separate multiple workers. + +Make sure `cloudflare.com`, `cloudflare.dev`, and `workers.dev` are reachable (not blocked) on your network. + +The worker script and the full step-by-step are maintained by tg-ws-proxy: [CfWorker.md](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfWorker.md). B4 dials the worker at `/apiws`, matching that script. + +### CF proxy fallback + +A rotating pool of Cloudflare-proxied domains used as a fallback when Telegram's native edge cannot reach a data center (notably DC 1, needed for media in foreign channels). The pool refreshes hourly. + +### Testing + +- **Test connection** probes DC 2 over the configured transport(s) and reports latency. +- **Test direct TCP** probes DC 2 over direct TCP, bypassing any DC Relay, to isolate whether a problem is the relay or Telegram itself. + +--- + +## Option 1: MTProto proxy server + +A Telegram proxy that clients connect to with a secret. B4 disguises the traffic as a regular HTTPS connection to a popular website. + +![20260531200322](../../../../static/img/mtproto/20260531200322.png) + +### Step 1: Configure B4 + +In the B4 web UI → **Settings** → **MTProto Proxy**: + +1. **Enable MTProto Proxy** - turn it on +2. **Port** - listen port (recommended: `443`) +3. **Fake SNI Domain** - domain to impersonate (e.g. `storage.googleapis.com`) +4. Click **Generate Secret** +5. Copy the **Secret** value +6. Save settings and restart B4 + +Set the **Telegram upstream** transport (above) according to where B4 runs: + +- **B4 on a VPS abroad** - Direct TCP. B4 reaches Telegram directly; leave DC Relay empty. +- **B4 on a router inside Russia** - Auto (WebSocket → TCP). B4 reaches Telegram over the WebSocket edge, so no VPS relay is required. If WebSocket is also blocked on your network, use a DC Relay (below). + +### Step 2: Configure Telegram + +1. Open **Telegram** → **Settings** → **Data and Storage** → **Proxy** +2. Tap **Add Proxy** +3. Choose **MTProto** +4. Fill in: + - **Server**: B4 IP or hostname (LAN IP for local devices; public IP or DDNS for remote use, with port forwarding) + - **Port**: the port from step 1 + - **Secret**: the copied secret +5. Tap **Done** and enable the proxy + +![telegra](/img/mtproto/20260322135130.png) + +You can also use the **Share connection link** button to generate a `tg://proxy` link or QR code for another device. + +--- + +## Option 2: Telegram over WebSocket (transparent bridge) + +A per-set routing mode that fixes Telegram for every device behind B4, with no in-app proxy and no VPS. When a device connects to a Telegram data center, B4 transparently intercepts the session and relays it over Telegram's WebSocket edge (with Cloudflare fallback). + +This mode runs on its own. The MTProto proxy server under Settings → MTProto does **not** need to be enabled. + +### Setup + +1. Create or open a set and give it the **`telegram`** target in both the geosite and geoip categories (so the set matches Telegram's domains and IP ranges). +2. In the set's **Routing** tab, enable routing and set **Routing mode** to **Telegram over WebSocket (built-in)**. +3. Choose the **source interfaces** (the LAN interfaces whose devices should be bridged). Leave empty to bridge all devices. +4. Save. + +A minimal set for this mode: + +```json +{ + "name": "telegram-ws", + "targets": { + "geosite_categories": ["telegram"], + "geoip_categories": ["telegram"] + }, + "enabled": true, + "routing": { "enabled": true, "mode": "mtproto-ws" } +} ``` -Setup takes 2 minutes. No extra software needed. +The shared **Telegram upstream** settings (Settings → MTProto) apply here, so configure a Cloudflare Worker domain there if media fails to load. -### Scenario 2: B4 on a router inside Russia (with relay) +:::info Best-effort +Only TCP MTProto sessions are bridged. Voice calls and transports B4 cannot map to a data center fall open to a direct connection. +::: -B4 runs on a router or machine inside Russia. An additional VPS is required to forward traffic, because TSPU blocks all direct IP-level connections to Telegram servers. +--- + +## DC Relay (VPS + socat) + +Use a DC Relay only when B4 runs inside the censored zone *and* the WebSocket transport is also blocked, so direct IP-level connections to Telegram must go through a VPS. ```text Phone ──────▶ B4 (router) ──────▶ VPS ──────▶ Telegram @@ -35,96 +146,36 @@ Phone ──────▶ B4 (router) ──────▶ VPS ──── (not blocked) (not blocked) ``` -The VPS only needs a simple TCP forwarder — no keys, no MTProto-specific software. - ---- - -## Scenario 1: B4 on a VPS - -### Step 1: Configure B4 - -In the B4 web UI → **Settings** → **General** → **MTProto Proxy**: - -1. **Enable MTProto Proxy** — turn it on -2. **Port** — listen port (recommended: `443`) -3. **Fake SNI Domain** — domain to impersonate (e.g. `storage.googleapis.com`) -4. Click **Generate Secret** -5. Copy the **Secret** value -6. Save settings and restart B4 - -Leave **DC Relay** empty — B4 on the VPS reaches Telegram directly. - -### Step 2: Configure Telegram - -1. Open **Telegram** → **Settings** → **Data and Storage** → **Proxy** -2. Tap **Add Proxy** -3. Choose **MTProto** -4. Fill in: - - **Server**: VPS IP or hostname - - **Port**: the port from step 1 - - **Secret**: the copied secret -5. Tap **Done** and enable the proxy - -![telegra](/img/mtproto/20260322135130.png) - ---- - -## Scenario 2: B4 on a router inside Russia +The VPS only needs a simple TCP forwarder (`socat`); no keys, no MTProto-specific software. ### Step 1: Install socat on the VPS -On any VPS abroad, install socat: - ```bash apt install -y socat ``` -B4 will generate the actual forwarding commands in the next step. There's no point hardcoding IPs in this guide — Telegram periodically changes DC addresses and adds new ones (for example, the media DC `203`). +### Step 2: Set the DC Relay address -### Step 2: Configure B4 +In **Settings** → **MTProto Proxy**, set **DC Relay** to the VPS address with the base port (e.g. `my-vps.com:7007`). The field appears when the transport mode is Direct TCP or Auto. -In the B4 web UI → **Settings** → **General** → **MTProto Proxy**: +With Auto + a DC Relay configured, relay TCP is tried first and WebSocket is used as the fallback. -1. **Enable MTProto Proxy** — turn it on -2. **Port** — listen port (e.g. `7002`) -3. **Fake SNI Domain** — domain to impersonate (e.g. `storage.googleapis.com`) -4. **DC Relay** — VPS address with the base port (e.g. `my-vps.com:7007`) -5. Click **Generate Secret** -6. Copy the **Secret** value +### Step 3: Get the socat commands -### Step 3: Get the socat commands for the VPS - -Click the **?** button next to the **DC Relay** field. The "DC Relay socat setup" dialog opens, listing the current Telegram DCs and ready-to-run `socat` commands for each one (including the media DC). - -Click **Copy all**, switch to the VPS, and run the commands. +Click the **?** button next to the **DC Relay** field. The "DC Relay socat setup" dialog lists the current Telegram DCs and ready-to-run `socat` commands for each one (including the media DC). Click **Copy all**, switch to the VPS, and run them. :::info Why the helper -The DC list is fetched live from `getProxyConfig` — Telegram's own published list. B4 computes the relay port as `base_port + |DC| - 1`. If Telegram adds a new DC or changes an IP, the helper shows the up-to-date commands without needing to update this guide. +The DC list is fetched live from `getProxyConfig` - Telegram's own published list. B4 computes the relay port as `base_port + |DC| - 1`. If Telegram adds a new DC or changes an IP, the helper shows up-to-date commands without needing to update this guide. ::: :::warning VPS firewall -Open every port the helper shows on the VPS firewall (the "Open these ports on the VPS firewall" line at the bottom of the dialog). Currently this is typically 6 ports: five for the main DCs (1–5) and one for the media DC `203`. +Open every port the helper shows (the "Open these ports on the VPS firewall" line at the bottom of the dialog). This is typically 6 ports: five for the main DCs (1-5) and one for the media DC `203`. ::: :::tip To auto-start `socat`, add the commands to `/etc/rc.local` or create a systemd service. ::: -### Step 4: Save B4 - -Once socat is running on the VPS, click **Save** in B4 and restart B4. - -### Step 5: Configure Telegram - -1. Open **Telegram** → **Settings** → **Data and Storage** → **Proxy** -2. Tap **Add Proxy** -3. Choose **MTProto** -4. Fill in: - - **Server**: IP of the router/machine running B4 - - **Port**: the port from step 2 - - **Secret**: the copied secret -5. Tap **Done** and enable the proxy - --- ## Choosing a fake SNI domain @@ -139,36 +190,44 @@ The domain should be: If someone connects to the B4 port without the correct secret, B4 transparently forwards them to the real site (the one configured in Fake SNI). A scanner sees an ordinary site, not a proxy. ::: +--- + ## Troubleshooting ### Telegram shows "Connecting…" -- Make sure `socat` is running on the VPS and the ports are reachable (Scenario 2) -- Double-check the VPS address in the DC Relay field -- B4 logs should show `MTProto fake-TLS handshake OK` and `MTProto relay` lines +- If using the WebSocket transport, run **Test connection** to confirm B4 can reach a DC. +- If using a DC Relay, make sure `socat` is running on the VPS and the ports are reachable, and double-check the VPS address. +- B4 logs should show `MTProto fake-TLS handshake OK` and `MTProto relay` lines. + +### Media, stickers, or reactions fail to load + +- Set a **Cloudflare Worker domain** in the Telegram upstream settings. DC 1 (media for foreign channels) is the usual culprit, and the CF Worker / CF proxy fallback rescues it. ### Wrong secret -In the logs: `HMAC verification failed` - -The secret in Telegram doesn't match the one configured in B4. +In the logs: `HMAC verification failed`. The secret in Telegram does not match the one configured in B4. ### Clock skew -In the logs: `timestamp out of range` +In the logs: `timestamp out of range`. The clocks on the device and the B4 machine disagree. Sync them (NTP). -The clocks on the device and the B4 machine disagree. Sync them (NTP). +### VPS unreachable (DC Relay) -### VPS unreachable - -In the logs: `dial DC ... i/o timeout` +In the logs: `dial DC ... i/o timeout`. - VPS is off, or `socat` is not running - VPS firewall blocks inbound connections on the required ports ### No response from Telegram -In the logs: `DC->client: 0 bytes` +In the logs: `DC->client: 0 bytes`. -- If DC Relay is **not set**: Telegram servers are blocked by IP. You need a VPS relay (Scenario 2). -- If DC Relay **is set**: `socat` is not running on the VPS, or the wrong port was specified. +- Direct TCP and no relay: Telegram servers are blocked by IP. Switch the transport to Auto/WebSocket, or set up a DC Relay. +- DC Relay set: `socat` is not running on the VPS, or the wrong port was specified. + +--- + +## Credits + +The WebSocket transport and the Cloudflare Worker relay are inspired by [tg-ws-proxy](https://github.com/Flowseal/tg-ws-proxy). diff --git a/docs/i18n/ru/docusaurus-plugin-content-docs/current/mtproto.md b/docs/i18n/ru/docusaurus-plugin-content-docs/current/mtproto.md index 500cb382..7f48dc2e 100644 --- a/docs/i18n/ru/docusaurus-plugin-content-docs/current/mtproto.md +++ b/docs/i18n/ru/docusaurus-plugin-content-docs/current/mtproto.md @@ -1,49 +1,75 @@ --- sidebar_position: 12 -title: MTProto Прокси +title: MTProto / Telegram --- -# MTProto Прокси для Telegram +# Telegram с B4 -B4 включает встроенный прокси для Telegram, который маскирует трафик под обычное HTTPS-соединение к популярному сайту. +B4 может сохранять работу Telegram в сети с цензурой двумя способами. Они независимы и могут работать одновременно: -## Два сценария использования +1. **MTProto прокси-сервер** - клиент добавляет B4 как MTProto-прокси в самом приложении Telegram (по секрету). Подходит, когда устройство должно подключаться *к* B4, например телефон в сотовой сети достучивается до домашнего B4. +2. **Telegram через WebSocket (прозрачный мост)** - режим маршрутизации на уровне сета, который перехватывает трафик Telegram от устройств в локальной сети и ретранслирует его за них. Без прокси в приложении и без секрета. Подходит, чтобы починить Telegram сразу для всех устройств за B4. -![mtproto](/img/mtproto/20260322135051.png) +Оба способа опираются на одни и те же настройки **Транспорт к Telegram** (как сам B4 достигает дата-центров Telegram), поэтому этот раздел описан один раз и относится к любому из режимов. -### Сценарий 1: B4 на VPS за границей (простой) - -B4 установлен на сервере за пределами цензурированной зоны. Пользователи из России подключают Telegram напрямую к VPS. - -```text -Телефон (Россия) ──────▶ B4 на VPS ──────▶ Telegram - ТСПУ видит - «HTTPS к google.com» - (не блокирует) -``` - -Настройка занимает 2 минуты. Дополнительное ПО не требуется. - -### Сценарий 2: B4 на роутере внутри России (с relay) - -B4 установлен на роутере или машине внутри России. Требуется дополнительный VPS для пересылки трафика, так как ТСПУ блокирует все прямые соединения к серверам Telegram по IP. - -```text -Телефон ──────▶ B4 (роутер) ──────▶ VPS ──────▶ Telegram - ТСПУ видит ТСПУ видит - «HTTPS к google.com» «трафик к VPS» - (не блокирует) (не блокирует) -``` - -На VPS достаточно запустить простую пересылку трафика - без ключей и настроек. +| Параметр | Прокси-сервер | WebSocket-мост | +| ------------------------ | ----------------------------------------------------------- | ----------------------------------- | +| Где настраивается | Settings → MTProto | Режим маршрутизации сета | +| Настройка на устройстве | Добавить прокси и секрет в Telegram | Не нужна (прозрачно) | +| Для чего | Одно устройство достучивается до B4 (в т.ч. удалённо/домой) | Все устройства локальной сети сразу | +| Нужен включённый MTProto | Да | Нет (независим) | --- -## Сценарий 1: B4 на VPS +## Транспорт к Telegram (общий) + +Settings → **MTProto Proxy** → **Telegram upstream**. Определяет, как B4 достигает дата-центров Telegram. Используется и прокси-сервером, и режимом WebSocket-моста, поэтому действует даже когда прокси-сервер выключен. + +### Режим транспорта + +- **Прямой TCP** - самый быстрый. Подходит, если хост с B4 может напрямую достучаться до `149.154.0.0/16` (например, VPS за границей). +- **Авто (WebSocket → TCP)** - сначала WebSocket через `kws*.web.telegram.org`, при неудаче - прямой TCP. Рекомендуется для сетей с цензурой. +- **Только WebSocket** - жёсткий WebSocket-транспорт, без TCP-резерва. + +:::info +Выбор режима транспорта касается только **прокси-сервера**. Режим маршрутизации **WebSocket-мост** всегда использует Авто (сначала WebSocket, затем TCP). +::: + +### Домен Cloudflare Worker (рекомендуемый резерв) + +Если не загружаются медиа, реакции или стикеры, укажите **домен Cloudflare Worker**. Это бесплатный персональный WebSocket-релей, который вы разворачиваете на собственном аккаунте Cloudflare (`*.workers.dev`). Через него B4 достаёт любой дата-центр, поэтому он выручает DC без родного WebSocket-узла (1, 3, 5) и соединения, упёршиеся в лимиты общего пула CF. Воркер пробуется после родного узла Telegram (чтобы для DC 2/4 побеждал быстрый родной путь) и перед общим пулом CF. + +Коротко о настройке: + +1. Заведите бесплатный аккаунт Cloudflare. +2. В **Compute → Workers & Pages** создайте Worker из шаблона по умолчанию и задеплойте его. +3. Замените код воркера на скрипт прокси и задеплойте снова. +4. Скопируйте домен воркера вида `name-1234.username.workers.dev` в поле **домен Cloudflare Worker**. Несколько воркеров указывайте через запятую. + +Убедитесь, что `cloudflare.com`, `cloudflare.dev` и `workers.dev` доступны (не заблокированы) в вашей сети. + +Скрипт воркера и подробная пошаговая инструкция поддерживаются проектом tg-ws-proxy: [CfWorker.md](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfWorker.md). B4 обращается к воркеру по пути `/apiws`, как и этот скрипт. + +### Резерв через CF-прокси + +Ротируемый пул проксированных через Cloudflare доменов, который используется как резерв, когда родной узел Telegram не может достучаться до дата-центра (особенно DC 1, нужного для медиа во внешних каналах). Пул обновляется раз в час. + +### Проверка + +- **Проверить соединение** пробует достучаться до DC 2 настроенным транспортом (или транспортами) и измеряет задержку. +- **Проверить прямой TCP** пробует DC 2 по прямому TCP, минуя DC Relay, чтобы понять, в чём проблема - в реле или в самом Telegram. + +--- + +## Вариант 1: MTProto прокси-сервер + +Telegram-прокси, к которому клиенты подключаются по секрету. B4 маскирует трафик под обычное HTTPS-соединение к популярному сайту. + +![20260531200322](../../../../static/img/mtproto/20260531200322.png) ### Шаг 1: Настройка B4 -В веб-интерфейсе B4 → **Settings** → **General** → **MTProto Proxy**: +В веб-интерфейсе B4 → **Settings** → **MTProto Proxy**: 1. **Enable MTProto Proxy** - включить 2. **Port** - порт для подключений (рекомендуется `443`) @@ -52,7 +78,10 @@ B4 установлен на роутере или машине внутри Р 5. Скопировать значение из поля **Secret** 6. Сохранить настройки и перезапустить B4 -Поле **DC Relay** оставить пустым - B4 на VPS подключается к Telegram напрямую. +Режим **Транспорт к Telegram** (см. выше) выбирается по тому, где работает B4: + +- **B4 на VPS за границей** - Прямой TCP. B4 достигает Telegram напрямую; поле DC Relay оставить пустым. +- **B4 на роутере внутри России** - Авто (WebSocket → TCP). B4 достигает Telegram через WebSocket-узел, и VPS-реле не требуется. Если WebSocket в вашей сети тоже заблокирован, используйте DC Relay (ниже). ### Шаг 2: Настройка Telegram @@ -60,38 +89,78 @@ B4 установлен на роутере или машине внутри Р 2. Нажать **Добавить прокси** 3. Выбрать тип **MTProto** 4. Заполнить: - - **Сервер**: IP-адрес или домен VPS + - **Сервер**: IP-адрес или домен B4 (локальный IP для устройств в сети; публичный IP или DDNS для удалённого доступа, с пробросом портов) - **Порт**: порт из шага 1 - **Секрет**: скопированный секрет 5. Нажать **Готово** и включить прокси - ![telegra](/img/mtproto/20260322135130.png) + +![telegra](/img/mtproto/20260322135130.png) + +Можно также воспользоваться кнопкой **Share connection link**, чтобы сгенерировать ссылку `tg://proxy` или QR-код для другого устройства. --- -## Сценарий 2: B4 на роутере внутри России +## Вариант 2: Telegram через WebSocket (прозрачный мост) + +Режим маршрутизации на уровне сета, который чинит Telegram для всех устройств за B4 - без прокси в приложении и без VPS. Когда устройство подключается к дата-центру Telegram, B4 прозрачно перехватывает сессию и ретранслирует её через WebSocket-узел Telegram (с запасным вариантом через Cloudflare). + +Этот режим работает самостоятельно. MTProto прокси-сервер в разделе Settings → MTProto включать **не** требуется. + +### Настройка + +1. Создайте или откройте сет и задайте ему цели **`telegram`** в категориях geosite и geoip (чтобы сет совпадал и с доменами, и с диапазонами IP Telegram). +2. На вкладке **Routing** сета включите маршрутизацию и выберите **Режим маршрутизации** → **Telegram через WebSocket (встроенный)**. +3. Выберите **source-интерфейсы** (интерфейсы локальной сети, чьи устройства нужно переправлять). Если не выбрать ни одного, под мост попадают все устройства. +4. Сохраните. + +Минимальный сет для этого режима: + +```json +{ + "name": "telegram-ws", + "targets": { + "geosite_categories": ["telegram"], + "geoip_categories": ["telegram"] + }, + "enabled": true, + "routing": { "enabled": true, "mode": "mtproto-ws" } +} +``` + +Здесь действуют общие настройки **Транспорт к Telegram** (Settings → MTProto), поэтому при проблемах с загрузкой медиа укажите там домен Cloudflare Worker. + +:::info Best-effort +Переправляются только TCP MTProto-сессии. Голосовые звонки и транспорты, для которых B4 не может определить дата-центр, идут напрямую (fail-open). +::: + +--- + +## DC Relay (VPS + socat) + +DC Relay нужен только когда B4 работает внутри цензурированной зоны *и* WebSocket-транспорт тоже заблокирован, так что прямые соединения к Telegram по IP приходится пускать через VPS. + +```text +Телефон ──────▶ B4 (роутер) ──────▶ VPS ──────▶ Telegram + ТСПУ видит ТСПУ видит + «HTTPS к google.com» «трафик к VPS» + (не блокирует) (не блокирует) +``` + +На VPS достаточно простой пересылки TCP (`socat`) - без ключей и MTProto-специфичного ПО. ### Шаг 1: Установка socat на VPS -На любом VPS за границей установить socat: - ```bash apt install -y socat ``` -Команды пересылки сгенерирует сам B4 на следующем шаге — жёстко прописывать IP в инструкции бессмысленно, потому что Telegram периодически меняет адреса DC и добавляет новые (например, медиа-DC `203`). +### Шаг 2: Указать адрес DC Relay -### Шаг 2: Настройка B4 +В **Settings** → **MTProto Proxy** укажите в поле **DC Relay** адрес VPS с базовым портом (например `my-vps.com:7007`). Поле появляется, когда режим транспорта - Прямой TCP или Авто. -В веб-интерфейсе B4 → **Settings** → **General** → **MTProto Proxy**: +При Авто с настроенным DC Relay сначала пробуется реле по TCP, а WebSocket используется как резерв. -1. **Enable MTProto Proxy** — включить -2. **Port** — порт для подключений (например `7002`) -3. **Fake SNI Domain** — домен для маскировки (например `storage.googleapis.com`) -4. **DC Relay** — адрес VPS с базовым портом (например `my-vps.com:7007`) -5. Нажать **Generate Secret** -6. Скопировать значение из поля **Secret** - -### Шаг 3: Получить команды socat для VPS +### Шаг 3: Получить команды socat Нажать кнопку **?** рядом с полем **DC Relay**. Откроется окно «Настройка socat для DC Relay» со списком текущих серверов Telegram и готовыми командами `socat` для каждого DC, включая медиа-DC. @@ -100,32 +169,17 @@ apt install -y socat Нажать **Копировать всё**, перейти на VPS и выполнить вставленные команды. :::info Почему через помощник -Список DC Telegram берётся напрямую из `getProxyConfig` — это официальный список, который обновляется на стороне Telegram. B4 рассчитывает порт relay по формуле `базовый_порт + |DC| - 1`. Если Telegram добавит новый DC или поменяет IP, помощник покажет актуальные команды без правок инструкции. +Список DC Telegram берётся напрямую из `getProxyConfig` - это официальный список, который обновляется на стороне Telegram. B4 рассчитывает порт relay по формуле `базовый_порт + |DC| - 1`. Если Telegram добавит новый DC или поменяет IP, помощник покажет актуальные команды без правок инструкции. ::: :::warning Firewall на VPS -Открыть на VPS все порты, которые показывает помощник (строка «Откройте эти порты в firewall VPS» внизу окна). Сейчас это обычно 6 портов: пять для основных DC (1–5) и один для медиа-DC `203`. +Открыть на VPS все порты, которые показывает помощник (строка «Откройте эти порты в firewall VPS» внизу окна). Сейчас это обычно 6 портов: пять для основных DC (1-5) и один для медиа-DC `203`. ::: :::tip Для автозапуска `socat` добавить команды в `/etc/rc.local` или создать systemd-сервис. ::: -### Шаг 4: Сохранить настройки B4 - -После того как socat запущен на VPS, в B4 нажать **Save** и перезапустить B4. - -### Шаг 5: Настройка Telegram - -1. [Открыть](https://core.telegram.org/proxy#adding-a-proxy) **Telegram** → **Настройки** → **Данные и память** → **Прокси** -2. Нажать **Добавить прокси** -3. Выбрать тип **MTProto** -4. Заполнить: - - **Сервер**: IP-адрес роутера или машины с B4 - - **Порт**: порт из шага 2 - - **Секрет**: скопированный секрет -5. Нажать **Готово** и включить прокси - --- ## Выбор домена для маскировки @@ -140,36 +194,44 @@ apt install -y socat При подключении к порту B4 без правильного секрета - B4 прозрачно перенаправляет на настоящий сайт (указанный в Fake SNI). Сканер видит обычный сайт, а не прокси. ::: -## Что-то пошло не так, памагити +--- -### Telegram показывает "Подключение..." +## Устранение неполадок -- Убедиться, что `socat` запущен на VPS и порты доступны (сценарий 2) -- Проверить правильность адреса VPS в поле DC Relay -- В логах B4 должны быть строки `MTProto fake-TLS handshake OK` и `MTProto relay` +### Telegram показывает «Подключение…» + +- Если используется WebSocket-транспорт, нажмите **Проверить соединение**, чтобы убедиться, что B4 достаёт DC. +- Если используется DC Relay, убедитесь, что `socat` запущен на VPS и порты доступны, и проверьте адрес VPS. +- В логах B4 должны быть строки `MTProto fake-TLS handshake OK` и `MTProto relay`. + +### Не загружаются медиа, стикеры или реакции + +- Укажите **домен Cloudflare Worker** в настройках «Транспорт к Telegram». Обычно виноват DC 1 (медиа во внешних каналах), и резерв через CF Worker / CF-прокси его вытягивает. ### Неправильный секрет -В логах: `HMAC verification failed` - -Секрет в Telegram не совпадает с секретом в B4. +В логах: `HMAC verification failed`. Секрет в Telegram не совпадает с секретом в B4. ### Расхождение времени -В логах: `timestamp out of range` +В логах: `timestamp out of range`. Часы на устройстве и на машине с B4 расходятся. Необходимо синхронизировать время (NTP). -Часы на устройстве и на машине с B4 расходятся. Необходимо синхронизировать время. +### VPS недоступен (DC Relay) -### VPS недоступен - -В логах: `dial DC ... i/o timeout` +В логах: `dial DC ... i/o timeout`. - VPS выключен или `socat` не запущен - Firewall на VPS блокирует входящие соединения на нужных портах ### Нет ответа от Telegram -В логах: `DC->client: 0 bytes` +В логах: `DC->client: 0 bytes`. -- Если DC Relay **не настроен**: серверы Telegram заблокированы по IP. Необходимо настроить VPS relay (придется использовать только сценарий 2). -- Если DC Relay **настроен**: `socat` на VPS не запущен или указан неправильный порт. +- Прямой TCP и без реле: серверы Telegram заблокированы по IP. Переключите транспорт на Авто/WebSocket или настройте DC Relay. +- DC Relay настроен: `socat` на VPS не запущен или указан неправильный порт. + +--- + +## Благодарности + +WebSocket-транспорт и релей через Cloudflare Worker вдохновлены проектом [tg-ws-proxy](https://github.com/Flowseal/tg-ws-proxy). diff --git a/docs/static/img/mtproto/20260531200322.png b/docs/static/img/mtproto/20260531200322.png new file mode 100644 index 0000000000000000000000000000000000000000..56f49875663b09dacdf2c5c5daa01ff34ae83313 GIT binary patch literal 196288 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 khn=rO{;WbJd*%%R*wu3A7(bWN)I~a| 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ϑ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 Ub1kFlYc$ja2I=3azHJ@#A1~Dy#Q*>R literal 0 HcmV?d00001 diff --git a/src/config/config.go b/src/config/config.go index 72e38916..f6111fa8 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -244,6 +244,8 @@ var DefaultConfig = Config{ FakeSNI: "storage.googleapis.com", UpstreamMode: "auto", WSEndpointHost: "149.154.167.220", + CFProxyEnabled: true, + CFProxyURL: "https://raw.githubusercontent.com/Flowseal/tg-ws-proxy/main/.github/cfproxy-domains.txt", }, Logging: Logging{ diff --git a/src/config/migration.go b/src/config/migration.go index e2c5511e..345a33ec 100644 --- a/src/config/migration.go +++ b/src/config/migration.go @@ -61,6 +61,21 @@ var migrationRegistry = map[int]MigrationFunc{ 37: migrateV37to38, // Add MTProto upstream transport (WS) fields 38: migrateV38to39, // Add system.memory_limit 39: migrateV39to40, // Add geo auto_update config + 40: migrateV40to41, // Add MTProto CF-proxy fallback config + 41: migrateV41to42, // Add MTProto CF Worker domain config +} + +func migrateV41to42(c *Config, _ map[string]interface{}) error { + log.Tracef("Migration v41->v42: Adding MTProto CF Worker domain config") + c.System.MTProto.CFWorkerDomain = "" + return nil +} + +func migrateV40to41(c *Config, _ map[string]interface{}) error { + log.Tracef("Migration v40->v41: Adding MTProto CF-proxy fallback config") + c.System.MTProto.CFProxyEnabled = true + c.System.MTProto.CFProxyURL = DefaultConfig.System.MTProto.CFProxyURL + return nil } func migrateV39to40(c *Config, _ map[string]interface{}) error { diff --git a/src/config/types.go b/src/config/types.go index 6847075e..65150237 100644 --- a/src/config/types.go +++ b/src/config/types.go @@ -17,8 +17,13 @@ const FakePayloadAutoQUIC = "@quic_initial" const ( RoutingModeInterface = "interface" RoutingModeProxy = "proxy" + RoutingModeMTProtoWS = "mtproto-ws" ) +func RoutingUsesTProxy(mode string) bool { + return mode == RoutingModeProxy || mode == RoutingModeMTProtoWS +} + const ( FakePayloadRandom = iota FakePayloadCustom @@ -226,6 +231,9 @@ type MTProtoConfig struct { UpstreamMode string `json:"upstream_mode"` WSCustomDomain string `json:"ws_custom_domain"` WSEndpointHost string `json:"ws_endpoint_host"` + CFProxyEnabled bool `json:"cfproxy_enabled"` // enable Cloudflare-proxied fallback WS domains (rescues DCs the network blocks) + CFProxyURL string `json:"cfproxy_url"` // URL to refresh CF-proxy domain list; empty = built-in default + CFWorkerDomain string `json:"cfworker_domain"` // user's Cloudflare Worker domain(s) (workers.dev), comma-separated; free per-user WS relay tried before the shared CF pool } type Socks5Config struct { diff --git a/src/config/validation.go b/src/config/validation.go index 76869e6c..69c7b68e 100644 --- a/src/config/validation.go +++ b/src/config/validation.go @@ -120,7 +120,7 @@ func (c *Config) Validate() error { switch set.Routing.Mode { case "": set.Routing.Mode = RoutingModeInterface - case RoutingModeProxy, RoutingModeInterface: + case RoutingModeProxy, RoutingModeInterface, RoutingModeMTProtoWS: default: v.addf(fmt.Sprintf("sets[%d].routing.mode", setIdx), "invalid_routing_mode", map[string]any{"set": set.Name, "mode": set.Routing.Mode}, "set %q: unknown routing mode %q", set.Name, set.Routing.Mode) return v.result() diff --git a/src/http/handler/common.go b/src/http/handler/common.go index 99dc196b..dbcf9ccb 100644 --- a/src/http/handler/common.go +++ b/src/http/handler/common.go @@ -16,8 +16,8 @@ import ( "github.com/daniellavrushin/b4/geodat" "github.com/daniellavrushin/b4/log" "github.com/daniellavrushin/b4/nfq" - "github.com/daniellavrushin/b4/watchdog" "github.com/daniellavrushin/b4/utils" + "github.com/daniellavrushin/b4/watchdog" "golang.org/x/sys/unix" ) @@ -33,13 +33,16 @@ type ConfigRefresher interface { } var ( - globalPool *nfq.Pool - globalSocks5Server ConfigRefresher - tablesRefreshFunc func() error - routingSyncFunc func(*config.Config) - discoveryRuntime *discovery.Runtime - globalWatchdog *watchdog.Watchdog - globalAIManager *ai.Manager + globalPool *nfq.Pool + globalSocks5Server ConfigRefresher + globalMTProtoServer ConfigRefresher + globalMTProtoBridge ConfigRefresher + tablesRefreshFunc func() error + routingSyncFunc func(*config.Config) + mtprotoCFRefreshFunc func(*config.Config) + discoveryRuntime *discovery.Runtime + globalWatchdog *watchdog.Watchdog + globalAIManager *ai.Manager ) func SetAIManager(m *ai.Manager) { @@ -68,6 +71,14 @@ func SetSocks5Server(s ConfigRefresher) { globalSocks5Server = s } +func SetMTProtoServer(s ConfigRefresher) { + globalMTProtoServer = s +} + +func SetMTProtoBridge(s ConfigRefresher) { + globalMTProtoBridge = s +} + func NewAPIHandler(cfgPtr *atomic.Pointer[config.Config]) *API { cfg := cfgPtr.Load() // Initialize geodata manager @@ -112,7 +123,7 @@ func NewAPIHandler(cfgPtr *atomic.Pointer[config.Config]) *API { cfgPtr: cfgPtr, geodataManager: geodataManager, discoveryRT: discoveryRuntime, - asnStore: config.NewAsnStore(cfg.ConfigPath), + asnStore: config.NewAsnStore(cfg.ConfigPath), } } func (api *API) RegisterEndpoints(mux *http.ServeMux, cfgPtr *atomic.Pointer[config.Config]) { @@ -159,6 +170,10 @@ func SetRoutingSyncFunc(fn func(*config.Config)) { routingSyncFunc = fn } +func SetMTProtoCFRefreshFunc(fn func(*config.Config)) { + mtprotoCFRefreshFunc = fn +} + func SetDiscoveryRuntime(rt *discovery.Runtime) { discoveryRuntime = rt } diff --git a/src/http/handler/config.go b/src/http/handler/config.go index 98fb8487..17201e61 100644 --- a/src/http/handler/config.go +++ b/src/http/handler/config.go @@ -341,7 +341,6 @@ func (a *API) updateConfig(w http.ResponseWriter, r *http.Request) { _ = enc.Encode(response) } - func (a *API) saveAndPushConfig(newCfg *config.Config) error { if err := newCfg.Validate(); err != nil { @@ -368,6 +367,18 @@ func (a *API) saveAndPushConfig(newCfg *config.Config) error { globalSocks5Server.UpdateConfig(newCfg) } + if globalMTProtoServer != nil { + globalMTProtoServer.UpdateConfig(newCfg) + } + + if globalMTProtoBridge != nil { + globalMTProtoBridge.UpdateConfig(newCfg) + } + + if mtprotoCFRefreshFunc != nil { + mtprotoCFRefreshFunc(newCfg) + } + err := newCfg.SaveToFile(newCfg.ConfigPath) if err != nil { return fmt.Errorf("failed to save config to file: %v", err) diff --git a/src/http/handler/mtproto.go b/src/http/handler/mtproto.go index 6dfa3ae0..00e8f600 100644 --- a/src/http/handler/mtproto.go +++ b/src/http/handler/mtproto.go @@ -22,7 +22,7 @@ func (api *API) RegisterMTProtoApi() { // @Tags MTProto // @Accept json // @Produce json -// @Param body body object false "optional overrides: upstream_mode, ws_custom_domain, dc" +// @Param body body object false "optional overrides: upstream_mode, ws_custom_domain, ws_endpoint_host, cfworker_domain, cfproxy_enabled, dc_relay, dc" // @Success 200 {object} map[string]interface{} // @Security BearerAuth // @Router /mtproto/test-ws [post] @@ -35,6 +35,8 @@ func (api *API) handleMTProtoTestWS(w http.ResponseWriter, r *http.Request) { UpstreamMode string `json:"upstream_mode"` WSCustomDomain *string `json:"ws_custom_domain"` WSEndpointHost *string `json:"ws_endpoint_host"` + CFWorkerDomain *string `json:"cfworker_domain"` + CFProxyEnabled *bool `json:"cfproxy_enabled"` DCRelay *string `json:"dc_relay"` DC int `json:"dc"` } @@ -61,6 +63,12 @@ func (api *API) handleMTProtoTestWS(w http.ResponseWriter, r *http.Request) { if req.WSEndpointHost != nil { probeCfg.WSEndpointHost = *req.WSEndpointHost } + if req.CFWorkerDomain != nil { + probeCfg.CFWorkerDomain = *req.CFWorkerDomain + } + if req.CFProxyEnabled != nil { + probeCfg.CFProxyEnabled = *req.CFProxyEnabled + } if req.DCRelay != nil { probeCfg.DCRelay = *req.DCRelay } @@ -219,12 +227,12 @@ func (api *API) updateMTProtoConfig(w http.ResponseWriter, r *http.Request) { return } - cfg := api.getCfg() - cfg.System.MTProto = req + cur := api.getCfg() + newCfg := cur.Clone() + newCfg.System.MTProto = req - if err := cfg.SaveToFile(cfg.ConfigPath); err != nil { - log.Errorf("Failed to save MTProto config: %v", err) - writeJsonError(w, http.StatusInternalServerError, "Failed to save configuration") + if err := api.saveAndPushConfig(newCfg); err != nil { + writeAPIError(w, err) return } @@ -232,6 +240,6 @@ func (api *API) updateMTProtoConfig(w http.ResponseWriter, r *http.Request) { sendResponse(w, map[string]interface{}{ "success": true, - "message": "MTProto configuration updated. Restart required for changes to take effect.", + "message": "MTProto configuration updated and applied.", }) } diff --git a/src/http/ui/src/components/sets/routing/TrafficRouting.tsx b/src/http/ui/src/components/sets/routing/TrafficRouting.tsx index a07a1449..33a3ead3 100644 --- a/src/http/ui/src/components/sets/routing/TrafficRouting.tsx +++ b/src/http/ui/src/components/sets/routing/TrafficRouting.tsx @@ -28,14 +28,21 @@ export const TrafficRouting = ({ }: TrafficRoutingProps) => { const { t } = useTranslation(); const routing = config.routing; - const mode: RoutingMode = routing.mode === "proxy" ? "proxy" : "interface"; + const mode: RoutingMode = + routing.mode === "proxy" + ? "proxy" + : routing.mode === "mtproto-ws" + ? "mtproto-ws" + : "interface"; const isProxy = mode === "proxy"; + const isMTProtoWS = mode === "mtproto-ws"; + const isInterface = mode === "interface"; const selectedIfaceAvailable = availableIfaces.includes( routing.egress_interface, ); const shouldShowUnavailableSelected = Boolean( - !isProxy && routing.egress_interface && !selectedIfaceAvailable, + isInterface && routing.egress_interface && !selectedIfaceAvailable, ); const toggleSourceIface = (iface: string) => { @@ -61,6 +68,8 @@ export const TrafficRouting = ({ upstream.host && upstream.port ? `${upstream.host}:${upstream.port}` : t("sets.routing.flowNoUpstream"); + } else if (isMTProtoWS) { + flowDestination = t("sets.routing.flowMTProtoWS"); } else { flowDestination = routing.egress_interface || t("sets.routing.flowNoOutput"); @@ -75,7 +84,7 @@ export const TrafficRouting = ({ onChange={(checked: boolean) => onChange("routing.enabled", checked)} description={t("sets.routing.enableDesc")} disabled={ - !isProxy && availableIfaces.length === 0 && !routing.enabled + isInterface && availableIfaces.length === 0 && !routing.enabled } /> @@ -94,7 +103,15 @@ export const TrafficRouting = ({ {t("sets.routing.modeInterface")} {t("sets.routing.modeProxy")} + + {t("sets.routing.modeMTProtoWS")} + + {isMTProtoWS && ( + + {t("sets.routing.mtprotoWsNote")} + + )} @@ -193,7 +210,9 @@ export const TrafficRouting = ({ > {isProxy ? t("sets.routing.flowProxyCaption") - : t("sets.routing.flowCaption")} + : isMTProtoWS + ? t("sets.routing.flowMTProtoWSCaption") + : t("sets.routing.flowCaption")} @@ -201,7 +220,9 @@ export const TrafficRouting = ({ {isProxy ? t("sets.routing.howItWorksProxy") - : t("sets.routing.howItWorks")} + : isMTProtoWS + ? t("sets.routing.howItWorksMTProtoWS") + : t("sets.routing.howItWorks")} @@ -240,7 +261,7 @@ export const TrafficRouting = ({ })} - {availableIfaces.length === 0 && !isProxy && ( + {availableIfaces.length === 0 && isInterface && ( {t("sets.routing.noInterfaces")} @@ -255,11 +276,15 @@ export const TrafficRouting = ({ )} - {isProxy ? t("sets.routing.infoProxy") : t("sets.routing.info")} + {isProxy + ? t("sets.routing.infoProxy") + : isMTProtoWS + ? t("sets.routing.infoMTProtoWS") + : t("sets.routing.info")} - {!isProxy && ( + {isInterface && ( { upstream_mode: config.system.mtproto?.upstream_mode || "auto", ws_custom_domain: config.system.mtproto?.ws_custom_domain || "", ws_endpoint_host: config.system.mtproto?.ws_endpoint_host || "", + cfworker_domain: config.system.mtproto?.cfworker_domain || "", + cfproxy_enabled: config.system.mtproto?.cfproxy_enabled ?? true, dc: 2, ...overrides, }), @@ -218,7 +221,11 @@ export const MTProtoSettings = ({ config, onChange }: MTProtoSettingsProps) => { description={t("settings.MTProto.description")} icon={} > - + { } description={t("settings.MTProto.enableDesc")} /> - {config.system.mtproto?.enabled && ( - - {t("settings.MTProto.restartNote")} - - )} { {(() => { const mode = config.system.mtproto?.upstream_mode || "auto"; - const enabled = !!config.system.mtproto?.enabled; - const showDcRelay = - !!dcRelay || mode === "tcp" || mode === "auto"; + const showDcRelay = !!dcRelay || mode === "tcp" || mode === "auto"; return ( { onChange={(e) => onChange("system.mtproto.upstream_mode", String(e.target.value)) } - disabled={!enabled} options={[ { value: "tcp", label: t("settings.MTProto.upstreamTcp") }, { value: "auto", label: t("settings.MTProto.upstreamAuto") }, { value: "ws", label: t("settings.MTProto.upstreamWs") }, ]} - helperText={ + helperText={`${ mode === "auto" && dcRelay ? t("settings.MTProto.upstreamAutoRelayDesc") : t( `settings.MTProto.upstream${upstreamDescSuffix(mode)}Desc`, ) - } + } ${t("settings.MTProto.upstreamBridgeNote")}`} /> {showDcRelay && ( { onChange("system.mtproto.dc_relay", e.target.value) } placeholder="vps-ip:7007" - disabled={!enabled} helperText={t("settings.MTProto.dcRelayHelp")} selectOnFocus slotProps={{ @@ -365,7 +363,6 @@ export const MTProtoSettings = ({ config, onChange }: MTProtoSettingsProps) => { @@ -378,29 +375,55 @@ export const MTProtoSettings = ({ config, onChange }: MTProtoSettingsProps) => { }} /> )} - {mode !== "tcp" && ( + + onChange("system.mtproto.cfworker_domain", e.target.value) + } + placeholder="my-worker-1234.username.workers.dev" + helperText={t("settings.MTProto.cfWorkerDomainHelp")} + selectOnFocus + slotProps={{ + input: { + endAdornment: ( + + + + + + + + + + ), + }, + }} + /> + + onChange("system.mtproto.cfproxy_enabled", checked) + } + description={t("settings.MTProto.cfProxyEnabledHelp")} + /> + {config.system.mtproto?.cfproxy_enabled !== false && ( - onChange("system.mtproto.ws_custom_domain", e.target.value) + onChange("system.mtproto.cfproxy_url", e.target.value) } - placeholder="your-domain.com" - disabled={!enabled} - helperText={t("settings.MTProto.wsCustomDomainHelp")} - selectOnFocus - /> - )} - {mode !== "tcp" && ( - - onChange("system.mtproto.ws_endpoint_host", e.target.value) - } - placeholder="149.154.167.220" - disabled={!enabled} - helperText={t("settings.MTProto.wsEndpointHostHelp")} + placeholder="https://raw.githubusercontent.com/Flowseal/tg-ws-proxy/main/.github/cfproxy-domains.txt" + helperText={t("settings.MTProto.cfProxyURLHelp")} selectOnFocus /> )} @@ -417,9 +440,7 @@ export const MTProtoSettings = ({ config, onChange }: MTProtoSettingsProps) => { ) } onClick={() => void handleTestWS()} - disabled={ - !config.system.mtproto?.enabled || wsTesting !== null - } + disabled={wsTesting !== null} > {wsTesting === "configured" ? t("settings.MTProto.testWsRunning") @@ -436,9 +457,7 @@ export const MTProtoSettings = ({ config, onChange }: MTProtoSettingsProps) => { ) : undefined } onClick={() => void handleTestDirectTCP()} - disabled={ - !config.system.mtproto?.enabled || wsTesting !== null - } + disabled={wsTesting !== null} > {wsTesting === "direct" ? t("settings.MTProto.testWsRunning") @@ -501,6 +520,22 @@ export const MTProtoSettings = ({ config, onChange }: MTProtoSettingsProps) => { ); })()} + + {t("settings.MTProto.credit")}{" "} + + tg-ws-proxy + + + setShareOpen(false)} diff --git a/src/http/ui/src/components/settings/Page.tsx b/src/http/ui/src/components/settings/Page.tsx index 039be43a..8405450a 100644 --- a/src/http/ui/src/components/settings/Page.tsx +++ b/src/http/ui/src/components/settings/Page.tsx @@ -22,6 +22,7 @@ import { ApiIcon, BackupIcon, CaptureIcon, + ConnectionIcon, CoreIcon, DiscoveryIcon, DomainIcon, @@ -83,6 +84,7 @@ enum TABS { GENERAL = 0, DOMAINS, DISCOVERY, + MTPROTO, API, PAYLOADS, BACKUP, @@ -127,6 +129,14 @@ export function SettingsPage() { description: t("settings.tabs.discoveryDesc"), requiresRestart: false, }, + { + id: TABS.MTPROTO, + path: "mtproto", + label: t("settings.tabs.mtproto"), + icon: , + description: t("settings.tabs.mtprotoDesc"), + requiresRestart: false, + }, { id: TABS.API, path: "api", @@ -199,8 +209,6 @@ export function SettingsPage() { JSON.stringify(originalConfig.system.web_server) || JSON.stringify(config.system.socks5) !== JSON.stringify(originalConfig.system.socks5) || - JSON.stringify(config.system.mtproto) !== - JSON.stringify(originalConfig.system.mtproto) || JSON.stringify(config.system.tables) !== JSON.stringify(originalConfig.system.tables) || JSON.stringify(config.queue.devices) !== @@ -216,6 +224,11 @@ export function SettingsPage() { JSON.stringify(config.system.checker) !== JSON.stringify(originalConfig.system.checker), + // MTProto + [TABS.MTPROTO]: + JSON.stringify(config.system.mtproto) !== + JSON.stringify(originalConfig.system.mtproto), + // API [TABS.API]: JSON.stringify(config.system.api) !== @@ -471,11 +484,6 @@ export function SettingsPage() { - - - - - @@ -508,6 +516,16 @@ export function SettingsPage() { + + + + + + + + + + diff --git a/src/http/ui/src/i18n/en.json b/src/http/ui/src/i18n/en.json index 272b296b..ea378288 100644 --- a/src/http/ui/src/i18n/en.json +++ b/src/http/ui/src/i18n/en.json @@ -104,6 +104,8 @@ "geodatDesc": "Global geodata configuration", "discovery": "Discovery", "discoveryDesc": "DPI bypass domains testing", + "mtproto": "MTProto Proxy", + "mtprotoDesc": "Built-in Telegram proxy", "api": "API", "apiDesc": "API settings for various services", "payloads": "Payloads", @@ -230,7 +232,9 @@ "MTProto": { "title": "MTProto Proxy", "description": "Built-in Telegram MTProto proxy with fake-TLS obfuscation", - "settings": "MTProto Settings", + "credit": "WebSocket transport idea inspired by", + "settings": "Proxy server", + "serverDesc": "A Telegram proxy that clients connect to with a secret (on this LAN or remotely, e.g. from a cellular network). Requires the toggle below. Not needed for the 'Telegram over WebSocket' routing mode.", "enable": "Enable MTProto Proxy", "enableDesc": "Telegram proxy that wraps traffic in fake TLS to bypass DPI", "bindAddress": "Bind Address", @@ -240,6 +244,9 @@ "portHelp": "MTProto listen port (default: 3128)", "fakeSNI": "Fake SNI Domain", "fakeSNIHelp": "Domain to impersonate in TLS handshake (e.g. storage.googleapis.com)", + "cfWorkerDomain": "Cloudflare Worker domain", + "cfWorkerDomainHelp": "Free per-user WebSocket relay (workers.dev). Recommended when media, reactions, or stickers fail to load. Reaches all data centers and avoids the shared-domain rate limits. Comma-separate multiple workers. Click the icon for setup.", + "cfWorkerSetup": "How to set up a Cloudflare Worker", "dcRelay": "DC Relay", "dcRelayHelp": "External relay address (host:port) to reach Telegram DCs when blocked by IP", "dcRelayHelpButton": "Show socat commands for relay setup", @@ -266,7 +273,6 @@ "refreshingDCs": "Fetching…", "refreshDCsOk": "Loaded {{count}} DC entries from Telegram.", "refreshDCsErr": "Refresh failed: {{error}}", - "restartNote": "Restart B4 after changing MTProto settings for changes to take effect.", "shareLink": "Share connection link", "shareNative": "Share", "shareDialogTitle": "Share MTProto connection", @@ -276,8 +282,9 @@ "shareLinkHelp": "Open this link on a device with Telegram installed to add the proxy.", "shareQrHelp": "Scan with the phone camera to open in Telegram.", "shareOpen": "Open in Telegram", - "upstreamTitle": "Upstream Transport", - "upstreamDesc": "How b4 reaches Telegram data centers from this host. Use WebSocket if your network blocks direct TCP to 149.154.x.x.", + "upstreamTitle": "Telegram upstream (shared)", + "upstreamDesc": "How b4 reaches Telegram data centers. Used by the proxy server above and by the 'Telegram over WebSocket' routing mode in sets, so it applies even when the proxy server is off. Use WebSocket if your network blocks direct TCP to 149.154.x.x.", + "upstreamBridgeNote": "Transport mode applies to the proxy server only; the 'Telegram over WebSocket' routing mode always uses Auto (WebSocket-first with TCP fallback).", "upstreamMode": "Transport mode", "upstreamTcp": "Direct TCP", "upstreamTcpDesc": "Fastest. Use when this host can reach 149.154.0.0/16 directly.", @@ -286,9 +293,13 @@ "upstreamWs": "WebSocket only", "upstreamWsDesc": "Strict WebSocket transport via kws*.web.telegram.org. No TCP fallback.", "wsCustomDomain": "Custom WebSocket domain", - "wsCustomDomainHelp": "Optional. Enter the base domain (e.g. your-domain.com) that proxies WebSocket traffic to Telegram. b4 prepends kws1., kws2., ... per DC automatically. Leave empty to use Telegram's native kws*.web.telegram.org.", + "wsCustomDomainHelp": "Optional extra domain (e.g. your-domain.com) that proxies WebSocket traffic to Telegram. b4 prepends kws1., kws2., ... per DC automatically. Independent of the CF proxy fallback below.", "wsEndpointHost": "Telegram WS edge IP (advanced)", "wsEndpointHostHelp": "Override the IP used when WS uses native kws*.web.telegram.org SNI. Default: 149.154.167.220. Leave empty for default. Does not affect the custom WebSocket domain path.", + "cfProxyEnabled": "CF proxy fallback", + "cfProxyEnabledHelp": "Use a rotating pool of Cloudflare-proxied domains as a fallback when Telegram's native edge can't reach a data center (notably DC 1, which is needed for media in foreign channels). The pool is refreshed once an hour from the URL below.", + "cfProxyURL": "CF proxy domain list URL", + "cfProxyURLHelp": "Source for the encoded CF-proxy domain list. Default is tg-ws-proxy's GitHub. Replace with your own if you host your own list.", "testWs": "Test connection", "testWsRunning": "Testing…", "testWsHelp": "Probes DC 2 over the configured transport(s) and reports latency.", @@ -1201,6 +1212,12 @@ "modeHelper": "Send matched traffic out a network interface, or hand it off to an upstream SOCKS5 proxy (e.g. local Xray/sing-box)", "modeInterface": "Network interface", "modeProxy": "Upstream SOCKS5 proxy", + "modeMTProtoWS": "Telegram over WebSocket (built-in)", + "flowMTProtoWS": "Telegram WS edge", + "flowMTProtoWSCaption": "Matched Telegram traffic is bridged over Telegram's WebSocket edge by B4 - no in-app proxy and no VPS needed.", + "howItWorksMTProtoWS": "When a device connects to a Telegram datacenter, B4 transparently intercepts it and relays the session over Telegram's WebSocket edge (with Cloudflare fallback), so Telegram works without configuring a proxy inside the app. Pair this with the 'telegram' geoip target. Uses the shared 'Telegram upstream' settings (Settings -> MTProto), which apply even when the proxy server is off. Best-effort: only TCP MTProto is bridged (voice calls and unknown transports fall through directly).", + "infoMTProtoWS": "Routing intercepts matched Telegram traffic from the source interfaces above and bridges it over the WebSocket edge. Connections B4 cannot map to a datacenter fall open to a direct connection.", + "mtprotoWsNote": "This mode runs on its own - the MTProto proxy under Settings -> MTProto does not need to be enabled. Matched Telegram traffic is relayed over B4's built-in WebSocket endpoint, with a Cloudflare fallback.", "outputInterface": "Output Interface", "outputInterfaceHelper": "Select egress interface for this set", "interfaceUnavailable": "Selected interface is currently unavailable", diff --git a/src/http/ui/src/i18n/ru.json b/src/http/ui/src/i18n/ru.json index 98e4303a..bf3ab7e3 100644 --- a/src/http/ui/src/i18n/ru.json +++ b/src/http/ui/src/i18n/ru.json @@ -104,6 +104,8 @@ "geodatDesc": "Глобальная конфигурация геоданных", "discovery": "Дискавери", "discoveryDesc": "Тестирование доменов для обхода DPI", + "mtproto": "MTProto-прокси", + "mtprotoDesc": "Встроенный Telegram-прокси", "api": "API", "apiDesc": "Настройки API для различных сервисов", "payloads": "Пэйлоады", @@ -226,7 +228,9 @@ "MTProto": { "title": "MTProto Прокси", "description": "Встроенный Telegram MTProto прокси с fake-TLS обфускацией", - "settings": "Настройки MTProto", + "credit": "Идея WebSocket-транспорта вдохновлена проектом", + "settings": "Прокси-сервер", + "serverDesc": "Telegram-прокси, к которому клиенты подключаются по секрету (в этой локальной сети или удалённо, например из сотовой сети). Требует переключатель ниже. Не нужен для режима маршрутизации «Telegram через WebSocket».", "enable": "Включить MTProto прокси", "enableDesc": "Telegram прокси, оборачивающий трафик в fake TLS для обхода DPI", "bindAddress": "Адрес привязки", @@ -236,6 +240,9 @@ "portHelp": "Порт MTProto (по умолчанию: 3128)", "fakeSNI": "Домен для Fake SNI", "fakeSNIHelp": "Домен для имитации в TLS (например storage.googleapis.com)", + "cfWorkerDomain": "Домен Cloudflare Worker", + "cfWorkerDomainHelp": "Бесплатный персональный WebSocket-релей (workers.dev). Рекомендуется, когда не загружаются медиа, реакции или стикеры. Достаёт все дата-центры и обходит лимиты общих доменов. Несколько воркеров указывайте через запятую. Нажмите на иконку для инструкции.", + "cfWorkerSetup": "Как настроить Cloudflare Worker", "dcRelay": "DC Relay", "dcRelayHelp": "Адрес внешнего релея (host:port) для доступа к Telegram DC при блокировке по IP", "dcRelayHelpButton": "Показать команды socat для настройки relay", @@ -262,7 +269,6 @@ "refreshingDCs": "Загрузка…", "refreshDCsOk": "Загружено {{count}} записей DC от Telegram.", "refreshDCsErr": "Не удалось обновить: {{error}}", - "restartNote": "Перезапустите B4 после изменения настроек MTProto.", "shareLink": "Поделиться ссылкой", "shareNative": "Поделиться", "shareDialogTitle": "Поделиться подключением MTProto", @@ -272,8 +278,9 @@ "shareLinkHelp": "Откройте эту ссылку на устройстве с установленным Telegram, чтобы добавить прокси.", "shareQrHelp": "Отсканируйте камерой телефона, чтобы открыть в Telegram.", "shareOpen": "Открыть в Telegram", - "upstreamTitle": "Транспорт к Telegram", - "upstreamDesc": "Как b4 достигает дата-центров Telegram с этого хоста. Используйте WebSocket, если ваша сеть блокирует прямой TCP к 149.154.x.x.", + "upstreamTitle": "Транспорт к Telegram (общий)", + "upstreamDesc": "Как b4 достигает дата-центров Telegram. Используется и прокси-сервером выше, и режимом маршрутизации «Telegram через WebSocket» в сетах, поэтому действует даже когда прокси-сервер выключен. Используйте WebSocket, если ваша сеть блокирует прямой TCP к 149.154.x.x.", + "upstreamBridgeNote": "Режим транспорта касается только прокси-сервера; режим маршрутизации «Telegram через WebSocket» всегда использует Auto (сначала WebSocket, затем TCP).", "upstreamMode": "Режим транспорта", "upstreamTcp": "Прямой TCP", "upstreamTcpDesc": "Самый быстрый. Подходит, если этот хост может напрямую достучаться до 149.154.0.0/16.", @@ -282,9 +289,13 @@ "upstreamWs": "Только WebSocket", "upstreamWsDesc": "Жёсткий WebSocket-транспорт через kws*.web.telegram.org. Без TCP-резерва.", "wsCustomDomain": "Свой WebSocket-домен", - "wsCustomDomainHelp": "Опционально. Укажите базовый домен (например, your-domain.com), который проксирует WebSocket-трафик к Telegram. b4 сам подставит kws1., kws2., ... перед каждым DC. Пусто — использовать встроенный kws*.web.telegram.org.", + "wsCustomDomainHelp": "Опциональный дополнительный домен (например, your-domain.com), который проксирует WebSocket-трафик к Telegram. b4 сам подставит kws1., kws2., ... перед каждым DC. Не зависит от CF-резерва ниже.", "wsEndpointHost": "IP edge-сервера Telegram (расширенное)", "wsEndpointHostHelp": "IP, к которому подключаемся для встроенного kws*.web.telegram.org SNI. По умолчанию 149.154.167.220. Оставьте пустым для значения по умолчанию. Не влияет на путь через свой WebSocket-домен.", + "cfProxyEnabled": "CF-резерв", + "cfProxyEnabledHelp": "Использовать ротируемый пул доменов через Cloudflare как запасной путь, когда нативный edge Telegram не доходит до нужного DC (особенно DC 1, через который идёт медиа из иностранных каналов). Список обновляется раз в час по ссылке ниже.", + "cfProxyURL": "URL списка CF-доменов", + "cfProxyURLHelp": "Источник зашифрованного списка CF-доменов. По умолчанию — GitHub tg-ws-proxy. Замените, если используете свой список.", "testWs": "Проверить соединение", "testWsRunning": "Проверка…", "testWsHelp": "Пробует достучаться до DC 2 настроенным транспортом (или транспортами) и измеряет задержку.", @@ -1198,6 +1209,12 @@ "modeHelper": "Отправлять совпавший трафик через сетевой интерфейс или передавать его в upstream SOCKS5 proxy (например, локальный Xray/sing-box)", "modeInterface": "Сетевой интерфейс", "modeProxy": "Upstream SOCKS5 proxy", + "modeMTProtoWS": "Telegram через WebSocket (встроенный)", + "flowMTProtoWS": "WS-узел Telegram", + "flowMTProtoWSCaption": "Совпавший трафик Telegram переправляется B4 через WebSocket-узел Telegram — без прокси в приложении и без VPS.", + "howItWorksMTProtoWS": "Когда устройство подключается к дата-центру Telegram, B4 прозрачно перехватывает соединение и ретранслирует сессию через WebSocket-узел Telegram (с запасным вариантом через Cloudflare), поэтому Telegram работает без настройки прокси в приложении. Используйте вместе с geoip-целью «telegram». Используются общие настройки «Транспорт к Telegram» (Настройки -> MTProto), которые действуют даже когда прокси-сервер выключен. Best-effort: переправляется только TCP MTProto (голосовые звонки и неизвестные транспорты идут напрямую).", + "infoMTProtoWS": "Маршрутизация перехватывает совпавший трафик Telegram с указанных выше source-интерфейсов и переправляет его через WebSocket-узел. Соединения, для которых B4 не может определить дата-центр, идут напрямую (fail-open).", + "mtprotoWsNote": "Этот режим работает самостоятельно - MTProto-прокси в разделе Настройки -> MTProto включать не требуется. Совпавший трафик Telegram переправляется через встроенный WebSocket-узел B4, с запасным вариантом через Cloudflare.", "outputInterface": "Выходной интерфейс", "outputInterfaceHelper": "Выберите выходной интерфейс для этого сета", "interfaceUnavailable": "Выбранный интерфейс сейчас недоступен", diff --git a/src/http/ui/src/models/config.ts b/src/http/ui/src/models/config.ts index c3155c80..14359eb4 100644 --- a/src/http/ui/src/models/config.ts +++ b/src/http/ui/src/models/config.ts @@ -299,6 +299,9 @@ export interface MTProtoConfig { upstream_mode: "tcp" | "ws" | "auto"; ws_custom_domain: string; ws_endpoint_host: string; + cfproxy_enabled: boolean; + cfproxy_url: string; + cfworker_domain: string; } @@ -377,7 +380,7 @@ export interface DNSConfig { fragment_query: boolean; } -export type RoutingMode = "interface" | "proxy"; +export type RoutingMode = "interface" | "proxy" | "mtproto-ws"; export interface UpstreamProxyConfig { host: string; diff --git a/src/http/ui/src/utils/clipboard.ts b/src/http/ui/src/utils/clipboard.ts index cda42a78..798ed1be 100644 --- a/src/http/ui/src/utils/clipboard.ts +++ b/src/http/ui/src/utils/clipboard.ts @@ -1,24 +1,51 @@ export async function copyText(text: string): Promise { try { - if (navigator.clipboard?.writeText) { + if (globalThis.isSecureContext && navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true; } } catch { /* fall through to legacy path */ } + + const active = document.activeElement as HTMLElement | null; + const ta = document.createElement("textarea"); ta.value = text; + ta.contentEditable = "true"; + ta.readOnly = false; ta.style.position = "fixed"; - ta.style.opacity = "0"; - document.body.appendChild(ta); + ta.style.top = "0"; + ta.style.left = "0"; + ta.style.width = "1px"; + ta.style.height = "1px"; + ta.style.padding = "0"; + ta.style.border = "none"; + ta.style.outline = "none"; + ta.style.boxShadow = "none"; + ta.style.background = "transparent"; + + const host = active?.closest("[role='dialog']") ?? document.body; + host.appendChild(ta); + + ta.focus(); ta.select(); + + const range = document.createRange(); + range.selectNodeContents(ta); + const sel = globalThis.getSelection(); + sel?.removeAllRanges(); + sel?.addRange(range); + ta.setSelectionRange(0, text.length); + let ok = false; try { ok = document.execCommand("copy"); } catch { /* legacy copy failed too */ } + ta.remove(); + active?.focus?.(); return ok; } diff --git a/src/main.go b/src/main.go index 41db8329..768acc49 100644 --- a/src/main.go +++ b/src/main.go @@ -117,6 +117,9 @@ func runB4(cmd *cobra.Command, args []string) error { var cfgPtr atomic.Pointer[config.Config] cfgPtr.Store(&cfg) + appCtx, appCancel := context.WithCancel(context.Background()) + defer appCancel() + aiManager := ai.NewManager(cfg.System.AI, cfg.ConfigPath) handler.SetAIManager(aiManager) @@ -125,6 +128,18 @@ func runB4(cmd *cobra.Command, args []string) error { tproxyResolver := tproxy.NewLearnedIPResolver(nil) tproxyMgr := tproxy.NewManager(tproxyResolver) + mtprotoBridge := mtproto.NewTransparentBridge(&cfg) + tproxyMgr.SetMTProtoBridge(mtprotoBridge) + handler.SetMTProtoBridge(mtprotoBridge) + go func() { _ = mtproto.RefreshDCs() }() + startCFRefresh := func(c *config.Config) { + if c.System.MTProto.CFProxyEnabled { + mtproto.StartCFProxyRefresh(appCtx, c.System.MTProto.CFProxyURL) + } + } + startCFRefresh(&cfg) + handler.SetMTProtoCFRefreshFunc(startCFRefresh) + handler.SetTablesRefreshFunc(func() error { c := cfgPtr.Load() if c.System.Tables.SkipSetup { @@ -268,6 +283,7 @@ func runB4(cmd *cobra.Command, args []string) error { metrics.RecordEvent("error", fmt.Sprintf("Failed to start MTProto server: %v", err)) log.Errorf("MTProto server did not start: %v (b4 continues without it; fix in Settings or config)", err) } + handler.SetMTProtoServer(mtprotoServer) wd := watchdog.New(&cfgPtr, discoveryRT, func(c *config.Config) error { if err := c.Validate(); err != nil { @@ -282,6 +298,9 @@ func runB4(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to save config: %v", err) } cfgPtr.Store(c) + mtprotoServer.UpdateConfig(c) + mtprotoBridge.UpdateConfig(c) + startCFRefresh(c) tproxyResolver.Set(pool.GetMatcher()) tproxyMgr.SyncConfig(c) tables.RoutingSyncConfig(c) diff --git a/src/mtproto/cfproxy.go b/src/mtproto/cfproxy.go new file mode 100644 index 00000000..391105b3 --- /dev/null +++ b/src/mtproto/cfproxy.go @@ -0,0 +1,384 @@ +package mtproto + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "io" + "net/http" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/daniellavrushin/b4/log" +) + +const ( + cfProxySuffix = ".co.uk" + DefaultCFProxyURL = "https://raw.githubusercontent.com/Flowseal/tg-ws-proxy/main/.github/cfproxy-domains.txt" + cfProxyRefreshInt = time.Hour + cfProxyMinValid = 3 + cfProxyFetchMaxLen = 65536 + // cfProxyDomainCooldown is how long a CF-proxy domain is skipped after it + // returns 429/503. Shared public domains get rate-limited in bursts; without + // this, every dial re-hammers all 11 and DC1/3/5 (which have no Telegram WS + // edge) stall entirely. Matches the observed recovery window in the field. + cfProxyDomainCooldown = 60 * time.Second +) + +// defaultCFProxyEncoded mirrors tg-ws-proxy/proxy/config.py:_CFPROXY_ENC. +// Caesar-shifted by alpha-char-count, .com suffix swapped for .co.uk at decode. +var defaultCFProxyEncoded = []string{ + "virkgj.com", + "vmmzovy.com", + "mkuosckvso.com", + "zaewayzmplad.com", + "twdmbzcm.com", + "awzwsldi.com", + "clngqrflngqin.com", + "tjacxbqtj.com", + "bxaxtxmrw.com", + "dmohrsgmohcrwb.com", +} + +// decodeCFDomain reverses the Flowseal/tg-ws-proxy obfuscation. +// Lines that don't end in .com pass through unchanged. +func decodeCFDomain(s string) string { + if !strings.HasSuffix(s, ".com") { + return s + } + p := s[:len(s)-4] + n := 0 + for _, c := range p { + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { + n++ + } + } + var b strings.Builder + b.Grow(len(p) + len(cfProxySuffix)) + for _, c := range p { + switch { + case c >= 'a' && c <= 'z': + shifted := ((int(c)-'a'-n)%26 + 26) % 26 + b.WriteByte(byte(shifted) + 'a') + case c >= 'A' && c <= 'Z': + shifted := ((int(c)-'A'-n)%26 + 26) % 26 + b.WriteByte(byte(shifted) + 'A') + default: + b.WriteRune(c) + } + } + b.WriteString(cfProxySuffix) + return b.String() +} + +func defaultCFProxyDomains() []string { + out := make([]string, 0, len(defaultCFProxyEncoded)) + for _, e := range defaultCFProxyEncoded { + out = append(out, decodeCFDomain(e)) + } + return out +} + +// isValidCFDomain mirrors tg-ws-proxy/proxy/config.py:_is_valid_domain. +func isValidCFDomain(domain string) bool { + if domain == "" || len(domain) > 253 { + return false + } + if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") { + return false + } + labels := strings.Split(domain, ".") + if len(labels) < 2 { + return false + } + for _, label := range labels { + if label == "" || len(label) > 63 { + return false + } + if label[0] == '-' || label[len(label)-1] == '-' { + return false + } + for _, c := range label { + ok := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '-' + if !ok { + return false + } + } + } + tld := labels[len(labels)-1] + if len(tld) < 2 { + return false + } + hasAlpha := false + for _, c := range tld { + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { + hasAlpha = true + break + } + } + return hasAlpha +} + +// normalizeCFDomains lowercases, trims, validates, and dedupes (preserves order). +func normalizeCFDomains(in []string) []string { + seen := map[string]bool{} + out := []string{} + for _, d := range in { + d = strings.ToLower(strings.TrimSpace(d)) + if !isValidCFDomain(d) || seen[d] { + continue + } + seen[d] = true + out = append(out, d) + } + return out +} + +// cfBalancer is the Go equivalent of tg-ws-proxy/proxy/balancer.py:_Balancer. +// Per-DC sticky domain selection over a rotating pool; the active domain is +// tried first, the remaining pool is shuffled. +type cfBalancer struct { + mu sync.Mutex + domains []string + perDC map[int]string + cooldown map[string]time.Time +} + +func newCFBalancer() *cfBalancer { + b := &cfBalancer{ + domains: defaultCFProxyDomains(), + perDC: map[int]string{}, + cooldown: map[string]time.Time{}, + } + b.seedPerDC() + return b +} + +// seedPerDC picks a random domain from the current pool for each known DC. +// Caller must hold mu. +func (b *cfBalancer) seedPerDC() { + if len(b.domains) == 0 { + b.perDC = map[int]string{} + return + } + dcs := []int{1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 203} + b.perDC = make(map[int]string, len(dcs)) + for _, dc := range dcs { + b.perDC[dc] = b.domains[cfproxyRand(len(b.domains))] + } +} + +func (b *cfBalancer) updateDomainsList(list []string) { + b.mu.Lock() + defer b.mu.Unlock() + if len(list) == 0 || sameStringSet(b.domains, list) { + return + } + b.domains = append([]string(nil), list...) + b.seedPerDC() +} + +// domainsForDC returns the trial order for `dc`: current pinned first, then a +// shuffle of the rest. Domains in 429/503 cooldown are dropped, unless that +// would leave nothing (DC1/3/5 have no other WS path), in which case cooldown +// is ignored so we still try. Empty only if the pool itself is empty. +func (b *cfBalancer) domainsForDC(dc int) []string { + b.mu.Lock() + defer b.mu.Unlock() + if len(b.domains) == 0 { + return nil + } + now := time.Now() + cooled := func(d string) bool { + t, ok := b.cooldown[d] + return ok && now.Before(t) + } + current := b.perDC[dc] + others := make([]string, 0, len(b.domains)) + for _, d := range b.domains { + if d != current { + others = append(others, d) + } + } + cfproxyShuffle(others) + + out := make([]string, 0, len(b.domains)) + if current != "" && !cooled(current) { + out = append(out, current) + } + for _, d := range others { + if !cooled(d) { + out = append(out, d) + } + } + if len(out) > 0 { + return out + } + // everything is cooled down: fall back to trying all (still better than no path) + if current != "" { + out = append(out, current) + } + return append(out, others...) +} + +// penalize puts a CF-proxy domain into cooldown after a 429/503 so subsequent +// dials skip it until it recovers. +func (b *cfBalancer) penalize(domain string, d time.Duration) { + if domain == "" { + return + } + b.mu.Lock() + defer b.mu.Unlock() + b.cooldown[domain] = time.Now().Add(d) +} + +// pin records that `domain` worked for `dc`. Returns true if the pin changed. +func (b *cfBalancer) pin(dc int, domain string) bool { + b.mu.Lock() + defer b.mu.Unlock() + if b.perDC[dc] == domain { + return false + } + b.perDC[dc] = domain + return true +} + +func (b *cfBalancer) size() int { + b.mu.Lock() + defer b.mu.Unlock() + return len(b.domains) +} + +// refreshFromURL fetches the encoded domain list from the given URL, +// decodes + validates, and replaces the pool if the new list is sane. +func (b *cfBalancer) refreshFromURL(url string) error { + if url == "" { + url = DefaultCFProxyURL + } + // match tg-ws-proxy: random querystring suffix to defeat caches + req, err := http.NewRequest("GET", url+"?cb="+cfproxyCacheBust(), nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", "b4-mtproto") + cli := &http.Client{Timeout: 10 * time.Second} + resp, err := cli.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("status %d", resp.StatusCode) + } + body, err := io.ReadAll(io.LimitReader(resp.Body, cfProxyFetchMaxLen)) + if err != nil { + return err + } + var decoded []string + for _, line := range strings.Split(string(body), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + decoded = append(decoded, decodeCFDomain(line)) + } + pool := normalizeCFDomains(decoded) + if len(pool) < cfProxyMinValid { + return fmt.Errorf("only %d valid domains, keeping current pool", len(pool)) + } + b.updateDomainsList(pool) + return nil +} + +// cfBalancerInst is the package-level singleton. Initialized with the bundled +// defaults so b4 has a working CF pool from process start even before the +// first GitHub refresh succeeds. +var cfBalancerInst = newCFBalancer() + +func sameStringSet(a, b []string) bool { + if len(a) != len(b) { + return false + } + counts := make(map[string]int, len(a)) + for _, s := range a { + counts[s]++ + } + for _, s := range b { + counts[s]-- + } + for _, v := range counts { + if v != 0 { + return false + } + } + return true +} + +// cfproxyRand returns an unbiased-ish int in [0,n) using crypto/rand. +// Non-crypto usage; modulo bias on a 64-bit value is negligible for our pool sizes. +func cfproxyRand(n int) int { + if n <= 0 { + return 0 + } + var buf [8]byte + _, _ = rand.Read(buf[:]) + v := uint64(buf[0]) | uint64(buf[1])<<8 | uint64(buf[2])<<16 | uint64(buf[3])<<24 | + uint64(buf[4])<<32 | uint64(buf[5])<<40 | uint64(buf[6])<<48 | uint64(buf[7])<<56 + return int(v % uint64(n)) +} + +func cfproxyShuffle(s []string) { + for i := len(s) - 1; i > 0; i-- { + j := cfproxyRand(i + 1) + s[i], s[j] = s[j], s[i] + } +} + +func cfproxyCacheBust() string { + var b [4]byte + _, _ = rand.Read(b[:]) + return hex.EncodeToString(b[:]) +} + +var ( + cfRefreshOnce sync.Once + cfRefreshURL atomic.Pointer[string] +) + +func StartCFProxyRefresh(ctx interface{ Done() <-chan struct{} }, url string) { + cfRefreshURL.Store(&url) + cfRefreshOnce.Do(func() { + go runCFProxyRefreshLoop(ctx) + }) +} + +func runCFProxyRefreshLoop(ctx interface{ Done() <-chan struct{} }) { + currentURL := func() string { + if p := cfRefreshURL.Load(); p != nil { + return *p + } + return "" + } + if err := cfBalancerInst.refreshFromURL(currentURL()); err != nil { + log.Warnf("CF proxy refresh failed at startup: %v", err) + } else { + log.Infof("CF proxy pool refreshed (%d domains)", cfBalancerInst.size()) + } + ticker := time.NewTicker(cfProxyRefreshInt) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := cfBalancerInst.refreshFromURL(currentURL()); err != nil { + log.Debugf("CF proxy refresh failed: %v", err) + } else { + log.Debugf("CF proxy pool refreshed (%d domains)", cfBalancerInst.size()) + } + } + } +} diff --git a/src/mtproto/cfproxy_cooldown_test.go b/src/mtproto/cfproxy_cooldown_test.go new file mode 100644 index 00000000..6e84c06c --- /dev/null +++ b/src/mtproto/cfproxy_cooldown_test.go @@ -0,0 +1,57 @@ +package mtproto + +import ( + "testing" + "time" +) + +func TestBalancer_PenalizeCooldownSkipsDomain(t *testing.T) { + b := newCFBalancer() + all := b.domainsForDC(2) + if len(all) < 2 { + t.Fatalf("need >=2 default domains, got %d", len(all)) + } + + victim := all[0] + b.penalize(victim, cfProxyDomainCooldown) + + got := b.domainsForDC(2) + for _, d := range got { + if d == victim { + t.Fatalf("penalized domain %q should be skipped, got %v", victim, got) + } + } + if len(got) != len(all)-1 { + t.Fatalf("expected %d domains after cooldown, got %d", len(all)-1, len(got)) + } +} + +func TestBalancer_AllCooledDownFallsBackToAll(t *testing.T) { + b := newCFBalancer() + all := b.domainsForDC(2) + for _, d := range all { + b.penalize(d, cfProxyDomainCooldown) + } + got := b.domainsForDC(2) + if len(got) != len(all) { + t.Fatalf("when all cooled, expect fallback to all %d, got %d", len(all), len(got)) + } +} + +func TestBalancer_CooldownExpires(t *testing.T) { + b := newCFBalancer() + all := b.domainsForDC(2) + victim := all[0] + b.penalize(victim, -time.Second) // already expired + got := b.domainsForDC(2) + found := false + for _, d := range got { + if d == victim { + found = true + break + } + } + if !found { + t.Fatalf("expired cooldown should restore %q, got %v", victim, got) + } +} diff --git a/src/mtproto/cfproxy_test.go b/src/mtproto/cfproxy_test.go new file mode 100644 index 00000000..ee291141 --- /dev/null +++ b/src/mtproto/cfproxy_test.go @@ -0,0 +1,104 @@ +package mtproto + +import ( + "testing" +) + +func TestDecodeCFDomain_MatchesPythonReference(t *testing.T) { + cases := map[string]string{ + // known good from user log: mkuosckvso.com decodes to cakeisalie.co.uk + "mkuosckvso.com": "cakeisalie.co.uk", + } + for in, want := range cases { + got := decodeCFDomain(in) + if got != want { + t.Errorf("decodeCFDomain(%q) = %q, want %q", in, got, want) + } + } +} + +func TestDecodeCFDomain_PassesThroughNonDotCom(t *testing.T) { + if got := decodeCFDomain("already.co.uk"); got != "already.co.uk" { + t.Errorf("expected passthrough for non-.com, got %q", got) + } +} + +func TestDefaultCFProxyDomains_AllValid(t *testing.T) { + pool := defaultCFProxyDomains() + if len(pool) != len(defaultCFProxyEncoded) { + t.Fatalf("default pool size mismatch: %d vs %d", len(pool), len(defaultCFProxyEncoded)) + } + for _, d := range pool { + if !isValidCFDomain(d) { + t.Errorf("default decoded domain failed validation: %q", d) + } + } +} + +func TestIsValidCFDomain(t *testing.T) { + good := []string{"example.com", "kws1.cakeisalie.co.uk", "a.b.co"} + bad := []string{"", ".com", "example.", "a..b", "-bad.com", "bad-.com", "x.1", "x." + string(rune('a'-1))} + for _, d := range good { + if !isValidCFDomain(d) { + t.Errorf("expected valid: %q", d) + } + } + for _, d := range bad { + if isValidCFDomain(d) { + t.Errorf("expected invalid: %q", d) + } + } +} + +func TestBalancer_PinAndRotation(t *testing.T) { + b := newCFBalancer() + if b.size() != len(defaultCFProxyEncoded) { + t.Fatalf("expected default pool size %d, got %d", len(defaultCFProxyEncoded), b.size()) + } + domains := b.domainsForDC(2) + if len(domains) != b.size() { + t.Fatalf("expected %d domains for DC2, got %d", b.size(), len(domains)) + } + // pin a domain different from the randomly-seeded current one and verify + // it's listed first. Picking != current avoids a flake when seedPerDC + // happened to seed our target as DC2's active domain already. + target := domains[0] + if target == b.perDC[2] { + target = domains[len(domains)-1] + } + if !b.pin(2, target) { + t.Fatal("first pin should report change=true") + } + if b.pin(2, target) { + t.Fatal("idempotent pin should report change=false") + } + got := b.domainsForDC(2) + if got[0] != target { + t.Errorf("expected pinned %q first, got %q", target, got[0]) + } +} + +func TestBalancer_UpdateDomainsList_ReplacesPool(t *testing.T) { + b := newCFBalancer() + newList := []string{"a.co.uk", "b.co.uk", "c.co.uk"} + b.updateDomainsList(newList) + if b.size() != 3 { + t.Errorf("expected size 3 after update, got %d", b.size()) + } + // unchanged update is a no-op + b.updateDomainsList([]string{"c.co.uk", "b.co.uk", "a.co.uk"}) // same set + if b.size() != 3 { + t.Errorf("set-equivalent update should not change pool, got size %d", b.size()) + } +} + +func TestNormalizeCFDomains_LowercaseTrimDedupe(t *testing.T) { + in := []string{"A.com", " b.co.uk ", "a.com", "invalid..", "c.com"} + out := normalizeCFDomains(in) + if len(out) != 3 { + t.Fatalf("expected 3 valid domains, got %v", out) + } + if out[0] != "a.com" { + t.Errorf("expected lowercase first, got %q", out[0]) + } +} diff --git a/src/mtproto/dc.go b/src/mtproto/dc.go index 5329c63b..3f0b5810 100644 --- a/src/mtproto/dc.go +++ b/src/mtproto/dc.go @@ -31,6 +31,58 @@ var dcAddressesV6 = map[int]string{ 5: "[2001:b28:f23f:f005::a]:443", } +type dcNet struct { + net *net.IPNet + dc int +} + +func mustParseDCNets(pairs [][2]interface{}) []dcNet { + out := make([]dcNet, 0, len(pairs)) + for _, p := range pairs { + cidr, _ := p[0].(string) + dc, _ := p[1].(int) + _, n, err := net.ParseCIDR(cidr) + if err != nil || n == nil { + continue + } + out = append(out, dcNet{net: n, dc: dc}) + } + return out +} + +var dcRangesV4 = mustParseDCNets([][2]interface{}{ + {"149.154.167.0/24", 2}, + {"149.154.161.0/24", 2}, + {"149.154.165.0/24", 4}, + {"149.154.166.0/24", 4}, + {"91.108.4.0/22", 4}, +}) + +var dcRangesV6 = mustParseDCNets([][2]interface{}{ + {"2001:67c:4e8:f002::/64", 2}, + {"2001:67c:4e8:f004::/64", 4}, +}) + +func dcForIPRange(ip net.IP) (int, bool) { + if ip == nil { + return 0, false + } + if ip.To4() != nil { + for _, e := range dcRangesV4 { + if e.net.Contains(ip) { + return e.dc, true + } + } + return 0, false + } + for _, e := range dcRangesV6 { + if e.net.Contains(ip) { + return e.dc, true + } + } + return 0, false +} + var ( dcRuntimeMu sync.RWMutex dcRuntime = map[int][]string{} @@ -63,6 +115,40 @@ func DCSnapshotAll() map[int][]string { return out } +func dcForIP(ip net.IP) (int, bool) { + if ip == nil { + return 0, false + } + target := ip.String() + + dcRuntimeMu.RLock() + for dc, addrs := range dcRuntime { + for _, a := range addrs { + host, _, err := net.SplitHostPort(a) + if err != nil { + host = a + } + if host == target { + dcRuntimeMu.RUnlock() + return dc, true + } + } + } + dcRuntimeMu.RUnlock() + + for dc, a := range dcAddressesV4 { + if host, _, err := net.SplitHostPort(a); err == nil && host == target { + return dc, true + } + } + for dc, a := range dcAddressesV6 { + if host, _, err := net.SplitHostPort(a); err == nil && host == target { + return dc, true + } + } + return 0, false +} + func RefreshDCs() error { cli := &http.Client{Timeout: 3 * time.Second} var body []byte diff --git a/src/mtproto/dc_test.go b/src/mtproto/dc_test.go new file mode 100644 index 00000000..51e7285e --- /dev/null +++ b/src/mtproto/dc_test.go @@ -0,0 +1,46 @@ +package mtproto + +import ( + "net" + "testing" +) + +func TestDCForIPRange(t *testing.T) { + cases := []struct { + ip string + want int + ok bool + }{ + {"149.154.167.50", 2, true}, + {"149.154.167.222", 2, true}, + {"149.154.161.144", 2, true}, + {"149.154.166.121", 4, true}, + {"149.154.165.109", 4, true}, + {"91.108.4.140", 4, true}, + {"2001:67c:4e8:f002::a", 2, true}, + {"2001:67c:4e8:f004::a", 4, true}, + {"149.154.162.123", 0, false}, + {"149.154.175.50", 0, false}, + {"91.105.192.100", 0, false}, + {"8.8.8.8", 0, false}, + } + for _, c := range cases { + got, ok := dcForIPRange(net.ParseIP(c.ip)) + if ok != c.ok || (ok && got != c.want) { + t.Errorf("dcForIPRange(%s) = (%d, %v), want (%d, %v)", c.ip, got, ok, c.want, c.ok) + } + } +} + +func TestDCForIPRangeOnlyWSServedDCs(t *testing.T) { + for _, e := range dcRangesV4 { + if !wsEdgeServesDC(e.dc) { + t.Errorf("dcRangesV4 entry %s maps to DC%d which is not WS-served; range resolution should only assert WS-served DCs", e.net, e.dc) + } + } + for _, e := range dcRangesV6 { + if !wsEdgeServesDC(e.dc) { + t.Errorf("dcRangesV6 entry %s maps to DC%d which is not WS-served", e.net, e.dc) + } + } +} diff --git a/src/mtproto/faketls.go b/src/mtproto/faketls.go index c8dd0055..d92f633c 100644 --- a/src/mtproto/faketls.go +++ b/src/mtproto/faketls.go @@ -23,7 +23,6 @@ const ( handshakeServerHello = 0x02 maxTLSRecordPayload = 16379 timestampTolerance = 120 - secondDuration = time.Second ) type FakeTLSConn struct { @@ -249,7 +248,6 @@ func buildServerHello(secret *Secret, clientRandom, sessionID []byte) []byte { rand.Read(x25519Key) var extensions bytes.Buffer - extensions.Write([]byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04}) keyShareData := make([]byte, 0, 36) keyShareData = append(keyShareData, 0x00, 0x1d, 0x00, 0x20) keyShareData = append(keyShareData, x25519Key...) @@ -257,6 +255,7 @@ func buildServerHello(secret *Secret, clientRandom, sessionID []byte) []byte { extLen := len(keyShareData) extensions.Write([]byte{byte(extLen >> 8), byte(extLen)}) extensions.Write(keyShareData) + extensions.Write([]byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04}) extBytes := extensions.Bytes() shBody.Write([]byte{byte(len(extBytes) >> 8), byte(len(extBytes))}) diff --git a/src/mtproto/obfuscated2.go b/src/mtproto/obfuscated2.go index 5fa2b0f0..5cf15713 100644 --- a/src/mtproto/obfuscated2.go +++ b/src/mtproto/obfuscated2.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "crypto/sha256" "encoding/binary" + "errors" "fmt" "io" "net" @@ -29,6 +30,41 @@ const ( tcpDialTimeout = 8 * time.Second ) +var wsEdgeServedDCs = map[int]bool{2: true, 4: true} + +func wsEdgeServesDC(absDC int) bool { + return wsEdgeServedDCs[absDC] +} + +// workerDomains parses the comma-separated CF Worker domain list from config. +func workerDomains(cfg *config.MTProtoConfig) []string { + raw := strings.TrimSpace(cfg.CFWorkerDomain) + if raw == "" { + return nil + } + var out []string + for _, d := range strings.Split(raw, ",") { + if d = strings.TrimSpace(d); d != "" { + out = append(out, d) + } + } + return out +} + +// workerDstIP returns the canonical public DC IP the worker should TCP-connect to +// (the worker's ?dst= target). Mirrors tg-ws-proxy DC_DEFAULT_IPS. +func workerDstIP(absDC int) string { + addr, ok := dcAddressesV4[absDC] + if !ok { + return "" + } + host, _, err := net.SplitHostPort(addr) + if err != nil { + return addr + } + return host +} + type ObfuscatedConn struct { net.Conn reader cipher.Stream @@ -56,15 +92,31 @@ type ClientHandshakeResult struct { } func AcceptObfuscated(conn net.Conn, secret *Secret) (*ClientHandshakeResult, error) { + return acceptObfuscatedFrame(conn, func(raw []byte) []byte { + return deriveKey(raw, secret.Key[:]) + }) +} + +func acceptObfuscatedFrame(conn net.Conn, derive func(raw []byte) []byte) (*ClientHandshakeResult, error) { frame := make([]byte, obfuscatedFrameLen) if _, err := io.ReadFull(conn, frame); err != nil { return nil, fmt.Errorf("read handshake: %w", err) } + return decodeObfuscatedFrame(frame, conn, derive) +} - decKey := deriveKey(frame[8:40], secret.Key[:]) +func decodeObfuscatedDirect(frame []byte, conn net.Conn) (*ClientHandshakeResult, error) { + return decodeObfuscatedFrame(frame, conn, func(raw []byte) []byte { + out := make([]byte, len(raw)) + copy(out, raw) + return out + }) +} + +func decodeObfuscatedFrame(frame []byte, conn net.Conn, derive func(raw []byte) []byte) (*ClientHandshakeResult, error) { decIV := make([]byte, 16) copy(decIV, frame[40:56]) - decStream, err := newAESCTR(decKey, decIV) + decStream, err := newAESCTR(derive(frame[8:40]), decIV) if err != nil { return nil, fmt.Errorf("init decrypt: %w", err) } @@ -73,10 +125,9 @@ func AcceptObfuscated(conn net.Conn, secret *Secret) (*ClientHandshakeResult, er for i := 0; i < 48; i++ { reversed[i] = frame[55-i] } - encKey := deriveKey(reversed[:32], secret.Key[:]) encIV := make([]byte, 16) copy(encIV, reversed[32:48]) - encStream, err := newAESCTR(encKey, encIV) + encStream, err := newAESCTR(derive(reversed[:32]), encIV) if err != nil { return nil, fmt.Errorf("init encrypt: %w", err) } @@ -117,11 +168,18 @@ type transportPlan struct { addr string sni string dialHost string + dc int + cfBase string // CF-proxy base domain (without "kwsN."); set => pin in balancer on success + wsPath string // WS request path; "" defaults to /apiws. CF Worker uses /apiws?dst=&dc= + isWorker bool // CF Worker plan: dedicated per-user relay, reaches any DC via ?dst= } func (p transportPlan) describe() string { switch p.kind { case transportWS: + if p.isWorker { + return "wsworker://" + p.sni + } if p.dialHost != "" && p.dialHost != p.sni { return fmt.Sprintf("ws://%s@%s", p.sni, p.dialHost) } @@ -153,6 +211,9 @@ func planTransports(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc i return } for _, a := range addrs { + if tcpAddrInCooldown(a) { + continue + } plans = append(plans, transportPlan{kind: transportTCP, addr: a}) } } @@ -161,30 +222,57 @@ func planTransports(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc i appendTCP() } - if mode == "ws" || mode == "auto" { + if (mode == "ws" || mode == "auto") && !wsIsBlacklisted(dc) { edgeIP := strings.TrimSpace(cfg.WSEndpointHost) if edgeIP == "" { edgeIP = telegramWSEdgeIP } - wsDC := absDC - if absDC == 203 { - wsDC = 2 - } - if wsDC >= 1 && wsDC <= 5 { - primary := transportPlan{kind: transportWS, sni: fmt.Sprintf("kws%d.web.telegram.org", wsDC), dialHost: edgeIP} - media := transportPlan{kind: transportWS, sni: fmt.Sprintf("kws%d-1.web.telegram.org", wsDC), dialHost: edgeIP} + if wsEdgeServesDC(absDC) { + primary := transportPlan{kind: transportWS, dc: dc, sni: fmt.Sprintf("kws%d.web.telegram.org", absDC), dialHost: edgeIP} + media := transportPlan{kind: transportWS, dc: dc, sni: fmt.Sprintf("kws%d-1.web.telegram.org", absDC), dialHost: edgeIP} if dc < 0 { plans = append(plans, media, primary) } else { plans = append(plans, primary, media) } } + // CF Worker (free per-user workers.dev relay). Tried after TG's own edge so + // the fast native path wins for DC2/4, but before the shared CF pool so DCs + // without a native edge (1/3/5) and throttled cases use the dedicated worker + // instead of the rate-limited shared domains. The worker reaches any DC via + // ?dst=&dc= (matches tg-ws-proxy CfWorker mode). + if dst := workerDstIP(absDC); dst != "" { + for _, wd := range workerDomains(cfg) { + plans = append(plans, transportPlan{ + kind: transportWS, + dc: dc, + sni: wd, + dialHost: wd, + wsPath: fmt.Sprintf("/apiws?dst=%s&dc=%d", dst, absDC), + isWorker: true, + }) + } + } if d := strings.TrimSpace(cfg.WSCustomDomain); d != "" { plans = append(plans, transportPlan{ - kind: transportWS, - sni: fmt.Sprintf("kws%d.%s", absDC, d), + kind: transportWS, + dc: dc, + sni: fmt.Sprintf("kws%d.%s", absDC, d), + cfBase: d, }) } + // CF-proxy fallback pool (matches tg-ws-proxy). Tried after TG's own edge so + // the fast path wins when it works; CF rescues DCs the network blocks (esp. DC 1). + if cfg.CFProxyEnabled { + for _, base := range cfBalancerInst.domainsForDC(dc) { + plans = append(plans, transportPlan{ + kind: transportWS, + dc: dc, + sni: fmt.Sprintf("kws%d.%s", absDC, base), + cfBase: base, + }) + } + } } if mode == "auto" && !relayFirst { @@ -202,35 +290,139 @@ func planTransports(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc i } func DialObfuscatedDC(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc int, protoTag uint32) (*ObfuscatedConn, string, error) { + return DialObfuscatedDCWithPool(cfg, queueCfg, dc, protoTag, nil) +} + +func DialObfuscatedDCWithPool(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc int, protoTag uint32, pool *wsPool) (*ObfuscatedConn, string, error) { + if pool != nil && !wsIsBlacklisted(dc) { + if raw := pool.get(dc); raw != nil { + obf, err := completeObfuscation(raw, dc, protoTag) + if err == nil && raw.liveNow() { + log.Infof("MTProto DC %d connected via ws-pool", dc) + wsRecordSuccess(dc) + return obf, "ws-pool", nil + } + if err != nil { + log.Debugf("MTProto DC %d pool conn obf init failed: %v", dc, err) + } else { + log.Debugf("MTProto DC %d pool conn died before relay; re-dialing fresh", dc) + } + _ = raw.Close() + } + } + plans, err := planTransports(cfg, queueCfg, dc) if err != nil { return nil, "", err } - var lastErr error + wsTimeout := wsDialTimeout + if wsCooldownActive(dc) { + wsTimeout = wsDialTimeoutCooldown + } + + var attempts []string + wsTried := 0 + wsRedirects := 0 for _, p := range plans { log.Debugf("MTProto DC %d dialing %s", dc, p.describe()) start := time.Now() - conn, err := dialOne(p, queueCfg.Mark) - if err != nil { - lastErr = err - log.Debugf("MTProto DC %d %s failed after %dms: %v", dc, p.describe(), time.Since(start).Milliseconds(), err) + var conn net.Conn + var derr error + if p.kind == transportWS { + conn, derr = dialOneWS(p, queueCfg.Mark, wsTimeout) + } else { + conn, derr = dialOne(p, queueCfg.Mark) + } + if derr != nil { + attempts = append(attempts, fmt.Sprintf("%s: %s", p.describe(), shortErr(derr))) + if p.kind == transportWS { + wsTried++ + if isWSRedirect(derr) { + wsRedirects++ + } + if p.cfBase != "" && wsRateLimited(derr) { + cfBalancerInst.penalize(p.cfBase, cfProxyDomainCooldown) + } + } else if isDialTimeout(derr) { + tcpRecordFailure(p.addr) + } + log.Debugf("MTProto DC %d %s failed after %dms: %v", dc, p.describe(), time.Since(start).Milliseconds(), derr) continue } - obfConn, err := completeObfuscation(conn, dc, protoTag) - if err != nil { - lastErr = err + obfConn, oerr := completeObfuscation(conn, dc, protoTag) + if oerr != nil { + attempts = append(attempts, fmt.Sprintf("%s: %s", p.describe(), shortErr(oerr))) conn.Close() - log.Debugf("MTProto DC %d obf init failed on %s: %v", dc, p.describe(), err) + log.Debugf("MTProto DC %d obf init failed on %s: %v", dc, p.describe(), oerr) continue } + if p.kind == transportWS { + wsRecordSuccess(dc) + // pin successful CF-proxy domain for this DC so subsequent connections + // try it first (mirrors tg-ws-proxy/proxy/balancer.py:update_domain_for_dc) + if p.cfBase != "" { + if cfBalancerInst.pin(dc, p.cfBase) { + log.Infof("MTProto DC %d switched active CF domain to %s", dc, p.cfBase) + } + } + } else { + tcpRecordSuccess(p.addr) + } log.Infof("MTProto DC %d connected via %s in %dms", dc, p.describe(), time.Since(start).Milliseconds()) return obfConn, p.describe(), nil } - if lastErr == nil { - lastErr = fmt.Errorf("no transport succeeded") + + if wsTried > 0 { + wsRecordFailure(dc, wsRedirects == wsTried) } - return nil, "", lastErr + if len(attempts) == 0 { + return nil, "", fmt.Errorf("no transport available (all in cooldown or blacklisted)") + } + return nil, "", fmt.Errorf("all transports failed: %s", strings.Join(attempts, "; ")) +} + +func isDialTimeout(err error) bool { + if err == nil { + return false + } + var ne net.Error + if errors.As(err, &ne) && ne.Timeout() { + return true + } + return strings.Contains(err.Error(), "i/o timeout") +} + +func shortErr(err error) string { + s := err.Error() + for _, p := range []string{"tcp dial ", "tls handshake ", "ws read response: ", "ws write upgrade: ", "ws handshake "} { + s = strings.TrimPrefix(s, p) + } + return s +} + +func isWSRedirect(err error) bool { + var he *wsHandshakeError + if !errors.As(err, &he) { + return false + } + return he.isRedirect() +} + +func wsRateLimited(err error) bool { + var he *wsHandshakeError + if !errors.As(err, &he) { + return false + } + return he.statusCode == 429 || he.statusCode == 503 +} + +func dialOneWS(p transportPlan, mark uint, timeout time.Duration) (net.Conn, error) { + host := p.dialHost + if host == "" { + host = p.sni + } + return dialWS(host, p.sni, p.wsPath, timeout, mark) } type TransportProbeResult struct { @@ -300,7 +492,7 @@ func dialOne(p transportPlan, mark uint) (net.Conn, error) { if host == "" { host = p.sni } - return dialWS(host, p.sni, wsDialTimeout, mark) + return dialWS(host, p.sni, p.wsPath, wsDialTimeout, mark) default: dialer := net.Dialer{Timeout: tcpDialTimeout} if mark > 0 { @@ -351,7 +543,11 @@ func completeObfuscation(conn net.Conn, dc int, protoTag uint32) (*ObfuscatedCon encrypted := make([]byte, obfuscatedFrameLen) copy(encrypted, frame) encStream.XORKeyStream(encrypted, encrypted) - copy(encrypted[8:56], frame[8:56]) + // Restore bytes 0:56 to plaintext - only bytes 56:64 (proto_tag||dc||rnd) are + // sent encrypted. tg-ws-proxy keeps the SKIP region (0:8) as plaintext too; + // b4 was only restoring 8:56, leaving 0:7 as ciphertext on the wire. Some TG + // endpoints inspect the SKIP bytes before decryption. + copy(encrypted[0:56], frame[0:56]) if _, err := conn.Write(encrypted); err != nil { return nil, fmt.Errorf("send handshake: %w", err) @@ -364,6 +560,37 @@ func completeObfuscation(conn net.Conn, dc int, protoTag uint32) (*ObfuscatedCon }, nil } +// reservedFirst4Words are the first-4-byte little-endian values an obfuscated +// frame must never start with: they collide with TLS/HTTP and the obfuscation +// transport tags, so TG treats a connection beginning with one of them as a +// different protocol. generateFrame avoids producing them; reservedFirst4 +// (transparent bridge) uses them to detect a non-obfuscated transport. Keep the +// two in sync via this single list. +var reservedFirst4Words = []uint32{ + 0x44414548, // "HEAD" + 0x54534f50, // "POST" + 0x20544547, // "GET " + 0x4954504f, // "OPTI" + 0x02010316, // TLS record header + 0xdddddddd, // padded intermediate tag + 0xeeeeeeee, // intermediate tag +} + +// isReservedFirst4 reports whether the first 4 bytes are a reserved value +// (0xef abridged-tag first byte, or any reservedFirst4Words value). +func isReservedFirst4(b []byte) bool { + if b[0] == 0xef { + return true + } + first4 := binary.LittleEndian.Uint32(b[:4]) + for _, w := range reservedFirst4Words { + if first4 == w { + return true + } + } + return false +} + func generateFrame(dc int, protoTag uint32) []byte { frame := make([]byte, obfuscatedFrameLen) for { @@ -371,14 +598,7 @@ func generateFrame(dc int, protoTag uint32) []byte { continue } - if frame[0] == 0xef { - continue - } - first4 := binary.LittleEndian.Uint32(frame[0:4]) - if first4 == 0x44414548 || first4 == 0x54534f50 || - first4 == 0x20544547 || first4 == 0x4954504f || - first4 == 0x02010316 || first4 == 0xdddddddd || - first4 == 0xeeeeeeee { + if isReservedFirst4(frame[0:4]) { continue } if binary.LittleEndian.Uint32(frame[4:8]) == 0 { diff --git a/src/mtproto/plantransports_test.go b/src/mtproto/plantransports_test.go index c33ece57..ca9bffee 100644 --- a/src/mtproto/plantransports_test.go +++ b/src/mtproto/plantransports_test.go @@ -44,27 +44,30 @@ func TestPlanTransports_WSOnly_DC2(t *testing.T) { func TestPlanTransports_MediaDC_ReversesOrdering(t *testing.T) { cfg := &config.MTProtoConfig{UpstreamMode: "ws"} - plans, err := planTransports(cfg, config.QueueConfig{IPv4Enabled: true}, -3) + plans, err := planTransports(cfg, config.QueueConfig{IPv4Enabled: true}, -4) if err != nil { t.Fatalf("unexpected error: %v", err) } got := wsSNIs(plans) - want := []string{"kws3-1.web.telegram.org", "kws3.web.telegram.org"} + want := []string{"kws4-1.web.telegram.org", "kws4.web.telegram.org"} if len(got) != 2 || got[0] != want[0] || got[1] != want[1] { - t.Fatalf("media DC -3 order: got %v want %v", got, want) + t.Fatalf("media DC -4 order: got %v want %v", got, want) } } -func TestPlanTransports_DC203_RemapsToKws2(t *testing.T) { - cfg := &config.MTProtoConfig{UpstreamMode: "ws"} +func TestPlanTransports_DC203_NoDirectEdge(t *testing.T) { + cfg := &config.MTProtoConfig{UpstreamMode: "auto"} plans, err := planTransports(cfg, config.QueueConfig{IPv4Enabled: true}, 203) if err != nil { t.Fatalf("unexpected error: %v", err) } - got := wsSNIs(plans) - want := []string{"kws2.web.telegram.org", "kws2-1.web.telegram.org"} - if len(got) != 2 || got[0] != want[0] || got[1] != want[1] { - t.Fatalf("DC 203 should remap to kws2: got %v want %v", got, want) + for _, s := range wsSNIs(plans) { + if strings.HasSuffix(s, ".web.telegram.org") { + t.Fatalf("DC 203 must not use the TG WS edge (front cannot serve CDN), got %q", s) + } + } + if !hasTCP(plans) { + t.Fatalf("DC 203 should fall back to TCP, got %+v", plans) } } @@ -247,3 +250,98 @@ func TestPlanTransports_DCRelay_IgnoredInWSMode(t *testing.T) { t.Fatalf("expected WS plans for DC 2 in ws mode, got none") } } + +func TestPlanTransports_WorkerForDC2BeforeCFPool(t *testing.T) { + cfg := &config.MTProtoConfig{ + UpstreamMode: "ws", + CFWorkerDomain: "my-worker-123.user.workers.dev", + CFProxyEnabled: true, + } + plans, err := planTransports(cfg, config.QueueConfig{}, 2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + var workerIdx, cfIdx, edgeIdx = -1, -1, -1 + for i, p := range plans { + switch { + case p.isWorker && workerIdx == -1: + workerIdx = i + case p.cfBase != "" && cfIdx == -1: + cfIdx = i + case !p.isWorker && p.cfBase == "" && edgeIdx == -1: + edgeIdx = i + } + } + if workerIdx == -1 { + t.Fatal("expected a worker plan for DC2") + } + if edgeIdx == -1 || workerIdx < edgeIdx { + t.Errorf("worker (%d) should come after native edge (%d)", workerIdx, edgeIdx) + } + if cfIdx != -1 && workerIdx > cfIdx { + t.Errorf("worker (%d) should come before shared CF pool (%d)", workerIdx, cfIdx) + } + wp := plans[workerIdx] + if wp.wsPath != "/apiws?dst=149.154.167.51&dc=2" { + t.Errorf("unexpected worker path %q", wp.wsPath) + } + if wp.sni != "my-worker-123.user.workers.dev" || wp.dialHost != wp.sni { + t.Errorf("worker sni/dialHost wrong: sni=%q dialHost=%q", wp.sni, wp.dialHost) + } +} + +func TestPlanTransports_WorkerForDC1NoNativeEdge(t *testing.T) { + cfg := &config.MTProtoConfig{ + UpstreamMode: "ws", + CFWorkerDomain: "w.user.workers.dev", + } + plans, err := planTransports(cfg, config.QueueConfig{}, 1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + found := false + for _, p := range plans { + if p.isWorker { + found = true + if p.wsPath != "/apiws?dst=149.154.175.50&dc=1" { + t.Errorf("unexpected DC1 worker path %q", p.wsPath) + } + } + } + if !found { + t.Fatal("expected a worker plan for DC1 (no native edge)") + } +} + +func TestPlanTransports_MultipleWorkerDomains(t *testing.T) { + cfg := &config.MTProtoConfig{ + UpstreamMode: "ws", + CFWorkerDomain: " a.workers.dev , b.workers.dev ", + } + plans, err := planTransports(cfg, config.QueueConfig{}, 2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + n := 0 + for _, p := range plans { + if p.isWorker { + n++ + } + } + if n != 2 { + t.Errorf("expected 2 worker plans (trimmed), got %d", n) + } +} + +func TestPlanTransports_NoWorkerWhenUnset(t *testing.T) { + cfg := &config.MTProtoConfig{UpstreamMode: "ws"} + plans, err := planTransports(cfg, config.QueueConfig{}, 2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + for _, p := range plans { + if p.isWorker { + t.Error("did not expect worker plans when CFWorkerDomain is empty") + } + } +} diff --git a/src/mtproto/server.go b/src/mtproto/server.go index 2dd62cc6..d1c81e6a 100644 --- a/src/mtproto/server.go +++ b/src/mtproto/server.go @@ -17,23 +17,27 @@ import ( const ( maxConnections = 512 - relayBufSize = 16384 + relayBufSize = 65536 ) type Server struct { - cfg *config.Config - secret *Secret + connSem chan struct{} + bufPool sync.Pool + active atomic.Int64 + + cfg atomic.Pointer[config.Config] + secret atomic.Pointer[Secret] + wsPool atomic.Pointer[wsPool] + + mu sync.Mutex + running bool listener net.Listener ctx context.Context cancel context.CancelFunc - active atomic.Int64 - connSem chan struct{} - bufPool sync.Pool } func NewServer(cfg *config.Config) *Server { - return &Server{ - cfg: cfg, + s := &Server{ connSem: make(chan struct{}, maxConnections), bufPool: sync.Pool{ New: func() interface{} { @@ -42,10 +46,19 @@ func NewServer(cfg *config.Config) *Server { }, }, } + s.cfg.Store(cfg) + return s } func (s *Server) Start() error { - mtCfg := &s.cfg.System.MTProto + s.mu.Lock() + defer s.mu.Unlock() + return s.startLocked() +} + +func (s *Server) startLocked() error { + cfg := s.cfg.Load() + mtCfg := &cfg.System.MTProto if !mtCfg.Enabled { log.Infof("MTProto proxy disabled") return nil @@ -65,8 +78,8 @@ func (s *Server) Start() error { return fmt.Errorf("MTProto generate secret: %w", err) } mtCfg.Secret = sec.Hex() - if s.cfg.ConfigPath != "" { - if err := s.cfg.SaveToFile(s.cfg.ConfigPath); err != nil { + if cfg.ConfigPath != "" { + if err := cfg.SaveToFile(cfg.ConfigPath); err != nil { log.Warnf("MTProto: failed to persist generated secret: %v", err) } } @@ -74,43 +87,115 @@ func (s *Server) Start() error { } else { return fmt.Errorf("MTProto: either secret or fake_sni must be configured") } - s.secret = sec addr := net.JoinHostPort(mtCfg.BindAddress, strconv.Itoa(mtCfg.Port)) - s.ctx, s.cancel = context.WithCancel(context.Background()) ln, err := net.Listen("tcp", addr) if err != nil { return fmt.Errorf("MTProto listen: %w", err) } s.listener = ln + s.secret.Store(sec) + s.ctx, s.cancel = context.WithCancel(context.Background()) log.Infof("MTProto proxy listening on %s (SNI: %s)", addr, sec.Host) - go s.acceptLoop() + if mode := mtCfg.UpstreamMode; mode == "ws" || mode == "auto" || mode == "" { + wsResetState() + tcpResetState() + pool := newWSPool(MTProtoUpstream{ + WSEndpointHost: mtCfg.WSEndpointHost, + WSCustomDomain: mtCfg.WSCustomDomain, + }, cfg.Queue.Mark, wsPoolDefaultSize) + pool.warmup([]int{2, 4}) + s.wsPool.Store(pool) + } else { + s.wsPool.Store(nil) + } + + s.running = true + go s.acceptLoop(ln) return nil } func (s *Server) Stop() error { + s.mu.Lock() + defer s.mu.Unlock() + return s.stopLocked() +} + +func (s *Server) stopLocked() error { if s.cancel != nil { s.cancel() + s.cancel = nil } + if pool := s.wsPool.Swap(nil); pool != nil { + pool.close() + } + var err error if s.listener != nil { - return s.listener.Close() + err = s.listener.Close() + s.listener = nil } - return nil + s.running = false + return err +} + +func (s *Server) UpdateConfig(newCfg *config.Config) { + s.mu.Lock() + defer s.mu.Unlock() + + old := s.cfg.Load() + s.cfg.Store(newCfg) + + if old != nil && !mtprotoNeedsRestart(old, newCfg) { + return + } + + wasEnabled := old != nil && old.System.MTProto.Enabled + if s.running { + _ = s.stopLocked() + } + + if newCfg.System.MTProto.Enabled { + if err := s.startLocked(); err != nil { + log.Errorf("MTProto reload failed: %v (proxy stopped; fix in Settings)", err) + } else { + log.Infof("MTProto reloaded with updated configuration") + } + } else if wasEnabled { + log.Infof("MTProto proxy stopped (disabled in configuration)") + } +} + +func mtprotoNeedsRestart(old, newCfg *config.Config) bool { + o := old.System.MTProto + n := newCfg.System.MTProto + if o.Enabled != n.Enabled || + o.Port != n.Port || + o.BindAddress != n.BindAddress || + o.Secret != n.Secret || + o.FakeSNI != n.FakeSNI || + o.UpstreamMode != n.UpstreamMode || + o.WSEndpointHost != n.WSEndpointHost || + o.WSCustomDomain != n.WSCustomDomain || + o.CFProxyEnabled != n.CFProxyEnabled || + o.CFProxyURL != n.CFProxyURL { + return true + } + return old.Queue.Mark != newCfg.Queue.Mark } func (s *Server) GetSecret() string { - if s.secret != nil { - return s.secret.Hex() + if sec := s.secret.Load(); sec != nil { + return sec.Hex() } return "" } -func (s *Server) acceptLoop() { +func (s *Server) acceptLoop(ln net.Listener) { for { - conn, err := s.listener.Accept() + conn, err := ln.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { return @@ -127,6 +212,13 @@ func (s *Server) acceptLoop() { continue } + // match tg-ws-proxy: tune accepted client socket to 256KB buffers + nodelay + if tc, ok := conn.(*net.TCPConn); ok { + _ = tc.SetNoDelay(true) + _ = tc.SetReadBuffer(256 * 1024) + _ = tc.SetWriteBuffer(256 * 1024) + } + s.active.Add(1) go func(c net.Conn) { defer func() { @@ -149,22 +241,28 @@ func (s *Server) handleConn(raw net.Conn) { } }() + secret := s.secret.Load() + if secret == nil { + return + } + cfg := s.cfg.Load() + if err := raw.SetDeadline(time.Now().Add(30 * time.Second)); err != nil { return } - tlsConn, err := AcceptFakeTLS(raw, s.secret) + tlsConn, err := AcceptFakeTLS(raw, secret) if err != nil { log.Debugf("MTProto fake-TLS failed from %s: %v", clientAddr, err) var vErr *FakeTLSVerifyError - if errors.As(err, &vErr) && s.cfg.System.MTProto.FakeSNI != "" { - proxyToMaskingDomain(raw, vErr.Initial, s.cfg.System.MTProto.FakeSNI, s.cfg.Queue.Mark) + if errors.As(err, &vErr) && cfg.System.MTProto.FakeSNI != "" { + proxyToMaskingDomain(raw, vErr.Initial, cfg.System.MTProto.FakeSNI, cfg.Queue.Mark) } return } log.Debugf("MTProto fake-TLS handshake OK from %s", clientAddr) - result, err := AcceptObfuscated(tlsConn, s.secret) + result, err := AcceptObfuscated(tlsConn, secret) if err != nil { log.Tracef("MTProto obfuscated2 failed from %s: %v", clientAddr, err) return @@ -172,9 +270,14 @@ func (s *Server) handleConn(raw net.Conn) { log.Debugf("MTProto client from %s wants DC %d proto=0x%08x", clientAddr, result.DC, result.ProtoTag) _ = raw.SetDeadline(time.Time{}) - dcConn, transport, err := DialObfuscatedDC(&s.cfg.System.MTProto, s.cfg.Queue, result.DC, result.ProtoTag) + dcConn, transport, err := DialObfuscatedDCWithPool(&cfg.System.MTProto, cfg.Queue, result.DC, result.ProtoTag, s.wsPool.Load()) if err != nil { - log.Errorf("MTProto dial DC %d: %v", result.DC, err) + // throttle ERROR-level spam from a permanently-broken DC; full detail still goes to Debug + if shouldLogDialError(result.DC) { + log.Errorf("MTProto dial DC %d: %v", result.DC, err) + } else { + log.Debugf("MTProto dial DC %d (suppressed): %v", result.DC, err) + } return } defer dcConn.Close() @@ -189,19 +292,42 @@ func (s *Server) handleConn(raw net.Conn) { } func (s *Server) relay(client, dc io.ReadWriteCloser, splitter *msgSplitter, label string) { - errCh := make(chan error, 2) + relayConns(client, dc, splitter, label, &s.bufPool) +} - cp := func(dst io.Writer, src io.Reader, dir string) { - bufPtr := s.bufPool.Get().(*[]byte) - defer s.bufPool.Put(bufPtr) - n, err := io.CopyBuffer(dst, src, *bufPtr) - log.Debugf("MTProto relay %s %s: %d bytes, err=%v", label, dir, n, err) +func relayConns(client, dc io.ReadWriteCloser, splitter *msgSplitter, label string, bufPool *sync.Pool) { + errCh := make(chan error, 2) + start := time.Now() + var upBytes, downBytes atomic.Int64 + + cp := func(dst io.Writer, src io.Reader, dir string, counter *atomic.Int64) { + bufPtr := bufPool.Get().(*[]byte) + defer bufPool.Put(bufPtr) + buf := *bufPtr + var total int64 + var err error + for { + var n int + n, err = src.Read(buf) + if n > 0 { + if _, werr := dst.Write(buf[:n]); werr != nil { + err = werr + } else { + total += int64(n) + } + } + if err != nil { + break + } + } + counter.Store(total) + log.Debugf("MTProto relay %s %s: %d bytes, err=%v", label, dir, total, err) errCh <- err } - cpSplit := func(dst io.Writer, src io.Reader, dir string) { - bufPtr := s.bufPool.Get().(*[]byte) - defer s.bufPool.Put(bufPtr) + cpSplit := func(dst io.Writer, src io.Reader, dir string, counter *atomic.Int64) { + bufPtr := bufPool.Get().(*[]byte) + defer bufPool.Put(bufPtr) buf := *bufPtr var total int64 var err error @@ -224,19 +350,21 @@ func (s *Server) relay(client, dc io.ReadWriteCloser, splitter *msgSplitter, lab break } } + counter.Store(total) log.Debugf("MTProto relay %s %s: %d bytes, err=%v", label, dir, total, err) errCh <- err } if splitter != nil { - go cpSplit(dc, client, "client->DC") + go cpSplit(dc, client, "client->DC", &upBytes) } else { - go cp(dc, client, "client->DC") + go cp(dc, client, "client->DC", &upBytes) } - go cp(client, dc, "DC->client") + go cp(client, dc, "DC->client", &downBytes) <-errCh _ = client.Close() _ = dc.Close() <-errCh + log.Infof("MTProto session %s closed: up=%d down=%d in %dms", label, upBytes.Load(), downBytes.Load(), time.Since(start).Milliseconds()) } diff --git a/src/mtproto/transparent.go b/src/mtproto/transparent.go new file mode 100644 index 00000000..683a7425 --- /dev/null +++ b/src/mtproto/transparent.go @@ -0,0 +1,171 @@ +package mtproto + +import ( + "fmt" + "io" + "net" + "sync" + "sync/atomic" + "time" + + "github.com/daniellavrushin/b4/config" + "github.com/daniellavrushin/b4/log" +) + +const transparentBufSize = 65536 + +type prefixConn struct { + net.Conn + prefix []byte +} + +func (c *prefixConn) Read(p []byte) (int, error) { + if len(c.prefix) > 0 { + n := copy(p, c.prefix) + c.prefix = c.prefix[n:] + return n, nil + } + return c.Conn.Read(p) +} + +func (c *prefixConn) CloseWrite() error { + if cw, ok := c.Conn.(interface{ CloseWrite() error }); ok { + return cw.CloseWrite() + } + return nil +} + +type TransparentBridge struct { + cfg atomic.Pointer[config.Config] + bufPool sync.Pool + + mu sync.Mutex + pool *wsPool + poolInit bool +} + +func NewTransparentBridge(cfg *config.Config) *TransparentBridge { + b := &TransparentBridge{ + bufPool: sync.Pool{New: func() interface{} { + buf := make([]byte, transparentBufSize) + return &buf + }}, + } + b.cfg.Store(cfg) + return b +} + +func (b *TransparentBridge) UpdateConfig(newCfg *config.Config) { + old := b.cfg.Swap(newCfg) + if old != nil && + old.System.MTProto.WSEndpointHost == newCfg.System.MTProto.WSEndpointHost && + old.System.MTProto.WSCustomDomain == newCfg.System.MTProto.WSCustomDomain && + old.Queue.Mark == newCfg.Queue.Mark { + return + } + b.mu.Lock() + oldPool := b.pool + b.pool = nil + b.poolInit = false + b.mu.Unlock() + if oldPool != nil { + oldPool.close() + } +} + +func (b *TransparentBridge) getPool() *wsPool { + b.mu.Lock() + defer b.mu.Unlock() + if !b.poolInit { + cfg := b.cfg.Load() + mt := cfg.System.MTProto + p := newWSPool(MTProtoUpstream{ + WSEndpointHost: mt.WSEndpointHost, + WSCustomDomain: mt.WSCustomDomain, + }, cfg.Queue.Mark, wsPoolDefaultSize) + p.warmup([]int{2, 4}) + b.pool = p + b.poolInit = true + } + return b.pool +} + +func (b *TransparentBridge) Handle(client net.Conn, origIP net.IP, origPort int) (bool, net.Conn) { + _ = client.SetReadDeadline(time.Now().Add(5 * time.Second)) + init := make([]byte, obfuscatedFrameLen) + head, herr := io.ReadFull(client, init[:4]) + if herr != nil { + _ = client.SetReadDeadline(time.Time{}) + if head == 0 { + return true, nil + } + return false, &prefixConn{Conn: client, prefix: append([]byte(nil), init[:head]...)} + } + if reservedFirst4(init[:4]) { + _ = client.SetReadDeadline(time.Time{}) + log.Debugf("MTProto transparent: non-obfuscated transport (% x) from %s -> fail open", init[:4], origIP) + return false, &prefixConn{Conn: client, prefix: append([]byte(nil), init[:4]...)} + } + n, rerr := io.ReadFull(client, init[4:]) + _ = client.SetReadDeadline(time.Time{}) + if rerr != nil { + return false, &prefixConn{Conn: client, prefix: append([]byte(nil), init[:4+n]...)} + } + + res, derr := decodeObfuscatedDirect(init, client) + if derr != nil { + log.Debugf("MTProto transparent: obfuscated decode failed from %s: %v -> fail open", origIP, derr) + return false, &prefixConn{Conn: client, prefix: append([]byte(nil), init...)} + } + + var dc int + var dcSrc string + if mapped, ok := dcForIP(origIP); ok { + dc, dcSrc = mapped, "ip" + } else if mapped, ok := dcForIPRange(origIP); ok { + dc, dcSrc = mapped, "ip-range" + } else if validTransparentDC(res.DC) { + dc, dcSrc = res.DC, "handshake" + } else { + log.Debugf("MTProto transparent: unresolved DC for %s (handshake dc=%d proto=0x%08x) -> fail open", origIP, res.DC, res.ProtoTag) + return false, &prefixConn{Conn: client, prefix: append([]byte(nil), init...)} + } + + cfg := b.cfg.Load() + mtCfg := cfg.System.MTProto + mtCfg.UpstreamMode = "auto" + mtCfg.DCRelay = "" + + dcConn, transport, err := DialObfuscatedDCWithPool(&mtCfg, cfg.Queue, dc, res.ProtoTag, b.getPool()) + if err != nil { + if shouldLogDialError(dc) { + log.Errorf("MTProto transparent dial DC %d: %v", dc, err) + } else { + log.Debugf("MTProto transparent dial DC %d (suppressed): %v", dc, err) + } + return true, nil + } + defer dcConn.Close() + + label := fmt.Sprintf("%s<->DC%d(transparent)", client.RemoteAddr(), dc) + log.Infof("MTProto transparent relay: %s -> DC%d (%s) [dc-from=%s]", origIP, dc, transport, dcSrc) + + var splitter *msgSplitter + if _, isWS := dcConn.Conn.(*wsConn); isWS { + splitter = newMsgSplitter(res.ProtoTag) + } + relayConns(res.Conn, dcConn, splitter, label, &b.bufPool) + return true, nil +} + +func validTransparentDC(dc int) bool { + a := dc + if a < 0 { + a = -a + } + return (a >= 1 && a <= 5) || a == 203 +} + +func reservedFirst4(b []byte) bool { + return isReservedFirst4(b) +} diff --git a/src/mtproto/transparent_test.go b/src/mtproto/transparent_test.go new file mode 100644 index 00000000..bbd2ceaf --- /dev/null +++ b/src/mtproto/transparent_test.go @@ -0,0 +1,189 @@ +package mtproto + +import ( + "bytes" + "io" + "net" + "testing" + "time" + + "github.com/daniellavrushin/b4/config" +) + +func TestReservedFirst4(t *testing.T) { + reserved := [][]byte{ + {0xef, 0x00, 0x00, 0x00}, + {0x48, 0x45, 0x41, 0x44}, // HEAD + {0x50, 0x4f, 0x53, 0x54}, // POST + {0x47, 0x45, 0x54, 0x20}, // "GET " + {0x4f, 0x50, 0x54, 0x49}, // OPTI + {0x16, 0x03, 0x01, 0x02}, // TLS record header + {0xdd, 0xdd, 0xdd, 0xdd}, + {0xee, 0xee, 0xee, 0xee}, + } + for _, b := range reserved { + if !reservedFirst4(b) { + t.Errorf("reservedFirst4(% x) = false, want true", b) + } + } + // a plausible random obfuscated prefix must not be flagged + normal := []byte{0x01, 0x02, 0x03, 0x04} + if reservedFirst4(normal) { + t.Errorf("reservedFirst4(% x) = true, want false", normal) + } +} + +func TestValidTransparentDC(t *testing.T) { + cases := []struct { + dc int + want bool + }{ + {1, true}, {2, true}, {5, true}, {203, true}, + {-1, true}, {-2, true}, {-5, true}, {-203, true}, + {0, false}, {6, false}, {99, false}, {-99, false}, + } + for _, c := range cases { + if got := validTransparentDC(c.dc); got != c.want { + t.Errorf("validTransparentDC(%d) = %v, want %v", c.dc, got, c.want) + } + } +} + +func TestPrefixConnReplaysBeforePassthrough(t *testing.T) { + prefix := []byte("PREFIX") + body := []byte("BODY") + pc := &prefixConn{Conn: fakeConn{r: bytes.NewReader(body)}, prefix: append([]byte(nil), prefix...)} + + got, err := io.ReadAll(pc) + if err != nil { + t.Fatalf("ReadAll: %v", err) + } + want := append(append([]byte(nil), prefix...), body...) + if !bytes.Equal(got, want) { + t.Errorf("prefixConn read = %q, want %q", got, want) + } +} + +func TestPrefixConnPartialReadDoesNotLosePrefix(t *testing.T) { + prefix := []byte("ABCDEF") + pc := &prefixConn{Conn: fakeConn{r: bytes.NewReader(nil)}, prefix: append([]byte(nil), prefix...)} + buf := make([]byte, 2) + var got []byte + for { + n, err := pc.Read(buf) + got = append(got, buf[:n]...) + if err != nil { + break + } + if len(got) >= len(prefix) { + break + } + } + if !bytes.Equal(got, prefix) { + t.Errorf("got %q, want %q", got, prefix) + } +} + +// fakeConn is a minimal net.Conn backed by a reader (and optional write sink). +type fakeConn struct { + r io.Reader + w io.Writer +} + +func (c fakeConn) Read(p []byte) (int, error) { + if c.r == nil { + return 0, io.EOF + } + return c.r.Read(p) +} +func (c fakeConn) Write(p []byte) (int, error) { + if c.w == nil { + return len(p), nil + } + return c.w.Write(p) +} +func (c fakeConn) Close() error { return nil } +func (c fakeConn) LocalAddr() net.Addr { return fakeAddr{} } +func (c fakeConn) RemoteAddr() net.Addr { return fakeAddr{} } +func (c fakeConn) SetDeadline(t time.Time) error { return nil } +func (c fakeConn) SetReadDeadline(t time.Time) error { return nil } +func (c fakeConn) SetWriteDeadline(t time.Time) error { return nil } + +type fakeAddr struct{} + +func (fakeAddr) Network() string { return "tcp" } +func (fakeAddr) String() string { return "1.2.3.4:5" } + +func newBridge() *TransparentBridge { + return NewTransparentBridge(&config.Config{}) +} + +func TestHandleEmptyConnReturnsHandledNil(t *testing.T) { + // immediate EOF (head==0) -> connection closed, nothing to fail open with + b := newBridge() + handled, failover := b.Handle(fakeConn{r: bytes.NewReader(nil)}, net.ParseIP("1.2.3.4"), 443) + if !handled || failover != nil { + t.Fatalf("empty conn: got (handled=%v, failover=%v), want (true, nil)", handled, failover) + } +} + +func TestHandleReservedPrefixFailsOpenWithBytes(t *testing.T) { + // non-obfuscated transport (TLS record header) -> fail open, replay the 4 bytes + b := newBridge() + in := []byte{0x16, 0x03, 0x01, 0x02, 0x00, 0xaa, 0xbb} + handled, failover := b.Handle(fakeConn{r: bytes.NewReader(in)}, net.ParseIP("1.2.3.4"), 443) + if handled { + t.Fatalf("reserved prefix should not be handled by bridge") + } + if failover == nil { + t.Fatal("reserved prefix: expected failover conn, got nil") + } + // failover is the original conn with the 4 read bytes re-prepended, so the + // whole original stream must be recoverable intact for the direct dial. + got, _ := io.ReadAll(failover) + if !bytes.Equal(got, in) { + t.Errorf("failover replayed % x, want full original stream % x", got, in) + } +} + +func TestHandlePartialReadFailsOpenWithBytes(t *testing.T) { + // fewer than 4 bytes then EOF -> fail open replaying what arrived + b := newBridge() + in := []byte{0x01, 0x02} + handled, failover := b.Handle(fakeConn{r: bytes.NewReader(in)}, net.ParseIP("1.2.3.4"), 443) + if handled { + t.Fatalf("partial read should fail open, not be handled") + } + if failover == nil { + t.Fatal("partial read: expected failover conn, got nil") + } + got, _ := io.ReadAll(failover) + if !bytes.Equal(got, in) { + t.Errorf("failover replayed % x, want % x", got, in) + } +} + +func TestHandleUnresolvedDCFailsOpenWithFullFrame(t *testing.T) { + // a full 64-byte obfuscated frame whose decoded DC is invalid and whose + // source IP maps to no DC -> fail open replaying all 64 bytes. + b := newBridge() + frame := make([]byte, obfuscatedFrameLen) + for i := range frame { + frame[i] = byte(i + 1) // non-reserved first 4 bytes (0x01..) + } + // ensure first4 is not accidentally reserved and byte0 != 0xef + if reservedFirst4(frame[:4]) { + t.Fatal("test frame unexpectedly reserved") + } + handled, failover := b.Handle(fakeConn{r: bytes.NewReader(frame)}, net.ParseIP("8.8.8.8"), 443) + if handled { + t.Fatalf("unresolved DC should fail open, not be handled") + } + if failover == nil { + t.Fatal("unresolved DC: expected failover conn, got nil") + } + got, _ := io.ReadAll(failover) + if len(got) != obfuscatedFrameLen || !bytes.Equal(got, frame) { + t.Errorf("failover replayed %d bytes, want full %d-byte frame intact", len(got), obfuscatedFrameLen) + } +} diff --git a/src/mtproto/wsdial.go b/src/mtproto/wsdial.go index 8ede71a5..95a60ca8 100644 --- a/src/mtproto/wsdial.go +++ b/src/mtproto/wsdial.go @@ -62,6 +62,7 @@ func (c *wsConn) Read(p []byte) (int, error) { c.rxBuf = c.rxBuf[n:] return n, nil } + var assembled []byte // accumulates fragmented data frames until FIN for { op, fin, payload, err := c.readFrame() if err != nil { @@ -69,14 +70,34 @@ func (c *wsConn) Read(p []byte) (int, error) { } switch op { case wsOpcodeBinary, 0x1: + // fragmented data frame: start accumulating; continuation frames will follow if !fin { - return 0, errors.New("ws: fragmented data frames not supported") + assembled = append(assembled, payload...) + continue } - n := copy(p, payload) - if n < len(payload) { - c.rxBuf = append(c.rxBuf, payload[n:]...) + full := payload + if len(assembled) > 0 { + full = append(assembled, payload...) + assembled = nil + } + n := copy(p, full) + if n < len(full) { + c.rxBuf = append(c.rxBuf, full[n:]...) } return n, nil + case 0x0: // continuation frame + if assembled == nil { + return 0, errors.New("ws: continuation frame without prior data frame") + } + assembled = append(assembled, payload...) + if fin { + n := copy(p, assembled) + if n < len(assembled) { + c.rxBuf = append(c.rxBuf, assembled[n:]...) + } + assembled = nil + return n, nil + } case wsOpcodePing: if err := c.writeFrame(wsOpcodePong, payload); err != nil { return 0, err @@ -109,6 +130,59 @@ func (c *wsConn) Close() error { return c.tls.Close() } +// alive does a non-destructive liveness check on the conn. Pool entries can +// sit idle long enough for TG to FIN/RST them; handing such a conn to a client +// causes up=N down=0 short-lived sessions, which break short RPCs (notably +// auth.importAuthorization on secondary DCs - the exact path that makes +// foreign-channel media downloads fail). Cheap (~few ms) since FIN/RST is +// already in the kernel buffer if it happened. +func (c *wsConn) alive() bool { + if c.closed.Load() { + return false + } + if err := c.tls.SetReadDeadline(time.Now().Add(5 * time.Millisecond)); err != nil { + return false + } + defer func() { _ = c.tls.SetReadDeadline(time.Time{}) }() + buf, err := c.br.Peek(1) + if err == nil && len(buf) >= 1 { + // any buffered byte indicates the conn is alive; reject if it's a CLOSE frame + if buf[0]&0x0F == wsOpcodeClose { + return false + } + return true + } + if ne, ok := err.(net.Error); ok && ne.Timeout() { + return true + } + return false +} + +// liveNow is a zero-wait liveness poll used right after the obfuscated handshake +// is written to a pooled conn, to catch conns Telegram FIN/RST'd between the +// pool's alive() check and the relay's first write (the up=N down=0 in ~1ms +// failure). Uses an already-expired read deadline so Peek returns immediately: +// a pending FIN/RST surfaces as a non-timeout error (dead), an idle-but-open +// conn surfaces as a timeout (alive). Peek does not consume, so buffered data is +// preserved for the relay. Cost is microseconds, unlike alive()'s 5ms wait. +func (c *wsConn) liveNow() bool { + if c.closed.Load() { + return false + } + if err := c.tls.SetReadDeadline(time.Now().Add(-time.Second)); err != nil { + return false + } + defer func() { _ = c.tls.SetReadDeadline(time.Time{}) }() + buf, err := c.br.Peek(1) + if err == nil && len(buf) >= 1 { + return buf[0]&0x0F != wsOpcodeClose + } + if ne, ok := err.(net.Error); ok && ne.Timeout() { + return true + } + return false +} + func (c *wsConn) LocalAddr() net.Addr { return c.tls.LocalAddr() } func (c *wsConn) RemoteAddr() net.Addr { return c.tls.RemoteAddr() } func (c *wsConn) SetDeadline(t time.Time) error { @@ -182,27 +256,25 @@ func (c *wsConn) writeFrame(op byte, payload []byte) error { if _, err := rand.Read(hdr[off : off+4]); err != nil { return err } - maskKey := hdr[off : off+4] off += 4 - masked := make([]byte, n) - for i := range payload { - masked[i] = payload[i] ^ maskKey[i%4] + // single buffer + single tls.Write to avoid emitting two TLS records per frame + buf := make([]byte, off+n) + copy(buf, hdr[:off]) + maskKey := buf[off-4 : off] + for i := 0; i < n; i++ { + buf[off+i] = payload[i] ^ maskKey[i%4] } c.wMu.Lock() defer c.wMu.Unlock() - if _, err := c.tls.Write(hdr[:off]); err != nil { - return err - } - if n > 0 { - if _, err := c.tls.Write(masked); err != nil { - return err - } - } - return nil + _, err := c.tls.Write(buf) + return err } -func dialWS(host, sni string, timeout time.Duration, mark uint) (net.Conn, error) { +func dialWS(host, sni, path string, timeout time.Duration, mark uint) (net.Conn, error) { + if path == "" { + path = "/apiws" + } dialer := &net.Dialer{Timeout: timeout} if mark > 0 { dialer.Control = func(network, address string, c syscall.RawConn) error { @@ -221,10 +293,19 @@ func dialWS(host, sni string, timeout time.Duration, mark uint) (net.Conn, error } if tc, ok := raw.(*net.TCPConn); ok { _ = tc.SetNoDelay(true) + // match tg-ws-proxy: 256KB send/recv buffers - kernel default (~87KB recv, + // ~16KB send) limits BDP for big media transfers from EU TG edge + _ = tc.SetReadBuffer(256 * 1024) + _ = tc.SetWriteBuffer(256 * 1024) } + // Telegram's WS edge only presents proper certs for kws2/kws4; kws1/kws3/kws5 + // fall back to a *.telegram.org cert that doesn't match the 3-label SNI. + // Cert verification adds no real security here - the MTProto payload is + // already end-to-end encrypted with the proxy secret. Match tg-ws-proxy. tlsConn := tls.Client(raw, &tls.Config{ - ServerName: sni, - MinVersion: tls.VersionTLS12, + ServerName: sni, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: true, }) _ = tlsConn.SetDeadline(time.Now().Add(timeout)) if err := tlsConn.Handshake(); err != nil { @@ -239,7 +320,7 @@ func dialWS(host, sni string, timeout time.Duration, mark uint) (net.Conn, error } wsKey := base64.StdEncoding.EncodeToString(keyBytes) - req := "GET /apiws HTTP/1.1\r\n" + + req := "GET " + path + " HTTP/1.1\r\n" + "Host: " + sni + "\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + diff --git a/src/mtproto/wspool.go b/src/mtproto/wspool.go new file mode 100644 index 00000000..c1820cc3 --- /dev/null +++ b/src/mtproto/wspool.go @@ -0,0 +1,397 @@ +package mtproto + +import ( + "context" + "net" + "strconv" + "sync" + "time" + + "github.com/daniellavrushin/b4/log" +) + +const ( + wsPoolMaxAge = 20 * time.Second + wsPoolDefaultSize = 4 + wsDCFailCooldown = 30 * time.Second + wsDialTimeoutCooldown = 2 * time.Second + tcpFailCooldown = 30 * time.Second +) + +type wsKey struct { + dc int + isMedia bool +} + +func (k wsKey) String() string { + s := strconv.Itoa(k.dc) + if k.isMedia { + s += "m" + } + return s +} + +var ( + wsStateMu sync.Mutex + wsBlacklist = map[wsKey]bool{} + wsCooldownTo = map[wsKey]time.Time{} + + tcpStateMu sync.Mutex + tcpCooldownTo = map[string]time.Time{} // keyed by host:port + + dialLogMu sync.Mutex + dialLogAt = map[int]time.Time{} // per-DC last full ERROR emit; throttles spam from known-broken DCs +) + +const dialLogInterval = 60 * time.Second + +// shouldLogDialError returns true if this is the first error for `dc` in the +// last dialLogInterval. Subsequent identical failures are silenced (caller can +// log at Debug instead) so a permanently-broken DC doesn't spam errors.log. +func shouldLogDialError(dc int) bool { + dialLogMu.Lock() + defer dialLogMu.Unlock() + now := time.Now() + if last, ok := dialLogAt[dc]; ok && now.Sub(last) < dialLogInterval { + return false + } + dialLogAt[dc] = now + return true +} + +// per-addr TCP cooldown: skip an upstream IP/port that just timed out so +// every retrying client doesn't burn another tcpDialTimeout against it. +func tcpAddrInCooldown(addr string) bool { + tcpStateMu.Lock() + defer tcpStateMu.Unlock() + t, ok := tcpCooldownTo[addr] + if !ok { + return false + } + if time.Now().After(t) { + delete(tcpCooldownTo, addr) + return false + } + return true +} + +func tcpRecordFailure(addr string) { + tcpStateMu.Lock() + defer tcpStateMu.Unlock() + tcpCooldownTo[addr] = time.Now().Add(tcpFailCooldown) +} + +func tcpRecordSuccess(addr string) { + tcpStateMu.Lock() + defer tcpStateMu.Unlock() + delete(tcpCooldownTo, addr) +} + +func tcpResetState() { + tcpStateMu.Lock() + defer tcpStateMu.Unlock() + tcpCooldownTo = map[string]time.Time{} +} + +func wsKeyFromDC(dc int) wsKey { + abs := dc + if abs < 0 { + abs = -abs + } + return wsKey{dc: abs, isMedia: dc < 0} +} + +func wsIsBlacklisted(dc int) bool { + k := wsKeyFromDC(dc) + wsStateMu.Lock() + defer wsStateMu.Unlock() + return wsBlacklist[k] +} + +func wsCooldownActive(dc int) bool { + k := wsKeyFromDC(dc) + wsStateMu.Lock() + defer wsStateMu.Unlock() + t, ok := wsCooldownTo[k] + if !ok { + return false + } + if time.Now().After(t) { + delete(wsCooldownTo, k) + return false + } + return true +} + +func wsRecordFailure(dc int, allRedirect bool) { + k := wsKeyFromDC(dc) + wsStateMu.Lock() + defer wsStateMu.Unlock() + if allRedirect { + wsBlacklist[k] = true + log.Warnf("MTProto WS %s blacklisted (all redirects)", k) + } + wsCooldownTo[k] = time.Now().Add(wsDCFailCooldown) +} + +func wsRecordSuccess(dc int) { + k := wsKeyFromDC(dc) + wsStateMu.Lock() + defer wsStateMu.Unlock() + delete(wsCooldownTo, k) + delete(wsBlacklist, k) +} + +func wsResetState() { + wsStateMu.Lock() + defer wsStateMu.Unlock() + wsBlacklist = map[wsKey]bool{} + wsCooldownTo = map[wsKey]time.Time{} +} + +type wsPoolEntry struct { + conn *wsConn + created time.Time +} + +type wsPool struct { + mu sync.Mutex + idle map[wsKey][]wsPoolEntry + refilling map[wsKey]bool + target int + maxAge time.Duration + + cfg *MTProtoUpstream + mark uint + ctx context.Context + cancel context.CancelFunc +} + +// MTProtoUpstream is the minimal upstream config the pool needs (subset of config.MTProtoConfig). +// Passed by value to detach pool from live config mutation. +type MTProtoUpstream struct { + WSEndpointHost string + WSCustomDomain string +} + +func newWSPool(cfg MTProtoUpstream, mark uint, target int) *wsPool { + if target <= 0 { + target = wsPoolDefaultSize + } + ctx, cancel := context.WithCancel(context.Background()) + return &wsPool{ + idle: map[wsKey][]wsPoolEntry{}, + refilling: map[wsKey]bool{}, + target: target, + maxAge: wsPoolMaxAge, + cfg: &cfg, + mark: mark, + ctx: ctx, + cancel: cancel, + } +} + +func (p *wsPool) close() { + p.cancel() + p.mu.Lock() + defer p.mu.Unlock() + for k, b := range p.idle { + for _, e := range b { + _ = e.conn.Close() + } + delete(p.idle, k) + } +} + +// get returns a pre-warmed *wsConn for the given signed DC (negative = media), +// or nil if the pool is empty. On hit and miss it schedules an async refill so +// the next caller can also hit. The returned conn has had no obfuscated init +// sent yet - caller must run completeObfuscation on it. +func (p *wsPool) get(dc int) *wsConn { + if p == nil { + return nil + } + k := wsKeyFromDC(dc) + if !wsEdgeServesDC(k.dc) || wsIsBlacklisted(dc) { + return nil + } + + p.mu.Lock() + bucket := p.idle[k] + now := time.Now() + var picked *wsConn + for len(bucket) > 0 { + e := bucket[0] + bucket = bucket[1:] + // stale check: TG may FIN/RST an idle conn server-side; handing such a conn + // to a client produces an up=N down=0 session (RPC sent, never answered). + // This is the path that breaks auth.importAuthorization on secondary DCs + // and makes foreign-channel media downloads hang. + if e.conn.closed.Load() || now.Sub(e.created) > p.maxAge || !e.conn.alive() { + go func(c *wsConn) { _ = c.Close() }(e.conn) + continue + } + picked = e.conn + break + } + p.idle[k] = bucket + p.mu.Unlock() + + p.scheduleRefill(dc) + return picked +} + +func (p *wsPool) scheduleRefill(dc int) { + if p == nil { + return + } + k := wsKeyFromDC(dc) + p.mu.Lock() + if p.refilling[k] { + p.mu.Unlock() + return + } + p.refilling[k] = true + p.mu.Unlock() + + go p.refill(dc) +} + +func (p *wsPool) refill(dc int) { + k := wsKeyFromDC(dc) + defer func() { + p.mu.Lock() + p.refilling[k] = false + p.mu.Unlock() + }() + + if p.ctx.Err() != nil { + return + } + if wsIsBlacklisted(dc) { + return + } + + p.mu.Lock() + need := p.target - len(p.idle[k]) + p.mu.Unlock() + if need <= 0 { + return + } + + // parallel dials so the pool fills in ~one RTT instead of need*RTT; + // individual failures don't abort siblings, matching tg-ws-proxy + type result struct { + conn *wsConn + err error + } + results := make(chan result, need) + for i := 0; i < need; i++ { + go func() { + if p.ctx.Err() != nil { + results <- result{} + return + } + c, err := p.dialFresh(dc) + results <- result{conn: c, err: err} + }() + } + added := 0 + for i := 0; i < need; i++ { + r := <-results + if r.err != nil || r.conn == nil { + if r.err != nil { + log.Tracef("MTProto WS pool refill %s slot failed: %v", k, r.err) + } + continue + } + if p.ctx.Err() != nil { + _ = r.conn.Close() + continue + } + p.mu.Lock() + p.idle[k] = append(p.idle[k], wsPoolEntry{conn: r.conn, created: time.Now()}) + p.mu.Unlock() + added++ + } + if added > 0 { + log.Debugf("MTProto WS pool %s refilled +%d (target=%d)", k, added, p.target) + } +} + +// dialFresh opens a raw WS connection (TLS + Upgrade) to a TG edge for `dc`. +// Tries both kwsN[-1] domains in the order matching media-vs-primary preference. +// Returns the first one to succeed, or the last error. +func (p *wsPool) dialFresh(dc int) (*wsConn, error) { + plans := wsPlansForDC(dc, p.cfg) + var lastErr error + for _, pl := range plans { + host := pl.dialHost + if host == "" { + host = pl.sni + } + conn, err := dialWS(host, pl.sni, pl.wsPath, wsDialTimeout, p.mark) + if err != nil { + lastErr = err + continue + } + if wsc, ok := conn.(*wsConn); ok { + return wsc, nil + } + _ = conn.Close() + } + if lastErr == nil { + lastErr = net.ErrClosed + } + return nil, lastErr +} + +func wsPlansForDC(dc int, cfg *MTProtoUpstream) []transportPlan { + absDC := dc + if absDC < 0 { + absDC = -absDC + } + var plans []transportPlan + edgeIP := "" + if cfg != nil { + edgeIP = cfg.WSEndpointHost + } + if edgeIP == "" { + edgeIP = telegramWSEdgeIP + } + if wsEdgeServesDC(absDC) { + primary := transportPlan{kind: transportWS, dc: dc, sni: kwsHost(absDC, ""), dialHost: edgeIP} + media := transportPlan{kind: transportWS, dc: dc, sni: kwsHost(absDC, "-1"), dialHost: edgeIP} + if dc < 0 { + plans = append(plans, media, primary) + } else { + plans = append(plans, primary, media) + } + } + if cfg != nil && cfg.WSCustomDomain != "" { + plans = append(plans, transportPlan{ + kind: transportWS, + dc: dc, + sni: kwsCustom(absDC, cfg.WSCustomDomain), + }) + } + return plans +} + +func kwsHost(dc int, suffix string) string { + return "kws" + strconv.Itoa(dc) + suffix + ".web.telegram.org" +} + +func kwsCustom(dc int, domain string) string { + return "kws" + strconv.Itoa(dc) + "." + domain +} + +func (p *wsPool) warmup(dcs []int) { + if p == nil { + return + } + for _, dc := range dcs { + p.scheduleRefill(dc) + p.scheduleRefill(-dc) + } +} diff --git a/src/nfq/handler.go b/src/nfq/handler.go index 479f2ac6..e23bcdff 100644 --- a/src/nfq/handler.go +++ b/src/nfq/handler.go @@ -432,7 +432,7 @@ func (w *Worker) handleTCPPacket(q *nfqueue.Nfqueue, id uint32, pkt *pktInfo, cf m.RecordPacket(uint64(len(pkt.raw))) } - if matched && set != nil && set.Routing.Enabled && set.Routing.Mode == config.RoutingModeProxy { + if matched && set != nil && set.Routing.Enabled && config.RoutingUsesTProxy(set.Routing.Mode) { return accept(q, id) } diff --git a/src/tables/routing.go b/src/tables/routing.go index d1b96cb3..0f23101d 100644 --- a/src/tables/routing.go +++ b/src/tables/routing.go @@ -125,7 +125,7 @@ func RoutingHandleDNS(cfg *config.Config, set *config.SetConfig, ips []net.IP) { if _, ok := routeRuleCache[set.Id]; !ok { var err error - if cur.mode == config.RoutingModeProxy { + if config.RoutingUsesTProxy(cur.mode) { err = routeEnsureProxyRule(be, cfg, set, cur, sources) } else { err = routeEnsureRule(be, cfg, set, cur, sources) @@ -135,9 +135,12 @@ func RoutingHandleDNS(cfg *config.Config, set *config.SetConfig, ips []net.IP) { return } routeRuleCache[set.Id] = cur - if cur.mode == config.RoutingModeProxy { + switch cur.mode { + case config.RoutingModeMTProtoWS: + log.Infof("Routing [%s]: enabled MTProto-WS set '%s' mark=0x%x port=%d", be.name(), set.Name, cur.mark, cur.tproxyPort) + case config.RoutingModeProxy: log.Infof("Routing [%s]: enabled proxy set '%s' -> %s:%d mark=0x%x port=%d", be.name(), set.Name, set.Routing.Upstream.Host, set.Routing.Upstream.Port, cur.mark, cur.tproxyPort) - } else { + default: log.Infof("Routing [%s]: enabled set '%s' -> iface=%s mark=0x%x table=%d", be.name(), set.Name, set.Routing.EgressInterface, cur.mark, cur.table) } } @@ -167,7 +170,7 @@ func buildRouteState(cfg *config.Config, set *config.SetConfig) routeState { chainPre: chainPre, chainOut: chainOut, chainSNAT: chainSNAT, } - if mode == config.RoutingModeProxy { + if config.RoutingUsesTProxy(mode) { mark, port := proxyMarkAndPort(set) st.mark = mark st.table = proxyTable() @@ -193,7 +196,7 @@ func routeStateEqual(a, b routeState) bool { } func routeCleanupAny(be routeBackend, st routeState) { - if st.mode == config.RoutingModeProxy { + if config.RoutingUsesTProxy(st.mode) { routeCleanupProxyRule(be, st) return } @@ -389,7 +392,7 @@ func RoutingSyncConfig(cfg *config.Config) { if _, ok := routeRuleCache[set.Id]; !ok { var err error - if cur.mode == config.RoutingModeProxy { + if config.RoutingUsesTProxy(cur.mode) { err = routeEnsureProxyRule(be, cfg, set, cur, sources) } else { err = routeEnsureRule(be, cfg, set, cur, sources) @@ -413,7 +416,7 @@ func RoutingSyncConfig(cfg *config.Config) { routeIfaceAuto = make(map[string]routeState) for _, st := range routeRuleCache { - if st.mode == config.RoutingModeProxy || st.iface == "" { + if config.RoutingUsesTProxy(st.mode) || st.iface == "" { continue } if _, ok := routeIfaceAuto[st.iface]; !ok { @@ -588,7 +591,7 @@ func routeAddMasqueradeRules(be routeBackend, iface, chain string, mark uint32, func interfaceShareCount(mark uint32, table int) int { n := 0 for _, st := range routeRuleCache { - if st.mode == config.RoutingModeProxy { + if config.RoutingUsesTProxy(st.mode) { continue } if st.mark == mark && st.table == table { @@ -676,7 +679,7 @@ func RoutingReinstallForInterface(cfg *config.Config, iface string) { ipv6 := cfg.Queue.IPv6Enabled count := 0 for _, st := range routeRuleCache { - if st.mode == config.RoutingModeProxy || st.iface != iface { + if config.RoutingUsesTProxy(st.mode) || st.iface != iface { continue } routeEnsurePolicyRouting(st.iface, st.mark, st.table, ipv4, ipv6) diff --git a/src/tables/routing_proxy.go b/src/tables/routing_proxy.go index b6f47553..32803c6c 100644 --- a/src/tables/routing_proxy.go +++ b/src/tables/routing_proxy.go @@ -89,7 +89,7 @@ func proxyTable() int { func proxyActiveCount() int { n := 0 for _, st := range routeRuleCache { - if st.mode == config.RoutingModeProxy { + if config.RoutingUsesTProxy(st.mode) { n++ } } diff --git a/src/tproxy/listener.go b/src/tproxy/listener.go index 78123eda..64039fc4 100644 --- a/src/tproxy/listener.go +++ b/src/tproxy/listener.go @@ -26,6 +26,10 @@ type DomainResolver interface { DomainFor(ip net.IP) string } +type MTProtoBridge interface { + Handle(client net.Conn, origIP net.IP, origPort int) (bool, net.Conn) +} + type Listener struct { SetID string SetName string @@ -36,6 +40,8 @@ type Listener struct { UseDomain bool FailOpen bool Resolver DomainResolver + MTProtoWS bool + Bridge MTProtoBridge ctx context.Context cancel context.CancelFunc @@ -170,6 +176,25 @@ func (l *Listener) handle(client net.Conn) { origIP := tcpAddr.IP origPort := tcpAddr.Port + if l.MTProtoWS { + src := "" + if r := client.RemoteAddr(); r != nil { + src = r.String() + } + log.LogConnectionStr("TCP", l.SetName, "", src, "", + net.JoinHostPort(origIP.String(), fmt.Sprintf("%d", origPort)), + "", "", "mtproto-ws") + if l.Bridge != nil { + if handled, failover := l.Bridge.Handle(client, origIP, origPort); handled { + return + } else if failover != nil { + client = failover + } + } + l.failOpenDirect(client, origIP, origPort) + return + } + domain := "" if l.Resolver != nil { domain = l.Resolver.DomainFor(origIP) @@ -213,20 +238,33 @@ func (l *Listener) handle(client net.Conn) { pipe(client, upstream) } +func (l *Listener) failOpenDirect(client net.Conn, origIP net.IP, origPort int) { + dialer := markedDialer(10*time.Second, l.Upstream.BypassMark) + ctx, cancel := context.WithTimeout(l.ctx, 10*time.Second) + direct, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(origIP.String(), fmt.Sprintf("%d", origPort))) + cancel() + if err != nil { + log.Tracef("tproxy: mtproto-ws fail-open direct dial failed for %s:%d: %v", origIP, origPort, err) + return + } + defer direct.Close() + pipe(client, direct) +} + func pipe(a, b net.Conn) { var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() _, _ = io.Copy(a, b) - if c, ok := a.(*net.TCPConn); ok { + if c, ok := a.(interface{ CloseWrite() error }); ok { _ = c.CloseWrite() } }() go func() { defer wg.Done() _, _ = io.Copy(b, a) - if c, ok := b.(*net.TCPConn); ok { + if c, ok := b.(interface{ CloseWrite() error }); ok { _ = c.CloseWrite() } }() diff --git a/src/tproxy/manager.go b/src/tproxy/manager.go index 4b6646d3..c0834d09 100644 --- a/src/tproxy/manager.go +++ b/src/tproxy/manager.go @@ -11,11 +11,21 @@ import ( ) type Manager struct { - mu sync.Mutex - listeners map[string]*Listener - resolver DomainResolver - ctx context.Context - cancel context.CancelFunc + mu sync.Mutex + listeners map[string]*Listener + resolver DomainResolver + mtprotoBridge MTProtoBridge + ctx context.Context + cancel context.CancelFunc +} + +func (m *Manager) SetMTProtoBridge(b MTProtoBridge) { + m.mu.Lock() + defer m.mu.Unlock() + m.mtprotoBridge = b + for _, l := range m.listeners { + l.Bridge = b + } } func NewManager(resolver DomainResolver) *Manager { @@ -52,7 +62,7 @@ func (m *Manager) SyncConfig(cfg *config.Config) { if set == nil || !set.Enabled || !set.Routing.Enabled { continue } - if set.Routing.Mode != config.RoutingModeProxy { + if !config.RoutingUsesTProxy(set.Routing.Mode) { continue } desired[set.Id] = set @@ -72,7 +82,9 @@ func (m *Manager) SyncConfig(cfg *config.Config) { if desiredHost == "" { desiredHost = "127.0.0.1" } + isMTWS := set.Routing.Mode == config.RoutingModeMTProtoWS if l.Port != port || + l.MTProtoWS != isMTWS || l.Upstream.Host != desiredHost || l.Upstream.Port != set.Routing.Upstream.Port || l.Upstream.Username != set.Routing.Upstream.Username || @@ -111,6 +123,8 @@ func (m *Manager) SyncConfig(cfg *config.Config) { UseDomain: set.Routing.Upstream.UseDomain, FailOpen: set.Routing.Upstream.FailOpen, Resolver: m.resolver, + MTProtoWS: set.Routing.Mode == config.RoutingModeMTProtoWS, + Bridge: m.mtprotoBridge, } if err := l.Start(m.ctx); err != nil { log.Errorf("tproxy: failed to start listener for set %q: %v", set.Name, err)