Files

286 lines
7.0 KiB
Go

package capture
import (
"bufio"
"context"
"fmt"
"io"
"os/exec"
"strconv"
"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
currentNetInfo *NetInfo
// 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)
// Increase buffer size to handle large packets (default is 64KB)
const maxCapacity = 5 * 1024 * 1024 // 5MB
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, maxCapacity)
var buffer strings.Builder
inSIPMessage := false
var msgNetInfo *NetInfo
contentLength := -1
for scanner.Scan() {
c.mu.Lock()
running := c.running
c.mu.Unlock()
if !running {
break
}
line := scanner.Text()
// Check for tcpdump header
if netInfo := parseTcpdumpHeader(line); netInfo != nil {
c.currentNetInfo = netInfo
continue
}
// Detect start of SIP message
if idx := findSIPStart(line); idx != -1 {
logger.Debug("SIP Start detected: %s", line)
// Clean the line (remove prefix garbage)
line = line[idx:]
// If we were building a message, parse it with its OWN net info (which was latched previously)
// Note: This edge case (buffer > 0 but new start) means previous message ended implicitly.
// But wait, the msgNetInfo we just latched is for the NEW message.
// The OLD message should have already been emitted or we are in a weird state.
// Use the PREVIOUS msgNetInfo for the existing buffer if any.
// Actually, single buffer logic is simple: emit what we have.
if buffer.Len() > 0 {
// We need to pass the net info that belongs to the buffered content.
// But we just overwrote msgNetInfo.
// Realistically, we should emit before latching new info.
// But tcpdump header comes BEFORE the message.
// So c.currentNetInfo is already the NEW info.
// And the buffer contains the OLD message.
// So when we started the OLD message, we latched OLD info.
// We should persist that OLD info until emit.
// This implies we need `pendingNetInfo` vs `currentNetInfo`.
// Simplified approach: msgNetInfo stores the info for the message currently being built in buffer.
// When we start a NEW message, the buffer contains the PREVIOUS message.
// So we emit the buffer with the OLD msgNetInfo.
// THEN we start the new message and update msgNetInfo to the NEW c.currentNetInfo.
c.parseAndEmit(buffer.String(), msgNetInfo)
buffer.Reset()
}
// NOW update msgNetInfo for the new message
if c.currentNetInfo != nil {
info := *c.currentNetInfo
msgNetInfo = &info
} else {
msgNetInfo = nil
}
inSIPMessage = true
contentLength = -1 // Reset for new message
}
if inSIPMessage {
buffer.WriteString(line)
buffer.WriteString("\r\n")
// Check for Content-Length
lowerLine := strings.ToLower(line)
if strings.HasPrefix(lowerLine, "content-length:") || strings.HasPrefix(lowerLine, "l:") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
val := strings.TrimSpace(parts[1])
if val != "" {
if cl, err := strconv.Atoi(val); err == nil {
contentLength = cl
}
}
}
}
// Check for end of headers (empty line)
if line == "" {
// If Content-Length is 0 (or not found, treating as 0 for safety in this context usually 0 for BYE/ACK)
// Flush immediately
if contentLength <= 0 {
c.parseAndEmit(buffer.String(), msgNetInfo)
buffer.Reset()
inSIPMessage = false
contentLength = -1
}
}
}
}
// Parse remaining buffer
if buffer.Len() > 0 {
c.parseAndEmit(buffer.String(), msgNetInfo)
}
if err := scanner.Err(); err != nil {
logger.Error("Scanner error: %v", err)
if c.OnError != nil {
c.OnError(fmt.Errorf("scanner error: %w", err))
}
}
}
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, netInfo *NetInfo) {
packet, err := sip.Parse(raw)
if err != nil {
// Suppress verbose error logging for partial packets unless debug
// logger.Error("Error parsing SIP packet: %v", err)
if c.OnError != nil {
c.OnError(err)
}
return
}
if packet != nil {
// Attach network info if available
if netInfo != nil {
packet.Timestamp = netInfo.Timestamp
packet.SourceIP = netInfo.SourceIP
packet.SourcePort = netInfo.SourcePort
packet.DestIP = netInfo.DestIP
packet.DestPort = netInfo.DestPort
}
logger.Debug("Packet parsed: %s %s -> %s", packet.Method, packet.SourceIP, packet.DestIP)
if c.OnPacket != nil {
c.OnPacket(packet)
}
}
}