Repository navigation
Internals Lint Rules
The reasoning behind the SIP conformance rules: why each rule cites the section it cites, why some rules are narrower than the sentence they implement, and what the validation corpus measured for each one.
The operator page, SIP conformance rules, lists the rules, shows how to run and suppress them, and says how to fix the SIP each one flags. This page records why the rules have the shape they have. Read it before you change a rule's scope, severity or citation.
The code is in src/sip/lint/.
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.
-
src/sip/diagnosis.rsanswers "why did this call fail". - The 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.
The RFC number and the section are data, not prose inside a sentence. That choice caught a mistake while the 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.
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.
Three of the observation 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.
Opus, AMR and the other variable-rate codecs have no fixed octet rate, so the two size rules skip them rather than invent a threshold.
Three of the syntactic 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.
An unexercised rule and a rule with nothing to find produce the same row. Only this note tells them apart.
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 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.
- 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.
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 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:alice@example.com;tag=1928301774 trips neither.
RFC 3261 uses that form in its own examples.
tag is a header parameter, so it belongs where it sits.
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.
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
SDP-3264-8.4-HOLD-CONNECTION-ZEROexisted, 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. That is why the first SDP body in a dialog is exempt.
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.
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.
- The same caveat applies to 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.
- 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.
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. - 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.
The rule needs the stream accepted (port non-zero) and 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.
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.
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.
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 be wrong on ordinary traffic. 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-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.
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.
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.
ObservedMedia::from_streams projects the RTP that the stream store attributed to
the dialog. RTCP arrives separately through with_rtcp.
- 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.
Website · Repository · Issues · Generated from docs/ — edit there, not here.
Getting started
- Installation
- First CLI Triage
- Glossary
- Cookbook
- Runnable Examples
- Real World Captures
- Troubleshooting
- Tuning Capture
- Encapsulations
- Rtpengine Relays
- Backers
Using the TUI
CLI & automation
- CLI Reference
- Error Messages
- 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 Tools Core
- MCP Tools Signaling
- MCP Tools Captures
- MCP Tools Security
- MCP Tools Media
- MCP Tools Relay
- MCP Tools TFPS
- MCP Tools Server
- MCP Tools vCon
- MCP Tools TLS
- MCP Protocol
- Uprobe Walkthrough
- Capture SIP over TLS
- WASM Plugins
- vCon Export
- vCon Capture Stack
- G729 Decoding
Your SIP server
- Use OpenSIPS as the SIP Server
- Run sipnab beside OpenSIPS
- Use Kamailio as the SIP Server
- Run sipnab beside Kamailio
- Use Asterisk as the PBX
- Run sipnab beside Asterisk
- Use FreeSWITCH in the Voice Stack
- Run sipnab beside FreeSWITCH
Add to your voice stack
- Add a vCon Server
- Send vCons to a vCon Server
- Send vCons to vcon store
- Add TFPS
- Control TFPS from sipnab
- Ban SIP Scanners with fail2ban
- Feed fail2ban from sipnab
- Add rtpengine
- Name rtpengine Media with sipnab
- Add rtpproxy
- Name rtpproxy Media with sipnab
- Add Homer
- Connect sipnab to Homer
- Add Prometheus and Grafana
- Add sipnab Metrics to Prometheus
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
- Threat Model
- Assurance Case
- Architecture
- Internals Threading
- Internals TUI Testing
- Internals Zero Copy Payloads
- Internals Uprobe Capture
- Internals Rtpengine Control Plane
- Internals Relay Control Decoding
- Internals vCon Exporter
- Internals Operator Guide Notes
- Internals Postmortems
- Internals Code Map
- Internals Lint Rules
- Internals Encapsulation Decoding
- Internals MCP Design Notes
- Internals Metrics And Output Notes