sipnab

See why a VoIP call failed, from its SIP signaling and its RTP audio.

Reads live traffic, a pcap file, or the copies your SIP proxies send (HEP). Shows the call flow, the audio quality and security warnings. One binary, no database, no server to run.

“Try a sample capture” runs in your browser: it parses the file on your own machine, uploads nothing and installs nothing.

How sipnab fits together

What it reads

What it works out

  1. Unwrap link layers, tunnels, IP fragments, TCP streams, TLS
  2. Parse SIP, SDP, RTP, RTCP, STUN
  3. Track each dialog, the legs of one call, every media stream
  4. Judge audio quality (MOS), call-setup problems, attacks, RFC conformance

One binary. No database, no server to run.

Where you read it

What it can act on

The terminal UI, the scripts’ output, the REST API and the MCP server all read the same dialog and stream records, so each gives the same answer about the same call. How it works inside →
sipnab interactive TUI — call-flow ladder of a complete call: INVITE with SDP, 100 Trying, 180 Ringing, 200 OK, ACK, the RTP media line, then BYE and its 200 OK, with the decoded INVITE in the detail pane. One of seven dialogs in the same sample capture the analyze page loads

Check what sipnab supports

sipnab runs as one process you start and stop. It has no database, no web interface, no login and no service to keep running, and that is on purpose. If you need dashboards and months of history, use Homer, and let sipnab feed it over HEP.

Capture and inputs

Live interfaces, files, archives and HEP feeds, decrypted where you hold the keys.

  • Live capture One or several interfaces, with the capture filter editable from the terminal UI.
  • Files and archives pcap and pcapng files, folders and globs, tar, gzip, and password-protected ZIP and 7z.
  • HEP v1/v2/v3 Sends HEP v3 to Homer, and receives HEP v1, v2 and v3 from Kamailio, OpenSIPS or Asterisk, so one sipnab reads every proxy with nothing installed on them.
  • Tunnels and transports VLAN, MPLS, GRE, VXLAN, SIP over WebSocket, and IP fragment and TCP reassembly.
  • TLS/SRTP decryption Decrypts TLS 1.2 and 1.3 with an SSLKEYLOGFILE or an RSA server key, and SRTP with the SDES keys from the SDP or DTLS-SRTP key logs.
  • TLS capture with uprobes (root, same host) This does not break TLS. On a host where you already have root, kernel uprobes read the SIP plaintext inside OpenSSL or wolfSSL before it is encrypted. Every Linux build has it, and its default tracefs backend needs no BTF. The eBPF backend, which also recovers the peer addresses, is in the released Linux gnu tarballs, .deb and .rpm, not the static musl builds, and needs a kernel with BTF. Read the security implications first.

Analysis and diagnosis

Why a call failed or sounded bad, with the evidence behind each finding.

  • RTP quality analysis Jitter, packet loss and estimated MOS (ITU-T G.107 E-model) for every stream, plus RTCP XR (RFC 3611) reports.
  • VoIP diagnosis Finds one-way audio and NAT mismatches, with the SDP offer and answer behind each, and calls slow to ring.
  • Calls across legs Ties the legs of a call through a B2BUA or PBX together, in one ladder.
  • Ranked analysis Every problem in a capture in one list, worst first.
  • Conformance linter Findings with rule IDs and RFC sections, and an exit code a CI job can fail on.
  • Relay statistics Asks rtpengine for its own counters and keeps them apart from what sipnab measured.
  • Filter DSL 42 fields, 8 operators and boolean logic, with named aliases and operator-defined named filters.

Call records (vCon)

vcon feature: gnu, macOS and Docker builds IETF vCon containers of the calls sipnab observed.

Security

Attacks and fraud on the wire, and a process that keeps its own privileges low.

Integrations and output

For dashboards, scripts and AI agents.

  • REST API Query dialogs, streams and stats over HTTP, with scoped tokens and TLS.
  • MCP server Lets an AI agent such as Claude Code query captures over stdio or HTTP. 76 tools cover dialogs, RTP, diagnostics, and security findings. 63 are read-only. Of the thirteen that write files or act on the host, each refuses to until you start the server with the flag that allows it.
  • Prometheus metrics Series for calls, SIP responses, MOS and jitter, and security alerts, and counters for what sipnab captured, lost and could not read.
  • Export formats PCAP, PCAP-NG, TXT, JSON, NDJSON, CSV, HTML, Markdown, WAV audio, SIPp XML and RTP JSON from the terminal UI, and YANG (RFC 7951) and vCon from the command line.
  • WASM plugins Loads your own dialog detections as sandboxed WebAssembly (--plugin). Off by default, and only in builds with the plugins feature: the gnu and macOS builds carry it, the static musl builds and the gnu no-audio packages do not.
  • Browser analysis Opens a pcap in your browser with WebAssembly. Nothing is uploaded or installed.

Built in Rust 113 unsafe blocks, most of them calls into the kernel or C libraries, and CI rejects any block that does not state its own soundness argument. 12840 automated tests. Under 23 MB static binary. Every capability

Install and open your first capture

On macOS, brew install NormB/tap/sipnab replaces the first step.

1. Install sipnab

bash
curl -fsSL https://sipnab.com/install.sh | sh

2. Download a sample capture

bash
curl -fsSLO https://sipnab.com/demos/sample-call.pcap

3. Open it in the terminal UI

bash
sipnab -I sample-call.pcap

4. Or print a report without the UI

bash
sipnab -N -I sample-call.pcap --report

5. Capture live SIP traffic

bash
sudo sipnab -d eth0

Ask an AI agent about a capture

Real answers from sipnab's MCP server, the interface an AI agent such as Claude Code talks to. Run these commands from a source checkout of the repository, where demos/mcp-stdio.sh and the sample captures live. “More demos” shows the terminal UI. Or open your own capture in the browser →

Ask what a capture holds, then ask about one call in it. The first command lists every dialog: a phone registering, the PBX probing it with an OPTIONS, two subscriptions, and one call placed, answered and hung up.

The second command draws that call's ladder, with media beside signaling. So the 64.3% packet loss on one direction of the audio sits next to the transactions it belongs to. That loss arrived in three bursts rather than spread evenly, which sounds far worse to the person on the call.

demos/mcp-stdio.sh tests/pcap-samples/Asterisk_ZFONE_XLITE.pcap list_dialogs \
  | jq -c '.dialogs[] | {method, state, msg_count, duration_sec}'

{"method":"REGISTER","state":"Registered","msg_count":4,"duration_sec":0.01}
{"method":"OPTIONS","state":"Completed","msg_count":2,"duration_sec":0.008}
{"method":"SUBSCRIBE","state":"Terminated","msg_count":4,"duration_sec":0.018}
{"method":"SUBSCRIBE","state":"Terminated","msg_count":4,"duration_sec":0.015}
{"method":"INVITE","state":"Completed","msg_count":13,"duration_sec":23.712}
demos/mcp-stdio.sh tests/pcap-samples/Asterisk_ZFONE_XLITE.pcap render_ladder \
  '{"call_id":"ZDYzOWVlNjEwM2NjZTBjNzliNmM1ZTNiOGZjNWFhN2E.","format":"text"}' \
  | sed -n '/^Result:/p; /^SIP Transactions:/,$p'

Result:     Completed (BYE)
SIP Transactions:
  INVITE -> 401
  ACK
  INVITE -> 100 (7ms) -> 180 (30ms) -> 200 (7650ms)
  ACK
  INVITE -> 200 (23624ms)
  ACK
  BYE -> 200 (23712ms)

Media Streams:
  RTP 192.168.10.40->192.168.10.41 PCMU SSRC=0xb72a7104 pkts=790 jitter=1ms loss=0.1%
    Loss pattern: random (not bursty)
  RTP 192.168.10.41->192.168.10.40 PCMU SSRC=0xbee0f2ed pkts=205 jitter=0ms loss=64.3%
    Loss pattern: BURSTY (3 bursts, avg duration 2473ms) — perceptually worse than random loss
  RTP 192.168.10.41->192.168.10.2 PCMU SSRC=0xbee0f2ed pkts=2 jitter=0ms loss=0.0%

Issues Detected:
  - Duration asymmetry: A leg lasted 15.8s, B leg 11.5s (Δ 4.4s) — one side may have hung up or dropped media early.

An agent asks why a call failed and gets a verdict, not a packet list: signaling rather than media, the 486 that ended it, and media explicitly ruled out. Over MCP on stdio — no daemon, no port, no database.

demos/mcp-stdio.sh tests/pcap-samples/sip-problem-call.pcap \
  triage_call '{"call_id":"busy-3a2b1c@192.0.2.30"}'

{
  "call_id": "busy-3a2b1c@192.0.2.30",
  "final_status_code": 486,
  "media": {
    "hints": [],
    "nat_mismatch": false,
    "no_media": false,
    "one_way_audio": false,
    "problem": false,
    "stream_count": 0
  },
  "schema_version": 1,
  "signaling": {
    "hints": [
      "Call failed: 486 Busy Here."
    ],
    "problem": true
  },
  "source_exhausted": true,
  "source_stopped_early": false,
  "state": "Failed",
  "verdict": "signaling"
}

Every finding cites the RFC section it comes from, says what the message should have carried and what it actually carried, and explains the consequence — so an agent can act on it instead of guessing what a lint code meant.

demos/mcp-stdio.sh tests/pcap-samples/sip-lint-findings.pcap \
  lint_dialog '{"call_id":"lint-maxfwd@192.0.2.10"}' \
  | jq '.findings[] | select(.severity == "error")'

{
  "basis": "must",
  "expected": "Contact: <sip:user@host>",
  "explanation": "§12.1.1 makes the UAS add a Contact to the response. It is the remote target for the dialog the 2xx creates, so without it the caller has nowhere to send the ACK and nowhere to send the BYE. The call answers and then cannot be hung up cleanly.",
  "frame_ref": "tests/pcap-samples/sip-lint-findings.pcap#1@ad0e14efbbab6ffa",
  "message_index": 1,
  "observed": "⟦untrusted-capture-data⟧2xx to INVITE with no Contact header field⟦/untrusted-capture-data⟧",
  "rfc": 3261,
  "rule_id": "SIP-3261-12.1.1-CONTACT-MISSING-IN-2XX",
  "section": "12.1.1",
  "severity": "error"
}

Any answer can be taken back to the bytes it came from. The pointer carries a digest, and verified means sipnab re-read that frame and it still hashes to what it hashed to when the answer was made. File access stays off until you name a root.

MCP_FILE_ROOT=tests/pcap-samples demos/mcp-stdio.sh \
  tests/pcap-samples/sip-problem-call.pcap show_evidence \
  '{"refs":["tests/pcap-samples/sip-problem-call.pcap#5@884d4525ec86f71b"],"max_bytes":64}'

{
  "frames": [
    {
      "frame_bytes": 540,
      "hex": "02 00 00 00 00 01 02 00 00 00 00 02 08 00 45 00 02 0e 00 00 40 00 40 11 b4 98 c0 00 02 1e c0 00 02 28 13 c4 13 c4 01 fa 00 00 49 4e 56 49 54 45 20 73 69 70 3a 64 61 76 65 40 31 39 32 2e 30 2e",
      "hex_bytes_shown": 64,
      "ordinal": 5,
      "pointer": "tests/pcap-samples/sip-problem-call.pcap#5@884d4525ec86f71b",
      "source": "sip-problem-call.pcap",
      "status": "verified",
      "truncated": true
    }
  ],
  "requested": 1,
  "resolved": 1,
  "schema_version": 1,
  "summary": "1 of 1 pointer(s) resolved; 1 verified against a recorded digest",
  "verified": 1
}

A B2BUA answers on one Call-ID and dials out on another, so the two halves of one conversation share no identifier. sipnab returns the matched leg with the strategy that matched it and flags heuristic_only when nothing but timing tied them together — an agent that is told how weak the match is can weigh it.

demos/mcp-stdio.sh tests/pcap-samples/b2bua-asterisk.pcapng \
  find_correlated '{"call_id":"b2bua-caller-synth@203.0.113.1"}'

