Skip to content

Fix Linux split-tunneling exclude mode for Steam games - #363

Open
MAST1999 wants to merge 10 commits into
Windscribe:masterfrom
MAST1999:fix/linux-split-tunneling-steam-games
Open

MAST1999 wants to merge 10 commits into
Windscribe:masterfrom
MAST1999:fix/linux-split-tunneling-steam-games

Conversation

@MAST1999

@MAST1999 MAST1999 commented Sep 4, 2026

Copy link
Copy Markdown

Problem

On Linux, adding Steam games to split tunneling in exclude mode did nothing: game traffic stayed inside the VPN tunnel. In Dota 2 every region showed ping: FAILED even though the game had been added to the exclude list.

Root causes

  1. Helper process matching (src/helper/linux/split_tunneling/process_monitor.*): processes were matched by comparing readlink(/proc/<pid>/exe) — always a resolved ELF path — against stored entries by raw string equality. Steam picker entries resolve to /usr/bin/steam (a shell script, which /proc/<pid>/exe never shows), and symlinked paths (~/.steam/steam, usrmerge) never compare equal either. Entries are now preprocessed into raw/canonical forms, script launchers match via their shebang interpreter plus an argv token (precise: no basename or arbitrary-token matching), and directory entries (game install trees) match by path prefix. Matched launchers also pull their already-running process trees into the cgroup.

  2. App-picker enumeration (linuxutils.cpp, new steamgames.*): every steam://rungameid/<id> desktop entry resolved to the Steam launcher script, collapsing all games into one "steam" row; games without desktop entries never appeared. Installed games are now enumerated from the Steam libraries themselves (libraryfolders.vdf, appmanifest_*.acf), covering libraries on other drives/locations, flatpak and snap roots, and XDG_DATA_HOME, and are offered as their install-directory entries.

  3. Firewall reply path (firewallcontroller.cpp): exclude-mode accepted reply packets only via meta cgroup, which cannot match on the input path for unconnected UDP sockets (no early demux — skb->sk is NULL). That is exactly the pattern game server browsers use (Steam SDR pings), so replies to excluded processes were dropped by the always-on firewall. The bypass mark is now saved on the connection at egress, restored from conntrack at ingress, and accepted by mark — mirroring the include-mode reply handling.

Testing

The helper-side matching was exercised with a standalone harness against live processes (symlinked paths, script launchers under their interpreter, directory trees, descendant sweeps, snap/flatpak entry shapes preserved, security guards for non-regular files and shebang-interpreter precision), including a live netlink EXEC-event test.

The fixes have been tested manually on my system (Arch-based, CachyOS): the client was built and installed as a package, Steam game entries on a secondary-drive library are matched and moved into the split-tunnel cgroup, marked traffic routes via the physical gateway, and connected vs. unconnected UDP reply paths were verified before and after the firewall fix (Dota 2 region pings now succeed in exclude mode).

Attribution

Most of the work was done with an AI agent (OpenCode); the fixes have been tested manually on my system and verified that they work.

@MAST1999
MAST1999 force-pushed the fix/linux-split-tunneling-steam-games branch from 0130654 to df8bcab Compare September 4, 2026 08:14
@MAST1999
MAST1999 force-pushed the fix/linux-split-tunneling-steam-games branch from df8bcab to 5d31123 Compare September 14, 2026 18:06
@jaxu

jaxu commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Hi there, apologies for the slow response on this one, we've been quite busy internally.

First, thank you for your contribution. We agree that Steam games are a common use case and that our split tunneling implementation should work with it. Your PR correctly touches on several points and we're grateful for the work you've done.

That said, we're unable to accept the patch as it is right now, since it contains various edge case issues:

  • Increasing the hostname limit is not desirable -- this can lead to various performance issues and is regardless outside the scope of the improvement.
  • The matching is not quite correct -- it currently matches by cwd, which means if you cd to a game dir and then did curl https://some/url, that curl command would be split tunneled. This may allow any executable to selectively tunnel/untunnel their traffic, which may be a security issue.
  • The symlink handling is suspect: realpath()/stat()/etc may hang on an unresponsive FUSE/SMB/NFS mounts, and links are resolved one-time only.
  • Rules added/removed while the game is already running do not take effect until next launch.
  • Various other minor things (modprobe cls_cgroup should probably just be removed instead of patched)

If you'd like to take a stab at the above, please feel free. I've added this task to our internal board as well, so we'll get to this in a future release, and you'll be credited in the changelog even if we end up implementing it differently.

Three defects kept Steam games from bypassing the VPN tunnel in
exclude mode:

* Helper process matching compared readlink(/proc/<pid>/exe) (always a
  resolved ELF path) against stored app entries by raw string equality,
  so script-wrapped launchers (/usr/bin/steam) and symlinked paths
  (~/.steam/steam, usrmerge) could never match. Entries are now
  preprocessed into raw/canonical/script-interpreter/directory forms:
  script launchers match via their shebang interpreter plus an argv
  token, directory entries (game install trees) match by path prefix,
  and matched launchers pull their already-running process trees into
  the cgroup.

* The GUI app picker resolved every steam://rungameid desktop entry to
  the Steam launcher script, collapsing all games into a single
  "steam" row, and games without desktop entries never appeared.
  Installed games are now enumerated from the Steam libraries
  themselves (libraryfolders.vdf and appmanifest_*.acf, covering
  libraries on other drives, flatpak/snap roots and XDG_DATA_HOME) and
  offered as their install-directory entries.

* The exclude-mode firewall accepted reply packets only by
  "meta cgroup", which cannot match on the input path for unconnected
  UDP sockets (no early demux) - the pattern game server browsers use
  (Steam SDR pings) - so replies were dropped by the always-on firewall
  and every region showed "ping: failed". The bypass mark is now saved
  on the connection at egress, restored from conntrack at ingress, and
  accepted by mark, mirroring the include-mode reply handling.
Wine reports the preloader as the process exe, so path rules never matched Proton games; classify them via working directory and the Windows drive-mapped argv[0] instead.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Rule construction (realpath/stat on client-supplied paths) can block indefinitely on unresponsive FUSE/SMB/NFS mounts, so it now runs on a dedicated worker thread: setApps swaps in raw entries and wakes the worker, which resolves, swaps in new rules, and rescans /proc so entries added while a game is already running take effect without a relaunch. Rules are also re-resolved periodically so symlink churn from game updates does not go stale. The cwd matching signal is removed entirely: a shell cd'd into an excluded directory would pass the exemption to every child it spawns, letting any executable selectively escape the tunnel; Wine/Proton games remain covered by the wine-exe-gated argv[0] match.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
The removal pass re-read rules_ after apps_ was already swapped, so a worker rebuild landing in between could drop the removed entry's rule and strand its processes in the cgroup until disable. Also drops the write-only lastRulesBuild_ field.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Orphaned when cwd matching was removed; windowsPathMatchesRule handles the /run/host remap inline. The test header still described the removed cwd signal.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
@MAST1999
MAST1999 force-pushed the fix/linux-split-tunneling-steam-games branch from d0ac7a8 to 83f93b5 Compare September 29, 2026 19:16
@MAST1999

Copy link
Copy Markdown
Author

Hey there

Sorry for the unrelated changes, I was using this branch locally for a few things I needed and accidentally pushed changes unrelated to the fix.

I've updated the branch to address the issues you've pointed out.
I've also tested the changes on my own system to make sure both native linux games and wine (proton) work correctly with split tunneling.

Hopefully it looks better now.

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.

3 participants