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 --problems sweeps 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.

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-dialogs emits one line per matching dialog.
  • --report prints 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-analyze reports 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 == INVITE
  • from.user == 1001
  • state == '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.0
  • rtp.mos < 3.0
  • rtp.loss > 2.0
  • duration < 5.0
  • retransmits > 3
  • rtp.jitter > 50.0
  • rtp.packets > 10000

Matching boolean fields

  • one_way == true
  • nat_mismatch == true
  • no_media == true
  • codec_asymmetry == true
  • late_media == true

Compound expressions

  • method == 'INVITE' AND rtp.mos < 3.0
  • from.user =~ '^1001' AND state == 'Failed'
  • pdd > 3.0 OR retransmits > 5
  • NOT 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.pcap for -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 > 0 clause is optional. Raise its number, for example rtp.packets > 500, to leave out calls that carried only a few packets.
  • To select the calls that carry no media, write rtp.packets == 0 or no_media == true instead.

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_way field 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.orphaned is a parse error.

Where to find them:

  • The --report output carries an “Orphaned Streams” section.
  • The REST API answers the same question at /v1/streams?orphaned=true when 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.ssrc compares against the SSRC rendered as 0x-prefixed lowercase hex.
  • So the literal needs the 0x prefix, 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_code is also spelled final_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

FieldDescriptionExample Values
from.userUser part of the SIP From header"1001", "alice"
to.userUser part of the SIP To header"1002", "bob"
from.hostHost of the From URI, without the port"carrier-a.example", "203.0.113.5"
to.hostHost of the To URI, without the port"pbx.example"
from.uriThe From URI as written, URI parameters included, without the display name or tag"sip:+15551230000@carrier-a.example;user=phone"
to.uriThe To URI as written"sip:7000@pbx.example"
ruriRequest-URI of the request that opened the dialog; empty when the capture began on a response"sip:7000@192.0.2.10"
pai.userUser part of the opening request’s P-Asserted-Identity; empty when it has none"+15559876543"
sideWhere 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"
endpointThe 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"
methodSIP request method"INVITE", "REGISTER", "BYE"
uaUser-Agent header (first non-empty across dialog messages)"Olle", "friendly-scanner"
call_idSIP Call-ID header"abc123@host"
payloadRaw message content — matches when ANY message in the dialog contains/matches the value (payload grep)"X-Custom-Header", "sipsak"
src.ipSource IP address (first message)"192.0.2.1"
dst.ipDestination IP address (first message)"192.0.2.2"
stateDialog state machine value"Trying", "InCall", "Failed"
response_classIANA class of the final response"2xx", "4xx", "5xx", "6xx"
rtp.codecRTP codec name (matches if ANY linked stream matches)"PCMU", "opus"
rtp.codecsEvery codec the dialog’s streams used, distinct, sorted and comma-joined; empty with no streams"PCMU,telephone-event"
rtp.ssrcRTP 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

payload vs -e/--match:

  • The payload field matches per dialog (true if any message matches).
  • The -e/--match flag 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 -e for grep-style streaming output.
  • Use payload inside a larger --filter expression.

Numeric fields

FieldDescriptionUnit
src.portSource port (first message)port number
dst.portDestination port (first message)port number
durationDialog durationseconds (float)
msg_countNumber of SIP messages in dialogcount
response_codeFinal 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 all100-699
pddPost-dial delay: from the INVITE to the first 180 or 183 responseseconds (float)
setup_timeCall setup time (INVITE to 200 OK)seconds (float)
retransmitsTotal retransmit count in dialogcount
rtp.mosMean Opinion Score (worst across streams, E-model R-factor approximation)1.0 - 5.0, unknown with no RTP
rtp.jitterJitter (worst/highest across streams)milliseconds, unknown with no RTP
rtp.lossPacket loss (worst/highest across streams)percentage (0-100), unknown with no RTP
rtp.packetsTotal 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.jitter and rtp.loss are 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 treats NULL the same way.
  • A saved filter carrying an AND rtp.packets > 0 clause 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:

ClassNumberName
Provisional1xxprovisional
Successful2xxsuccessful, success
Redirection3xxredirection, redirect
Request Failure4xxrequest failure, client failure
Server Failure5xxserver failure
Global Failures6xxglobal failures, global failure
sipnab -N -I capture.pcap --filter "response_class == 'server failure'"

failure means all three failure classes: 4xx, 5xx and 6xx together.

  • failures and failed mean 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_class is 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 state to 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_class follows 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

