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() }