/src/wireshark/wiretap/rtpdump.c
Line | Count | Source (jump to first uncovered line) |
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-int.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 13 |
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 (strncmp(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 | if (!wtap_read_bytes(wth->fh, &start_time.secs, 4, err, err_info)) FAIL; |
214 | 0 | start_time.secs = g_ntohl(start_time.secs); |
215 | |
|
216 | 0 | if (!wtap_read_bytes(wth->fh, &start_time.nsecs, 4, err, err_info)) FAIL; |
217 | 0 | start_time.nsecs = g_ntohl(start_time.nsecs) * 1000; |
218 | |
|
219 | 0 | if (!wtap_read_bytes(wth->fh, &bin_addr, 4, err, err_info)) FAIL; |
220 | 0 | if (!wtap_read_bytes(wth->fh, &bin_port, 2, err, err_info)) FAIL; |
221 | 0 | bin_port = g_ntohs(bin_port); |
222 | | |
223 | | /* Finally, padding */ |
224 | 0 | if (!wtap_read_bytes(wth->fh, NULL, 2, err, err_info)) FAIL; |
225 | | |
226 | 0 | #undef FAIL |
227 | | /* If we made it this far, we have all the info we need to generate |
228 | | * most of the Exported PDU headers for every packet in this stream. |
229 | | */ |
230 | 0 | priv = g_new0(rtpdump_priv_t, 1); |
231 | 0 | priv->start_time = start_time; |
232 | 0 | buf = &priv->epdu_headers; /* shorthand */ |
233 | 0 | ws_buffer_init(buf, RTP_BUFFER_INIT_LEN); |
234 | 0 | wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_PORT_TYPE, EXP_PDU_PT_UDP); |
235 | 0 | if (is_ipv6) { |
236 | | /* File must be generated by Wireshark. Text address is IPv6 destination, |
237 | | * binary address is invalid and ignored here. |
238 | | */ |
239 | 0 | wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV6_DST, (const uint8_t *)&txt_addr.ipv6, EXP_PDU_TAG_IPV6_LEN); |
240 | 0 | wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_DST_PORT, txt_port); |
241 | 0 | } |
242 | 0 | else if (txt_addr.ipv4 == bin_addr && txt_port == bin_port) { |
243 | | /* File must be generated by rtpdump. Both addresses are IPv4 source. */ |
244 | 0 | wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV4_SRC, (const uint8_t *)&bin_addr, EXP_PDU_TAG_IPV4_LEN); |
245 | 0 | wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_SRC_PORT, bin_port); |
246 | 0 | } |
247 | 0 | else { |
248 | | /* File must be generated by Wireshark. Text is IPv4 destination, |
249 | | * binary is IPv4 source. |
250 | | */ |
251 | 0 | wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV4_DST, (const uint8_t *)&txt_addr.ipv4, EXP_PDU_TAG_IPV4_LEN); |
252 | 0 | wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_DST_PORT, txt_port); |
253 | |
|
254 | 0 | wtap_buffer_append_epdu_tag(buf, EXP_PDU_TAG_IPV4_SRC, (const uint8_t *)&bin_addr, EXP_PDU_TAG_IPV4_LEN); |
255 | 0 | wtap_buffer_append_epdu_uint(buf, EXP_PDU_TAG_SRC_PORT, bin_port); |
256 | 0 | } |
257 | |
|
258 | 0 | wth->priv = (void *)priv; |
259 | 0 | wth->subtype_close = rtpdump_close; |
260 | 0 | wth->subtype_read = rtpdump_read; |
261 | 0 | wth->subtype_seek_read = rtpdump_seek_read; |
262 | 0 | wth->file_type_subtype = rtpdump_file_type_subtype; |
263 | 0 | wth->file_encap = WTAP_ENCAP_WIRESHARK_UPPER_PDU; |
264 | | /* Starting timestamp has microsecond precision, but delta time |
265 | | * between packets is only milliseconds. |
266 | | */ |
267 | 0 | wth->file_tsprec = WTAP_TSPREC_MSEC; |
268 | |
|
269 | 0 | return WTAP_OPEN_MINE; |
270 | 0 | } |
271 | | |
272 | | static bool |
273 | | rtpdump_read_packet(wtap *wth, FILE_T fh, wtap_rec *rec, |
274 | | int *err, char **err_info) |
275 | 0 | { |
276 | 0 | rtpdump_priv_t *priv = (rtpdump_priv_t *)wth->priv; |
277 | 0 | nstime_t ts = NSTIME_INIT_ZERO; |
278 | 0 | uint32_t epdu_len = 0; /* length of the Exported PDU headers we add */ |
279 | 0 | const uint8_t hdr_len = 8; /* Header comprised of the following 3 fields: */ |
280 | 0 | uint16_t length; /* length of packet, including this header (may |
281 | | be smaller than plen if not whole packet recorded) */ |
282 | 0 | uint16_t plen; /* actual header+payload length for RTP, 0 for RTCP */ |
283 | 0 | uint32_t offset; /* milliseconds since the start of recording */ |
284 | |
|
285 | 0 | if (!wtap_read_bytes_or_eof(fh, (void *)&length, 2, err, err_info)) return false; |
286 | 0 | length = g_ntohs(length); |
287 | 0 | if (!wtap_read_bytes(fh, (void *)&plen, 2, err, err_info)) return false; |
288 | 0 | plen = g_ntohs(plen); |
289 | 0 | if (!wtap_read_bytes(fh, (void *)&offset, 4, err, err_info)) return false; |
290 | 0 | offset = g_ntohl(offset); |
291 | | |
292 | | /* Set length to remaining length of packet data */ |
293 | 0 | length -= hdr_len; |
294 | |
|
295 | 0 | ws_buffer_append_buffer(&rec->data, &priv->epdu_headers); |
296 | 0 | if (plen == 0) { |
297 | | /* RTCP sample */ |
298 | 0 | plen = length; |
299 | 0 | wtap_buffer_append_epdu_string(&rec->data, EXP_PDU_TAG_DISSECTOR_NAME, "rtcp"); |
300 | 0 | } |
301 | 0 | else { |
302 | | /* RTP sample */ |
303 | 0 | wtap_buffer_append_epdu_string(&rec->data, EXP_PDU_TAG_DISSECTOR_NAME, "rtp"); |
304 | 0 | } |
305 | 0 | epdu_len = wtap_buffer_append_epdu_end(&rec->data); |
306 | | |
307 | | /* Offset is milliseconds since the start of recording */ |
308 | 0 | ts.secs = offset / 1000; |
309 | 0 | ts.nsecs = (offset % 1000) * 1000000; |
310 | 0 | nstime_sum(&rec->ts, &priv->start_time, &ts); |
311 | 0 | rec->presence_flags |= WTAP_HAS_TS | WTAP_HAS_CAP_LEN; |
312 | 0 | rec->rec_header.packet_header.caplen = epdu_len + plen; |
313 | 0 | rec->rec_header.packet_header.len = epdu_len + length; |
314 | 0 | rec->rec_type = REC_TYPE_PACKET; |
315 | |
|
316 | 0 | return wtap_read_bytes_buffer(fh, &rec->data, length, err, err_info); |
317 | 0 | } |
318 | | |
319 | | static bool |
320 | | rtpdump_read(wtap *wth, wtap_rec *rec, int *err, char **err_info, |
321 | | int64_t *data_offset) |
322 | 0 | { |
323 | 0 | *data_offset = file_tell(wth->fh); |
324 | 0 | return rtpdump_read_packet(wth, wth->fh, rec, err, err_info); |
325 | 0 | } |
326 | | |
327 | | static bool |
328 | | rtpdump_seek_read(wtap *wth, int64_t seek_off, wtap_rec *rec, |
329 | | int *err, char **err_info) |
330 | 0 | { |
331 | 0 | if (file_seek(wth->random_fh, seek_off, SEEK_SET, err) == -1) |
332 | 0 | return false; |
333 | 0 | return rtpdump_read_packet(wth, wth->random_fh, rec, err, err_info); |
334 | 0 | } |
335 | | |
336 | | static void |
337 | | rtpdump_close(wtap *wth) |
338 | 0 | { |
339 | 0 | rtpdump_priv_t *priv = (rtpdump_priv_t *)wth->priv; |
340 | 0 | ws_buffer_free(&priv->epdu_headers); |
341 | 0 | } |
342 | | |
343 | | static const struct supported_block_type rtpdump_blocks_supported[] = { |
344 | | /* We support packet blocks, with no comments or other options. */ |
345 | | { WTAP_BLOCK_PACKET, MULTIPLE_BLOCKS_SUPPORTED, NO_OPTIONS_SUPPORTED } |
346 | | }; |
347 | | |
348 | | static const struct file_type_subtype_info rtpdump_info = { |
349 | | "RTPDump stream file", "rtpdump", "rtp", "rtpdump", |
350 | | false, BLOCKS_SUPPORTED(rtpdump_blocks_supported), |
351 | | NULL, NULL, NULL |
352 | | }; |
353 | | |
354 | | void register_rtpdump(void) |
355 | 14 | { |
356 | 14 | rtpdump_file_type_subtype = wtap_register_file_type_subtype(&rtpdump_info); |
357 | 14 | } |