Docs CLI Reference

CLI Reference

Complete flag reference for sipnab, organized by functional group.

On this page

Quick start: sipnab -I capture.pcap to analyze a file, or sudo sipnab for live capture on the default interface. Add -N for non-interactive output.

Complete flag reference for sipnab. This page groups flags by function.

CLI flags always override config file values (see config-reference.md). Boolean flags default to off (false) unless otherwise noted. For task-oriented recipes rather than a flag catalog, start with examples.md.

Common Recipes

A few flag combinations to get productive fast. For the full task-oriented collection — triage, filtering, recording, security, HEP — see the Cookbook. For symptom-driven diagnostics see Troubleshooting. This page is otherwise a flag reference (grouped below).

Debug a failed call

Start by listing every failed call in the pcap, which is where the Call-IDs worth chasing come from.

sipnab -N -I capture.pcap --filter "state == 'Failed'"

With one of those Call-IDs in hand, print just that dialog’s call flow. --no-cli-print is what makes it just that: on its own, --call-report appends the report to the whole capture’s per-message dump, so the report you came for arrives after every message in the file.

sipnab -N -I capture.pcap --call-report "abc123@host" --no-cli-print

When the finding belongs in a ticket, write the same report as Markdown to a file instead of reading it on the terminal. Keep --no-cli-print here too, or report.md opens with hundreds of lines of raw SIP before its first heading.

sipnab -N -I capture.pcap --call-report "abc123@host" --markdown --no-cli-print > report.md

Monitor live SIP quality

Watch live traffic for calls that are already degraded — MOS below 3.0, or audio flowing in only one direction.

sudo sipnab -N -d eth0 --filter "rtp.mos < 3.0 OR one_way == true"

Feed the same set of problem calls into a monitoring pipeline as NDJSON while keeping a copy on disk.

sudo sipnab -N -d eth0 --problems --json | tee /var/log/sipnab/problems.ndjson

Hand each quality drop to an external alerting script rather than reading it yourself. --exec-rate-limit bounds the invocations per second, so one bad trunk cannot fork a process per stream.

sudo sipnab -d eth0 --on-quality-exec "/usr/local/bin/pagerduty-alert.sh" \
  --quality-threshold 3.0 --exec-rate-limit 5

Measure post-dial delay across calls

Find the calls whose setup took longer than three seconds, as NDJSON for whatever consumes it next.

sipnab -N -I capture.pcap --filter "pdd > 3.0" --json

The --slow-setup alias asks the same question against the run’s own post-dial delay threshold — 11 seconds, unless --pdd-threshold or [diagnosis] post_dial_delay_secs moves it — so a quick check needs no filter expression at all.

sipnab -N -I capture.pcap --slow-setup --report

Security monitoring

Detect SIP scanning and append it in fail2ban’s format to the log its jail reads. --kill-scanner is what detects — --fail2ban only chooses the format, so leaving it out leaves the log empty.

Every example below passes -N, and that is not a style choice. Only the headless single-capture path builds the detectors: the interactive TUI (the default when you leave -N off) and the --cores N parallel reader both run without them. Ask for detection on either and sipnab warns at startup that it accepts the flag and ignores it, because an empty finding list otherwise reads as an all-clear.

sudo sipnab -N -d eth0 --kill-scanner --fail2ban >> /var/log/sipnab/scanners.log

Before that log reaches a jail, run the same detectors over a capture of an ordinary hour and read the list of addresses they would ban. On a carrier trunk it routinely names your own SBCs, because the enumeration signature and a busy hunt group look alike.

sipnab -N -I capture.pcap --kill-scanner --fail2ban \
  | grep -oE 'src=[^ ]+' | sort | uniq -c | sort -rn

Audit a capture for digest credentials that went out where anyone could read them.

sipnab -N -I capture.pcap --digest-leak

Run the whole sweep at once — scanners, fraud heuristics, and registration floods — with alerts going to both syslog and structured JSON.

sudo sipnab -N -d eth0 --kill-scanner --fraud-detect --reg-flood \
  --alert syslog --alert json --syslog

Export for Wireshark analysis

Print a display filter to paste into Wireshark after opening the capture.

sipnab -N -I capture.pcap --wireshark

Or print a tshark-compatible filter string, when the next step is a shell pipeline rather than the Wireshark GUI.

sipnab -I capture.pcap --tshark-filter 'sip.from.user == "1001"'

Export call audio as WAV

Audio export is a TUI workflow — see Keybindings.

Pipe through jq for custom analysis

Count failures by response code, so the dominant one is obvious.

sipnab -N -I capture.pcap --filter "state == 'Failed'" --json \
  | jq -r 'select(.is_request == false) | .status_code' \
  | sort | uniq -c | sort -rn

List every distinct User-Agent the capture saw, to find the odd endpoint out.

sipnab -N -I capture.pcap --json \
  | jq -r '.ua // empty' | sort -u

Bound, split, and multi-interface captures

Stop after a fixed packet count and summarize the capture.

sipnab -N -d eth0 -n 1000 --report

Roll to a new pcapng every 50 MiB, so a long capture does not become one file too large to open.

sipnab -d eth0 -O /var/captures/sip.pcapng --pcapng --split filesize:50

Keep only the newest four of those files, so a long capture fits a fixed amount of disk. --split-keep deletes the older files as rotation creates new ones, and it deletes only the files this run wrote.

sipnab -d eth0 -O /var/captures/sip.pcapng --pcapng --split filesize:50 --split-keep 4

--split-keep deletes capture files. sipnab deletes nothing unless you pass the flag, and nothing at --split-keep 0, because a capture is very often the only copy of the evidence. sipnab deletes only the files the running process created and named — it never lists the directory, so a file an earlier run, another tool, or you left beside them stays where it is, however closely its name resembles a rotation. A run that dies mid-capture leaves behind whatever it had not yet deleted; the next run starts its own list and never adopts those files. sipnab names each file it deletes in the log and counts them in the closing summary.

Capture across every interface at once, timestamping each message relative to the one before it. On Linux the any pseudo-device is what makes this every interface — --multi-device is for naming a specific list, as below.

sipnab -d any --delta-time

Capture on two named interfaces instead, one libpcap handle each.

sipnab -d eth0,eth1 --multi-device --delta-time

Tip: Every output flag (--json, --report, --fail2ban, etc.) needs -N. Think of it as “non-interactive mode” – it disables the TUI and writes to stdout instead.


Capture

FlagValueDefaultDescription
-d, --device<IFACE>platform defaultNetwork interface to capture on. With no -d, no -I file and no -L HEP listener, sipnab picks a default that differs by platform — see the note below
-I, --input<FILE|DIR|GLOB>Read packets from a capture file, a directory of them, or a glob, instead of live capture. Repeatable. sipnab reads the files in capture order, never in filename order — see the note below
--recursiveoffDescend into subdirectories when -I names a directory
--input-name<GLOB>Read only files whose name matches this pattern when -I names a directory. Applies at every depth under --recursive
-O, --output<FILE>Write captured packets to a pcap file
-B, --buffer<MIB>64Kernel capture buffer size in MiB (per device). See Tuning capture
--buffer-budget<MIB>64Memory budget for the in-flight capture→processing queue. The queue grows under load up to this budget (capped, never OOM) and shrinks when idle; overrides [capture] buffer_budget_mb
--snaplen<BYTES>65535Snapshot length for packet capture (bytes)
--capture-profilesignaling|fullPicks a --snaplen for you. signaling uses 1500, keeping every SIP header whole while dropping the bulk of an RTP stream; full uses 65535, which is what sipnab has always done. A large snaplen costs on EVERY packet — the kernel copy and the ring occupancy — and that is what makes a busy server drop, so the saving is real. It is a named profile rather than a smaller default because truncation is not free: it breaks --retain-audio, WAV export and Opus decode, which need RTP payload and not just headers, and it degrades -O re-emit to truncated frames. 1500 rather than a tighter 200-400: one INVITE with a full Record-Route set, a long Contact, ISUP encapsulation or a fat SDP offer passes 400 bytes routinely, and a snaplen that cuts a header makes the message stop parsing — reporting the peer that sent a valid message as broken. An explicit --snaplen overrides it. See sipnab_capture_snapped_frames_total for how much of a capture arrived truncated
-S, --limitlen<BYTES>Parse only the first N bytes of each packet. Caps what the SIP parser and matchers inspect, independent of --snaplen (capture length) and --payload-limit (display truncation)
--no-reassemblyoffDisable IP-fragment and TCP-segment reassembly; sipnab parses every packet standalone (inverse of segment reassembly). Useful for pure single-packet UDP scanning
-x, --quiet-bad-parseoffSuppress the per-packet “SIP parse error” diagnostic emitted when a SIP-looking packet fails to parse. sipnab drops the packet either way; this only silences the notice on a noisy link
--portrange<RANGE>5060-5061SIP signaling port range. Media is never gated — RTP uses SDP-negotiated dynamic ports. The default is narrow and carriers routinely run SIP on 5070, 5080 and elsewhere, so widen it or analyze a fraction of the file — see the note below
--ws-portrange<RANGE>80, 443, 8080, 8443Ports carrying SIP-over-WebSocket (RFC 7118), as one inclusive START-END range in the same grammar as --portrange. The shipped set is the browser’s view of the web, not a deployment’s: Kamailio, OpenSIPS and Janus each default to WSS outside it, and behind a reverse proxy sipnab sees whichever port the proxy forwards to — so the whole WebRTC signaling leg stays invisible. A range replaces the shipped set, exactly as --portrange replaces the default signaling ports. sipnab counts the SIP-over-WebSocket it declines to unwrap and names the ports it arrived on. Config: [capture] ws_ports
--multi-deviceoffOpen one capture per interface named in a comma-separated -d list, e.g. -d eth0,docker0 --multi-device. It does not enumerate interfaces for you: with a single -d (or none) it falls back to an ordinary single capture. On Linux the zero-argument default already sniffs every interface via the any pseudo-device
--no-rtpoffDisable RTP capture and analysis
-p, --no-promiscoffDo not put the interface into promiscuous mode. Promisc is on by default for a named device; the any pseudo-device is never promiscuous
--bpf-file<FILE>Read BPF filter from a file
--capture-tunnels[<PORTS>]offAlso capture all traffic on the UDP tunnel ports, so SIP inside GTP-U, VXLAN or GENEVE reaches sipnab. Bare flag means 2152,4789,6081; pass a list for non-standard ports (--capture-tunnels=8472). Off by default because it is not a narrowing filter — BPF cannot walk a GTP-U extension-header chain to the inner port, so covering these means taking the whole port, which on a mobile core is the entire user plane. Ignored when you supply your own filter
-n, --count<N>Stop after receiving N packets (counts every packet received, including any a HEP listener later drops by allowlist, rate limit, or auth)
--duration<DURATION>Stop after duration (e.g., 30s, 5m, 1h)
--autostop<CONDITION>Autostop condition: filesize:N stops after N MiB of output, duration:N after N seconds. filesize counts MiB, the unit --split filesize and -B/--buffer also use
--split<CONDITION>Split output files (e.g., filesize:50 for 50 MiB chunks)
--split-keep<N>Keep only the newest N split files: sipnab deletes the older ones as --split rotates, turning -O into a ring buffer. Off unless you pass it, and off at 0. sipnab deletes only the files the running process created and named, so a file left by an earlier run, another tool, or you survives however closely its name resembles a rotation. See the warning above
--replayoffReplay packets from a pcap file at original timing
--pcapngoffUse pcapng format for output files. pcapng Metadata covers the metadata sipnab writes into pcapng output
<BPF_FILTER>...positionalBPF display filter expression (trailing positional args)

The auto-generated filter looks through VLAN, QinQ, PPPoE and MPLS. On a live capture with no filter of your own, sipnab installs one built from --portrange. It is not a bare portrange 5060-5061: that one matches the outer headers only, so on a tagged trunk, a PPPoE access link or an MPLS core it matches nothing, and the kernel discards the frames where no sipnab counter, metric or report can see them. You get “No SIP traffic found” on a link carrying calls.

The generated filter adds an encapsulated arm instead, covering one VLAN tag (802.1Q, 802.1ad or 0x9100), QinQ, PPPoE Session, VLAN over PPPoE, and one or two MPLS labels, for IPv4 and IPv6, UDP and TCP. The arm still demands a signaling port, so it matches more of the same traffic, not a new class of it: VLAN-tagged RTP reaches sipnab no more often than untagged RTP did.

It covers cooked captures too, so omitting -d costs you nothing. The arm asks “does this frame carry an encapsulation?” through libpcap’s ether proto, which resolves to the right byte offset for whatever link type the filter compiles against — offset 12 on Ethernet, 14 on Linux cooked v1, 0 on Linux cooked v2, and a constant false on raw IP and the two loopback link types, which carry no protocol field at all. Measured on a capture of each type with tcpdump -d.

Asking the same question with a fixed ether[12:2] is the trap this avoids. That offset holds the EtherType on Ethernet and part of the link-layer address on a cooked capture, so an arm written that way compiles, runs and matches nothing there: 1 of 11 encapsulated SIP frames on cooked v1 and cooked v2, against 11 of 11 on Ethernet. Cooked is what Linux gives you when you name no interface, so that shape would have left the default invocation blind.

Two limits worth knowing. On the encapsulated arm an IPv4 header carrying options stays unmatched. A BPF byte offset has to be a constant, so the arm cannot multiply the IHL nibble into the port offset the way libpcap’s own portrange does. The untagged portrange handles those, so this costs you only IPv4-options traffic that is also encapsulated.

And one filter string serving three link types has to carry all three sets of inner offsets, because BPF offers no way to ask which link type it compiled against. Seven offsets get probed on every link type, four of which belong to a different link header. Those four can fire only on a frame that already carries one of the six encapsulating protocols, and only if its bytes at the wrong offset spell a complete IPv4-or-IPv6 header with a signaling port — so the worst case is a stray tagged packet reaching userspace, where the parser rejects it. Ordinary traffic never reaches those probes, because the outer ether proto test is exact.

UDP tunnels are opt-in. GTP-U, VXLAN and GENEVE are not covered by default and sipnab says so at startup. BPF cannot parse a variable-length GTP-U extension-header chain to reach the inner port, so the only way to cover them is to capture everything on the port — see --capture-tunnels.

A filter you supply is never rewritten. It goes to pcap_compile exactly as typed. If it looks encapsulation-blind, sipnab says so once and still uses your expression.

What you get when you omit -d. The default is not the same everywhere, and the difference decides whether you see loopback traffic:

PlatformDefaultScope
Linuxthe any pseudo-deviceevery interface at once, loopback included
macOS / BSDlibpcap’s default device, from the routing table; otherwise the first non-loopback interfaceone interface

On Linux this is deliberate and matches the terminal viewer: a SIP proxy often talks to itself over loopback, so capturing only eth0 silently misses it. Pass -d any to say so explicitly. Promiscuous mode does not apply to any, so --no-promisc changes nothing there.

On macOS you get a single interface. If SIP is not on the one libpcap picked, you see nothing and the capture looks merely quiet — name -d explicitly.

Reading a set of files: order comes from the packets, not the names. tcpdump -C 100 -W 10 writes a ring buffer — tg.pcap0 through tg.pcap9 — and then wraps, overwriting the oldest file in place. A real set measured for this feature ran tg.pcap7, tg.pcap8, tg.pcap9, tg.pcap0tg.pcap6 in time order: the numeric suffix records where tcpdump was in its cycle, not when the packets arrived.

So sipnab sorts by each file’s first packet timestamp. Neither lexicographic nor natural-numeric filename order reconstructs that capture, and replaying it out of order corrupts every timing derivation — post-dial delay, setup time, retransmission detection, and the RFC 3261 Timer B/C/H bounds all assume timestamps only move forward.

sipnab recognizes a capture by opening it, not by its extension — tg.pcap0 has the extension pcap0, and plenty of captures have none at all. It decompresses gzip members transparently, so a directory holding both .pcap and .pcap.gz needs nothing special.

A file you name directly with -I that sipnab cannot read is an error. One it discovers by expanding a directory or glob it skips with a warning, because directories hold other things.

Why it matters beyond tidiness: reading a split capture as a set is the only way to see a call whose INVITE lands in one file and whose BYE lands in the next. Analyzed one file at a time, that call appears as one that never ends plus a stray BYE, and neither half is the truth. On the 10-file, 921 MB set above, 2271 of 20512 calls — 11% — spanned a boundary.

-I and -d are alternatives, not companions. sipnab accepts both, and the FILE wins: sipnab reads it, never opens the interface, and the output looks like a normal run. sipnab warns on stderr when you do this. To switch a file command to live capture, remove -I rather than adding -d beside it.

--portrange decides how much of the file you analyze. The default, 5060-5061, is narrow. SIP on other ports is ordinary: carriers and SBCs use 5070, 5080 and others routinely, and a capture from a real trunk commonly carries a large share of its signaling outside the default.

Reading a file, sipnab skips any SIP message whose source and destination ports both fall outside the range. A skipped message reaches no message count, no dialog, and no output format — so every total you read, and every ratio you compute from one, describes the range and not the capture. sipnab counts what it skipped and says so on stderr and at the end of the run, naming the busiest ports so there is something to widen to:

NOT ANALYZED: 1 further SIP message(s) were seen on ports outside --portrange
and are in none of the totals above. Busiest: 8090 (1). Re-run with
--portrange 1-65535 to include them.

--portrange 1-65535 analyzes everything the capture holds. Reach for it first on an unfamiliar capture, then narrow once you know what is in there.

Live capture is different, and worse. With no explicit BPF filter sipnab compiles the range into the filter, so the kernel drops the traffic before sipnab sees it — nothing downstream, this counter included, can report what went missing. Set the range correctly before the capture, because no rerun recovers it.

NOT DECODED is the other line to read before the totals. --portrange is about SIP sipnab chose not to analyze; this is about frames it could not read at all — an unsupported link type, an EtherType carrying no IP, an IP protocol that is no transport, a truncated frame, a decode error. Such a frame counts as a packet (it arrived) and reaches no message, dialog or stream, so on its own the summary reports the same thing whether the capture held no SIP or sipnab understood none of it:

NOT DECODED: 49 of 49 frame(s) (100.0%) produced nothing and are in none of
the counts above. Reasons: unsupported link type 0 (49). NOTHING IN THIS
CAPTURE WAS READ — every frame failed to decode, so the totals above describe
no traffic whatsoever and a zero among them is not evidence of absence.

Every reason carries the number that identifies it, because that number is what you act on: unsupported link type 0 says the file is DLT_NULL and editcap -T ether in.pcap out.pcap converts it. A small count is normal — ARP is undecodable by definition and appears on any Ethernet capture — so read the share, not the count. When the share is high, sipnab additionally refuses to state “No SIP traffic found” as a finding, because it has no basis for one. The same breakdown appears as a NOT DECODED (capture-wide) section in --report, and as sipnab_capture_undecodable_frames_total{reason} plus sipnab_capture_undecoded_fraction on /metrics.

docs/troubleshooting.md tables what each reason means and what to do about it.

