Possibility to pass few cfproxy and worker domains

This commit is contained in:
Flowseal
2026-05-30 20:30:47 +03:00
parent 05d6de269b
commit ea4e8e790a
10 changed files with 202 additions and 103 deletions

View File

@@ -47,8 +47,8 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
| `--secret` | `random` | 32-значный hex-ключ для авторизации клиентов | | `--secret` | `random` | 32-значный hex-ключ для авторизации клиентов |
| `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (параметр можно указывать несколько раз) | | `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (параметр можно указывать несколько раз) |
| `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare](./CfProxy.md) | | `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare](./CfProxy.md) |
| `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudflare. [Подробнее](./CfProxy.md) | | `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudflare [Подробнее](./CfProxy.md). Можно указать несколько через повторение аргумента. |
| `--cfproxy-worker-domain` | | Домен Cloudflare Worker [Подробнее](./CfWorker.md) | | `--cfproxy-worker-domain` | | Домен Cloudflare Worker [Подробнее](./CfWorker.md). Можно указать несколько через повторение аргумента. |
| `--fake-tls-domain` | | Включить маскировку Fake TLS (ee-secret) с указанным SNI-доменом | | `--fake-tls-domain` | | Включить маскировку Fake TLS (ee-secret) с указанным SNI-доменом |
| `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) | | `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) |
| `--buf-kb` | `256` | Размер буфера в КБ | | `--buf-kb` | `256` | Размер буфера в КБ |

View File

