Skip to content

feat(rtsp): always apply ZTE NAT traversal - #701

Merged
stackia merged 4 commits into
mainfrom
codex/rtsp-zte-nat
Aug 6, 2026
Merged

feat(rtsp): always apply ZTE NAT traversal#701
stackia merged 4 commits into
mainfrom
codex/rtsp-zte-nat

Conversation

@stackia

@stackia stackia commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

Apply ZTE ZXV10STB NAT traversal to every IPv4 upstream RTSP connection, with no configuration switch. For IPv4 control connections rtp2httpd now:

  • Sends x-NAT: <address>:<port> on DESCRIBE and SETUP.
  • Appends client_address=<address> and mode=PLAY to the UDP transport candidates in SETUP.
  • Punches the media path with the 84-byte ZXV10STB packet once the server confirms UDP, keeping the existing 3x burst and per-keepalive retransmission.

The advertised address and port are resolved in one place (rtsp_nat_endpoint()): the STUN-discovered public mapping when rtsp-stun-server is configured and discovery succeeds, otherwise the RTSP control connection's own local endpoint (which follows r2h-ifname / upstream-interface-rtsp / upstream-interface / routing table priority). RTP/RTCP sockets are pinned to that local address so the TCP and UDP source addresses stay aligned on multi-interface systems.

Because DESCRIBE already carries x-NAT, STUN discovery now has to settle before DESCRIBE is sent rather than before SETUP.

Why

Issue #700 reports a Shandong Unicom line behind what looks like symmetric NAT, where no STUN server produces a usable mapping but rtsproxy's ZTE mode connects. The ZXV10STB packet is an authentication/punch datagram those operator networks expect on the RTP path; the wire format here is derived from that project and documented inline.

This started out as an opt-in rtsp-nat-mode = auto|stun|zte switch. It was dropped in favour of always-on because the behaviour is additive rather than exclusive: the punch packet replaces only the payload of a probe that was already being sent, STUN still runs and now feeds the advertised endpoint instead of being bypassed, and the SETUP transport candidate list is unchanged. That leaves nothing a user would need to choose between.

Refs #700.

Impact

  • No new configuration, and no user-facing documentation: the feature is transparent.
  • SETUP always offers the full candidate list (TCP interleaved and UDP); the server still picks the transport, and no punch packet is sent when it selects TCP interleaved.
  • IPv6 upstream connections are unaffected — the punch packet's address field is IPv4-only, so those fall back to plain RTSP negotiation with the previous minimal-RTP probe.
  • Existing STUN deployments keep working: the discovered mapping is now also what x-NAT and client_address advertise, instead of only client_port.
  • The punch destination is the source= address when the server provides one, falling back to the RTSP control peer. Previously no probe was sent at all when source= was absent.

Notable implementation details

  • DESCRIBE must not be pipelined. AWAITING_OPTIONS means "OPTIONS sent, response not yet received", so resuming the state machine from the STUN handler on that state alone wrote OPTIONS and DESCRIBE into a single segment (CSeq 1 and 2 back to back). An explicit describe_waiting_for_stun flag now marks the parked state.
  • STUN's retry budget vs. the handshake timeout. Both are ~3s, so the handshake timeout is suspended while parked; STUN's own bounded retries still terminate the wait.
  • stun_state_t now retains the mapped IPv4 address, which was previously parsed only to be logged.
  • Drive-by: config_reload() leaked the partially parsed bind address list on its failure path.
  • Drive-by: the mock RTSP server folded pipelined requests into a single parsed request, which is what masked the DESCRIBE pipelining above. It now buffers the remainder so an unexpectedly pipelined request is visible to tests.

Validation

  • Release CMake build, no warnings
  • pnpm run lint (biome, ruff, clang-format)
  • ZTE E2E suite: 8 passed
  • STUN E2E suite: 5 passed
  • Full E2E suite: 563 passed, 8 skipped

Note: test_rtsp_misc.py::TestRTSPTeardownTimeout::test_teardown_timeout_cleanup failed once across several full-suite runs and did not reproduce afterwards. It does not touch the NAT/STUN path; likely pre-existing timing sensitivity, but not confirmed.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Documentation preview

The documentation preview has been deployed for this pull request.

