Engineering notes

Hand over evidence somebody else can check

howto

You found the answer. Now a carrier that does not trust you has to confirm it. Four artifacts, each answering a different doubt, and the one thing sipnab deliberately refuses to claim.

The investigation is the easy half. The hard half starts when the answer has to survive contact with a reader whose interest runs the other way — a carrier disputing a fault, a customer disputing a bill, a colleague six months from now who does not remember any of this.

That reader asks four different questions, and each one wants a different artifact. Sending a pcap answers none of them well: it is large, it carries traffic belonging to everybody else, and it says nothing about what your tap missed.

What happened on the call

sipnab -N -I capture.pcap --export-vcon-when "state == 'Failed'" --export-vcon-dir ./evidence/ --no-cli-print

One container per matching dialog, in the format draft-ietf-vcon-vcon-core defines, carrying the parties, the timing and the full message trace. The selector is the same filter language --filter speaks, so the policy is yours rather than an enumeration of the cases somebody thought of.

Both write paths stage under a dot-prefixed sibling, flush, rename, and sync the directory, so a bridge polling that spool never sees a half-written container and a crash mid-write does not destroy the previous one.

Whether the container is the one you sent

sipnab -N -I capture.pcap --export-vcon-when "state == 'Failed'" --export-vcon-dir ./evidence/ --vcon-digest > ./evidence/SHA256SUMS

--vcon-digest prints <sha256> <filename> on stdout while progress stays on stderr, in sha256sum’s own format, so the redirect above produces a file the standard tool reads with no glue. Run the check from inside the export directory, because the file names each container relative to itself:

sha256sum -c SHA256SUMS
1-1968_192.0.2.20-cdd887baa4507e9a.vcon.json: OK

That is a digest and deliberately NOT a signature, and the reason is worth understanding before somebody asks for one. A signature over the bytes sipnab emits could never verify against the object a store holds, because a conserver adds fields on ingest — so the signature would fail for the ordinary reason and tell the operator nothing. A digest makes a smaller and honest claim: this is what sipnab wrote, at this path, at this moment. It says nothing about the conversation and nothing about what a store did afterwards. What it buys is a way to bind your emission to the store’s own ledger entry out of band, so that “is the container you have the one we sent?” has an answer neither side has to take on faith.

What the capture missed

This is the artifact that separates evidence from a claim. vCon has no field meaning “this record is incomplete” — dialog.type: "incomplete" accuses the CALL of not completing, which is a statement about the traffic rather than about the tap. So sipnab carries the caveat in the analysis object and in a sipnab-capture-completeness attachment, both built from one value so the two cannot disagree:

"capture_completeness": {
  "frames_read": 852,
  "undecodable_frames": 0,
  "sip_discarded_by_port_gate": 0,
  "sip_discarded_by_websocket_gate": 0,
  "messages_evicted": 0,
  "dialogs_rotated": 0,
  "dialogs_refused": 0,
  "headers_dropped_oversize": 0,
  "blind_spots": [],
  "media": "not-considered",
  "node": "capture-01",
  "sipnab_version": "0.5.142"
}

Read media first. It takes one of four values — carried, refused-over-budget, none-decodable, or not-considered — and the distinction is the whole point: an absent recording object never has to read as a call that had no audio. The budget behind refused-over-budget has a note of its own.

The block also renders as prose, which is what a non-technical reader actually reads:

sipnab OBSERVED this dialog and took no part in it: the parties below are what
the From and To headers said, not identities anyone established, and nothing
here is signed. This container carries SIGNALING ONLY — no media, and no
reference to media held elsewhere. That is a fact about this EXPORT, not about
the call: nothing here says whether the conversation carried audio. sipnab read
852 frame(s) for this capture. No omissions recorded: every message sipnab held
for this dialog is in this container.

Note the sentence in the middle. A signaling-only container states that it is signaling-only, so nobody can read the missing audio as a silent call.

Which command produced it

A report says what sipnab concluded and nothing says which invocation produced it — which capture, which filter, which port range:

sipnab -N -I capture.pcap --report --run-provenance-file /var/log/sipnab/runs.jsonl

One JSON line per run, appended, created mode 0600 because argv holds capture paths and a path holds a customer name:

{ "record": "run", "seq": 1,
  "argv": ["sipnab", "-N", "-I", "capture.pcap", "--report"],
  "cwd": "/srv/captures", "user": "voip", "uid": 1000,
  "started": "2026-09-02T02:03:30.547317594+00:00",
  "version": "0.5.142 features: native,tui,audio,tls,hep,api,mcp,mcp-http,metrics,plugins,vcon",
  "capture": { "node": "capture-01", "instance": "27f4618d15ea9d4509f3e-1" } }

The capture.instance is the same string every MCP and REST answer carries, so an artifact joins back to the command that made it. A record sipnab cannot write stops the run, before it has read a single packet. A best-effort line would be worse than none, because its absence would mean either “not enabled” or “the disk was full” and nobody could tell which.

Validate before you send

A store that refuses a container reports the refusal to whoever POSTed it, never to whoever built it. Check first:

// validate_vcon { "call_id": "..." }
{ "verdict": "valid", "errors": [], "deviations": [], "explanations": [],
  "schema_id": "https://ietf.org/vcon/schemas/unsigned-vcon.json" }

Three verdicts rather than two. valid-except-documented-deviation means every finding is a shape sipnab emits on purpose that the schema rejects, with deviations naming each and explanations saying why. Folding that into a clean pass would teach a producer that the deviation is fine in general, and some of those shapes really are defects when another producer emits them.

The claim sipnab refuses to make

Every party in the container carries validation: "none", and there is no signature anywhere. That is not an omission. sipnab watched packets go past a tap: it did not place the call, record it, or obtain anyone’s permission to keep it. A From header is what the sender chose to write, not an identity anyone established, and a container asserting otherwise would be the one field in the evidence that nothing measured.

sipnab’s own party in the array carries "role": "observer" for the same reason.

That restraint is what makes the rest of the container worth something. A reader who finds one over-claim in an artifact discounts all of it, and an observer that never claims more than it saw gives them nothing to discount.

The set

Send the containers, the SHA256SUMS beside them, and the provenance line for the run that made them. Between them they answer what happened, whether the bytes are yours, what the tap could not see, and which command produced the lot. If the containers have to leave your organization, redact them first.