Skip to content

fix(stream): drop stray datagrams instead of splicing them into the TS output - #704

Merged
stackia merged 1 commit into
mainfrom
fix/rtsp-stray-datagram
Aug 7, 2026
Merged

fix(stream): drop stray datagrams instead of splicing them into the TS output#704
stackia merged 1 commit into
mainfrom
fix/rtsp-stray-datagram

Conversation

@stackia

@stackia stackia commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Summary

A non-RTP datagram arriving on a media socket was forwarded to the client verbatim. The media sockets are unconnected, so anything can land in the same recv() as the media — in practice a ZTE ZXV10STB punch reply, which rtp2httpd solicits on every 30s keepalive since #701.

The 84-byte packet shifts the rest of the HTTP body off the 188-byte TS grid. ffmpeg-based players (aptv, VLC) resync on the next sync byte and show nothing; the embedded web player (mpegts.js) only probes for TS alignment at the start of a stream, so it stalls on "loading" and re-breaks every 30 seconds.

Only bare MPEG-TS is legitimate on the non-RTP path (servers negotiating plain MP2T, raw TS multicast), so the pass-through is now gated on the existing stream_payload_is_mpegts() check and everything else is dropped with a debug log.

Why the reporter saw what they saw

Feedback on #701: the web player stalls ~30s in while other clients are fine, and routing through rtsproxy fixes it. All three observations fall out of this bug:

  • 30s is exactly RTSP_KEEPALIVE_INTERVAL_MS, when the punch is re-sent.
  • Only the web player breaks, because only it needs the byte stream to stay aligned mid-stream.
  • rtsproxy is clean for two independent reasons: it hard-drops non-RTP packets in RtpPipeline::process() (src/protocol/rtp_pipeline.cpp:19), and it only punches once after SETUP — the per-keepalive resend is commented out in both of its clients (src/clients/rtsp_to_rtsp_client.cpp:977, src/clients/rtsp_to_http_client.cpp:177). Its 20s vs our 30s keepalive period is not a factor.

The periodic punch is deliberately kept: it maintains the media-path NAT mapping that #700's line needs. rtsproxy dropping it looks like a workaround for this same corruption rather than a protocol requirement.

One caveat worth stating plainly: the origin of the stray datagram is inferred from code on both sides, not from a capture. This fix closes the injection path, so it holds regardless of the source — but if the reporter still sees a 30s cadence afterwards, the new -v 4 log line (Stream: Dropped N-byte datagram that is neither RTP nor MPEG-TS) plus a tcpdump -i any -n udp port <client_rtp_port> -X will show what is really arriving.

Test plan

New test_probe_echo_never_reaches_the_client drives the real failure: MockRTSPServerZTE gains an echo_probe_after option that bounces the received 84-byte punch packet back onto the media port mid-stream, the way a ZTE server acks it. The test asserts the body contains no ZXV10STB bytes, that every 188-byte boundary still holds a 0x47 sync byte, and that the drop is logged.

Verified non-vacuous: with src/stream.c reverted and the binary rebuilt, it fails on assert b'ZXV10STB' not in body — the punch bytes really are relayed into the TS stream today.

  • Full e2e suite: 564 passed, 8 skipped
  • uv run ruff check e2e: clean

Refs #700.

🤖 Generated with Claude Code

…S output

A non-RTP datagram arriving on a media socket was forwarded to the client
verbatim. The media sockets are unconnected, so anything can land in the
same recv() as the media -- in practice a ZTE ZXV10STB punch reply, which
rtp2httpd solicits on every 30s keepalive since #701.

The 84-byte packet shifts the rest of the HTTP body off the 188-byte TS
grid. ffmpeg-based players resync on the next sync byte and show nothing;
the embedded web player (mpegts.js) only probes for TS alignment at the
start of a stream, so it stalls on "loading" and re-breaks every 30s.

Only bare MPEG-TS is legitimate on the non-RTP path (servers negotiating
plain MP2T, raw TS multicast), so gate the pass-through on the existing
stream_payload_is_mpegts() check and drop everything else.

For comparison, rtsproxy discards the same packets in RtpPipeline::process()
and additionally sends the punch only once, after SETUP. Keeping our
periodic punch is deliberate: it maintains the media-path NAT mapping that
issue #700's line needs.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Documentation preview

The documentation preview has been deployed for this pull request.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24680e91da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/stream.c
* unconnected and accept from anyone. Splicing such a datagram into the
* output breaks the 188-byte TS alignment for the rest of the stream,
* which strict demuxers (mpegts.js) never recover from. */
if (!stream_payload_is_mpegts(payload, payload_len)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require an integral number of TS packets before forwarding

When a stray datagram is 189–375 bytes long and happens to start with 0x47, stream_payload_is_mpegts() returns true because it checks only offset 0 and never verifies that payload_len is divisible by 188. This branch then forwards the entire datagram, so an unrelated sender can still shift every subsequent TS packet off the 188-byte grid that this change intends to preserve. Use a stricter validation here, including an integral TS-packet length, before passing the buffer through.

Useful? React with 👍 / 👎.

@stackia
stackia merged commit 6a8ed01 into main Aug 7, 2026
11 checks passed
@stackia
stackia deleted the fix/rtsp-stray-datagram branch August 7, 2026 07:38
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