docker-auth

This commit is contained in:
borisovmsw 2026-03-19 23:53:37 +03:00
parent 4ae7cb92f7
commit 99c5340f09
4 changed files with 200 additions and 123 deletions

28
.dockerignore Normal file
View File

@ -0,0 +1,28 @@
.git
.github
.gitignore
__pycache__/
*.py[cod]
*.pyo
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.venv/
venv/
dist/
build/
packaging/
windows.py
icon.ico
*.spec
*.spec.bak
*.manifest
*.log
.vscode/
.idea/
*.swp
*.swo
.DS_Store
Thumbs.db
Desktop.ini

45
Dockerfile Normal file
View File

@ -0,0 +1,45 @@
# syntax=docker/dockerfile:1.7
FROM python:3.12-slim AS builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
VIRTUAL_ENV=/opt/venv
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential cargo libffi-dev libssl-dev \
&& python -m venv "$VIRTUAL_ENV" \
&& "$VIRTUAL_ENV/bin/pip" install --upgrade pip setuptools wheel \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN "$VIRTUAL_ENV/bin/pip" install cryptography==46.0.5
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH=/opt/venv/bin:$PATH \
TG_WS_PROXY_HOST=0.0.0.0 \
TG_WS_PROXY_PORT=1080 \
TG_WS_PROXY_DC_IPS="2:149.154.167.220 4:149.154.167.220"
RUN apt-get update \
&& apt-get install -y --no-install-recommends tini ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system app \
&& useradd --system --gid app --create-home --home-dir /home/app app
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY proxy ./proxy
COPY README.md LICENSE ./
USER app
EXPOSE 1080/tcp
ENTRYPOINT ["/usr/bin/tini", "--", "/bin/sh", "-lc", "set -eu; args=\"--host ${TG_WS_PROXY_HOST} --port ${TG_WS_PROXY_PORT}\"; for dc in ${TG_WS_PROXY_DC_IPS}; do args=\"$args --dc-ip $dc\"; done; exec python -u proxy/tg_ws_proxy.py $args \"$@\"", "--"]
CMD []

124
README.md
View File

