diff --git a/docs/EN/BuildFromSource.md b/docs/EN/BuildFromSource.md index 191fb81..8ee690a 100644 --- a/docs/EN/BuildFromSource.md +++ b/docs/EN/BuildFromSource.md @@ -51,6 +51,7 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v] | `--no-cfproxy` | `false` | Disable [Cloudflare proxying](./CfProxy.md) attempts | | `--cfproxy-domain` | | Specify your own domain for Cloudflare proxying [Learn more](./CfProxy.md). Can be specified multiple times. | | `--cfproxy-worker-domain` | | Cloudflare Worker domain [Learn more](./CfWorker.md). Can be specified multiple times. | +| `--no-secure` | `false` | Use 80 port for CF-proxy and CF-worker connections | | `--fake-tls-domain` | | Enable Fake TLS masquerading (ee-secret) with specified SNI domain | | `--proxy-protocol` | disabled | Accept HAProxy PROXY protocol v1 (for use behind nginx/haproxy with `proxy_protocol on`) | | `--buf-kb` | `256` | Buffer size in KB | @@ -64,13 +65,13 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v] ```bash # Standard startup -tg-ws-proxy +tg-ws-proxy --secret 00112233445566778899aabbccddeeff # Different port and additional DCs tg-ws-proxy --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149.154.167.220 -# With verbose logging -tg-ws-proxy -v +# With verbose logging, without direct connections to DCs +tg-ws-proxy -v --dc-ip # Fake TLS masquerading (ee-secret) tg-ws-proxy --fake-tls-domain example.com diff --git a/docs/RU/BuildFromSource.md b/docs/RU/BuildFromSource.md index 87ad53d..3d01b25 100644 --- a/docs/RU/BuildFromSource.md +++ b/docs/RU/BuildFromSource.md @@ -51,6 +51,7 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v] | `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare](./CfProxy.md) | | `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudflare [Подробнее](./CfProxy.md). Можно указать несколько через повторение аргумента. | | `--cfproxy-worker-domain` | | Домен Cloudflare Worker [Подробнее](./CfWorker.md). Можно указать несколько через повторение аргумента. | +| `--no-secure` | `false` | Принудительно использует 80 порт для CF-proxy и CF-worker | | `--fake-tls-domain` | | Включить маскировку Fake TLS (ee-secret) с указанным SNI-доменом | | `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) | | `--buf-kb` | `256` | Размер буфера в КБ | @@ -64,13 +65,13 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v] ```bash # Стандартный запуск -tg-ws-proxy +tg-ws-proxy --secret 00112233445566778899aabbccddeeff # Другой порт и дополнительные DC tg-ws-proxy --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149.154.167.220 -# С подробным логированием -tg-ws-proxy -v +# С подробным логированием, без прямого подключения к DC +tg-ws-proxy -v --dc-ip # Fake TLS маскировка (ee-secret) tg-ws-proxy --fake-tls-domain example.com diff --git a/proxy/bridge.py b/proxy/bridge.py index 8ca829c..404fc3a 100644 --- a/proxy/bridge.py +++ b/proxy/bridge.py @@ -201,7 +201,8 @@ async def _cfproxy_worker_fallback(reader, writer, relay_init, label, try: ws = await RawWebSocket.connect(worker_domain, worker_domain, - timeout=10.0, path=path) + timeout=10.0, path=path, + secure=not proxy_config.disable_secure) break except Exception as exc: cf_worker_pool.report_failure(worker_domain, exc) @@ -234,7 +235,8 @@ async def _cfproxy_fallback(reader, writer, relay_init, label, for base_domain in balancer.get_domains_for_dc(dc): domain = f'kws{dc}.{base_domain}' try: - ws = await RawWebSocket.connect(domain, domain, timeout=10.0) + ws = await RawWebSocket.connect(domain, domain, timeout=10.0, + secure=not proxy_config.disable_secure) chosen_domain = base_domain break except Exception as exc: diff --git a/proxy/config.py b/proxy/config.py index c172ab0..f4834c5 100644 --- a/proxy/config.py +++ b/proxy/config.py @@ -70,6 +70,7 @@ class ProxyConfig: fallback_cfproxy: bool = True cfproxy_user_domains: List[str] = field(default_factory=list) cfproxy_worker_domains: List[str] = field(default_factory=list) + disable_secure: bool = False fake_tls_domain: str = '' proxy_protocol: bool = False force_test_dc: bool = False diff --git a/proxy/pool.py b/proxy/pool.py index 6d05dfe..dd19f94 100644 --- a/proxy/pool.py +++ b/proxy/pool.py @@ -289,7 +289,8 @@ class _CfWorkerPool: for worker_domain in self.available_domains(worker_domains): try: ws = await RawWebSocket.connect( - worker_domain, worker_domain, timeout=8, path=path) + worker_domain, worker_domain, timeout=8, path=path, + secure=not proxy_config.disable_secure) return ws, worker_domain except Exception as exc: self.report_failure(worker_domain, exc) diff --git a/proxy/raw_websocket.py b/proxy/raw_websocket.py index 6a49107..bdf1c02 100644 --- a/proxy/raw_websocket.py +++ b/proxy/raw_websocket.py @@ -22,9 +22,8 @@ _st_H = struct.Struct('>H') _st_Q = struct.Struct('>Q') _ssl_ctx = ssl.create_default_context() -_ssl_ctx.check_hostname = False -_ssl_ctx.verify_mode = ssl.CERT_NONE - +_ssl_ctx_fronting = ssl.create_default_context() +_ssl_ctx_fronting.check_hostname = False class WsHandshakeError(Exception): def __init__(self, status_code: int, status_line: str, @@ -87,14 +86,25 @@ class RawWebSocket: @staticmethod async def connect(host: str, domain: str, timeout: float = 10.0, path: str = '/apiws', *, - sni: Optional[str] = None) -> 'RawWebSocket': + sni: Optional[str] = None, secure = True) -> 'RawWebSocket': + ssl = _ssl_ctx_fronting if sni else _ssl_ctx + print(f"Connecting to {host} with secure={secure}, sni={sni}, path={path}") + if sni is None: sni = domain reader, writer = await asyncio.wait_for( - asyncio.open_connection(host, 443, ssl=_ssl_ctx, - server_hostname=sni), - timeout=min(timeout, 10)) + ( + asyncio.open_connection( + host, 443, + ssl=ssl, + server_hostname=sni, + ) + if secure + else asyncio.open_connection(host, 80) + ), + timeout=min(timeout, 10), + ) set_sock_opts(writer.transport, proxy_config.buffer_size) diff --git a/proxy/tg_ws_proxy.py b/proxy/tg_ws_proxy.py index fb54db6..5c861b8 100644 --- a/proxy/tg_ws_proxy.py +++ b/proxy/tg_ws_proxy.py @@ -531,6 +531,8 @@ async def _run(stop_event: Optional[asyncio.Event] = None): if proxy_config.cfproxy_worker_domains: log.info(" CF worker: enabled (%s)", ", ".join(proxy_config.cfproxy_worker_domains)) + if proxy_config.disable_secure: + log.info(" No secure: enabled (port 80 for CF proxy/worker)") log.info("=" * 60) log.info(" Connect:") if ftls: @@ -673,6 +675,8 @@ def main(): 'repeatable for multiple domains)') ap.add_argument('--no-cfproxy', action='store_true', help='Disable Cloudflare proxy fallback') + ap.add_argument('--no-secure', action='store_true', + help='Use 80 port for CF-proxy and CF-worker connections') ap.add_argument('--fake-tls-domain', type=str, default='', metavar='DOMAIN', help='Enable Fake TLS (ee-secret) masking with the given ' @@ -724,6 +728,7 @@ def main(): proxy_config.fallback_cfproxy = not args.no_cfproxy proxy_config.cfproxy_user_domains = coerce_domain_list(args.cfproxy_domain) proxy_config.cfproxy_worker_domains = coerce_domain_list(args.cfproxy_worker_domain) + proxy_config.disable_secure = args.no_secure proxy_config.fake_tls_domain = args.fake_tls_domain.strip() proxy_config.proxy_protocol = args.proxy_protocol proxy_config.force_test_dc = args.force_test_dc diff --git a/ui/ctk_tray_ui.py b/ui/ctk_tray_ui.py index 035c042..62a8731 100644 --- a/ui/ctk_tray_ui.py +++ b/ui/ctk_tray_ui.py @@ -44,19 +44,21 @@ _CFWORKER_TEST_DST = { } -def _run_connectivity_test(cases: list) -> dict: +def _run_connectivity_test(cases: list, *, secure: bool = True) -> dict: import base64 + from contextlib import nullcontext import ssl import socket as _socket - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE + ctx = ssl.create_default_context() if secure else None + port = 443 if secure else 80 results = {} for dc, connect_host, sni_host, req_host, path in cases: try: - with _socket.create_connection((connect_host, 443), timeout=5) as raw: - with ctx.wrap_socket(raw, server_hostname=sni_host) as ssock: + with _socket.create_connection((connect_host, port), timeout=5) as raw: + connection = (ctx.wrap_socket(raw, server_hostname=sni_host) + if secure else nullcontext(raw)) + with connection as ssock: ws_key = base64.b64encode(os.urandom(16)).decode() req = ( f"GET {path} HTTP/1.1\r\n" @@ -91,36 +93,36 @@ def _run_connectivity_test(cases: list) -> dict: return results -def _run_cfproxy_connectivity_test(domain: str) -> dict: +def _run_cfproxy_connectivity_test(domain: str, *, secure: bool = True) -> dict: cases = [] for dc in _CFPROXY_TEST_DCS: host = f"kws{dc}.{domain}" cases.append((dc, host, host, host, "/apiws")) - return _run_connectivity_test(cases) + return _run_connectivity_test(cases, secure=secure) -def _run_cfworker_connectivity_test(domain: str) -> dict: +def _run_cfworker_connectivity_test(domain: str, *, secure: bool = True) -> dict: cases = [] for dc in _CFPROXY_TEST_DCS: dst = _CFWORKER_TEST_DST[dc] path = f"/apiws?dst={dst}&dc={dc}&media=0" cases.append((dc, domain, domain, domain, path)) - return _run_connectivity_test(cases) + return _run_connectivity_test(cases, secure=secure) -def _run_cfproxy_multi_test(domains: list) -> dict: - return {domain: _run_cfproxy_connectivity_test(domain) for domain in domains} +def _run_cfproxy_multi_test(domains: list, *, secure: bool = True) -> dict: + return {domain: _run_cfproxy_connectivity_test(domain, secure=secure) for domain in domains} -def _run_cfworker_multi_test(domains: list) -> dict: - return {domain: _run_cfworker_connectivity_test(domain) for domain in domains} +def _run_cfworker_multi_test(domains: list, *, secure: bool = True) -> dict: + return {domain: _run_cfworker_connectivity_test(domain, secure=secure) for domain in domains} -def _run_cfproxy_auto_test(domains: list) -> tuple: +def _run_cfproxy_auto_test(domains: list, *, secure: bool = True) -> tuple: merged: dict = {} best_domain = None for domain in reversed(domains): - res = _run_cfproxy_connectivity_test(domain) + res = _run_cfproxy_connectivity_test(domain, secure=secure) if all(v is True for v in res.values()): return domain, res for dc, v in res.items(): @@ -350,6 +352,7 @@ class TrayConfigFormWidgets: secret_var: Any dc_textbox: Any verbose_var: Any + no_secure_var: Any adv_entries: List[Any] adv_keys: Tuple[str, ...] autostart_var: Optional[Any] @@ -536,6 +539,7 @@ def install_tray_config_form( _cf_test_btn = [None] def _on_cf_test(): + secure = not no_secure_var.get() user_domains = ( coerce_domain_list(cfproxy_user_domain_var.get()) if cf_custom_cb_var.get() else [] @@ -547,7 +551,7 @@ def install_tray_config_form( if user_domains: def _worker(): try: - per = _run_cfproxy_multi_test(user_domains) + per = _run_cfproxy_multi_test(user_domains, secure=secure) if btn: btn.after( 0, @@ -564,7 +568,7 @@ def install_tray_config_form( else: def _worker_auto(): try: - ok_domain, res = _run_cfproxy_auto_test(balancer.domains) + ok_domain, res = _run_cfproxy_auto_test(balancer.domains, secure=secure) if btn: btn.after( 0, @@ -672,6 +676,7 @@ def install_tray_config_form( btn.configure(state="normal" if enabled else "disabled") def _on_cfworker_test(): + secure = not no_secure_var.get() domains = coerce_domain_list(cfproxy_worker_domain_var.get()) btn = _cfworker_test_btn[0] if not cfproxy_worker_enabled_var.get() or not domains or btn is None: @@ -681,7 +686,7 @@ def install_tray_config_form( def _worker(): try: - per = _run_cfworker_multi_test(domains) + per = _run_cfworker_multi_test(domains, secure=secure) btn.after( 0, lambda: _show_multi_connectivity_results( @@ -730,6 +735,11 @@ def install_tray_config_form( verbose_cb.pack(anchor="w", pady=(0, 6)) attach_ctk_tooltip(verbose_cb, t("tip.verbose")) + no_secure_var = ctk.BooleanVar(value=cfg.get("no_secure", False)) + no_secure_cb = _checkbox(ctk, log_inner, theme, t("label.no_secure"), no_secure_var) + no_secure_cb.pack(anchor="w", pady=(0, 6)) + attach_ctk_tooltip(no_secure_cb, t("tip.no_secure")) + adv_frame = ctk.CTkFrame(log_inner, fg_color="transparent") adv_frame.pack(fill="x") @@ -823,7 +833,7 @@ def install_tray_config_form( return TrayConfigFormWidgets( host_var=host_var, port_var=port_var, secret_var=secret_var, - dc_textbox=dc_textbox, verbose_var=verbose_var, + dc_textbox=dc_textbox, verbose_var=verbose_var, no_secure_var=no_secure_var, adv_entries=adv_entries, adv_keys=adv_keys, autostart_var=autostart_var, check_updates_var=check_updates_var, cfproxy_var=cfproxy_var, @@ -935,6 +945,8 @@ def validate_config_form( new_cfg["appearance"] = _appearance_to_cfg(widgets.appearance_var.get()) if widgets.language_var is not None: new_cfg["language"] = language_from_label(widgets.language_var.get()).value + if widgets.no_secure_var is not None: + new_cfg["no_secure"] = bool(widgets.no_secure_var.get()) return new_cfg diff --git a/ui/i18n/en.json b/ui/i18n/en.json index 7de55ac..a4b5253 100644 --- a/ui/i18n/en.json +++ b/ui/i18n/en.json @@ -32,6 +32,7 @@ "label.cf_custom_domain": "Custom domain", "label.cfworker_domains": "Cloudflare Worker domains (comma-separated)", "label.verbose": "Verbose logging", + "label.no_secure": "Disable SSL for CF-proxy and CF-worker", "label.buf_kb": "Buffer, KB (default 256)", "label.pool_size": "WebSocket session pool (default 4)", "label.log_max_mb": "Max log size, MB (default 5)", @@ -44,6 +45,7 @@ "tip.secret": "Secret key for client authorization", "tip.dc": "Mapping of Telegram data center (DC) number to web.telegram.org dc server IP.\nEach line: «number:IP», e.g. 4:149.154.167.220. The proxy routes traffic to Telegram servers using these rules\n\nIf connection fails then fallbacks are used", "tip.verbose": "When enabled, more details are written to the log file — useful for troubleshooting", + "tip.no_secure": "When enabled, the proxy will use port 80 for CF-proxy and CF-worker connections", "tip.buf_kb": "Receive/send buffer size in kilobytes.\nA larger value allocates more memory per socket", "tip.pool": "How many parallel WebSocket sessions per data center can be kept open.\nIncreasing may help under high load", "tip.log_mb": "Maximum log file size; the file is overwritten when the limit is reached", diff --git a/ui/i18n/ru.json b/ui/i18n/ru.json index 67f266a..bc0919a 100644 --- a/ui/i18n/ru.json +++ b/ui/i18n/ru.json @@ -32,6 +32,7 @@ "label.cf_custom_domain": "Свой домен", "label.cfworker_domains": "Cloudflare Worker домены (через запятую)", "label.verbose": "Подробное логирование (verbose)", + "label.no_secure": "Выключиь SSL для CF-прокси и CF-worker", "label.buf_kb": "Буфер, КБ (по умолчанию 256)", "label.pool_size": "Пул WebSocket-сессий (по умолчанию 4)", "label.log_max_mb": "Макс. размер лога, МБ (по умолчанию 5)", @@ -44,6 +45,7 @@ "tip.secret": "Секретный ключ для авторизации клиентов", "tip.dc": "Соответствие номера датацентра Telegram (DC) и IP-адреса сервера.\nКаждая строка: «номер:IP», например 4:149.154.167.220. Прокси по этим правилам направляет трафик к нужным серверам Telegram\n\nЕсли у вас не работают медиа и работает CF-прокси, то попробуйте убрать строку 2:149.154.167.220", "tip.verbose": "Если включено, в файл логов пишется больше подробностей — необходимо при поиске неполадок", + "tip.no_secure": "Если включено, прокси будет использовать 80 порт для CF-прокси и CF-worker подключений", "tip.buf_kb": "Размер буфера приёма/передачи в килобайтах.\nБольше значение — больше выделение памяти на сокет", "tip.pool": "Сколько параллельных WebSocket-сессий к одному датацентру можно держать.\nУвеличение может помочь при высокой нагрузке", "tip.log_mb": "Максимальный размер файла лога; при достижении лимита файл перезаписывается", diff --git a/utils/default_config.py b/utils/default_config.py index 4a0556f..ab50496 100644 --- a/utils/default_config.py +++ b/utils/default_config.py @@ -25,6 +25,7 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = { "cfproxy_worker_enabled": False, "cfproxy_worker_domain": [], "force_test_dc": False, + "no_secure": False, } diff --git a/utils/tray_common.py b/utils/tray_common.py index 137cfda..2970d3d 100644 --- a/utils/tray_common.py +++ b/utils/tray_common.py @@ -374,6 +374,7 @@ def apply_proxy_config(cfg: dict) -> bool: else [] ) pc.force_test_dc = cfg.get("force_test_dc", DEFAULT_CONFIG["force_test_dc"]) + pc.disable_secure = cfg.get("no_secure", DEFAULT_CONFIG["no_secure"]) return True