[PR #197] [CLOSED] fixed: RFC-compliant IPv6 reply, __slots__ on hot classes, parallel pool refill, graceful pool shutdown + cross-platform CLI #11

Closed
opened 2026-07-19 11:33:44 +03:00 by mirror-bot · 0 comments

📋 Pull Request Information

Original PR: https://github.com/Flowseal/tg-ws-proxy/pull/197
Author: @lifeofcapo
Created: 3/16/2026
Status: ❌ Closed

Base: main ← Head: fix/socks5-ipv6-slots-gather-graceful-shutdown


📝 Commits (1)

  • bef6a4f fix(proxy): RFC-compliant IPv6 reply, slots, parallel pool refill, graceful shutdown + cross-platform CLI

📊 Changes

5 files changed (+107 additions, -14 deletions)

View changed files

➕ packaging/com.tg-ws-proxy.plist (+25 -0)
➕ packaging/tg-ws-proxy.service (+12 -0)
📝 proxy/tg_ws_proxy.py (+38 -14)
➕ pyproject.toml (+31 -0)
➕ requirements-core.txt (+1 -0)

📄 Description

Summary

Four focused improvements to proxy/tg_ws_proxy.py plus cross-platform
packaging — no behaviour changes for existing Windows users.


Changes

1. Fix IPv6 SOCKS5 reply code (0x05 → 0x08)

File: proxy/tg_ws_proxy.py

Previously, when a client connected via IPv6, the proxy replied with
0x05 (Connection refused). Per RFC 1928 §6, the correct
code for an unsupported address type is 0x08 (Address type not
supported). This matters because SOCKS5-aware clients (including
Telegram Desktop) use the reply code to decide whether to retry or
report a different error to the user.

- writer.write(_socks5_reply(0x05))
+ writer.write(_socks5_reply(0x08))  # RFC 1928 §6: Address type not supported

2. __slots__ on Stats and _WsPool

File: proxy/tg_ws_proxy.py

Both classes are instantiated once at module level and live for the
entire process lifetime. Adding __slots__ removes the per-instance
__dict__, which gives a small but free memory saving and slightly
faster attribute access — relevant for Stats since its counters are
incremented on every packet.

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',
    )

class _WsPool:
    __slots__ = ('_idle', '_refilling')

3. Parallel pool refill via asyncio.gather

File: proxy/tg_ws_proxy.py

The previous _refill implementation created tasks with
asyncio.create_task but then awaited them one by one in a loop —
connections were established sequentially, not in parallel.

Replacing the loop with asyncio.gather makes all needed connections
race concurrently, so the pool fills up in the time it takes to complete
the slowest single handshake instead of needed × handshake_time.

- 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

+ 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()))

4. Graceful WS pool shutdown

File: proxy/tg_ws_proxy.py

On shutdown, idle WebSocket connections sitting in _WsPool._idle were
simply abandoned — the server-side (Telegram DC) would see a TCP RST
instead of a clean WebSocket close frame. The new _WsPool.close()
method drains all buckets and closes every idle connection before the
event loop is cancelled.

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,
        )

close() is called in wait_stop() after server.close() but before
tasks are cancelled, so in-flight connections are not affected.


5. Cross-platform CLI entrypoint

New files: pyproject.toml, requirements-core.txt,
packaging/tg-ws-proxy.service, packaging/com.tg-ws-proxy.plist

The proxy core has no OS-specific dependencies — only windows.py does.
This PR exposes proxy/tg_ws_proxy.py:main as a proper console script
so Linux and macOS users can install and run the proxy without touching
windows.py at all.

Install and run (any OS):

pip install -e .          # registers the tg-ws-proxy command
tg-ws-proxy               # same as: python proxy/tg_ws_proxy.py

requirements-core.txt contains only cryptography — useful for
Docker images or servers where the GUI stack is not needed.

The packaging/ additions follow the existing windows.spec convention:

File Purpose
packaging/tg-ws-proxy.service systemd user service (Linux)
packaging/com.tg-ws-proxy.plist launchd agent (macOS)

Linux autostart:

mkdir -p ~/.config/systemd/user
cp packaging/tg-ws-proxy.service ~/.config/systemd/user/
systemctl --user enable --now tg-ws-proxy

macOS autostart:

cp packaging/com.tg-ws-proxy.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.tg-ws-proxy.plist

