Docs Config Reference

Config Reference

TOML configuration file format and all configurable sections.

On this page

sipnab reads configuration from a TOML file. CLI flags (cli-reference.md) always override config file values.

Configuration is optional: with no file present sipnab runs on its built-in defaults. A config file exists to set persistent defaults for your environment.

Minimal Config

If you only need to override a few defaults, keep it short:

# ~/.config/sipnab/sipnab.toml
[capture]
device = "eth0"

[display]
delta_time = true

[theme]
background = "#1e1e2e"
foreground = "#cdd6f4"

File Locations

sipnab reads configuration from the first file it finds in this order:

PrioritySource
1--config <FILE> (must exist; errors if missing)
2$SIPNAB_CONFIG environment variable
3~/.config/sipnab/sipnab.toml
4~/.sipnabrc
5/etc/sipnab/sipnab.toml

Use --no-config (-F) to skip all file loading. Use --dump-config (-D) to print which file sipnab loaded and the keys it set — see the note under Full example for what -D does and does not show.

Unknown keys produce a warning and go no further, so one config can span versions.

Environment variables in values

Any string value may contain ${NAME}. sipnab substitutes that environment variable’s value when it reads the file:

[crash]
report_dir = "/home/${SUDO_USER}/sipnab"

The case this exists for is sudo. Capture needs privileges, so sipnab is often run under sudo, and after sudo -i a file written by that run lands in root’s directory rather than in the directory of whoever ran the command. ${SUDO_USER} is the only thing that can say which one that is at the moment sipnab reads the file.

Four rules, and they hold everywhere a string does:

  • ${NAME} expands. NAME is an ASCII letter or _ followed by letters, digits or _. sipnab rejects shell’s ${NAME:-default} and ${NAME-other} outright rather than half-supporting them.
  • An unset variable is an error. sipnab names the setting and the variable and does not start. Expanding to nothing would turn /home/${SUDO_USER}/sipnab into /home//sipnab, which is a real, writable directory that is not the one you meant.
  • $$ is a literal $. Write $${NAME} for a literal ${NAME}.
  • A bare $NAME is literal text. Braces are the whole syntax.

What a variable expands to is data. It is never scanned again, so a value that itself contains ${...} stays as it is.

--dump-config prints values after expansion, which is what sipnab is actually using.

Format

Standard TOML. All sections and keys are optional. Only set values you want to change from defaults.

Sections

[capture]

Packet capture defaults.

KeyTypeDefaultDescription
devicestringDefault network interface
node_namestringhostnameName this box reports as, in capture_identity.node on every MCP and REST answer. Lets an agent querying several servers tell WHICH one saw a given fact. --node-name overrides it, so a deployed config can name the box while a one-off command relabels it. The default puts the hostname on the wire. Clipped to 64 characters
portrangestring"5060-5061"SIP signaling port range; media is never gated by it. sipnab skips any SIP message with both ports outside the range, and a skipped message reaches no count, no dialog and no output — so this key decides how much of a capture you analyze at all. Widen it ("1-65535") unless you know every port in play. --portrange overrides it
ws_portsstring"80, 443, 8080, 8443"Ports 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 — on such a capture the entire 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. --ws-portrange overrides it
snapleninteger65535Snapshot length in bytes
bufferinteger64Kernel capture buffer size in MiB (per device)
buffer_budget_mbinteger64Memory budget for the in-flight capture→processing queue. Grows under load up to this budget (capped, never OOM) and shrinks when idle. --buffer-budget overrides it
no_rtpbooleanfalseDisable RTP capture by default
promiscbooleantruePut a named interface into promiscuous mode (the any device is never promiscuous). --no-promisc overrides this to false
[capture]
device = "eth0"
portrange = "5060-5080"
ws_ports = "8080-8090"
snaplen = 65535
buffer = 16
buffer_budget_mb = 64
no_rtp = false
promisc = true

[display]

Output and TUI display settings.