Examples

  • sipnab -N -I capture.pcap --sandbox best-effort --report — analyze a capture with the filesystem bounded to the input and the crash directory. On a kernel without Landlock this warns and analyzes anyway, which is the point of best-effort

  • sudo sipnab -N -d eth0 -O /var/captures/live.pcap --sandbox required — refuse to capture at all unless the ruleset installed. Use this where an unsandboxed capture is worse than none; the run exits non-zero and names the reason rather than starting

  • sipnab -N -I capture.pcap --seccomp log — read a capture with every system call recorded. The filter refuses nothing, so the analysis matches a run without the flag; what you get is the record of which calls a file-reading run makes

  • sudo sipnab -N -d eth0 --count 5000 --seccomp log && auditctl -s — record the calls a LIVE capture makes, bounded by --count so the log cannot run away, then check where the records went. A connected audit daemon has them in ausearch -m SECCOMP; a pid of 0 means they are in dmesg

  • sudo sipnab --device eth0 --output capture.pcap --portrange 5060-5080 --count 10000 — record up to 10000 packets from eth0 into a pcap, watching a widened SIP port range

  • sudo sipnab --device eth0 --buffer 16 --buffer-budget 128 --snaplen 2048 --quiet-bad-parse — live-capture a busy link with bigger kernel and queue buffers, a capped snapshot length, and parse-error notices silenced (the CLI matcher -x)

  • sudo sipnab -N -d eth0,eth1 --multi-device --output capture.pcap --autostop filesize:100 — capture on two named interfaces at once, headlessly, stopping once the output file reaches 100 MiB. --multi-device needs the list; without one it is a no-op

  • sipnab -N --input capture.pcap --replay --no-rtp — replay a pcap at its original timing with RTP capture and analysis disabled

  • sudo sipnab -N -d eth0 --capture-profile signaling --output signaling.pcap — record signaling on a busy link: 1500 bytes keeps every SIP header whole while dropping the bulk of each RTP packet, which is where the ring pressure comes from. Check sipnab_capture_snapped_frames_total afterwards to see how many frames arrived short

  • sudo sipnab -N -d eth0 --capture-profile signaling --snaplen 4096 — the explicit number wins over the profile, for a trunk carrying INVITEs too large even for one MTU (deep Record-Route sets, ISUP encapsulation). Use --capture-profile full instead when you need --retain-audio, WAV export or a faithful -O re-emit, all of which need RTP payload

  • sipnab -N --input /var/captures/ --json-dialogs --no-cli-print — read every capture in a directory as one timeline, so a call split across the ring buffer resolves to one dialog instead of two fragments

  • sipnab -N --input /var/captures/ --recursive --input-name '*.pcap.gz' --json-dialogs --no-cli-print — descend into per-day subdirectories and read only the compressed archives

  • sipnab -N --input 'captures/tg.pcap[0-4]' --report — analyze the first five members of a ring buffer with a glob sipnab expands itself, no shell needed

  • sipnab -N --input a.pcap --input b.pcap --json-dialogs --no-cli-print — read two named captures as a single set, ordered by their packets

  • sipnab -N --input /var/captures/ --input-name 'edge1-*' --recursive --json — pick one host’s captures out of a tree holding several

  • sipnab -N --input capture.pcap --limitlen 512 --no-reassembly --quiet-bad-parse — scan a pcap quickly: parse only the first 512 bytes of each packet, every packet standalone (no reassembly), without parse-error noise

  • sudo sipnab --device eth0 --bpf-file sip.bpf --no-promisc --duration 5m — capture for 5 minutes using a BPF filter read from sip.bpf, without putting the interface into promiscuous mode (the CLI matcher -p)

  • sudo sipnab -N --device eth0 --capture-tunnels --buffer 64 --duration 5m — capture SIP traveling inside GTP-U, VXLAN or GENEVE as well as the encapsulations the auto-filter already covers. This takes every packet on ports 2152, 4789 and 6081, so the same command widens the kernel buffer; check the drop counters in the summary before trusting a long run

  • sudo sipnab -N --device eth0 --capture-tunnels=8472 --portrange 5060-5080 --report — cover a Linux VXLAN fabric on its pre-IANA port 8472 instead of the three defaults, across a widened signaling range

  • sudo sipnab --device eth0 --portrange 5060-5090 --buffer 8 --buffer-budget 256 --duration 1h — monitor an hour of traffic across a wide SIP port range with enlarged capture buffers

  • sipnab -N --input capture.pcap --replay --limitlen 1500 --no-rtp — replay signaling only from a pcap, parsing at most 1500 bytes of each packet

  • sudo sipnab --device eth0 --bpf-file sip.bpf --no-promisc --snaplen 9000 --count 500 — stop after 500 packets that pass the sip.bpf filter, non-promiscuous, with the snapshot length sized for jumbo frames

  • sudo sipnab -N --device eth0 --output capture.pcap --autostop duration:60 --no-reassembly — write a one-minute capture that treats every packet standalone (IP-fragment and TCP-segment reassembly off)

  • sipnab -N --input webrtc.pcap --ws-portrange 8081-8081 --portrange 1-65535 --json-dialogs --no-cli-print — a WSS listener behind a reverse proxy that forwards to 8081: without the range the entire WebRTC signaling leg is invisible, and sipnab reports how many messages it skipped and on which port

  • sudo sipnab -d eth0 --ws-portrange 1-65535 --portrange 1-65535 — unwrap SIP-over-WebSocket wherever it appears on a box whose WSS port you do not know yet, then read the skip line to learn which ports were carrying it

Mode

FlagValueDefaultDescription
-N, --no-tuioffNon-interactive mode (no TUI). Required for batch/output flags
-c, --calls-onlyoffShow only SIP dialogs (calls), not standalone messages
-t, --telephone-eventoffDecode telephone-event (DTMF) RTP payloads and log each event at info, digit value masked as x
--dtmf-cleartextoffLog the DTMF digit VALUES instead of the mask, at debug. Publishes PINs and card numbers
-q, --quietoffSuppress informational output; only show results

Examples

  • sipnab --no-tui -I capture.pcap --calls-only — analyze a pcap headlessly, showing only complete SIP dialogs (calls), not standalone messages
  • sudo sipnab --no-tui -d eth0 --telephone-event — headless live capture that decodes DTMF and logs each event with its duration and SSRC, digit value masked
  • sipnab --no-tui -I capture.pcap --calls-only --telephone-event — read a capture headlessly, report only complete dialogs, and log how many DTMF events each one carried
  • sipnab --no-tui -I lab.pcap --telephone-event --dtmf-cleartext — read a capture you own and disclose the digit values; also set SIPNAB_LOG=debug, or the run prints nothing but the mask
  • sudo sipnab --no-tui -d eth0 --telephone-event --dtmf-cleartext 2>dtmf.log — capture live and steer the cleartext digits into a file whose permissions you control instead of a shared terminal or journald; again needs SIPNAB_LOG=debug

Read this before using --dtmf-cleartext. DTMF digits keyed after answer are PINs, calling-card numbers, account numbers and credit-card numbers with their CVVs, and RFC 4733 carries them in the clear no matter how well the signaling layer protected the call. So -t alone logs everything you diagnose with — that an event arrived, its duration, its SSRC, its timestamp — with the digit value replaced by x:

DTMF digit='x' duration=200ms ssrc=0xdeadbeef

--dtmf-cleartext adds a second line carrying the value. It is not a display setting. It puts a caller’s PIN wherever this run’s log goes — your terminal, a redirected file, journald, and every aggregator downstream of journald. Turning it on takes two deliberate acts, because sipnab writes the cleartext line at debug while the masked line stays at info: pass the flag and raise the level. Either one alone shows you nothing but the mask. Both, in one copyable line:

SIPNAB_LOG=debug sipnab --no-tui -I lab.pcap --telephone-event --dtmf-cleartext 2>dtmf.log

Where the events go. sipnab writes one masked line per decoded event and keeps a count. Nothing else carries the digits: no report, no JSON field, no MCP tool. Two consequences follow, and both bite the obvious command lines. Adding -t to a TUI session shows you nothing, because TUI mode floors the log level at error to keep the alternate screen intact. Adding --quiet also hides them, because it floors the level at warn. Use -N without --quiet. SIPNAB_LOG=info does override the TUI floor, but sipnab sets that floor to stop log lines corrupting the alternate screen, so redirect stderr if you do.

Matching

FlagValueDefaultDescription
-e, --match<PATTERN>SIP payload match-expression (the positional match expression). Regex tested against the whole raw message; once any message in a dialog matches, sipnab shows the rest of that dialog too (dialog-following). Honors -i/-v/-w/--single-line. Independent of the trailing <BPF_FILTER> positional
-i, --ignore-caseoffCase-insensitive matching for header filters and patterns
-v, --invertoffInvert the match: show messages that do NOT match
-w, --wordoffMatch whole words only
--single-lineoffPrevent . from matching newlines in payload regexes; does not unfold headers
--from<PATTERN>Filter by SIP From header (regex pattern)
--to<PATTERN>Filter by SIP To header (regex pattern)
--contact<PATTERN>Filter by SIP Contact header (regex pattern)
--ua<PATTERN>Filter by User-Agent header (regex pattern)
--filter<EXPR>Filter DSL expression OR a diagnostic alias name (codec-asym, late-media, etc.) — see filter-dsl.md

Examples

  • sipnab -N -I capture.pcap --match "[email protected]" --ignore-case — show every dialog that mentions [email protected], case-insensitively (dialog-following payload match)
  • sipnab -N -I capture.pcap --match "486 Busy Here" --word --single-line — whole-word match for 486 rejections, preventing . from spanning header lines
  • sudo sipnab -d eth0 --match "REGISTER" --invert — live view of everything except REGISTER traffic (inverted match)
  • sudo sipnab -d eth0 --ua "friendly-scanner" --contact "203\.0\.113\." --ignore-case — flag scanner traffic live: a known scanner User-Agent (any case) with a Contact pointing into 203.0.113.0/24
  • sipnab -N -I capture.pcap --ua "sipcli" --contact "192\.0\.2\." --single-line — filter a pcap by User-Agent and a Contact in 192.0.2.0/24, restricting payload regex dots to a single line
  • sipnab -N -I capture.pcap --match "OPTIONS" --word --invert — suppress keep-alive noise: show messages that do not contain the whole word OPTIONS

Name resolution

FlagValueDefaultDescription
--resolveoffTurn name resolution on (manual mappings + /etc/hosts). In the TUI, press n to cycle Off / Static / DNS; in headless -O --pcapng export it embeds a Name Resolution Block
--reverse-dnsoffAlso use reverse DNS (PTR) lookups. Implies --resolve. Emits DNS queries for captured IPs
--dns-cache-entries<N>4096Reverse-DNS results held at once. Past the cap sipnab drops the oldest entry, so a capture touching more hosts than this – a carrier edge, a peering point, or any long --reverse-dns window – keeps re-looking-up addresses it already resolved. Nothing reports that: a dropped lookup only shows as an address displayed unresolved, so the symptom is names that flicker. The worker queue’s depth follows this figure; sipnab derives it rather than taking a second number. Config: [names] dns_cache_entries
--names<FILE>Preload IP → name mappings from an /etc/hosts-format file. Repeatable

See the Name Resolution keys for in-TUI naming (N) and persistence.

Examples

  • sudo sipnab -d eth0 --resolve --names /etc/sipnab/hosts.map — live capture with name resolution from a static hosts-format mapping file
  • sipnab -N -I capture.pcap --resolve --names /etc/sipnab/hosts.map --names ~/.config/sipnab/lab-names — annotate an offline pcap with names, preloading two mapping files on top of /etc/hosts
  • sudo sipnab -d eth0 --reverse-dns — live capture that also resolves captured IPs via reverse DNS (PTR) lookups
  • sipnab -N -I capture.pcap --reverse-dns --names ~/.config/sipnab/lab-names — replay an offline pcap and resolve its addresses with reverse DNS, supplemented by a local mapping file
  • sudo sipnab -N -d eth0 --reverse-dns --dns-cache-entries 65536 — a peering point or carrier edge, where a capture touches far more than four thousand hosts: the wider cache stops sipnab re-resolving addresses it already knows, and the worker queue widens with it
  • sipnab -N -I lab.pcap --reverse-dns --dns-cache-entries 256 — the opposite, for a small lab capture on a memory-tight box, where a few hundred entries cover every host in the file

pcapng metadata

FlagValueDefaultDescription
--strip-secrets<OUTPUT>With -I <input>, write a copy of the input pcapng to <OUTPUT> with all Decryption Secrets Blocks removed (the editcap --discard-all-secrets analog), then exit. sipnab never touches the input and writes the output atomically.
--show-frame<POINTER>Resolve a frame pointer from a previous run, print that frame, then exit. Takes <source>#<ordinal> or <source>#<ordinal>@<digest> — the form the frame field of --json-dialogs, --report, the REST API and MCP carries. With a digest, sipnab checks the bytes against it and refuses a capture that changed after sipnab minted the pointer, writing nothing to stdout. Without one, sipnab prints the frame and marks it UNVERIFIED.

Note: with resolution active, sipnab saves name mappings into a pcapng Name Resolution Block — on both the TUI save path and the headless -O --pcapng export (whenever --resolve/--names apply). Headless pcapng exports also describe themselves: the Section Header Block records the producing application (sipnab <version>) and OS, and the Interface Description Block records the capture source as the interface name. Opening a pcapng reads embedded NRB names and DSB TLS secrets back, and decrypts with them. See the design doc.

Examples

  • sipnab --show-frame 'capture.pcap#41@6f3a1c02b8d4e795' — print the frame a dialog opened in, verifying the capture has not changed since
  • sipnab --show-frame 'capture.pcap#41' — same frame, printed as UNVERIFIED because the short form carries nothing to check against
  • sipnab -N -I capture.pcapng --strip-secrets clean.pcapng — write a sanitized copy of a pcapng with every Decryption Secrets Block removed
  • sipnab -N -I tls-call.pcapng --strip-secrets tls-call-clean.pcapng — strip embedded TLS secrets from a decrypted-session capture before sharing it in a support ticket

Diagnostic aliases

Shortcut flags that expand to predefined filter DSL expressions. See filter-dsl.md for the exact expansion of each alias.

FlagValueDefaultDescription
--problemsoffShow calls matching any diagnostic signal: failed state, one-way audio, RTP loss past --loss-bad-pct (5% by default), jitter past --jitter-bad-ms (50 ms by default), NAT mismatch, more than 3 retransmits, post-dial delay past --pdd-threshold (11 s by default), codec/ptime/payload/duration asymmetry, or late media. Every one of those thresholds tracks the run’s own setting, so a network that tuned [diagnosis] or [quality] to its own SLA gets an alias that agrees with the rest of the run — see Named Aliases for the exact expansion. Orphaned RTP is not among them: an orphaned stream belongs to no dialog, so it cannot select one. Find it in the “Orphaned Streams” section of --report, or /v1/streams?orphaned=true
--slow-setupoffShow calls whose post-dial delay passes --pdd-threshold (11 seconds by default, from [diagnosis] post_dial_delay_secs)
--short-callsoffShow completed calls shorter than --fraud-short-call (3 seconds by default, from [security] fraud_short_call_secs)
--one-wayoffShow calls with potential one-way audio issues
--nat-issuesoffShow calls whose RTP arrived from an address no SDP advertised (NAT-rewritten media source)

Multiple diagnostic flags combine with OR. Adding --filter ANDs its expression with that selection: --problems --filter "from.user == '1001'" selects only problem calls from user 1001.

Examples

  • sipnab -N -I capture.pcap --short-calls --one-way — flag completed calls under 3 seconds and calls with suspected one-way audio in a capture
  • sudo sipnab -d eth0 -N --one-way --nat-issues — live-monitor for one-way audio and NAT-rewritten media sources
  • sipnab -N -I capture.pcap --short-calls --report — summarize short completed calls from a capture in a post-run report

Output

FlagValueDefaultDescription
--jsonoffOutput as NDJSON (one JSON object per line, schema in output-formats.md). Requires -N
--json-prettyoffOutput each message as pretty-printed multi-line JSON (use --json for line-oriented NDJSON). Requires -N
--json-dialogsoffNDJSON, one object per dialog, emitted after capture (needs -N; pair with --no-cli-print to get only the objects). --json is per message: a dialog filter such as state == 'Failed' selects dialogs and then emits every message of them, provisional responses included. This is the per-call shape, carrying final_status_code and final_status_reason so a failed call says which code failed it — those two read INVITE transactions only and are null on a REGISTER/OPTIONS/SUBSCRIBE dialog, where signaling_diagnosis.final_failure.code carries it instead.
--plugin<PATH>Load a WASM plugin that contributes its own dialog detections; repeatable. Findings appear under plugin_findings. Requires the plugins Cargo feature (not in the default set). A plugin runs with no imports — no filesystem, network or clock — but still sees each message’s headers, so loading one is a trust decision. See wasm-plugin-api.md
--reportoffGenerate summary report after capture completes. Requires -N
--call-report<CALL-ID>Generate a detailed report for a specific Call-ID. Implies non-interactive
--export-vcon<CALL-ID>Export one dialog as a vCon container (draft-ietf-vcon-vcon-core, syntax 0.4.0), the interchange format a conversation travels in when it leaves the system that captured it. What sipnab writes is an observer vCon and says so in its own parties: sipnab watched packets go past a tap, so it signs nothing, no party carries a name — a From header is what the sender chose to write, not an identity anyone established — and no URL ever points at media held elsewhere, because sipnab hosts nothing. Audio this run RETAINED travels INSIDE the container as a recording Dialog Object – the WAV inline as base64url with a sha512 content_hash – carrying the same “not a recording made by the endpoints” note the exported file carries; a recording-set wraps it when the payload ring dropped frames, so the call’s clock and the file’s stand side by side. Above a measured 5 MiB budget sipnab REFUSES the media out loud instead of dropping it, because one probed store answers 204 and discards the payload without telling the producer. What the capture MISSED travels with it: undecodable frames, SIP a port gate discarded, messages a retention cap evicted, and the blind spots the capture analysis ranked. vCon has no field meaning “this record is incomplete” (dialog.type: "incomplete" says the CALL did not complete, which accuses the traffic rather than the tap), so that caveat rides in the analysis object AND in a sipnab-capture-completeness attachment, both built from one value so the two cannot disagree. Goes to stdout unless --vcon-out names a file, and OWNS stdout when it goes there, silencing the per-message stream, because one stray line makes the container unparseable. Implies non-interactive. Needs the vcon Cargo feature (in full, not in the default set); a build without it refuses the flag by name
--export-vcon-when<EXPR>Emit a vCon for every dialog matching this filter expression, one container per dialog, into --export-vcon-dir. EXPR is the language --filter already speaks (docs/filter-dsl.md), so state == 'Failed' and response_code >= 400 and rtp.codec == 'PCMU' work without new syntax. Reusing the language rather than growing a flag per policy is deliberate: one flag for failures, another for calls with media, another for a duration threshold, and so on enumerates the cases somebody thought of. The case nobody thought of is the one an operator needs at three in the morning. Parsed before the capture opens, so a malformed expression fails the run instead of leaving an empty directory a reader takes for “nothing matched”. Conflicts with --export-vcon, which answers the same question for one named call. Implies non-interactive, and unlike --export-vcon it leaves stdout alone, because the containers go to a directory. Needs the vcon Cargo feature
--export-vcon-dir<DIR>Directory for the containers --export-vcon-when produces, created if absent. Each file takes its Call-ID as a name, with every character outside A-Za-z0-9._- becoming _ and the whole truncated, because a Call-ID is text whoever placed the call chose and this is the first path sipnab builds from one. Requires --export-vcon-when: a destination for containers no predicate selects is a flag that does nothing
--vcon-max-inline-media<MIB>5Largest inline media body a vCon may carry, in MiB. The shipped 5 comes from a measurement rather than taste: one probed vCon store answered HTTP 204 for a roughly 12 MB container, wrote the record to its database, and then had its file spool refuse the payload, with neither transport reporting the partial write. That budget is a property of the CONSUMER and not of the format, so raise it once you know what reads your containers. 0 refuses every inline body, which says “never inline media” without turning the exporter off, and the refusal still appears in the completeness caveat. Batch export, the REST server and the MCP server all read this one value. Needs the vcon Cargo feature
--content-deny-header<NAME>Suppress content for any dialog carrying this header. No default, so the feature stays inert until you name one: sipnab ships no opinion about which header your switches emit, and a built-in guess would either miss yours or silently match one you did not mean. PRESENCE suppresses and the value plays no part, because a rule keyed on a value raises the question of what an unrecognized value means, and the only safe answer to “I do not understand this deny flag” is to deny. Matched case-insensitively, as SIP header names are (RFC 3261 7.3.1). Deny only. A header asking sipnab to RECORD is an assertion by whoever sent the request, and this tool already refuses that class of claim – every vCon party carries validation: "none". Acting on such an assertion to be more conservative costs at worst a container nobody kept. Acting on one to retain content would hand the retention decision to anyone who can set a header, so no permit flag exists here. Needs the vcon Cargo feature
--content-deny-tombstoneoffWrite an identity-only container for each dialog --content-deny-header suppressed, instead of nothing. Requires --content-deny-header. The container carries the dialog’s identity and a redacted object declaring type alone, because no fuller version of the container exists anywhere to point at. It carries no message trace, no media and no bodies. Off by default, and the trade is worth stating: a tombstone reveals that the call EXISTED, so leave it off when the header means “this call must leave no trace”. Needs the vcon Cargo feature
--vcon-out<PATH>stdoutWrite the --export-vcon container to this path instead of stdout. Requires --export-vcon. Refuses a path that names a capture this run reads, before opening any writer — a container written over the capture it describes destroys the evidence it summarizes. A write that fails exits non-zero rather than reporting a file that is not there. Leaves the per-message stream alone, since the container no longer shares stdout with it. Pair with --no-cli-print for a silent run
--vcon-digestoffPrint a SHA-256 of every container written, in sha256sum format, on stdout while the progress summary stays on stderr. Deliberately not a signature and deliberately outside the container: a conserver adds fields on ingest, so a signature over the bytes sipnab emitted would fail for an ordinary reason and tell an operator nothing. A digest makes the smaller, honest claim — this is what sipnab wrote, at this path, at this moment — which binds an emission to a store’s own ledger entry out of band. sipnab ... --vcon-digest > SHA256SUMS and a later sha256sum -c SHA256SUMS both work with no glue. Needs the vcon Cargo feature
--redactoffReplace identities, addresses, hostnames and correlation identifiers in an exported vCon with keyed pseudonyms, addresses, hostnames and correlation identifiers in an exported vCon. Not masking. Every identity becomes a keyed token equal exactly when the original was equal, and every address goes through a prefix-preserving map, so “these forty failures came from one subscriber” and “the media went to a subnet the SDP never advertised” both stay answerable. Masking answers neither. sipnab DELETES digest credentials and inline audio rather than tokenizing them, because no pseudonym of either carries diagnostic value. Rewrites the serialized container only: the TUI, the reports and every in-process analysis keep the real values. Refused when the run exports no container — a --redact that wrote nothing redacted would read as a redacted capture. Needs the vcon Cargo feature
--redact-key-file<FILE>fresh keyRead the redaction secret from this file instead of drawing a fresh one from the operating system. Without it nothing can join the tokens against any other export and nothing anywhere can reverse them, which is the safe default; supply a file when the same subscriber has to read the same across yesterday’s containers and today’s. The whole file is the secret, trailing newline included, so neither a 32-byte key nor a passphrase is silently truncated. Requires --redact
--redact-keep-prefix<N>0Keep this many leading digits of a number verbatim, so route and NPA analysis survives. Zero by default, and the default is an argument: every retained digit is a digit of a real subscriber number published in the clear, and sipnab has no basis for choosing how many — a country code is one to three digits, an NANP area code is three, a national destination code is anything. Requires --redact
--redact-map<FILE>Write the token-to-original table to this file, mode 0600, set at creation rather than by a chmod afterwards. It reverses every pseudonym the run produced, so it is exactly as sensitive as the capture it came from. Refuses to write over an existing file: that file may be the map for containers already sent somewhere. Requires --redact
--markdownoffFormat report output as Markdown
--stunoffReport the STUN and TURN activity in the capture, and what it achieved: one row per transaction (Binding, Allocate, Refresh, ChannelBind, …) naming the method, how many requests it took, whether anything answered, and the reflexive or relayed address the server reported back. The row that matters is the unanswered one — a client whose Binding Request draws no reply never learns its public address, advertises the private one in its SDP, and the media never arrives. A TURN allocations section appears when a relay is in the capture, with the lifetime that decides when each one lapses. Columns for the relayed address, the ICE role and a FAILED FINGERPRINT appear only when the capture holds one. On a capture holding ICE connectivity checks an ICE section names the candidate pair the agents nominated – the ICE analogue of the mapped address, and the only thing that says which path the media took – and any pair where both agents claimed the same role or answered 487 Role Conflict. Where a relay carried media, the allocations section says which channel carried which SSRC, so a reader can trace a relayed stream in the stream list back to the allocation that carried it. Silent when the capture holds no STUN and no TURN. Requires -N
--json-stunoffNDJSON, one object per STUN/TURN transaction and per TURN allocation, emitted after capture. Each carries a record field (transaction, turn_allocation or ice) so a consumer never infers the kind from which keys are present; an allocation also carries the derived lapsed and the channels that attribute relayed media to it, and the single ice record carries the checks, the nominated pairs and any role conflicts. The machine-readable form of --stun. Requires -N
--analyzeoffAnalyze the capture and print every problem in it, worst first — one ranked list instead of one dialog at a time. It aggregates the diagnosis sipnab already computes (one-way audio, media that never arrived, an SDP address STUN contradicts, ICMP unreachables, failed and unacknowledged calls, codec and framing asymmetry) plus capture-level evidence, counted exactly and evidenced with Call-IDs, addresses, timestamps and packet counts. It derives nothing new. What sipnab did NOT read — undecodable frames, SIP a port gate discarded, records a retention cap dropped — appears at the top, because those make every count below them a floor, and a capture that did not fully decode never gets a clean verdict. Requires -N
--json-analyzeoffThe --analyze result as one JSON object, emitted after capture. One object rather than a line per finding: the frames read and the dialogs examined are properties of the run, and a clean capture must still serialize to something that states them. Requires -N
--hexdumpoffInclude hex dump of SIP payloads. Requires -N
--delta-timeoffShow delta time between consecutive messages
-A, --after<N>Show N messages after each match (like grep -A)
--show-empty (--full)offShow the full header block of bodyless messages (responses, OPTIONS, REGISTER, ACK, BYE); by default they show only the summary line
--proto-numberoffAnnotate the transport tag with the IANA IP protocol number, e.g. UDP(17) / TCP(6). Long-only because -N is --no-tui here; TLS/WS report their TCP carrier’s number (6)
--line-bufferoffFlush output after each line (useful for piping)
--color<WHEN>autoColor output mode: auto, always, never
--from-to-mode<MODE>defaultDefault TUI From/To column display: default (user else host:port), host-port, user, user-host-port. Cycle at runtime with u. Overrides [display] from_to
--payload-limit<BYTES>Maximum payload bytes to display
-T, --text-dumpoffDump raw SIP message text (like the CLI matcher -T)
--no-cli-printoffSuppress per-message CLI output (useful with --report / --call-report so only the post-capture summary reaches stdout)
--wiresharkoffPrint a display filter for the current capture to paste into Wireshark; use with -N
--tshark-filter<EXPR>Generate a tshark-compatible display filter string
--fail2banoffSwitch the per-message stream to fail2ban-readable log lines. Requires -N. It selects a format, not a detection: only two events ever reach it, and each needs its own detector armed beside it — --kill-scanner (or --kill-ua) produces scanner_detected, --reg-flood produces reg_flood. On its own it emits nothing, and warns on stderr about the coming silence, because an empty jail log reads as “nothing attacked me”. Detections carried by HEP input (--hep-listen, --hep-parse) never reach it without --hep-allow-kill: the inner addresses are the sender’s claim, a jail line would ban whatever address the sender chose, and the run says so once at startup
--lintoffRun the RFC conformance linter over every dialog and print each finding with its rule identifier and the RFC section it reads from. The linter compares what the capture holds against what the cited section calls for, so a finding names a section rather than an opinion. Informational on its own: it changes what gets printed and never the exit code. Pair it with --lint-fail-on to make a pipeline stop. The rule catalog is in sip-lint-rules.md, and over MCP as explain_rule
--lint-fail-on<SEVERITY>Exit 3 when the linter reports a finding at or above this severity: info, warning (or warn), or error. Needs --lint. Exit 3 is deliberately not 1 or 2, so a pipeline can tell a broken sipnab (1) and a wrong invocation (2) apart from a non-conformant CAPTURE (3). info parses and buys nothing, because that severity exists for findings that are never a reason to fail a build
--group-by<FIELD>Group output by field (e.g., call-id, from, method)
--max-groups<N>100000Distinct --group-by keys one run retains, the same figure -l/--limit ships so a grouped run cannot outgrow an ordinary capture. Past it sipnab refuses new keys and warns that the output is incomplete; -l/--limit bounds tracked dialogs and never reached this buffer. Requires --group-by. Config: [limits] max_groups
--max-grouped-messages<N>200000Messages --group-by buffers across every group. Grouping cannot stream — the last packet may belong to the first group — so this is memory sipnab holds until the capture ends. Requires --group-by. Config: [limits] max_grouped_messages
--node-name<NAME>hostnameName this box reports as, in capture_identity.node on every MCP and REST answer. Lets an agent querying several servers at once tell WHICH one saw a given fact — “answered 407” is incomplete until you know where. Distinct from the capture instance, which rotates when a different capture loads; the node is the box and stays put, so a capture restart does not read as a topology change. The default puts your hostname on the wire. Clipped to 64 characters

