-
Notifications
You must be signed in to change notification settings - Fork 4
SIP Lint Rules
Every rule the conformance linter runs, the RFC section behind it, and how to turn it off.
Each rule carries a stable identifier such as
SIP-3261-8.1.1.7-BRANCH-COOKIE. Quote that identifier in a carrier ticket,
and use it to suppress the rule in CI. Identifiers never change meaning, and
nothing reuses a retired one.
Most SIP linters read messages against a grammar. They catch a malformed
Contact header. They cannot catch the far end sending payload type 8 when the
SDP declared payload type 0, because they never see the RTP.
sipnab holds the signaling and the media in one process. The OBS- rules
compare what the SDP declared against what the wire carried, and that class of
defect stays invisible to any tool that reads only text.
src/sip/diagnosis.rs answers "why did this call
fail". This linter answers "does this traffic obey the specification". A call
can complete over messages that break four MUSTs, and a fully conformant call
can hit a busy signal. Keep the two questions apart.
A finding carries the rule identifier, a severity, a basis, the RFC number and section as separate fields, the index of the message it came from, what the capture held, what the section calls for, and why the difference matters.
The RFC number and the section are data, not prose inside a sentence. That
choice caught a mistake while this module was still new: three sources place
the angle-bracket rule for a Contact URI in RFC 3261 section 20.10, and the sentence
actually sits in the preamble of RFC 3261 section 20, above section 20.1. A citation nothing
can read is a citation nothing can check.
Severity says how much attention a finding deserves: error, warning,
notice, info.
Basis says what kind of claim the rule makes, and the four values do not overlap:
| Basis | Meaning |
|---|---|
must |
The cited section says MUST or MUST NOT, and the message disobeys it. |
should |
The cited section says SHOULD or RECOMMENDED. Deviating stays legal. |
interop |
No specification forbids this. Deployed equipment mishandles it anyway. |
observation |
The declaration and the observed media disagree. |
Keeping must apart from interop matters more than it looks. A reader who
cannot tell a broken MUST from a vendor-compatibility hint learns to discount
both, and the MUST is the one worth acting on.
Run a named subset instead of the whole catalog. Name it in the rulesets
parameter of the MCP lint_dialog tool, or in
LintConfig::with_ruleset when you drive the linter from Rust:
| Name | Contents |
|---|---|
all |
Every rule. The default. |
must |
Only MUST violations. Defensible in a carrier ticket without argument. |
rfc |
MUST and SHOULD. Excludes the vendor heuristics and the media rules. |
interop |
Only the "this breaks real equipment" heuristics. |
observation |
Only the declaration-versus-observation rules. obs is the same name, shortened. |
syntax |
Only the rules that read a single message with no dialog context. |
A suppression pattern is an exact rule identifier, or a prefix ending in *:
# Our carrier rewrites Contact and we have stopped arguing about it
SIP-3261-19.1.1-URI-PARAM-DEMOTED
# No media in these captures, so the observation rules have nothing to read
OBS-*
Patterns separate on commas, spaces or newlines, and # starts a comment.
Put them in a .sipnablint and check it in beside the config it belongs to.
Both surfaces read the same file: the MCP lint tools take a suppression_file
parameter, and the binary takes --lint-suppress-file. In the ordinary case you
need not tell either one where the file is — see the discovery rule below.
Discovery honors the .sipnablint beside calls.pcap, and reports which file
it applied and how many findings it silenced:
sipnab -N -I calls.pcap --lint --lint-fail-on errorAn explicit list, for a CI job whose suppressions differ from the tree's:
sipnab -N -I calls.pcap --lint --lint-suppress-file ci.sipnablintEverything, including what the project has agreed to live with:
sipnab -N -I calls.pcap --lint --lint-no-suppressA file named with --lint-suppress-file that sipnab cannot open is a hard error
rather than a full-catalog run: pointing at a suppression list states an intent,
and linting with every rule on would read as "my suppressions matched nothing".
sipnab looks for one next to the capture, then climbs toward the project root —
the nearest ancestor holding a .git — and stops there. A capture living
outside any project, such as a corpus mount or a shared drop, picks up nothing
from above itself: adopting a stranger's suppression list would switch off
rules nobody on this project turned off, and the run would come back clean for
a reason four directories away.
Suppression never hides itself. Every lint_dialog and validate_message
response names the file it applied and counts what that file silenced, next to
separate counts for the severity floor and the per-rule cap. A short finding
list always says why it is short — the
MCP tool reference names the exact fields.
One further guard rail keeps CI readable: a single rule reports at most 25
findings per dialog by default. A dialog retransmitting an INVITE eleven
times trips a message rule eleven times, and every one of them is true, but
printing all eleven buries the other rules. Change the cap with
--lint-max-per-rule N, or with [limits] lint_max_per_rule in the config
file.
These read the media against the declaration. Every one of them needs RTP or RTCP that sipnab attributed to the dialog.
| Rule | Severity | Cites | Fires when |
|---|---|---|---|
OBS-3264-6.1-PT-UNDECLARED |
error | RFC 3264 section 6.1 | The wire carries an RTP payload type that no offer or answer in the dialog declared. Comfort noise stays exempt on payload type 13 (RFC 3389) and on the reserved payload type 19 that earlier profiles used for it, because equipment sends both without ever listing them. |
OBS-4566-5.14-MEDIA-PORT-MISMATCH |
warning | RFC 4566 section 5.14 | RTP arrives at a declared media address on a port nobody advertised. The RTCP port one higher stays exempt. |
OBS-3264-6.1-DIRECTION-UNMET |
warning | RFC 3264 section 6.1 | Both ends negotiated sendrecv, media flowed, and one negotiated endpoint received none of it. |
OBS-4566-6-PTIME-MISMATCH |
notice | RFC 4566 section 6 | The packetization on the wire differs from a=ptime by more than half. |
OBS-5761-5.1.1-RTCP-MUX-UNANSWERED |
error | RFC 5761 section 5.1.1 | An offer asked for a=rtcp-mux, the answer stayed silent, and RTCP arrived on the RTP port regardless. |
OBS-3551-4.2-FRAME-SIZE-IMPOSSIBLE |
warning | RFC 3551 section 4.2 | The payload size implies more media per packet than RFC 3551 section 4.2 asks a receiver to accept, or more media than the elapsed time between packets. |
Three of these rules compare one duration against another, and every comparison runs in one direction only.
Silence suppression, comfort noise and a congested path all stretch the gap between packets. None of them compresses it. A rule that fires on "slower than declared" therefore fires on a large share of ordinary traffic, and a rule that fires on "carrying more media than time elapsed" fires only on the impossible.
The packetization measurement reads payload size rather than arrival times wherever the codec has a fixed octet rate, for the same reason: 20 ms packets stay 20 ms packets however far apart they arrive.
Deriving a duration from a payload size needs a codec with a fixed octet rate. RFC 3551 Table 1 supplies five, and only those five participate:
| Codec | Octets per millisecond | Frame |
|---|---|---|
| PCMU | 8 | sample-based |
| PCMA | 8 | sample-based |
| G722 | 8 | sample-based |
| G729 | 1 | 10 octets per 10 ms |
| GSM | 1.65 | 33 octets per 20 ms |
Opus, AMR and the other variable-rate codecs have no such number, so the two size rules skip them rather than invent a threshold.
These read one message on its own.
| Rule | Severity | Basis | Cites | Fires when |
|---|---|---|---|---|
SIP-3261-8.1.1-MANDATORY-HEADER-MISSING |
error | must | RFC 3261 section 8.1.1 | One of Call-ID, CSeq, From, To or Via is absent. |
SIP-3261-20.16-CSEQ-MALFORMED |
error | must | RFC 3261 section 20.16 | A CSeq arrives that nothing can read as a number and a method. |
SIP-3261-20.14-CONTENT-LENGTH-MISMATCH |
error | must | RFC 3261 section 20.14 |
Content-Length exceeds the body that arrived. |
SIP-3261-25.1-HEADER-CONTROL-BYTE |
error | must | RFC 3261 section 25.1 | A header name or value holds a control byte other than tab. |
SIP-3261-20-URI-BRACKETS |
error | must | RFC 3261 section 20 | A Contact, From or To URI holds a comma or a question mark outside angle brackets. |
SIP-3261-19.1.1-URI-PARAM-DEMOTED |
warning | interop | RFC 3261 section 19.1.1 | A URI parameter — transport, user, method, ttl, maddr or lr — sits outside the angle brackets, where the receiver reads it as a header parameter. |
SIP-3261-8.1.1.6-MAX-FORWARDS-MISSING |
warning | must | RFC 3261 section 8.1.1.6 | A request carries no Max-Forwards. |
SIP-3261-20.22-MAX-FORWARDS-RANGE |
notice | should | RFC 3261 section 20.22 |
Max-Forwards reads zero, exceeds the recommended 70, or holds no integer. |
SIP-3261-8.1.1.7-BRANCH-COOKIE |
warning | must | RFC 3261 section 8.1.1.7 | A request's top Via branch lacks the z9hG4bK magic cookie, or carries no branch at all. |
SIP-3261-8.1.1.5-CSEQ-METHOD-MISMATCH |
error | must | RFC 3261 section 8.1.1.5 | The CSeq method disagrees with the request line. |
SIP-3261-12.1.1-CONTACT-MISSING-IN-2XX |
error | must | RFC 3261 section 12.1.1 | A 2xx answer to INVITE carries no Contact, so the dialog it creates has no remote target for the ACK or the BYE. |
SIP-3262-3-RELIABLE-PROVISIONAL-WITHOUT-RSEQ |
error | must | RFC 3262 section 3 | A provisional demands 100rel and carries no RSeq, so the receiver has to acknowledge a response it cannot name. |
SIP-4028-7.1-SESSION-EXPIRES-BELOW-MIN-SE |
error | must | RFC 4028 section 7.1 | One message carries a Session-Expires smaller than the Min-SE beside it, so it asks for a refresh interval it has already declared too short. |
SIP-4028-4-SESSION-EXPIRES-TOO-SMALL |
warning | must | RFC 4028 section 4 |
Session-Expires sits below the 90-second absolute minimum. |
SIP-4028-5-MIN-SE-TOO-SMALL |
warning | must | RFC 4028 section 5 |
Min-SE sits below 90 seconds, wherever it appears. |
SIP-4028-9-REFRESHER-MISSING |
warning | must | RFC 4028 section 9 | A 2xx answer to INVITE negotiates Session-Expires and names no refresher. |
SIP-7989-5-SESSION-ID-MALFORMED |
error | must | RFC 7989 section 5 | A Session-ID half is not 32 characters of [0-9a-f], so it is not a sess-uuid at all. Correlation drops the half, and across a B2BUA there may be nothing well formed left to match one leg to the other. |
SIP-7989-5-SESSION-ID-UPPERCASE |
warning | must | RFC 7989 section 5 | A Session-ID UUID arrives in uppercase hexadecimal. sipnab compares case-insensitively and still correlates on it; any peer, SBC or log pipeline comparing the header byte for byte sees two identifiers for one session. |
SIP-7989-11-SESSION-ID-LEGACY-FORM |
notice | interop | RFC 7989 section 11 | A Session-ID carries no remote parameter, the obsoleted RFC 7329 single-UUID form. RFC 7989 section 5 makes remote a MUST with a section 11 exception for interworking with that older form, which one message cannot confirm — so this names the peer as an interop observation rather than asserting a violation. Correlation then works in one direction only. |
SIP-3261-7.3.1-SINGULAR-HEADER-REPEATED |
error | must | RFC 3261 section 7.3.1 | A header field whose value is not defined as a comma-separated list arrives on more than one row. RFC 3261 section 7.3.1 names WWW-Authenticate, Authorization, Proxy-Authenticate and Proxy-Authorization as its own exception, so the rule skips those four. |
SIP-3261-16.6-RECORD-ROUTE-NOT-LOOSE |
error | must | RFC 3261 section 16.6 | A Record-Route URI carries no lr parameter, so the hop it records is a strict route. |
SIP-3261-8.1.1.7-VIA-BRANCH-DUPLICATE |
error | must | RFC 3261 section 8.1.1.7 | Two Via header field values in one request carry the same branch. Requests only: a response copies the request's stack verbatim (RFC 3261 section 8.2.6.2). |
Three of these rules report zero against the local corpus, and the zeros do not all mean the same thing.
SIP-3261-12.1.1-CONTACT-MISSING-IN-2XX is well exercised: the corpus holds
1,989 2xx answers to INVITE, every one of them carrying a Contact. The rule
reaches its own code path 1,989 times and declines each time, which is the
strongest evidence available short of a positive hit.
The two RFC 3262 rules are not exercised. The corpus contains exactly one
reliable provisional and one PRACK, so their silence rests on their unit
tests rather than on real traffic. Both fire on crafted input and both stay
quiet under their guards, but nobody should read their zero as a measurement.
Saying so is the point: an unexercised rule and a rule with nothing to find
produce the same row, and only this note tells them apart.
RFC 4028 numbers its behavior sections 7 UAC, 8 Proxy, 9 UAS — and the refresher obligation belongs to the UAS, so it cites RFC 4028 section 9. Recalling it as section 8 sends a reader to the proxy's rules, which say something different about the same header field. The citation here came from the table of contents in RFC 4028 rather than from memory, for the same reason the angle-bracket rule cites the RFC 3261 section 20 preamble instead of section 20.10.
All four session-timer rules read one message on its own. Two header fields that contradict each other sit in the same request, a floor is a fixed number, and a 2xx either names a refresher or does not. None of it needs the dialog.
The refresher rule is the one with false-positive potential, so three guards
fence it: the message has to be a response, its status has to be 2xx, and its
CSeq method has to be INVITE.
Measured against the local corpus, where all four rules report zero. That
number means nothing on its own — a rule that never fires and a rule with
nothing to find produce the same row — so a probe took it apart. The corpus holds
1,849 messages carrying Session-Expires, of which 471 are 2xx answers to
INVITE, and every one of those 471 names a refresher. The rule reaches its
own code path 471 times and declines each time, which is silence with evidence
behind it rather than a rule that cannot fire. The 358 values in the corpus
that carry no refresher are all requests, where a UAC proposes a timer and RFC 4028 section 9
places no obligation. No Session-Expires anywhere in the corpus sits below
90, so both floor rules are quiet for the same checkable reason. A request offering a timer is the UAC
proposing rather than answering, and RFC 4028 section 9 puts the obligation on the answer.
RFC 3261 section 20 gives one sentence for three characters, and the three do not behave alike.
A comma or a question mark in a bare URI breaks the MUST outright: the receiver
splits the value there and reads a different URI than the one sent. That is
SIP-3261-20-URI-BRACKETS, and it goes in the must ruleset.
A semicolon is different. Contact: sip:a@b;transport=tcp parses as perfectly
legal SIP that means something the sender did not intend, because transport
is a URI parameter and outside the brackets it lands on the header. Nothing on
the wire breaks a MUST, so SIP-3261-19.1.1-URI-PARAM-DEMOTED reports as
interop and stays out of the must ruleset. It remains the defect most often
found at the bottom of a "calls reach the wrong trunk" ticket.
A bare From: sip:alice@example.com;tag=1928301774 trips neither, and RFC 3261
uses that form in its own examples. tag is a header parameter, so it belongs
where it sits.
These read a dialog's messages against each other.
| Rule | Severity | Basis | Cites | Fires when |
|---|---|---|---|---|
SIP-3261-8.1.1.2-TO-TAG-IN-INITIAL-REQUEST |
warning | must | RFC 3261 section 8.1.1.2 | A REGISTER carries a To tag, or the dialog's first request carries one and its own transaction answers with a different tag. |
SIP-3261-17.1.1.3-ACK-CSEQ-MISMATCH |
error | must | RFC 3261 section 17.1.1.3 | An ACK carries a sequence number belonging to no INVITE in the dialog. |
SDP-3264-6.1-ANSWER-NO-COMMON-FORMAT |
error | must | RFC 3264 section 6.1 | An answer shares no media format with the offer, on a stream it did not decline. |
SDP-3264-6.1-ANSWER-EXTRA-FORMAT |
info | interop | RFC 3264 section 6.1 | An answer lists a format the offer never carried. |
SDP-3264-6.1-ANSWER-DIRECTION-ILLEGAL |
error | must | RFC 3264 section 6.1 | The answer's direction attribute contradicts what the offer's admits. |
SIP-3262-4-PRACK-MISSING |
warning | must | RFC 3262 section 4 | A reliable provisional went unacknowledged in a dialog whose INVITE reached a final response, so the PRACK is absent rather than merely off the end of the capture. |
SDP-3264-8.4-HOLD-CONNECTION-ZERO |
warning | should | RFC 3264 section 8.4 | A re-offer blanks the connection address to signal hold. |
SIP-3261-17.1.1.3-ACK-BRANCH-MISMATCH |
error | must | RFC 3261 section 17.1.1.3 | An ACK to a non-2xx carries a branch other than its INVITE's. A 2xx ACK is exempt and must be — see below. |
SIP-3261-12.1.1-RECORD-ROUTE-NOT-COPIED |
error | must | RFC 3261 section 12.1.1 | A 2xx to the dialog-forming request drops or reorders a Record-Route value the request carried. |
SDP-3264-8.3.2-DYNAMIC-PT-REBOUND |
error | must | RFC 3264 section 8.3.2 | A dynamic payload type (96-127) means one codec in one body and a different codec in a later body of the same m= line. |
SDP-3264-7-TELEPHONE-EVENT-ONE-WAY |
warning | interop | RFC 3264 section 7 | An offer declares telephone-event on an audio stream the answer accepted and shares a codec with, and the answer omits it. |
SDP-3264-8.2-REJECTED-STREAM-ATTRIBUTES |
notice | interop | RFC 3264 section 8.2 | A stream the answer declined with port zero still carries a=rtpmap, a=fmtp, a=crypto, a=candidate, a=ptime, a=rtcp-mux or a=rtcp. |
SDP-7587-7-OPUS-RTPMAP-RATE |
error | must | RFC 7587 section 7 | An a=rtpmap names opus at a clock rate other than 48000, or with a channel count other than 2. |
A widely repeated claim holds that an answer containing a codec absent from the offer breaks RFC 3264. It does not. RFC 3264 section 6.1 permits the extra listing in as many words, and explains why it rarely helps: the answerer cannot send with a format the offer never listed.
So the MUST violation is the absence of any shared format, which
SDP-3264-6.1-ANSWER-NO-COMMON-FORMAT reports as an error. The extra listing
reports separately, as info, under interop. Equipment that reads the answer
as the negotiated set picks one of the extras and sends media the far end
drops, which is worth knowing and is not a broken MUST.
sipnab has always found hold through a=sendonly and a=inactive.
RFC 3264 section 8.4 describes a third mechanism that RFC 2543 defined and section 8.4 discourages:
setting the connection address to 0.0.0.0. Until this rule, a call held that
way looked to sipnab like a call that simply stopped.
RFC 3264 section 8.4 keeps one legitimate use — an initial offer from an agent that does not yet know its own address — so the first SDP body in a dialog stays exempt and a later one does not. A stream declined with port zero stays exempt as well.
One message cannot say whether a request sits inside a dialog. A message-scoped
version of this rule fires on every re-INVITE in every capture that starts
mid-call, which is most captures.
Two shapes settle it. A REGISTER never sits inside a dialog, so a To tag
there is wrong wherever the capture started. Otherwise the rule needs the
answer to that same transaction, matched on the Via branch that RFC 3261 section 8.1.1.7 defines, to carry a
different tag — proof that the responder treated the request as new and chose
its own.
An earlier form of this rule compared against every response in the dialog and
fired on 2,182 dialogs of the validation corpus, 2,160 of them SUBSCRIBE. The
cause was not subscriptions. A SUBSCRIBE dialog carries NOTIFY requests in
the reverse direction, and a response to a NOTIFY correctly carries the
subscriber's tag, which is not the tag the SUBSCRIBE addressed. Matching on
the transaction took the count to zero.
Measured against the local validation corpus: 62 readable captures, 39,239 dialogs, 3,371 of them carrying SDP and 272 with RTP linked to them.
| Rule | Dialogs | Reading |
|---|---|---|
SIP-3261-17.1.1.3-ACK-BRANCH-MISMATCH |
129 (0.3%) | Real hits, and the rate is the one that matters: a rule that read every ACK rather than only the non-2xx ones would have fired on a large share of the 39,239. |
SIP-3261-12.1.1-RECORD-ROUTE-NOT-COPIED |
55 (0.1%) | Real hits. |
SDP-7587-7-OPUS-RTPMAP-RATE |
16 | Real hits. |
SDP-3264-8.3.2-DYNAMIC-PT-REBOUND |
2 | Real hits, and rare, which is what a rule about a re-INVITE changing a codec binding should be. |
SIP-3261-16.6-RECORD-ROUTE-NOT-LOOSE |
0 |
Exercised and declined. RECORD-ROUTE-NOT-COPIED fires 55 times on the same corpus, and it only reaches its own body when a request carries a bracketed Record-Route — the exact values this rule reads. So the corpus does hold recorded routes, and every one of them carries lr. |
SIP-3261-7.3.1-SINGULAR-HEADER-REPEATED |
0 | Not exercised. Its silence rests on its unit tests, not on this traffic. |
SIP-3261-8.1.1.7-VIA-BRANCH-DUPLICATE |
0 | Not exercised, and a corpus with no looping request is the expected shape of one. |
SDP-3264-7-TELEPHONE-EVENT-ONE-WAY |
0 | Not exercised. |
SDP-3264-8.2-REJECTED-STREAM-ATTRIBUTES |
0 | Not exercised. |
The distinction in that last column is the point. An unexercised rule and a rule with nothing to find produce the same row, and only this note tells them apart — see the same caveat for the RFC 3262 rules above.
RFC 3261 section 17.1.1.3 says an ACK "MUST contain a single Via header field,
and this MUST be equal to the top Via header field of the original request".
Reused verbatim, that sentence would report the correct behavior on every
answered call in every capture ever taken — because the same section opens by
sending the 2xx case somewhere else: "A UAC core that generates an ACK for 2xx
MUST instead follow the rules described in Section 13."
An ACK to a 2xx is a new transaction. RFC 3261 section 13.2.2.4 builds it from the dialog's
route set, and section 8.1.1.7 requires a new branch for it. An ACK to a non-2xx is
hop-by-hop, absorbed by the same INVITE server transaction, and shares that
transaction's branch — which RFC 3261 section 8.1.1.7 names as one of exactly two exceptions to
branch uniqueness, alongside CANCEL.
So the rule reports only where the capture has already shown a final response
between 300 and 699 for that CSeq. An ACK whose INVITE the capture never
carried settles nothing, so sipnab skips it.
RFC 3261 section 12.1.1 makes the UAS "copy all Record-Route header field values from the request into the response" and "MUST maintain the order of those values", and section 12.1.2 has the caller build its route set from the response, in reverse. A value the response dropped is a proxy removed from a path it recorded itself into. A value reordered sends every in-dialog request through the hops backwards. Both fail after the call is up, which is why they arrive as a network ticket rather than a signaling one.
The rule does not report a response carrying more values than the request. A capture taken anywhere but at the UAS sees the request before the last recording proxy inserted its value and the response after — so "the response has one extra" is the ordinary shape of a proxy-side capture, and an equality comparison would fire on most of them. The rule reports only the request's own list going missing from the tail of the response's.
The telephone-event rule is not in RFC 4733
The widely held belief is that RFC 4733 requires both ends to agree a
telephone-event payload type. It does not. RFC 4733 contains no offer/answer
rule at all: section 2.5.1.1 says negotiation happens "by out-of-band means, using SDP,
for example" and never says what an omitted telephone-event means. This
rule's author read the document end to end, and the only sender-side obligation
in it is about the events parameter.
The binding text is RFC 3264 section 7: "The offerer MAY immediately cease listening
for media formats that were listed in the initial offer, but not present in the
answer." A MAY, so nothing here breaks — which is why the rule reports as
interop and not as a MUST violation, and why it cites RFC 3264.
What makes it a one-way fault rather than simply no DTMF is that equipment sends the event anyway on the payload type it offered. The far end receives a payload type it never agreed to and either drops it or decodes it as audio, while DTMF in the other direction works — which is the whole of every "the IVR cannot hear our digits" ticket.
The rule needs the stream accepted (port non-zero) and sharing at least one
audio format. A declined stream negotiated nothing, and a stream with no common
format is already SDP-3264-6.1-ANSWER-NO-COMMON-FORMAT.
RFC 3264 section 6 is where this is usually attributed, and section 6 does not say it. Its whole statement about a rejected stream is: "To reject an offered stream, the port number in the corresponding stream in the answer MUST be set to zero. Any media formats listed are ignored. At least one MUST be present, as specified by SDP." Nothing about attributes.
The attribute sentence is in RFC 3264 section 8.2: "the answer MAY omit all attributes present
previously, and MAY list just a single media format." A MAY — so keeping
them is legal, and the rule reports at notice under interop.
It is still worth a line, for two reasons the finding names in its observed
field. An a=crypto on a declined stream is SRTP key material published for a
stream that never carries a packet. And equipment that reads attributes before
it reads the port allocates a relay leg and a transcoder for a stream nobody
answered.
A stream the offer already removed at port zero is exempt: that is RFC 3264 section 8.2's own mechanism for tearing a stream down, and the answer marking it zero too is what section 8.2 requires.
RFC 7587 section 7 is unambiguous: "The RTP clock rate in a=rtpmap MUST be 48000,
and the number of channels MUST be 2." Every example in the RFC writes
opus/48000/2, including the one titled "16000 Hz clock rate" — an endpoint
signals the narrower band with maxplaybackrate in a=fmtp, never in the
rtpmap. An a=rtpmap carrying no channel count is opus/48000/1 by RFC 4566 section 6's default,
and RFC 7587 section 7 admits neither.
The rule cites RFC 7587 section 7 and not section 4.1. Section 4.1 states the same 48 kHz clock as a fact about the wire — "The RTP timestamp is incremented with a 48000 Hz clock rate for all modes of Opus and all sampling rates" — and it is not RFC 2119 language. The SDP bullet in RFC 7587 section 7 is the only place the requirement is a MUST.
The observation half is deliberately absent. "Opus negotiated, and the wire carries 160-octet packets at an 8 kHz cadence" is not decidable from what a stream records. 160 octets per 20 ms is 64 kbit/s, which is exactly G.711 and is also a legal Opus CBR configuration. Separating the two needs the RTP timestamp cadence, and the stream store keeps a last timestamp and no first one, so nothing in it yields a clock rate. A rule that reported legal Opus CBR as a defect would not survive week one, and this one is decidable from the SDP alone.
SIP-3261-7.3.1-SINGULAR-HEADER-REPEATED reads the rule the section actually
states: "Multiple header field rows with the same field-name MAY be present in a
message if and only if the entire field-value for that header field is
defined as a comma-separated list."
The same paragraph writes down its own exception: WWW-Authenticate,
Authorization, Proxy-Authenticate and Proxy-Authorization may appear on
several rows, and no sender may combine them with commas. A 407 carrying two
challenges is ordinary traffic, so the rule's list omits all four.
The compact forms count. The parser expands them at parse (RFC 3261 section 7.3.3), so i:
beside Call-ID: is two rows of one field — which is exactly the shape a
header-smuggling attempt takes, because a parser that reads only one spelling
sees a message with one Call-ID.
RFC 3261 section 8.1.1.7 makes a branch "unique across space and time", and section 16.6
item 8 spells out what that means for a proxy: "the branch parameter will be
different for different instances of a spiraled or looped request through a
proxy." Two identical branches in one Via stack therefore say the request
returned to an element that failed to re-derive its own value — the loop
RFC 3261 section 16.3's loop-detection step exists to catch, running unbounded until
Max-Forwards stops it.
The rule reports a repeated value once however many times it repeats, and reads requests only.
A rule that fires on nearly every dialog is a bug in the rule, not a discovery
about the traffic. tests/corpus_lint_test.rs runs the whole catalog over a
directory of captures named by SIPNAB_CORPUS, prints a hit count per rule,
and fails when any rule trips more than 95% of dialogs.
That test also reports how many dialogs carried media the observation rules
could read. A zero hit rate on an OBS- rule means one of two very different
things — the traffic is clean, or the rule saw nothing — and the rule table
alone cannot tell them apart.
use sipnab::sip::lint::{LintConfig, Linter, ObservedMedia, Ruleset};
let config = LintConfig::new()
.with_ruleset(Ruleset::Must)
.suppress_list("OBS-*, SIP-3261-19.1.1-URI-PARAM-DEMOTED");
let linter = Linter::new(config);
// Signaling only.
// let findings = linter.lint_dialog(&dialog);
// Signaling against the media observed for it.
// let media = ObservedMedia::from_streams(streams.streams_for(&dialog.call_id));
// let findings = linter.lint_dialog_with_media(&dialog, &media);ObservedMedia::from_streams projects the RTP the stream store attributed to
the dialog. RTCP arrives separately through with_rtcp, because the stream
store folds reception reports into the stream they describe and keeps no record
of which port they landed on — which is the question RFC 5761 section 5.1.1 asks.
See Library API for the wider crate surface.
Website · Repository · Issues · Generated from docs/ — edit there, not here.
Getting started
- Installation
- Cookbook
- Runnable Examples
- Real World Captures
- Troubleshooting
- Tuning Capture
- Encapsulations
- Rtpengine Relays
- Backers
Using the TUI
CLI & automation
- CLI Reference
- Filter DSL
- Output Formats
- SIP Response Codes
- SIP Methods
- SIP Header Fields
- SIP Parameters
- SIP Lint Rules
- MOS and Codecs
Configuration
Integrations (API & MCP)
- REST API
- Prometheus Metrics
- Authentication
- MCP
- MCP Deployment
- MCP Across an Estate
- MCP Tools
- MCP Protocol
- Uprobe Walkthrough
- Capture SIP over TLS
- WASM Plugins
- vCon Export
- vCon Capture Stack
Development & internals
- Internals Index
- Internals Subsystem Guide
- Internals Invariants
- Internals Testing
- Internals Walkthroughs
- Internals Build CI Release
- Internals Profiling
- Internals Domain Primer
- Library API
- Benchmarks
- Fault Model
- Architecture
- Internals Threading
- Internals TUI Testing
- Internals Zero Copy Payloads
- Internals Uprobe Capture
- Internals Rtpengine Control Plane
- Internals Relay Control Decoding
- Internals vCon Exporter