13 Commits

Author SHA1 Message Date
Flowseal
46aec5e3b6 Win7 bundle 2026-03-07 18:08:42 +03:00
Flowseal
7e3732b04b reqs version freeze 2026-03-07 17:18:05 +03:00
Flowseal
5586d194db workflow windows path fix 2026-03-06 19:59:32 +03:00
Flowseal
f69d20ad85 Restructure 2026-03-06 19:48:12 +03:00
Flowseal
01b3aca85e code simplify 2026-03-06 19:08:46 +03:00
Flowseal
9e9448dda0 imports clear 2026-03-06 17:13:00 +03:00
Flowseal
f8a10d9940 Mapping unknown DC by IP for mobile clients 2026-03-06 02:47:59 +03:00
Flowseal
e57f61a621 unused const 2026-03-05 20:42:29 +03:00
Flowseal
2d1ca21293 Merge pull request #7 from Flowseal/copilot/fix-attribute-error-dict-add
Fix AttributeError when handling 302 redirects: initialize _ws_blacklist as set()
2026-03-05 00:03:17 +03:00
copilot-swe-agent[bot]
0401a4c6bb fix: initialize _ws_blacklist as set() instead of {}
Co-authored-by: Flowseal <50780822+Flowseal@users.noreply.github.com>
2026-03-04 21:00:58 +00:00
copilot-swe-agent[bot]
5228dbbdad Initial plan 2026-03-04 21:00:13 +00:00
Flowseal
98e7b374b2 Update README.md 2026-03-04 20:14:08 +03:00
Flowseal
a57be0971f Tray exit lag fix 2026-03-04 18:04:15 +03:00
7 changed files with 144 additions and 69 deletions

View File