Examples

  • sipnab -N -I calls.pcap --lint --no-cli-print — run the RFC conformance linter over every dialog in a capture and print each finding with the rule identifier and the RFC section it reads from. Informational: it leaves the exit code alone
  • sipnab -N -I calls.pcap --export-vcon-when "state == 'Failed'" --export-vcon-dir out --redact --redact-keep-prefix 4 --no-cli-print — hand a vendor every failed call with keyed tokens standing in for the subscriber numbers and the area code intact, so they can still see which route the failures came from
  • sipnab -N -I calls.pcap --export-vcon call-1 --vcon-out out.vcon --redact --redact-key-file team.key --redact-map out.map --no-cli-print — a stable key, so today’s tokens join against last week’s, and the reversal table written beside the container at 0600
  • sipnab -N -I calls.pcap --export-vcon-dir out --redact --redact-keep-prefix 3 --redact-map out.map --no-cli-print — keep the NANP area code readable so a route analysis still works, and write the reversal table beside the export. Three digits is a choice about a real subscriber number, not a default sipnab can make for you: a country code is one to three digits and a national destination code is anything
  • sipnab -N -I calls.pcap --export-vcon-dir out --redact --redact-key-file team.key --redact-keep-prefix 0 --no-cli-print — the shape to send outside the team. A shared key still joins a subscriber across exports, no digits survive in the clear, and with no --redact-map written nothing anywhere reverses the tokens
  • sipnab -N -I calls.pcap --lint --lint-fail-on error --no-cli-print — the CI gate. Exits 3 when any finding is at or above error, so a pipeline stops on a non-conformant capture. Exit 3 is not 1 or 2, so a failing gate is distinguishable from a failing tool and from a bad invocation
  • sipnab -N -I calls.pcap --lint --lint-fail-on warning --no-cli-print — a stricter gate: stop on warnings as well as errors, for a pipeline that treats interop degradation as a build failure rather than a note
  • sipnab -N -I capture.pcap --json-dialogs --no-cli-print --plugin ./short-calls.wasm — run a custom detection over every dialog and emit its findings beside sipnab’s own
  • sudo sipnab -d eth0 -N --json-dialogs --no-cli-print --plugin ./site-rules.wasm --plugin ./fraud.wasm — stack two site-specific detections over live traffic; each plugin is sandboxed and a failure in one never stops the capture
  • sipnab -N -I capture.pcap --stun --no-cli-print — read what NAT traversal actually achieved: which probes drew an answer, what public address came back, and which drew nothing. A capture holding only failed STUN is not an empty capture, it is the cause of a one-way-audio complaint
  • sipnab -N -I relay.pcap --stun --markdown --no-cli-print — the same tables as Markdown, for pasting into a ticket. On a capture that went through a TURN relay this adds the allocations section, where a LAPSED status means the relay tore the allocation down while media was still crossing it
  • sipnab -N -I capture.pcap --json-stun --no-cli-print | jq 'select(.record == "transaction" and .responded_at == null)' — every transaction nothing answered, as JSON
  • sipnab -N -I relay.pcap --json-stun --no-cli-print | jq 'select(.lapsed == true)' — the relay allocations that ran out under live media, which no SIP message anywhere reports
  • sipnab -N -I capture.pcap --analyze --no-cli-print — one ranked list of everything wrong with the file, worst first, with the evidence for each finding
  • sipnab -N -I capture.pcap --analyze --filter "state == 'Failed'" --no-cli-print — narrow the DIALOG findings to failed calls; the capture-level evidence (undecodable frames, discarded ports, dropped records) is deliberately not narrowed, because it bounds every count in the report
  • sipnab -N -I capture.pcap --json-analyze --no-cli-print | jq '.findings[] | select(.severity == "critical")' — the critical findings only, for a pipeline
  • sipnab -N -I capture.pcap --json-analyze --no-cli-print | jq '.complete' — whether the capture decoded fully. false means every count in the analysis is a floor, so do not trust a clean-looking verdict
  • sipnab -N -I capture.pcap --json-dialogs --no-cli-print --quiet | jq -c 'select(.state == "Failed")' — one line per failed call, each carrying the code that failed it, instead of every message of every failed dialog
  • sudo sipnab -d eth0 -N --json-dialogs --no-cli-print --line-buffer > calls.ndjson — record one summary object per call from live traffic, flushed per line for a downstream collector
  • sudo sipnab -N -d eth0 --node-name sbc-edge-1 --mcp --mcp-transport http — one node of a federated setup, naming itself so an agent can attribute each answer to this box rather than another
  • sudo sipnab -N -d eth0 --node-name pbx-core-2 --report — override the hostname on a box whose real name should not travel, while still labeling the capture
  • sipnab -N -I capture.pcap --json-pretty --payload-limit 1000 > messages.json — export every SIP message from a capture as pretty-printed JSON, truncating displayed payloads to 1000 bytes
  • sudo sipnab -d eth0 -N --json-pretty --group-by method --line-buffer > live.json — stream live SIP traffic as pretty-printed JSON grouped by method, flushing after each line for downstream tooling
  • sipnab -N -I capture.pcap --text-dump --hexdump --proto-number --color never — dump raw SIP text with hex payloads and IANA protocol numbers, uncolored for log archiving
  • sudo sipnab -d eth0 -N --match REGISTER --after 2 --text-dump --line-buffer --color always — follow live REGISTER traffic in real time, printing raw text plus 2 messages of context after each match
  • sipnab -N -I capture.pcap --show-empty --delta-time --hexdump --group-by call-id — review a capture with per-message delta times, empty-bodied messages included, and hex dumps grouped per call
  • sudo sipnab -d eth0 -N --match OPTIONS --after 5 --show-empty --proto-number --payload-limit 256 — inspect OPTIONS keepalives with 5 messages of trailing context, empty bodies shown, and display capped at 256 payload bytes
  • sudo sipnab -N -d eth0 --from-to-mode host-port --wireshark — capture live with host:port From/To columns and print a Wireshark display filter when capture ends
  • sipnab -I capture.pcap --from-to-mode user-host-port — browse an existing capture in the TUI with full user@host:port From/To columns
  • sipnab -N -I busy-day.pcap --group-by call-id --max-groups 250000 --no-cli-print — group a capture holding more calls than the shipped 100000-key cap, instead of taking the first hundred thousand and a warning naming how many keys sipnab turned away
  • sipnab -N -I busy-day.pcap --group-by from --max-grouped-messages 2000000 --json — regroup a large capture by caller with room for every message, keeping the output one valid JSON object per line
  • sipnab -N -I untrusted.pcap --group-by call-id --max-groups 500 --max-grouped-messages 5000 --no-cli-print — group a capture from outside your network under a tight pair of caps, so an attacker-chosen Call-ID cannot buy more memory than you allowed
  • sipnab -N -I capture.pcap --export-vcon '[email protected]' > call.vcon – hand one observed dialog to a conversation-data pipeline in the interchange format it already reads, instead of a sipnab-shaped JSON nobody else parses. The container names sipnab as an observer party and carries what this capture missed, so a consumer weighs it as an observation rather than as a recording
  • sipnab -N -I capture.pcap --export-vcon '[email protected]' --vcon-out ./call.vcon --no-cli-print – the same container to a file, with the run otherwise silent. Use this spelling in a script: the exit code is the whole answer, and a path sipnab cannot write fails the run rather than leaving an operator to discover the missing file later
  • sipnab -N -I capture.pcap --export-vcon-when "state == 'Failed'" --export-vcon-dir ./failed/ --content-deny-header X-No-Record – the same failures, minus any call whose signaling asked sipnab to leave it alone. A switch that marks sensitive calls can keep them out of an export without sipnab needing to understand why
  • sipnab -N -I ./captures --export-vcon-when 'duration > 30' --export-vcon-dir ./long/ --content-deny-header Privacy – presence is the whole rule, so Privacy: none suppresses exactly as Privacy: id does. If you do not understand a deny flag, denying is the only safe reading of it
  • sipnab -N -I capture.pcap --export-vcon-when "state == 'Failed'" --export-vcon-dir ./failed/ – one container per failed dialog, for handing a morning’s failures to a conversation-data pipeline without naming each Call-ID by hand. A capture where nothing failed writes nothing and says so on stderr, which is the answer an operator needs rather than an empty directory
  • sipnab -N -I ./captures --export-vcon-when "duration > 30 and rtp.codec == 'PCMU'" --export-vcon-dir ./long-g711/ --no-cli-print – the predicate is the filter language, so the same conditions that narrow a report also choose a container set. The containers carry the same ICMP media evidence the report shows, because both go through one selection path
  • sipnab -N -I ./captures --export-vcon '[email protected]' --vcon-out ./captures/1-1966.vcon – refused before anything opens a writer. --vcon-out goes through the guard -O does, so a container cannot land inside a directory this run reads: a name that is free today becomes an input on the next run, and the container would then overwrite the evidence it summarizes
  • sipnab -N -I capture.pcap --export-vcon '[email protected]' | jq -r '.analysis[0].body | fromjson | .capture_completeness.note' – read the caveat before the contents. The fromjson is not optional: the draft makes every body a JSON-encoded STRING, so indexing into it without parsing yields nothing. The note comes from this run’s own counters and says what sipnab READ and dropped; it never says the call was short, silent or broken, because none of that follows from a capture that missed something
  • sipnab -N -I ./captures --export-vcon-when 'duration > 30' --export-vcon-dir ./long/ --vcon-max-inline-media 64 – carry audio the shipped 5 MiB budget would refuse. One store answers HTTP 204 and then drops the payload in its file spool, telling the producer nothing, and that measurement set the default – so raise it only when you know what reads your containers
  • sipnab -N -I ./captures --export-vcon-when 'response_code >= 200' --export-vcon-dir ./calls/ --vcon-max-inline-media 0 --retain-audio – keep the containers and inline none of the audio. 0 says “never inline media” without turning the exporter off, and the refusal still appears in the completeness caveat rather than passing as a call that had no audio
  • sipnab -N -I ./captures --export-vcon-when "state == 'Failed'" --export-vcon-dir ./failed/ --content-deny-header X-No-Record --content-deny-tombstone – write an identity-only container for each denied dialog instead of nothing, so a consumer can tell a call that was deliberately withheld from one that never happened. It carries no message trace, no media and no bodies, and declares redacted with type alone, because no fuller version of the container exists anywhere to point at
  • sipnab -N -I capture.pcap --export-vcon-when 'duration > 0' --export-vcon-dir ./out/ --content-deny-header Privacy --content-deny-tombstone – off by default for a reason worth stating: a tombstone reveals that the call EXISTED. If the header means “this call must leave no trace”, leave the flag off and let the dialog produce nothing at all
  • sipnab -N -I ./captures --export-vcon-when 'duration > 30' --export-vcon-dir ./long/ --vcon-digest > SHA256SUMS – record what sipnab wrote. The digests go to stdout in sha256sum format while the progress summary stays on stderr, so the redirect captures the one and not the other
  • sipnab -N -I capture.pcap --export-vcon-when 'duration > 0' --export-vcon-dir ./out/ --vcon-digest | tee ./out/SHA256SUMS – keep the digests beside the containers they describe, so the spool carries its own manifest. The name in each line is the bare file name rather than a path, so a spool that is later moved or mounted elsewhere still verifies from inside its own directory
  • cd ./long/ && sha256sum -c ../SHA256SUMS – verify later with the standard tool and no glue. This is a digest, not a signature: a signature over sipnab’s bytes could never verify against the object a store holds, because a conserver adds fields on ingest. What the digest buys is binding an emission to a store’s own ledger entry out of band
  • sipnab -N -I capture.pcap --tshark-filter 'sip.Method == "INVITE"' — print a tshark-compatible display filter for the INVITE traffic in a capture. sipnab hands the expression to tshark’s -Y verbatim, so it takes WIRESHARK display-filter syntax rather than sipnab’s --filter DSL: sip.Method, not method. Quote the whole expression in single quotes so the inner double quotes survive the shell

Dialog

FlagValueDefaultDescription
-l, --limit<N>100000Maximum dialogs held in TOTAL over the run. Not a concurrency limit — nothing removes a completed dialog, so this bound scales with uptime rather than load: a box carrying five concurrent calls still evicts once 100,000 have completed, oldest first. Lower it for untrusted/high-volume capture
-R, --rotateonEvict the oldest dialog at --limit capacity (LRU). On by default; kept for back-compat/explicitness
--no-rotateoffDisable rotation: drop new dialogs at capacity instead of evicting the oldest (inverts the safe default)
--dialog-track<METHOD>call-idGroup messages by call-id (one unit per dialog) or branch (one per SIP transaction)
--leg-correlation-window<MS>2000How far apart one call’s two legs may start and still correlate on TIMING alone. The B2BUA timing heuristic’s whole content, and the only strategy left once a B2BUA has rewritten every identifier the other six strategies compare. The shipped two seconds describes a PBX placing the outbound leg immediately, not one doing an LNP or ENUM dip, or walking an LCR cascade, before it places one. Every correlation still reports the strategy that matched, so a widened window does not turn a guess into a claim. Config: [sip] leg_correlation_window_ms
--active-idle-window<SECS>3600Seconds a dialog may go untouched and still count toward the active-dialog and active-call gauges every surface publishes. The shipped hour is twice RFC 4028’s default Session-Expires, which grounds it for a trunk carrying session timers and not for a contact center, where a caller parked on hold past an hour is a channel in use the gauge stops counting. Widening it widens the opposite error – a call that never sent its BYE keeps counting for longer, and that one never recovers on its own. Config: [sip] active_idle_window_secs
--no-dialogoffDisable dialog tracking entirely (message-only mode)
--tag<TAG>Filter dialogs by tag value

branch counts transactions, not calls. RFC 3261 gives the ACK to a 2xx a new branch (RFC 3261 section 17.1.1.3) and the BYE another, so one ordinary call appears as three or more units. That is the transaction view working as intended. Use it when a capture reuses one Call-ID across many transactions — load generators, proxies under test — and note that --limit then counts transactions too.

Examples

  • sipnab -N -I loadtest.pcapng --dialog-track branch --report — per-transaction view of a load-generator capture that reuses one Call-ID
  • sipnab -N -I loadtest.pcapng --dialog-track call-id --report — same capture as dialogs (the default), for a per-call view
  • sudo sipnab -d eth0 --limit 5000 --rotate — monitor a busy proxy with a tight 5000-dialog memory bound, explicitly evicting the oldest dialog at capacity
  • sipnab -N -I capture.pcap --limit 20000 --no-rotate — analyze a capture keyed by Via branch, dropping new dialogs (instead of evicting old ones) past 20000 tracked
  • sipnab -N -I capture.pcap --tag 1928301774 --rotate — show only dialogs carrying a specific From/To tag, with explicit LRU rotation
  • sudo sipnab -d eth0 --tag as7d60e14a --no-rotate — live-follow dialogs matching a tag while refusing new dialogs once the tracker is full
  • sipnab -N -I capture.pcap --no-dialog — scan a capture message-by-message with dialog tracking disabled entirely
  • sudo sipnab -d eth0 -N --no-dialog — watch raw live SIP messages on an interface without keeping any per-dialog state
  • sipnab -N -I sbc.pcap --leg-correlation-window 8000 --mcp — correlate the two legs of a call across a B2BUA that dips an ENUM or LNP database before placing the outbound leg, which the shipped two seconds cannot reach
  • sipnab -N -I gateway.pcap --leg-correlation-window 500 --report — a PBX that places the outbound leg immediately, where a tighter window stops a busy server’s unrelated calls turning into one
  • sudo sipnab -N -d eth0 --metrics 127.0.0.1:9090 --active-idle-window 14400 — a contact center parking callers on hold for hours: at the shipped hour the active-call gauge stops counting them, and four hours covers the queue
  • sudo sipnab -N -d eth0 --metrics 127.0.0.1:9090 --active-idle-window 300 — a trunk where every call refreshes on a short session timer, so five minutes of silence already means the BYE went missing and counting it longer only inflates the gauge

