/* rtpdump.c * * Wiretap Library * Copyright (c) 1998 by Gilbert Ramirez * * Support for RTPDump file format * Copyright (c) 2023 by David Perry * * SPDX-License-Identifier: GPL-2.0-or-later */ /* The rtpdump file format is the "dump" format as generated by rtpdump from * . It starts with an ASCII text header: * * #!rtpplay1.0 source_address/port\n * * followed by a binary header: * * typedef struct { * struct timeval start; // start of recording (GMT) * uint32_t source; // network source (multicast) * uint16_t port; // UDP port * } RD_hdr_t; * * Note that the SAME source address and port are included twice in * this header, as seen here: * * * Wireshark can also generate rtpdump files, but it uses DIFFERENT addresses * and ports in the text vs binary headers. See rtp_write_header() in * ui/tap-rtp-common.c -- Wireshark puts the destination address and port * in the text header, but the source address and port in the binary header. * * Wireshark may also generate the file with an IPv6 address in the text * header, whereas rtpdump only supports IPv4 here. The binary header * can't hold an IPv6 address without fully breaking compatibility with * the rtptools project, so Wireshark truncates the address. * * Either way, each packet which follows is a RTP or RTCP packet of the form * * typedef struct { * uint16_t length; // length of original packet, including header * uint16_t plen; // actual header+payload length for RTP, 0 for RTCP * uint32_t offset; // ms since the start of recording * } RD_packet_t; * * followed by length bytes of packet data. */ #include "config.h" #include #include #include #include #include #include #include #include "rtpdump.h" #include /* NB. I've included the version string in the magic for stronger identification. * If we add/change version support, we'll also need to edit: * - wiretap/mime_file.c * - resources/freedesktop/org.wireshark.Wireshark-mime.xml * - epan/dissectors/file-rtpdump.c */ #define RTP_MAGIC "#!rtpplay1.0 " #define RTP_MAGIC_LEN 13 /* Reasonable maximum length for the RTP header (after the magic): * - WS_INET6_ADDRSTRLEN characters for a IPv6 address * - 1 for a slash * - 5 characters for a port number * - 1 character for a newline * - 4 bytes for each of start seconds, start useconds, IPv4 address * - 2 bytes for each of port, padding * and 2 bytes of fudge factor, just in case. */ #define RTP_HEADER_MAX_LEN 25+WS_INET6_ADDRSTRLEN /* Reasonable buffer size for holding the Exported PDU headers: * - 8 bytes for the port type header * - 8 bytes for one port * - 4+EXP_PDU_TAG_IPV6_LEN for one IPv6 address * (same space would hold 2 IPv4 addresses with room to spare) */ #define RTP_BUFFER_INIT_LEN 20+EXP_PDU_TAG_IPV6_LEN static gboolean rtpdump_read(wtap *wth, wtap_rec *rec, Buffer *buf, int *err, gchar **err_info, gint64 *data_offset); static gboolean rtpdump_seek_read(wtap *wth, gint64 seek_off, wtap_rec *rec, Buffer *buf, int *err, gchar **err_info); static void rtpdump_close(wtap *wth); static int rtpdump_file_type_subtype = -1; typedef union ip_addr_u { ws_in4_addr ipv4; ws_in6_addr ipv6; } ip_addr_t; typedef struct rtpdump_priv_s { Buffer epdu_headers; nstime_t start_time; } rtpdump_priv_t; void register_rtpdump(void); wtap_open_return_val rtpdump_open(wtap *wth, int *err, char **err_info) { guint8 buf_magic[RTP_MAGIC_LEN]; char ch = '\0'; rtpdump_priv_t *priv = NULL; ip_addr_t txt_addr; ws_in4_addr bin_addr; guint16 txt_port = 0; guint16 bin_port = 0; GString *header_str = NULL; gboolean is_ipv6 = FALSE; gboolean got_ip = FALSE; nstime_t start_time = NSTIME_INIT_ZERO; Buffer *buf; if (!wtap_read_bytes(wth->fh, buf_magic, RTP_MAGIC_LEN, err, err_info)) { return (*err == WTAP_ERR_SHORT_READ) ? WTAP_OPEN_NOT_MINE : WTAP_OPEN_ERROR; } if (strncmp(buf_magic, RTP_MAGIC, RTP_MAGIC_LEN) != 0) { return WTAP_OPEN_NOT_MINE; } /* Parse the text header for an IP and port. */ header_str = g_string_sized_new(RTP_HEADER_MAX_LEN); do { if (!wtap_read_bytes(wth->fh, &ch, 1, err, err_info)) { g_string_free(header_str, TRUE); return (*err == WTAP_ERR_SHORT_READ) ? WTAP_OPEN_NOT_MINE : WTAP_OPEN_ERROR; } if (ch == '/') { /* Everything up to now should be an IP address */ if (ws_inet_pton4(header_str->str, &txt_addr.ipv4)) { is_ipv6 = FALSE; } else if (ws_inet_pton6(header_str->str, &txt_addr.ipv6)) { is_ipv6 = TRUE; } else { *err = WTAP_ERR_BAD_FILE; *err_info = ws_strdup("rtpdump: bad IP in header text"); g_string_free(header_str, TRUE); return WTAP_OPEN_ERROR; } got_ip = TRUE; g_string_truncate(header_str, 0); } else if (ch == '\n') { if (!got_ip) { *err = WTAP_ERR_BAD_FILE; *err_info = ws_strdup("rtpdump: no IP in header text"); g_string_free(header_str, TRUE); return WTAP_OPEN_ERROR; } if (!ws_strtou16(header_str->str, NULL, &txt_port)) { *err = WTAP_ERR_BAD_FILE; *err_info = ws_strdup("rtpdump: bad port in header text"); g_string_free(header_str, TRUE); return WTAP_OPEN_ERROR; } break; } else if (g_ascii_isprint(ch)) { g_string_append_c(header_str, ch); } else { g_string_free(header_str, TRUE); return WTAP_OPEN_NOT_MINE; } } while (ch != '\n'); g_string_free(header_str, TRUE); if (!got_ip || txt_port == 0) { *err = WTAP_ERR_BAD_FILE; *err_info = ws_strdup("rtpdump: bad header text"); return WTAP_OPEN_ERROR; } /* Whether generated by rtpdump or Wireshark, the binary header * has the source IP and port. If the text header has an IPv6 address, * this address was likely truncated from an IPv6 address as well * and is likely inaccurate, so we will ignore it. */ #define FAIL G_STMT_START { \ return (*err == WTAP_ERR_SHORT_READ) \ ? WTAP_OPEN_NOT_MINE \ : WTAP_OPEN_ERROR; \ } G_STMT_END if (!wtap_read_bytes(wth->fh, &start_time.secs, 4, err, err_info)) FAIL; start_time.secs = g_ntohl(start_time.secs); if (!wtap_read_bytes(wth->fh, &start_time.nsecs, 4, err, err_info)) FAIL; start_time.nsecs = g_ntohl(start_time.nsecs) * 1000; if (!wtap_read_bytes(wth->fh, &bin_addr, 4, err, err_info)) FAIL; if (!wtap_read_bytes(wth->fh, &bin_port, 2, err, err_info)) FAIL; bin_port = g_ntohs(bin_port); /* Finally, padding */ if (!wtap_read_bytes(wth->fh, NULL, 2, err, err_info)) FAIL; #undef FAIL /* If we made it this far, we have all the info we need to generate * most of the Exported PDU headers for every packet in this stream. */ priv = g_new0(rtpdump_priv_t, 1); priv->start_time = start_time; buf = &priv->epdu_headers; /* shorthand */ ws_buffer_init(buf, RTP_BUFFER_INIT_LEN); wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_PORT_TYPE, EXP_PDU_PT_UDP); if (is_ipv6) { /* File must be generated by Wireshark. Text address is IPv6 destination, * binary address is invalid and ignored here. */ wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV6_DST, (const guint8 *)&txt_addr.ipv6, EXP_PDU_TAG_IPV6_LEN); wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_DST_PORT, txt_port); } else if (txt_addr.ipv4 == bin_addr && txt_port == bin_port) { /* File must be generated by rtpdump. Both addresses are IPv4 source. */ wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV4_SRC, (const guint8 *)&bin_addr, EXP_PDU_TAG_IPV4_LEN); wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_SRC_PORT, bin_port); } else { /* File must be generated by Wireshark. Text is IPv4 destination, * binary is IPv4 source. */ wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV4_DST, (const guint8 *)&txt_addr.ipv4, EXP_PDU_TAG_IPV4_LEN); wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_DST_PORT, txt_port); wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV4_SRC, (const guint8 *)&bin_addr, EXP_PDU_TAG_IPV4_LEN); wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_SRC_PORT, bin_port); } wth->priv = (void *)priv; wth->subtype_close = rtpdump_close; wth->subtype_read = rtpdump_read; wth->subtype_seek_read = rtpdump_seek_read; wth->file_type_subtype = rtpdump_file_type_subtype; wth->file_encap = WTAP_ENCAP_WIRESHARK_UPPER_PDU; /* Starting timestamp has microsecond precision, but delta time * between packets is only milliseconds. */ wth->file_tsprec = WTAP_TSPREC_MSEC; return WTAP_OPEN_MINE; } static gboolean rtpdump_read_packet(wtap *wth, FILE_T fh, wtap_rec *rec, Buffer *buf, int *err, gchar **err_info) { rtpdump_priv_t *priv = (rtpdump_priv_t *)wth->priv; nstime_t ts = NSTIME_INIT_ZERO; guint32 epdu_len = 0; /* length of the Exported PDU headers we add */ const guint8 hdr_len = 8; /* Header comprised of the following 3 fields: */ guint16 length; /* length of packet, including this header (may be smaller than plen if not whole packet recorded) */ guint16 plen; /* actual header+payload length for RTP, 0 for RTCP */ guint32 offset; /* milliseconds since the start of recording */ if (!wtap_read_bytes_or_eof(fh, (void *)&length, 2, err, err_info)) return FALSE; length = g_ntohs(length); if (!wtap_read_bytes(fh, (void *)&plen, 2, err, err_info)) return FALSE; plen = g_ntohs(plen); if (!wtap_read_bytes(fh, (void *)&offset, 4, err, err_info)) return FALSE; offset = g_ntohl(offset); /* Set length to remaining length of packet data */ length -= hdr_len; ws_buffer_append_buffer(buf, &priv->epdu_headers); if (plen == 0) { /* RTCP sample */ plen = length; wtap_buffer_append_epdu_string(buf, EXP_PDU_TAG_DISSECTOR_NAME, "rtcp"); } else { /* RTP sample */ wtap_buffer_append_epdu_string(buf, EXP_PDU_TAG_DISSECTOR_NAME, "rtp"); } epdu_len = wtap_buffer_append_epdu_end(buf); /* Offset is milliseconds since the start of recording */ ts.secs = offset / 1000; ts.nsecs = (offset % 1000) * 1000000; nstime_sum(&rec->ts, &priv->start_time, &ts); rec->presence_flags |= WTAP_HAS_TS | WTAP_HAS_CAP_LEN; rec->rec_header.packet_header.caplen = epdu_len + plen; rec->rec_header.packet_header.len = epdu_len + length; rec->rec_type = REC_TYPE_PACKET; return wtap_read_packet_bytes(fh, buf, length, err, err_info); } static gboolean rtpdump_read(wtap *wth, wtap_rec *rec, Buffer *buf, int *err, gchar **err_info, gint64 *data_offset) { *data_offset = file_tell(wth->fh); return rtpdump_read_packet(wth, wth->fh, rec, buf, err, err_info); } static gboolean rtpdump_seek_read(wtap *wth, gint64 seek_off, wtap_rec *rec, Buffer *buf, int *err, gchar **err_info) { if (file_seek(wth->random_fh, seek_off, SEEK_SET, err) == -1) return FALSE; return rtpdump_read_packet(wth, wth->random_fh, rec, buf, err, err_info); } static void rtpdump_close(wtap *wth) { rtpdump_priv_t *priv = (rtpdump_priv_t *)wth->priv; ws_buffer_free(&priv->epdu_headers); } static const struct supported_block_type rtpdump_blocks_supported[] = { /* We support packet blocks, with no comments or other options. */ { WTAP_BLOCK_PACKET, MULTIPLE_BLOCKS_SUPPORTED, NO_OPTIONS_SUPPORTED } }; static const struct file_type_subtype_info rtpdump_info = { "RTPDump stream file", "rtpdump", "rtp", "rtpdump", FALSE, BLOCKS_SUPPORTED(rtpdump_blocks_supported), NULL, NULL, NULL }; void register_rtpdump(void) { rtpdump_file_type_subtype = wtap_register_file_type_subtype(&rtpdump_info); }