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']
|
return [f'kws{dc}.web.telegram.org', f'kws{dc}-1.web.telegram.org']
|
||||||
|
|
||||||
|
|
||||||
|
# __slots__ added to Stats ---
|
||||||
class 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):
|
def __init__(self):
|
||||||
self.connections_total = 0
|
self.connections_total = 0
|
||||||
self.connections_ws = 0
|
self.connections_ws = 0
|
||||||
|
|
@ -494,7 +501,11 @@ class Stats:
|
||||||
_stats = Stats()
|
_stats = Stats()
|
||||||
|
|
||||||
|
|
||||||
|
# __slots__ added to _WsPool;
|
||||||
|
|
||||||
class _WsPool:
|
class _WsPool:
|
||||||
|
__slots__ = ('_idle', '_refilling')
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._idle: Dict[Tuple[int, bool], list] = {}
|
self._idle: Dict[Tuple[int, bool], list] = {}
|
||||||
self._refilling: Set[Tuple[int, bool]] = set()
|
self._refilling: Set[Tuple[int, bool]] = set()
|
||||||
|
|
@ -535,17 +546,14 @@ class _WsPool:
|
||||||
needed = _WS_POOL_SIZE - len(bucket)
|
needed = _WS_POOL_SIZE - len(bucket)
|
||||||
if needed <= 0:
|
if needed <= 0:
|
||||||
return
|
return
|
||||||
tasks = []
|
# using gather for true parallel connections ---
|
||||||
for _ in range(needed):
|
results = await asyncio.gather(
|
||||||
tasks.append(asyncio.create_task(
|
*[self._connect_one(target_ip, domains) for _ in range(needed)],
|
||||||
self._connect_one(target_ip, domains)))
|
return_exceptions=True,
|
||||||
for t in tasks:
|
)
|
||||||
try:
|
for result in results:
|
||||||
ws = await t
|
if isinstance(result, RawWebSocket):
|
||||||
if ws:
|
bucket.append((result, time.monotonic()))
|
||||||
bucket.append((ws, time.monotonic()))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
log.debug("WS pool refilled DC%d%s: %d ready",
|
log.debug("WS pool refilled DC%d%s: %d ready",
|
||||||
dc, 'm' if is_media else '', len(bucket))
|
dc, 'm' if is_media else '', len(bucket))
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -584,6 +592,21 @@ class _WsPool:
|
||||||
self._schedule_refill(key, target_ip, domains)
|
self._schedule_refill(key, target_ip, domains)
|
||||||
log.info("WS pool warmup started for %d DC(s)", len(dc_opt))
|
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()
|
_ws_pool = _WsPool()
|
||||||
|
|
||||||
|
|
@ -815,7 +838,7 @@ async def _handle_client(reader, writer):
|
||||||
"IPv6 addresses are not supported; "
|
"IPv6 addresses are not supported; "
|
||||||
"disable IPv6 to continue using the proxy.",
|
"disable IPv6 to continue using the proxy.",
|
||||||
label, dst, port)
|
label, dst, port)
|
||||||
writer.write(_socks5_reply(0x05))
|
writer.write(_socks5_reply(0x08))
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
writer.close()
|
writer.close()
|
||||||
return
|
return
|
||||||
|
|
@ -1069,6 +1092,7 @@ async def _run(port: int, dc_opt: Dict[int, Optional[str]],
|
||||||
async def wait_stop():
|
async def wait_stop():
|
||||||
await stop_event.wait()
|
await stop_event.wait()
|
||||||
server.close()
|
server.close()
|
||||||
|
await _ws_pool.close()
|
||||||
me = asyncio.current_task()
|
me = asyncio.current_task()
|
||||||
for task in list(asyncio.all_tasks()):
|
for task in list(asyncio.all_tasks()):
|
||||||
if task is not me:
|
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