add: every launch of a listed app lands in bypass (#67) #112

Merged
mysticalsoap merged 4 commits from feature/bypass-every-launch into trunk 2026-08-21 00:15:54 -04:00
Owner

Closes #67.

Problem: an app on the bypass list was only bypassed when launched from the GUI's list button (or the raw-cgroup console trick). Launching the same app from the desktop menu, taskbar or a terminal ignored the list entirely — the opposite of the mark-once UX. The one architecture that must not fix this is a process-event watcher (eBPF/proc-events); placement has to stay deterministic at launch time.

Fix: launcher shims, per the candidate design on the issue.

  • aqomui-bypass <command> CLI (aqomui/launcher.py, new console script): joins the bypass cgroup and execs the command in place, so terminals/launchers see the pid they started. A forked child runs the existing reclaim_strays Electron sweep. When the cgroup is absent — the service deletes it on teardown — it degrades to a plain exec: a listed app still starts with the VPN down, and joining a rule-less cgroup is inert anyway.
  • .desktop overrides: adding an app writes ~/.local/share/applications/<id>.desktop (XDG precedence shadows the system entry) with every Exec= wrapped by the CLI — [Desktop Action] sections included — and DBusActivatable forced off so launchers can't skip Exec. Marker key X-Aqomui-Bypass guards all destructive ops: removal only ever deletes marker files, and a picked entry that was itself user-local is backed up and restored on removal. Overrides regenerate from their sources at every GUI start (absorbs package updates, clears uninstalled apps). The CLI is written by absolute path when resolvable (pip user installs aren't on launcher PATH), self-healing via the same regeneration.
  • GUI: launch-on-click rows replaced with inline remove buttons (the old select-then-Remove two-step is gone, per issue comment); add/remove notifies that the change applies from the app's next start. bypass_tunnel/sweep_bypass and the temp-script machinery are deleted. The app picker skips aqomui's own overrides so a wrapper never wraps a wrapper.
  • README: bypass section reframed as opt-in split tunneling; the raw-cgroup console recipe and serverfault link are gone (the page is Cloudflare-walled now, but its technique — cgroup + mark + policy routing — is exactly what the code already implements; nothing left to mine), replaced by the one-line aqomui-bypass terminal usage. Kept: second-tunnel paragraphs, DNS behavior, "membership survives reconnects, no restarts needed".

Deferred, still tracked on #67's comments: exclude/include/bypass mode split, folding bypass into the one-page UX rework, auto-connect for the bypass server — all belong to the UX-overhaul version, not 0.9.1. Restart prompting (detecting a running listed app) is approximated by the unconditional "restart it if it is running now" notification; real process detection from Exec lines is guesswork across flatpak/wrappers and didn't seem worth it.

Verification: 224 tests pass (18 new in tests/test_launcher.py: override wrapping incl. actions/DBusActivatable/TryExec, double-wrap refusal, backup/restore, marker-guarded removal, refresh regenerate/clear/skip-bad, CLI join/degrade/exec/127/usage). ruff + compileall clean. Override generation smoke-tested against a real system .desktop. Live round: add an app in the tab → check ~/.local/share/applications/ → launch it from the desktop menu with bypass up → its traffic should show the physical interface; remove it → override gone (menu entry back to stock).

🤖 Generated with Claude Code

