//go:build linux // Package cli wires command-line flags to the network monitor and runs it. package cli import ( "context" "os" "os/signal" "syscall" "github.com/spf13/cobra" "github.com/spf13/viper" "git.eeqj.de/sneak/rtnetmon/internal/monitor" ) // 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(_ *cobra.Command, _ []string) error { return runMonitor(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 constructs and runs the monitor from cfg. func runMonitor(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) 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) } // Execute builds the root command and runs it. func Execute() error { return newRootCmd().Execute() }