12 Commits

Author SHA1 Message Date
Flowseal
6c94d3a39d tip block 2026-04-07 17:51:59 +03:00
Flowseal
746cd66b35 build fixes 2026-04-07 17:15:30 +03:00
Flowseal
e5d8ff7769 version changing & readme update 2026-04-07 17:12:29 +03:00
Flowseal
3ee82e5114 typos 2026-04-07 17:07:41 +03:00
Qirashi
db1308e3f5 Tray dark theme (#591) 2026-04-07 17:06:21 +03:00
Flowseal
6231499c39 lock fixes 2026-04-07 17:04:01 +03:00
Flowseal
826554abfb CfProxy UI setup 2026-04-07 17:04:01 +03:00
Flowseal
7f44c524c8 lists clear on restart 2026-04-07 17:04:01 +03:00
Flowseal
6310fcd6eb docs 2026-04-07 17:03:01 +03:00
Flowseal
081b150b3d Removed dc overriding 2026-04-07 17:02:13 +03:00
Flowseal
15001980dc cloudflare proxy; closes #576 2026-04-07 17:02:13 +03:00
gogamlg3
da4b521aba Изменение README для AUR (#485) 2026-03-30 09:55:44 +03:00
12 changed files with 458 additions and 59 deletions

View File

@@ -35,7 +35,7 @@ RUN apt-get update \
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY proxy ./proxy
COPY README.md LICENSE ./
COPY docs/README.md LICENSE ./
USER app

27
docs/CfProxy.md Normal file
View File

@@ -0,0 +1,27 @@
# Cloudflare Proxy
Для недоступных датацентров можно использовать альтернативный бесплатный метод подключения - проксирование через Cloudflare. **Для работы нужен только домен**. В приложении есть домен по умолчанию, но его можно (и лучше) заменить на свой.
## Зачем мне настраивать свой домен?
Cloudflare имеет лимиты на одновременное количество подключений WS. Домен по умолчанию может перестать работать в любой момент.
## Настройка своего домена
1. Добавьте свой домен в Cloudflare (либо купив у них напрямую, либо поменяв NS сервера: https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/)
2. В `SSL/TLS` -> `Overview` выставьте режим **Flexible**
3. В `DNS` -> `Records` добавьте следующие `A` записи через `+ Add Record`:
- Name=`kws1` IPv4=`149.154.175.50`
- Name=`kws2` IPv4=`149.154.167.51`
- Name=`kws3` IPv4=`149.154.175.100`
- Name=`kws4` IPv4=`149.154.167.91`
- Name=`kws5` IPv4=`149.154.171.5`
- Name=`kws203` IPv4=`149.154.175.50`
4. **Добавьте домен в [zapret](https://github.com/Flowseal/zapret-discord-youtube/) или другой софт для обхода блокировок, так как подсеть Cloudflare забанена (по крайней мере, если вы из России)**
5. В настройках TgWsProxy поменяйте домен на свой
## Mentions
Idea - https://github.com/Nekogram/WSProxy
Thanks to [@UjuiUjuMandan](https://github.com/UjuiUjuMandan) for the information

View File

@@ -10,6 +10,14 @@
>
> **Всегда проверяйте, что скачиваете из интернета, тем более из непроверенных источников. Всегда лучше смотреть на детекты широко известных антивирусов на VirusTotal**
> [!TIP]
>
> ### 🎉 Поддержать меня
>
> USDT (TRC20): `TXPnKs2Ww1RD8JN6nChFUVmi5r2hqrWjuu`
> BTC: `bc1qr8vd6jelkyyry3m4mq6z5txdx4pl856fu6ss0w`
> ETH: `0x1417878fdc5047E670a77748B34819b9A49C72F1`
# TG WS Proxy
**Локальный MTProto-прокси** для Telegram Desktop, который **ускоряет работу Telegram**, перенаправляя трафик через WebSocket-соединения. Данные передаются в том же зашифрованном виде, а для работы не нужны сторонние сервера.
@@ -26,7 +34,7 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
2. Перехватывает подключения к IP-адресам Telegram
3. Извлекает DC ID из MTProto obfuscation init-пакета
4. Устанавливает WebSocket (TLS) соединение к соответствующему DC через домены Telegram
5. Если WS недоступен (302 redirect) — автоматически переключается на прямое TCP-соединение
5. Если WS недоступен (302 redirect) — автоматически переключается на CfProxy / прямое TCP-соединение
## 🚀 Быстрый старт
@@ -69,8 +77,9 @@ makepkg -si
# При помощи AUR-helper
paru -S tg-ws-proxy-bin
# Если вы установили -cli пакет, то запуск осуществляется через systemctl, где 8888 это номер порта прокси:
sudo systemctl start tg-ws-proxy-cli@8888
# Если вы установили -cli пакет, то запуск осуществляется через systemctl, где 8888 это номер порта,
# разделитель ":" и secret, который можно сгенерировать командой: openssl rand -hex 16
sudo systemctl start tg-ws-proxy-cli@8888:3075abe65830f0325116bb0416cadf9f
```
Для остальных дистрибутивов можно использовать **`TgWsProxy_linux_amd64`** (бинарный файл для x86_64).
@@ -128,11 +137,14 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
| `--host` | `127.0.0.1` | Хост прокси |
| `--secret` | `random` | 32 hex chars secret для авторизации клиентов |
| `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (можно указать несколько раз) |
| `--buf-kb` | `256` | Размер буфера в КБ
| `--pool-size` | `4` | Количество заготовленных соединений на каждый DC
| `--log-file` | выкл. | Путь до файла, в который сохранять логи
| `--log-max-mb` | `5` | Максимальный размер файла логов в МБ (после идёт перезапись)
| `--log-backups` | `0` | Количество сохранений логов после перезаписи
| `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare]((https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md)) |
| `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudfalre. [Подробнее тут](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md) |
| `--cfproxy-priority` | `true` | Пробовать проксировать через Cloudflare перед прямым TCP подключением |
| `--buf-kb` | `256` | Размер буфера в КБ |
| `--pool-size` | `4` | Количество заготовленных соединений на каждый DC |
| `--log-file` | выкл. | Путь до файла, в который сохранять логи |
| `--log-max-mb` | `5` | Максимальный размер файла логов в МБ (после идёт перезапись) |
| `--log-backups` | `0` | Количество сохранений логов после перезаписи |
| `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) |
**Примеры:**

View File

@@ -392,6 +392,27 @@ def _edit_config_dialog() -> None:
except ValueError:
pass
cfproxy = _ask_yes_no_close("Включить Cloudflare Proxy (CfProxy)?")
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.\n"
"pclead.co.uk готовый настроенный домен. Подробнее про настройку читайте в репозитории - docs/CfProxy.md",
cfg.get("cfproxy_domain", DEFAULT_CONFIG.get("cfproxy_domain", "pclead.co.uk")),
)
if cfproxy_domain is None:
return
cfproxy_domain = cfproxy_domain.strip()
new_cfg = {
"host": host,
"port": port,
@@ -402,6 +423,9 @@ def _edit_config_dialog() -> None:
"pool_size": adv.get("pool_size", cfg.get("pool_size", DEFAULT_CONFIG["pool_size"])),
"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_domain": cfproxy_domain or DEFAULT_CONFIG.get("cfproxy_domain", "pclead.co.uk"),
}
save_config(new_cfg)
log.info("Config saved: %s", new_cfg)

View File

@@ -1 +1 @@
__version__ = "1.4.0"
__version__ = "1.5.0"

View File

@@ -26,9 +26,11 @@ class ProxyConfig:
host: str = '127.0.0.1'
secret: str = field(default_factory=lambda: os.urandom(16).hex())
dc_redirects: Dict[int, str] = field(default_factory=lambda: {2: '149.154.167.220', 4: '149.154.167.220'})
dc_overrides: Dict[int, int] = field(default_factory=lambda: {203: 2})
buffer_size: int = 256 * 1024
pool_size: int = 4
fallback_cfproxy: bool = True
fallback_cfproxy_priority: bool = True
fallback_cfproxy_domain: str = 'pclead.co.uk'
proxy_config = ProxyConfig()
@@ -40,7 +42,7 @@ DC_DEFAULT_IPS: Dict[int, str] = {
3: '149.154.175.100',
4: '149.154.167.91',
5: '149.154.171.5',
203: '91.105.192.100'
203: '149.154.175.50'
}
HANDSHAKE_LEN = 64
@@ -475,7 +477,6 @@ class _MsgSplitter:
def _ws_domains(dc: int, is_media) -> List[str]:
dc = proxy_config.dc_overrides.get(dc, dc)
if is_media is None or is_media:
return [f'kws{dc}-1.web.telegram.org', f'kws{dc}.web.telegram.org']
return [f'kws{dc}.web.telegram.org', f'kws{dc}-1.web.telegram.org']
@@ -487,6 +488,7 @@ class Stats:
self.connections_active = 0
self.connections_ws = 0
self.connections_tcp_fallback = 0
self.connections_cfproxy = 0
self.connections_bad = 0
self.ws_errors = 0
self.bytes_up = 0
@@ -502,6 +504,7 @@ class Stats:
f"active={self.connections_active} "
f"ws={self.connections_ws} "
f"tcp_fb={self.connections_tcp_fallback} "
f"cf={self.connections_cfproxy} "
f"bad={self.connections_bad} "
f"err={self.ws_errors} "
f"pool={pool_s} "
@@ -784,6 +787,88 @@ def _fallback_ip(dc: int) -> Optional[str]:
return DC_DEFAULT_IPS.get(dc)
def _cfproxy_domains(dc: int) -> List[str]:
base = proxy_config.fallback_cfproxy_domain
return [f'kws{dc}.{base}']
async def _cfproxy_fallback(reader, writer, relay_init, label,
dc=None, is_media=False,
clt_decryptor=None, clt_encryptor=None,
tg_encryptor=None, tg_decryptor=None,
splitter=None):
domains = _cfproxy_domains(dc)
media_tag = ' media' if is_media else ''
ws = None
for domain in domains:
log.info("[%s] DC%d%s -> CF proxy wss://%s/apiws",
label, dc, media_tag, domain)
try:
ws = await RawWebSocket.connect(domain, domain,
timeout=10.0)
break
except Exception as exc:
log.warning("[%s] DC%d%s CF proxy %s failed: %s",
label, dc, media_tag, domain, exc)
if ws is None:
return False
_stats.connections_cfproxy += 1
await ws.send(relay_init)
await _bridge_ws_reencrypt(reader, writer, ws, label,
dc=dc, is_media=is_media,
clt_decryptor=clt_decryptor,
clt_encryptor=clt_encryptor,
tg_encryptor=tg_encryptor,
tg_decryptor=tg_decryptor,
splitter=splitter)
return True
async def _do_fallback(reader, writer, relay_init, label,
dc, is_media, media_tag,
clt_decryptor, clt_encryptor,
tg_encryptor, tg_decryptor,
splitter=None):
"""Try CF proxy and/or TCP fallback based on config priority."""
fallback_dst = _fallback_ip(dc)
use_cf = proxy_config.fallback_cfproxy
cf_first = proxy_config.fallback_cfproxy_priority
methods: List[str] = []
if use_cf and cf_first:
methods = ['cf', 'tcp']
elif use_cf:
methods = ['tcp', 'cf']
else:
methods = ['tcp']
for method in methods:
if method == 'cf':
ok = await _cfproxy_fallback(
reader, writer, relay_init, label,
dc=dc, is_media=is_media,
clt_decryptor=clt_decryptor,
clt_encryptor=clt_encryptor,
tg_encryptor=tg_encryptor,
tg_decryptor=tg_decryptor,
splitter=splitter)
if ok:
return True
elif method == 'tcp' and fallback_dst:
log.info("[%s] DC%d%s -> TCP fallback to %s:443",
label, dc, media_tag, fallback_dst)
ok = await _tcp_fallback(
reader, writer, fallback_dst, 443,
relay_init, label, dc=dc, is_media=is_media,
clt_decryptor=clt_decryptor,
clt_encryptor=clt_encryptor,
tg_encryptor=tg_encryptor,
tg_decryptor=tg_decryptor)
if ok:
return True
return False
async def _handle_client(reader, writer, secret: bytes):
_stats.connections_total += 1
_stats.connections_active += 1
@@ -867,27 +952,29 @@ async def _handle_client(reader, writer, secret: bytes):
tg_encryptor.update(ZERO_64)
dc_key = (dc, is_media)
dc_key = f'{dc}{"m" if is_media else ""}'
media_tag = " media" if is_media else ""
# 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:
fallback_dst = _fallback_ip(dc)
if fallback_dst:
if dc not in proxy_config.dc_redirects:
log.info("[%s] DC%d not in config -> TCP fallback %s:443",
label, dc, fallback_dst)
else:
log.info("[%s] DC%d%s WS blacklisted -> TCP fallback %s:443",
label, dc, media_tag, fallback_dst)
await _tcp_fallback(reader, writer, fallback_dst, 443,
relay_init, label, dc=dc,
is_media=is_media,
clt_decryptor=clt_decryptor,
clt_encryptor=clt_encryptor,
tg_encryptor=tg_encryptor,
tg_decryptor=tg_decryptor)
if dc not in proxy_config.dc_redirects:
log.info("[%s] DC%d not in config -> fallback",
label, dc)
else:
log.info("[%s] DC%d%s WS blacklisted -> fallback",
label, dc, media_tag)
splitter = None
try:
splitter = _MsgSplitter(relay_init, proto_int)
except Exception:
pass
ok = await _do_fallback(
reader, writer, relay_init, label,
dc, is_media, media_tag,
clt_decryptor, clt_encryptor,
tg_encryptor, tg_decryptor,
splitter=splitter)
if not ok:
log.warning("[%s] DC%d%s no fallback available",
label, dc, media_tag)
return
@@ -948,18 +1035,19 @@ async def _handle_client(reader, writer, secret: bytes):
log.info("[%s] DC%d%s WS cooldown for %ds",
label, dc, media_tag, int(DC_FAIL_COOLDOWN))
fallback_dst = _fallback_ip(dc) or target
log.info("[%s] DC%d%s -> TCP fallback to %s:443",
label, dc, media_tag, fallback_dst)
ok = await _tcp_fallback(reader, writer, fallback_dst, 443,
relay_init, label, dc=dc,
is_media=is_media,
clt_decryptor=clt_decryptor,
clt_encryptor=clt_encryptor,
tg_encryptor=tg_encryptor,
tg_decryptor=tg_decryptor)
splitter_fb = None
try:
splitter_fb = _MsgSplitter(relay_init, proto_int)
except Exception:
pass
ok = await _do_fallback(
reader, writer, relay_init, label,
dc, is_media, media_tag,
clt_decryptor, clt_encryptor,
tg_encryptor, tg_decryptor,
splitter=splitter_fb)
if ok:
log.info("[%s] DC%d%s TCP fallback closed",
log.info("[%s] DC%d%s fallback closed",
label, dc, media_tag)
return
@@ -1015,6 +1103,9 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
global _server_instance, _server_stop_event
_server_stop_event = stop_event
ws_blacklist.clear()
dc_fail_until.clear()
secret_bytes = bytes.fromhex(proxy_config.secret)
def client_cb(r, w):
@@ -1040,6 +1131,10 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
for dc in sorted(proxy_config.dc_redirects.keys()):
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'
log.info(" CF proxy: %s (%s)",
proxy_config.fallback_cfproxy_domain, prio)
log.info("=" * 60)
log.info(" Connect link:")
log.info(" %s", tg_link)
@@ -1119,7 +1214,7 @@ def main():
ap = argparse.ArgumentParser(
description='Telegram MTProto WebSocket Bridge Proxy')
ap.add_argument('--port', type=int, default=1443,
help=f'Listen port (default 1443)')
help='Listen port (default 1443)')
ap.add_argument('--host', type=str, default='127.0.0.1',
help='Listen host (default 127.0.0.1)')
ap.add_argument('--secret', type=str, default=None,
@@ -1139,6 +1234,14 @@ def main():
help='Socket send/recv buffer size in KB (default 256)')
ap.add_argument('--pool-size', type=int, default=4, metavar='N',
help='WS connection pool size per DC (default 4, min 0)')
ap.add_argument('--cfproxy-domain', type=str, default='pclead.co.uk',
metavar='DOMAIN',
help='Cloudflare-proxied domain for WS fallback '
'(default: pclead.co.uk)')
ap.add_argument('--no-cfproxy', action='store_true',
help='Disable Cloudflare proxy fallback')
ap.add_argument('--cfproxy-priority', type=bool, default=True,
help='Try cfproxy before tcp fallback (default: true)')
args = ap.parse_args()
if not args.dc_ip:
@@ -1171,7 +1274,10 @@ def main():
secret=secret_hex,
dc_redirects=dc_redirects,
buffer_size=max(4, args.buf_kb) * 1024,
pool_size=max(0, args.pool_size)
pool_size=max(0, args.pool_size),
fallback_cfproxy=not args.no_cfproxy,
fallback_cfproxy_priority=args.cfproxy_priority,
fallback_cfproxy_domain=args.cfproxy_domain,
)
log_level = logging.DEBUG if args.verbose else logging.INFO

View File

@@ -7,7 +7,7 @@ name = "tg-ws-proxy"
dynamic=["version"]
description = "Telegram Desktop WebSocket Bridge Proxy"
readme = "README.md"
readme = "docs/README.md"
requires-python = ">=3.8"
license = { name = "MIT", file = "LICENSE" }

View File

@@ -27,8 +27,9 @@ _TIP_PORT = (
_TIP_SECRET = "Секретный ключ для авторизации клиентов"
_TIP_DC = (
"Соответствие номера датацентра Telegram (DC) и IP-адреса сервера.\n"
"Каждая строка: «номер:IP», например 2:149.154.167.220. "
"Прокси по этим правилам направляет трафик к нужным серверам Telegram"
"Каждая строка: «номер:IP», например 4:149.154.167.220. "
"Прокси по этим правилам направляет трафик к нужным серверам Telegram\n\n"
"Если у вас не работают медиа и работает CF-прокси, то попробуйте убрать строку 2:149.154.167.220"
)
_TIP_VERBOSE = (
"Если включено, в файл логов пишется больше подробностей — "
@@ -50,9 +51,100 @@ _TIP_AUTOSTART = (
"Если вы переместите программу в другую папку, автозапуск сбросится"
)
_TIP_CHECK_UPDATES = "При запуске проверять наличие обновлений"
_TIP_CFPROXY = (
"Использовать Cloudflare прокси для недоступных датацентров"
)
_TIP_CFPROXY_PRIORITY = (
"Пробовать CF-прокси раньше прямого TCP-подключения"
)
_TIP_CFPROXY_DOMAIN = (
"Домен, проксируемый через Cloudflare, для WS-подключения"
)
_TIP_SAVE = "Сохранить настройки"
_TIP_CANCEL = "Закрыть окно без сохранения изменений"
_CFPROXY_HELP_URL = "https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md"
_CFPROXY_TEST_DCS = [1, 2, 3, 4, 5, 203]
def _run_cfproxy_connectivity_test(domain: str) -> dict:
import base64
import ssl
import socket as _socket
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
results = {}
for dc in _CFPROXY_TEST_DCS:
host = f"kws{dc}.{domain}"
try:
with _socket.create_connection((host, 443), timeout=5) as raw:
with ctx.wrap_socket(raw, server_hostname=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"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {ws_key}\r\n"
f"Sec-WebSocket-Version: 13\r\n"
f"Sec-WebSocket-Protocol: binary\r\n"
f"\r\n"
).encode()
ssock.sendall(req)
ssock.settimeout(5)
buf = b""
while b"\r\n\r\n" not in buf:
chunk = ssock.recv(512)
if not chunk:
break
buf += chunk
first = buf.decode("utf-8", errors="replace").split("\r\n")[0]
if "101" in first:
results[dc] = True
else:
results[dc] = first or "нет ответа"
ssock.close()
raw.close()
except _socket.timeout:
results[dc] = "таймаут"
except OSError as exc:
msg = str(exc)
results[dc] = msg[:60] if len(msg) > 60 else msg
return results
def _cfproxy_show_test_results(domain: str, results: dict) -> 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)
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()
_INNER_W = 396
@@ -155,6 +247,9 @@ class TrayConfigFormWidgets:
adv_keys: Tuple[str, ...]
autostart_var: Optional[Any]
check_updates_var: Optional[Any]
cfproxy_var: Optional[Any] = None
cfproxy_priority_var: Optional[Any] = None
cfproxy_domain_var: Optional[Any] = None
def install_tray_config_form(
@@ -233,6 +328,76 @@ def install_tray_config_form(
dc_textbox.insert("1.0", "\n".join(cfg.get("dc_ip", default_config["dc_ip"])))
attach_tooltip_to_widgets([dc_lbl, dc_textbox], _TIP_DC)
cf_inner = _config_section(ctk, frame, theme, "Cloudflare Proxy")
cf_row = ctk.CTkFrame(cf_inner, fg_color="transparent")
cf_row.pack(fill="x", pady=(0, 6))
cfproxy_var = ctk.BooleanVar(
value=cfg.get("cfproxy", default_config.get("cfproxy", True))
)
cf_cb = _checkbox(ctk, cf_row, theme, "Включить CF-прокси", cfproxy_var)
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, "Приоритет CF-прокси", cfproxy_priority_var)
cf_prio_cb.pack(side="left")
attach_ctk_tooltip(cf_prio_cb, _TIP_CFPROXY_PRIORITY)
cf_domain_row = ctk.CTkFrame(cf_inner, fg_color="transparent")
cf_domain_row.pack(fill="x")
cf_domain_col, cfproxy_domain_var = _labeled_entry(
ctk, cf_domain_row, theme, "Домен",
cfg.get("cfproxy_domain", default_config.get("cfproxy_domain", "pclead.co.uk")),
tip=_TIP_CFPROXY_DOMAIN, width=160, pack_fill=True,
)
cf_domain_col.pack(side="left", fill="x", expand=True, padx=(0, 10))
_cf_test_btn = [None]
def _on_cf_test():
domain = cfproxy_domain_var.get().strip()
if not domain:
return
btn = _cf_test_btn[0]
if btn:
btn.configure(text="...", state="disabled")
import threading as _threading
def _worker():
res = _run_cfproxy_connectivity_test(domain)
if btn:
btn.after(0, lambda: btn.configure(text="Тест", state="normal"))
btn.after(0, lambda: _cfproxy_show_test_results(domain, res))
_threading.Thread(target=_worker, daemon=True).start()
cf_test_col = ctk.CTkFrame(cf_domain_row, fg_color="transparent")
cf_test_col.pack(side="left", anchor="s", padx=(0, 6))
ctk.CTkLabel(cf_test_col, text="", font=(theme.ui_font_family, 12)).pack(pady=(0, 2))
_cf_test_widget = ctk.CTkButton(
cf_test_col, text="Тест", width=56, height=36,
font=(theme.ui_font_family, 13), corner_radius=10,
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
text_color="#ffffff", border_width=1, border_color=theme.field_border,
command=_on_cf_test,
)
_cf_test_widget.pack()
_cf_test_btn[0] = _cf_test_widget
cf_help_col = ctk.CTkFrame(cf_domain_row, fg_color="transparent")
cf_help_col.pack(side="left", anchor="s")
ctk.CTkLabel(cf_help_col, text="", font=(theme.ui_font_family, 12)).pack(pady=(0, 2))
ctk.CTkButton(
cf_help_col, text="?", width=36, height=36,
font=(theme.ui_font_family, 18), corner_radius=10,
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(_CFPROXY_HELP_URL),
).pack()
log_inner = _config_section(ctk, frame, theme, "Логи и производительность")
verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False))
@@ -321,6 +486,9 @@ def install_tray_config_form(
dc_textbox=dc_textbox, verbose_var=verbose_var,
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_domain_var=cfproxy_domain_var,
)
@@ -363,9 +531,9 @@ def validate_config_form(
return "Порт должен быть числом 1-65535"
lines = [
l.strip()
for l in widgets.dc_textbox.get("1.0", "end").strip().splitlines()
if l.strip()
line.strip()
for line in widgets.dc_textbox.get("1.0", "end").strip().splitlines()
if line.strip()
]
try:
tg_ws_proxy.parse_dc_ip_list(lines)
@@ -397,6 +565,14 @@ def validate_config_form(
merge_adv_from_form(widgets, new_cfg, default_config)
if widgets.check_updates_var is not None:
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_domain_var is not None:
domain = widgets.cfproxy_domain_var.get().strip()
if domain:
new_cfg["cfproxy_domain"] = domain
return new_cfg

View File

@@ -17,6 +17,9 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
"log_max_mb": 5,
"buf_kb": 256,
"pool_size": 4,
"cfproxy": True,
"cfproxy_priority": True,
"cfproxy_domain": "pclead.co.uk",
}

View File

@@ -60,12 +60,6 @@ def _same_process(meta: dict, proc: psutil.Process, script_hint: str) -> bool:
return False
if IS_FROZEN:
return APP_NAME.lower() in proc.name().lower()
try:
for arg in proc.cmdline():
if script_hint in arg:
return True
except Exception:
pass
return False
@@ -76,7 +70,10 @@ def acquire_lock(script_hint: str = "") -> bool:
try:
pid = int(f.stem)
except Exception:
f.unlink(missing_ok=True)
try:
f.unlink(missing_ok=True)
except OSError:
pass
continue
meta: dict = {}
try:
@@ -85,12 +82,17 @@ def acquire_lock(script_hint: str = "") -> bool:
meta = json.loads(raw)
except Exception:
pass
is_running = False
try:
if _same_process(meta, psutil.Process(pid), script_hint):
return False
is_running = _same_process(meta, psutil.Process(pid), script_hint)
except Exception:
pass
f.unlink(missing_ok=True)
if is_running:
return False
try:
f.unlink(missing_ok=True)
except OSError:
pass
lock_file = APP_DIR / f"{os.getpid()}.lock"
try:
@@ -100,7 +102,10 @@ def acquire_lock(script_hint: str = "") -> bool:
encoding="utf-8",
)
except Exception:
lock_file.touch()
try:
lock_file.touch()
except Exception:
pass
_lock_file_path = lock_file
return True
@@ -264,6 +269,9 @@ def apply_proxy_config(cfg: dict) -> bool:
pc.dc_redirects = dc_redirects
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.fallback_cfproxy_domain = cfg.get("cfproxy_domain", DEFAULT_CONFIG["cfproxy_domain"])
return True

35
utils/win32_theme.py Normal file
View File

@@ -0,0 +1,35 @@
from __future__ import annotations
import sys
def is_windows_dark_theme() -> bool:
if sys.platform != "win32":
return False
try:
import winreg
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")
value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
return value == 0
except Exception:
return False
def apply_windows_dark_theme() -> None:
try:
import ctypes
uxtheme = ctypes.windll.uxtheme
try:
set_preferred = uxtheme[135]
result = set_preferred(2)
if result == 0:
flush = uxtheme[136]
flush()
except Exception:
try:
allow_dark = uxtheme[135]
allow_dark(True)
except Exception:
pass
except Exception:
pass

View File

@@ -32,6 +32,10 @@ except ImportError:
import proxy.tg_ws_proxy as tg_ws_proxy
from utils.win32_theme import (
is_windows_dark_theme,
apply_windows_dark_theme,
)
from utils.tray_common import (
APP_NAME, DEFAULT_CONFIG, FIRST_RUN_MARKER, IS_FROZEN, LOG_FILE,
acquire_lock, bootstrap, check_ipv6_warning, ctk_run_dialog,
@@ -316,6 +320,10 @@ def run_tray() -> None:
global _tray_icon, _config
_config = load_config()
if is_windows_dark_theme:
apply_windows_dark_theme()
bootstrap(_config)
if pystray is None or Image is None or ctk is None: