# sipmon
A passive SIP/RTP signaling and media quality monitoring tool. A standalone Rust
executable deployed on a mirrored port / packet capture box, **with no dependency
on a running PBX**. Inputs may be a live capture, a pcap file, a stdin stream, or a
previously recorded event log; output is a live TUI monitor plus exportable
analysis results (JSONL).


## Features
- **Inputs**: live interface (libpcap + BPF), offline pcap/pcapng, stdin `tcpdump -w -` stream, event-log replay
- **Modes**: `live` interactive monitoring / `record` recording with live TUI (`--headless` = no UI, `-d` daemonizable) / `replay`
- **SIP correlation**: Call-ID call state machine, transactions, Call-ID / number / IP / SSRC indexes, SIP-over-TCP reassembly
- **Media quality**: RFC3550 jitter/loss, RTCP RR RTT, one-way delay estimate, E-model MOS
- **TURN detection**: auto-learns TURN servers, labels `turn-client` / `turn-peer` relay legs
- **Diagnostics**: 20+ rules for Contact reachability, Record-Route, SDP/RTP consistency, one-way media, TURN allocation/refresh
- **TUI**: Overview / Search / Call Detail / Heatmap / Streams / Event Log / IP Stats pages
- **Analysis**: PDD/setup/ring timing, hangup initiator (BYE, CANCEL, reject), per-IP loss over 1s…1h windows
- **Export**: JSONL on exit or via `export`; `query` fetches a Call-ID flow for scripting
## Quick start
```sh
sipmon live -i any # live monitoring (TUI)
sipmon live -i eth0 -f "udp port 5060" # with a BPF filter
sipmon record -i any -w cap.evlog --headless # record to an event log
sipmon record -i any -w cap.evlog -d --pidfile /run/sipmon.pid --logfile /var/log/sipmon.log
sipmon replay -l cap.evlog # replay a recording (TUI)
sipmon file -r capture.pcap # offline pcap analysis
sipmon capture.pcap # default mode: dispatch by extension
sipmon cap.evlog # *.pcap/.pcapng → file, *.evlog → replay, *.jsonl → snapshot view
sipmon out.jsonl
## Commands
| `(none)` | Default mode: positional `FILE` dispatched by extension (`.pcap/.pcapng` → `file`, `.evlog` → `replay`, `.jsonl` → snapshot view); no FILE starts a live capture. `--no-tui` for headless output |
| `live` | Live capture + TUI. `-i` interface, `-f` BPF filter, `--no-media` disables RTP/RTCP analysis, `-w` also writes an event log |
| `record` | Live capture → event log (`-w` required). Live TUI on a tty; `--headless` disables it. `-d` daemonizes, `--pidfile`/`--logfile` for daemon runs. Flushes gracefully on SIGTERM/SIGINT |
| `-` | Read a pcap byte stream from stdin |
| `file` | Offline pcap/pcapng. `--rate 1x` replay speed multiplier, `--no-tui`, `--print-events` |
| `replay` | Replay an event log (TUI / `--no-tui`) |
| `query` | No TUI; exports flow + stream stats + RTT + diagnostics for a Call-ID (script friendly) |
| `export` | Rebuild a snapshot from an event log → JSONL, with `--from/--to` time filtering |
### Common options
```
--dry-run In-memory analysis only, writes no files
--max-calls N Max calls retained in memory (default 100000)
--max-streams N RTP stream ring cap (default 50000)
--max-diagnostics N Diagnostics ring cap (default 50000)
--local-ips IP,… Local (monitored) machine IPs: Call Detail flow/media pin
the local endpoint to the right with ingress/egress arrows
(default: this host's own interface addresses)
--raw-truncate N Truncate stored raw SIP messages to N bytes
--export-jsonl PATH Export JSONL on exit
```
## TUI
Pages: **Overview** `1` · **Search** `2`/`/` · **Call Detail** `3` · **Heatmap** `4` · **Streams** `5` · **Event Log** `6` · **IP Stats** `7`. `Tab`/`Shift-Tab` cycles pages, `Space` pauses, `e` exports JSONL, `x` clears in-memory stats, `q`/`Esc`/`Ctrl-C` quits.
Call Detail uses a fixed four-pane layout: **Flow** (chronological SIP messages, `↑`/`↓` selects) · **Raw** (full bytes of the selected message, `PgUp`/`PgDn` scrolls) · **Diagnostics** · **Network** (traffic totals + per-stream media table). With a known local IP (`--local-ips`) the local endpoint is pinned to the right and arrows show the direction — `remote -> local` is inbound, `remote <- local` outbound.
IP Stats aggregates per-IP conditions, split by direction (**TX** = sent by the IP, **RX** = received): `c` collapses to a loss-only summary, `w` cycles the time window, `s` sorts, `Enter` drills down to the calls involving an IP.
## Event log format
Private binary append-only format. Header holds the `SMON` magic, version, and timezone; records are `ts_delta | ev_type | len | payload`. Event types: `1` SipMsgEvt, `2` TxnEvt, `3` CallEvt, `4` StreamSnapEvt (every 5s), `5` RtcpRttEvt, `6` HealthBucketEvt, `7` ErrorEvt, `8` DiagEvt.
**No raw RTP payload is stored** — only the summaries needed to rebuild the analysis, plus truncated raw SIP messages (`--raw-truncate` controls the cap). `record` always writes to disk; `live` needs an explicit `-w`.
## Diagnostic codes
| `CONTACT_UNREACHABLE` / `CONTACT_PRIVATE_NAT` / `CONTACT_MCAST` | Contact address unreachable, private NAT, or multicast |
| `RR_NOT_HONORED` / `RR_DEPTH_MISMATCH` | Record-Route not honored / depth mismatch |
| `SDP_HOLD` | SDP carries hold (`sendonly`/`inactive`) |
| `RTP_PT_MISMATCH` / `RTP_PT_CHANGED` / `RTP_FLOW_UNEXPECTED` | Payload type mismatch / changed mid-call / RTP flow disagrees with SDP |
| `ONE_WAY_MEDIA` | One-way media (only receiving, not sending) |
| `TURN_ALLOC_OK` / `TURN_ALLOC_FAILED` / `TURN_REFRESH_FAILED` | TURN allocation succeeded / failed / refresh failed |
| `TURN_RELAY_MEDIA` / `TURN_CHANNEL_MEDIA` / `TURN_SEND_IND_MEDIA` | Media relayed via TURN Relay / ChannelData / Send-Ind |
| `TURN_LEG_IMBALANCE` | TURN leg packet imbalance (suspected one-way) |
## Metric definitions
- **RTT**: `RTT = arrival_NTP − LSR − DLSR` from the RTCP RR
- **One-way delay**: RTCP SR NTP↔RTP mapping (when both directions are visible); otherwise an indirect estimate from RTP arrival intervals (labeled "estimate")
- **jitter/loss**: RFC3550, 64-packet reorder window
- **MOS**: simplified E-model (G.107): `R = 93.2 − Id − Ie`, labeled "estimate"
## Limitations
- TLS/SRTP encrypted payloads cannot be parsed; capture at the decryption point
- Absolute one-way delay under one-way passive observation is an estimate; RTCP RTT is the primary metric
- Capturing without interface permissions requires root / elevated privileges (same as tcpdump)
## Tests
```sh
cargo test # unit + integration tests (pcap fixture)
cargo test --test cli_integration
```