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, signals and tui) 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 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,
  • 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. This includes 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 Packet → ParsedPacket
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. 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] thiserror enums (std::error::Error).
  • So you can propagate them into anyhow/Box<dyn Error> unchanged.
  • Or you can 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}"),
}

Every one of these enums is #[non_exhaustive].

  • So 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 carry #[non_exhaustive] for the same reason. Examples are TransportProto, RtcpPacket, FraudType and CipherSuite.
  • 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_discover and uprobe_sock_offsets report on the host rather than a capture.
  • tls_plaintext_records decodes records laid out the way the BPF TLS probe writes them, as Runnable client examples describes.
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

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 example export_vcon without vcon:

    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’s Library entries 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_packet or parse_sip filters 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, api and mcp pull in their respective subsystems.
  • See the feature table in install.md.