Library API - NormB/sipnab GitHub Wiki

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, 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.
  • 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

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_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.
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 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, 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.
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

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.
โš ๏ธ **GitHub.com Fallback** โš ๏ธ