diff options
Diffstat (limited to 'wiretap/rtpdump.c')
-rw-r--r-- | wiretap/rtpdump.c | 356 |
1 files changed, 356 insertions, 0 deletions
diff --git a/wiretap/rtpdump.c b/wiretap/rtpdump.c new file mode 100644 index 00000000..cd18b28a --- /dev/null +++ b/wiretap/rtpdump.c @@ -0,0 +1,356 @@ +/* rtpdump.c + * + * Wiretap Library + * Copyright (c) 1998 by Gilbert Ramirez <gram@alumni.rice.edu> + * + * Support for RTPDump file format + * Copyright (c) 2023 by David Perry <boolean263@protonmail.com> + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ +/* The rtpdump file format is the "dump" format as generated by rtpdump from + * <https://github.com/irtlab/rtptools>. 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: + * <https://github.com/irtlab/rtptools/blob/9356740cb0c/rtpdump.c#L189> + * + * 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 <wtap-int.h> +#include <file_wrappers.h> +#include <wsutil/exported_pdu_tlvs.h> +#include <wsutil/inet_addr.h> +#include <wsutil/nstime.h> +#include <wsutil/strtoi.h> +#include <wsutil/wslog.h> +#include "rtpdump.h" +#include <string.h> + +/* 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); +} |