Coverage Report

Created: 2025-12-27 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wireshark/wiretap/rtpdump.c
Line
Count
Source
1
/* rtpdump.c
2
 *
3
 * Wiretap Library
4
 * Copyright (c) 1998 by Gilbert Ramirez <gram@alumni.rice.edu>
5
 *
6
 * Support for RTPDump file format
7
 * Copyright (c) 2023 by David Perry <boolean263@protonmail.com>
8
 *
9
 * SPDX-License-Identifier: GPL-2.0-or-later
10
 */
11
/* The rtpdump file format is the "dump" format as generated by rtpdump from
12
 * <https://github.com/irtlab/rtptools>. It starts with an ASCII text header:
13
 *
14
 *      #!rtpplay1.0 source_address/port\n
15
 *
16
 * followed by a binary header:
17
 *
18
 *      typedef struct {
19
 *          struct timeval start;  // start of recording (GMT)
20
 *          uint32_t source;       // network source (multicast)
21
 *          uint16_t port;         // UDP port
22
 *      } RD_hdr_t;
23
 *
24
 * Note that the SAME source address and port are included twice in
25
 * this header, as seen here:
26
 * <https://github.com/irtlab/rtptools/blob/9356740cb0c/rtpdump.c#L189>
27
 *
28
 * Wireshark can also generate rtpdump files, but it uses DIFFERENT addresses
29
 * and ports in the text vs binary headers. See rtp_write_header() in
30
 * ui/tap-rtp-common.c -- Wireshark puts the destination address and port
31
 * in the text header, but the source address and port in the binary header.
32
 *
33
 * Wireshark may also generate the file with an IPv6 address in the text
34
 * header, whereas rtpdump only supports IPv4 here. The binary header
35
 * can't hold an IPv6 address without fully breaking compatibility with
36
 * the rtptools project, so Wireshark truncates the address.
37
 *
38
 * Either way, each packet which follows is a RTP or RTCP packet of the form
39
 *
40
 *      typedef struct {
41
 *          uint16_t length; // length of original packet, including header
42
 *          uint16_t plen;   // actual header+payload length for RTP, 0 for RTCP
43
 *          uint32_t offset; // ms since the start of recording
44
 *      } RD_packet_t;
45
 *
46
 * followed by length bytes of packet data.
47
 */
48
49
#include "config.h"
50
#include "rtpdump.h"
51
52
#include <wtap_module.h>
53
#include <file_wrappers.h>
54
#include <wsutil/exported_pdu_tlvs.h>
55
#include <wsutil/inet_addr.h>
56
#include <wsutil/nstime.h>
57
#include <wsutil/strtoi.h>
58
#include <wsutil/wslog.h>
59
#include <string.h>
60
61
/* NB. I've included the version string in the magic for stronger identification.
62
 * If we add/change version support, we'll also need to edit:
63
 *  - wiretap/mime_file.c
64
 *  - resources/freedesktop/org.wireshark.Wireshark-mime.xml
65
 *  - epan/dissectors/file-rtpdump.c
66
 */
67
0
#define RTP_MAGIC "#!rtpplay1.0 "
68
0
#define RTP_MAGIC_LEN (sizeof RTP_MAGIC - 1)
69
70
/* Reasonable maximum length for the RTP header (after the magic):
71
 * - WS_INET6_ADDRSTRLEN characters for a IPv6 address
72
 * - 1 for a slash
73
 * - 5 characters for a port number
74
 * - 1 character for a newline
75
 * - 4 bytes for each of start seconds, start useconds, IPv4 address
76
 * - 2 bytes for each of port, padding
77
 * and 2 bytes of fudge factor, just in case.
78
 */
79
0
#define RTP_HEADER_MAX_LEN 25+WS_INET6_ADDRSTRLEN
80
81
/* Reasonable buffer size for holding the Exported PDU headers:
82
 * - 8 bytes for the port type header
83
 * - 8 bytes for one port
84
 * - 4+EXP_PDU_TAG_IPV6_LEN for one IPv6 address
85
 *    (same space would hold 2 IPv4 addresses with room to spare)
86
 */
87
0
#define RTP_BUFFER_INIT_LEN 20+EXP_PDU_TAG_IPV6_LEN
88
89
static bool
90
rtpdump_read(wtap *wth, wtap_rec *rec,
91
               int *err, char **err_info,
92
               int64_t *data_offset);
