sip-doctor 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ramesh Audireddy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ include LICENSE README.md
2
+ include tests/*.py tests/*.sh
@@ -0,0 +1,348 @@
1
+ Metadata-Version: 2.4
2
+ Name: sip-doctor
3
+ Version: 0.3.2
4
+ Summary: Trace SIP calls (UDP, TCP, TLS, proxies, HEP) live on a Linux server or from pcap files, and explain failures
5
+ Author: Ramesh Audireddy
6
+ Keywords: sip,voip,pcap,tcpdump,kamailio,asterisk,freeswitch,hep
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Python :: 3.6
10
+ Classifier: Programming Language :: Python :: 3.7
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Environment :: Console
20
+ Classifier: Topic :: Communications :: Telephony
21
+ Classifier: Topic :: System :: Networking :: Monitoring
22
+ Requires-Python: >=3.6
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # sip-doctor
28
+
29
+ Trace SIP calls on a Linux server, live or from pcap files, and get a plain-English explanation of what went wrong
30
+ and how to fix it.
31
+
32
+ - **Transports:** SIP over UDP (including IP-fragmented messages), TCP (stream reassembly, several messages per
33
+ segment, lost or out-of-order segments) and TLS. For TLS, the handshake, certificate and alerts are inspected. The
34
+ SIP inside TLS is encrypted, so to see those calls, receive them decrypted through HEP.
35
+ - **Proxies and B2BUAs:** every hop of a proxied call is shown (caller → proxy → callee, including failover), so you
36
+ can see which hop failed. B2BUA legs with different Call-IDs (Asterisk, FreeSWITCH, SBCs) are linked together.
37
+ - **Audio:** every RTP stream is matched to its call through the SDP, with packet loss, jitter, dropouts, an
38
+ estimated MOS and DTMF digits. It detects one-way and missing audio, audio arriving from an address the SDP didn't
39
+ announce (NAT), and audio that starts late or stops early.
40
+ - **Mid-call events:** hold and resume, codec changes, transfers (REFER and their result), DTMF over SIP INFO, and
41
+ rejected re-INVITEs.
42
+ - **Live:** capture from any interface with tcpdump (the kernel filters the traffic) or a raw socket. You can also
43
+ receive HEP from Kamailio, OpenSIPS, FreeSWITCH or Asterisk.
44
+ - **Reports:** a call table, a ranked problem list, per-peer statistics, a time-window filter, JSON, and an HTML
45
+ report to attach to tickets.
46
+ - **Files:** pcap and pcapng files from tcpdump, Wireshark or `tcpdump -i any`, with Ethernet, VLAN, Linux cooked
47
+ capture, IPv4 or IPv6. You can also pipe a capture in on stdin.
48
+ - **No dependencies:** only the Python standard library is needed. It works on **Python 3.6 to 3.14**, from CentOS 7 /
49
+ FreePBX SNG7 and Ubuntu 18.04 (3.6) to current releases, and each version is tested to produce identical reports.
50
+
51
+ ```
52
+ ━━ Call #1 ANSWERED 2001 → 3001 03:55:00 UDP 203.0.113.10 → 198.51.100.20
53
+ ringing after 50ms · answered after 1.0s · talk 32.0s · callee hung up · codecs PCMU, PCMA, telephone-event
54
+ Call-ID missing-ack@203.0.113.10
55
+
56
+ 203.0.113.10:5060 198.51.100.20:5060
57
+ caller callee
58
+ +0.000 |---------- INVITE (SDP) ---------->|
59
+ +0.050 |<---------- 180 Ringing -----------| ringing after 50ms
60
+ +1.000 |<--------- 200 OK (SDP) -----------| answered after 1.0s
61
+ +1.500 |<------- 200 OK ×7 retrans --------| until +20.500 · ✖ no ACK came back
62
+ +33.000 |<-------------- BYE ---------------| callee hung up · talk 32.0s
63
+ +33.020 |---------- 200 OK (BYE) ---------->|
64
+
65
+ ✖ Missing ACK: the call dropped after ~32 seconds
66
+ 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.
67
+ 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).
68
+ ▲ NAT problem: 198.51.100.20 advertises a private Contact (192.168.1.50)
69
+ 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.
70
+ Fix: Set the external/public IP on that device (or enable NAT handling / fix_nated_contact on the proxy).
71
+ ```
72
+
73
+ ## Install on a Linux server
74
+
75
+ Pick one of these methods.
76
+
77
+ **Single file:** no pip and no internet needed on the server.
78
+
79
+ ```bash
80
+ ./deploy/build-pyz.sh # on your machine: builds dist/sip-doctor (~45 KB)
81
+ scp dist/sip-doctor root@server:/usr/local/bin/
82
+ ssh root@server sip-doctor --version
83
+ ```
84
+
85
+ **pipx:**
86
+
87
+ ```bash
88
+ scp -r sip-doctor server: # or git clone
89
+ 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
90
+ ```
91
+
92
+ **Virtualenv:** this works on Debian 12 and Ubuntu 24, where system-wide `pip install` is blocked.
93
+
94
+ ```bash
95
+ sudo python3 -m venv /opt/sip-doctor
96
+ sudo /opt/sip-doctor/bin/pip install ./sip-doctor
97
+ sudo ln -s /opt/sip-doctor/bin/sip-doctor /usr/local/bin/sip-doctor
98
+ ```
99
+
100
+ **Without installing:** run `sudo python3 -m sipdoctor ...` from inside the project folder.
101
+
102
+ **Which method suits your server's Python:**
103
+
104
+ | Server (default `python3`) | Recommended |
105
+ |-------------------------------------------------------------|-------------------------------------------------------------|
106
+ | 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) |
107
+ | Debian 10 (3.7), RHEL 8 with `python38`, Ubuntu 20.04 (3.8) | any method |
108
+ | 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) |
109
+
110
+ `pip install` from the source folder needs Python 3.7 or newer, because the packaging tools no longer run on 3.6.
111
+ The single file and the wheel work on 3.6. Check the version with `python3 --version`.
112
+
113
+ Install `tcpdump` as well (`apt install tcpdump` or `dnf install tcpdump`). When tcpdump is available, sip-doctor uses
114
+ it so the kernel filters packets before Python sees them, which matters on busy servers. Without tcpdump, sip-doctor
115
+ falls back to a raw socket. Live capture needs root (or `CAP_NET_RAW`); reading files does not.
116
+
117
+ ## Live tracing
118
+
119
+ ```bash
120
+ sudo sip-doctor live # all interfaces, ports 5060,5061,5080,5081
121
+ sudo sip-doctor live -i eth0 -p 5060-5070 # one interface, a port range
122
+ sudo sip-doctor live --number 1001 # only calls from/to 1001
123
+ sudo sip-doctor live --ip 203.0.113.50 # only traffic to/from one peer (e.g. a trunk)
124
+ sudo sip-doctor live -q # no per-message lines; a call flow per finished call
125
+ sudo sip-doctor live -q -v # call flows with headers and SDP under each message
126
+ sudo sip-doctor live -w /tmp/sip.pcap --max-mb 200 # also save the SIP packets (rotates to .1)
127
+ sudo sip-doctor live --json-log /var/log/sip-doctor/calls.jsonl # one JSON line per finished call
128
+ sudo sip-doctor live --rtp # also analyse the audio: one-way audio, loss, jitter, MOS
129
+ sudo sip-doctor live -p any # find SIP on any port (slower: every TCP/UDP packet is checked)
130
+ ```
131
+
132
+ Each SIP message prints as it arrives. When a call ends, its call flow and findings follow:
133
+
134
+ ```
135
+ 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]
136
+ 20:09:06.118 #2 127.0.0.9:5080 → 127.0.0.1:48548 TCP 100 Trying (INVITE)
137
+ 20:09:06.118 #2 127.0.0.9:5080 → 127.0.0.1:48548 TCP 486 Busy Here (INVITE)
138
+ ```
139
+
140
+ OPTIONS keep-alives and successful registrations are hidden unless they fail. Add `-a` to show everything. Press
141
+ Ctrl-C to stop; any calls still in progress are then summarised.
142
+
143
+ ### Run it permanently (systemd)
144
+
145
+ ```bash
146
+ sudo cp deploy/sip-doctor.service /etc/systemd/system/
147
+ sudo systemctl daemon-reload && sudo systemctl enable --now sip-doctor
148
+ journalctl -u sip-doctor -f # call summaries
149
+ tail -f /var/log/sip-doctor/calls.jsonl | jq . # structured log
150
+ ```
151
+
152
+ ## Reading capture files
153
+
154
+ ```bash
155
+ sip-doctor capture.pcap # overview, call table, call flow + findings per call
156
+ sip-doctor capture.pcap -v # call flows with key headers and SDP under each message
157
+ sip-doctor capture.pcap -vv # ... and the complete SIP messages
158
+ sip-doctor capture.pcap --no-flow # summaries and findings only
159
+ sip-doctor capture.pcap --all # also OPTIONS and REGISTER transactions
160
+ sip-doctor capture.pcap --call 7 # one call (number or part of its Call-ID)
161
+ sip-doctor capture.pcap --call 7 -w call7.pcap # save that call's SIP and RTP, for Wireshark (Telephony › VoIP Calls › Play)
162
+ sip-doctor capture.pcap --from 14:00 --to 14:30 # only calls in that window (or 'YYYY-MM-DD HH:MM')
163
+ sip-doctor capture.pcap --html report.html # also save the report as a web page, to attach to a ticket
164
+ sip-doctor capture.pcap --no-rtp # skip the audio analysis
165
+ sip-doctor capture.pcap --json # machine-readable (includes audio streams and events)
166
+ sudo tcpdump -i any -w - port 5060 | sip-doctor read - # analyse a live tcpdump stream
167
+ ```
168
+
169
+ Call flow through a proxy that asks for authentication (in a capture with more than 20 calls, flows are drawn
170
+ only for calls with problems unless you add `-l`):
171
+
172
+ ```
173
+ 10.0.0.5:5060 10.0.0.1:5060 10.0.0.9:5060
174
+ caller proxy callee
175
+ +0.000 |------------ INVITE (SDP) ----------->| |
176
+ +0.010 |< 407 Proxy Authentication Required --| |
177
+ +0.020 |---------------- ACK ---------------->| |
178
+ +0.030 |------------ INVITE (SDP) ----------->| |
179
+ +0.040 |<------------ 100 Trying -------------| |
180
+ +0.050 | |------------ INVITE (SDP) ----------->|
181
+ +0.060 | |<------------ 100 Trying -------------|
182
+ +0.800 | |<----------- 180 Ringing -------------|
183
+ +0.810 |<----------- 180 Ringing -------------| |
184
+ +3.000 | |<----------- 200 OK (SDP) ------------|
185
+ ```
186
+
187
+ ## What a call report contains
188
+
189
+ Each call shows its result and timings, who called whom (display name, P-Asserted-Identity, forwarding), the software
190
+ on each side, every proxy hop with its response time, a timeline of mid-call events, an audio quality table, and the
191
+ call flow with RTP streams and notes on each step:
192
+
193
+ ```
194
+ ━━ Call #1 ANSWERED alice → 1012 04:15:00 UDP 10.0.0.5 → 10.0.0.9
195
+ ringing after 300ms · answered after 1.0s · talk 18.2s · caller hung up · codecs PCMU, PCMA, telephone-event
196
+ caller name "Alice Smith" · asserted identity +14155550123 · forwarded from 1000 · setup 1.0s
197
+ Call-ID holdxfer@10.0.0.5
198
+ Software: caller Yealink T54W 96.86 · callee Asterisk PBX 20.5.0
199
+ Events:
200
+ +5.000 caller put the call on hold → 200 OK
201
+ +9.000 caller resumed the call → 200 OK
202
+ +10.000 re-INVITE from caller: media update (G729) → 488 Not Acceptable Here
203
+ +14.000 caller transferred the call to 1099 → 202 Accepted
204
+ +15.100 transfer result: 404 Not Found
205
+ Audio:
206
+ → callee 10.0.0.5:20030 → 10.0.0.9:30030 PCMU 900 pkts 18.0s loss 0.0% jitter 0ms MOS 4.4
207
+ → caller 10.0.0.9:30030 → 10.0.0.5:20030 PCMU 900 pkts 18.0s loss 0.0% jitter 0ms MOS 4.4
208
+
209
+ 10.0.0.5:5060 10.0.0.9:5060
210
+ caller callee
211
+ +0.000 |---------- INVITE (SDP) ---------->|
212
+ +0.300 |<---------- 180 Ringing -----------| ringing after 300ms
213
+ +1.000 |<--------- 200 OK (SDP) -----------| answered after 1.0s
214
+ +1.020 |--------------- ACK -------------->|
215
+ +1.100 |======== RTP PCMU 900 pkts =======>| 18.0s · loss 0.0% · jitter 0ms · MOS 4.4
216
+ +1.100 |<======= RTP PCMU 900 pkts ========| 18.0s · loss 0.0% · jitter 0ms · MOS 4.4
217
+ +5.000 |---------- INVITE (SDP) ---------->| caller put the call on hold → 200 OK
218
+ +5.050 |<--------- 200 OK (SDP) -----------|
219
+ +5.060 |--------------- ACK -------------->|
220
+ +9.000 |---------- INVITE (SDP) ---------->| caller resumed the call → 200 OK
221
+ +9.050 |<--------- 200 OK (SDP) -----------|
222
+ +9.060 |--------------- ACK -------------->|
223
+ +10.000 |---------- INVITE (SDP) ---------->| re-INVITE from caller: media update (G729) → 488 Not Acceptable Here
224
+ +10.050 |<---- 488 Not Acceptable Here -----|
225
+ +10.060 |--------------- ACK -------------->|
226
+ +11.000 |-------------- INFO -------------->| DTMF 4
227
+ +11.010 |<--------- 200 OK (INFO) ----------|
228
+ +12.000 |-------------- INFO -------------->| DTMF 2
229
+ +12.010 |<--------- 200 OK (INFO) ----------|
230
+ +14.000 |-------------- REFER ------------->| caller transferred the call to 1099 → 202 Accepted
231
+ +14.020 |<----- 202 Accepted (REFER) -------|
232
+ ...
233
+ ```
234
+
235
+ The audio table flags bad streams:
236
+
237
+ ```
238
+ Audio:
239
+ → 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
240
+ → callee 10.0.0.5:20020 → 10.0.0.9:30020 PCMU 1000 pkts 20.0s loss 0.0% jitter 0ms MOS 4.4
241
+
242
+ ✖ Poor audio callee → caller: estimated MOS 2.9
243
+ ▲ Audio callee → caller comes from 10.0.0.99:41000, not from the SDP address
244
+ ```
245
+
246
+ With many calls, the report first prints a call table, a **Problems** list ranked across the whole capture, and a
247
+ **Peers** table: for every trunk, gateway or phone that INVITEs were sent to, it shows attempts, answer rate (ASR),
248
+ calls with no reply, average reply time and post-dial delay, and failure codes. RTP that no captured SIP set up (for
249
+ example because the SIP was encrypted) is listed separately.
250
+
251
+ ## Troubleshooting explanations
252
+
253
+ The first time each kind of problem appears in a report, sip-doctor prints a full troubleshooting entry under it:
254
+
255
+ - **What it means:** the problem in plain words.
256
+ - **User impact:** what callers notice.
257
+ - **Likely causes.**
258
+ - **How to confirm:** numbered checks with concrete commands.
259
+ - **How to fix.**
260
+ - **Platform settings:** the relevant setting or command for Asterisk (PJSIP and chan_sip), FreeSWITCH, Kamailio and
261
+ OpenSIPS.
262
+ - **The RFC.**
263
+
264
+ Later occurrences of the same problem point back to it. Add `--brief` to hide the explanations.
265
+
266
+ ```
267
+ ✖ Missing ACK: the call dropped after ~32 seconds
268
+ ...
269
+ ┌─ Troubleshooting: Missing ACK
270
+ │ What it means: When a call is answered, the callee sends 200 OK and the caller must confirm it with an ACK...
271
+ │ User impact: The call connects and audio may work, but it always drops after about 32 seconds.
272
+ │ Likely causes:
273
+ │ • NAT: the callee's 200 OK has a private Contact (192.168.x.x / 10.x.x.x), so the ACK goes nowhere
274
+ │ ...
275
+ │ How to confirm:
276
+ │ 1. Look at the 200 OK's Contact and Record-Route headers (run with -v)
277
+ │ ...
278
+ │ Platform settings:
279
+ │ Asterisk (PJSIP): ... rtp_symmetric=yes, force_rport=yes, rewrite_contact=yes, direct_media=no
280
+ │ Kamailio: record_route() for initial INVITEs; loose_route() + handle_ruri_alias() for in-dialog requests ...
281
+ └─
282
+ ```
283
+
284
+ You can also look anything up directly:
285
+
286
+ ```bash
287
+ sip-doctor explain # list every topic
288
+ sip-doctor explain one-way-audio # a problem
289
+ sip-doctor explain 488 # a SIP status code: meaning, causes, checks, fixes, platform commands
290
+ sip-doctor explain q850-34 # a Q.850 cause from a Reason header
291
+ sip-doctor explain unknown_ca # a TLS alert
292
+ sip-doctor explain mos jitter asr # terms
293
+ ```
294
+
295
+ `--json` output includes each finding's `topic` and, once per topic, the full explanation under `explanations`.
296
+
297
+ ## SIP over TLS: what you can see
298
+
299
+ On the wire, sip-doctor shows each TLS connection's SNI, TLS version, server certificate (TLS 1.2 and older), alerts,
300
+ and whether the handshake completed. It reports:
301
+
302
+ - a certificate rejected by the client (unknown CA, self-signed), an expired certificate, or a name mismatch
303
+ - version or cipher mismatches, and a client certificate required (mutual TLS)
304
+ - a refused or timed-out TCP connection to 5061, and plain SIP sent to a TLS port
305
+
306
+ The SIP messages inside TLS are encrypted. To trace those calls, have the proxy or PBX send a decrypted copy over
307
+ HEP and run:
308
+
309
+ ```bash
310
+ sudo sip-doctor live --hep 9060 # HEP plus the normal capture
311
+ sip-doctor live -i none --hep 9060 # HEP only (no root needed)
312
+ ```
313
+
314
+ | Server | HEP configuration |
315
+ |------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
316
+ | 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`) |
317
+ | 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");` |
318
+ | 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`) |
319
+ | Asterisk | load `res_hep` and `res_hep_pjsip`; `hep.conf`: `enabled = yes`, `capture_address = 127.0.0.1:9060` |
320
+
321
+ HEP received on UDP 9060 inside a pcap file is decoded automatically.
322
+
323
+ ## What it diagnoses
324
+
325
+ | Area | Findings |
326
+ |---------------|--------------------------------------------------------------------------------------------------------------------------------|
327
+ | Reachability | no response from a hop (with retransmission count), INVITE retransmissions, TCP connection refused or timed out, TCP reset |
328
+ | 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 |
329
+ | Auth | challenge with no credentials sent, and wrong username/password (401/407/403 after credentials) for calls and registrations |
330
+ | Dialog | missing ACK (the call drops after 32 s), BYE rejected (481), session-timer expiry, slow post-dial delay |
331
+ | NAT | private Contact or SDP audio address sent from a public IP (no ACK/BYE, one-way audio) |
332
+ | Media (SDP) | 488/606 with the offered codecs, audio rejected (port 0), no common codec, SRTP |
333
+ | 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) |
334
+ | Mid-call | hold/resume, re-INVITE or UPDATE rejected (488, 491 glare), transfer (REFER) and its result, DTMF via SIP INFO, PRACK |
335
+ | Peers | per destination: attempts, ASR, no-reply count, reply time, post-dial delay, failure codes |
336
+ | Transport | SIP over UDP above ~1300 bytes or IP-fragmented, TLS failures (see above) |
337
+ | Other | REGISTER results, unanswered OPTIONS pings (repeats collapsed as ×N), failed MESSAGE/SUBSCRIBE/REFER |
338
+
339
+ ## Development
340
+
341
+ ```bash
342
+ python3 -m unittest discover -s tests # tests (no pytest needed)
343
+ tests/run-matrix.sh # the tests on Python 3.6 ... 3.14 in Docker
344
+ python3 tests/samplegen.py samples # regenerate the example captures in samples/
345
+ sip-doctor samples/all_scenarios.pcap # every scenario in one file (see samples/README.md for the list)
346
+ ```
347
+
348
+ `tests/live_traffic.py` sends real SIP over loopback (UDP, TCP, HEP), for testing `sip-doctor live` on Linux.