KeyTypeDefaultDescription
colorstring"auto"Color mode: "auto", "always", "never"
payload_limitintegerMaximum payload bytes to display
delta_timebooleanfalseShow delta time between messages by default
from_tostring"default"From/To column display: "default" (user else host:port), "host-port", "user", "user-host-port". Cycle at runtime with u; --from-to-mode overrides this
visible_columnsarray of stringsall columnsCall-list columns to show, by name (case-insensitive): "#", "Method", "From", "To", "Source", "Destination", "State", "Msgs", "Date", "PDD", "Duration". Adjust at runtime with F10; s in the column selector writes the layout back to your sipnabrc, so it persists across sessions
[display]
color = "always"
payload_limit = 4096
delta_time = true
from_to = "user-host-port"
visible_columns = ["method", "from", "to", "source", "destination", "state", "msgs", "pdd"]

[filter]

Default filter presets applied at startup.

KeyTypeDefaultDescription
fromstringDefault From header filter (regex)
tostringDefault To header filter (regex)
expressionstringDefault filter DSL expression
[filter]
from = "^1001@"
to = "^1002@"
expression = "method == 'INVITE'"

[sip]

SIP protocol handling.

KeyTypeDefaultDescription
xcid_headersarray["X-Call-ID"]Header names used to correlate B2BUA call legs (sip.xcid). A dialog whose message carries one of these headers pointing at another dialog’s Call-ID joins that dialog. Add carrier-specific headers here; an empty/unset list keeps the X-Call-ID default
leg_correlation_window_msinteger2000How far apart, in milliseconds, one call’s two legs may start and still correlate on TIMING alone. This is the B2BUA timing heuristic’s whole content, and the only strategy left once a B2BUA has rewritten every identifier the other six 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. Widen it on such a hop; every correlation still reports the strategy that matched, so a guess stays labeled as one. --leg-correlation-window overrides it
active_idle_window_secsinteger3600Seconds 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 – so raise it for traffic that genuinely goes quiet. --active-idle-window overrides it. 0 fails validation and names the key
[sip]
xcid_headers = ["X-Call-ID", "X-CID"]
leg_correlation_window_ms = 8000
active_idle_window_secs = 7200

[security]

Security detection defaults.

KeyTypeDefaultDescription
kill_scannerbooleanfalseEnable scanner detection
kill_responseinteger200SIP response code for scanner reports (100-699)
fraud_detectbooleanfalseEnable fraud detection heuristics
alertarray of strings[]Alert channels: "syslog", "json", "exec"
alert_execstringCommand to execute on alert
reg_flood_thresholdinteger50Challenged failures per second from one source before --reg-flood reports a flood: REGISTERs that carried credentials and drew a 401 or 407 on the same transaction. 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. --reg-flood-threshold overrides it. 0 fails validation and names the key
kill_rate_limitinteger10Scanner-kill responses per second sipnab may put on the wire. This bounds the one feature that answers an address out of the capture, and whoever forged the source address chose where each response goes, so there is no unlimited setting and 0 fails validation. 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. --kill-rate-limit overrides it
business_hoursstringBusiness hours as "START-END" in whole UTC hours, for example "8-18". A wrapping range such as "22-6" is the 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. --business-hours overrides it
fraud_short_call_secsinteger3Measured call 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, which reports ordinary unanswered calls as lures. --fraud-short-call overrides it
fraud_wangiri_callsinteger3Short calls to one destination prefix before --fraud-detect reports wangiri. --fraud-wangiri-calls overrides it
fraud_destinationstringDestination countries --fraud-detect reports an INVITE to, as comma-separated 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. --fraud-destination overrides it
fraud_sequential_callsinteger3Consecutive refused numbers before --fraud-detect reports sequential scanning. --fraud-sequential-calls overrides it
fraud_volume_multiplierinteger5Multiple of a source’s own baseline call rate that --fraud-detect reports as a volume spike. --fraud-volume-multiplier overrides it
fraud_volume_min_callsinteger6Calls a source must place inside the volume window before --fraud-detect reports a spike at all. --fraud-volume-min-calls overrides it
fraud_volume_window_secsinteger60How much capture time one volume-spike window spans, in seconds. The count and the source’s own baseline are both measured over this window, so a steady source reads the same at any width; what the width alone decides is how CONCENTRATED a burst has to be, since a burst shorter than the window averages into the ordinary traffic beside it. --fraud-volume-window overrides it
fraud_wangiri_window_secsinteger60How much capture time one wangiri window spans, in seconds. 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: the only count that reports anything is one, which reports every ordinary short call as a lure too. --fraud-wangiri-window overrides it
scanner_behavioral_probesinteger10Probes 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. --scanner-behavioral-probes overrides it
scanner_enumeration_targetsinteger5Distinct target extensions from one source inside the scanner window, above which --kill-scanner reports extension enumeration. --scanner-enumeration-targets overrides it
scanner_rejected_probesinteger5Rejected 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. --scanner-rejected-probes overrides it
scanner_unanswered_probesinteger5Probes 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. --scanner-unanswered-probes overrides it
scanner_window_secsinteger5How much capture time one scanner window spans, in seconds. 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. --scanner-window overrides it
scanner_established_factorinteger4How 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. --scanner-established-factor overrides it
scanner_answer_grace_msinteger500How long a probe may go without a response before --kill-scanner counts it as unanswered, in milliseconds. 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. --scanner-answer-grace overrides it
findings_historyinteger1000Security findings kept in memory for later retrieval. 0 keeps none, which is a real setting rather than a mistake. --findings-history overrides it
hep_hmac_window_secsinteger30Seconds 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 it is how far back the receiver’s nonce cache must remember. Range 1-300. Past 300 the sender has no working time daemon, which is what to repair, so sipnab refuses the value and names the key. --hep-hmac-window overrides it

