From b5c1968f69b77b792e76a440bec855e55fac4a07 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Mon, 3 Aug 2026 18:49:46 +0800 Subject: [PATCH 1/3] feat(rtsp): add ZTE NAT traversal mode --- docs/en/reference/configuration.md | 11 + docs/reference/configuration.md | 11 + e2e/helpers/__init__.py | 2 + e2e/helpers/mock_rtsp.py | 130 ++++++++ e2e/test_rtsp_zte_nat.py | 291 ++++++++++++++++++ ikuai-support/rtp2httpd/app/option.json | 13 + ikuai-support/rtp2httpd/scripts/start.sh | 3 + .../luci-static/resources/view/rtp2httpd.js | 15 + .../po/templates/rtp2httpd.pot | 20 ++ .../po/zh_Hans/rtp2httpd.po | 20 ++ .../rtp2httpd/files/rtp2httpd.conf | 1 + .../rtp2httpd/files/rtp2httpd.init | 1 + rtp2httpd.conf | 5 + src/configuration.c | 73 ++++- src/configuration.h | 28 +- src/rtsp.c | 164 +++++++++- src/rtsp.h | 11 + 17 files changed, 781 insertions(+), 18 deletions(-) create mode 100644 e2e/test_rtsp_zte_nat.py diff --git a/docs/en/reference/configuration.md b/docs/en/reference/configuration.md index b606c6e1..324dd199 100644 --- a/docs/en/reference/configuration.md +++ b/docs/en/reference/configuration.md @@ -108,6 +108,13 @@ Unix socket listen paths must be absolute and must not contain whitespace. At st - Format: `host:port` or `host` (default port: 3478) - Example: `stun.miwifi.com` or `stun.miwifi.com:3478` +- `--rtsp-nat-mode ` - RTSP NAT traversal mode + - `none`: Disable STUN and ZTE traversal + - `stun`: Use `rtsp-stun-server` to discover the public UDP port; a valid STUN server is required + - `zte`: Use the ZTE `ZXV10STB` UDP punch protocol without contacting the configured STUN server + - When omitted, legacy configurations implicitly use `stun` if `rtsp-stun-server` is set, or `none` otherwise + - ZTE mode supports IPv4 only. Its address comes from the actual local endpoint of the connected RTSP TCP socket, so it follows the priority of `r2h-ifname`, `upstream-interface-rtsp`, `upstream-interface`, and the system routing table + ### Other Options - `-S, --video-snapshot` - Enable video snapshot feature (default: disabled) @@ -245,6 +252,10 @@ http-proxy-user-agent = rtp2httpd-http-proxy/1.0 # Configure this when an upstream RTSP server requires a specific User-Agent for compatibility rtsp-user-agent = rtp2httpd/custom +# RTSP NAT traversal mode: none, stun, or zte (default: none) +# If omitted while rtsp-stun-server is configured, stun is selected for backward compatibility +rtsp-nat-mode = none + # STUN server for RTSP NAT traversal (default: disabled) # When an RTSP server only supports UDP transport and the client is behind NAT, STUN can be used to attempt NAT traversal (not guaranteed to succeed) # Format: host:port or host (default port: 3478) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 509282ef..db9c8d9a 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -108,6 +108,13 @@ Unix socket 监听路径必须是绝对路径,且路径中不能包含空白 - 格式:`host:port` 或 `host`(默认端口 3478) - 示例:`stun.miwifi.com` 或 `stun.miwifi.com:3478` +- `--rtsp-nat-mode ` - RTSP NAT 穿透模式 + - `none`:禁用 STUN 和 ZTE 穿透 + - `stun`:使用 `rtsp-stun-server` 探测公网 UDP 端口;必须配置有效的 STUN 服务器 + - `zte`:使用中兴 `ZXV10STB` UDP 打洞协议,不访问已配置的 STUN 服务器 + - 未显式设置时,为兼容旧配置,存在 `rtsp-stun-server` 即使用 `stun`,否则使用 `none` + - ZTE 模式仅支持 IPv4。其地址取自 RTSP TCP 连接成功后的实际本地 endpoint,因此会遵循 `r2h-ifname`、`upstream-interface-rtsp`、`upstream-interface` 和系统路由表的优先级 + ### 其他 - `-S, --video-snapshot` - 启用视频快照功能 (默认: 关闭) @@ -243,6 +250,10 @@ http-proxy-user-agent = rtp2httpd-http-proxy/1.0 # 当上游 RTSP 服务器要求特定 User-Agent 时可配置此项 rtsp-user-agent = rtp2httpd/custom +# RTSP NAT 穿透模式:none、stun 或 zte(默认: none) +# 未设置此项但配置了 rtsp-stun-server 时,自动使用 stun 以兼容旧配置 +rtsp-nat-mode = none + # STUN 服务器用于 RTSP NAT 穿透(默认: 禁用) # 当 RTSP 服务器仅支持 UDP 传输且客户端位于 NAT 后时,可尝试使用 STUN 进行 NAT 穿透(不保证成功) # 格式: host:port 或 host(默认端口 3478) diff --git a/e2e/helpers/__init__.py b/e2e/helpers/__init__.py index ca4ecdb5..c78c9e61 100644 --- a/e2e/helpers/__init__.py +++ b/e2e/helpers/__init__.py @@ -46,6 +46,7 @@ MockRTSPServerNoTeardownResponse, MockRTSPServerSilent, MockRTSPServerUDP, + MockRTSPServerZTE, ) from .mock_stun import MockSTUNServer from .ports import ( @@ -73,6 +74,7 @@ "MockRTSPServerNoTeardownResponse", "MockRTSPServerSilent", "MockRTSPServerUDP", + "MockRTSPServerZTE", "MockSTUNServer", "MulticastSender", "R2HProcess", diff --git a/e2e/helpers/mock_rtsp.py b/e2e/helpers/mock_rtsp.py index e315e785..75504211 100644 --- a/e2e/helpers/mock_rtsp.py +++ b/e2e/helpers/mock_rtsp.py @@ -78,6 +78,8 @@ def __init__( self._stop = threading.Event() self.requests_received: list[str] = [] self.requests_detailed: list[dict] = [] + self.control_peer: tuple | None = None + self.events: list[str] = [] # -- lifecycle ----------------------------------------------------------- @@ -126,6 +128,7 @@ def _accept(self) -> None: def _handle(self, conn: socket.socket, addr: tuple) -> None: conn.settimeout(10.0) + self.control_peer = addr transport_hdr = "" try: while True: @@ -236,7 +239,9 @@ def _handle(self, conn: socket.socket, addr: tuple) -> None: return elif method == "SETUP": conn.sendall(self._setup_response(cseq, transport_hdr).encode()) + self.events.append("setup_response_sent") elif method == "PLAY": + self.events.append("play_received") extra_headers = "".join("%s: %s\r\n" % item for item in self._play_response_headers) conn.sendall( ( @@ -401,6 +406,131 @@ def _after_play(self, conn: socket.socket, addr: tuple) -> None: udp_sock.close() +# --------------------------------------------------------------------------- +# MockRTSPServerZTE -- ZTE UDP NAT traversal mode +# --------------------------------------------------------------------------- + + +class MockRTSPServerZTE(_RTSPServerBase): + """RTSP server that starts UDP media only after a valid ZTE punch packet.""" + + def __init__(self, port: int = 0, num_packets: int = 200): + super().__init__(port) + self._num_packets = num_packets + self._server_rtp_socket: socket.socket | None = None + self._server_rtcp_socket: socket.socket | None = None + self._receiver_thread: threading.Thread | None = None + self._play_started = threading.Event() + self._valid_probe = threading.Event() + self._client_rtp_port = 0 + self._client_address = "" + self._server_rtp_port = 0 + self._server_rtcp_port = 0 + self.udp_datagrams: list[tuple[bytes, tuple]] = [] + + @property + def valid_probe_received(self) -> bool: + return self._valid_probe.is_set() + + def stop(self) -> None: + self._play_started.set() + super().stop() + if self._server_rtp_socket: + self._server_rtp_socket.close() + if self._server_rtcp_socket: + self._server_rtcp_socket.close() + if self._receiver_thread: + self._receiver_thread.join(timeout=2) + + def _setup_response(self, cseq: str, transport_hdr: str) -> str: + for part in transport_hdr.split(";"): + part = part.strip() + if part.startswith("client_port="): + self._client_rtp_port = int(part.split("=", 1)[1].split("-", 1)[0]) + elif part.startswith("client_address="): + self._client_address = part.split("=", 1)[1] + + while True: + rtp_port, rtcp_port = find_free_udp_port_pair() + rtp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + rtcp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + rtp_socket.bind((self.host, rtp_port)) + rtcp_socket.bind((self.host, rtcp_port)) + break + except OSError: + rtp_socket.close() + rtcp_socket.close() + + self._server_rtp_socket = rtp_socket + self._server_rtcp_socket = rtcp_socket + self._server_rtp_port = rtp_port + self._server_rtcp_port = rtcp_port + self._receiver_thread = threading.Thread(target=self._receive_probes, daemon=True) + self._receiver_thread.start() + + return ( + "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" + "Transport: MP2T/RTP/UDP;unicast;client_port=%d-%d;server_port=%d-%d\r\n" + "Session: t1\r\n\r\n" + % (cseq, self._client_rtp_port, self._client_rtp_port + 1, self._server_rtp_port, self._server_rtcp_port) + ) + + def _receive_probes(self) -> None: + assert self._server_rtp_socket is not None + self._server_rtp_socket.settimeout(0.05) + while not self._stop.is_set(): + try: + payload, source = self._server_rtp_socket.recvfrom(2048) + self.udp_datagrams.append((payload, source)) + if self._probe_is_valid(payload, source): + self.events.append("zte_probe_received") + self._valid_probe.set() + except socket.timeout: + if self._play_started.is_set(): + return + except OSError: + return + + def _probe_is_valid(self, payload: bytes, source: tuple) -> bool: + if not self.control_peer or len(payload) != 84: + return False + expected_ip = socket.inet_aton(self.control_peer[0]) + expected_tcp_port = self.control_peer[1] + return ( + payload[:8] == b"ZXV10STB" + and payload[8:12] == b"\x7f\xff\xff\xff" + and payload[12:16] == expected_ip + and struct.unpack("!H", payload[16:18])[0] == self._client_rtp_port + and struct.unpack("!H", payload[18:20])[0] == expected_tcp_port + and payload[20:] == bytes(64) + and source[0] == self.control_peer[0] + and source[1] == self._client_rtp_port + ) + + def _after_play(self, conn: socket.socket, addr: tuple) -> None: + self._play_started.set() + if not self._valid_probe.wait(timeout=2.0) or not self.udp_datagrams: + return + if self._receiver_thread: + self._receiver_thread.join(timeout=0.2) + + assert self._server_rtp_socket is not None + destination = self.udp_datagrams[0][1] + seq = 0 + ts = 0 + try: + for _ in range(self._num_packets): + if self._stop.is_set(): + break + self._server_rtp_socket.sendto(make_rtp_packet(seq, ts), destination) + seq = (seq + 1) & 0xFFFF + ts = (ts + 3600) & 0xFFFFFFFF + time.sleep(0.001) + except OSError: + pass + + # --------------------------------------------------------------------------- # MockRTSPServerSilent -- accepts connection but never responds # --------------------------------------------------------------------------- diff --git a/e2e/test_rtsp_zte_nat.py b/e2e/test_rtsp_zte_nat.py new file mode 100644 index 00000000..dd266875 --- /dev/null +++ b/e2e/test_rtsp_zte_nat.py @@ -0,0 +1,291 @@ +"""End-to-end coverage for the ZTE RTSP NAT traversal mode.""" + +import os +import signal +import socket +import struct +import time + +import pytest + +from helpers import ( + LOOPBACK_IF, + MockRTSPServer, + MockRTSPServerZTE, + MockSTUNServer, + R2HProcess, + build_config, + find_free_port, + ipv6_loopback_available, + stream_get, +) + +pytestmark = pytest.mark.rtsp + + +def _request(server, method): + return next(request for request in server.requests_detailed if request["method"] == method) + + +class TestZTEProtocol: + def test_zte_headers_endpoint_invariants_and_probe_bytes(self, r2h_binary): + stun = MockSTUNServer() + rtsp = MockRTSPServerZTE(num_packets=500) + stun.start() + rtsp.start() + r2h_port = find_free_port() + r2h = R2HProcess( + r2h_binary, + r2h_port, + extra_args=[ + "-v", + "4", + "-m", + "100", + "--rtsp-nat-mode", + "zte", + "--rtsp-stun-server", + "127.0.0.1:%d" % stun.port, + ], + capture_log=True, + ) + r2h.start() + try: + status, _, body = stream_get( + "127.0.0.1", + r2h_port, + "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + read_bytes=4096, + timeout=20.0, + ) + assert status == 200 + assert len(body) >= 188 + assert rtsp.valid_probe_received + assert stun.requests_received == 0 + + assert rtsp.control_peer is not None + tcp_source_ip, tcp_source_port = rtsp.control_peer[:2] + expected_x_nat = "%s:%d" % (tcp_source_ip, tcp_source_port) + describe = _request(rtsp, "DESCRIBE") + setup = _request(rtsp, "SETUP") + assert describe["headers"]["x-NAT"] == expected_x_nat + assert setup["headers"]["x-NAT"] == expected_x_nat + + transport = setup["headers"]["Transport"] + assert transport == ( + "MP2T/RTP/UDP;unicast;client_address=%s;client_port=%d-%d;mode=PLAY" + % (tcp_source_ip, rtsp._client_rtp_port, rtsp._client_rtp_port + 1) + ) + + assert len(rtsp.udp_datagrams) == 1 + payload, udp_source = rtsp.udp_datagrams[0] + assert len(payload) == 84 + assert payload[:8] == b"ZXV10STB" + assert payload[8:12] == b"\x7f\xff\xff\xff" + assert payload[12:16] == socket.inet_aton(tcp_source_ip) + assert struct.unpack("!H", payload[16:18])[0] == rtsp._client_rtp_port + assert struct.unpack("!H", payload[18:20])[0] == tcp_source_port + assert payload[20:] == bytes(64) + assert udp_source[0] == tcp_source_ip + assert udp_source[1] == rtsp._client_rtp_port + assert rtsp.events.index("setup_response_sent") < rtsp.events.index("zte_probe_received") + assert rtsp.events.index("zte_probe_received") < rtsp.events.index("play_received") + + log = r2h.read_log() + assert "RTSP STUN server ignored because rtsp-nat-mode=zte" in log + assert "RTSP: Upstream interface route-selected, local endpoint %s:" % tcp_source_ip in log + finally: + r2h.stop() + rtsp.stop() + stun.stop() + + @pytest.mark.parametrize("interface_source", ["global", "request"]) + def test_interface_selection_keeps_tcp_and_udp_source_aligned(self, r2h_binary, interface_source): + rtsp = MockRTSPServerZTE(num_packets=200) + rtsp.start() + r2h_port = find_free_port() + extra_args = ["--rtsp-nat-mode", "zte"] + path = "/rtsp/127.0.0.1:%d/stream" % rtsp.port + if interface_source == "global": + extra_args.extend(["--upstream-interface-rtsp", LOOPBACK_IF]) + else: + path += "?r2h-ifname=%s" % LOOPBACK_IF + r2h = R2HProcess(r2h_binary, r2h_port, extra_args=extra_args) + r2h.start() + try: + status, _, body = stream_get("127.0.0.1", r2h_port, path, read_bytes=188, timeout=20.0) + assert status == 200 + assert body + assert rtsp.valid_probe_received + assert rtsp.control_peer is not None + assert rtsp.udp_datagrams[0][1][0] == rtsp.control_peer[0] + finally: + r2h.stop() + rtsp.stop() + + def test_ipv6_upstream_falls_back_to_ordinary_rtsp(self, r2h_binary): + if not ipv6_loopback_available(): + pytest.skip("IPv6 loopback is unavailable") + rtsp = MockRTSPServer(num_packets=300, host="::1") + rtsp.start() + r2h_port = find_free_port() + r2h = R2HProcess( + r2h_binary, + r2h_port, + extra_args=["-v", "4", "--rtsp-nat-mode", "zte"], + capture_log=True, + ) + r2h.start() + try: + status, _, body = stream_get( + "127.0.0.1", + r2h_port, + "/rtsp/[::1]:%d/stream" % rtsp.port, + read_bytes=188, + timeout=20.0, + ) + assert status == 200 + assert body + assert "x-NAT" not in _request(rtsp, "DESCRIBE")["headers"] + assert "ZTE NAT traversal only supports IPv4" in r2h.read_log() + finally: + r2h.stop() + rtsp.stop() + + def test_redirect_recaptures_control_endpoint(self, r2h_binary): + target = MockRTSPServerZTE(num_packets=300) + target.start() + redirect = MockRTSPServer(redirect_describe_to="rtsp://127.0.0.1:%d/stream" % target.port) + redirect.start() + r2h_port = find_free_port() + r2h = R2HProcess(r2h_binary, r2h_port, extra_args=["--rtsp-nat-mode", "zte"]) + r2h.start() + try: + status, _, body = stream_get( + "127.0.0.1", + r2h_port, + "/rtsp/127.0.0.1:%d/stream" % redirect.port, + read_bytes=188, + timeout=20.0, + ) + assert status == 200 + assert body + assert target.valid_probe_received + assert target.control_peer is not None + expected_x_nat = "%s:%d" % target.control_peer[:2] + assert _request(target, "DESCRIBE")["headers"]["x-NAT"] == expected_x_nat + finally: + r2h.stop() + redirect.stop() + target.stop() + + +class TestNATModeCompatibility: + def test_config_file_enables_zte(self, r2h_binary): + rtsp = MockRTSPServerZTE(num_packets=200) + rtsp.start() + r2h_port = find_free_port() + r2h = R2HProcess( + r2h_binary, + r2h_port, + config_content=build_config(r2h_port, global_lines=["rtsp-nat-mode = zte"]), + ) + r2h.start() + try: + status, _, body = stream_get( + "127.0.0.1", + r2h_port, + "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + read_bytes=188, + timeout=20.0, + ) + assert status == 200 + assert body + assert rtsp.valid_probe_received + finally: + r2h.stop() + rtsp.stop() + + def test_explicit_none_disables_legacy_stun_inference(self, r2h_binary): + stun = MockSTUNServer() + rtsp = MockRTSPServer(num_packets=500) + stun.start() + rtsp.start() + r2h_port = find_free_port() + r2h = R2HProcess( + r2h_binary, + r2h_port, + extra_args=[ + "--rtsp-nat-mode", + "none", + "--rtsp-stun-server", + "127.0.0.1:%d" % stun.port, + ], + ) + r2h.start() + try: + status, _, body = stream_get( + "127.0.0.1", + r2h_port, + "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + read_bytes=4096, + timeout=20.0, + ) + assert status == 200 + assert body + time.sleep(0.1) + assert stun.requests_received == 0 + assert "x-NAT" not in _request(rtsp, "DESCRIBE")["headers"] + finally: + r2h.stop() + rtsp.stop() + stun.stop() + + def test_explicit_stun_requires_server(self, r2h_binary): + r2h = R2HProcess( + r2h_binary, + find_free_port(), + extra_args=["--rtsp-nat-mode", "stun"], + capture_log=True, + ) + r2h.start(wait=False) + try: + assert r2h.process is not None + assert r2h.process.wait(timeout=5) != 0 + assert "rtsp-nat-mode=stun requires rtsp-stun-server" in r2h.read_log() + finally: + r2h.stop() + + def test_cli_mode_override_survives_reload(self, r2h_binary): + rtsp = MockRTSPServer(num_packets=300) + rtsp.start() + r2h_port = find_free_port() + r2h = R2HProcess( + r2h_binary, + r2h_port, + config_content=build_config(r2h_port, global_lines=["rtsp-nat-mode = zte"]), + extra_args=["--rtsp-nat-mode", "none"], + capture_log=True, + ) + r2h.start() + try: + path = "/rtsp/127.0.0.1:%d/stream" % rtsp.port + status, _, body = stream_get("127.0.0.1", r2h_port, path, read_bytes=188, timeout=20.0) + assert status == 200 + assert body + + assert r2h.process is not None + os.kill(r2h.process.pid, signal.SIGHUP) + deadline = time.time() + 5.0 + while "Configuration reloaded successfully" not in r2h.read_log() and time.time() < deadline: + time.sleep(0.05) + + status, _, body = stream_get("127.0.0.1", r2h_port, path, read_bytes=188, timeout=20.0) + assert status == 200 + assert body + describe_requests = [request for request in rtsp.requests_detailed if request["method"] == "DESCRIBE"] + assert len(describe_requests) >= 2 + assert all("x-NAT" not in request["headers"] for request in describe_requests) + finally: + r2h.stop() + rtsp.stop() diff --git a/ikuai-support/rtp2httpd/app/option.json b/ikuai-support/rtp2httpd/app/option.json index 6077d3ba..5e9f1fc1 100644 --- a/ikuai-support/rtp2httpd/app/option.json +++ b/ikuai-support/rtp2httpd/app/option.json @@ -298,6 +298,19 @@ "min": 0, "max": 1 }, + { + "default": "none", + "attrname": "RTP2HTTPD_RTSP_NAT_MODE", + "label": { + "en": "RTSP NAT mode: none, stun, or zte", + "zh": "RTSP NAT 模式:none、stun 或 zte" + }, + "required": true, + "scope": "config", + "type": "text", + "min": 3, + "max": 4 + }, { "default": "", "attrname": "RTP2HTTPD_RTSP_STUN_SERVER", diff --git a/ikuai-support/rtp2httpd/scripts/start.sh b/ikuai-support/rtp2httpd/scripts/start.sh index 3c0099ec..fb3de600 100755 --- a/ikuai-support/rtp2httpd/scripts/start.sh +++ b/ikuai-support/rtp2httpd/scripts/start.sh @@ -161,6 +161,9 @@ fi if [ "$RTP2HTTPD_ZEROCOPY_ON_SEND" = "1" ]; then set -- "$@" --zerocopy-on-send fi +if [ -n "$RTP2HTTPD_RTSP_NAT_MODE" ]; then + set -- "$@" --rtsp-nat-mode "$RTP2HTTPD_RTSP_NAT_MODE" +fi if [ -n "$RTP2HTTPD_RTSP_STUN_SERVER" ]; then set -- "$@" --rtsp-stun-server "$RTP2HTTPD_RTSP_STUN_SERVER" fi diff --git a/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js b/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js index cb36d09b..d7b8e486 100644 --- a/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js +++ b/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js @@ -634,6 +634,21 @@ return view.extend({ o.default = "0"; o.depends("use_config_file", "0"); + o = s.taboption( + "network", + form.ListValue, + "rtsp_nat_mode", + _("RTSP NAT Traversal Mode"), + _( + "Select none to disable traversal, STUN for standard address discovery, or ZTE for the ZXV10STB UDP punch protocol. ZTE uses the actual IPv4 endpoint selected for the RTSP TCP connection and ignores the STUN server." + ) + ); + o.value("none", _("None")); + o.value("stun", _("STUN")); + o.value("zte", _("ZTE")); + o.default = "none"; + o.depends("use_config_file", "0"); + o = s.taboption( "network", form.Value, diff --git a/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot b/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot index cb2972bd..818db230 100644 --- a/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot +++ b/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot @@ -410,6 +410,26 @@ msgstr "" msgid "rtp2httpd" msgstr "" +#: htdocs/luci-static/resources/view/rtp2httpd.js:644 +msgid "RTSP NAT Traversal Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:646 +msgid "Select none to disable traversal, STUN for standard address discovery, or ZTE for the ZXV10STB UDP punch protocol. ZTE uses the actual IPv4 endpoint selected for the RTSP TCP connection and ignores the STUN server." +msgstr "" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:650 +msgid "None" +msgstr "" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:651 +msgid "STUN" +msgstr "" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:652 +msgid "ZTE" +msgstr "" + #: htdocs/luci-static/resources/view/rtp2httpd.js:281 msgid "" "rtp2httpd converts RTP/UDP/RTSP media into http stream. Here you can " diff --git a/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po b/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po index 280b0045..3cd46096 100644 --- a/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po +++ b/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po @@ -457,6 +457,26 @@ msgstr "启用零拷贝发送" msgid "rtp2httpd" msgstr "rtp2httpd" +#: htdocs/luci-static/resources/view/rtp2httpd.js:644 +msgid "RTSP NAT Traversal Mode" +msgstr "RTSP NAT 穿透模式" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:646 +msgid "Select none to disable traversal, STUN for standard address discovery, or ZTE for the ZXV10STB UDP punch protocol. ZTE uses the actual IPv4 endpoint selected for the RTSP TCP connection and ignores the STUN server." +msgstr "选择 none 可禁用穿透,选择 STUN 可进行标准地址探测,选择 ZTE 可使用 ZXV10STB UDP 打洞协议。ZTE 使用 RTSP TCP 连接实际选择的 IPv4 endpoint,并忽略 STUN 服务器。" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:650 +msgid "None" +msgstr "无" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:651 +msgid "STUN" +msgstr "STUN" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:652 +msgid "ZTE" +msgstr "ZTE" + #: htdocs/luci-static/resources/view/rtp2httpd.js:281 msgid "" "rtp2httpd converts RTP/UDP/RTSP media into http stream. Here you can " diff --git a/openwrt-support/rtp2httpd/files/rtp2httpd.conf b/openwrt-support/rtp2httpd/files/rtp2httpd.conf index 46285fb1..334c1cd0 100644 --- a/openwrt-support/rtp2httpd/files/rtp2httpd.conf +++ b/openwrt-support/rtp2httpd/files/rtp2httpd.conf @@ -47,6 +47,7 @@ config rtp2httpd # option zerocopy_on_send '0' # option http_proxy_user_agent 'rtp2httpd-http-proxy/1.0' # option rtsp_user_agent 'rtp2httpd/custom' + # option rtsp_nat_mode 'none' # option rtsp_stun_server 'stun.miwifi.com' # option cors_allow_origin '*' # option ffmpeg_path 'ffmpeg' diff --git a/openwrt-support/rtp2httpd/files/rtp2httpd.init b/openwrt-support/rtp2httpd/files/rtp2httpd.init index b8674cfb..d094d09b 100644 --- a/openwrt-support/rtp2httpd/files/rtp2httpd.init +++ b/openwrt-support/rtp2httpd/files/rtp2httpd.init @@ -87,6 +87,7 @@ start_instance() { append_arg "$cfg" fcc_listen_port_range "--fcc-listen-port-range" append_arg "$cfg" http_proxy_user_agent "--http-proxy-user-agent" append_arg "$cfg" rtsp_user_agent "--rtsp-user-agent" + append_arg "$cfg" rtsp_nat_mode "--rtsp-nat-mode" append_arg "$cfg" rtsp_stun_server "--rtsp-stun-server" append_arg "$cfg" cors_allow_origin "--cors-allow-origin" append_arg "$cfg" ffmpeg_path "--ffmpeg-path" diff --git a/rtp2httpd.conf b/rtp2httpd.conf index cffa879f..dae12508 100644 --- a/rtp2httpd.conf +++ b/rtp2httpd.conf @@ -119,6 +119,11 @@ verbosity = 1 # User-Agent header used for upstream RTSP requests (default: rtp2httpd/) ;rtsp-user-agent = rtp2httpd/custom +# RTSP NAT traversal mode: none, stun, or zte (default: none) +# For backward compatibility, omitting this option enables STUN automatically +# when rtsp-stun-server is configured. +;rtsp-nat-mode = none + # STUN server for RTSP NAT traversal (default: disabled) # When RTSP server only supports UDP transport and client is behind NAT, # try using STUN for NAT traversal (may not always succeed) diff --git a/src/configuration.c b/src/configuration.c index db1f4c0b..13f06f71 100644 --- a/src/configuration.c +++ b/src/configuration.c @@ -50,6 +50,7 @@ int cmd_zerocopy_on_send_set = 0; int cmd_workers_set = 0; int cmd_external_m3u_url_set = 0; int cmd_external_m3u_update_interval_set = 0; +int cmd_rtsp_nat_mode_set = 0; int cmd_rtsp_stun_server_set = 0; int cmd_http_proxy_user_agent_set = 0; int cmd_rtsp_user_agent_set = 0; @@ -65,7 +66,8 @@ enum long_option_e { OPT_USE_RELATIVE_PATH_IN_M3U, OPT_ACCESS_LOG, OPT_LOG_FORMAT, - OPT_PID_FILE + OPT_PID_FILE, + OPT_RTSP_NAT_MODE }; /* M3U parsing state variables */ @@ -96,6 +98,45 @@ static int parse_bool(const char *value) { (strcasecmp("1", value) == 0); } +static rtsp_nat_mode_t parse_rtsp_nat_mode(const char *value) { + if (strcasecmp(value, "none") == 0) + return RTSP_NAT_MODE_NONE; + if (strcasecmp(value, "stun") == 0) + return RTSP_NAT_MODE_STUN; + if (strcasecmp(value, "zte") == 0) + return RTSP_NAT_MODE_ZTE; + return RTSP_NAT_MODE_INVALID; +} + +rtsp_nat_mode_t config_get_effective_rtsp_nat_mode(void) { + if (config.rtsp_nat_mode == RTSP_NAT_MODE_AUTO) { + if (config.rtsp_stun_server && config.rtsp_stun_server[0] != '\0') + return RTSP_NAT_MODE_STUN; + return RTSP_NAT_MODE_NONE; + } + return config.rtsp_nat_mode; +} + +static int validate_rtsp_nat_config(void) { + static int zte_stun_warning_logged = 0; + rtsp_nat_mode_t mode = config_get_effective_rtsp_nat_mode(); + + if (mode == RTSP_NAT_MODE_INVALID) { + logger(LOG_ERROR, "Invalid rtsp-nat-mode (expected none, stun, or zte)"); + return -1; + } + if (mode == RTSP_NAT_MODE_STUN && (!config.rtsp_stun_server || config.rtsp_stun_server[0] == '\0')) { + logger(LOG_ERROR, "rtsp-nat-mode=stun requires rtsp-stun-server"); + return -1; + } + if (mode == RTSP_NAT_MODE_ZTE && config.rtsp_stun_server && config.rtsp_stun_server[0] != '\0' && + !zte_stun_warning_logged) { + logger(LOG_WARN, "RTSP STUN server ignored because rtsp-nat-mode=zte"); + zte_stun_warning_logged = 1; + } + return 0; +} + /* Set config value if not already set by command line */ static int set_if_not_cmd_override(int cmd_flag, const char *param_name) { if (cmd_flag) { @@ -751,7 +792,16 @@ void parse_global_sec(char *line) { return; } - /* STUN NAT traversal configuration */ + /* RTSP NAT traversal configuration */ + if (strcasecmp("rtsp-nat-mode", param) == 0) { + if (set_if_not_cmd_override(cmd_rtsp_nat_mode_set, "rtsp-nat-mode")) { + config.rtsp_nat_mode = parse_rtsp_nat_mode(value); + if (config.rtsp_nat_mode == RTSP_NAT_MODE_INVALID) + logger(LOG_ERROR, "Invalid rtsp-nat-mode value: %s", value); + } + return; + } + if (strcasecmp("rtsp-stun-server", param) == 0) { if (!cmd_rtsp_stun_server_set) { safe_free_string(&config.rtsp_stun_server); @@ -1176,6 +1226,8 @@ void config_init(void) { config.zerocopy_on_send = 0; if (!cmd_use_relative_path_in_m3u_set) config.use_relative_path_in_m3u = 0; + if (!cmd_rtsp_nat_mode_set) + config.rtsp_nat_mode = RTSP_NAT_MODE_AUTO; if (!cmd_fcc_listen_port_range_set) { config.fcc_listen_port_min = 0; config.fcc_listen_port_max = 0; @@ -1257,6 +1309,9 @@ int config_reload(int *out_bind_changed) { return -1; } + if (validate_rtsp_nat_config() < 0) + return -1; + apply_bind_side_effects(); /* Check if bind addresses changed */ @@ -1349,6 +1404,8 @@ void usage(FILE *f, char *progname) { "(default: rtp2httpd/)\n" "\t-N --rtsp-stun-server STUN server for RTSP NAT traversal " "(default: disabled)\n" + "\t --rtsp-nat-mode RTSP NAT traversal mode " + "(default: infer stun when rtsp-stun-server is set)\n" "\t-O --cors-allow-origin Set Access-Control-Allow-Origin header " "(default: disabled)\n" "\t --access-log Write access logs to this file (default: disabled)\n" @@ -1432,6 +1489,7 @@ void parse_cmd_line(int argc, char *argv[]) { {"zerocopy-on-send", no_argument, 0, 'Z'}, {"http-proxy-user-agent", required_argument, 0, 'g'}, {"rtsp-stun-server", required_argument, 0, 'N'}, + {"rtsp-nat-mode", required_argument, 0, OPT_RTSP_NAT_MODE}, {"rtsp-user-agent", required_argument, 0, 'u'}, {"cors-allow-origin", required_argument, 0, 'O'}, {"access-log", required_argument, 0, OPT_ACCESS_LOG}, @@ -1645,6 +1703,14 @@ void parse_cmd_line(int argc, char *argv[]) { cmd_rtsp_stun_server_set = 1; logger(LOG_INFO, "RTSP STUN server: %s", config.rtsp_stun_server); break; + case OPT_RTSP_NAT_MODE: + config.rtsp_nat_mode = parse_rtsp_nat_mode(optarg); + if (config.rtsp_nat_mode == RTSP_NAT_MODE_INVALID) { + logger(LOG_FATAL, "Invalid --rtsp-nat-mode value: %s (expected none, stun, or zte)", optarg); + exit(EXIT_FAILURE); + } + cmd_rtsp_nat_mode_set = 1; + break; case 'u': safe_free_string(&config.rtsp_user_agent); if (optarg[0] != '\0') { @@ -1695,6 +1761,9 @@ void parse_cmd_line(int argc, char *argv[]) { set_config_file_path(NULL); } + if (validate_rtsp_nat_config() < 0) + exit(EXIT_FAILURE); + apply_bind_side_effects(); /* External M3U will be loaded asynchronously by workers after startup diff --git a/src/configuration.h b/src/configuration.h index 11f5e631..8ee283e7 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -26,6 +26,14 @@ typedef enum loglevel { typedef enum { BIND_ADDR_TCP = 0, BIND_ADDR_UNIX } bindaddr_type_t; +typedef enum { + RTSP_NAT_MODE_INVALID = -1, + RTSP_NAT_MODE_AUTO = 0, + RTSP_NAT_MODE_NONE, + RTSP_NAT_MODE_STUN, + RTSP_NAT_MODE_ZTE +} rtsp_nat_mode_t; + /* * Linked list of addresses to bind */ @@ -118,13 +126,14 @@ typedef struct { int zerocopy_on_send; /* Enable zero-copy send with MSG_ZEROCOPY (0=disabled, 1=enabled) */ - /* STUN NAT traversal settings */ - char *rtsp_stun_server; /* STUN server host:port for RTSP NAT traversal - (NULL=disabled) */ - char *http_proxy_user_agent; /* Override User-Agent header for upstream HTTP - proxy requests (NULL=disabled) */ - char *rtsp_user_agent; /* User-Agent header for upstream RTSP requests - (NULL=use default) */ + /* RTSP NAT traversal settings */ + rtsp_nat_mode_t rtsp_nat_mode; /* Explicit NAT traversal mode, or AUTO for legacy inference */ + char *rtsp_stun_server; /* STUN server host:port for RTSP NAT traversal + (NULL=disabled) */ + char *http_proxy_user_agent; /* Override User-Agent header for upstream HTTP + proxy requests (NULL=disabled) */ + char *rtsp_user_agent; /* User-Agent header for upstream RTSP requests + (NULL=use default) */ /* CORS settings */ char *cors_allow_origin; /* CORS Access-Control-Allow-Origin value @@ -233,4 +242,9 @@ int bind_addresses_equal(bindaddr_t *a, bindaddr_t *b); */ int bind_addresses_has_unix(void); +/** + * Resolve the configured RTSP NAT mode, including legacy STUN inference. + */ +rtsp_nat_mode_t config_get_effective_rtsp_nat_mode(void); + #endif /* __CONFIGURATION_H__ */ diff --git a/src/rtsp.c b/src/rtsp.c index ace92e3a..e42194bb 100644 --- a/src/rtsp.c +++ b/src/rtsp.c @@ -53,6 +53,8 @@ static void rtsp_close_udp_sockets(rtsp_session_t *session, const char *reason); static char *rtsp_find_header(const char *response, const char *header_name); static void rtsp_parse_transport_header(rtsp_session_t *session, const char *transport); static void rtsp_send_udp_nat_probe(rtsp_session_t *session); +static void rtsp_send_zte_nat_probe(rtsp_session_t *session); +static int rtsp_capture_control_endpoints(rtsp_session_t *session); static int rtsp_process_interleaved_buffer(rtsp_session_t *session, connection_t *conn); static int rtsp_handle_redirect(rtsp_session_t *session, const char *location); static void rtsp_parse_describe_sdp(rtsp_session_t *session, const char *header_start, const char *sdp_body); @@ -300,6 +302,12 @@ void rtsp_session_init(rtsp_session_t *session) { session->metadata_probe = 0; session->peer_closed = 0; session->connect_generation = 0; + session->control_local_addr_len = 0; + session->control_peer_addr_len = 0; + session->control_local_ip[0] = '\0'; + session->control_local_port = 0; + session->control_endpoints_valid = 0; + session->zte_active = 0; /* Initialize transport parameters - mode will be negotiated during SETUP */ session->transport_mode = RTSP_TRANSPORT_TCP; /* Default preference */ @@ -739,6 +747,73 @@ static void rtsp_free_connect_results(rtsp_session_t *session) { } } +static int rtsp_capture_control_endpoints(rtsp_session_t *session) { + struct sockaddr_storage local_addr; + struct sockaddr_storage peer_addr; + socklen_t local_len = sizeof(local_addr); + socklen_t peer_len = sizeof(peer_addr); + char local_host[RTSP_SERVER_HOST_SIZE]; + char local_service[RTSP_PORT_STRING_SIZE]; + int local_ip_changed = 0; + int zte_requested = config_get_effective_rtsp_nat_mode() == RTSP_NAT_MODE_ZTE; + + memset(&local_addr, 0, sizeof(local_addr)); + memset(&peer_addr, 0, sizeof(peer_addr)); + if (getsockname(session->socket, (struct sockaddr *)&local_addr, &local_len) < 0) { + logger(LOG_ERROR, "RTSP: getsockname() failed: %s", strerror(errno)); + session->control_endpoints_valid = 0; + session->zte_active = 0; + return -1; + } + if (getpeername(session->socket, (struct sockaddr *)&peer_addr, &peer_len) < 0) { + logger(LOG_ERROR, "RTSP: getpeername() failed: %s", strerror(errno)); + session->control_endpoints_valid = 0; + session->zte_active = 0; + return -1; + } + if (getnameinfo((struct sockaddr *)&local_addr, local_len, local_host, sizeof(local_host), local_service, + sizeof(local_service), NI_NUMERICHOST | NI_NUMERICSERV) != 0) { + logger(LOG_ERROR, "RTSP: Failed to format local control endpoint"); + session->control_endpoints_valid = 0; + session->zte_active = 0; + return -1; + } + + if (session->control_endpoints_valid && session->control_local_addr.ss_family == AF_INET && + local_addr.ss_family == AF_INET) { + const struct sockaddr_in *old_local = (const struct sockaddr_in *)&session->control_local_addr; + const struct sockaddr_in *new_local = (const struct sockaddr_in *)&local_addr; + local_ip_changed = old_local->sin_addr.s_addr != new_local->sin_addr.s_addr; + } + if (zte_requested && local_ip_changed && session->rtp_socket >= 0) + rtsp_close_udp_sockets(session, "control connection local address changed"); + + memcpy(&session->control_local_addr, &local_addr, local_len); + session->control_local_addr_len = local_len; + memcpy(&session->control_peer_addr, &peer_addr, peer_len); + session->control_peer_addr_len = peer_len; + snprintf(session->control_local_ip, sizeof(session->control_local_ip), "%s", local_host); + session->control_local_port = (uint16_t)atoi(local_service); + session->control_endpoints_valid = 1; + session->zte_active = zte_requested && local_addr.ss_family == AF_INET && peer_addr.ss_family == AF_INET; + + logger(LOG_INFO, "RTSP: Upstream interface %s, local endpoint %s:%u", + session->upstream_ifname && session->upstream_ifname[0] ? session->upstream_ifname : "route-selected", + session->control_local_ip, session->control_local_port); + +#ifdef __FreeBSD__ + if (zte_requested && session->upstream_ifname && session->upstream_ifname[0]) + logger(LOG_WARN, + "RTSP: FreeBSD cannot reliably pin unicast sockets to upstream interface %s; using the actual " + "route-selected local endpoint", + session->upstream_ifname); +#endif + if (zte_requested && !session->zte_active) + logger(LOG_WARN, "RTSP: ZTE NAT traversal only supports IPv4 control connections; using ordinary RTSP negotiation"); + + return 0; +} + /** * Try connecting to the next candidate from the getaddrinfo result list * (sequential dual-stack fallback, IPv6/IPv4 in resolver order). @@ -874,7 +949,7 @@ int rtsp_connect(rtsp_session_t *session) { * This allows STUN to run in parallel with TCP connection establishment * Only do this on initial connect (not on redirect or reconnect for TEARDOWN) * Check: UDP socket not yet created and STUN not already in progress/completed */ - if (!session->metadata_probe && config.rtsp_stun_server && config.rtsp_stun_server[0] != '\0' && + if (!session->metadata_probe && config_get_effective_rtsp_nat_mode() == RTSP_NAT_MODE_STUN && session->rtp_socket < 0 && !session->stun.in_progress && !session->stun.completed) { if (rtsp_setup_udp_sockets(session) == 0) { if (stun_send_request(&session->stun, session->rtp_socket) == 0) { @@ -963,6 +1038,7 @@ int rtsp_handle_socket_event(rtsp_session_t *session, uint32_t events) { /* Connection succeeded - drop remaining candidates */ rtsp_free_connect_results(session); logger(LOG_INFO, "RTSP: Connected to %s:%d", session->server_host, session->server_port); + rtsp_capture_control_endpoints(session); /* Update poller to monitor both read and write */ if (session->epoll_fd >= 0) { @@ -1268,7 +1344,7 @@ int rtsp_send_keepalive(rtsp_session_t *session) { } /* Send NAT probe packets to maintain UDP hole-punching (UDP only) */ - if (session->transport_mode == RTSP_TRANSPORT_UDP) { + if (session->transport_mode == RTSP_TRANSPORT_UDP && !session->zte_active) { rtsp_send_udp_nat_probe(session); } @@ -1599,7 +1675,12 @@ int rtsp_state_machine_advance(rtsp_session_t *session) { case RTSP_STATE_AWAITING_OPTIONS: /* OPTIONS response received, ready to send DESCRIBE */ - snprintf(extra_headers, sizeof(extra_headers), "Accept: application/sdp\r\n"); + if (session->zte_active) { + snprintf(extra_headers, sizeof(extra_headers), "Accept: application/sdp\r\nx-NAT: %s:%u\r\n", + session->control_local_ip, session->control_local_port); + } else { + snprintf(extra_headers, sizeof(extra_headers), "Accept: application/sdp\r\n"); + } if (rtsp_prepare_request(session, RTSP_METHOD_DESCRIBE, NULL, extra_headers) < 0) { logger(LOG_ERROR, "RTSP: Failed to prepare DESCRIBE request"); return -1; @@ -1628,6 +1709,14 @@ int rtsp_state_machine_advance(rtsp_session_t *session) { "RTP/AVP/TCP;unicast;interleaved=%d-%d\r\n", session->rtp_channel, session->rtcp_channel, session->rtp_channel, session->rtcp_channel, session->rtp_channel, session->rtcp_channel); + } else if (session->zte_active) { + advertised_rtp_port = session->local_rtp_port; + advertised_rtcp_port = session->local_rtcp_port; + snprintf(extra_headers, sizeof(extra_headers), + "Transport: MP2T/RTP/UDP;unicast;client_address=%s;client_port=%d-%d;mode=PLAY\r\n" + "x-NAT: %s:%u\r\n", + session->control_local_ip, advertised_rtp_port, advertised_rtcp_port, session->control_local_ip, + session->control_local_port); } else { /* Check STUN status and determine which port to advertise */ if (session->stun.in_progress) { @@ -1650,7 +1739,7 @@ int rtsp_state_machine_advance(rtsp_session_t *session) { } else { advertised_rtp_port = session->local_rtp_port; advertised_rtcp_port = session->local_rtcp_port; - if (config.rtsp_stun_server && config.rtsp_stun_server[0] != '\0') { + if (config_get_effective_rtsp_nat_mode() == RTSP_NAT_MODE_STUN) { logger(LOG_DEBUG, "RTSP: STUN timed out, using local ports %d-%d", advertised_rtp_port, advertised_rtcp_port); } } @@ -2542,7 +2631,12 @@ static int rtsp_setup_udp_sockets(rtsp_session_t *session) { } else { struct sockaddr_in *sin = (struct sockaddr_in *)&local_addr; sin->sin_family = AF_INET; - sin->sin_addr.s_addr = INADDR_ANY; + if (session->zte_active) { + const struct sockaddr_in *control_local = (const struct sockaddr_in *)&session->control_local_addr; + sin->sin_addr = control_local->sin_addr; + } else { + sin->sin_addr.s_addr = INADDR_ANY; + } local_addr_len = sizeof(struct sockaddr_in); } @@ -2661,8 +2755,13 @@ static int rtsp_setup_udp_sockets(rtsp_session_t *session) { logger(LOG_DEBUG, "RTSP: RTCP socket registered with poller"); } - logger(LOG_DEBUG, "RTSP: UDP sockets bound to ports %d (RTP) and %d (RTCP)", session->local_rtp_port, - session->local_rtcp_port); + if (session->zte_active) { + logger(LOG_DEBUG, "RTSP: UDP sockets bound to %s:%d (RTP) and %s:%d (RTCP)", session->control_local_ip, + session->local_rtp_port, session->control_local_ip, session->local_rtcp_port); + } else { + logger(LOG_DEBUG, "RTSP: UDP sockets bound to ports %d (RTP) and %d (RTCP)", session->local_rtp_port, + session->local_rtcp_port); + } return 0; } @@ -3100,6 +3199,49 @@ static void rtsp_send_udp_nat_probe(rtsp_session_t *session) { session->server_rtcp_port); } +static void rtsp_send_zte_nat_probe(rtsp_session_t *session) { + uint8_t packet[84]; + struct sockaddr_storage destination; + const struct sockaddr_in *local_addr; + uint16_t network_port; + ssize_t sent; + + if (!session || !session->zte_active || !session->control_endpoints_valid || session->rtp_socket < 0 || + session->control_peer_addr.ss_family != AF_INET) + return; + if (session->server_rtp_port <= 0 || session->server_rtp_port > 65535) { + logger(LOG_WARN, "RTSP: Cannot send ZTE NAT probe without a valid server RTP port"); + return; + } + + memset(packet, 0, sizeof(packet)); + memcpy(packet, "ZXV10STB", 8); + packet[8] = 0x7f; + packet[9] = 0xff; + packet[10] = 0xff; + packet[11] = 0xff; + + local_addr = (const struct sockaddr_in *)&session->control_local_addr; + memcpy(packet + 12, &local_addr->sin_addr.s_addr, sizeof(local_addr->sin_addr.s_addr)); + network_port = htons((uint16_t)session->local_rtp_port); + memcpy(packet + 16, &network_port, sizeof(network_port)); + network_port = htons(session->control_local_port); + memcpy(packet + 18, &network_port, sizeof(network_port)); + + memcpy(&destination, &session->control_peer_addr, session->control_peer_addr_len); + sockaddr_set_port((struct sockaddr *)&destination, (uint16_t)session->server_rtp_port); + sent = sendto(session->rtp_socket, packet, sizeof(packet), 0, (struct sockaddr *)&destination, + session->control_peer_addr_len); + if (sent != (ssize_t)sizeof(packet)) { + logger(LOG_WARN, "RTSP: Failed to send ZTE NAT probe to control peer port %d: %s", session->server_rtp_port, + sent < 0 ? strerror(errno) : "short send"); + return; + } + + logger(LOG_DEBUG, "RTSP: Sent 84-byte ZTE NAT probe from %s:%d to control peer port %d", session->control_local_ip, + session->local_rtp_port, session->server_rtp_port); +} + static void rtsp_parse_transport_header(rtsp_session_t *session, const char *transport) { char *server_port_param; char *interleaved_param; @@ -3204,8 +3346,12 @@ static void rtsp_parse_transport_header(rtsp_session_t *session, const char *tra } } - /* Send NAT probe packets if server provided source address and ports */ - rtsp_send_udp_nat_probe(session); + if (session->zte_active) { + rtsp_send_zte_nat_probe(session); + } else { + /* Send ordinary NAT probe packets if server provided source address and ports */ + rtsp_send_udp_nat_probe(session); + } } } diff --git a/src/rtsp.h b/src/rtsp.h index 73789911..3af5ccaf 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -2,6 +2,7 @@ #define __RTSP_H__ #include +#include #include #include "stun.h" @@ -152,6 +153,16 @@ typedef struct { * fd numbers cannot be used for this: close()+socket() often reuses them. */ unsigned connect_generation; + /* Actual control connection endpoints selected by the kernel. */ + struct sockaddr_storage control_local_addr; + socklen_t control_local_addr_len; + struct sockaddr_storage control_peer_addr; + socklen_t control_peer_addr_len; + char control_local_ip[RTSP_SERVER_HOST_SIZE]; + uint16_t control_local_port; + int control_endpoints_valid; + int zte_active; + /* Authentication state */ char username[RTSP_CREDENTIAL_SIZE]; /* RTSP username for authentication */ char password[RTSP_CREDENTIAL_SIZE]; /* RTSP password for authentication */ From 17c95a4e6c3e6a42fe0f1deaa3cc4e31b02cfa63 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Mon, 3 Aug 2026 19:03:03 +0800 Subject: [PATCH 2/3] test(rtsp): avoid cross-protocol ordering race --- e2e/helpers/mock_rtsp.py | 4 ---- e2e/test_rtsp_zte_nat.py | 9 +++++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/e2e/helpers/mock_rtsp.py b/e2e/helpers/mock_rtsp.py index 75504211..63d9dce2 100644 --- a/e2e/helpers/mock_rtsp.py +++ b/e2e/helpers/mock_rtsp.py @@ -79,7 +79,6 @@ def __init__( self.requests_received: list[str] = [] self.requests_detailed: list[dict] = [] self.control_peer: tuple | None = None - self.events: list[str] = [] # -- lifecycle ----------------------------------------------------------- @@ -239,9 +238,7 @@ def _handle(self, conn: socket.socket, addr: tuple) -> None: return elif method == "SETUP": conn.sendall(self._setup_response(cseq, transport_hdr).encode()) - self.events.append("setup_response_sent") elif method == "PLAY": - self.events.append("play_received") extra_headers = "".join("%s: %s\r\n" % item for item in self._play_response_headers) conn.sendall( ( @@ -484,7 +481,6 @@ def _receive_probes(self) -> None: payload, source = self._server_rtp_socket.recvfrom(2048) self.udp_datagrams.append((payload, source)) if self._probe_is_valid(payload, source): - self.events.append("zte_probe_received") self._valid_probe.set() except socket.timeout: if self._play_started.is_set(): diff --git a/e2e/test_rtsp_zte_nat.py b/e2e/test_rtsp_zte_nat.py index dd266875..ba49bf28 100644 --- a/e2e/test_rtsp_zte_nat.py +++ b/e2e/test_rtsp_zte_nat.py @@ -88,8 +88,13 @@ def test_zte_headers_endpoint_invariants_and_probe_bytes(self, r2h_binary): assert payload[20:] == bytes(64) assert udp_source[0] == tcp_source_ip assert udp_source[1] == rtsp._client_rtp_port - assert rtsp.events.index("setup_response_sent") < rtsp.events.index("zte_probe_received") - assert rtsp.events.index("zte_probe_received") < rtsp.events.index("play_received") + + # TCP control requests and UDP probes are consumed by separate mock + # threads, so their server-side observation order is inherently + # racy. The probe target port is disclosed only by the SETUP + # response, and MockRTSPServerZTE withholds media until this exact + # packet validates, which covers the protocol dependency without a + # cross-protocol scheduling assertion. log = r2h.read_log() assert "RTSP STUN server ignored because rtsp-nat-mode=zte" in log From fa476195c8fa7394962a0fe8c8c8cea4d198fb60 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Mon, 3 Aug 2026 19:18:06 +0800 Subject: [PATCH 3/3] fix(rtsp): address NAT mode review feedback --- docs/en/reference/configuration.md | 11 +++---- docs/reference/configuration.md | 11 +++---- e2e/test_rtsp_zte_nat.py | 32 ++++++++++++++----- ikuai-support/rtp2httpd/app/option.json | 6 ++-- ikuai-support/rtp2httpd/scripts/start.sh | 1 + .../luci-static/resources/view/rtp2httpd.js | 6 ++-- .../po/templates/rtp2httpd.pot | 4 +-- .../po/zh_Hans/rtp2httpd.po | 8 ++--- .../rtp2httpd/files/rtp2httpd.conf | 2 +- rtp2httpd.conf | 7 ++-- src/configuration.c | 18 ++++++----- src/configuration.h | 5 ++- 12 files changed, 63 insertions(+), 48 deletions(-) diff --git a/docs/en/reference/configuration.md b/docs/en/reference/configuration.md index 324dd199..3c5fd1a9 100644 --- a/docs/en/reference/configuration.md +++ b/docs/en/reference/configuration.md @@ -108,11 +108,10 @@ Unix socket listen paths must be absolute and must not contain whitespace. At st - Format: `host:port` or `host` (default port: 3478) - Example: `stun.miwifi.com` or `stun.miwifi.com:3478` -- `--rtsp-nat-mode ` - RTSP NAT traversal mode - - `none`: Disable STUN and ZTE traversal +- `--rtsp-nat-mode ` - RTSP NAT traversal mode + - `auto`: Use STUN when `rtsp-stun-server` is configured; otherwise disable NAT traversal (default) - `stun`: Use `rtsp-stun-server` to discover the public UDP port; a valid STUN server is required - `zte`: Use the ZTE `ZXV10STB` UDP punch protocol without contacting the configured STUN server - - When omitted, legacy configurations implicitly use `stun` if `rtsp-stun-server` is set, or `none` otherwise - ZTE mode supports IPv4 only. Its address comes from the actual local endpoint of the connected RTSP TCP socket, so it follows the priority of `r2h-ifname`, `upstream-interface-rtsp`, `upstream-interface`, and the system routing table ### Other Options @@ -252,9 +251,9 @@ http-proxy-user-agent = rtp2httpd-http-proxy/1.0 # Configure this when an upstream RTSP server requires a specific User-Agent for compatibility rtsp-user-agent = rtp2httpd/custom -# RTSP NAT traversal mode: none, stun, or zte (default: none) -# If omitted while rtsp-stun-server is configured, stun is selected for backward compatibility -rtsp-nat-mode = none +# RTSP NAT traversal mode: auto, stun, or zte (default: auto) +# Auto uses STUN when rtsp-stun-server is configured; otherwise NAT traversal is disabled +rtsp-nat-mode = auto # STUN server for RTSP NAT traversal (default: disabled) # When an RTSP server only supports UDP transport and the client is behind NAT, STUN can be used to attempt NAT traversal (not guaranteed to succeed) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index db9c8d9a..f4c0101f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -108,11 +108,10 @@ Unix socket 监听路径必须是绝对路径,且路径中不能包含空白 - 格式:`host:port` 或 `host`(默认端口 3478) - 示例:`stun.miwifi.com` 或 `stun.miwifi.com:3478` -- `--rtsp-nat-mode ` - RTSP NAT 穿透模式 - - `none`:禁用 STUN 和 ZTE 穿透 +- `--rtsp-nat-mode ` - RTSP NAT 穿透模式 + - `auto`:配置了 `rtsp-stun-server` 时使用 STUN,否则不启用 NAT 穿透(默认) - `stun`:使用 `rtsp-stun-server` 探测公网 UDP 端口;必须配置有效的 STUN 服务器 - `zte`:使用中兴 `ZXV10STB` UDP 打洞协议,不访问已配置的 STUN 服务器 - - 未显式设置时,为兼容旧配置,存在 `rtsp-stun-server` 即使用 `stun`,否则使用 `none` - ZTE 模式仅支持 IPv4。其地址取自 RTSP TCP 连接成功后的实际本地 endpoint,因此会遵循 `r2h-ifname`、`upstream-interface-rtsp`、`upstream-interface` 和系统路由表的优先级 ### 其他 @@ -250,9 +249,9 @@ http-proxy-user-agent = rtp2httpd-http-proxy/1.0 # 当上游 RTSP 服务器要求特定 User-Agent 时可配置此项 rtsp-user-agent = rtp2httpd/custom -# RTSP NAT 穿透模式:none、stun 或 zte(默认: none) -# 未设置此项但配置了 rtsp-stun-server 时,自动使用 stun 以兼容旧配置 -rtsp-nat-mode = none +# RTSP NAT 穿透模式:auto、stun 或 zte(默认: auto) +# auto 在配置了 rtsp-stun-server 时使用 STUN,否则不启用 NAT 穿透 +rtsp-nat-mode = auto # STUN 服务器用于 RTSP NAT 穿透(默认: 禁用) # 当 RTSP 服务器仅支持 UDP 传输且客户端位于 NAT 后时,可尝试使用 STUN 进行 NAT 穿透(不保证成功) diff --git a/e2e/test_rtsp_zte_nat.py b/e2e/test_rtsp_zte_nat.py index ba49bf28..d0dcde16 100644 --- a/e2e/test_rtsp_zte_nat.py +++ b/e2e/test_rtsp_zte_nat.py @@ -211,8 +211,9 @@ def test_config_file_enables_zte(self, r2h_binary): r2h.stop() rtsp.stop() - def test_explicit_none_disables_legacy_stun_inference(self, r2h_binary): - stun = MockSTUNServer() + def test_explicit_auto_uses_configured_stun(self, r2h_binary): + mapped_port = 50004 + stun = MockSTUNServer(mapped_port=mapped_port) rtsp = MockRTSPServer(num_packets=500) stun.start() rtsp.start() @@ -222,7 +223,7 @@ def test_explicit_none_disables_legacy_stun_inference(self, r2h_binary): r2h_port, extra_args=[ "--rtsp-nat-mode", - "none", + "auto", "--rtsp-stun-server", "127.0.0.1:%d" % stun.port, ], @@ -238,9 +239,9 @@ def test_explicit_none_disables_legacy_stun_inference(self, r2h_binary): ) assert status == 200 assert body - time.sleep(0.1) - assert stun.requests_received == 0 - assert "x-NAT" not in _request(rtsp, "DESCRIBE")["headers"] + assert stun.requests_received >= 1 + transport = _request(rtsp, "SETUP")["headers"]["Transport"] + assert "client_port=%d-%d" % (mapped_port, mapped_port + 1) in transport finally: r2h.stop() rtsp.stop() @@ -261,7 +262,22 @@ def test_explicit_stun_requires_server(self, r2h_binary): finally: r2h.stop() - def test_cli_mode_override_survives_reload(self, r2h_binary): + def test_none_mode_is_rejected(self, r2h_binary): + r2h = R2HProcess( + r2h_binary, + find_free_port(), + extra_args=["--rtsp-nat-mode", "none"], + capture_log=True, + ) + r2h.start(wait=False) + try: + assert r2h.process is not None + assert r2h.process.wait(timeout=5) != 0 + assert "expected auto, stun, or zte" in r2h.read_log() + finally: + r2h.stop() + + def test_cli_auto_override_survives_reload(self, r2h_binary): rtsp = MockRTSPServer(num_packets=300) rtsp.start() r2h_port = find_free_port() @@ -269,7 +285,7 @@ def test_cli_mode_override_survives_reload(self, r2h_binary): r2h_binary, r2h_port, config_content=build_config(r2h_port, global_lines=["rtsp-nat-mode = zte"]), - extra_args=["--rtsp-nat-mode", "none"], + extra_args=["--rtsp-nat-mode", "auto"], capture_log=True, ) r2h.start() diff --git a/ikuai-support/rtp2httpd/app/option.json b/ikuai-support/rtp2httpd/app/option.json index 5e9f1fc1..143dbb2d 100644 --- a/ikuai-support/rtp2httpd/app/option.json +++ b/ikuai-support/rtp2httpd/app/option.json @@ -299,11 +299,11 @@ "max": 1 }, { - "default": "none", + "default": "auto", "attrname": "RTP2HTTPD_RTSP_NAT_MODE", "label": { - "en": "RTSP NAT mode: none, stun, or zte", - "zh": "RTSP NAT 模式:none、stun 或 zte" + "en": "RTSP NAT mode: auto, stun, or zte", + "zh": "RTSP NAT 模式:auto、stun 或 zte" }, "required": true, "scope": "config", diff --git a/ikuai-support/rtp2httpd/scripts/start.sh b/ikuai-support/rtp2httpd/scripts/start.sh index fb3de600..c3276e4f 100755 --- a/ikuai-support/rtp2httpd/scripts/start.sh +++ b/ikuai-support/rtp2httpd/scripts/start.sh @@ -95,6 +95,7 @@ load_env_file "$RUNTIME_ENV" : "${RTP2HTTPD_MCAST_REJOIN_INTERVAL:=0}" : "${RTP2HTTPD_FCC_LISTEN_PORT_RANGE:=}" : "${RTP2HTTPD_ZEROCOPY_ON_SEND:=0}" +: "${RTP2HTTPD_RTSP_NAT_MODE:=auto}" : "${RTP2HTTPD_RTSP_STUN_SERVER:=}" : "${RTP2HTTPD_EXTERNAL_M3U:=}" : "${RTP2HTTPD_EXTERNAL_M3U_UPDATE_INTERVAL:=7200}" diff --git a/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js b/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js index d7b8e486..41858931 100644 --- a/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js +++ b/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js @@ -640,13 +640,13 @@ return view.extend({ "rtsp_nat_mode", _("RTSP NAT Traversal Mode"), _( - "Select none to disable traversal, STUN for standard address discovery, or ZTE for the ZXV10STB UDP punch protocol. ZTE uses the actual IPv4 endpoint selected for the RTSP TCP connection and ignores the STUN server." + "Select Auto to use STUN when a server is configured and otherwise disable traversal, STUN to require standard address discovery, or ZTE for the ZXV10STB UDP punch protocol. ZTE uses the actual IPv4 endpoint selected for the RTSP TCP connection and ignores the STUN server." ) ); - o.value("none", _("None")); + o.value("auto", _("Auto")); o.value("stun", _("STUN")); o.value("zte", _("ZTE")); - o.default = "none"; + o.default = "auto"; o.depends("use_config_file", "0"); o = s.taboption( diff --git a/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot b/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot index 818db230..f003b988 100644 --- a/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot +++ b/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot @@ -415,11 +415,11 @@ msgid "RTSP NAT Traversal Mode" msgstr "" #: htdocs/luci-static/resources/view/rtp2httpd.js:646 -msgid "Select none to disable traversal, STUN for standard address discovery, or ZTE for the ZXV10STB UDP punch protocol. ZTE uses the actual IPv4 endpoint selected for the RTSP TCP connection and ignores the STUN server." +msgid "Select Auto to use STUN when a server is configured and otherwise disable traversal, STUN to require standard address discovery, or ZTE for the ZXV10STB UDP punch protocol. ZTE uses the actual IPv4 endpoint selected for the RTSP TCP connection and ignores the STUN server." msgstr "" #: htdocs/luci-static/resources/view/rtp2httpd.js:650 -msgid "None" +msgid "Auto" msgstr "" #: htdocs/luci-static/resources/view/rtp2httpd.js:651 diff --git a/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po b/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po index 3cd46096..09eef8ff 100644 --- a/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po +++ b/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po @@ -462,12 +462,12 @@ msgid "RTSP NAT Traversal Mode" msgstr "RTSP NAT 穿透模式" #: htdocs/luci-static/resources/view/rtp2httpd.js:646 -msgid "Select none to disable traversal, STUN for standard address discovery, or ZTE for the ZXV10STB UDP punch protocol. ZTE uses the actual IPv4 endpoint selected for the RTSP TCP connection and ignores the STUN server." -msgstr "选择 none 可禁用穿透,选择 STUN 可进行标准地址探测,选择 ZTE 可使用 ZXV10STB UDP 打洞协议。ZTE 使用 RTSP TCP 连接实际选择的 IPv4 endpoint,并忽略 STUN 服务器。" +msgid "Select Auto to use STUN when a server is configured and otherwise disable traversal, STUN to require standard address discovery, or ZTE for the ZXV10STB UDP punch protocol. ZTE uses the actual IPv4 endpoint selected for the RTSP TCP connection and ignores the STUN server." +msgstr "选择 Auto 可在配置了服务器时使用 STUN,否则禁用穿透;选择 STUN 可强制进行标准地址探测;选择 ZTE 可使用 ZXV10STB UDP 打洞协议。ZTE 使用 RTSP TCP 连接实际选择的 IPv4 endpoint,并忽略 STUN 服务器。" #: htdocs/luci-static/resources/view/rtp2httpd.js:650 -msgid "None" -msgstr "无" +msgid "Auto" +msgstr "自动" #: htdocs/luci-static/resources/view/rtp2httpd.js:651 msgid "STUN" diff --git a/openwrt-support/rtp2httpd/files/rtp2httpd.conf b/openwrt-support/rtp2httpd/files/rtp2httpd.conf index 334c1cd0..473268fe 100644 --- a/openwrt-support/rtp2httpd/files/rtp2httpd.conf +++ b/openwrt-support/rtp2httpd/files/rtp2httpd.conf @@ -47,7 +47,7 @@ config rtp2httpd # option zerocopy_on_send '0' # option http_proxy_user_agent 'rtp2httpd-http-proxy/1.0' # option rtsp_user_agent 'rtp2httpd/custom' - # option rtsp_nat_mode 'none' + # option rtsp_nat_mode 'auto' # option rtsp_stun_server 'stun.miwifi.com' # option cors_allow_origin '*' # option ffmpeg_path 'ffmpeg' diff --git a/rtp2httpd.conf b/rtp2httpd.conf index dae12508..33648837 100644 --- a/rtp2httpd.conf +++ b/rtp2httpd.conf @@ -119,10 +119,9 @@ verbosity = 1 # User-Agent header used for upstream RTSP requests (default: rtp2httpd/) ;rtsp-user-agent = rtp2httpd/custom -# RTSP NAT traversal mode: none, stun, or zte (default: none) -# For backward compatibility, omitting this option enables STUN automatically -# when rtsp-stun-server is configured. -;rtsp-nat-mode = none +# RTSP NAT traversal mode: auto, stun, or zte (default: auto) +# Auto uses STUN when rtsp-stun-server is configured; otherwise NAT traversal is disabled. +;rtsp-nat-mode = auto # STUN server for RTSP NAT traversal (default: disabled) # When RTSP server only supports UDP transport and client is behind NAT, diff --git a/src/configuration.c b/src/configuration.c index 13f06f71..b6342282 100644 --- a/src/configuration.c +++ b/src/configuration.c @@ -99,8 +99,8 @@ static int parse_bool(const char *value) { } static rtsp_nat_mode_t parse_rtsp_nat_mode(const char *value) { - if (strcasecmp(value, "none") == 0) - return RTSP_NAT_MODE_NONE; + if (strcasecmp(value, "auto") == 0) + return RTSP_NAT_MODE_AUTO; if (strcasecmp(value, "stun") == 0) return RTSP_NAT_MODE_STUN; if (strcasecmp(value, "zte") == 0) @@ -112,7 +112,6 @@ rtsp_nat_mode_t config_get_effective_rtsp_nat_mode(void) { if (config.rtsp_nat_mode == RTSP_NAT_MODE_AUTO) { if (config.rtsp_stun_server && config.rtsp_stun_server[0] != '\0') return RTSP_NAT_MODE_STUN; - return RTSP_NAT_MODE_NONE; } return config.rtsp_nat_mode; } @@ -122,7 +121,7 @@ static int validate_rtsp_nat_config(void) { rtsp_nat_mode_t mode = config_get_effective_rtsp_nat_mode(); if (mode == RTSP_NAT_MODE_INVALID) { - logger(LOG_ERROR, "Invalid rtsp-nat-mode (expected none, stun, or zte)"); + logger(LOG_ERROR, "Invalid rtsp-nat-mode (expected auto, stun, or zte)"); return -1; } if (mode == RTSP_NAT_MODE_STUN && (!config.rtsp_stun_server || config.rtsp_stun_server[0] == '\0')) { @@ -1309,8 +1308,11 @@ int config_reload(int *out_bind_changed) { return -1; } - if (validate_rtsp_nat_config() < 0) + if (validate_rtsp_nat_config() < 0) { + if (old_bind_addresses) + free_bindaddr(old_bind_addresses); return -1; + } apply_bind_side_effects(); @@ -1404,8 +1406,8 @@ void usage(FILE *f, char *progname) { "(default: rtp2httpd/)\n" "\t-N --rtsp-stun-server STUN server for RTSP NAT traversal " "(default: disabled)\n" - "\t --rtsp-nat-mode RTSP NAT traversal mode " - "(default: infer stun when rtsp-stun-server is set)\n" + "\t --rtsp-nat-mode RTSP NAT traversal mode " + "(default: auto)\n" "\t-O --cors-allow-origin Set Access-Control-Allow-Origin header " "(default: disabled)\n" "\t --access-log Write access logs to this file (default: disabled)\n" @@ -1706,7 +1708,7 @@ void parse_cmd_line(int argc, char *argv[]) { case OPT_RTSP_NAT_MODE: config.rtsp_nat_mode = parse_rtsp_nat_mode(optarg); if (config.rtsp_nat_mode == RTSP_NAT_MODE_INVALID) { - logger(LOG_FATAL, "Invalid --rtsp-nat-mode value: %s (expected none, stun, or zte)", optarg); + logger(LOG_FATAL, "Invalid --rtsp-nat-mode value: %s (expected auto, stun, or zte)", optarg); exit(EXIT_FAILURE); } cmd_rtsp_nat_mode_set = 1; diff --git a/src/configuration.h b/src/configuration.h index 8ee283e7..c568c047 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -29,7 +29,6 @@ typedef enum { BIND_ADDR_TCP = 0, BIND_ADDR_UNIX } bindaddr_type_t; typedef enum { RTSP_NAT_MODE_INVALID = -1, RTSP_NAT_MODE_AUTO = 0, - RTSP_NAT_MODE_NONE, RTSP_NAT_MODE_STUN, RTSP_NAT_MODE_ZTE } rtsp_nat_mode_t; @@ -127,7 +126,7 @@ typedef struct { 1=enabled) */ /* RTSP NAT traversal settings */ - rtsp_nat_mode_t rtsp_nat_mode; /* Explicit NAT traversal mode, or AUTO for legacy inference */ + rtsp_nat_mode_t rtsp_nat_mode; /* RTSP NAT traversal mode (AUTO by default) */ char *rtsp_stun_server; /* STUN server host:port for RTSP NAT traversal (NULL=disabled) */ char *http_proxy_user_agent; /* Override User-Agent header for upstream HTTP @@ -243,7 +242,7 @@ int bind_addresses_equal(bindaddr_t *a, bindaddr_t *b); int bind_addresses_has_unix(void); /** - * Resolve the configured RTSP NAT mode, including legacy STUN inference. + * Resolve AUTO to STUN when an RTSP STUN server is configured. */ rtsp_nat_mode_t config_get_effective_rtsp_nat_mode(void);