Skip to content

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

@parkour86

Description

@parkour86

Step 1: Please describe your environment

  • EpixNet version: 0.2.7 (source from main-fc3486d8f9e5196870aa451531cf4712a213ed02)
  • Operating system: Ubuntu 24.04 (Python 3.12.3, Gevent 26.4.0)
  • Web browser: N/A (crash happens before the UI is opened)
  • Tor status: always (--tor always)
  • Opened port: no
  • Special configuration: Started via start-venv.sh with flags --tor always --no-dht. System tray (Trayicon plugin) enabled, X11 session with DISPLAY=:1. pystray and python-xlib installed in the venv.

Step 2: Describe the problem

When starting EpixNet with --tor always, the Trayicon plugin crashes the whole process at startup because the global socket.socket has been replaced with socks.socksocket by src/util/SocksProxy.py:monkeyPatch. pystray's xorg backend imports Xlib, which then tries to open the X server connection through the SOCKS-patched socket. Unix-domain X11 sockets aren't supported by PySocks, and the TCP fallback fails because there's no destination configured for a local X server connection, so EpixNet exits.

Steps to reproduce

  1. On Linux with an X11 session (e.g. DISPLAY=:1), set up the venv and install requirements.
  2. Make sure pystray and python-xlib are installed (they're pulled in for the system tray).
  3. Run ./start-venv.sh (which launches python3 epixnet.py --tor always --no-dht).
  4. EpixNet patches sockets to the Tor SOCKS proxy, then the Trayicon plugin tries to create the tray icon.

Observed Results

EpixNet crashes at startup with the following traceback:

[21:27:52] - Patching sockets to tor socks proxy: 127.0.0.1:9050
[21:27:52] - epixnet 0.2.7 (source from main-fc3486d8f9e5196870aa451531cf4712a213ed02-dirty) on Python 3.12.3 ... Gevent 26.4.0
[21:27:52] - Unhandled exception: Can't connect to display ":1": Invalid destination-connection (host, port) pair
Traceback (most recent call last):
  File ".../Xlib/support/unix_connect.py", line 104, in get_socket
    s = _get_unix_socket(address)
  File ".../Xlib/support/unix_connect.py", line 82, in _get_unix_socket
    s.connect(address)
  File ".../socks.py", line 742, in connect
    raise socket.error("PySocks doesn't support IPv6: %s"
OSError: PySocks doesn't support IPv6: /tmp/.X11-unix/X1

During handling of the above exception, another exception occurred:
  File ".../Xlib/support/unix_connect.py", line 76, in _get_tcp_socket
    s.connect((host, 6000 + dno))
  File ".../socks.py", line 769, in connect
    raise GeneralProxyError(...)
socks.GeneralProxyError: Invalid destination-connection (host, port) pair

During handling of the above exception, another exception occurred:
  File ".../plugins/Trayicon/TrayiconPlugin.py", line 205, in main
    self.icon = pystray.Icon(...)
  File ".../pystray/_xorg.py", line 107, in __init__
    self._display = Xlib.display.Display()
  ...
Xlib.error.DisplayConnectionError: Can't connect to display ":1": Invalid destination-connection (host, port) pair

Exception ignored in: <function Icon.__del__ at ...>
AttributeError: 'Icon' object has no attribute '_display'

Root cause: src/util/SocksProxy.py:monkeyPatch replaces socket.socket with socks.socksocket globally. When pystray._xorg later constructs Xlib.display.Display() (both during import and inside Icon.__init__ / Icon.run), Xlib.support.unix_connect calls socket.socket(AF_UNIX, ...) to talk to /tmp/.X11-unix/X*. PySocks rejects the AF_UNIX address (PySocks doesn't support IPv6) and the TCP fallback to localhost:6001 fails too (Invalid destination-connection (host, port) pair).

Expected Results

When --tor always is set on a Linux desktop with a system tray, EpixNet should still start normally and show its tray icon. X11 connections to the local display should never be routed through the Tor SOCKS proxy. If the X display genuinely cannot be opened (truly headless), the Trayicon plugin should log a warning and continue without a tray icon instead of crashing the entire process.

Suggested fix

In plugins/Trayicon/TrayiconPlugin.py, before importing/initializing pystray, restore the original socket class (saved by SocksProxy.monkeyPatch as socket.socket_noproxy) and patch the socket module reference inside Xlib.support.unix_connect, Xlib.support.connect, and Xlib.protocol.display with a shim whose .socket attribute is socket.socket_noproxy. This keeps the global SOCKS patch in place for normal network traffic but ensures Xlib's local X11 connections always use the unproxied socket. Also wrap the pystray initialization in a generic except Exception so a missing/unreachable display degrades gracefully instead of crashing EpixNet.

Suggested patch

Applied to plugins/Trayicon/TrayiconPlugin.py, inside ActionsPlugin.main(), replacing the original try: import pystray ... block:

    def main(self):
        # SocksProxy.monkeyPatch may have replaced socket.socket with
        # socks.socksocket, which doesn't support AF_UNIX. Xlib (used by
        # pystray's xorg backend) connects to /tmp/.X11-unix/X* via AF_UNIX,
        # so temporarily restore the real socket while pystray initializes.
        import socket
        _patched_socket = None
        if hasattr(socket, "socket_noproxy"):
            _patched_socket = socket.socket
            socket.socket = socket.socket_noproxy
        try:
            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:
                print("Trayicon plugin: failed to initialize tray icon backend (%s), skipping tray icon." % err)
                super(ActionsPlugin, self).main()
                return

            # pystray's xorg backend opens (and uses) the X display lazily
            # inside Icon.__init__ / Icon.run, long after this function
            # returns. Patch Xlib's module-level `socket` references with a
            # shim that exposes the un-proxied socket class, so X11
            # connections never go through PySocks.
            if _patched_socket is not None:
                try:
                    import types
                    shim = types.ModuleType("socket_noproxy_shim")
                    for _name in dir(socket):
                        try:
                            setattr(shim, _name, getattr(socket, _name))
                        except AttributeError:
                            pass
                    shim.socket = socket.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
                except Exception as err:
                    print("Trayicon plugin: failed to patch Xlib socket: %s" % err)
        finally:
            if _patched_socket is not None:
                socket.socket = _patched_socket

What this does:

  1. Temporarily un-patches socket.socket while pystray (and transitively Xlib) is imported, so the import-time Xlib.display.Display() call in pystray/_xorg.py succeeds.
  2. Installs a socket shim into the loaded Xlib modules (Xlib.support.unix_connect, Xlib.support.connect, Xlib.protocol.display). The shim mirrors the real socket module but exposes socket.socket_noproxy as its .socket class. This is necessary because pystray._xorg.Icon.__init__ opens its own Xlib.display.Display() later, in the trayicon thread — long after main() returns — and that call must also bypass PySocks.
  3. Restores the global socket.socket (= socks.socksocket) in finally, so all other EpixNet/Gevent network traffic continues to be routed through the Tor SOCKS proxy as before.
  4. Adds a generic except Exception around the pystray import so a truly unavailable display (headless server, Wayland-only, etc.) just logs a warning and continues without a tray icon, instead of taking the whole process down.

No other files are modified, and the change is a no-op when --tor always is not in use (because socket.socket_noproxy only exists after SocksProxy.monkeyPatch has run).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions