Compare commits
30 Commits
854cf926ed
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
713903c463 | ||
|
|
c052cc72fb | ||
|
|
1fb7d447a4 | ||
|
|
65a7cd10f6 | ||
|
|
b7a5bede87 | ||
|
|
a5ba4d6c11 | ||
|
|
d234bfb427 | ||
|
|
6abea047d1 | ||
|
|
b2ff043923 | ||
|
|
bdf6dfc1e1 | ||
|
|
d3b31f02c8 | ||
|
|
3b73d30e27 | ||
|
|
1df4a48769 | ||
|
|
23349e2039 | ||
|
|
f5a2730bc3 | ||
|
|
a02b7533d5 | ||
|
|
daefefb5aa | ||
|
|
c4794ad787 | ||
|
|
0566cb9268 | ||
|
|
42d27c0647 | ||
|
|
751bf380d7 | ||
|
|
1344b730af | ||
|
|
ee0e827b78 | ||
|
|
ee7201ce57 | ||
|
|
0c61db8f5b | ||
|
|
cd9b0db44d | ||
|
|
f0911737f4 | ||
|
|
2d99d8ddc4 | ||
|
|
4fa44fb9c7 | ||
|
|
b8984f25a0 |
223
export_20260120_222728_e06cb346194a4f9295d3a325b185912f.log
Normal file
223
export_20260120_222728_e06cb346194a4f9295d3a325b185912f.log
Normal file
@@ -0,0 +1,223 @@
|
||||
Call Detail Export
|
||||
==================
|
||||
Date: Tue, 20 Jan 2026 22:27:28 CET
|
||||
Call-ID: e06cb346194a4f9295d3a325b185912f
|
||||
From: <sip:1001@192.168.0.162>;tag=d715109fcadd492b8fa1ce009ae2c095
|
||||
To: <sip:123456@192.168.0.162>
|
||||
State: Connected
|
||||
Duration: 2.81879s
|
||||
Packets: 8
|
||||
|
||||
Network Layer:
|
||||
Source: Windows (Carrier) (192.168.0.164:51416)
|
||||
Destination: Asterisk (PBX) (192.168.0.162:5060)
|
||||
|
||||
Network Context:
|
||||
- Windows (Carrier): 192.168.0.164
|
||||
- Asterisk (PBX): 192.168.0.162
|
||||
|
||||
Transaction Flow:
|
||||
-----------------
|
||||
01. [14:24:30.477] Windows (Carrier) (192.168.0.164) -> Asterisk (PBX) (192.168.0.162) (INVITE line 1153)
|
||||
02. [14:24:30.478] Asterisk (PBX) (192.168.0.162) <- Windows (Carrier) (192.168.0.164) (Trying line 382)
|
||||
03. [14:24:30.479] Asterisk (PBX) (192.168.0.162) <- Windows (Carrier) (192.168.0.164) (OK line 1075)
|
||||
04. [14:24:30.479] Windows (Carrier) (192.168.0.164) -> Asterisk (PBX) (192.168.0.162) (ACK line 371)
|
||||
05. [14:24:30.479] Windows (Carrier) (192.168.0.164) -> Asterisk (PBX) (192.168.0.162) (UPDATE line 870)
|
||||
06. [14:24:30.480] Asterisk (PBX) (192.168.0.162) <- Windows (Carrier) (192.168.0.164) (OK line 939)
|
||||
07. [14:24:33.296] Windows (Carrier) (192.168.0.164) -> Asterisk (PBX) (192.168.0.162) (BYE line 400)
|
||||
08. [14:24:33.296] Asterisk (PBX) (192.168.0.162) <- Windows (Carrier) (192.168.0.164) (OK line 416)
|
||||
|
||||
|
||||
Packet Details:
|
||||
---------------
|
||||
|
||||
--- Packet 1 [14:24:30.477] ---
|
||||
Windows (Carrier) (192.168.0.164) -> Asterisk (PBX) (192.168.0.162)
|
||||
INVITE sip:123456@192.168.0.162 SIP/2.0
|
||||
Via: SIP/2.0/UDP 192.168.0.164:51416;rport;branch=z9hG4bKPjc988f0aca12d47aa900f472ea93fc0d6
|
||||
Max-Forwards: 70
|
||||
From: <sip:1001@192.168.0.162>;tag=d715109fcadd492b8fa1ce009ae2c095
|
||||
To: <sip:123456@192.168.0.162>
|
||||
Contact: <sip:1001@192.168.0.164:51416;ob>
|
||||
Call-ID: e06cb346194a4f9295d3a325b185912f
|
||||
CSeq: 12831 INVITE
|
||||
Allow: PRACK, INVITE, ACK, BYE, CANCEL, UPDATE, INFO, SUBSCRIBE, NOTIFY, REFER, MESSAGE, OPTIONS
|
||||
Supported: replaces, 100rel, timer, norefersub
|
||||
Session-Expires: 1800
|
||||
Min-SE: 90
|
||||
User-Agent: MicroSIP/3.21.6
|
||||
Content-Type: application/sdp
|
||||
Content-Length: 527
|
||||
|
||||
v=0
|
||||
o=- 3977821470 3977821470 IN IP4 192.168.0.164
|
||||
s=pjmedia
|
||||
b=AS:84
|
||||
t=0 0
|
||||
a=X-nat:0
|
||||
m=audio 4040 RTP/AVP 0 8 96 101 102
|
||||
c=IN IP4 192.168.0.164
|
||||
b=TIAS:64000
|
||||
a=rtcp:4041 IN IP4 192.168.0.164
|
||||
a=sendrecv
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtpmap:96 opus/48000/2
|
||||
a=fmtp:96 maxplaybackrate=24000;sprop-maxcapturerate=24000;maxaveragebitrate=64000;useinbandfec=1
|
||||
a=rtpmap:101 telephone-event/8000
|
||||
a=fmtp:101 0-16
|
||||
a=rtpmap:102 telephone-event/48000
|
||||
a=fmtp:102 0-16
|
||||
a=ssrc:825569477 cname:1f442b6f1a4c18c5
|
||||
|
||||
|
||||
--- Packet 2 [14:24:30.478] ---
|
||||
Asterisk (PBX) (192.168.0.162) -> Windows (Carrier) (192.168.0.164)
|
||||
SIP/2.0 100 Trying
|
||||
Via: SIP/2.0/UDP 192.168.0.164:51416;rport=51416;received=192.168.0.164;branch=z9hG4bKPjc988f0aca12d47aa900f472ea93fc0d6
|
||||
Call-ID: e06cb346194a4f9295d3a325b185912f
|
||||
From: <sip:1001@192.168.0.162>;tag=d715109fcadd492b8fa1ce009ae2c095
|
||||
To: <sip:123456@192.168.0.162>
|
||||
CSeq: 12831 INVITE
|
||||
Server: Asterisk PBX 18.10.0~dfsg+~cs6.10.40431411-2
|
||||
Content-Length: 0
|
||||
|
||||
|
||||
|
||||
--- Packet 3 [14:24:30.479] ---
|
||||
Asterisk (PBX) (192.168.0.162) -> Windows (Carrier) (192.168.0.164)
|
||||
SIP/2.0 200 OK
|
||||
Via: SIP/2.0/UDP 192.168.0.164:51416;rport=51416;received=192.168.0.164;branch=z9hG4bKPjc988f0aca12d47aa900f472ea93fc0d6
|
||||
Call-ID: e06cb346194a4f9295d3a325b185912f
|
||||
From: <sip:1001@192.168.0.162>;tag=d715109fcadd492b8fa1ce009ae2c095
|
||||
To: <sip:123456@192.168.0.162>;tag=643ebe4c-3476-4110-98e6-1b14f62996e1
|
||||
CSeq: 12831 INVITE
|
||||
Server: Asterisk PBX 18.10.0~dfsg+~cs6.10.40431411-2
|
||||
Contact: <sip:192.168.0.162:5060>
|
||||
Allow: OPTIONS, REGISTER, SUBSCRIBE, NOTIFY, PUBLISH, INVITE, ACK, BYE, CANCEL, UPDATE, PRACK, MESSAGE, REFER
|
||||
Supported: 100rel, timer, replaces, norefersub
|
||||
Session-Expires: 1800;refresher=uac
|
||||
Require: timer
|
||||
Content-Type: application/sdp
|
||||
Content-Length: 375
|
||||
|
||||
v=0
|
||||
o=- 3977821470 3977821472 IN IP4 192.168.0.162
|
||||
s=Asterisk
|
||||
c=IN IP4 192.168.0.162
|
||||
t=0 0
|
||||
m=audio 12862 RTP/AVP 0 8 96 101
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtpmap:96 opus/48000/2
|
||||
a=fmtp:96 maxplaybackrate=24000;sprop-maxcapturerate=24000;maxaveragebitrate=64000
|
||||
a=rtpmap:101 telephone-event/8000
|
||||
a=fmtp:101 0-16
|
||||
a=ptime:20
|
||||
a=maxptime:60
|
||||
a=sendrecv
|
||||
|
||||
|
||||
--- Packet 4 [14:24:30.479] ---
|
||||
Windows (Carrier) (192.168.0.164) -> Asterisk (PBX) (192.168.0.162)
|
||||
ACK sip:192.168.0.162:5060 SIP/2.0
|
||||
Via: SIP/2.0/UDP 192.168.0.164:51416;rport;branch=z9hG4bKPjf0cf112e8b5d4759a3417bc63cd5481e
|
||||
Max-Forwards: 70
|
||||
From: <sip:1001@192.168.0.162>;tag=d715109fcadd492b8fa1ce009ae2c095
|
||||
To: <sip:123456@192.168.0.162>;tag=643ebe4c-3476-4110-98e6-1b14f62996e1
|
||||
Call-ID: e06cb346194a4f9295d3a325b185912f
|
||||
CSeq: 12831 ACK
|
||||
Content-Length: 0
|
||||
|
||||
|
||||
|
||||
--- Packet 5 [14:24:30.479] ---
|
||||
Windows (Carrier) (192.168.0.164) -> Asterisk (PBX) (192.168.0.162)
|
||||
UPDATE sip:192.168.0.162:5060 SIP/2.0
|
||||
Via: SIP/2.0/UDP 192.168.0.164:51416;rport;branch=z9hG4bKPj1453a8f281f04023b1a9d246fb587ca3
|
||||
Max-Forwards: 70
|
||||
From: <sip:1001@192.168.0.162>;tag=d715109fcadd492b8fa1ce009ae2c095
|
||||
To: <sip:123456@192.168.0.162>;tag=643ebe4c-3476-4110-98e6-1b14f62996e1
|
||||
Contact: <sip:1001@192.168.0.164:51416;ob>
|
||||
Call-ID: e06cb346194a4f9295d3a325b185912f
|
||||
CSeq: 12832 UPDATE
|
||||
Supported: replaces, 100rel, timer, norefersub
|
||||
Session-Expires: 1800;refresher=uac
|
||||
Min-SE: 90
|
||||
Content-Type: application/sdp
|
||||
Content-Length: 318
|
||||
|
||||
v=0
|
||||
o=- 3977821470 3977821471 IN IP4 192.168.0.164
|
||||
s=pjmedia
|
||||
b=AS:84
|
||||
t=0 0
|
||||
a=X-nat:0
|
||||
m=audio 4040 RTP/AVP 0 101
|
||||
c=IN IP4 192.168.0.164
|
||||
b=TIAS:64000
|
||||
a=rtcp:4041 IN IP4 192.168.0.164
|
||||
a=ssrc:825569477 cname:1f442b6f1a4c18c5
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:101 telephone-event/8000
|
||||
a=fmtp:101 0-16
|
||||
a=sendrecv
|
||||
|
||||
|
||||
--- Packet 6 [14:24:30.480] ---
|
||||
Asterisk (PBX) (192.168.0.162) -> Windows (Carrier) (192.168.0.164)
|
||||
SIP/2.0 200 OK
|
||||
Via: SIP/2.0/UDP 192.168.0.164:51416;rport=51416;received=192.168.0.164;branch=z9hG4bKPj1453a8f281f04023b1a9d246fb587ca3
|
||||
Call-ID: e06cb346194a4f9295d3a325b185912f
|
||||
From: <sip:1001@192.168.0.162>;tag=d715109fcadd492b8fa1ce009ae2c095
|
||||
To: <sip:123456@192.168.0.162>;tag=643ebe4c-3476-4110-98e6-1b14f62996e1
|
||||
CSeq: 12832 UPDATE
|
||||
Session-Expires: 1800;refresher=uac
|
||||
Require: timer
|
||||
Contact: <sip:192.168.0.162:5060>
|
||||
Allow: OPTIONS, REGISTER, SUBSCRIBE, NOTIFY, PUBLISH, INVITE, ACK, BYE, CANCEL, UPDATE, PRACK, MESSAGE, REFER
|
||||
Supported: 100rel, timer, replaces, norefersub
|
||||
Server: Asterisk PBX 18.10.0~dfsg+~cs6.10.40431411-2
|
||||
Content-Type: application/sdp
|
||||
Content-Length: 239
|
||||
|
||||
v=0
|
||||
o=- 3977821470 3977821473 IN IP4 192.168.0.162
|
||||
s=Asterisk
|
||||
c=IN IP4 192.168.0.162
|
||||
t=0 0
|
||||
m=audio 12862 RTP/AVP 0 101
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:101 telephone-event/8000
|
||||
a=fmtp:101 0-16
|
||||
a=ptime:20
|
||||
a=maxptime:150
|
||||
a=sendrecv
|
||||
|
||||
|
||||
--- Packet 7 [14:24:33.296] ---
|
||||
Windows (Carrier) (192.168.0.164) -> Asterisk (PBX) (192.168.0.162)
|
||||
BYE sip:192.168.0.162:5060 SIP/2.0
|
||||
Via: SIP/2.0/UDP 192.168.0.164:51416;rport;branch=z9hG4bKPj15f5d8bca6884d6794f27c43437f1201
|
||||
Max-Forwards: 70
|
||||
From: <sip:1001@192.168.0.162>;tag=d715109fcadd492b8fa1ce009ae2c095
|
||||
To: <sip:123456@192.168.0.162>;tag=643ebe4c-3476-4110-98e6-1b14f62996e1
|
||||
Call-ID: e06cb346194a4f9295d3a325b185912f
|
||||
CSeq: 12833 BYE
|
||||
User-Agent: MicroSIP/3.21.6
|
||||
Content-Length: 0
|
||||
|
||||
|
||||
|
||||
--- Packet 8 [14:24:33.296] ---
|
||||
Asterisk (PBX) (192.168.0.162) -> Windows (Carrier) (192.168.0.164)
|
||||
SIP/2.0 200 OK
|
||||
Via: SIP/2.0/UDP 192.168.0.164:51416;rport=51416;received=192.168.0.164;branch=z9hG4bKPj15f5d8bca6884d6794f27c43437f1201
|
||||
Call-ID: e06cb346194a4f9295d3a325b185912f
|
||||
From: <sip:1001@192.168.0.162>;tag=d715109fcadd492b8fa1ce009ae2c095
|
||||
To: <sip:123456@192.168.0.162>;tag=643ebe4c-3476-4110-98e6-1b14f62996e1
|
||||
CSeq: 12833 BYE
|
||||
Server: Asterisk PBX 18.10.0~dfsg+~cs6.10.40431411-2
|
||||
Content-Length: 0
|
||||
|
||||
|
||||
BIN
inspector.exe
BIN
inspector.exe
Binary file not shown.
@@ -4,8 +4,10 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"telephony-inspector/internal/sip"
|
||||
internalSSH "telephony-inspector/internal/ssh"
|
||||
@@ -13,10 +15,11 @@ import (
|
||||
|
||||
// Capturer handles SIP packet capture via SSH
|
||||
type Capturer struct {
|
||||
sshClient *internalSSH.Client
|
||||
cleanup func() error
|
||||
running bool
|
||||
mu sync.Mutex
|
||||
sshClient *internalSSH.Client
|
||||
cleanup func() error
|
||||
running bool
|
||||
mu sync.Mutex
|
||||
currentNetInfo *NetInfo
|
||||
|
||||
// Callbacks
|
||||
OnPacket func(*sip.Packet)
|
||||
@@ -55,7 +58,8 @@ func (c *Capturer) Start(iface string, port int) error {
|
||||
// -l: line buffered for real-time output
|
||||
// -A: print packet payload in ASCII
|
||||
// -s 0: capture full packets
|
||||
cmd := fmt.Sprintf("sudo tcpdump -l -A -s 0 -i %s port %d 2>/dev/null", iface, port)
|
||||
// -nn: don't resolve hostnames or port names
|
||||
cmd := fmt.Sprintf("sudo tcpdump -l -nn -A -s 0 -i %s port %d 2>/dev/null", iface, port)
|
||||
|
||||
stdout, stderr, cleanup, err := c.sshClient.StartCommand(cmd)
|
||||
if err != nil {
|
||||
@@ -100,8 +104,15 @@ func (c *Capturer) IsRunning() bool {
|
||||
|
||||
func (c *Capturer) processStream(r io.Reader) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
// Increase buffer size to handle large packets
|
||||
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()
|
||||
@@ -113,28 +124,77 @@ func (c *Capturer) processStream(r io.Reader) {
|
||||
|
||||
line := scanner.Text()
|
||||
|
||||
// Check for tcpdump header
|
||||
if netInfo := parseTcpdumpHeader(line); netInfo != nil {
|
||||
c.currentNetInfo = netInfo
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect start of SIP message
|
||||
if isSIPStart(line) {
|
||||
// If we were building a message, parse it
|
||||
if idx := findSIPStart(line); idx != -1 {
|
||||
|
||||
// Clean the line (remove prefix garbage)
|
||||
line = line[idx:]
|
||||
|
||||
// If we were building a message, parse it with its OWN net info
|
||||
if buffer.Len() > 0 {
|
||||
c.parseAndEmit(buffer.String())
|
||||
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
|
||||
}
|
||||
|
||||
if inSIPMessage {
|
||||
buffer.WriteString(line)
|
||||
buffer.WriteString("\r\n")
|
||||
|
||||
// Detect end of SIP message (double CRLF or content complete)
|
||||
// This is simplified - real implementation would track Content-Length
|
||||
// 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)
|
||||
// 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())
|
||||
c.parseAndEmit(buffer.String(), msgNetInfo)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if c.OnError != nil {
|
||||
c.OnError(fmt.Errorf("scanner error: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +207,7 @@ func (c *Capturer) processErrors(r io.Reader) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Capturer) parseAndEmit(raw string) {
|
||||
func (c *Capturer) parseAndEmit(raw string, netInfo *NetInfo) {
|
||||
packet, err := sip.Parse(raw)
|
||||
if err != nil {
|
||||
if c.OnError != nil {
|
||||
@@ -155,26 +215,140 @@ func (c *Capturer) parseAndEmit(raw string) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if packet != nil && c.OnPacket != nil {
|
||||
c.OnPacket(packet)
|
||||
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
|
||||
}
|
||||
|
||||
if c.OnPacket != nil {
|
||||
c.OnPacket(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isSIPStart checks if a line looks like the start of a SIP message
|
||||
func isSIPStart(line string) bool {
|
||||
// findSIPStart returns the index of the start of a SIP message, or -1 if not found
|
||||
func findSIPStart(line string) int {
|
||||
sipMethods := []string{"INVITE", "ACK", "BYE", "CANCEL", "REGISTER", "OPTIONS", "PRACK", "SUBSCRIBE", "NOTIFY", "PUBLISH", "INFO", "REFER", "MESSAGE", "UPDATE"}
|
||||
|
||||
// Response
|
||||
if strings.HasPrefix(line, "SIP/2.0") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Request
|
||||
for _, m := range sipMethods {
|
||||
if strings.HasPrefix(line, m+" ") {
|
||||
return true
|
||||
// Check for Response "SIP/2.0"
|
||||
if idx := strings.Index(line, "SIP/2.0 "); idx != -1 {
|
||||
// Verify it's not part of a header like Via or Record-Route
|
||||
// We look at what comes before. If it's the start of the line or preceded by garbage (nulls etc), it's likely a start.
|
||||
// If it is preceded by "Via: " or "Route: ", it is a header.
|
||||
prefix := strings.ToUpper(line[:idx])
|
||||
if !strings.HasSuffix(prefix, "VIA: ") &&
|
||||
!strings.HasSuffix(prefix, "ROUTE: ") &&
|
||||
!strings.HasSuffix(prefix, "VIA:") { // Handle varying spacing
|
||||
return idx
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
// Check for Request "METHOD "
|
||||
for _, m := range sipMethods {
|
||||
target := m + " "
|
||||
if idx := strings.Index(line, target); idx != -1 {
|
||||
// Verify it's not CSeq, Allow, Rack, etc.
|
||||
prefix := strings.ToUpper(line[:idx])
|
||||
if !strings.HasSuffix(prefix, "CSEQ: ") &&
|
||||
!strings.HasSuffix(prefix, "ALLOW: ") &&
|
||||
!strings.HasSuffix(prefix, "RACK: ") &&
|
||||
!strings.HasSuffix(prefix, "SUPPORTED: ") {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// NetInfo stores network layer information from tcpdump headers
|
||||
type NetInfo struct {
|
||||
Timestamp time.Time
|
||||
SourceIP string
|
||||
SourcePort int
|
||||
DestIP string
|
||||
DestPort int
|
||||
}
|
||||
|
||||
func parseTcpdumpHeader(line string) *NetInfo {
|
||||
// Robust parsing
|
||||
// Look for " IP " or " IP6 "
|
||||
parts := strings.Fields(line)
|
||||
// Need at least: Time IP Src > Dst:
|
||||
if len(parts) < 5 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find the direction arrow ">"
|
||||
arrowIdx := -1
|
||||
for i, p := range parts {
|
||||
if p == ">" {
|
||||
arrowIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if arrowIdx == -1 || arrowIdx < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify "IP" or "IP6" exists before the source (usually parts[1] or parts[2])
|
||||
// Example: 15:35... IP src > dst: ...
|
||||
// Example: 15:35... IP6 src > dst: ...
|
||||
// Example (verbose): 15:35... IP (tos 0x0...) src > dst: ...
|
||||
// We'll trust the arrow for now, but double check IPs.
|
||||
|
||||
// Assume SRC is before arrow, DST is after
|
||||
srcStr := parts[arrowIdx-1]
|
||||
dstStr := parts[arrowIdx+1]
|
||||
|
||||
// Find timestamp (usually first field)
|
||||
now := time.Now()
|
||||
t, err := time.Parse("15:04:05.000000", parts[0])
|
||||
if err == nil {
|
||||
t = time.Date(now.Year(), now.Month(), now.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), now.Location())
|
||||
} else {
|
||||
// Try without microseconds
|
||||
t, err = time.Parse("15:04:05", parts[0])
|
||||
if err == nil {
|
||||
t = time.Date(now.Year(), now.Month(), now.Day(), t.Hour(), t.Minute(), t.Second(), 0, now.Location())
|
||||
} else {
|
||||
t = now
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to extract IP and Port
|
||||
parseIPPort := func(s string) (string, int) {
|
||||
lastDot := strings.LastIndex(s, ".")
|
||||
if lastDot == -1 {
|
||||
// IPv6 might use different separation or just be IP
|
||||
return s, 0
|
||||
}
|
||||
ip := s[:lastDot]
|
||||
portStr := s[lastDot+1:]
|
||||
// Remove trailing colon if present (dest)
|
||||
portStr = strings.TrimSuffix(portStr, ":")
|
||||
// Remove trailing comma if present
|
||||
portStr = strings.TrimSuffix(portStr, ",")
|
||||
|
||||
var port int
|
||||
fmt.Sscanf(portStr, "%d", &port)
|
||||
return ip, port
|
||||
}
|
||||
|
||||
srcIP, srcPort := parseIPPort(srcStr)
|
||||
dstIP, dstPort := parseIPPort(dstStr)
|
||||
|
||||
return &NetInfo{
|
||||
Timestamp: t,
|
||||
SourceIP: srcIP,
|
||||
SourcePort: srcPort,
|
||||
DestIP: dstIP,
|
||||
DestPort: dstPort,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,21 @@ import (
|
||||
"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
|
||||
cmd *exec.Cmd
|
||||
cancel context.CancelFunc
|
||||
running bool
|
||||
mu sync.Mutex
|
||||
currentNetInfo *NetInfo
|
||||
|
||||
// Callbacks
|
||||
OnPacket func(*sip.Packet)
|
||||
@@ -46,9 +49,12 @@ func (c *LocalCapturer) Start(iface string, port int) error {
|
||||
// -l: line buffered
|
||||
// -A: print packet payload in ASCII
|
||||
// -s 0: capture full packets
|
||||
args := []string{"-l", "-A", "-s", "0", "-i", iface, "port", fmt.Sprintf("%d", port)}
|
||||
// -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()
|
||||
@@ -72,6 +78,8 @@ func (c *LocalCapturer) Start(iface string, port int) error {
|
||||
return fmt.Errorf("failed to start tcpdump: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Local capture started successfully")
|
||||
|
||||
// Process stdout in goroutine
|
||||
go c.processStream(stdout)
|
||||
|
||||
@@ -89,6 +97,7 @@ func (c *LocalCapturer) Stop() {
|
||||
if !c.running {
|
||||
return
|
||||
}
|
||||
logger.Info("Stopping local capture")
|
||||
c.running = false
|
||||
|
||||
if c.cancel != nil {
|
||||
@@ -117,8 +126,15 @@ func (c *LocalCapturer) Close() error {
|
||||
|
||||
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()
|
||||
@@ -130,25 +146,99 @@ func (c *LocalCapturer) processStream(r io.Reader) {
|
||||
|
||||
line := scanner.Text()
|
||||
|
||||
// Check for tcpdump header
|
||||
if netInfo := parseTcpdumpHeader(line); netInfo != nil {
|
||||
c.currentNetInfo = netInfo
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect start of SIP message
|
||||
if isSIPStart(line) {
|
||||
// If we were building a message, parse it
|
||||
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 {
|
||||
c.parseAndEmit(buffer.String())
|
||||
// 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())
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,23 +248,38 @@ func (c *LocalCapturer) processErrors(r io.Reader) {
|
||||
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) {
|
||||
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 && c.OnPacket != nil {
|
||||
c.OnPacket(packet)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,27 @@ func (s *CallFlowStore) GetRecentFlows(n int) []*CallFlow {
|
||||
return flows
|
||||
}
|
||||
|
||||
// GetSortedFlows returns all call flows sorted by StartTime (oldest first)
|
||||
func (s *CallFlowStore) GetSortedFlows() []*CallFlow {
|
||||
flows := s.GetAllFlows()
|
||||
|
||||
// Sort by start time ascending (oldest first), then by CallID for stable order
|
||||
for i := 0; i < len(flows)-1; i++ {
|
||||
for j := i + 1; j < len(flows); j++ {
|
||||
// Compare by StartTime first
|
||||
if flows[i].StartTime.After(flows[j].StartTime) {
|
||||
flows[i], flows[j] = flows[j], flows[i]
|
||||
} else if flows[i].StartTime.Equal(flows[j].StartTime) {
|
||||
// If same time, sort by CallID for stable order
|
||||
if flows[i].CallID > flows[j].CallID {
|
||||
flows[i], flows[j] = flows[j], flows[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return flows
|
||||
}
|
||||
|
||||
// Count returns the number of call flows
|
||||
func (s *CallFlowStore) Count() int {
|
||||
s.mu.RLock()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user