feat(rtsp): always apply ZTE NAT traversal - #701
Conversation
Documentation previewThe documentation preview has been deployed for this pull request. |
There was a problem hiding this comment.
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 viaconfig_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.
There was a problem hiding this comment.
💡 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".
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.
Summary
Apply ZTE
ZXV10STBNAT traversal to every IPv4 upstream RTSP connection, with no configuration switch. For IPv4 control connections rtp2httpd now:x-NAT: <address>:<port>on DESCRIBE and SETUP.client_address=<address>andmode=PLAYto the UDP transport candidates in SETUP.ZXV10STBpacket 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 whenrtsp-stun-serveris configured and discovery succeeds, otherwise the RTSP control connection's own local endpoint (which followsr2h-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
ZXV10STBpacket 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|zteswitch. 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
x-NATandclient_addressadvertise, instead of onlyclient_port.source=address when the server provides one, falling back to the RTSP control peer. Previously no probe was sent at all whensource=was absent.Notable implementation details
AWAITING_OPTIONSmeans "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 explicitdescribe_waiting_for_stunflag now marks the parked state.stun_state_tnow retains the mapped IPv4 address, which was previously parsed only to be logged.config_reload()leaked the partially parsed bind address list on its failure path.Validation
pnpm run lint(biome, ruff, clang-format)Note:
test_rtsp_misc.py::TestRTSPTeardownTimeout::test_teardown_timeout_cleanupfailed 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.