{
  "capture_identity": {
    "dialog_generation": 64,
    "instance": "3990fa18d04ed7cf971eae-2",
    "node": "sbc-edge-1",
    "stream_generation": 2
  },
  "heuristic_only": true,
  "legs": [
    {
      "call_id": "b2bua-leg-synth@203.0.113.101:5060",
      "identifier_match": false,
      "observed_gap_ms": 3,
      "score": 50,
      "strategy": "timing_heuristic"
    }
  ],
  "schema_version": 1,
  "source_call_id": "b2bua-caller-synth@203.0.113.1",
  "source_exhausted": true,
  "source_stopped_early": false,
  "timing_clock": {
    "available": true,
    "est_error_us": 0,
    "max_error_us": 524000,
    "synchronized": true
  },
  "total_matched": 1
}

The terminal UI, which is what sipnab -I capture.pcap opens. Browse SIP dialogs, select one, and drill into its call-flow ladder with the parsed message beside it. Open a different capture any time with O — no restart.

sipnab TUI demo: browsing dialogs and the call-flow ladder

The detail pane of the terminal UI reads the parsed message beside the ladder. Cycle how SDP is shown with d, and toggle wrapping with w when a header runs wider than the pane.

sipnab detail pane demo: SDP display modes and wrap toggle

Measuring a gap, in the terminal UI. Press m on any message to mark it, then move to another: the Δ badge shows the exact millisecond distance between the two, so “the 200 came back slowly” becomes a number.

sipnab mark and delta demo

Finding one call among thousands, in the terminal UI. Press / and the dialog list narrows as you type, matching method, From, To and Call-ID at once.

sipnab live search demo narrowing the dialog list

One conversation, two Call-IDs. A back-to-back user agent answers on one and dials out on another, so the two halves look like unrelated calls. In the terminal UI, x stitches the correlated legs into a single ladder you can read end to end.

sipnab multi-leg B2BUA extended-flow demo

The media side, in the terminal UI. Tab switches from dialogs to RTP streams. Selecting one opens its detail: SSRC, codec, jitter, loss and estimated MOS, with quality plotted over the life of the call.

sipnab RTP streams and per-stream quality demo

No terminal UI at all. -N runs sipnab headless for a script or a pipeline: emit every dialog as JSON with --json, narrow to the calls that went wrong with --problems, or print a full call report with --report.

sipnab CLI demo: JSON dialogs, --problems surfacing failed calls, and a call report

Add it to your voice stack

Step-by-step guides that install your SIP proxy, OpenSIPS or Kamailio, or your PBX, Asterisk or FreeSWITCH, and each piece beside it, then connect sipnab to it. Each was run command by command on fresh virtual machines, and each page says which.

SIP proxy

OpenSIPS and Kamailio carry your calls, and every other guide builds on them. Set up either one from its packages, OpenSIPS from source too, or both on one machine, with sipnab watching beside them.

PBX

Asterisk and FreeSWITCH answer each call and connect it onward as a second call. Build either one from source and connect a test call with audio both ways, then see both of its calls in sipnab and receive the PBX's own HEP.

Call records

A vCon server keeps every call as a vCon. OpenSIPS records calls into it over SIPREC, and sipnab can send its own vCons, built from what it saw on the wire. Kamailio has no SIPREC module, so on a Kamailio stack sipnab's vCons are the ones the server gets. sipnab --vcon-forward delivers them, to the server or to vcon.store (in the gnu and macOS builds, which carry the vcon feature).

Attack blocking

TFPS stops SIP scanners and fraud in the kernel with XDP, before they reach your SIP proxy, and sipnab shows you what it has blocked. fail2ban bans scanners and password guessers with an nftables rule, from the lines OpenSIPS logs or the ones sipnab writes.

Media relay

rtpengine or rtpproxy anchors every call's audio, and sipnab works beside either one to match the media on the relay with the call it belongs to.

Call history

Homer keeps a searchable history of every call your OpenSIPS or Kamailio sends it over HEP, and sipnab can join the same feed.

Metrics

Prometheus and Grafana graph your SIP proxy's statistics, and sipnab adds its own call and media series beside them.

Contributor License Agreement OpenSSF Best Practices — Passing · Baseline 3 Signed build provenance & SBOM Code coverage on Codecov XERJ Corpus Hub: contributor