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..dd04ae60 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,16 +127,22 @@ def _accept(self) -> None: def _handle(self, conn: socket.socket, addr: tuple) -> None: conn.settimeout(10.0) + self.control_peer = addr transport_hdr = "" try: + pending = b"" while True: - data = b"" - while b"\r\n\r\n" not in data: + while b"\r\n\r\n" not in pending: chunk = conn.recv(4096) if not chunk: return - data += chunk - req = data.decode(errors="replace") + pending += chunk + # Split off exactly one request; anything left is a later + # request that arrived in the same segment. Keeping it buffered + # (rather than folding it into this one) is what makes an + # unexpectedly pipelined request visible to tests. + data, pending = pending.split(b"\r\n\r\n", 1) + req = data.decode(errors="replace") + "\r\n\r\n" first_line = req.split("\r\n")[0].split() method = first_line[0] uri = first_line[1] if len(first_line) > 1 else "" @@ -401,6 +408,148 @@ 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. + + ``expected_ip`` / ``expected_control_port`` override what the punch packet is + validated against; leave them unset to expect the RTSP control connection's + own endpoint. Set them when rtp2httpd advertises a STUN-discovered mapping + instead, in which case the UDP source port no longer matches the advertised + RTP port and ``check_source_port`` must be disabled. + """ + + def __init__( + self, + port: int = 0, + num_packets: int = 200, + expected_ip: str | None = None, + expected_control_port: int | None = None, + check_source_port: bool = True, + ): + super().__init__(port) + self._num_packets = num_packets + self._expected_ip = expected_ip + self._expected_control_port = expected_control_port + self._check_source_port = check_source_port + 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._expected_ip or self.control_peer[0]) + expected_tcp_port = self._expected_control_port or self.control_peer[1] + if self._check_source_port and source[1] != self._client_rtp_port: + return False + 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] + ) + + 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 = next(source for payload, source in self.udp_datagrams if self._probe_is_valid(payload, source)) + 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..b9993d95 --- /dev/null +++ b/e2e/test_rtsp_zte_nat.py @@ -0,0 +1,292 @@ +"""End-to-end coverage for the always-on ZTE RTSP NAT traversal behaviour.""" + +import socket +import struct + +import pytest + +from helpers import ( + LOOPBACK_IF, + MockRTSPServer, + MockRTSPServerZTE, + MockSTUNServer, + R2HProcess, + 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_headers_endpoint_invariants_and_probe_bytes(self, r2h_binary): + rtsp = MockRTSPServerZTE(num_packets=500) + rtsp.start() + r2h_port = find_free_port() + r2h = R2HProcess(r2h_binary, r2h_port, extra_args=["-v", "4", "-m", "100"], 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 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 + + # The full candidate list is always offered so a UDP-incapable server + # can fall back to TCP interleaved; the ZTE-specific + # client_address/mode parameters ride on the UDP alternatives only. + transport = setup["headers"]["Transport"] + assert transport == ",".join( + [ + "MP2T/RTP/TCP;unicast;interleaved=0-1", + "MP2T/TCP;unicast;interleaved=0-1", + "RTP/AVP/TCP;unicast;interleaved=0-1", + ] + + [ + "%s;unicast;client_address=%s;client_port=%d-%d;mode=PLAY" + % (profile, tcp_source_ip, rtsp._client_rtp_port, rtsp._client_rtp_port + 1) + for profile in ("MP2T/RTP/UDP", "MP2T/UDP", "RTP/AVP") + ] + ) + + # Probes are sent three times per attempt (UDP has no delivery + # guarantee); every one of them must be a well-formed punch packet. + assert len(rtsp.udp_datagrams) >= 3 + for payload, udp_source in rtsp.udp_datagrams: + 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. + assert "RTSP: Upstream interface route-selected, local endpoint %s:" % tcp_source_ip in r2h.read_log() + finally: + r2h.stop() + rtsp.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 = [] + 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_tcp_only_server_falls_back_to_interleaved_without_probe(self, r2h_binary): + """The server picks the transport; UDP is never forced.""" + rtsp = MockRTSPServer(num_packets=300) + rtsp.start() + r2h_port = find_free_port() + r2h = R2HProcess(r2h_binary, r2h_port, extra_args=["-v", "4"], 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=188, + timeout=20.0, + ) + assert status == 200 + assert body + + setup = _request(rtsp, "SETUP") + transport = setup["headers"]["Transport"] + # Both families are offered; x-NAT still rides along for ZTE servers + assert "interleaved=0-1" in transport + assert "MP2T/RTP/UDP;unicast;client_address=" in transport + assert "x-NAT" in setup["headers"] + + log = r2h.read_log() + assert "Using TCP interleaved transport" in log + assert "NAT probe" not in log + 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"], 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 "client_address=" not in _request(rtsp, "SETUP")["headers"]["Transport"] + assert "IPv6 control connection, skipping ZTE NAT traversal" 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) + 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 TestSTUNInteraction: + def test_stun_mapping_is_advertised_in_x_nat_and_client_address(self, r2h_binary): + """With STUN configured, the discovered public mapping wins everywhere.""" + mapped_ip = "203.0.113.7" + mapped_port = 50006 + stun = MockSTUNServer(mapped_ip=mapped_ip, mapped_port=mapped_port) + # The punch packet now carries the STUN mapping, and the UDP source port + # is the real local port rather than the advertised one. + rtsp = MockRTSPServerZTE( + num_packets=300, + expected_ip=mapped_ip, + expected_control_port=mapped_port, + check_source_port=False, + ) + stun.start() + rtsp.start() + r2h_port = find_free_port() + r2h = R2HProcess( + r2h_binary, + r2h_port, + extra_args=["-v", "4", "--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=188, + timeout=20.0, + ) + assert status == 200 + assert body + assert stun.requests_received >= 1 + assert rtsp.valid_probe_received + + expected_x_nat = "%s:%d" % (mapped_ip, mapped_port) + # DESCRIBE is held back until STUN settles, so it already carries + # the public mapping rather than the private endpoint. + assert _request(rtsp, "DESCRIBE")["headers"]["x-NAT"] == expected_x_nat + setup = _request(rtsp, "SETUP") + assert setup["headers"]["x-NAT"] == expected_x_nat + transport = setup["headers"]["Transport"] + assert "client_address=%s;client_port=%d-%d" % (mapped_ip, mapped_port, mapped_port + 1) in transport + finally: + r2h.stop() + rtsp.stop() + stun.stop() + + def test_silent_stun_falls_back_to_local_endpoint(self, r2h_binary): + """A dead STUN server must not stall the handshake past its own budget.""" + stun = MockSTUNServer(silent=True) + rtsp = MockRTSPServerZTE(num_packets=300) + stun.start() + rtsp.start() + r2h_port = find_free_port() + r2h = R2HProcess( + r2h_binary, + r2h_port, + extra_args=["-v", "4", "--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=188, + timeout=30.0, + ) + assert status == 200 + assert body + assert rtsp.valid_probe_received + assert rtsp.control_peer is not None + assert _request(rtsp, "DESCRIBE")["headers"]["x-NAT"] == "%s:%d" % rtsp.control_peer[:2] + + log = r2h.read_log() + # DESCRIBE really was parked, and STUN's ~3s retry budget did not + # trip the 3s handshake timeout. + assert "Waiting for STUN response before sending DESCRIBE" in log + assert "STUN: Timeout after 3 attempts" in log + # Parking must not pipeline DESCRIBE behind an unanswered OPTIONS. + assert rtsp.requests_received[:4] == ["OPTIONS", "DESCRIBE", "SETUP", "PLAY"] + finally: + r2h.stop() + rtsp.stop() + stun.stop() diff --git a/src/configuration.c b/src/configuration.c index db1f4c0b..86f8f6c8 100644 --- a/src/configuration.c +++ b/src/configuration.c @@ -1247,14 +1247,7 @@ int config_reload(int *out_bind_changed) { /* Step 3: Parse config file */ if (parse_config_file(config_file_path) != 0) { logger(LOG_ERROR, "Failed to parse config file during reload: %s", config_file_path); - /* Restore old bind addresses */ - if (!cmd_bind_set) { - bind_addresses = old_bind_addresses; - old_bind_addresses = NULL; /* Don't free it */ - } - if (old_bind_addresses) - free_bindaddr(old_bind_addresses); - return -1; + goto reload_failed; } apply_bind_side_effects(); @@ -1271,6 +1264,17 @@ int config_reload(int *out_bind_changed) { logger(LOG_INFO, "Configuration reloaded successfully from %s", config_file_path); return 0; + +reload_failed: + /* Restore the bind addresses captured before the failed reload */ + if (!cmd_bind_set) { + free_bindaddr(bind_addresses); + bind_addresses = old_bind_addresses; + old_bind_addresses = NULL; /* Now owned by the global */ + } + if (old_bind_addresses) + free_bindaddr(old_bind_addresses); + return -1; } void usage(FILE *f, char *progname) { diff --git a/src/configuration.h b/src/configuration.h index 11f5e631..1e0b4699 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -118,7 +118,7 @@ typedef struct { int zerocopy_on_send; /* Enable zero-copy send with MSG_ZEROCOPY (0=disabled, 1=enabled) */ - /* STUN NAT traversal settings */ + /* RTSP 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 diff --git a/src/rtsp.c b/src/rtsp.c index ace92e3a..58a23483 100644 --- a/src/rtsp.c +++ b/src/rtsp.c @@ -12,11 +12,13 @@ #include "stream.h" #include "utils.h" #include "worker.h" +#include #include #include #include #include #include +#include #include #include #include @@ -53,6 +55,7 @@ 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 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 +303,13 @@ void rtsp_session_init(rtsp_session_t *session) { session->metadata_probe = 0; session->peer_closed = 0; session->connect_generation = 0; + session->control_local_ip4.s_addr = INADDR_ANY; + session->control_peer_ip4.s_addr = INADDR_ANY; + session->control_local_ip[0] = '\0'; + session->control_local_port = 0; + session->control_endpoints_valid = 0; + session->zte_nat_active = 0; + session->describe_waiting_for_stun = 0; /* Initialize transport parameters - mode will be negotiated during SETUP */ session->transport_mode = RTSP_TRANSPORT_TCP; /* Default preference */ @@ -739,6 +749,77 @@ 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[INET6_ADDRSTRLEN]; + struct in_addr local_ip4; + struct in_addr peer_ip4; + int local_ip_changed; + + memset(&local_addr, 0, sizeof(local_addr)); + memset(&peer_addr, 0, sizeof(peer_addr)); + local_ip4.s_addr = INADDR_ANY; + peer_ip4.s_addr = INADDR_ANY; + + if (getsockname(session->socket, (struct sockaddr *)&local_addr, &local_len) < 0) { + logger(LOG_ERROR, "RTSP: getsockname() failed: %s", strerror(errno)); + goto fail; + } + if (getpeername(session->socket, (struct sockaddr *)&peer_addr, &peer_len) < 0) { + logger(LOG_ERROR, "RTSP: getpeername() failed: %s", strerror(errno)); + goto fail; + } + if (sockaddr_format_ip((struct sockaddr *)&local_addr, local_host, sizeof(local_host)) < 0) { + logger(LOG_ERROR, "RTSP: Failed to format local control endpoint"); + goto fail; + } + + if (local_addr.ss_family == AF_INET) + local_ip4 = ((const struct sockaddr_in *)&local_addr)->sin_addr; + if (peer_addr.ss_family == AF_INET) + peer_ip4 = ((const struct sockaddr_in *)&peer_addr)->sin_addr; + + /* A reconnect or redirect can land on a different local address; UDP sockets + * pinned to the previous one must be recreated before SETUP. */ + local_ip_changed = session->control_endpoints_valid && session->control_local_ip4.s_addr != local_ip4.s_addr; + if (local_ip_changed && session->rtp_socket >= 0) + rtsp_close_udp_sockets(session, "control connection local address changed"); + + session->control_local_ip4 = local_ip4; + session->control_peer_ip4 = peer_ip4; + snprintf(session->control_local_ip, sizeof(session->control_local_ip), "%s", local_host); + session->control_local_port = sockaddr_get_port((struct sockaddr *)&local_addr); + session->control_endpoints_valid = 1; + /* The ZTE NAT traversal behaviours (x-NAT header, client_address parameter, + * ZXV10STB punch packet) all encode an IPv4 address, so they are limited to + * IPv4 control connections. IPv6 upstreams fall back to plain negotiation. */ + session->zte_nat_active = 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 (session->upstream_ifname && session->upstream_ifname[0]) + logger(LOG_DEBUG, + "RTSP: FreeBSD cannot reliably pin unicast sockets to upstream interface %s; using the actual " + "route-selected local endpoint", + session->upstream_ifname); +#endif + if (!session->zte_nat_active) + logger(LOG_DEBUG, "RTSP: IPv6 control connection, skipping ZTE NAT traversal headers and punch packet"); + + return 0; + +fail: + session->control_endpoints_valid = 0; + session->zte_nat_active = 0; + return -1; +} + /** * Try connecting to the next candidate from the getaddrinfo result list * (sequential dual-stack fallback, IPv6/IPv4 in resolver order). @@ -963,6 +1044,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) { @@ -1582,8 +1664,127 @@ static int rtsp_try_receive_response(rtsp_session_t *session) { return RTSP_RESPONSE_OK; } +/* + * Resolve the endpoint advertised to the upstream server for NAT traversal. + * + * When STUN discovery succeeded, its public mapping wins: that is the address + * an upstream behind NAT can actually reach. Otherwise the RTSP control + * connection's own local endpoint is used, which is the correct answer when + * rtp2httpd itself holds the operator-facing address (the usual router + * deployment). + * + * Note that STUN discovers the mapping of the *UDP* socket; RTSP has no way to + * discover the TCP control port's mapping, so `control_port` (what the x-NAT + * header names) falls back to the STUN UDP port when STUN is in play. + */ +static void rtsp_nat_endpoint(const rtsp_session_t *session, rtsp_nat_endpoint_t *out) { + uint16_t stun_port = stun_get_mapped_port(&session->stun); + struct in_addr stun_ip = stun_get_mapped_ipv4(&session->stun); + + memset(out, 0, sizeof(*out)); + + if (stun_port > 0) { + out->rtp_port = stun_port; + out->rtcp_port = stun_port + 1; + out->control_port = stun_port; + } else { + out->rtp_port = (uint16_t)session->local_rtp_port; + out->rtcp_port = (uint16_t)session->local_rtcp_port; + out->control_port = session->control_local_port; + } + + if (stun_ip.s_addr != INADDR_ANY) { + out->ip4 = stun_ip; + inet_ntop(AF_INET, &out->ip4, out->ip, sizeof(out->ip)); + } else { + out->ip4 = session->control_local_ip4; + snprintf(out->ip, sizeof(out->ip), "%s", session->control_local_ip); + } +} + +/* + * Append a formatted chunk to a header buffer, keeping *len in sync. Writes + * are clamped to the buffer so callers can chain appends without repeating the + * remaining-space arithmetic. + */ +static void rtsp_append_header(char *buf, size_t size, size_t *len, const char *fmt, ...) + __attribute__((format(printf, 4, 5))); + +static void rtsp_append_header(char *buf, size_t size, size_t *len, const char *fmt, ...) { + va_list args; + int written; + + if (*len + 1 >= size) + return; + + va_start(args, fmt); + written = vsnprintf(buf + *len, size - *len, fmt, args); + va_end(args); + + if (written < 0) + return; + *len += (size_t)written; + if (*len >= size) + *len = size - 1; +} + +/* + * Append the ZTE "x-NAT: :" header naming the endpoint the server + * should treat as ours. No-op on IPv6 control connections. + */ +static void rtsp_append_x_nat_header(const rtsp_session_t *session, char *buf, size_t size, size_t *len) { + rtsp_nat_endpoint_t nat; + + if (!session->zte_nat_active) + return; + rtsp_nat_endpoint(session, &nat); + rtsp_append_header(buf, size, len, "x-NAT: %s:%u\r\n", nat.ip, nat.control_port); +} + +/* + * Build the SETUP "Transport:" header. Every supported alternative is listed + * in preference order (TCP interleaved first, then UDP) so a server that + * cannot serve one can still select another; the punch packet is only sent + * once the server has actually confirmed UDP. The UDP alternatives carry + * client_address/mode=PLAY, matching what ZTE ZXV10 set-top boxes send. + */ +static void rtsp_build_setup_transport(const rtsp_session_t *session, char *buf, size_t size, size_t *len, + int offer_tcp, int offer_udp, int rtp_port, int rtcp_port) { + static const char *const tcp_profiles[] = {"MP2T/RTP/TCP", "MP2T/TCP", "RTP/AVP/TCP"}; + static const char *const udp_profiles[] = {"MP2T/RTP/UDP", "MP2T/UDP", "RTP/AVP"}; + char udp_address[sizeof("client_address=;") + INET6_ADDRSTRLEN] = ""; + const char *udp_mode = ""; + const char *separator = ""; + size_t i; + + if (session->zte_nat_active) { + rtsp_nat_endpoint_t nat; + rtsp_nat_endpoint(session, &nat); + snprintf(udp_address, sizeof(udp_address), "client_address=%s;", nat.ip); + udp_mode = ";mode=PLAY"; + } + + rtsp_append_header(buf, size, len, "Transport: "); + if (offer_tcp) { + for (i = 0; i < ARRAY_SIZE(tcp_profiles); i++) { + rtsp_append_header(buf, size, len, "%s%s;unicast;interleaved=%d-%d", separator, tcp_profiles[i], + session->rtp_channel, session->rtcp_channel); + separator = ","; + } + } + if (offer_udp) { + for (i = 0; i < ARRAY_SIZE(udp_profiles); i++) { + rtsp_append_header(buf, size, len, "%s%s;unicast;%sclient_port=%d-%d%s", separator, udp_profiles[i], udp_address, + rtp_port, rtcp_port, udp_mode); + separator = ","; + } + } + rtsp_append_header(buf, size, len, "\r\n"); +} + int rtsp_state_machine_advance(rtsp_session_t *session) { char extra_headers[RTSP_HEADERS_BUFFER_SIZE]; + size_t headers_len = 0; switch (session->state) { case RTSP_STATE_CONNECTED: @@ -1598,8 +1799,25 @@ int rtsp_state_machine_advance(rtsp_session_t *session) { return 0; case RTSP_STATE_AWAITING_OPTIONS: - /* OPTIONS response received, ready to send DESCRIBE */ - snprintf(extra_headers, sizeof(extra_headers), "Accept: application/sdp\r\n"); + /* OPTIONS response received, ready to send DESCRIBE. + * DESCRIBE already carries the x-NAT header, so a STUN discovery started + * back at connect() has to finish first - otherwise x-NAT would name the + * private endpoint while SETUP later advertises the public mapping. + * STUN is bounded by its own retry budget, and rtsp_session_tick() pauses + * the handshake timeout while it runs. */ + if (session->stun.in_progress) { + stun_check_timeout(&session->stun, session->rtp_socket); + if (session->stun.in_progress) { + session->describe_waiting_for_stun = 1; + logger(LOG_DEBUG, "RTSP: Waiting for STUN response before sending DESCRIBE"); + return 0; /* Stay in AWAITING_OPTIONS, will be called again */ + } + } + session->describe_waiting_for_stun = 0; + + extra_headers[0] = '\0'; + rtsp_append_header(extra_headers, sizeof(extra_headers), &headers_len, "Accept: application/sdp\r\n"); + rtsp_append_x_nat_header(session, extra_headers, sizeof(extra_headers), &headers_len); if (rtsp_prepare_request(session, RTSP_METHOD_DESCRIBE, NULL, extra_headers) < 0) { logger(LOG_ERROR, "RTSP: Failed to prepare DESCRIBE request"); return -1; @@ -1611,7 +1829,7 @@ int rtsp_state_machine_advance(rtsp_session_t *session) { case RTSP_STATE_DESCRIBED: { /* Ready to send SETUP - first setup UDP sockets if needed */ int udp_setup_ok = 0; - int advertised_rtp_port, advertised_rtcp_port; + int advertised_rtp_port = 0, advertised_rtcp_port = 0; /* Check if UDP sockets were already created for STUN */ if (session->rtp_socket >= 0) { @@ -1620,61 +1838,25 @@ int rtsp_state_machine_advance(rtsp_session_t *session) { udp_setup_ok = 1; } - if (!udp_setup_ok) { - logger(LOG_DEBUG, "RTSP: Failed to setup UDP sockets, will only offer TCP transport"); - snprintf(extra_headers, sizeof(extra_headers), - "Transport: MP2T/RTP/TCP;unicast;interleaved=%d-%d," - "MP2T/TCP;unicast;interleaved=%d-%d," - "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 { - /* Check STUN status and determine which port to advertise */ - if (session->stun.in_progress) { - /* STUN still in progress - check for timeout/retry */ - stun_check_timeout(&session->stun, session->rtp_socket); - - /* If STUN is still in progress after timeout check, wait for it */ - if (session->stun.in_progress) { - logger(LOG_DEBUG, "RTSP: Waiting for STUN response before sending SETUP"); - return 0; /* Stay in DESCRIBED state, will be called again */ - } - } - - /* Use STUN mapped port if available, otherwise use local port */ - advertised_rtp_port = stun_get_mapped_port(&session->stun); - if (advertised_rtp_port > 0) { - advertised_rtcp_port = advertised_rtp_port + 1; + /* STUN, if any, already settled before DESCRIBE was sent */ + if (udp_setup_ok) { + rtsp_nat_endpoint_t nat; + rtsp_nat_endpoint(session, &nat); + advertised_rtp_port = nat.rtp_port; + advertised_rtcp_port = nat.rtcp_port; + if (stun_get_mapped_port(&session->stun) > 0) logger(LOG_DEBUG, "RTSP: Using STUN mapped ports %d-%d for SETUP Transport", advertised_rtp_port, advertised_rtcp_port); - } 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') { - logger(LOG_DEBUG, "RTSP: STUN timed out, using local ports %d-%d", advertised_rtp_port, advertised_rtcp_port); - } - } - - if (RTSP_DISABLE_TCP_TRANSPORT) { - snprintf(extra_headers, sizeof(extra_headers), - "Transport: MP2T/RTP/UDP;unicast;client_port=%d-%d," - "MP2T/UDP;unicast;client_port=%d-%d," - "RTP/AVP;unicast;client_port=%d-%d\r\n", - advertised_rtp_port, advertised_rtcp_port, advertised_rtp_port, advertised_rtcp_port, - advertised_rtp_port, advertised_rtcp_port); - } else { - snprintf(extra_headers, sizeof(extra_headers), - "Transport: MP2T/RTP/TCP;unicast;interleaved=%d-%d," - "MP2T/TCP;unicast;interleaved=%d-%d," - "RTP/AVP/TCP;unicast;interleaved=%d-%d," - "MP2T/RTP/UDP;unicast;client_port=%d-%d," - "MP2T/UDP;unicast;client_port=%d-%d," - "RTP/AVP;unicast;client_port=%d-%d\r\n", - session->rtp_channel, session->rtcp_channel, session->rtp_channel, session->rtcp_channel, - session->rtp_channel, session->rtcp_channel, advertised_rtp_port, advertised_rtcp_port, - advertised_rtp_port, advertised_rtcp_port, advertised_rtp_port, advertised_rtcp_port); - } + } else { + logger(LOG_DEBUG, "RTSP: Failed to setup UDP sockets, will only offer TCP transport"); } + + extra_headers[0] = '\0'; + rtsp_build_setup_transport(session, extra_headers, sizeof(extra_headers), &headers_len, + !udp_setup_ok || !RTSP_DISABLE_TCP_TRANSPORT, udp_setup_ok, advertised_rtp_port, + advertised_rtcp_port); + rtsp_append_x_nat_header(session, extra_headers, sizeof(extra_headers), &headers_len); + if (rtsp_prepare_request(session, RTSP_METHOD_SETUP, session->setup_url[0] ? session->setup_url : NULL, extra_headers) < 0) { logger(LOG_ERROR, "RTSP: Failed to prepare SETUP request"); @@ -1746,6 +1928,13 @@ int rtsp_session_tick(rtsp_session_t *session, int64_t now) { switch (session->state) { case RTSP_STATE_CONNECTING: case RTSP_STATE_AWAITING_OPTIONS: + /* DESCRIBE is deliberately held back until STUN settles; STUN has its own + * bounded retry budget, so don't also count it against the handshake. */ + if (session->describe_waiting_for_stun) + timeout_sec = 0; + else + timeout_sec = RTSP_HANDSHAKE_TIMEOUT_SEC; + break; case RTSP_STATE_AWAITING_DESCRIBE: case RTSP_STATE_AWAITING_SETUP: case RTSP_STATE_AWAITING_PLAY: @@ -1780,12 +1969,12 @@ int rtsp_session_tick(rtsp_session_t *session, int64_t now) { } /* Check STUN timeout if waiting for STUN response */ - if (session->stun.in_progress && session->state == RTSP_STATE_DESCRIBED) { + if (session->stun.in_progress && session->describe_waiting_for_stun) { if (stun_check_timeout(&session->stun, session->rtp_socket) > 0) { - /* STUN finally timed out, advance state machine to continue with local - * port */ + /* STUN finally timed out, advance state machine to continue with the + * local endpoint */ if (rtsp_state_machine_advance(session) == 0) { - /* Re-arm POLLER_OUT so the pending SETUP request gets sent */ + /* Re-arm POLLER_OUT so the pending DESCRIBE request gets sent */ if (session->epoll_fd >= 0) { poller_mod(session->epoll_fd, session->socket, POLLER_IN | POLLER_OUT | POLLER_HUP | POLLER_ERR | POLLER_RDHUP); @@ -2013,9 +2202,9 @@ int rtsp_handle_udp_rtp_data(rtsp_session_t *session, connection_t *conn) { if (stun_parse_response(&session->stun, stun_buf, stun_len) == 0) { logger(LOG_INFO, "RTSP: STUN discovery completed, mapped RTP port: %d", stun_get_mapped_port(&session->stun)); /* If state machine was waiting for STUN, advance it now */ - if (session->state == RTSP_STATE_DESCRIBED) { + if (session->describe_waiting_for_stun) { if (rtsp_state_machine_advance(session) == 0) { - /* Re-arm POLLER_OUT so the pending SETUP request gets sent */ + /* Re-arm POLLER_OUT so the pending DESCRIBE request gets sent */ if (session->epoll_fd >= 0) { poller_mod(session->epoll_fd, session->socket, POLLER_IN | POLLER_OUT | POLLER_HUP | POLLER_ERR | POLLER_RDHUP); @@ -2542,7 +2731,10 @@ 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; + /* ZTE mode pins the media sockets to the same local address the RTSP + * control connection uses, since that address is what the x-NAT header and + * the punch packet advertise to the server. */ + sin->sin_addr.s_addr = session->zte_nat_active ? session->control_local_ip4.s_addr : INADDR_ANY; local_addr_len = sizeof(struct sockaddr_in); } @@ -2661,7 +2853,8 @@ 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, + logger(LOG_DEBUG, "RTSP: UDP sockets bound to %s ports %d (RTP) and %d (RTCP)", + session->zte_nat_active ? session->control_local_ip : "any address", session->local_rtp_port, session->local_rtcp_port); return 0; @@ -3035,68 +3228,124 @@ static void rtsp_parse_describe_sdp(rtsp_session_t *session, const char *header_ } } +/* + * Punch the media path so the operator network starts forwarding. + * + * On the RTP socket this sends the ZTE ZXV10 set-top box authentication + * datagram, which those deployments require before media flows. Wire format + * (multi-byte fields are big-endian): + * + * 0..7 "ZXV10STB" magic + * 8..11 0x7fffffff + * 12..15 client IPv4 address + * 16..17 client RTP port + * 18..19 client RTSP control port + * 20..83 zero padding + * + * The protocol details are derived from https://github.com/plsy1/rtsproxy. + * Addresses and ports come from rtsp_nat_endpoint(), so behind NAT they carry + * the STUN-discovered public mapping rather than the private endpoint. + * + * On IPv6 control connections the packet cannot be built (its address field is + * IPv4) and a minimal RTP datagram is sent instead. RTCP is punched with a + * minimal Receiver Report in both cases. Everything is sent three times and + * repeated on every keepalive, since UDP gives no delivery guarantee and NAT + * bindings expire. + */ static void rtsp_send_udp_nat_probe(rtsp_session_t *session) { - char port_str[RTSP_PORT_STRING_SIZE]; - struct addrinfo hints; - struct addrinfo *result = NULL; - struct addrinfo *rp; - uint8_t rtp_packet[12]; + struct sockaddr_storage destination; + socklen_t destination_len = 0; + rtsp_nat_endpoint_t nat; + uint8_t rtp_packet[84]; + size_t rtp_packet_len; uint8_t rtcp_packet[8]; + uint16_t network_port; + char destination_ip[INET6_ADDRSTRLEN] = ""; - if (!session || session->server_source_addr[0] == '\0') { + if (!session || session->server_rtp_port <= 0 || session->server_rtp_port > 65535) return; + + memset(&destination, 0, sizeof(destination)); + + /* Prefer the media source the server named in Transport; fall back to the + * RTSP control peer, which is where ZTE servers expect the punch anyway. */ + if (session->server_source_addr[0] != '\0') { + struct addrinfo hints; + struct addrinfo *result = NULL; + struct addrinfo *rp; + char port_str[RTSP_PORT_STRING_SIZE]; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + snprintf(port_str, sizeof(port_str), "%d", session->server_rtp_port); + + if (getaddrinfo(session->server_source_addr, port_str, &hints, &result) == 0) { + for (rp = result; rp != NULL; rp = rp->ai_next) { + if (rp->ai_family == session->upstream_family) + break; + } + if (!rp) + rp = result; /* Fallback: try the first resolved address */ + memcpy(&destination, rp->ai_addr, rp->ai_addrlen); + destination_len = rp->ai_addrlen; + freeaddrinfo(result); + } + } + if (destination_len == 0 && session->control_endpoints_valid && session->zte_nat_active) { + struct sockaddr_in *sin = (struct sockaddr_in *)&destination; + sin->sin_family = AF_INET; + sin->sin_addr = session->control_peer_ip4; + destination_len = sizeof(struct sockaddr_in); } + if (destination_len == 0) + return; + + sockaddr_format_ip((struct sockaddr *)&destination, destination_ip, sizeof(destination_ip)); + rtsp_nat_endpoint(session, &nat); - /* Build minimal RTP packet - 12 bytes */ memset(rtp_packet, 0, sizeof(rtp_packet)); - rtp_packet[0] = 0x80; /* V=2, P=0, X=0, CC=0 */ - rtp_packet[1] = 0x00; /* M=0, PT=0 */ + if (session->zte_nat_active) { + rtp_packet_len = 84; + memcpy(rtp_packet, "ZXV10STB", 8); + rtp_packet[8] = 0x7f; + rtp_packet[9] = 0xff; + rtp_packet[10] = 0xff; + rtp_packet[11] = 0xff; + memcpy(rtp_packet + 12, &nat.ip4.s_addr, sizeof(nat.ip4.s_addr)); + network_port = htons(nat.rtp_port); + memcpy(rtp_packet + 16, &network_port, sizeof(network_port)); + network_port = htons(nat.control_port); + memcpy(rtp_packet + 18, &network_port, sizeof(network_port)); + } else { + /* Minimal RTP packet - 12 bytes */ + rtp_packet_len = 12; + rtp_packet[0] = 0x80; /* V=2, P=0, X=0, CC=0 */ + rtp_packet[1] = 0x00; /* M=0, PT=0 */ + } - /* Build minimal RTCP RR (Receiver Report) - 8 bytes */ + /* Minimal RTCP RR (Receiver Report) - 8 bytes */ memset(rtcp_packet, 0, sizeof(rtcp_packet)); rtcp_packet[0] = 0x80; /* V=2, P=0, RC=0 */ rtcp_packet[1] = 201; /* PT=201 (Receiver Report) */ rtcp_packet[2] = 0x00; /* length in words - 1 (high byte) */ rtcp_packet[3] = 0x01; /* length in words - 1 (low byte) = 1 */ - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_DGRAM; - hints.ai_protocol = IPPROTO_UDP; - - /* Resolve server address once for both RTP and RTCP */ - snprintf(port_str, sizeof(port_str), "%d", session->server_rtp_port); - if (getaddrinfo(session->server_source_addr, port_str, &hints, &result) != 0) { - return; - } - - /* Pick the first address matching the UDP socket address family */ - for (rp = result; rp != NULL; rp = rp->ai_next) { - if (rp->ai_family == session->upstream_family) { - break; - } - } - if (!rp) { - rp = result; /* Fallback: try the first resolved address */ - } - - /* Send 3 NAT probe packets for both RTP and RTCP */ for (int attempt = 0; attempt < 3; attempt++) { - /* Send RTP probe */ - if (session->server_rtp_port > 0 && session->rtp_socket >= 0) { - sockaddr_set_port(rp->ai_addr, (uint16_t)session->server_rtp_port); - sendto(session->rtp_socket, rtp_packet, sizeof(rtp_packet), 0, rp->ai_addr, rp->ai_addrlen); + if (session->rtp_socket >= 0) { + sockaddr_set_port((struct sockaddr *)&destination, (uint16_t)session->server_rtp_port); + sendto(session->rtp_socket, rtp_packet, rtp_packet_len, 0, (struct sockaddr *)&destination, destination_len); } - - /* Send RTCP probe - update port in sockaddr */ if (session->server_rtcp_port > 0 && session->rtcp_socket >= 0) { - sockaddr_set_port(rp->ai_addr, (uint16_t)session->server_rtcp_port); - sendto(session->rtcp_socket, rtcp_packet, sizeof(rtcp_packet), 0, rp->ai_addr, rp->ai_addrlen); + sockaddr_set_port((struct sockaddr *)&destination, (uint16_t)session->server_rtcp_port); + sendto(session->rtcp_socket, rtcp_packet, sizeof(rtcp_packet), 0, (struct sockaddr *)&destination, + destination_len); } } - freeaddrinfo(result); - logger(LOG_DEBUG, "RTSP: Sent NAT probe packets to %s:%d/%d", session->server_source_addr, session->server_rtp_port, + logger(LOG_DEBUG, "RTSP: Sent 3x %zu-byte %s NAT probe to %s:%d/%d", rtp_packet_len, + session->zte_nat_active ? "ZXV10STB" : "RTP", destination_ip, session->server_rtp_port, session->server_rtcp_port); } @@ -3204,7 +3453,7 @@ static void rtsp_parse_transport_header(rtsp_session_t *session, const char *tra } } - /* Send NAT probe packets if server provided source address and ports */ + /* Punch the media path now that the server has confirmed UDP */ rtsp_send_udp_nat_probe(session); } } diff --git a/src/rtsp.h b/src/rtsp.h index 73789911..cdb1f33b 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -1,6 +1,7 @@ #ifndef __RTSP_H__ #define __RTSP_H__ +#include #include #include @@ -9,6 +10,18 @@ /* Forward declaration */ struct addrinfo; +/* + * Endpoint advertised upstream for NAT traversal: either the STUN-discovered + * public mapping or the RTSP control connection's own local endpoint. + */ +typedef struct { + struct in_addr ip4; + char ip[INET6_ADDRSTRLEN]; + uint16_t control_port; /* port named by the x-NAT header */ + uint16_t rtp_port; /* port advertised as client_port / in the punch packet */ + uint16_t rtcp_port; +} rtsp_nat_endpoint_t; + #define RTSP_DISABLE_TCP_TRANSPORT 0 /* To debug UDP transport, set to 1 */ /* Timeout constants for RTSP state machine */ @@ -152,6 +165,28 @@ typedef struct { * fd numbers cannot be used for this: close()+socket() often reuses them. */ unsigned connect_generation; + /* Control connection endpoints as actually selected by the kernel. The ZTE + * x-NAT header and punch packet are IPv4-only, so only the IPv4 addresses are + * retained next to the printable form of the local address (which is also + * filled for IPv6 control connections, for logging). */ + struct in_addr control_local_ip4; + struct in_addr control_peer_ip4; + char control_local_ip[INET6_ADDRSTRLEN]; + uint16_t control_local_port; + int control_endpoints_valid; + /* ZTE NAT traversal is in effect for this session: send the x-NAT header and + * the client_address parameter, pin the media sockets to the control + * connection's local address, and punch with the ZXV10STB packet. Requires + * IPv4 on both ends of the control connection. */ + int zte_nat_active; + + /* Set while the OPTIONS response has been processed but DESCRIBE is held + * back waiting for STUN. The state stays AWAITING_OPTIONS throughout, so + * this flag is what distinguishes "parked, safe to resume" from "OPTIONS + * still in flight" - resuming in the latter would pipeline DESCRIBE behind + * an unanswered OPTIONS. */ + int describe_waiting_for_stun; + /* Authentication state */ char username[RTSP_CREDENTIAL_SIZE]; /* RTSP username for authentication */ char password[RTSP_CREDENTIAL_SIZE]; /* RTSP password for authentication */ diff --git a/src/stun.c b/src/stun.c index e26581c8..e338cb09 100644 --- a/src/stun.c +++ b/src/stun.c @@ -211,17 +211,18 @@ int stun_parse_response(stun_state_t *state, const uint8_t *data, size_t len) { ((uint32_t)data[val_off + 6] << 8) | data[val_off + 7]; uint32_t addr = xaddr ^ STUN_MAGIC_COOKIE; - state->mapped_rtp_port = port; - state->mapped_rtcp_port = port + 1; - state->in_progress = 0; - state->completed = 1; - /* Log the mapped address */ struct in_addr ina; ina.s_addr = htonl(addr); char ip_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &ina, ip_str, sizeof(ip_str)); + state->mapped_rtp_port = port; + state->mapped_rtcp_port = port + 1; + state->mapped_ip4 = ina; + state->in_progress = 0; + state->completed = 1; + logger(LOG_INFO, "STUN: Discovered mapped address %s:%d", ip_str, port); return 0; } @@ -260,6 +261,10 @@ int stun_parse_response(stun_state_t *state, const uint8_t *data, size_t len) { state->mapped_rtp_port = port; state->mapped_rtcp_port = port + 1; + if (family == STUN_ADDR_FAMILY_IPV4) { + /* MAPPED-ADDRESS carries the address unobfuscated */ + memcpy(&state->mapped_ip4.s_addr, data + val_off + 4, 4); + } state->in_progress = 0; state->completed = 1; @@ -318,6 +323,15 @@ uint16_t stun_get_mapped_port(const stun_state_t *state) { return state->mapped_rtp_port; } +struct in_addr stun_get_mapped_ipv4(const stun_state_t *state) { + struct in_addr none; + + if (state) + return state->mapped_ip4; + none.s_addr = INADDR_ANY; + return none; +} + int stun_is_stun_packet(const uint8_t *data, size_t len) { if (!data || len < 20) { return 0; diff --git a/src/stun.h b/src/stun.h index caea756a..09a96821 100644 --- a/src/stun.h +++ b/src/stun.h @@ -6,6 +6,7 @@ #ifndef __STUN_H__ #define __STUN_H__ +#include #include #include @@ -26,6 +27,7 @@ typedef struct { int retry_count; /* Number of retries attempted */ uint16_t mapped_rtp_port; /* Discovered mapped RTP port (0=none) */ uint16_t mapped_rtcp_port; /* Discovered mapped RTCP port (0=none) */ + struct in_addr mapped_ip4; /* Discovered mapped IPv4 address (INADDR_ANY=none) */ unsigned char transaction_id[STUN_TRANSACTION_ID_SIZE]; /* Transaction ID */ } stun_state_t; @@ -66,6 +68,13 @@ int stun_check_timeout(stun_state_t *state, int socket_fd); */ uint16_t stun_get_mapped_port(const stun_state_t *state); +/** + * Get the discovered mapped IPv4 address + * @param state STUN state structure + * @return Mapped IPv4 address, or INADDR_ANY if not discovered (or IPv6) + */ +struct in_addr stun_get_mapped_ipv4(const stun_state_t *state); + /** * Check if a UDP packet looks like a STUN response * STUN messages have first two bits as 00 diff --git a/src/utils.c b/src/utils.c index d59c2f09..ec7702ce 100644 --- a/src/utils.c +++ b/src/utils.c @@ -515,6 +515,31 @@ void sockaddr_set_port(struct sockaddr *sa, uint16_t port) { } } +uint16_t sockaddr_get_port(const struct sockaddr *sa) { + if (!sa) + return 0; + if (sa->sa_family == AF_INET) + return ntohs(((const struct sockaddr_in *)(uintptr_t)sa)->sin_port); + if (sa->sa_family == AF_INET6) + return ntohs(((const struct sockaddr_in6 *)(uintptr_t)sa)->sin6_port); + return 0; +} + +int sockaddr_format_ip(const struct sockaddr *sa, char *buf, size_t size) { + const void *addr; + + if (!sa || !buf || size == 0) + return -1; + if (sa->sa_family == AF_INET) + addr = &((const struct sockaddr_in *)(uintptr_t)sa)->sin_addr; + else if (sa->sa_family == AF_INET6) + addr = &((const struct sockaddr_in6 *)(uintptr_t)sa)->sin6_addr; + else + return -1; + + return inet_ntop(sa->sa_family, addr, buf, (socklen_t)size) ? 0 : -1; +} + char *build_proxy_base_url(const char *host_header, const char *x_forwarded_host, const char *x_forwarded_proto) { const char *host = NULL; const char *proto = "http"; diff --git a/src/utils.h b/src/utils.h index 74335aa3..32bfbd3a 100644 --- a/src/utils.h +++ b/src/utils.h @@ -216,6 +216,24 @@ int parse_host_port(const char *input, char *host, size_t host_size, int *port); */ void sockaddr_set_port(struct sockaddr *sa, uint16_t port); +/** + * Get the port of a sockaddr (AF_INET or AF_INET6). + * + * @param sa Socket address + * @return Port number in host byte order, 0 for an unsupported family + */ +uint16_t sockaddr_get_port(const struct sockaddr *sa); + +/** + * Format the address of a sockaddr in numeric form (AF_INET or AF_INET6). + * + * @param sa Socket address + * @param buf Output buffer, should be at least INET6_ADDRSTRLEN bytes + * @param size Output buffer size + * @return 0 on success, -1 on unsupported family or insufficient space + */ +int sockaddr_format_ip(const struct sockaddr *sa, char *buf, size_t size); + /* Array size calculation macro */ #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))