Travel Mode
A Linux-first, open-source alternative to TripMode: a root daemon, CLI, and GTK4/libadwaita desktop app that show which applications are using the network and let you block any of them, live, down to the packet.
Metered tethering and data caps are the everyday trigger: knowing that Steam or a backup client is quietly saturating a hotspot, and being able to shut it off without killing the app or firewalling the whole machine.
Overview
GoalPer-application network control on Linux: see who's using the connection, how much data they're moving, and block any of them with a single command.
InspirationTripMode, a macOS/Windows-only app that does exactly this. Nothing equivalent existed for Linux, so I built one.
Architecture
Travel Mode is a Rust workspace split into a root daemon, a CLI, and a desktop GUI, deliberately kept as three thin clients around one source of truth.
travelmoded (root)Owns everything: interface/route detection, a conntrack poller for live flow byte counts, a procfs process scanner, socket-to-process attribution, the rule store, and the firewall.
travelmode (CLI)Talks to the daemon over a Unix socket with length-prefixed JSON. status, ps, connections, top, block, allow, watch, --json for scripting.
travelmode-guiA stateless GTK4/libadwaita client (relm4) with a tray icon. Subscribes to the daemon's event stream and renders it; carries no networking logic of its own.
Enforcement runs through the daemon’s own inet travelmode nftables table: new outbound connections get queued to userspace via NFQUEUE, the daemon attributes the packet to a process, and drops it if that executable has an active Block rule. Everything is designed to fail open: if filtering can’t be set up (no root, missing kernel support), traffic just flows and the daemon reports filtering_active = false rather than breaking the network.
Development highlights
Kill-on-blockBlocking an app doesn't just stop new connections, it kills what's already open: TCP sockets are torn down with `ss -K`, and matching conntrack entries are deleted so any in-flight packets re-enter as NEW and hit the block instead of coasting through on an old connection.
Best-effort attributionFlows get mapped to a process by matching the socket inode against every process's open file descriptors in /proc. Anything that can't be attributed shows as unknown and stays fail-open rather than risk blocking the wrong thing.
Optimistic GUI stateToggling a switch in the GUI records the request locally and doesn't let the daemon's 1-second status tick overwrite it until the confirming event actually arrives, otherwise a slow round-trip visibly yanks the switch back mid-click.
Chasing a rendering bugStray fragments of other pages' text were showing up at repaint edges. Traced it back to a damage-tracking bug in GTK's GPU renderer on this hardware/compositor combination, not application code, and worked around it by forcing full-redraw.
Result & impact
In daily usePhase 2 (daemon, CLI, and GUI end to end) is complete and is what I actually run day to day on tethered and metered connections.
What's nextUsage history and per-app data limits, then automatic profile switching so home Wi-Fi and a phone hotspot can carry different rule sets without touching anything by hand.
The bigger draw was the systems side rather than the UI: designing something that sits directly on nftables and NFQUEUE, has to reason correctly about kernel state (conntrack entries, socket lifetimes, fail-open behaviour under every failure mode), and still stays boring and predictable enough to trust running as root on a machine I use every day.