Closes #67. **Problem**: an app on the bypass list was only bypassed when launched from the GUI's list button (or the raw-cgroup console trick). Launching the same app from the desktop menu, taskbar or a terminal ignored the list entirely — the opposite of the mark-once UX. The one architecture that must not fix this is a process-event watcher (eBPF/proc-events); placement has to stay deterministic at launch time. **Fix**: launcher shims, per the candidate design on the issue. - **`aqomui-bypass <command>` CLI** (`aqomui/launcher.py`, new console script): joins the bypass cgroup and execs the command in place, so terminals/launchers see the pid they started. A forked child runs the existing `reclaim_strays` Electron sweep. When the cgroup is absent — the service deletes it on teardown — it degrades to a plain exec: a listed app still starts with the VPN down, and joining a rule-less cgroup is inert anyway. - **.desktop overrides**: adding an app writes `~/.local/share/applications/<id>.desktop` (XDG precedence shadows the system entry) with every `Exec=` wrapped by the CLI — `[Desktop Action]` sections included — and `DBusActivatable` forced off so launchers can't skip Exec. Marker key `X-Aqomui-Bypass` guards all destructive ops: removal only ever deletes marker files, and a picked entry that was itself user-local is backed up and restored on removal. Overrides regenerate from their sources at every GUI start (absorbs package updates, clears uninstalled apps). The CLI is written by absolute path when resolvable (pip user installs aren't on launcher PATH), self-healing via the same regeneration. - **GUI**: launch-on-click rows replaced with inline **remove** buttons (the old select-then-Remove two-step is gone, per issue comment); add/remove notifies that the change applies from the app's next start. `bypass_tunnel`/`sweep_bypass` and the temp-script machinery are deleted. The app picker skips aqomui's own overrides so a wrapper never wraps a wrapper. - **README**: bypass section reframed as opt-in split tunneling; the raw-cgroup console recipe and serverfault link are gone (the page is Cloudflare-walled now, but its technique — cgroup + mark + policy routing — is exactly what the code already implements; nothing left to mine), replaced by the one-line `aqomui-bypass` terminal usage. Kept: second-tunnel paragraphs, DNS behavior, "membership survives reconnects, no restarts needed". Deferred, still tracked on #67's comments: exclude/include/bypass mode split, folding bypass into the one-page UX rework, auto-connect for the bypass server — all belong to the UX-overhaul version, not 0.9.1. Restart *prompting* (detecting a running listed app) is approximated by the unconditional "restart it if it is running now" notification; real process detection from Exec lines is guesswork across flatpak/wrappers and didn't seem worth it. **Verification**: 224 tests pass (18 new in `tests/test_launcher.py`: override wrapping incl. actions/DBusActivatable/TryExec, double-wrap refusal, backup/restore, marker-guarded removal, refresh regenerate/clear/skip-bad, CLI join/degrade/exec/127/usage). ruff + compileall clean. Override generation smoke-tested against a real system .desktop. Live round: add an app in the tab → check `~/.local/share/applications/` → launch it from the desktop menu with bypass up → its traffic should show the physical interface; remove it → override gone (menu entry back to stock). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
add: every launch of a listed app lands in bypass (#67)
Some checks failed
ci / test (pull_request) Failing after 26s
99bfc46cc7
Marking an app in the bypass tab now covers launches from anywhere --
desktop menu, taskbar, krunner, terminal -- not just the GUI's own
launch button. No process-event watcher is involved: placement stays
deterministic at launch time, per the constraint on #67.

The mechanism is a launcher shim. A new aqomui-bypass CLI joins the
bypass cgroup and execs its command in place (falling back to a plain
exec when bypass is inactive, so listed apps still start with the VPN
down), with a forked child running the Electron stray sweep. For each
listed app the GUI writes a user-local .desktop override that shadows
the system entry per XDG precedence and wraps every Exec with the CLI;
DBusActivatable is forced off so launchers can't sidestep Exec.
Overrides are written on add, deleted on remove (marker-guarded, with
backup/restore when the picked entry was itself user-local), and
regenerated from their sources every GUI start to absorb package
updates and clear uninstalled apps.

With every launch covered, the GUI's launch-on-click rows are gone:
each row now carries an inline remove button, replacing the
select-then-delete two-step, and add/remove notifies that the change
applies from the app's next start. The bypass_tunnel/sweep machinery
moved out of the GUI wholesale. The app picker skips aqomui's own
overrides so a wrapper can never wrap a wrapper. README's bypass
section drops the raw-cgroup console recipe and the serverfault link
(the technique it described is what the code already implements) in
favor of the opt-in split-tunneling framing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mysticalsoap force-pushed feature/bypass-every-launch from 99bfc46cc7
Some checks failed
ci / test (pull_request) Failing after 26s
to c4bc104c68
All checks were successful
ci / test (pull_request) Successful in 32s
2026-08-20 22:54:22 -04:00
Compare
Autostart launches never resolve the applications-dir override -- XDG
autostart is its own precedence chain (~/.config/autostart shadowing
/etc/xdg/autostart by basename). Listed apps now get their autostart
entry wrapped through the same machinery: a user entry is wrapped in
place (backup/restore as with local application entries), a system
entry gets a wrapped shadow copy, and refresh regenerates from the
real source, drops shadows whose system entry was uninstalled, and
picks up autostart entries created after the app was listed.

Autostart wrappers carry a new --wait flag: at login, autostarted apps
race the GUI's bypass activation, and a plain shim would launch before
the cgroup exists. With bypass enabled in settings the CLI waits
briefly for the cgroup before exec'ing; with it disabled, or if
activation never comes, the app starts normally.

README now states the launch-tree model -- children inherit the
bypass, so the entry point is what belongs on the list, with Steam's
per-game 'aqomui-bypass %command%' Launch Options as the tree-splitting
escape hatch -- and the known gaps of launch-time placement: KDE
global-shortcut launches resolve desktop entries once per session
(kglobalaccel captures the KService at registration; re-login picks up
changes), session restore bypasses launchers entirely, and direct
binary exec by unlisted processes passes through no launcher.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix: autostart entries are matched by Exec too, and wrapped on sight
All checks were successful
ci / test (pull_request) Successful in 35s
398f62e864
Two holes in the first autostart pass. Matching by basename only
covered entries KDE's Autostart KCM creates (it copies the app's
entry); apps writing their own login entry pick their own names, so
those were never wrapped -- silently and permanently. Entries now also
match by the Exec line's first command word, still pure file
inspection. In-place-wrapped entries match against their backup's
Exec, since the wrapped copy's is the CLI's. Entries hiding the app
behind a shell wrapper stay uncaught.

And wrapping only happened at add-time and GUI start, so enabling
start-on-login inside an app after listing it left the new entry raw
until the next GUI start -- one login too late. The GUI now watches
the autostart directory and refreshes when it changes, wrapping an
entry the moment it appears. write_override skips content-identical
rewrites so the refresh can't feed its own watcher.

refresh_autostart is now the single entry point (add/remove/startup/
watcher all call it): wrap everything a listed app backs, then prune
every shim nothing backs -- delisted apps, uninstalled system entries,
disabled autostarts. write_autostart/remove_autostart are gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refactor: autostart watcher moves to gui/ (#83 doctrine)
All checks were successful
ci / test (pull_request) Successful in 30s
ci / test (push) Successful in 27s
de18ca0fc4
Qt glue that isn't a widget still doesn't belong in the monolith. It
can't join the rest of the shim logic in launcher.py either -- that
module backs the aqomui-bypass CLI and stays Qt-free -- so the
directory watching lives in gui/autostart.py as its own QObject, with
the GUI passing in the refresh callable and nothing else.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mysticalsoap deleted branch feature/bypass-every-launch 2026-08-21 00:15:54 -04:00
Sign in to join this conversation.
No description provided.