HEP HMAC token version 2, and what a mixed fleet sees. sipnab 0.5.131 moved HMAC_TOKEN_VERSION from 1 to 2 and refuses v1 by name. Two things follow. A sender still on 0.5.130 or earlier against a newer collector has every packet refused, and the symptom is a collector receiving nothing – which operators reliably attribute to routing, a firewall, or a dead agent long before they suspect a token version. Upgrade the collector last, or upgrade both together.

The reason it is not a compatible change is the second thing: v1’s MAC did not cover the addressing chunks, so an attacker who captured a v1 token from one sender could replay it against a third party, and could point a published --hep-allow-kill control somewhere nobody issued it for. v2 covers them. There is no flag to accept v1 again, deliberately.

Every scanner_* key above rejects 0 and names the key. A zero count reports the first probe of any kind as a scanner, a zero window resets the counters on every packet so nothing ever accumulates, and a zero grace restores the very defect scanner_answer_grace_ms exists to prevent.

[security]
kill_scanner = true
kill_response = 403
kill_rate_limit = 10
fraud_detect = true
business_hours = "8-18"
fraud_short_call_secs = 2
fraud_wangiri_window_secs = 900
reg_flood_threshold = 10
scanner_window_secs = 60
scanner_behavioral_probes = 40
scanner_enumeration_targets = 12
findings_history = 5000
alert = ["syslog", "json"]
alert_exec = "/usr/local/bin/sipnab-alert.sh"

[tfps]

The peer this needs. These read TFPS through its tfps_ctl program in JSON mode, the --json flag. TFPS gained that mode in sippulse/tfps#6, merged on 2026-09-18, and no tagged release carries it yet: v0.2.1, the newest, rejects --json. Until the next release, build TFPS from its master branch. To check the tfps_ctl you have, run tfps_ctl status --json. One line of JSON means it is ready, and unknown option: --json means it predates the mode. Against an older tfps_ctl these answer with that error and name what to install.

The kernel-drop question (GET /v1/tfps/dropped, the tfps_dropped MCP tool) is the exception: it needs a dropped subcommand that no TFPS build has, released or on master.

Where the toll-fraud prevention system (TFPS) is, when one runs on this host.

TFPS is optional peer software: it condemns sources and enforces that decision in the firewall, and sipnab never bans anything. The tfps_* MCP tools and the /v1/tfps/ REST routes ask it through its tfps_ctl program, and this section says where that program and its database are. Leave the section out and sipnab looks for tfps_ctl on PATH the moment a TFPS tool runs, and does nothing about TFPS at any other time.

KeyTypeDefaultDescription
ctlstringPath to tfps_ctl. --tfps-ctl overrides it. Absent: sipnab looks on PATH the moment a TFPS tool runs, and a machine with no tfps_ctl anywhere answers installed: false on every TFPS surface
dbstringThe TFPS database, passed to every tfps_ctl call as --db=<path>. Absent: tfps_ctl uses its own default
[tfps]
ctl = "/usr/local/bin/tfps_ctl"
db = "/var/lib/tfps/tfps.db"