@stackia
stackia marked this pull request as ready for review August 3, 2026 10:57
@stackia
stackia requested a review from Copilot August 3, 2026 10:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an explicit RTSP NAT traversal mode selection (none|stun|zte) and implements ZTE ZXV10STB NAT punching by deriving the actual local/peer endpoints from the established RTSP TCP control socket, keeping UDP socket binding aligned on multi-interface systems. It also updates OpenWrt/iKuai integration, configuration samples, and adds E2E coverage for the new mode and compatibility behavior.

Changes:

  • Add rtsp-nat-mode (config + CLI) with legacy STUN inference via config_get_effective_rtsp_nat_mode().
  • Implement ZTE NAT punch flow and control-endpoint capture; bind UDP sockets to the RTSP TCP-selected local IPv4 when in ZTE mode.
  • Add OpenWrt/iKuai wiring, docs updates, and new E2E tests + mock server behavior for ZTE.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/rtsp.h Extend RTSP session state with captured control socket endpoints and ZTE mode flags.
src/rtsp.c Implement endpoint capture, ZTE header + probe behavior, and ZTE-aligned UDP binding.
src/configuration.h Introduce rtsp_nat_mode_t and expose effective NAT-mode resolver API.
src/configuration.c Parse/validate rtsp-nat-mode, add CLI flag, and implement legacy inference logic.
rtp2httpd.conf Document new NAT mode option in sample config.
openwrt-support/rtp2httpd/files/rtp2httpd.init Pass --rtsp-nat-mode from UCI config to daemon.
openwrt-support/rtp2httpd/files/rtp2httpd.conf Add UCI option example for rtsp_nat_mode.
openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po Add localized strings for NAT mode UI.
openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot Add template strings for NAT mode UI.
openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js Add LuCI form option for selecting NAT mode.
ikuai-support/rtp2httpd/scripts/start.sh Add env-driven --rtsp-nat-mode CLI wiring.
ikuai-support/rtp2httpd/app/option.json Add iKuai UI/config option for NAT mode.
e2e/test_rtsp_zte_nat.py New end-to-end tests covering ZTE punch bytes, ordering, interface alignment, redirects, IPv6 fallback, and reload behavior.
e2e/helpers/mock_rtsp.py Add ZTE-capable mock RTSP server and request event instrumentation.
e2e/helpers/init.py Export the new MockRTSPServerZTE helper.
docs/reference/configuration.md Document --rtsp-nat-mode and add sample config entries (ZH).
docs/en/reference/configuration.md Document --rtsp-nat-mode and add sample config entries (EN).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rtp2httpd.conf Outdated
Comment thread docs/reference/configuration.md Outdated
Comment thread docs/en/reference/configuration.md Outdated

@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: b5c1968f69

ℹ️ 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 ikuai-support/rtp2httpd/scripts/start.sh Outdated
Comment thread ikuai-support/rtp2httpd/app/option.json Outdated
Comment thread e2e/helpers/mock_rtsp.py Outdated
Comment thread src/configuration.c Outdated
stackia added 2 commits August 3, 2026 19:18
Drop the rtsp-nat-mode switch and make the ZTE behaviours the default for
IPv4 upstream control connections:

- send the x-NAT header on DESCRIBE and SETUP, and client_address/mode=PLAY
  on the UDP transport candidates
- replace the generic RTP hole-punch payload with the 84-byte ZXV10STB
  packet, keeping the existing 3x burst and per-keepalive retransmission
- resolve the advertised endpoint once in rtsp_nat_endpoint(): the
  STUN-discovered public mapping when available, otherwise the control
  connection's own local endpoint

STUN discovery now has to settle before DESCRIBE is sent, since DESCRIBE
already carries x-NAT. The session parks in AWAITING_OPTIONS behind an
explicit describe_waiting_for_stun flag; keying the resume on the state
alone would pipeline DESCRIBE behind an unanswered OPTIONS. The handshake
timeout is suspended while parked so STUN's retry budget cannot trip it.

The feature is transparent to users, so it is not documented.

Also fix config_reload() leaking the partially parsed bind address list on
its failure path, and stop the mock RTSP server from folding pipelined
requests into one parsed request, which had masked the DESCRIBE pipelining.
@stackia stackia changed the title feat(rtsp): add ZTE NAT traversal mode feat(rtsp): always apply ZTE NAT traversal Aug 6, 2026
@stackia
stackia merged commit 31f529d into main Aug 6, 2026
11 checks passed
@stackia
stackia deleted the codex/rtsp-zte-nat branch August 6, 2026 11:25
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.

2 participants