Owner feature order (sneak, 2026-09-21, chat, verbatim): "when running on linux it should work as it does now (it's designed to be run on a linux system with those specific bridge interfaces on it). when run on macOS it should detect if there is a vpn running (e.g. mullvad) and use that as the gu0 equivalent interface. when it only detects one normal internet interface, it should display only one interface in the UI. it will only run on linux with 1 or 2 interfaces (either default route interface, or the gu/vanilla ones) or on macOS with 1 or 2 interfaes (either default route internet interface, or same plus a mullvad vpn client interface)."
Definition of done:
Linux behavior unchanged from today: the existing gu/vanilla bridge-interface setup works exactly as it does now.
Linux, no gu/vanilla interfaces present: the default-route interface is monitored alone.
macOS: the tool runs; it detects a running VPN client interface (e.g. Mullvad's utun) and uses it as the gu0 equivalent alongside the default-route internet interface.
macOS, no VPN running: only the default-route internet interface is monitored.
Whenever only one interface is detected (either OS), the UI displays a single interface — no empty second pane.
Supported matrix, exhaustively: Linux with 1 (default-route) or 2 (gu/vanilla) interfaces; macOS with 1 (default-route) or 2 (default-route plus VPN client) interfaces. Anything else exits with a clear error.
Tests cover the detection logic per platform (interface enumeration/VPN detection behind a mockable boundary; no live-network requirement in unit tests).
README updated: platform support, detection behavior, the supported matrix.
Owner feature order (sneak, 2026-09-21, chat, verbatim): "when running on linux it should work as it does now (it's designed to be run on a linux system with those specific bridge interfaces on it). when run on macOS it should detect if there is a vpn running (e.g. mullvad) and use that as the gu0 equivalent interface. when it only detects one normal internet interface, it should display only one interface in the UI. it will only run on linux with 1 or 2 interfaces (either default route interface, or the gu/vanilla ones) or on macOS with 1 or 2 interfaes (either default route internet interface, or same plus a mullvad vpn client interface)."
Definition of done:
- Linux behavior unchanged from today: the existing gu/vanilla bridge-interface setup works exactly as it does now.
- Linux, no gu/vanilla interfaces present: the default-route interface is monitored alone.
- macOS: the tool runs; it detects a running VPN client interface (e.g. Mullvad's utun) and uses it as the `gu0` equivalent alongside the default-route internet interface.
- macOS, no VPN running: only the default-route internet interface is monitored.
- Whenever only one interface is detected (either OS), the UI displays a single interface — no empty second pane.
- Supported matrix, exhaustively: Linux with 1 (default-route) or 2 (gu/vanilla) interfaces; macOS with 1 (default-route) or 2 (default-route plus VPN client) interfaces. Anything else exits with a clear error.
- Tests cover the detection logic per platform (interface enumeration/VPN detection behind a mockable boundary; no live-network requirement in unit tests).
- README updated: platform support, detection behavior, the supported matrix.
Blocked by https://git.eeqj.de/sneak/rtnetmon/issues/1 (standards adoption lands first).
model: claude-fable-5
Plan (implementer's brief). One unit, one PR based on next, after #1 lands.
Why it does not run on macOS today. Every Go file carries //go:build linux, ping is invoked with Linux-only flags (-I iface, -W in seconds), and the two interfaces are hard-wired as interfaceA/interfaceB with the UI always drawing both.
Detection, behind one mockable boundary. A small interface in internal/monitor (or a new internal/netdetect package) with two methods, both pure data: list interfaces (name, up flag, IPv4 addresses; from net.Interfaces()), and list default routes (interface name, gateway). The real implementation of the route query is per platform; the selection logic that consumes the data is platform-independent and takes the OS name as a parameter, so every branch of the matrix below is unit-tested on Linux with fake data.
Linux default routes: parse ip -4 route show default (fallback /proc/net/route, destination 00000000).
macOS default routes: parse netstat -rn -f inet, every row whose destination is default. When a VPN is up macOS keeps the physical default as a second, interface-scoped default row, so both interfaces appear.
macOS VPN interface: a utun* interface that carries a default route, or that is up with a non-link-local IPv4 address (Mullvad and other WireGuard/OpenVPN clients use utun; idle system utuns only have fe80:: addresses).
Selection (the supported matrix, exhaustive).
Linux: both --ifaceA and --ifaceB (defaults gu0/backhaul0) exist: two panes, exactly today's behavior. Neither exists: the one default-route interface, one pane. Exactly one of the pair exists, or no/multiple default routes: exit with an error naming what was found and what is supported.
macOS: VPN interface found: it is pane A (the gu0 equivalent, label "VPN"), the non-VPN default-route interface is pane B. No VPN: one pane on the default-route interface. No default route, or more than one non-VPN default route: exit with an error.
The --ifaceA/--ifaceB/--labelA/--labelB flags keep working on both platforms; detection only fills in what was not given explicitly on macOS, and on Linux only when the named pair is absent.
Per-platform command shapes, built by a pure function keyed on OS name (unit-testable for both): Linux ping -I iface -c1 -W1; macOS ping -b iface -c1 -W1000 (-b binds the interface, -W is milliseconds). curl --interface is the same on both. TCP timing binds the source address today; on macOS a Control hook additionally sets IP_BOUND_IF (thin build-tagged file) so traffic leaves the chosen interface rather than the VPN default route. Linux dialing is untouched.
UI.Monitor holds a slice of one or two interfaces; Run starts loops and uiLoop draws panes per entry, so a single interface draws a single pane. NewMonitor takes the list; the README API example follows.
Build tags removed from all files except the two thin per-platform files (darwin / linux). CI builds both (GOOS=darwin go build cross-compiles without a mac; this goes into script/cibuild).
Tests. Table tests over fake interface/route data for every matrix row on both OS names; parser tests for ip route, /proc/net/route and netstat -rn sample output; ping-argument tests. Not executable on this Linux host, to be disclosed in the PR body: real netstat parsing against a live mac, IP_BOUND_IF dialing, and whether Mullvad's leak-protection firewall lets bound traffic out of the physical interface.
README. Platform support, detection behavior, the matrix, updated API example.
Model: fable-5-1
Plan (implementer's brief). One unit, one PR based on `next`, after https://git.eeqj.de/sneak/rtnetmon/issues/1 lands.
**Why it does not run on macOS today.** Every Go file carries `//go:build linux`, `ping` is invoked with Linux-only flags (`-I iface`, `-W` in seconds), and the two interfaces are hard-wired as `interfaceA`/`interfaceB` with the UI always drawing both.
**Detection, behind one mockable boundary.** A small interface in `internal/monitor` (or a new `internal/netdetect` package) with two methods, both pure data: list interfaces (name, up flag, IPv4 addresses; from `net.Interfaces()`), and list default routes (interface name, gateway). The real implementation of the route query is per platform; the selection logic that consumes the data is platform-independent and takes the OS name as a parameter, so every branch of the matrix below is unit-tested on Linux with fake data.
- Linux default routes: parse `ip -4 route show default` (fallback `/proc/net/route`, destination `00000000`).
- macOS default routes: parse `netstat -rn -f inet`, every row whose destination is `default`. When a VPN is up macOS keeps the physical default as a second, interface-scoped `default` row, so both interfaces appear.
- macOS VPN interface: a `utun*` interface that carries a default route, or that is up with a non-link-local IPv4 address (Mullvad and other WireGuard/OpenVPN clients use `utun`; idle system `utun`s only have `fe80::` addresses).
**Selection (the supported matrix, exhaustive).**
- Linux: both `--ifaceA` and `--ifaceB` (defaults `gu0`/`backhaul0`) exist: two panes, exactly today's behavior. Neither exists: the one default-route interface, one pane. Exactly one of the pair exists, or no/multiple default routes: exit with an error naming what was found and what is supported.
- macOS: VPN interface found: it is pane A (the `gu0` equivalent, label "VPN"), the non-VPN default-route interface is pane B. No VPN: one pane on the default-route interface. No default route, or more than one non-VPN default route: exit with an error.
- The `--ifaceA/--ifaceB/--labelA/--labelB` flags keep working on both platforms; detection only fills in what was not given explicitly on macOS, and on Linux only when the named pair is absent.
**Per-platform command shapes**, built by a pure function keyed on OS name (unit-testable for both): Linux `ping -I iface -c1 -W1`; macOS `ping -b iface -c1 -W1000` (`-b` binds the interface, `-W` is milliseconds). `curl --interface` is the same on both. TCP timing binds the source address today; on macOS a `Control` hook additionally sets `IP_BOUND_IF` (thin build-tagged file) so traffic leaves the chosen interface rather than the VPN default route. Linux dialing is untouched.
**UI.** `Monitor` holds a slice of one or two interfaces; `Run` starts loops and `uiLoop` draws panes per entry, so a single interface draws a single pane. `NewMonitor` takes the list; the README API example follows.
**Build tags** removed from all files except the two thin per-platform files (`darwin` / `linux`). CI builds both (`GOOS=darwin go build` cross-compiles without a mac; this goes into `script/cibuild`).
**Tests.** Table tests over fake interface/route data for every matrix row on both OS names; parser tests for `ip route`, `/proc/net/route` and `netstat -rn` sample output; ping-argument tests. Not executable on this Linux host, to be disclosed in the PR body: real `netstat` parsing against a live mac, `IP_BOUND_IF` dialing, and whether Mullvad's leak-protection firewall lets bound traffic out of the physical interface.
**README.** Platform support, detection behavior, the matrix, updated API example.
Model: fable-5-1
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Owner feature order (sneak, 2026-09-21, chat, verbatim): "when running on linux it should work as it does now (it's designed to be run on a linux system with those specific bridge interfaces on it). when run on macOS it should detect if there is a vpn running (e.g. mullvad) and use that as the gu0 equivalent interface. when it only detects one normal internet interface, it should display only one interface in the UI. it will only run on linux with 1 or 2 interfaces (either default route interface, or the gu/vanilla ones) or on macOS with 1 or 2 interfaes (either default route internet interface, or same plus a mullvad vpn client interface)."
Definition of done:
gu0equivalent alongside the default-route internet interface.Blocked by #1 (standards adoption lands first).
model: claude-fable-5
@clawbot begin work on this
Plan (implementer's brief). One unit, one PR based on
next, after #1 lands.Why it does not run on macOS today. Every Go file carries
//go:build linux,pingis invoked with Linux-only flags (-I iface,-Win seconds), and the two interfaces are hard-wired asinterfaceA/interfaceBwith the UI always drawing both.Detection, behind one mockable boundary. A small interface in
internal/monitor(or a newinternal/netdetectpackage) with two methods, both pure data: list interfaces (name, up flag, IPv4 addresses; fromnet.Interfaces()), and list default routes (interface name, gateway). The real implementation of the route query is per platform; the selection logic that consumes the data is platform-independent and takes the OS name as a parameter, so every branch of the matrix below is unit-tested on Linux with fake data.ip -4 route show default(fallback/proc/net/route, destination00000000).netstat -rn -f inet, every row whose destination isdefault. When a VPN is up macOS keeps the physical default as a second, interface-scopeddefaultrow, so both interfaces appear.utun*interface that carries a default route, or that is up with a non-link-local IPv4 address (Mullvad and other WireGuard/OpenVPN clients useutun; idle systemutuns only havefe80::addresses).Selection (the supported matrix, exhaustive).
--ifaceAand--ifaceB(defaultsgu0/backhaul0) exist: two panes, exactly today's behavior. Neither exists: the one default-route interface, one pane. Exactly one of the pair exists, or no/multiple default routes: exit with an error naming what was found and what is supported.gu0equivalent, label "VPN"), the non-VPN default-route interface is pane B. No VPN: one pane on the default-route interface. No default route, or more than one non-VPN default route: exit with an error.--ifaceA/--ifaceB/--labelA/--labelBflags keep working on both platforms; detection only fills in what was not given explicitly on macOS, and on Linux only when the named pair is absent.Per-platform command shapes, built by a pure function keyed on OS name (unit-testable for both): Linux
ping -I iface -c1 -W1; macOSping -b iface -c1 -W1000(-bbinds the interface,-Wis milliseconds).curl --interfaceis the same on both. TCP timing binds the source address today; on macOS aControlhook additionally setsIP_BOUND_IF(thin build-tagged file) so traffic leaves the chosen interface rather than the VPN default route. Linux dialing is untouched.UI.
Monitorholds a slice of one or two interfaces;Runstarts loops anduiLoopdraws panes per entry, so a single interface draws a single pane.NewMonitortakes the list; the README API example follows.Build tags removed from all files except the two thin per-platform files (
darwin/linux). CI builds both (GOOS=darwin go buildcross-compiles without a mac; this goes intoscript/cibuild).Tests. Table tests over fake interface/route data for every matrix row on both OS names; parser tests for
ip route,/proc/net/routeandnetstat -rnsample output; ping-argument tests. Not executable on this Linux host, to be disclosed in the PR body: realnetstatparsing against a live mac,IP_BOUND_IFdialing, and whether Mullvad's leak-protection firewall lets bound traffic out of the physical interface.README. Platform support, detection behavior, the matrix, updated API example.
Model: fable-5-1
Landed on
nextvia #4 (squash commit486a4739) after a passed independent review.Model: fable-5-1