diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..60b788c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Necronicle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 55c94d1..837cfe3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ - USDT (ERC20): `0xA1D6d7d339f05C1560ecAF0c5CB8c4dc80Dc46A9` -**Важно:** после установки применяются autocircular стратегии. Им нужно время и несколько попыток, чтобы подстроиться под DPI. Если сайт не открывается сразу — дайте странице несколько раз перезагрузиться. Параметры перебираются автоматически, после чего сайт обычно начинает открываться. +**Важно:** после установки применяются autocircular стратегии. Им нужно время и несколько попыток, чтобы подстроиться под сетевую среду. Если сайт не открывается сразу — дайте странице несколько раз перезагрузиться. Параметры перебираются автоматически, после чего соединение обычно стабилизируется. + +> Данный проект предназначен для исследования сетевых протоколов и изучения работы систем анализа трафика. Используется исключительно в учебных целях. --- @@ -14,7 +16,7 @@ z2k — модульный установщик zapret2 для роутеров Keenetic с Entware. -Цель проекта: максимально упростить установку zapret2 на Keenetic и дать рабочий набор стратегий с автоподбором (autocircular) и поддержкой IPv6 там, где это возможно. +Цель проекта: упростить установку zapret2 на Keenetic и предоставить набор сетевых стратегий с автоподбором (autocircular) и поддержкой IPv6. --- @@ -22,14 +24,15 @@ z2k — модульный установщик zapret2 для роутеров - Установка zapret2 (openwrt-embedded релиз) без компиляции, с проверкой работоспособности `nfqws2` - Три TCP autocircular профиля с разными стратегиями: - - **RKN** — заблокированные сайты (TCP/TLS + HTTP) — 45 стратегий + - **RKN** — список ресурсов (TCP/TLS + HTTP) — 45 стратегий - **YouTube TCP** — youtube.com и связанные домены — 22 стратегии - **YouTube GV** — googlevideo CDN (стриминг) — 22 стратегии - QUIC autocircular профиль: YouTube QUIC (UDP/443) — 12 стратегий с z2k morph - Discord профили: - TCP: hostlist Discord включён в RKN-профиль - UDP voice/video: `circular_locked` (стратегия закрепляется per-domain) -- Hostlist режим: стратегии применяются только к доменам из списков (не "на весь интернет") +- Telegram: прозрачное проксирование через Cloudflare WebSocket (тестовая функция) +- Hostlist режим: стратегии применяются только к доменам из списков - Whitelist: домены-исключения (госуслуги, Steam, VK, Яндекс и др.) не обрабатываются - IPv6: автоопределение и включение правил если поддерживается - Списки доменов устанавливаются автоматически @@ -84,8 +87,9 @@ curl -fsSL https://raw.githubusercontent.com/necronicle/z2k/master/z2k.sh | sh | **[5]** | Удалить zapret2 | | **[A]** | Режим без хостлистов (Austerus) — обработка всего TLS-трафика | | **[W]** | Whitelist — управление списком исключений | -| **[R]** | RST-фильтр — блокировка поддельных TCP RST от ТСПУ | -| **[F]** | Silent fallback для РКН — ускоренная ротация при тихих блокировках | +| **[R]** | RST-фильтр — фильтрация аномальных TCP RST | +| **[F]** | Silent fallback — ускоренная ротация при отсутствии ответа | +| **[T]** | Telegram прокси — прозрачное проксирование через WebSocket | | **[S]** | Скрипты custom.d | --- @@ -96,9 +100,9 @@ curl -fsSL https://raw.githubusercontent.com/necronicle/z2k/master/z2k.sh | sh ### Детекция неудач -- **Стандартный детектор** — TCP ретрансмиссии (сервер не отвечает) и RST от DPI -- **TLS alert детектор** (`z2k_tls_alert_fatal`) — ловит TLS fatal alert от ТСПУ. Включён по умолчанию для РКН -- **Silent fallback** — детектор тихих чёрных дыр: если несколько ClientHello подряд без ответа, принудительно ротирует стратегию. Включается через меню [F] +- **Стандартный детектор** — TCP ретрансмиссии и аномальные RST +- **TLS alert детектор** (`z2k_tls_alert_fatal`) — анализирует TLS alert. Включён по умолчанию для РКН +- **Silent fallback** — детектор отсутствия ответа: если несколько запросов подряд без ответа, принудительно ротирует стратегию. Включается через меню [F] ### Персистентность @@ -106,6 +110,14 @@ curl -fsSL https://raw.githubusercontent.com/necronicle/z2k/master/z2k.sh | sh --- +## Telegram прокси + +Прозрачное проксирование Telegram через Cloudflare WebSocket. Не требует настройки на устройствах — работает автоматически для всех устройств в сети. + +Включается через меню `[T]`. + +--- + ## Управление сервисом ```bash @@ -119,14 +131,14 @@ curl -fsSL https://raw.githubusercontent.com/necronicle/z2k/master/z2k.sh | sh ## Полная зачистка (z2k_cleanup) -Если zapret или zapret2 были удалены некорректно, остались зависшие процессы `nfqws`/`nfqws2`, мусорные iptables правила или директории — используйте скрипт полной зачистки: +Если zapret или zapret2 были удалены некорректно, остались зависшие процессы или мусорные правила — используйте скрипт полной зачистки: ```bash curl -fsSL https://raw.githubusercontent.com/necronicle/z2k/master/z2k_cleanup.sh | sh ``` **ВНИМАНИЕ:** Скрипт удаляет ВСЁ связанное с zapret и zapret2: -- Убивает все процессы `nfqws` и `nfqws2` (kill -9) +- Останавливает все процессы `nfqws` и `nfqws2` - Удаляет init-скрипты, netfilter хуки, iptables цепочки - **Полностью удаляет директории `/opt/zapret` и `/opt/zapret2`** (включая конфиги, списки, стратегии) - Очищает ipset и временные файлы @@ -157,7 +169,7 @@ curl -fsSL https://raw.githubusercontent.com/necronicle/z2k/master/z2k_cleanup.s - Если вы используете IPv6 в сети, убедитесь что он включён в прошивке (см. требования выше). - Если в системе нет `cron`, автообновление списков может быть недоступно — обновляйте списки вручную. -- Если многие РКН-сайты не открываются — попробуйте включить Silent fallback через меню [F]. +- Если многие сайты не открываются — попробуйте включить Silent fallback через меню [F]. --- diff --git a/lib/install.sh b/lib/install.sh index 01890d3..18c58ec 100644 --- a/lib/install.sh +++ b/lib/install.sh @@ -523,8 +523,32 @@ step_load_kernel_modules() { step_build_zapret2() { print_header "Шаг 5/12: Установка zapret2" - # Удалить старую установку если существует + # Сохранить пользовательские данные перед удалением + local backup_tmp="/tmp/z2k_upgrade_backup" + rm -rf "$backup_tmp" if [ -d "$ZAPRET2_DIR" ]; then + print_info "Сохранение пользовательских настроек..." + mkdir -p "$backup_tmp" + # Config (содержит DROP_DPI_RST, RKN_SILENT_FALLBACK и др.) + [ -f "$ZAPRET2_DIR/config" ] && cp -f "$ZAPRET2_DIR/config" "$backup_tmp/config" + # Whitelist (пользовательские исключения) + [ -f "$ZAPRET2_DIR/lists/whitelist.txt" ] && cp -f "$ZAPRET2_DIR/lists/whitelist.txt" "$backup_tmp/whitelist.txt" + # Autocircular state (найденные рабочие стратегии) + [ -f "$ZAPRET2_DIR/extra_strats/cache/autocircular/state.tsv" ] && \ + cp -f "$ZAPRET2_DIR/extra_strats/cache/autocircular/state.tsv" "$backup_tmp/state.tsv" + # Strategy.txt файлы + for cat_dir in TCP/YT TCP/YT_GV TCP/RKN UDP/YT; do + local sfile="$ZAPRET2_DIR/extra_strats/$cat_dir/Strategy.txt" + if [ -f "$sfile" ]; then + mkdir -p "$backup_tmp/strats/$cat_dir" + cp -f "$sfile" "$backup_tmp/strats/$cat_dir/Strategy.txt" + fi + done + # Silent fallback flag + [ -f "$ZAPRET2_DIR/extra_strats/cache/autocircular/rkn_silent_fallback.flag" ] && \ + touch "$backup_tmp/rkn_silent_fallback.flag" + print_success "Настройки сохранены" + print_info "Удаление старой установки..." rm -rf "$ZAPRET2_DIR" print_success "Старая установка удалена" @@ -817,6 +841,51 @@ step_build_zapret2() { print_warning "Не удалось загрузить orchestrator.sh (circular_locked будет без ротации)" fi + # =========================================================================== + # Восстановление пользовательских данных после переустановки + # =========================================================================== + if [ -d "$backup_tmp" ]; then + print_info "Восстановление пользовательских настроек..." + + # Восстановить config (содержит DROP_DPI_RST, RKN_SILENT_FALLBACK) + if [ -f "$backup_tmp/config" ]; then + cp -f "$backup_tmp/config" "$ZAPRET2_DIR/config" + print_success "Конфигурация восстановлена" + fi + + # Восстановить whitelist + if [ -f "$backup_tmp/whitelist.txt" ]; then + mkdir -p "$ZAPRET2_DIR/lists" + cp -f "$backup_tmp/whitelist.txt" "$ZAPRET2_DIR/lists/whitelist.txt" + print_success "Whitelist восстановлен" + fi + + # Восстановить autocircular state (рабочие стратегии) + if [ -f "$backup_tmp/state.tsv" ]; then + cp -f "$backup_tmp/state.tsv" "$ZAPRET2_DIR/extra_strats/cache/autocircular/state.tsv" + chown nobody "$ZAPRET2_DIR/extra_strats/cache/autocircular/state.tsv" 2>/dev/null || true + print_success "Стратегии autocircular восстановлены" + fi + + # Восстановить Strategy.txt файлы + for cat_dir in TCP/YT TCP/YT_GV TCP/RKN UDP/YT; do + if [ -f "$backup_tmp/strats/$cat_dir/Strategy.txt" ]; then + mkdir -p "$ZAPRET2_DIR/extra_strats/$cat_dir" + cp -f "$backup_tmp/strats/$cat_dir/Strategy.txt" "$ZAPRET2_DIR/extra_strats/$cat_dir/Strategy.txt" + fi + done + print_success "Стратегии категорий восстановлены" + + # Восстановить silent fallback flag + if [ -f "$backup_tmp/rkn_silent_fallback.flag" ]; then + touch "$ZAPRET2_DIR/extra_strats/cache/autocircular/rkn_silent_fallback.flag" + chown nobody "$ZAPRET2_DIR/extra_strats/cache/autocircular/rkn_silent_fallback.flag" 2>/dev/null || true + fi + + rm -rf "$backup_tmp" + print_success "Все пользовательские настройки восстановлены" + fi + # =========================================================================== # ШАГ 4.7: Установить custom.d скрипты (STUN + Discord media backup) # =========================================================================== @@ -1494,6 +1563,67 @@ step_finalize() { print_info "Установите: opkg install cron" fi + # Instagram DNS redirect (Keenetic static DNS) + # Прописывает рабочие IP для Instagram если записи ещё не заданы. + # Решает проблему DNS-отравления провайдером. + if command -v ndmc >/dev/null 2>&1; then + if ! ndmc -c "show running-config" 2>/dev/null | grep -q "ip host instagram.com"; then + print_info "Настройка DNS для Instagram..." + ndmc -c "ip host instagram.com 157.240.251.174" 2>/dev/null + ndmc -c "ip host www.instagram.com 157.240.9.174" 2>/dev/null + ndmc -c "ip host graph.instagram.com 157.240.0.63" 2>/dev/null + ndmc -c "ip host api.instagram.com 157.240.253.63" 2>/dev/null + ndmc -c "ip host instagram.c10r.instagram.com 157.240.214.63" 2>/dev/null + ndmc -c "ip host static.cdninstagram.com 163.70.147.63" 2>/dev/null + ndmc -c "ip host scontent.cdninstagram.com 163.70.147.63" 2>/dev/null + ndmc -c "ip host instagram.com 157.240.9.174" 2>/dev/null + ndmc -c "ip host static.cdninstagram.com 57.144.112.192" 2>/dev/null + ndmc -c "ip host scontent.cdninstagram.com 57.144.112.192" 2>/dev/null + ndmc -c "system configuration save" 2>/dev/null + print_success "DNS записи для Instagram добавлены" + else + print_info "DNS записи для Instagram уже настроены" + fi + fi + + # Telegram transparent proxy (tg-mtproxy-client) + if true; then + print_info "Установка/обновление Telegram прокси..." + local tg_arch="" + local hw_arch=$(uname -m) + case "$hw_arch" in + aarch64|arm64) tg_arch="arm64" ;; + armv7*|armv6*) tg_arch="arm" ;; + mipsel|mipsle) tg_arch="mipsel" ;; + mips) tg_arch="mips" ;; + mips64el|mips64le) tg_arch="mips64el" ;; + x86_64|amd64) tg_arch="amd64" ;; + i?86) tg_arch="x86" ;; + riscv64) tg_arch="riscv64" ;; + ppc64*) tg_arch="ppc64" ;; + esac + if [ -n "$tg_arch" ]; then + local tg_url="https://github.com/necronicle/z2k/releases/download/tg-mtproxy-v1.0/tg-mtproxy-client-linux-${tg_arch}" + if curl -fsSL "$tg_url" -o /opt/sbin/tg-mtproxy-client; then + chmod +x /opt/sbin/tg-mtproxy-client + print_success "Telegram прокси установлен ($tg_arch)" + else + print_warning "Не удалось скачать Telegram прокси для $tg_arch" + fi + else + print_warning "Неизвестная архитектура $hw_arch, пропускаем Telegram прокси" + fi + else + print_info "Telegram прокси уже установлен" + fi + + # Init script — always update + local tg_init_url="${GITHUB_RAW:-https://raw.githubusercontent.com/necronicle/z2k/master}/mtproxy-client/S97tg-mtproxy" + if curl -fsSL "$tg_init_url" -o /opt/etc/init.d/S97tg-mtproxy; then + chmod +x /opt/etc/init.d/S97tg-mtproxy + print_success "Init скрипт Telegram прокси установлен" + fi + # Показать итоговую информацию print_separator print_success "Установка zapret2 завершена!" diff --git a/lib/menu.sh b/lib/menu.sh index 325512a..21e8f06 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -80,12 +80,13 @@ MENU [W] Whitelist (исключения) [R] RST-фильтр (пассивный DPI) [F] Silent fallback для РКН (осторожно, возможны поломки) +[T] Telegram MTProxy (тестовая функция) [S] Скрипты custom.d [0] Выход MENU - printf "Выберите опцию [0-5,A,R,F,W,S]: " + printf "Выберите опцию [0-5,A,R,F,T,W,S]: " read_input choice case "$choice" in @@ -113,6 +114,9 @@ MENU f|F) menu_rkn_silent_fallback ;; + t|T) + menu_telegram_mtproxy + ;; w|W) menu_whitelist ;; @@ -903,6 +907,126 @@ SUBMENU esac } +# ============================================================================== +# ПОДМЕНЮ: TELEGRAM MTPROXY +# ============================================================================== + +menu_telegram_mtproxy() { + local MTPROXY_BIN="/opt/sbin/tg-mtproxy-client" + local MTPROXY_PID="/var/run/tg-mtproxy-client.pid" + local MTPROXY_PORT="9443" + + while true; do + clear_screen + print_header "Telegram прокси (тестовая функция)" + + # Check status + local running=false + if [ -f "$MTPROXY_PID" ] && kill -0 "$(cat $MTPROXY_PID 2>/dev/null)" 2>/dev/null; then + running=true + elif pgrep -f tg-mtproxy-client >/dev/null 2>&1; then + running=true + fi + + print_separator + if $running; then + printf " Статус: Включен\n" + printf " Telegram работает автоматически на всех устройствах\n" + else + printf " Статус: Выключен\n" + fi + print_separator + + cat <<'SUBMENU' + +Обход блокировки Telegram через Cloudflare WebSocket. +Провайдер видит только HTTPS к Cloudflare CDN. +Настройка устройств не требуется — работает прозрачно. + +[1] Включить +[2] Выключить +[B] Назад + +SUBMENU + + printf "Выберите опцию [1-2,B]: " + read_input sub_choice + + case "$sub_choice" in + 1) + if ! [ -f "$MTPROXY_BIN" ]; then + print_warning "Бинарник не найден, скачиваю..." + local tg_arch="" + case "$(uname -m)" in + aarch64|arm64) tg_arch="arm64" ;; + armv7*|armv6*) tg_arch="arm" ;; + mipsel|mipsle) tg_arch="mipsel" ;; + mips) tg_arch="mips" ;; + x86_64|amd64) tg_arch="amd64" ;; + i?86) tg_arch="x86" ;; + *) tg_arch="" ;; + esac + if [ -n "$tg_arch" ]; then + if curl -fsSL "https://github.com/necronicle/z2k/releases/download/tg-mtproxy-v1.0/tg-mtproxy-client-linux-${tg_arch}" \ + -o "$MTPROXY_BIN"; then + chmod +x "$MTPROXY_BIN" + print_success "Скачан для $tg_arch" + else + print_error "Не удалось скачать для $tg_arch" + pause + continue + fi + else + print_error "Неизвестная архитектура: $(uname -m)" + pause + continue + fi + fi + + # Use init script for proper restart loop + if [ -f "/opt/etc/init.d/S97tg-mtproxy" ]; then + /opt/etc/init.d/S97tg-mtproxy restart + else + # Fallback: download init script + curl -fsSL "https://raw.githubusercontent.com/necronicle/z2k/master/mtproxy-client/S97tg-mtproxy" \ + -o /opt/etc/init.d/S97tg-mtproxy 2>/dev/null + chmod +x /opt/etc/init.d/S97tg-mtproxy 2>/dev/null + /opt/etc/init.d/S97tg-mtproxy start + fi + sleep 3 + + if pgrep -f tg-mtproxy-client >/dev/null 2>&1; then + print_success "Telegram прокси включен" + print_info "Все устройства — Telegram работает автоматически" + else + print_error "Не удалось запустить" + tail -5 /tmp/tg-mtproxy.log 2>/dev/null + fi + pause + ;; + + 2) + killall tg-mtproxy-client 2>/dev/null + rm -f "$MTPROXY_PID" + iptables -t nat -D PREROUTING -j TG_TRANSPARENT 2>/dev/null + iptables -t nat -F TG_TRANSPARENT 2>/dev/null + iptables -t nat -X TG_TRANSPARENT 2>/dev/null + print_success "Telegram прокси выключен" + pause + ;; + + [Bb]) + return + ;; + + *) + print_error "Неверный выбор" + pause + ;; + esac + done +} + # ============================================================================== # ПОДМЕНЮ: WHITELIST (ИСКЛЮЧЕНИЯ) # ============================================================================== diff --git a/mtproxy-client/S97tg-mtproxy b/mtproxy-client/S97tg-mtproxy new file mode 100644 index 0000000..55ecabf --- /dev/null +++ b/mtproxy-client/S97tg-mtproxy @@ -0,0 +1,71 @@ +#!/bin/sh +# Telegram transparent proxy init script with auto-restart + +BINARY="/opt/sbin/tg-mtproxy-client" +PIDFILE="/var/run/tg-mtproxy-client.pid" +PORT="9443" +LOGFILE="/tmp/tg-mtproxy.log" + +start() { + if [ -f "$PIDFILE" ] && kill -0 "$(cat $PIDFILE 2>/dev/null)" 2>/dev/null; then + echo "tg-mtproxy already running (PID $(cat $PIDFILE))" + return 0 + fi + + [ ! -f "$BINARY" ] && { echo "Binary not found: $BINARY"; return 1; } + + # Setup iptables + iptables -t nat -N TG_TRANSPARENT 2>/dev/null || iptables -t nat -F TG_TRANSPARENT + iptables -t nat -D PREROUTING -j TG_TRANSPARENT 2>/dev/null + for cidr in 149.154.160.0/20 91.108.4.0/22 91.108.8.0/22 91.108.12.0/22 \ + 91.108.16.0/22 91.108.20.0/22 91.108.56.0/22 95.161.64.0/20 \ + 91.105.192.0/23 185.76.151.0/24; do + iptables -t nat -A TG_TRANSPARENT -d "$cidr" -p tcp -j REDIRECT --to-ports $PORT + done + iptables -t nat -I PREROUTING -j TG_TRANSPARENT + + # Start with auto-restart loop + _run_loop & + echo $! > "$PIDFILE" + echo "tg-mtproxy started (PID $!)" +} + +_run_loop() { + while true; do + "$BINARY" --transparent --listen ":$PORT" >> "$LOGFILE" 2>&1 + EXIT_CODE=$? + echo "$(date '+%Y/%m/%d %H:%M:%S') [crash] process exited with code $EXIT_CODE, restarting..." >> "$LOGFILE" + iptables -t nat -D PREROUTING -j TG_TRANSPARENT 2>/dev/null + iptables -t nat -F TG_TRANSPARENT 2>/dev/null + sleep 1 + # Re-add rules + for cidr in 149.154.160.0/20 91.108.4.0/22 91.108.8.0/22 91.108.12.0/22 \ + 91.108.16.0/22 91.108.20.0/22 91.108.56.0/22 95.161.64.0/20 \ + 91.105.192.0/23 185.76.151.0/24; do + iptables -t nat -A TG_TRANSPARENT -d "$cidr" -p tcp -j REDIRECT --to-ports $PORT + done + iptables -t nat -I PREROUTING -j TG_TRANSPARENT + sleep 1 + done +} + +stop() { + if [ -f "$PIDFILE" ]; then + kill "$(cat $PIDFILE)" 2>/dev/null + fi + killall tg-mtproxy-client 2>/dev/null + rm -f "$PIDFILE" + iptables -t nat -D PREROUTING -j TG_TRANSPARENT 2>/dev/null + iptables -t nat -F TG_TRANSPARENT 2>/dev/null + iptables -t nat -X TG_TRANSPARENT 2>/dev/null + # Flush conntrack to force clients to reconnect immediately + conntrack -F 2>/dev/null + echo "tg-mtproxy stopped" +} + +case "$1" in + start) start ;; + stop) stop ;; + restart) stop; sleep 1; start ;; + *) echo "Usage: $0 {start|stop|restart}" ;; +esac diff --git a/mtproxy-client/faketls.go b/mtproxy-client/faketls.go deleted file mode 100644 index 4644b87..0000000 --- a/mtproxy-client/faketls.go +++ /dev/null @@ -1,242 +0,0 @@ -package main - -import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "encoding/binary" - "fmt" - "io" - "math/rand" - "net" - "time" -) - -const clientHelloSize = 517 - -// buildClientHello constructs a FakeTLS ClientHello with the proxy secret's SNI. -func buildClientHello(secret *ProxySecret) ([]byte, error) { - sni := secret.SNI - if sni == "" { - sni = "s3.amazonaws.com" - } - - // Base TLS 1.2 ClientHello structure. - // Offsets are carefully aligned to match what MTProxy servers expect. - hello := make([]byte, clientHelloSize) - - // TLS record header - hello[0] = tlsRecordHandshake // content type - binary.BigEndian.PutUint16(hello[1:3], 0x0301) // TLS 1.0 for ClientHello record - binary.BigEndian.PutUint16(hello[3:5], uint16(clientHelloSize-5)) // record length - - // Handshake header - hello[5] = 0x01 // ClientHello - hello[6] = 0x00 - binary.BigEndian.PutUint16(hello[7:9], uint16(clientHelloSize-9)) // handshake length - - // Client version: TLS 1.2 - binary.BigEndian.PutUint16(hello[9:11], tlsVersion12) - - // Random (32 bytes at offset 11) - will be filled with HMAC later - // For now, fill with random - randomOffset := 11 - rand.Read(hello[randomOffset : randomOffset+32]) - - // Session ID (32 bytes) - sessionIDOffset := 43 - hello[sessionIDOffset] = 32 // session ID length - rand.Read(hello[sessionIDOffset+1 : sessionIDOffset+33]) - - // Cipher suites - csOffset := sessionIDOffset + 33 - cipherSuites := []uint16{ - 0xcca9, 0xcca8, 0xc02c, 0xc02b, 0xc030, 0xc02f, - 0x009f, 0x009e, 0xccaa, 0xc0a3, 0xc09f, 0xc0ad, - 0xc0a7, 0x009d, 0xc0a2, 0xc09e, 0xc0ac, 0xc0a6, - 0x00ff, - } - binary.BigEndian.PutUint16(hello[csOffset:csOffset+2], uint16(len(cipherSuites)*2)) - for i, cs := range cipherSuites { - binary.BigEndian.PutUint16(hello[csOffset+2+i*2:csOffset+4+i*2], cs) - } - - // Compression methods - compOffset := csOffset + 2 + len(cipherSuites)*2 - hello[compOffset] = 1 // length - hello[compOffset+1] = 0 // null compression - - // Extensions - extStart := compOffset + 2 - extBuf := new(bytes.Buffer) - - // SNI extension - sniBytes := []byte(sni) - extBuf.Write([]byte{0x00, 0x00}) // extension type: SNI - sniLen := len(sniBytes) + 5 - binary.Write(extBuf, binary.BigEndian, uint16(sniLen)) - binary.Write(extBuf, binary.BigEndian, uint16(sniLen-2)) - extBuf.WriteByte(0x00) // host name type - binary.Write(extBuf, binary.BigEndian, uint16(len(sniBytes))) - extBuf.Write(sniBytes) - - // Supported versions extension (TLS 1.3, 1.2) - extBuf.Write([]byte{0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x03}) - - // Signature algorithms - extBuf.Write([]byte{0x00, 0x0d, 0x00, 0x10, 0x00, 0x0e, - 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x02, 0x03, - 0x08, 0x04, 0x08, 0x05, 0x08, 0x06}) - - // Key share (empty x25519 placeholder) - extBuf.Write([]byte{0x00, 0x33, 0x00, 0x02, 0x00, 0x00}) - - // Supported groups - extBuf.Write([]byte{0x00, 0x0a, 0x00, 0x04, 0x00, 0x02, 0x00, 0x1d}) - - // EC point formats - extBuf.Write([]byte{0x00, 0x0b, 0x00, 0x02, 0x01, 0x00}) - - // Padding to fill exactly clientHelloSize - extData := extBuf.Bytes() - paddingNeeded := clientHelloSize - extStart - 2 - len(extData) - if paddingNeeded > 4 { - // Padding extension - padExt := make([]byte, 4+paddingNeeded-4) - padExt[0] = 0x00 - padExt[1] = 0x15 // padding extension type - binary.BigEndian.PutUint16(padExt[2:4], uint16(paddingNeeded-4)) - extData = append(extData, padExt...) - } - - binary.BigEndian.PutUint16(hello[extStart:extStart+2], uint16(len(extData))) - copy(hello[extStart+2:], extData) - - // Now compute HMAC for the random field. - // Zero out random field, compute HMAC(secret, hello), put result in random field. - saved := make([]byte, 32) - copy(saved, hello[randomOffset:randomOffset+32]) - - // Zero the random field - for i := 0; i < 32; i++ { - hello[randomOffset+i] = 0 - } - - mac := hmac.New(sha256.New, secret.Secret) - mac.Write(hello) - digest := mac.Sum(nil) - - copy(hello[randomOffset:randomOffset+32], digest) - - // XOR last 4 bytes of random with current timestamp - ts := uint32(time.Now().Unix()) - tsBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(tsBytes, ts) - for i := 0; i < 4; i++ { - hello[randomOffset+28+i] ^= tsBytes[i] - } - - return hello, nil -} - -// doFakeTLSHandshake performs the FakeTLS handshake with the MTProxy server. -// Returns a net.Conn that wraps the connection with TLS record framing. -func doFakeTLSHandshake(conn net.Conn, secret *ProxySecret) (clientRandom []byte, err error) { - // Build and send ClientHello - hello, err := buildClientHello(secret) - if err != nil { - return nil, fmt.Errorf("build ClientHello: %w", err) - } - - clientRandom = make([]byte, 32) - copy(clientRandom, hello[11:43]) - - if _, err := conn.Write(hello); err != nil { - return nil, fmt.Errorf("send ClientHello: %w", err) - } - - // Read server response: multiple Handshake records, then ChangeCipherSpec, - // then optionally Application Data. Some MTProxy servers send more TLS - // handshake records than the minimum (ServerHello, Certificate, etc.). - for i := 0; i < 10; i++ { - recType, _, err := readTLSRecord(conn) - if err != nil { - return nil, fmt.Errorf("read server record %d: %w", i, err) - } - if recType == tlsRecordChangeCipherSpec { - // Read one more Application Data record after CCS - recType, _, err = readTLSRecord(conn) - if err != nil { - return nil, fmt.Errorf("read post-CCS record: %w", err) - } - _ = recType // may be Application or another type - break - } - if recType != tlsRecordHandshake && recType != tlsRecordApplication { - return nil, fmt.Errorf("unexpected record type 0x%02x at position %d", recType, i) - } - } - - return clientRandom, nil -} - -// tlsRecordConn wraps a connection to read/write TLS Application Data records. -type tlsRecordConn struct { - conn net.Conn - readBuf []byte // buffered decrypted data from last record -} - -func newTLSRecordConn(conn net.Conn) *tlsRecordConn { - return &tlsRecordConn{conn: conn} -} - -func (c *tlsRecordConn) Read(p []byte) (int, error) { - if len(c.readBuf) > 0 { - n := copy(p, c.readBuf) - c.readBuf = c.readBuf[n:] - return n, nil - } - - recType, payload, err := readTLSRecord(c.conn) - if err != nil { - return 0, err - } - if recType != tlsRecordApplication { - return 0, fmt.Errorf("unexpected TLS record type: 0x%02x", recType) - } - - n := copy(p, payload) - if n < len(payload) { - c.readBuf = payload[n:] - } - return n, nil -} - -func (c *tlsRecordConn) Write(p []byte) (int, error) { - // Split into max-size TLS records - total := 0 - for len(p) > 0 { - chunk := p - if len(chunk) > 16384 { - chunk = chunk[:16384] - } - if err := writeTLSRecord(c.conn, tlsRecordApplication, chunk); err != nil { - return total, err - } - total += len(chunk) - p = p[len(chunk):] - } - return total, nil -} - -func (c *tlsRecordConn) Close() error { - return c.conn.Close() -} - -func (c *tlsRecordConn) LocalAddr() net.Addr { return c.conn.LocalAddr() } -func (c *tlsRecordConn) RemoteAddr() net.Addr { return c.conn.RemoteAddr() } -func (c *tlsRecordConn) SetDeadline(t time.Time) error { return c.conn.SetDeadline(t) } -func (c *tlsRecordConn) SetReadDeadline(t time.Time) error { return c.conn.SetReadDeadline(t) } -func (c *tlsRecordConn) SetWriteDeadline(t time.Time) error { return c.conn.SetWriteDeadline(t) } - -var _ io.ReadWriteCloser = (*tlsRecordConn)(nil) diff --git a/mtproxy-client/go.mod b/mtproxy-client/go.mod index be81069..a452ead 100644 --- a/mtproxy-client/go.mod +++ b/mtproxy-client/go.mod @@ -1,3 +1,20 @@ module github.com/necronicle/z2k/mtproxy-client go 1.26.1 + +require ( + github.com/gorilla/websocket v1.5.3 + github.com/gotd/td v0.143.0 +) + +require ( + github.com/go-faster/errors v0.7.1 // indirect + github.com/go-faster/jx v1.2.0 // indirect + github.com/go-faster/xor v1.0.0 // indirect + github.com/gotd/ige v0.2.2 // indirect + github.com/gotd/neo v0.1.5 // indirect + github.com/segmentio/asm v1.2.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + golang.org/x/sys v0.42.0 // indirect +) diff --git a/mtproxy-client/go.sum b/mtproxy-client/go.sum new file mode 100644 index 0000000..ff853ad --- /dev/null +++ b/mtproxy-client/go.sum @@ -0,0 +1,72 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= +github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI= +github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE= +github.com/go-faster/xor v0.3.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ= +github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38= +github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ= +github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I= +github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk= +github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0= +github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ= +github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ= +github.com/gotd/td v0.143.0 h1:p0U/Nn92zXmAsahDn5CIVzay2kQ36lBBENT/FlWR2nQ= +github.com/gotd/td v0.143.0/go.mod h1:8GA5ecTI5iswLwBAlqf0u6/+j+BqSWUARSrX2Xk1usQ= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ogen-go/ogen v1.19.0 h1:YvdNpeQJ8A8dLLpS6Vs4WxXL53BT6tBPxH0VSjfALhA= +github.com/ogen-go/ogen v1.19.0/go.mod h1:DeShwO+TEpLYXNCuZliSAedphphXsJaTGGbmSomWUjE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mtproxy-client/main.go b/mtproxy-client/main.go index 384a2c1..67746c1 100644 --- a/mtproxy-client/main.go +++ b/mtproxy-client/main.go @@ -1,116 +1,416 @@ package main import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "encoding/binary" "flag" + "fmt" + "io" "log" - "math/rand" + "math/big" + mrand "math/rand" "net" + "net/http" + "sync" "time" + + "github.com/gorilla/websocket" ) var ( - listenAddr = flag.String("listen", ":9443", "Local listen address for redirected Telegram traffic") - serverAddr = flag.String("server", "api.mtproto.ru:443", "Remote MTProxy server address") - secretHex = flag.String("secret", "", "MTProxy secret (ee-prefixed hex string)") - dialTimeout = flag.Duration("timeout", 15*time.Second, "Connection timeout to MTProxy server") - verbose = flag.Bool("v", false, "Verbose logging") + listenAddr = flag.String("listen", ":1443", "Local listen address") + secretHex = flag.String("secret", "", "Proxy secret (dd-prefixed hex, auto-generated if empty)") + transparent = flag.Bool("transparent", false, "Transparent mode: redirect Telegram DC traffic via iptables (no client config needed)") + verbose = flag.Bool("v", false, "Verbose logging") ) -func handleConnection(clientConn *net.TCPConn, secret *ProxySecret) { +const handshakeLen = 64 + +// DC WebSocket domains +func wsDomains(dc int, isMedia bool) []string { + if dc == 203 { + dc = 2 + } + if isMedia { + return []string{ + fmt.Sprintf("kws%d-1.web.telegram.org", dc), + fmt.Sprintf("kws%d.web.telegram.org", dc), + } + } + return []string{ + fmt.Sprintf("kws%d.web.telegram.org", dc), + fmt.Sprintf("kws%d-1.web.telegram.org", dc), + } +} + +// tryHandshake decrypts client's obfuscated2 header using proxy secret. +// Returns DC id, isMedia flag, protocol tag, and AES key material. +func tryHandshake(header []byte, secret []byte) (dc int, isMedia bool, protoTag uint32, decKey, decIV, encKey, encIV []byte, err error) { + if len(header) != handshakeLen { + return 0, false, 0, nil, nil, nil, nil, fmt.Errorf("header len %d", len(header)) + } + + // Decrypt direction: client→proxy uses header[8:40] as key, header[40:56] as IV + rawKey := make([]byte, 32) + copy(rawKey, header[8:40]) + rawIV := make([]byte, 16) + copy(rawIV, header[40:56]) + + // Mix with secret: key = SHA256(rawKey || secret) + h := sha256.New() + h.Write(rawKey) + h.Write(secret) + decKey = h.Sum(nil) + decIV = rawIV + + // Decrypt entire header to read protocol tag and DC + block, _ := aes.NewCipher(decKey) + stream := cipher.NewCTR(block, decIV) + decrypted := make([]byte, handshakeLen) + stream.XORKeyStream(decrypted, header) + + protoTag = binary.LittleEndian.Uint32(decrypted[56:60]) + // Validate protocol tag + if protoTag != 0xefefefef && protoTag != 0xeeeeeeee && protoTag != 0xdddddddd { + return 0, false, 0, nil, nil, nil, nil, fmt.Errorf("bad proto tag 0x%08x", protoTag) + } + + dcIdx := int16(binary.LittleEndian.Uint16(decrypted[60:62])) + dc = int(dcIdx) + if dc < 0 { + dc = -dc + isMedia = true + } + if dc == 0 { + dc = 2 + } + + // Encrypt direction (proxy→client): reversed header[8:56] + reversed := make([]byte, 48) + copy(reversed, header[8:56]) + for i, j := 0, len(reversed)-1; i < j; i, j = i+1, j-1 { + reversed[i], reversed[j] = reversed[j], reversed[i] + } + h2 := sha256.New() + h2.Write(reversed[:32]) + h2.Write(secret) + encKey = h2.Sum(nil) + encIV = reversed[32:48] + + return dc, isMedia, protoTag, decKey, decIV, encKey, encIV, nil +} + +// generateRelayInit creates a new obfuscated2 header for connecting to Telegram DC +// (without proxy secret — direct DC connection). +func generateRelayInit(protoTag uint32, dcIdx int) (header []byte, relayEncKey, relayEncIV, relayDecKey, relayDecIV []byte, err error) { + header = make([]byte, handshakeLen) + for { + if _, err := io.ReadFull(rand.Reader, header); err != nil { + return nil, nil, nil, nil, nil, err + } + if header[0] == 0xef { + continue + } + first4 := binary.LittleEndian.Uint32(header[0:4]) + if first4 == 0x44414548 || first4 == 0x54534f50 || first4 == 0x20544547 || + first4 == 0x4954504f || first4 == 0x02010316 || + first4 == 0xdddddddd || first4 == 0xeeeeeeee { + continue + } + if header[4]|header[5]|header[6]|header[7] == 0 { + continue + } + break + } + + // Encryption key for relay→DC (our writes to DC) + relayEncKey = make([]byte, 32) + copy(relayEncKey, header[8:40]) + relayEncIV = make([]byte, 16) + copy(relayEncIV, header[40:56]) + + // Decryption key for DC→relay (reads from DC): reversed + reversed := make([]byte, 48) + copy(reversed, header[8:56]) + for i, j := 0, len(reversed)-1; i < j; i, j = i+1, j-1 { + reversed[i], reversed[j] = reversed[j], reversed[i] + } + relayDecKey = reversed[:32] + relayDecIV = reversed[32:48] + + // Write protocol tag and DC, encrypt with AES-CTR + block, _ := aes.NewCipher(relayEncKey) + encStream := cipher.NewCTR(block, relayEncIV) + encrypted := make([]byte, handshakeLen) + encStream.XORKeyStream(encrypted, header) + + // Build tail: protocol_tag + dc_bytes + 2 random bytes + tail := make([]byte, 8) + binary.LittleEndian.PutUint32(tail[0:4], protoTag) + binary.LittleEndian.PutUint16(tail[4:6], uint16(int16(dcIdx))) + rand.Read(tail[6:8]) + + // XOR tail with keystream at position 56 + for i := 0; i < 8; i++ { + tail[i] ^= encrypted[56+i] ^ header[56+i] + } + copy(header[56:64], tail) + + return header, relayEncKey, relayEncIV, relayDecKey, relayDecIV, nil +} + +func resolveIPv4(host string) (string, error) { + ips, err := net.LookupIP(host) + if err != nil { + return "", err + } + for _, ip := range ips { + if ip.To4() != nil { + return ip.String(), nil + } + } + return "", fmt.Errorf("no IPv4 for %s", host) +} + +func connectWS(dc int, isMedia bool) (*websocket.Conn, error) { + domains := wsDomains(dc, isMedia) + + // Try direct WS first, then Cloudflare proxy fallback + allDomains := append(domains, fmt.Sprintf("kws%d.pclead.co.uk", dc)) + + for _, domain := range allDomains { + ip, err := resolveIPv4(domain) + if err != nil { + if *verbose { + log.Printf("[debug] resolve %s failed: %v", domain, err) + } + continue + } + + dialer := websocket.Dialer{ + TLSClientConfig: &tls.Config{ + ServerName: domain, + }, + HandshakeTimeout: 5 * time.Second, + Subprotocols: []string{"binary"}, + NetDial: func(network, addr string) (net.Conn, error) { + return net.DialTimeout("tcp4", ip+":443", 5*time.Second) + }, + } + headers := http.Header{} + headers.Set("Origin", "http://web.telegram.org") + headers.Set("Host", domain) + + url := fmt.Sprintf("wss://%s/apiws", domain) + ws, _, err := dialer.Dial(url, headers) + if err != nil { + if *verbose { + log.Printf("[debug] WS dial %s (%s) failed: %v", domain, ip, err) + } + continue + } + if *verbose { + log.Printf("[debug] WS connected to %s (%s)", domain, ip) + } + return ws, nil + } + return nil, fmt.Errorf("all WS domains failed for DC%d", dc) +} + +func handleConnection(clientConn *net.TCPConn, secret []byte) { defer clientConn.Close() - // Get original destination (before iptables REDIRECT) - origIP, origPort, err := getOriginalDst(clientConn) + // Read client obfuscated2 header + header := make([]byte, handshakeLen) + if _, err := io.ReadFull(clientConn, header); err != nil { + return + } + + // Decrypt with proxy secret + dc, isMedia, protoTag, cltDecKey, cltDecIV, cltEncKey, cltEncIV, err := tryHandshake(header, secret) if err != nil { - log.Printf("[error] getOriginalDst: %v", err) + if *verbose { + log.Printf("[error] handshake: %v", err) + } return } - // Map destination IP to Telegram DC number - dc := LookupDC(origIP) - - if *verbose { - log.Printf("[conn] %s -> %s:%d (DC%d)", clientConn.RemoteAddr(), origIP, origPort, dc) + mediaTag := "" + if isMedia { + mediaTag = "m" } - - // Connect to remote MTProxy server - serverConn, err := net.DialTimeout("tcp", *serverAddr, *dialTimeout) - if err != nil { - log.Printf("[error] dial %s: %v", *serverAddr, err) - return - } - defer serverConn.Close() - - serverConn.(*net.TCPConn).SetKeepAlive(true) - serverConn.(*net.TCPConn).SetKeepAlivePeriod(30 * time.Second) - - // FakeTLS handshake with the MTProxy server - _, err = doFakeTLSHandshake(serverConn, secret) - if err != nil { - log.Printf("[error] FakeTLS handshake: %v", err) - return - } - - // Wrap server connection in TLS record framing - tlsConn := newTLSRecordConn(serverConn) - - // Send ChangeCipherSpec (required after FakeTLS handshake) - if err := writeTLSRecord(serverConn, tlsRecordChangeCipherSpec, []byte{0x01}); err != nil { - log.Printf("[error] send ChangeCipherSpec: %v", err) - return - } - - // Obfuscated2 layer on top of TLS records - obfsConn, err := newObfuscated2Conn(tlsConn, secret.Secret, dc) - if err != nil { - log.Printf("[error] obfuscated2 init: %v", err) - return + dcIdx := dc + if isMedia { + dcIdx = -dc } if *verbose { - log.Printf("[relay] %s <-> %s (DC%d)", clientConn.RemoteAddr(), *serverAddr, dc) + log.Printf("[conn] %s DC%d%s proto=0x%08x", clientConn.RemoteAddr(), dc, mediaTag, protoTag) } - // Bidirectional relay: client <-> obfuscated2(TLS(MTProxy server)) - relay(clientConn, obfsConn) + // Generate relay header for Telegram DC (no secret) + relayInit, relayEncKey, relayEncIV, relayDecKey, relayDecIV, err := generateRelayInit(protoTag, dcIdx) + if err != nil { + log.Printf("[error] relay init: %v", err) + return + } + + // Connect via WebSocket to Telegram DC + ws, err := connectWS(dc, isMedia) + if err != nil { + log.Printf("[error] WS connect DC%d%s: %v", dc, mediaTag, err) + return + } + defer ws.Close() + + // Send relay init header as first WS message + if err := ws.WriteMessage(websocket.BinaryMessage, relayInit); err != nil { + log.Printf("[error] WS write init: %v", err) + return + } + + // Create AES-CTR streams + // Client decrypt: decrypt what client sends (client encrypted with SHA256(key+secret)) + cltDecBlock, _ := aes.NewCipher(cltDecKey) + cltDecStream := cipher.NewCTR(cltDecBlock, cltDecIV) + // Advance past the 64-byte header + skip := make([]byte, handshakeLen) + cltDecStream.XORKeyStream(skip, skip) + + // Client encrypt: encrypt data we send back to client + cltEncBlock, _ := aes.NewCipher(cltEncKey) + cltEncStream := cipher.NewCTR(cltEncBlock, cltEncIV) + + // Relay encrypt: encrypt data for Telegram DC + relayEncBlock, _ := aes.NewCipher(relayEncKey) + relayEncStream := cipher.NewCTR(relayEncBlock, relayEncIV) + // Advance past header + relayEncStream.XORKeyStream(make([]byte, handshakeLen), make([]byte, handshakeLen)) + + // Relay decrypt: decrypt data from Telegram DC + relayDecBlock, _ := aes.NewCipher(relayDecKey) + relayDecStream := cipher.NewCTR(relayDecBlock, relayDecIV) if *verbose { - log.Printf("[done] %s", clientConn.RemoteAddr()) + log.Printf("[relay] %s <-> WS DC%d%s", clientConn.RemoteAddr(), dc, mediaTag) + } + + var wg sync.WaitGroup + wg.Add(2) + var upBytes, downBytes int64 + + // client → WS + go func() { + defer wg.Done() + buf := make([]byte, 65536) + for { + n, err := clientConn.Read(buf) + if n > 0 { + plain := make([]byte, n) + cltDecStream.XORKeyStream(plain, buf[:n]) + encrypted := make([]byte, n) + relayEncStream.XORKeyStream(encrypted, plain) + if werr := ws.WriteMessage(websocket.BinaryMessage, encrypted); werr != nil { + break + } + upBytes += int64(n) + } + if err != nil { + break + } + } + }() + + // WS → client + go func() { + defer wg.Done() + for { + _, msg, err := ws.ReadMessage() + if err != nil { + break + } + if len(msg) > 0 { + plain := make([]byte, len(msg)) + relayDecStream.XORKeyStream(plain, msg) + encrypted := make([]byte, len(msg)) + cltEncStream.XORKeyStream(encrypted, plain) + if _, werr := clientConn.Write(encrypted); werr != nil { + break + } + downBytes += int64(len(msg)) + } + } + }() + + wg.Wait() + + if *verbose { + log.Printf("[done] %s DC%d%s up=%d down=%d", clientConn.RemoteAddr(), dc, mediaTag, upBytes, downBytes) } } func main() { flag.Parse() + _ = mrand.Int63 + _ = big.NewInt + + if *transparent { + // Transparent mode: iptables REDIRECT, no client config needed + if err := transparentListener(*listenAddr); err != nil { + log.Fatal(err) + } + return + } + + // MTProxy mode: requires client configuration + var secret []byte if *secretHex == "" { - log.Fatal("--secret is required (ee-prefixed hex string)") + secret = make([]byte, 16) + rand.Read(secret) + *secretHex = fmt.Sprintf("dd%x", secret) + log.Printf("Generated secret: %s", *secretHex) + } else { + parsed, err := parseSecretHex(*secretHex) + if err != nil { + log.Fatalf("Invalid secret: %v", err) + } + secret = parsed } - secret, err := ParseSecret(*secretHex) - if err != nil { - log.Fatalf("Invalid secret: %v", err) - } - - rand.Seed(time.Now().UnixNano()) - ln, err := net.Listen("tcp", *listenAddr) if err != nil { log.Fatalf("Listen %s: %v", *listenAddr, err) } - log.Printf("tg-mtproxy-client listening on %s -> %s (SNI: %s)", *listenAddr, *serverAddr, secret.SNI) + host := "ROUTER_IP" + port := (*listenAddr)[1:] + log.Printf("tg-ws-proxy listening on %s", *listenAddr) + log.Printf("Add proxy in Telegram: tg://proxy?server=%s&port=%s&secret=%s", host, port, *secretHex) for { conn, err := ln.Accept() if err != nil { - log.Printf("[error] accept: %v", err) continue } go handleConnection(conn.(*net.TCPConn), secret) } } -func init() { - // Suppress timestamp prefix for cleaner log output - log.SetFlags(log.Ldate | log.Ltime) +func parseSecretHex(s string) ([]byte, error) { + if len(s) < 34 || (s[:2] != "dd" && s[:2] != "ee") { + return nil, fmt.Errorf("secret must start with dd or ee and be at least 34 hex chars") + } + raw := make([]byte, 16) + for i := 0; i < 16; i++ { + _, err := fmt.Sscanf(s[2+i*2:4+i*2], "%02x", &raw[i]) + if err != nil { + return nil, err + } + } + return raw, nil } diff --git a/mtproxy-client/mtproxy-client b/mtproxy-client/mtproxy-client new file mode 100755 index 0000000..e4683a6 Binary files /dev/null and b/mtproxy-client/mtproxy-client differ diff --git a/mtproxy-client/obfuscated2.go b/mtproxy-client/obfuscated2.go deleted file mode 100644 index f72bc4e..0000000 --- a/mtproxy-client/obfuscated2.go +++ /dev/null @@ -1,153 +0,0 @@ -package main - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/sha256" - "encoding/binary" - "io" - "math/rand" -) - -const ( - obfuscated2HeaderSize = 64 - tagPaddedIntermediate = 0xdddddddd -) - -// obfuscated2Conn wraps an io.ReadWriteCloser with obfuscated2 encryption. -type obfuscated2Conn struct { - inner io.ReadWriteCloser - encStream cipher.Stream - decStream cipher.Stream -} - -// generateObfuscated2Header creates the 64-byte init header with DC number encoded. -func generateObfuscated2Header(secret []byte, dc int16) (header []byte, encStream, decStream cipher.Stream, err error) { - header = make([]byte, obfuscated2HeaderSize) - - // Generate random bytes, avoiding certain patterns that look like other protocols. - for { - rand.Read(header) - - // First byte must not be 0xef (abridged protocol marker) - if header[0] == 0xef { - continue - } - // First 4 bytes must not match known protocol signatures - first4 := binary.LittleEndian.Uint32(header[0:4]) - if first4 == 0x44414548 || // "HEAD" - first4 == 0x54534f50 || // "POST" - first4 == 0x20544547 || // "GET " - first4 == 0x4954504f || // "OPTI" - first4 == 0xdddddddd || // padded intermediate - first4 == 0xeeeeeeee || // intermediate - first4 == 0x16030102 { // TLS - continue - } - // Bytes [4:8] must not be all zeros - if binary.LittleEndian.Uint32(header[4:8]) == 0 { - continue - } - break - } - - // Encode protocol tag at offset 56 (PaddedIntermediate) - binary.LittleEndian.PutUint32(header[56:60], tagPaddedIntermediate) - - // Encode DC number at offset 60 as int16 LE - binary.LittleEndian.PutUint16(header[60:62], uint16(dc)) - - // Derive encryption keys - // encrypt: key = header[8:40], iv = header[40:56] - encKeyRaw := make([]byte, 32) - copy(encKeyRaw, header[8:40]) - encIV := make([]byte, 16) - copy(encIV, header[40:56]) - - // decrypt: reverse of header[8:56] - reversed := make([]byte, 48) - for i := 0; i < 48; i++ { - reversed[i] = header[8+47-i] - } - decKeyRaw := reversed[:32] - decIV := reversed[32:48] - - // Mix in proxy secret: key = SHA256(key || secret) - if len(secret) >= 16 { - h := sha256.New() - h.Write(encKeyRaw) - h.Write(secret[:16]) - encKeyRaw = h.Sum(nil) - - h = sha256.New() - h.Write(decKeyRaw) - h.Write(secret[:16]) - decKeyRaw = h.Sum(nil) - } - - // Create AES-256-CTR streams - encBlock, err := aes.NewCipher(encKeyRaw) - if err != nil { - return nil, nil, nil, err - } - encStream = cipher.NewCTR(encBlock, encIV) - - decBlock, err := aes.NewCipher(decKeyRaw) - if err != nil { - return nil, nil, nil, err - } - decStream = cipher.NewCTR(decBlock, decIV) - - // Encrypt bytes [56:64] of the header (tag + DC) with the encrypt stream. - // First, advance the encrypt stream by processing bytes [0:56] (discarded). - dummy := make([]byte, 56) - encStream.XORKeyStream(dummy, header[:56]) - - // Now encrypt bytes [56:64] in place - encStream.XORKeyStream(header[56:64], header[56:64]) - - // Reset encrypt stream for actual data (need fresh stream from same key/IV) - encBlock2, _ := aes.NewCipher(encKeyRaw) - encStream = cipher.NewCTR(encBlock2, encIV) - // Advance past the 64-byte header - skip := make([]byte, 64) - encStream.XORKeyStream(skip, skip) - - return header, encStream, decStream, nil -} - -func newObfuscated2Conn(inner io.ReadWriteCloser, secret []byte, dc int16) (*obfuscated2Conn, error) { - header, encStream, decStream, err := generateObfuscated2Header(secret, dc) - if err != nil { - return nil, err - } - - // Send the header - if _, err := inner.Write(header); err != nil { - return nil, err - } - - return &obfuscated2Conn{ - inner: inner, - encStream: encStream, - decStream: decStream, - }, nil -} - -func (c *obfuscated2Conn) Read(p []byte) (int, error) { - n, err := c.inner.Read(p) - if n > 0 { - c.decStream.XORKeyStream(p[:n], p[:n]) - } - return n, err -} - -func (c *obfuscated2Conn) Write(p []byte) (int, error) { - buf := make([]byte, len(p)) - c.encStream.XORKeyStream(buf, p) - return c.inner.Write(buf) -} - -func (c *obfuscated2Conn) Close() error { - return c.inner.Close() -} diff --git a/mtproxy-client/relay.go b/mtproxy-client/relay.go index 192fefe..cebc076 100644 --- a/mtproxy-client/relay.go +++ b/mtproxy-client/relay.go @@ -2,33 +2,37 @@ package main import ( "io" + "log" + "net" "sync" + + "github.com/gotd/td/mtproxy/obfuscator" ) -// relay bidirectionally copies data between two connections. -// Closes both when either direction finishes. -func relay(a io.ReadWriteCloser, b io.ReadWriteCloser) { +// relay bidirectionally copies data between a TCP client and an obfuscated MTProxy connection. +func relay(client net.Conn, server *obfuscator.Conn) { var wg sync.WaitGroup wg.Add(2) + // client → server (raw MTProto → encrypted) go func() { defer wg.Done() - io.Copy(b, a) - // Signal the other side to stop - if closer, ok := b.(interface{ CloseWrite() error }); ok { - closer.CloseWrite() + n, err := io.Copy(server, client) + if *verbose && (err != nil || n == 0) { + log.Printf("[relay-detail] client→server: %d bytes, err=%v", n, err) } }() + // server → client (encrypted → raw MTProto) go func() { defer wg.Done() - io.Copy(a, b) - if closer, ok := a.(interface{ CloseWrite() error }); ok { - closer.CloseWrite() + n, err := io.Copy(client, server) + if *verbose && (err != nil || n == 0) { + log.Printf("[relay-detail] server→client: %d bytes, err=%v", n, err) } }() wg.Wait() - a.Close() - b.Close() + client.Close() + server.Close() } diff --git a/mtproxy-client/secret.go b/mtproxy-client/secret.go index f398947..725ee7d 100644 --- a/mtproxy-client/secret.go +++ b/mtproxy-client/secret.go @@ -3,34 +3,35 @@ package main import ( "encoding/hex" "fmt" + + "github.com/gotd/td/mtproxy" ) -// ProxySecret holds parsed MTProxy FakeTLS secret (ee-prefixed). -type ProxySecret struct { - Tag byte // protocol tag (0xdd = PaddedIntermediate) - Secret []byte // 16-byte crypto secret - SNI string // disguise domain (e.g. "s3.amazonaws.com") -} - // ParseSecret decodes an ee-prefixed hex secret string. -// Format: "ee" + 1 byte tag + 16 bytes secret + N bytes SNI domain (ASCII). -func ParseSecret(hexStr string) (*ProxySecret, error) { +// Format (tdesktop-compatible): ee + tag(1) + secret(16) + sni(rest) +// Tag byte may not match standard codec tags — we force PaddedIntermediate. +func ParseSecret(hexStr string) (mtproxy.Secret, error) { if len(hexStr) < 4 || hexStr[:2] != "ee" { - return nil, fmt.Errorf("secret must start with 'ee' (FakeTLS mode)") + return mtproxy.Secret{}, fmt.Errorf("secret must start with 'ee' (FakeTLS mode)") } raw, err := hex.DecodeString(hexStr[2:]) if err != nil { - return nil, fmt.Errorf("invalid hex in secret: %w", err) + return mtproxy.Secret{}, fmt.Errorf("invalid hex: %w", err) } if len(raw) < 17 { - return nil, fmt.Errorf("secret too short: need at least 17 bytes, got %d", len(raw)) + return mtproxy.Secret{}, fmt.Errorf("secret too short: need 1+16+sni bytes, got %d", len(raw)) } - return &ProxySecret{ - Tag: raw[0], - Secret: raw[1:17], - SNI: string(raw[17:]), + // mtg format (confirmed working with mtproto.ru servers): + // raw[0:16] = secret key (includes tag byte as part of key) + // raw[16:] = SNI domain + // Force PaddedIntermediate (0xdd) as protocol tag. + return mtproxy.Secret{ + Secret: raw[0:16], + Tag: 0xdd, + CloakHost: string(raw[16:]), + Type: mtproxy.TLS, }, nil } diff --git a/mtproxy-client/splitter.go b/mtproxy-client/splitter.go new file mode 100644 index 0000000..d366bd5 --- /dev/null +++ b/mtproxy-client/splitter.go @@ -0,0 +1,141 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/binary" +) + +// MsgSplitter splits an encrypted MTProto stream into individual packets +// for WebSocket framing. Each WS message must be exactly one MTProto packet. +type MsgSplitter struct { + dec cipher.Stream // decrypts relay-encrypted data to read headers + proto uint32 + cipherBuf []byte + plainBuf []byte + disabled bool +} + +// NewMsgSplitter creates a splitter from relay init header. +// It needs to decrypt the stream to read packet lengths, but returns +// the original encrypted chunks (not decrypted). +func NewMsgSplitter(relayInit []byte, proto uint32) *MsgSplitter { + // Create a decrypt stream using relay keys (no secret) + key := make([]byte, 32) + copy(key, relayInit[8:40]) + iv := make([]byte, 16) + copy(iv, relayInit[40:56]) + + block, _ := aes.NewCipher(key) + dec := cipher.NewCTR(block, iv) + + // Advance past the 64-byte header + skip := make([]byte, 64) + dec.XORKeyStream(skip, skip) + + return &MsgSplitter{ + dec: dec, + proto: proto, + } +} + +// Split takes an encrypted chunk and returns individual encrypted packets. +func (s *MsgSplitter) Split(chunk []byte) [][]byte { + if len(chunk) == 0 { + return nil + } + if s.disabled { + return [][]byte{chunk} + } + + s.cipherBuf = append(s.cipherBuf, chunk...) + // Decrypt to read headers + plain := make([]byte, len(chunk)) + s.dec.XORKeyStream(plain, chunk) + s.plainBuf = append(s.plainBuf, plain...) + + var parts [][]byte + for len(s.cipherBuf) > 0 { + pktLen := s.nextPacketLen() + if pktLen < 0 { + // Error — send rest as-is + parts = append(parts, append([]byte(nil), s.cipherBuf...)) + s.cipherBuf = nil + s.plainBuf = nil + s.disabled = true + break + } + if pktLen == 0 { + // Not enough data yet + break + } + parts = append(parts, append([]byte(nil), s.cipherBuf[:pktLen]...)) + s.cipherBuf = s.cipherBuf[pktLen:] + s.plainBuf = s.plainBuf[pktLen:] + } + return parts +} + +// Flush returns any remaining buffered data. +func (s *MsgSplitter) Flush() []byte { + if len(s.cipherBuf) == 0 { + return nil + } + out := append([]byte(nil), s.cipherBuf...) + s.cipherBuf = nil + s.plainBuf = nil + return out +} + +func (s *MsgSplitter) nextPacketLen() int { + switch s.proto { + case 0xefefefef: // Abridged + return s.nextAbridgedLen() + case 0xeeeeeeee, 0xdddddddd: // Intermediate, PaddedIntermediate + return s.nextIntermediateLen() + default: + return -1 + } +} + +func (s *MsgSplitter) nextAbridgedLen() int { + if len(s.plainBuf) < 1 { + return 0 + } + first := s.plainBuf[0] + var headerLen, payloadLen int + if first == 0x7f || first == 0xff { + if len(s.plainBuf) < 4 { + return 0 + } + payloadLen = int(s.plainBuf[1]) | int(s.plainBuf[2])<<8 | int(s.plainBuf[3])<<16 + payloadLen *= 4 + headerLen = 4 + } else { + payloadLen = int(first&0x7f) * 4 + headerLen = 1 + } + if payloadLen <= 0 { + return -1 + } + total := headerLen + payloadLen + if len(s.plainBuf) < total { + return 0 + } + return total +} + +func (s *MsgSplitter) nextIntermediateLen() int { + if len(s.plainBuf) < 4 { + return 0 + } + payloadLen := int(binary.LittleEndian.Uint32(s.plainBuf[0:4]) & 0x7FFFFFFF) + if payloadLen <= 0 { + return -1 + } + total := 4 + payloadLen + if len(s.plainBuf) < total { + return 0 + } + return total +} diff --git a/mtproxy-client/tlsrecord.go b/mtproxy-client/tlsrecord.go deleted file mode 100644 index becab68..0000000 --- a/mtproxy-client/tlsrecord.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "encoding/binary" - "fmt" - "io" -) - -const ( - tlsRecordChangeCipherSpec = 0x14 - tlsRecordHandshake = 0x16 - tlsRecordApplication = 0x17 - tlsVersion12 = 0x0303 - tlsMaxRecordSize = 16384 + 256 // max TLS record payload -) - -// readTLSRecord reads one TLS record: 5-byte header + payload. -func readTLSRecord(r io.Reader) (recordType byte, payload []byte, err error) { - hdr := make([]byte, 5) - if _, err = io.ReadFull(r, hdr); err != nil { - return 0, nil, fmt.Errorf("read TLS header: %w", err) - } - - recordType = hdr[0] - length := binary.BigEndian.Uint16(hdr[3:5]) - - if int(length) > tlsMaxRecordSize { - return 0, nil, fmt.Errorf("TLS record too large: %d", length) - } - - payload = make([]byte, length) - if _, err = io.ReadFull(r, payload); err != nil { - return 0, nil, fmt.Errorf("read TLS payload: %w", err) - } - - return recordType, payload, nil -} - -// writeTLSRecord writes one TLS record with the given type. -func writeTLSRecord(w io.Writer, recordType byte, payload []byte) error { - hdr := make([]byte, 5) - hdr[0] = recordType - binary.BigEndian.PutUint16(hdr[1:3], tlsVersion12) - binary.BigEndian.PutUint16(hdr[3:5], uint16(len(payload))) - - if _, err := w.Write(hdr); err != nil { - return err - } - _, err := w.Write(payload) - return err -} diff --git a/mtproxy-client/transparent.go b/mtproxy-client/transparent.go new file mode 100644 index 0000000..fc9dc37 --- /dev/null +++ b/mtproxy-client/transparent.go @@ -0,0 +1,222 @@ +package main + +import ( + "crypto/tls" + "fmt" + "log" + "net" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +// DNS cache to survive temporary resolver failures. +var ( + dnsCache = make(map[string]string) // domain → IPv4 + dnsCacheMu sync.RWMutex + dnsCacheTTL = 5 * time.Minute + dnsCacheTime = make(map[string]time.Time) +) + + +func resolveIPv4Cached(host string) (string, error) { + dnsCacheMu.RLock() + ip, ok := dnsCache[host] + t := dnsCacheTime[host] + dnsCacheMu.RUnlock() + + // Return cache if fresh + if ok && time.Since(t) < dnsCacheTTL { + return ip, nil + } + + // Try resolving + newIP, err := resolveIPv4(host) + if err != nil { + // DNS failed — use stale cache if available + if ok { + if *verbose { + log.Printf("[debug] DNS failed for %s, using cached %s", host, ip) + } + return ip, nil + } + return "", err + } + + // Update cache + dnsCacheMu.Lock() + dnsCache[host] = newIP + dnsCacheTime[host] = time.Now() + dnsCacheMu.Unlock() + + return newIP, nil +} + +// handleTransparent redirects intercepted Telegram traffic through +// Cloudflare WebSocket without any encryption/decryption. +func handleTransparent(clientConn *net.TCPConn) { + defer clientConn.Close() + defer func() { + if r := recover(); r != nil { + log.Printf("[panic] %s: %v", clientConn.RemoteAddr(), r) + } + }() + + origIP, _, err := getOriginalDst(clientConn) + if err != nil { + return + } + + dc := LookupDC(origIP) + isMedia := false + + if *verbose { + log.Printf("[conn] %s -> DC%d (%s)", clientConn.RemoteAddr(), dc, origIP) + } + + // Connect via WebSocket with retry + var ws *websocket.Conn + for attempt := 0; attempt < 3; attempt++ { + ws, err = connectWSTransparent(int(dc), isMedia) + if err == nil { + break + } + if attempt < 2 { + time.Sleep(500 * time.Millisecond) + } + } + if err != nil { + if *verbose { + log.Printf("[error] WS DC%d: %v", dc, err) + } + return + } + defer ws.Close() + + if *verbose { + log.Printf("[relay] %s <-> WS DC%d", clientConn.RemoteAddr(), dc) + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + buf := make([]byte, 65536) + for { + n, err := clientConn.Read(buf) + if n > 0 { + if werr := ws.WriteMessage(websocket.BinaryMessage, buf[:n]); werr != nil { + break + } + } + if err != nil { + break + } + } + }() + + go func() { + defer wg.Done() + for { + _, msg, err := ws.ReadMessage() + if err != nil { + break + } + if len(msg) > 0 { + if _, werr := clientConn.Write(msg); werr != nil { + break + } + } + } + }() + + wg.Wait() + + if *verbose { + log.Printf("[done] %s DC%d", clientConn.RemoteAddr(), dc) + } +} + +func connectWSTransparent(dc int, isMedia bool) (*websocket.Conn, error) { + cfDomain := fmt.Sprintf("kws%d.pclead.co.uk", dc) + + ip, err := resolveIPv4Cached(cfDomain) + if err != nil { + return nil, fmt.Errorf("resolve %s: %w", cfDomain, err) + } + + dialer := websocket.Dialer{ + TLSClientConfig: &tls.Config{ + ServerName: cfDomain, + }, + HandshakeTimeout: 5 * time.Second, + Subprotocols: []string{"binary"}, + NetDial: func(network, addr string) (net.Conn, error) { + return net.DialTimeout("tcp4", ip+":443", 5*time.Second) + }, + } + headers := http.Header{} + headers.Set("Origin", "http://web.telegram.org") + headers.Set("Host", cfDomain) + + url := fmt.Sprintf("wss://%s/apiws", cfDomain) + ws, _, err := dialer.Dial(url, headers) + if err != nil { + return nil, fmt.Errorf("dial %s (%s): %w", cfDomain, ip, err) + } + + if *verbose { + log.Printf("[debug] WS connected to %s (%s)", cfDomain, ip) + } + return ws, nil +} + +// transparentListener runs the transparent proxy mode. +func transparentListener(listenAddr string) error { + ln, err := net.Listen("tcp", listenAddr) + if err != nil { + return err + } + + // Pre-warm DNS cache for all DCs + for _, dc := range []int{1, 2, 3, 4, 5} { + domain := fmt.Sprintf("kws%d.pclead.co.uk", dc) + if ip, err := resolveIPv4(domain); err == nil { + dnsCacheMu.Lock() + dnsCache[domain] = ip + dnsCacheTime[domain] = time.Now() + dnsCacheMu.Unlock() + log.Printf("DNS cache: %s -> %s", domain, ip) + } + } + + log.Printf("tg-transparent-proxy listening on %s", listenAddr) + + // Periodic DNS cache refresh + go func() { + for { + time.Sleep(dnsCacheTTL) + for _, dc := range []int{1, 2, 3, 4, 5} { + domain := fmt.Sprintf("kws%d.pclead.co.uk", dc) + if ip, err := resolveIPv4(domain); err == nil { + dnsCacheMu.Lock() + dnsCache[domain] = ip + dnsCacheTime[domain] = time.Now() + dnsCacheMu.Unlock() + } + } + } + }() + + for { + conn, err := ln.Accept() + if err != nil { + continue + } + go handleTransparent(conn.(*net.TCPConn)) + } + return nil +}