From f544cc6f0f23e7af55553aeaf977b99ace0b180a Mon Sep 17 00:00:00 2001 From: Mud <44410798+MudDev@users.noreply.github.com> Date: Mon, 18 May 2026 17:44:26 -0600 Subject: [PATCH] fix(trayicon): bypass SOCKS proxy for X11 connections under --tor always MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- plugins/Trayicon/TrayiconPlugin.py | 68 +++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/plugins/Trayicon/TrayiconPlugin.py b/plugins/Trayicon/TrayiconPlugin.py index 7b0eb740a..fc96d9169 100644 --- a/plugins/Trayicon/TrayiconPlugin.py +++ b/plugins/Trayicon/TrayiconPlugin.py @@ -72,23 +72,59 @@ def main(self): super(ActionsPlugin, self).main() return + # With --tor always, SocksProxy.monkeyPatch replaces socket.socket + # with socks.socksocket globally. Xlib (used by pystray's _xorg + # backend) connects to /tmp/.X11-unix/X* via AF_UNIX, which PySocks + # rejects. Temporarily restore the real socket while pystray imports + # and install a socket shim into the loaded Xlib modules so the + # tray thread's later Display() calls also bypass SOCKS. The global + # socket.socket is restored in `finally` so the rest of EpixNet + # keeps routing through Tor. + 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: - print("Trayicon plugin: pystray or Pillow not installed (%s), skipping tray icon." % err) - super(ActionsPlugin, self).main() - return - except Exception as err: - # pystray's backend selection runs at import time and can raise - # backend-specific errors (e.g. Xlib.error.DisplayNameError on - # X11 systems, AppKit errors on broken macOS installs). Treat - # any import-time failure as "tray unavailable" rather than - # crashing the whole daemon. - print("Trayicon plugin: failed to initialize tray backend (%s), skipping tray icon." % err) - super(ActionsPlugin, self).main() - return + try: + import pystray + import pystray._base + from PIL import Image + except ImportError as err: + print("Trayicon plugin: pystray or Pillow not installed (%s), skipping tray icon." % err) + super(ActionsPlugin, self).main() + return + except Exception as err: + # pystray's backend selection runs at import time and can raise + # backend-specific errors (e.g. Xlib.error.DisplayNameError on + # X11 systems, AppKit errors on broken macOS installs). Treat + # any import-time failure as "tray unavailable" rather than + # crashing the whole daemon. + print("Trayicon plugin: failed to initialize tray backend (%s), skipping tray icon." % err) + super(ActionsPlugin, self).main() + return + + # pystray._xorg opens the X display lazily inside Icon.__init__ + # / Icon.run, in the trayicon thread — long after this function + # returns. Replace the `socket` module reference inside Xlib's + # loaded modules with a shim that exposes the un-proxied socket + # class, so those later X11 connections also bypass PySocks. + 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 # pystray runs on a real OS thread and uses queue.Queue for signaling. # gevent.monkey.patch_all() replaces queue.Queue with a cooperative