Skip to content

fix(trayicon): bypass SOCKS proxy for X11 connections under --tor always#40

Merged
MudDev merged 1 commit into
mainfrom
fix/trayicon-tor-mode
May 18, 2026
Merged

fix(trayicon): bypass SOCKS proxy for X11 connections under --tor always#40
MudDev merged 1 commit into
mainfrom
fix/trayicon-tor-mode

Conversation

@MudDev
Copy link
Copy Markdown

@MudDev MudDev commented May 18, 2026

Summary

Fixes #30. With `--tor always` on a desktop Linux session with `$DISPLAY` set, EpixNet's tray icon previously crashed the daemon (pre-#38) or silently failed to appear (post-#38). The root cause is that local X11 traffic was being routed through Tor, which can't speak X11 to `/tmp/.X11-unix/X*`.

Credit to @parkour86 for the detailed report and a working patch in the issue body — this PR is a lightly cleaned-up version of their fix.

Root cause

`src/util/SocksProxy.py:monkeyPatch` replaces `socket.socket` with `socks.socksocket` globally so all outbound traffic routes through the Tor SOCKS proxy. When pystray's `_xorg` backend imports `Xlib`, Xlib.support.unix_connect does `socket.socket(socket.AF_UNIX, ...)` to talk to the local X server. PySocks rejects `AF_UNIX` (`PySocks doesn't support IPv6`), and the TCP fallback to `localhost:6001` also fails (`Invalid destination-connection (host, port) pair`).

Local X11 should never go over Tor anyway, so the right fix is to make Xlib use the un-proxied socket.

Fix

In `plugins/Trayicon/TrayiconPlugin.py`, inside `ActionsPlugin.main()`:

  1. Temporarily restore `socket.socket = socket.socket_noproxy` while pystray imports, so the import-time `Xlib.display.Display()` in `pystray/_xorg.py` succeeds.
  2. Install a delegating `socket`-module shim into the loaded Xlib modules (`Xlib.support.unix_connect`, `Xlib.support.connect`, `Xlib.protocol.display`). The shim delegates every attribute to the real `socket` module via `getattr`, but overrides `.socket` to point at the un-proxied class. This handles `pystray._xorg`'s later `Display()` calls inside `Icon.init` / `Icon.run` (running in the tray thread, long after `main()` returns) which would otherwise re-hit the SOCKS-patched global.
  3. Restore `socket.socket` in `finally` so all other EpixNet/Gevent traffic continues routing through Tor.

The fix is a no-op when `--tor always` is not in use, because `socket.socket_noproxy` only exists after `SocksProxy.monkeyPatch` has run (gated by `hasattr(socket, 'socket_noproxy')`).

+        import socket as _socket_mod
+        _socks_patched = hasattr(_socket_mod, "socket_noproxy")
+        _saved_socket = _socket_mod.socket if _socks_patched else None
+        if _socks_patched:
+            _socket_mod.socket = _socket_mod.socket_noproxy
+
         try:
-            import pystray
-            import pystray._base
-            from PIL import Image
-        except ImportError as err:
-            ...
+            try:
+                import pystray
+                ...
+            except ImportError as err:
+                ...
+
+            if _socks_patched:
+                class _UnproxiedSocketModule:
+                    def __getattr__(self, name):
+                        return getattr(_socket_mod, name)
+                _shim = _UnproxiedSocketModule()
+                _shim.socket = _socket_mod.socket_noproxy
+                for _modname in ("Xlib.support.unix_connect",
+                                 "Xlib.support.connect",
+                                 "Xlib.protocol.display"):
+                    _mod = sys.modules.get(_modname)
+                    if _mod is not None and hasattr(_mod, "socket"):
+                        _mod.socket = _shim
+        finally:
+            if _socks_patched:
+                _socket_mod.socket = _saved_socket

Differences from @parkour86's suggested patch

  • Used a class with `getattr` delegation instead of iterating `dir(socket)` to build the shim. Functionally equivalent but shorter and avoids the `try/except AttributeError` noise on `dir()` entries that are always valid.
  • The broad `except Exception` around `import pystray` is already present from fix(trayicon): skip tray init on headless Linux/BSD instead of crashing #38, so I didn't re-add it.
  • Same overall structure: un-patch around import, install module shim, restore in `finally`.

Test plan

  • `python3 -c "import ast; ast.parse(...)" ` — syntax OK
  • Verified `try/finally` correctly fires on early `return` inside inner `except` (Python semantics)
  • Reproduce on Linux desktop with `DISPLAY=:1`, `--tor always`, tor running locally: tray icon should appear and the daemon should boot normally
  • Verify under `--tor disable`: no behavior change (the `hasattr(socket, 'socket_noproxy')` guard skips all the patching)
  • Verify on truly headless box: still hits the existing `DISPLAY/WAYLAND_DISPLAY` pre-check and skips trayicon cleanly
  • Verify Windows/macOS: untouched (the socket patching logic runs but `Xlib.*` modules aren't loaded, so the `sys.modules.get(...)` loop is a no-op)

Notes

The Xlib module shim is not restored — it's a permanent side effect for the lifetime of the process. That's intentional: any future X11 connection should bypass SOCKS, not just the trayicon's. If at some point we add another X11-using component, it'll get the same correct behavior automatically.

When --tor always is set, src/util/SocksProxy.py:monkeyPatch replaces
socket.socket globally with socks.socksocket. pystray's _xorg backend
then connects to /tmp/.X11-unix/X* via AF_UNIX, which PySocks rejects
("PySocks doesn't support IPv6"). The TCP fallback also fails because
no destination is configured for the local X server.

Before #38 this killed the daemon. After #38 the broad except handler
catches DisplayConnectionError and continues without a tray icon —
but Tor users on a desktop still lose their tray icon for no good
reason (local X11 must never go over Tor anyway).

Fix:
- Temporarily restore socket.socket = socket.socket_noproxy while
  pystray imports, so the import-time Xlib.display.Display() in
  pystray/_xorg.py succeeds.
- Install a delegating socket-module shim into the loaded Xlib
  modules (Xlib.support.unix_connect, Xlib.support.connect,
  Xlib.protocol.display) whose .socket attribute is the un-proxied
  class. This handles pystray._xorg's later Display() calls in
  Icon.__init__ / Icon.run (running in the tray thread, long after
  main() returns) which would otherwise re-hit the SOCKS-patched
  global socket.
- Restore socket.socket in `finally` so all other EpixNet/Gevent
  traffic continues to route through Tor.

No-op when --tor always is not in use, because socket.socket_noproxy
only exists after SocksProxy.monkeyPatch has run.

Credit: @parkour86 reported the bug and supplied a working patch in
the issue; this commit is a lightly cleaned-up version of their fix.

Fixes #30
@MudDev MudDev merged commit 0c283d0 into main May 18, 2026
3 checks passed
@MudDev MudDev deleted the fix/trayicon-tor-mode branch May 18, 2026 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trayicon plugin crashes EpixNet at startup with --tor always (Xlib X11 connection routed through PySocks)

1 participant