[diagnosis]

Thresholds the signaling and media checks compare against. A number here decides whether a call that is working gets reported as broken, so the defaults are standards figures and a network that knows its own numbers beats a recommendation written for the general case. Every value must be a finite number greater than zero, and a value that is not fails validation and names the key.

KeyTypeDefaultDescription
post_dial_delay_secsfloat11.0Post-dial delay over which sipnab reports a call as slow, in seconds. 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. --pdd-threshold overrides it
ack_timeout_secsfloat32.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. --ack-timeout overrides it
no_final_response_secsfloat180.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. --no-final-response-timeout overrides it
duration_asymmetry_pctfloat5.0Percentage difference between the two legs’ durations that counts as asymmetric. Must be 100 or less. --duration-asymmetry-pct overrides it
duration_asymmetry_secsfloat2.0Absolute difference between the two legs’ durations that counts as asymmetric, in seconds. A call has to clear both this and the percentage, so raising either one alone quiets the detection. --duration-asymmetry-secs overrides it
late_media_msinteger500Milliseconds after the 200 OK that media may start before it gets reported as late. --late-media-ms overrides it
cn_suppression_ratiofloat0.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 — the most-reported VoIP fault there is. Raise it toward 1 on such a trunk; lower it where a call carrying any comfort noise at all still has to be bidirectional. Must be greater than 0 and 1 or less, and a value that is not fails validation and names the key. --cn-suppression-ratio overrides it
[diagnosis]
post_dial_delay_secs = 6.0
ack_timeout_secs = 32.0
no_final_response_secs = 180.0
duration_asymmetry_pct = 5.0
duration_asymmetry_secs = 2.0
late_media_ms = 500
cn_suppression_ratio = 0.3

[media]

Properties of the observed media path that a passive tap cannot measure for itself.

KeyTypeDefaultDescription
one_way_delay_msfloatOne-way network path delay in milliseconds, feeding the delay term of every MOS. The single MOS input no observer can measure from the wire directly: only the endpoints and you have it. A declared value beats an RTCP-reported round trip, because no packet can rewrite a config file; that beats the round trip sipnab derives from a sender-report echo carried in a receiver report, which anchors on the capture point and so reads as a lower bound; with none of the three, sipnab assumes 100 ms and labels the figure assumed rather than presenting it as measured
codec_ietableEquipment impairment factors (ITU-T G.107 Ie) for codecs sipnab has no published value for, written as a [media.codec_ie] sub-table of "CODEC" = <Ie> pairs. sipnab knows G.711, G.729 and Opus; every other codec – G.722, G.726, iLBC, AMR, EVS – falls to a placeholder and scores identically to a stream whose codec was never identified. A declared codec comes back as mos_grounding = "operator_declared" rather than as published, so a figure from this file is never presented as an ITU-T citation, and a codec nobody declared still says its MOS is a placeholder. Keys match case-insensitively. Values must sit in 0.0 to just under 95.0: at 95 the E-model’s loss term vanishes, and above it more packet loss would RAISE the score, so sipnab fails validation on such a value and names the codec
[media]
one_way_delay_ms = 45.0

# Impairment factors for codecs sipnab has no published value for.
[media.codec_ie]
G722 = 12.0
iLBC = 11.0

[quality]

Where the quality color column turns yellow, and where it turns red. A number here decides only what catches an operator’s eye during triage, which is a different question from [diagnosis]: that one decides whether a call that is working counts as broken. The defaults 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.

Unset keys keep the shipped default, so a file may move one boundary without restating the other seven. Every value must be a finite number of zero or more, and each warn boundary must leave a reachable middle against its matching bad boundary. A set that does not fails validation and names the key. Zero itself counts as a real setting: loss_warn_pct = 0.0 means any loss at all is worth a color.

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.