@@ -24,7 +24,7 @@ try:
except ImportError: except ImportError:
pyperclip = None pyperclip = None
from proxy import __version__, get_link_host, parse_dc_ip_list, proxy_config from proxy import __version__, get_link_host, parse_dc_ip_list, proxy_config, coerce_domain_list
from proxy.tg_ws_proxy import _run from proxy.tg_ws_proxy import _run
from utils.tray_common import ( from utils.tray_common import (
@@ -115,7 +115,7 @@ def _ask_cfworker_domain(default: str) -> Optional[str]:
value = default value = default
while True: while True:
script = ( script = (
f'set d to display dialog "{_esc("Cloudflare Worker домен (например, name.account.workers.dev):")}" ' f'set d to display dialog "{_esc("Cloudflare Worker домены через запятую (например, name.account.workers.dev):")}" '
f'default answer "{_esc(value)}" ' f'default answer "{_esc(value)}" '
f'with title "TG WS Proxy" ' f'with title "TG WS Proxy" '
f'buttons {{"Закрыть", "?", "OK"}} ' f'buttons {{"Закрыть", "?", "OK"}} '
@@ -425,19 +425,24 @@ def _edit_config_dialog() -> None:
return return
cfproxy_domain = _osascript_input( cfproxy_domain = _osascript_input(
"Свой CF-домен (оставьте пустым для автоматического выбора):\n" "Свои CF-домены через запятую (оставьте пустым для автоматического выбора):\n"
"DNS записи kws1-kws5,kws203 должны указывать на IP датацентров Telegram через Cloudflare.", "DNS записи kws1-kws5,kws203 должны указывать на IP датацентров Telegram через Cloudflare.",
cfg.get("cfproxy_user_domain", DEFAULT_CONFIG.get("cfproxy_user_domain", "")), ", ".join(coerce_domain_list(
cfg.get("cfproxy_user_domain", DEFAULT_CONFIG.get("cfproxy_user_domain", []))
)),
) )
if cfproxy_domain is None: if cfproxy_domain is None:
return return
cfproxy_domain = cfproxy_domain.strip() cfproxy_domains = coerce_domain_list(cfproxy_domain)
cfworker_domain = _ask_cfworker_domain( cfworker_domain = _ask_cfworker_domain(
cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG.get("cfproxy_worker_domain", "")) ", ".join(coerce_domain_list(
cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG.get("cfproxy_worker_domain", []))
))
) )
if cfworker_domain is None: if cfworker_domain is None:
return return
cfworker_domains = coerce_domain_list(cfworker_domain)
new_cfg = { new_cfg = {
"host": host, "host": host,
@@ -450,8 +455,8 @@ def _edit_config_dialog() -> None:
"log_max_mb": adv.get("log_max_mb", cfg.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"])), "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), "check_updates": cfg.get("check_updates", True),
"cfproxy": cfproxy, "cfproxy": cfproxy,
"cfproxy_user_domain": cfproxy_domain, "cfproxy_user_domain": cfproxy_domains,
"cfproxy_worker_domain": cfworker_domain, "cfproxy_worker_domain": cfworker_domains,
} }
save_config(new_cfg) save_config(new_cfg)
log.info("Config saved: %s", new_cfg) log.info("Config saved: %s", new_cfg)

View File

@@ -1,6 +1,6 @@
from .config import parse_dc_ip_list, proxy_config 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.7.0" __version__ = "1.7.0"
__all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list", "build_github_opener"] __all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list", "build_github_opener", "coerce_domain_list"]

View File

@@ -133,11 +133,11 @@ async def do_fallback(reader, writer, relay_init, label,
ctx: CryptoCtx, splitter=None): ctx: CryptoCtx, splitter=None):
fallback_dst = DC_DEFAULT_IPS.get(dc) fallback_dst = DC_DEFAULT_IPS.get(dc)
use_cf = proxy_config.fallback_cfproxy use_cf = proxy_config.fallback_cfproxy
worker_domain = proxy_config.cfproxy_worker_domain worker_domains = proxy_config.cfproxy_worker_domains
methods: List[str] = [] methods: List[str] = []
if worker_domain and fallback_dst: if worker_domains and fallback_dst:
methods.append('cf_worker') methods.append('cf_worker')
if use_cf: if use_cf:
methods.append('cf') methods.append('cf')
@@ -176,38 +176,40 @@ async def _cfproxy_worker_fallback(reader, writer, relay_init, label,
fallback_dst: str, fallback_dst: str,
splitter=None): splitter=None):
media_tag = ' media' if is_media else '' media_tag = ' media' if is_media else ''
worker_domain = proxy_config.cfproxy_worker_domain worker_domains = proxy_config.cfproxy_worker_domains
if not worker_domain: if not worker_domains:
return False return False
ws = await cf_worker_pool.get(dc, worker_domain, fallback_dst) for worker_domain in worker_domains:
if ws: ws = await cf_worker_pool.get(dc, worker_domain, fallback_dst)
log.info("[%s] DC%d%s -> CF worker pool hit for %s", if ws:
label, dc, media_tag, fallback_dst) log.info("[%s] DC%d%s -> CF worker pool hit for %s",
else: label, dc, media_tag, fallback_dst)
query = urlencode({ else:
'dst': fallback_dst, query = urlencode({
'dc': str(dc), 'dst': fallback_dst,
}) 'dc': str(dc),
path = f'/apiws?{query}' })
path = f'/apiws?{query}'
log.info("[%s] DC%d%s -> trying CF worker for %s", log.info("[%s] DC%d%s -> trying CF worker %s for %s",
label, dc, media_tag, fallback_dst) label, dc, media_tag, worker_domain, fallback_dst)
try: try:
ws = await RawWebSocket.connect(worker_domain, worker_domain, ws = await RawWebSocket.connect(worker_domain, worker_domain,
timeout=10.0, path=path) timeout=10.0, path=path)
except Exception as exc: except Exception as exc:
log.warning("[%s] DC%d%s CF worker failed: %s", log.warning("[%s] DC%d%s CF worker %s failed: %s",
label, dc, media_tag, repr(exc)) label, dc, media_tag, worker_domain, repr(exc))
return False continue
stats.connections_cfproxy += 1 stats.connections_cfproxy += 1
await ws.send(relay_init) await ws.send(relay_init)
await bridge_ws_reencrypt(reader, writer, ws, label, ctx, await bridge_ws_reencrypt(reader, writer, ws, label, ctx,
dc=dc, is_media=is_media, dc=dc, is_media=is_media,
splitter=splitter) splitter=splitter)
return True return True
return False
async def _cfproxy_fallback(reader, writer, relay_init, label, async def _cfproxy_fallback(reader, writer, relay_init, label,

View File

@@ -58,8 +58,8 @@ class ProxyConfig:
buffer_size: int = 256 * 1024 buffer_size: int = 256 * 1024
pool_size: int = 4 pool_size: int = 4
fallback_cfproxy: bool = True fallback_cfproxy: bool = True
cfproxy_user_domain: str = '' cfproxy_user_domains: List[str] = field(default_factory=list)
cfproxy_worker_domain: str = '' 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
@@ -67,6 +67,30 @@ class ProxyConfig:
proxy_config = ProxyConfig() proxy_config = ProxyConfig()
def coerce_domain_list(value) -> List[str]:
if isinstance(value, str):
items = value.replace(',', ' ').replace(';', ' ').split()
elif isinstance(value, (list, tuple)):
items: List[str] = []
for entry in value:
if isinstance(entry, str):
items.extend(entry.replace(',', ' ').replace(';', ' ').split())
else:
return []
seen = set()
result: List[str] = []
for item in items:
item = item.strip()
if not item:
continue
key = item.lower()
if key in seen:
continue
seen.add(key)
result.append(item)
return result
def _fetch_cfproxy_domain_list() -> List[str]: def _fetch_cfproxy_domain_list() -> List[str]:
try: try:
req = Request(CFPROXY_DOMAINS_URL + "?" + "".join(random.choices(string.ascii_letters, k=7)), req = Request(CFPROXY_DOMAINS_URL + "?" + "".join(random.choices(string.ascii_letters, k=7)),
@@ -120,7 +144,7 @@ def _normalize_domain_pool(domains: List[str]) -> List[str]:
def refresh_cfproxy_domains() -> None: def refresh_cfproxy_domains() -> None:
if proxy_config.cfproxy_user_domain: if proxy_config.cfproxy_user_domains:
return return
fetched = _fetch_cfproxy_domain_list() fetched = _fetch_cfproxy_domain_list()

View File

@@ -114,16 +114,17 @@ class _CfWorkerPool:
WS_POOL_MAX_AGE = 120.0 WS_POOL_MAX_AGE = 120.0
def __init__(self): def __init__(self):
self._idle: Dict[int, deque] = {} self._idle: Dict[Tuple[int, str], deque] = {}
self._refilling: Set[int] = set() self._refilling: Set[Tuple[int, str]] = set()
async def get(self, dc: int, worker_domain: str, fallback_dst: str) -> Optional[RawWebSocket]: async def get(self, dc: int, worker_domain: str, fallback_dst: str) -> Optional[RawWebSocket]:
now = time.monotonic() now = time.monotonic()
key = (dc, worker_domain)
bucket = self._idle.get(dc) bucket = self._idle.get(key)
if bucket is None: if bucket is None:
bucket = deque() bucket = deque()
self._idle[dc] = bucket self._idle[key] = bucket
while bucket: while bucket:
ws, created = bucket.popleft() ws, created = bucket.popleft()
age = now - created age = now - created
@@ -134,22 +135,23 @@ class _CfWorkerPool:
stats.cf_pool_hits += 1 stats.cf_pool_hits += 1
log.debug("CF worker pool hit DC%d (age=%.1fs, left=%d)", log.debug("CF worker pool hit DC%d (age=%.1fs, left=%d)",
dc, age, len(bucket)) dc, age, len(bucket))
self._schedule_refill(dc, worker_domain, fallback_dst) self._schedule_refill(key, fallback_dst)
return ws return ws
stats.cf_pool_misses += 1 stats.cf_pool_misses += 1
self._schedule_refill(dc, worker_domain, fallback_dst) self._schedule_refill(key, fallback_dst)
return None return None
def _schedule_refill(self, dc, worker_domain, fallback_dst): def _schedule_refill(self, key, fallback_dst):
if dc in self._refilling: if key in self._refilling:
return return
self._refilling.add(dc) self._refilling.add(key)
asyncio.create_task(self._refill(dc, worker_domain, fallback_dst)) asyncio.create_task(self._refill(key, fallback_dst))
async def _refill(self, dc, worker_domain, fallback_dst): async def _refill(self, key, fallback_dst):
dc, worker_domain = key
try: try:
bucket = self._idle.setdefault(dc, deque()) bucket = self._idle.setdefault(key, deque())
needed = proxy_config.pool_size - len(bucket) needed = proxy_config.pool_size - len(bucket)
if needed <= 0: if needed <= 0:
return return
@@ -166,7 +168,7 @@ class _CfWorkerPool:
log.debug("CF worker pool refilled DC%d: %d ready", log.debug("CF worker pool refilled DC%d: %d ready",
dc, len(bucket)) dc, len(bucket))
finally: finally:
self._refilling.discard(dc) self._refilling.discard(key)
@staticmethod @staticmethod
async def _connect_one(worker_domain, fallback_dst, dc) -> Optional[RawWebSocket]: async def _connect_one(worker_domain, fallback_dst, dc) -> Optional[RawWebSocket]:
@@ -194,12 +196,13 @@ class _CfWorkerPool:
if dc not in proxy_config.dc_redirects if dc not in proxy_config.dc_redirects
} }
if not cf_fallbacks or not proxy_config.cfproxy_worker_domain: if not cf_fallbacks or not proxy_config.cfproxy_worker_domains:
return return
for dc, fallback_dst in cf_fallbacks.items(): for worker_domain in proxy_config.cfproxy_worker_domains:
self._schedule_refill(dc, proxy_config.cfproxy_worker_domain, fallback_dst) for dc, fallback_dst in cf_fallbacks.items():
self._schedule_refill((dc, worker_domain), fallback_dst)
log.info("CF worker pool warmup started for %d DC(s)", len(cf_fallbacks)) log.info("CF worker pool warmup started for %d DC(s)", len(cf_fallbacks))
def reset(self): def reset(self):

View File

@@ -22,7 +22,7 @@ if __name__ == '__main__' and (__package__ is None or __package__ == ''):
from .utils import * from .utils import *
from .stats import stats from .stats import stats
from .config import proxy_config, parse_dc_ip_list, start_cfproxy_domain_refresh from .config import proxy_config, parse_dc_ip_list, start_cfproxy_domain_refresh, coerce_domain_list
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 from .fake_tls import proxy_to_masking_domain, verify_client_hello, build_server_hello, FakeTlsStream, TLS_RECORD_HANDSHAKE
@@ -439,9 +439,9 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
_client_tasks.clear() _client_tasks.clear()
if proxy_config.fallback_cfproxy: if proxy_config.fallback_cfproxy:
user = proxy_config.cfproxy_user_domain user = proxy_config.cfproxy_user_domains
if user: if user:
balancer.update_domains_list([user]) balancer.update_domains_list(user)
else: else:
start_cfproxy_domain_refresh() start_cfproxy_domain_refresh()
@@ -484,11 +484,11 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
ip = proxy_config.dc_redirects.get(dc) ip = proxy_config.dc_redirects.get(dc)
log.info(" DC%d: %s", dc, ip) log.info(" DC%d: %s", dc, ip)
if proxy_config.fallback_cfproxy: if proxy_config.fallback_cfproxy:
user_domain = "user" if proxy_config.cfproxy_user_domain else "auto" user_domain = "user" if proxy_config.cfproxy_user_domains else "auto"
log.info(" CF proxy: enabled (%s)", user_domain) log.info(" CF proxy: enabled (%s)", user_domain)
if proxy_config.cfproxy_worker_domain: if proxy_config.cfproxy_worker_domains:
log.info(" CF worker: enabled (%s)", log.info(" CF worker: enabled (%s)",
proxy_config.cfproxy_worker_domain) ", ".join(proxy_config.cfproxy_worker_domains))
log.info("=" * 60) log.info("=" * 60)
log.info(" Connect:") log.info(" Connect:")
if ftls: if ftls:
@@ -574,13 +574,15 @@ def main():
help='Socket send/recv buffer size in KB (default 256)') help='Socket send/recv buffer size in KB (default 256)')
ap.add_argument('--pool-size', type=int, default=4, metavar='N', ap.add_argument('--pool-size', type=int, default=4, metavar='N',
help='WS connection pool size per DC (default 4, min 0)') help='WS connection pool size per DC (default 4, min 0)')
ap.add_argument('--cfproxy-domain', type=str, default='', ap.add_argument('--cfproxy-domain', action='append', default=None,
metavar='DOMAIN', metavar='DOMAIN',
help='User defined Cloudflare-proxied domain for WS fallback') help='User defined Cloudflare-proxied domain for WS fallback '
ap.add_argument('--cfproxy-worker-domain', type=str, default='', '(repeatable for multiple domains)')
ap.add_argument('--cfproxy-worker-domain', action='append', default=None,
metavar='DOMAIN', metavar='DOMAIN',
help='Cloudflare Worker domain for WS fallback ' help='Cloudflare Worker domain for WS fallback '
'(tried before other fallback methods)') '(tried before other fallback methods, '
'repeatable for multiple domains)')
ap.add_argument('--no-cfproxy', action='store_true', ap.add_argument('--no-cfproxy', action='store_true',
help='Disable Cloudflare proxy fallback') help='Disable Cloudflare proxy fallback')
ap.add_argument('--fake-tls-domain', type=str, default='', ap.add_argument('--fake-tls-domain', type=str, default='',
@@ -622,8 +624,8 @@ def main():
proxy_config.buffer_size = max(4, args.buf_kb) * 1024 proxy_config.buffer_size = max(4, args.buf_kb) * 1024
proxy_config.pool_size = max(0, args.pool_size) proxy_config.pool_size = max(0, args.pool_size)
proxy_config.fallback_cfproxy = not args.no_cfproxy proxy_config.fallback_cfproxy = not args.no_cfproxy
proxy_config.cfproxy_user_domain = args.cfproxy_domain.strip() proxy_config.cfproxy_user_domains = coerce_domain_list(args.cfproxy_domain)
proxy_config.cfproxy_worker_domain = args.cfproxy_worker_domain.strip() 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

View File

@@ -6,7 +6,7 @@ import webbrowser
from dataclasses import dataclass 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, coerce_domain_list
from proxy.balancer import balancer from proxy.balancer import balancer
from utils.update_check import RELEASES_PAGE_URL, get_status from utils.update_check import RELEASES_PAGE_URL, get_status
@@ -59,15 +59,17 @@ _TIP_CFPROXY = (
"Использовать Cloudflare прокси для недоступных датацентров" "Использовать Cloudflare прокси для недоступных датацентров"
) )
_TIP_CFPROXY_DOMAIN = ( _TIP_CFPROXY_DOMAIN = (
"Ваш собственный домен, проксируемый через Cloudflare, для WS-подключения.\n" "Ваши собственные домены, проксируемые через Cloudflare, для WS-подключения.\n"
"Если не указан — выбирается автоматически из поддерживаемых доменов" "Несколько доменов указывайте через запятую.\n"
"Если не указаны — выбираются автоматически из поддерживаемых доменов"
) )
_TIP_CFPROXY_USER_DOMAIN_CB = ( _TIP_CFPROXY_USER_DOMAIN_CB = (
"Указать свой домен вместо автоматического выбора" "Указать свои домены вместо автоматического выбора"
) )
_TIP_CFWORKER_DOMAIN = ( _TIP_CFWORKER_DOMAIN = (
"Домен Cloudflare Worker (например, name.account.workers.dev).\n" "Домены Cloudflare Worker (например, name.account.workers.dev).\n"
"Прокси передает через него подключение к Telegram DC по IP" "Несколько доменов указывайте через запятую.\n"
"Прокси передает через них подключение к Telegram DC по IP"
) )
_TIP_SAVE = "Сохранить настройки" _TIP_SAVE = "Сохранить настройки"
_TIP_CANCEL = "Закрыть окно без сохранения изменений" _TIP_CANCEL = "Закрыть окно без сохранения изменений"
@@ -149,6 +151,14 @@ def _run_cfworker_connectivity_test(domain: str) -> dict:
return _run_connectivity_test(cases) return _run_connectivity_test(cases)
def _run_cfproxy_multi_test(domains: list) -> dict:
return {domain: _run_cfproxy_connectivity_test(domain) 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_cfproxy_auto_test(domains: list) -> tuple: def _run_cfproxy_auto_test(domains: list) -> tuple:
merged: dict = {} merged: dict = {}
best_domain = None best_domain = None
@@ -207,6 +217,52 @@ def _show_connectivity_results(title_base: str, results: dict,
_mb.showinfo(title, msg, parent=root) _mb.showinfo(title, msg, parent=root)
root.destroy() root.destroy()
def _show_multi_connectivity_results(title_base: str, per_domain: dict,
label_prefix: str = 'DC') -> None:
import tkinter as _tk
from tkinter import messagebox as _mb
total = len(_CFPROXY_TEST_DCS)
all_ok = True
any_ok = False
blocks = []
for domain, results in per_domain.items():
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) == total:
any_ok = True
blocks.append(f"\u2713 {domain}: все {total} серверов доступны")
elif not ok:
all_ok = False
blocks.append(f"\u2717 {domain}: недоступен")
else:
all_ok = False
any_ok = True
blocks.append(
f"~ {domain}: работают "
f"{', '.join(f'{label_prefix}{dc}' for dc in ok)}; "
f"недоступны "
f"{', '.join(f'{label_prefix}{dc}' for dc, _ in fail)}"
)
if all_ok:
title = f"{title_base}: всё работает"
elif any_ok:
title = f"{title_base}: частично работает"
else:
title = f"{title_base}: недоступен"
msg = "\n\n".join(blocks)
root = _tk.Tk()
root.withdraw()
try:
root.attributes("-topmost", True)
except Exception:
pass
_mb.showinfo(title, msg, parent=root)
root.destroy()
_INNER_W = 396 _INNER_W = 396
_APPEARANCE_OPTIONS = ["Авто", "Светлая", "Тёмная"] _APPEARANCE_OPTIONS = ["Авто", "Светлая", "Тёмная"]
@@ -450,20 +506,23 @@ def install_tray_config_form(
_cf_test_btn = [None] _cf_test_btn = [None]
def _on_cf_test(): def _on_cf_test():
user_domain = cfproxy_user_domain_var.get().strip() if cf_custom_cb_var.get() else "" user_domains = (
coerce_domain_list(cfproxy_user_domain_var.get())
if cf_custom_cb_var.get() else []
)
btn = _cf_test_btn[0] btn = _cf_test_btn[0]
if btn: if btn:
btn.configure(text="...", state="disabled") btn.configure(text="...", state="disabled")
import threading as _threading import threading as _threading
if user_domain: if user_domains:
def _worker(): def _worker():
try: try:
res = _run_cfproxy_connectivity_test(user_domain) per = _run_cfproxy_multi_test(user_domains)
if btn: if btn:
btn.after( btn.after(
0, 0,
lambda: _show_connectivity_results( lambda: _show_multi_connectivity_results(
"CF-прокси", res, domain=user_domain, label_prefix='kws', "CF-прокси", per, label_prefix='kws',
), ),
) )
except Exception as exc: except Exception as exc:
@@ -508,8 +567,10 @@ def install_tray_config_form(
cf_custom_row = ctk.CTkFrame(cf_inner, fg_color="transparent") cf_custom_row = ctk.CTkFrame(cf_inner, fg_color="transparent")
cf_custom_row.pack(fill="x") cf_custom_row.pack(fill="x")
saved_user_domain = cfg.get("cfproxy_user_domain", default_config.get("cfproxy_user_domain", "")) saved_user_domains = coerce_domain_list(
cf_custom_cb_var = ctk.BooleanVar(value=bool(saved_user_domain)) cfg.get("cfproxy_user_domain", default_config.get("cfproxy_user_domain", ""))
)
cf_custom_cb_var = ctk.BooleanVar(value=bool(saved_user_domains))
cf_custom_cb = _checkbox(ctk, cf_custom_row, theme, "Свой домен", cf_custom_cb_var) cf_custom_cb = _checkbox(ctk, cf_custom_row, theme, "Свой домен", cf_custom_cb_var)
cf_custom_cb.pack(side="left", padx=(0, 10)) cf_custom_cb.pack(side="left", padx=(0, 10))
attach_ctk_tooltip(cf_custom_cb, _TIP_CFPROXY_USER_DOMAIN_CB) attach_ctk_tooltip(cf_custom_cb, _TIP_CFPROXY_USER_DOMAIN_CB)
@@ -522,7 +583,7 @@ def install_tray_config_form(
command=lambda: webbrowser.open(_CFPROXY_HELP_URL), command=lambda: webbrowser.open(_CFPROXY_HELP_URL),
).pack(side="right") ).pack(side="right")
cfproxy_user_domain_var = ctk.StringVar(value=saved_user_domain) cfproxy_user_domain_var = ctk.StringVar(value=", ".join(saved_user_domains))
cf_domain_entry = _entry( cf_domain_entry = _entry(
ctk, cf_custom_row, theme, var=cfproxy_user_domain_var, ctk, cf_custom_row, theme, var=cfproxy_user_domain_var,
height=32, radius=8, height=32, radius=8,
@@ -543,14 +604,16 @@ def install_tray_config_form(
cf_worker_row = ctk.CTkFrame(cf_worker_inner, fg_color="transparent") cf_worker_row = ctk.CTkFrame(cf_worker_inner, fg_color="transparent")
cf_worker_row.pack(fill="x", pady=(0, 4)) 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 = _label(ctk, cf_worker_row, theme, "Cloudflare Worker домены (через запятую)", size=11)
cf_worker_lbl.pack(anchor="w", pady=(0, 2)) cf_worker_lbl.pack(anchor="w", pady=(0, 2))
cf_worker_input = ctk.CTkFrame(cf_worker_inner, fg_color="transparent") cf_worker_input = ctk.CTkFrame(cf_worker_inner, fg_color="transparent")
cf_worker_input.pack(fill="x") cf_worker_input.pack(fill="x")
cfproxy_worker_domain_var = ctk.StringVar( cfproxy_worker_domain_var = ctk.StringVar(
value=cfg.get("cfproxy_worker_domain", default_config.get("cfproxy_worker_domain", "")) value=", ".join(coerce_domain_list(
cfg.get("cfproxy_worker_domain", default_config.get("cfproxy_worker_domain", ""))
))
) )
cf_worker_entry = _entry( cf_worker_entry = _entry(
ctk, cf_worker_input, theme, var=cfproxy_worker_domain_var, ctk, cf_worker_input, theme, var=cfproxy_worker_domain_var,
@@ -565,24 +628,24 @@ def install_tray_config_form(
btn = _cfworker_test_btn[0] btn = _cfworker_test_btn[0]
if btn is None: if btn is None:
return return
enabled = bool(cfproxy_worker_domain_var.get().strip()) enabled = bool(coerce_domain_list(cfproxy_worker_domain_var.get()))
btn.configure(state="normal" if enabled else "disabled") btn.configure(state="normal" if enabled else "disabled")
def _on_cfworker_test(): def _on_cfworker_test():
domain = cfproxy_worker_domain_var.get().strip() domains = coerce_domain_list(cfproxy_worker_domain_var.get())
btn = _cfworker_test_btn[0] btn = _cfworker_test_btn[0]
if not domain or btn is None: if not domains or btn is None:
return return
btn.configure(text="...", state="disabled") btn.configure(text="...", state="disabled")
import threading as _threading import threading as _threading
def _worker(): def _worker():
try: try:
res = _run_cfworker_connectivity_test(domain) per = _run_cfworker_multi_test(domains)
btn.after( btn.after(
0, 0,
lambda: _show_connectivity_results( lambda: _show_multi_connectivity_results(
"CF Worker", res, domain=domain, label_prefix='DC', "CF Worker", per, label_prefix='DC',
), ),
) )
except Exception as exc: except Exception as exc:
@@ -784,9 +847,9 @@ def validate_config_form(
if widgets.cfproxy_var is not None: if widgets.cfproxy_var is not None:
new_cfg["cfproxy"] = bool(widgets.cfproxy_var.get()) new_cfg["cfproxy"] = bool(widgets.cfproxy_var.get())
if widgets.cfproxy_user_domain_var is not None: if widgets.cfproxy_user_domain_var is not None:
new_cfg["cfproxy_user_domain"] = widgets.cfproxy_user_domain_var.get().strip() new_cfg["cfproxy_user_domain"] = coerce_domain_list(widgets.cfproxy_user_domain_var.get())
if widgets.cfproxy_worker_domain_var is not None: if widgets.cfproxy_worker_domain_var is not None:
new_cfg["cfproxy_worker_domain"] = widgets.cfproxy_worker_domain_var.get().strip() new_cfg["cfproxy_worker_domain"] = coerce_domain_list(widgets.cfproxy_worker_domain_var.get())
if widgets.appearance_var is not None: if widgets.appearance_var is not None:
new_cfg["appearance"] = _APPEARANCE_TO_CFG.get(widgets.appearance_var.get(), "auto") new_cfg["appearance"] = _APPEARANCE_TO_CFG.get(widgets.appearance_var.get(), "auto")
return new_cfg return new_cfg

View File

@@ -18,8 +18,8 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
"buf_kb": 256, "buf_kb": 256,
"pool_size": 4, "pool_size": 4,
"cfproxy": True, "cfproxy": True,
"cfproxy_user_domain": "", "cfproxy_user_domain": [],
"cfproxy_worker_domain": "", "cfproxy_worker_domain": [],
} }

View File

@@ -14,7 +14,7 @@ from typing import Any, Callable, Dict, Optional, Tuple
import psutil import psutil
from proxy import __version__, get_link_host, parse_dc_ip_list, proxy_config from proxy import __version__, get_link_host, parse_dc_ip_list, proxy_config, coerce_domain_list
from proxy.tg_ws_proxy import _run from proxy.tg_ws_proxy import _run
from utils.default_config import default_tray_config from utils.default_config import default_tray_config
@@ -271,8 +271,8 @@ def apply_proxy_config(cfg: dict) -> bool:
pc.buffer_size = max(4, cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])) * 1024 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.pool_size = max(0, cfg.get("pool_size", DEFAULT_CONFIG["pool_size"]))
pc.fallback_cfproxy = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"]) pc.fallback_cfproxy = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"])
pc.cfproxy_user_domain = 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_domain = 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"]))
return True return True