diff --git a/docs/en/reference/configuration.md b/docs/en/reference/configuration.md index b606c6e1..3c5fd1a9 100644 --- a/docs/en/reference/configuration.md +++ b/docs/en/reference/configuration.md @@ -108,6 +108,12 @@ 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 + - `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 + - 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 +251,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: 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) # Format: host:port or host (default port: 3478) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 509282ef..f4c0101f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -108,6 +108,12 @@ Unix socket 监听路径必须是绝对路径,且路径中不能包含空白 - 格式:`host:port` 或 `host`(默认端口 3478) - 示例:`stun.miwifi.com` 或 `stun.miwifi.com:3478` +- `--rtsp-nat-mode ` - RTSP NAT 穿透模式 + - `auto`:配置了 `rtsp-stun-server` 时使用 STUN,否则不启用 NAT 穿透(默认) + - `stun`:使用 `rtsp-stun-server` 探测公网 UDP 端口;必须配置有效的 STUN 服务器 + - `zte`:使用中兴 `ZXV10STB` UDP 打洞协议,不访问已配置的 STUN 服务器 + - ZTE 模式仅支持 IPv4。其地址取自 RTSP TCP 连接成功后的实际本地 endpoint,因此会遵循 `r2h-ifname`、`upstream-interface-rtsp`、`upstream-interface` 和系统路由表的优先级 + ### 其他 - `-S, --video-snapshot` - 启用视频快照功能 (默认: 关闭) @@ -243,6 +249,10 @@ http-proxy-user-agent = rtp2httpd-http-proxy/1.0 # 当上游 RTSP 服务器要求特定 User-Agent 时可配置此项 rtsp-user-agent = rtp2httpd/custom +# RTSP NAT 穿透模式:auto、stun 或 zte(默认: auto) +# auto 在配置了 rtsp-stun-server 时使用 STUN,否则不启用 NAT 穿透 +rtsp-nat-mode = auto + # 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..63d9dce2 100644 --- a/e2e/helpers/mock_rtsp.py +++ b/e2e/helpers/mock_rtsp.py @@ -78,6 +78,7 @@ def __init__( self._stop = threading.Event() self.requests_received: list[str] = [] self.requests_detailed: list[dict] = [] + self.control_peer: tuple | None = None # -- lifecycle ----------------------------------------------------------- @@ -126,6 +127,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: @@ -401,6 +403,130 @@ 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._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..d0dcde16 --- /dev/null +++ b/e2e/test_rtsp_zte_nat.py @@ -0,0 +1,312 @@ +"""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 + + # 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 + 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_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() + r2h_port = find_free_port() + r2h = R2HProcess( + r2h_binary, + r2h_port, + extra_args=[ + "--rtsp-nat-mode", + "auto", + "--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 + 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() + 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_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() + r2h = R2HProcess( + r2h_binary, + r2h_port, + config_content=build_config(r2h_port, global_lines=["rtsp-nat-mode = zte"]), + extra_args=["--rtsp-nat-mode", "auto"], + 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..143dbb2d 100644 --- a/ikuai-support/rtp2httpd/app/option.json +++ b/ikuai-support/rtp2httpd/app/option.json @@ -298,6 +298,19 @@ "min": 0, "max": 1 }, + { + "default": "auto", + "attrname": "RTP2HTTPD_RTSP_NAT_MODE", + "label": { + "en": "RTSP NAT mode: auto, stun, or zte", + "zh": "RTSP NAT 模式:auto、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..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}" @@ -161,6 +162,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..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 @@ -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 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("auto", _("Auto")); + o.value("stun", _("STUN")); + o.value("zte", _("ZTE")); + o.default = "auto"; + 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..f003b988 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 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 "Auto" +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..09eef8ff 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 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 "Auto" +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..473268fe 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 'auto' # 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..33648837 100644 --- a/rtp2httpd.conf +++ b/rtp2httpd.conf @@ -119,6 +119,10 @@ verbosity = 1 # User-Agent header used for upstream RTSP requests (default: rtp2httpd/) ;rtsp-user-agent = rtp2httpd/custom +# 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, # try using STUN for NAT traversal (may not always succeed) diff --git a/src/configuration.c b/src/configuration.c index db1f4c0b..b6342282 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,44 @@ 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, "auto") == 0) + return RTSP_NAT_MODE_AUTO; + 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 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 auto, 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 +791,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 +1225,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 +1308,12 @@ int config_reload(int *out_bind_changed) { return -1; } + if (validate_rtsp_nat_config() < 0) { + if (old_bind_addresses) + free_bindaddr(old_bind_addresses); + return -1; + } + apply_bind_side_effects(); /* Check if bind addresses changed */ @@ -1349,6 +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: 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" @@ -1432,6 +1491,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 +1705,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 auto, 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 +1763,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..c568c047 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -26,6 +26,13 @@ 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_STUN, + RTSP_NAT_MODE_ZTE +} rtsp_nat_mode_t; + /* * Linked list of addresses to bind */ @@ -118,13 +125,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; /* 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 + 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 +241,9 @@ int bind_addresses_equal(bindaddr_t *a, bindaddr_t *b); */ int bind_addresses_has_unix(void); +/** + * Resolve AUTO to STUN when an RTSP STUN server is configured. + */ +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 */