KeyTypeDefaultDescription
jitter_warn_msfloat30.0Jitter at or above which the column turns yellow, in milliseconds. --jitter-warn-ms overrides it
jitter_bad_msfloat50.0Jitter at or above which the column turns red, in milliseconds. --jitter-bad-ms overrides it
loss_warn_pctfloat1.0Loss at or above which the column turns yellow, in percent. --loss-warn-pct overrides it
loss_bad_pctfloat5.0Loss at or above which the column turns red, in percent. --loss-bad-pct overrides it
mos_warnfloat4.0MOS below which the column turns yellow. MOS bands run downward, so this must sit at or above mos_bad. --mos-warn overrides it
mos_badfloat3.0MOS below which the column turns red. --mos-bad overrides it
rtt_warn_msfloat300.0Round trip at or above which the column turns yellow, in milliseconds. The default is ITU-T G.114’s 150 ms one-way guidance doubled. --rtt-warn-ms overrides it
rtt_bad_msfloat800.0Round trip at or above which the column turns red, in milliseconds. The default is G.114’s 400 ms one-way figure doubled. --rtt-bad-ms overrides it
[quality]
jitter_warn_ms = 10.0
jitter_bad_ms = 20.0
loss_warn_pct = 0.5
loss_bad_pct = 2.0
mos_warn = 4.2
mos_bad = 3.5
rtt_warn_ms = 120.0
rtt_bad_ms = 300.0

[limits]

Resource limits to prevent unbounded memory growth.

KeyTypeDefaultDescription
dialog_limitinteger100000Maximum tracked dialogs
mcp_max_rowsinteger1000Maximum rows in ONE list-style MCP response. Distinct from dialog_limit above, which bounds the whole run; these differ by 100x and bound different things. 0 fails validation and names the key
max_streamsinteger50000Maximum RTP streams
max_reassemblyinteger10000Maximum TCP reassembly sessions
reassembly_ttl_secsinteger30Seconds 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. --reassembly-ttl overrides it. 0 fails validation and names the key
hep_rate_limitinteger50000Maximum HEP packets per second
max_header_lineinteger8192Maximum bytes in a single SIP header (defense-in-depth)
max_headers_per_messageinteger200Maximum SIP headers per message (defense-in-depth)
max_messages_per_dialoginteger500Maximum stored messages per dialog (defense-in-depth)
idle_compact_after_secsinteger600Seconds of silence before sipnab compacts a dialog’s stored messages. 0 fails validation and names the key
keep_messages_per_idle_dialoginteger20Messages an idle dialog keeps after compaction
max_audio_framesinteger1500Maximum RTP payload frames stored per stream for WAV export (~30s at G.711 50pps)
lint_max_per_ruleinteger25Findings 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. --lint-max-per-rule overrides it. 0 fails validation and names the key
exec_queue_depthinteger100Hook 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. --exec-queue-depth overrides it. 0 fails validation and names the key
mcp_max_body_bytesinteger4096Bytes 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. --mcp-max-body-bytes overrides it. 0 fails validation and names the key
mcp_max_wait_secondsinteger60Longest ONE await_condition MCP call may wait, in seconds. mcp_max_rows and mcp_max_body_bytes bound what an answer carries; this bounds how long a caller may hold one of --mcp-max-concurrent slots while carrying nothing. sipnab clamps a larger request to it and says so in the response. --mcp-max-wait-seconds overrides it. 0 fails validation and names the key
mcp_max_findingsinteger1000Findings 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, where a thousand annotations is a session doing its job. --mcp-max-findings overrides it. 0 fails validation and names the key
max_lost_sequencesinteger1000Lost RTP sequence numbers retained per stream. This is the window the Packet Loss Map draws and the burst/gap analysis reasons over, so a 30-minute call losing 1 % shows only its last minute at the default. The burst/gap window widens with it. --max-lost-sequences overrides it. 0 fails validation and names the key
quality_interval_secsinteger5Seconds between RTP quality snapshots, which is the resolution of the per-stream quality trend on rtp_stats, GET /v1/streams/{id}, the TUI and the browser analyzer. The shipped five seconds averages away any burst shorter than itself. The trend still covers an hour of call time whatever this is, so a one-second setting retains 3600 snapshots per stream rather than 720 — resolution costs memory, never history. --quality-interval overrides it. Anything outside 1-300 fails validation and names the key
max_groupsinteger100000Distinct --group-by keys one run retains, the same figure dialog_limit ships so a grouped run cannot outgrow an ordinary capture. Past it sipnab refuses new keys and warns that the output is incomplete. --max-groups overrides it
max_grouped_messagesinteger200000Messages --group-by buffers across every group. Grouping cannot stream — the last packet may belong to the first group — so this is memory held until the capture ends. --max-grouped-messages overrides it
max_metadata_file_bytesinteger2147483648Bytes of pcapng sipnab reads into memory for embedded names and TLS secrets. 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, before sipnab can tell the file is a capture at all. Raise it for captures you produced; leave it for captures someone sent you. --max-metadata-file-bytes overrides it
max_gunzip_bytesinteger1073741824Bytes a gzip-compressed capture may inflate to where sipnab does the inflating: the embedded names and TLS secrets read 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 it. A gzip-bomb guard. Inflation stops one byte past the ceiling, so raising it to N lets a few kilobytes claim N bytes of RAM. Raise it for archives you compressed yourself. --max-gunzip-bytes overrides it
max_tcp_bufferinteger65536Bytes 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 so both halves parse as malformed — the peer that sent a valid message is the one reported broken. Raise it on such a trunk. The floor is one SIP header line (8192); below that no message survives, and sipnab refuses the value by name. --max-tcp-buffer overrides it
api_max_rowsinteger1000Rows 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. --api-max-rows overrides it. 0 fails validation and names the key
api_rate_limit_per_peerinteger100REST requests one client IP may make per second. 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 and see 503 (503 rather than 429 because the limiter runs before authentication, so the refusal says nothing about the credential). 0 disables the cap, the reading hep_rate_limit and mcp_rate_limit_per_peer also give it. --api-rate-limit-per-peer overrides it
metrics_max_conninteger16Metrics 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, so raise it where several collectors share one sipnab. --metrics-max-conn overrides it. 0 fails validation and names the key: the gate would then refuse every scrape
max_tracked_peersinteger4096Distinct peers one rate-limit window holds, across every surface sipnab meters: HEP source addresses and MCP callers. Past it sipnab REFUSES a peer it has not already seen this second rather than waving it through, so on a collector aggregating from more agents than this the surplus never enters the capture. Raise it there. The floor is 2, and sipnab refuses a smaller value by name: at 1 the first peer to send in a window takes the only slot and sipnab turns every other peer away for the rest of it
[limits]
dialog_limit = 50000
max_streams = 25000
max_reassembly = 5000
hep_rate_limit = 25000
max_header_line = 8192
max_headers_per_message = 200
max_messages_per_dialog = 500
idle_compact_after_secs = 600
keep_messages_per_idle_dialog = 20
max_audio_frames = 1500