RTP

FlagValueDefaultDescription
--max-streams<N>50000Maximum number of RTP streams to track simultaneously
--max-lost-sequences<N>1000Lost RTP sequence numbers retained per stream, for the Packet Loss Map and the burst/gap analysis. The default is about a minute of a call losing 1 % at 50 packets a second, so on a half-hour call the map shows the tail and marks itself truncated. The burst/gap window widens with it, and each retained loss costs two bytes per stream. Config: [limits] max_lost_sequences
--quality-interval<SECONDS>5Seconds between RTP quality snapshots — the resolution of the per-stream quality trend, which carries a MOS, an R-factor and an acceptable/degraded/not_scorable verdict per interval. The shipped five seconds averages away any burst shorter than itself, so three half-second dropouts read as a mild five-second average. The trend still covers an hour of call time whatever this is, so one second retains 3600 snapshots per stream instead of 720: resolution costs memory, never history. Accepts 1 to 300. Config: [limits] quality_interval_secs
--quality-threshold<MOS>3.0MOS quality threshold for alerts (1.0-5.0 scale)
--rtpengine-control<ADDR>Ask an rtpengine relay which calls it currently has up, so a capture that started mid-call can still name the dialog behind a stream. Off unless you give an address, and the address is never inferred from captured traffic. Only rtpengine’s read-only list and query are reachable from this path — there is no value in the code that means delete or start recording, so sipnab cannot change a relay’s behavior through it. Not a poller: it asks at startup, before the capture opens, and again only when a stream turns up that nothing explains — never on a timer. sipnab asks about each relay-side socket at most once for the run, under a per-run ceiling on control transactions that does not grow with the traffic. Refused on -I <file>: a live relay would answer about calls that are up TODAY, which are not the calls in the capture. Feature: native
--relay-statsAsk the relay named by --rtpengine-control for its OWN statistics – packets relayed, sessions, its own loss and jitter – and print them. They are relay_reported: a claim from the box, not a measurement sipnab made, and the header says so. Same live source --rtpengine-control needs, because asking transmits; on -I <file> it prints why it declines to ask rather than asking, and with no --rtpengine-control it says to name one. Feature: native
--relay-stats-call<CALL-ID>Ask the relay named by --rtpengine-control for its own counters about ONE call, by Call-ID: per-stream and per-SSRC packet and byte counts, RTP and RTCP totals, as the relay counts them (relay_reported). A relay not holding the call answers in its own words, which sipnab reports rather than inventing a not-found. Same live-source gate as --relay-stats. Feature: native
--relay-stats-listList which statistics the relay named by --rtpengine-control knows, rather than their values. The key set is version-specific, so “what can I ask for?” is a real question; sipnab asks the relay rather than reading a built-in table, and the header names how it got the set (for rtpengine, the relay listed them in a statistics reply). Same live-source gate as --relay-stats, because listing asks the relay and asking transmits. Feature: native
--relay-stats-interval<SECONDS>Poll the relay named by --rtpengine-control for its statistics every SECONDS, for as long as the capture runs. Nothing polls by default: naming an interval is the request, because a poll transmits where every other answer comes from bytes sipnab already holds. It runs on its own thread, and the interval is the spend bound – one transaction per interval, the next beginning only once the previous returns, so a slow relay slows the cadence rather than stacking requests. Each polled reading says it was polled and names the interval. CLI only (a poll is a standing instruction to transmit; the REST/MCP caller does not own the host). Same live-source gate as --relay-stats. Range 1–3600. Feature: native
--relay-compare<CALL-ID>Compare the relay’s own RTP packet count for one call against what sipnab measured on the wire. It shows the relay’s totals.RTP.packets for the call (relay_reported) beside sipnab’s own count of the RTP packets it captured for it (sipnab_measured), both labeled, with a word verdict (match or differ) and a direction-aware note. The two count different sockets over different windows, so an ordinary gap is not a relay fault: if sipnab counted fewer the note names the undercount causes (a mirror-port drop, a mid-call restart) and points at capture_health; if sipnab counted more it names the usual cause – a capture that sees both sides of the relay hairpin counts each packet twice, so a point upstream and downstream reads about twice the relay’s count. It never sums the counts. Runs after capture, when sipnab’s tally is final; sipnab reports a call the relay does not hold, or one it captured no RTP for, distinctly rather than comparing it against a made-up zero. Same live-source gate as --relay-stats. Feature: native

Examples

  • sudo sipnab -d eth0 --quality-threshold 3.5 --max-streams 10000 — monitor live RTP with MOS alerts below 3.5. sipnab reports stream statistics once, at end of capture. There is no periodic interval report
  • sipnab -N -I capture.pcap --max-streams 100000 — batch-analyze RTP streams with a raised stream cap. The statistics arrive once, when the capture ends
  • sipnab -N -I long-call.pcap --max-lost-sequences 100000 --json-dialogs --no-cli-print — keep every loss from a half-hour call that an operator escalated, so the Packet Loss Map covers the whole call and the burst count is the real one rather than the tail’s
  • sudo sipnab -d eth0 --max-lost-sequences 200 — watch a busy trunk on a small box, holding a fifth of the shipped loss history per stream; the map still shows where loss is landing right now and marks itself truncated
  • sipnab -N -I dead-air.pcap --quality-interval 1 --json-dialogs --no-cli-print — one snapshot a second on the call somebody complained about, so a half-second dropout appears as its own degraded interval instead of disappearing into a five-second mean
  • sudo sipnab -d eth0 --quality-interval 30 --max-streams 20000 — the other direction, on a busy trunk where twenty thousand streams each holding 720 snapshots is memory better spent elsewhere; thirty seconds still covers the retained hour, in 120 entries per stream
  • sudo sipnab -N -d eth0 --rtpengine-control 127.0.0.1:22222 — start capturing on a proxy that shares a host with rtpengine, and ask the relay once, at startup, which calls are already up. Streams whose offer happened before sipnab started get a Call-ID, where otherwise sipnab would report them as orphans
  • sudo sipnab -N -d eth0 --rtpengine-control 192.0.2.50:22222 --max-streams 10000 — the relay on its own host: same question, over the network to its ng control port. sipnab sends only list and query, and asks again only when a stream turns up that the signaling does not explain — each socket once, under a ceiling it reports when it reaches
  • sipnab -N -I capture.pcap --rtpengine-control 127.0.0.1:22222 — refused, and says so: reading a file, sipnab never transmits, and the relay’s answer would describe today’s calls rather than the capture’s
  • sudo sipnab -N -d eth0 --rtpengine-control 127.0.0.1:22222 --relay-stats — capture live and print the relay’s own counters at startup: packets relayed, sessions held, its own loss. The header marks them relay_reported, because the relay reports on itself rather than sipnab measuring the wire
  • sipnab -N -I capture.pcap --relay-stats --rtpengine-control 127.0.0.1:22222 — refused, and says why: asking a relay transmits, and a file-backed run may not, so it prints not_permitted rather than talking to an address a capture named
  • sudo sipnab -N -d eth0 --rtpengine-control 127.0.0.1:22222 --relay-stats-call [email protected] — the relay’s own packet and byte counts for one call, by Call-ID, relay_reported; a header names the call and the moment asked
  • sipnab -N -I capture.pcap --relay-stats-call [email protected] --rtpengine-control 127.0.0.1:22222 — refused on a file run for the same reason the relay-wide form is: asking transmits, so it prints not_permitted
  • sudo sipnab -N -d eth0 --rtpengine-control 127.0.0.1:22222 --relay-stats-list — ask the relay which statistics it knows, listed by name and not by value, so a caller learns what it can ask for before a request fails on a name this build does not have. The header says the relay listed them, rather than sipnab guessing from a table
  • sipnab -N -I capture.pcap --relay-stats-list --rtpengine-control 127.0.0.1:22222 — refused on a file run like the other relay-stats forms: listing asks the relay, and asking transmits, so it prints not_permitted rather than talking to an address the capture named
  • sudo sipnab -N -d eth0 --rtpengine-control 127.0.0.1:22222 --relay-compare [email protected] — after capture, put the relay’s own RTP packet count for the call beside sipnab’s measured count, each labeled with its tier, with a match/differ verdict and a note explaining that an ordinary gap is not a relay fault. It shows both counts and never sums them
  • sipnab -N -I capture.pcap --relay-compare [email protected] --rtpengine-control 127.0.0.1:22222 — refused on a file run: comparing asks the relay for its side, and asking transmits, so it prints not_permitted rather than reaching an address the capture named
  • sudo sipnab -N -d eth0 --rtpengine-control 127.0.0.1:22222 --relay-stats-interval 30 — capture live and ask the relay for its own counters every thirty seconds, each reading marked polled with the interval so it reads as a timer’s output rather than a one-shot answer. Stop it with Ctrl-C; nothing keeps transmitting after the process ends
  • sipnab -N -I capture.pcap --relay-stats-interval 30 --rtpengine-control 127.0.0.1:22222 — refused on a file run like every relay-stats form: polling asks the relay, and asking transmits, so it prints not_permitted rather than reaching an address the capture named
  • sudo sipnab -N -d eth0 --rtpengine-control 127.0.0.1:22222 --relay-stats --json — the same relay counters as one JSON object instead of the table, each value carrying its tier and refusals listed separately with the relay’s code; both forms read the same data, so --json and the table cannot disagree about a number

Diagnosis thresholds

The numbers the signaling and media checks compare against. Each decides whether a call that is working gets reported as broken, so the defaults are standards figures for the general case and your own network beats them. Every flag has a config key under [diagnosis], and the flag wins.

FlagValueDefaultDescription
--pdd-threshold<SECS>11.0Post-dial delay over which sipnab reports a call as slow. The default is the ITU-T E.721 Table 2 target that 95 percent of international connections must meet, because a capture does not say which kind of call it holds. Tighten it to 8.0 for toll or 6.0 for local traffic. Config: [diagnosis] post_dial_delay_secs
--ack-timeout<SECS>32.0Seconds a 2xx may go unacknowledged before the missing ACK counts as a fault rather than as a capture that stopped early. The default is RFC 3261 Timer H. Config: [diagnosis] ack_timeout_secs
--no-final-response-timeout<SECS>180.0Seconds an INVITE may sit without a final response before the silence gets reported. The default is RFC 3261 Timer C. Below it, every call still ringing when the capture stopped gets reported. Config: [diagnosis] no_final_response_secs
--duration-asymmetry-pct<PCT>5.0Percentage difference between the two legs’ durations that counts as asymmetric. Config: [diagnosis] duration_asymmetry_pct
--duration-asymmetry-secs<SECS>2.0Absolute difference between the two legs’ durations that counts as asymmetric. A call has to clear both this and the percentage, so raising either one alone quiets the detection. Config: [diagnosis] duration_asymmetry_secs
--late-media-ms<MS>500Milliseconds after the 200 OK that media may start before it gets reported as late. Config: [diagnosis] late_media_ms
--cn-suppression-ratio<RATIO>0.3Share of a call’s packets, as a fraction of 1, that must be comfort noise before sipnab accepts comfort noise as the explanation for one-directional media. The one threshold here that withholds a finding instead of raising one, so it fails as silence: a VoLTE or mobile trunk running aggressive voice-activity detection routinely passes 30 percent comfort noise, and above the ratio sipnab never reports one-way audio on that trunk. Must be greater than 0 and 1 or less. Config: [diagnosis] cn_suppression_ratio

Examples

  • sipnab -N -I calls.pcap --json-dialogs --pdd-threshold 6 --no-cli-print — judge a capture you know is local traffic against E.721’s local target rather than the international one the default assumes
  • sipnab -N -I trunk.pcap --json-dialogs --late-media-ms 150 --duration-asymmetry-secs 0.5 --no-cli-print — a tight media audit for a trunk where a 150 ms media gap is already a clipped first syllable
  • sipnab -N -I sat-trunk.pcap --json-dialogs --pdd-threshold 15 --late-media-ms 900 --no-cli-print — the other direction, for a satellite path where the shipped figures report every healthy call as slow
  • sipnab -N -I proxy.pcap --json-dialogs --ack-timeout 8 --no-final-response-timeout 30 --no-cli-print — a proxy tap where the interesting window is far shorter than the RFC 3261 timers, so a stalled transaction shows up while the capture is still running
  • sipnab -N -I trunk.pcap --json-dialogs --ack-timeout 64 --no-final-response-timeout 300 --no-cli-print — a lossy trunk where the RFC timers themselves fire too early, so only a genuinely dead transaction gets reported
  • sipnab -N -I b2bua.pcap --json-dialogs --duration-asymmetry-pct 25 --duration-asymmetry-secs 5 --no-cli-print — a B2BUA capture where the legs never tear down together, so only a large gap is worth a line
  • sipnab -N -I b2bua.pcap --json-dialogs --duration-asymmetry-pct 1 --duration-asymmetry-secs 0.2 --no-cli-print — the strict form of the same audit, for hunting a leg that drops media a fraction early
  • sipnab -N -I volte-trunk.pcap --json-dialogs --cn-suppression-ratio 0.8 --no-cli-print — a mobile trunk whose voice-activity detection sends comfort noise on well over 30 percent of packets: at the shipped ratio sipnab treats that as the explanation for a one-directional flow and reports no one-way audio on any call
  • sipnab -N -I pbx.pcap --json-dialogs --cn-suppression-ratio 0.05 --no-cli-print — the opposite, for a LAN PBX where a call carrying any comfort noise at all is still expected to be bidirectional

Quality color bands

Where the quality color column turns yellow, and where it turns red. This is a different question from the diagnosis thresholds above: those decide whether a working call counts as broken, while these decide only what catches an operator’s eye during triage. Every flag has a config key under [quality], and the flag wins.

The shipped figures suit a general-purpose trunk, and the right values belong to the network you are watching — 30 ms of jitter is already a fault on a LAN PBX, and 1 percent loss is unremarkable on an international one. A column tuned for neither is wrong in both directions.

These bands paint the TUI. A -N run prints the measurements themselves rather than a color, so sipnab validates a band set on a non-interactive run and then never consults it.

FlagValueDefaultDescription
--jitter-warn-ms<MS>30.0Jitter at or above which the column turns yellow. Config: [quality] jitter_warn_ms
--jitter-bad-ms<MS>50.0Jitter at or above which the column turns red. Config: [quality] jitter_bad_ms
--loss-warn-pct<PCT>1.0Loss at or above which the column turns yellow. 0 is a legitimate setting: it means any loss at all is worth a color. Config: [quality] loss_warn_pct
--loss-bad-pct<PCT>5.0Loss at or above which the column turns red. Config: [quality] loss_bad_pct
--mos-warn<MOS>4.0MOS below which the column turns yellow. MOS bands run downward, so this must sit at or above --mos-bad. Config: [quality] mos_warn
--mos-bad<MOS>3.0MOS below which the column turns red. Config: [quality] mos_bad
--rtt-warn-ms<MS>300.0Round trip at or above which the column turns yellow. The default is ITU-T G.114’s 150 ms one-way guidance doubled. Config: [quality] rtt_warn_ms
--rtt-bad-ms<MS>800.0Round trip at or above which the column turns red. The default is G.114’s 400 ms one-way figure doubled. Config: [quality] rtt_bad_ms

sipnab refuses a warn boundary that sits above its matching bad boundary, rather than silently reordering the pair, because that pair leaves an unreachable middle: nothing would ever render as a warning, and whoever wrote it would see green until the value was already bad. A boundary that is not a finite, non-negative number fails for a worse reason — every comparison against NaN is false, so a single one would paint the whole column green and report a healthy network in the middle of an outage.

Examples

  • sipnab -I lan-pbx.pcap --jitter-warn-ms 10 --jitter-bad-ms 20 — a LAN PBX, where the shipped 30 ms boundary hides a fault worth chasing
  • sipnab -I wifi-softphone.pcap --jitter-warn-ms 60 --jitter-bad-ms 120 — the other direction, for a Wi-Fi leg where the defaults paint every healthy call yellow
  • sipnab -I intl-trunk.pcap --loss-warn-pct 2 --loss-bad-pct 8 — an international trunk, where 1 percent loss is a Tuesday rather than an incident
  • sipnab -I strict-lan.pcap --loss-warn-pct 0 --loss-bad-pct 1 — the strict form: any loss at all takes a color
  • sipnab -I sat-trunk.pcap --rtt-warn-ms 700 --rtt-bad-ms 1200 — a satellite path, where G.114’s terrestrial figures report every call as bad
  • sipnab -I campus.pcap --rtt-warn-ms 50 --rtt-bad-ms 150 — a campus network, where a 300 ms round trip is already an escalation
  • sipnab -I hd-codec.pcap --mos-warn 4.3 --mos-bad 3.8 — a wideband codec deployment, where 4.0 is not the good score it is on narrowband
  • sipnab -I gsm-gateway.pcap --mos-warn 3.6 --mos-bad 2.8 — a low-bitrate gateway, where nothing ever clears the shipped 4.0 warning
  • sipnab -I triage.pcap --jitter-warn-ms 15 --loss-warn-pct 0.5 --rtt-warn-ms 120 --mos-warn 4.2 — one strict pass across all four columns, for a first look at a network you have not seen before

Security

