Files
telephony-inspector/internal/capture/local.go
2026-01-19 15:28:16 +01:00

196 lines
3.9 KiB
Go

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