#include "recording.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "xt_RTPENGINE.h" #include "call.h" #include "kernel.h" #include "bencode.h" #include "rtplib.h" #include "cdr.h" struct pcap_format { int linktype; int headerlen; void (*header)(unsigned char *, struct packet_stream *); }; static int check_main_spool_dir(const char *spoolpath); static char *recording_setup_file(struct recording *recording); static char *meta_setup_file(struct recording *recording); // pcap methods static int pcap_create_spool_dir(const char *dirpath); static void pcap_init(struct call *); static void sdp_after_pcap(struct recording *, struct iovec *sdp_iov, int iovcnt, unsigned int str_len, struct call_monologue *, enum call_opmode opmode); static void dump_packet_pcap(struct recording *recording, struct packet_stream *sink, const str *s); static void finish_pcap(struct call *); static void response_pcap(struct recording *, bencode_item_t *); // proc methods static void proc_init(struct call *); static void sdp_before_proc(struct recording *, const str *, struct call_monologue *, enum call_opmode); static void sdp_after_proc(struct recording *, struct iovec *sdp_iov, int iovcnt, unsigned int str_len, struct call_monologue *, enum call_opmode opmode); static void meta_chunk_proc(struct recording *, const char *, const str *); static void finish_proc(struct call *); static void dump_packet_proc(struct recording *recording, struct packet_stream *sink, const str *s); static void init_stream_proc(struct packet_stream *); static void setup_stream_proc(struct packet_stream *); static void setup_media_proc(struct call_media *); static void kernel_info_proc(struct packet_stream *, struct rtpengine_target_info *); static void pcap_eth_header(unsigned char *, struct packet_stream *); static const struct recording_method methods[] = { { .name = "pcap", .kernel_support = 0, .create_spool_dir = pcap_create_spool_dir, .init_struct = pcap_init, .sdp_after = sdp_after_pcap, .dump_packet = dump_packet_pcap, .finish = finish_pcap, .response = response_pcap, }, { .name = "proc", .kernel_support = 1, .create_spool_dir = check_main_spool_dir, .init_struct = proc_init, .sdp_before = sdp_before_proc, .sdp_after = sdp_after_proc, .meta_chunk = meta_chunk_proc, .dump_packet = dump_packet_proc, .finish = finish_proc, .init_stream_struct = init_stream_proc, .setup_stream = setup_stream_proc, .setup_media = setup_media_proc, .stream_kernel_info = kernel_info_proc, }, }; static const struct pcap_format pcap_format_raw = { .linktype = DLT_RAW, .headerlen = 0, }; static const struct pcap_format pcap_format_eth = { .linktype = DLT_EN10MB, .headerlen = 14, .header = pcap_eth_header, }; // Global file reference to the spool directory. static char *spooldir = NULL; const struct recording_method *selected_recording_method; static const struct pcap_format *pcap_format; /** * Initialize RTP Engine filesystem settings and structure. * Check for or create the RTP Engine spool directory. */ void recording_fs_init(const char *spoolpath, const char *method_str, const char *format_str) { int i; // Whether or not to fail if the spool directory does not exist. if (spoolpath == NULL || spoolpath[0] == '\0') return; for (i = 0; i < G_N_ELEMENTS(methods); i++) { if (!strcmp(methods[i].name, method_str)) { selected_recording_method = &methods[i]; goto found; } } ilog(LOG_ERROR, "Recording method '%s' not supported", method_str); return; found: if(!strcmp("raw", format_str)) pcap_format = &pcap_format_raw; else if(!strcmp("eth", format_str)) pcap_format = &pcap_format_eth; else { ilog(LOG_ERR, "Invalid value for recording format \"%s\".", format_str); exit(-1); } spooldir = strdup(spoolpath); int path_len = strlen(spooldir); // Get rid of trailing "/" if it exists. Other code adds that in when needed. if (spooldir[path_len-1] == '/') { spooldir[path_len-1] = '\0'; } if (!_rm_ret(create_spool_dir, spooldir)) { ilog(LOG_ERR, "Error while setting up spool directory \"%s\".", spooldir); ilog(LOG_ERR, "Please run `mkdir %s` and start rtpengine again.", spooldir); exit(-1); } } static int check_create_dir(const char *dir, const char *desc, mode_t creat_mode) { struct stat info; if (stat(dir, &info) != 0) { if (!creat_mode) { ilog(LOG_WARN, "%s directory \"%s\" does not exist.", desc, dir); return FALSE; } ilog(LOG_INFO, "Creating %s directory \"%s\".", desc, dir); // coverity[toctou : FALSE] if (mkdir(dir, creat_mode) == 0) return TRUE; ilog(LOG_ERR, "Failed to create %s directory \"%s\": %s", desc, dir, strerror(errno)); return FALSE; } if(!S_ISDIR(info.st_mode)) { ilog(LOG_ERR, "%s file exists, but \"%s\" is not a directory.", desc, dir); return FALSE; } return TRUE; } static int check_main_spool_dir(const char *spoolpath) { return check_create_dir(spoolpath, "spool", 0700); } /** * Sets up the spool directory for RTP Engine. * If the directory does not exist, return FALSE. * If the directory exists, but "$spoolpath/metadata" or "$spoolpath/pcaps" * exist as non-directory files, return FALSE. * Otherwise, return TRUE. * * Create the "metadata" and "pcaps" directories if they are not there. */ static int pcap_create_spool_dir(const char *spoolpath) { int spool_good = TRUE; if (!check_main_spool_dir(spoolpath)) return FALSE; // Spool directory exists. Make sure it has inner directories. int path_len = strlen(spoolpath); char meta_path[path_len + 10]; char rec_path[path_len + 7]; char tmp_path[path_len + 5]; snprintf(meta_path, sizeof(meta_path), "%s/metadata", spoolpath); snprintf(rec_path, sizeof(rec_path), "%s/pcaps", spoolpath); snprintf(tmp_path, sizeof(tmp_path), "%s/tmp", spoolpath); if (!check_create_dir(meta_path, "metadata", 0777)) spool_good = FALSE; if (!check_create_dir(rec_path, "pcaps", 0777)) spool_good = FALSE; if (!check_create_dir(tmp_path, "tmp", 0777)) spool_good = FALSE; return spool_good; } // lock must be held void recording_start(struct call *call, const char *prefix, str *metadata) { if (call->recording) // already active return; if (!spooldir) { ilog(LOG_ERR, "Call recording requested, but no spool directory configured"); return; } ilog(LOG_NOTICE, "Turning on call recording."); call->recording = g_slice_alloc0(sizeof(struct recording)); struct recording *recording = call->recording; recording->escaped_callid = g_uri_escape_string(call->callid.s, NULL, 0); if (!prefix) { const int rand_bytes = 8; char rand_str[rand_bytes * 2 + 1]; rand_hex_str(rand_str, rand_bytes); if (asprintf(&recording->meta_prefix, "%s-%s", recording->escaped_callid, rand_str) < 0) abort(); } else recording->meta_prefix = strdup(prefix); if (metadata->len) { call_str_cpy(call, &recording->metadata, metadata); } _rm(init_struct, call); // if recording has been turned on after initial call setup, we must walk // through all related objects and initialize the recording stuff. if this // function is called right at the start of the call, all of the following // is essentially a no-op GList *l; for (l = call->streams.head; l; l = l->next) { struct packet_stream *ps = l->data; recording_setup_stream(ps); __unkernelize(ps); ps->handler = NULL; } } void recording_stop(struct call *call) { if (!call->recording) return; ilog(LOG_NOTICE, "Turning off call recording."); recording_finish(call); } /** * * Controls the setting of recording variables on a `struct call *`. * Sets the `record_call` value on the `struct call`, initializing the * recording struct if necessary. * If we do not yet have a PCAP file associated with the call, create it * and write its file URL to the metadata file. * * Returns a boolean for whether or not the call is being recorded. */ void detect_setup_recording(struct call *call, const str *recordcall, str *metadata) { if (!recordcall || !recordcall->s) return; if (!str_cmp(recordcall, "yes") || !str_cmp(recordcall, "on")) recording_start(call, NULL, metadata); else if (!str_cmp(recordcall, "no") || !str_cmp(recordcall, "off")) recording_stop(call); else ilog(LOG_INFO, "\"record-call\" flag "STR_FORMAT" is invalid flag.", STR_FMT(recordcall)); } static void pcap_init(struct call *call) { struct recording *recording = call->recording; // Wireshark starts at packet index 1, so we start there, too recording->u.pcap.packet_num = 1; mutex_init(&recording->u.pcap.recording_lock); meta_setup_file(recording); // set up pcap file char *pcap_path = recording_setup_file(recording); if (pcap_path != NULL && recording->u.pcap.recording_pdumper != NULL && recording->u.pcap.meta_fp) { // Write the location of the PCAP file to the metadata file fprintf(recording->u.pcap.meta_fp, "%s\n\n", pcap_path); } } static char *file_path_str(const char *id, const char *prefix, const char *suffix) { char *ret; if (asprintf(&ret, "%s%s%s%s", spooldir, prefix, id, suffix) < 0) abort(); return ret; } /** * Create a call metadata file in a temporary location. * Attaches the filepath and the file pointer to the call struct. */ static char *meta_setup_file(struct recording *recording) { if (spooldir == NULL) { // No spool directory was created, so we cannot have metadata files. return NULL; } char *meta_filepath = file_path_str(recording->meta_prefix, "/tmp/rtpengine-meta-", ".tmp"); recording->meta_filepath = meta_filepath; FILE *mfp = fopen(meta_filepath, "w"); // coverity[check_return : FALSE] chmod(meta_filepath, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); if (mfp == NULL) { ilog(LOG_ERROR, "Could not open metadata file: %s", meta_filepath); free(meta_filepath); recording->meta_filepath = NULL; return NULL; } recording->u.pcap.meta_fp = mfp; ilog(LOG_DEBUG, "Wrote metadata file to temporary path: %s", meta_filepath); return meta_filepath; } /** * Write out a block of SDP to the metadata file. */ static void sdp_after_pcap(struct recording *recording, struct iovec *sdp_iov, int iovcnt, unsigned int str_len, struct call_monologue *ml, enum call_opmode opmode) { FILE *meta_fp = recording->u.pcap.meta_fp; if (!meta_fp) return; int meta_fd = fileno(meta_fp); // File pointers buffer data, whereas direct writing using the file // descriptor does not. Make sure to flush any unwritten contents // so the file contents appear in order. fprintf(meta_fp, "\nSDP mode: "); fprintf(meta_fp, "%s", get_opmode_text(opmode)); fprintf(meta_fp, "\nSDP before RTP packet: %" PRIu64 "\n\n", recording->u.pcap.packet_num); fflush(meta_fp); if (writev(meta_fd, sdp_iov, iovcnt) <= 0) ilog(LOG_WARN, "Error writing SDP body to metadata file: %s", strerror(errno)); } /** * Writes metadata to metafile, closes file, and renames it to finished location. * Returns non-zero for failure. */ static int pcap_meta_finish_file(struct call *call) { // This should usually be called from a place that has the call->master_lock struct recording *recording = call->recording; int return_code = 0; if (recording == NULL || recording->u.pcap.meta_fp == NULL) { ilog(LOG_INFO, "Trying to clean up recording meta file without a file pointer opened."); return 0; } // Print start timestamp and end timestamp // YYYY-MM-DDThh:mm:ss time_t start = call->created.tv_sec; time_t end = rtpe_now.tv_sec; char timebuffer[20]; struct tm *timeinfo; timeinfo = localtime(&start); strftime(timebuffer, 20, "%FT%T", timeinfo); fprintf(recording->u.pcap.meta_fp, "\n\ncall start time: %s\n", timebuffer); timeinfo = localtime(&end); strftime(timebuffer, 20, "%FT%T", timeinfo); fprintf(recording->u.pcap.meta_fp, "call end time: %s\n", timebuffer); // Print metadata if (recording->metadata.len) fprintf(recording->u.pcap.meta_fp, "\n\n"STR_FORMAT"\n", STR_FMT(&recording->metadata)); fclose(recording->u.pcap.meta_fp); recording->u.pcap.meta_fp = NULL; // Get the filename (in between its directory and the file extension) // and move it to the finished file location. // Rename extension to ".txt". int fn_len; char *meta_filename = strrchr(recording->meta_filepath, '/'); char *meta_ext = NULL; if (meta_filename == NULL) { meta_filename = recording->meta_filepath; } else { meta_filename = meta_filename + 1; } // We can always expect a file extension meta_ext = strrchr(meta_filename, '.'); fn_len = meta_ext - meta_filename; int prefix_len = strlen(spooldir) + 10; // constant for "/metadata/" suffix int ext_len = 4; // for ".txt" char new_metapath[prefix_len + fn_len + ext_len + 1]; snprintf(new_metapath, prefix_len+fn_len+1, "%s/metadata/%s", spooldir, meta_filename); snprintf(new_metapath + prefix_len+fn_len, ext_len+1, ".txt"); return_code = return_code || rename(recording->meta_filepath, new_metapath); if (return_code != 0) { ilog(LOG_ERROR, "Could not move metadata file \"%s\" to \"%s/metadata/\"", recording->meta_filepath, spooldir); } else { ilog(LOG_INFO, "Moved metadata file \"%s\" to \"%s/metadata\"", recording->meta_filepath, spooldir); } mutex_destroy(&recording->u.pcap.recording_lock); return return_code; } /** * Generate a random PCAP filepath to write recorded RTP stream. * Returns path to created file. */ static char *recording_setup_file(struct recording *recording) { char *recording_path = NULL; if (!spooldir) return NULL; if (recording->u.pcap.recording_pd || recording->u.pcap.recording_pdumper) return NULL; recording_path = file_path_str(recording->meta_prefix, "/pcaps/", ".pcap"); recording->u.pcap.recording_path = recording_path; recording->u.pcap.recording_pd = pcap_open_dead(pcap_format->linktype, 65535); recording->u.pcap.recording_pdumper = pcap_dump_open(recording->u.pcap.recording_pd, recording_path); if (recording->u.pcap.recording_pdumper == NULL) { pcap_close(recording->u.pcap.recording_pd); recording->u.pcap.recording_pd = NULL; ilog(LOG_INFO, "Failed to write recording file: %s", recording_path); } else { ilog(LOG_INFO, "Writing recording file: %s", recording_path); } return recording_path; } /** * Flushes PCAP file, closes the dumper and descriptors, and frees object memory. */ static void pcap_recording_finish_file(struct recording *recording) { if (recording->u.pcap.recording_pdumper != NULL) { pcap_dump_flush(recording->u.pcap.recording_pdumper); pcap_dump_close(recording->u.pcap.recording_pdumper); free(recording->u.pcap.recording_path); } if (recording->u.pcap.recording_pd != NULL) { pcap_close(recording->u.pcap.recording_pd); } } // "out" must be at least inp->len + MAX_PACKET_HEADER_LEN bytes static unsigned int fake_ip_header(unsigned char *out, struct packet_stream *stream, const str *inp) { endpoint_t *src_endpoint = &stream->advertised_endpoint; endpoint_t *dst_endpoint = &stream->selected_sfd->socket.local; unsigned int hdr_len = endpoint_packet_header(out, src_endpoint, dst_endpoint, inp->len); assert(hdr_len <= MAX_PACKET_HEADER_LEN); // payload memcpy(out + hdr_len, inp->s, inp->len); return hdr_len + inp->len; } static void pcap_eth_header(unsigned char *pkt, struct packet_stream *stream) { memset(pkt, 0, 14); uint16_t *hdr16 = (void *) pkt; hdr16[6] = htons(stream->selected_sfd->socket.local.address.family->ethertype); } /** * Write out a PCAP packet with payload string. * A fair amount extraneous of packet data is spoofed. */ static void stream_pcap_dump(pcap_dumper_t *pdumper, struct packet_stream *stream, const str *s) { if (!pdumper) return; unsigned char pkt[s->len + MAX_PACKET_HEADER_LEN + pcap_format->headerlen]; unsigned int pkt_len = fake_ip_header(pkt + pcap_format->headerlen, stream, s) + pcap_format->headerlen; if (pcap_format->header) pcap_format->header(pkt, stream); // Set up PCAP packet header struct pcap_pkthdr header; ZERO(header); header.ts = rtpe_now; header.caplen = pkt_len; header.len = pkt_len; // Write the packet to the PCAP file // Casting quiets compiler warning. pcap_dump((unsigned char *)pdumper, &header, pkt); } static void dump_packet_pcap(struct recording *recording, struct packet_stream *stream, const str *s) { mutex_lock(&recording->u.pcap.recording_lock); stream_pcap_dump(recording->u.pcap.recording_pdumper, stream, s); recording->u.pcap.packet_num++; mutex_unlock(&recording->u.pcap.recording_lock); } static void finish_pcap(struct call *call) { pcap_recording_finish_file(call->recording); pcap_meta_finish_file(call); } static void response_pcap(struct recording *recording, bencode_item_t *output) { if (!recording->u.pcap.recording_path) return; bencode_item_t *recordings = bencode_dictionary_add_list(output, "recordings"); bencode_list_add_string(recordings, recording->u.pcap.recording_path); } void recording_finish(struct call *call) { if (!call || !call->recording) return; struct recording *recording = call->recording; _rm(finish, call); free(recording->meta_prefix); free(recording->escaped_callid); free(recording->meta_filepath); g_slice_free1(sizeof(*(recording)), recording); call->recording = NULL; } static int open_proc_meta_file(struct recording *recording) { int fd; fd = open(recording->meta_filepath, O_WRONLY | O_APPEND | O_CREAT, 0666); if (fd == -1) { ilog(LOG_ERR, "Failed to open recording metadata file '%s' for writing: %s", recording->meta_filepath, strerror(errno)); return -1; } return fd; } static int vappend_meta_chunk_iov(struct recording *recording, struct iovec *in_iov, int iovcnt, unsigned int str_len, const char *label_fmt, va_list ap) { int fd = open_proc_meta_file(recording); if (fd == -1) return -1; char label[128]; int lablen = vsnprintf(label, sizeof(label), label_fmt, ap); char infix[128]; int inflen = snprintf(infix, sizeof(infix), "\n%u:\n", str_len); // use writev for an atomic write struct iovec iov[iovcnt + 3]; iov[0].iov_base = label; iov[0].iov_len = lablen; iov[1].iov_base = infix; iov[1].iov_len = inflen; memcpy(&iov[2], in_iov, iovcnt * sizeof(*iov)); iov[iovcnt + 2].iov_base = "\n\n"; iov[iovcnt + 2].iov_len = 2; if (writev(fd, iov, iovcnt + 3) != (str_len + lablen + inflen + 2)) ilog(LOG_WARN, "writev return value incorrect"); close(fd); // this triggers the inotify return 0; } static int append_meta_chunk_iov(struct recording *recording, struct iovec *iov, int iovcnt, unsigned int str_len, const char *label_fmt, ...) __attribute__((format(printf,5,6))); static int append_meta_chunk_iov(struct recording *recording, struct iovec *iov, int iovcnt, unsigned int str_len, const char *label_fmt, ...) { va_list ap; va_start(ap, label_fmt); int ret = vappend_meta_chunk_iov(recording, iov, iovcnt, str_len, label_fmt, ap); va_end(ap); return ret; } static int append_meta_chunk(struct recording *recording, const char *buf, unsigned int buflen, const char *label_fmt, ...) __attribute__((format(printf,4,5))); static int append_meta_chunk(struct recording *recording, const char *buf, unsigned int buflen, const char *label_fmt, ...) { struct iovec iov; iov.iov_base = (void *) buf; iov.iov_len = buflen; va_list ap; va_start(ap, label_fmt); int ret = vappend_meta_chunk_iov(recording, &iov, 1, buflen, label_fmt, ap); va_end(ap); return ret; } #define append_meta_chunk_str(r, str, f...) append_meta_chunk(r, (str)->s, (str)->len, f) #define append_meta_chunk_s(r, str, f...) append_meta_chunk(r, (str), strlen(str), f) static void proc_init(struct call *call) { struct recording *recording = call->recording; recording->u.proc.call_idx = UNINIT_IDX; if (!kernel.is_open) { ilog(LOG_WARN, "Call recording through /proc interface requested, but kernel table not open"); return; } recording->u.proc.call_idx = kernel_add_call(recording->meta_prefix); if (recording->u.proc.call_idx == UNINIT_IDX) { ilog(LOG_ERR, "Failed to add call to kernel recording interface: %s", strerror(errno)); return; } ilog(LOG_DEBUG, "kernel call idx is %u", recording->u.proc.call_idx); recording->meta_filepath = file_path_str(recording->meta_prefix, "/", ".meta"); unlink(recording->meta_filepath); // start fresh XXX good idea? append_meta_chunk_str(recording, &call->callid, "CALL-ID"); append_meta_chunk_s(recording, recording->meta_prefix, "PARENT"); if (recording->metadata.len) recording_meta_chunk(recording, "METADATA", &recording->metadata); } static void sdp_before_proc(struct recording *recording, const str *sdp, struct call_monologue *ml, enum call_opmode opmode) { append_meta_chunk_str(recording, &ml->tag, "TAG %u", ml->unique_id); append_meta_chunk_str(recording, sdp, "SDP from %u before %s", ml->unique_id, get_opmode_text(opmode)); } static void sdp_after_proc(struct recording *recording, struct iovec *sdp_iov, int iovcnt, unsigned int str_len, struct call_monologue *ml, enum call_opmode opmode) { append_meta_chunk_iov(recording, sdp_iov, iovcnt, str_len, "SDP from %u after %s", ml->unique_id, get_opmode_text(opmode)); } static void finish_proc(struct call *call) { struct recording *recording = call->recording; if (!kernel.is_open) return; if (recording->u.proc.call_idx != UNINIT_IDX) kernel_del_call(recording->u.proc.call_idx); unlink(recording->meta_filepath); } static void init_stream_proc(struct packet_stream *stream) { stream->recording.u.proc.stream_idx = UNINIT_IDX; } static void setup_stream_proc(struct packet_stream *stream) { struct call_media *media = stream->media; struct call_monologue *ml = media->monologue; struct call *call = stream->call; struct recording *recording = call->recording; char buf[128]; int len; if (!recording) return; if (!kernel.is_open) return; if (stream->recording.u.proc.stream_idx != UNINIT_IDX) return; len = snprintf(buf, sizeof(buf), "TAG %u MEDIA %u TAG-MEDIA %u COMPONENT %u FLAGS %u", ml->unique_id, media->unique_id, media->index, stream->component, stream->ps_flags); append_meta_chunk(recording, buf, len, "STREAM %u details", stream->unique_id); len = snprintf(buf, sizeof(buf), "tag-%u-media-%u-component-%u-%s-id-%u", ml->unique_id, media->index, stream->component, (PS_ISSET(stream, RTCP) && !PS_ISSET(stream, RTP)) ? "RTCP" : "RTP", stream->unique_id); stream->recording.u.proc.stream_idx = kernel_add_intercept_stream(recording->u.proc.call_idx, buf); if (stream->recording.u.proc.stream_idx == UNINIT_IDX) { ilog(LOG_ERR, "Failed to add stream to kernel recording interface: %s", strerror(errno)); return; } ilog(LOG_DEBUG, "kernel stream idx is %u", stream->recording.u.proc.stream_idx); append_meta_chunk(recording, buf, len, "STREAM %u interface", stream->unique_id); } static void setup_media_proc(struct call_media *media) { struct call *call = media->call; struct recording *recording = call->recording; if (!recording) return; GList *pltypes = g_hash_table_get_values(media->codecs); for (GList *l = pltypes; l; l = l->next) { struct rtp_payload_type *pt = l->data; append_meta_chunk(recording, pt->encoding_with_params.s, pt->encoding_with_params.len, "MEDIA %u PAYLOAD TYPE %u", media->unique_id, pt->payload_type); } g_list_free(pltypes); } static void dump_packet_proc(struct recording *recording, struct packet_stream *stream, const str *s) { if (stream->recording.u.proc.stream_idx == UNINIT_IDX) return; struct rtpengine_message *remsg; unsigned char pkt[sizeof(*remsg) + s->len + MAX_PACKET_HEADER_LEN]; remsg = (void *) pkt; ZERO(*remsg); remsg->cmd = REMG_PACKET; //remsg->u.packet.call_idx = stream->call->recording->u.proc.call_idx; // unused remsg->u.packet.stream_idx = stream->recording.u.proc.stream_idx; unsigned int pkt_len = fake_ip_header(remsg->data, stream, s); pkt_len += sizeof(*remsg); int ret = write(kernel.fd, pkt, pkt_len); if (ret < 0) ilog(LOG_ERR, "Failed to submit packet to kernel intercepted stream: %s", strerror(errno)); } static void kernel_info_proc(struct packet_stream *stream, struct rtpengine_target_info *reti) { if (!stream->call->recording) return; if (stream->recording.u.proc.stream_idx == UNINIT_IDX) return; ilog(LOG_DEBUG, "enabling kernel intercept with stream idx %u", stream->recording.u.proc.stream_idx); reti->do_intercept = 1; reti->intercept_stream_idx = stream->recording.u.proc.stream_idx; } static void meta_chunk_proc(struct recording *recording, const char *label, const str *data) { append_meta_chunk_str(recording, data, "%s", label); }