package sipcall import ( "bytes" "context" "encoding/binary" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" ) // This opt-in test uses real tcpdump/tshark only as OFFLINE decoders inside a // network-disabled disposable container. Packets are synthetic documentation // addresses: no host capture, SIP traffic, human audio or live service calls. func TestOfflineRealPacketDecoders(t *testing.T) { image := os.Getenv("GO_SIP_TSHARK_TEST_IMAGE") if image == "" { t.Skip("offline decoder image opt-in not supplied") } docker, err := exec.LookPath("docker") if err != nil { t.Fatal(err) } tools := fakeTools(t, false) dir := t.TempDir() source := filepath.Join(tools, "synthetic.pcap") if err = os.WriteFile(source, syntheticPCAP(), 0600); err != nil { t.Fatal(err) } script, err := os.ReadFile(filepath.Join(tools, "tcpdump")) if err != nil { t.Fatal(err) } script = bytes.Replace(script, []byte("/bin/dd if=/dev/zero of=\"$out\" bs=24 count=1 2>/dev/null"), []byte("/bin/cp '"+source+"' \"$out\""), 1) if err = os.WriteFile(filepath.Join(tools, "tcpdump"), script, 0700); err != nil { t.Fatal(err) } decoder := fmt.Sprintf("#!/bin/sh\nexec '%s' run --rm --network none -v '%s:%s:ro' '%s' tshark \"$@\"\n", docker, dir, dir, image) if err = os.WriteFile(filepath.Join(tools, "tshark"), []byte(decoder), 0700); err != nil { t.Fatal(err) } c := fixtureConfig(t) cap, err := startCapture(context.Background(), c, dir) if err != nil { t.Fatal(err) } e, err := cap.Stop(context.Background(), "sip-id-123") if err != nil { t.Fatal(err) } if e.Packets != 3 || e.FinalINVITECode != 200 || e.RTPFromServer != 1 || e.RTPToServer != 1 { t.Fatalf("real offline decoding mismatch: %+v", e) } cmd := exec.Command(docker, "run", "--rm", "--network", "none", "-v", dir+":"+dir+":ro", image, "tcpdump", "-nn", "-r", e.PCAP) if out, err := cmd.CombinedOutput(); err != nil || !strings.Contains(string(out), "INVITE") { t.Fatalf("real tcpdump offline inspection failed: %v %s", err, out) } } // Static fixture construction, not a runtime SIP/RTP stack. Checksums are // disabled in these synthetic packets; no packet is ever sent to a socket. func syntheticPCAP() []byte { var pcap bytes.Buffer for _, v := range []any{uint32(0xa1b2c3d4), uint16(2), uint16(4), uint32(0), uint32(0), uint32(65535), uint32(1)} { _ = binary.Write(&pcap, binary.LittleEndian, v) } sdp := func(ip string, port int) string { return fmt.Sprintf("v=0\r\no=- 1 1 IN IP4 %s\r\ns=offline\r\nc=IN IP4 %s\r\nt=0 0\r\nm=audio %d RTP/AVP 8\r\na=rtpmap:8 PCMA/8000\r\n", ip, ip, port) } message := func(first, cseq, branch, body string) []byte { return []byte(fmt.Sprintf("%s\r\nVia: SIP/2.0/UDP 192.0.2.1:5060;branch=%s\r\nFrom: ;tag=offline\r\nTo: \r\nCall-ID: sip-id-123\r\nCSeq: %s\r\nContent-Type: application/sdp\r\nContent-Length: %d\r\n\r\n%s", first, branch, cseq, len(body), body)) } add := func(src, dst byte, sport, dport uint16, payload []byte) { packet := make([]byte, 14+20+8+len(payload)) packet[12], packet[13] = 8, 0 ip := packet[14:34] ip[0] = 0x45 binary.BigEndian.PutUint16(ip[2:], uint16(20+8+len(payload))) ip[8], ip[9] = 64, 17 copy(ip[12:], []byte{192, 0, 2, src}) copy(ip[16:], []byte{192, 0, 2, dst}) udp := packet[34:42] binary.BigEndian.PutUint16(udp, sport) binary.BigEndian.PutUint16(udp[2:], dport) binary.BigEndian.PutUint16(udp[4:], uint16(8+len(payload))) copy(packet[42:], payload) for _, v := range []uint32{1, uint32(pcap.Len()), uint32(len(packet)), uint32(len(packet))} { _ = binary.Write(&pcap, binary.LittleEndian, v) } pcap.Write(packet) } add(1, 10, 5060, 5060, message("INVITE sip:123@192.0.2.10 SIP/2.0", "10 INVITE", "z9hG4bK-test", sdp("192.0.2.1", 10000))) add(10, 1, 5060, 5060, message("SIP/2.0 200 OK", "10 INVITE", "z9hG4bK-test", sdp("192.0.2.10", 12000))) add(10, 1, 5060, 5060, message("SIP/2.0 200 OK", "11 BYE", "z9hG4bK-bye", "")) add(10, 1, 12000, 10000, append([]byte{0x80, 8, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}, bytes.Repeat([]byte{0xd5}, 160)...)) add(1, 10, 10000, 12000, append([]byte{0x80, 8, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2}, bytes.Repeat([]byte{0xd5}, 160)...)) return pcap.Bytes() }