The compaction pair — idle_compact_after_secs and keep_messages_per_idle_dialog — is where a limit discards a ladder sipnab already holds, so a call that went quiet shows fewer messages than crossed the wire. sipnab warns once per run when this first happens. The retention logs (max_audio_frames, max_lost_sequences) drop their oldest entries as they fill. Every other key here refuses to take something in.

Raise both when the ladder matters more than the footprint — a call parked on hold, a dialog waiting on a slow PSTN leg, or a capture you paused all go quiet for longer than ten minutes while still being the thing under investigation:

[limits]
idle_compact_after_secs = 3600
keep_messages_per_idle_dialog = 500

[privilege]

Privilege separation settings (Linux only).

KeyTypeDefaultDescription
userstring"nobody"User to drop privileges to after opening capture devices
no_priv_dropbooleanfalseDisable privilege dropping
chrootstringChroot directory after initialization
[privilege]
user = "sipnab"
no_priv_drop = false
chroot = "/var/lib/sipnab"

[names]

Address name-resolution settings (display host:port instead of ip:port).

KeyTypeDefaultDescription
enabledbooleanfalseStart with name resolution on (offline sources)
reverse_dnsbooleanfalseAlso use reverse DNS (PTR) lookups
hosts_filestring/etc/hosts-format file of IP → name mappings to preload
persist_to_configbooleanfalseWhen set, in-TUI N edits are also written into the [names.manual] table below, preserving the rest of this file
dns_cache_entriesinteger4096Reverse-DNS results (positive and negative) held at once (default MAX_DNS_CACHE_ENTRIES). 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. --dns-cache-entries overrides it
manualtableInline "IP" = "name" mappings, loaded at startup (highest-priority manual layer)
[names]
enabled = true
reverse_dns = false
hosts_file = "/etc/sipnab/hosts"
persist_to_config = true