FlagValueDefaultDescription
--kill-scanneroffDetect SIP scanning (known UA signatures + behavioral rate/enumeration), alert on it, and send the kill response back to the scanner (the CLI matcher -J/-j)
--kill-ua<PATTERN>Add a custom scanner User-Agent pattern (regex) to --kill-scanner detection. Refused without it: --kill-scanner (or [security] kill_scanner = true) builds the detector that reads this pattern, so given alone it would feed nothing and the run would report no scanners — which is what a clean capture looks like. sipnab does not arm the detector for you, because on a live capture --kill-scanner also arms the response path
--kill-response<CODE>200SIP response code for the kill response (100-699)
-K, --kill-target<ADDR[:PORT-RANGE]>Targeted kill: send the kill response to any SIP request whose source matches ADDR and an optional port range (192.0.2.1:5060-5090, [::1]:5060), regardless of UA/behavioral detection. Repeatable; spawns the kill worker on its own (no --kill-scanner needed)
--kill-spoof<MODE>autoSource-address strategy for the kill response (Linux only; other platforms always ephemeral). auto forges the victim’s ip:port via a raw socket when CAP_NET_RAW is available (so the reply appears to come from the targeted SIP port), falling back to an ephemeral source otherwise; raw requires the spoof and errors when it cannot open the raw socket; ephemeral never spoofs
--kill-rate-limit<N>10Scanner-kill responses per second sipnab may put on the wire. This bounds the one feature that answers an address out of the capture, and the sender of each packet sipnab answers chose the address that answer goes to, so there is no unlimited setting and sipnab rejects 0. A per-destination cap of 3 per minute applies underneath, so raising this widens how many distinct hosts sipnab answers, never how hard it hits one. Config: [security] kill_rate_limit
--sandbox<MODE>offBound which files this process may reach, using Landlock. best-effort installs what the kernel supports and captures either way; required refuses to capture when no sandbox is in force. Reads and writes outside the input set, the output directory, the keylog and the crash directory come back EACCES. Sockets are not bounded: Landlock’s network rules reach TCP bind and connect only, which would miss the HEP UDP listener and the pre-drop raw socket, so this ruleset governs the filesystem alone. Nothing here can end a run — Landlock denies an open, it does not signal — and a kernel without it degrades to a warning unless required says otherwise
--seccomp<MODE>offRecord every system call this process makes, and allow every one. log installs a seccomp filter whose only action is SECCOMP_RET_LOG. It denies nothing and protects nothing — it exists to derive an allowlist from evidence rather than a guess, which is why no enforcing mode exists yet. Where the records land depends on the host: auditctl -s prints a connected audit daemon’s pid and the records are then in ausearch -m SECCOMP; a pid of 0 means no daemon and they are in dmesg | grep 'type=1326'. Point it at a bounded offline run — a live capture emits one record per received packet and floods the log
--fraud-detectoffEnable fraud detection heuristics
--evidence-out<PATH|->offPublish every security finding that names a source as JSON Lines, for a system that decides what to do with it. - is standard output, for a pipe; a path receives one appended line per finding, and sipnab opens it at startup, so a path it cannot write is an error before the first packet. sipnab still bans nothing. A finding carried by HEP publishes nothing without --hep-allow-kill, for the reason --fail2ban gives: the inner address is the sender’s claim
--fraud-destination<ISO,...>offDestination countries --fraud-detect reports an INVITE to, as ISO 3166-1 alpha-2 codes (DO,VG,MA). sipnab reads numbers through the common international prefixes (+, 00, 011) and resolves them by longest calling code, so +1 809 is DO, not US; a number with no prefix is domestic and never matches. Every fraud finding also names the resolved destination. Absent: nothing changes. Config: [security] fraud_destination
--business-hours<START-END>Business hours in whole UTC hours, for example 8-18, or 22-6 for an overnight window. This is what makes the off-hours fraud detection reachable: with no window declared there is no outside for a call to fall in. Needs --fraud-detect. Config: [security] business_hours
--fraud-short-call<SECS>3Measured duration below which --fraud-detect counts a completed call as short for wangiri detection. Three seconds is under a normal ring-no-answer on some carriers. Config: [security] fraud_short_call_secs
--fraud-wangiri-calls<N>3Short calls to one destination prefix before --fraud-detect reports wangiri. Config: [security] fraud_wangiri_calls
--fraud-sequential-calls<N>3Consecutive refused numbers before --fraud-detect reports sequential scanning. Config: [security] fraud_sequential_calls
--fraud-volume-multiplier<N>5Multiple of a source’s own baseline call rate that --fraud-detect reports as a volume spike. Config: [security] fraud_volume_multiplier
--fraud-volume-min-calls<N>6Calls a source must place inside the volume window before --fraud-detect reports a spike at all. Config: [security] fraud_volume_min_calls
--fraud-volume-window<SECS>60How much capture time one volume-spike window spans. The count and the baseline are both measured over it, so a steady source reads the same at any width; the width alone decides how concentrated a burst has to be, since a burst shorter than the window averages into the traffic beside it. Config: [security] fraud_volume_window_secs
--fraud-wangiri-window<SECS>60How much capture time one wangiri window spans. The detector drops short calls older than this, so it decides how slowly a lure may arrive and still count as one pattern. No setting of --fraud-wangiri-calls reaches a lure paced wider than the window. Config: [security] fraud_wangiri_window_secs
--scanner-behavioral-probes<N>10Probes from one source inside the scanner window, above which --kill-scanner reports a rate detection. Behind an SBC every source collapses to one address, so ordinary aggregated traffic clears ten in five seconds and the whole site reads as one scanner. Config: [security] scanner_behavioral_probes
--scanner-enumeration-targets<N>5Distinct target extensions from one source inside the scanner window, above which --kill-scanner reports extension enumeration. Config: [security] scanner_enumeration_targets
--scanner-rejected-probes<N>5Rejected probes inside the scanner window at which a source reads as probing rather than operating. This is the evidence gate: neither behavioral signal reports anything until a source clears this or --scanner-unanswered-probes, which is what separates an enumeration sweep from a trunk running keepalives at the same rate. Config: [security] scanner_rejected_probes
--scanner-unanswered-probes<N>5Probes inside the scanner window that drew no response, at which a source reads as sweeping, provided they also outnumber the rest of what it sent. Config: [security] scanner_unanswered_probes
--scanner-window<SECS>5How much capture time one scanner window spans. Every scanner count above is per window, so this is the binding constraint on a paced sweep rather than the counts: one probe every ten seconds never puts two inside the shipped five-second window, so the rate and the spread both stay at one however low the counts go. Config: [security] scanner_window_secs
--scanner-established-factor<N>4How much more evidence --kill-scanner needs from a source that has completed a registration or a call. A registered endpoint that starts probing is a compromised phone worth reporting, but it is also the peer whose ordinary working traffic looks most like probing, and the peer a false positive costs most. Config: [security] scanner_established_factor
--scanner-answer-grace<MS>500How long a probe may go without a response before --kill-scanner counts it as unanswered. The default is RFC 3261’s Timer T1, the round-trip estimate at which SIP itself gives up waiting and retransmits. Raise it on a link whose round trip runs longer than that, where the default reports every probe still in flight as one nobody answered. Config: [security] scanner_answer_grace_ms
--reg-floodoffDetect registration floods: credentialed REGISTERs the registrar keeps refusing. sipnab reports a source when the REGISTERs it sent with Authorization that drew a 401/407 on the same transaction exceed --reg-flood-threshold inside one second of capture time. A REGISTER on its own is never evidence, so a re-REGISTER storm the registrar accepts after a restart is the customer’s SBC and not a flood, and any 2xx to a REGISTER clears the source’s count
--reg-flood-threshold<N>50Challenged failures per second from one source before --reg-flood reports a flood: REGISTERs that carried credentials and drew a 401 or 407. The default is a carrier-registrar figure: it never sees the ten-a-second brute force a small PBX gets. Counted in capture time, so a file replays as the traffic it recorded rather than as fast as the disk reads it. Config: [security] reg_flood_threshold
--digest-leakoffDetect digest credential leaks in SIP messages
--tfps-ctl<PATH>Where the toll-fraud prevention system’s tfps_ctl program is, for the tfps_* MCP tools and the /v1/tfps/ REST routes. TFPS is optional peer software that condemns sources and enforces the decision in the firewall; sipnab asks it and never bans anything itself. Absent: sipnab looks for tfps_ctl on PATH only the moment a TFPS tool runs, and a machine without one answers installed: false rather than failing. Config: [tfps] ctl; the database goes in [tfps] db
--recommend-block<DIALECT>Print a firewall rule for every source the detectors accused: fail2ban, nftables, iptables or all. sipnab recommends and does not apply — the rule is text on stdout, and nothing here reaches a firewall or holds a credential. Each block carries its evidence (how many findings named the address, which rules they tripped, and when) and its COUNTER-evidence beside it: a source that also completed a registration or a call is one a block would disconnect, so the address-specific dialects comment their commands out for it and the fail2ban dialect puts it in ignoreip. Needs a detector armed beside it; with none, nothing is ever accused and the empty output says which silence that is rather than reading as an all-clear. Requires -N
--findings-history<N>1000Security findings kept in memory for later retrieval. 0 keeps none. Config: [security] findings_history
--alert<CHANNEL>Repeatable alert channel (syslog, json, exec) or rule (<name>:<threshold>/<window>[:<cooldown>])
--alert-exec<CMD>Execute this command when an alert fires
--alert-jsonoffEmit each security alert as a structured JSON line on stderr (in addition to the human [ALERT] line)
--stir-shakenoffReport STIR/SHAKEN Identity claims — decodes the PASSporT, does NOT verify the signature
--run-provenance-file<FILE>Record the command that started this run, as one JSON line appended to FILE (seq, ts, record, argv, cwd, uid, user, pid, started, version, features, capture). A report, a vCon container or an exported pcap says what sipnab concluded and nothing in it says which invocation produced it — which capture, which filter, which port range, which retention caps. --portrange alone changes what a run can see, so a report produced under a narrow range is afterwards indistinguishable from one that examined everything. capture is the same instance token the MCP and REST surfaces stamp on their answers, which is what joins a record to an artefact. sipnab writes it once, at startup, before it loads the config and before it opens any capture device. Opened O_APPEND and never truncated, so successive runs accumulate; created mode 0600 when absent, because argv holds capture paths and a path holds a customer name. A record sipnab cannot write stops the run, because a best-effort line would leave its own absence ambiguous between “not enabled” and “the disk was full”. Leave it off and nothing changes
--tui-audit-file<FILE>Record what the operator did in the TUI, one JSON line per action appended to FILE (seq, ts, record, action, target, format, caller, outcome, error). Actions, not keystrokes: the capture opened, the capture swapped, a filter applied or cleared, an export and its destination — including one sipnab refused. A keystroke log of the TUI bindings would be mostly navigation, unreadable at review time, and a privacy hazard of its own, so the search field is never recorded, neither the query nor the fact that the operator typed one. Same writer and same file shape as --mcp-audit-file: append-only, never truncated, one sequence number per record so a gap is a lost record, created mode 0600. A path sipnab cannot open stops the run before sipnab takes the terminal, and sipnab refuses -N rather than accepting it with nothing to record. A write that fails mid-session does NOT stop the TUI — an operator holding a live capture that exists nowhere else must not lose it because a log partition filled; the lost record leaves a permanent hole in the numbering, the status line says the trail is incomplete, and the closing session_end record and the exit message on standard error name the count. Leave it off and nothing changes. Feature: tui

--alert accepts a channel or a threshold rule. Channels are syslog, json or exec. --syslog and --alert-json are the equivalent boolean forms; naming the channel here does the same thing. A value containing : is instead parsed as an alert rule (<name>:<threshold>/<window>[:<cooldown>], window needs an s/m/h suffix). Rule names are scanner, fraud, digest and reg-flood (also reg_flood); an unknown rule name exits 2 at startup. A rule counts detector findings, not raw packets. An unrecognized bare word draws a warning naming the valid channels. It used to fail silently, so a documented --alert syslog enabled nothing at all.

Examples

  • sudo sipnab -d eth0 --kill-scanner --kill-ua 'friendly-scanner' --kill-response 486 --kill-spoof auto — detect SIP scanners (plus a custom UA pattern) and reply 486 with the victim’s spoofed source
  • sudo sipnab -d eth0 --kill-target 192.0.2.66:5060-5090 --kill-ua 'sipvicious' --kill-response 480 --kill-spoof raw — targeted kill of a scanning host across a port range, plus a second scanner UA, replying 480 via raw-socket spoof
  • sudo sipnab -d eth0 --kill-target 198.51.100.77:5060 --kill-spoof ephemeral — kill requests from one more source port using a non-spoofed ephemeral reply
  • sudo sipnab -N -d eth0 --reg-flood --digest-leak --fraud-detect --stir-shaken --alert json --alert-json --alert-exec '/usr/local/bin/notify.sh' — live security monitoring: registration floods, digest leaks, fraud, STIR/SHAKEN, with JSON alerts and an exec hook
  • sipnab -N -I capture.pcap --stir-shaken --digest-leak --alert-json — offline audit of a pcap for digest leaks and STIR/SHAKEN attestation claims (as the originator presented them — sipnab checks no signature), emitting structured JSON alerts
  • sudo sipnab -N -d eth0 --reg-flood --reg-flood-threshold 10 --fraud-detect --business-hours 8-18 --fraud-short-call 1 — tune the detectors to a small PBX: ten REGISTERs a second is a brute force here, calls outside office hours are worth a line, and a one-second call is the only one short enough to be a lure
  • sudo sipnab -N -d eth0 --reg-flood --reg-flood-threshold 400 --fraud-detect --business-hours 22-6 — the carrier-registrar shape instead: only a genuine flood clears 400 REGISTERs a second, and the quiet window is overnight
  • sipnab -N -I trunk.pcap --fraud-detect --fraud-short-call 6 --fraud-wangiri-calls 5 --fraud-sequential-calls 6 — audit a wholesale trunk where short calls are ordinary, so a lure needs five of them and a dial-plan walk needs six consecutive dead numbers
  • sipnab -d eth0 -N --kill-scanner --evidence-out - — publish findings on standard output for a reader to consume: sipnab says what it saw, and whatever consumes the stream decides what to do about it
  • sipnab -N -I trunk.pcap --kill-scanner --reg-flood --evidence-out findings.jsonl — replay a capture and collect every source-naming finding in one file, to read before you wire anything to a response
  • sipnab -d eth0 -N --fraud-detect --fraud-destination DO,VG,MA — an SBC that never dials the Dominican Republic, the British Virgin Islands or Morocco, three destinations where premium-rate fraud concentrates; the first INVITE to any of them alerts on sight
  • sipnab -N -I trunk.pcap --fraud-detect --fraud-destination CU,KP --json — replay a trunk capture and write every INVITE to an embargoed destination as one JSON line, for the audit that asked whether any call ever went there
  • sipnab -N -I pbx.pcap --fraud-detect --fraud-wangiri-calls 2 --fraud-sequential-calls 2 --fraud-volume-multiplier 3 --fraud-volume-min-calls 4 — the sensitive form for a small PBX, where two short calls to one prefix and four calls in a minute are already unusual
  • sipnab -N -I trunk.pcap --fraud-detect --fraud-volume-multiplier 20 --fraud-volume-min-calls 200 — a busy carrier trunk, where a spike has to be twenty times its own baseline and 200 calls a minute before it means anything
  • sipnab -N -I trunk.pcap --fraud-detect --fraud-wangiri-window 900 --fraud-short-call 5 — hunt a paced lure: three short calls to one prefix over fifteen minutes, which the shipped sixty-second window forgets between calls
  • sipnab -N -I pbx.pcap --fraud-detect --fraud-volume-window 5 --fraud-volume-min-calls 20 — catch a burst shorter than a minute, which a sixty-second window averages into the ordinary traffic around it
  • sudo sipnab -N -d eth0 --fraud-detect --fraud-volume-window 300 --fraud-wangiri-window 300 — a low-volume site where five minutes is the shortest span that holds enough calls to say anything about either pattern
  • sipnab -N -I sbc.pcap --kill-scanner --scanner-behavioral-probes 200 --scanner-enumeration-targets 60 — an SBC that fronts a whole site behind one address, where ordinary aggregated traffic clears the shipped ten probes and five extensions within seconds
  • sipnab -N -I slow-sweep.pcap --kill-scanner --scanner-window 600 --scanner-enumeration-targets 8 — hunt a paced sweep across a ten-minute window: one probe every ten seconds never puts two inside the shipped five-second window, so widening the window is the only setting that reaches it
  • sudo sipnab -N -d eth0 --kill-scanner --scanner-window 60 --scanner-behavioral-probes 40 --scanner-rejected-probes 20 — a busy registrar that refuses every unauthenticated first attempt, so refusals only count as evidence once there are twenty of them in a minute
  • sipnab -N -I sat-trunk.pcap --kill-scanner --scanner-answer-grace 3000 --scanner-unanswered-probes 20 — a satellite trunk whose round trip runs well past RFC 3261’s Timer T1, where the shipped 500 ms grace calls every probe still in flight unanswered
  • sudo sipnab -N -d eth0 --kill-scanner --scanner-established-factor 1 --scanner-rejected-probes 8 — judge a registered endpoint like any other source, for a site hunting compromised handsets rather than outside scanners
  • sipnab -N -I pbx.pcap --kill-scanner --scanner-answer-grace 1500 --scanner-established-factor 8 --scanner-unanswered-probes 10 — a small PBX on a slow access circuit, where a registered phone needs eight times the evidence and every probe gets a second and a half to draw a reply
  • sudo sipnab -N -d eth0 --kill-scanner --kill-rate-limit 2 --findings-history 20000 — answer scanners at a deliberately small two responses a second while keeping a long detection history for an agent to read back
  • sipnab -N -I capture.pcap --kill-scanner --recommend-block nftables — read a capture you already have and print an nft rule per accused source, with the evidence and the counter-evidence in the same block. sipnab prints; you decide and run it
  • sipnab -N -I trunk.pcap --kill-scanner --reg-flood --digest-leak --recommend-block all --quiet > blocks.txt — every dialect for every accused source on a trunk capture, with the packet echo suppressed so the file holds nothing but the recommendation. Read blocks.txt before running any of it: an address that also completed a call is one a block disconnects, and the block says so
  • sudo sipnab -N -d eth0 --kill-target 192.0.2.66 --kill-rate-limit 50 --findings-history 0 — a targeted response with a wider transmit budget and no findings retained in memory
  • sipnab -N -I capture.pcap --json --run-provenance-file /var/log/sipnab-runs.jsonl — record the invocation beside the report, so a reader can trace the JSON above back to the capture, the filters and the build that produced it
  • sudo sipnab -N -d eth0 --portrange 5060-5090 --report --run-provenance-file /var/log/sipnab-runs.jsonl — the case the record exists for: the port range decided what this run could see, and without the record a later reader cannot tell a narrow run from a complete one
  • sipnab -I capture.pcap --tui-audit-file /var/log/sipnab-tui.jsonl — record which capture the analyst opened, what they filtered it down to and what they exported, for the review that opens with “who exported which calls”
  • sudo sipnab -d eth0 --tui-audit-file /var/log/sipnab-tui.jsonl --run-provenance-file /var/log/sipnab-runs.jsonl — both halves on a live capture: one line saying how the session started, and one record per state-changing thing the operator then did
  • sipnab -N -d eth0 --mcp --tfps-ctl /usr/local/bin/tfps_ctl — serve the MCP tools with TFPS named outright, so tfps_status and tfps_banned answer from that installation rather than whatever PATH finds
  • sipnab -N -I trunk.pcap --api 127.0.0.1:8080 --api-key "$SIPNAB_API_KEY" --tfps-ctl /opt/tfps/bin/tfps_ctl — replay a capture behind the REST API and let GET /v1/tfps/labels fetch the verdict log the label harness scores it against

Event execution

FlagValueDefaultDescription
--on-dialog-exec<CMD>Execute command when a dialog state changes
--on-quality-exec<CMD>Execute command when RTP quality drops below threshold
--exec-rate-limit<N>10Maximum exec invocations per second
--exec-queue-depth<N>100Hook commands allowed to be running at once before sipnab drops --on-dialog-exec and --on-quality-exec events. The second ceiling above --exec-rate-limit, and the binding one for any hook that takes longer than a second: its slot is still occupied when the next second’s budget arrives, so on a busy trunk this is what events actually meet. Config: [limits] exec_queue_depth

What a hook receives. Every command gets the event as environment variables. --on-dialog-exec sets SIPNAB_CALL_ID, SIPNAB_FROM, SIPNAB_TO, SIPNAB_STATE, SIPNAB_METHOD and SIPNAB_JSON (the whole dialog). --on-quality-exec sets SIPNAB_SSRC, SIPNAB_SRC, SIPNAB_MOS, SIPNAB_JITTER, SIPNAB_LOSS and SIPNAB_STREAM_JSON — the stream object under its own name, never SIPNAB_JSON. Rule hooks add SIPNAB_RULE, SIPNAB_DETAIL and SIPNAB_VARIABLE.

A hook inherits sipnab’s whole environment, credentials included. sipnab does not clear the environment before spawning, so a hook sees SIPNAB_API_SIGNING_KEY, SIPNAB_MCP_TOKEN and SIPNAB_HEP_AUTH if you set them there — and Authentication recommends exactly that. This is deliberate: a hook that must call back into the API needs the credential, and stripping the environment would break that without making anything safer, since a hook already runs as you. It does mean a hook command is as trusted as the process: do not pass one a string built from capture data, and do not run one you would not run by hand with your keys exported.

Hooks cannot gain privileges. sipnab sets PR_SET_NO_NEW_PRIVS at startup on Linux, on every run and whether or not it is root, and every command it spawns inherits that flag. A hook may run anything you can already run. What it cannot do is get more than you have through a setuid or setgid helper: sudo, pkexec and ping start, then fail for want of the privilege they normally acquire. A hook that needs to act privileged should ask something that already is — a socket to a daemon, a systemd unit it triggers — rather than trying to become privileged itself. Root runs have always behaved this way. Unprivileged runs (sipnab --setup-caps) now do too.

Examples

  • sudo sipnab -d eth0 --on-dialog-exec 'logger sipnab $SIPNAB_CALL_ID' --exec-rate-limit 5 --exec-queue-depth 20 — a slow syslog hook on a busy trunk, where twenty concurrent children is the ceiling events actually meet rather than the five-a-second budget
  • sipnab -N -I trunk.pcap --on-quality-exec 'curl -m 30 -X POST http://hook/quality' --exec-queue-depth 4 — a webhook that may take thirty seconds, held to four in flight so a stalled endpoint cannot fork the box flat

Network listeners

