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"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"telephony-inspector/internal/sip"
|
"telephony-inspector/internal/sip"
|
||||||
internalSSH "telephony-inspector/internal/ssh"
|
internalSSH "telephony-inspector/internal/ssh"
|
||||||
@@ -17,6 +19,7 @@ type Capturer struct {
|
|||||||
cleanup func() error
|
cleanup func() error
|
||||||
running bool
|
running bool
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
currentNetInfo *NetInfo
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
OnPacket func(*sip.Packet)
|
OnPacket func(*sip.Packet)
|
||||||
@@ -55,7 +58,8 @@ func (c *Capturer) Start(iface string, port int) error {
|
|||||||
// -l: line buffered for real-time output
|
// -l: line buffered for real-time output
|
||||||
// -A: print packet payload in ASCII
|
// -A: print packet payload in ASCII
|
||||||
// -s 0: capture full packets
|
// -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)
|
stdout, stderr, cleanup, err := c.sshClient.StartCommand(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -100,8 +104,15 @@ func (c *Capturer) IsRunning() bool {
|
|||||||
|
|
||||||
func (c *Capturer) processStream(r io.Reader) {
|
func (c *Capturer) processStream(r io.Reader) {
|
||||||
scanner := bufio.NewScanner(r)
|
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
|
var buffer strings.Builder
|
||||||
inSIPMessage := false
|
inSIPMessage := false
|
||||||
|
var msgNetInfo *NetInfo
|
||||||
|
contentLength := -1
|
||||||
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
@@ -113,28 +124,77 @@ func (c *Capturer) processStream(r io.Reader) {
|
|||||||
|
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
|
|
||||||
|
// Check for tcpdump header
|
||||||
|
if netInfo := parseTcpdumpHeader(line); netInfo != nil {
|
||||||
|
c.currentNetInfo = netInfo
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Detect start of SIP message
|
// Detect start of SIP message
|
||||||
if isSIPStart(line) {
|
if idx := findSIPStart(line); idx != -1 {
|
||||||
// If we were building a message, parse it
|
|
||||||
|
// 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 {
|
if buffer.Len() > 0 {
|
||||||
c.parseAndEmit(buffer.String())
|
c.parseAndEmit(buffer.String(), msgNetInfo)
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOW update msgNetInfo for the new message
|
||||||
|
if c.currentNetInfo != nil {
|
||||||
|
info := *c.currentNetInfo
|
||||||
|
msgNetInfo = &info
|
||||||
|
} else {
|
||||||
|
msgNetInfo = nil
|
||||||
|
}
|
||||||
|
|
||||||
inSIPMessage = true
|
inSIPMessage = true
|
||||||
|
contentLength = -1 // Reset
|
||||||
}
|
}
|
||||||
|
|
||||||
if inSIPMessage {
|
if inSIPMessage {
|
||||||
buffer.WriteString(line)
|
buffer.WriteString(line)
|
||||||
buffer.WriteString("\r\n")
|
buffer.WriteString("\r\n")
|
||||||
|
|
||||||
// Detect end of SIP message (double CRLF or content complete)
|
// Check for Content-Length
|
||||||
// This is simplified - real implementation would track 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
|
// Parse remaining buffer
|
||||||
if buffer.Len() > 0 {
|
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)
|
packet, err := sip.Parse(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if c.OnError != nil {
|
if c.OnError != nil {
|
||||||
@@ -155,26 +215,140 @@ func (c *Capturer) parseAndEmit(raw string) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if packet != nil && c.OnPacket != nil {
|
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)
|
c.OnPacket(packet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// isSIPStart checks if a line looks like the start of a SIP message
|
// findSIPStart returns the index of the start of a SIP message, or -1 if not found
|
||||||
func isSIPStart(line string) bool {
|
func findSIPStart(line string) int {
|
||||||
sipMethods := []string{"INVITE", "ACK", "BYE", "CANCEL", "REGISTER", "OPTIONS", "PRACK", "SUBSCRIBE", "NOTIFY", "PUBLISH", "INFO", "REFER", "MESSAGE", "UPDATE"}
|
sipMethods := []string{"INVITE", "ACK", "BYE", "CANCEL", "REGISTER", "OPTIONS", "PRACK", "SUBSCRIBE", "NOTIFY", "PUBLISH", "INFO", "REFER", "MESSAGE", "UPDATE"}
|
||||||
|
|
||||||
// Response
|
// Check for Response "SIP/2.0"
|
||||||
if strings.HasPrefix(line, "SIP/2.0") {
|
if idx := strings.Index(line, "SIP/2.0 "); idx != -1 {
|
||||||
return true
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request
|
// Check for Request "METHOD "
|
||||||
for _, m := range sipMethods {
|
for _, m := range sipMethods {
|
||||||
if strings.HasPrefix(line, m+" ") {
|
target := m + " "
|
||||||
return true
|
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 false
|
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,9 +6,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"telephony-inspector/internal/logger"
|
||||||
"telephony-inspector/internal/sip"
|
"telephony-inspector/internal/sip"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ type LocalCapturer struct {
|
|||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
running bool
|
running bool
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
currentNetInfo *NetInfo
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
OnPacket func(*sip.Packet)
|
OnPacket func(*sip.Packet)
|
||||||
@@ -46,9 +49,12 @@ func (c *LocalCapturer) Start(iface string, port int) error {
|
|||||||
// -l: line buffered
|
// -l: line buffered
|
||||||
// -A: print packet payload in ASCII
|
// -A: print packet payload in ASCII
|
||||||
// -s 0: capture full packets
|
// -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...)
|
c.cmd = exec.CommandContext(ctx, "tcpdump", args...)
|
||||||
|
|
||||||
|
logger.Info("Starting local capture: tcpdump %v", args)
|
||||||
|
|
||||||
stdout, err := c.cmd.StdoutPipe()
|
stdout, err := c.cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.mu.Lock()
|
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)
|
return fmt.Errorf("failed to start tcpdump: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Info("Local capture started successfully")
|
||||||
|
|
||||||
// Process stdout in goroutine
|
// Process stdout in goroutine
|
||||||
go c.processStream(stdout)
|
go c.processStream(stdout)
|
||||||
|
|
||||||
@@ -89,6 +97,7 @@ func (c *LocalCapturer) Stop() {
|
|||||||
if !c.running {
|
if !c.running {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
logger.Info("Stopping local capture")
|
||||||
c.running = false
|
c.running = false
|
||||||
|
|
||||||
if c.cancel != nil {
|
if c.cancel != nil {
|
||||||
@@ -117,8 +126,15 @@ func (c *LocalCapturer) Close() error {
|
|||||||
|
|
||||||
func (c *LocalCapturer) processStream(r io.Reader) {
|
func (c *LocalCapturer) processStream(r io.Reader) {
|
||||||
scanner := bufio.NewScanner(r)
|
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
|
var buffer strings.Builder
|
||||||
inSIPMessage := false
|
inSIPMessage := false
|
||||||
|
var msgNetInfo *NetInfo
|
||||||
|
contentLength := -1
|
||||||
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
@@ -130,25 +146,99 @@ func (c *LocalCapturer) processStream(r io.Reader) {
|
|||||||
|
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
|
|
||||||
|
// Check for tcpdump header
|
||||||
|
if netInfo := parseTcpdumpHeader(line); netInfo != nil {
|
||||||
|
c.currentNetInfo = netInfo
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Detect start of SIP message
|
// Detect start of SIP message
|
||||||
if isSIPStart(line) {
|
if idx := findSIPStart(line); idx != -1 {
|
||||||
// If we were building a message, parse it
|
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 {
|
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()
|
buffer.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOW update msgNetInfo for the new message
|
||||||
|
if c.currentNetInfo != nil {
|
||||||
|
info := *c.currentNetInfo
|
||||||
|
msgNetInfo = &info
|
||||||
|
} else {
|
||||||
|
msgNetInfo = nil
|
||||||
|
}
|
||||||
|
|
||||||
inSIPMessage = true
|
inSIPMessage = true
|
||||||
|
contentLength = -1 // Reset for new message
|
||||||
}
|
}
|
||||||
|
|
||||||
if inSIPMessage {
|
if inSIPMessage {
|
||||||
buffer.WriteString(line)
|
buffer.WriteString(line)
|
||||||
buffer.WriteString("\r\n")
|
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
|
// Parse remaining buffer
|
||||||
if buffer.Len() > 0 {
|
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()
|
text := scanner.Text()
|
||||||
// tcpdump prints "listening on..." to stderr, ignore it
|
// tcpdump prints "listening on..." to stderr, ignore it
|
||||||
if strings.Contains(text, "listening on") {
|
if strings.Contains(text, "listening on") {
|
||||||
|
logger.Info("tcpdump: %s", text)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
logger.Error("tcpdump stderr: %s", text)
|
||||||
if c.OnError != nil {
|
if c.OnError != nil {
|
||||||
c.OnError(fmt.Errorf("tcpdump: %s", text))
|
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)
|
packet, err := sip.Parse(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Suppress verbose error logging for partial packets unless debug
|
||||||
|
// logger.Error("Error parsing SIP packet: %v", err)
|
||||||
if c.OnError != nil {
|
if c.OnError != nil {
|
||||||
c.OnError(err)
|
c.OnError(err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if packet != nil && c.OnPacket != nil {
|
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)
|
c.OnPacket(packet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -153,6 +153,27 @@ func (s *CallFlowStore) GetRecentFlows(n int) []*CallFlow {
|
|||||||
return flows
|
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
|
// Count returns the number of call flows
|
||||||
func (s *CallFlowStore) Count() int {
|
func (s *CallFlowStore) Count() int {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user