# Inline mappings (also written here when persist_to_config = true):
[names.manual]
"192.0.2.1" = "sbc-edge"
"2001:db8::1" = "core6"

[crash]

What happens when sipnab panics: the panic hook restores the terminal (release builds abort without unwinding, so raw mode / mouse capture would otherwise stay on), writes a crash report, and then either exits cleanly or aborts so the OS can produce a core dump.

KeyTypeDefaultDescription
reportsbooleantrueWrite a crash-report file on panic (message, location, thread, version, backtrace)
backtracebooleantrueCapture a full backtrace in the report (independent of RUST_BACKTRACE)
report_dirstring~/.local/state/sipnabDirectory crash reports (sipnab-crash-<timestamp>-<pid>.log) land in
corebooleanfalsetrue: abort after the report so the kernel can dump core (subject to ulimit -c / core_pattern); false: exit cleanly with status 101, suppressing the core
[crash]
reports = true
backtrace = true
report_dir = "/var/log/sipnab"
core = false

[theme]

TUI color theme with 11 semantic color slots (plus highlight, a legacy alias for selected). Each field accepts a color name or a hex RGB value. Unset fields use built-in defaults. See theme-guide.md for the full customization guide and its preset themes.

KeyTypeDefaultDescription
backgroundstring"reset" (terminal default)Terminal background
foregroundstring"white"Default text color
highlightstringLegacy alias for selected (backward compat)
headerstring"cyan"Status bar, column headers, endpoint labels
selectedstring"yellow"Selected/highlighted row, cursor, focused item
accentstring"magenta"Correlation info, PDD, extended flow labels
goodstring"green"Positive quality, success states (InCall, Registered)
warningstring"yellow"Medium quality, caution states (Ringing, CANCEL)
badstring"red"Poor quality, failures, errors
mutedstring"dark_gray"Separators, pipes, disabled text, timestamps
borderstring"white"Widget borders, panel frames
status_bgstring"#303040"Status bar background band, kept distinct from the terminal background so the status line stays visible

Supported color values:

  • Named: black, white, red, green, yellow, blue, magenta, cyan, gray, dark_gray, reset
  • Hex RGB: "#RRGGBB" (e.g., "#ff8800")
[theme]
background = "#1a1a2e"
foreground = "#e0e0e0"
header = "cyan"
selected = "#e94560"
accent = "magenta"
good = "green"
warning = "yellow"
bad = "red"
muted = "dark_gray"
border = "#444466"

[keybindings]

TUI key binding overrides. The 11 configurable actions appear below. Unset fields use built-in defaults.

Accepted key formats:

  • Single characters: "q", "/", "A"
  • Function keys: "F1" through "F12"
  • Special names: "Esc", "Space", "Enter", "Tab", "Backspace"
KeyTypeDefaultDescription
quitstring"q"Quit the application
helpstring"F1"Show help overlay
filterstring"F7"Open filter dialog
savestring"F2"Open save capture dialog
searchstring"/"Activate search
settingsstring"F8"Open settings popup
pausestring"p"Pause/resume capture
autoscrollstring"A"Toggle autoscroll
extended_flowstring"F4"Toggle extended multi-leg flow
clear_callsstring"F5"Clear all calls
column_selectorstring"F10"Open column selector

See keybindings.md for the full shortcut reference, including the keys that are not remappable.

[keybindings]
quit = "q"
help = "F1"
filter = "F7"
save = "F2"
search = "/"
settings = "F8"
pause = "p"
autoscroll = "A"
extended_flow = "F4"
clear_calls = "F5"
column_selector = "F10"

Full example

A configuration for a SIP monitoring server:

# /etc/sipnab/sipnab.toml
# Production SIP monitoring configuration

# -- Packet capture --
[capture]
device = "eth0"                    # Primary SIP-facing interface
portrange = "5060-5080"            # Cover SIP, SIP-TLS, and alternate ports
snaplen = 65535                    # Full packet capture (no truncation)
buffer = 32                        # 32 MiB kernel buffer for burst tolerance
buffer_budget_mb = 128             # Cap on the in-flight capture->processing queue
no_rtp = false                     # RTP analysis enabled

