14 Commits

Author SHA1 Message Date
Flowseal
b48ac67b9f donate web link 2026-04-11 21:27:21 +03:00
Flowseal
937acdb461 Version bump 2026-04-11 21:09:46 +03:00
Flowseal
6f3da84e48 Refresh domains schedule 2026-04-11 21:09:08 +03:00
Flowseal
3c3e9eb34b fix domains testing 2026-04-11 21:03:53 +03:00
Flowseal
ba89cad8b8 fake-tls cli 2026-04-11 20:52:24 +03:00
Flowseal
bf905ec54f docs update 2026-04-11 19:11:47 +03:00
Flowseal
ace0a5e968 docs update 2026-04-11 18:54:32 +03:00
Flowseal
e47eef4709 docs update 2026-04-11 15:28:37 +03:00
Flowseal
abe1d1f01e docs update 2026-04-11 15:28:37 +03:00
Flowseal
cc31c02c9d donate button ctk 2026-04-11 15:28:37 +03:00
Flowseal
f39bb15ff6 docs update 2026-04-11 15:28:31 +03:00
kreker06
5a62cd82b2 Update Dockerfile (#586) 2026-04-11 14:54:32 +03:00
Flowseal
fe4e0e8234 docs update 2026-04-10 19:28:36 +03:00
Flowseal
172dc67093 docs update 2026-04-10 02:57:25 +03:00
12 changed files with 521 additions and 42 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: ['https://nowpayments.io/donation/flowseal']

View File

@@ -1,2 +1,5 @@
virkgj.com
vmmzovy.com
mkuosckvso.com
zaewayzmplad.com
twdmbzcm.com

View File

@@ -41,5 +41,5 @@ USER app
EXPOSE 1443/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 \"$@\"", "--"]
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 /opt/venv/bin/python -u proxy/tg_ws_proxy.py $args \"$@\"", "--"]
CMD []

View File

@@ -20,7 +20,7 @@ Cloudflare имеет лимиты на одновременное количе
- Name=`kws5` IPv4=`149.154.171.5`
- 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 поменяйте домен на свой

View File