93
94
static bool
95
rtpdump_seek_read(wtap *wth, int64_t seek_off,
96
                    wtap_rec *rec,
97
                    int *err, char **err_info);
98
99
static void
100
rtpdump_close(wtap *wth);
101
102
static int rtpdump_file_type_subtype = -1;
103
104
typedef union ip_addr_u {
105
    ws_in4_addr ipv4;
106
    ws_in6_addr ipv6;
107
} ip_addr_t;
108
109
typedef struct rtpdump_priv_s {
110
    Buffer epdu_headers;
111
    nstime_t start_time;
112
} rtpdump_priv_t;
113
114
void register_rtpdump(void);
115
116
wtap_open_return_val
117
rtpdump_open(wtap *wth, int *err, char **err_info)
118
0
{
119
0
    uint8_t buf_magic[RTP_MAGIC_LEN];
120
0
    char ch = '\0';
121
0
    rtpdump_priv_t *priv = NULL;
122
0
    ip_addr_t txt_addr;
123
0
    ws_in4_addr bin_addr;
124
0
    uint16_t txt_port = 0;
125
0
    uint16_t bin_port = 0;
126
0
    GString *header_str = NULL;
127
0
    bool is_ipv6 = false;
128
0
    bool got_ip = false;
129
0
    nstime_t start_time = NSTIME_INIT_ZERO;
130
0
    Buffer *buf;
131
132
0
    if (!wtap_read_bytes(wth->fh, buf_magic, RTP_MAGIC_LEN, err, err_info)) {
133
0
        return (*err == WTAP_ERR_SHORT_READ)
134
0
            ? WTAP_OPEN_NOT_MINE
135
0
            : WTAP_OPEN_ERROR;
136
0
    }
137
0
    if (memcmp(buf_magic, RTP_MAGIC, RTP_MAGIC_LEN) != 0) {
138
0
        return WTAP_OPEN_NOT_MINE;
139
0
    }
140
141
    /* Parse the text header for an IP and port. */
142
0
    header_str = g_string_sized_new(RTP_HEADER_MAX_LEN);
143
0
    do {
144
0
        if (!wtap_read_bytes(wth->fh, &ch, 1, err, err_info)) {
145
0
            g_string_free(header_str, TRUE);
146
0
            return (*err == WTAP_ERR_SHORT_READ)
147
0
                ? WTAP_OPEN_NOT_MINE
148
0
                : WTAP_OPEN_ERROR;
149
0
        }
150
151
0
        if (ch == '/') {
152
            /* Everything up to now should be an IP address */
153
0
            if (ws_inet_pton4(header_str->str, &txt_addr.ipv4)) {
154
0
                is_ipv6 = false;
155
0
            }
156
0
            else if (ws_inet_pton6(header_str->str, &txt_addr.ipv6)) {
157
0
                is_ipv6 = true;
158
0
            }
159
0
            else {
160
0
                *err = WTAP_ERR_BAD_FILE;
161
0
                *err_info = ws_strdup("rtpdump: bad IP in header text");
162
0
                g_string_free(header_str, TRUE);
163
0
                return WTAP_OPEN_ERROR;
164
0
            }
165
166
0
            got_ip = true;
167
0
            g_string_truncate(header_str, 0);
168
0
        }
169
0
        else if (ch == '\n') {
170
0
            if (!got_ip) {
171
0
                *err = WTAP_ERR_BAD_FILE;
172
0
                *err_info = ws_strdup("rtpdump: no IP in header text");
173
0
                g_string_free(header_str, TRUE);
174
0
                return WTAP_OPEN_ERROR;
175
0
            }
176
0
            if (!ws_strtou16(header_str->str, NULL, &txt_port)) {
177
0
                *err = WTAP_ERR_BAD_FILE;
178
0
                *err_info = ws_strdup("rtpdump: bad port in header text");
179
0
                g_string_free(header_str, TRUE);
180
0
                return WTAP_OPEN_ERROR;
181
0
            }
182
0
            break;
183
0
        }
184
0
        else if (g_ascii_isprint(ch)) {
185
0
            g_string_append_c(header_str, ch);
186
0
        }
187
0
        else {
188
0
            g_string_free(header_str, TRUE);
189
0
            return WTAP_OPEN_NOT_MINE;
190
0
        }
191
0
    } while (ch != '\n');
192
193
0
    g_string_free(header_str, TRUE);
194
195
0
    if (!got_ip || txt_port == 0) {
196
0
        *err = WTAP_ERR_BAD_FILE;
197
0
        *err_info = ws_strdup("rtpdump: bad header text");
198
0
        return WTAP_OPEN_ERROR;
199
0
    }
200
201
    /* Whether generated by rtpdump or Wireshark, the binary header
202
     * has the source IP and port. If the text header has an IPv6 address,
203
     * this address was likely truncated from an IPv6 address as well
204
     * and is likely inaccurate, so we will ignore it.
205
     */
206
207
0
#define FAIL G_STMT_START {              \
208
0
    return (*err == WTAP_ERR_SHORT_READ) \
209
0
        ? WTAP_OPEN_NOT_MINE             \
210
0
        : WTAP_OPEN_ERROR;               \
211
0
} G_STMT_END
212
213
0
    uint32_t u32;
214
215
0
    if (!wtap_read_bytes(wth->fh, &u32, 4, err, err_info)) FAIL;
216
0
    start_time.secs = (time_t)g_ntohl(u32);
217
218
0
    if (!wtap_read_bytes(wth->fh, &u32, 4, err, err_info)) FAIL;
219
0
    start_time.nsecs = (int)g_ntohl(u32) * 1000;
220
221
0
    if (!wtap_read_bytes(wth->fh, &bin_addr, 4, err, err_info)) FAIL;
222
0
    if (!wtap_read_bytes(wth->fh, &bin_port, 2, err, err_info)) FAIL;
223
0
    bin_port = g_ntohs(bin_port);
224
225
    /* Finally, padding */
226
0
    if (!wtap_read_bytes(wth->fh, NULL, 2, err, err_info)) FAIL;
227
228
0
#undef FAIL
229
    /* If we made it this far, we have all the info we need to generate
230
     * most of the Exported PDU headers for every packet in this stream.
231
     */
232
0
    priv = g_new0(rtpdump_priv_t, 1);
233
0
    priv->start_time = start_time;
234
0
    buf = &priv->epdu_headers; /* shorthand */
235
0
    ws_buffer_init(buf, RTP_BUFFER_INIT_LEN);
236
0
    wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_PORT_TYPE, EXP_PDU_PT_UDP);
237
0
    if (is_ipv6) {
238
        /* File must be generated by Wireshark. Text address is IPv6 destination,
239
         * binary address is invalid and ignored here.
240
         */
241
0
        wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV6_DST, (const uint8_t *)&txt_addr.ipv6, EXP_PDU_TAG_IPV6_LEN);
242
0
        wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_DST_PORT, txt_port);
243
0
    }
244
0
    else if (txt_addr.ipv4 == bin_addr && txt_port == bin_port) {
245
        /* File must be generated by rtpdump. Both addresses are IPv4 source. */
246
0
        wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV4_SRC, (const uint8_t *)&bin_addr, EXP_PDU_TAG_IPV4_LEN);
247
0
        wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_SRC_PORT, bin_port);
248
0
    }
249
0
    else {
250
        /* File must be generated by Wireshark. Text is IPv4 destination,
251
         * binary is IPv4 source.
252
         */
253
0
        wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV4_DST, (const uint8_t *)&txt_addr.ipv4, EXP_PDU_TAG_IPV4_LEN);
254
0
        wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_DST_PORT, txt_port);
255
256
0
        wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV4_SRC, (const uint8_t *)&bin_addr, EXP_PDU_TAG_IPV4_LEN);
257
0
        wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_SRC_PORT, bin_port);
258
0
    }
259
260
0
    wth->priv              = (void *)priv;
261
0
    wth->subtype_close     = rtpdump_close;
262
0
    wth->subtype_read      = rtpdump_read;
263
0
    wth->subtype_seek_read = rtpdump_seek_read;
264
0
    wth->file_type_subtype = rtpdump_file_type_subtype;
265
0
    wth->file_encap        = WTAP_ENCAP_WIRESHARK_UPPER_PDU;
266
    /* Starting timestamp has microsecond precision, but delta time
267
     * between packets is only milliseconds.
268
     */
269
0
    wth->file_tsprec       = WTAP_TSPREC_MSEC;
270
271
0
    return WTAP_OPEN_MINE;
272
0
}
273
274
static bool
275
rtpdump_read_packet(wtap *wth, FILE_T fh, wtap_rec *rec,
276
                      int *err, char **err_info)
277
0
{
278
0
    rtpdump_priv_t *priv = (rtpdump_priv_t *)wth->priv;
279
0
    nstime_t ts = NSTIME_INIT_ZERO;
280
0
    uint32_t epdu_len = 0;       /* length of the Exported PDU headers we add */
281
0
    const uint8_t hdr_len = 8;   /* Header comprised of the following 3 fields: */
282
0
    uint16_t length;             /* length of packet, including this header (may
283
                                   be smaller than plen if not whole packet recorded) */
284
0
    uint16_t plen;               /* actual header+payload length for RTP, 0 for RTCP */
285
0
    uint32_t offset;             /* milliseconds since the start of recording */
286
287
0
    if (!wtap_read_bytes_or_eof(fh, (void *)&length, 2, err, err_info)) return false;
288
0
    length = g_ntohs(length);
289
0
    if (!wtap_read_bytes(fh, (void *)&plen, 2, err, err_info)) return false;
290
0
    plen = g_ntohs(plen);
291
0
    if (!wtap_read_bytes(fh, (void *)&offset, 4, err, err_info)) return false;
292
0
    offset = g_ntohl(offset);
293
294
    /* Set length to remaining length of packet data */
295
0
    length -= hdr_len;
296
297
0
    ws_buffer_append_buffer(&rec->data, &priv->epdu_headers);
298
0
    if (plen == 0) {
299
        /* RTCP sample */
300
0
        plen = length;
301
0
        wtap_buffer_append_epdu_string(&rec->data, EXP_PDU_TAG_DISSECTOR_NAME, "rtcp");
302
0
    }
303
0
    else {
304
        /* RTP sample */
305
0
        wtap_buffer_append_epdu_string(&rec->data, EXP_PDU_TAG_DISSECTOR_NAME, "rtp");
306
0
    }
307
0
    epdu_len = wtap_buffer_append_epdu_end(&rec->data);
308
309
0
    wtap_setup_packet_rec(rec, wth->file_encap);
310
311
    /* Offset is milliseconds since the start of recording */
312
0
    ts.secs = offset / 1000;
313
0
    ts.nsecs = (offset % 1000) * 1000000;
314
0
    nstime_sum(&rec->ts, &priv->start_time, &ts);
315
0
    rec->presence_flags |= WTAP_HAS_TS | WTAP_HAS_CAP_LEN;
316
0
    rec->rec_header.packet_header.caplen = epdu_len + plen;
317
0
    rec->rec_header.packet_header.len = epdu_len + length;
318
319
0
    return wtap_read_bytes_buffer(fh, &rec->data, length, err, err_info);
320
0
}
321
322
static bool
323
rtpdump_read(wtap *wth, wtap_rec *rec, int *err, char **err_info,
324
               int64_t *data_offset)
325
0
{
326
0
    *data_offset = file_tell(wth->fh);
327
0
    return rtpdump_read_packet(wth, wth->fh, rec, err, err_info);
328
0
}
329
330
static bool
331
rtpdump_seek_read(wtap *wth, int64_t seek_off, wtap_rec *rec,
332
                    int *err, char **err_info)
333
0
{
334
0
    if (file_seek(wth->random_fh, seek_off, SEEK_SET, err) == -1)
335
0
        return false;
336
0
    return rtpdump_read_packet(wth, wth->random_fh, rec, err, err_info);
337
0
}
338
339
static void
340
rtpdump_close(wtap *wth)
341
0
{
342
0
    rtpdump_priv_t *priv = (rtpdump_priv_t *)wth->priv;
343
0
    ws_buffer_free(&priv->epdu_headers);
344
0
}
345
346
static const struct supported_block_type rtpdump_blocks_supported[] = {
347
    /* We support packet blocks, with no comments or other options. */
348
    { WTAP_BLOCK_PACKET, MULTIPLE_BLOCKS_SUPPORTED, NO_OPTIONS_SUPPORTED }
349
};
350
351
static const struct file_type_subtype_info rtpdump_info = {
352
    "RTPDump stream file", "rtpdump", "rtp", "rtpdump",
353
    false, BLOCKS_SUPPORTED(rtpdump_blocks_supported),
354
    NULL, NULL, NULL
355
};
356
357
void register_rtpdump(void)
358
14
{
359
14
    rtpdump_file_type_subtype = wtap_register_file_type_subtype(&rtpdump_info);
360
14
}