Docs Filter calls and streams
Filter calls and streams
Declarative filter language for matching SIP dialogs and RTP streams.
On this page
Quick start:
sipnab -N -I capture.pcap --filter "state == 'Failed'"shows the failed calls in a capture.sipnab -N -I capture.pcap --problemssweeps for every kind of problem at once.- Keep the
-N. Without it, the TUI opens instead.
A filter needs no build feature, no privilege beyond what the capture itself
needs, and no change on any other system. The same expression works on the
command line, in the terminal interface (through a named filter), over REST
and over MCP, where the
validate_filter tool checks one
without running it.
sipnab includes a declarative, non-Turing-complete filter language. It matches SIP dialogs and their associated RTP streams.
- You pass expressions through either:
- the
--filterCLI flag, or - the
expressionkey in the[filter]config section.
- the
- The Diagnostic Aliases CLI flags (
--problems,--slow-setup, and so on) expand to the named aliases documented below.
What a filter narrows
A filter selects dialogs, and every output that lists dialogs honors it:
- The per-message stream (the default output,
--json,-T) emits only messages belonging to matching dialogs. --json-dialogsemits one line per matching dialog.--reportprints only matching dialogs.- The RTP tables beneath the dialog table hold only the streams linked to them.
- With no filter, it still lists every stream the capture holds, orphans included.
--call-report <CALL-ID>is not narrowed. It names one call, and a lookup by name is not a listing.
The --since/--until time window
joins the filter.
- The run’s filter is
(<your filter>) AND time >= '<since>' AND time < '<until>'. --json-analyzereports it in that form.
Examples
Each entry below is one complete expression. --filter takes exactly one.
These are catalogs to pick a line from. They are not blocks to copy whole.
Basic field matching
method == INVITEfrom.user == 1001state == 'InCall'
A value needs no quotes unless it contains whitespace, a quote character or an
unmatched ), or begins with =, !, <, > or @. method == INVITE,
method == 'INVITE' and method == "INVITE" are the same filter. See
Values.
Regex matching
ua =~ 'friendly-scanner'from.user =~ '^100[0-9]'call_id =~ 'abc.*@'
Numeric comparisons
pdd > 3.0rtp.mos < 3.0rtp.loss > 2.0duration < 5.0retransmits > 3rtp.jitter > 50.0rtp.packets > 10000
Matching boolean fields
one_way == truenat_mismatch == trueno_media == truecodec_asymmetry == truelate_media == true
Compound expressions
method == 'INVITE' AND rtp.mos < 3.0from.user =~ '^1001' AND state == 'Failed'pdd > 3.0 OR retransmits > 5NOT ua =~ 'friendly-scanner'(state == 'Failed' OR state == 'Canceled') AND duration < 1.0
Real-world diagnostic queries
The DSL has no comment syntax, so this page labels each query in prose. A #
line handed to --filter is a parse error, not a note.
- Find calls with poor quality from a specific extension:
from.user =~ '^1001' AND rtp.mos < 3.0 - Find failed registrations from a subnet:
method == 'REGISTER' AND state == 'Failed' AND src.ip =~ '^198\.51\.100\.' - Find short calls that completed (possible robocalls):
duration < 5.0 AND state == 'Completed' AND method == 'INVITE' - Find calls with audio issues:
one_way == true OR no_media == true OR rtp.jitter > 100.0 - Find scanner activity by User-Agent:
ua =~ 'sipvicious|friendly-scanner|sipcli'
Note: The filter DSL evaluates against dialogs, not individual messages. A filter like
method == 'INVITE'matches dialogs that opened with an INVITE. That includes all subsequent messages in that dialog (180, 200, ACK, BYE, etc.).
Operational Recipes
Filter-DSL recipes, one per real-world task. Each one is a complete command line.
- Swap
-I capture.pcapfor-d eth0(as root) to run any of them against live traffic. - For broader task recipes beyond the DSL, see the Cookbook and Troubleshooting.
Poor audio quality (low MOS)
sipnab -N -I capture.pcap --filter "rtp.mos < 3.0 AND rtp.packets > 0 AND state == 'Completed'" --json
An unmeasured rtp.mos is unknown, and an unknown value matches no threshold
in either direction. A dialog that carried no RTP never matches
rtp.mos < 3.0.
- The
rtp.packets > 0clause is optional. Raise its number, for examplertp.packets > 500, to leave out calls that carried only a few packets. - To select the calls that carry no media, write
rtp.packets == 0orno_media == trueinstead.
state == 'Completed' keeps only completed calls. In-progress calls may not
have enough RTP data for an accurate MOS calculation.
MOS values follow the ITU-T G.107 E-model:
- 4.0+ is toll quality.
- 3.5-4.0 is acceptable.
- Below 3.0 is noticeable degradation.
One-way audio
sipnab -N -I capture.pcap --filter "one_way == true AND duration > 10.0" --report
The duration check avoids false positives during early call setup, when RTP hasn’t started flowing yet.
For calls where no RTP ever flowed at all, use no_media == true instead.
NAT issues
sipnab -N -I capture.pcap --filter "nat_mismatch == true AND method == 'INVITE'" --json
NAT mismatch means RTP reached the capture point from an address that no SDP in the dialog advertised. That is what a NAT rewriting the media source looks like from the wire.
- It is a common cause of one-way audio, and of call setup failures behind NAT.
- Combine it with the
one_wayfield to catch the classic case.
High jitter or packet loss
sipnab -N -I capture.pcap --filter "rtp.jitter > 50.0 OR rtp.loss > 1.0" --json
- Jitter arrives in milliseconds (RFC 3550 interarrival jitter algorithm). High values indicate network congestion.
- Loss is a percentage (0.0-100.0). Its acceptable thresholds are codec-dependent.
Failed international calls
sipnab -N -I capture.pcap --filter "from.user =~ '^\+' AND (state == 'Failed' OR state == 'Canceled')" --json
The ^\+ regex matches E.164 formatted numbers (international prefix).
Registration storms
sipnab -N -I capture.pcap --filter "method == 'REGISTER' AND retransmits > 5" --report
High retransmit counts on REGISTER indicate:
- network issues,
- DNS failures,
- server overload.
Append AND src.ip == '192.0.2.50' to isolate a specific endpoint.
Scanner activity
sipnab -N -I capture.pcap --filter "ua =~ 'sipvicious|friendly-scanner|sipcli'" --json
Short completed calls (possible robocalls)
sipnab -N -I capture.pcap --filter "duration < 5.0 AND state == 'Completed' AND method == 'INVITE'" --json
SIP trunk failures
sipnab -N -I capture.pcap --filter "dst.ip == '198.51.100.100' AND state == 'Failed' AND method == 'INVITE'" --report
Filter for failures targeting a specific SIP trunk IP.
Orphaned RTP streams
sipnab -N -I capture.pcap --report
Orphaned streams have no matching SIP dialog or SDP.
- No dialog filter can select them, and the DSL offers no field for them.
- The boolean-field note above covers why
rtp.orphanedis a parse error.
Where to find them:
- The
--reportoutput carries an “Orphaned Streams” section. - The REST
API answers the same question at
/v1/streams?orphaned=truewhen you run sipnab with--api.
Orphans usually mean one of these:
- RTP arriving on unexpected ports (check your NAT/ALG config),
- calls that started before capture began.
Track one user’s packet loss (B2BUA debugging)
sipnab -N -I capture.pcap --filter "(from.user == '1001' OR to.user == '1001') AND rtp.loss > 0.5" --report
Tracks a specific user’s calls that have packet loss, regardless of call direction.
Chatty dialogs (debugging retransmissions)
sipnab -N -I capture.pcap --filter "msg_count > 20 AND method == 'INVITE'" --json
Dialogs with many messages often indicate:
- retransmission issues,
- complex call flows (transfers, re-INVITEs).
Stream investigation by codec or SSRC
Select every dialog carrying one codec, for codec-specific quality analysis:
sipnab -N -I capture.pcap --filter "rtp.codec == 'PCMU'" --json
Trace a single media stream by its SSRC.
rtp.ssrccompares against the SSRC rendered as0x-prefixed lowercase hex.- So the literal needs the
0xprefix, or it matches nothing:
sipnab -N -I capture.pcap --filter "rtp.ssrc == '0x12345678'" --json
RTCP extended reports
When a capture carries RTCP XR (PT=207), sipnab decodes the VoIP Metrics block (RFC 3611 Section 4.7). It keeps the block beside the stream the block names. The block holds:
- Round-trip delay and end-system delay
- Signal level, noise level and residual echo return loss
- R-factor and external R-factor
- MOS-LQ and MOS-CQ
- Burst and gap loss densities and durations
- Jitter buffer nominal, maximum and absolute maximum delay
Every figure above belongs to the endpoint that sent it, not to sipnab.
- The TUI Stream Detail view shows them in a Reported by far end (RTCP XR) section of their own, below everything sipnab measured.
- One figure feeds sipnab’s MOS: the round-trip delay. Halved, it is the
one-way delay behind the MOS when the run declares no
--one-way-delay. See mos-and-codecs.md. - Nothing else there feeds the MOS, jitter or loss sipnab computes.
- If the two disagree, that is the finding. It is not a conflict to resolve.
The reasons:
- RTCP carries no authentication.
- A mid-path capture watches a different path segment than the endpoint reports on.
- This is the same rule the reception-report figures follow. See mos-and-codecs.md.
RFC 3611 reserves the value 127 for “this parameter is unavailable”. It does this on all seven of its single-byte quality fields:
- the R-factor and the external R-factor,
- both MOS fields,
- the signal and noise levels,
- the residual echo return loss.
sipnab renders each of those as n/a. A raw render would put:
- an R-factor of 127 on a scale that stops at 100,
- a MOS of 12.7 on a scale that stops at 5.0.
No surface reads any other XR block type yet.
- The parser turns three of them into typed values: Loss RLE, Duplicate RLE and Receiver Reference Time.
- It records Packet Receipt Times, DLRR and Statistics Summary by block-type number alone.
--json, --report, the REST API and the Prometheus exporter carry no XR
field. Their MOS can rest on the XR round-trip delay, as above.
No filter DSL field reads an XR value directly.
Grammar
expr = or_expr
or_expr = and_expr ("OR" and_expr)*
and_expr = not_expr ("AND" not_expr)*
not_expr = "NOT" atom | atom
atom = comparison | "(" expr ")"
comparison = field operator value
Operator precedence (highest to lowest): NOT, AND, OR.
Use parentheses to override.
Fields
All 42 addressable fields, organized by type.
- They answer to 43 names.
response_codeis also spelledfinal_status_code. An alias is not a second field. - Beyond these,
header.<name>reads any SIP header by a name you choose. See Any header by name.
String fields
| Field | Description | Example Values |
|---|---|---|
from.user | User part of the SIP From header | "1001", "alice" |
to.user | User part of the SIP To header | "1002", "bob" |
from.host | Host of the From URI, without the port | "carrier-a.example", "203.0.113.5" |
to.host | Host of the To URI, without the port | "pbx.example" |
from.uri | The From URI as written, URI parameters included, without the display name or tag | "sip:+15551230000@carrier-a.example;user=phone" |
to.uri | The To URI as written | "sip:7000@pbx.example" |
ruri | Request-URI of the request that opened the dialog; empty when the capture began on a response | "sip:7000@192.0.2.10" |
pai.user | User part of the opening request’s P-Asserted-Identity; empty when it has none | "+15559876543" |
side | Where the opening request went relative to the PBX addresses in [pbx] addresses: inbound (sent to the PBX), outbound (sent by it), internal (between two of its addresses), or empty | "inbound", "outbound" |
endpoint | The name the run gives the source or destination address, from the hosts file, --names files and [names.manual]; matches when either end’s name matches. Reverse DNS is never used here | "pbx-1", "carrier-a-gw" |
method | SIP request method | "INVITE", "REGISTER", "BYE" |
ua | User-Agent header (first non-empty across dialog messages) | "Olle", "friendly-scanner" |
call_id | SIP Call-ID header | "abc123@host" |
payload | Raw message content — matches when ANY message in the dialog contains/matches the value (payload grep) | "X-Custom-Header", "sipsak" |
src.ip | Source IP address (first message) | "192.0.2.1" |
dst.ip | Destination IP address (first message) | "192.0.2.2" |
state | Dialog state machine value | "Trying", "InCall", "Failed" |
response_class | IANA class of the final response | "2xx", "4xx", "5xx", "6xx" |
rtp.codec | RTP codec name (matches if ANY linked stream matches) | "PCMU", "opus" |
rtp.codecs | Every codec the dialog’s streams used, distinct, sorted and comma-joined; empty with no streams | "PCMU,telephone-event" |
rtp.ssrc | RTP SSRC in hex format (matches if ANY linked stream matches) | "0x12345678" |
Valid state values: Trying, Ringing, InCall, Completed, Canceled, Failed, Redirected, Registered, Expired, Pending, Active, Terminated, Transferring
payloadvs-e/--match:
- The
payloadfield matches per dialog (true if any message matches).- The
-e/--matchflag is the per-message match-expression.
- It selects the matching messages and then follows the dialog.
- sipnab emits every message after the first match in that dialog too.
- Use
-efor grep-style streaming output.- Use
payloadinside a larger--filterexpression.
Numeric fields
| Field | Description | Unit |
|---|---|---|
src.port | Source port (first message) | port number |
dst.port | Destination port (first message) | port number |
duration | Dialog duration | seconds (float) |
msg_count | Number of SIP messages in dialog | count |
response_code | Final response code of the dialog’s INVITE, or of the request that opened a dialog without one, such as REGISTER, PUBLISH or SUBSCRIBE. Also spelled final_status_code, which is the name the JSON, the schema and the MCP answers return. Unknown while the call is in progress, so a ringing dialog matches no comparison at all | 100-699 |
pdd | Post-dial delay: from the INVITE to the first 180 or 183 response | seconds (float) |
setup_time | Call setup time (INVITE to 200 OK) | seconds (float) |
retransmits | Total retransmit count in dialog | count |
rtp.mos | Mean Opinion Score (worst across streams, E-model R-factor approximation) | 1.0 - 5.0, unknown with no RTP |
rtp.jitter | Jitter (worst/highest across streams) | milliseconds, unknown with no RTP |
rtp.loss | Packet loss (worst/highest across streams) | percentage (0-100), unknown with no RTP |
rtp.packets | Total RTP packets (sum across all streams) | count (0 is a real count, never unknown) |
An unmeasured value matches no comparison.
pdd,setup_time,rtp.mos,rtp.jitterandrtp.lossare unknown when the capture holds nothing to measure them from:
- no RTP for the media fields,
- no captured INVITE or 200 OK for the timing ones.
- An unknown matches no numeric operator, including
!=. SQL treatsNULLthe same way.- A saved filter carrying an
AND rtp.packets > 0clause still works. The clause no longer changes which signaling-only dialogs match.
Find the calls that carried no media
A threshold on an rtp.* field never selects a call without RTP. Ask for
those calls directly. On a capture that holds only signaling, the threshold
matches nothing:
sipnab -N -I tests/pcap-samples/sip-auth-failure.pcapng --filter "rtp.mos < 3.0" --report --no-cli-print 2>/dev/null
2>/dev/null leaves out the log, so the output is the report alone:
Call-ID From To State Code Duration Msgs PDD Tags
-------------------------------------------------------------------------------------------------------------------------
rtp.packets == 0 is a real count of zero, so it selects every dialog in the
same capture:
sipnab -N -I tests/pcap-samples/sip-auth-failure.pcapng --filter "rtp.packets == 0" --report --no-cli-print 2>/dev/null
Call-ID From To State Code Duration Msgs PDD Tags
-------------------------------------------------------------------------------------------------------------------------
auth-fail-register-synth@20... alice alice Failed 403 0s 4 - -
auth-fail-subscribe-synth@2... alice alice Terminated 403 0s 4 - -
no_media == true is narrower: it selects answered calls whose negotiated
media never arrived. See Boolean fields.
Asking by class
The IANA registry
groups every response code into six classes. response_class is that
class. Ask by number or by the registry’s own name. They mean the same thing:
| Class | Number | Name |
|---|---|---|
| Provisional | 1xx | provisional |
| Successful | 2xx | successful, success |
| Redirection | 3xx | redirection, redirect |
| Request Failure | 4xx | request failure, client failure |
| Server Failure | 5xx | server failure |
| Global Failures | 6xx | global failures, global failure |
sipnab -N -I capture.pcap --filter "response_class == 'server failure'"
failure means all three failure classes: 4xx, 5xx and 6xx together.
failuresandfailedmean the same.- To select one class, name that class:
sipnab -N -I capture.pcap --filter "response_class == 'failure'"
Matching tests class membership, not string equality.
- A 503 is a server failure by every spelling.
- So
response_class != 'Server Failure'is false for it. - Case, hyphens and underscores do not change the match.
response_code >= 500 AND response_code < 600 selects the same dialogs as
'5xx'.
A dialog never matches 1xx.
response_classis the class of the final response.- A call that has received only a provisional response, such as 183, has no final response and so no class. The same holds for a call that has received no response.
- Use
stateto select those calls.
This is not the classification on the response-code reference.
- That page groups codes by what they mean for a call, and it is narrower.
- There, a challenge, a cancellation and a decline are each not failures. Each one calls for a different action.
response_classfollows the numeric registry only.
Why response_code and not just state
state answers what happened. response_code answers why. They are not the
same question.
Every one of 403, 404, 408, 486, 503 and 603 is state == 'Failed'. They have
different owners:
- a 403 is authorization,
- a 408 is a timer,
- a 486 is the callee,
- a 503 is capacity upstream.
Asking “which release cause dominates on this trunk in the last ten minutes” needs the code. Asking it by class needs a numeric field rather than a set of names:
Every server-side failure, whatever the specific cause:
sipnab -N -I capture.pcap --filter "response_code >= 500 AND response_code < 600"
The one cause you suspect, on the carrier you suspect:
sipnab -N -I capture.pcap --filter "response_code == 503 AND dst.ip == '198.51.100.7'"
A call still in progress has no final response. It matches no comparison:
- not
response_code < 400, - not
>= 400, - not
== 0.
Authentication challenges follow the same rule as final_status_code in
--json-dialogs output:
- A call challenged and then answered reports 200, not the 407.
- A call that was only ever challenged reports the challenge code.
Boolean fields
| Field | Description |
|---|---|
one_way | True if one-way audio detected (via diagnosis engine) |
nat_mismatch | True if RTP arrived from an address no SDP in the dialog advertised — see the note below |
no_media | True if an answered call negotiated media that never arrived — see the note below |
codec_asymmetry | True if A and B legs negotiated different RTP codecs |
ptime_asymmetry | True if the two legs use different ptime (packetization interval) |
payload_asymmetry | True if dynamic payload type numbers differ across legs (with the same codec) |
duration_asymmetry | True if one leg’s media duration is materially shorter than the other’s |
late_media | True if RTP starts noticeably later than the answering 200 OK |
What
nat_mismatchcompares.
- A dialog advertises a media address on each side:
- the caller’s in the offer,
- the callee’s in the answer.
- A re-INVITE may add more.
nat_mismatchis true when a stream carries a source address that none of them named.It uses set membership, not equality against one
c=line.
- A healthy two-way call sources RTP from two different advertised addresses.
- So comparing each stream against a single
c=would flag one direction of every call in the capture.It compares addresses only, never ports. NAT and RTP proxies rewrite the port on ordinary calls without breaking anything.
What
no_mediarequires. All four:
- the far end answered an offer,
- the dialog reached a 2xx,
- at least one exchange described media that should carry audio,
- no RTP arrived for the call.
“Should carry audio” excludes the forms that ask for silence:
a=inactive,m=port0,- a black-holed
c=0.0.0.0,- non-RTP transports such as T.38 (
m=image 4000 udptl t38).So a call held for its whole life is not reported as a media failure.
no_mediais silent on a capture that holds no RTP.
- A signaling-only tap, a HEP feed or
--no-rtpcannot show that one call had no audio. No call in it has any.- On such a capture, the flag stays false by design. It does not select every answered dialog.
- Pair
no_mediawith the knowledge of where your tap sits. If media does not traverse it, the question is unanswerable, and sipnab declines to answer it.
rtp.orphanedis a parse error, not a field.
- It would ask whether a stream belonging to this dialog belongs to no dialog.
- A stream counts as an orphan exactly when no dialog claims it. So the two halves exclude each other.
- The field would match nothing on any capture, while
NOT rtp.orphanedwould match everything. sipnab refuses it withunknown field 'rtp.orphaned'.To find orphaned media, use one of these. Both list streams rather than dialogs:
- the “Orphaned Streams” section of
--report,- the REST API’s
/v1/streams?orphaned=true.
Time field
| Field | Description | Value |
|---|---|---|
time | The instant of the dialog’s first message | an RFC 3339 instant, quoted or bare, such as 2026-10-09T10:00:00Z |
time takes <, <=, >, >= and ==, and nothing else.
!=,=~andin_subnetare parse errors.- So is a literal that is not an RFC 3339 instant.
- sipnab converts an offset other than
Z. So'2026-10-09T12:00:00+02:00'is the same instant as'2026-10-09T10:00:00Z'.
It is the instant that these bound:
- the REST
/v1/dialogsafter/beforewindow, - the MCP
search_by_timetool, - the
--since/--untilflags.
Write a window half-open, >= at the start and < at the end. Then it selects
what those select:
time >= '2026-10-09T10:00:00Z' AND time < '2026-10-09T11:00:00Z'
Named filters
Define a filter once in the config file’s [filters]
table. Use it by name, as @name, wherever sipnab takes a filter expression.
- A name can appear inside a larger expression and inside another named filter.
- An expression can reference the built-in aliases the same way:
[filters]
carrier-a = "src.ip in_subnet '203.0.113.0/24'"
sales = "@carrier-a AND to.user =~ '^8[0-9]{3}$'"
bad-sales = "@sales AND @problems"
sipnab -N -I capture.pcap --config sipnab.toml --filter "@sales AND method == 'INVITE'"
- sipnab replaces
@namewith the named filter’s expression in parentheses. So each definition keeps the precedence it has as written. - An
@inside a quoted string is text, not a reference. - A reference begins an atom: its
@is the first character of the expression or follows whitespace or(. An@inside a word is text, socall_id == a84b4c76@pc33.example.comcompares the Call-ID and does not reference a filter namedpc33. - sipnab resolves every name when it starts. See
[filters]for what it refuses. - sipnab refuses a filter given at run time that names something undefined.
- This applies on the command line or through REST or MCP.
- sipnab lists the defined names.
For example, with this sipnab.toml:
[filters]
failed-here = "state == 'Failed' AND src.ip in_subnet '192.0.2.0/24'"
sipnab -N -I tests/pcap-samples/sip-problem-call.pcap --config sipnab.toml --filter "@failed-here" --report --no-cli-print --quiet
Call-ID From To State Code Duration Msgs PDD Tags
-------------------------------------------------------------------------------------------------------------------------
busy-3a2b1c@192.0.2.30 carol dave Failed 486 1s 4 - -
unavail-4e5f60@192.0.2.50 ivan judy Failed 503 1s 4 - -
A name the file does not define stops the run with exit 2, and the message lists the names it does define:
'@failed-there' is not a defined filter. Defined in [filters]: failed-here. Built-in aliases: problems, slow-setup, short-calls, one-way, nat-issues, codec-asym, ptime-asym, payload-asym, duration-asym, late-media
Calls into, out of and inside a PBX
side says which way a call went relative to your PBX. List the PBX’s own
addresses under [pbx] addresses, each an IP
address or a CIDR range. Nothing changes on the PBX.
inbound: the opening request went to one of those addresses from another one.outbound: it went from one of them to another address.internal: it went from one of them to another of them.- An empty
side: the call touched none of them.
With this sipnab.toml, which names two of the addresses in
sip-problem-call.pcap as the PBX:
[pbx]
addresses = ["192.0.2.40", "192.0.2.50"]
sipnab -N -I tests/pcap-samples/sip-problem-call.pcap --config sipnab.toml --filter "side == 'inbound'" --report --no-cli-print --quiet
Call-ID From To State Code Duration Msgs PDD Tags
-------------------------------------------------------------------------------------------------------------------------
busy-3a2b1c@192.0.2.30 carol dave Failed 486 1s 4 - -
side == 'outbound' selects unavail-4e5f60@192.0.2.50, the call the PBX
placed. Combine side with the fields a PBX rewrites or adds, such as ruri,
pai.user and header.<name>, to follow a call through it.
An entry that is not an address or a range stops sipnab at startup with exit 1:
[pbx] addresses: 'pbx.example.com' is not an IP address or a CIDR range: invalid CIDR 'pbx.example.com': invalid IP: invalid IP address syntax
Any header by name
header.<name> compares the value of a SIP header you name, as a string.
- It matches when any header of that name satisfies the comparison. That header can be on any message sipnab holds for the dialog.
- sipnab compares a header that appears on several lines one line at a time.
- It matches header names without regard to case.
- It expands a compact form first, so
header.freadsFrom.
header.X-Trunk == 'north-east'
header."P-Asserted-Identity" =~ 'sip:alice@'
Write the name bare. Use quotes when it holds a character that would end a bare name. Either way, it must be an RFC 3261 token of at most 256 bytes (see Parser constraints).
Operators
| Operator | Applies To | Description |
|---|---|---|
== | string, numeric, boolean, time | Equal |
!= | string, numeric, boolean | Not equal |
< | string, numeric, time | Less than (for time, earlier) |
> | string, numeric, time | Greater than (for time, later) |
<= | string, numeric, time | Less than or equal |
>= | string, numeric, time | Greater than or equal |
=~ | string | Regex match (Rust regex syntax) |
in_subnet | address-valued string | The left address falls inside the right CIDR block |
Notes:
- Boolean fields only support
==and!=. - Regex (
=~) is not applicable to numeric or boolean fields. in_subnettakes a CIDR literal on the right and an address-valued field on the left (src.ip,dst.ip).- It uses the same rule as
--hep-allow. - So IPv4, IPv6 and IPv4-mapped IPv6 addresses (RFC 4291 section 2.5.5.2) all compare as the allowlist compares them.
- A bare address with no prefix reads as a host route.
- An unparseable address or block matches nothing, rather than matching everything.
- It uses the same rule as
- Numeric equality uses epsilon comparison for floating-point precision.
- For computed values (
duration,pdd,rtp.mos), prefer range operators (>=,<) over==. - An exact match on a derived float rarely hits.
- For computed values (
Note: String comparisons are case-sensitive.
statevalues must exactly match one of the 13 values listed under String Fields above ('Failed', not'failed').- Use
=~with a case-insensitive regex pattern if you need case-insensitive matching:state =~ '(?i)failed'.
Values
You write the value on the right of a comparison bare, in single quotes, or
in double quotes. All three spell the same text, and the result does not
depend on which one you use: method == INVITE, method == 'INVITE' and
method == "INVITE" select the same dialogs.
The field on the left decides how sipnab reads the text:
| Field type | sipnab reads the text as | Examples |
|---|---|---|
String (including header.<name>) | The text as written, compared case-sensitively | INVITE, 1001, 'Friendly Scanner' |
| Numeric | A finite number (f64) | 3.0, 100, '5060' |
| Boolean | true or false, in any case | true, FALSE |
time | An RFC 3339 instant | 2026-10-09T10:00:00Z, '2026-10-09T12:00:00+02:00' |
Any field, with =~ | A regex | ^100[0-9]$, 'friendly.*scanner' |
- A number-shaped value on a string field is text.
to.user == 1001selects the user1001. - A numeric field refuses a value that is not a finite number, and a boolean
field refuses anything but
trueandfalse. The error names the reason. - The right-hand side is always a value: the grammar has no field-to-field
comparison. So a word that is a keyword, a field name or an alias name
elsewhere is text in value position.
from.user == ANDcompares with the textAND,from.user == to.userwith the textto.user, andside == problemswith the textproblems.
Bare values
A bare value is the run of characters up to whitespace, a quote character, or
a ) that closes a group of the expression.
- Its own parentheses must balance. A
(inside the value opens a pair, and a)with no(to close ends the value. So(method == INVITE)reads the valueINVITE, andmethod =~ ^(INVITE|BYE)$reads the whole regex. - For
=~only, two regex rules also apply. A backslash keeps the next character in the value, so\)is regex text. Inside a bracketed class such as[()], parentheses are literal. - Every other character is part of the value, including
@ . : ; = + - / | * # %.ruri == sip:7000@192.0.2.10;user=phoneandsrc.ip in_subnet 203.0.113.0/24need no quotes. - sipnab takes a bare value verbatim. It has no escapes.
When a value needs quotes
A value needs quotes only where sipnab cannot tell where a bare value ends or what it means. Each case has its own error message.
| The value | Why a bare value cannot carry it | Error message |
|---|---|---|
Contains whitespace, such as 'Friendly Scanner' | Whitespace ends a bare value, and sipnab reads the next word as the rest of the expression | a value containing whitespace must be quoted, or AND/OR is missing |
Contains ' or ", such as "O'Brien" | A quote character begins or ends a quoted string anywhere in the expression | a value containing ' or " must be quoted with the other quote or a backslash |
Contains an unmatched ), such as 'a)b' | An unmatched ) closes a group of the expression | unmatched ')'; a value containing an unmatched ')' must be quoted |
Begins with =, !, < or > | These begin an operator, so method === 'INVITE' stays an error rather than the value ='INVITE' | a value beginning with = ! < > or @ needs quotes |
Begins with @ | @name is a named filter reference | the unknown-reference message, or the message above |
| Is empty | A bare value has at least one character | pai.user == '' is the only spelling |
&& and || are not part of the filter language. sipnab refuses a bare
value followed by either with join comparisons with AND, OR and NOT; && and || are not supported.
Quoted values
Inside a quoted string, a backslash escapes the next character.
- So the delimiter itself is expressible.
\'and\"yield a literal quote ('it\'s',"say \"hi\""). - Every other
\xsequence, including\\, stays verbatim, backslash and all. - So regex metacharacters and classes still reach the engine unchanged
(
from.user =~ '\d\d\d\d',payload =~ 'example\.com', and'\\'for a literal backslash in a regex).
Tip: sipnab compiles each regex once and reuses it across all messages. Avoid unbounded quantifiers on large captures (e.g., prefer
from.user =~ '^100[0-9]$'overfrom.user =~ '.*100[0-9].*').
Boolean combinators
| Keyword | Description |
|---|---|
AND | Both sides must match (case-insensitive) |
OR | Either side must match (case-insensitive) |
NOT | Negates the following atom (case-insensitive) |
Parentheses ( ) group sub-expressions to override default precedence.
Named aliases
These preset expressions are available as:
- dedicated CLI flags where they exist (
--problems, etc.), - shorthand to
--filter(e.g.--filter codec-asym), - the value of any MCP tool’s
filterargument, - the value of the REST API’s
filterquery parameter (e.g./v1/dialogs?filter=codec-asym), kindsentries for the MCPfind_problemstool.
Each alias expands to a DSL expression. The expansion uses the thresholds the run’s flags and config set.
| Alias | Dedicated CLI Flag | Expansion |
|---|---|---|
problems | --problems | state == 'Failed' OR one_way == true OR rtp.loss > 5.0 OR rtp.jitter > 50.0 OR nat_mismatch == true OR retransmits > 3 OR pdd > 11.0 OR codec_asymmetry == true OR ptime_asymmetry == true OR payload_asymmetry == true OR duration_asymmetry == true OR late_media == true |
slow-setup | --slow-setup | pdd > 11.0 |
short-calls | --short-calls | duration < 3.0 AND state == 'Completed' |
one-way | --one-way | one_way == true |
nat-issues | --nat-issues | nat_mismatch == true |
codec-asym | — (use --filter codec-asym) | codec_asymmetry == true |
ptime-asym | — (use --filter ptime-asym) | ptime_asymmetry == true |
payload-asym | — (use --filter payload-asym) | payload_asymmetry == true |
duration-asym | — (use --filter duration-asym) | duration_asymmetry == true |
late-media | — (use --filter late-media) | late_media == true |
--filter first tries to resolve the argument as an alias name. If no alias
matches, it parses the argument as a DSL expression.
The alias and the expression it expands to select the same dialogs. So these are equivalent:
sipnab -N -I capture.pcap --filter codec-asymsipnab -N -I capture.pcap --filter "codec_asymmetry == true"
Pick whichever reads better in the command you are writing.
The dedicated flag, the --filter <alias> spelling and the MCP kinds entry
are three names for one expression. These all select the same dialogs:
sipnab --short-callssipnab --filter short-callssipnab --filter "duration < 3.0 AND state == 'Completed'"
Combining flags:
- Combining several flags ORs their expansions together.
- If
--filteris also present, its expression is ANDed with that result. - For example,
--problems --filter "from.user == '1001'"keeps problem calls from user 1001.
--nat-issues / --filter nat-issues selects the calls whose RTP arrived from
an address the SDP never advertised. The boolean-field note above says:
- what that means,
- what it ignores.
Parser constraints
sipnab refuses a filter that breaks one of these limits. The run exits with status 2 before it reads any packet, and the error names the limit. The same limits apply to every flag, config key, REST parameter and MCP argument that takes a filter.
| Limit | Value | Error |
|---|---|---|
| Expression size | 1024 nodes | expression exceeds maximum size of 1024 nodes (this one has 1199); split it into several filters |
| Parenthesis nesting | 50 levels | expression exceeds maximum nesting depth of 50 |
header.<name> name length | 256 bytes | header name longer than 256 bytes |
| Regex pattern size | 1,000,000 bytes | the regex does not compile or is larger than 1000000 bytes |
| Empty expression | not allowed | filter expression is empty |
Each comparison, AND, OR and NOT counts toward the node limit. A chain
of 600 comparisons joined with OR has 1199 nodes. A named
filter expands in place, so its nodes count too. To stay under
the limit, replace a chain of OR comparisons on one field with one regex,
such as from.user =~ '^(1001|1002|1003)$', or split the question across
several runs.
Errors that point into the expression print its text with a caret under the position, a 0-based character column:
sipnab -N -I capture.pcap --filter 'method == INVITE extra'
Invalid --filter expression: a value containing whitespace must be quoted, or AND/OR is missing at position 10: 'INVITE'
method == INVITE extra
^
valid operators: ==, !=, <, <=, >, >=, =~ (regex)
see docs/filter-dsl.md for fields, values, and diagnostic aliases
An unknown field name adds the list of valid fields:
sipnab -N -I capture.pcap --filter 'nosuch == 1'
Invalid --filter expression: unexpected input at position 0: 'nosuch'
nosuch == 1
^
hint: unknown field 'nosuch'
valid fields: from.user, to.user, from.host, to.host, from.uri, to.uri, ruri, pai.user, method, ua, call_id, payload, src.ip, dst.ip, src.port, dst.port, state, duration, msg_count, response_code, final_status_code, response_class, pdd, setup_time, retransmits, rtp.mos, rtp.jitter, rtp.loss, rtp.packets, rtp.codec, rtp.codecs, rtp.ssrc, side, endpoint, one_way, nat_mismatch, no_media, codec_asymmetry, ptime_asymmetry, payload_asymmetry, duration_asymmetry, late_media, time, header.<name>
valid operators: ==, !=, <, <=, >, >=, =~ (regex)
see docs/filter-dsl.md for fields, values, and diagnostic aliases
sipnab refuses a regex that does not compile with the regex-size message above.
Check the pattern against the Rust regex crate syntax.
See also
- cli-reference.md — the
--filterflag and the dedicated diagnostic-alias flags - keybindings.md — the TUI filter dialog (F7), which compiles its fields down to this DSL
- examples.md — recipes that put these filters to work