Architecture - NormB/sipnab GitHub Wiki
A codemap for contributors. It covers:
- what lives where,
- how data flows,
- 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 in one of two modes:
- 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 are readers of the same two stores.
- None of them is 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). - It takes the same read locks the TUI takes. That 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/dialogsand the MCPlist_dialogspage each report which methods their match set holds. - Both call
method_breakdowninsrc/sip/dialog.rsrather 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. 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, for the optional API and MCP servers.
- Those run on one shared runtime (
src/app/servers.rs).
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::Bytesslices of the captured frame end to end (seedocs/internals/zero-copy-payloads.md).
All four packet paths classify through the one
pipeline::classify_packetrouter:
- live,
- batch,
- TUI file-open,
- the
--coressharded path (parallel.rs).Only the per-path appliers differ.
docs/internals/subsystem-guide.md narrates the same journey packet by packet:
- every decision point,
- both store writes,
- where each of the four paths diverges.
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
β βββ vcon_forward.rs # --vcon-forward: a separate process that delivers a vCon spool to a store (vcon feature)
βββ 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); the one secret-file permission rule
βββ 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
β βββ libpcap.rs # which libpcap runs + the backends its banner names (--version, capabilities)
β βββ 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 v1/v2/v3 in, v3 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 (20), 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 Nshards packets by host pair to worker threads with thread-local stores. sipnab merges those stores 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. - Check
Cargo.tomlbefore assuming a build includes a module.
-
-
D11 β Key material is toxic waste.
-
zeroizeon key types. - Redacting
Debugimpls. -
--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.- That flag has no precondition.
- The recommended
--setup-capsinstall has no root to drop.
- It then drops root right after socket open.
- It disables core dumps whenever decryption keys are
resident (
src/privilege.rs). -
--chrootis available for daemon deployments.
- sipnab sets
-
D16 β Process isolation: scanner-kill yes, the REST API no.
- D16
(
docs/design/implementation-plan-v6.md:624) called for scanner-kill and the REST API to run in forked children. -
Scanner-kill does.
- Its worker is a process of its own (
src/process_isolation.rs). It is this binary, re-executed before the chroot and the privilege drop. - The parent creates the only sockets the kill path sends through. It hands them to the worker and closes its own copies.
- So the process parsing captured traffic holds none of them.
- The worker never creates a socket. Holding none, it refuses every request.
- Its worker is a process of its own (
- The REST/MCP servers
still run as tasks on a shared runtime thread (
src/app/servers.rs).- They share one address space with the parsers, the stores, TLS key material and bearer tokens.
- Treat the API bind address and key accordingly.
- Note that
panic = "abort"(Cargo.toml:328) means a panic on any thread ends the whole process. So threads buy no fault containment either.
-
docs/design/process-isolation-and-hot-path-cost.mdanalyzes whether to close the rest of this gap, and why most of it should stay open.
- D16
(
-
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,
- the gates that reject it.
docs/internals/walkthroughs.md has an ordered checklist for each row.