Skip to content

Add sip_probe.py: minimal SIP UAC for capturing majestic RTP/RTCP - #1

Open
openipc-ai wants to merge 1 commit into
mainfrom
add-sip-probe
Open

Add sip_probe.py: minimal SIP UAC for capturing majestic RTP/RTCP#1
openipc-ai wants to merge 1 commit into
mainfrom
add-sip-probe

Conversation

@openipc-ai

Copy link
Copy Markdown
Contributor

Adds harness/sip_probe.py, a minimal SIP UAC that places a P2P call to a majestic camera and captures the RTP + RTCP it sends, analyzing RTP timestamp clock rates and RTCP Sender Reports.

Used to diagnose the Linphone 6.2.2 (Android) SIP call-drop: the probe showed majestic sends clean RTP (audio 8 kHz, video 90 kHz, zero loss) but zero RTCP Sender Reports on non-Hisilicon SoCs — the trigger for Linphone's adaptive jitter buffer diverging and dropping the call. Filed upstream as widgetii/majestic#398.

🤖 Generated with Claude Code

A diagnostic probe that places a P2P SIP call to a majestic camera and captures
the RTP + RTCP the camera sends, analyzing RTP timestamp clock rates and RTCP
Sender Reports. Used to diagnose the Linphone 6.2.2 SIP call-drop: it showed
majestic sends clean RTP but zero RTCP SR on non-Hisi SoCs (widgetii/majestic#398).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DStUrnmn8TqWrobQ7D2wmX
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add SIP probe tool to capture and analyze Majestic RTP/RTCP (SR/clock rates)

✨ Enhancement 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Add a minimal SIP UAC probe to place a P2P call and capture RTP/RTCP.
• Analyze RTP timestamp clock rates and presence/contents of RTCP Sender Reports.
• Document the new harness tool and its diagnostic purpose (Linphone call-drop).
Diagram

graph TD
  P["sip_probe.py"] -->|"UDP SIP (INVITE/ACK/BYE)"| CAM{{"Majestic camera"}}
  P --> MS["Media sockets"]
  CAM -->|"RTP/RTCP"| MS --> PAR["RTP/RTCP stats"] --> OUT[("outdir artifacts")]
  P --> LOG[("sip.log")]

  subgraph Legend
    direction LR
    _proc["Process"] ~~~ _ext{{"External"}} ~~~ _file[("File/dir")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a full SIP/RTP stack (PJSIP / baresip automation)
  • ➕ Less custom SIP state handling (timers, retransmits, parsing edge cases)
  • ➕ Potentially more accurate RTP/RTCP behavior and SDP negotiation
  • ➕ Easier extension to other auth/transport scenarios
  • ➖ Heavier dependency footprint and setup complexity
  • ➖ Harder to keep as a single-file portable diagnostic tool
  • ➖ Less direct control/visibility into raw packets unless instrumented
2. Capture with tcpdump/Wireshark and analyze offline
  • ➕ No need to implement SIP auth/transactions in code
  • ➕ Wireshark already decodes RTCP SR and RTP timestamp fields robustly
  • ➖ Still needs a call initiator (softphone) to trigger media
  • ➖ More steps and tooling required on the host
  • ➖ Less reproducible/automatable for repeated lab runs
3. Use Scapy to parse RTCP/RTP and build a thin SIP initiator
  • ➕ Cleaner packet parsing primitives than manual struct unpacking
  • ➕ Easier extension to additional RTCP types or RTP header extensions
  • ➖ Adds a dependency and environment constraints
  • ➖ Doesn’t remove the need for SIP transaction/auth logic

Recommendation: Keep the PR’s current stdlib-only approach: for a targeted diagnostic probe, minimizing dependencies and maximizing control over raw RTP/RTCP fields is the priority. If this tool grows beyond diagnostics (broader SIP interoperability, more transactions, TLS/TCP), revisit using a SIP stack to avoid accumulating fragile protocol logic.

Files changed (2) +293 / -0

Enhancement (1) +290 / -0
sip_probe.pyAdd minimal SIP UAC to capture RTP/RTCP and inspect RTCP SR behavior +290/-0

Add minimal SIP UAC to capture RTP/RTCP and inspect RTCP SR behavior

• Introduces a standalone, stdlib-only diagnostic script that sends SIP INVITE/ACK/BYE to a Majestic camera, handling digest authentication (401/407). Binds local RTP/RTCP UDP ports for audio/video, records packet timing and RTP timestamp deltas, parses RTCP compound packets for Sender Reports, and prints clock-rate and SR presence/offset analysis while writing basic artifacts to an outdir.

harness/sip_probe.py

Documentation (1) +3 / -0
CLAUDE.mdDocument new SIP probe harness tool +3/-0

Document new SIP probe harness tool

• Adds a new entry under the harness tool list describing sip_probe.py, its purpose (P2P SIP call), and what it captures/analyzes (RTP/RTCP timestamps and RTCP Sender Reports).

CLAUDE.md

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. RTCP port bind race 🐞 Bug ☼ Reliability
Description
sip_probe binds RTP to an ephemeral port and then binds RTCP to (RTP+1) without reserving the pair,
so the RTCP bind can fail if that adjacent port is already in use, crashing the probe before the
call. This also makes the probe flaky across runs/hosts because the port selection is
nondeterministic and not validated/retried.
Code

harness/sip_probe.py[R163-166]

+    audc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); audc.bind(("0.0.0.0", aport + 1))
+    vid = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); vid.bind(("0.0.0.0", 0))
+    vport = vid.getsockname()[1]
+    vidc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); vidc.bind(("0.0.0.0", vport + 1))
Evidence
The probe explicitly binds RTCP sockets to aport + 1 / vport + 1 after selecting aport /
vport via ephemeral bind, with no validation or retry; if the adjacent port is occupied, bind()
raises and the probe aborts.

