SIP Lint Rules - NormB/sipnab GitHub Wiki
This page lists every rule that the conformance linter runs. For each rule it gives:
- the RFC section behind it,
- 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.
- Use it to suppress the rule in CI.
- Identifiers never change meaning.
- Nothing reuses a retired one.
What makes these rules different
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. - That class of defect stays invisible to any tool that reads only text.
Conformance is not the same question as outcome
src/sip/diagnosis.rsanswers "why did this call fail".- This linter answers "does this traffic obey the specification".
A call can complete over messages that break four MUSTs. A fully conformant call can hit a busy signal. Keep the two questions apart.
Reading a finding
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,
- 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
ContactURI in RFC 3261 section 20.10. - The sentence actually sits in the preamble of RFC 3261 section 20, above section 20.1.
A citation that nothing can read is a citation that nothing can check.
Severity and basis are separate axes
Severity says how much attention a finding deserves: error, warning,
notice, info.
Basis says what kind of claim the rule makes. 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.
- Some readers cannot tell a broken MUST from a vendor-compatibility hint. Those readers learn to discount both.
- The MUST is the one worth acting on.
Rulesets
Run a named subset instead of the whole catalog. Name it in one of these places:
- the
rulesetsparameter of the MCPlint_dialogtool, 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. |
Suppression
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. # starts a comment.
Put them in a .sipnablint. Check it in beside the config it belongs to.
- Both surfaces read the same file:
- the MCP lint tools take a
suppression_fileparameter, - the binary takes
--lint-suppress-file.
- the MCP lint tools take a
- 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. It reports:
- which file it applied,
- how many findings it silenced:
sipnab -N -I calls.pcap --lint --lint-fail-on error
sipnab exits 3 when a finding reaches the severity --lint-fail-on names.
So a CI job that runs this command fails on it.
An explicit list, for a CI job whose suppressions differ from the tree's:
sipnab -N -I calls.pcap --lint --lint-suppress-file ci.sipnablint
Everything, including what the project has agreed to live with:
sipnab -N -I calls.pcap --lint --lint-no-suppress
A file named with --lint-suppress-file that sipnab cannot open is a hard error.
It is not a full-catalog run.
- Pointing at a suppression list states an intent.
- Linting with every rule on would read as "my suppressions matched nothing".
sipnab looks for one in this order:
- next to the capture,
- then climbing toward the project root, the nearest ancestor holding a
.git. It stops there.
A capture living outside any project picks up nothing from above itself. A corpus mount or a shared drop are examples.
- Adopting a stranger's suppression list would switch off rules that nobody on this project turned off.
- The run would then come back clean for a reason four directories away.
Suppression never hides itself.
- Every
lint_dialogandvalidate_messageresponse names the file it applied. - It counts what that file silenced.
- Next to that, it gives 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. By default, a single rule reports at most 25 findings per dialog.
- A dialog retransmitting an
INVITEeleven times trips a message rule eleven times. - Every one of them is true. But printing all eleven buries the other rules.
- Change the cap in one of two ways:
- with
--lint-max-per-rule N, - with
[limits] lint_max_per_rulein the config file.
- with
Observation rules
These read the media against the declaration. Every one of them needs RTP or RTCP. sipnab must have attributed that RTP or RTCP 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. |
One-sided thresholds
Three of these rules compare one duration against another. 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.
- So a rule that fires on "slower than declared" fires on a large share of ordinary traffic.
- 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. The reason is the same: 20 ms packets stay 20 ms packets however far apart they arrive.
Codec shapes
Deriving a duration from a payload size needs a codec with a fixed octet rate. RFC 3551 Table 1 supplies five. 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.
Syntactic rules
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). |
What the corpus can and cannot vouch for
Three of these rules report zero against the local corpus. 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 carries aContact. - The rule reaches its own code path 1,989 times and declines each time.
- That 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. 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. Only this note tells them apart.
Session timers, and the section number that is easy to get wrong
RFC 4028 numbers its behavior sections 7 UAC, 8 Proxy, 9 UAS.
- The refresher obligation belongs to the UAS. So the rule cites RFC 4028 section 9.
- Recalling it as section 8 sends a reader to the proxy's rules. Those rules say something different about the same header field.
The citation here came from the table of contents in RFC 4028, not from memory. The angle-bracket rule cites the RFC 3261 section 20 preamble instead of section 20.10 for the same reason.
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.
- 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,
- its
CSeqmethod has to beINVITE.
Measured against the local corpus, 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.
- 471 of them are 2xx answers to
INVITE. - Every one of those 471 names a refresher.
- The rule reaches its own code path 471 times and declines each time.
- That is silence with evidence behind it. It is not a rule that cannot fire.
The 358 values in the corpus that carry no refresher are all requests.
- In a request, a UAC proposes a timer. RFC 4028 section 9 places no obligation there.
- A request offering a timer is the UAC proposing rather than answering.
- RFC 4028 section 9 puts the obligation on the answer.
No Session-Expires anywhere in the corpus sits below 90.
So both floor rules are quiet for the same checkable reason.
Why the bracket rules split in two
RFC 3261 section 20 gives one sentence for three characters. 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.
- It reads a different URI than the one sent.
- That is
SIP-3261-20-URI-BRACKETS. It goes in themustruleset.
A semicolon is different.
Contact: sip:a@b;transport=tcpparses as perfectly legal SIP. But it means something the sender did not intend.transportis a URI parameter.- Outside the brackets it lands on the header.
- Nothing on the wire breaks a MUST.
So
SIP-3261-19.1.1-URI-PARAM-DEMOTEDreports as interop. It stays out of themustruleset. - It remains the defect most often found at the bottom of a "calls reach the wrong trunk" ticket.
A bare From: sip:[email protected];tag=1928301774 trips neither.
RFC 3261 uses that form in its own examples.
tag is a header parameter, so it belongs where it sits.
Dialog rules
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. |
An answer listing an extra codec stays legal
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.
- It also explains why it rarely helps. The answerer cannot send with a format that the offer never listed.
So the MUST violation is the absence of any shared format.
SDP-3264-6.1-ANSWER-NO-COMMON-FORMATreports that as an error.- The extra listing reports separately, as info, under
interop. - Some equipment reads the answer as the negotiated set.
- It picks one of the extras.
- It sends media that the far end drops.
- That is worth knowing. It is not a broken MUST.
Hold by blanking the address
sipnab has always found hold through a=sendonly and a=inactive.
- RFC 3264 section 8.4 describes a third mechanism:
setting the connection address to
0.0.0.0.- RFC 2543 defined it.
- Section 8.4 discourages it.
- 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. That use is an initial offer from an agent that does not yet know its own address.
- So the first SDP body in a dialog stays exempt.
- A later one does not.
- A stream declined with port zero stays exempt as well.
Why the To tag rule is hard to trigger
One message cannot say whether a request sits inside a dialog.
- A message-scoped version of this rule fires on every re-
INVITEin every capture that starts mid-call. - That is most captures.
Two shapes settle it:
- A
REGISTERnever sits inside a dialog. So aTotag there is wrong wherever the capture started. - Otherwise the rule needs the answer to that same transaction to carry a different tag.
- The rule matches the answer on the
Viabranch that RFC 3261 section 8.1.1.7 defines. - A different tag is proof that the responder treated the request as new and chose its own.
- The rule matches the answer on the
An earlier form of this rule compared against every response in the dialog.
It fired on 2,182 dialogs of the validation corpus. 2,160 of them were SUBSCRIBE.
- The cause was not subscriptions.
- A
SUBSCRIBEdialog carriesNOTIFYrequests in the reverse direction. - A response to a
NOTIFYcorrectly carries the subscriber's tag. That is not the tag theSUBSCRIBEaddressed. - Matching on the transaction took the count to zero.
What the corpus says about the nine newest rules
Measured against the local validation corpus:
- 62 readable captures,
- 39,239 dialogs,
- 3,371 of them carrying SDP,
- 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.
- Only this note tells them apart.
- See the same caveat for the RFC 3262 rules above.
Why the ACK branch rule covers only non-2xx
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.
- The reason is that 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.
- section 8.1.1.7 requires a new branch for it.
An ACK to a non-2xx is different:
- It is hop-by-hop.
- The same INVITE server transaction absorbs it.
- It shares that transaction's branch.
RFC 3261 section 8.1.1.7 names this as one of exactly two exceptions to
branch uniqueness. The other is
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. sipnab skips it.
Why the Record-Route rule compares a suffix and not a list
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".
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. That 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. It sees the response after.
- So "the response has one extra" is the ordinary shape of a proxy-side capture.
- 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".
- It never says what an omitted
telephone-eventmeans. - This rule's author read the document end to end.
The only sender-side obligation in it is about the
eventsparameter.
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."
- That is a MAY, so nothing here breaks.
- That is why the rule reports as
interopand not as a MUST violation. - It is also why the rule cites RFC 3264.
This is a one-way fault, not simply no DTMF. The reason:
- Equipment sends the event anyway on the payload type it offered.
- The far end receives a payload type it never agreed to. It either drops it or decodes it as audio.
- DTMF in the other direction works.
- That is the whole of every "the IVR cannot hear our digits" ticket.
The rule needs two things:
- the stream accepted (port non-zero),
- the stream sharing at least one audio format.
A declined stream negotiated nothing.
A stream with no common format is already SDP-3264-6.1-ANSWER-NO-COMMON-FORMAT.
A declined stream that kept its attributes cites section 8.2 of RFC 3264, not section 6
RFC 3264 section 6 is where this is usually attributed. 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."
- That is a MAY, so keeping them is legal.
- The rule reports at
noticeunderinterop.
It is still worth a line. The finding names two reasons in its observed
field:
- An
a=cryptoon a declined stream is SRTP key material published for a stream that never carries a packet. - Some equipment reads attributes before it reads the port. That equipment allocates a relay leg and a transcoder for a stream nobody answered.
A stream that the offer already removed at port zero is exempt.
- That is RFC 3264 section 8.2's own mechanism for tearing a stream down.
- The answer marking it zero too is what section 8.2 requires.
Opus: the decidable half, and the half that is not
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. That includes the one titled "16000 Hz clock rate". - An endpoint signals the narrower band with
maxplaybackrateina=fmtp, never in the rtpmap. - An
a=rtpmapcarrying no channel count isopus/48000/1by RFC 4566 section 6's default. - 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".
- That 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. That is exactly G.711. It is also a legal Opus CBR configuration.
- Separating the two needs the RTP timestamp cadence.
- 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. This one is decidable from the SDP alone.
The four header fields that section 7.3.1 of RFC 3261 exempts
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-AuthenticateandProxy-Authorizationmay appear on several rows.- No sender may combine them with commas.
- A
407carrying 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:besideCall-ID:is two rows of one field. - That is exactly the shape a header-smuggling attempt takes.
A parser that reads only one spelling sees a message with one
Call-ID.
Two branches, one stack
RFC 3261 section 8.1.1.7 makes a branch "unique across space and time". 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."
- So two identical branches in one
Viastack say the request returned to an element that failed to re-derive its own value. - That is the loop that RFC 3261 section 16.3's loop-detection step exists to catch.
- The loop runs unbounded until
Max-Forwardsstops it.
The rule reports a repeated value once, however many times it repeats. It reads requests only.
Validating a rule against real traffic
A rule that fires on nearly every dialog is a bug in the rule. It is not a discovery about the traffic.
- runs the whole catalog over a directory of captures named by
SIPNAB_CORPUS, - prints a hit count per rule,
- fails when any rule trips more than 95% of dialogs.
That test also reports how many dialogs carried media that 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.
- The rule table alone cannot tell them apart.
Using the linter from Rust
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 that the stream store attributed to
the dialog.
- RTCP arrives separately through
with_rtcp. - The reason is that the stream store folds reception reports into the stream they describe.
- It keeps no record of which port they landed on.
- That port is the question RFC 5761 section 5.1.1 asks.
See Library API for the wider crate surface.