Testing

  • Existing Windows tray workflow: no changes — windows.py is untouched.
  • IPv6 reply code: verified against RFC 1928 Table 6.
  • __slots__: confirmed no AttributeError on Stats() and _WsPool() instantiation.
  • gather refill: pool fills in parallel; exceptions are caught via return_exceptions=True.
  • _WsPool.close(): called on stop, logs idle connection count, no impact on active sessions.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/Flowseal/tg-ws-proxy/pull/197 **Author:** [@lifeofcapo](https://github.com/lifeofcapo) **Created:** 3/16/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/socks5-ipv6-slots-gather-graceful-shutdown` --- ### 📝 Commits (1) - [`bef6a4f`](https://github.com/Flowseal/tg-ws-proxy/commit/bef6a4fd3e637a323c24322752f30f42618950e2) fix(proxy): RFC-compliant IPv6 reply, __slots__, parallel pool refill, graceful shutdown + cross-platform CLI ### 📊 Changes **5 files changed** (+107 additions, -14 deletions) <details> <summary>View changed files</summary> ➕ `packaging/com.tg-ws-proxy.plist` (+25 -0) ➕ `packaging/tg-ws-proxy.service` (+12 -0) 📝 `proxy/tg_ws_proxy.py` (+38 -14) ➕ `pyproject.toml` (+31 -0) ➕ `requirements-core.txt` (+1 -0) </details> ### 📄 Description ## Summary Four focused improvements to `proxy/tg_ws_proxy.py` plus cross-platform packaging — no behaviour changes for existing Windows users. --- ## Changes ### 1. Fix IPv6 SOCKS5 reply code (`0x05` → `0x08`) **File:** `proxy/tg_ws_proxy.py` Previously, when a client connected via IPv6, the proxy replied with `0x05` (Connection refused). Per [RFC 1928 §6][rfc1928], the correct code for an unsupported address type is `0x08` (Address type not supported). This matters because SOCKS5-aware clients (including Telegram Desktop) use the reply code to decide whether to retry or report a different error to the user. ```diff - writer.write(_socks5_reply(0x05)) + writer.write(_socks5_reply(0x08)) # RFC 1928 §6: Address type not supported ``` [rfc1928]: https://datatracker.ietf.org/doc/html/rfc1928#section-6 --- ### 2. `__slots__` on `Stats` and `_WsPool` **File:** `proxy/tg_ws_proxy.py` Both classes are instantiated once at module level and live for the entire process lifetime. Adding `__slots__` removes the per-instance `__dict__`, which gives a small but free memory saving and slightly faster attribute access — relevant for `Stats` since its counters are incremented on every packet. ```python 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', ) class _WsPool: __slots__ = ('_idle', '_refilling') ``` --- ### 3. Parallel pool refill via `asyncio.gather` **File:** `proxy/tg_ws_proxy.py` The previous `_refill` implementation created tasks with `asyncio.create_task` but then awaited them one by one in a loop — connections were established sequentially, not in parallel. Replacing the loop with `asyncio.gather` makes all `needed` connections race concurrently, so the pool fills up in the time it takes to complete the *slowest single* handshake instead of `needed × handshake_time`. ```diff - 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 + 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())) ``` --- ### 4. Graceful WS pool shutdown **File:** `proxy/tg_ws_proxy.py` On shutdown, idle WebSocket connections sitting in `_WsPool._idle` were simply abandoned — the server-side (Telegram DC) would see a TCP RST instead of a clean WebSocket close frame. The new `_WsPool.close()` method drains all buckets and closes every idle connection before the event loop is cancelled. ```python 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, ) ``` `close()` is called in `wait_stop()` after `server.close()` but before tasks are cancelled, so in-flight connections are not affected. --- ### 5. Cross-platform CLI entrypoint **New files:** `pyproject.toml`, `requirements-core.txt`, `packaging/tg-ws-proxy.service`, `packaging/com.tg-ws-proxy.plist` The proxy core has no OS-specific dependencies — only `windows.py` does. This PR exposes `proxy/tg_ws_proxy.py:main` as a proper console script so Linux and macOS users can install and run the proxy without touching `windows.py` at all. **Install and run (any OS):** ```bash pip install -e . # registers the tg-ws-proxy command tg-ws-proxy # same as: python proxy/tg_ws_proxy.py ``` `requirements-core.txt` contains only `cryptography` — useful for Docker images or servers where the GUI stack is not needed. The `packaging/` additions follow the existing `windows.spec` convention: | File | Purpose | |---|---| | `packaging/tg-ws-proxy.service` | systemd user service (Linux) | | `packaging/com.tg-ws-proxy.plist` | launchd agent (macOS) | **Linux autostart:** ```bash mkdir -p ~/.config/systemd/user cp packaging/tg-ws-proxy.service ~/.config/systemd/user/ systemctl --user enable --now tg-ws-proxy ``` **macOS autostart:** ```bash cp packaging/com.tg-ws-proxy.plist ~/Library/LaunchAgents/ launchctl load ~/Library/LaunchAgents/com.tg-ws-proxy.plist ``` --- ## Testing - Existing Windows tray workflow: **no changes** — `windows.py` is untouched. - IPv6 reply code: verified against RFC 1928 Table 6. - `__slots__`: confirmed no `AttributeError` on `Stats()` and `_WsPool()` instantiation. - `gather` refill: pool fills in parallel; exceptions are caught via `return_exceptions=True`. - `_WsPool.close()`: called on stop, logs idle connection count, no impact on active sessions. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
mirror-bot added the pull-request label 2026-07-19 11:33:44 +03:00
mirror-bot changed title from [PR #197] [CLOSED] fixed: RFC-compliant IPv6 reply, __slots__ on hot classes, parallel pool refill, graceful pool shutdown + cross-platform CLI to [PR #197] fixed: RFC-compliant IPv6 reply, __slots__ on hot classes, parallel pool refill, graceful pool shutdown + cross-platform CLI 2026-07-20 04:08:30 +03:00
mirror-bot changed title from [PR #197] fixed: RFC-compliant IPv6 reply, __slots__ on hot classes, parallel pool refill, graceful pool shutdown + cross-platform CLI to [PR #197] [CLOSED] fixed: RFC-compliant IPv6 reply, __slots__ on hot classes, parallel pool refill, graceful pool shutdown + cross-platform CLI 2026-07-20 22:54:29 +03:00
mirror-bot changed title from [PR #197] [CLOSED] fixed: RFC-compliant IPv6 reply, __slots__ on hot classes, parallel pool refill, graceful pool shutdown + cross-platform CLI to [PR #197] fixed: RFC-compliant IPv6 reply, __slots__ on hot classes, parallel pool refill, graceful pool shutdown + cross-platform CLI 2026-07-29 15:14:38 +03:00
mirror-bot changed title from [PR #197] fixed: RFC-compliant IPv6 reply, __slots__ on hot classes, parallel pool refill, graceful pool shutdown + cross-platform CLI to [PR #197] [CLOSED] fixed: RFC-compliant IPv6 reply, __slots__ on hot classes, parallel pool refill, graceful pool shutdown + cross-platform CLI 2026-07-29 19:28:31 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Utilities/tg-ws-proxy#11