harness/sip_probe.py[160-167]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`sip_probe.py` binds RTP sockets to port 0 (ephemeral) and then immediately binds RTCP sockets to `rtp_port + 1`. The `+1` port is not guaranteed to be free, so the bind can fail and crash the script.

### Issue Context
This probe is intended for quick diagnosis; failing nondeterministically due to port collisions will waste time and produce confusing failures.

### Fix Focus Areas
- harness/sip_probe.py[155-169]

### Suggested direction
- Implement a small helper to allocate a port pair by trying an even base port and binding both RTP and RTCP in a retry loop (closing any partially-created sockets on failure).
- Alternatively, allow explicit `--audio-port`/`--video-port` and validate that `port+1` is available before sending the SDP.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Leaked files/sockets on abort 🐞 Bug ☼ Reliability
Description
sip_probe opens log/output files and creates multiple sockets but never closes them; on the early
abort path (no 200 OK) it returns immediately, leaking resources. Re-running the probe repeatedly in
a harness environment can exhaust file descriptors or leave confusing open handles until process
exit.
Code

harness/sip_probe.py[R231-233]

+    if not ok_headers:
+        L("[probe] no 200 OK — aborting"); return
+
Evidence
The script opens sip.log before the SIP handshake and can return on failure without closing it;
it also opens audio_rtp.bin and never closes it, and there is no cleanup for any sockets. Other
harness scripts commonly use context managers for file I/O.

