Files
go-sip/internal/sipcall/capture_test.go
T

126 lines
4.1 KiB
Go

package sipcall
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
const testTimeline = "1\t1.0\t192.0.2.1\t192.0.2.10\tINVITE sip:186@192.0.2.10 SIP/2.0\t\tINVITE\t10\tz9hG4bK-test\t\t192.0.2.1\taudio\t10000\n2\t1.1\t192.0.2.10\t192.0.2.1\t\t200\tINVITE\t10\tz9hG4bK-test\tSIP/2.0 200 OK\t192.0.2.10\taudio\t12000\n3\t2.1\t192.0.2.10\t192.0.2.1\t\t200\tBYE\t11\tz9hG4bK-bye\tSIP/2.0 200 OK\t\t\t\n"
func TestDecodedEvidenceUsesOriginalINVITEAndNotBYE(t *testing.T) {
e, ports, err := parseTimeline([]byte(testTimeline))
if err != nil || e.Packets != 3 || e.FinalINVITECode != 200 || len(ports) != 2 {
t.Fatalf("%+v %v %v", e, ports, err)
}
bad := strings.Replace(testTimeline, "\t200\tINVITE\t10\t", "\t480\tINVITE\t10\t", 1)
e, _, err = parseTimeline([]byte(bad))
if err != nil || e.FinalINVITECode != 480 {
t.Fatal("BYE 200 must not turn a rejected INVITE into a connected call")
}
if _, _, err = parseTimeline([]byte("unexpected\n")); err == nil {
t.Fatal("malformed decoder output accepted")
}
if _, _, err = parseTimeline(nil); err == nil {
t.Fatal("missing original INVITE accepted")
}
conflict := testTimeline + strings.Replace(strings.Split(testTimeline, "\n")[1], "\t200\t", "\t480\t", 1) + "\n"
if _, _, err = parseTimeline([]byte(conflict)); err == nil {
t.Fatal("conflicting final INVITE response accepted")
}
}
func fakeTools(t *testing.T, broken bool) string {
t.Helper()
dir := t.TempDir()
tcpdump := `#!/bin/sh
out=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "-w" ]; then shift; out="$1"; fi
shift
done
/bin/dd if=/dev/zero of="$out" bs=24 count=1 2>/dev/null
echo "tcpdump: listening on any" >&2
trap 'exit 0' INT TERM
while :; do /bin/sleep 0.05; done
`
if broken {
tcpdump = "#!/bin/sh\necho 'permission denied' >&2\nexit 1\n"
}
decoder := "#!/bin/sh\ncase \"$*\" in\n*'rtp &&'*) printf '160\\t192.0.2.10\\t192.0.2.1\\n160\\t192.0.2.1\\t192.0.2.10\\n' ;;\n*) /bin/cat '" + filepath.Join(dir, "timeline.tsv") + "' ;;\nesac\n"
for name, data := range map[string]string{"tcpdump": tcpdump, "tshark": decoder, "timeline.tsv": testTimeline} {
mode := os.FileMode(0700)
if name == "timeline.tsv" {
mode = 0600
}
if err := os.WriteFile(filepath.Join(dir, name), []byte(data), mode); err != nil {
t.Fatal(err)
}
}
t.Setenv("PATH", dir)
return dir
}
func TestDebugCaptureOwnsProcessAndProducesPrivateLinkedEvidence(t *testing.T) {
fakeTools(t, false)
c := fixtureConfig(t)
dir := t.TempDir()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cap, err := startCapture(ctx, c, dir)
if err != nil {
t.Fatal(err)
}
if err = cap.Check(); err != nil {
t.Fatal(err)
}
e, err := cap.Stop(ctx, "sip-id-123")
if err != nil {
t.Fatal(err)
}
if len(e.PCAPSHA256) != 64 || e.FinalINVITECode != 200 || e.RTPFromServer != 1 || e.RTPToServer != 1 || e.RTPBytes != 320 {
t.Fatalf("wrong evidence: %+v", e)
}
for _, path := range []string{e.PCAP, e.SIPTimeline, filepath.Join(dir, "tcpdump.log"), filepath.Join(dir, "rtp.tsv")} {
info, err := os.Stat(path)
if err != nil || info.Mode().Perm() != 0600 {
t.Fatalf("private artifact expected: %s %v", path, err)
}
}
if err = cap.Check(); err == nil {
t.Fatal("ended capture still reported ready")
}
}
func TestDebugCaptureFailureDoesNotSilentlyDisableEvidence(t *testing.T) {
fakeTools(t, true)
c := fixtureConfig(t)
if _, err := startCapture(context.Background(), c, t.TempDir()); err == nil {
t.Fatal("broken tcpdump accepted")
}
t.Setenv("PATH", t.TempDir())
if _, err := startCapture(context.Background(), c, t.TempDir()); err == nil {
t.Fatal("missing tcpdump accepted")
}
}
func TestDebugCaptureRejectsUnsafeCallIDAndCancelledStartup(t *testing.T) {
fakeTools(t, false)
c := fixtureConfig(t)
cap, err := startCapture(context.Background(), c, t.TempDir())
if err != nil {
t.Fatal(err)
}
if _, err = cap.Stop(context.Background(), "id\" || ip"); err == nil {
t.Fatal("unsafe filter correlation accepted")
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err = startCapture(ctx, c, t.TempDir()); err == nil {
t.Fatal("cancelled capture accepted")
}
}