From 6f02fc1c46855fbcbb380db2ea0a0277e3f25019 Mon Sep 17 00:00:00 2001 From: Flowseal Date: Sat, 16 May 2026 11:14:23 +0300 Subject: [PATCH] remove cf priority flag, cf worker ui setup --- docs/BuildFromSource.md | 1 - docs/TrayConfig.md | 1 - macos.py | 43 ++++++-- proxy/bridge.py | 11 +- proxy/config.py | 1 - proxy/tg_ws_proxy.py | 16 +-- ui/ctk_tray_ui.py | 221 +++++++++++++++++++++++++++++----------- utils/default_config.py | 1 - utils/tray_common.py | 1 - 9 files changed, 203 insertions(+), 93 deletions(-) diff --git a/docs/BuildFromSource.md b/docs/BuildFromSource.md index adbd9b9..3f80747 100644 --- a/docs/BuildFromSource.md +++ b/docs/BuildFromSource.md @@ -49,7 +49,6 @@ 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) | -| `--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` | Размер буфера в КБ | diff --git a/docs/TrayConfig.md b/docs/TrayConfig.md index 14822b9..d9c2947 100644 --- a/docs/TrayConfig.md +++ b/docs/TrayConfig.md @@ -21,7 +21,6 @@ Tray-приложение хранит данные в: "log_max_mb": 5.0, "check_updates": true, "cfproxy": true, - "cfproxy_priority": true, "cfproxy_user_domain": "", "cfproxy_worker_domain": "", "appearance": "auto" diff --git a/macos.py b/macos.py index 3b1915d..b0232c1 100644 --- a/macos.py +++ b/macos.py @@ -41,6 +41,8 @@ _app: Optional[object] = None _config: dict = {} _exiting: bool = False +_CFWORKER_HELP_URL = "https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfWorker.md" + # osascript dialogs @@ -109,6 +111,32 @@ def _osascript_input(prompt: str, default: str, title: str = "TG WS Proxy") -> O return r.stdout.rstrip("\r\n") +def _ask_cfworker_domain(default: str) -> Optional[str]: + value = default + while True: + script = ( + f'set d to display dialog "{_esc("Cloudflare Worker домен (например, name.account.workers.dev):")}" ' + f'default answer "{_esc(value)}" ' + f'with title "TG WS Proxy" ' + f'buttons {{"Закрыть", "?", "OK"}} ' + f'default button "OK" cancel button "Закрыть"\n' + f'return (button returned of d) & "\\n" & (text returned of d)' + ) + r = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) + if r.returncode != 0: + return None + + out_lines = r.stdout.splitlines() + button = out_lines[0].strip() if out_lines else "" + value = out_lines[1].strip() if len(out_lines) > 1 else value + + if button == "?": + webbrowser.open(_CFWORKER_HELP_URL) + continue + if button == "OK": + return value.strip() + + # menubar icon @@ -396,13 +424,6 @@ def _edit_config_dialog() -> None: if cfproxy is None: return - cfproxy_priority = True - if cfproxy: - cfproxy_priority_result = _ask_yes_no_close("Приоритет CfProxy (пробовать раньше прямого TCP)?") - if cfproxy_priority_result is None: - return - cfproxy_priority = cfproxy_priority_result - cfproxy_domain = _osascript_input( "Свой CF-домен (оставьте пустым для автоматического выбора):\n" "DNS записи kws1-kws5,kws203 должны указывать на IP датацентров Telegram через Cloudflare.", @@ -412,6 +433,12 @@ def _edit_config_dialog() -> None: return cfproxy_domain = cfproxy_domain.strip() + cfworker_domain = _ask_cfworker_domain( + cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG.get("cfproxy_worker_domain", "")) + ) + if cfworker_domain is None: + return + new_cfg = { "host": host, "port": port, @@ -423,8 +450,8 @@ def _edit_config_dialog() -> None: "log_max_mb": adv.get("log_max_mb", cfg.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"])), "check_updates": cfg.get("check_updates", True), "cfproxy": cfproxy, - "cfproxy_priority": cfproxy_priority, "cfproxy_user_domain": cfproxy_domain, + "cfproxy_worker_domain": cfworker_domain, } save_config(new_cfg) log.info("Config saved: %s", new_cfg) diff --git a/proxy/bridge.py b/proxy/bridge.py index b4c51cd..0fa2e15 100644 --- a/proxy/bridge.py +++ b/proxy/bridge.py @@ -132,15 +132,16 @@ async def do_fallback(reader, writer, relay_init, label, ctx: CryptoCtx, splitter=None): fallback_dst = DC_DEFAULT_IPS.get(dc) use_cf = proxy_config.fallback_cfproxy - cf_first = proxy_config.fallback_cfproxy_priority worker_domain = proxy_config.cfproxy_worker_domain - methods: List[str] = ['tcp'] + methods: List[str] = [] + if worker_domain and fallback_dst: + methods.append('cf_worker') if use_cf: - methods.insert(0 if cf_first else 1, 'cf') - if worker_domain: - methods.insert(0, 'cf_worker') + methods.append('cf') + if fallback_dst: + methods.append('tcp') for method in methods: if method == 'cf_worker' and fallback_dst: diff --git a/proxy/config.py b/proxy/config.py index 85a8948..907a4b2 100644 --- a/proxy/config.py +++ b/proxy/config.py @@ -58,7 +58,6 @@ class ProxyConfig: buffer_size: int = 256 * 1024 pool_size: int = 4 fallback_cfproxy: bool = True - fallback_cfproxy_priority: bool = True cfproxy_user_domain: str = '' cfproxy_worker_domain: str = '' fake_tls_domain: str = '' diff --git a/proxy/tg_ws_proxy.py b/proxy/tg_ws_proxy.py index fa984b5..8e875c3 100644 --- a/proxy/tg_ws_proxy.py +++ b/proxy/tg_ws_proxy.py @@ -587,9 +587,8 @@ async def _run(stop_event: Optional[asyncio.Event] = None): ip = proxy_config.dc_redirects.get(dc) log.info(" DC%d: %s", dc, ip) if proxy_config.fallback_cfproxy: - prio = 'CF first' if proxy_config.fallback_cfproxy_priority else 'TCP first' 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)", user_domain) if proxy_config.cfproxy_worker_domain: log.info(" CF worker: enabled (%s)", proxy_config.cfproxy_worker_domain) @@ -654,16 +653,6 @@ def run_proxy(stop_event: Optional[asyncio.Event] = None): def main(): - def _parse_bool(value: str) -> bool: - lowered = value.strip().lower() - if lowered == 'true': - return True - if lowered == 'false': - return False - raise argparse.ArgumentTypeError( - "Expected boolean value: true or false", - ) - ap = argparse.ArgumentParser( description='Telegram MTProto WebSocket Bridge Proxy') ap.add_argument('--port', type=int, default=1443, @@ -696,8 +685,6 @@ def main(): '(tried before other fallback methods)') ap.add_argument('--no-cfproxy', action='store_true', help='Disable Cloudflare proxy fallback') - ap.add_argument('--cfproxy-priority', type=_parse_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 ' @@ -737,7 +724,6 @@ def main(): proxy_config.buffer_size = max(4, args.buf_kb) * 1024 proxy_config.pool_size = max(0, args.pool_size) proxy_config.fallback_cfproxy = not args.no_cfproxy - proxy_config.fallback_cfproxy_priority = args.cfproxy_priority proxy_config.cfproxy_user_domain = args.cfproxy_domain.strip() proxy_config.cfproxy_worker_domain = args.cfproxy_worker_domain.strip() proxy_config.fake_tls_domain = args.fake_tls_domain.strip() diff --git a/ui/ctk_tray_ui.py b/ui/ctk_tray_ui.py index 6026c45..4b58b66 100644 --- a/ui/ctk_tray_ui.py +++ b/ui/ctk_tray_ui.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import os import webbrowser from dataclasses import dataclass @@ -17,6 +18,8 @@ from ui.ctk_theme import ( ) from ui.ctk_tooltip import attach_ctk_tooltip, attach_tooltip_to_widgets +log = logging.getLogger('tg-mtproto-proxy') + _TIP_HOST = ( "Адрес, на котором прокси принимает подключения.\n" "Обычно 127.0.0.1 — локальная сеть, 0.0.0.0 - все интерфейсы" @@ -55,9 +58,6 @@ _TIP_CHECK_UPDATES = "При запуске проверять наличие о _TIP_CFPROXY = ( "Использовать Cloudflare прокси для недоступных датацентров" ) -_TIP_CFPROXY_PRIORITY = ( - "Пробовать CF-прокси раньше прямого TCP-подключения" -) _TIP_CFPROXY_DOMAIN = ( "Ваш собственный домен, проксируемый через Cloudflare, для WS-подключения.\n" "Если не указан — выбирается автоматически из поддерживаемых доменов" @@ -65,14 +65,27 @@ _TIP_CFPROXY_DOMAIN = ( _TIP_CFPROXY_USER_DOMAIN_CB = ( "Указать свой домен вместо автоматического выбора" ) +_TIP_CFWORKER_DOMAIN = ( + "Домен Cloudflare Worker (например, name.account.workers.dev).\n" + "Прокси передает через него подключение к Telegram DC по IP" +) _TIP_SAVE = "Сохранить настройки" _TIP_CANCEL = "Закрыть окно без сохранения изменений" _CFPROXY_HELP_URL = "https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md" +_CFWORKER_HELP_URL = "https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfWorker.md" _CFPROXY_TEST_DCS = [1, 2, 3, 4, 5, 203] +_CFWORKER_TEST_DST = { + 1: '149.154.175.50', + 2: '149.154.167.51', + 3: '149.154.175.100', + 4: '149.154.167.91', + 5: '149.154.171.5', + 203: '91.105.192.100', +} -def _run_cfproxy_connectivity_test(domain: str) -> dict: +def _run_connectivity_test(cases: list) -> dict: import base64 import ssl import socket as _socket @@ -81,15 +94,14 @@ def _run_cfproxy_connectivity_test(domain: str) -> dict: ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE results = {} - for dc in _CFPROXY_TEST_DCS: - host = f"kws{dc}.{domain}" + for dc, connect_host, sni_host, req_host, path in cases: try: - with _socket.create_connection((host, 443), timeout=5) as raw: - with ctx.wrap_socket(raw, server_hostname=host) as ssock: + with _socket.create_connection((connect_host, 443), timeout=5) as raw: + with ctx.wrap_socket(raw, server_hostname=sni_host) as ssock: ws_key = base64.b64encode(os.urandom(16)).decode() req = ( - f"GET /apiws HTTP/1.1\r\n" - f"Host: {host}\r\n" + f"GET {path} HTTP/1.1\r\n" + f"Host: {req_host}\r\n" f"Upgrade: websocket\r\n" f"Connection: Upgrade\r\n" f"Sec-WebSocket-Key: {ws_key}\r\n" @@ -120,6 +132,23 @@ def _run_cfproxy_connectivity_test(domain: str) -> dict: return results +def _run_cfproxy_connectivity_test(domain: str) -> 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) + + +def _run_cfworker_connectivity_test(domain: str) -> 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) + + def _run_cfproxy_auto_test(domains: list) -> tuple: merged: dict = {} best_domain = None @@ -136,49 +165,39 @@ def _run_cfproxy_auto_test(domains: list) -> tuple: return best_domain, merged -def _cfproxy_show_test_results(domain: str, results: dict) -> None: +def _show_connectivity_results(title_base: str, results: dict, + domain: str = '', label_prefix: str = 'DC', + auto_mode: bool = False, + unavailable_message: str = '') -> None: import tkinter as _tk from tkinter import messagebox as _mb ok = [dc for dc, v in results.items() if v is True] - fail = [(dc, v) for dc, v in results.items() if v is not True] - if len(ok) == len(_CFPROXY_TEST_DCS): - title = "CF-прокси: всё работает" - msg = f"\u2713 Все {len(_CFPROXY_TEST_DCS)} серверов доступны через {domain}." - elif not ok: - title = "CF-прокси: недоступен" - msg = f"\u2717 Ни один сервер не отвечает через {domain}.\n\nОшибки:\n" - msg += "\n".join(f" kws{dc}: {v}" for dc, v in fail) + if auto_mode: + if domain: + title = f"{title_base}: доступен" + msg = f"\u2713 {title_base} работает. {len(ok)} из {len(_CFPROXY_TEST_DCS)} серверов доступны." + else: + title = f"{title_base}: недоступен" + msg = unavailable_message else: - title = "CF-прокси: частично работает" - msg = ( - f"Домен: {domain}\n\n" - f"\u2713 Работают: {', '.join(f'kws{dc}' for dc in ok)}\n\n" - f"\u2717 Недоступны:\n" - + "\n".join(f" kws{dc}: {v}" for dc, v in fail) - ) - root = _tk.Tk() - root.withdraw() - try: - root.attributes("-topmost", True) - except Exception: - pass - _mb.showinfo(title, msg, parent=root) - root.destroy() + fail = [(dc, v) for dc, v in results.items() if v is not True] + if len(ok) == len(_CFPROXY_TEST_DCS): + title = f"{title_base}: всё работает" + msg = f"\u2713 Все {len(_CFPROXY_TEST_DCS)} серверов доступны через {domain}." + elif not ok: + title = f"{title_base}: недоступен" + msg = f"\u2717 Ни один сервер не отвечает через {domain}.\n\nОшибки:\n" + msg += "\n".join(f" {label_prefix}{dc}: {v}" for dc, v in fail) + else: + title = f"{title_base}: частично работает" + msg = ( + f"Домен: {domain}\n\n" + f"\u2713 Работают: {', '.join(f'{label_prefix}{dc}' for dc in ok)}\n\n" + f"\u2717 Недоступны:\n" + + "\n".join(f" {label_prefix}{dc}: {v}" for dc, v in fail) + ) - -def _cfproxy_show_auto_test_results(ok_domain, results: dict) -> None: - import tkinter as _tk - from tkinter import messagebox as _mb - - if ok_domain is not None: - title = "CF-прокси: доступен" - ok = [dc for dc, v in results.items() if v is True] - msg = f"\u2713 CF-прокси работает. {len(ok)} из {len(_CFPROXY_TEST_DCS)} серверов доступны." - else: - title = "CF-прокси: недоступен" - msg = "\u2717 Ни один из автоматических CF-доменов не отвечает.\n" - msg += "Возможно, блокировка или проблемы с сетью." root = _tk.Tk() root.withdraw() try: @@ -296,8 +315,8 @@ class TrayConfigFormWidgets: autostart_var: Optional[Any] check_updates_var: Optional[Any] cfproxy_var: Optional[Any] = None - cfproxy_priority_var: Optional[Any] = None cfproxy_user_domain_var: Optional[Any] = None + cfproxy_worker_domain_var: Optional[Any] = None appearance_var: Optional[Any] = None @@ -428,13 +447,6 @@ def install_tray_config_form( cf_cb.pack(side="left", padx=(0, 16)) attach_ctk_tooltip(cf_cb, _TIP_CFPROXY) - cfproxy_priority_var = ctk.BooleanVar( - value=cfg.get("cfproxy_priority", default_config.get("cfproxy_priority", True)) - ) - cf_prio_cb = _checkbox(ctk, cf_row, theme, "Приоритет", cfproxy_priority_var) - cf_prio_cb.pack(side="left") - attach_ctk_tooltip(cf_prio_cb, _TIP_CFPROXY_PRIORITY) - _cf_test_btn = [None] def _on_cf_test(): @@ -448,7 +460,12 @@ def install_tray_config_form( try: res = _run_cfproxy_connectivity_test(user_domain) if btn: - btn.after(0, lambda: _cfproxy_show_test_results(user_domain, res)) + btn.after( + 0, + lambda: _show_connectivity_results( + "CF-прокси", res, domain=user_domain, label_prefix='kws', + ), + ) except Exception as exc: log.error("CF proxy test failed: %s", exc) finally: @@ -460,7 +477,17 @@ def install_tray_config_form( try: ok_domain, res = _run_cfproxy_auto_test(balancer.domains) if btn: - btn.after(0, lambda: _cfproxy_show_auto_test_results(ok_domain, res)) + btn.after( + 0, + lambda: _show_connectivity_results( + "CF-прокси", res, + domain=ok_domain or '', + auto_mode=True, + unavailable_message=( + "\u2717 Ни один из автоматических CF-доменов не отвечает." + ), + ), + ) except Exception as exc: log.error("CF proxy auto-test failed: %s", exc) finally: @@ -512,6 +539,80 @@ def install_tray_config_form( cf_custom_cb_var.trace_add("write", _sync_domain_entry) _sync_domain_entry() + cf_worker_inner = _config_section(ctk, frame, theme, "Cloudflare Worker") + + cf_worker_row = ctk.CTkFrame(cf_worker_inner, fg_color="transparent") + cf_worker_row.pack(fill="x", pady=(0, 4)) + cf_worker_lbl = _label(ctk, cf_worker_row, theme, "Cloudflare Worker домен", size=11) + cf_worker_lbl.pack(anchor="w", pady=(0, 2)) + + cf_worker_input = ctk.CTkFrame(cf_worker_inner, fg_color="transparent") + cf_worker_input.pack(fill="x") + + cfproxy_worker_domain_var = ctk.StringVar( + value=cfg.get("cfproxy_worker_domain", default_config.get("cfproxy_worker_domain", "")) + ) + cf_worker_entry = _entry( + ctk, cf_worker_input, theme, var=cfproxy_worker_domain_var, + height=32, radius=8, + ) + cf_worker_entry.pack(side="left", fill="x", expand=True, padx=(0, 6)) + attach_tooltip_to_widgets([cf_worker_lbl, cf_worker_entry], _TIP_CFWORKER_DOMAIN) + + _cfworker_test_btn = [None] + + def _sync_cfworker_test_button(*_): + btn = _cfworker_test_btn[0] + if btn is None: + return + enabled = bool(cfproxy_worker_domain_var.get().strip()) + btn.configure(state="normal" if enabled else "disabled") + + def _on_cfworker_test(): + domain = cfproxy_worker_domain_var.get().strip() + btn = _cfworker_test_btn[0] + if not domain or btn is None: + return + btn.configure(text="...", state="disabled") + import threading as _threading + + def _worker(): + try: + res = _run_cfworker_connectivity_test(domain) + btn.after( + 0, + lambda: _show_connectivity_results( + "CF Worker", res, domain=domain, label_prefix='DC', + ), + ) + except Exception as exc: + log.error("CF worker test failed: %s", exc) + finally: + btn.after(0, lambda: btn.configure(text="Тест")) + btn.after(0, _sync_cfworker_test_button) + + _threading.Thread(target=_worker, daemon=True).start() + + ctk.CTkButton( + cf_worker_input, text="?", width=28, height=32, + font=(theme.ui_font_family, 14), corner_radius=8, + fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover, + text_color="#ffffff", border_width=1, border_color=theme.field_border, + command=lambda: webbrowser.open(_CFWORKER_HELP_URL), + ).pack(side="right") + + _cfworker_test_widget = ctk.CTkButton( + cf_worker_input, text="Тест", width=56, height=32, + font=(theme.ui_font_family, 13), corner_radius=8, + fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover, + text_color="#ffffff", border_width=1, border_color=theme.field_border, + command=_on_cfworker_test, + ) + _cfworker_test_widget.pack(side="right", padx=(0, 6)) + _cfworker_test_btn[0] = _cfworker_test_widget + cfproxy_worker_domain_var.trace_add("write", _sync_cfworker_test_button) + _sync_cfworker_test_button() + log_inner = _config_section(ctk, frame, theme, "Логи и производительность") verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False)) @@ -601,8 +702,8 @@ def install_tray_config_form( adv_entries=adv_entries, adv_keys=adv_keys, autostart_var=autostart_var, check_updates_var=check_updates_var, cfproxy_var=cfproxy_var, - cfproxy_priority_var=cfproxy_priority_var, cfproxy_user_domain_var=cfproxy_user_domain_var, + cfproxy_worker_domain_var=cfproxy_worker_domain_var, appearance_var=appearance_var, ) @@ -682,10 +783,10 @@ def validate_config_form( new_cfg["check_updates"] = bool(widgets.check_updates_var.get()) if widgets.cfproxy_var is not None: new_cfg["cfproxy"] = bool(widgets.cfproxy_var.get()) - if widgets.cfproxy_priority_var is not None: - new_cfg["cfproxy_priority"] = bool(widgets.cfproxy_priority_var.get()) if widgets.cfproxy_user_domain_var is not None: new_cfg["cfproxy_user_domain"] = widgets.cfproxy_user_domain_var.get().strip() + if widgets.cfproxy_worker_domain_var is not None: + new_cfg["cfproxy_worker_domain"] = widgets.cfproxy_worker_domain_var.get().strip() if widgets.appearance_var is not None: new_cfg["appearance"] = _APPEARANCE_TO_CFG.get(widgets.appearance_var.get(), "auto") return new_cfg diff --git a/utils/default_config.py b/utils/default_config.py index 037edd7..fa6d427 100644 --- a/utils/default_config.py +++ b/utils/default_config.py @@ -18,7 +18,6 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = { "buf_kb": 256, "pool_size": 4, "cfproxy": True, - "cfproxy_priority": True, "cfproxy_user_domain": "", "cfproxy_worker_domain": "", } diff --git a/utils/tray_common.py b/utils/tray_common.py index 9d63e5c..6595fb2 100644 --- a/utils/tray_common.py +++ b/utils/tray_common.py @@ -271,7 +271,6 @@ def apply_proxy_config(cfg: dict) -> bool: pc.buffer_size = max(4, cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])) * 1024 pc.pool_size = max(0, cfg.get("pool_size", DEFAULT_CONFIG["pool_size"])) pc.fallback_cfproxy = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"]) - pc.fallback_cfproxy_priority = cfg.get("cfproxy_priority", DEFAULT_CONFIG["cfproxy_priority"]) pc.cfproxy_user_domain = cfg.get("cfproxy_user_domain", DEFAULT_CONFIG["cfproxy_user_domain"]) pc.cfproxy_worker_domain = cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG["cfproxy_worker_domain"]) return True