Repository navigation
Internals Code Map
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. The operator explanation of the same
pipeline is Architecture.
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 (see Zero-copy payloads).
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.
The subsystem guide 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 Threading 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.mdline 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:480) 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 (the fault model).
| 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.
Walkthroughs has an ordered checklist for each row.
Website · Repository · Issues · Generated from docs/ — edit there, not here.
Getting started
- Installation
- First CLI Triage
- Glossary
- Cookbook
- Runnable Examples
- Real World Captures
- Troubleshooting
- Tuning Capture
- Encapsulations
- Rtpengine Relays
- Backers
Using the TUI
CLI & automation
- CLI Reference
- Error Messages
- Filter DSL
- Output Formats
- SIP Response Codes
- SIP Methods
- SIP Header Fields
- SIP Parameters
- SIP Lint Rules
- MOS and Codecs
Configuration
Integrations (API & MCP)
- REST API
- Prometheus Metrics
- Authentication
- MCP
- MCP Deployment
- MCP Across an Estate
- MCP Tools
- MCP Tools Core
- MCP Tools Signaling
- MCP Tools Captures
- MCP Tools Security
- MCP Tools Media
- MCP Tools Relay
- MCP Tools TFPS
- MCP Tools Server
- MCP Tools vCon
- MCP Tools TLS
- MCP Protocol
- Uprobe Walkthrough
- Capture SIP over TLS
- WASM Plugins
- vCon Export
- vCon Capture Stack
- G729 Decoding
Your SIP server
- Use OpenSIPS as the SIP Server
- Run sipnab beside OpenSIPS
- Use Kamailio as the SIP Server
- Run sipnab beside Kamailio
- Use Asterisk as the PBX
- Run sipnab beside Asterisk
- Use FreeSWITCH in the Voice Stack
- Run sipnab beside FreeSWITCH
Add to your voice stack
- Add a vCon Server
- Send vCons to a vCon Server
- Send vCons to vcon store
- Add TFPS
- Control TFPS from sipnab
- Ban SIP Scanners with fail2ban
- Feed fail2ban from sipnab
- Add rtpengine
- Name rtpengine Media with sipnab
- Add rtpproxy
- Name rtpproxy Media with sipnab
- Add Homer
- Connect sipnab to Homer
- Add Prometheus and Grafana
- Add sipnab Metrics to Prometheus
Development & internals
- Internals Index
- Internals Subsystem Guide
- Internals Invariants
- Internals Testing
- Internals Walkthroughs
- Internals Build CI Release
- Internals Profiling
- Internals Domain Primer
- Library API
- Benchmarks
- Fault Model
- Threat Model
- Assurance Case
- Architecture
- Internals Threading
- Internals TUI Testing
- Internals Zero Copy Payloads
- Internals Uprobe Capture
- Internals Rtpengine Control Plane
- Internals Relay Control Decoding
- Internals vCon Exporter
- Internals Operator Guide Notes
- Internals Postmortems
- Internals Code Map
- Internals Lint Rules
- Internals Encapsulation Decoding
- Internals MCP Design Notes
- Internals Metrics And Output Notes