package capture import ( "bufio" "context" "fmt" "io" "os/exec" "strings" "sync" "telephony-inspector/internal/logger" "telephony-inspector/internal/sip" ) // LocalCapturer handles SIP packet capture locally via tcpdump type LocalCapturer struct { cmd *exec.Cmd cancel context.CancelFunc running bool mu sync.Mutex // Callbacks OnPacket func(*sip.Packet) OnError func(error) } // NewLocalCapturer creates a new local capturer func NewLocalCapturer() *LocalCapturer { return &LocalCapturer{} } // Start begins capturing SIP traffic locally func (c *LocalCapturer) Start(iface string, port int) error { c.mu.Lock() if c.running { c.mu.Unlock() return fmt.Errorf("capture already running") } c.running = true c.mu.Unlock() ctx, cancel := context.WithCancel(context.Background()) c.cancel = cancel // Build tcpdump command // -l: line buffered // -A: print packet payload in ASCII // -s 0: capture full packets // -nn: don't resolve hostnames or port names args := []string{"-l", "-nn", "-A", "-s", "0", "-i", iface, "port", fmt.Sprintf("%d", port)} c.cmd = exec.CommandContext(ctx, "tcpdump", args...) logger.Info("Starting local capture: tcpdump %v", args) stdout, err := c.cmd.StdoutPipe() if err != nil { c.mu.Lock() c.running = false c.mu.Unlock() return fmt.Errorf("failed to get stdout: %w", err) } stderr, err := c.cmd.StderrPipe() if err != nil { c.mu.Lock() c.running = false c.mu.Unlock() return fmt.Errorf("failed to get stderr: %w", err) } if err := c.cmd.Start(); err != nil { c.mu.Lock() c.running = false c.mu.Unlock() return fmt.Errorf("failed to start tcpdump: %w", err) } logger.Info("Local capture started successfully") // Process stdout in goroutine go c.processStream(stdout) // Log stderr go c.processErrors(stderr) return nil } // Stop stops the capture func (c *LocalCapturer) Stop() { c.mu.Lock() defer c.mu.Unlock() if !c.running { return } logger.Info("Stopping local capture") c.running = false if c.cancel != nil { c.cancel() c.cancel = nil } if c.cmd != nil && c.cmd.Process != nil { c.cmd.Process.Kill() c.cmd.Wait() } } // IsRunning returns whether capture is active func (c *LocalCapturer) IsRunning() bool { c.mu.Lock() defer c.mu.Unlock() return c.running } // Close cleans up resources func (c *LocalCapturer) Close() error { c.Stop() return nil } func (c *LocalCapturer) processStream(r io.Reader) { scanner := bufio.NewScanner(r) var buffer strings.Builder inSIPMessage := false for scanner.Scan() { c.mu.Lock() running := c.running c.mu.Unlock() if !running { break } line := scanner.Text() // logger.Debug("Stdout: %s", line) // Commented out to reduce noise, enable if needed // Detect start of SIP message if isSIPStart(line) { logger.Debug("SIP Start detected: %s", line) // If we were building a message, parse it if buffer.Len() > 0 { c.parseAndEmit(buffer.String()) buffer.Reset() } inSIPMessage = true } if inSIPMessage { buffer.WriteString(line) buffer.WriteString("\r\n") } } // Parse remaining buffer if buffer.Len() > 0 { c.parseAndEmit(buffer.String()) } } func (c *LocalCapturer) processErrors(r io.Reader) { scanner := bufio.NewScanner(r) for scanner.Scan() { text := scanner.Text() // tcpdump prints "listening on..." to stderr, ignore it if strings.Contains(text, "listening on") { logger.Info("tcpdump: %s", text) continue } logger.Error("tcpdump stderr: %s", text) if c.OnError != nil { c.OnError(fmt.Errorf("tcpdump: %s", text)) } } } func (c *LocalCapturer) parseAndEmit(raw string) { packet, err := sip.Parse(raw) if err != nil { logger.Error("Error parsing SIP packet: %v", err) if c.OnError != nil { c.OnError(err) } return } if packet != nil { logger.Debug("Packet parsed: %s %s -> %s", packet.Method, packet.SourceIP, packet.DestIP) if c.OnPacket != nil { c.OnPacket(packet) } } }