FlagValueDefaultDescription
--metrics<ADDR>Prometheus metrics endpoint (e.g., 127.0.0.1:9090). Serves in BOTH TUI and headless (-N) runs — headless is where a container or systemd unit uses it. sipnab refuses a non-loopback bind (e.g. 0.0.0.0:9090) unless you also pass --metrics-auth/--metrics-auth-file, and the run then exits non-zero rather than carrying on without an endpoint — so sipnab --metrics ... && ... fails the way a script expects, matching --api. Note a file run (-I) exits as soon as it finishes the capture, so there is little to scrape; the endpoint is for long-lived runs — a live device, --hep-listen, or a served API/MCP. Not served on the --cores N parallel offline path, which finishes and exits before a scrape could land; sipnab warns when you combine them. Feature: metrics
--metrics-auth<USER:PASS>HTTP Basic auth credentials (user:pass) required by the metrics endpoint; requests must send Authorization: Basic <base64>. Prefer --metrics-auth-file. Feature: metrics
--metrics-auth-file<FILE>Read the metrics Basic-auth user:pass from a file (contents trimmed), keeping the secret out of the process list. Takes precedence over --metrics-auth. Feature: metrics
--api<ADDR>REST API endpoint (e.g., 0.0.0.0:8080). Feature: api
--api-key<KEY>API key for REST API authentication. Also reads $SIPNAB_API_KEY Feature: api
--api-tls-cert<FILE>Not yet implemented — nothing wires up built-in API TLS, and sipnab exits when you pass this. Terminate TLS at a reverse proxy instead. Feature: api
--api-tls-key<FILE>Not yet implemented — see --api-tls-cert; terminate TLS at a reverse proxy. Feature: api
--api-max-conn<N>100Maximum concurrent API connections Feature: api
--api-signing-key<KEY>HMAC signing key for self-describing bearer tokens, taken as raw bytes (any string — not hex-decoded). Repeatable: the first mints, verification accepts every one, so keys can rotate with overlap. Also reads $SIPNAB_API_SIGNING_KEY. See auth.md. Feature: api
--api-signing-key-file<FILE>Read an API signing key from a file (contents trimmed); it becomes the minting key. Feature: api
--api-revoked-file<FILE>Revocation denylist: one revoked token id per line; reloaded on mtime change. Feature: api
--api-token-ttl<SECS>3600Default TTL (seconds) when minting API tokens with --mint-token. Feature: api
--api-max-rows<N>1000Rows one list-style REST response returns. The REST counterpart of --mcp-max-rows, settable for the same reason: the right ceiling belongs to the consumer, not to sipnab. A batch consumer piping /v1/dialogs to a file wants every row; a dashboard drawing a table wants far fewer. A caller may always ask for less with ?limit=, and nothing it sends asks for more than this. Config: [limits] api_max_rows Feature: api
--api-rate-limit-per-peer<N>100REST requests one client IP may make per second; 0 disables the cap, the reading --mcp-rate-limit-per-peer and --hep-rate-limit also give it. The limiter counts by source address, so a dashboard polling /v1/streams on a short timer, or several collectors behind one NAT, share a single allowance. A refusal is 503 rather than 429 because the limiter runs before authentication, so it says nothing about the credential. Config: [limits] api_rate_limit_per_peer Feature: api
--api-allow-relay-queryLet REST clients query the relay named by --rtpengine-control through the GET /v1/relay/... routes, which transmit. The REST counterpart of --mcp-allow-relay-query, off for the same reason: every other REST answer comes from bytes sipnab already holds, and these put a packet on the network — at the address that flag names, never one a client chooses. Without this flag those routes answer not_permitted; without a relay, or on a file-backed run, not_configured. Each response is HTTP 200 carrying an outcome, so a refusal is content, not a 4xx. Polling on an interval is not offered over REST at all. Feature: api
--api-file-root<DIR>Directory of capture files GET /v1/captures/compare may diff. The REST counterpart of --mcp-file-root, off for the same reason: every other REST answer comes from bytes sipnab already holds, while this reads FILES a client names. It takes a bare FILENAME, never a path: sipnab rejects a separator, a .. or an absolute prefix before touching the filesystem, and refuses a symlink that resolves out of the root at open, by the same check -O uses. Without this flag the compare route answers 503. Naming one directory means the worst a client can do is name files inside it. Feature: api
--metrics-max-conn<N>16Metrics scrapes served at once before further ones get 503. The gate stops a burst of slow clients exhausting threads and taking monitoring down, and sixteen suits one Prometheus; an HA pair, a federating parent, a remote_write shard, an alertmanager sidecar and one engineer’s curl reach it without anything unusual happening. A refused scrape leaves a hole in the series that reads as a capture that died rather than as a busy endpoint. Config: [limits] metrics_max_conn Feature: metrics
-L, --hep-listen<ADDR>Listen for HEP (Homer Encapsulation Protocol) packets. Combines with -d <iface>: HEP then supplies signaling while the interface supplies the RTP a HEP feed cannot carry, and streams bind to dialogs by SDP media endpoint. Give that run an explicit media-only BPF expression (sipnab -N -d eth0 -L 127.0.0.1:9060 "udp portrange 10000-20000") — without one the interface gets the auto-generated signaling filter, captures no media, and sees every mirrored message a second time. One interface and one listener only: --multi-device with -L, -I with -L, and -O alongside the pair are each refused with the reason. See cookbook recipe 6d. Feature: hep
-H, --hep-send<ADDR>Send captured packets via HEP to a remote collector: SIP as protocol type 1 and RTCP as type 5, so the collector can report media quality and not only call setup. RTP is never forwarded. On -I <file> this forwards the file’s contents: every SIP message and RTCP report sipnab reads out of the capture goes to <ADDR> as recorded, redacted in no way. sipnab announces that at startup, naming the flag, the destination and the capture files, before it reads the first packet. See What --hep-send sends. Feature: hep
--hep-send-transport<udp|tcp|tls>udpTransport --hep-send uses to reach the collector. Homer’s collectors accept all three. HEP v3 carries its own total length, so a tcp or tls feed is packets laid end to end with no extra framing, and sipnab sets TCP_NODELAY because these are small packets whose value is timeliness. A write that fails drops the connection and the next packet dials again — one reconnect per packet, never a loop, so a collector that is down does not turn forwarding into a spin. Refused without --hep-send, which is the side it governs. Feature: hep
--hep-listen-transport<udp|tcp|tls>udpTransport --hep-listen accepts. A tcp or tls listener serves several agents at once, one connection each, and reads every connection as HEP v3 packets delimited by the total length in each header. HEP v2 is datagram-only: it declares no total length, so nothing can delimit it on a stream. A peer that sends bytes that are not HEP v3 loses its connection, because a stream offers no point to resynchronize at. Refused without --hep-listen, which is the side it governs. Feature: hep
--hep-tls-ca<FILE>Certificate authority (PEM) this sender checks the collector’s certificate against under --hep-send-transport tls. A named CA replaces the trust store rather than joining it, so a private collector’s issuer is the whole of what this sender accepts; a file sipnab cannot read, or that holds no certificate, raises an error rather than an empty store. Omit it to use the host’s CA bundle ($SSL_CERT_FILE, else /etc/ssl/certs/ca-certificates.crt and its equivalents). sipnab refuses it unless --hep-send-transport tls names the transport. Feature: hep
--hep-tls-cert<FILE>Server certificate chain (PEM, leaf first) a --hep-listen-transport tls listener presents to connecting agents. It must be an end-entity certificate: rustls refuses a CA certificate offered as a server certificate. sipnab refuses it unless --hep-listen-transport tls names the transport, and that transport requires it. Feature: hep
--hep-tls-key<FILE>Private key (PEM) for --hep-tls-cert. sipnab refuses a key any other user on the host can readchmod 600 it. Group-readable stays allowed, because root:sipnab 0640 is how a key is normally handed to a service account. sipnab refuses it unless --hep-listen-transport tls names the transport, and that transport requires it. Feature: hep
--hep-id<ID>1Capture-agent id (HEP 0x000c chunk) stamped on packets sent via --hep-send. Feature: hep
--hep-auth<KEY>Homer authenticate key (HEP 0x000e chunk). On --hep-send sipnab stamps it on every outgoing packet; on --hep-listen it enables receiver-side authentication — incoming packets must carry a matching key, which sipnab compares in constant time, or it drops them. Also read from SIPNAB_HEP_AUTH. Security note: the key travels in cleartext inside the HEP datagram, so it defeats blind/off-path spoofing but an on-path sniffer can capture and replay it. Over an untrusted path, tunnel HEP through WireGuard/IPsec/stunnel (the same posture as terminating API TLS in a reverse proxy) rather than relying on the key alone. Feature: hep
--hep-auth-file<FILE>Read the HEP shared secret from a file (contents trimmed), keeping it out of the process list. Takes precedence over --hep-auth. Feature: hep
--hep-auth-mode<plain|hmac>plainHEP auth mode. plain sends/expects the shared secret verbatim in the 0x000e chunk (Homer-compatible, but replayable by an on-path sniffer). hmac sends/expects a per-message token (timestamp + nonce + HMAC-SHA256 over the payload) that resists replay — sipnab-to-sipnab only; a stock Homer/Kamailio peer does not understand it. Feature: hep
--hep-hmac-window<SECS>30Seconds either side of now within which sipnab still honors a --hep-auth-mode hmac token’s timestamp. On an agent/collector pair with poor NTP sipnab turns every packet away as out-of-window, and what the operator sees is a collector receiving NOTHING – a symptom they attribute to routing, a firewall, or a dead agent long before a clock. Widening it is a security trade rather than a convenience: the window is exactly how long a packet an on-path attacker captured stays acceptable, and how far back the receiver’s nonce cache must remember. Range 1-300. Config: [security] hep_hmac_window_secs Feature: hep
-E, --hep-parseoffParse incoming HEP packets (enable HEP decoding). Feature: hep
--hep-allow<ADDR>Allowed source addresses for HEP input (repeatable). Takes CIDR (192.0.2.0/8, 2001:db8::/32) or a bare address (192.0.2.40), which means that host alone — /32 for IPv4, /128 for IPv6. A missing prefix always narrows, never widens: 192.0.2.0 is one host, not 192.0.2.0/8. sipnab refuses a non-loopback --hep-listen bind unless you pass either this or --hep-auth/--hep-auth-file. Feature: hep
--hep-rate-limit<N>50000Maximum HEP packets per second (global ceiling across all senders); 0 disables the global ceiling, consistent with off on the per-peer knob Feature: hep
--hep-rate-limit-per-peer<N|auto|off>offMaximum HEP packets/second from any single source IP: a number, off (the default), or auto. Adds fairness so one flooding peer cannot exhaust the global --hep-rate-limit. auto divides the global ceiling evenly across the --hep-allow sources (stays off without an allowlist). The listener logs its active limiters at startup. Feature: hep
--hep-allow-killoffAllow scanner-kill to send active responses for packets received via HEP, and admit HEP-carried detections to the --fail2ban jail log. Off by default: a HEP sender asserts the inner src/dst, so absent --hep-auth an attacker could aim the kill at a victim of their choosing, or have the jail ban an address of their choosing. Only enable with authenticated, trusted HEP input. Feature: hep
--syslogoffSend alerts to syslog
--mint-tokenoffMint a signed bearer token from the first configured signing key (API or MCP), print it to stdout, and exit (no capture/servers). See auth.md.
--token-id<ID>Token id (jti) for --mint-token, used for revocation. Defaults to a generated id.
--token-scope<full|metrics|read>fullScope for --mint-token. metrics reaches GET /metrics and returns 401 everywhere else — mint one for a scrape job rather than a credential that also reads /v1/dialogs and the message bodies underneath. read is the MCP counterpart: it reaches the read-only tools and refuses the five that write. A cross-surface mint fails at mint time, so metrics with MCP and read with the REST API are both refused rather than issued and then rejected.

Examples

  • sudo sipnab -d eth0 --api 127.0.0.1:8080 --api-signing-key-file /etc/sipnab/signing.key --api-revoked-file /etc/sipnab/revoked.txt --api-token-ttl 7200 --api-max-conn 200 --metrics 127.0.0.1:9090 --metrics-auth alice:s3cret — live capture serving a signed-token REST API, a revocation list, and a Basic-auth’d Prometheus endpoint (terminate TLS at a reverse proxy)
  • sudo sipnab -d eth0 --api 0.0.0.0:8080 --api-signing-key-file /etc/sipnab/signing.key --api-token-ttl 3600 --api-max-conn 100 --metrics 127.0.0.1:9090 --metrics-auth bob:hunter2 — public-facing API tuned to 100 connections and 1h token TTL, with its own auth’d metrics endpoint
  • sudo sipnab -N -d eth0 --api 127.0.0.1:8080 --api-key s3cret --api-max-rows 100000 --api-rate-limit-per-peer 0 — a batch consumer’s API: one GET /v1/dialogs?limit=100000 drains the whole store in a single page, and the per-peer cap is off so a scripted pager is not throttled against itself
  • sudo sipnab -N -d eth0 --api 127.0.0.1:8080 --api-key s3cret --api-max-rows 200 --api-rate-limit-per-peer 600 — the opposite, for a dashboard: short pages a browser can render, and six hundred requests a second per address, so a dozen browsers refreshing twice a second behind one office NAT share an allowance that fits them
  • sudo sipnab -N -d eth0 --api 127.0.0.1:8080 --api-key s3cret --rtpengine-control 127.0.0.1:22222 --api-allow-relay-query — a live capture whose REST clients may also ask the relay: GET /v1/relay/stats returns its counters as one JSON object with an outcome, transmitting once per request. Off without this flag, when those routes answer not_permitted
  • sipnab -N -I capture.pcap --api 127.0.0.1:8080 --api-key s3cret --api-allow-relay-query --rtpengine-control 127.0.0.1:22222 — the relay routes still answer on a file run, but with outcome: not_permitted: a file-backed run holds no transmit permit, so no packet reaches the relay, and the addresses in a capture belong to third parties
  • sudo sipnab -N -d eth0 --api 127.0.0.1:8080 --api-key s3cret --api-file-root /var/lib/sipnab/captures — a live capture whose REST clients may also diff rotations: GET /v1/captures/compare?a=yesterday.pcap&b=today.pcap reads two files from that one directory and nothing outside it, ranking what moved. Off without the flag, when the route answers 503
  • sipnab -N -I today.pcap --api 127.0.0.1:8080 --api-key s3cret --api-file-root /srv/pcaps — the compare route works on a file-backed run too: it diffs the two NAMED files under /srv/pcaps, never the loaded today.pcap, so a monitoring box can poll one sipnab for a rolling day-over-day comparison
  • sudo sipnab -N -d eth0 --metrics 127.0.0.1:9090 --metrics-max-conn 64 — an HA Prometheus pair, a federating parent, a remote_write shard and an alertmanager sidecar all scraping one sipnab: at sixteen slots a slow scrape turns the next one away with 503, and the hole in the series reads as a capture that died
  • sudo sipnab -N -d eth0 --metrics 127.0.0.1:9090 --metrics-max-conn 2 — a single scraper on a small box, where two slots covers every client this endpoint has, and a third connection is something to turn away
  • sipnab -N -L 0.0.0.0:9060 --hep-parse --hep-auth-file /etc/sipnab/hep.key --hep-auth-mode hmac --hep-hmac-window 120 — a collector whose agents run on hardware with a drifting clock: two minutes of tolerance keeps them heard while someone repairs the NTP problem
  • sipnab -N -L 127.0.0.1:9060 --hep-parse --hep-auth-file /etc/sipnab/hep.key --hep-auth-mode hmac --hep-hmac-window 5 — the opposite, where every agent shares a local time source: five seconds narrows the replay window a captured packet stays valid in
  • sudo sipnab -N -d eth0 --mcp --mcp-transport http --mcp-bind 127.0.0.1:8731 --mcp-token t0ken-alice --mcp-signing-key-file /etc/sipnab/mcp-signing.key --mcp-revoked-file /etc/sipnab/mcp-revoked.txt --mcp-token-ttl 1800 — loopback HTTP MCP server with a bearer token, file-loaded signing key, revocation denylist, and a 30-minute mint TTL
  • sudo sipnab -N -d eth0 --mcp --mcp-transport http --mcp-bind 0.0.0.0:8731 --mcp-token t0ken-bob --mcp-signing-key-file /etc/sipnab/mcp-signing.key --mcp-revoked-file /etc/sipnab/mcp-revoked.txt --mcp-allowed-host mcp.example.com — non-loopback HTTP MCP server (token required) accepting an extra Host header for named clients
  • sudo sipnab -N -d eth0 --hep-send 192.0.2.10:9060 --hep-id 42 --hep-auth s3cr3t-homer-key — forward captured packets to a Homer collector, stamping capture-agent id 42 and an authenticate key
  • sudo sipnab -N -d eth0 --hep-send 198.51.100.20:9060 --hep-id 7 --hep-auth homerkey2 — forward to a second collector under a different agent id and auth key
  • sudo sipnab -N -d eth0 --hep-send 198.51.100.30:9060 --hep-auth-file /etc/sipnab/hep.key --hep-auth-mode hmac — replay-resistant forwarding to another sipnab: HMAC-token auth over an untrusted path (both ends must set –hep-auth-mode hmac)
  • sipnab -N -I archive.pcap --hep-send 127.0.0.1:9060 --hep-id 9 — replay an archived capture into a collector on this host. sipnab warns at startup that the file’s signaling leaves the machine, then forwards it
  • sipnab -N -L 0.0.0.0:9060 --hep-parse --hep-auth-file /etc/sipnab/hep.key --hep-auth-mode hmac — the matching sipnab-to-sipnab HMAC collector: verifies the per-message token and rejects replays
  • sipnab -N -L 0.0.0.0:9060 --hep-parse --hep-allow 192.0.2.0/24 --hep-allow 198.51.100.20/32 --hep-rate-limit 20000 — run a HEP collector that parses incoming packets, only from two allowed CIDRs, capped at 20k pkts/sec
  • sipnab -N -L 0.0.0.0:9060 --hep-parse --hep-auth-file /etc/sipnab/hep.key --hep-rate-limit 40000 --hep-rate-limit-per-peer 5000 — authenticated HEP collector on a routable address: incoming packets must carry the shared secret, with a 5k/s per-peer fairness cap
  • sipnab -N -L 0.0.0.0:9060 --hep-parse --hep-auth-file /etc/sipnab/hep.key --hep-allow-kill --kill-scanner — authenticated HEP collector that may also actively kill scanners seen in the HEP stream (only safe because the feed carries authentication)
  • sipnab -N -L 0.0.0.0:9060 --hep-parse --hep-auth s3cr3t-homer-key --hep-rate-limit-per-peer 2000 --hep-allow-kill --kill-target 198.51.100.7 — inline HEP secret (visible in the process list; prefer –hep-auth-file) with a tight per-peer cap for a busy multi-proxy fleet
  • sudo sipnab -N -d eth0 --hep-send 192.0.2.10:9061 --hep-send-transport tcp --hep-id 42 — forward to a collector that speaks HEP over TCP, so a busy collector orders and keeps the feed instead of losing datagrams
  • sipnab -N -I archive.pcap --hep-send 127.0.0.1:9061 --hep-send-transport tcp — replay an archived capture into a local TCP collector; the stream is packets laid end to end, framed by each HEP v3 header’s total length
  • sudo sipnab -N -d eth0 --hep-send collector.example.com:9063 --hep-send-transport tls --hep-tls-ca /etc/sipnab/collector-ca.pem — encrypted forwarding across a path you do not control, verifying the collector against your own issuer
  • sudo sipnab -N -d eth0 --hep-send homer.example.net:9063 --hep-send-transport tls --hep-tls-ca /etc/pki/tls/certs/homer-ca.pem --hep-auth-file /etc/sipnab/hep.key — the same, with the Homer authenticate key inside the session as well as the session around it
  • sipnab -N -L 0.0.0.0:9061 --hep-listen-transport tcp --hep-parse --hep-auth-file /etc/sipnab/hep.key — an authenticated TCP HEP collector: several proxies connect at once and sipnab reads each connection as a stream of HEP v3 packets
  • sipnab -N -L 127.0.0.1:9061 --hep-listen-transport tcp --hep-parse — the loopback version, for an agent mirroring to a collector on the same host over a connection rather than datagrams
  • sipnab -N -L 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 — a TLS HEP collector: agents reach it over an encrypted stream and still prove who they are with the shared secret
  • sipnab -N -L 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-allow 192.0.2.0/24 — the same listener bounded by source address instead: TLS encrypts the path, and --hep-allow is still what says which addresses may speak on it
  • sipnab -N -I capture.pcap --metrics 127.0.0.1:9090 --metrics-auth-file /etc/sipnab/metrics.cred — loopback metrics endpoint reading its Basic-auth credential from a file (keeps user:pass out of the process list)
  • sudo sipnab -d eth0 --metrics 0.0.0.0:9090 --metrics-auth-file /etc/sipnab/metrics.cred — routable metrics endpoint (non-loopback requires auth) using a file-backed credential; terminate TLS at a reverse proxy
  • sipnab --mint-token --token-id alice-2026 --api-signing-key-file /etc/sipnab/signing.key --api-token-ttl 3600 — mint a signed bearer token with a fixed id (for later revocation) and a 1-hour TTL, then exit
  • sipnab --mint-token --token-scope metrics --token-id prom-scraper --api-signing-key-file /etc/sipnab/signing.key --api-token-ttl 86400 — mint a scrape-only token for Prometheus: it reaches /metrics, and every /v1/ route refuses it
  • sipnab --mint-token --token-scope full --token-id ops-oncall --api-signing-key-file /etc/sipnab/signing.key — the default scope, stated explicitly: full access to the REST API surface

What --hep-send sends

--hep-send <ADDR> forwards every SIP message and every RTCP report sipnab reads to the collector at <ADDR>, byte for byte as the capture holds it. On a live capture that matches what the flag sounds like. Traffic passes the interface, and a copy reaches Homer.

Each datagram’s IP protocol chunk carries the transport the message arrived on: 17 for UDP, 6 for TCP, TLS and WebSocket, 132 for SCTP. A collector filtering by transport finds a TCP trunk under TCP.

RTCP travels as HEP protocol type 5, which is what lets a remote collector report media quality — loss, jitter, MOS — rather than only whether calls connect. RTP is never forwarded. RTCP is a control channel that RFC 3550 section 6.2 holds to a small fraction of session bandwidth, so it carries the quality summary at a rate a WAN link and a UDP feed can absorb. The media itself is the opposite on both counts, and forwarding it would make this a call recorder pointed at the collector.

On -I <file> the same sentence carries a sharper meaning. The messages sipnab reads come out of the capture file, so that file’s signaling leaves the machine: request lines, headers, URIs, and any message bodies it holds. sipnab redacts nothing and drops nothing beyond what --portrange and the matching flags already exclude. Testing a HEP pipeline against a customer capture therefore ships that customer’s signaling to whatever <ADDR> names.

sipnab announces this before it reads the first packet:

WARN sipnab::app::bootstrap: --hep-send collector.example:9060 forwards every
SIP message and every RTCP report this run reads to that address, and this run
is reading a capture FILE (customer.pcap). The signaling in those captures
leaves this machine ...