FieldDescription
one_wayTrue if one-way audio detected (via diagnosis engine)
nat_mismatchTrue if RTP arrived from an address no SDP in the dialog advertised — see the note below
no_mediaTrue if an answered call negotiated media that never arrived — see the note below
codec_asymmetryTrue if A and B legs negotiated different RTP codecs
ptime_asymmetryTrue if the two legs use different ptime (packetization interval)
payload_asymmetryTrue if dynamic payload type numbers differ across legs (with the same codec)
duration_asymmetryTrue if one leg’s media duration is materially shorter than the other’s
late_mediaTrue if RTP starts noticeably later than the answering 200 OK

What nat_mismatch compares.

  • 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_mismatch is 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_media requires. 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= port 0,
  • 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_media is silent on a capture that holds no RTP.

  • A signaling-only tap, a HEP feed or --no-rtp cannot 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_media with the knowledge of where your tap sits. If media does not traverse it, the question is unanswerable, and sipnab declines to answer it.

rtp.orphaned is 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.orphaned would match everything. sipnab refuses it with unknown 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

FieldDescriptionValue
timeThe instant of the dialog’s first messagean RFC 3339 instant, quoted or bare, such as 2026-10-09T10:00:00Z

time takes <, <=, >, >= and ==, and nothing else.

  • !=, =~ and in_subnet are 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/dialogs after/before window,
  • the MCP search_by_time tool,
  • the --since/--until flags.

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 @name with 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, so call_id == a84b4c76@pc33.example.com compares the Call-ID and does not reference a filter named pc33.
  • 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.f reads From.
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

OperatorApplies ToDescription
==string, numeric, boolean, timeEqual
!=string, numeric, booleanNot equal
<string, numeric, timeLess than (for time, earlier)
>string, numeric, timeGreater than (for time, later)
<=string, numeric, timeLess than or equal
>=string, numeric, timeGreater than or equal
=~stringRegex match (Rust regex syntax)
in_subnetaddress-valued stringThe left address falls inside the right CIDR block

Notes:

  • Boolean fields only support == and !=.
  • Regex (=~) is not applicable to numeric or boolean fields.
  • in_subnet takes 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.
  • 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.

Note: String comparisons are case-sensitive.

  • state values 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 typesipnab reads the text asExamples
String (including header.<name>)The text as written, compared case-sensitivelyINVITE, 1001, 'Friendly Scanner'
NumericA finite number (f64)3.0, 100, '5060'
Booleantrue or false, in any casetrue, FALSE
timeAn RFC 3339 instant2026-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 == 1001 selects the user 1001.
  • A numeric field refuses a value that is not a finite number, and a boolean field refuses anything but true and false. 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 == AND compares with the text AND, from.user == to.user with the text to.user, and side == problems with the text problems.

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 value INVITE, and method =~ ^(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=phone and src.ip in_subnet 203.0.113.0/24 need 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 valueWhy a bare value cannot carry itError message
Contains whitespace, such as 'Friendly Scanner'Whitespace ends a bare value, and sipnab reads the next word as the rest of the expressiona 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 expressiona 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 expressionunmatched ')'; 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 referencethe unknown-reference message, or the message above
Is emptyA bare value has at least one characterpai.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 \x sequence, 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]$' over from.user =~ '.*100[0-9].*').

Boolean combinators

KeywordDescription
ANDBoth sides must match (case-insensitive)
OREither side must match (case-insensitive)
NOTNegates 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 filter argument,
  • the value of the REST API’s filter query parameter (e.g. /v1/dialogs?filter=codec-asym),
  • kinds entries for the MCP find_problems tool.

Each alias expands to a DSL expression. The expansion uses the thresholds the run’s flags and config set.

AliasDedicated CLI FlagExpansion
problems--problemsstate == '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-setuppdd > 11.0
short-calls--short-callsduration < 3.0 AND state == 'Completed'
one-way--one-wayone_way == true
nat-issues--nat-issuesnat_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-asym
  • sipnab -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-calls
  • sipnab --filter short-calls
  • sipnab --filter "duration < 3.0 AND state == 'Completed'"

Combining flags:

  • Combining several flags ORs their expansions together.
  • If --filter is 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.

LimitValueError
Expression size1024 nodesexpression exceeds maximum size of 1024 nodes (this one has 1199); split it into several filters
Parenthesis nesting50 levelsexpression exceeds maximum nesting depth of 50
header.<name> name length256 bytesheader name longer than 256 bytes
Regex pattern size1,000,000 bytesthe regex does not compile or is larger than 1000000 bytes
Empty expressionnot allowedfilter 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 --filter flag 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