Docs Examples & Recipes
Examples & Recipes
Worked examples with real output: triage, filtering, HEP, TLS decryption, MCP, observability, security and audio export, each one a command you can paste and the result it produces.
On this page
- What do you want to do?
- 1. Triage a pcap fast
- 2. Live capture, narrow to a single user
- 3. Find every failed call, grouped by response code
- 4. Diagnose a one-way audio complaint
- 5. Filter for the five things you look for most
- 6. Wire HEP from your SIP stack to a central sipnab
- 7. Decrypt SIP/TLS via SSLKEYLOGFILE
- 8. Run sipnab as an MCP server
- 9. Graph call rate, response codes and PDD over time
- 10. Detect SIP scanners and auto-block via fail2ban
- 11. Find why a call sounds bad in one direction only
- 12. Generate a call report (text / Markdown / JSON)
- 13. Export RTP audio as WAV
- 13b. Export one call to a conversation archive as a vCon
- 14. Analyze a pcap without installing anything
- 15. Check a capture against the RFCs
- 16. Analyze a capture into one machine-readable verdict
- 17. Inspect what NAT did to a call
- 18. Collect messages by call, host or method
- 19. Read a very large capture faster
- 20. Verify the exact bytes behind a finding
- 21. Detect your own fault patterns with a plugin
- 22. Measure whether the loss is yours or the network’s
- 23. Find the device flooding REGISTER
- 24. Diagnose a codec mismatch
- 25. Find out why DTMF does not reach the IVR
- 26. Run sipnab as a HEP relay
- 27. Compare the same call at two nodes
- 28. Run sipnab as a service
- 29. Read a capture whose SIP is not on port 5060
- 30. Find calls that answered and were never acknowledged
- 31. Set the quality thresholds to your own network
- 32. Export one customer’s calls as a smaller capture
- 33. Check what a STIR/SHAKEN Identity header actually claims
- 34. Detect weak digest authentication on a registrar
- 35. Read SIP that arrives in TCP segments or IP fragments
- 36. Read SIP over WebSocket from a WebRTC gateway
- 37. Read SIP carried inside a GTP-U or VXLAN tunnel
- 38. Search a capture for a header or a body string
- 39. Collect a directory of rotated captures into one analysis
- 40. Open the same evidence in Wireshark
- 41. Measure the gap between consecutive messages
- 42. Record which invocation produced a report
- 43. Keep a long-running capture inside a memory budget
- 44. Run a capture that stops on its own
- 45. Check whether comfort noise explains a one-way finding
- 46. Detect fraud placed outside business hours
- 47. Follow a load generator’s traffic by transaction, not by call
- 48. Run a live capture without giving sipnab root
- 49. Configure sipnab from a file instead of a long command line
- 50. Install shell completions
- 51. Export every failed call as a redacted vCon in one pass
- 52. Check a vCon against the schema before a store rejects it
- 53. Export a vCon per failed call in one round trip
- 54. Find out where a stream’s endpoint came from
- 55. Set up the MCP server for a hosted agent
- 56. Read TLS from an agent, with no keys and no restart
- 57. Ask the capture how many calls failed
- 58. Ask why the MOS is what it is
- 59. Decrypt TLS with the server’s private key
- 60. See who is on a recorded call
- Look up a one-liner by task
- 61. Ask a relay whether it is dropping packets
- 62. Ask a relay what it is still holding
- 63. Compare a relay’s per-call count with your capture
- 64. Read a relay’s loss beside the loss you measured
- Next steps
Recipe-style walkthroughs for the things people actually want to do. Each recipe states the problem, gives exact commands, tells you what to look for in the output, and flags common pitfalls.
Each recipe stands alone — nothing here depends on anything above it. If you are new, recipe 1 is the broadest starting point.
What do you want to do?
Where a recipe fits
Most recipes are one of three shapes. Knowing which you are in tells you what the commands are doing:
flowchart LR
P[pcap file] --> A[sipnab]
L[live interface] --> A
H[HEP from proxies] --> A
A --> T[TUI: look at it]
A --> J[JSON / report: pipe it]
A --> M[metrics / alerts: watch it]
- Look at it — the TUI, for a human working a ticket.
- Pipe it — JSON, reports, WAV, Wireshark filters, for tooling.
- Watch it — metrics, scanner detection, alerts, for something long-running.
1. Triage a pcap fast
Problem: Someone handed you a capture.pcap and asked “is anything wrong?”
Commands:
Open the call list and scan it visually — the interactive TUI:
sipnab -I capture.pcap
The same capture as a headless overview: dialog count, methods, average PDD.
sipnab -N -I capture.pcap
A one-flag diagnostic sweep — retransmits and failed dialogs:
sipnab -N -I capture.pcap --problems
The same sweep in JSON, for piping into another tool:
sipnab -N -I capture.pcap --problems --json
The same sweep spelled the long way. --problems expands to the problems DSL alias, so this selects exactly the same calls.
sipnab -N -I capture.pcap --filter problems
The --problems sweep prints one line per SIP message of each flagged call, then the end-of-capture summary. You should see something like (abridged):
INVITE +15551234 -> +15559876 192.0.2.6:5060 -> 192.0.2.7:5060 Failed 408 Request Timeout
...
852 packets captured, 10 SIP messages, 839 RTP packets across 2 streams
What to look for:
- The
--problemsflag and the DSL alias reached with--filter problemsexpand to one and the same expression:state == 'Failed' OR one_way == true OR rtp.loss > 5.0 OR rtp.jitter > 50.0 OR nat_mismatch == true OR retransmits > 3 OR pdd > 11.0 OR codec_asymmetry == true OR ptime_asymmetry == true OR payload_asymmetry == true OR duration_asymmetry == true OR late_media == true. Either spelling flags the same calls, so an empty answer means the capture is probably clean. - The end-of-capture summary distinguishes RTP packets from RTP streams:
852 packets captured, 10 SIP messages, 839 RTP packets across 2 streams. A capture with media but no SIP usually means the SIP signaling happened off-pcap (different VLAN, different host, different port).
Pitfalls:
- The TUI requires a tty. If you’re SSH’d in without
-t, force-Nmode. - For large pcaps (>1 GB), prefer
-Nfirst; the TUI loads everything into memory.
2. Live capture, narrow to a single user
Problem: A user reports their calls are flaky. Capture only their traffic in real time.
Commands:
Capture on eth0, keeping only this user’s calls — the filter matches From or To:
sudo sipnab -d eth0 --filter "from.user == '1001' OR to.user == '1001'"
The same capture with a CLI summary line per dialog instead of the TUI:
sudo sipnab -N -d eth0 --filter "from.user == '1001' OR to.user == '1001'" --json
What to look for:
- The TUI’s call list updates as new dialogs appear. Press
Tabto switch to the RTP stream view; pressEnteron a stream to see jitter/loss/MOS history. - In CLI mode, each completed dialog is one line of JSON. Pipe to
jqorteeto a log.
Pitfalls:
- Live capture needs
CAP_NET_RAW(Linux) or root.setcap cap_net_raw,cap_net_admin=eip $(which sipnab)lets you skipsudoafter the first run. - The filter DSL evaluates against complete dialog records — once the dialog state machine has enough information (typically after the first response, or earlier for fields that only depend on the request). For per-header regex filtering on individual messages, use the older
--from,--to,--contact,--uaflags listed insipnab --help.
3. Find every failed call, grouped by response code
Problem: “We had a spike in failures around 14:00. What was it?”
Commands:
sipnab -N --json emits per-message records (one JSON line per SIP message), not per-dialog summaries. The status_code field is on response messages. Combined with --filter (which evaluates against the dialog so all messages from matched dialogs flow through), you get a histogram of every response code seen during failed dialogs:
Every failed call’s response messages — Call-ID, status_code and reason:
sipnab -N -I capture.pcap --filter "state == 'Failed'" --json \
| jq 'select(.is_request == false) | {call_id, status_code, reason}'
A histogram of the response codes seen in failed dialogs:
sipnab -N -I capture.pcap --filter "state == 'Failed'" --json \
| jq -r 'select(.is_request == false) | .status_code' \
| sort | uniq -c | sort -rn
A detailed report for one failure, in Markdown, ready to paste into a ticket:
sipnab -N -I capture.pcap --call-report 'abc123@host' --markdown > failure-report.md
The histogram output looks like (uniq -c count, then status code):
23 100
14 486
6 503
3 488
What to look for:
- A 401/407 spike usually means a credential-rotation push hit the wrong realm.
- A 408 spike on outbound is upstream timeout — check rtpengine / SBC.
- A 488 spike (Not Acceptable Here) usually means a codec mismatch — combine with Recipe 11.
Pitfalls:
- The histogram counts all response codes seen in messages of failed dialogs (so a single failed call with
100 Trying → 488contributes both 100 and 488). For just the final response per call, use--call-report <id>per dialog. - The dialog summary returned by the REST API (
/v1/dialogs) has nostatus_codefield; that’s a per-message field only available in CLI--jsonoutput or via/v1/dialogs/{id}(which includes the full message list).
4. Diagnose a one-way audio complaint
Problem: A user said “I can hear them but they can’t hear me.” There’s a Call-ID in the ticket.
Commands:
First, confirm the diagnosis engine flagged it. The diagnosis block lives on the dialog-level JSON that --call-report emits, not on per-message records.
sipnab -N -I capture.pcap --call-report 'abc123@host' --json --no-cli-print \
| jq '{call_id, state, diagnosis}'
Then get the human-readable call report — NAT mismatch, SDP offer/answer, media path:
sipnab -N -I capture.pcap --call-report 'abc123@host' --markdown --no-cli-print
Finally, inspect the actual RTP streams for that call in the TUI:
sipnab -I capture.pcap
# → press '/' to search, type 'abc123', Enter
# → Tab to switch to RTP streams view
# → Enter on each stream to see packet count, jitter, loss
The first command should print a diagnosis object like:
{
"call_id": "abc123@host",
"state": "Completed",
"diagnosis": {
"one_way_audio": true,
"nat_mismatch": true,
"no_media": false,
"hints": [
"RTP flowed 203.0.113.7:41002 -> 192.0.2.5:16386 only (SSRC 0x1a2b3c4d). No reverse media flow detected.",
"RTP arrived from 203.0.113.7:41002 at 192.0.2.5:16386, and no SDP in this dialog advertised 203.0.113.7 (it offered 198.51.100.20:16384) — the media source was rewritten, typically by NAT, so replies sent to 198.51.100.20:16384 never reach it.",
"One-way audio combined with NAT mismatch — media likely being sent to the wrong address."
]
}
}
What to look for:
diagnosis.one_way_audio: trueconfirms the engine saw RTP in only one direction for ≥6s after call establishment.- The ports in the hint are where the fix goes. Each side advertises a receive port in its SDP and, under symmetric RTP (RFC 4961), should send from that same port. A hint reading
advertised 16384 but sends from 41002means the far end is replying to a port nothing is sending from, so no NAT pinhole was ever opened there — that is the firewall rule, port-forward or RTP port range to go and check. When the ports agree, the hint stays quiet about them. diagnosis.nat_mismatch: trueis the usual root cause — the Contact header / Via address differs from the SDPc=line. Common when the upstream SBC isn’t rewriting Contact.- In the TUI’s RTP stream view, look for one stream with packets and one with
0 packets received— that’s the silenced direction.
Pitfalls:
- If both streams show packets but the user still reports silence, the issue is downstream of sipnab (codec mismatch, jitter buffer underflow, bad headset). Use Recipe 11 for codec asymmetry checks.
5. Filter for the five things you look for most
The filter DSL has 33 fields and 7 operators. These five cover most operational triage:
Slow setup — every dialog that took more than 3 seconds from INVITE to 200 OK:
sipnab -N -I capture.pcap --filter "pdd > 3.0" --json
REGISTER dialogs that failed:
sipnab -N -I capture.pcap --filter "method == 'REGISTER' AND state == 'Failed'" --json
Short calls — completed, but under 10 seconds, which usually points at a UX or cancellation problem:
sipnab -N -I capture.pcap --filter "duration < 10.0 AND state == 'Completed'" --json
Heavy retransmits, the signature of packet loss on the SIP path:
sipnab -N -I capture.pcap --filter "retransmits > 5" --json
A specific User-Agent, matched as a regex:
sipnab -N -I capture.pcap --filter "ua =~ '(?i)friendly.*scanner|sipvicious'" --json
For per-call asymmetry checks (different codec on each leg, late media, etc.), see Recipe 11.
Pitfalls:
- String comparisons are case-sensitive. State names must match exactly (
'Completed', not'completed'). Use=~ '(?i)...'if you want case-insensitive. - Boolean fields only support
==and!=—one_way > trueis a parse error.
6. Wire HEP from your SIP stack to a central sipnab
Problem: You want one sipnab box collecting traffic mirrors from multiple SIP servers.
6a. Set up the listener
Build sipnab with HEP support — skip this if you installed a package that already has it:
cargo build --release --no-default-features \
--features native,hep,api,mcp,mcp-http
Run it as a daemon. UDP :9060 receives HEP, TCP :9100 serves REST + Prometheus. sipnab refuses a routable bind that nothing guards, so the command carries a HEP source allowlist and an API signing key:
sipnab -N --hep-listen 0.0.0.0:9060 --hep-allow 192.0.2.0/24 --api 0.0.0.0:9100 --api-signing-key-file /etc/sipnab/signing.key --no-priv-drop --syslog
A ready-to-deploy systemd unit lives at contrib/observability/sipnab-hep.service. Put sipnab on the collector host first — the install guide covers every channel.
6b. Configure the SIP server to mirror
OpenSIPS:
loadmodule "proto_hep.so"
modparam("proto_hep", "hep_id", "[hep_central]udp:capture.example.com:9060;version=3")
loadmodule "siptrace.so"
modparam("siptrace", "trace_id", "[hep_central]uri=hep:hep_central")
route {
sip_trace("hep_central", "d", "sip");
...
}
Reload with opensipsctl restart (or systemctl reload opensips for graceful reload).
rtpengine:
# /etc/rtpengine/rtpengine.conf
homer = capture.example.com:9060
homer-protocol = udp
homer-id = 1
# Without this, rtpengine mirrors RTCP statistics only -- which carry no
# Call-ID and no SDP, so nothing sipnab reads can name a call or find a media
# endpoint. It is the first thing to check when a relay looks silent.
homer-enable-ng = true
Restart with systemctl restart rtpengine. See
the rtpengine page for what sipnab does with the result, and
for the two ways to read it that do not cost you your collector.
Kamailio:
loadmodule "siptrace.so"
modparam("siptrace", "duplicate_uri", "sip:capture.example.com:9060")
modparam("siptrace", "hep_mode_on", 1)
modparam("siptrace", "hep_version", 3)
route {
sip_trace();
...
}
FreeSWITCH (mod_sofia):
<!-- conf/sip_profiles/external.xml -->
<param name="capture-server" value="udp:capture.example.com:9060;hep=3"/>
<param name="sip-capture" value="yes"/>
6c. Verify packets are arriving
On the sipnab host, tcpdump the HEP socket to see whether anything is arriving at all:
sudo tcpdump -i eth0 -n udp port 9060
Confirm the HEP feed is producing dialogs:
curl -s http://localhost:9100/v1/stats | jq
Watch dialogs accumulate live:
watch -n 1 'curl -s http://localhost:9100/v1/dialogs?limit=5 | jq ".dialogs[] | {call_id, state}"'
6d. Take signaling from HEP and media off the wire, in one process
Problem: the HEP mirror is the robust way to get decrypted SIP — it is
already plaintext at the source, so there is no TLS key extraction to go wrong —
but a HEP feed carries no RTP, so a --hep-listen run measures no media at all.
Capturing the interface instead gets you media and puts you back on key
extraction for the signaling.
Run both in one sipnab. HEP supplies the signaling, the NIC supplies the RTP for the same calls, and streams bind to dialogs by SDP media endpoint:
sipnab -N -d eth0 --hep-listen 127.0.0.1:9060 "udp portrange 10000-20000"
Raised by Dan Jenkins (@danjenkins) from OpenSIPS deployment experience.
The BPF expression is not optional decoration. Without one, the interface gets sipnab’s auto-generated signaling filter and captures no media whatsoever — while every message the mirror already sent also arrives off the wire, doubling each dialog’s message ladder. Name your media ports and nothing else, because sipnab warns if you forget.
What you get that a single source cannot give you. The two accounts are complementary rather than redundant, and their DISAGREEMENT is the finding. HEP reports what the proxy believes it did. The wire reports what actually left the box. When the question is “is the proxy misbehaving, or did I configure it to”, a mirror produced by the suspect cannot answer it — it is the same witness twice.
A composite run therefore reports, per call:
| Finding | What it means |
|---|---|
| Seen by both | The proxy did what it says it did |
| Mirror-only | The proxy believes it sent something the wire never carried |
| Wire-only | Traffic left the box that the tracing never reported |
| Differing SDP | Both saw the message and disagree about the media endpoint, so the report carries both accounts side by side |
Neither account carries the label expected or actual. sipnab pairs copies by transaction identity (RFC 3261 section 17.1.3 and RFC 3261 section 17.2.3), never by arrival order, because the mirror usually arrives FIRST — the proxy mirrors as it processes while the wire copy takes a network hop, so any “first one wins” rule would quietly make the suspect authoritative.
A single-source run reports none of this and allocates nothing for it.
Trace both directions, or this measures nothing. In OpenSIPS, use transaction scope so the tracer mirrors the message it sent as well as the one it received:
route {
$var(tid) = "hep_central";
trace($var(tid), "t", "sip"); # "t", not "m"
}
At "m" (message) scope the tracer mirrors only the received copy. That matters
the
moment rtpengine or any other relay rewrites the SDP: the address OpenSIPS
received is not the address the media flows to, so every stream comes out
orphaned. Measured on OpenSIPS 3.6.7 with rtpengine 12.5.1 anchoring: at "t"
every advertised media endpoint was a socket the capture actually saw RTP on
(4 of 4). At "m" it observed 1 of 4.
Note also that proto_hep refuses to start without a HEP listener socket even
when the config only sends — add socket=hep_udp:127.0.0.1:9061 alongside your
hep_id or OpenSIPS exits with No HEP listener defined!.
Where this helps, and where it cannot. It helps where media transits the
machine running the mirror — a proxy anchoring media through rtpengine on the
same box is the case this composition targets. Where media flows end-to-end and
never
crosses that machine, no capture there can see it, and no amount of correlation
changes that: put sipnab’s -d where the media actually is.
Pitfalls:
- HEP is UDP — silently drops if the listener can’t keep up. The
--hep-rate-limit 50000default lets you tune. - A routable HEP listener needs a guard: sipnab refuses a non-loopback
--hep-listenbind unless you pass--hep-allow 192.0.2.0/24(repeatable) or--hep-auth/--hep-auth-file. A loopback bind needs neither. --hep-allowguards the listener and nothing else. HEP that sipnab merely sniffs off the wire — including rtpengine’s mirrored control plane — reaches no socket, so no allowlist applies to it. See rtpengine.md for what a sniffed assertion is worth.- If your central host is reachable by hostname only, set
--mcp-allowed-hostfor the MCP transport too (see Recipe 8). -dwith--hep-listentakes exactly one interface and one listener. sipnab refuses-Iwith--hep-listen(a file’s addresses are historical and belong to third parties, and sipnab keeps them off its active-response path by refusing to transmit for a file run at all); so are--multi-devicewith--hep-listen, and-Owith the pair — the two sources disagree about the link layer, so there is no honest pcap to write. Use--hep-sendto forward the signaling instead.- One sipnab, one mirroring node. The SDP endpoint index keys on address and port with no node dimension, so two nodes advertising the same RFC 1918 socket would overwrite each other and a stream would bind to whichever offer arrived last.
7. Decrypt SIP/TLS via SSLKEYLOGFILE
Problem: TLS-encrypted SIP captures are unreadable without keys.
7a. Live decryption (UA produces keys, sipnab follows)
Build sipnab with the tls feature, or use --features full:
cargo build --release --features tls,hep,api
On the SIP user agent — a different machine from the capture host — set SSLKEYLOGFILE in its environment:
SSLKEYLOGFILE=/tmp/sipua.keylog /opt/myua/bin/start
Then, on the capture host, start sipnab watching that keylog file for live updates:
sudo sipnab -N -d eth0 \
--keylog /tmp/sipua.keylog --keylog-watch
7b. Decrypt a capture you already recorded
Capture the encrypted pcap normally:
sudo sipnab -N -d eth0 -O encrypted.pcap
Later — minutes or months — decrypt it using the keylog the UA wrote during the call:
sipnab -I encrypted.pcap --keylog /tmp/sipua.keylog
7c. Export encrypted packets for Wireshark
The default raw mode writes original packets without embedding TLS keys:
sipnab -I encrypted.pcap --keylog /tmp/sipua.keylog \
-O original.pcap --pcap-export-mode raw
To let Wireshark decrypt the original bytes, explicitly embed a Decryption Secrets Block in PCAP-NG. This file contains TLS keys:
sipnab -I encrypted.pcap --keylog /tmp/sipua.keylog \
--pcapng -O wireshark-friendly.pcapng --pcap-export-mode encrypted+dsb
decrypted plaintext-frame export is not supported. Requesting it exits 2.
7d. Decrypt SRTP from a DTLS keylog
sipnab -I capture.pcap --dtls-keylog /tmp/dtls.keylog
7e. Decrypt traffic from a daemon you cannot restart
Everything above needs the SIP daemon to cooperate: SSLKEYLOGFILE has to be in
its environment when it starts. On a running production Kamailio, OpenSIPS or
Asterisk that means a restart, and a restart is usually the reason you are
looking at the capture in the first place.
eCapture reads the TLS master secrets straight out of the process with eBPF uprobes on the TLS library. It needs nothing from the daemon — no environment variable, no configuration, no restart — and it writes the same NSS keylog format sipnab already consumes.
On the SIP host, as root:
ecapture tls -m keylog --keylogfile=/tmp/sip.keylog
Then point sipnab at that file, exactly as in 7a:
sudo sipnab -N -d eth0 --keylog /tmp/sip.keylog --keylog-watch
-m keylog is the mode for a LIVE capture, because it hands sipnab keys while
sipnab reads the wire itself. eCapture’s -m text emits plaintext, which is a
different artifact: bytes that never existed on the wire in that form.
-m pcap is neither, and an earlier version of this page had it wrong. It
writes the real encrypted frames and embeds the secrets in the file as a
pcapng Decryption Secrets Block, so it produces exactly the artifact this
section argues for – with no live sipnab at all. That is 7i, and it is the
easier recipe of the two when you can work from a file.
Measured, not assumed (2026-08-14): a TLS 1.3 REGISTER over
TLS_AES_256_GCM_SHA384, keys taken from a running process with no
SSLKEYLOGFILE anywhere, decoded by sipnab as
127.0.0.1:53810 -> 127.0.0.1:15300 REGISTER TLS. Verified on both aarch64
(kernel 6.8, no BTF — eCapture falls back to its non-CO-RE bytecode
automatically) and x86_64 (Debian 13, OpenSSL 3.5.6, BTF present).
Pitfalls:
tlsis a build-time feature, not a runtime flag. There is nosipnab --features tlsinvocation; pass--featurestocargo buildand use the resulting binary. To check what a binary carries, runsipnab --version: it prints the version, the commit, and the compiled feature list, sotlsin that list means the build decrypts.sipnab --help | grep -E '\-\-keylog|\-\-tls-key'answers the same question from the flag surface — if the flags appear, the build hastls.- The keylog format is the standard NSS
SSLKEYLOGFILE(one line per session). Same format Firefox/Chrome/curl produce. - TLS 1.3 + ECDH ephemeral handshakes are fully supported via the
ringbackend. - eCapture is a separate program under Apache-2.0; sipnab neither bundles nor links it. It needs
CAP_BPF/CAP_PERFMONor root, and Linux 4.18+ on x86_64 or 5.5+ on aarch64. - A keylog is key material. It decrypts every session it covers, so treat the file as a secret: sipnab disables core dumps once decryption is active for the same reason.
- Keys only appear for handshakes eCapture was running for. Start it before the calls you care about — it cannot recover a session whose handshake it missed.
7f. Decrypt without writing the keys to disk
7e leaves master secrets in a file, and that file decrypts every session it covers — which is why the pitfall above says to treat it as a secret. sipnab can take the same keylog lines over a pipe instead, so they never reach a disk at all.
Hand sipnab the read end of a pipe with --keylog-fd:
sudo sh -c 'ecapture tls -m keylog --keylogfile=/dev/stdout | sipnab -N -d eth0 --keylog-fd 0'
Or use a named pipe, when a supervisor starts the two halves separately:
Create the pipe once:
sudo mkfifo -m 600 /run/sip.keys
then read it as a live stream:
sudo sipnab -N -d eth0 --keylog /run/sip.keys --keylog-watch
--keylog accepts a FIFO and reads it as a live stream. --keylog-fd implies
--keylog-watch, since a descriptor from a running producer has nothing to read
at startup and everything to read later. Pass one or the other, never both.
sipnab cannot start the extractor for you, and that is deliberate. It sets
PR_SET_NO_NEW_PRIVS at startup and every child inherits it, so a process
sipnab spawns can never acquire the CAP_BPF eCapture needs. Start the
extractor from a supervisor and hand sipnab the read end.
Pitfalls:
- sipnab opens a FIFO named by
--keylogbefore it drops privileges, for the same reason it opens capture devices there. A path under/runis unreachable once sipnab has dropped to an unprivileged user or entered a--chroot. - An inherited descriptor needs no privilege at all, so
--keylog-fdworks whatever sipnab drops to afterwards. - Core dumps are still disabled, exactly as for a keylog file: the secrets arrive over a pipe but land in the same process memory.
7g. Read TLS with no keys at all
7e and 7f still need session secrets from somewhere. This recipe needs none: sipnab puts a kernel uprobe on the TLS library’s write function and reads the plaintext before encryption. No certificate, no private key, no keylog, and nothing restarted.
Look before you probe. This installs nothing and answers whether the capture is worth starting:
sudo sipnab --uprobe-list
FLAVOR INODE PIDS LIBRARY
OpenSSL 14166752 1 /proc/982690/root/usr/lib/aarch64-linux-gnu/libssl.so.3
wolfSSL 17433084 1 /proc/982702/root/usr/lib/aarch64-linux-gnu/libwolfssl.so.42.2.0
OpenSSL 21143 12 /usr/lib/aarch64-linux-gnu/libssl.so.3
Then capture. sipnab probes every library listed, not one — a host commonly runs both OpenSSL and wolfSSL, and probing only the one you had in mind misses the rest without saying so:
sudo sipnab -N --uprobe-tls
Narrow it if only one stack is yours to read:
sudo sipnab -N --uprobe-tls --uprobe-flavor openssl
Or name a library yourself, which is the only way to attach to a daemon that has not started yet — discovery can only see what is already mapped:
sudo sipnab -N --uprobe-tls --uprobe-library /usr/lib/x86_64-linux-gnu/libssl.so.3
What this gives up. A uprobe sees the bytes an application handed its TLS
library and nothing about the socket beneath, so dialogs from this source carry
no addresses and port 0. sipnab labels them uprobe:<comm>/<pid> instead —
the process, not a peer. It never invents an address it did not observe.
Pitfalls:
- Needs root (or
CAP_SYS_ADMIN+CAP_PERFMON) and a mountedtracefs. Unprivileged,/proc/<pid>/mapsis readable only for your own processes, so--uprobe-listquietly shows a fraction of the host — run it as root before concluding a daemon is not using TLS. - Containers. The path a containerized process sees names a different
file from sipnab’s namespace. sipnab handles this by matching inodes and
probing through
/proc/<pid>/root, which is why the listing above shows such paths. If you pass--uprobe-libraryby hand for a container, pass the/proc/<pid>/root/...form, or the probe attaches to the host’s copy and captures nothing. - GnuTLS is not probed. Its write function has a different signature, and attaching the OpenSSL probe shape to it would read the wrong register.
- This input can never transmit.
--hep-allow-killhas an equivalent for HEP input; there is deliberately no such flag here, because sipnab has no observed peer to answer to. - A write larger than 2048 bytes arrives truncated, and sipnab marks it as such rather than presenting a fragment as a whole message.
7h. Read TLS and who the peer was
7g gives you the plaintext but no addresses: a uprobe sees the bytes an application handed its TLS library and nothing about the socket beneath, so those dialogs name a process rather than a peer.
The bpf backend closes that gap. It pairs each write with the tcp_sendmsg
that carried it — same thread, back to back — and reports the addresses the
plaintext actually went out on:
sudo sipnab -N --uprobe-tls --uprobe-backend bpf --portrange 0-65535
200 OK 127.0.0.1:15061 -> 127.0.0.1:36160 TCP uprobe:python3/349147#0
REGISTER 127.0.0.1:36160 -> 127.0.0.1:15061 TCP uprobe:python3/349147#1
200 OK 127.0.0.1:15061 -> 127.0.0.1:36172 TCP uprobe:python3/349147#2
Each request and its response share an ephemeral port, and a second connection gets a different one — the pairing binds each write to its own socket.
What it needs, and what it refuses:
- a sipnab built with
--features bpf, which needs a nightly toolchain andcargo install bpf-linker. Without them sipnab still builds and every other backend still works; this one refuses at runtime and names the missing tool; - a kernel with
CONFIG_DEBUG_INFO_BTF— BTF is the BPF Type Format, the kernel’s description of its own structs and where each member sits. Check withls /sys/kernel/btf/vmlinux. sipnab reads the socket layout out of that BTF at load time, so the program keeps working across kernels instead of matching only the one that compiled it; - root, as 7g does.
Asked for on a build or a kernel that cannot run it, sipnab refuses rather
than falling back to tracefs. The addresses are the only reason to choose
this backend, and a silent downgrade would hand you a capture with none.
Pitfalls:
- Widen
--portrange. A TLS trunk on 5061 is the exception, and the port a uprobe reports is whatever the socket actually used — often ephemeral. The default range drops them. - A write the TLS library buffered rather than sent arrives with no addresses, exactly like a 7g capture. sipnab does not guess a peer for it, because a guessed one would be indistinguishable from an observed one.
- The kernel and the daemon may disagree about which symbol carries the
plaintext: OpenSSL 3 applications increasingly call
SSL_write_ex. Check withnm -D --undefined-only /path/to/app | grep SSL_writeand pass--uprobe-symbolif it differs.
7i. One file, taken off the host, that decrypts itself
7e and 7f keep sipnab running beside the daemon. This recipe does not run sipnab on the SIP host at all. eCapture writes one file there, you copy it somewhere else, and sipnab reads it with no keylog, no flags and no configuration – because the secrets are inside the file.
On the SIP host, as root:
ecapture tls -m pcap -i eth0 -w /tmp/sip-tls.pcapng tcp port 5061
Stop it with Ctrl-C when you have the calls you need, copy the file off, and read it anywhere:
sipnab -N -I sip-tls.pcapng --portrange 1-65535 --report
That is the whole recipe. There is no --keylog, because
-m pcap puts the master secrets in the file as a pcapng Decryption Secrets
Block, and sipnab reads a DSB automatically. Wireshark reads the same file
for the same reason.
Measured end to end, not assumed (2026-09-09). eCapture v2.5.2, keys taken
by uprobe from a Python 3.13 process running OpenSSL 3.5.6 that had no
SSLKEYLOGFILE and set no keylog of its own, over a TLS 1.3
TLS_AES_256_GCM_SHA384 session carrying a complete SIP dialog – INVITE
split across two records, 100, 180, 200 with SDP, ACK, BYE, 200.
sipnab read the resulting 50-packet file and reported:
TLS decryption active: 6 secret(s) from embedded DSB in sip-tls.pcapng
Call-ID From To State Code Msgs
[email protected] uac echo Completed 200 7
Which recipe to reach for:
| You want | Use |
|---|---|
| A live view, decrypted as calls happen | 7e or 7f – sipnab runs beside the daemon |
| One artifact to take away, open later, or hand to somebody | 7i – nothing runs on the host afterwards |
| The real wire bytes, verifiable independently | Either. Both keep the encrypted frames; neither writes plaintext |
Pitfalls, each one measured:
-m pcapneeds more from the kernel than-m keylogdoes, and this is the one that stops most people. It attaches a TC classifier to the interface as well as the uprobes, so the kernel needsCONFIG_NET_CLS_BPF. On a kernel without it, eCapture starts, loads its bytecode, and then fails withcouldn't add a ingress filter to interface 1: netlink receive: no such file or directory. Measured on Linux 6.8.12-rt-tegra, wherezcat /proc/config.gz | grep NET_CLS_BPFprints# CONFIG_NET_CLS_BPF is not set. Check that before you plan around this recipe.-m keylog(7e) needs only the uprobes and runs on that same kernel.-itakes a real interface, and it is not optional.-m pcapcaptures through the interface, not through the process, so-i lofor loopback traffic and the SIP-facing NIC otherwise.- The port range is yours to widen. sipnab decrypted the file above and
still reported no calls on the first run, because the lab used port 15061
and the default
--portrangeis 5060-5061. It said so rather than staying silent –SIP outside --portrange 5060-5061 is being skippednames the busiest port it saw. - eCapture needs no BTF. On a kernel with none it falls back to its own
non-CO-RE bytecode and logs the file it loaded
(
bytecode/openssl_3_0_0_kern_noncore.o). A missing/sys/kernel/btf/vmlinuxis not a reason to skip this. - The file is key material. A DSB decrypts every session in it, so the pcapng is as sensitive as the keylog in 7e, and more portable. Treat it that way when you copy it off the host.
- Same as 7e: eCapture is a separate Apache-2.0 program that sipnab neither bundles nor links, and it captures only handshakes it was running for.
8. Run sipnab as an MCP server
Problem: You want an AI agent (Claude Code, Claude Desktop, anything MCP-capable) to query a capture without you typing CLI flags.
8a. Drive sipnab from an agent on the same machine
One-shot: the agent reads a pcap you already have.
sipnab -N --mcp -I capture.pcap --quiet
The same, against a live capture instead of a file:
sudo sipnab -N --mcp -d eth0 --quiet
Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"sipnab": {
"command": "sipnab",
"args": ["--mcp", "-N", "-I", "/path/to/capture.pcap", "--quiet"]
}
}
}
Claude Code (in your project directory):
claude mcp add sipnab -- sipnab -N --mcp -I "$PWD/capture.pcap" --quiet
8b. Drive sipnab from an agent on another machine
Generate the bearer token once, when you first set the host up. openssl rand overwrites /etc/sipnab/mcp-token — run it against a live server and it starts serving a secret none of the configured agents hold, with no way to recover the old one.
# Run all of these, in order.
mkdir -p /etc/sipnab && chmod 0755 /etc/sipnab
openssl rand -hex 32 > /etc/sipnab/mcp-token
chmod 0600 /etc/sipnab/mcp-token
Then run sipnab listening on a private network interface. This is the every-boot command: it reads the token file, it does not create one.
sipnab -N --mcp --mcp-transport http \
--mcp-bind 0.0.0.0:8731 \
--mcp-token-file /etc/sipnab/mcp-token \
--mcp-allowed-host capture.example.com \
--hep-listen 0.0.0.0:9060 --hep-allow 192.0.2.0/24 --quiet
The agent connects to http://capture.example.com:8731/mcp with Authorization: Bearer <token>.
8c. Test the JSON-RPC handshake from a shell
Each probe below sends the bearer token, so read it into the shell first. The three requests are independent — none of them carries a session on to the next, so run whichever one you need.
TOKEN=$(cat /etc/sipnab/mcp-token)
Initialize, pretending to be an MCP client. Keep the session id the server
returns: every later request must carry it in Mcp-Session-Id. The transport
rejects one that does not, answering HTTP 422 Unexpected message, expect initialize request:
# Run all of these, in order.
SID=$(curl -sS -D - -o /dev/null http://capture.example.com:8731/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18",
"capabilities":{},
"clientInfo":{"name":"curl","version":"0"}}}' \
| awk 'tolower($1) == "mcp-session-id:" { print $2 }' | tr -d '\r')
Send the initialized notification the protocol requires before any tool call.
It answers 202 Accepted with no body:
curl -sS http://capture.example.com:8731/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
List every registered tool:
curl -sS http://capture.example.com:8731/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
Call find_problems and get JSON of the problematic dialogs:
# Run all of these, in order.
curl -sS http://capture.example.com:8731/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"find_problems",
"arguments":{"kinds":["one-way","nat-issues"]}}}'
The tools/list response is a standard JSON-RPC envelope with a result.tools array (descriptions and input schemas truncated here):
{"jsonrpc":"2.0","id":2,"result":{"tools":[
{"name":"list_dialogs","description":"...","inputSchema":{"type":"object","properties":{"...":{}}}},
{"name":"get_dialog_report","description":"...","inputSchema":{"...":"..."}},
{"name":"find_problems","description":"...","inputSchema":{"...":"..."}}
]}}
Every registered tool appears, grouped here by what they do. The table in
docs/mcp-tools.md is the authoritative list, and
mcp_tool_table_lists_every_registered_tool in tests/docs_drift_test.rs
asserts it against the registry — the grouping below is a reading aid, not a
second source of truth. (This list said “all 25 tools” and enumerated 25 until
2026-08-05, by which point the registry held 31. The six it had never gained
were capture_health, explain_rule, find_correlated, lint_dialog,
save_findings and validate_message.)
- Browse and quote the capture —
list_dialogs,get_dialog,get_dialog_report,get_message,search_messages,search_by_time,tail_dialogs,render_ladder,compare_dialogs,find_correlated,show_evidence - Diagnose —
find_problems,triage_call,diagnose_registration,check_codec_negotiation,explain_response_code,get_sdp_timeline,rtp_stats,security_findings,capture_health - Check a message or dialog against the RFCs —
lint_dialog,validate_message,explain_rule - Ask about the session itself —
capture_status,server_capabilities,list_captures - Write a file or a note, swap the capture, or end the run —
export_capture,export_audio,save_findings,open_capture,shutdown_server
Only that last group reaches past the query surface, and each member needs a flag you passed at startup: the two exports write only under --mcp-file-root, save_findings records only under --mcp-allow-save-findings, open_capture acts only under --mcp-allow-open-capture, and shutdown_server only under --mcp-allow-shutdown. All five still appear in tools/list when you omit those flags, because sipnab registers the tools unconditionally and refuses the call instead. Seeing shutdown_server listed does not mean an agent can stop your capture.
Pitfalls:
- Stdout is the JSON-RPC wire in stdio mode. Use
--quietand don’t combine with--json/--report/etc. — sipnab refuses to start. - Non-loopback bind without a token: refused at startup. Loopback bind needs no token.
- Pass
--mcp-allowed-hostwhen the client connects via the actual hostname (rmcp’s default Host allowlist is justlocalhost/127.0.0.1/::1).
9. Graph call rate, response codes and PDD over time
Problem: You want a dashboard tracking call rate, response codes, and PDD over time.
9a. Use the bundled stack
Clone the repository and take a copy of the sample environment. Do this once per host: cp .env.example .env overwrites .env, so on a host where you have already edited it, skip straight to starting the stack.
# Run all of these, in order.
git clone https://github.com/NormB/sipnab.git
cd sipnab/contrib/observability
cp .env.example .env
If sipnab runs on a different host, point the stack at it before starting: echo 'SIPNAB_HOST=192.0.2.10' >> .env, or the hostname (capture.example.com) if that is how it resolves.
Start the stack from contrib/observability — docker compose reads its compose file out of the working directory, so a fresh shell needs the cd above first:
docker compose up -d
This boots Prometheus (:9090), Grafana (:3000, admin/admin), an OTel Collector (:4317/:4318), and Tempo. The included Grafana dashboard provisions automatically — log in and look for the sipnab folder.
9b. Run sipnab so Prometheus can scrape it
Both listeners below bind a routable address, and sipnab refuses either one without a credential — so each command supplies one, and the scrape job has to send the matching half.
A standalone metrics endpoint, which takes HTTP Basic:
sipnab -N -d eth0 --metrics 0.0.0.0:9100 --metrics-auth-file /etc/sipnab/metrics.cred --json
Or serve the metrics from the REST API, so one port carries both. That side takes the same Bearer token as every other REST route:
sipnab -N -d eth0 --api 0.0.0.0:9100 --api-signing-key-file /etc/sipnab/signing.key
9c. Verify the scrape
From the Prometheus host, confirm the target is up:
curl -s http://localhost:9090/api/v1/query?query=up{job=\"sipnab\"} | jq
Then spot-check a metric value:
curl -s 'http://localhost:9090/api/v1/query?query=rate(sipnab_messages_total[1m])' | jq
9d. Query the metrics with PromQL
# Call rate (per method)
rate(sipnab_messages_total[5m])
# Active dialogs (in-progress)
sum(sipnab_dialogs_total{state=~"trying|ringing|incall"})
# Setup time p95
histogram_quantile(0.95, rate(sipnab_pdd_seconds_bucket[5m]))
# RTP MOS p10 (worst 10%)
histogram_quantile(0.1, rate(sipnab_mos_bucket[5m]))
Pitfalls:
- The dashboard ships with the metric names sipnab actually emits. If you wrote a custom panel using older docs, double-check against the Prometheus metrics reference.
- Some metrics (
sipnab_responses_total,sipnab_security_alerts_total) exist in name only, with nothing wired — they’ll stay empty until upstream populates them. Don’t put alerts on them today.
10. Detect SIP scanners and auto-block via fail2ban
Problem: Your honeypot or edge box is getting probed by friendly-scanner, sipvicious, etc.
10a. Detect + log
sudo sipnab -N -d eth0 \
--kill-scanner \
--alert syslog \
--json
--kill-scanner actively responds to known scanner User-Agents (uses a scanner-kill worker thread). The response code defaults to 200. Pass --kill-response 403 (or any 100–699 code) to change it. --alert syslog writes alerts to LOCAL0 so you can pick them up from /var/log/syslog (--syslog is the equivalent boolean form).
10b. Wire to fail2ban
--fail2ban is a boolean flag — it switches sipnab’s stdout to fail2ban-friendly log lines. Pipe to a file (or run under systemd and capture the unit’s stdout).
--fail2ban writes detections, so it needs a detector switched on beside it: --kill-scanner for scanner_detected, --reg-flood for reg_flood. On its own it selects the format and nothing produces lines for it, and a jail reading an always-empty file never says so. Detections carried by HEP input never reach the log without --hep-allow-kill, for the reason the kill path has always refused them: the inner addresses are the sender’s claim, and a jail line would ban whatever address the sender chose. sipnab warns once at startup when --fail2ban meets HEP input without the opt-in, so the empty file is not mistaken for an all-clear.
# Run sipnab with fail2ban-format output, write to a logfile
sudo sipnab -N -d eth0 --kill-scanner --fail2ban \
>> /var/log/sipnab/fail2ban.log 2>&1
Measure it against your own traffic before any of it reaches fail2ban. Point sipnab at a capture of a normal hour and count who it would have banned:
sipnab -N -I trunk.pcap --kill-scanner --fail2ban | grep -oE 'src=[^ ]+' | sort | uniq -c | sort -rn
Every address in that list is one the jail below would ban. Use ignoreip in the jail for the peers you already trust.
10c. Block a scanner with a rule sipnab wrote, after reading the evidence
Counting src= fields tells you who. It does not tell you whether banning them costs you a customer. --recommend-block groups the detections by source and prints one block per accused address, carrying the evidence, the counter-evidence and a rule in the dialect you name (fail2ban, nftables, iptables or all):
sipnab -N -I trunk.pcap --kill-scanner --reg-flood --recommend-block all --quiet
sipnab recommends and does not apply. sipnab ran none of that output, contacted no firewall and holds no credential anywhere in sipnab. It prints text and stops. The line to read in every block is COUNTER-EVIDENCE:, which says one of three things:
also completed a registration or a call— the source is a working peer and a block disconnects it, so thenftablesandiptablescommands arrive commented out and the fail2ban jail arrives withignoreipalready filled in.none— nothing in this capture says the source has a relationship, and the commands are live.UNKNOWN— no scanner detector ran, so sipnab asked nothing. The commands are live, and the block tells you to re-run with--kill-scannerbefore acting on them.
The third case is the one worth pausing on: “asked and it had not” and “never asked” produce the same silence, and only the first is a reason to feel safe.
What the behavioral rules actually test
Neither behavioral rule fires on volume, because volume does not separate reconnaissance from operation. A trunk sends OPTIONS keepalives continuously by design — that is how each end learns the other is alive — and an SBC fronting a hunt group reaches dozens of distinct extensions a second. Both rules therefore need an OUTCOME as well as a rate:
| Signal | What it counts | What arms it |
|---|---|---|
behavioral | More than 10 probe transactions from one source in 5s | Probing evidence |
enumeration | More than 5 distinct target extensions in 5s | Probing evidence |
Probing evidence means one of two things inside that window:
- 5 refusals — final responses past
4xxthat are neither an auth challenge (401,407) nor an ordinary call outcome (408,480,486,487,488,491,600,603). A5xxblames the server, so it counts for nothing. Only a refusal on a probe transaction counts, matched by theViabranch: a481answering a strayNOTIFYsays nothing about the OPTIONS beside it. - 5 probes with no answer, still unanswered half a second later, and outnumbering the ones that drew a reply. Any response settles a probe, including a
100 Trying— the question is whether anything is there, and waiting for the final response would count every ringing call as a probe into a hole. Retransmissions of one request count once, so a peer resending an INVITE is one probe rather than four.
A source that has completed a registration or a call — a 2xx to its REGISTER or its INVITE — needs four times either number. Answering its OPTIONS earns it nothing, because sipnab answers anyone’s.
Two consequences follow for anyone reading a file rather than watching an interface. A capture of one direction holds no responses, so sipnab stands the unanswered test down entirely and leaves only the signature and refusal rules. And a capture taken upstream of whatever generates your 404s — between two proxies, say — never shows the refusals either.
Sample log line shape (from src/output/fail2ban.rs):
2026-05-05 12:34:56 sipnab[12345]: scanner_detected src=203.0.113.42 ua="friendly-scanner" method="OPTIONS"
2026-05-05 12:34:57 sipnab[12345]: scanner_detected src=203.0.113.43 ua=- method="REGISTER"
2026-05-05 12:34:57 sipnab[12345]: reg_flood src=203.0.113.42 count=37
The ua= and method= values are quoted, and a bare - means the request
carried none — an absent User-Agent is itself a scanner signal, so it is worth
keeping distinct from a client that sends the string -, which renders as
"-". Both fields carry attacker-influenced text (method can be a
non-standard token), so quoting is also what stops a crafted value forging a
second src= field inside the line, and sipnab escapes embedded " and \. src=
carries no quotes: it holds a parsed IP address, not text from the wire.
/etc/fail2ban/filter.d/sipnab.conf:
[Definition]
failregex = ^.*sipnab\[\d+\]: scanner_detected src=<HOST>.*$
^.*sipnab\[\d+\]: reg_flood src=<HOST>.*$
ignoreregex =
/etc/fail2ban/jail.d/sipnab.local:
[sipnab]
enabled = true
filter = sipnab
logpath = /var/log/sipnab/fail2ban.log
# Never ban the boxes the phone system needs. List every carrier SBC, every
# trunk peer and the PBX itself, before enabling the jail — these are the
# addresses that talk to you most, so they are the ones a detector tuned for a
# honeypot flags first.
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24 198.51.100.10
findtime = 600
# Several detections inside findtime, not one. A single enumeration alert is
# how a busy trunk looks, and `maxretry = 1` turns any one of them into a ban.
maxretry = 5
# An hour, not a day. Long enough to shed a scan, short enough that a wrong
# ban of your own carrier heals without an engineer.
bantime = 3600
action = iptables-allports
Verify the filter against a real log file before enabling the jail, and check the ban list afterwards — fail2ban-regex reports what it would have matched without banning anything:
fail2ban-regex /var/log/sipnab/fail2ban.log /etc/fail2ban/filter.d/sipnab.conf
10c. Detect toll fraud and wangiri call-back bait
Symptom: an unexpected spike of international or premium-rate calls, bursts of short calls to one number prefix (wangiri call-back bait), or sequential dialing through a number range.
# Live fraud heuristics on the edge box (batch mode required)
sudo sipnab -N -d eth0 --fraud-detect --alert syslog
Where a call is going is a signal the shape-based heuristics cannot see. Name the destinations you never expect to dial and sipnab reports an INVITE to one the moment it sees it, no pattern required:
sipnab -d eth0 -N --fraud-detect --fraud-destination DO,VG,MA
sipnab reads numbers through the common international prefixes and resolves
them by the longest calling code, so +1 809 555 0100 is the Dominican Republic
and not the United States. A number dialed with no prefix is domestic and never
matches, and every other fraud finding now names the resolved destination beside
its detail.
--fraud-detect runs three heuristics over INVITE traffic per source IP: VolumeSpike (call rate far above the rolling baseline), Wangiri (repeated short calls to the same number prefix), and SequentialScanning (consecutive destination numbers). Alerts fire through the same alert engine as the scanner detectors, so --alert syslog and --alert-exec both work.
You should see alert lines like:
[ALERT] fraud src=203.0.113.42 Wangiri: 4 short calls to prefix '+44900' in 60s
[ALERT] fraud src=203.0.113.42 SequentialScanning: sequential dialing detected: 3 consecutive numbers ending at 15550104
[ALERT] fraud src=203.0.113.42 VolumeSpike: 40 calls in 60s (baseline: 1.5/min)
What to look for: a Wangiri alert on a premium-rate prefix (+44 9xx, +2xx IRSF ranges) is the classic revenue-fraud signature — block the destination prefix at the trunk, not just the source IP. SequentialScanning from an external source usually precedes a toll-fraud attempt: feed the source IP to fail2ban (10b) and review outbound dial permissions.
10d. Run your own script when an alert fires
For exec hooks instead of syslog/fail2ban:
sudo sipnab -N -d eth0 --kill-scanner \
--alert-exec '/usr/local/bin/notify-slack.sh "$SIPNAB_RULE" "$SIPNAB_SRC" "$SIPNAB_DETAIL"'
Alert data reaches the hook as the SIPNAB_RULE, SIPNAB_SRC, and SIPNAB_DETAIL environment variables — never interpolated into the command string. sipnab rewrites only the three legacy placeholders %rule, %src, and %detail into those $SIPNAB_* references for you. Anything else (%type%, %source_ip%, …) reaches the shell verbatim.
The hook is rate-limited (--exec-rate-limit 10 default) and runs in a sandboxed process.
Pitfalls:
- The scanner-kill worker needs
CAP_NET_RAWto forge SIP responses. Run sipnab as root or with capabilities — privilege drop happens after the worker opens its raw socket. --kill-ua "<regex>"adds a custom User-Agent pattern beyond the built-in scanner list.
11. Find why a call sounds bad in one direction only
Problem: A call sounds bad in one direction. The codec/ptime might differ between legs.
The asymmetry signals (Phase 8.7) live on sipnab’s internal MediaDiagnosis struct and surface through the filter DSL — not the dialog JSON output’s diagnosis block. --filter accepts the alias name directly (codec-asym) and falls back to the raw DSL expression if it isn’t an alias. Both forms are equivalent.
All five asymmetry checks at once, via the problems DSL alias — the --problems flag expands to the same expression, so either spelling works:
sipnab -N -I capture.pcap --filter problems --json
Targeted, one signal at a time:
sipnab -N -I capture.pcap --filter codec-asym --json— different codec on each legsipnab -N -I capture.pcap --filter ptime-asym --json— different packetization interval on each legsipnab -N -I capture.pcap --filter payload-asym --json— same codec, different dynamic payload typesipnab -N -I capture.pcap --filter duration-asym --json— the two streams ran for noticeably different lengthssipnab -N -I capture.pcap --filter late-media --json— media started well after the answering 200 OK
The equivalent raw-DSL forms, for the two most common of those:
sipnab -N -I capture.pcap --filter "codec_asymmetry == true" --jsonsipnab -N -I capture.pcap --filter "ptime_asymmetry == true" --json
Multiple signals OR’d together require raw DSL — an alias name covers only one signal each:
sipnab -N -I capture.pcap \
--filter "codec_asymmetry == true OR ptime_asymmetry == true OR late_media == true" \
--json
From an MCP client, multiple alias names go through find_problems instead: tools/call find_problems {"kinds": ["codec-asym", "ptime-asym", "late-media"]}. See the MCP docs for the full client-side syntax.
What to look for:
codec_asymmetry: trueon a call from PSTN to internal: usually a transcoding policy that fired in one direction only.ptime_asymmetry: truebetween two SIP UAs: one is usingptime=20, the otherptime=30. Some downstream jitter buffers can’t handle the mismatch.payload_asymmetry: true: same codec, but each side picked a different dynamic payload type number. Causes audio cut-out on RFC-strict implementations.late_media: true: media starts noticeably after the answering 200 OK. Usually means an SBC is doing late-attach NAT — first real RTP arrives only after media-binding.
Pitfalls:
sipnab -N --filter '<expr>' --jsonemits per-message records for every message of every matching dialog. Pipe throughjq -s 'unique_by(.call_id)'if you want one record per affected call.- The
diagnosisblock in CLI--jsonoutput and in the REST API today only exposesone_way_audio,nat_mismatch,no_media, and free-formhints. The five asymmetry booleans are filterable via the DSL but aren’t in the JSON shape — if you need them in your output, use the MCPfind_problemstool. JSON--call-reportuses the same dialog projection and does not add them.
12. Generate a call report (text / Markdown / JSON)
Problem: A support ticket needs full call details attached.
In -N (non-interactive) mode, sipnab normally prints each captured SIP message to stdout and then emits the report. Pass --no-cli-print to suppress the per-message dump so only the report reaches stdout. (-N is not optional: without it sipnab tries to start the TUI and the report output never reaches stdout.)
Markdown, to paste into a ticket or a markdown editor:
sipnab -N -I capture.pcap --call-report 'abc123@host' --markdown --no-cli-print > ticket.md
Plain text, the default report format:
sipnab -N -I capture.pcap --call-report 'abc123@host' --no-cli-print > ticket.txt
JSON, for a tool on the other end:
sipnab -N -I capture.pcap --call-report 'abc123@host' --json --no-cli-print > ticket.json
The report covers: SIP message timeline, SDP offers/answers, RTP stream stats per direction, computed timing (PDD, setup time, retransmits), and the diagnosis engine’s findings.
Tip: combine with Recipe 3’s filter to bulk-generate reports for every failed call. The CLI --filter outputs per-message records, so deduplicate to call_ids first:
# Run all of these, in order.
mkdir -p /tmp/reports
# First pass: enumerate matching calls (no --no-cli-print here — we want the
# per-message JSON so jq can extract call_id).
sipnab -N -I capture.pcap --filter "state == 'Failed'" --json 2>/dev/null \
| jq -r '.call_id' | sort -u \
| while read cid; do
# Second pass per call: --no-cli-print so only the report is written.
sipnab -N -I capture.pcap --call-report "$cid" --markdown --no-cli-print \
> "/tmp/reports/$(echo "$cid" | tr '/' '_').md"
done
Compatibility note:
--no-cli-printarrived in v0.3.2. On older binaries strip the leading per-message text by piping throughsed -n '/^# Call Report:/,$p'(markdown) orawk '/^{$/{found=1} found'(JSON).
13. Export RTP audio as WAV
Problem: A call sounds bad. You want the actual audio to listen to or share.
Export a WAV from the TUI
sipnab -I capture.pcap
# → select the call in the call list (Up/Down)
# → press Tab to switch to the RTP stream view
# → highlight a stream
# → F2 to open the Save dialog
# → cycle the format (Tab or Up/Down) until you reach "WAV — Decoded G.711 audio per RTP stream"
# → Enter to save
A timestamped .wav lands at the path you choose. The Save dialog also exposes PCAP, PCAP-NG, TXT, JSON, NDJSON, CSV, HTML, Markdown, RTP JSON, and SIPp XML formats — WAV is the format you want for audio.
Live audio playback (TUI)
If you’ve built with the audio feature (in default), press Enter on a stream to open Stream Detail, then P plays that stream through your local audio device. Only Stream Detail binds that key — the stream list ignores it.
Check whether the file holds the whole call
Every exported WAV carries a comment recording its origin — the sipnab
version, the mechanism (sipnab-capture), and whether anything is missing.
Read it with any tool that shows RIFF metadata:
ffprobe -v error -show_entries format_tags -of default=noprint_wrappers=1 call.wav
A complete file says No omissions recorded. A partial one says INCOMPLETE
and names the reason, because these are different problems:
| The note says | What happened | What to do |
|---|---|---|
the payload ring wrapped and dropped N earlier frame(s) | The call was longer than the buffer. The file holds the END of it. | Raise [limits] max_audio_frames |
N of M stream(s) on this call are NOT in this file | A WAV carries two channels; a three-legged call does not fit | Export the legs separately |
(G729) beside that | A stream’s codec is not one sipnab decodes | Nothing — the audio is not recoverable from this capture |
N frame(s) failed to decode | The decoder rejected individual Opus frames | Check for truncation (--snaplen) |
media in only ONE direction reached this capture | sipnab saw no stream going the other way. Either the call really was one-way, or the capture point only sees one leg | Check where the tap sits before treating it as a fault |
sipnab writes the note after the audio, so the first 44 bytes stay a standard WAV header and every player reads the file normally.
Pitfalls:
- Supported codecs for WAV decode and playback: G.711 µ-law (PT 0), G.711 A-law (PT 8), Opus (dynamic PT). sipnab matches codec names case-insensitively, as SDP allows. Other codecs (G.729, AMR, etc.) aren’t decoded today.
- An export refusing with “No audio payload retained” is telling you about the run, not the call. It means this run kept no payload to decode — most often because retention was off. It is not a finding that the call was silent.
- A failed audio device (headless servers, Tegra without ALSA) no longer crashes the TUI — it disables playback gracefully and surfaces a message suggesting F2 → WAV as an offline alternative.
- A CLI batch audio-export flag does not exist today. The library functions (
rtp::audio_export::export_stream_to_wav,export_dialog_to_wav) are available if you want to build it; until then, scripted batch export means driving the TUI underexpect/tmuxor writing a small Rust binary that links the library.
13b. Export one call to a conversation archive as a vCon
--export-vcon writes one observed dialog as a vCon container — the IETF
interchange format a conversation archive, a compliance store or an
agent-facing pipeline already reads. Handing one of those a pcap makes the
decoding their problem. Handing them a vCon does not.
sipnab -N -I capture.pcap --export-vcon '[email protected]' --vcon-out call.vcon
Straight into a conserver, without a file in between:
sipnab -N -I capture.pcap --export-vcon '[email protected]' | curl -fsS -X POST "$CONSERVER/vcon" -H 'Content-Type: application/json' -H "Authorization: Bearer $CONSERVER_TOKEN" --data-binary @-
Read the caveat before you trust the contents. Every body is a JSON-encoded
string, which means jq needs a parse on the way in:
sipnab -N -I capture.pcap --export-vcon '[email protected]' | jq -r '.analysis[0].body | fromjson | .capture_completeness.note'
Needs a build carrying the non-default vcon feature. sipnab --version lists
what yours has. As a library, examples/export_vcon.rs
is the same thing as a program you can run. The vCon page covers what
a consumer may and may not conclude from a container sipnab wrote.
14. Analyze a pcap without installing anything
Problem: You don’t want to install anything. The pcap is on your laptop. You want to look at it.
Open https://sipnab.com/analyze/ in any modern browser. Drag-and-drop a pcap or .pcapng file. Everything runs locally via WebAssembly — the pcap never leaves your machine.
The analyze page supports .pcap, .pcapng, .cap (pcap format), and their gzip-compressed variants (.pcap.gz, .pcapng.gz — decompressed transparently, with a notice), and gives you the same call list, ladder diagram, RTP stream view, search, and filter DSL as the native TUI. Keyboard shortcuts match the TUI (? opens the help popup).
Pitfalls:
- WASM has no network access — live capture is native-only.
- Very large pcaps (>200 MB) may strain browser memory. Use the native
sipnab -Nfor those.
15. Check a capture against the RFCs
Problem: A call works “most of the time”. Nothing in the ladder looks wrong, and both vendors say their side is fine.
--lint reads the signaling against RFC 3261 and reports what violates it, citing the section:
sipnab -N -I capture.pcap --lint
Each finding names what sipnab saw and what the RFC requires:
error: SIP-3261-12.1.1-CONTACT-MISSING-IN-2XX [call-id@host] §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.
(RFC 3261 §12.1.1) observed=2xx to INVITE with no Contact header field
expected=Contact: <sip:user@host>
Lint: 4 finding(s) across 3 dialog(s)
That is the whole value: observed= and expected= are what you paste into a
vendor ticket, and the § reference is what ends the argument.
Use it as a gate in CI, where a violation should fail the build:
sipnab -N -I regression.pcap --lint --lint-fail-on error
Pitfalls:
--lint-fail-on errorexits non-zero when it finds something — that is the point, but it means a shell withset -estops there. Exit 3 is “lint findings at or above the threshold”, not a crash.--lint-fail-on warningis stricter than most real traffic survives. Start aterror.- Findings are per dialog. A capture with one broken proxy repeats the same rule many times;
--lint-max-per-rulecaps the noise.
16. Analyze a capture into one machine-readable verdict
Problem: You have a pipeline, not a person. It needs one answer per capture, not a stream of messages.
sipnab -N -I capture.pcap --analyze
sipnab -N -I capture.pcap --json-analyze
--json-analyze emits one object, not one line per finding — deliberately.
The frames read and the dialogs examined are properties of the run, not of any
finding, so a clean capture still serializes to something that states them. A
per-finding stream would make “no findings” and “never ran” identical, which is
the distinction a pipeline most needs.
Pitfalls:
- This is a summary, not a substitute for
--report: it tells you the capture’s verdict, not each stream’s jitter. - A capture whose SIP sits outside
--portrangeanalyzes cleanly because it saw no SIP. Check the packet counts in the same object before trusting a green result.
17. Inspect what NAT did to a call
Problem: One-way audio, and the SDP addresses look like private space. You suspect NAT traversal failed but cannot prove where.
sipnab -N -I capture.pcap --stun
sipnab -N -I capture.pcap --json-stun
You get one record per STUN/TURN transaction and per TURN allocation, each
carrying a record field naming which it is. That is what tells you whether
the endpoint ever learned its public address, whether the relay allocation
succeeded,
and whether the candidate it then advertised in SDP matches either.
Pitfalls:
- STUN rides on the media ports, so a capture filtered to port 5060 contains none of it. Capture the media range too, or use
--portrange. - A missing STUN transaction is not proof of a broken client: an endpoint with a public address does not need one.
18. Collect messages by call, host or method
Problem: A busy capture interleaves twenty calls. Reading it message by message means reconstructing each call in your head.
sipnab -N -I capture.pcap --group-by call-id
sipnab -N -I capture.pcap --group-by src --json
sipnab emits messages sharing the field together, reordered but not
reformatted, so --json output stays one valid object per line and anything
downstream keeps working.
Accepts call-id, from, to, method, src, dst.
Pitfalls:
- Requires
-N/--no-tui. - It buffers until the capture ends, so it is an offline tool. On a live device you get nothing until you stop it.
--max-groups(default 100000) bounds that buffer. A capture with more distinct keys drops the excess rather than growing without limit.
19. Read a very large capture faster
Problem: A multi-gigabyte capture takes long enough that you stop using the tool.
sipnab -N -I huge.pcap --cores 8 --report
Offline, --cores N runs N reconstruction workers, sharding packets by host
pair, each with private dialog and RTP-stream stores.
Pitfalls:
- It covers reconstruction and
--report/--json. Per-message output ordering, the security detectors and SRTP decryption stay on the single-threaded path regardless — so a run that needs those gains nothing. - On a live device
--coresmeans something different: N capture sockets, which widens capture, not analysis. Processing stays on one thread either way. See tuning. --coresand--metricsdo not combine usefully offline; the run exits before a scrape lands, and sipnab says so.
20. Verify the exact bytes behind a finding
Problem: You are disputing a finding with a vendor. Paraphrasing the packet is not evidence.
Every --json, --report, REST and MCP result carries a frame pointer:
"frame": "capture.pcap#3@86eadc8a324bb487"
Hand it straight back to sipnab:
sipnab --show-frame 'capture.pcap#3@86eadc8a324bb487'
VERIFIED capture.pcap#3@86eadc8a324bb487
348 bytes, frame 3 of capture.pcap
00000030 30 20 34 30 33 20 46 6f 72 62 69 64 64 65 6e 0d |0 403 Forbidden.|
VERIFIED is the word that matters. With the digest, sipnab checks the
frame’s bytes against it and refuses a capture that has since rotated,
truncated or recompressed, rather than answering with whatever now sits at that
position. Without a digest — the form a human types — sipnab prints the frame
and marks it UNVERIFIED, because it has nothing to check against.
Pitfalls:
- Quote the pointer.
#starts a comment in most shells and everything after it disappears. UNVERIFIEDis not a warning about that packet; it means you did not give sipnab a way to prove it is the same file.
21. Detect your own fault patterns with a plugin
Problem: You have a site-specific fault pattern. A filter can select the calls, but you want sipnab to diagnose it — as a finding, with evidence, everywhere findings appear.
sipnab -N -I capture.pcap --plugin ./my-detector.wasm --report
Findings appear beside the built-in ones, in the same shape, so --call-report,
the TUI and the JSON all render yours with no extra work.
Before loading one someone sent you, know what you are trusting:
A plugin has no imports at all. Not a restricted set — none. No WASI, no filesystem, no network, no clock, not even logging. A module that imports anything fails to instantiate. Fuel metering bounds CPU, a 16 MiB ceiling bounds memory, 4 MiB bounds each reply, and a trap or exhausted budget fails that dialog’s plugin findings and nothing else.
What is not bounded is what it reads: a plugin sees every dialog sipnab
reconstructs, including Authorization headers and MESSAGE bodies. It cannot
send them anywhere, but it chooses what to report. Load one the way you would
run a script — from someone you trust, or after reading it.
Pitfalls:
- Needs a build with
--features plugins, which is not in the default feature set.sipnab --versiontells you whether the binary in front of you can load one. - If a filter or a
jqpipeline already answers your question, use those — no build step, no trust decision. See plugins.
22. Measure whether the loss is yours or the network’s
Problem: RTP loss figures look terrible. Before you escalate to the carrier, you need to know the capture itself was not the thing dropping packets.
A lossy capture does not produce a smaller answer. It produces a wrong one. sipnab counts a dropped RTP packet as network loss that never happened, so MOS reads worse than the call actually was.
sipnab polls libpcap’s kernel counters once a second and reports two numbers that mean different things:
PACKETS ARE BEING DROPPED on 'eth0' (kernel buffer: 18432, interface/driver: 0).
| Counter | libpcap field | What it means | What fixes it |
|---|---|---|---|
| kernel buffer | ps_drop | The ring was full when the packet arrived. sipnab was not draining fast enough. | a bigger -B, a narrower BPF filter, a lower --snaplen |
| interface/driver | ps_ifdrop | The NIC or its driver discarded it before libpcap ever saw it. | a bigger buffer cannot fix this — it is the NIC |
The same two numbers appear in four places, so whichever surface you are on can answer the question:
sipnab -N -I capture.pcap --report # named separately, each with its own fix
sipnab -N -d eth0 --metrics 127.0.0.1:9100 # sipnab_capture_kernel_dropped_packets_total
# sipnab_capture_interface_dropped_packets_total
A clean run says so explicitly, so silence is never ambiguous:
Live capture on 'eth0' finished: 4821003 packets, no drops
Pitfalls:
- Operators routinely respond to any drop by raising
-B. That does nothing at all for interface drops and wastes memory while the real problem goes unaddressed. - Both counters zero does not mean the capture is complete: a frame can arrive intact and still be unreadable, or
--snaplencan cut it short.--reportcounts those separately. See tuning.
23. Find the device flooding REGISTER
Problem: A device is hammering REGISTER and the proxy’s CPU shows it. You need to name the device and the reason.
sipnab -N -I capture.pcap --filter "method == 'REGISTER' AND state == 'Failed'" --json
sipnab -N -I capture.pcap --group-by src --filter "method == 'REGISTER'"
The pattern to look for is a 401/403 answered by an immediate retry with the
same credentials. That is a client that treats an auth challenge as a transient
error, and it does not stop on its own:
{"status_code":403,"reason":"Forbidden","cseq":{"number":2,"method":"REGISTER"},
"ua":"SynthSwitch/1.0","from":"Alice <sip:[email protected]>"}
ua and from are what you take to the desk that owns the endpoint. The
frame field in the same object is what proves it (recipe 20).
Pitfalls:
- A storm from many sources with one
Fromis credential reuse, not one broken phone. Group bysrcbefore concluding. - Registration traffic is often on a different port from calls. If the counts look impossibly low, widen
--portrange.
24. Diagnose a codec mismatch
Problem: Calls to one destination fail immediately. The far end says “your equipment is wrong”.
sipnab -N -I capture.pcap --filter "state == 'Failed'" --json
sipnab -N -I capture.pcap --call-report '<call-id>' --markdown
A 488 Not Acceptable Here after an INVITE is the signature. The call report
puts the offered and answered codec lists next to each other, which is the part
that settles it: if the offer contains only PCMU and the far end supports only
G729, no configuration on your side of the trunk was ever going to work.
Pitfalls:
488is not always codec: it also covers unsupported transport or an unacceptableptime. Read the SDP, not just the status code.- A re-INVITE can renegotiate mid-call, so a call that answered can still fail this way later. Filter on the dialog, not the first transaction.
25. Find out why DTMF does not reach the IVR
Problem: Callers press keys and the IVR does not respond. Everyone blames the IVR.
DTMF travels three different ways, and the failure is nearly always that the two ends chose differently:
sipnab -N -I capture.pcap --json | jq 'select(.method == "INFO")' # SIP INFO
sipnab -N -I capture.pcap --report # RFC 2833 / telephone-event
In the SDP, look for a=rtpmap:101 telephone-event/8000 on both sides. If
one side offers it and the other does not, that side sends in-band audio tones
instead — which survive G.711 and which G.729 destroys.
Pitfalls:
- In-band DTMF through a low-bitrate codec is not a bug you can fix in signaling. The codec destroys the tones before they reach the IVR.
- A
telephone-eventpayload type that differs between offer and answer (101 vs 96) is legal and still works. A mismatch in whether it exists at all is not.
26. Run sipnab as a HEP relay
Problem: Several edge nodes send HEP. You want them analyzed locally and forwarded to a central Homer, without installing an agent twice.
sipnab both receives and sends HEP, and the two are not the same job:
sipnab -N \
--hep-listen 0.0.0.0:9060 --hep-allow 192.0.2.0/24 \
--hep-send homer.example.com:9060 --hep-id 42
Receiving makes sipnab a collector: your SBC or proxy already speaks HEP, so nothing touches the capture path — no port mirror, no root. Sending makes it a capture agent: SIP goes as HEP protocol type 1 and RTCP as type 5, so the collector can report media quality and not only call setup. RTP is never forwarded.
Doing both makes it a tap in the middle: analyze at the edge, keep the central Homer authoritative.
Pitfalls:
- sipnab refuses a non-loopback
--hep-listenunless you pass--hep-allowor--hep-auth. That is deliberate: an open HEP port accepts forged call records from anyone who can reach it. --hep-authtravels in cleartext inside the datagram. It defeats blind spoofing, but it does not survive an on-path sniffer. Tunnel HEP over WireGuard or IPsec on an untrusted path.--hep-auth-mode hmacsigns the whole datagram — every chunk, so the signature covers the source and destination the sender asserts, not only the payload. That is what makes--hep-allow-killsafe to pair with it: the kill response targets exactly those fields, and an unsigned assertion could aim it at a third party. Earlier builds signed the payload only, so anyone who observed a packet could re-send it with address chunks appended and it still verified. sipnab now refuses those version-1 tokens and logs why. There is no compatibility switch, so upgrade both ends together.- Give each agent its own
--hep-id, or the collector cannot tell your nodes apart.
26a. Carry the feed over TCP and TLS
Both halves of HEP default to UDP, which is what Homer’s agents have always spoken and what sipnab has always done. Homer’s collectors also accept TCP and TLS, and each side of sipnab names its own transport so a relay can take one in and send another out:
sipnab -N \
--hep-listen 0.0.0.0:9061 --hep-listen-transport tcp --hep-allow 192.0.2.0/24 \
--hep-send homer.example.com:9060 --hep-id 42
There is deliberately no one flag covering both. sipnab is both agent and collector, often in one process, so a transport flag that named neither side would leave a reader of the command line guessing which half it meant.
Across a path you do not control, wrap the outgoing feed in TLS and name the issuer that signs the collector’s certificate:
sipnab -N -d eth0 \
--hep-send collector.example.com:9063 --hep-send-transport tls \
--hep-tls-ca /etc/sipnab/collector-ca.pem
The receiving side needs a certificate of its own, and refuses a private key any other user on the host can read:
# Run all of these, in order.
chmod 600 /etc/sipnab/collector.key
sipnab -N \
--hep-listen 0.0.0.0:9063 --hep-listen-transport tls \
--hep-tls-cert /etc/sipnab/collector.pem --hep-tls-key /etc/sipnab/collector.key \
--hep-parse --hep-auth-file /etc/sipnab/hep.key
Pitfalls:
- HEP v2 cannot travel on a stream. It declares no total length, so nothing
can say where one packet ends and the next begins. A
tcportlslistener reads HEP v3 only; senders that emit v2 must stay onudp. - TLS says the path is private, not who is on it. sipnab asks connecting
agents for no certificate of their own, so the identity of a peer still comes
from
--hep-auth-file.--hep-allowstill applies too, and means the same thing it means on UDP and TCP: the source address of the connection. Keep both — TLS does not lift the bind refusal for a routable--hep-listen, for exactly this reason. - A named
--hep-tls-careplaces the trust store rather than joining it. That is what you want for a private collector, and it means a public certificate stops verifying the moment you name a private issuer. Omit the flag to use the host’s CA bundle instead. - A
tcportlssender rebuilds its connection when the collector restarts, one attempt per packet. The packet that discovers the break can vanish; nothing after it does.
27. Compare the same call at two nodes
Problem: The SBC says it sent the call. The PBX says it never arrived. Both are looking at their own logs.
Capture at both ends and read the same Call-ID from each:
# Run all of these, in order.
sipnab -N -I sbc.pcap --call-report '<call-id>' --markdown > sbc.md
sipnab -N -I pbx.pcap --call-report '<call-id>' --markdown > pbx.md
diff sbc.md pbx.md
What the difference tells you:
- Present at the SBC, absent at the PBX — it never crossed. Look at routing, firewall, or the network in between.
- Present at both, different bodies — something rewrote it. An SBC that changes SDP addresses is doing its job; one that drops a header is not.
- Present at both, different timing — the message crossed but late. Compare against the retransmission timers in each report.
Pitfalls:
- Clocks. Two captures from two machines are only comparable if their clocks are, and a few hundred milliseconds of skew makes a normal exchange look like a retransmission. Check NTP before reading timing differences as evidence.
- A B2BUA changes the
Call-IDbetween its legs by design, so this recipe compares a proxy’s two sides, not a B2BUA’s. Correlate those byFrom/Toand time instead.
28. Run sipnab as a service
Problem: The capture should survive a reboot and an SSH disconnect, and write somewhere you can find it.
# /etc/systemd/system/sipnab.service
[Unit]
Description=sipnab SIP capture
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/sipnab -N -d eth0 --syslog \
--metrics 127.0.0.1:9100 \
--alert-json
Restart=on-failure
RestartSec=5
# sipnab drops privileges itself after opening the capture socket.
AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
# Run all of these, in order.
sudo systemctl daemon-reload
sudo systemctl enable --now sipnab
journalctl -u sipnab -f
Pitfalls:
--syslogis what makesjournalctluseful. Without it the interesting output goes to stdout, where journald merges it without structure.- Granting
CAP_NET_RAWviaAmbientCapabilitiesis what lets the unit run without being root at all. Do not addUser=rootback “to be safe” — that undoes it. - A live run with no
-Owrites no capture file. If you want the packets kept, say where.
29. Read a capture whose SIP is not on port 5060
Problem: A trunk capture shows almost no SIP. The calls are in the file. The signaling is on 5070, or 5080, or whatever port the SBC uses.
--portrange gates signaling only. sipnab skips SIP whose source and destination both sit outside the range, and that traffic then appears in no message count, no dialog and no output format.
Widen the gate to everything the file holds:
sipnab -N -I capture.pcap --portrange 1-65535 --report --no-cli-print
Or name the range you actually run, which keeps a busy file fast:
sipnab -N -I capture.pcap --portrange 5060-5090 --report --no-cli-print
What to look for:
- sipnab counts the SIP the gate discarded and names the busiest ports it was on, so a default run tells you what to widen to instead of reporting a quiet capture.
- Media is never gated by
--portrange, because RTP uses SDP-negotiated dynamic ports. A too-narrow signaling range does not hide the streams; it hides the calls they belong to, and the streams then arrive as orphans.
Pitfalls:
- On a live device the range becomes the BPF filter when you supply no filter of your own. There the kernel drops the traffic, so nothing downstream — this counter included — can see it was ever there.
- SIP over WebSocket has a gate of its own,
--ws-portrange. Widening this one does not widen that one (recipe 36).
30. Find calls that answered and were never acknowledged
Problem: Calls answer and drop a second later, or the switch logs “no ACK”. A 200 OK with no ACK behind it is a call both sides’ CDRs record as connected and nobody could speak on.
Two timers decide when sipnab treats silence as a fault rather than as a capture that stopped early:
sipnab -N -I capture.pcap --ack-timeout 5 --analyze
--no-final-response-timeout does the same for an INVITE that never drew a final response at all:
sipnab -N -I capture.pcap --no-final-response-timeout 30 --analyze
What to look for:
- The defaults are the RFC 3261 timers: Timer H (32 s) for the missing ACK, Timer C (180 s) for the missing final response. A capture shorter than the timer cannot distinguish a fault from a truncation, which is exactly why sipnab does not call it one.
- Lower them when the capture is short and the calls are local. A five-second
--ack-timeouton a LAN is a real finding; the same number on an international trunk is a stopwatch running against the network.
Pitfalls:
- Lowering
--no-final-response-timeoutreports every call still ringing when the capture stopped, which on a live tap is a normal state and not a fault. Read it against how the capture ended. - These change what sipnab reports, not what the capture holds. A missing ACK that arrived after the last packet in the file is missing from the file, not from the network.
31. Set the quality thresholds to your own network
Problem: Every stream shows yellow, or nothing ever does. The shipped thresholds describe a general network, and yours is not one.
The eight quality knobs carry no built-in command-line default, so a value you do not pass leaves the config file’s value in charge:
sipnab -N -I capture.pcap --mos-warn 4.0 --mos-bad 3.2 --jitter-warn-ms 20 --jitter-bad-ms 50 --loss-warn-pct 1.0 --loss-bad-pct 3.0 --report --no-cli-print
Round-trip time has its own pair, read from RTCP when the capture carries it:
sipnab -N -I capture.pcap --rtt-warn-ms 150 --rtt-bad-ms 300 --report --no-cli-print
The one MOS input a passive tap cannot measure is the one-way delay. Declare it rather than letting the default stand:
sipnab -N -I capture.pcap --one-way-delay 40 --report --no-cli-print
What to look for:
- A MOS built on an assumed delay is only as good as the assumption.
media_diagnostics(recipe 58) reportsdelay.assumed: truewhen nothing supplied one, which is the flag to read before quoting a score to a carrier. --pdd-thresholddefaults to ITU-T E.721’s target for an international connection, because a capture does not say what kind of call it holds. A network that knows its traffic is local or toll wants a tighter number.
Pitfalls:
- Thresholds change which figures the output colors and which ones it reports. They do not change the measurement, so widening one to quiet a report hides the finding rather than fixing it.
--one-way-delayis a declaration about the observed path. Declaring one you have not measured trades an honest default for a confident wrong number.
32. Export one customer’s calls as a smaller capture
Problem: A vendor wants the packets, and the file is 4 GB of everyone’s traffic. Sending all of it is both slow and a disclosure.
Only the BPF expression narrows -O. It writes each packet as it arrives,
inside the capture loop, before any dialog is complete enough to judge –
nothing can tell whether a call matches from.user == '1001' until sipnab has
seen and correlated its INVITE. So --filter does not reach it.
Measured: -O under a filter matching nothing and -O with no filter at all
produce a byte-identical file.
That matters here more than anywhere else on this page, because the thing you are about to do is mail the result to somebody else. Narrow with the positional BPF expression, which runs in the kernel before sipnab sees a packet:
sipnab -N -I capture.pcap -O one-customer.pcap --no-cli-print "host 192.0.2.10"
The same as pcapng, which is the format that can also carry decryption secrets:
sipnab -N -I capture.pcap -O one-customer.pcapng --pcapng --no-cli-print "host 192.0.2.10"
BPF is a network-level language: it selects by address, port and protocol, not
by SIP user. If the subset you need is a SIP-level one, you have two honest
options and neither is -O. Export the dialogs as vCon containers, which IS
dialog-filtered:
sipnab -N -I capture.pcap --export-vcon-when "from.user == '1001'" --export-vcon-dir ./out
Or export those dialogs with the identifiers replaced. --redact rewrites an
exported CONTAINER, not a pcap – there is no redacted-capture path, and the
run refuses rather than pretending:
sipnab -N -I capture.pcap --export-vcon-when "from.user == '1001'" --export-vcon-dir ./out --redact
What to look for:
- Read the output back before you send it. A file that reconstructs the same dialogs is one the far end can work with; one that does not means the filter cut a leg off the call.
--pcapngis worth the extra bytes when the capture carried decryption secrets, because pcapng is the format that can carry them (recipe 7c).
Pitfalls:
- A filter selects dialogs, so a call whose INVITE was outside the capture has no dialog to match and its packets are not written.
- sipnab refuses to write its output over its input. Name a different path, in a different directory if you are working in place.
33. Check what a STIR/SHAKEN Identity header actually claims
Problem: Somebody is spoofing a number, or a carrier says your calls arrive with a bad attestation, and the argument is about what the header actually held.
--stir-shaken decodes the RFC 8224 Identity header’s PASSporT and reports what it claims:
sipnab -N -I capture.pcap --stir-shaken --no-cli-print
Each decoded token prints one line:
STIR/SHAKEN: attest=A orig=+15551234567 dest=["+15559876543"] verified=NotChecked
What to look for:
attest=Ameans the originator claimed full attestation. Nothing here confirms the claim, and a forged Identity header decodes exactly like a genuine one — so this is evidence about what the originator claimed, never grounds for trusting a calling number.verified=Expiredis the one check sipnab applies locally: RFC 8224 section 6.2, Step 4,iatfreshness, against the capture timestamp of the packet carrying the header. An old pcap therefore reports the tokens that were still fresh at the moment they crossed the tap.- No
STIR/SHAKEN:line at all means noIdentityheader reached the capture point. On an inbound trunk that is itself the finding.
Pitfalls:
- sipnab makes no outbound request to analyze a capture, so it never fetches the certificate the token references and never checks the signature over it.
verified=NotCheckedis the honest answer, not a failure. - sipnab logs the line at
info.--quietfloors the log atwarnand hides it, and so does the TUI.
34. Detect weak digest authentication on a registrar
Problem: An audit asks whether the phones authenticate properly, and nobody can answer from the switch’s configuration alone because the challenge is what the wire actually carried.
--digest-leak reads the 401/407 challenges and the Authorization headers answering them, and reports four weaknesses: an MD5 algorithm, a nonce reused across challenges, a challenge with no qop, and a response with no cnonce where the challenge offered qop.
sipnab -N -I capture.pcap --digest-leak --alert-json --no-cli-print
Findings arrive as one JSON object per detection:
{"alert":"digest","detail":"WeakAlgorithm: challenge uses algorithm=MD5 (should be SHA-256+)","src":"203.0.113.101","ts":"2026-05-05T12:34:56Z"}
Group the accused sources and get a firewall rule with the evidence beside it:
sipnab -N -I capture.pcap --digest-leak --recommend-block fail2ban --quiet
What to look for:
WeakAlgorithmwith noalgorithmparameter in the challenge is the same finding asalgorithm=MD5: RFC 2617 makes MD5 the default when the parameter is absent, so silence is a choice.NonceReuseacross challenges is the one to act on first. A nonce that repeats is a replay window, and it usually means a registrar cluster whose nodes do not share nonce state. A 401 the registrar retransmitted – the same Call-ID, CSeq and Via branch – is the same challenge arriving twice, and does not count.
Pitfalls:
- The detector needs an alert channel or it prints nothing.
--digest-leakon its own arms the detection and has nowhere to put it; pass--alert-json, or--alert syslog, or read it through--recommend-block. A run with neither looks exactly like a clean capture. - This is a configuration audit, not a credential dump. sipnab reports the weakness in the exchange; it never prints the
responsehash, which is an offline attack against the subscriber’s password.
35. Read SIP that arrives in TCP segments or IP fragments
Problem: A capture full of SIP over TCP shows half the messages, or a large INVITE with many codecs and ICE candidates never parses.
sipnab reassembles IP fragments and TCP segments by default, and two limits bound what that costs:
sipnab -N -I capture.pcap --max-reassembly 500 --reassembly-ttl 15 --report --no-cli-print
sipnab drops a message larger than the TCP buffer rather than growing one into memory. Raise it for a switch that sends very large bodies:
sipnab -N -I capture.pcap --max-tcp-buffer 262144 --report --no-cli-print
Turn reassembly off when the capture is pure single-packet UDP and reassembly is only overhead:
sipnab -N -I capture.pcap --no-reassembly --report --no-cli-print
What to look for:
--reportcounts frames no decoder could read separately from frames that were never captured. A capture that lost the second half of every large INVITE reports differently from one that never saw them.- sipnab holds an incomplete datagram or half-read stream for
--reassembly-ttlseconds and then sweeps it. On a lossy path that sweep is where a truncated message goes, and its count is what tells you the path is lossy.
Pitfalls:
--no-reassemblyis the inverse of segment reassembly. On a TCP or TLS capture it does not make the run faster in any useful sense; it makes every multi-segment message unparseable.- A
--snaplenthat cut the frame short is a different problem with the same symptom. Reassembly cannot restore bytes the capture never took (recipe 22).
36. Read SIP over WebSocket from a WebRTC gateway
Problem: A WebRTC leg is invisible. The browser talks SIP over WebSocket (RFC 7118) to Kamailio, OpenSIPS or Janus, and the capture shows TCP and no calls.
The shipped WebSocket port set — 80, 443, 8080, 8443 — is the browser’s view of the web, not a deployment’s. Behind a reverse proxy sipnab sees whichever port the proxy forwards to:
sipnab -N -I capture.pcap --ws-portrange 1-65535 --report --no-cli-print
Or name the port the gateway actually listens on:
sipnab -N -I capture.pcap --ws-portrange 5066-5066 --report --no-cli-print
sipnab refuses a WebSocket frame whose payload exceeds 64 KiB (65,536 bytes,
MAX_FRAME_SIZE) rather than reassembling it: no SIP message comes close, so a frame that
large is either not SIP or not well-formed.
What to look for:
- sipnab tallies the SIP-over-WebSocket it declined to unwrap and names the ports it was on, so a run with the default set still tells you where the traffic is.
- A range replaces the shipped set, exactly as
--portrangereplaces the default signaling ports. Naming 5066 alone stops unwrapping 443.
Pitfalls:
- WSS is TLS. Unwrapping the WebSocket framing needs the TLS decrypted first — a keylog (recipe 7) or a uprobe (recipe 7g) — and
--ws-portrangealone does nothing for it. - A browser leg carries no RTP that a server-side tap can see when the media goes peer to peer. Put the capture where the media is, or read the relay’s account of it (recipe 54).
37. Read SIP carried inside a GTP-U or VXLAN tunnel
Problem: On a mobile core or a data-center fabric the SIP is real and the capture is empty, because every packet is an outer UDP header with the call inside it.
--capture-tunnels takes all traffic on the tunnel ports so the inner SIP reaches sipnab. With no value it covers GTP-U (2152), VXLAN (4789) and GENEVE (6081):
sudo sipnab -N -d eth0 --capture-tunnels
Name the ports yourself for a non-standard deployment — Linux’s pre-IANA VXLAN port, for instance:
sudo sipnab -N -d eth0 --capture-tunnels=8472
It applies to a file too, where the tunnel is already in the bytes:
sipnab -N -I capture.pcap --capture-tunnels --report --no-cli-print
What to look for:
- Without the flag the auto-generated filter still sees VLAN, QinQ, PPPoE and MPLS-encapsulated SIP. Those cost nothing to add and are already on. Tunnels are the case that is off.
- The dialogs come out with the inner addresses, which are the ones the SIP layer used and the ones a routing argument is about.
Pitfalls:
- This is not a narrowing filter. BPF cannot walk a GTP-U extension-header chain to reach the inner port, so the only way to cover these is to take everything on the port — on a mobile core, that is the entire user plane. Size
-Bfor it (recipe 43). - Ignored when you supply your own BPF expression. Your filter is then the whole filter, tunnel ports included or not.
38. Search a capture for a header or a body string
Problem: You know a string that identifies the traffic — a P-Asserted-Identity, an X- header your SBC adds, a trunk group name — and not which calls carry it.
-e is the positional match expression: a regex against the whole raw message. Once any message in a dialog matches, sipnab shows every later message of that dialog too:
sipnab -N -I capture.pcap -e 'X-Trunk-Group' -i
Whole words only, with two messages of context after each hit:
sipnab -N -I capture.pcap -e 'INVITE' -w -A 2
Multi-line headers folded into one line before matching, which is what a regex spanning a wrapped Contact needs:
sipnab -N -I capture.pcap -e 'User-Agent' -i --single-line
Everything that does not match, for finding the odd one out:
sipnab -N -I capture.pcap -v -e 'OPTIONS' --report --no-cli-print
What to look for:
-ematches the message text;--filtermatches the reconstructed dialog. Use-efor “which calls mention this string” and--filterfor “which calls behaved this way”.payloadin the filter DSL is the bridge: it matches when any message in the dialog contains the value.- Add
--hexdumpwhen the argument is about bytes rather than text — a header with trailing whitespace, or a body whose line endings are wrong.
Pitfalls:
-eis a regex, so.and+in a phone number or a header name match more than you meant. Escape them or add-w.- The trailing positional argument is a BPF filter, not a match expression.
sipnab -I capture.pcap INVITEasks libpcap to compileINVITEand fails; the match expression needs-e.
39. Collect a directory of rotated captures into one analysis
Problem: tcpdump -C -W left you 27 files and the call you want crosses three of them.
-I takes a file, a directory or a glob, and it is repeatable. sipnab reads the files in the order their packets arrived, never by filename — a ring buffer wraps, so tg.pcap7 can hold older traffic than tg.pcap0:
sipnab -N -I '/var/captures/*.pcap' --report --no-cli-print
Descend into subdirectories, which is off by default:
sipnab -N -I '/var/captures/*.pcap' --recursive --report --no-cli-print
What to look for:
- One store serves the whole set, so a dialog whose INVITE is in one file and whose BYE is in another comes out as one call.
sipnab -N -I /var/captures --input-name 'tg.pcap[0-4]'narrows a directory to the files worth reading. sipnab matches the pattern against the filename alone, so it behaves the same at every depth under--recursive— and sipnab refuses the pair when-Inames one file directly, rather than filtering nothing and saying nothing.
Pitfalls:
-l/--limitbounds dialogs for the whole run, not per file. A 27-file directory reaches the cap 27 times sooner than one file does, and eviction drops the oldest dialogs — the worst ones to lose for a post-mortem. Raise it (recipe 43).--recursiveis off on purpose: descending silently can analyze several times the traffic you pointed at, and nothing in the output would say so.
40. Open the same evidence in Wireshark
Problem: The argument has moved to a layer sipnab does not decode, or the person you are arguing with only trusts Wireshark.
--tshark-filter prints a ready-to-run tshark command for this capture and this display filter:
sipnab -N -I capture.pcap --tshark-filter 'sip.Method == "INVITE"' --no-cli-print
tshark -r capture.pcap -Y 'sip.Method == "INVITE"' -V
Narrow it to one call, using a Call-ID you already have:
sipnab -N -I capture.pcap --tshark-filter 'sip.Call-ID == "abc123@host"' --no-cli-print
What to look for:
- Pair it with recipe 20. A frame pointer names one frame and a digest proves it is the same file; the tshark command puts that frame in front of somebody who wants to see the whole stack under it.
- The printed command is text. Read it, edit the filter, and run it yourself — sipnab does not run it for you.
sipnab -N -I capture.pcap --wiresharkis the same idea for the GUI: it builds a display filter naming every Call-ID in the capture and prints it for you to paste into Wireshark.
Pitfalls:
--wiresharkonly prints text, so it needs neither Wireshark nor a display on the capture host. Open the capture in Wireshark separately and paste the printed filter.- A tshark display filter is not sipnab’s filter DSL and not a BPF expression. All three languages appear in this cookbook, and none of them accepts the syntax of the other two.
41. Measure the gap between consecutive messages
Problem: The ladder looks correct and the call still feels slow, or a retransmission argument needs the actual timers rather than “it looked immediate”.
--delta-time replaces the wall clock in front of each message with the gap since the previous one:
sipnab -N -I capture.pcap --delta-time
+0.000s 192.0.2.20:5060 -> 192.0.2.15:5060 INVITE UDP
+0.512s 192.0.2.15:5060 -> 192.0.2.20:5060 100 Trying UDP
Add the full header block of the bodyless messages, which otherwise print as a one-line summary:
sipnab -N -I capture.pcap --delta-time --show-empty
Group the messages by call first, so the gaps you are reading belong to one ladder:
sipnab -N -I capture.pcap --group-by call-id --delta-time
What to look for:
- 500 ms, then 1 s, then 2 s, then 4 s between copies of one request is RFC 3261’s Timer A backing off. That is a retransmission, and it says the first copy never reached anything that would answer it — not that the sender is chatty.
- A long gap before
180 Ringingis post-dial delay and belongs to the far end. A long gap before100 Tryingbelongs to the next hop, which should answer immediately.
Pitfalls:
- Without
--group-by, a busy capture interleaves twenty calls and the “gap” is the time to the next message of any of them. - sipnab computes each delta from capture timestamps. Two captures from two machines are only comparable if their clocks are (recipe 27).
42. Record which invocation produced a report
Problem: A report from three weeks ago says the capture was clean, and nobody can say which file, which filter or which port range produced it.
--run-provenance-file appends one JSON line per run, at startup — before sipnab loads the config and before it opens any capture device:
sipnab -N -I capture.pcap --run-provenance-file runs.jsonl --report --no-cli-print
Under systemd, point it somewhere durable — --run-provenance-file /var/log/sipnab/runs.jsonl — and every restart appends rather than replaces.
{"record":"run","seq":1,"argv":["sipnab","-N","-I","capture.pcap","--report"],
"cwd":"/var/captures","user":"sipnab","uid":993,
"version":"0.5.142 (d2965454) features: native,tui,audio,tls,hep,api,mcp,metrics,plugins,vcon",
"capture":{"instance":"3f9b0718d158c64087d09c-1","node":"capture-01"},
"started":"2026-05-05T12:34:56Z"}
What to look for:
capture.instanceis the identifier every MCP and REST answer carries, so it joins an artifact back to the command that made it.featuresis the build, not the flags. A report missing a section because the binary lacked the Cargo feature reads identically to a clean one, and this line is where that difference shows up.
Pitfalls:
- A record sipnab cannot write stops the run. That is deliberate: a best-effort line’s absence would mean either “not enabled” or “the disk was full”, and nobody could tell which. Stopping costs nothing, because sipnab has read no packet yet.
- sipnab opens the file for append and never truncates it, and creates it mode 0600 — argv holds capture paths, and a path holds a customer’s name.
43. Keep a long-running capture inside a memory budget
Problem: A capture that should run for a week dies on day three under the out-of-memory killer.
Five caps bound what a run retains, and each bounds a different thing:
sipnab -N -I capture.pcap --limit 20000 --max-streams 4000 --max-lost-sequences 4000 --findings-history 500 --report --no-cli-print
The in-flight queue between capture and processing has a budget of its own, separate from the kernel ring -B sizes:
sudo sipnab -N -d eth0 --buffer-budget 128 -B 128
What to look for:
-l/--limitis not a concurrency limit. Nothing removes a completed dialog, so the bound scales with uptime rather than with load: a box carrying five concurrent calls still evicts once 100,000 calls have completed.- The eviction count appears wherever a dialog count appears, so a run that hit a cap says so rather than quietly answering from a truncated store.
Pitfalls:
- Eviction drops the oldest dialogs, which are the ones a post-mortem wants. On a long run, prefer rotating output files (recipe 44) over relying on the store to hold a week.
--max-groupsbounds--group-bykeys and is a separate map from--limit. A run that groups a week of traffic by source address needs both raised.
44. Run a capture that stops on its own
Problem: A capture running over a weekend either fills the disk or waits for somebody to stop it at the wrong moment.
Stop after a packet count, a wall-clock duration, or a size:
sudo sipnab -N -d eth0 -n 500000 --duration 8h --autostop filesize:2048 -O /var/capture/sip.pcapng --pcapng
Or keep capturing forever inside a fixed budget, by rotating and deleting the oldest chunks:
sudo sipnab -N -d eth0 -O /var/capture/sip.pcapng --pcapng --split filesize:50 --split-keep 8
What to look for:
filesizecounts MiB, the same unit--split filesizeand-Buse, sofilesize:2048stops at what a file browser calls 2 GiB.-n/--countcounts every packet received from the capture source. On a HEP listener that includes packets later dropped by the allowlist, the rate limiter or authentication.
Pitfalls:
--split-keepdeletes capture files. It touches only the files that running process created and named, so anything else in the directory stays — but leave it off whenever the capture is evidence you cannot retake.- A run killed mid-capture leaves behind whatever it had not yet deleted, and the next run does not adopt it.
45. Check whether comfort noise explains a one-way finding
Problem: A mobile or VoLTE trunk reports one-way audio on calls that were fine, or a trunk that really is one-way reports nothing.
--cn-suppression-ratio is the share of a call’s packets that must be comfort noise before sipnab accepts comfort noise as the explanation for media in one direction. It is the one threshold that suppresses a finding, so getting it wrong fails silently:
sipnab -N -I capture.pcap --cn-suppression-ratio 0.6 --report --no-cli-print
Lower it where a call carrying any comfort noise at all is still expected to be bidirectional:
sipnab -N -I capture.pcap --cn-suppression-ratio 0.1 --one-way --json-dialogs
What to look for:
- A trunk with aggressive voice activity detection routinely passes 30 % comfort noise, and above the ratio one-way audio is never reported on it. If a trunk has stopped producing one-way findings entirely, this is the first knob to check.
media_diagnostics(recipe 58) counts the comfort-noise frames and the silence periods per stream, which is the measurement this ratio governs.
Pitfalls:
- Refused at 0 and above 1 by name, in the flag and in the config file, so a config file is not the lenient way in.
- Raising it is a statement about the trunk, not about the call. Set it per capture source rather than globally.
46. Detect fraud placed outside business hours
Problem: The international bill jumped and the calls all happened at 03:00.
--fraud-detect runs the volume, wangiri and sequential-dialing heuristics. --business-hours adds the off-hours detector, which is otherwise unreachable — with no window declared there is no “outside” for a call to fall in:
sipnab -N -I capture.pcap --fraud-detect --business-hours 8-18 --alert-json --no-cli-print
A wrapping range is the overnight window:
sipnab -N -I capture.pcap --fraud-detect --business-hours 22-6 --alert-json --no-cli-print
What to look for:
- The window is whole UTC hours. A site that works 08:00–18:00 local is a different pair of numbers, and getting it wrong moves the alert rather than silencing it.
- Off-hours volume from a single extension is the classic compromised-handset signature. Feed the source to recipe 10’s block recommendation before banning it, so a night-shift desk is not the thing you disconnect.
Pitfalls:
--business-hourswithout--fraud-detectarms nothing: the second flag is what runs the detector.- As with every detector on this page, an alert channel is what makes the findings visible.
--alert-json,--alert syslogor--recommend-block— with none of them the run is silent.
47. Follow a load generator’s traffic by transaction, not by call
Problem: A SIPp run or a proxy under test reuses one Call-ID across hundreds of transactions, and the whole capture reconstructs as a single enormous dialog.
--dialog-track branch groups by SIP transaction — the Via branch — instead of by Call-ID:
sipnab -N -I capture.pcap --dialog-track branch --report --no-cli-print
What to look for:
- A single ordinary call yields several units under
branch: RFC 3261 gives the ACK to a 2xx a new branch and the BYE another. That is the transaction view, not a miscount. - Compare the two counts. A capture whose
call-idcount is 1 and whosebranchcount is 400 is a load generator; one where they are close is ordinary traffic.
Pitfalls:
- sipnab computes every dialog-level diagnosis — one-way audio, codec asymmetry, PDD — per tracked unit. Under
branchthose units are transactions, so a per-call figure is not what you get. - Switch back to
call-id(the default) before quoting durations or MOS at anyone.
48. Run a live capture without giving sipnab root
Problem: Security refuses to sign off on a packet capture running as root, and sudo sipnab on every invocation is what the procedure currently says.
Grant the binary the two capabilities live capture needs, once:
sipnab --setup-caps
Then run it as an ordinary user. sipnab opens its capture devices first and drops privileges afterwards, so name the user and a directory to confine it to:
sudo sipnab -N -d eth0 --user sipnab --chroot /var/empty
What to look for:
--setup-capssetscap_net_raw,cap_net_admin+epviasetcapand exits, re-invoking itself throughsudowhen it is not already root. After it,sudois no longer part of the daily command.- Under systemd,
AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMINdoes the same job for the unit (recipe 28). Do not addUser=rootback “to be safe” — that undoes it.
Pitfalls:
setcapdoes not survive a new binary. Every upgrade, every rebuild, every package update drops it, and the failure looks like a permissions problem that appeared from nowhere. Re-run--setup-capsafter an upgrade.- sipnab opens anything it must reach by path — a keylog FIFO, an output file, a config — before the drop and the chroot. A path under
/runis unreachable afterwards (recipe 7f). --no-priv-dropkeeps the privileges. It exists for the cases that genuinely need them, such as the scanner-kill worker forging responses, and it is not the way to fix a permissions error.
49. Configure sipnab from a file instead of a long command line
Problem: The capture command has grown to eleven flags, three of them thresholds, and it now lives in four places that disagree.
Print the configuration this invocation would actually use, and exit:
sipnab -D
Point at a file explicitly, which is what a systemd unit should do rather than relying on search order:
sipnab --dump-config --config /etc/sipnab/sipnab.toml
Ignore every config file, which is how you prove a behavior comes from the flags and not from a file you forgot:
sipnab -D --no-config
What to look for:
- A flag beats the file. Several knobs deliberately carry no built-in flag default precisely so that “not typed” and “typed the default” stay distinguishable and the config key has something to override — the eight quality thresholds (recipe 31) and
--mcp-max-rowsamong them. -Dis the fastest way to settle “is this host reading the config I think it is”. It answers before any capture starts.
Pitfalls:
-Ddumps the effective configuration, which includes values that came from a file you did not name. Add--no-configto see the flags alone.- A config file is per host; the tool set an MCP server registers is per client. That is why
--mcp-toolshas an ordinary flag default and the row caps do not.
50. Install shell completions
Problem: sipnab has a long flag for nearly everything on this page, and you are typing them from memory.
sipnab --completions bash
Write it where your shell looks for it:
sipnab --completions zsh
What to look for:
- Accepted shells are
bash,elvish,fish,powershellandzsh. The script goes to stdout, so redirect it:sipnab --completions bash > /etc/bash_completion.d/sipnab. - sipnab generates the completions from the same flag definitions
--helpprints, so they cannot drift from the binary that produced them.
Pitfalls:
- Regenerate after an upgrade. A completion file from an older build offers flags the new binary may have renamed, which is the one failure mode a completion has.
- Completions know the flags, not your captures. They do not complete a Call-ID or a filter expression.
51. Export every failed call as a redacted vCon in one pass
Problem: A conversation archive, a compliance store or an agent pipeline wants the calls that failed — as containers, not as a pcap — and the subscriber numbers may not leave the building in the clear.
--export-vcon-when takes a filter and writes one container per matching dialog into a directory, with --redact turning every identity into a keyed pseudonym:
sipnab -N -I capture.pcap --export-vcon-when "state == 'Failed'" --export-vcon-dir ./vcons --redact --vcon-digest
Write the reversal table, keep three leading digits of each number, and cap what a container may inline:
sipnab -N -I capture.pcap --export-vcon-when "response_code >= 400" --export-vcon-dir ./vcons --redact --redact-map ./redact-map.json --redact-keep-prefix 3 --vcon-max-inline-media 2
--vcon-digest prints a sha256sum-format line per container, so sipnab ... --vcon-digest > SHA256SUMS and a later sha256sum -c SHA256SUMS both work with no glue:
fcce469e74ae2242e892faaf1952cb3f13764dde0280366e3edc2fd43a30bfd3 1-1966_192.0.2.20-1cc03f18.vcon.json
What to look for:
- Redaction is not masking. Every identity becomes a token that is equal exactly when the original was equal, and addresses go through a prefix-preserving map — so “these forty failures came from one subscriber” and “the media went to a subnet no SDP advertised” are both still answerable on the output.
- Two things go entirely rather than becoming tokens, because no pseudonym of them carries diagnostic value: digest credentials and inline audio.
- Redaction affects the serialized container only. The TUI, the reports and every in-process analysis keep the real values, so a redacted export and a live triage session read the same capture.
- Every run draws a fresh redaction key unless you pass
--redact-key-file, and that default is the safe one: the tokens join against no other export and nothing anywhere can reverse them. Supply a file when tokens must stay stable across captures or across hosts — the whole file is the secret, trailing newline included — and understand what that buys whoever holds it.
Pitfalls:
--export-vcon-whentakes a raw DSL expression, not an alias.--filter problemsworks;--export-vcon-when problemsfails at parse time, naming the position of the offending token. Spell the expression out.--redact-mapis the reversal of every pseudonym the run produced, so it is exactly as sensitive as the capture. sipnab creates it mode 0600 and refuses to overwrite an existing one, because that file may be the map for containers already sent somewhere.--redact-keep-prefixpublishes that many leading digits of a real subscriber number in the clear. Zero is the default for that reason; three buys you NANP area-code analysis and costs three digits.--vcon-max-inline-mediabounds inline audio (5 MiB by default, a figure measured against a real store that answered 204 and dropped a larger payload). Over the budget sipnab refuses the media out loud, andcapture_completeness.mediasays so rather than letting an absent recording read as a silent call.
52. Check a vCon against the schema before a store rejects it
Problem: A conserver refuses a container and reports the refusal to whoever POSTed it — never to whoever built it. A validation pass over 4,216 real containers found 2 the schema rejects, with nothing on any surface saying so.
Run sipnab as an MCP server over the capture, and ask validate_vcon before you hand anything to a store:
sipnab -N --mcp -I capture.pcap --quiet
// validate_vcon { "call_id": "[email protected]" }
{
"verdict": "valid",
"schema_path": "tests/schemas/vcon.schema.json",
"errors": [],
"deviations": [],
"explanations": []
}
Pass a container somebody else produced — one a store already rejected — as an object rather than a Call-ID:
// validate_vcon { "container": { "vcon": "0.4.0", "dialog": [ { "type": "transfer" } ] } }
{
"verdict": "invalid",
"errors": [
{ "instance_path": "/dialog/0", "keyword": "required",
"detail": "missing required properties: start" }
],
"deviations": []
}
What to look for:
- There are three verdicts, and the middle one carries the point.
valid-except-documented-deviationmeans every finding is a shape sipnab emits deliberately that the schema rejects: section 4.3 of draft-ietf-vcon-vcon-core-03 says a Dialog Object with no parameters is possible, the working group agreed that shape in issue #20 after IETF 124, and the draft’s own Appendix B schema forbids it because every Dialog Object requires astart. sipnab emits one — the consultative leg of an attended transfer, which the observed leg never saw. - The exemption is narrow. Only a Dialog Object with no members at all counts. A typed object missing
startis an ordinary error, and it is exactly the defect the corpus pass found; folding the two together would teach a producer that a missingstartis fine. - A container that disagrees with the schema is an answer, not a tool error. The call fails only when the request is wrong: neither argument, both, an unknown Call-ID, or a
containerthat is not a JSON object.
Pitfalls:
- The validator reads the vendored schema file rather than a transcription of it, and it refuses to guess: a keyword outside the draft-07 subset the file uses makes every validation report
invalidnaming the keyword. Re-vendoring a richer schema fails loudly instead of quietly certifying less than it claims. - Needs a build carrying the non-default
vconCargo feature. Without it the tool refuses by name;server_capabilitieslists what a given binary has.
53. Export a vCon per failed call in one round trip
Problem: An agent asked for “every failed call as a container” lists the dialogs and then issues one export per row — on a real capture, hundreds of round trips to do what one invocation does.
export_vcon takes a filter as well as a call_id, and returns the whole matching set inline:
sipnab -N --mcp -I capture.pcap --retain-audio --mcp-max-rows 200 --quiet
// export_vcon { "filter": "state == 'Failed'", "limit": 50 }
{
"returned": 2,
"total_matched": 7,
"truncated": true,
"containers": [
{ "call_id": "[email protected]",
"digest": "b6f0a1c9d84e2f7a3b5c6d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c",
"completeness": { "media": "carried", "complete": true, "omissions": [] },
"container": {} }
]
}
What to look for:
total_matchedcounts the whole store, so it is the number to page against —returnedis only what this call fitted underlimit.- Give
call_idorfilter, never both. A request naming a dialog and a rule for choosing dialogs has two answers and names neither, and the CLI refuses the same pair. - A filter matching nothing answers with an empty set. “No call failed” is a finding, and a refusal there would make a clean capture look like a broken request.
- Every container comes back with its SHA-256, computed exactly as
--vcon-digestcomputes it — one function — so a store’s ledger entry compares against this value directly. The digest identifies the document: re-exporting one call produces a second document with a new digest and the sameuuid, so deduplicate on theuuid.
Pitfalls:
--retain-audiois what puts audio inside the containers, and it requires--mcp: the MCP server is the only batch-mode consumer that can read the buffers back. Without itcapture_completeness.mediareadsnone-decodable, which reports the run and never claims the call was silent.- Unlike its file-writing neighbors this tool writes nothing and needs no
--mcp-file-root. It returns the container inline and the agent decides what to do with it. - There is no
formatparameter. A vCon is a JSON container defined by the draft, and a Markdown arm would render a document whose whole purpose is to travel between machines.
54. Find out where a stream’s endpoint came from
Problem: A report gives the address a call’s media went through. Whether that came from SDP the two parties exchanged, from a relay sipnab asked, or from a mirrored datagram anybody on the segment could have sent decides what the claim is worth — and every other surface renders the three identically.
Start the server with the relay’s control address, and opt in to the one tool that transmits:
sudo sipnab -N --mcp -d eth0 --rtpengine-control 127.0.0.1:22222 --mcp-allow-relay-query --quiet
explain_attribution answers for one call, endpoint by endpoint:
// explain_attribution { "call_id": "[email protected]" }
{
"endpoints": [
{ "address": "192.0.2.10", "port": 10000,
"asserted_by": "signaled",
"delivery_trust": "not-relay-asserted",
"delivery_note": "the parties' own claim in SDP, not a relay's statement about its allocation" }
],
"unauthenticated_endpoints": 0
}
reconcile_orphans says why a stream has no dialog, rather than counting the ones that do not:
// reconcile_orphans { "limit": 2 }
{ "orphans": [ { "ssrc": 305419896, "reason": "never-named" } ],
"total_orphans": 4, "relay_was_consulted": false }
query_relay asks the relay itself about a call that was already up when the capture started, and decode_ng decodes one captured relay control message and says which path carried it.
What to look for:
delivery_trust, strongest first:asked(sipnab asked the relay over its control socket, so no third party could answer),hmac-verified,plain-secret,port-gated-only— the source is not authenticated — andnot-relay-asserted, the parties’ own SDP claim. With no posture configured sipnab reports the weakest reading, because a tool whose job is telling you what a claim is worth must not round up in the absence of information.reconcile_orphanshas three verdicts and the difference is the point:relay-asserted-but-no-dialogmeans the signaling is missing, not the media;signaled-but-no-dialogmeans the capture missed the dialog or its SDP predates the capture;never-namedmeans nothing in the capture named the endpoint at all.relay_was_consulted: falsebeside anever-namedverdict means nobody asked — an absence of evidence, not evidence of absence.
Pitfalls:
query_relayneeds three things and says which one is missing:--mcp-allow-relay-query, the relay control flag, and a live source. A run reading a file can obtain no transmit permit.- There is no address parameter, deliberately. The destination comes from
--rtpengine-controland from nowhere else. An address sipnab could otherwise infer is one it learned from packets — a host that was a relay during the capture and may be somebody’s laptop now. --rtpengine-controltransmitslistandqueryand nothing else, structurally so:offer,answer,deleteandstart recordingeach change a production relay, and none is representable on that path.
55. Set up the MCP server for a hosted agent
Problem: The agent driving sipnab is not on your laptop and not run by you, and “loopback with no token” stops being the deployment.
Every knob below is a separate decision, and the defaults are the conservative half of each:
sipnab -N --mcp --mcp-transport http --mcp-bind 0.0.0.0:8731 --mcp-signing-key-file /etc/sipnab/mcp.key --mcp-token-ttl 900 --mcp-rate-limit-per-peer 20 --mcp-max-rows 200 --mcp-max-body-bytes 8192 --mcp-audit-file /var/log/sipnab/mcp-audit.jsonl --mcp-tools core --mcp-file-root /var/lib/sipnab/captures --quiet
Mint a short-lived token from the same signing key, rather than sharing a static secret:
sipnab --mint-token --token-scope read --token-id agent-a --mcp-signing-key-file /etc/sipnab/mcp.key --mcp-token-ttl 900
What to look for:
--mcp-tools coreregisters a small set that still answers a whole call. sipnab sends every registered tool’s name, description and JSON schema ontools/list, and the model then carries them in context for the whole session, before the agent has asked anything — on a client with a small context window that fixed cost is worth cutting.--mcp-rate-limit-per-peerand--mcp-max-concurrentanswer different questions. That one bounds calls in flight; this one bounds their arrival rate. An agent that never exceeds the concurrency cap and simply loops as fast as sipnab answers holds one slot at a time and asks again the moment it frees up, which the concurrency cap alone does not bound.--mcp-audit-fileis the durable copy of what an agent looked at. The same record already rides the ordinary log, but that is a console view:SIPNAB_LOGfilters it and--quietsuppresses it, and the question comes later, from somebody who did not choose the log level.
Pitfalls:
- sipnab refuses a tool call it cannot write to the audit file. An audit trail that silently skipped what it could not record would be worse than none, so a full disk stops the answers rather than the recording.
--mcp-file-rootis the whole security model for the file tools, and it is not negotiable: they take a filename, never a path, and sipnab rejects anything containing a separator, a..or an absolute prefix before touching the filesystem. An agent-supplied path is an arbitrary file write wearing a feature’s clothes.- A peer is the source IP over HTTP — the address, not the socket, so reconnecting does not mint a fresh allowance — and the pipe itself over stdio.
- The write verbs still appear in
tools/listwhen you omit their flags, because sipnab registers tools unconditionally and refuses the call instead. Seeingshutdown_serverlisted does not mean an agent can stop your capture.
56. Read TLS from an agent, with no keys and no restart
Problem: Somebody wants an agent to look at SIP over TLS on a host you cannot restart. Whether that is even possible is a property of the host, and asking it is a smaller act than doing it.
list_tls_libraries stays available whatever else is off, so an agent can always report what a capture would see:
sudo sipnab -N --mcp -d eth0 --quiet
// list_tls_libraries { }
{
"supported": true,
"privileged": true,
"libraries": [
{ "flavor": "OpenSSL", "path": "/usr/lib/libssl.so.3", "inode": 21143,
"process_count": 12, "symbol": "SSL_write",
"probe_path": "/proc/954/root/usr/lib/libssl.so.3" },
{ "flavor": "wolfSSL", "path": "/usr/lib/libwolfssl.so.42.2.0", "inode": 17433084,
"process_count": 1, "symbol": "wolfSSL_write", "probe_path": null }
],
"unreachable_count": 1
}
Actually installing the probes is a separate opt-in, and the most consequential one on this surface:
sudo sipnab -N --mcp -d eth0 --mcp-allow-tls-capture --quiet
// start_tls_capture { "flavors": ["openssl"] }
// stop_tls_capture { }
{ "running": false, "messages": 412, "lost": 0, "uptime_sec": 96 }
What to look for:
- Read
privilegedbefore believing an empty list. Unprivileged,/proc/<pid>/mapsis readable only for the server’s own processes, so a short list is evidence about privilege rather than about the host. Thesummaryfield says which of the two produced the answer, so a relayed conclusion does not lose it. probe_path: nullis a finding, not a blank. That library is carrying traffic sipnab cannot capture — usually a containerized process whose/proc/<pid>/rootthis server cannot read. Reporting it is what keeps a capture from looking complete when it is not.inodeis there becausepathis not unique. The same string names different files in different mount namespaces, and on an ordinary host with containers several distinctlibssl.so.3files coexist.loston the stop is the number the kernel dropped because the reader fell behind — messages that existed and are missing, which is a different fact from a quiet trunk and the only one you cannot discover any other way.
Pitfalls:
- Keep calling
stop_tls_captureuntilrunningis false. The stop is a request: the worker owns the probes and removes them on its way out, a kernel round trip per probe. Probes left installed cost every process that maps the library, and they outlive sipnab. - Three refusals arrive before any kernel state exists, each naming itself: not root (a server started with
--userhas already dropped privileges and cannot attach probes later), a live source already running (sipnab’s stores have one writer), and a capture still loading (pollcapture_statusuntilload.done). - An attach failure arrives later, not from the start call: a background thread installs the probes and the call returns as soon as it starts. Poll to see whether messages actually arrive.
- The uprobe path costs you the addresses. See recipe 7g for what a dialog from this source can and cannot say, and 7h for the backend that pairs the write with its socket.
57. Ask the capture how many calls failed
Problem: An agent asked “how many calls failed?” fetches rows and tallies them in its head. Counting is the operation a language model gets wrong most reliably, and a truncated page makes a confident wrong total.
aggregate_dialogs counts inside the store and returns the buckets:
sipnab -N --mcp -I capture.pcap --mcp-max-rows 200 --quiet
// aggregate_dialogs { "group_by": "response_code", "filter": "state == 'Failed'" }
{
"group_by": "response_code",
"buckets": [ { "value": "503", "count": 412 }, { "value": "486", "count": 77 } ],
"other_count": 11,
"distinct_values": 6,
"total_matched": 500
}
get_capture_report is the whole-capture analysis — the one --report prints — for the questions that are about the capture rather than about one call:
// get_capture_report { "format": "json" }
{ "findings": [], "dialogs_examined": 2, "streams_examined": 2,
"frames_read": 852, "complete": true }
What to look for:
- The buckets plus
other_countalways equaltotal_matched. A truncated aggregate that does not say what it left out is a wrong total rather than a partial one, so nothing is silently dropped — including nulls, which become the literal(none), because “how many dialogs carry no User-Agent” is a real question. - Read
completebefore the findings. It isfalsewhen the capture lost packets, hit a retention cap, or held frames no decoder could read, and a findings list from such a capture is a floor, not a total. It also readsfalsewhile a load is still running and for a source whose read stopped before its end. - Legal
group_byvalues arestate,response_code,method,from.user,to.user,ua,src.ip,dst.ipandrtp.codec. Anything else fails naming the legal set.
Pitfalls:
- One dimension, and no time bucketing. That is a deliberate cap: two dimensions is a pivot table, and a pivot table wants a UI. Narrow the window with
filterinstead. - Grouping by
from.user,to.useroruareturns fenced values, because those are text the packet’s sender wrote. A state name, a status code, an address or a codec is sipnab’s own derivation and comes back verbatim. markdownandtextrenderings of the report have no envelope to carry the completeness flags, so they state it in the document — anINCOMPLETE RUNblock, the same one--reportappends. Ask forjsonwhen you want the booleans as fields.
58. Ask why the MOS is what it is
Problem: “The MOS is 3.6.” A score is a conclusion, and the argument with a carrier is about the facts underneath it.
rtp_stats gives the score. media_diagnostics gives the inputs, each labeled with what kind of number it is:
sipnab -N --mcp -I capture.pcap --quiet
// media_diagnostics { "call_id": "[email protected]" }
{
"applicable": true,
"streams": [
{ "ssrc": "0x343da99b", "codec": "PCMU", "packets": 425,
"qos": { "marking_observed": true, "dscp": 0, "name": "CS0 / default (best effort)" },
"jitter": { "grounded": true, "clock_basis": "rfc3551", "clock_rate_hz": 8000,
"measured_ms": 0.0054 },
"delay": { "source": "assumed", "assumed": true, "one_way_ms": 100.0 },
"silence": { "cn_frames": 0, "periods": 0, "total_ms": 0 } }
]
}
What to look for:
- Read
applicablefirst. It isfalsewhen no RTP stream belongs to the dialog, and the response then carries almost nothing. An emptystreamsarray would read as “sipnab checked the media and it was fine”, which is a different claim from “no media reached the capture point”. - Three things in the answer above read together. The media is in the default queue, so it competes with bulk traffic — the most common cause of jitter that adding bandwidth does not fix. The jitter figure is a measurement, because payload type 0 has a clock rate RFC 3551 fixes. And the delay behind the MOS is a default, not anything this capture showed, so the score is only as good as that assumption (recipe 31).
jitter.grounded: falsemeans the stream supplied no clock rate and sipnab fell back to a default. Jitter is an RTP-timestamp difference divided by that rate, so a wrong divisor gives a different quantity, not a rough one — and an ungrounded stream reports nomeasured_msat all.qos.remarked_toappears only when the stream’s last packet carries a different code point from its first: an SBC or a policy boundary rewriting the marking in flight. Its presence is the finding, and a steady stream omits it rather than repeating the same number.
Pitfalls:
endpoint_reported— what the far end said over RTCP — sits apart from everything beside it and feeds nothing. Nobody authenticates RTCP, anyone can forge it, and a report describes the path from the sender to the reporter, which on a mid-path capture is a different segment from the one sipnab watches. The two disagreeing is normal and informative.marking_observed: falseon a HEP-fed stream is not a missing marking. sipnab saw no IP header, because a mirror carries the message and not the frame.
59. Decrypt TLS with the server’s private key
Problem: You hold the SIP server’s TLS private key and no keylog, because nobody was running one when the calls happened.
-k/--tls-key takes the PEM private key and uses it to recover the pre-master secret:
sudo sipnab -N -d eth0 -k /etc/sipnab/server-key.pem
The same key reads a capture you already recorded: sipnab -N -I capture.pcap -k /etc/sipnab/server-key.pem --report. sipnab refuses to start when it cannot read the key, rather than capturing everything and decrypting nothing.
What to look for:
- This works for TLS 1.2 RSA key exchange only — the handshakes with no forward secrecy, where the client encrypts the pre-master secret to the server’s key, so that key recovers it afterwards.
- If the decryption produces nothing, look at the cipher suite in the handshake before suspecting the key. An ECDHE or DHE suite is doing exactly the job it exists to do.
Pitfalls:
- Forward secrecy is the normal case now, and it defeats this. ECDHE and DHE handshakes need
--keylog(recipe 7) or a uprobe (recipe 7g); a server key cannot recover them, and no flag changes that. - TLS 1.3 removed RSA key exchange entirely. On a TLS 1.3 capture this flag has nothing to do.
- A server private key is a larger secret than a keylog: a keylog decrypts the sessions it covers, and this decrypts every session that key ever protected. sipnab disables core dumps once decryption is active, for the same reason it does with a keylog.
60. See who is on a recorded call
Problem: Something is recording a call and you need to know what the recorder learned about it — which parties, and which media stream belongs to which one.
A session recording client (an SBC, or OpenSIPS’s siprec module) sends the recording server an INVITE whose body carries an application/rs-metadata+xml part. sipnab reads that part off the wire like any other body:
sipnab -N -I recorded.pcap --call-report "[email protected]"
SIPREC recording ([email protected]): session 4f1c0a2e, complete
participant sip:[email protected] (Alice)
participant sip:[email protected]
stream label 0 -> sip:[email protected]
stream label 1 -> sip:[email protected]
stream label 2 -> sip:[email protected]
The same facts reach an agent through MCP as siprec_metadata { "call_id": "..." }, and the REST API serves them on GET /v1/dialogs/{call_id} as a siprec block:
"siprec": {
"session_id": "4f1c0a2e",
"mode": "complete",
"participants": [
{ "participant_id": "9b2d1f00", "aor": "sip:[email protected]", "name": "Alice" },
{ "participant_id": "c7e40a13", "aor": "sip:[email protected]" }
],
"streams": [
{ "stream_id": "1a2b3c4d", "label": "0", "participant_id": "9b2d1f00" },
{ "stream_id": "2b3c4d5e", "label": "1", "participant_id": "9b2d1f00" },
{ "stream_id": "3c4d5e6f", "label": "2", "participant_id": "c7e40a13" }
]
}
In the TUI the ladder annotates the recording INVITE in place, so you see it without leaving the call flow.
What to look for:
labelis them=line. Alice has two streams here, labels 0 and 1 — the audio and the video of one call. The label ties a recorded stream back to the media description it came from, and on a multi-stream call nothing else does.participant_idis who SENDS the stream. Bob receives Alice’s audio and video, and neither is his. Ownership comes from<participantstreamassoc>, whose<send>children mean origination and whose<recv>children do not.- A missing
nameis missing, not empty. An SRC that has no display name for a party sends the AOR alone.
Pitfalls:
recorded: falsedoes not mean the call went unrecorded. It means no SIPREC metadata reached the capture point. A recorder signaling on a path sipnab cannot see looks exactly the same from here, and the tool says so rather than implying the stronger claim.- sipnab reads SIPREC and does not speak it. It is not a session recording client and not a recording server; putting it in the recording path is not what it is for.
- Recording metadata rides in the INVITE that opens the recording dialog. A capture that starts mid-call has missed it, and no later message repeats it unless the SRC re-sends.
Look up a one-liner by task
The recipes above walk through a problem end to end. This section is the other shape: dense one-line commands to copy when you already know what you want and just need the invocation. Every flag used here appears in cli-reference.md.
Triage a capture fast
sudo sipnab -d eth0— watch SIP interactively on an interface (TUI)sipnab -N -I capture.pcap --problems— show only problem calls from a pcap. The flag expands to theproblemsDSL alias, so it covers the whole diagnostic set (Failed state, one-way audio, loss/jitter, NAT mismatch, retransmits, PDD, asymmetry, late media) andsipnab -N -I capture.pcap --filter problemsreturns the same callssipnab -N -I capture.pcap --call-report '[email protected]'— deep-dive one call: ladder, timing, SDP, RTP quality, diagnosissipnab -N -I capture.pcap --call-report '[email protected]' --markdown > call.md— the same as a Markdown report for a ticketsipnab -N -I capture.pcap --report --no-cli-print— post-capture aggregate summary only, no per-message noise
Narrow a capture to the calls you care about
sudo sipnab -N -d eth0 --from '^1001@' --to '^18005551212'— calls from/to specific users, matched as regexessipnab -N -I capture.pcap --filter "method == 'INVITE' and rtp.mos < 3.5"— filter DSL: INVITE dialogs that ended with bad audio qualitysipnab -N -I capture.pcap --filter codec-asym— diagnostic aliases go through the same flag (see docs/filter-dsl.md);sipnab -N -I capture.pcap --filter late-mediais the same flag with the late-media aliassipnab -N -I capture.pcap --slow-setup— slow call setup, meaning long post-dial delay
Feed NDJSON into jq and other tools
NDJSON to jq, counting failures by status code:
sipnab -N -I capture.pcap --json | jq -s 'map(select(.status_code >= 400)) | group_by(.status_code) | map({code: .[0].status_code, n: length})'
Every Call-ID seen on the wire, ready to feed back into --call-report:
sipnab -N -I capture.pcap --json | jq -r '.call_id // empty' | sort -u
More in output-formats.md.
Record traffic to disk, encrypted or not
Capture SIP+RTP to rotating pcapng files, 50 MiB chunks:
sudo sipnab -N -d eth0 -O /var/capture/sip.pcapng --pcapng --split filesize:50
Run that capture forever inside 400 MiB, keeping the newest eight chunks:
sudo sipnab -N -d eth0 -O /var/capture/sip.pcapng --pcapng \
--split filesize:50 --split-keep 8
--split-keep deletes capture files — the older chunks, as rotation
creates new ones. sipnab deletes nothing without the flag, and deletes only
the files that running process created and named, so anything else in
/var/capture stays. Leave it off whenever the capture is evidence you cannot
retake.
Decrypt SIPS signaling with a TLS key log and export decryptable pcapng. --keylog is the SIP/TLS NSS keylog — signaling only, and it does not decrypt media.
sudo sipnab -N -d eth0 --keylog /tmp/sslkeys.log --keylog-watch \
-O decrypted.pcapng --pcapng
SRTP needs media keys instead:
sipnab -N -I capture.pcap --dtls-keylog /tmp/dtls.keylog— keys recovered from DTLS-SRTP handshakessipnab -N -I capture.pcap --srtp-keys /tmp/srtp-keys.txt— AES-CM master keys; SDESa=cryptokeys are also learned from SDP
Detect scanners and block abuse
sudo sipnab -N -d eth0 --kill-scanner --alert syslog— detect SIP scanners and answer them, rate-limitedsudo sipnab -N -d eth0 --fail2ban— emit fail2ban-compatible lines for scanner/flood sources
Run a command when a call or its quality changes
sudo sipnab -N -d eth0 --on-dialog-exec '/usr/local/bin/call-logger'— run a command on every dialog state change; details arrive asSIPNAB_*env vars plus aSIPNAB_JSONpayload, never shell-interpolatedsudo sipnab -N -d eth0 --on-quality-exec '/usr/local/bin/page-noc'— alert when RTP quality drops
Exchange HEP with Kamailio, OpenSIPS or Homer
sipnab -N -L 0.0.0.0:9060 --hep-allow 192.0.2.0/24— receive HEP from Kamailio/OpenSIPS/Asterisk and analyze live. A routable bind needs the allowlist (or--hep-auth), or sipnab refuses to start.-L/--hep-listendecodes HEP on its own;--hep-parseis only for unwrapping HEP that arrives inside ordinary UDP capturesudo sipnab -N -d eth0 -H homer.example.net:9060— mirror captured traffic to Homersudo sipnab -N -d eth0 -H collector:9060 --hep-send-transport tcp— mirror over TCP instead of UDP, for a collector that wants a streamsipnab -N -L 0.0.0.0:9060 --hep-listen-transport tls --hep-tls-cert cert.pem --hep-tls-key key.pem --hep-allow 192.0.2.0/24— receive HEP inside TLS. Each side names its own transport, so a relay can take TCP in and send UDP out
61. Ask a relay whether it is dropping packets
Problem: Audio is bad and the media relay (rtpengine) is the prime suspect. You want the relay’s OWN drop and throughput counters, not a guess from the packets your capture happened to see.
A relay statistic is a claim from the relay, not something sipnab measured. Asking over the relay’s read-only control port prints what it says, tagged as the relay’s own count so you never mistake it for sipnab’s:
sipnab -N -d eth0 --rtpengine-control 127.0.0.1:22222 --relay-stats
Every figure names the relay as its source (wire tier relay_reported). Reading it as “sipnab saw this” is the one mistake this whole view exists to stop.
Pitfalls:
- A relay that restarted a second ago reads as a quiet one. rtpengine’s counters are cumulative and reset to zero on restart. Where the relay publishes
uptime, sipnab shows it beside them, and rtpproxy publishes none — so a small number can mean “handled little” or “just restarted”, and only the uptime tells them apart. sipnab flags a polled series (--relay-stats-interval) that steps backwards as a probable restart rather than a drop in traffic. - Asking transmits to the relay, so this needs a live source (
-d), never a capture file. sipnab refuses--relay-statson-I file.pcap, because a file’s addresses belong to third parties. - A statistic missing from the reply was not asked for. A zero means the relay counted zero. They are different answers and never render the same.
62. Ask a relay what it is still holding
Problem: You suspect leaked sessions — calls that ended but whose media ports the relay never tore down.
sipnab -N -d eth0 --rtpengine-control 127.0.0.1:22222 --relay-stats
Read the active-session count beside the created and destroyed totals: a created count far above destroyed, with a high active count, is the shape of sessions nobody released.
Pitfalls:
- “Active” is this instant, not a window. A session torn down a second ago is already gone from the count. Poll on a timer with
--relay-stats-interval 5to watch it move rather than reading one snapshot. - The counters are the relay’s own (
relay_reported), cumulative, and reset on restart — see the uptime caveat in recipe 61.
63. Compare a relay’s per-call count with your capture
Problem: You have a Call-ID and want to know whether the relay’s view of it matches what your capture saw — “we sent it” / “we never got it”, one hop up.
sipnab -N -d eth0 --rtpengine-control 127.0.0.1:22222 --relay-compare 'abc123@host'
This shows two figures side by side — the relay’s own RTP packet count for the call, and the count sipnab measured from the media it captured — each showing which it is, and a one-word verdict (match or differ) with a plain-language note.
Pitfalls:
- A difference is not automatically a fault. The two count different sockets over different windows with different start times. Three ordinary causes: a capture off a mirror port under load misses packets, a capture that sees BOTH sides of the relay’s hairpin counts each packet twice, and a relay that restarted mid-call zeroed its counters. The note names these — read it before you escalate.
- The relay side is absent, not zero, when the relay does not hold the call, and sipnab’s side is absent, not zero, when your capture saw no RTP for it. An absent side never renders as
0, and sipnab flags a per-call total too large to compare as suspect rather than reading it as a call the relay does not hold.
64. Read a relay’s loss beside the loss you measured
Problem: The relay reports packet loss and so does sipnab, and the two numbers disagree. You need to know which to believe.
Neither — because they are not the same measurement. This is the recipe most easily got wrong, and getting it wrong is how a real fault gets blamed on the wrong hop. There are three different “loss” numbers for one call, and each is a different claim:
| The number | Wire tier | What it actually is |
|---|---|---|
| The relay’s own loss counter | the relay’s own count (relay_reported) | what the relay’s box counted, on its sockets, since its last restart |
| sipnab’s sequence-gap loss | what sipnab saw on the wire (sipnab_measured) | gaps in the RTP sequence numbers that reached the capture point — bounded by what your capture saw |
the far end’s RTCP fraction lost | what the far end claims (endpoint_reported) | the remote endpoint’s own assertion, never checkable |
sipnab -N -d eth0 --rtpengine-control 127.0.0.1:22222 --relay-stats
sipnab -N -I capture.pcap --call-report 'abc123@host'
The first asks the relay for its own loss counter, live. The second reads what sipnab measured on the wire and the far end’s RTCP claim, from the capture. None is authoritative over the others: the relay’s count and sipnab’s differ whenever the capture is off the relay’s path, and the far end’s claim is about a third socket again. Cite the tier when you quote the number — “the relay reports 40 lost, our capture measured 3 gaps” — and the disagreement stops being a contradiction and becomes three facts about three points on the path.
Pitfalls:
- Do not subtract one tier from another. The relay’s count minus sipnab’s is not “packets sipnab missed” — the two count different sockets over different windows, so the difference describes nothing.
- For sipnab’s own capture loss versus the network’s (a different axis — whether YOUR capture dropped packets), see recipe 22.
Next steps
- keybindings.md — the interactive TUI these captures feed
- filter-dsl.md — the full filter language behind
--filter - mcp.md — drive the same analysis from an AI agent