That line is a warning rather than a refusal, because you chose the destination. Replaying an archive into your own collector stays a supported workflow. Two habits keep it uneventful:

  • Name a collector you control. sipnab never takes the destination from the capture. The address always comes from your command line or your config file, and no code path exists that turns a recorded address into an export target.
  • Read the startup warning before you walk away. It names the flag, the destination, and the capture files.

The scanner-kill path works the other way round and refuses to run offline. It aims at addresses recorded inside the capture, which belong to third parties who have nothing to do with your analysis, so -I file grants it nothing at all. See Security.

MCP server

Run sipnab as a Model Context Protocol server so an AI agent can drive it. See MCP Server for the full guide. Network Listeners lists the --mint-token / --token-id pair that issues MCP bearer tokens — it serves the REST API too.

FlagValueDefaultDescription
--mcpoffRun sipnab as an MCP server. Requires -N/--no-tui (stdout carries the JSON-RPC wire) — sipnab exits with an error without it — and rejects stdout-writing flags (--json, --report, …). Feature: mcp (or mcp-http for HTTP transport). See mcp.md.
--mcp-transportstdio|httpstdioMCP transport: stdio (default) or http (requires the mcp-http feature). Feature: mcp
--mcp-bind<ADDR>– (defaults to 127.0.0.1:8731 at runtime when --mcp-transport http appears without an explicit bind)HTTP MCP bind address. Non-loopback requires --mcp-token. Feature: mcp-http
--mcp-token<TOKEN>Bearer token for HTTP MCP; required for non-loopback binds. Precedence: explicit flag, token file, then $SIPNAB_MCP_TOKEN. Feature: mcp-http
--mcp-token-file<FILE>Read bearer token from file (preferred over env in systemd units). Feature: mcp-http
--mcp-signing-key<KEY>HMAC signing key for MCP bearer tokens, taken as raw bytes (any string — not hex-decoded). Repeatable: the first mints, verification accepts every one. Also reads $SIPNAB_MCP_SIGNING_KEY. See auth.md. Feature: mcp-http
--mcp-signing-key-file<FILE>Read an MCP signing key from a file (contents trimmed); it becomes the minting key. Feature: mcp-http
--mcp-revoked-file<FILE>MCP revocation denylist (one token id per line; reloaded on mtime change). Feature: mcp-http
--mcp-token-ttl<SECS>3600Default TTL (seconds) when minting MCP tokens with --mint-token. Feature: mcp-http
--mcp-audit-file<FILE>Append every MCP tool call to this file, one JSON record per line (seq, ts, tool, id, caller, outcome, elapsed_ms, args, error). The same facts already ride the normal log under the mcp_audit target, but that is a console view --quiet suppresses and SIPNAB_LOG filters; this is the durable copy for the question the record exists for — what did an agent look at in this capture — which somebody who did not choose the log level asks later. Opened O_APPEND and never truncated, so restarts and a second sipnab on the same path add to it; created mode 0600 when absent. The seq counter starts at 1 each run, so a gap within a run is a missing record. sipnab refuses a call whose record it cannot write, rather than answering it, so no result leaves the server that is not in the file; a path sipnab cannot open stops the run at startup. Records reach the kernel rather than an fsync. Leave it off and nothing changes. Feature: mcp
--mcp-max-concurrent<N>100Maximum tool calls the MCP server runs at once (0 = unlimited). sipnab refuses a call that cannot take a slot immediately, with a retry-shortly error, rather than queueing it — an unbounded backlog behind the cap is the exhaustion the cap prevents. The default mirrors --api-max-conn and bounds a flooding client without impeding an agent’s ordinary parallel calls. Applies to both stdio and HTTP servers, though a network-exposed HTTP server is the case it matters for. Feature: mcp
--mcp-toolscore|fullfullWhich tools the MCP server registers. Every registered tool’s name, description and JSON schema goes to the client on tools/list and then rides in the model’s context for the whole session — a fixed cost paid before the agent asks anything, and it scales with the surface rather than with the question. core registers eight that still answer a whole call end to end: capture_status, list_dialogs, get_dialog, triage_call, rtp_stats, find_problems, aggregate_dialogs, search_messages. full stays the default, because taking tools away at upgrade time would change what every existing client can do. sipnab removes the dropped tools from the router rather than hiding them, so a core server answers a call to one of them as an unknown tool. Feature: mcp
--one-way-delay<MS>One-way network path delay, in milliseconds — the single MOS input a passive tap cannot measure directly. Declared here it beats an RTCP-reported round trip, which an unauthenticated packet can move, and that in turn beats the round trip sipnab derives from a sender-report echo carried in a receiver report; with none of the three, sipnab assumes 100 ms and says so. Config: [media] one_way_delay_ms
--mcp-max-rows<N>1000Maximum rows in one list-style MCP response. The consumer decides the right value: a small-context agent wants fewer, a batch client wants more. Config: [limits] mcp_max_rows. Do not mistake this for -l/--limit, which bounds dialogs tracked over the run
--mcp-max-body-bytes<N>4096Maximum bytes of SIP body or matched snippet in one MCP response. --mcp-max-rows bounds how many rows an answer carries; this bounds how wide one row may be, and a caller can ask for fewer rows but cannot widen one. An SDP body with a dozen codecs and ICE candidates passes the default, and the agent reading the clipped half cannot tell a truncated answer from a short one. Config: [limits] mcp_max_body_bytes
--mcp-max-wait-seconds<N>60Longest ONE await_condition MCP call may wait, in seconds. --mcp-max-rows and --mcp-max-body-bytes bound how much an answer carries; this bounds how long a caller may hold one of --mcp-max-concurrent slots while carrying nothing. await_condition lets an agent wait for a condition on a live capture instead of re-asking tail_dialogs and paying a model turn per empty reply, and an unbounded wait is a held connection by another name. sipnab clamps a larger request to it and says so in the response. Config: [limits] mcp_max_wait_seconds
--mcp-max-findings<N>1000Findings the MCP save_findings tool accepts before refusing further writes. The one WRITE budget on that surface: --mcp-max-rows and --mcp-max-body-bytes bound what an agent may READ, this bounds what it puts into the operator’s journal. Past it sipnab refuses the write and says so, and drops nothing to make room – a finding is a log line the journal already holds, so sipnab keeps no copy a newer one could displace. Raise it for a long agent session on a large capture. Config: [limits] mcp_max_findings
--mcp-sampling-budget<PER_HOUR>offRequests per hour sipnab may ask the CLIENT’s model to narrate. Off unless set, because client support for the sampling primitive is thin and uneven and nothing here may depend on a narration arriving; a client that did not advertise the capability is never asked and gets structured evidence instead. Requests dedupe by finding signature before they spend from this budget, so a rule a scanner trips five hundred times costs one narration. 0 means NONE, not unlimited: elsewhere a zero limit removes a ceiling, but this one spends the operator’s money and the client’s rate limit, so zero reads restrictively. sipnab forwards named fields only – control characters stripped, length clamped, under a system prompt stating every value is untrusted observation – and never raw message text. Feature: mcp
--mcp-rate-limit-per-peer<N>100Maximum tool calls one peer may make per second (0 = unlimited). The other half of --mcp-max-concurrent: that caps calls in flight, this caps their arrival rate, and without it an agent that stays under the concurrency cap while looping as fast as sipnab answers has no bound at all. A call over the cap gets the same retry-shortly error, never a queue slot. A peer is the source IP over HTTP (the address, not the socket, so reconnecting mints no fresh allowance) and the pipe itself over stdio. Shares its per-peer accounting with --hep-rate-limit-per-peer. Feature: mcp
--mcp-allowed-host<HOST>Additional Host header values the HTTP MCP server accepts (repeatable). rmcp’s DNS-rebind protection defaults to localhost, 127.0.0.1, ::1 only — add the public hostname or bind IP when clients connect via that name. Use * to disable host checking entirely (not recommended; pair the resulting open binding with a network-level source-IP allowlist). Feature: mcp-http
--mcp-resource-url<URL>Public URL clients reach this MCP server at, published as the OAuth 2.0 protected-resource identifier (RFC 9728). Setting it turns on discovery: the 401 challenge gains a resource_metadata parameter and sipnab serves the metadata document unauthenticated at the path RFC 9728 section 3.1 derives from this value, so https://sipnab.example.com/mcp publishes at /.well-known/oauth-protected-resource/mcp. Leave it unset and the challenge still appears — only the metadata half is off. The operator names it rather than sipnab guessing: behind the TLS-terminating proxy this deployment expects, sipnab sees a cleartext request and no scheme, and anything derived from the socket or the Host header would name http:// for a resource the client reached over https:// — which RFC 9728 section 3.3 makes a conformant client discard. Discovery only: sipnab issues and validates no OAuth tokens, so the document advertises no authorization_servers, and the bearer tokens it accepts are still its own. Must be absolute, http or https, with no userinfo, query or fragment; a rejected value is a startup error. Feature: mcp-http
--mcp-file-root<DIR>Directory the MCP file tools (export_capture, export_audio, list_captures) may read and write. Without it those tools refuse to run. They take a bare FILENAME, never a path — an agent cannot escape this directory. Feature: mcp
--mcp-evidence-ring<MIB>Retain this many MiB of raw frames so a provenance pointer into a LIVE capture resolves. A reader can seek a capture file again and needs none of this; a device or a HEP listener offers no second read, because sipnab holds parsed messages and not frames. The ring keeps frames only from a source nothing can read twice. A pointer it cannot answer names which kind of miss it hit — the ring dropped that frame, the ring has not reached that ordinal, or the ring keeps nothing for that source — because only one of those means “use a bigger ring”. A frame it does answer carries the label retained rather than resolved: those bytes come from this process’s own buffer, and no second reader can confirm them. Feature: mcp
--mcp-allow-shutdownoffPermit the shutdown_server MCP tool to stop this process. Off by default, so an agent cannot stop a stock server. Even enabled, the tool dry-runs unless told otherwise and refuses to discard an unsaved live capture. Feature: mcp
--mcp-allow-open-captureoffPermit the open_capture MCP tool to load a different capture from --mcp-file-root, discarding every dialog and stream held. Off by default, so a stock server keeps the capture the command line named. The tool refuses while the source is live or still filling the stores, loads in the background, and mints a new capture identity every later answer carries. Feature: mcp
--mcp-allow-relay-queryoffPermit the query_relay MCP tool to ask the configured relay what calls it holds right now. Off by default because this tool transmits: every other MCP tool answers from bytes sipnab already has, and this one puts a packet on the network at the address --rtpengine-control names. That address comes from the flag and from nowhere else, never from a tool argument — an agent that could name the destination would turn the MCP surface into a way to send packets to a host of its own choosing. Without --rtpengine-control, or on a run reading a file, the tool refuses and names which of the two is missing. Feature: mcp
--mcp-allow-tls-captureoffLet an agent install kernel uprobes and read TLS plaintext (start_tls_capture, stop_tls_capture). The most consequential opt-in on this surface: it lets an agent read the plaintext of TLS sessions belonging to processes it does not own, needs the server to still be root, and creates kernel state that outlives a crash. list_tls_libraries stays available without it, so an agent can always report what a capture WOULD see. Feature: mcp
--mcp-allow-save-findingsoffPermit the save_findings MCP tool to record an agent’s conclusion. The only write verb on sipnab’s network surface, and off by default. A finding goes to sipnab’s log and nowhere else: no tool reads it back, it appears in no query result, and no analysis consumes it, so it cannot return as evidence in a later answer. Clipped at 500 characters of summary and bounded at 1000 findings per process, both reported rather than silent. Feature: mcp
--retain-audiooffRetain RTP audio payload in memory, so sipnab can look at the audio itself. Two things read it back: the export_audio MCP tool, which decodes a WAV, and the amplitude measurement in the media diagnosis, which reports dead-air spans and hard-clip runs on --call-report and --json-dialogs. Off by default: call audio is content, not signaling, and holding it is an operator decision rather than a side effect of running a capture. It used to require --mcp, back when the MCP server was the only reader; that would now keep the one finding that needs samples behind a server nobody analyzing a pcap wants to start. Costs a per-packet payload clone, bounded by [limits] max_audio_frames per stream across --max-streams streams. Without it export_audio refuses and names this flag, and the amplitude object is absent — both describe what the run kept, never whether the call carried audio. --cores honors this flag: the parallel workers used to discard payload regardless, so a run could accept the flag and keep nothing

TLS / decryption

FlagValueDefaultDescription
-k, --tls-key<FILE>RSA private key (PEM) for TLS 1.2 RSA-key-exchange decryption. Non-PFS RSA only; ECDHE/DHE handshakes need --keylog. Feature: tls
--keylog<FILE>TLS key log file (NSS SSLKEYLOGFILE format). Accepts a FIFO here and reads it as a live stream, so a producer can feed secrets in without writing them to disk. Feature: tls
--keylog-fd<N>Read NSS keylog lines from an already-open descriptor, for a privileged producer that hands secrets over a pipe rather than a file. Implies --keylog-watch, and conflicts with --keylog — pass one, never both. sipnab cannot start that producer itself: it sets PR_SET_NO_NEW_PRIVS at startup and every child inherits it, so a child can never acquire the CAP_BPF an eBPF extractor needs. Start it from a supervisor and pass the read end here. Feature: tls
--keylog-watchoffWatch the key log for new entries (live decryption). Feature: tls
--tls-lockon-window<RECORDS>1048576How far into an established TLS connection a capture may start and still be readable. No TLS version puts the record number on the wire, so a capture that joined a running connection searches for it, widening only as records fail to open — raising this costs nothing on a connection captured from its handshake, because the search stops at the first candidate that authenticates. Raise it for a carrier trunk held open for days; lower it where key material for unrelated connections is common. Feature: tls
--dtls-keylog<FILE>DTLS key log (NSS SSLKEYLOGFILE); extracts SRTP keys from DTLS-SRTP handshakes (RFC 5764 exporter, AES-CM profiles). sipnab does not treat a record declaring more than 2^14 + 2048 bytes as DTLS (MAX_RECORD_LEN, the ciphertext limit TLS sets and DTLS inherits). Feature: tls
--srtp-keys<FILE>SRTP master-keys file for media decryption (AES-CM, RFC 3711); also honors SDES a=crypto keys from SDP. Feature: tls
--pcap-export-mode<MODE>rawraw writes original packets without TLS keys. encrypted+dsb explicitly embeds TLS keys in PCAP-NG for Wireshark. decrypted is not supported and exits 2 before capture starts
--allow-coredumpoffAllow core dumps (do not call prctl to disable them)
--uprobe-tlsoffRead SIP plaintext straight out of the TLS libraries this host is running, using kernel uprobes. No certificate, no private key, no keylog and no restart of the process it observes. Probes every mapped TLS library rather than one, because an ordinary host runs OpenSSL and wolfSSL together. Needs root (or CAP_SYS_ADMIN + CAP_PERFMON) and a mounted tracefs. Linux only. Read the walkthrough before using this: it reads the plaintext of every SIP session on the host, and states what that means. Feature: native
--uprobe-library<PATH>discoveredProbe this library instead of discovering them; repeatable. Bypasses discovery, so it also reaches a library nothing has mapped yet. For a process inside a container, give the path as sipnab sees it: /proc/<pid>/root/usr/lib/libssl.so.3. Feature: native
--uprobe-symbol<NAME>per flavorWrite symbol to probe. Defaults to the one the library’s flavor exports — SSL_write for OpenSSL, wolfSSL_write for wolfSSL — so you need it only for a library sipnab cannot classify by name. Feature: native
--uprobe-flavor<NAME>allProbe only these flavors (openssl, wolfssl); repeatable. Feature: native. Also accepts --uprobe-flavour, the spelling the flag shipped with through 0.5.104: a released flag is a contract rather than a spelling choice
--uprobe-listoffList the TLS libraries sipnab would probe, then exit without installing anything in the kernel. Run this first: it answers the question that decides whether the capture is worth starting. Exits 1 when nothing is visible, so a health check does not read “no TLS library” as success. Feature: native
--uprobe-backend<NAME>tracefsWhich machinery reads the plaintext. tracefs works on any Linux with tracefs mounted and sees no socket, so its dialogs name a process rather than a peer. bpf pairs each write with its tcp_sendmsg and so recovers the real addresses — but needs a sipnab built with --features bpf and a kernel with CONFIG_DEBUG_INFO_BTF (BTF is the BPF Type Format, which tells sipnab where the socket’s fields sit on this kernel). sipnab refuses bpf without those rather than quietly downgrading: the addresses are the only reason to ask for it. Feature: native

