Detect interfaces per platform; add macOS and single-interface support (closes #2)
check / check (push) Failing after 0s

Linux runs exactly as before when both bridge interfaces exist. When they do
not, the lone default-route interface is monitored, and a single interface
draws a single UI pane instead of an empty second one.

macOS is newly supported: a running VPN tunnel (utun) is monitored as the
primary pane alongside the physical default-route interface, or the physical
interface alone when no VPN is up.

Detection lives in internal/netdetect: interface/route data types, pure
selection logic keyed on OS name, and route parsers, all unit-tested on Linux
for both platforms. Only the real route query and the per-platform TCP dial
binding are build-tagged. NewMonitor now takes a list of interfaces.

Not verified on a real mac: live netstat parsing, IP_BOUND_IF dialing, Mullvad leak protection.

Model: opus-4-8 (implementation); fable-5-1 (landing commit)
This commit was merged in pull request #4.
This commit is contained in:
2026-09-21 15:01:59 +02:00
parent 7dd4ac798d
commit 486a47397c
21 changed files with 1092 additions and 93 deletions
-2
View File
@@ -1,5 +1,3 @@
//go:build linux
package cli
import "github.com/spf13/cobra"
+52 -10
View File
@@ -1,5 +1,3 @@
//go:build linux
// Package cli wires command-line flags to the network monitor and runs it.
package cli
@@ -7,12 +5,14 @@ import (
"context"
"os"
"os/signal"
"runtime"
"syscall"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"git.eeqj.de/sneak/rtnetmon/internal/monitor"
"git.eeqj.de/sneak/rtnetmon/internal/netdetect"
)
// config holds the application configuration.
@@ -59,8 +59,8 @@ func newRootCmd() *cobra.Command {
Short: "Real-time network monitoring dashboard",
Long: `rtnetmon is a dual-interface network monitoring dashboard that provides
real-time visibility into network health, packet loss, and latency.`,
RunE: func(_ *cobra.Command, _ []string) error {
return runMonitor(cfg)
RunE: func(cmd *cobra.Command, _ []string) error {
return runMonitor(cmd, cfg)
},
}
registerFlags(cmd, cfg)
@@ -86,14 +86,17 @@ func registerFlags(cmd *cobra.Command, cfg *config) {
}
}
// runMonitor constructs and runs the monitor from cfg.
func runMonitor(cfg *config) error {
// runMonitor detects the interfaces to monitor, then constructs and runs the
// monitor from cfg.
func runMonitor(cmd *cobra.Command, cfg *config) error {
monitor.Logf(cfg.LogFile, "Starting rtnetmon")
monitor.Logf(cfg.LogFile, "Monitoring interfaces %s and %s",
cfg.IfaceA, cfg.IfaceB)
mon := monitor.NewMonitor(cfg.IfaceA, cfg.LabelA, cfg.IfaceB, cfg.LabelB,
cfg.LogFile)
specs, err := detectInterfaces(cmd, cfg)
if err != nil {
return err
}
mon := monitor.NewMonitor(specs, cfg.LogFile)
for _, host := range cfg.Hosts {
mon.AddReachabilityHost(host)
@@ -122,6 +125,45 @@ func runMonitor(cfg *config) error {
return mon.Run(ctx)
}
// detectInterfaces enumerates the host, reads its default routes, and applies
// the platform selection rules to decide which interfaces to monitor.
func detectInterfaces(
cmd *cobra.Command, cfg *config,
) ([]monitor.IfaceSpec, error) {
ifaces, err := netdetect.Interfaces()
if err != nil {
return nil, err
}
routes, err := netdetect.DefaultRoutes()
if err != nil {
return nil, err
}
flags := netdetect.Flags{
IfaceA: cfg.IfaceA,
LabelA: cfg.LabelA,
IfaceB: cfg.IfaceB,
LabelB: cfg.LabelB,
LabelASet: cmd.Flags().Changed("labelA"),
LabelBSet: cmd.Flags().Changed("labelB"),
}
panes, err := netdetect.Select(runtime.GOOS, ifaces, routes, flags)
if err != nil {
return nil, err
}
specs := make([]monitor.IfaceSpec, len(panes))
for i, p := range panes {
specs[i] = monitor.IfaceSpec{Name: p.Name, Label: p.Label}
}
monitor.Logf(cfg.LogFile, "Monitoring %d interface(s)", len(specs))
return specs, nil
}
// Execute builds the root command and runs it.
func Execute() error {
return newRootCmd().Execute()
-2
View File
@@ -1,5 +1,3 @@
//go:build linux
package cli_test
import (