package capture import ( "bufio" "context" "fmt" "io" "os/exec" "strings" "sync" "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...) 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) } // 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 } 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() // Detect start of SIP message if isSIPStart(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") { continue } 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 { if c.OnError != nil { c.OnError(err) } return } if packet != nil && c.OnPacket != nil { c.OnPacket(packet) } }