mirror of
https://github.com/Flowseal/tg-ws-proxy.git
synced 2026-05-24 16:31:43 +03:00
Compare commits
2 Commits
v1.6.2
...
bedc9e4d16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bedc9e4d16 | ||
|
|
b66732f7f9 |
9
.gitattributes
vendored
9
.gitattributes
vendored
@@ -1,9 +0,0 @@
|
|||||||
* text=auto eol=lf
|
|
||||||
|
|
||||||
*.py text diff=python
|
|
||||||
*.spec text linguist-language=Python
|
|
||||||
|
|
||||||
*.toml text
|
|
||||||
*.txt text
|
|
||||||
|
|
||||||
*.ico binary
|
|
||||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
|||||||
custom: ['https://nowpayments.io/donation/flowseal']
|
|
||||||
2
.github/cfproxy-domains.txt
vendored
2
.github/cfproxy-domains.txt
vendored
@@ -1,5 +1,3 @@
|
|||||||
virkgj.com
|
virkgj.com
|
||||||
vmmzovy.com
|
vmmzovy.com
|
||||||
mkuosckvso.com
|
mkuosckvso.com
|
||||||
zaewayzmplad.com
|
|
||||||
twdmbzcm.com
|
|
||||||
|
|||||||
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.12"
|
||||||
cache: "pip"
|
cache: "pip"
|
||||||
|
|
||||||
- name: Setup MSVC 14.40 toolset
|
- name: Setup MSVC 14.40 toolset
|
||||||
@@ -38,11 +38,10 @@ jobs:
|
|||||||
run: pip install .
|
run: pip install .
|
||||||
|
|
||||||
- name: Build PyInstaller bootloader from source
|
- name: Build PyInstaller bootloader from source
|
||||||
env:
|
|
||||||
PYINSTALLER_COMPILE_BOOTLOADER: "1"
|
|
||||||
run: |
|
run: |
|
||||||
pip download --no-binary pyinstaller --no-deps --no-cache-dir -d pyinstaller_src "pyinstaller==6.10.0"
|
pip install "pyinstaller==6.16.0" --no-binary pyinstaller
|
||||||
pip install (Get-ChildItem pyinstaller_src\*.tar.gz).FullName
|
env:
|
||||||
|
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
||||||
|
|
||||||
- name: Build EXE with PyInstaller
|
- name: Build EXE with PyInstaller
|
||||||
run: pyinstaller packaging/windows.spec --noconfirm
|
run: pyinstaller packaging/windows.spec --noconfirm
|
||||||
@@ -194,7 +193,7 @@ jobs:
|
|||||||
|
|
||||||
python3.12 -m pip install --no-deps wheelhouse/universal2/*.whl
|
python3.12 -m pip install --no-deps wheelhouse/universal2/*.whl
|
||||||
python3.12 -m pip install .
|
python3.12 -m pip install .
|
||||||
python3.12 -m pip install pyinstaller==6.13.0
|
python3.12 -m pip install pyinstaller==6.16.0
|
||||||
|
|
||||||
- name: Create macOS icon from ICO
|
- name: Create macOS icon from ICO
|
||||||
run: |
|
run: |
|
||||||
@@ -296,7 +295,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
.venv/bin/pip install --upgrade pip
|
.venv/bin/pip install --upgrade pip
|
||||||
.venv/bin/pip install .
|
.venv/bin/pip install .
|
||||||
.venv/bin/pip install "pyinstaller==6.13.0"
|
.venv/bin/pip install "pyinstaller==6.16.0"
|
||||||
|
|
||||||
- name: Build binary with PyInstaller
|
- name: Build binary with PyInstaller
|
||||||
run: .venv/bin/pyinstaller packaging/linux.spec --noconfirm
|
run: .venv/bin/pyinstaller packaging/linux.spec --noconfirm
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,8 +6,6 @@ __pycache__/
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
*.spec.bak
|
*.spec.bak
|
||||||
venv/
|
|
||||||
.venv/
|
|
||||||
|
|
||||||
# PyInstaller
|
# PyInstaller
|
||||||
*.manifest
|
*.manifest
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Cloudflare имеет лимиты на одновременное количе
|
|||||||
- Name=`kws5` IPv4=`149.154.171.5`
|
- Name=`kws5` IPv4=`149.154.171.5`
|
||||||
- Name=`kws203` IPv4=`91.105.192.100`
|
- Name=`kws203` IPv4=`91.105.192.100`
|
||||||
|
|
||||||
4. **Добавьте домен в [zapret](https://github.com/Flowseal/zapret-discord-youtube/) или в любое другое ПО, так как подсеть Cloudflare забанена (по крайней мере, если вы из России)**
|
4. **Добавьте домен в [zapret](https://github.com/Flowseal/zapret-discord-youtube/) или другой софт для обхода блокировок, так как подсеть Cloudflare забанена (по крайней мере, если вы из России)**
|
||||||
|
|
||||||
5. В настройках TgWsProxy поменяйте домен на свой
|
5. В настройках TgWsProxy поменяйте домен на свой
|
||||||
|
|
||||||
|
|||||||
148
docs/README.md
148
docs/README.md
@@ -5,19 +5,18 @@
|
|||||||
> **USDT (TRC20)**: `TXPnKs2Ww1RD8JN6nChFUVmi5r2hqrWjuu`
|
> **USDT (TRC20)**: `TXPnKs2Ww1RD8JN6nChFUVmi5r2hqrWjuu`
|
||||||
> **BTC**: `bc1qr8vd6jelkyyry3m4mq6z5txdx4pl856fu6ss0w`
|
> **BTC**: `bc1qr8vd6jelkyyry3m4mq6z5txdx4pl856fu6ss0w`
|
||||||
> **ETH**: `0x1417878fdc5047E670a77748B34819b9A49C72F1`
|
> **ETH**: `0x1417878fdc5047E670a77748B34819b9A49C72F1`
|
||||||
> **Другие монеты**: https://nowpayments.io/donation/flowseal
|
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
>
|
>
|
||||||
> ### Реакция антивирусов
|
> ### Реакция антивирусов
|
||||||
>
|
>
|
||||||
> Антивирусы часто ошибочно помечают приложение как вирус из-за упаковщика.
|
> Windows Defender часто ошибочно помечает приложение как **Wacatac**.
|
||||||
> Если вы не можете скачать из-за блокировки антивирусом, то:
|
> Если вы не можете скачать из-за блокировки, то:
|
||||||
>
|
>
|
||||||
> 1) **Попробуйте скачать версию win7 (она ничем не отличается в плане функционала)**
|
> 1) Попробуйте скачать версию win7 (она ничем не отличается в плане функционала)
|
||||||
> 2) Отключите антивирус на время скачивания, добавьте файл в исключения и включите обратно
|
> 2) Отключите антивирус на время скачивания, добавьте файл в исключения и включите обратно
|
||||||
>
|
>
|
||||||
> Всегда проверяйте, что скачиваете из интернета, тем более из непроверенных источников. Всегда лучше смотреть на детекты широко известных антивирусов на VirusTotal
|
> **Всегда проверяйте, что скачиваете из интернета, тем более из непроверенных источников. Всегда лучше смотреть на детекты широко известных антивирусов на VirusTotal**
|
||||||
|
|
||||||
# TG WS Proxy
|
# TG WS Proxy
|
||||||
|
|
||||||
@@ -39,9 +38,7 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
|
|||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> ### Не грузит фото/видео?
|
> ### Не грузит фото/видео?
|
||||||
> **Удалите в настройках прокси в DC->IP всё, кроме `4:149.154.167.220`**
|
> ### Удалите в настройках прокси в DC->IP всё, кроме `4:149.154.167.220`
|
||||||
> **Если не помогло, то удалите вообще всё из этого поля**
|
|
||||||
> ####
|
|
||||||
> Подобная проблема встречается на аккаунтах без Premium
|
> Подобная проблема встречается на аккаунтах без Premium
|
||||||
> Если вам не помогло, то настраивайте свой домен по гайду отсюда: https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md
|
> Если вам не помогло, то настраивайте свой домен по гайду отсюда: https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md
|
||||||
|
|
||||||
@@ -56,7 +53,6 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
|
|||||||
**Меню трея:**
|
**Меню трея:**
|
||||||
|
|
||||||
- **Открыть в Telegram** — автоматически настроить прокси через `tg://proxy` ссылку
|
- **Открыть в Telegram** — автоматически настроить прокси через `tg://proxy` ссылку
|
||||||
- **Скопировать ссылку** — скопировать ссылку для подключения
|
|
||||||
- **Перезапустить прокси** — перезапуск без выхода из приложения
|
- **Перезапустить прокси** — перезапуск без выхода из приложения
|
||||||
- **Настройки...** — GUI-редактор конфигурации (в т.ч. версия приложения, опциональная проверка обновлений с GitHub)
|
- **Настройки...** — GUI-редактор конфигурации (в т.ч. версия приложения, опциональная проверка обновлений с GitHub)
|
||||||
- **Открыть логи** — открыть файл логов
|
- **Открыть логи** — открыть файл логов
|
||||||
@@ -64,26 +60,6 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
|
|||||||
|
|
||||||
При первом запуске после старта может появиться запрос об открытии страницы релиза, если на GitHub вышла новая версия (отключается в настройках).
|
При первом запуске после старта может появиться запрос об открытии страницы релиза, если на GitHub вышла новая версия (отключается в настройках).
|
||||||
|
|
||||||
### Настройка Telegram Desktop
|
|
||||||
|
|
||||||
### Автоматически:
|
|
||||||
|
|
||||||
ПКМ по иконке в трее → **«Открыть в Telegram»**
|
|
||||||
Если не сработало (не открылся Telegram с подключением), то:
|
|
||||||
1. ПКМ по иконке в трее → **«Скопировать ссылку»**
|
|
||||||
2. Отправьте ссылку себе в избранное в Telegram клиенте и нажмите по ней ЛКМ
|
|
||||||
3. Подключитесь
|
|
||||||
|
|
||||||
### Вручную:
|
|
||||||
|
|
||||||
1. Telegram → **Настройки** → **Продвинутые настройки** → **Тип подключения** → **Прокси**
|
|
||||||
2. Добавить прокси:
|
|
||||||
- **Тип:** MTProto
|
|
||||||
- **Сервер:** `127.0.0.1` (или переопределенный вами)
|
|
||||||
- **Порт:** `1443` (или переопределенный вами)
|
|
||||||
- **Secret:** из настроек или логов
|
|
||||||
|
|
||||||
##
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_macos_universal.dmg`** — универсальная сборка для Apple Silicon и Intel.
|
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_macos_universal.dmg`** — универсальная сборка для Apple Silicon и Intel.
|
||||||
@@ -112,6 +88,50 @@ paru -S tg-ws-proxy-bin
|
|||||||
sudo systemctl start tg-ws-proxy-cli@8888:3075abe65830f0325116bb0416cadf9f
|
sudo systemctl start tg-ws-proxy-cli@8888:3075abe65830f0325116bb0416cadf9f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Для NixOS используйте [tg-ws-proxy-flake](https://github.com/dmfrpro/tg-ws-proxy-flake):
|
||||||
|
|
||||||
|
```nix
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
tg-ws-proxy.url = "github:dmfrpro/tg-ws-proxy-flake";
|
||||||
|
tg-ws-proxy.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Далее добавляете или системный модуль, или home-manager модуль:
|
||||||
|
```nix
|
||||||
|
outputs = { self, nixpkgs, tg-ws-proxy, ... }: {
|
||||||
|
|
||||||
|
# Или NixOS модуль
|
||||||
|
nixosConfigurations.host = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
tg-ws-proxy.nixosModules.tg-ws-proxy
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Или Home-manager модуль
|
||||||
|
homeConfigurations."user@host" = home-manager.lib.homeManagerConfiguration {
|
||||||
|
inherit pkgs;
|
||||||
|
modules = [
|
||||||
|
tg-ws-proxy.homeModules.tg-ws-proxy
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
и включаете сервис:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
services.tg-ws-proxy = {
|
||||||
|
enable = true;
|
||||||
|
secret = "3075abe65830f0325116bb0416cadf9f"; # openssl rand -hex 16
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Для остальных дистрибутивов можно использовать **`TgWsProxy_linux_amd64`** (бинарный файл для x86_64).
|
Для остальных дистрибутивов можно использовать **`TgWsProxy_linux_amd64`** (бинарный файл для x86_64).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -170,8 +190,6 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
|
|||||||
| `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare]((https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md)) |
|
| `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare]((https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md)) |
|
||||||
| `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudfalre. [Подробнее тут](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md) |
|
| `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudfalre. [Подробнее тут](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md) |
|
||||||
| `--cfproxy-priority` | `true` | Пробовать проксировать через Cloudflare перед прямым TCP подключением |
|
| `--cfproxy-priority` | `true` | Пробовать проксировать через Cloudflare перед прямым TCP подключением |
|
||||||
| `--fake-tls-domain` | | Включить Fake TLS (ee-secret) маскировку с указанным SNI-доменом |
|
|
||||||
| `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) |
|
|
||||||
| `--buf-kb` | `256` | Размер буфера в КБ |
|
| `--buf-kb` | `256` | Размер буфера в КБ |
|
||||||
| `--pool-size` | `4` | Количество заготовленных соединений на каждый DC |
|
| `--pool-size` | `4` | Количество заготовленных соединений на каждый DC |
|
||||||
| `--log-file` | выкл. | Путь до файла, в который сохранять логи |
|
| `--log-file` | выкл. | Путь до файла, в который сохранять логи |
|
||||||
@@ -190,64 +208,24 @@ tg-ws-proxy --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149.154.167.220
|
|||||||
|
|
||||||
# С подробным логированием
|
# С подробным логированием
|
||||||
tg-ws-proxy -v
|
tg-ws-proxy -v
|
||||||
|
|
||||||
# Fake TLS маскировка (ee-secret)
|
|
||||||
tg-ws-proxy --fake-tls-domain example.com
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Fake TLS + nginx upstream
|
## Настройка Telegram Desktop
|
||||||
### Домен (`--fake-tls-domain`) должен указывать на тот же IP, на котором стоит прокси
|
|
||||||
|
|
||||||
**Пример `nginx.conf` (stream):**
|
### Автоматически
|
||||||
|
|
||||||
```nginx
|
ПКМ по иконке в трее → **«Открыть в Telegram»**
|
||||||
upstream mtproto {
|
|
||||||
server 127.0.0.1:8446;
|
|
||||||
}
|
|
||||||
|
|
||||||
map $ssl_preread_server_name $sni_name {
|
### Вручную
|
||||||
hostnames;
|
|
||||||
example.com mtproto;
|
|
||||||
# if you have xray with selfsni running:
|
|
||||||
# sub.example.com www;
|
|
||||||
# default xray;
|
|
||||||
}
|
|
||||||
|
|
||||||
# upstream xray {
|
1. Telegram → **Настройки** → **Продвинутые настройки** → **Тип подключения** → **Прокси**
|
||||||
# server 127.0.0.1:8443;
|
2. Добавить прокси:
|
||||||
# }
|
- **Тип:** MTProto
|
||||||
#
|
- **Сервер:** `127.0.0.1` (или переопределенный вами)
|
||||||
# upstream www {
|
- **Порт:** `1443` (или переопределенный вами)
|
||||||
# server 127.0.0.1:7443;
|
- **Secret:** из настроек или логов
|
||||||
# }
|
|
||||||
|
|
||||||
server {
|
## Конфигурация
|
||||||
proxy_protocol on;
|
|
||||||
set_real_ip_from unix:;
|
|
||||||
listen 443;
|
|
||||||
proxy_pass $sni_name;
|
|
||||||
ssl_preread on;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Запуск прокси за nginx:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 proxy/tg_ws_proxy.py \
|
|
||||||
--port 8446 \
|
|
||||||
--host 127.0.0.1 \
|
|
||||||
--fake-tls-domain example.com \
|
|
||||||
--proxy-protocol \
|
|
||||||
--secret <32-hex-chars>
|
|
||||||
```
|
|
||||||
|
|
||||||
Ссылка для подключения будет в формате `ee`-секрета:</p>
|
|
||||||
|
|
||||||
```
|
|
||||||
tg://proxy?server=your.domain.com&port=443&secret=ee<secret><domain_hex>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Файлы конфигурации Tray-приложения
|
|
||||||
|
|
||||||
Tray-приложение хранит данные в:
|
Tray-приложение хранит данные в:
|
||||||
|
|
||||||
@@ -268,11 +246,7 @@ Tray-приложение хранит данные в:
|
|||||||
"buf_kb": 256,
|
"buf_kb": 256,
|
||||||
"pool_size": 4,
|
"pool_size": 4,
|
||||||
"log_max_mb": 5.0,
|
"log_max_mb": 5.0,
|
||||||
"check_updates": true,
|
"check_updates": true
|
||||||
"cfproxy": true,
|
|
||||||
"cfproxy_priority": true,
|
|
||||||
"cfproxy_user_domain": "",
|
|
||||||
"appearance": "auto"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
2
linux.py
2
linux.py
@@ -273,7 +273,7 @@ def run_tray() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if not acquire_lock():
|
if not acquire_lock("linux.py"):
|
||||||
_show_info("Приложение уже запущено.", os.path.basename(sys.argv[0]))
|
_show_info("Приложение уже запущено.", os.path.basename(sys.argv[0]))
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|||||||
2
macos.py
2
macos.py
@@ -610,7 +610,7 @@ def run_menubar() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if not acquire_lock():
|
if not acquire_lock("macos.py"):
|
||||||
_show_info("Приложение уже запущено.")
|
_show_info("Приложение уже запущено.")
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
|
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
|
||||||
VSVersionInfo(
|
VSVersionInfo(
|
||||||
ffi=FixedFileInfo(
|
ffi=FixedFileInfo(
|
||||||
filevers=(1, 6, 2, 0),
|
filevers=(1, 0, 0, 0),
|
||||||
prodvers=(1, 6, 2, 0),
|
prodvers=(1, 0, 0, 0),
|
||||||
mask=0x3f,
|
mask=0x3f,
|
||||||
flags=0x0,
|
flags=0x0,
|
||||||
OS=0x40004,
|
OS=0x40004,
|
||||||
@@ -21,12 +21,12 @@ VSVersionInfo(
|
|||||||
[
|
[
|
||||||
StringStruct(u'CompanyName', u'Flowseal'),
|
StringStruct(u'CompanyName', u'Flowseal'),
|
||||||
StringStruct(u'FileDescription', u'Telegram Desktop WebSocket Bridge Proxy'),
|
StringStruct(u'FileDescription', u'Telegram Desktop WebSocket Bridge Proxy'),
|
||||||
StringStruct(u'FileVersion', u'1.6.2.0'),
|
StringStruct(u'FileVersion', u'1.0.0.0'),
|
||||||
StringStruct(u'InternalName', u'TgWsProxy'),
|
StringStruct(u'InternalName', u'TgWsProxy'),
|
||||||
StringStruct(u'LegalCopyright', u'Copyright (c) Flowseal. MIT License.'),
|
StringStruct(u'LegalCopyright', u'Copyright (c) Flowseal. MIT License.'),
|
||||||
StringStruct(u'OriginalFilename', u'TgWsProxy.exe'),
|
StringStruct(u'OriginalFilename', u'TgWsProxy.exe'),
|
||||||
StringStruct(u'ProductName', u'TG WS Proxy'),
|
StringStruct(u'ProductName', u'TG WS Proxy'),
|
||||||
StringStruct(u'ProductVersion', u'1.6.2.0'),
|
StringStruct(u'ProductVersion', u'1.0.0.0'),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from .config import parse_dc_ip_list, proxy_config
|
from .config import parse_dc_ip_list, proxy_config
|
||||||
from .utils import get_link_host
|
from .utils import get_link_host
|
||||||
|
|
||||||
__version__ = "1.6.2"
|
__version__ = "1.6.0"
|
||||||
|
|
||||||
__all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list"]
|
__all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list"]
|
||||||
@@ -16,7 +16,7 @@ CFPROXY_DOMAINS_URL = (
|
|||||||
"/.github/cfproxy-domains.txt"
|
"/.github/cfproxy-domains.txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
_CFPROXY_ENC: List[str] = ['virkgj.com', 'vmmzovy.com', 'mkuosckvso.com', 'zaewayzmplad.com', 'twdmbzcm.com']
|
_CFPROXY_ENC: List[str] = ['virkgj.com']
|
||||||
_S = ''.join(chr(c) for c in (46, 99, 111, 46, 117, 107))
|
_S = ''.join(chr(c) for c in (46, 99, 111, 46, 117, 107))
|
||||||
|
|
||||||
|
|
||||||
@@ -47,8 +47,6 @@ class ProxyConfig:
|
|||||||
cfproxy_user_domain: str = ''
|
cfproxy_user_domain: str = ''
|
||||||
cfproxy_domains: List[str] = field(default_factory=lambda: list(CFPROXY_DEFAULT_DOMAINS))
|
cfproxy_domains: List[str] = field(default_factory=lambda: list(CFPROXY_DEFAULT_DOMAINS))
|
||||||
active_cfproxy_domain: str = field(default_factory=lambda: random.choice(CFPROXY_DEFAULT_DOMAINS))
|
active_cfproxy_domain: str = field(default_factory=lambda: random.choice(CFPROXY_DEFAULT_DOMAINS))
|
||||||
fake_tls_domain: str = ''
|
|
||||||
proxy_protocol: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
proxy_config = ProxyConfig()
|
proxy_config = ProxyConfig()
|
||||||
@@ -87,21 +85,12 @@ def refresh_cfproxy_domains() -> None:
|
|||||||
proxy_config.active_cfproxy_domain = random.choice(pool)
|
proxy_config.active_cfproxy_domain = random.choice(pool)
|
||||||
|
|
||||||
|
|
||||||
_refresh_stop: threading.Event = threading.Event()
|
|
||||||
|
|
||||||
|
|
||||||
def start_cfproxy_domain_refresh() -> None:
|
def start_cfproxy_domain_refresh() -> None:
|
||||||
global _refresh_stop
|
threading.Thread(
|
||||||
_refresh_stop.set()
|
target=refresh_cfproxy_domains,
|
||||||
_refresh_stop = threading.Event()
|
daemon=True,
|
||||||
stop = _refresh_stop
|
name='cfproxy-domains-refresh',
|
||||||
|
).start()
|
||||||
def _loop():
|
|
||||||
refresh_cfproxy_domains()
|
|
||||||
while not stop.wait(timeout=3600):
|
|
||||||
refresh_cfproxy_domains()
|
|
||||||
|
|
||||||
threading.Thread(target=_loop, daemon=True, name='cfproxy-domains-refresh').start()
|
|
||||||
|
|
||||||
|
|
||||||
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]:
|
||||||
|
|||||||
@@ -1,256 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import struct
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from typing import Optional, Tuple
|
|
||||||
from .stats import stats
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('tg-mtproto-proxy')
|
|
||||||
|
|
||||||
TLS_RECORD_HANDSHAKE = 0x16
|
|
||||||
TLS_RECORD_CCS = 0x14
|
|
||||||
TLS_RECORD_APPDATA = 0x17
|
|
||||||
|
|
||||||
TLS_VERSION_10 = b'\x03\x01'
|
|
||||||
TLS_VERSION_12 = b'\x03\x03'
|
|
||||||
TLS_VERSION_13 = b'\x03\x04'
|
|
||||||
|
|
||||||
CLIENT_RANDOM_OFFSET = 11
|
|
||||||
CLIENT_RANDOM_LEN = 32
|
|
||||||
SESSION_ID_OFFSET = 44
|
|
||||||
SESSION_ID_LEN = 32
|
|
||||||
|
|
||||||
TIMESTAMP_TOLERANCE = 120
|
|
||||||
|
|
||||||
TLS_APPDATA_MAX = 16384
|
|
||||||
|
|
||||||
|
|
||||||
_CCS_FRAME = b'\x14\x03\x03\x00\x01\x01'
|
|
||||||
|
|
||||||
_SERVER_HELLO_TEMPLATE = bytearray(
|
|
||||||
b'\x16\x03\x03\x00\x7a'
|
|
||||||
b'\x02\x00\x00\x76'
|
|
||||||
b'\x03\x03'
|
|
||||||
+ b'\x00' * 32
|
|
||||||
+ b'\x20'
|
|
||||||
+ b'\x00' * 32
|
|
||||||
+ b'\x13\x01\x00'
|
|
||||||
+ b'\x00\x2e'
|
|
||||||
+ b'\x00\x33\x00\x24\x00\x1d\x00\x20'
|
|
||||||
+ b'\x00' * 32
|
|
||||||
+ b'\x00\x2b\x00\x02\x03\x04'
|
|
||||||
)
|
|
||||||
|
|
||||||
_SH_RANDOM_OFF = 11
|
|
||||||
_SH_SESSID_OFF = 44
|
|
||||||
_SH_PUBKEY_OFF = 89
|
|
||||||
|
|
||||||
|
|
||||||
def verify_client_hello(data: bytes, secret: bytes) -> Optional[Tuple[bytes, bytes, int]]:
|
|
||||||
n = len(data)
|
|
||||||
# 5 (record hdr) + 6 (hs type+len+version) + 32 (random) = 43
|
|
||||||
if n < 43:
|
|
||||||
return None
|
|
||||||
if data[0] != TLS_RECORD_HANDSHAKE:
|
|
||||||
return None
|
|
||||||
if data[5] != 0x01:
|
|
||||||
return None
|
|
||||||
|
|
||||||
client_random = bytes(data[CLIENT_RANDOM_OFFSET:CLIENT_RANDOM_OFFSET + CLIENT_RANDOM_LEN])
|
|
||||||
|
|
||||||
zeroed = bytearray(data)
|
|
||||||
zeroed[CLIENT_RANDOM_OFFSET:CLIENT_RANDOM_OFFSET + CLIENT_RANDOM_LEN] = b'\x00' * CLIENT_RANDOM_LEN
|
|
||||||
|
|
||||||
expected = hmac.new(secret, bytes(zeroed), hashlib.sha256).digest()
|
|
||||||
|
|
||||||
if not hmac.compare_digest(expected[:28], client_random[:28]):
|
|
||||||
return None
|
|
||||||
|
|
||||||
ts_xor = bytes(client_random[28 + i] ^ expected[28 + i] for i in range(4))
|
|
||||||
timestamp = struct.unpack('<I', ts_xor)[0]
|
|
||||||
|
|
||||||
now = int(time.time())
|
|
||||||
if abs(now - timestamp) > TIMESTAMP_TOLERANCE:
|
|
||||||
return None
|
|
||||||
|
|
||||||
session_id = b'\x00' * SESSION_ID_LEN
|
|
||||||
if n >= SESSION_ID_OFFSET + SESSION_ID_LEN and data[43] == 0x20:
|
|
||||||
session_id = bytes(data[SESSION_ID_OFFSET:SESSION_ID_OFFSET + SESSION_ID_LEN])
|
|
||||||
|
|
||||||
return client_random, session_id, timestamp
|
|
||||||
|
|
||||||
|
|
||||||
def build_server_hello(secret: bytes, client_random: bytes, session_id: bytes) -> bytes:
|
|
||||||
sh = bytearray(_SERVER_HELLO_TEMPLATE)
|
|
||||||
sh[_SH_SESSID_OFF:_SH_SESSID_OFF + 32] = session_id
|
|
||||||
sh[_SH_PUBKEY_OFF:_SH_PUBKEY_OFF + 32] = os.urandom(32)
|
|
||||||
|
|
||||||
ccs = _CCS_FRAME
|
|
||||||
encrypted_size = random.randint(1900, 2100)
|
|
||||||
encrypted_data = os.urandom(encrypted_size)
|
|
||||||
app_record = b'\x17\x03\x03' + struct.pack('>H', encrypted_size) + encrypted_data
|
|
||||||
|
|
||||||
response = bytes(sh) + ccs + app_record
|
|
||||||
|
|
||||||
hmac_input = client_random + response
|
|
||||||
server_random = hmac.new(secret, hmac_input, hashlib.sha256).digest()
|
|
||||||
|
|
||||||
final = bytearray(response)
|
|
||||||
final[_SH_RANDOM_OFF:_SH_RANDOM_OFF + 32] = server_random
|
|
||||||
|
|
||||||
return bytes(final)
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_tls_record(data: bytes) -> bytes:
|
|
||||||
parts = []
|
|
||||||
offset = 0
|
|
||||||
while offset < len(data):
|
|
||||||
chunk = data[offset:offset + TLS_APPDATA_MAX]
|
|
||||||
parts.append(
|
|
||||||
b'\x17\x03\x03'
|
|
||||||
+ struct.pack('>H', len(chunk))
|
|
||||||
+ chunk
|
|
||||||
)
|
|
||||||
offset += len(chunk)
|
|
||||||
return b''.join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
class FakeTlsStream:
|
|
||||||
__slots__ = ('_reader', '_writer', '_read_buf', '_read_left')
|
|
||||||
|
|
||||||
def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
|
||||||
self._reader = reader
|
|
||||||
self._writer = writer
|
|
||||||
self._read_buf = bytearray()
|
|
||||||
self._read_left = 0
|
|
||||||
|
|
||||||
async def readexactly(self, n: int) -> bytes:
|
|
||||||
while len(self._read_buf) < n:
|
|
||||||
payload = await self._read_tls_payload()
|
|
||||||
if not payload:
|
|
||||||
raise asyncio.IncompleteReadError(bytes(self._read_buf), n)
|
|
||||||
self._read_buf.extend(payload)
|
|
||||||
result = bytes(self._read_buf[:n])
|
|
||||||
del self._read_buf[:n]
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def read(self, n: int) -> bytes:
|
|
||||||
if self._read_buf:
|
|
||||||
chunk = bytes(self._read_buf[:n])
|
|
||||||
del self._read_buf[:n]
|
|
||||||
return chunk
|
|
||||||
payload = await self._read_tls_payload()
|
|
||||||
if not payload:
|
|
||||||
return b''
|
|
||||||
if len(payload) > n:
|
|
||||||
self._read_buf.extend(payload[n:])
|
|
||||||
return payload[:n]
|
|
||||||
return payload
|
|
||||||
|
|
||||||
async def _read_tls_payload(self) -> bytes:
|
|
||||||
if self._read_left > 0:
|
|
||||||
data = await self._reader.read(self._read_left)
|
|
||||||
if not data:
|
|
||||||
return b''
|
|
||||||
self._read_left -= len(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
while True:
|
|
||||||
hdr = await self._reader.readexactly(5)
|
|
||||||
rtype = hdr[0]
|
|
||||||
rec_len = struct.unpack('>H', hdr[3:5])[0]
|
|
||||||
|
|
||||||
if rtype == TLS_RECORD_CCS:
|
|
||||||
if rec_len > 0:
|
|
||||||
await self._reader.readexactly(rec_len)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if rtype != TLS_RECORD_APPDATA:
|
|
||||||
return b''
|
|
||||||
|
|
||||||
data = await self._reader.read(min(rec_len, 65536))
|
|
||||||
if not data:
|
|
||||||
return b''
|
|
||||||
remaining = rec_len - len(data)
|
|
||||||
if remaining > 0:
|
|
||||||
self._read_left = remaining
|
|
||||||
return data
|
|
||||||
|
|
||||||
def write(self, data: bytes) -> None:
|
|
||||||
self._writer.write(wrap_tls_record(data))
|
|
||||||
|
|
||||||
async def drain(self) -> None:
|
|
||||||
await self._writer.drain()
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self._writer.close()
|
|
||||||
|
|
||||||
async def wait_closed(self) -> None:
|
|
||||||
await self._writer.wait_closed()
|
|
||||||
|
|
||||||
def get_extra_info(self, name, default=None):
|
|
||||||
return self._writer.get_extra_info(name, default)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def transport(self):
|
|
||||||
return self._writer.transport
|
|
||||||
|
|
||||||
def is_closing(self):
|
|
||||||
return self._writer.is_closing()
|
|
||||||
|
|
||||||
|
|
||||||
async def proxy_to_masking_domain(reader, writer, initial_data: bytes,
|
|
||||||
domain: str, label: str) -> None:
|
|
||||||
try:
|
|
||||||
up_reader, up_writer = await asyncio.wait_for(
|
|
||||||
asyncio.open_connection(domain, 443), timeout=10)
|
|
||||||
except Exception as exc:
|
|
||||||
log.debug("[%s] masking: cannot connect to %s:443: %s",
|
|
||||||
label, domain, exc)
|
|
||||||
return
|
|
||||||
|
|
||||||
log.debug("[%s] masking -> %s:443", label, domain)
|
|
||||||
stats.connections_masked += 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
if initial_data:
|
|
||||||
up_writer.write(initial_data)
|
|
||||||
await up_writer.drain()
|
|
||||||
|
|
||||||
async def _relay(src, dst):
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
chunk = await src.read(16384)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
dst.write(chunk)
|
|
||||||
await dst.drain()
|
|
||||||
except (ConnectionResetError, BrokenPipeError, OSError,
|
|
||||||
asyncio.CancelledError):
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
dst.close()
|
|
||||||
await dst.wait_closed()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
await asyncio.gather(
|
|
||||||
_relay(reader, up_writer),
|
|
||||||
_relay(up_reader, writer),
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
up_writer.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
@@ -8,7 +8,6 @@ class _Stats:
|
|||||||
self.connections_tcp_fallback = 0
|
self.connections_tcp_fallback = 0
|
||||||
self.connections_cfproxy = 0
|
self.connections_cfproxy = 0
|
||||||
self.connections_bad = 0
|
self.connections_bad = 0
|
||||||
self.connections_masked = 0
|
|
||||||
self.ws_errors = 0
|
self.ws_errors = 0
|
||||||
self.bytes_up = 0
|
self.bytes_up = 0
|
||||||
self.bytes_down = 0
|
self.bytes_down = 0
|
||||||
@@ -25,7 +24,6 @@ class _Stats:
|
|||||||
f"tcp_fb={self.connections_tcp_fallback} "
|
f"tcp_fb={self.connections_tcp_fallback} "
|
||||||
f"cf={self.connections_cfproxy} "
|
f"cf={self.connections_cfproxy} "
|
||||||
f"bad={self.connections_bad} "
|
f"bad={self.connections_bad} "
|
||||||
f"masked={self.connections_masked} "
|
|
||||||
f"err={self.ws_errors} "
|
f"err={self.ws_errors} "
|
||||||
f"pool={pool_s} "
|
f"pool={pool_s} "
|
||||||
f"up={human_bytes(self.bytes_up)} "
|
f"up={human_bytes(self.bytes_up)} "
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from .stats import stats
|
|||||||
from .config import proxy_config, parse_dc_ip_list, start_cfproxy_domain_refresh, CFPROXY_DEFAULT_DOMAINS
|
from .config import proxy_config, parse_dc_ip_list, start_cfproxy_domain_refresh, CFPROXY_DEFAULT_DOMAINS
|
||||||
from .bridge import MsgSplitter, CryptoCtx, do_fallback, bridge_ws_reencrypt
|
from .bridge import MsgSplitter, CryptoCtx, do_fallback, bridge_ws_reencrypt
|
||||||
from .raw_websocket import RawWebSocket, WsHandshakeError, set_sock_opts
|
from .raw_websocket import RawWebSocket, WsHandshakeError, set_sock_opts
|
||||||
from .fake_tls import proxy_to_masking_domain, verify_client_hello, build_server_hello, FakeTlsStream, TLS_RECORD_HANDSHAKE
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('tg-mtproto-proxy')
|
log = logging.getLogger('tg-mtproto-proxy')
|
||||||
@@ -215,115 +214,25 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
|
|
||||||
set_sock_opts(writer.transport, proxy_config.buffer_size)
|
set_sock_opts(writer.transport, proxy_config.buffer_size)
|
||||||
|
|
||||||
tls_stream = None
|
|
||||||
masking = proxy_config.fake_tls_domain
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if proxy_config.proxy_protocol:
|
|
||||||
try:
|
|
||||||
pp_line = await asyncio.wait_for(
|
|
||||||
reader.readline(), timeout=10)
|
|
||||||
except asyncio.IncompleteReadError:
|
|
||||||
log.debug("[%s] disconnected during PROXY header", label)
|
|
||||||
return
|
|
||||||
pp_text = pp_line.decode('ascii', errors='replace').strip()
|
|
||||||
if pp_text.startswith('PROXY '):
|
|
||||||
parts = pp_text.split()
|
|
||||||
if len(parts) >= 6:
|
|
||||||
label = f"{parts[2]}:{parts[4]}"
|
|
||||||
log.debug("[%s] PROXY protocol: %s", label, pp_text)
|
|
||||||
else:
|
|
||||||
log.debug("[%s] expected PROXY header, got: %r", label,
|
|
||||||
pp_text[:60])
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
first_byte = await asyncio.wait_for(
|
handshake = await asyncio.wait_for(
|
||||||
reader.readexactly(1), timeout=10)
|
reader.readexactly(HANDSHAKE_LEN), timeout=10)
|
||||||
except asyncio.IncompleteReadError:
|
except asyncio.IncompleteReadError:
|
||||||
log.debug("[%s] client disconnected before handshake", label)
|
log.debug("[%s] client disconnected before handshake", label)
|
||||||
return
|
return
|
||||||
|
|
||||||
if first_byte[0] == TLS_RECORD_HANDSHAKE and masking:
|
|
||||||
try:
|
|
||||||
hdr_rest = await asyncio.wait_for(
|
|
||||||
reader.readexactly(4), timeout=10)
|
|
||||||
except asyncio.IncompleteReadError:
|
|
||||||
log.debug("[%s] incomplete TLS record header", label)
|
|
||||||
return
|
|
||||||
|
|
||||||
tls_header = first_byte + hdr_rest
|
|
||||||
record_len = struct.unpack('>H', tls_header[3:5])[0]
|
|
||||||
|
|
||||||
try:
|
|
||||||
record_body = await asyncio.wait_for(
|
|
||||||
reader.readexactly(record_len), timeout=10)
|
|
||||||
except asyncio.IncompleteReadError:
|
|
||||||
log.debug("[%s] incomplete TLS record body", label)
|
|
||||||
return
|
|
||||||
|
|
||||||
client_hello = tls_header + record_body
|
|
||||||
|
|
||||||
tls_result = verify_client_hello(client_hello, secret)
|
|
||||||
|
|
||||||
if tls_result is None:
|
|
||||||
log.debug("[%s] Fake TLS verify failed (size=%d rec=%d) "
|
|
||||||
"-> masking",
|
|
||||||
label, len(client_hello), record_len)
|
|
||||||
await proxy_to_masking_domain(
|
|
||||||
reader, writer, client_hello, masking, label)
|
|
||||||
return
|
|
||||||
|
|
||||||
client_random, session_id, ts = tls_result
|
|
||||||
log.debug("[%s] Fake TLS handshake ok (ts=%d)", label, ts)
|
|
||||||
|
|
||||||
server_hello = build_server_hello(secret, client_random, session_id)
|
|
||||||
writer.write(server_hello)
|
|
||||||
await writer.drain()
|
|
||||||
|
|
||||||
tls_stream = FakeTlsStream(reader, writer)
|
|
||||||
|
|
||||||
try:
|
|
||||||
handshake = await asyncio.wait_for(
|
|
||||||
tls_stream.readexactly(HANDSHAKE_LEN), timeout=10)
|
|
||||||
except asyncio.IncompleteReadError:
|
|
||||||
log.debug("[%s] incomplete obfs2 init inside TLS", label)
|
|
||||||
return
|
|
||||||
elif masking:
|
|
||||||
log.debug("[%s] non-TLS byte 0x%02X -> HTTP redirect", label,
|
|
||||||
first_byte[0])
|
|
||||||
redirect = (
|
|
||||||
f"HTTP/1.1 301 Moved Permanently\r\n"
|
|
||||||
f"Location: https://{masking}/\r\n"
|
|
||||||
f"Content-Length: 0\r\n"
|
|
||||||
f"Connection: close\r\n\r\n"
|
|
||||||
).encode()
|
|
||||||
writer.write(redirect)
|
|
||||||
await writer.drain()
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
rest = await asyncio.wait_for(
|
|
||||||
reader.readexactly(HANDSHAKE_LEN - 1), timeout=10)
|
|
||||||
except asyncio.IncompleteReadError:
|
|
||||||
log.debug("[%s] client disconnected before handshake", label)
|
|
||||||
return
|
|
||||||
handshake = first_byte + rest
|
|
||||||
|
|
||||||
result = _try_handshake(handshake, secret)
|
result = _try_handshake(handshake, secret)
|
||||||
if result is None:
|
if result is None:
|
||||||
stats.connections_bad += 1
|
stats.connections_bad += 1
|
||||||
log.debug("[%s] bad handshake (wrong secret or proto)", label)
|
log.debug("[%s] bad handshake (wrong secret or proto)", label)
|
||||||
try:
|
try:
|
||||||
drain_src = tls_stream or reader
|
while await reader.read(4096):
|
||||||
while await drain_src.read(4096):
|
|
||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
clt_reader = tls_stream or reader
|
|
||||||
clt_writer = tls_stream or writer
|
|
||||||
|
|
||||||
dc, is_media, proto_tag, client_dec_prekey_iv = result
|
dc, is_media, proto_tag, client_dec_prekey_iv = result
|
||||||
|
|
||||||
if proto_tag == PROTO_TAG_ABRIDGED:
|
if proto_tag == PROTO_TAG_ABRIDGED:
|
||||||
@@ -399,7 +308,7 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
ok = await do_fallback(
|
ok = await do_fallback(
|
||||||
clt_reader, clt_writer, relay_init, label,
|
reader, writer, relay_init, label,
|
||||||
dc, is_media, media_tag,
|
dc, is_media, media_tag,
|
||||||
ctx, splitter=splitter)
|
ctx, splitter=splitter)
|
||||||
if not ok:
|
if not ok:
|
||||||
@@ -469,7 +378,7 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
ok = await do_fallback(
|
ok = await do_fallback(
|
||||||
clt_reader, clt_writer, relay_init, label,
|
reader, writer, relay_init, label,
|
||||||
dc, is_media, media_tag,
|
dc, is_media, media_tag,
|
||||||
ctx, splitter=splitter_fb)
|
ctx, splitter=splitter_fb)
|
||||||
if ok:
|
if ok:
|
||||||
@@ -490,7 +399,7 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
|
|
||||||
await ws.send(relay_init)
|
await ws.send(relay_init)
|
||||||
|
|
||||||
await bridge_ws_reencrypt(clt_reader, clt_writer, ws, label,
|
await bridge_ws_reencrypt(reader, writer, ws, label,
|
||||||
dc=dc, is_media=is_media,
|
dc=dc, is_media=is_media,
|
||||||
ctx=ctx, splitter=splitter)
|
ctx=ctx, splitter=splitter)
|
||||||
|
|
||||||
@@ -513,7 +422,6 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
stats.connections_active -= 1
|
stats.connections_active -= 1
|
||||||
try:
|
try:
|
||||||
writer.close()
|
writer.close()
|
||||||
await writer.wait_closed()
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -559,23 +467,12 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
link_host = get_link_host(proxy_config.host)
|
link_host = get_link_host(proxy_config.host)
|
||||||
ftls = proxy_config.fake_tls_domain
|
tg_link = f"tg://proxy?server={link_host}&port={proxy_config.port}&secret=dd{proxy_config.secret}"
|
||||||
dd_link = (f"tg://proxy?server={link_host}"
|
|
||||||
f"&port={proxy_config.port}"
|
|
||||||
f"&secret=dd{proxy_config.secret}")
|
|
||||||
ee_link = ""
|
|
||||||
if ftls:
|
|
||||||
domain_hex = ftls.encode('ascii').hex()
|
|
||||||
ee_link = (f"tg://proxy?server={link_host}"
|
|
||||||
f"&port={proxy_config.port}"
|
|
||||||
f"&secret=ee{proxy_config.secret}{domain_hex}")
|
|
||||||
|
|
||||||
log.info("=" * 60)
|
log.info("=" * 60)
|
||||||
log.info(" Telegram MTProto WS Bridge Proxy")
|
log.info(" Telegram MTProto WS Bridge Proxy")
|
||||||
log.info(" Listening on %s:%d", proxy_config.host, proxy_config.port)
|
log.info(" Listening on %s:%d", proxy_config.host, proxy_config.port)
|
||||||
log.info(" Secret: %s", proxy_config.secret)
|
log.info(" Secret: %s", proxy_config.secret)
|
||||||
if ftls:
|
|
||||||
log.info(" Fake TLS: %s", ftls)
|
|
||||||
log.info(" Target DC IPs:")
|
log.info(" Target DC IPs:")
|
||||||
for dc in sorted(proxy_config.dc_redirects.keys()):
|
for dc in sorted(proxy_config.dc_redirects.keys()):
|
||||||
ip = proxy_config.dc_redirects.get(dc)
|
ip = proxy_config.dc_redirects.get(dc)
|
||||||
@@ -585,12 +482,8 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
|
|||||||
user_domain = "user" if proxy_config.cfproxy_user_domain else "auto"
|
user_domain = "user" if proxy_config.cfproxy_user_domain else "auto"
|
||||||
log.info(" CF proxy: enabled (%s | %s)", prio, user_domain)
|
log.info(" CF proxy: enabled (%s | %s)", prio, user_domain)
|
||||||
log.info("=" * 60)
|
log.info("=" * 60)
|
||||||
log.info(" Connect links:")
|
log.info(" Connect link:")
|
||||||
if ftls:
|
log.info(" %s", tg_link)
|
||||||
log.info(" ee (Fake TLS): %s", ee_link)
|
|
||||||
else:
|
|
||||||
log.info(" (standard): %s", proxy_config.secret)
|
|
||||||
log.info(" dd (random padding): %s", dd_link)
|
|
||||||
log.info("=" * 60)
|
log.info("=" * 60)
|
||||||
|
|
||||||
async def log_stats():
|
async def log_stats():
|
||||||
@@ -676,13 +569,6 @@ def main():
|
|||||||
help='Disable Cloudflare proxy fallback')
|
help='Disable Cloudflare proxy fallback')
|
||||||
ap.add_argument('--cfproxy-priority', type=bool, default=True,
|
ap.add_argument('--cfproxy-priority', type=bool, default=True,
|
||||||
help='Try cfproxy before tcp fallback (default: true)')
|
help='Try cfproxy before tcp fallback (default: true)')
|
||||||
ap.add_argument('--fake-tls-domain', type=str, default='',
|
|
||||||
metavar='DOMAIN',
|
|
||||||
help='Enable Fake TLS (ee-secret) masking with the given '
|
|
||||||
'SNI domain, e.g. example.com')
|
|
||||||
ap.add_argument('--proxy-protocol', action='store_true',
|
|
||||||
help='Accept PROXY protocol v1 header '
|
|
||||||
'(for use behind nginx/haproxy with proxy_protocol on)')
|
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
if not args.dc_ip:
|
if not args.dc_ip:
|
||||||
@@ -717,8 +603,6 @@ def main():
|
|||||||
proxy_config.fallback_cfproxy = not args.no_cfproxy
|
proxy_config.fallback_cfproxy = not args.no_cfproxy
|
||||||
proxy_config.fallback_cfproxy_priority = args.cfproxy_priority
|
proxy_config.fallback_cfproxy_priority = args.cfproxy_priority
|
||||||
proxy_config.cfproxy_user_domain = args.cfproxy_domain
|
proxy_config.cfproxy_user_domain = args.cfproxy_domain
|
||||||
proxy_config.fake_tls_domain = args.fake_tls_domain.strip()
|
|
||||||
proxy_config.proxy_protocol = args.proxy_protocol
|
|
||||||
|
|
||||||
log_level = logging.DEBUG if args.verbose else logging.INFO
|
log_level = logging.DEBUG if args.verbose else logging.INFO
|
||||||
log_fmt = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s',
|
log_fmt = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s',
|
||||||
@@ -740,8 +624,6 @@ def main():
|
|||||||
fh.setFormatter(log_fmt)
|
fh.setFormatter(log_fmt)
|
||||||
root.addHandler(fh)
|
root.addHandler(fh)
|
||||||
|
|
||||||
logging.getLogger('asyncio').setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
asyncio.run(_run())
|
asyncio.run(_run())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from proxy import __version__, get_link_host, parse_dc_ip_list
|
from proxy import __version__, get_link_host, parse_dc_ip_list
|
||||||
from proxy.config import proxy_config
|
from proxy.config import CFPROXY_DEFAULT_DOMAINS
|
||||||
from utils.update_check import RELEASES_PAGE_URL, get_status
|
from utils.update_check import RELEASES_PAGE_URL, get_status
|
||||||
|
|
||||||
|
|
||||||
@@ -121,19 +121,13 @@ def _run_cfproxy_connectivity_test(domain: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _run_cfproxy_auto_test(domains: list) -> tuple:
|
def _run_cfproxy_auto_test(domains: list) -> tuple:
|
||||||
merged: dict = {}
|
last: dict = {}
|
||||||
best_domain = None
|
for domain in domains:
|
||||||
for domain in reversed(domains):
|
|
||||||
res = _run_cfproxy_connectivity_test(domain)
|
res = _run_cfproxy_connectivity_test(domain)
|
||||||
if all(v is True for v in res.values()):
|
last = res
|
||||||
|
if any(v is True for v in res.values()):
|
||||||
return domain, res
|
return domain, res
|
||||||
for dc, v in res.items():
|
return None, last
|
||||||
if v is True:
|
|
||||||
merged[dc] = True
|
|
||||||
best_domain = domain
|
|
||||||
elif dc not in merged:
|
|
||||||
merged[dc] = v
|
|
||||||
return best_domain, merged
|
|
||||||
|
|
||||||
|
|
||||||
def _cfproxy_show_test_results(domain: str, results: dict) -> None:
|
def _cfproxy_show_test_results(domain: str, results: dict) -> None:
|
||||||
@@ -314,7 +308,7 @@ def install_tray_config_form(
|
|||||||
header = ctk.CTkFrame(frame, fg_color="transparent")
|
header = ctk.CTkFrame(frame, fg_color="transparent")
|
||||||
header.pack(fill="x", pady=(0, 2))
|
header.pack(fill="x", pady=(0, 2))
|
||||||
ctk.CTkLabel(
|
ctk.CTkLabel(
|
||||||
header, text="Настройки",
|
header, text="Настройки прокси",
|
||||||
font=(theme.ui_font_family, 17, "bold"),
|
font=(theme.ui_font_family, 17, "bold"),
|
||||||
text_color=theme.text_primary, anchor="w",
|
text_color=theme.text_primary, anchor="w",
|
||||||
).pack(side="left")
|
).pack(side="left")
|
||||||
@@ -351,17 +345,6 @@ def install_tray_config_form(
|
|||||||
command=_on_appearance_change,
|
command=_on_appearance_change,
|
||||||
).pack(side="right")
|
).pack(side="right")
|
||||||
|
|
||||||
ctk.CTkButton(
|
|
||||||
header, text="Donate ♥", width=90, height=28,
|
|
||||||
font=(theme.ui_font_family, 13, "bold"), corner_radius=8,
|
|
||||||
fg_color="#22c55e", hover_color="#16a34a",
|
|
||||||
text_color="#ffffff", border_width=0,
|
|
||||||
command=lambda: (
|
|
||||||
header.winfo_toplevel().iconify(),
|
|
||||||
webbrowser.open("https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/README.md"),
|
|
||||||
),
|
|
||||||
).pack(side="right", padx=(0, 6))
|
|
||||||
|
|
||||||
conn = _config_section(ctk, frame, theme, "Подключение MTProto")
|
conn = _config_section(ctk, frame, theme, "Подключение MTProto")
|
||||||
|
|
||||||
host_row = ctk.CTkFrame(conn, fg_color="transparent")
|
host_row = ctk.CTkFrame(conn, fg_color="transparent")
|
||||||
@@ -451,7 +434,7 @@ def install_tray_config_form(
|
|||||||
_threading.Thread(target=_worker, daemon=True).start()
|
_threading.Thread(target=_worker, daemon=True).start()
|
||||||
else:
|
else:
|
||||||
def _worker_auto():
|
def _worker_auto():
|
||||||
ok_domain, res = _run_cfproxy_auto_test(proxy_config.cfproxy_domains)
|
ok_domain, res = _run_cfproxy_auto_test(CFPROXY_DEFAULT_DOMAINS)
|
||||||
if btn:
|
if btn:
|
||||||
btn.after(0, lambda: btn.configure(text="Тест", state="normal"))
|
btn.after(0, lambda: btn.configure(text="Тест", state="normal"))
|
||||||
btn.after(0, lambda: _cfproxy_show_auto_test_results(ok_domain, res))
|
btn.after(0, lambda: _cfproxy_show_auto_test_results(ok_domain, res))
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ def ensure_dirs() -> None:
|
|||||||
_lock_file_path: Optional[Path] = None
|
_lock_file_path: Optional[Path] = None
|
||||||
|
|
||||||
|
|
||||||
def _same_process(meta: dict, proc: psutil.Process) -> bool:
|
def _same_process(meta: dict, proc: psutil.Process, script_hint: str) -> bool:
|
||||||
try:
|
try:
|
||||||
lock_ct = float(meta.get("create_time", 0.0))
|
lock_ct = float(meta.get("create_time", 0.0))
|
||||||
if lock_ct > 0 and abs(lock_ct - proc.create_time()) > 1.0:
|
if lock_ct > 0 and abs(lock_ct - proc.create_time()) > 1.0:
|
||||||
@@ -63,7 +63,7 @@ def _same_process(meta: dict, proc: psutil.Process) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def acquire_lock() -> bool:
|
def acquire_lock(script_hint: str = "") -> bool:
|
||||||
global _lock_file_path
|
global _lock_file_path
|
||||||
ensure_dirs()
|
ensure_dirs()
|
||||||
for f in list(APP_DIR.glob("*.lock")):
|
for f in list(APP_DIR.glob("*.lock")):
|
||||||
@@ -84,7 +84,7 @@ def acquire_lock() -> bool:
|
|||||||
pass
|
pass
|
||||||
is_running = False
|
is_running = False
|
||||||
try:
|
try:
|
||||||
is_running = _same_process(meta, psutil.Process(pid))
|
is_running = _same_process(meta, psutil.Process(pid), script_hint)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if is_running:
|
if is_running:
|
||||||
@@ -153,7 +153,6 @@ def setup_logging(verbose: bool = False, log_max_mb: float = 5) -> None:
|
|||||||
level = logging.DEBUG if verbose else logging.INFO
|
level = logging.DEBUG if verbose else logging.INFO
|
||||||
root = logging.getLogger()
|
root = logging.getLogger()
|
||||||
root.setLevel(level)
|
root.setLevel(level)
|
||||||
logging.getLogger('asyncio').setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
fh = logging.handlers.RotatingFileHandler(
|
fh = logging.handlers.RotatingFileHandler(
|
||||||
str(LOG_FILE),
|
str(LOG_FILE),
|
||||||
|
|||||||
37
windows.py
37
windows.py
@@ -56,39 +56,6 @@ from ui.ctk_theme import (
|
|||||||
_tray_icon: Optional[object] = None
|
_tray_icon: Optional[object] = None
|
||||||
_config: dict = {}
|
_config: dict = {}
|
||||||
_exiting = False
|
_exiting = False
|
||||||
_win_mutex_handle = None
|
|
||||||
|
|
||||||
_ERROR_ALREADY_EXISTS = 183
|
|
||||||
|
|
||||||
|
|
||||||
def _acquire_win_mutex() -> bool | None:
|
|
||||||
global _win_mutex_handle
|
|
||||||
try:
|
|
||||||
kernel32 = ctypes.windll.kernel32
|
|
||||||
kernel32.CreateMutexW.restype = ctypes.c_void_p
|
|
||||||
kernel32.CreateMutexW.argtypes = [ctypes.c_void_p, ctypes.c_bool, ctypes.c_wchar_p]
|
|
||||||
handle = kernel32.CreateMutexW(None, True, "Local\\TgWsProxy_SingleInstance")
|
|
||||||
if kernel32.GetLastError() == _ERROR_ALREADY_EXISTS:
|
|
||||||
kernel32.CloseHandle(ctypes.c_void_p(handle))
|
|
||||||
return False
|
|
||||||
if not handle:
|
|
||||||
return None
|
|
||||||
_win_mutex_handle = handle
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _release_win_mutex() -> None:
|
|
||||||
global _win_mutex_handle
|
|
||||||
if _win_mutex_handle:
|
|
||||||
try:
|
|
||||||
kernel32 = ctypes.windll.kernel32
|
|
||||||
kernel32.ReleaseMutex(ctypes.c_void_p(_win_mutex_handle))
|
|
||||||
kernel32.CloseHandle(ctypes.c_void_p(_win_mutex_handle))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
_win_mutex_handle = None
|
|
||||||
|
|
||||||
ICON_PATH = str(Path(__file__).parent / "icon.ico")
|
ICON_PATH = str(Path(__file__).parent / "icon.ico")
|
||||||
|
|
||||||
@@ -383,15 +350,13 @@ def run_tray() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if (mutex_result := _acquire_win_mutex()) is False or mutex_result is None and not acquire_lock():
|
if not acquire_lock("windows.py"):
|
||||||
_show_info("Приложение уже запущено.", os.path.basename(sys.argv[0]))
|
_show_info("Приложение уже запущено.", os.path.basename(sys.argv[0]))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
run_tray()
|
run_tray()
|
||||||
finally:
|
finally:
|
||||||
release_lock()
|
release_lock()
|
||||||
_release_win_mutex()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user