mirror of
https://github.com/Flowseal/tg-ws-proxy.git
synced 2026-07-08 04:50:25 +03:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e81f0a973d | |||
| 3143eb7147 | |||
| c3309ed11b | |||
| e9b74d7d7d | |||
| 050dcbce44 | |||
| 4f9edf3072 | |||
| 9ff95d1222 | |||
| 4c19a6cce4 | |||
| bb900e0c9e |
@@ -12,6 +12,31 @@ on:
|
|||||||
description: "Release version tag (e.g. v1.0.0)"
|
description: "Release version tag (e.g. v1.0.0)"
|
||||||
required: false
|
required: false
|
||||||
default: "v1.0.0"
|
default: "v1.0.0"
|
||||||
|
build_windows_x64:
|
||||||
|
description: 'Build Windows x64?'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
build_windows_arm64:
|
||||||
|
description: 'Build Windows ARM64?'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
build_win7:
|
||||||
|
description: 'Build Windows 7 (x64 + x86)?'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
build_macos:
|
||||||
|
description: 'Build macOS universal?'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
build_linux:
|
||||||
|
description: 'Build Linux (amd64/.deb/.rpm)?'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -19,6 +44,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
build-windows-x64:
|
build-windows-x64:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
if: ${{ github.event.inputs.build_windows_x64 == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -78,6 +104,7 @@ jobs:
|
|||||||
|
|
||||||
build-windows-arm64:
|
build-windows-arm64:
|
||||||
runs-on: windows-11-arm
|
runs-on: windows-11-arm
|
||||||
|
if: ${{ github.event.inputs.build_windows_arm64 == 'true' }}
|
||||||
env:
|
env:
|
||||||
CRYPTOGRAPHY_VERSION: "46.0.5"
|
CRYPTOGRAPHY_VERSION: "46.0.5"
|
||||||
ARM64_WHEELHOUSE: wheelhouse-arm64
|
ARM64_WHEELHOUSE: wheelhouse-arm64
|
||||||
@@ -154,6 +181,7 @@ jobs:
|
|||||||
|
|
||||||
build-win7:
|
build-win7:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
if: ${{ github.event.inputs.build_win7 == 'true' }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
@@ -207,6 +235,7 @@ jobs:
|
|||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
if: ${{ github.event.inputs.build_macos == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -345,6 +374,7 @@ jobs:
|
|||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.inputs.build_linux == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -511,7 +541,13 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
needs: [build-windows-x64, build-windows-arm64, build-win7, build-macos, build-linux]
|
needs: [build-windows-x64, build-windows-arm64, build-win7, build-macos, build-linux]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.inputs.make_release == 'true' }}
|
if: >-
|
||||||
|
${{ github.event.inputs.make_release == 'true'
|
||||||
|
&& github.event.inputs.build_windows_x64 == 'true'
|
||||||
|
&& github.event.inputs.build_windows_arm64 == 'true'
|
||||||
|
&& github.event.inputs.build_win7 == 'true'
|
||||||
|
&& github.event.inputs.build_macos == 'true'
|
||||||
|
&& github.event.inputs.build_linux == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v8
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
@@ -525,7 +561,7 @@ jobs:
|
|||||||
tag_name: ${{ github.event.inputs.version }}
|
tag_name: ${{ github.event.inputs.version }}
|
||||||
name: "TG WS Proxy ${{ github.event.inputs.version }}"
|
name: "TG WS Proxy ${{ github.event.inputs.version }}"
|
||||||
body: |
|
body: |
|
||||||
##
|
---
|
||||||
### [❤️ Поддержать развитие проекта](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/Funding.md)
|
### [❤️ Поддержать развитие проекта](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/Funding.md)
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
- **[Docker](./README.docker.md)**
|
- **[Docker](./README.docker.md)**
|
||||||
- [Настройка Cloudflare Worker'а (бесплатный аналог CF-прокси)](./CfWorker.md)
|
- [Настройка Cloudflare Worker'а (бесплатный аналог CF-прокси)](./CfWorker.md)
|
||||||
- [Настройка Cloudflare-домена (CF-прокси)](./CfProxy.md)
|
- [Настройка Cloudflare-домена (CF-прокси)](./CfProxy.md)
|
||||||
|
- [Тестовое окружение Telegram (тестовые DC)](./TestDc.md)
|
||||||
- [Fake TLS + upstream в Nginx](./FakeTlsNginx.md)
|
- [Fake TLS + upstream в Nginx](./FakeTlsNginx.md)
|
||||||
- [Файлы конфигурации Tray-приложения](./TrayConfig.md)
|
- [Файлы конфигурации Tray-приложения](./TrayConfig.md)
|
||||||
- [Установка из исходников](./BuildFromSource.md)
|
- [Установка из исходников](./BuildFromSource.md)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Тестовое окружение Telegram (тестовые DC)
|
||||||
|
|
||||||
|
Маршрутизация трафика к тестовым дата-центрам Telegram (test environment).
|
||||||
|
Нужна для разработки/тестирования ботов и клиентов в тестовой среде Telegram.
|
||||||
|
|
||||||
|
## Как включается
|
||||||
|
|
||||||
|
**Автоматически.** Telegram Desktop помечает тестовые DC сдвигом +10000
|
||||||
|
(10001–10003). Прокси распознаёт сдвиг сам — включать ничего не нужно, можно
|
||||||
|
одновременно пользоваться продовым и тестовым аккаунтом в одном клиенте.
|
||||||
|
|
||||||
|
**Принудительно.** Для клиентов, которые сообщают тестовые DC как обычные 1-3
|
||||||
|
(Telethon, TDLib) — распознать их автоматически нельзя. Тогда весь трафик
|
||||||
|
принудительно направляется на тестовые DC (продовые аккаунты через этот прокси
|
||||||
|
работать перестанут). Для принудительной работы замените в конфиге force_test_dc на true или используйте флаг `--force-test-dc` в CLI
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
Работает только для маршрутов **прямой DC → IP** и **Cloudflare Worker** (см. [Настройка Cloudflare Worker'а](./CfWorker.md)).
|
||||||
@@ -23,6 +23,7 @@ Tray-приложение хранит данные в:
|
|||||||
"cfproxy": true,
|
"cfproxy": true,
|
||||||
"cfproxy_user_domain": "",
|
"cfproxy_user_domain": "",
|
||||||
"cfproxy_worker_domain": "",
|
"cfproxy_worker_domain": "",
|
||||||
|
"force_test_dc": false,
|
||||||
"appearance": "auto"
|
"appearance": "auto"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ def _edit_config_dialog() -> None:
|
|||||||
messagebox.showerror(t("app.error_title"), merged, parent=root)
|
messagebox.showerror(t("app.error_title"), merged, parent=root)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
merged["force_test_dc"] = _config.get("force_test_dc", DEFAULT_CONFIG["force_test_dc"])
|
||||||
|
|
||||||
_ui_only_keys = {"appearance", "check_updates", "language"}
|
_ui_only_keys = {"appearance", "check_updates", "language"}
|
||||||
config_changed = any(merged.get(k) != _config.get(k) for k in merged)
|
config_changed = any(merged.get(k) != _config.get(k) for k in merged)
|
||||||
proxy_changed = any(merged.get(k) != _config.get(k) for k in merged if k not in _ui_only_keys)
|
proxy_changed = any(merged.get(k) != _config.get(k) for k in merged if k not in _ui_only_keys)
|
||||||
|
|||||||
@@ -475,6 +475,7 @@ def _edit_config_dialog() -> None:
|
|||||||
"cfproxy": cfproxy,
|
"cfproxy": cfproxy,
|
||||||
"cfproxy_user_domain": cfproxy_domains,
|
"cfproxy_user_domain": cfproxy_domains,
|
||||||
"cfproxy_worker_domain": cfworker_domains,
|
"cfproxy_worker_domain": cfworker_domains,
|
||||||
|
"force_test_dc": cfg.get("force_test_dc", DEFAULT_CONFIG["force_test_dc"]),
|
||||||
}
|
}
|
||||||
save_config(new_cfg)
|
save_config(new_cfg)
|
||||||
log.info("Config saved: %s", new_cfg)
|
log.info("Config saved: %s", new_cfg)
|
||||||
|
|||||||
@@ -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, 8, 0, 0),
|
filevers=(1, 8, 1, 0),
|
||||||
prodvers=(1, 8, 0, 0),
|
prodvers=(1, 8, 1, 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.8.0.0'),
|
StringStruct(u'FileVersion', u'1.8.1.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.8.0.0'),
|
StringStruct(u'ProductVersion', u'1.8.1.0'),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
from .config import parse_dc_ip_list, proxy_config, coerce_domain_list
|
from .config import parse_dc_ip_list, proxy_config, coerce_domain_list
|
||||||
from .utils import get_link_host, build_github_opener
|
from .utils import get_link_host, build_github_opener
|
||||||
|
|
||||||
__version__ = "1.8.0"
|
__version__ = "1.8.1"
|
||||||
|
|
||||||
__all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list", "build_github_opener", "coerce_domain_list"]
|
__all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list", "build_github_opener", "coerce_domain_list"]
|
||||||
+8
-7
@@ -130,10 +130,11 @@ class MsgSplitter:
|
|||||||
|
|
||||||
|
|
||||||
async def do_fallback(reader, writer, relay_init, label,
|
async def do_fallback(reader, writer, relay_init, label,
|
||||||
dc: int, is_media: bool, media_tag: str,
|
dc: int, is_test_dc: bool, is_media: bool, media_tag: str,
|
||||||
ctx: CryptoCtx, splitter=None):
|
ctx: CryptoCtx, splitter=None):
|
||||||
fallback_dst = DC_DEFAULT_IPS.get(dc)
|
ip_table = DC_TEST_IPS if is_test_dc else DC_DEFAULT_IPS
|
||||||
use_cf = proxy_config.fallback_cfproxy
|
fallback_dst = ip_table.get(dc)
|
||||||
|
use_cf = proxy_config.fallback_cfproxy and not is_test_dc
|
||||||
worker_domains = proxy_config.cfproxy_worker_domains
|
worker_domains = proxy_config.cfproxy_worker_domains
|
||||||
|
|
||||||
methods: List[str] = []
|
methods: List[str] = []
|
||||||
@@ -149,8 +150,8 @@ async def do_fallback(reader, writer, relay_init, label,
|
|||||||
if method == 'cf_worker' and fallback_dst:
|
if method == 'cf_worker' and fallback_dst:
|
||||||
ok = await _cfproxy_worker_fallback(
|
ok = await _cfproxy_worker_fallback(
|
||||||
reader, writer, relay_init, label, ctx,
|
reader, writer, relay_init, label, ctx,
|
||||||
dc=dc, is_media=is_media, fallback_dst=fallback_dst,
|
dc=dc, is_test_dc=is_test_dc, is_media=is_media,
|
||||||
splitter=splitter)
|
fallback_dst=fallback_dst, splitter=splitter)
|
||||||
if ok:
|
if ok:
|
||||||
return True
|
return True
|
||||||
elif method == 'cf':
|
elif method == 'cf':
|
||||||
@@ -173,7 +174,7 @@ async def do_fallback(reader, writer, relay_init, label,
|
|||||||
|
|
||||||
async def _cfproxy_worker_fallback(reader, writer, relay_init, label,
|
async def _cfproxy_worker_fallback(reader, writer, relay_init, label,
|
||||||
ctx: CryptoCtx,
|
ctx: CryptoCtx,
|
||||||
dc: int, is_media: bool,
|
dc: int, is_test_dc: bool, is_media: bool,
|
||||||
fallback_dst: str,
|
fallback_dst: str,
|
||||||
splitter=None):
|
splitter=None):
|
||||||
media_tag = ' media' if is_media else ''
|
media_tag = ' media' if is_media else ''
|
||||||
@@ -184,7 +185,7 @@ async def _cfproxy_worker_fallback(reader, writer, relay_init, label,
|
|||||||
random.shuffle(worker_domains)
|
random.shuffle(worker_domains)
|
||||||
|
|
||||||
for worker_domain in worker_domains:
|
for worker_domain in worker_domains:
|
||||||
ws = await cf_worker_pool.get(dc, worker_domain, fallback_dst)
|
ws = None if is_test_dc else await cf_worker_pool.get(dc, worker_domain, fallback_dst)
|
||||||
if ws:
|
if ws:
|
||||||
log.info("[%s] DC%d%s -> CF worker pool hit for %s",
|
log.info("[%s] DC%d%s -> CF worker pool hit for %s",
|
||||||
label, dc, media_tag, fallback_dst)
|
label, dc, media_tag, fallback_dst)
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class ProxyConfig:
|
|||||||
cfproxy_worker_domains: List[str] = field(default_factory=list)
|
cfproxy_worker_domains: List[str] = field(default_factory=list)
|
||||||
fake_tls_domain: str = ''
|
fake_tls_domain: str = ''
|
||||||
proxy_protocol: bool = False
|
proxy_protocol: bool = False
|
||||||
|
force_test_dc: bool = False
|
||||||
|
|
||||||
|
|
||||||
proxy_config = ProxyConfig()
|
proxy_config = ProxyConfig()
|
||||||
|
|||||||
+43
-18
@@ -33,13 +33,15 @@ from ._aes import Cipher, algorithms, modes
|
|||||||
|
|
||||||
log = logging.getLogger('tg-mtproto-proxy')
|
log = logging.getLogger('tg-mtproto-proxy')
|
||||||
|
|
||||||
DC_FAIL_COOLDOWN = 30.0
|
IP_FAIL_COOLDOWN = 3600.0
|
||||||
|
DC_FAIL_COOLDOWN = 60.0
|
||||||
WS_FAIL_TIMEOUT = 2.0
|
WS_FAIL_TIMEOUT = 2.0
|
||||||
FRONTING_COOLDOWN = 1800.0
|
FRONTING_COOLDOWN = 1800.0
|
||||||
LISTENER_CHECK_INTERVAL = 5.0
|
LISTENER_CHECK_INTERVAL = 5.0
|
||||||
LISTENER_RESTART_DELAY = 1.0
|
LISTENER_RESTART_DELAY = 1.0
|
||||||
ws_blacklist: Set[str] = set()
|
ws_blacklist: Set[str] = set()
|
||||||
dc_fail_until: Dict[str, float] = {}
|
dc_fail_until: Dict[str, float] = {}
|
||||||
|
ip_fail_until: Dict[str, float] = {}
|
||||||
fronting_until: float = 0.0
|
fronting_until: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
@@ -278,6 +280,11 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
|
|
||||||
dc, is_media, proto_tag, client_dec_prekey_iv = result
|
dc, is_media, proto_tag, client_dec_prekey_iv = result
|
||||||
|
|
||||||
|
is_test_dc = proxy_config.force_test_dc or dc >= 10000
|
||||||
|
if dc >= 10000:
|
||||||
|
log.info("[%s] test DC%d -> DC%d", label, dc, dc - 10000)
|
||||||
|
dc -= 10000
|
||||||
|
|
||||||
if proto_tag == PROTO_TAG_ABRIDGED:
|
if proto_tag == PROTO_TAG_ABRIDGED:
|
||||||
proto_int = PROTO_ABRIDGED_INT
|
proto_int = PROTO_ABRIDGED_INT
|
||||||
elif proto_tag == PROTO_TAG_INTERMEDIATE:
|
elif proto_tag == PROTO_TAG_INTERMEDIATE:
|
||||||
@@ -293,17 +300,27 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
relay_init = _generate_relay_init(proto_tag, dc_idx)
|
relay_init = _generate_relay_init(proto_tag, dc_idx)
|
||||||
ctx = _build_crypto_ctx(client_dec_prekey_iv, secret, relay_init)
|
ctx = _build_crypto_ctx(client_dec_prekey_iv, secret, relay_init)
|
||||||
|
|
||||||
dc_key = f'{dc}{"m" if is_media else ""}'
|
dc_key = f'{dc}{"t" if is_test_dc else ""}{"m" if is_media else ""}'
|
||||||
media_tag = " media" if is_media else ""
|
media_tag = " media" if is_media else ""
|
||||||
|
now = time.monotonic()
|
||||||
|
ws_path = WS_PATH_TEST if is_test_dc else WS_PATH
|
||||||
|
target = proxy_config.dc_redirects.get(dc)
|
||||||
|
is_any_cf_fallback = proxy_config.fallback_cfproxy or proxy_config.cfproxy_worker_domains
|
||||||
|
|
||||||
|
# Fallback if DC not in config, if WS blacklisted for this DC/is_media or if connect to ip is timed out
|
||||||
|
if (dc not in proxy_config.dc_redirects
|
||||||
|
or dc_key in ws_blacklist
|
||||||
|
or now < ip_fail_until.get(target, 0) and is_any_cf_fallback):
|
||||||
|
|
||||||
# Fallback if DC not in config or WS blacklisted for this DC/is_media
|
|
||||||
if dc not in proxy_config.dc_redirects or dc_key in ws_blacklist:
|
|
||||||
if dc not in proxy_config.dc_redirects:
|
if dc not in proxy_config.dc_redirects:
|
||||||
log.info("[%s] DC%d not in config -> fallback",
|
log.info("[%s] DC%d not in config -> fallback",
|
||||||
label, dc)
|
label, dc)
|
||||||
else:
|
elif dc_key in ws_blacklist:
|
||||||
log.info("[%s] DC%d%s WS blacklisted -> fallback",
|
log.info("[%s] DC%d%s WS blacklisted -> fallback",
|
||||||
label, dc, media_tag)
|
label, dc, media_tag)
|
||||||
|
else:
|
||||||
|
log.info("[%s] DC%d%s WS connect to %s was timed out -> fallback",
|
||||||
|
label, dc, media_tag, target)
|
||||||
splitter = None
|
splitter = None
|
||||||
try:
|
try:
|
||||||
splitter = MsgSplitter(relay_init, proto_int)
|
splitter = MsgSplitter(relay_init, proto_int)
|
||||||
@@ -311,26 +328,23 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
pass
|
pass
|
||||||
ok = await do_fallback(
|
ok = await do_fallback(
|
||||||
clt_reader, clt_writer, relay_init, label,
|
clt_reader, clt_writer, relay_init, label,
|
||||||
dc, is_media, media_tag,
|
dc, is_test_dc, is_media, media_tag,
|
||||||
ctx, splitter=splitter)
|
ctx, splitter=splitter)
|
||||||
if not ok:
|
if not ok:
|
||||||
log.warning("[%s] DC%d%s no fallback available",
|
log.warning("[%s] DC%d%s no fallback available",
|
||||||
label, dc, media_tag)
|
label, dc, media_tag)
|
||||||
return
|
return
|
||||||
|
|
||||||
now = time.monotonic()
|
ws_timeout = WS_FAIL_TIMEOUT if now < dc_fail_until.get(dc_key, 0) else 5.0
|
||||||
fail_until = dc_fail_until.get(dc_key, 0)
|
|
||||||
ws_timeout = WS_FAIL_TIMEOUT if now < fail_until else 10.0
|
|
||||||
fronting_active = now < fronting_until
|
fronting_active = now < fronting_until
|
||||||
|
|
||||||
domains = ws_domains(dc, is_media)
|
domains = ws_domains(dc, is_media)
|
||||||
target = proxy_config.dc_redirects[dc]
|
|
||||||
ws = None
|
ws = None
|
||||||
ws_failed_redirect = False
|
ws_failed_redirect = False
|
||||||
ws_timed_out = False
|
ws_timed_out = False
|
||||||
all_redirects = True
|
all_redirects = True
|
||||||
|
|
||||||
ws = await ws_pool.get(dc, is_media, target, domains)
|
ws = await ws_pool.get(dc, is_media, target, domains) if not is_test_dc else None
|
||||||
if ws:
|
if ws:
|
||||||
log.info("[%s] DC%d%s -> pool hit via %s",
|
log.info("[%s] DC%d%s -> pool hit via %s",
|
||||||
label, dc, media_tag, target)
|
label, dc, media_tag, target)
|
||||||
@@ -340,7 +354,7 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
label, dc, media_tag, domains[0])
|
label, dc, media_tag, domains[0])
|
||||||
try:
|
try:
|
||||||
ws = await RawWebSocket.connect(target, domains[0],
|
ws = await RawWebSocket.connect(target, domains[0],
|
||||||
timeout=10.0,
|
timeout=5.0, path=ws_path,
|
||||||
sni="sprinthost.ru")
|
sni="sprinthost.ru")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
stats.ws_errors += 1
|
stats.ws_errors += 1
|
||||||
@@ -355,12 +369,13 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
ws_pool.fronting_until = 0.0
|
ws_pool.fronting_until = 0.0
|
||||||
else:
|
else:
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
url = f'wss://{domain}/apiws'
|
url = f'wss://{domain}{ws_path}'
|
||||||
log.info("[%s] DC%d%s -> %s via %s",
|
log.info("[%s] DC%d%s -> %s via %s",
|
||||||
label, dc, media_tag, url, target)
|
label, dc, media_tag, url, target)
|
||||||
try:
|
try:
|
||||||
ws = await RawWebSocket.connect(target, domain,
|
ws = await RawWebSocket.connect(target, domain,
|
||||||
timeout=ws_timeout)
|
timeout=ws_timeout,
|
||||||
|
path=ws_path)
|
||||||
all_redirects = False
|
all_redirects = False
|
||||||
break
|
break
|
||||||
except WsHandshakeError as exc:
|
except WsHandshakeError as exc:
|
||||||
@@ -381,6 +396,7 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
ws_timed_out = True
|
ws_timed_out = True
|
||||||
log.warning("[%s] DC%d%s WS connect timed out via %s",
|
log.warning("[%s] DC%d%s WS connect timed out via %s",
|
||||||
label, dc, media_tag, domain)
|
label, dc, media_tag, domain)
|
||||||
|
break
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
stats.ws_errors += 1
|
stats.ws_errors += 1
|
||||||
all_redirects = False
|
all_redirects = False
|
||||||
@@ -395,7 +411,7 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
label, dc, media_tag, domains[0])
|
label, dc, media_tag, domains[0])
|
||||||
try:
|
try:
|
||||||
ws = await RawWebSocket.connect(target, domains[0],
|
ws = await RawWebSocket.connect(target, domains[0],
|
||||||
timeout=10.0,
|
timeout=5.0, path=ws_path,
|
||||||
sni="sprinthost.ru")
|
sni="sprinthost.ru")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
stats.ws_errors += 1
|
stats.ws_errors += 1
|
||||||
@@ -410,6 +426,9 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
|
|
||||||
# WS failed -> fallback
|
# WS failed -> fallback
|
||||||
if ws is None:
|
if ws is None:
|
||||||
|
if ws_timed_out:
|
||||||
|
ip_fail_until[target] = now + IP_FAIL_COOLDOWN
|
||||||
|
|
||||||
if ws_failed_redirect and all_redirects:
|
if ws_failed_redirect and all_redirects:
|
||||||
ws_blacklist.add(dc_key)
|
ws_blacklist.add(dc_key)
|
||||||
log.warning("[%s] DC%d%s blacklisted for WS (all 302)",
|
log.warning("[%s] DC%d%s blacklisted for WS (all 302)",
|
||||||
@@ -418,8 +437,6 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
dc_fail_until[dc_key] = now + DC_FAIL_COOLDOWN
|
dc_fail_until[dc_key] = now + DC_FAIL_COOLDOWN
|
||||||
else:
|
else:
|
||||||
dc_fail_until[dc_key] = now + DC_FAIL_COOLDOWN
|
dc_fail_until[dc_key] = now + DC_FAIL_COOLDOWN
|
||||||
# TODO: may be don't try regular WS connection and do fallback instanstly
|
|
||||||
# instead of waiting for WS_FAIL_TIMEOUT and then fallback
|
|
||||||
log.info("[%s] DC%d%s WS cooldown for %ds",
|
log.info("[%s] DC%d%s WS cooldown for %ds",
|
||||||
label, dc, media_tag, int(DC_FAIL_COOLDOWN))
|
label, dc, media_tag, int(DC_FAIL_COOLDOWN))
|
||||||
|
|
||||||
@@ -430,7 +447,7 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
pass
|
pass
|
||||||
ok = await do_fallback(
|
ok = await do_fallback(
|
||||||
clt_reader, clt_writer, relay_init, label,
|
clt_reader, clt_writer, relay_init, label,
|
||||||
dc, is_media, media_tag,
|
dc, is_test_dc, is_media, media_tag,
|
||||||
ctx, splitter=splitter_fb)
|
ctx, splitter=splitter_fb)
|
||||||
if ok:
|
if ok:
|
||||||
log.info("[%s] DC%d%s fallback closed",
|
log.info("[%s] DC%d%s fallback closed",
|
||||||
@@ -438,6 +455,7 @@ async def _handle_client(reader, writer, secret: bytes):
|
|||||||
return
|
return
|
||||||
|
|
||||||
dc_fail_until.pop(dc_key, None)
|
dc_fail_until.pop(dc_key, None)
|
||||||
|
ip_fail_until.pop(target, None)
|
||||||
stats.connections_ws += 1
|
stats.connections_ws += 1
|
||||||
|
|
||||||
splitter = None
|
splitter = None
|
||||||
@@ -491,6 +509,7 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
|
|||||||
cf_worker_pool.reset()
|
cf_worker_pool.reset()
|
||||||
ws_blacklist.clear()
|
ws_blacklist.clear()
|
||||||
dc_fail_until.clear()
|
dc_fail_until.clear()
|
||||||
|
ip_fail_until.clear()
|
||||||
_client_tasks.clear()
|
_client_tasks.clear()
|
||||||
fronting_until = 0.0
|
fronting_until = 0.0
|
||||||
|
|
||||||
@@ -691,6 +710,11 @@ def main():
|
|||||||
metavar='DOMAIN',
|
metavar='DOMAIN',
|
||||||
help='Enable Fake TLS (ee-secret) masking with the given '
|
help='Enable Fake TLS (ee-secret) masking with the given '
|
||||||
'SNI domain, e.g. example.com')
|
'SNI domain, e.g. example.com')
|
||||||
|
ap.add_argument('--force-test-dc', action='store_true',
|
||||||
|
help='Force ALL traffic to Telegram TEST datacenters. '
|
||||||
|
'Not needed for Telegram Desktop (test DCs 10001+ '
|
||||||
|
'are detected automatically); use for clients that '
|
||||||
|
'signal test DCs as plain 1-3')
|
||||||
ap.add_argument('--proxy-protocol', action='store_true',
|
ap.add_argument('--proxy-protocol', action='store_true',
|
||||||
help='Accept PROXY protocol v1 header '
|
help='Accept PROXY protocol v1 header '
|
||||||
'(for use behind nginx/haproxy with proxy_protocol on)')
|
'(for use behind nginx/haproxy with proxy_protocol on)')
|
||||||
@@ -730,6 +754,7 @@ def main():
|
|||||||
proxy_config.cfproxy_worker_domains = coerce_domain_list(args.cfproxy_worker_domain)
|
proxy_config.cfproxy_worker_domains = coerce_domain_list(args.cfproxy_worker_domain)
|
||||||
proxy_config.fake_tls_domain = args.fake_tls_domain.strip()
|
proxy_config.fake_tls_domain = args.fake_tls_domain.strip()
|
||||||
proxy_config.proxy_protocol = args.proxy_protocol
|
proxy_config.proxy_protocol = args.proxy_protocol
|
||||||
|
proxy_config.force_test_dc = args.force_test_dc
|
||||||
|
|
||||||
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',
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ DC_DEFAULT_IPS: Dict[int, str] = {
|
|||||||
203: '91.105.192.100'
|
203: '91.105.192.100'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DC_TEST_IPS: Dict[int, str] = {
|
||||||
|
1: '149.154.175.10',
|
||||||
|
2: '149.154.167.40',
|
||||||
|
3: '149.154.175.117',
|
||||||
|
}
|
||||||
|
|
||||||
|
WS_PATH = '/apiws'
|
||||||
|
WS_PATH_TEST = WS_PATH + '_test'
|
||||||
|
|
||||||
|
|
||||||
def ws_domains(dc: int, is_media) -> List[str]:
|
def ws_domains(dc: int, is_media) -> List[str]:
|
||||||
if dc == 203:
|
if dc == 203:
|
||||||
|
|||||||
+4
-1
@@ -25,7 +25,10 @@ class LocaleEnum(str, Enum):
|
|||||||
return _DEFAULT_LOCALE
|
return _DEFAULT_LOCALE
|
||||||
|
|
||||||
|
|
||||||
_LOCALES_DIR = Path(__file__).resolve().parent
|
try:
|
||||||
|
_LOCALES_DIR = Path(__file__).resolve(strict=False).parent
|
||||||
|
except OSError:
|
||||||
|
_LOCALES_DIR = Path(os.path.realpath(__file__)).parent
|
||||||
_DEFAULT_LOCALE = LocaleEnum.english
|
_DEFAULT_LOCALE = LocaleEnum.english
|
||||||
|
|
||||||
_translations: Dict[str, str] = {}
|
_translations: Dict[str, str] = {}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
|
|||||||
"cfproxy": True,
|
"cfproxy": True,
|
||||||
"cfproxy_user_domain": [],
|
"cfproxy_user_domain": [],
|
||||||
"cfproxy_worker_domain": [],
|
"cfproxy_worker_domain": [],
|
||||||
|
"force_test_dc": False,
|
||||||
"ws_keepalive_interval": 30,
|
"ws_keepalive_interval": 30,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ def _exe_dir() -> Optional[Path]:
|
|||||||
return None
|
return None
|
||||||
if not base:
|
if not base:
|
||||||
return None
|
return None
|
||||||
p = Path(base).resolve()
|
try:
|
||||||
|
p = Path(base).resolve(strict=False)
|
||||||
|
except OSError:
|
||||||
|
p = Path(os.path.realpath(base))
|
||||||
return p.parent if p.is_file() else p
|
return p.parent if p.is_file() else p
|
||||||
|
|
||||||
|
|
||||||
@@ -334,6 +337,7 @@ def apply_proxy_config(cfg: dict) -> bool:
|
|||||||
pc.fallback_cfproxy = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"])
|
pc.fallback_cfproxy = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"])
|
||||||
pc.cfproxy_user_domains = coerce_domain_list(cfg.get("cfproxy_user_domain", DEFAULT_CONFIG["cfproxy_user_domain"]))
|
pc.cfproxy_user_domains = coerce_domain_list(cfg.get("cfproxy_user_domain", DEFAULT_CONFIG["cfproxy_user_domain"]))
|
||||||
pc.cfproxy_worker_domains = coerce_domain_list(cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG["cfproxy_worker_domain"]))
|
pc.cfproxy_worker_domains = coerce_domain_list(cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG["cfproxy_worker_domain"]))
|
||||||
|
pc.force_test_dc = cfg.get("force_test_dc", DEFAULT_CONFIG["force_test_dc"])
|
||||||
pc.ws_keepalive_interval = max(0, cfg.get("ws_keepalive_interval", DEFAULT_CONFIG["ws_keepalive_interval"]))
|
pc.ws_keepalive_interval = max(0, cfg.get("ws_keepalive_interval", DEFAULT_CONFIG["ws_keepalive_interval"]))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -522,6 +522,8 @@ def _edit_config_dialog() -> None:
|
|||||||
messagebox.showerror(t("app.error_title"), merged, parent=root)
|
messagebox.showerror(t("app.error_title"), merged, parent=root)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
merged["force_test_dc"] = _config.get("force_test_dc", DEFAULT_CONFIG["force_test_dc"])
|
||||||
|
|
||||||
_ui_only_keys = {"appearance", "autostart", "check_updates", "language"}
|
_ui_only_keys = {"appearance", "autostart", "check_updates", "language"}
|
||||||
config_changed = any(merged.get(k) != _config.get(k) for k in merged)
|
config_changed = any(merged.get(k) != _config.get(k) for k in merged)
|
||||||
proxy_changed = any(merged.get(k) != _config.get(k) for k in merged if k not in _ui_only_keys)
|
proxy_changed = any(merged.get(k) != _config.get(k) for k in merged if k not in _ui_only_keys)
|
||||||
|
|||||||
Reference in New Issue
Block a user