Docs Library API

Library API

Use the Rust parser and analysis types from an external application.

On this page

sipnab is primarily a CLI/TUI tool, and 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, tui, privilege, …) 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, and 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 update can 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, and read the changelog for what moved.

Public parser types

sipnab::bytes and sipnab::chrono re-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_error reports whether the message was only partially parseable, including a truncated declared body.

Crate-root surface

ItemWhat it is
PcapReaderPure-Rust pcap/pcapng reader (iterator of packets)
decompress_captureTransparent, bounded gunzip for gzip-compressed captures
sip::parser::parse_sip / parse_sip_bytesParse raw bytes → SipMessage
capture::parse::parse_packetDecode a captured PacketParsedPacket
rtp::parser::parse_rtp_headerParse an RTP header
sip::sdp::parse_sdpParse an SDP body
DialogStore / StreamStoreCapped, indexed dialog / RTP-stream stores (both Debug)
SipMessage, SipDialog, RtpStreamCore value types
SipMethod, DialogStateThe method and dialog-state enums those types report in. Both #[non_exhaustive] — see Error handling
StreamKeyWhat a StreamStore indexes by: SSRC plus the source and destination sockets, so one SSRC arriving on two paths stays two streams
FilterExprCompiled filter-DSL expression
estimate_mosE-model MOS from jitter/loss/codec

PcapReader yields the capture file’s own records. parse_packet decodes a Packet, which is the same frame plus the facts only the caller can supply — the timestamp at full resolution, the captured-vs-wire lengths, and the link type, which a multi-interface pcapng varies 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 — not anyhow::Result. All three are #[non_exhaustive] thiserror enums (std::error::Error), so you can propagate them into anyhow/Box<dyn Error> unchanged, or match on their variants.

FunctionReturns
parse_sip, parse_sip_bytes, parse_rtp_header, parse_sdpResult<_, ParseError>
parse_packet, PcapReader::newResult<_, CaptureError>
config::Config::load, address/rule/CIDR parsingResult<_, Error>
  • ParseError — protocol parsers. Variants include Empty { what }, TooShort { what, need, got }, InvalidUtf8 { what }, MissingCrlf, NotSip { line }, InvalidStatusCode { code }, BadRtpVersion { version }, SdpMissingVersion, BadSdpVersion { version }.
  • CaptureError — capture-file / packet decode. Variants include TooShort { what, need, got }, UnsupportedLinkType(i32), EncapTooDeep { kind, limit }, NotIp { what }, NoTransport, Icmp, NetMonFormat, UnknownFormat { magic }.
  • Error — config/CLI/validation surface. ConfigRead / ConfigParse chain the underlying std::io::Error / toml::de::Error via #[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}"),
}

Because every one of these enums is #[non_exhaustive], a downstream match must include a wildcard arm — sipnab can add a variant in a minor release without it being a breaking change. Twenty-four other public enums (TransportProto, RtcpPacket, FraudType, CipherSuite, …) carry #[non_exhaustive] for the same reason, as do three public structs (BurstGapAnalysis, RemoteReceptionReport, RemoteVoipMetrics), which a downstream literal therefore cannot construct.

Worked examples

Four of the six 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. (The other two, uprobe_discover and uprobe_sock_offsets, report on the host rather than a capture.)

ExampleRun it againstWhat it is for
call_summary.rsany capture with SIPThe 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.rsrtp-protocol.pcap, then invite-opus-bye.pcapJitter, 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.rsany capture, plus an expressionFilterExpr::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.rsany capture with SIPThe 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

One difference bites before any other: the binary defaults to --portrange 5060-5061, and the library applies no port policy at all. Nothing in parse_packet or parse_sip filters by port. A capture signaling on 5080 gives the library twelve SIP messages and the tool “No SIP signaling found” until you pass --portrange 1-65535. Neither is wrong — they answer different questions, and 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, api and mcp pull in their respective subsystems. See the feature table in install.md.