// Package cli wires command-line flags to the network monitor and runs it. 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 } // defaultReachabilityHosts is the default reachability host list. func defaultReachabilityHosts() []string { return []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 is the default packet-loss host list. func defaultPacketLossHosts() []string { return []string{ "github.com", "google.com", "8.8.8.8", "captive.apple.com", "62.115.190.68", } } // defaultTCPHosts is the default TCP connect host:port list. func defaultTCPHosts() []string { return []string{ "datavi.be:443", "fast.com:443", "console.aws.amazon.com:443", "console.cloud.google.com:443", "captive.apple.com:80", "google.com:443", } } // newRootCmd builds the cobra root command with its flags bound. func newRootCmd() *cobra.Command { cfg := &config{} cmd := &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: func(cmd *cobra.Command, _ []string) error { return runMonitor(cmd, cfg) }, } registerFlags(cmd, cfg) return cmd } // registerFlags defines and viper-binds the command's flags. func registerFlags(cmd *cobra.Command, cfg *config) { f := cmd.Flags() f.StringVar(&cfg.IfaceA, "ifaceA", "gu0", "primary network interface") f.StringVar(&cfg.LabelA, "labelA", "gu LAN - VPN outbound", "label for ifaceA") f.StringVar(&cfg.IfaceB, "ifaceB", "backhaul0", "secondary network interface") f.StringVar(&cfg.LabelB, "labelB", "Cox cable direct", "label for ifaceB") f.StringSliceVar(&cfg.Hosts, "hosts", defaultReachabilityHosts(), "comma-separated reachability hosts") f.StringVar(&cfg.LogFile, "logfile", "/tmp/rtnetmon.log", "path to log file") for _, name := range []string{ "ifaceA", "labelA", "ifaceB", "labelB", "hosts", "logfile", } { _ = viper.BindPFlag(name, f.Lookup(name)) } } // 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") specs, err := detectInterfaces(cmd, cfg) if err != nil { return err } mon := monitor.NewMonitor(specs, cfg.LogFile) for _, host := range cfg.Hosts { mon.AddReachabilityHost(host) } for _, host := range defaultPacketLossHosts() { mon.AddPacketLossHost(host) } for _, host := range defaultTCPHosts() { mon.AddTCPHost(host) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() 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() }() 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() }