fix(proxy): RFC-compliant IPv6 reply, __slots__, parallel pool refill, graceful shutdown + cross-platform CLI
This commit is contained in:
parent
cf3e3b2aec
commit
bef6a4fd3e
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.tg-ws-proxy</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>tg-ws-proxy</string>
|
||||
<string>--host</string>
|
||||
<string>127.0.0.1</string>
|
||||
<string>--port</string>
|
||||
<string>1080</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/tg-ws-proxy.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/tg-ws-proxy.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=Telegram WebSocket Bridge Proxy
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=tg-ws-proxy --host 127.0.0.1 --port 1080
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
|
@ -467,7 +467,14 @@ def _ws_domains(dc: int, is_media) -> List[str]:
|
|||
return [f'kws{dc}.web.telegram.org', f'kws{dc}-1.web.telegram.org']
|
||||
|
||||
|
||||
# __slots__ added to Stats ---
|
||||
class Stats:
|
||||
__slots__ = (
|
||||
'connections_total', 'connections_ws', 'connections_tcp_fallback',
|
||||
'connections_http_rejected', 'connections_passthrough', 'ws_errors',
|
||||
'bytes_up', 'bytes_down', 'pool_hits', 'pool_misses',
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.connections_total = 0
|
||||
self.connections_ws = 0
|
||||
|
|
@ -494,7 +501,11 @@ class Stats:
|
|||
_stats = Stats()
|
||||
|
||||
|
||||
# __slots__ added to _WsPool;
|
||||
|
||||
class _WsPool:
|
||||
__slots__ = ('_idle', '_refilling')
|
||||
|
||||
def __init__(self):
|
||||
self._idle: Dict[Tuple[int, bool], list] = {}
|
||||
self._refilling: Set[Tuple[int, bool]] = set()
|
||||
|
|
@ -535,17 +546,14 @@ class _WsPool:
|
|||
needed = _WS_POOL_SIZE - len(bucket)
|
||||
if needed <= 0:
|
||||
return
|
||||
tasks = []
|
||||
for _ in range(needed):
|
||||
tasks.append(asyncio.create_task(
|
||||
self._connect_one(target_ip, domains)))
|
||||
for t in tasks:
|
||||
try:
|
||||
ws = await t
|
||||
if ws:
|
||||
bucket.append((ws, time.monotonic()))
|
||||
except Exception:
|
||||
pass
|
||||
# using gather for true parallel connections ---
|
||||
results = await asyncio.gather(
|
||||
*[self._connect_one(target_ip, domains) for _ in range(needed)],
|
||||
return_exceptions=True,
|
||||
)
|
||||
for result in results:
|
||||
if isinstance(result, RawWebSocket):
|
||||
bucket.append((result, time.monotonic()))
|
||||
log.debug("WS pool refilled DC%d%s: %d ready",
|
||||
dc, 'm' if is_media else '', len(bucket))
|
||||
finally:
|
||||
|
|
@ -584,6 +592,21 @@ class _WsPool:
|
|||
self._schedule_refill(key, target_ip, domains)
|
||||
log.info("WS pool warmup started for %d DC(s)", len(dc_opt))
|
||||
|
||||
async def close(self):
|
||||
"""Close all idle WebSocket connections in the pool."""
|
||||
all_ws = [
|
||||
ws
|
||||
for bucket in self._idle.values()
|
||||
for ws, _ in bucket
|
||||
]
|
||||
self._idle.clear()
|
||||
if all_ws:
|
||||
log.debug("WS pool closing %d idle connection(s)", len(all_ws))
|
||||
await asyncio.gather(
|
||||
*[self._quiet_close(ws) for ws in all_ws],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
|
||||
_ws_pool = _WsPool()
|
||||
|
||||
|
|
@ -815,7 +838,7 @@ async def _handle_client(reader, writer):
|
|||
"IPv6 addresses are not supported; "
|
||||
"disable IPv6 to continue using the proxy.",
|
||||
label, dst, port)
|
||||
writer.write(_socks5_reply(0x05))
|
||||
writer.write(_socks5_reply(0x08))
|
||||
await writer.drain()
|
||||
writer.close()
|
||||
return
|
||||
|
|
@ -1069,6 +1092,7 @@ async def _run(port: int, dc_opt: Dict[int, Optional[str]],
|
|||
async def wait_stop():
|
||||
await stop_event.wait()
|
||||
server.close()
|
||||
await _ws_pool.close()
|
||||
me = asyncio.current_task()
|
||||
for task in list(asyncio.all_tasks()):
|
||||
if task is not me:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.backends.legacy:build"
|
||||
|
||||
[project]
|
||||
name = "tg-ws-proxy"
|
||||
version = "1.1.0"
|
||||
description = "Local SOCKS5 proxy for bypassing partial Telegram blocking"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"cryptography>=41.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
windows = [
|
||||
"cryptography>=41.0",
|
||||
"customtkinter>=5.2.2",
|
||||
"Pillow>=10.4.0",
|
||||
"psutil>=5.9.8",
|
||||
"pystray>=0.19.5",
|
||||
"pyperclip>=1.9.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
tg-ws-proxy = "proxy.tg_ws_proxy:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["proxy*"]
|
||||
|
|
@ -0,0 +1 @@
|
|||
cryptography>=41.0.7
|
||||
Loading…
Reference in New Issue