Examples

  • sipnab -N -I capture.pcap --mcp --mcp-file-root /var/spool/sipnab-exports — let an agent save captures and audio, confined to one directory
  • sudo sipnab -N -d eth0 --mcp --mcp-file-root /var/spool/sipnab-exports --mcp-allow-shutdown — a live capture an agent may export from and, deliberately, stop
  • sipnab -N -I capture.pcap --mcp --mcp-sampling-budget 20 — let sipnab ask the connected client’s model to characterize an alert in a sentence, at most twenty times an hour. No API key in the config and no weights in the binary: the client already has a model, and this borrows it
  • sipnab -d eth0 -N --mcp --mcp-sampling-budget 0 — sampling explicitly refused rather than left unset, which is what to write when a policy forbids sending observations to a model at all. The difference from omitting the flag is that this states the decision
  • sipnab -N -I capture.pcap --mcp --mcp-allow-shutdown — a replay session an agent may end when it has finished; nothing to lose, since the file is already on disk
  • sipnab -N -I first.pcap --mcp --mcp-transport http --mcp-file-root /var/spool/sipnab-captures --mcp-allow-open-capture — a long-lived service an agent may move through a corpus with, one capture at a time
  • sudo sipnab -N -d eth0 --mcp --mcp-evidence-ring 256 — a live capture an agent can quote FROM. Without it sipnab refuses every pointer into eth0, honestly: it holds parsed messages and not frames, so nothing remains to seek to. 256 MiB buys a window; outside it the refusal reports that the ring dropped the frame rather than that the frame never existed
  • sipnab -N -I capture.pcap --mcp --mcp-evidence-ring 64 — harmless and pointless together, which is worth knowing: a reader can open a capture file again, so the ring keeps nothing and never spends the 64 MiB. It holds frames only from a source nothing can read twice
  • sipnab -N -I capture.pcap --mcp --mcp-file-root /var/spool/sipnab-captures --mcp-allow-open-capture --mcp-allow-shutdown — the same, plus the ability to end the session; both opt-ins are separate on purpose
  • sipnab -N -I capture.pcap --mcp --mcp-allow-save-findings — let an agent write its conclusions into the log while it works through a capture; read them back with journalctl -u sipnab, never through a tool
  • sudo sipnab -N -d eth0 --mcp --mcp-transport http --mcp-allow-save-findings — a live triage session whose findings survive in the journal after the agent disconnects, without granting it any other write
  • sipnab -N -I capture.pcap --mcp --mcp-file-root /var/spool/sipnab-exports --retain-audio — hold call audio in memory so an agent can export_audio a WAV of a problem call
  • sudo sipnab -N -d eth0 "portrange 5060-5061 or portrange 10000-20000" --mcp --retain-audio --mcp-file-root /var/spool/sipnab-exports — live capture with media in scope AND retained; without --retain-audio the same run measures quality but keeps no payload to export
  • sipnab -N -I dead-air.pcap --retain-audio --call-report <call-id> --json — no MCP server involved: retain the samples so the media diagnosis can measure them, and read diagnosis.amplitude for dead-air spans and clip runs with the thresholds that produced them
  • sudo sipnab -N -d eth0 --mcp --mcp-transport http --mcp-bind 127.0.0.1:8731 --mcp-max-concurrent 8 — a network-facing MCP server that runs at most eight tool calls at once and refuses the ninth with a retry-shortly error rather than queueing it
  • sipnab -N -I capture.pcap --mcp --mcp-max-concurrent 0 — a stdio replay for one trusted agent with no concurrency cap (0 = unlimited)
  • sipnab -N -I capture.pcap --mcp --mcp-tools core — a small-context agent: eight tools instead of the whole surface, so the schema block sipnab sends it before it asks anything is a fraction of the size
  • sudo sipnab -N -d eth0 --mcp --mcp-transport http --mcp-tools full — the default written out, for a service definition that should say what it offers rather than leave it to whatever the next release registers
  • sipnab -N -I capture.pcap --mcp --quiet --mcp-audit-file /var/log/sipnab-mcp.jsonl — record every tool call to a file that survives --quiet, for the question asked after the fact rather than at the console
  • sipnab -N -I sat-trunk.pcap --one-way-delay 280 — score MOS for a satellite trunk, where the real one-way delay is 280 ms; without it sipnab falls back to the trunk’s own RTCP and, on a capture carrying none, to an assumed 100 ms that reports roughly a full point too high, because G.107’s delay penalty has a knee at 177.3 ms the assumption never crosses
  • sipnab -N -I lan.pcap --one-way-delay 5 — a LAN capture, where assuming 100 ms understates the score; the declared figure also beats any round trip the far end reports or sipnab derives from RTCP, since no packet on the wire can rewrite a config value
  • sipnab -N -I capture.pcap --mcp --mcp-max-rows 50 — cap every list-style MCP response at fifty rows, for an agent whose context window a thousand-row page would swamp; a caller asking for more gets fifty
  • sipnab -N -I capture.pcap --mcp --mcp-max-rows 5000 — raise the ceiling above the 1000 default for a batch client that pipes whole pages to a file, where the round trips cost more than the bytes
  • sipnab -N -I capture.pcap --mcp --mcp-max-body-bytes 65536 — let an agent read a whole INVITE with a long SDP body rather than the first 4096 bytes of it, on a capture whose bodies are what the investigation is about
  • sipnab -N -I capture.pcap --mcp --mcp-max-rows 20 --mcp-max-body-bytes 512 — a small-context agent: few rows, and each one short, so a page of hits fits the window it has to reason in
  • sudo sipnab -N -d eth0 --mcp --mcp-max-wait-seconds 300 — let an agent sit on a live capture for up to five minutes per await_condition call, on a box watching for a fault that reproduces slowly
  • sudo sipnab -N -d eth0 --mcp --mcp-max-wait-seconds 5 — hold every wait to five seconds, for a host where MCP slots are scarce and a stalled agent must not occupy one
  • sudo sipnab -N -d eth0 --mcp --mcp-transport http --mcp-rate-limit-per-peer 20 — a network-facing MCP server where any one client may make twenty tool calls a second; sipnab answers the twenty-first that second with a retry-shortly error instead of serving it
  • sipnab -N -I capture.pcap --mcp --mcp-max-concurrent 8 --mcp-rate-limit-per-peer 0 — bound how many calls run at once but put no cap on the arrival rate (0 = unlimited), for a scripted client that sweeps a capture as fast as it can
  • sipnab -N -I capture.pcap --tls-key /etc/sipnab/tls-rsa.key --keylog /etc/sipnab/keys.log --allow-coredump — decrypt TLS 1.2 RSA-key-exchange SIP from a pcap using an RSA private key, with core dumps left enabled
  • sipnab -N -I capture.pcap --srtp-keys /etc/sipnab/srtp.keys --dtls-keylog /etc/sipnab/dtls.log — decrypt SRTP media in an offline pcap from an SRTP master-keys file plus DTLS-SRTP handshake keys
  • sudo sipnab -d eth0 --tls-key /etc/sipnab/tls-rsa.key --srtp-keys /etc/sipnab/srtp.keys --keylog /etc/sipnab/keys.log --keylog-watch --allow-coredump — live decrypt both SIP (RSA key) and SRTP media, watching the key log for new PFS session keys
  • sudo sh -c 'ecapture tls -m keylog --keylogfile=/dev/stdout | sipnab -N -d eth0 --keylog-fd 0' — read SIP over TLS with no certificate and no restart of the SIP daemon: an eBPF extractor pulls session secrets out of the running daemon’s OpenSSL and pipes them straight in, so nothing is ever written to disk. sipnab cannot launch the extractor itself, because every child inherits PR_SET_NO_NEW_PRIVS and so can never acquire CAP_BPF
  • sudo sipnab -N -d eth0 --keylog-fd 3 --user sipnab 3< /run/sip.keys — take the secrets on descriptor 3 opened by the shell, then drop to an unprivileged user for the capture itself; the pipe is already open, so nothing has to be reachable from /run afterwards
  • sudo mkfifo -m 600 /run/sip.keys && sudo sipnab -N -d eth0 --keylog /run/sip.keys --keylog-watch — the same idea through a named pipe rather than a descriptor, for a producer started separately by systemd; sipnab opens the FIFO while still privileged, before it drops to an unprivileged user and can no longer reach /run
  • sudo sipnab -N -d eth0 --keylog /run/sip.keys --keylog-watch --tls-lockon-window 8388608 — read a carrier trunk that has been up for days without restarting it. The record counter is not on the wire, so a capture joining an established connection has to find it; the default reaches about a day of a trunk at ten records a second, and this raises the ceiling to roughly a week
  • sipnab -N -I trunk.pcap --keylog keys.log --tls-lockon-window 4096 — narrow the search when reading a capture whose key log holds secrets for many unrelated connections, so a session that can never match gives up sooner instead of spending the run’s trial budget
  • sudo sipnab --uprobe-listrun this first. Report which TLS libraries processes on this host are actually mapping, and exit without installing a single probe. Answers the only question that matters before starting: is the daemon you care about using a library sipnab can read?
  • sudo sipnab --uprobe-list --uprobe-flavor wolfssl — the same listing narrowed to one flavor, so the output says what this command would probe rather than what merely exists
  • sudo sipnab -N --uprobe-tls — read SIP over TLS with no certificate, no key and no restart of the SIP daemon. sipnab probes every mapped TLS library, so one command covers a host running OpenSSL for one daemon and wolfSSL for another
  • sudo sipnab -N --uprobe-tls --uprobe-flavor openssl — probe only the OpenSSL side on a mixed host, when the wolfSSL processes are something else entirely and their plaintext is not yours to read
  • sudo sipnab -N --uprobe-tls --uprobe-library /usr/lib/x86_64-linux-gnu/libssl.so.3 — skip discovery and probe one named library, which is how you attach to a daemon that has not started yet: discovery can only see what is already mapped
  • sudo sipnab -N --uprobe-tls --uprobe-library /proc/$(docker inspect -f '{{.State.Pid}}' opensips)/root/usr/lib/libssl.so.3 — probe the OpenSSL inside a container. The path a container process sees names a different file from sipnab’s namespace, so the probe must go through /proc/<pid>/root or it silently attaches to the host’s copy and captures nothing
  • sudo sipnab -N --uprobe-tls --uprobe-library /opt/vendor/libtls-custom.so --uprobe-symbol vendor_write — probe a library sipnab cannot classify by name; without --uprobe-symbol it refuses rather than guessing which function to attach to
  • sudo sipnab -N --uprobe-tls --uprobe-library /usr/lib/libssl.so.3 --uprobe-symbol SSL_write_ex — probe OpenSSL 3’s newer write entry point instead of the default SSL_write, for a daemon built against it; the argument positions match, so the probe shape does not change
  • sudo sipnab -N --uprobe-tls --json — the same capture as JSON. Dialogs from this source carry uprobe:<comm>/<pid> as their interface and unspecified addresses with port 0, because a uprobe sees the bytes an application handed its TLS library and nothing about the socket beneath; sipnab names the process rather than inventing a peer
  • sudo sipnab -N --uprobe-tls --uprobe-backend bpf --portrange 0-65535 — read SIP over TLS with the peer addresses. Verified live: a REGISTER and its 200 OK came back as 127.0.0.1:36160 -> 127.0.0.1:15061 and the reverse, each connection carrying its own ephemeral port. Widen --portrange, because a TLS trunk on 5061 is the exception and the port a uprobe reports is whatever the socket used
  • sudo sipnab -N --uprobe-tls --uprobe-backend tracefs — the default, spelled out. Use it on a kernel without BTF, where the bpf backend cannot run at all; dialogs then name the process instead of a peer
  • sudo sipnab -N -I done.pcap --mcp --mcp-allow-tls-capture — let an agent decide, mid-investigation, that the answer is in traffic it cannot see, and start reading TLS plaintext itself. Finish the file source first: sipnab’s stores have one writer, and a uprobe capture is a live one
  • sudo sipnab -N -I done.pcap --mcp --mcp-allow-tls-capture --mcp-allow-save-findings — the same, plus somewhere for the agent to record what it concluded; without --mcp-allow-tls-capture the agent can still call list_tls_libraries and report what a capture would have seen

Privilege

FlagValueDefaultDescription
--user<USER>Drop privileges to this user after opening capture devices. Only takes effect when the process is root — see the note below
--no-priv-dropoffDo not drop privileges after opening capture devices
--chroot<DIR>Chroot to this directory after initialization
--setup-capsoffGrant this binary the Linux capabilities for live capture (cap_net_raw,cap_net_admin+ep via setcap) so it runs without sudo, then exit. Re-invokes through sudo when not already root. Linux only.

--user does nothing unless the process is root. sipnab skips the drop when geteuid() != 0 and says so only at debug — so under the recommended install (--setup-caps, then run without sudo) clap parses the flag, the log line goes where nobody watches, and the drop never arms. That is the intended behavior: an unprivileged process has nothing to drop. This note exists because the flag’s presence otherwise reads as a guarantee.

The platforms also differ. On Linux, root plus --user drops to that user. On macOS, sipnab skips the drop even as root unless you pass --user: dropping to nobody strands the process without a per-user login session, which crashes CoreAudio the moment anything touches it.

Examples

  • sudo sipnab -d eth0 --user sipnab — live capture that drops root to the sipnab service user once the capture device is open
  • sudo sipnab -d eth0 --user nobody --chroot /var/empty — long-running monitor that drops to nobody and confines itself to an empty chroot
  • sudo sipnab -d eth0 --chroot /var/empty --no-priv-drop — chrooted capture that keeps root privileges for the whole run
  • sudo sipnab --setup-caps — grant the binary the capture capabilities (cap_net_raw,cap_net_admin) so future runs work without sudo, then exit

Resource limits

FlagValueDefaultDescription
--max-reassembly<N>10000Maximum concurrent TCP/TLS reassembly sessions
--reassembly-ttl<SECS>30Seconds sipnab holds an incomplete IP datagram or half-read TCP stream before a sweep drops it. --max-reassembly bounds how MANY entries sipnab holds and says nothing about how long. Thirty seconds describes IP fragments in flight, and the TCP reassembler inherited it: a persistent SIP/TCP or SIP/TLS trunk to a carrier goes quiet for far longer on any ordinary night, and sweeping its half-read stream means the next segment re-initializes mid-message, so the peer that sent a valid message is the one reported broken. Raise it on such a trunk; --max-reassembly caps the extra state either way. Config: [limits] reassembly_ttl_secs
--lint-max-per-rule<N>25Findings one lint rule may report for one dialog. A dialog that retransmits an INVITE eleven times trips a message rule eleven times and every one of them is true, so this decides whether the other rules stay readable underneath. Needs --lint. Config: [limits] lint_max_per_rule
--lint-suppress-file<FILE>discoveredRead lint suppressions from this file instead of discovering one. Without it the binary looks for a .sipnablint beside the capture and climbs toward the project root — the nearest ancestor holding a .git — which is exactly what the MCP lint tools already do. Until this shipped the two surfaces disagreed about the same file on disk: the MCP tools honored a .sipnablint checked in beside a capture while the binary silently ignored it, so the CI user was on the side that could not see it. A named file sipnab cannot open fails hard rather than falling back to a full-catalog run. sipnab names whatever applies on stderr, with a count of what it silenced. Needs --lint
--lint-no-suppressoffIgnore any .sipnablint, including one named by --lint-suppress-file. The “show me everything, including what we have agreed to live with” switch; it wins over both the explicit file and discovery, so a wrapper script that always passes a suppression file can still be overridden from the command line. Needs --lint
--cores<N>1CPU cores for offline pcap reconstruction (-I). 1 = single-threaded; >1 shards by host pair for multi-core throughput (dialog+RTP reconstruction, --report/--json). At 2 or more cores sipnab reads a plain uncompressed .pcap by mapping it, which is where most of the multi-core gain comes from; pcapng, gzip, a non-regular file, or any run with a BPF filter reads through libpcap instead, exactly as before. Set SIPNAB_NO_MMAP=1 to force the libpcap path everywhere — an escape hatch for a filesystem where mapping misbehaves, such as some network or FUSE mounts. Results are identical either way. With --retain-audio the payload cap applies PER WORKER, so the run’s ceiling is [limits] max_audio_frames times this number times --max-streams
--max-metadata-file-bytes<BYTES>2147483648Bytes of pcapng sipnab reads into memory for embedded names and TLS secrets. A tcpdump -C or dumpcap -b ring member passes 2 GiB on a host with the RAM to spare, and the refusal is fatal. A memory-exhaustion guard on untrusted input: raising it to N lets ONE file claim N bytes of this host’s RAM, roughly 2N while --strip-secrets writes its copy, on nothing but a file size and before sipnab can tell the file is a capture at all. Raise it for captures you produced. Config: [limits] max_metadata_file_bytes
--max-gunzip-bytes<BYTES>1073741824Bytes a gzip-compressed capture may inflate to where sipnab does the inflating: the embedded names and TLS secrets it reads out of a .pcapng.gz, the copy --strip-secrets rewrites, and the whole capture in the browser build. libpcap inflates the packet stream of a -I capture.pcap.gz run, and this does not bound that. The documented alternative — gunzip the file and open the plain one — costs the disk the compression was saving. A gzip-bomb guard: inflation stops one byte past the ceiling, so raising it to N lets a few kilobytes of input claim N bytes of RAM. Raise it for archives you compressed yourself. Config: [limits] max_gunzip_bytes
--max-tcp-buffer<BYTES>65536Bytes one SIP/TCP direction may buffer before sipnab flushes it. The only limit here that destroys data rather than truncating a report. TCP sets no such ceiling and neither does RFC 3261: on a carrier trunk a message carrying ISUP encapsulation, a long Record-Route set or a fat SDP offer passes 64 KiB legitimately, and sipnab then flushes the buffer mid-message — both halves parse as malformed, the cut destroys the framing for every message behind it, and the peer that sent a valid message is the one sipnab reports as broken. Raising it to N lets one TCP direction hold N bytes. The floor is one SIP header line (8192), below which no message survives, and sipnab refuses a smaller value by name. Config: [limits] max_tcp_buffer

Examples

  • sudo sipnab -d eth0 --max-reassembly 50000 — live capture on a busy TCP/TLS trunk with a raised reassembly-session ceiling
  • sipnab -N -I capture.pcap --cores 4 --max-reassembly 2000 — offline reconstruction sharded across 4 cores, with a tight reassembly bound for an untrusted capture
  • sipnab -N -I calls.pcap --lint --lint-max-per-rule 3 --no-cli-print — keep the lint output to three repeats of any one rule, so a capture full of retransmissions still shows what else it trips
  • sipnab -N -I calls.pcap --lint --lint-max-per-rule 500 --no-cli-print — the opposite: every repeat, for counting how often one rule actually fires
  • sipnab -N -I calls.pcap --lint --lint-fail-on error --no-cli-print — honors the .sipnablint sitting beside calls.pcap, and names it on stderr with a count of what it silenced. A short finding list always says why it is short
  • sipnab -N -I calls.pcap --lint --lint-suppress-file ci.sipnablint --no-cli-print — a suppression list of the pipeline’s own, for a CI job whose tolerances differ from the tree’s
  • sipnab -N -I calls.pcap --lint --lint-no-suppress --no-cli-print — the full catalog, ignoring every suppression file, for auditing what the project has agreed to live with
  • sipnab -N -I ring-00042.pcapng --max-metadata-file-bytes 8589934592 --report — read the embedded names and TLS secrets out of an 8 GiB ring member you captured yourself, which the shipped ceiling refuses outright
  • sipnab -N -I ring-00042.pcapng --max-metadata-file-bytes 8589934592 --strip-secrets sanitized.pcapng — the same file, sanitized for handover; the copy costs roughly twice the ceiling in memory, so raise it only for a file you trust
  • sipnab -N -I archive.pcapng.gz --max-gunzip-bytes 8589934592 --strip-secrets sanitized.pcapng — sanitize an 8 GiB pcapng you compressed yourself, without spending the disk that decompressing it by hand would need
  • sipnab -N -I from-customer.pcapng.gz --max-gunzip-bytes 268435456 --report — the opposite, for a file that arrived from outside: a quarter-gigabyte ceiling on the embedded names and secrets sipnab reads out of it, so a gzip bomb wearing a capture’s name cannot take the box down
  • sipnab -N -I isup-trunk.pcap --max-tcp-buffer 1048576 --json-dialogs --no-cli-print — a carrier trunk whose SIP/TCP messages carry encapsulated ISUP bodies past 64 KiB: at the shipped ceiling sipnab cuts each one in half and reports it malformed, and at 1 MiB the same bytes produce the call
  • sudo sipnab -d eth0 --max-tcp-buffer 262144 — watch a live SBC that answers with long Record-Route sets, holding a quarter-megabyte per TCP direction so a large response frames whole instead of arriving as two malformed fragments
  • sudo sipnab -N -d eth0 --reassembly-ttl 600 --max-reassembly 50000 — a persistent SIP/TLS trunk to a carrier that goes quiet overnight: ten minutes of patience keeps the half-read stream, so the first segment of the morning continues a message rather than landing in the middle of one
  • sipnab -N -I untrusted.pcap --reassembly-ttl 5 --max-reassembly 2000 — the opposite, for a capture from outside: a five-second wait drops a half-sent stream almost at once, so a peer that opens streams and never finishes them cannot hold state
  • sipnab -N -I calls.pcap --mcp --mcp-allow-save-findings --mcp-max-findings 20000 — a long agent session over a large capture, where a thousand annotations is a session doing its job and the journal on this box can take twenty
  • sipnab -N -I calls.pcap --mcp --mcp-allow-save-findings --mcp-max-findings 25 — a scripted agent run on a box with a small journal: twenty-five annotations, then sipnab refuses further writes and says so rather than filling the disk

Config

FlagValueDefaultDescription
-f, --config<FILE>Path to configuration file (must exist)
-F, --no-configoffSkip loading any configuration file
-D, --dump-configoffDump effective configuration and exit
--completions<SHELL>Print a shell completion script (bash, zsh, fish, elvish, powershell) to stdout and exit

Examples

  • sipnab --config /etc/sipnab/sipnab.toml --dump-config — dump the effective configuration produced by a specific config file, then exit
  • sipnab --no-config --dump-config — dump the built-in defaults, skipping any configuration file, then exit
  • sudo sipnab -d eth0 --config ~/.config/sipnab/config.toml — live capture using a per-user configuration file
  • sipnab -N -I capture.pcap --no-config — analyze an offline pcap with all configuration files ignored
  • sipnab --completions bash > sipnab.bash — print a bash completion script into a file suitable for /etc/bash_completion.d
  • sipnab --completions zsh > _sipnab — print a zsh completion script into a file suitable for the zsh fpath

Validation rules

  • Output flags (--json, --json-pretty, --report, --hexdump, --fail2ban) require -N / --no-tui mode, unless --call-report is also specified.
  • --kill-response accepts values 100-699 only.
  • Feature-gated flags (tls, hep, api, mcp, mcp-http) produce startup errors when the required feature is not compiled in.
  • --mcp is incompatible with stdout-writing flags (--json, --json-pretty, --report, --call-report, --hexdump, --wireshark, --tshark-filter) on every transport, not just stdio — sipnab refuses to start. Combine --mcp with --quiet to suppress text-mode capture output.
  • HTTP MCP transport (--mcp --mcp-transport http) on a non-loopback --mcp-bind requires --mcp-token / --mcp-token-file / SIPNAB_MCP_TOKEN; loopback binds need no token.

Examples

  • sipnab -d eth0 — capture on eth0
  • sipnab -I capture.pcap — read from pcap file
  • sipnab -N --json -I capture.pcap — non-interactive JSON output
  • sipnab --problems — show problematic calls
  • sipnab --kill-scanner -d eth0 — detect SIP scanners
  • sipnab --from alice --to bob — filter by From/To headers
  • sipnab 'host 192.0.2.1 and port 5060' — BPF display filter
  • sipnab --filter "method == 'INVITE' AND rtp.mos < 3.0" — advanced filter DSL
  • sipnab -N -I capture.pcap --call-report "abc123@host" --markdown --no-cli-print — generate detailed report for a call (drop --no-cli-print and the whole capture’s message dump precedes it)
  • sipnab -d eth0 -H 192.0.2.50:9060 — capture with HEP mirror
  • sipnab -d eth0 --keylog /tmp/sslkeys.log --keylog-watch — live TLS decryption

Exit codes

Scripts can rely on these:

CodeMeaning
0Success
1Runtime failure — capture error, I/O error, a capture file that was not read in full (a truncated pcap, a member that would not open, a BPF filter that would not compile against one), a --plugin that would not load, or sipnab could not produce a requested report (e.g. --call-report Call-ID not found)
2Invalid usage — bad flag value or combination, or a flag whose feature is not compiled into this binary
3Lint gate tripped — --lint --lint-fail-on <severity> found a conformance finding at or above that severity. Distinct from 1 on purpose: the tool worked, the CAPTURE is non-conformant

A run that read its input in full and stopped where you asked it to stop — -l/--limit, --duration — still exits 0. The distinction is whether sipnab failed to do what you asked, not whether it read everything there was.

When the answer is partial

1 says something went wrong. It cannot say what, and a consumer reading --json-dialogs on a pipe never sees it at all. So a run that could not deliver the whole answer also says so in its output:

  • --json-dialogs emits one extra NDJSON line, after the dialogs, under a top-level sipnab_run key that no dialog object has. A clean run emits nothing extra, so runs with nothing wrong with them look exactly as they do today.
  • --report ends with an INCOMPLETE RUN block naming each reason.
{"sipnab_run":{"input_complete":false,"reasons":["1 of 1 capture file(s) was not read to the end; every report from this run rests on a partial read"],"files":{"given":1,"read_in_full":0,"stopped_early":1,"skipped":0,"not_reached":0},"plugins":{"requested":0,"loaded":0,"failed":0},"retention":{"messages_dropped":0}}}

input_complete is the same predicate as the exit status, so a script reading stdout and a script reading $? cannot reach different verdicts. retention.messages_dropped counts captured messages that idle compaction discarded ([limits] idle_compact_after_secs) — it appears because those ladders are short, and it is deliberately NOT a failure, because a retention policy doing what you configured it to do is not a run that went wrong.