Docs Use sipnab as a Rust library
Use sipnab as a Rust library
Use the Rust parser and analysis types from an external application.
On this page
sipnab is primarily a CLI/TUI tool. Its analysis engine is also a Rust library on crates.io.
- The curated public API is re-exported at the crate root.
- Anything under a
#[doc(hidden)]module (cli,privilege,process_isolation,settings,signalsandtui) is binary-internal.
Stability
The library API is not stable.
- The supported way to use sipnab is the program,
cargo install sipnab. - sipnab releases often.
- Any release, including a patch release, can rename, move or remove a public item.
That matters because of how cargo reads a version.
- A requirement of
"0.5"means “any 0.5.x release”. - So
cargo updatecan move your build to a release that no longer compiles against your code. - Pin the exact release you built against, with
=:
[dependencies]
# Replace N with the patch number of the release you tested.
sipnab = { version = "=0.5.N", default-features = false, features = ["native"] }
Then upgrade on purpose:
- change the pin,
- rebuild,
- read the changelog for what moved.
Public parser types
sipnab::bytesandsipnab::chronore-export the buffer and timestamp types used in public signatures.- An application can import
sipnab::{bytes::Bytes, chrono::Utc}without adding matching dependency versions. - After parsing,
SipMessage::parse_errorreports whether the message was only partially parseable. This includes a truncated declared body.
Crate-root surface
| Item | What it is |
|---|---|
PcapReader | Pure-Rust pcap/pcapng reader (iterator of packets) |
decompress_capture | Transparent, bounded gunzip for gzip-compressed captures |
sip::parser::parse_sip / parse_sip_bytes | Parse raw bytes → SipMessage |
capture::parse::parse_packet | Decode a captured Packet → ParsedPacket |
rtp::parser::parse_rtp_header | Parse an RTP header |
sip::sdp::parse_sdp | Parse an SDP body |
DialogStore / StreamStore | Capped, indexed dialog / RTP-stream stores (both Debug) |
SipMessage, SipDialog, RtpStream | Core value types |
SipMethod, DialogState | The method and dialog-state enums those types report in. Both #[non_exhaustive] — see Error handling |
StreamKey | What a StreamStore indexes by: SSRC plus the source and destination sockets, so one SSRC arriving on two paths stays two streams |
FilterExpr | Compiled filter-DSL expression |
estimate_mos | E-model MOS from jitter/loss/codec |
PcapReader yields the capture file’s own records.
parse_packetdecodes aPacket. That is the same frame plus the facts only the caller can supply:- the timestamp at full resolution,
- the captured-vs-wire lengths,
- the link type. A multi-interface pcapng varies it per packet rather than per file.
- Building that bridge is the one step between a path and a parsed frame:
use sipnab::PcapReader;
use sipnab::capture::Packet;
use sipnab::capture::parse::parse_packet;
fn payload_bytes(path: &str) -> Result<usize, Box<dyn std::error::Error>> {
let data = std::fs::read(path)?;
let mut total = 0;
for pkt in PcapReader::new(&data)? {
let timestamp = sipnab::chrono::DateTime::from_timestamp(
pkt.timestamp_secs as i64,
pkt.timestamp_usecs * 1000,
)
.unwrap_or_default();
let caplen = pkt.data.len();
let origlen = pkt.orig_len as usize;
let link_type = pkt.link_type as i32;
let frame =
Packet::new(timestamp, pkt.data, caplen, origlen, pkt.interface, link_type);
// A capture holds frames sipnab does not decode — ARP, a truncated
// header, an unsupported link type. That is an `Err` per frame, not a
// failed read: skipping it silently is how a partial result comes to
// look like a clean one.
if let Ok(parsed) = parse_packet(&frame) {
total += parsed.payload.len();
}
}
Ok(total)
}
Error handling
The parsing and capture entry points return typed, matchable error
enums. They do not return anyhow::Result.
- All three are
#[non_exhaustive]thiserrorenums (std::error::Error). - So you can propagate them into
anyhow/Box<dyn Error>unchanged. - Or you can match on their variants.
| Function | Returns |
|---|---|
parse_sip, parse_sip_bytes, parse_rtp_header, parse_sdp | Result<_, ParseError> |
parse_packet, PcapReader::new | Result<_, CaptureError> |
config::Config::load, address/rule/CIDR parsing | Result<_, Error> |
ParseError— protocol parsers. Variants includeEmpty { what },TooShort { what, need, got },InvalidUtf8 { what },MissingCrlf,NotSip { line },InvalidStatusCode { code },BadRtpVersion { version },SdpMissingVersion,BadSdpVersion { version }.CaptureError— capture-file / packet decode. Variants includeTooShort { what, need, got },UnsupportedLinkType(i32),EncapTooDeep { kind, limit },NotIp { what },NoTransport,Icmp,NetMonFormat,UnknownFormat { magic }.Error— config/CLI/validation surface.ConfigRead/ConfigParsechain the underlyingstd::io::Error/toml::de::Errorvia#[source].
Match on variants rather than message text:
use sipnab::ParseError;
use sipnab::rtp::parser::parse_rtp_header;
match parse_rtp_header(&[0x80, 0x00]) {
Err(ParseError::TooShort { need, got, .. }) => {
eprintln!("need {need} bytes, got {got}");
}
Ok(header) => { /* use header */ }
Err(e) => eprintln!("parse failed: {e}"),
}
Every one of these enums is #[non_exhaustive].
- So a downstream
matchmust include a wildcard arm. - sipnab can add a variant in a minor release without it being a breaking change.
- Twenty-four other public enums carry
#[non_exhaustive]for the same reason. Examples areTransportProto,RtcpPacket,FraudTypeandCipherSuite. - Three public structs carry it too:
BurstGapAnalysis,RemoteReceptionReport,RemoteVoipMetrics.- A downstream literal therefore cannot construct them.
Worked examples
Four of the seven programs in examples/ take the library path.
- They run against a real capture and print a real answer.
- Each exists because it demonstrates something a doctest on this page structurally cannot: state accumulated across a whole file.
Of the other three:
uprobe_discoveranduprobe_sock_offsetsreport on the host rather than a capture.tls_plaintext_recordsdecodes records laid out the way the BPF TLS probe writes them, as Runnable client examples describes.
| Example | Run it against | What it is for |
|---|---|---|
call_summary.rs | any capture with SIP | The path above carried to its end: file → frame → message → call. Dialog state is what you learn from every packet in order, so one INVITE in a doctest can only ever print Trying. |
rtp_quality.rs | rtp-protocol.pcap, then invite-opus-bye.pcap | Jitter, loss and MOS per stream — none of which exist in a packet. The second capture shows StreamStore declining to score a stream whose clock rate no a=rtpmap grounded. |
filter_dialogs.rs | any capture, plus an expression | FilterExpr::parse once, then select_dialogs — the join across both stores that --report and --json-dialogs also go through, and the reason those two never disagree about a capture. |
export_vcon.rs | any capture with SIP | The same four steps plus a fifth: one reconstructed call out as a vCon container a conserver accepts. Needs --features vcon, which the default build omits. |
cargo run --features native --example call_summary -- tests/pcap-samples/register-invite-reinvite-bye.pcap
On 2026-11-03, from a checkout of sipnab 0.5.232, it printed:
229 frames, 0 undecodable, 12 SIP messages, 2 dialogs
REGISTER Registered [200] alice -> alice (2 messages) 1-1177095@127.0.0.1
INVITE Completed [200] alice -> bob (10 messages) 1-1177106@127.0.0.1
When an example does not build or run:
-
An example run without the feature it needs says so and exits
2, for exampleexport_vconwithoutvcon:This example needs the `vcon` feature: cargo run --features vcon --example export_vcon -- <capture.pcap>Add the feature it names.
-
Code that compiled against an earlier release no longer compiles: a public item moved or changed. Pin the release with
=as Stability says, and read the changelog’sLibraryentries for what moved. -
The library finds more SIP than the binary on a capture that signals off port 5060: that is the port policy described below, not a defect.
One difference bites before any other:
- The binary defaults to
--portrange 5060-5061. - The library applies no port policy at all. Nothing in
parse_packetorparse_sipfilters by port. - A capture signaling on 5080 gives:
- the library twelve SIP messages,
- the tool “No SIP signaling found” until you pass
--portrange 1-65535.
- Neither is wrong. They answer different questions.
- Knowing that is cheaper than rediscovering it against a capture you care about.
Features
The crate is heavily feature-gated (see Cargo.toml).
- For pure parsing you only need
native. tls,hep,apiandmcppull in their respective subsystems.- See the feature table in install.md.