harness/sip_probe.py[155-166]
harness/sip_probe.py[196-233]
harness/sip_probe.py[248-287]
harness/analyze_pcap.py[18-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The script opens `sip.log` and `audio_rtp.bin` and creates multiple sockets, but does not close them; an early `return` path leaks resources.

### Issue Context
This is a diagnostic tool likely to be run iteratively. Clean shutdown improves reliability and avoids FD exhaustion in long-lived harness environments.

### Fix Focus Areas
- harness/sip_probe.py[155-233]
- harness/sip_probe.py[248-287]

### Suggested direction
- Wrap sockets/files in `contextlib.ExitStack()` or `try/finally` to ensure all are closed.
- Replace `log = open(...)` / `araw = open(...)` with `with open(...) as ...` (or register them with ExitStack) so the early-abort path also closes them.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Socket errors silently ignored 🐞 Bug ◔ Observability
Description
The media capture loop catches socket.error broadly and ignores it, which can hide real failures
(e.g., bad file descriptor, invalid state) and lead to misleading “no RTCP SR” conclusions. This
reduces debuggability because the probe will continue running without surfacing the underlying I/O
error.
Code

harness/sip_probe.py[R265-266]

+            except (BlockingIOError, socket.error):
+                pass
Evidence
The capture loop treats all socket.error as ignorable, even though only the non-blocking “no data
available” condition is expected during polling.

harness/sip_probe.py[251-267]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The media receive loop catches `socket.error` and discards it. For non-blocking UDP reads, only `BlockingIOError` (and optionally EWOULDBLOCK/EAGAIN) should be ignored; other errors should be logged or raised.

### Issue Context
This probe is used to diagnose subtle RTP/RTCP behavior; silently masking I/O errors can invalidate its conclusions.

### Fix Focus Areas
- harness/sip_probe.py[251-267]

### Suggested direction
- Catch `BlockingIOError` separately.
- For other `OSError`, log `errno` and either continue (if known transient) or abort.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Password exposed via argv 🐞 Bug ⛨ Security
Description
The probe takes --password as a command-line flag with a default value, which encourages passing
credentials via argv where they can be visible to other local users/process tooling. This is
avoidable by reading from an env var or prompting when omitted.
Code

harness/sip_probe.py[R148-151]

+    ap.add_argument("--user", default="222")
+    ap.add_argument("--password", default="1234")
+    ap.add_argument("--duration", type=float, default=12)
+    ap.add_argument("--outdir", required=True)
Evidence
Argument parsing defines --password and assigns a default value, meaning credentials are expected
to be provided via argv unless the default happens to work.

harness/sip_probe.py[144-152]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Credentials are accepted via `--password` (argv) and default to a real-looking value. Passing secrets via argv is commonly observable (process list, shell history, CI logs).

### Issue Context
Even though this is a harness tool, small changes can prevent accidental credential exposure.

### Fix Focus Areas
- harness/sip_probe.py[144-152]

### Suggested direction
- Prefer `--password-env VAR` (defaulting to something like `MAJESTIC_PASSWORD`) or read from `os.environ`.
- If password is not provided, prompt via `getpass.getpass()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display preferences

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread harness/sip_probe.py
Comment on lines +163 to +166
audc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); audc.bind(("0.0.0.0", aport + 1))
vid = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); vid.bind(("0.0.0.0", 0))
vport = vid.getsockname()[1]
vidc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); vidc.bind(("0.0.0.0", vport + 1))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Rtcp port bind race 🐞 Bug ☼ Reliability

sip_probe binds RTP to an ephemeral port and then binds RTCP to (RTP+1) without reserving the pair,
so the RTCP bind can fail if that adjacent port is already in use, crashing the probe before the
call. This also makes the probe flaky across runs/hosts because the port selection is
nondeterministic and not validated/retried.
Agent Prompt
### Issue description
`sip_probe.py` binds RTP sockets to port 0 (ephemeral) and then immediately binds RTCP sockets to `rtp_port + 1`. The `+1` port is not guaranteed to be free, so the bind can fail and crash the script.

### Issue Context
This probe is intended for quick diagnosis; failing nondeterministically due to port collisions will waste time and produce confusing failures.

### Fix Focus Areas
- harness/sip_probe.py[155-169]

### Suggested direction
- Implement a small helper to allocate a port pair by trying an even base port and binding both RTP and RTCP in a retry loop (closing any partially-created sockets on failure).
- Alternatively, allow explicit `--audio-port`/`--video-port` and validate that `port+1` is available before sending the SDP.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread harness/sip_probe.py
Comment on lines +231 to +233
if not ok_headers:
L("[probe] no 200 OK — aborting"); return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Leaked files/sockets on abort 🐞 Bug ☼ Reliability

sip_probe opens log/output files and creates multiple sockets but never closes them; on the early
abort path (no 200 OK) it returns immediately, leaking resources. Re-running the probe repeatedly in
a harness environment can exhaust file descriptors or leave confusing open handles until process
exit.
Agent Prompt
### Issue description
The script opens `sip.log` and `audio_rtp.bin` and creates multiple sockets, but does not close them; an early `return` path leaks resources.

### Issue Context
This is a diagnostic tool likely to be run iteratively. Clean shutdown improves reliability and avoids FD exhaustion in long-lived harness environments.

### Fix Focus Areas
- harness/sip_probe.py[155-233]
- harness/sip_probe.py[248-287]

### Suggested direction
- Wrap sockets/files in `contextlib.ExitStack()` or `try/finally` to ensure all are closed.
- Replace `log = open(...)` / `araw = open(...)` with `with open(...) as ...` (or register them with ExitStack) so the early-abort path also closes them.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread harness/sip_probe.py
Comment on lines +265 to +266
except (BlockingIOError, socket.error):
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Socket errors silently ignored 🐞 Bug ◔ Observability

The media capture loop catches socket.error broadly and ignores it, which can hide real failures
(e.g., bad file descriptor, invalid state) and lead to misleading “no RTCP SR” conclusions. This
reduces debuggability because the probe will continue running without surfacing the underlying I/O
error.
Agent Prompt
### Issue description
The media receive loop catches `socket.error` and discards it. For non-blocking UDP reads, only `BlockingIOError` (and optionally EWOULDBLOCK/EAGAIN) should be ignored; other errors should be logged or raised.

### Issue Context
This probe is used to diagnose subtle RTP/RTCP behavior; silently masking I/O errors can invalidate its conclusions.

### Fix Focus Areas
- harness/sip_probe.py[251-267]

### Suggested direction
- Catch `BlockingIOError` separately.
- For other `OSError`, log `errno` and either continue (if known transient) or abort.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread harness/sip_probe.py
Comment on lines +148 to +151
ap.add_argument("--user", default="222")
ap.add_argument("--password", default="1234")
ap.add_argument("--duration", type=float, default=12)
ap.add_argument("--outdir", required=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

4. Password exposed via argv 🐞 Bug ⛨ Security

The probe takes --password as a command-line flag with a default value, which encourages passing
credentials via argv where they can be visible to other local users/process tooling. This is
avoidable by reading from an env var or prompting when omitted.
Agent Prompt
### Issue description
Credentials are accepted via `--password` (argv) and default to a real-looking value. Passing secrets via argv is commonly observable (process list, shell history, CI logs).

### Issue Context
Even though this is a harness tool, small changes can prevent accidental credential exposure.

### Fix Focus Areas
- harness/sip_probe.py[144-152]

### Suggested direction
- Prefer `--password-env VAR` (defaulting to something like `MAJESTIC_PASSWORD`) or read from `os.environ`.
- If password is not provided, prompt via `getpass.getpass()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant