357 lines
13 KiB
C
357 lines
13 KiB
C
/* 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_tag(buf, EXP_PDU_TAG_DISSECTOR_NAME, (const guint8 *)"rtcp", 4);
|
|
}
|
|
else {
|
|
/* RTP sample */
|
|
wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_DISSECTOR_NAME, (const guint8 *)"rtp", 3);
|
|
}
|
|
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);
|
|
}
|