Files
rtnetmon/internal/cli/root.go
T
sneak ef3c373a12 Detect interfaces per platform; add macOS and single-interface support (closes #2)
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 now
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 (source address on Linux, IP_BOUND_IF on macOS) are build-tagged. Ping
argument construction is a pure, OS-keyed function. NewMonitor now takes a list
of interfaces.

Model: opus-4-8
2026-09-21 06:53:39 +00:00

160 lines
4.1 KiB
Go

package cli
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
type Config struct {
IfaceA string
LabelA string
IfaceB string
LabelB string
Hosts []string
LogFile string
}
var (
cfg Config
rootCmd = &cobra.Command{
Use: "rtnetmon",
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: runMonitor,
}
)
// Default hosts for monitoring
var (
defaultReachabilityHosts = []string{
"8.8.8.8", "8.8.4.4", "google.com", "github.com",
"console.aws.amazon.com", "console.cloud.google.com",
"fast.com", "datavi.be", "captive.apple.com",
}
defaultPacketLossHosts = []string{
"github.com", "google.com", "8.8.8.8", "captive.apple.com", "62.115.190.68",
}
defaultTCPHosts = []string{
"datavi.be:443", "fast.com:443",
"console.aws.amazon.com:443", "console.cloud.google.com:443",
"captive.apple.com:80", "google.com:443",
}
)
func init() {
// Define flags
rootCmd.Flags().StringVar(&cfg.IfaceA, "ifaceA", "gu0", "primary network interface")
rootCmd.Flags().StringVar(&cfg.LabelA, "labelA", "gu LAN - VPN outbound", "label for ifaceA")
rootCmd.Flags().StringVar(&cfg.IfaceB, "ifaceB", "backhaul0", "secondary network interface")
rootCmd.Flags().StringVar(&cfg.LabelB, "labelB", "Cox cable direct", "label for ifaceB")
rootCmd.Flags().StringSliceVar(&cfg.Hosts, "hosts", defaultReachabilityHosts, "comma-separated reachability hosts")
rootCmd.Flags().StringVar(&cfg.LogFile, "logfile", "/tmp/rtnetmon.log", "path to log file")
// Bind flags to viper
_ = viper.BindPFlag("ifaceA", rootCmd.Flags().Lookup("ifaceA"))
_ = viper.BindPFlag("labelA", rootCmd.Flags().Lookup("labelA"))
_ = viper.BindPFlag("ifaceB", rootCmd.Flags().Lookup("ifaceB"))
_ = viper.BindPFlag("labelB", rootCmd.Flags().Lookup("labelB"))
_ = viper.BindPFlag("hosts", rootCmd.Flags().Lookup("hosts"))
_ = viper.BindPFlag("logfile", rootCmd.Flags().Lookup("logfile"))
}
func runMonitor(cmd *cobra.Command, _ []string) error {
monitor.Logf(cfg.LogFile, "Starting rtnetmon")
// Detect which interfaces to monitor for this platform.
specs, err := detectInterfaces(cmd)
if err != nil {
return err
}
// Create the monitor
mon := monitor.NewMonitor(specs, cfg.LogFile)
// Add reachability hosts
for _, host := range cfg.Hosts {
mon.AddReachabilityHost(host)
}
// Add packet loss hosts
for _, host := range defaultPacketLossHosts {
mon.AddPacketLossHost(host)
}
// Add TCP hosts
for _, host := range defaultTCPHosts {
mon.AddTCPHost(host)
}
// Create context with signal handling
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Handle signals
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
go func() {
s := <-sig
monitor.Logf(cfg.LogFile, "Signal received: %v", s)
cancel()
}()
// Run the monitor
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) ([]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 runs the root command
func Execute() error {
return rootCmd.Execute()
}