@@ -2,9 +2,10 @@
>
> ### 🎉 Поддержать меня
>
> USDT (TRC20): `TXPnKs2Ww1RD8JN6nChFUVmi5r2hqrWjuu`
> BTC: `bc1qr8vd6jelkyyry3m4mq6z5txdx4pl856fu6ss0w`
> ETH: `0x1417878fdc5047E670a77748B34819b9A49C72F1`
> **USDT (TRC20)**: `TXPnKs2Ww1RD8JN6nChFUVmi5r2hqrWjuu`
> **BTC**: `bc1qr8vd6jelkyyry3m4mq6z5txdx4pl856fu6ss0w`
> **ETH**: `0x1417878fdc5047E670a77748B34819b9A49C72F1`
> **Другие монеты**: https://nowpayments.io/donation/flowseal
> [!CAUTION]
>
@@ -38,7 +39,9 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
> [!IMPORTANT]
> ### Не грузит фото/видео?
> ### Удалите в настройках прокси в DC->IP всё, кроме `4:149.154.167.220`
> **Удалите в настройках прокси в DC->IP всё, кроме `4:149.154.167.220`**
> **Если не помогло, то удалите вообще всё из этого поля**
> ####
> Подобная проблема встречается на аккаунтах без Premium
> Если вам не помогло, то настраивайте свой домен по гайду отсюда: https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md
@@ -53,6 +56,7 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
**Меню трея:**
- **Открыть в Telegram** — автоматически настроить прокси через `tg://proxy` ссылку
- **Скопировать ссылку** — скопировать ссылку для подключения
- **Перезапустить прокси** — перезапуск без выхода из приложения
- **Настройки...** — GUI-редактор конфигурации (в т.ч. версия приложения, опциональная проверка обновлений с GitHub)
- **Открыть логи** — открыть файл логов
@@ -60,6 +64,26 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
При первом запуске после старта может появиться запрос об открытии страницы релиза, если на GitHub вышла новая версия (отключается в настройках).
### Настройка Telegram Desktop
### Автоматически:
ПКМ по иконке в трее → **«Открыть в Telegram»**
Если не сработало (не открылся Telegram с подключением), то:
1. ПКМ по иконке в трее → **«Скопировать ссылку»**
2. Отправьте ссылку себе в избранное в Telegram клиенте и нажмите по ней ЛКМ
3. Подключитесь
### Вручную:
1. Telegram → **Настройки****Продвинутые настройки****Тип подключения****Прокси**
2. Добавить прокси:
- **Тип:** MTProto
- **Сервер:** `127.0.0.1` (или переопределенный вами)
- **Порт:** `1443` (или переопределенный вами)
- **Secret:** из настроек или логов
##
### macOS
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_macos_universal.dmg`** — универсальная сборка для Apple Silicon и Intel.
@@ -146,6 +170,8 @@ 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)) |
| `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudfalre. [Подробнее тут](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md) |
| `--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` | Размер буфера в КБ |
| `--pool-size` | `4` | Количество заготовленных соединений на каждый DC |
| `--log-file` | выкл. | Путь до файла, в который сохранять логи |
@@ -164,24 +190,64 @@ tg-ws-proxy --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149.154.167.220
# С подробным логированием
tg-ws-proxy -v
# Fake TLS маскировка (ee-secret)
tg-ws-proxy --fake-tls-domain example.com
```
## Настройка Telegram Desktop
## Fake TLS + nginx upstream
### Домен (`--fake-tls-domain`) должен указывать на тот же IP, на котором стоит прокси
### Автоматически
**Пример `nginx.conf` (stream):**
ПКМ по иконке в трее → **«Открыть в Telegram»**
```nginx
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;
}
1. Telegram → **Настройки****Продвинутые настройки****Тип подключения****Прокси**
2. Добавить прокси:
- **Тип:** MTProto
- **Сервер:** `127.0.0.1` (или переопределенный вами)
- **Порт:** `1443` (или переопределенный вами)
- **Secret:** из настроек или логов
# upstream xray {
# server 127.0.0.1:8443;
# }
#
# upstream www {
# server 127.0.0.1:7443;
# }
## Конфигурация
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-приложение хранит данные в:
@@ -202,7 +268,11 @@ Tray-приложение хранит данные в:
"buf_kb": 256,
"pool_size": 4,
"log_max_mb": 5.0,
"check_updates": true
"check_updates": true,
"cfproxy": true,
"cfproxy_priority": true,
"cfproxy_user_domain": "",
"appearance": "auto"
}
```

View File

@@ -1,6 +1,6 @@
from .config import parse_dc_ip_list, proxy_config
from .utils import get_link_host
__version__ = "1.6.0"
__version__ = "1.6.1"
__all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list"]

View File

@@ -16,7 +16,7 @@ CFPROXY_DOMAINS_URL = (
"/.github/cfproxy-domains.txt"
)
_CFPROXY_ENC: List[str] = ['virkgj.com']
_CFPROXY_ENC: List[str] = ['virkgj.com', 'vmmzovy.com', 'mkuosckvso.com', 'zaewayzmplad.com', 'twdmbzcm.com']
_S = ''.join(chr(c) for c in (46, 99, 111, 46, 117, 107))
@@ -47,6 +47,8 @@ class ProxyConfig:
cfproxy_user_domain: str = ''
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))
fake_tls_domain: str = ''
proxy_protocol: bool = False
proxy_config = ProxyConfig()
@@ -85,12 +87,21 @@ def refresh_cfproxy_domains() -> None:
proxy_config.active_cfproxy_domain = random.choice(pool)
_refresh_stop: threading.Event = threading.Event()
def start_cfproxy_domain_refresh() -> None:
threading.Thread(
target=refresh_cfproxy_domains,
daemon=True,
name='cfproxy-domains-refresh',
).start()
global _refresh_stop
_refresh_stop.set()
_refresh_stop = threading.Event()
stop = _refresh_stop
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]:

256
proxy/fake_tls.py Normal file
View File

@@ -0,0 +1,256 @@
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

View File

@@ -8,6 +8,7 @@ class _Stats:
self.connections_tcp_fallback = 0
self.connections_cfproxy = 0
self.connections_bad = 0
self.connections_masked = 0
self.ws_errors = 0
self.bytes_up = 0
self.bytes_down = 0
@@ -24,6 +25,7 @@ class _Stats:
f"tcp_fb={self.connections_tcp_fallback} "
f"cf={self.connections_cfproxy} "
f"bad={self.connections_bad} "
f"masked={self.connections_masked} "
f"err={self.ws_errors} "
f"pool={pool_s} "
f"up={human_bytes(self.bytes_up)} "

View File

@@ -28,6 +28,7 @@ from .stats import stats
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 .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')
@@ -214,25 +215,115 @@ async def _handle_client(reader, writer, secret: bytes):
set_sock_opts(writer.transport, proxy_config.buffer_size)
tls_stream = None
masking = proxy_config.fake_tls_domain
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:
handshake = await asyncio.wait_for(
reader.readexactly(HANDSHAKE_LEN), timeout=10)
first_byte = await asyncio.wait_for(
reader.readexactly(1), timeout=10)
except asyncio.IncompleteReadError:
log.debug("[%s] client disconnected before handshake", label)
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)
if result is None:
stats.connections_bad += 1
log.debug("[%s] bad handshake (wrong secret or proto)", label)
try:
while await reader.read(4096):
drain_src = tls_stream or reader
while await drain_src.read(4096):
pass
except Exception:
pass
return
clt_reader = tls_stream or reader
clt_writer = tls_stream or writer
dc, is_media, proto_tag, client_dec_prekey_iv = result
if proto_tag == PROTO_TAG_ABRIDGED:
@@ -308,7 +399,7 @@ async def _handle_client(reader, writer, secret: bytes):
except Exception:
pass
ok = await do_fallback(
reader, writer, relay_init, label,
clt_reader, clt_writer, relay_init, label,
dc, is_media, media_tag,
ctx, splitter=splitter)
if not ok:
@@ -378,7 +469,7 @@ async def _handle_client(reader, writer, secret: bytes):
except Exception:
pass
ok = await do_fallback(
reader, writer, relay_init, label,
clt_reader, clt_writer, relay_init, label,
dc, is_media, media_tag,
ctx, splitter=splitter_fb)
if ok:
@@ -399,7 +490,7 @@ async def _handle_client(reader, writer, secret: bytes):
await ws.send(relay_init)
await bridge_ws_reencrypt(reader, writer, ws, label,
await bridge_ws_reencrypt(clt_reader, clt_writer, ws, label,
dc=dc, is_media=is_media,
ctx=ctx, splitter=splitter)
@@ -422,6 +513,7 @@ async def _handle_client(reader, writer, secret: bytes):
stats.connections_active -= 1
try:
writer.close()
await writer.wait_closed()
except BaseException:
pass
@@ -467,12 +559,23 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
pass
link_host = get_link_host(proxy_config.host)
tg_link = f"tg://proxy?server={link_host}&port={proxy_config.port}&secret=dd{proxy_config.secret}"
ftls = proxy_config.fake_tls_domain
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(" Telegram MTProto WS Bridge Proxy")
log.info(" Listening on %s:%d", proxy_config.host, proxy_config.port)
log.info(" Secret: %s", proxy_config.secret)
if ftls:
log.info(" Fake TLS: %s", ftls)
log.info(" Target DC IPs:")
for dc in sorted(proxy_config.dc_redirects.keys()):
ip = proxy_config.dc_redirects.get(dc)
@@ -482,8 +585,12 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
user_domain = "user" if proxy_config.cfproxy_user_domain else "auto"
log.info(" CF proxy: enabled (%s | %s)", prio, user_domain)
log.info("=" * 60)
log.info(" Connect link:")
log.info(" %s", tg_link)
log.info(" Connect links:")
if ftls:
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)
async def log_stats():
@@ -569,6 +676,13 @@ def main():
help='Disable Cloudflare proxy fallback')
ap.add_argument('--cfproxy-priority', type=bool, 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()
if not args.dc_ip:
@@ -603,6 +717,8 @@ def main():
proxy_config.fallback_cfproxy = not args.no_cfproxy
proxy_config.fallback_cfproxy_priority = args.cfproxy_priority
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_fmt = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s',
@@ -624,6 +740,8 @@ def main():
fh.setFormatter(log_fmt)
root.addHandler(fh)
logging.getLogger('asyncio').setLevel(logging.WARNING)
try:
asyncio.run(_run())
except KeyboardInterrupt:

View File

@@ -6,7 +6,7 @@ from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from proxy import __version__, get_link_host, parse_dc_ip_list
from proxy.config import CFPROXY_DEFAULT_DOMAINS
from proxy.config import proxy_config
from utils.update_check import RELEASES_PAGE_URL, get_status
@@ -121,13 +121,19 @@ def _run_cfproxy_connectivity_test(domain: str) -> dict:
def _run_cfproxy_auto_test(domains: list) -> tuple:
last: dict = {}
for domain in domains:
merged: dict = {}
best_domain = None
for domain in reversed(domains):
res = _run_cfproxy_connectivity_test(domain)
last = res
if any(v is True for v in res.values()):
if all(v is True for v in res.values()):
return domain, res
return None, last
for dc, v in res.items():
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:
@@ -308,7 +314,7 @@ def install_tray_config_form(
header = ctk.CTkFrame(frame, fg_color="transparent")
header.pack(fill="x", pady=(0, 2))
ctk.CTkLabel(
header, text="Настройки прокси",
header, text="Настройки",
font=(theme.ui_font_family, 17, "bold"),
text_color=theme.text_primary, anchor="w",
).pack(side="left")
@@ -345,6 +351,17 @@ def install_tray_config_form(
command=_on_appearance_change,
).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")
host_row = ctk.CTkFrame(conn, fg_color="transparent")
@@ -434,7 +451,7 @@ def install_tray_config_form(
_threading.Thread(target=_worker, daemon=True).start()
else:
def _worker_auto():
ok_domain, res = _run_cfproxy_auto_test(CFPROXY_DEFAULT_DOMAINS)
ok_domain, res = _run_cfproxy_auto_test(proxy_config.cfproxy_domains)
if btn:
btn.after(0, lambda: btn.configure(text="Тест", state="normal"))
btn.after(0, lambda: _cfproxy_show_auto_test_results(ok_domain, res))

View File

@@ -153,6 +153,7 @@ def setup_logging(verbose: bool = False, log_max_mb: float = 5) -> None:
level = logging.DEBUG if verbose else logging.INFO
root = logging.getLogger()
root.setLevel(level)
logging.getLogger('asyncio').setLevel(logging.WARNING)
fh = logging.handlers.RotatingFileHandler(
str(LOG_FILE),