# -- Display settings --
[display]
color = "always"                   # Force color even when piped
payload_limit = 8192               # Show up to 8K of SIP body (large SDP)
delta_time = true                  # Show timing between messages by default
from_to = "host-port"              # From/To columns show host:port
# visible_columns = ["method", "from", "to", "state", "msgs", "pdd"]  # Persistent column prefs

# -- Default filter (optional) --
[filter]
from = "^1001@"
to = "^1002@"
expression = "method == 'INVITE' OR method == 'REGISTER'"

# -- Security detection --
[security]
kill_scanner = true                # Detect SIP scanners (sipvicious, etc.)
kill_response = 403                # Reply to scanners with 403
fraud_detect = true                # Heuristic fraud detection
alert = ["syslog", "json"]        # Send alerts to syslog and JSON log
alert_exec = "/usr/local/bin/sipnab-alert.sh"  # Custom alert handler
reg_flood_threshold = 10           # Refused credentialed REGISTERs/sec that is a flood
kill_rate_limit = 10               # Kill responses/sec sipnab may transmit
business_hours = "8-18"            # Enables off-hours fraud detection (UTC hours)
scanner_window_secs = 60           # Wide enough to hold a sweep paced at one probe/10s
scanner_behavioral_probes = 40     # Raised: this site aggregates behind one SBC address
fraud_wangiri_window_secs = 900    # A lure paced over fifteen minutes is still one lure

# -- Diagnosis thresholds --
[diagnosis]
post_dial_delay_secs = 8.0         # Toll-call target; the default 11.0 is international
ack_timeout_secs = 32.0            # RFC 3261 Timer H
late_media_ms = 500                # Media allowed to start this late after the 200 OK

# -- Resource limits --
[limits]
dialog_limit = 50000               # Max tracked dialogs (tune for RAM)
max_streams = 25000                # Max RTP streams
max_reassembly = 5000              # Max TCP reassembly sessions
hep_rate_limit = 25000             # Max HEP packets/sec
lint_max_per_rule = 25             # Repeats of one lint finding per dialog
exec_queue_depth = 20              # Hook commands allowed to run at once

# -- Privilege separation (Linux) --
[privilege]
user = "sipnab"                    # Drop to unprivileged user after device open
no_priv_drop = false               # Keep privilege dropping enabled
chroot = "/var/lib/sipnab"         # Chroot after initialization

# -- Address naming --
[names]
enabled = true                     # Resolve addresses to names at startup
hosts_file = "/etc/sipnab/hosts"   # Preloaded IP -> name mappings

[names.manual]
"192.0.2.1" = "sbc-edge"

# -- Crash handling --
[crash]
reports = true                     # Write a crash report on panic
backtrace = true                   # Include a full backtrace
core = false                       # Exit 101 rather than dumping core

# -- Theme: Catppuccin Mocha --
[theme]
background = "#1e1e2e"
foreground = "#cdd6f4"
header = "#89b4fa"
selected = "#f9e2af"
accent = "#cba6f7"
good = "#a6e3a1"
warning = "#fab387"
bad = "#f38ba8"
muted = "#585b70"
border = "#6c7086"

# -- Keybindings (defaults shown) --
[keybindings]
quit = "q"
help = "F1"
filter = "F7"
save = "F2"
search = "/"
settings = "F8"
pause = "p"
autoscroll = "A"
extended_flow = "F4"
clear_calls = "F5"
column_selector = "F10"

Tip: Use sipnab --dump-config to see which file sipnab actually loaded and what that file set. It prints the path it came from, then every section header with the keys that file supplied under each.

Read the omissions carefully, because -D shows less than “effective configuration” suggests:

  • Built-in defaults do not appear. A key you did not set prints nothing, not its default. sipnab -F --dump-config therefore prints a list of empty section headers, which is correct output and not a fault. The defaults are the ones in the tables on this page.
  • CLI flags do not appear. They arrive later in startup, so -D cannot show what a flag would override. Compare against the CLI reference for that.
  • There is no environment-variable override layer. SIPNAB_CONFIG only selects which file to read.

So -D answers “did sipnab read the file I meant, and did it accept my keys?” — which is the question behind most configuration surprises. It does not answer “what value is this setting running with?”.