package main import ( "context" "fmt" "os" "os/signal" "strings" "sync/atomic" "syscall" "github.com/spf13/cobra" "github.com/netsynth/netsynth/aggregate" "github.com/netsynth/netsynth/capture" "github.com/netsynth/netsynth/classify" ) var ( ifaceName string listIfaces bool verbose bool ) func main() { rootCmd := &cobra.Command{ Use: "netsynth", Short: "Sonify live network traffic into ambient audio", Long: "NetSynth captures network traffic, classifies it by protocol, and (in future phases) synthesizes an ambient MP3 soundscape.", RunE: run, } rootCmd.Flags().StringVarP(&ifaceName, "interface", "i", "", "Network interface to capture on (required unless --list-interfaces)") rootCmd.Flags().BoolVar(&listIfaces, "list-interfaces", false, "List available network interfaces and exit") rootCmd.Flags().BoolVar(&verbose, "verbose", false, "Print per-window protocol activity to stderr") if err := rootCmd.Execute(); err != nil { os.Exit(1) } } func run(cmd *cobra.Command, args []string) error { // --list-interfaces mode (CAPT-02) if listIfaces { return runListInterfaces() } // Require -i flag if ifaceName == "" { return fmt.Errorf("interface required: use -i or --list-interfaces to see available interfaces") } // Set up signal handling (Ctrl+C) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() // Stage 1: Capture (CAPT-01) fmt.Fprintf(os.Stderr, "Starting capture on %s... (press Ctrl+C to stop)\n", ifaceName) packets, droppedPtr, err := capture.StartCapture(ctx, ifaceName) if err != nil { return err // CAPT-04: permission error already has platform-specific message } // Stage 2: Classify (CLAS-01) classifier := classify.NewClassifier(classify.DefaultRules) classified := make(chan classify.ClassifiedPacket, 1024) go func() { defer close(classified) for pkt := range packets { classified <- classifier.Classify(pkt) } }() // Stage 3: Aggregate with optional verbose callback (CLAS-04) var onSnapshot func(classify.WindowSnapshot) if verbose { onSnapshot = func(snap classify.WindowSnapshot) { aggregate.PrintWindowLine(os.Stderr, snap) // CLAS-04: --verbose output } } snapshots := aggregate.Aggregate(ctx.Done(), classified, aggregate.DefaultWindowMs, onSnapshot) // Consume snapshots and accumulate totals totals := make(map[classify.TrafficClass]int64) for snap := range snapshots { aggregate.AccumulateTotals(totals, snap) } // Print exit summary (CLAS-03) dropped := atomic.LoadInt64(droppedPtr) if dropped > 0 { fmt.Fprintf(os.Stderr, "\nWarning: %d packets dropped (channel buffer full)\n", dropped) } aggregate.PrintSummary(os.Stderr, totals) return nil } func runListInterfaces() error { ifaces, err := capture.ListInterfaces() if err != nil { return fmt.Errorf("error listing interfaces: %w", err) } if len(ifaces) == 0 { fmt.Fprintln(os.Stderr, "No interfaces found. If interfaces are missing, run with sudo.") return nil } fmt.Fprintln(os.Stderr, "Available interfaces:") for _, iface := range ifaces { addrs, _ := iface.Addrs() addrStrs := make([]string, len(addrs)) for i, a := range addrs { addrStrs[i] = a.String() } fmt.Fprintf(os.Stderr, " %-15s flags=%s addrs=%s\n", iface.Name, iface.Flags, strings.Join(addrStrs, ", ")) } return nil }