251 lines
6.0 KiB
Go
251 lines
6.0 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
|
|
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)
|
|
var buffer strings.Builder
|
|
inSIPMessage := false
|
|
var msgNetInfo *NetInfo
|
|
|
|
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)
|
|
// Latch the current network info for this message
|
|
if c.currentNetInfo != nil {
|
|
// Make a copy
|
|
info := *c.currentNetInfo
|
|
msgNetInfo = &info
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
if inSIPMessage {
|
|
buffer.WriteString(line)
|
|
buffer.WriteString("\r\n")
|
|
}
|
|
}
|
|
|
|
// Parse remaining buffer
|
|
if buffer.Len() > 0 {
|
|
c.parseAndEmit(buffer.String(), msgNetInfo)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|