Architecture - NormB/sipnab GitHub Wiki
A codemap for contributors: what lives where, how data flows, and the design
decisions that explain the shape of the code. For the historical roadmap and
the full design-decision catalog (D1βD21), see design/implementation-plan-v6.md.
This file tracks the code as it exists.
sipnab is a single binary (plus the sipnab-audio dlopen'd plugin crate).
It runs either as a TUI (interactive, ratatui) or in batch/CLI mode
(parse β print/JSON/report β exit). Both modes share one library
(src/lib.rs). The binary (src/main.rs) wires CLI flags to library calls.
The run mode decides who drives. It does not decide who can ask: the same analysis is readable over four surfaces, all of them readers of the same two stores, none of them a second copy of the state.
| Surface | Flag | Who asks |
|---|---|---|
| Terminal | default | A person at a TUI, or a script reading text/JSON/NDJSON |
| REST API | --api |
Another program, over HTTP |
| MCP |
--mcp, plus --mcp-transport http to serve it over HTTP |
An AI agent, calling Model Context Protocol tools |
| Prometheus | --metrics |
A scraper |
MCP is not a wrapper bolted over the CLI. It is a first-class reader
(src/mcp/server.rs) that takes the same read locks
the TUI takes, which is why an agent can query a live capture while it is
still running. No tool mutates the analysis. For the write-back question and
why it stays closed, see
design/mcp-write-back.md.
Two surfaces answering one question derive the answer once. GET /v1/dialogs
and the MCP list_dialogs page each report which methods their match set
holds, and both call method_breakdown in src/sip/dialog.rs rather than
tallying separately, so the ordering rule and its tie-break cannot drift apart
between them. Each tallies the whole match set rather than the page it returns,
because the composition of one page answers a different question. A fact two
surfaces state is a fact one function derives.
The core is synchronous. Async (tokio) exists only at the edges, on one shared
runtime (src/app/servers.rs), for the optional API and
MCP servers.
capture sources per-packet path consumers
βββββββββββββββ βββββββββββββββββ ββββββββββ
live device ββ
pcap file ββ€ Packet PacketProcessor::process ParsedPacket
HEP listener ββΌβββΊ (Bytes) βββΊ link/IP decap, IP-frag βββΊ (payload = Bytes βββΊ pipeline::process_packet
stdin ββ channel + TCP reassembly, slice, zero-copy) β ws unwrap β SIP parse
TCP SIP framing β β RTCP β RTP/heuristic
βΌ
DialogStore βββββ SDP links βββββΊ StreamStore
(Arc<RwLock>) (Arc<RwLock>)
β² β²
TUI (try_read) ββ΄β outputs / API / MCP (read) ββββ
Parsing always happens outside the store locks. Each store is
write-locked once per packet, briefly. Payloads are bytes::Bytes slices of
the captured frame end to end (see docs/internals/zero-copy-payloads.md).
All four packet paths β live, batch, TUI file-open and the
--coressharded path (parallel.rs) β classify through the onepipeline::classify_packetrouter; only the per-path appliers differ.
For the same journey narrated packet by packet β every decision point, both store writes, and where each of the four paths diverges β see docs/internals/subsystem-guide.md.
Annotations cover only the load-bearing files. Siblings follow the same pattern.
src/
βββ lib.rs # curated public API; #[doc(hidden)] on bin-internal modules
βββ main.rs # binary: thin dispatcher into app/
βββ app/ # binary orchestration as library code
β βββ bootstrap.rs # CLI+config β RunPlan (mode/source/policy) + launch
β βββ batch.rs # BatchRunner: batch/offline receive loop, reports
β βββ servers.rs # API + MCP servers on one shared tokio runtime
β βββ tui_mode.rs # TUI mode entry
βββ cli.rs # clap definitions (the terminal SIP tools flag superset)
βββ config.rs # sipnabrc parsing/merging (toml_edit for surgical writes)
βββ pipeline.rs # THE shared per-packet protocol router (all four paths)
βββ parallel.rs # --cores N: shard-by-host-pair offline reconstruction
βββ auth.rs / crypto.rs # HMAC bearer tokens for api/mcp
βββ analysis.rs # capture-level verdict behind --analyze / find_problems
βββ expect.rs # capture thresholds as a pass/fail gate (MCP only)
βββ provenance.rs # capture identity + store generation (frame pointers)
βββ stun.rs # STUN/TURN parse and the request that never came back
βββ error.rs # typed error enums: Error (config/CLI), ParseError (parse_sip/_bytes/_rtp_header/_sdp), CaptureError (parse_packet/PcapReader) β all re-exported at the crate root
βββ names.rs # name resolution + [names.manual] persistence
βββ privilege.rs # setuid drop, chroot (drop early, drop hard)
βββ process_isolation.rs # isolated child for active responses (scanner kill)
βββ signals.rs # signal handling
βββ capture/ # sources + L2-L4
β βββ mod.rs # PacketProcessor: decap, IP-frag/TCP reassembly, TCP SIP framing
β βββ live.rs / file.rs / pcap_reader.rs # libpcap live, libpcap file, pure-Rust reader
β βββ channel.rs # capped packet channel (capture β processing)
β βββ parse.rs # link/IP/transport decap β ParsedPacket
β βββ reassembly.rs # IPv4/IPv6 fragments + TCP segments (RFC-annotated)
β βββ hep.rs # HEP v2/v3 in/out (Homer)
β βββ tls.rs / decrypt.rs / dtls.rs / rsa_key.rs # TLS record decryption (tls feature)
β βββ websocket.rs # WS frame unwrap (SIP over WebSocket)
β βββ tunnel/ # MPLS/PPPoE/GTP-U/VXLAN/NSH decap β one offset each
β βββ uprobe/ # eBPF uprobe TLS plaintext source (bpf feature)
β βββ merged.rs / mapped.rs / native.rs # merged-pcapng probe, mmap reader, AF_PACKET
β βββ writer.rs / atomic.rs / pcapng_meta.rs # pcap/pcapng export
βββ sip/
β βββ parser.rs # zero-copy-spine SIP parser (DoS caps on headers/line length)
β βββ message.rs # SipMessage (raw/body are Bytes slices; lazy accessors)
β βββ dialog.rs # SipDialog state machine
β βββ dialog_store.rs # capped store; Call-ID map + endpoint index; batched eviction
β βββ sdp.rs / sdp_timeline.rs # SDP parse; offer/answer timeline (hold/resume/T.38)
β βββ dsl.rs # --filter expression language (nom); regexes compiled at parse
β βββ matcher.rs # header/payload regex matching
β βββ lint/ # RFC conformance rules (message/dialog/media)
β βββ method.rs / response_codes.rs / timing.rs / stir_shaken.rs / siprec.rs
βββ rtp/ # first-class peer of sip/ (streams exist without dialogs)
β βββ stream.rs / stream_store.rs # RtpStream; SSRC-indexed capped store
β βββ parser.rs / rtcp.rs # RTP header, RTCP SR/RR/XR
β βββ quality.rs # jitter/loss/MOS, burst-gap
β βββ heuristic.rs # RTP discovery without SDP
β βββ diagnosis.rs # one-way audio, NAT mismatch
β βββ srtp.rs # SRTP auth+decrypt (tls feature)
β βββ dtmf.rs # RFC 4733 events
β βββ loss_map.rs # per-interval loss/jitter map behind the TUI's `L` view
β βββ g711.rs / opus_decode.rs / wav.rs / audio_export.rs / playback.rs
βββ security/ # passive-first detection
β βββ scanner_detect.rs / fraud_detect.rs / digest_leak.rs / reg_flood.rs
β βββ scanner_kill.rs # active response (via process_isolation)
β βββ tfps.rs # optional TFPS peer: find tfps_ctl, ask it, read the answer
β βββ alerting.rs # rule engine + event-exec hooks
βββ output/
β βββ model.rs # canonical DialogSummary/StreamSummary (one JSON+MCP wire shape)
β βββ cli_print.rs / json.rs / hexdump.rs / fail2ban.rs / wireshark.rs
β βββ dialog_report.rs / call_report.rs / synthetic.rs
β βββ api.rs # axum REST (api feature)
β βββ prometheus.rs / prometheus_server.rs
β βββ vcon.rs / vcon_schema.rs # vCon container export (vcon feature)
β βββ redact.rs # --redact rewriting, with the reversible map
β βββ event_exec.rs # external command hooks
βββ mcp/ # MCP server (mcp feature): server.rs, transport.rs, shape.rs
β βββ tools/ # tool bodies split out of server.rs, one module per group
βββ rtpengine/ # rtpengine ng control-plane decode (bencode, ng, control)
βββ relay/ # the media-relay seam: what sipnab needs from ANY relay
βββ plugin/ # WASM plugin host (plugins feature; no interpreter without it)
βββ llmnr/ # LLMNR (RFC 4795) decode + name store
βββ tui/
β βββ mod.rs # App state + event loop
β βββ state.rs # per-view state structs + derived-data caches (LadderCache/DisplayedCache), refreshed by App::sync_caches()
β βββ controllers/ # key/mouse handling, one module per view/popup (KeyAction mapping layer)
β βββ render/ # frame composition: mod.rs + popups.rs + status.rs (render pass is read-only)
β βββ call_list.rs / stream_list.rs / stream_detail.rs / msg_raw.rs
β βββ call_flow/ # ladder diagram: prepare.rs (layout) + render.rs + arrows/export
β βββ save.rs / help.rs / theme.rs
βββ wasm.rs # wasm-bindgen browser surface (wasm feature)
βββ test_utils.rs # shared SIP message builder for tests
crates/sipnab-audio/ # rodio/ALSA playback plugin, dlopen'd (no libasound NEEDED)
fuzz/ # cargo-fuzz targets (18), out-of-workspace; weekly run via .github/workflows/fuzz.yml
tests/property_test.rs # proptest properties (SIP/SDP round-trip, filter-DSL total-function)
harness/ # docker-compose e2e (opensips + rtpengine + sipp)
See docs/internals/threading.md for the full topology and lock discipline.
Short version: capture threads β bounded channel β one processing thread
that owns all store writes. TUI, API and MCP are readers. Batch --cores N shards
packets by host pair to worker threads with thread-local stores, merged at
EOF.
- D2 β Synchronous core, async only at the edges. The packet path is plain threads + channels; tokio appears only inside the optional API/MCP/Prometheus servers.
-
D3 β Zero-copy payload spine.
Packet.data,ParsedPacket.payload,SipMessage.raw/bodyare refcountedBytesslices of one buffer. Header parsing allocates lazily (canonical-name table +Cowunfold, WS4.1) β 3β1 allocations per header on the hot path. -
D10 β Feature gates keep the binary small.
native,tui,tls,hep,api,mcp,mcp-http,audio,wasm. CheckCargo.tomlbefore assuming a build includes a module. -
D11 β Key material is toxic waste.
zeroizeon key types, redactingDebugimpls,--tls-key/keylog material never logged. -
D13 β RTP is first-class. sipnab discovers streams heuristically even
without SIP/SDP;
rtp/never depends on a dialog existing. -
D15 β Privilege drop. sipnab sets
PR_SET_NO_NEW_PRIVSat startup on every run mode β root or not, because that flag has no precondition and the recommended--setup-capsinstall has no root to drop β then drops root right after socket open and disables core dumps whenever decryption keys are resident (src/privilege.rs);--chrootis available for daemon deployments. -
D16 β Process isolation: specified, not shipped. D16
(
docs/design/implementation-plan-v6.md:624) called for scanner-kill and the REST API to run in forked children behind a Unix socket pair. Neither does. Scanner-kill runs in thescanner-killthread (src/process_isolation.rs) and the REST/MCP servers run as tasks on a shared runtime thread (src/app/servers.rs), all in one address space alongside the parsers, the stores, TLS key material and bearer tokens. Treat the API bind address and key accordingly β and note thatpanic = "abort"(Cargo.toml:328) means a panic on any thread ends the whole process, so threads buy no fault containment either. The analysis of whether to close this gap, and why most of it should stay open, is indocs/design/process-isolation-and-hot-path-cost.md. -
D17 β Warn and continue. Malformed input must never crash the
process; parsers set
parse_errorand keep going (docs/fault-model.md).
| You want to⦠| Touch |
|---|---|
| Support a new SIP header accessor |
sip/message.rs (+ parser test) |
| Add a per-packet protocol behavior |
pipeline.rs (one classifier β all paths route through it) |
| Add an output format |
output/ + dispatch in app/batch.rs
|
| Add a TUI view/keybinding |
tui/mod.rs (App/Popup) + tui/state.rs + tui/controllers/ + render/ + keybinding drift test |
| Add a detection |
security/ + wiring in app/batch.rs
|
| Add an MCP tool |
mcp/tools/ for a new group, else mcp/server.rs (#[tool]) + mcp/shape.rs
|
| Add a CLI flag |
cli.rs + flag_coverage_test.rs forces a test |
This table names the files. It does not name the order, the tests each change owes, or the gates that reject it. docs/internals/walkthroughs.md has an ordered checklist for each row.