@ -1,10 +1,8 @@
> [!CAUTION] > [!CAUTION]
> >
> ### Реакция антивирусов > ### Реакция антивирусов
>
> Windows Defender часто ошибочно помечает приложение как **Wacatac**. > Windows Defender часто ошибочно помечает приложение как **Wacatac**.
> Если вы не можете скачать из-за блокировки, то: > Если вы не можете скачать из-за блокировки, то:
>
> 1) Попробуйте скачать версию win7 (она ничем не отличается в плане функционала) > 1) Попробуйте скачать версию win7 (она ничем не отличается в плане функционала)
> 2) Отключите антивирус на время скачивания, добавьте файл в исключения и включите обратно > 2) Отключите антивирус на время скачивания, добавьте файл в исключения и включите обратно
> >
@ -12,138 +10,77 @@
# TG WS Proxy # TG WS Proxy
**Локальный SOCKS5-прокси** для Telegram Desktop, который **ускоряет работу Telegram**, перенаправляя трафик через WebSocket-соединения. Данные передаются в том же зашифрованном виде, а для работы не нужны сторонние сервера. Локальный SOCKS5-прокси для Telegram Desktop, который перенаправляет трафик через WebSocket-соединения к указанным серверам, помогая частично ускорить работу Telegram.
**Ожидаемый результат аналогичен прокидыванию hosts для Web Telegram**: ускорение загрузки и скачивания файлов, загрузки сообщений и части медиа.
<img width="529" height="487" alt="image" src="https://github.com/user-attachments/assets/6a4cf683-0df8-43af-86c1-0e8f08682b62" /> <img width="529" height="487" alt="image" src="https://github.com/user-attachments/assets/6a4cf683-0df8-43af-86c1-0e8f08682b62" />
## Как это работает ## Как это работает
``` ```
Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegram DC Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS (kws*.web.telegram.org) → Telegram DC
``` ```
1. Приложение поднимает локальный SOCKS5-прокси на `127.0.0.1:1080` 1. Приложение поднимает локальный SOCKS5-прокси на `127.0.0.1:1080`
2. Перехватывает подключения к IP-адресам Telegram 2. Перехватывает подключения к IP-адресам Telegram
3. Извлекает DC ID из MTProto obfuscation init-пакета 3. Извлекает DC ID из MTProto obfuscation init-пакета
4. Устанавливает WebSocket (TLS) соединение к соответствующему DC через домены Telegram 4. Устанавливает WebSocket (TLS) соединение к соответствующему DC через домены `kws{N}.web.telegram.org`
5. Если WS недоступен (302 redirect) — автоматически переключается на прямое TCP-соединение 5. Если WS недоступен (302 redirect) — автоматически переключается на прямое TCP-соединение
## 🚀 Быстрый старт ## 🚀 Быстрый старт
### Windows ### Windows
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy.exe`**. Он собирается автоматически через [Github Actions](https://github.com/Flowseal/tg-ws-proxy/actions) из открытого исходного кода.
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_windows.exe`**. Он собирается автоматически через [Github Actions](https://github.com/Flowseal/tg-ws-proxy/actions) из открытого исходного кода.
При первом запуске откроется окно с инструкцией по подключению Telegram Desktop. Приложение сворачивается в системный трей. При первом запуске откроется окно с инструкцией по подключению Telegram Desktop. Приложение сворачивается в системный трей.
**Меню трея:** **Меню трея:**
- **Открыть в Telegram** — автоматически настроить прокси через `tg://socks` ссылку - **Открыть в Telegram** — автоматически настроить прокси через `tg://socks` ссылку
- **Перезапустить прокси** — перезапуск без выхода из приложения - **Перезапустить прокси** — перезапуск без выхода из приложения
- **Настройки...** — GUI-редактор конфигурации - **Настройки...** — GUI-редактор конфигурации
- **Открыть логи** — открыть файл логов - **Открыть логи** — открыть файл логов
- **Выход** — остановить прокси и закрыть приложение - **Выход** — остановить прокси и закрыть приложение
### macOS
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_macos_universal.dmg`** — универсальная сборка для Apple Silicon и Intel.
1. Открыть образ
2. Перенести **TG WS Proxy.app** в папку **Applications**
3. При первом запуске macOS может попросить подтвердить открытие: **Системные настройки → Конфиденциальность и безопасность → Всё равно открыть**
### Linux
Для Debian/Ubuntu скачайте со [страницы релизов](https://github.com/Flowseal/tg-ws-proxy/releases) пакет **`TgWsProxy_linux_amd64.deb`**.
Для остальных дистрибутивов можно использовать **`TgWsProxy_linux_amd64`** (бинарный файл для x86_64).
```bash
chmod +x TgWsProxy_linux_amd64
./TgWsProxy_linux_amd64
```
При первом запуске откроется окно с инструкцией. Приложение работает в системном трее (требуется AppIndicator).
## Установка из исходников ## Установка из исходников
### Консольный proxy
Для запуска только SOCKS5/WebSocket proxy без tray-интерфейса достаточно базовой установки:
```bash ```bash
pip install -e . pip install -r requirements.txt
tg-ws-proxy
``` ```
### Windows 10+ ### Windows (Tray-приложение)
```bash ```bash
pip install -e ".[win10]" python windows.py
tg-ws-proxy-tray-win
``` ```
### Windows 7 ### Консольный режим
```bash ```bash
pip install -e ".[win7]" python proxy/tg_ws_proxy.py [--port PORT] [--dc-ip DC:IP ...] [-v]
tg-ws-proxy-tray-win
```
### macOS
```bash
pip install -e ".[macos]"
tg-ws-proxy-tray-macos
```
### Linux
```bash
pip install -e ".[linux]"
tg-ws-proxy-tray-linux
```
### Консольный режим из исходников
```bash
tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
``` ```
**Аргументы:** **Аргументы:**
| Аргумент | По умолчанию | Описание | | Аргумент | По умолчанию | Описание |
|---|---|---| |-------------------------------------|------------------------------------------|------------------------------------------------|
| `--port` | `1080` | Порт SOCKS5-прокси | | `--host` | `127.0.0.1` | IP-адрес SOCKS5-прокси |
| `--host` | `127.0.0.1` | Хост SOCKS5-прокси | | `--port` | `1080` | Порт SOCKS5-прокси |
| `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (можно указать несколько раз) | | `-u`, `-user`<br/>`-P`,`--password` | выкл. | Логин и Пароль для авторизации в SOCKS5-прокси |
| `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) | | `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (можно указать несколько раз)|
| `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) |
**Примеры:** **Примеры:**
```bash ```bash
# Стандартный запуск # Стандартный запуск
tg-ws-proxy python proxy/tg_ws_proxy.py
# Другой порт и дополнительные DC # Другой порт и дополнительные DC
tg-ws-proxy --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149.154.167.220 python proxy/tg_ws_proxy.py --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149.154.167.220
# С подробным логированием # С подробным логированием
tg-ws-proxy -v python proxy/tg_ws_proxy.py -v
```
## CLI-скрипты (pyproject.toml)
CLI команды объявляются в `pyproject.toml` в секции `[project.scripts]` и должны указывать на `module:function`.
Пример:
```toml
[project.scripts]
tg-ws-proxy = "proxy.tg_ws_proxy:main"
tg-ws-proxy-tray-win = "windows:main"
tg-ws-proxy-tray-macos = "macos:main"
tg-ws-proxy-tray-linux = "linux:main"
``` ```
## Настройка Telegram Desktop ## Настройка Telegram Desktop
@ -163,11 +100,7 @@ tg-ws-proxy-tray-linux = "linux:main"
## Конфигурация ## Конфигурация
Tray-приложение хранит данные в: Tray-приложение хранит данные в `%APPDATA%/TgWsProxy`:
- **Windows:** `%APPDATA%/TgWsProxy`
- **macOS:** `~/Library/Application Support/TgWsProxy`
- **Linux:** `~/.config/TgWsProxy` (или `$XDG_CONFIG_HOME/TgWsProxy`)
```json ```json
{ {
@ -182,15 +115,12 @@ Tray-приложение хранит данные в:
## Автоматическая сборка ## Автоматическая сборка
Проект содержит спецификации PyInstaller ([`packaging/windows.spec`](packaging/windows.spec), [`packaging/macos.spec`](packaging/macos.spec), [`packaging/linux.spec`](packaging/linux.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки. Проект содержит спецификацию PyInstaller ([`windows.spec`](packaging/windows.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки.
Минимально поддерживаемые версии ОС для текущих бинарных сборок: ```bash
pip install pyinstaller
- Windows 10+ для `TgWsProxy_windows.exe` pyinstaller packaging/windows.spec
- Windows 7 для `TgWsProxy_windows_7.exe` ```
- Intel macOS 10.15+
- Apple Silicon macOS 11.0+
- Linux x86_64 (требуется AppIndicator для системного трея)
## Лицензия ## Лицензия

View File

@ -12,14 +12,15 @@ import sys
import time import time
from typing import Dict, List, Optional, Set, Tuple from typing import Dict, List, Optional, Set, Tuple
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from urllib.parse import quote
DEFAULT_PORT = 1080 DEFAULT_PORT = 1080
log = logging.getLogger('tg-ws-proxy') log = logging.getLogger('tg-ws-proxy')
_TCP_NODELAY = True _TCP_NODELAY = True
_RECV_BUF = 256 * 1024 _RECV_BUF = 131072
_SEND_BUF = 256 * 1024 _SEND_BUF = 131072
_WS_POOL_SIZE = 4 _WS_POOL_SIZE = 4
_WS_POOL_MAX_AGE = 120.0 _WS_POOL_MAX_AGE = 120.0
@ -68,13 +69,11 @@ _IP_TO_DC: Dict[str, Tuple[int, bool]] = {
'91.105.192.100': (203, False), '91.105.192.100': (203, False),
} }
# This case might work but not actually sure
_DC_OVERRIDES: Dict[int, int] = {
203: 2
}
_dc_opt: Dict[int, Optional[str]] = {} _dc_opt: Dict[int, Optional[str]] = {}
_auth_user = ''
_auth_password = ''
# DCs where WS is known to fail (302 redirect) # DCs where WS is known to fail (302 redirect)
# Raw TCP fallback will be used instead # Raw TCP fallback will be used instead
# Keyed by (dc, is_media) # Keyed by (dc, is_media)
@ -82,8 +81,7 @@ _ws_blacklist: Set[Tuple[int, bool]] = set()
# Rate-limit re-attempts per (dc, is_media) # Rate-limit re-attempts per (dc, is_media)
_dc_fail_until: Dict[Tuple[int, bool], float] = {} _dc_fail_until: Dict[Tuple[int, bool], float] = {}
_DC_FAIL_COOLDOWN = 30.0 # seconds to keep reduced WS timeout after failure _DC_FAIL_COOLDOWN = 60.0 # seconds
_WS_FAIL_TIMEOUT = 2.0 # quick-retry timeout after a recent WS failure
_ssl_ctx = ssl.create_default_context() _ssl_ctx = ssl.create_default_context()
@ -383,7 +381,7 @@ def _dc_from_init(data: bytes) -> Tuple[Optional[int], bool]:
proto, dc_raw, plain.hex()) proto, dc_raw, plain.hex())
if proto in (0xEFEFEFEF, 0xEEEEEEEE, 0xDDDDDDDD): if proto in (0xEFEFEFEF, 0xEEEEEEEE, 0xDDDDDDDD):
dc = abs(dc_raw) dc = abs(dc_raw)
if 1 <= dc <= 5 or dc == 203: if 1 <= dc <= 1000:
return dc, (dc_raw < 0) return dc, (dc_raw < 0)
except Exception as exc: except Exception as exc:
log.debug("DC extraction failed: %s", exc) log.debug("DC extraction failed: %s", exc)
@ -470,10 +468,16 @@ class _MsgSplitter:
def _ws_domains(dc: int, is_media) -> List[str]: def _ws_domains(dc: int, is_media) -> List[str]:
dc = _DC_OVERRIDES.get(dc, dc) """
Return domain names to try for WebSocket connection to a DC.
DC 1-5: kws{N}[-1].web.telegram.org
DC >5: kws{N}[-1].telegram.org
"""
base = 'telegram.org' if dc > 5 else 'web.telegram.org'
if is_media is None or is_media: if is_media is None or is_media:
return [f'kws{dc}-1.web.telegram.org', f'kws{dc}.web.telegram.org'] return [f'kws{dc}-1.{base}', f'kws{dc}.{base}']
return [f'kws{dc}.web.telegram.org', f'kws{dc}-1.web.telegram.org'] return [f'kws{dc}.{base}', f'kws{dc}-1.{base}']
class Stats: class Stats:
@ -614,7 +618,7 @@ async def _bridge_ws(reader, writer, ws: RawWebSocket, label,
nonlocal up_bytes, up_packets nonlocal up_bytes, up_packets
try: try:
while True: while True:
chunk = await reader.read(65536) chunk = await reader.read(131072)
if not chunk: if not chunk:
break break
_stats.bytes_up += len(chunk) _stats.bytes_up += len(chunk)
@ -787,10 +791,53 @@ async def _handle_client(reader, writer):
log.debug("[%s] not SOCKS5 (ver=%d)", label, hdr[0]) log.debug("[%s] not SOCKS5 (ver=%d)", label, hdr[0])
writer.close() writer.close()
return return
# Auth check
nmethods = hdr[1] nmethods = hdr[1]
await reader.readexactly(nmethods) auth_methods = await reader.readexactly(nmethods)
writer.write(b'\x05\x00') # no-auth
await writer.drain() if not _auth_user and not _auth_password:
writer.write(b'\x05\x00') # no-auth
await writer.drain()
elif _auth_user and _auth_password:
if b'\x02' not in auth_methods:
writer.write(b'\x05\xff')
await writer.drain()
writer.close()
return
writer.write(b'\x05\x02') # username/password required
log.debug("[%s] auth required", label)
# Username/password subnegotiation
auth_hdr = await asyncio.wait_for(reader.readexactly(1), timeout=10)
if auth_hdr[0] != 1:
log.debug("[%s] bad auth request ver: %s", label, auth_hdr)
writer.write(b'\x01\x01')
await writer.drain()
writer.close()
return
ulen = (await reader.readexactly(1))[0]
client_username = (await reader.readexactly(ulen)).decode('utf-8', errors='ignore')
plen = (await reader.readexactly(1))[0]
client_password = (await reader.readexactly(plen)).decode('utf-8', errors='ignore')
if client_username != _auth_user or client_password != _auth_password:
log.warning("[%s] auth failed with creds: %s/%s", label, client_username, client_password)
writer.write(b'\x01\x01')
await writer.drain()
writer.close()
return
log.debug("[%s] auth success", label)
writer.write(b'\x01\x00') # Success
else:
# If have some problems
writer.write(b'\x05\xff')
await writer.drain()
writer.close()
return
# -- SOCKS5 CONNECT request -- # -- SOCKS5 CONNECT request --
req = await asyncio.wait_for(reader.readexactly(4), timeout=10) req = await asyncio.wait_for(reader.readexactly(4), timeout=10)
@ -911,10 +958,20 @@ async def _handle_client(reader, writer):
label, dc, media_tag) label, dc, media_tag)
return return
# -- Try WebSocket via direct connection -- # -- Cooldown check --
fail_until = _dc_fail_until.get(dc_key, 0) fail_until = _dc_fail_until.get(dc_key, 0)
ws_timeout = _WS_FAIL_TIMEOUT if now < fail_until else 10.0 if now < fail_until:
remaining = fail_until - now
log.debug("[%s] DC%d%s WS cooldown (%.0fs) -> TCP",
label, dc, media_tag, remaining)
ok = await _tcp_fallback(reader, writer, dst, port, init,
label, dc=dc, is_media=is_media)
if ok:
log.info("[%s] DC%d%s TCP fallback closed",
label, dc, media_tag)
return
# -- Try WebSocket via direct connection --
domains = _ws_domains(dc, is_media) domains = _ws_domains(dc, is_media)
target = _dc_opt[dc] target = _dc_opt[dc]
ws = None ws = None
@ -932,7 +989,7 @@ async def _handle_client(reader, writer):
label, dc, media_tag, dst, port, url, target) label, dc, media_tag, dst, port, url, target)
try: try:
ws = await RawWebSocket.connect(target, domain, ws = await RawWebSocket.connect(target, domain,
timeout=ws_timeout) timeout=10)
all_redirects = False all_redirects = False
break break
except WsHandshakeError as exc: except WsHandshakeError as exc:
@ -1050,6 +1107,7 @@ async def _run(port: int, dc_opt: Dict[int, Optional[str]],
log.info("=" * 60) log.info("=" * 60)
log.info(" Configure Telegram Desktop:") log.info(" Configure Telegram Desktop:")
log.info(" SOCKS5 proxy -> %s:%d (no user/pass)", host, port) log.info(" SOCKS5 proxy -> %s:%d (no user/pass)", host, port)
log.info(f" tg://socks/?server={host}&port={port}&user={quote(_auth_user)}&pass={quote(_auth_password)}")
log.info("=" * 60) log.info("=" * 60)
async def log_stats(): async def log_stats():
@ -1086,6 +1144,20 @@ async def _run(port: int, dc_opt: Dict[int, Optional[str]],
_server_instance = None _server_instance = None
def set_auth_credentials(username: str, password: str):
"""Validate 'user' and 'password': both are specified, or both aren't"""
empty_user = username is None or not str(username).strip()
empty_pass = password is None or not str(password).strip()
if empty_user != empty_pass:
raise ValueError(
"Both --username (-u) and --password (-P) must be specified together, or neither should be used")
global _auth_user, _auth_password
_auth_user = username
_auth_password = password
def parse_dc_ip_list(dc_ip_list: List[str]) -> Dict[int, str]: def parse_dc_ip_list(dc_ip_list: List[str]) -> Dict[int, str]:
"""Parse list of 'DC:IP' strings into {dc: ip} dict.""" """Parse list of 'DC:IP' strings into {dc: ip} dict."""
dc_opt: Dict[int, str] = {} dc_opt: Dict[int, str] = {}
@ -1112,23 +1184,25 @@ def run_proxy(port: int, dc_opt: Dict[int, str],
def main(): def main():
ap = argparse.ArgumentParser( ap = argparse.ArgumentParser(
description='Telegram Desktop WebSocket Bridge Proxy') description='Telegram Desktop WebSocket Bridge Proxy')
ap.add_argument('--port', type=int, default=DEFAULT_PORT,
help=f'Listen port (default {DEFAULT_PORT})')
ap.add_argument('--host', type=str, default='127.0.0.1', ap.add_argument('--host', type=str, default='127.0.0.1',
help='Listen host (default 127.0.0.1)') help='Listen host (default 127.0.0.1)')
ap.add_argument('--port', type=int, default=DEFAULT_PORT,
help=f'Listen port (default {DEFAULT_PORT})')
ap.add_argument('-u', '--user', type=str, default='',
help='User for proxy')
ap.add_argument('-P', '--password', type=str, default='',
help='Password for proxy')
ap.add_argument('--dc-ip', metavar='DC:IP', action='append', ap.add_argument('--dc-ip', metavar='DC:IP', action='append',
default=[], default=['2:149.154.167.220', '4:149.154.167.220'],
help='Target IP for a DC, e.g. --dc-ip 1:149.154.175.205' help='Target IP for a DC, e.g. --dc-ip 1:149.154.175.205'
' --dc-ip 2:149.154.167.220') ' --dc-ip 2:149.154.167.220')
ap.add_argument('-v', '--verbose', action='store_true', ap.add_argument('-v', '--verbose', action='store_true',
help='Debug logging') help='Debug logging')
args = ap.parse_args() args = ap.parse_args()
if not args.dc_ip:
args.dc_ip = ['2:149.154.167.220', '4:149.154.167.220']
try: try:
dc_opt = parse_dc_ip_list(args.dc_ip) dc_opt = parse_dc_ip_list(args.dc_ip)
set_auth_credentials(args.user, args.password)
except ValueError as e: except ValueError as e:
log.error(str(e)) log.error(str(e))
sys.exit(1) sys.exit(1)