@@ -27,8 +27,11 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pip install -r requirements.txt run: pip install -r requirements.txt
- name: Install pyinstaller
run: pip install pyinstaller
- name: Build EXE with PyInstaller - name: Build EXE with PyInstaller
run: pyinstaller tg_ws_proxy.spec --noconfirm run: pyinstaller packaging/windows.spec --noconfirm
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -36,6 +39,52 @@ jobs:
name: TgWsProxy name: TgWsProxy
path: dist/TgWsProxy.exe path: dist/TgWsProxy.exe
build-win7:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python 3.8 (last version supporting Win7)
uses: actions/setup-python@v5
with:
python-version: "3.8"
cache: "pip"
- name: Install dependencies (Win7-compatible)
run: pip install -r requirements-win7.txt
- name: Install pyinstaller
run: pip install "pyinstaller==5.13.2"
- name: Build EXE with PyInstaller (Win7)
run: pyinstaller packaging/windows.spec --noconfirm
- name: Rename artifact
run: mv dist/TgWsProxy.exe dist/TgWsProxy-win7.exe
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: TgWsProxy-win7
path: dist/TgWsProxy-win7.exe
release:
needs: [build, build-win7]
runs-on: ubuntu-latest
steps:
- name: Download main build
uses: actions/download-artifact@v4
with:
name: TgWsProxy
path: dist
- name: Download Win7 build
uses: actions/download-artifact@v4
with:
name: TgWsProxy-win7
path: dist
- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
@@ -43,8 +92,10 @@ jobs:
name: "TG WS Proxy ${{ github.event.inputs.version }}" name: "TG WS Proxy ${{ github.event.inputs.version }}"
body: | body: |
## TG WS Proxy ${{ github.event.inputs.version }} ## TG WS Proxy ${{ github.event.inputs.version }}
files: dist/TgWsProxy.exe files: |
dist/TgWsProxy.exe
dist/TgWsProxy-win7.exe
draft: false draft: false
prerelease: false prerelease: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -4,6 +4,8 @@
**Ожидаемый результат аналогичен прокидыванию hosts для Web Telegram**: ускорение загрузки и скачивания файлов, загрузки сообщений и части медиа. **Ожидаемый результат аналогичен прокидыванию hosts для Web Telegram**: ускорение загрузки и скачивания файлов, загрузки сообщений и части медиа.
<img width="529" height="487" alt="image" src="https://github.com/user-attachments/assets/6a4cf683-0df8-43af-86c1-0e8f08682b62" />
## Как это работает ## Как это работает
``` ```
@@ -16,21 +18,10 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS (kws*.web.t
4. Устанавливает WebSocket (TLS) соединение к соответствующему DC через домены `kws{N}.web.telegram.org` 4. Устанавливает WebSocket (TLS) соединение к соответствующему DC через домены `kws{N}.web.telegram.org`
5. Если WS недоступен (302 redirect) — автоматически переключается на прямое TCP-соединение 5. Если WS недоступен (302 redirect) — автоматически переключается на прямое TCP-соединение
## Установка ## 🚀 Быстрый старт
### Из исходников ### Windows
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy.exe`**. Он собирается автоматически через [Github Actions](https://github.com/Flowseal/tg-ws-proxy/actions) из открытого исходного кода.
```bash
pip install -r requirements.txt
```
## Использование
### Tray-приложение (рекомендуется для Windows)
```bash
python tg_ws_tray.py
```
При первом запуске откроется окно с инструкцией по подключению Telegram Desktop. Приложение сворачивается в системный трей. При первом запуске откроется окно с инструкцией по подключению Telegram Desktop. Приложение сворачивается в системный трей.
@@ -41,10 +32,22 @@ python tg_ws_tray.py
- **Открыть логи** — открыть файл логов - **Открыть логи** — открыть файл логов
- **Выход** — остановить прокси и закрыть приложение - **Выход** — остановить прокси и закрыть приложение
## Установка из исходников
```bash
pip install -r requirements.txt
```
### Windows (Tray-приложение)
```bash
python windows.py
```
### Консольный режим ### Консольный режим
```bash ```bash
python tg_ws_proxy.py [--port PORT] [--dc-ip DC:IP ...] [-v] python proxy/tg_ws_proxy.py [--port PORT] [--dc-ip DC:IP ...] [-v]
``` ```
**Аргументы:** **Аргументы:**
@@ -59,13 +62,13 @@ python tg_ws_proxy.py [--port PORT] [--dc-ip DC:IP ...] [-v]
```bash ```bash
# Стандартный запуск # Стандартный запуск
python tg_ws_proxy.py python proxy/tg_ws_proxy.py
# Другой порт и дополнительные DC # Другой порт и дополнительные DC
python tg_ws_proxy.py --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
# С подробным логированием # С подробным логированием
python tg_ws_proxy.py -v python proxy/tg_ws_proxy.py -v
``` ```
## Настройка Telegram Desktop ## Настройка Telegram Desktop
@@ -85,7 +88,7 @@ python tg_ws_proxy.py -v
## Конфигурация ## Конфигурация
Tray-приложение хранит конфигурацию в `%APPDATA%/TgWsProxy/config.json`: Tray-приложение хранит данные в `%APPDATA%/TgWsProxy`:
```json ```json
{ {
@@ -98,20 +101,15 @@ Tray-приложение хранит конфигурацию в `%APPDATA%/Tg
} }
``` ```
Логи записываются в `%APPDATA%/TgWsProxy/proxy.log`. ## Автоматическая сборка
## Сборка exe Проект содержит спецификацию PyInstaller ([`windows.spec`](packaging/windows.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки.
Проект содержит спецификацию PyInstaller ([`tg_ws_proxy.spec`](tg_ws_proxy.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки.
```bash ```bash
pip install pyinstaller pip install pyinstaller
pyinstaller tg_ws_proxy.spec pyinstaller packaging/windows.spec
``` ```
## Дисклеймер
Проект частично vibecoded by Opus 4.6. Если вы найдете баг, то создайте Issue с его описанем.
## Лицензия ## Лицензия
[MIT License](LICENSE) [MIT License](LICENSE)

View File

@@ -10,12 +10,11 @@ import customtkinter
ctk_path = os.path.dirname(customtkinter.__file__) ctk_path = os.path.dirname(customtkinter.__file__)
a = Analysis( a = Analysis(
['tg_ws_tray.py'], [os.path.join(os.path.dirname(SPEC), os.pardir, 'windows.py')],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[(ctk_path, 'customtkinter/')], datas=[(ctk_path, 'customtkinter/')],
hiddenimports=[ hiddenimports=[
'tg_ws_proxy',
'pystray._win32', 'pystray._win32',
'PIL._tkinter_finder', 'PIL._tkinter_finder',
'customtkinter', 'customtkinter',
@@ -34,7 +33,7 @@ a = Analysis(
noarchive=False, noarchive=False,
) )
icon_path = os.path.join(os.path.dirname(SPEC), 'icon.ico') icon_path = os.path.join(os.path.dirname(SPEC), os.pardir, 'icon.ico')
if os.path.exists(icon_path): if os.path.exists(icon_path):
a.datas += [('icon.ico', icon_path, 'DATA')] a.datas += [('icon.ico', icon_path, 'DATA')]

View File

@@ -15,8 +15,6 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
DEFAULT_PORT = 1080 DEFAULT_PORT = 1080
DEFAULT_TARGET_IP = '149.154.167.220' # unthrottled, works for DC2 and DC4
log = logging.getLogger('tg-ws-proxy') log = logging.getLogger('tg-ws-proxy')
_TG_RANGES = [ _TG_RANGES = [
@@ -34,12 +32,32 @@ _TG_RANGES = [
struct.unpack('!I', _socket.inet_aton('91.108.255.255'))[0]), struct.unpack('!I', _socket.inet_aton('91.108.255.255'))[0]),
] ]
_IP_TO_DC: Dict[str, int] = {
# DC1
'149.154.175.50': 1, '149.154.175.51': 1, '149.154.175.54': 1,
# DC2
'149.154.167.41': 2,
'149.154.167.50': 2, '149.154.167.51': 2, '149.154.167.220': 2,
# DC3
'149.154.175.100': 3, '149.154.175.101': 3,
# DC4
'149.154.167.91': 4, '149.154.167.92': 4,
# DC5
'91.108.56.100': 5,
'91.108.56.126': 5, '91.108.56.101': 5, '91.108.56.116': 5,
# DC203
'91.105.192.100': 203,
# Media DCs
'149.154.167.151': 2, '149.154.167.223': 2,
'149.154.166.120': 4, '149.154.166.121': 4,
}
_dc_opt: Dict[int, Optional[str]] = {} _dc_opt: Dict[int, Optional[str]] = {}
# 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)
_ws_blacklist: Set[Tuple[int, bool]] = {} _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] = {}
@@ -333,9 +351,7 @@ def _ws_domains(dc: int, is_media) -> List[str]:
DC >5: kws{N}[-1].telegram.org DC >5: kws{N}[-1].telegram.org
""" """
base = 'telegram.org' if dc > 5 else 'web.telegram.org' base = 'telegram.org' if dc > 5 else 'web.telegram.org'
if is_media is None: if is_media is None or is_media:
return [f'kws{dc}-1.{base}', f'kws{dc}.{base}']
if is_media:
return [f'kws{dc}-1.{base}', f'kws{dc}.{base}'] return [f'kws{dc}-1.{base}', f'kws{dc}.{base}']
return [f'kws{dc}.{base}', f'kws{dc}-1.{base}'] return [f'kws{dc}.{base}', f'kws{dc}-1.{base}']
@@ -580,7 +596,7 @@ async def _handle_client(reader, writer):
rr, rw = await asyncio.wait_for( rr, rw = await asyncio.wait_for(
asyncio.open_connection(dst, port), timeout=10) asyncio.open_connection(dst, port), timeout=10)
except Exception as exc: except Exception as exc:
log.warning("[%s] passthrough failed: %s", label, exc) log.warning("[%s] passthrough failed to %s: %s", label, dst, exc)
writer.write(_socks5_reply(0x05)) writer.write(_socks5_reply(0x05))
await writer.drain() await writer.drain()
writer.close() writer.close()
@@ -623,6 +639,9 @@ async def _handle_client(reader, writer):
# -- Extract DC ID -- # -- Extract DC ID --
dc, is_media = _dc_from_init(init) dc, is_media = _dc_from_init(init)
if dc is None and dst in _IP_TO_DC:
dc = _IP_TO_DC.get(dst)
if dc is None or dc not in _dc_opt: if dc is None or dc not in _dc_opt:
log.warning("[%s] unknown DC%s for %s:%d -> TCP passthrough", log.warning("[%s] unknown DC%s for %s:%d -> TCP passthrough",
label, dc, dst, port) label, dc, dst, port)
@@ -790,7 +809,14 @@ async def _run(port: int, dc_opt: Dict[int, Optional[str]],
async def wait_stop(): async def wait_stop():
await stop_event.wait() await stop_event.wait()
server.close() server.close()
await server.wait_closed() me = asyncio.current_task()
for task in list(asyncio.all_tasks()):
if task is not me:
task.cancel()
try:
await server.wait_closed()
except asyncio.CancelledError:
pass
asyncio.create_task(wait_stop()) asyncio.create_task(wait_stop())
async with server: async with server:

5
requirements-win7.txt Normal file
View File

@@ -0,0 +1,5 @@
cryptography==41.0.7
customtkinter==5.2.2
Pillow==10.4.0
psutil==5.9.8
pystray==0.19.5

View File

@@ -1,6 +1,5 @@
cryptography cryptography==46.0.5
pystray customtkinter==5.2.2
Pillow Pillow==12.1.1
customtkinter psutil==7.0.0
pyinstaller pystray==0.19.5
psutil

View File

@@ -9,27 +9,14 @@ import sys
import threading import threading
import time import time
import webbrowser import webbrowser
import pystray
import asyncio as _asyncio import asyncio as _asyncio
import customtkinter as ctk
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, Optional
from PIL import Image, ImageDraw, ImageFont
try: import proxy.tg_ws_proxy as tg_ws_proxy
from PIL import Image, ImageDraw, ImageFont
except ImportError:
Image = ImageDraw = ImageFont = None # type: ignore
try:
import pystray
except ImportError:
pystray = None # type: ignore
try:
import customtkinter as ctk
except ImportError:
ctk = None # type: ignore
# Proxy engine
import tg_ws_proxy
APP_NAME = "TgWsProxy" APP_NAME = "TgWsProxy"
@@ -47,10 +34,10 @@ DEFAULT_CONFIG = {
_proxy_thread: Optional[threading.Thread] = None _proxy_thread: Optional[threading.Thread] = None
_stop_event: Optional[threading.Event] = None
_async_stop: Optional[object] = None _async_stop: Optional[object] = None
_tray_icon: Optional[object] = None _tray_icon: Optional[object] = None
_config: dict = {} _config: dict = {}
_exiting: bool = False
log = logging.getLogger("tg-ws-tray") log = logging.getLogger("tg-ws-tray")
@@ -198,7 +185,7 @@ def stop_proxy():
loop, stop_ev = _async_stop loop, stop_ev = _async_stop
loop.call_soon_threadsafe(stop_ev.set) loop.call_soon_threadsafe(stop_ev.set)
if _proxy_thread: if _proxy_thread:
_proxy_thread.join(timeout=5) _proxy_thread.join(timeout=2)
_proxy_thread = None _proxy_thread = None
log.info("Proxy stopped") log.info("Proxy stopped")
@@ -401,8 +388,18 @@ def _on_open_logs(icon=None, item=None):
def _on_exit(icon=None, item=None): def _on_exit(icon=None, item=None):
global _exiting
if _exiting:
os._exit(0)
return
_exiting = True
log.info("User requested exit") log.info("User requested exit")
stop_proxy()
def _force_exit():
time.sleep(3)
os._exit(0)
threading.Thread(target=_force_exit, daemon=True, name="force-exit").start()
if icon: if icon:
icon.stop() icon.stop()