sip-doctor
Trace SIP calls on a Linux server, live or from pcap files, and get a plain-English explanation of what went wrong and how to fix it.
- Transports: SIP over UDP (including IP-fragmented messages), TCP (stream reassembly, several messages per segment, lost or out-of-order segments) and TLS. For TLS, the handshake, certificate and alerts are inspected. The SIP inside TLS is encrypted, so to see those calls, receive them decrypted through HEP.
- Proxies and B2BUAs: every hop of a proxied call is shown (caller → proxy → callee, including failover), so you can see which hop failed. B2BUA legs with different Call-IDs (Asterisk, FreeSWITCH, SBCs) are linked together.
- Audio: every RTP stream is matched to its call through the SDP, with packet loss, jitter, dropouts, an estimated MOS and DTMF digits. It detects one-way and missing audio, audio arriving from an address the SDP didn't announce (NAT), and audio that starts late or stops early.
- Mid-call events: hold and resume, codec changes, transfers (REFER and their result), DTMF over SIP INFO, and rejected re-INVITEs.
- Live: capture from any interface with tcpdump (the kernel filters the traffic) or a raw socket. You can also receive HEP from Kamailio, OpenSIPS, FreeSWITCH or Asterisk.
- Reports: a call table, a ranked problem list, per-peer statistics, a time-window filter, JSON, and an HTML report to attach to tickets.
- Files: pcap and pcapng files from tcpdump, Wireshark or
tcpdump -i any, with Ethernet, VLAN, Linux cooked capture, IPv4 or IPv6. You can also pipe a capture in on stdin. - No dependencies: only the Python standard library is needed. It works on Python 3.6 to 3.14, from CentOS 7 / FreePBX SNG7 and Ubuntu 18.04 (3.6) to current releases, and each version is tested to produce identical reports.
━━ Call #1 ANSWERED 2001 → 3001 03:55:00 UDP 203.0.113.10 → 198.51.100.20
ringing after 50ms · answered after 1.0s · talk 32.0s · callee hung up · codecs PCMU, PCMA, telephone-event
Call-ID missing-ack@203.0.113.10
203.0.113.10:5060 198.51.100.20:5060
caller callee
+0.000 |---------- INVITE (SDP) ---------->|
+0.050 |<---------- 180 Ringing -----------| ringing after 50ms
+1.000 |<--------- 200 OK (SDP) -----------| answered after 1.0s
+1.500 |<------- 200 OK ×7 retrans --------| until +20.500 · ✖ no ACK came back
+33.000 |<-------------- BYE ---------------| callee hung up · talk 32.0s
+33.020 |---------- 200 OK (BYE) ---------->|
✖ Missing ACK: the call dropped after ~32 seconds
The 200 OK was sent 8 times but no ACK came back to it. The answering side gives up and hangs up after 32 seconds.
Fix: The ACK is sent to the 200 OK's Contact (<sip:3001@192.168.1.50:5060>) via any Record-Route proxies. Make sure that address is reachable from the caller (NAT: set the external/public address on the callee/PBX, or enable Record-Route on the proxy).
▲ NAT problem: 198.51.100.20 advertises a private Contact (192.168.1.50)
Packets come from public 198.51.100.20 but its 200 OK Contact says 192.168.1.50. In-dialog requests (ACK, BYE, re-INVITE) will be sent to an unreachable address.
Fix: Set the external/public IP on that device (or enable NAT handling / fix_nated_contact on the proxy).
Install on a Linux server
Pick one of these methods.
Single file: no pip and no internet needed on the server.
./deploy/build-pyz.sh # on your machine: builds dist/sip-doctor (~45 KB)
scp dist/sip-doctor root@server:/usr/local/bin/
ssh root@server sip-doctor --version
pipx:
scp -r sip-doctor server: # or git clone
sudo pipx install ./sip-doctor --global # pipx >= 1.5; older pipx: sudo PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ./sip-doctor
Virtualenv: this works on Debian 12 and Ubuntu 24, where system-wide pip install is blocked.
sudo python3 -m venv /opt/sip-doctor
sudo /opt/sip-doctor/bin/pip install ./sip-doctor
sudo ln -s /opt/sip-doctor/bin/sip-doctor /usr/local/bin/sip-doctor
Without installing: run sudo python3 -m sipdoctor ... from inside the project folder.
Which method suits your server's Python:
Server (default python3) |
Recommended |
|---|---|
| CentOS 7, FreePBX SNG7, Ubuntu 18.04, RHEL 8 (3.6) | the single file, or pip3 install sip_doctor-*.whl (build it with python -m build -w on another machine) |
Debian 10 (3.7), RHEL 8 with python38, Ubuntu 20.04 (3.8) |
any method |
| Debian 11/12, Ubuntu 22.04/24.04, RHEL 9, Rocky/Alma 9 (3.9-3.12) | any method (virtualenv or pipx on Debian 12 / Ubuntu 24.04) |
pip install from the source folder needs Python 3.7 or newer, because the packaging tools no longer run on 3.6.
The single file and the wheel work on 3.6. Check the version with python3 --version.
Install tcpdump as well (apt install tcpdump or dnf install tcpdump). When tcpdump is available, sip-doctor uses
it so the kernel filters packets before Python sees them, which matters on busy servers. Without tcpdump, sip-doctor
falls back to a raw socket. Live capture needs root (or CAP_NET_RAW); reading files does not.
Live tracing
sudo sip-doctor live # all interfaces, ports 5060,5061,5080,5081
sudo sip-doctor live -i eth0 -p 5060-5070 # one interface, a port range
sudo sip-doctor live --number 1001 # only calls from/to 1001
sudo sip-doctor live --ip 203.0.113.50 # only traffic to/from one peer (e.g. a trunk)
sudo sip-doctor live -q # no per-message lines; a call flow per finished call
sudo sip-doctor live -q -v # call flows with headers and SDP under each message
sudo sip-doctor live -w /var/tmp/sip.pcap --max-mb 200 # also save the SIP packets (rotates to .1)
sudo sip-doctor live --json-log /var/log/sip-doctor/calls.jsonl # one JSON line per finished call
sudo sip-doctor live --rtp # also analyse the audio: one-way audio, loss, jitter, MOS
sudo sip-doctor live -p any # find SIP on any port (slower: every TCP/UDP packet is checked)
Each SIP message prints as it arrives. When a call ends, its call flow and findings follow:
20:09:06.064 #2 127.0.0.1:48548 → 127.0.0.9:5080 TCP INVITE sip:1002@127.0.0.9;transport=tcp [SDP]
20:09:06.118 #2 127.0.0.9:5080 → 127.0.0.1:48548 TCP 100 Trying (INVITE)
20:09:06.118 #2 127.0.0.9:5080 → 127.0.0.1:48548 TCP 486 Busy Here (INVITE)
OPTIONS keep-alives and successful registrations are hidden unless they fail. Add -a to show everything. Press
Ctrl-C to stop; any calls still in progress are then summarised.
Run it permanently (systemd)
sudo cp deploy/sip-doctor.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now sip-doctor
journalctl -u sip-doctor -f # call summaries
tail -f /var/log/sip-doctor/calls.jsonl | jq . # structured log
Reading capture files
sip-doctor capture.pcap # overview, call table, call flow + findings per call
sip-doctor capture.pcap -v # call flows with key headers and SDP under each message
sip-doctor capture.pcap -vv # ... and the complete SIP messages
sip-doctor capture.pcap --no-flow # summaries and findings only
sip-doctor capture.pcap --all # also OPTIONS and REGISTER transactions
sip-doctor capture.pcap --call 7 # one call (number or part of its Call-ID)
sip-doctor capture.pcap --call 7 -w call7.pcap # save that call's SIP and RTP, for Wireshark (Telephony › VoIP Calls › Play)
sip-doctor capture.pcap --from 14:00 --to 14:30 # only calls in that window (or 'YYYY-MM-DD HH:MM')
sip-doctor capture.pcap --html report.html # also save the report as a web page, to attach to a ticket
sip-doctor capture.pcap --no-rtp # skip the audio analysis
sip-doctor capture.pcap --json # machine-readable (includes audio streams and events)
sudo tcpdump -i any -w - port 5060 | sip-doctor read - # analyse a live tcpdump stream
Call flow through a proxy that asks for authentication (in a capture with more than 20 calls, flows are drawn
only for calls with problems unless you add -l):
10.0.0.5:5060 10.0.0.1:5060 10.0.0.9:5060
caller proxy callee
+0.000 |------------ INVITE (SDP) ----------->| |
+0.010 |< 407 Proxy Authentication Required --| |
+0.020 |---------------- ACK ---------------->| |
+0.030 |------------ INVITE (SDP) ----------->| |
+0.040 |<------------ 100 Trying -------------| |
+0.050 | |------------ INVITE (SDP) ----------->|
+0.060 | |<------------ 100 Trying -------------|
+0.800 | |<----------- 180 Ringing -------------|
+0.810 |<----------- 180 Ringing -------------| |
+3.000 | |<----------- 200 OK (SDP) ------------|
What a call report contains
Each call shows its result and timings, who called whom (display name, P-Asserted-Identity, forwarding), the software on each side, every proxy hop with its response time, a timeline of mid-call events, an audio quality table, and the call flow with RTP streams and notes on each step:
━━ Call #1 ANSWERED alice → 1012 04:15:00 UDP 10.0.0.5 → 10.0.0.9
ringing after 300ms · answered after 1.0s · talk 18.2s · caller hung up · codecs PCMU, PCMA, telephone-event
caller name "Alice Smith" · asserted identity +14155550123 · forwarded from 1000 · setup 1.0s
Call-ID holdxfer@10.0.0.5
Software: caller Yealink T54W 96.86 · callee Asterisk PBX 20.5.0
Events:
+5.000 caller put the call on hold → 200 OK
+9.000 caller resumed the call → 200 OK
+10.000 re-INVITE from caller: media update (G729) → 488 Not Acceptable Here
+14.000 caller transferred the call to 1099 → 202 Accepted
+15.100 transfer result: 404 Not Found
Audio:
→ callee 10.0.0.5:20030 → 10.0.0.9:30030 PCMU 900 pkts 18.0s loss 0.0% jitter 0ms MOS 4.4
→ caller 10.0.0.9:30030 → 10.0.0.5:20030 PCMU 900 pkts 18.0s loss 0.0% jitter 0ms MOS 4.4
10.0.0.5:5060 10.0.0.9:5060
caller callee
+0.000 |---------- INVITE (SDP) ---------->|
+0.300 |<---------- 180 Ringing -----------| ringing after 300ms
+1.000 |<--------- 200 OK (SDP) -----------| answered after 1.0s
+1.020 |--------------- ACK -------------->|
+1.100 |======== RTP PCMU 900 pkts =======>| 18.0s · loss 0.0% · jitter 0ms · MOS 4.4
+1.100 |<======= RTP PCMU 900 pkts ========| 18.0s · loss 0.0% · jitter 0ms · MOS 4.4
+5.000 |---------- INVITE (SDP) ---------->| caller put the call on hold → 200 OK
+5.050 |<--------- 200 OK (SDP) -----------|
+5.060 |--------------- ACK -------------->|
+9.000 |---------- INVITE (SDP) ---------->| caller resumed the call → 200 OK
+9.050 |<--------- 200 OK (SDP) -----------|
+9.060 |--------------- ACK -------------->|
+10.000 |---------- INVITE (SDP) ---------->| re-INVITE from caller: media update (G729) → 488 Not Acceptable Here
+10.050 |<---- 488 Not Acceptable Here -----|
+10.060 |--------------- ACK -------------->|
+11.000 |-------------- INFO -------------->| DTMF 4
+11.010 |<--------- 200 OK (INFO) ----------|
+12.000 |-------------- INFO -------------->| DTMF 2
+12.010 |<--------- 200 OK (INFO) ----------|
+14.000 |-------------- REFER ------------->| caller transferred the call to 1099 → 202 Accepted
+14.020 |<----- 202 Accepted (REFER) -------|
...
The audio table flags bad streams:
Audio:
→ caller 10.0.0.99:41000 → 10.0.0.5:20020 PCMU 848 pkts 20.0s loss 15.2% jitter 25ms MOS 2.9 1 dropout(s) ≤1.5s
→ callee 10.0.0.5:20020 → 10.0.0.9:30020 PCMU 1000 pkts 20.0s loss 0.0% jitter 0ms MOS 4.4
✖ Poor audio callee → caller: estimated MOS 2.9
▲ Audio callee → caller comes from 10.0.0.99:41000, not from the SDP address
With many calls, the report first prints a call table, a Problems list ranked across the whole capture, and a Peers table: for every trunk, gateway or phone that INVITEs were sent to, it shows attempts, answer rate (ASR), calls with no reply, average reply time and post-dial delay, and failure codes. RTP that no captured SIP set up (for example because the SIP was encrypted) is listed separately.
Troubleshooting explanations
The first time each kind of problem appears in a report, sip-doctor prints a full troubleshooting entry under it:
- What it means: the problem in plain words.
- User impact: what callers notice.
- Likely causes.
- How to confirm: numbered checks with concrete commands.
- How to fix.
- Platform settings: the relevant setting or command for Asterisk (PJSIP and chan_sip), FreeSWITCH, Kamailio and OpenSIPS.
- The RFC.
Later occurrences of the same problem point back to it. Add --brief to hide the explanations.
✖ Missing ACK: the call dropped after ~32 seconds
...
┌─ Troubleshooting: Missing ACK
│ What it means: When a call is answered, the callee sends 200 OK and the caller must confirm it with an ACK...
│ User impact: The call connects and audio may work, but it always drops after about 32 seconds.
│ Likely causes:
│ • NAT: the callee's 200 OK has a private Contact (192.168.x.x / 10.x.x.x), so the ACK goes nowhere
│ ...
│ How to confirm:
│ 1. Look at the 200 OK's Contact and Record-Route headers (run with -v)
│ ...
│ Platform settings:
│ Asterisk (PJSIP): ... rtp_symmetric=yes, force_rport=yes, rewrite_contact=yes, direct_media=no
│ Kamailio: record_route() for initial INVITEs; loose_route() + handle_ruri_alias() for in-dialog requests ...
└─
You can also look anything up directly:
sip-doctor explain # list every topic
sip-doctor explain one-way-audio # a problem
sip-doctor explain 488 # a SIP status code: meaning, causes, checks, fixes, platform commands
sip-doctor explain q850-34 # a Q.850 cause from a Reason header
sip-doctor explain unknown_ca # a TLS alert
sip-doctor explain mos jitter asr # terms
--json output includes each finding's topic and, once per topic, the full explanation under explanations.
SIP over TLS: what you can see
On the wire, sip-doctor shows each TLS connection's SNI, TLS version, server certificate (TLS 1.2 and older), alerts, and whether the handshake completed. It reports:
- a certificate rejected by the client (unknown CA, self-signed), an expired certificate, or a name mismatch
- version or cipher mismatches, and a client certificate required (mutual TLS)
- a refused or timed-out TCP connection to 5061, and plain SIP sent to a TLS port
The SIP messages inside TLS are encrypted. To trace those calls, have the proxy or PBX send a decrypted copy over HEP and run:
sudo sip-doctor live --hep 9060 # HEP plus the normal capture
sip-doctor live -i none --hep 9060 # HEP only (no root needed)
| Server | HEP configuration |
|---|---|
| Kamailio | loadmodule "siptrace.so" · modparam("siptrace", "duplicate_uri", "sip:127.0.0.1:9060") · modparam("siptrace", "hep_mode_on", 1) · modparam("siptrace", "hep_version", 3), then call sip_trace() in your routes (or set trace_mode) |
| OpenSIPS | loadmodule "proto_hep.so" · modparam("proto_hep", "hep_id", "[hep_dst] 127.0.0.1:9060;transport=udp;version=3") · loadmodule "tracer.so" · modparam("tracer", "trace_id", "[tid]uri=hep:hep_dst"), then trace("tid", "d", "sip"); |
| FreeSWITCH | sofia.conf.xml: <param name="capture-server" value="udp:127.0.0.1:9060"/>; profile: <param name="sip-capture" value="yes"/> (or sofia global capture on) |
| Asterisk | load res_hep and res_hep_pjsip; hep.conf: enabled = yes, capture_address = 127.0.0.1:9060 |
HEP received on UDP 9060 inside a pcap file is decoded automatically.
What it diagnoses
| Area | Findings |
|---|---|
| Reachability | no response from a hop (with retransmission count), INVITE retransmissions, TCP connection refused or timed out, TCP reset |
| Call failures | every 4xx/5xx/6xx with its meaning and fix, which hop sent it, per-hop results through proxies, Reason: (Q.850) and Warning: headers |
| Auth | challenge with no credentials sent, and wrong username/password (401/407/403 after credentials) for calls and registrations |
| Dialog | missing ACK (the call drops after 32 s), BYE rejected (481), session-timer expiry, slow post-dial delay |
| NAT | private Contact or SDP audio address sent from a public IP (no ACK/BYE, one-way audio) |
| Media (SDP) | 488/606 with the offered codecs, audio rejected (port 0), no common codec, SRTP |
| Audio (RTP) | one-way or no audio, packet loss, jitter, dropouts, MOS, RTCP loss reports, NAT source mismatch, unexpected codec, late or early-stopping audio, DTMF (RFC 4733) |
| Mid-call | hold/resume, re-INVITE or UPDATE rejected (488, 491 glare), transfer (REFER) and its result, DTMF via SIP INFO, PRACK |
| Peers | per destination: attempts, ASR, no-reply count, reply time, post-dial delay, failure codes |
| Transport | SIP over UDP above ~1300 bytes or IP-fragmented, TLS failures (see above) |
| Other | REGISTER results, unanswered OPTIONS pings (repeats collapsed as ×N), failed MESSAGE/SUBSCRIBE/REFER |
Security
sip-doctor parses traffic sent by anyone who can reach your SIP ports, including the scanners that probe every public SIP server, and capture files people send you. It is built for that:
- Hostile input: malformed packets and files are skipped (and counted), never fatal. Numbers are parsed strictly, and no pattern can be made to backtrack catastrophically.
- Bounded resources: every buffer and table has a limit, so floods cannot exhaust memory: open calls (50,000), messages per call, TCP connections (20,000), out-of-order TCP data, IP fragments, TLS handshake data, RTP streams, HEP decompression and capture-file records. At the limit, the oldest entries are reported and released.
- Terminal escape sequences: control and bidirectional-override characters from packets are shown as visible
escapes (
\x1b), so a packet cannot rewrite your terminal, set its title or write to your clipboard. The HTML report escapes everything as well. - Files written safely: sip-doctor refuses to write through a symbolic or hard link, or into another user's file
(no symlink attacks in
/tmpwhen run as root). New files are private (0600): captures and reports contain phone numbers, call content and credentials. - Credentials: digest
responseandcnoncevalues are redacted in-vvand HTML output. Files saved with-ware raw captures and contain everything. - HEP: the listener uses 127.0.0.1 unless you choose another address. If you open it to the network, restrict
the senders with
--hep-allow(and a firewall), otherwise anyone who can reach the port can inject fake calls. - Least privilege: capturing needs only
CAP_NET_RAWandCAP_NET_ADMIN, not root. The systemd unit indeploy/runs as an unprivileged dynamic user with just those two capabilities and a read-only system. For one-off use,sudo sip-doctor liveis fine; to avoid full root:sudo setpriv --reuid=$USER --regid=$(id -g) --init-groups --inh-caps=+net_raw,+net_admin --ambient-caps=+net_raw,+net_admin sip-doctor live. - External tools:
tcpdumpandipare run from the system directories (/usr/sbin, ...) without a shell, so a directory early inPATHcannot substitute them.
Development
python3 -m unittest discover -s tests # tests (no pytest needed)
tests/run-matrix.sh # the tests on Python 3.6 ... 3.14 in Docker
python3 tests/samplegen.py samples # regenerate the example captures in samples/
sip-doctor samples/all_scenarios.pcap # every scenario in one file (see samples/README.md for the list)
tests/live_traffic.py sends real SIP over loopback (UDP, TCP, HEP), for testing sip-doctor live on Linux.
Metadata
Release files for sip-doctor 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sip_doctor-0.4.0.tar.gz | 128.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sip_doctor-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 229.7 kB
Release files / sip_doctor-0.4.0.tar.gz
| Download URL | sip_doctor-0.4.0.tar.gz |
|---|---|
| Size | 128.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9526e0cce6dd552c5fc990525d6978805667ea46b3b3ba9d3a29b9cc5804093f
|
|
BLAKE2b-256 checksum How to use checksums |
c84d9f72bd71c74ab4bed54e2555103f2eb01906487b3e2c90089ad13fb718f8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.15.0
|
Release files / sip_doctor-0.4.0-py3-none-any.whl
| Download URL | sip_doctor-0.4.0-py3-none-any.whl |
|---|---|
| Size | 101.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c369ac32375cf9530c7839ef784e2be8f4b5192549b4ece8f9b06e2d5a217885
|
|
BLAKE2b-256 checksum How to use checksums |
3414954102dac2b6f44e42c02c6b7a1c4dbb402c26dc6a81da994bb216b141c2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.15.0
|