/** Process stats for a single interval * */ static void rs_stats_process(void *ctx) { size_t i; size_t rs_codes_len = (sizeof(rs_useful_codes) / sizeof(*rs_useful_codes)); fr_pcap_t *in_p; rs_update_t *this = ctx; rs_stats_t *stats = this->stats; rs_t *conf = this->conf; struct timeval now; gettimeofday(&now, NULL); stats->intervals++; /* * Verify that none of the pcap handles have dropped packets. */ INFO("Interface capture rate:"); for (in_p = this->in; in_p; in_p = in_p->next) { if (rs_check_pcap_drop(in_p, conf->stats.interval) < 0) { ERROR("Muting stats for the next %i seconds", conf->stats.timeout); stats->quiet = now; stats->quiet.tv_sec += conf->stats.timeout; goto clear; } } if ((stats->quiet.tv_sec + (stats->quiet.tv_usec / 1000000.0)) - (now.tv_sec + (now.tv_usec / 1000000.0)) > 0) { INFO("Stats still muted because of previous error"); goto clear; } /* * Latency stats need a bit more work to calculate the CMA. * * No further work is required for codes. */ for (i = 0; i < rs_codes_len; i++) { rs_stats_process_latency(&stats->exchange[rs_useful_codes[i]], rs_useful_codes[i], stats->intervals); // rs_stats_process_latency(&stats->forward[rs_useful_codes[i]]); } #ifdef HAVE_COLLECTDC_H /* * Update stats in collectd using the complex structures we * initialised earlier. */ if (conf->stats.out == RS_STATS_OUT_COLLECTD) { rs_stats_collectd_do_stats(conf, conf->stats.tmpl, &now); } #endif clear: /* * Rinse and repeat... */ for (i = 0; i < rs_codes_len; i++) { memset(&stats->exchange[rs_useful_codes[i]].interval, 0, sizeof(stats->exchange[rs_useful_codes[i]].interval)); } memset(&stats->gauge.type, 0, sizeof(stats->gauge.type)); { now.tv_sec += conf->stats.interval; fr_event_insert(this->list, rs_stats_process, ctx, &now, NULL); } }
int main(int argc, char *argv[]) { rs_t *conf; fr_pcap_t *in = NULL, *in_p; fr_pcap_t **in_head = ∈ fr_pcap_t *out = NULL; int ret = 1; /* Exit status */ int limit = -1; /* How many packets to sniff */ char errbuf[PCAP_ERRBUF_SIZE]; /* Error buffer */ int port = 1812; char buffer[1024]; int opt; FR_TOKEN parsecode; char const *radius_dir = RADIUS_DIR; rs_stats_t stats; fr_debug_flag = 2; log_dst = stdout; talloc_set_log_stderr(); conf = talloc_zero(NULL, rs_t); if (!fr_assert(conf)) { exit (1); } /* * We don't really want probes taking down machines */ #ifdef HAVE_TALLOC_SET_MEMLIMIT talloc_set_memlimit(conf, 52428800); /* 50 MB */ #endif /* * Get options */ while ((opt = getopt(argc, argv, "c:d:DFf:hi:I:p:qr:s:Svw:xXW:P:O:")) != EOF) { switch (opt) { case 'c': limit = atoi(optarg); if (limit <= 0) { fprintf(stderr, "radsniff: Invalid number of packets \"%s\"", optarg); exit(1); } break; case 'd': radius_dir = optarg; break; case 'D': { pcap_if_t *all_devices = NULL; pcap_if_t *dev_p; if (pcap_findalldevs(&all_devices, errbuf) < 0) { ERROR("Error getting available capture devices: %s", errbuf); goto finish; } int i = 1; for (dev_p = all_devices; dev_p; dev_p = dev_p->next) { INFO("%i.%s", i++, dev_p->name); } ret = 0; goto finish; } case 'F': conf->from_stdin = true; conf->to_stdout = true; break; case 'f': conf->pcap_filter = optarg; break; case 'h': usage(0); break; case 'i': *in_head = fr_pcap_init(conf, optarg, PCAP_INTERFACE_IN); if (!*in_head) { goto finish; } in_head = &(*in_head)->next; conf->from_dev = true; break; case 'I': *in_head = fr_pcap_init(conf, optarg, PCAP_FILE_IN); if (!*in_head) { goto finish; } in_head = &(*in_head)->next; conf->from_file = true; break; case 'p': port = atoi(optarg); break; case 'q': if (fr_debug_flag > 0) { fr_debug_flag--; } break; case 'r': conf->radius_filter = optarg; break; case 's': conf->radius_secret = optarg; break; case 'S': conf->do_sort = true; break; case 'v': #ifdef HAVE_COLLECTDC_H INFO("%s, %s, collectdclient version %s", radsniff_version, pcap_lib_version(), lcc_version_string()); #else INFO("%s %s", radsniff_version, pcap_lib_version()); #endif exit(0); break; case 'w': out = fr_pcap_init(conf, optarg, PCAP_FILE_OUT); conf->to_file = true; break; case 'x': case 'X': fr_debug_flag++; break; case 'W': conf->stats.interval = atoi(optarg); if (conf->stats.interval <= 0) { ERROR("Stats interval must be > 0"); usage(64); } break; case 'T': conf->stats.timeout = atoi(optarg); if (conf->stats.timeout <= 0) { ERROR("Timeout value must be > 0"); usage(64); } break; #ifdef HAVE_COLLECTDC_H case 'P': conf->stats.prefix = optarg; break; case 'O': conf->stats.collectd = optarg; conf->stats.out = RS_STATS_OUT_COLLECTD; break; #endif default: usage(64); } } /* What's the point in specifying -F ?! */ if (conf->from_stdin && conf->from_file && conf->to_file) { usage(64); } /* Can't read from both... */ if (conf->from_file && conf->from_dev) { usage(64); } /* Reading from file overrides stdin */ if (conf->from_stdin && (conf->from_file || conf->from_dev)) { conf->from_stdin = false; } /* Writing to file overrides stdout */ if (conf->to_file && conf->to_stdout) { conf->to_stdout = false; } if (conf->to_stdout) { out = fr_pcap_init(conf, "stdout", PCAP_STDIO_OUT); if (!out) { goto finish; } } if (conf->from_stdin) { *in_head = fr_pcap_init(conf, "stdin", PCAP_STDIO_IN); if (!*in_head) { goto finish; } in_head = &(*in_head)->next; } if (!conf->radius_secret) { conf->radius_secret = RS_DEFAULT_SECRET; } if (conf->stats.interval && !conf->stats.out) { conf->stats.out = RS_STATS_OUT_STDIO; } if (conf->stats.timeout == 0) { conf->stats.timeout = RS_DEFAULT_TIMEOUT; } /* * If were writing pcap data stdout we *really* don't want to send * logging there as well. */ log_dst = conf->to_stdout ? stderr : stdout; #if !defined(HAVE_PCAP_FOPEN_OFFLINE) || !defined(HAVE_PCAP_DUMP_FOPEN) if (conf->from_stdin || conf->to_stdout) { ERROR("PCAP streams not supported"); goto finish; } #endif if (!conf->pcap_filter) { snprintf(buffer, sizeof(buffer), "udp port %d or %d or %d", port, port + 1, 3799); conf->pcap_filter = buffer; } if (dict_init(radius_dir, RADIUS_DICTIONARY) < 0) { fr_perror("radsniff"); ret = 64; goto finish; } fr_strerror(); /* Clear out any non-fatal errors */ if (conf->radius_filter) { parsecode = userparse(NULL, conf->radius_filter, &filter_vps); if (parsecode == T_OP_INVALID) { ERROR("Invalid RADIUS filter \"%s\" (%s)", conf->radius_filter, fr_strerror()); ret = 64; goto finish; } if (!filter_vps) { ERROR("Empty RADIUS filter \"%s\"", conf->radius_filter); ret = 64; goto finish; } filter_tree = rbtree_create((rbcmp) fr_packet_cmp, _rb_rad_free, 0); if (!filter_tree) { ERROR("Failed creating filter tree"); ret = 64; goto finish; } } /* * Setup the request tree */ request_tree = rbtree_create((rbcmp) fr_packet_cmp, _rb_rad_free, 0); if (!request_tree) { ERROR("Failed creating request tree"); goto finish; } /* * Allocate a null packet for decrypting attributes in CoA requests */ nullpacket = rad_alloc(conf, 0); if (!nullpacket) { ERROR("Out of memory"); goto finish; } /* * Get the default capture device */ if (!conf->from_stdin && !conf->from_file && !conf->from_dev) { pcap_if_t *all_devices; /* List of all devices libpcap can listen on */ pcap_if_t *dev_p; if (pcap_findalldevs(&all_devices, errbuf) < 0) { ERROR("Error getting available capture devices: %s", errbuf); goto finish; } if (!all_devices) { ERROR("No capture files specified and no live interfaces available"); ret = 64; goto finish; } for (dev_p = all_devices; dev_p; dev_p = dev_p->next) { /* Don't use the any devices, it's horribly broken */ if (!strcmp(dev_p->name, "any")) continue; *in_head = fr_pcap_init(conf, dev_p->name, PCAP_INTERFACE_IN); in_head = &(*in_head)->next; } conf->from_auto = true; conf->from_dev = true; INFO("Defaulting to capture on all interfaces"); } /* * Print captures values which will be used */ if (fr_debug_flag > 2) { DEBUG1("Sniffing with options:"); if (conf->from_dev) { char *buff = fr_pcap_device_names(conf, in, ' '); DEBUG1(" Device(s) : [%s]", buff); talloc_free(buff); } if (conf->to_file || conf->to_stdout) { DEBUG1(" Writing to : [%s]", out->name); } if (limit > 0) { DEBUG1(" Capture limit (packets) : [%d]", limit); } DEBUG1(" PCAP filter : [%s]", conf->pcap_filter); DEBUG1(" RADIUS secret : [%s]", conf->radius_secret); if (filter_vps){ DEBUG1(" RADIUS filter :"); vp_printlist(log_dst, filter_vps); } } /* * Open our interface to collectd */ #ifdef HAVE_COLLECTDC_H if (conf->stats.out == RS_STATS_OUT_COLLECTD) { size_t i; rs_stats_tmpl_t *tmpl, **next; if (rs_stats_collectd_open(conf) < 0) { exit(1); } next = &conf->stats.tmpl; for (i = 0; i < (sizeof(rs_useful_codes) / sizeof(*rs_useful_codes)); i++) { tmpl = rs_stats_collectd_init_latency(conf, next, conf, "radius_pkt_ex", &stats.exchange[rs_useful_codes[i]], rs_useful_codes[i]); if (!tmpl) { goto tmpl_error; } next = &(tmpl->next); tmpl = rs_stats_collectd_init_counter(conf, next, conf, "radius_pkt", &stats.gauge.type[rs_useful_codes[i]], rs_useful_codes[i]); if (!tmpl) { tmpl_error: ERROR("Error allocating memory for stats template"); goto finish; } next = &(tmpl->next); } } #endif /* * This actually opens the capture interfaces/files (we just allocated the memory earlier) */ { fr_pcap_t *prev = NULL; for (in_p = in; in_p; in_p = in_p->next) { if (fr_pcap_open(in_p) < 0) { if (!conf->from_auto) { ERROR("Failed opening pcap handle for %s", in_p->name); goto finish; } DEBUG("Failed opening pcap handle: %s", fr_strerror()); /* Unlink it from the list */ if (prev) { prev->next = in_p->next; talloc_free(in_p); in_p = prev; } else { in = in_p->next; talloc_free(in_p); in_p = in; } goto next; } if (conf->pcap_filter) { if (fr_pcap_apply_filter(in_p, conf->pcap_filter) < 0) { ERROR("Failed applying filter"); goto finish; } } next: prev = in_p; } } /* * Open our output interface (if we have one); */ if (out) { if (fr_pcap_open(out) < 0) { ERROR("Failed opening pcap output"); goto finish; } } /* * Setup and enter the main event loop. Who needs libev when you can roll your own... */ { struct timeval now; fr_event_list_t *events; rs_update_t update; char *buff; memset(&stats, 0, sizeof(stats)); memset(&update, 0, sizeof(update)); events = fr_event_list_create(conf, _rs_event_status); if (!events) { ERROR(); goto finish; } for (in_p = in; in_p; in_p = in_p->next) { rs_event_t *event; event = talloc_zero(events, rs_event_t); event->conf = conf; event->in = in_p; event->out = out; event->stats = &stats; if (!fr_event_fd_insert(events, 0, in_p->fd, rs_got_packet, event)) { ERROR("Failed inserting file descriptor"); goto finish; } } buff = fr_pcap_device_names(conf, in, ' '); INFO("Sniffing on (%s)", buff); talloc_free(buff); gettimeofday(&now, NULL); start_pcap = now; /* * Insert our stats processor */ if (conf->stats.interval) { update.list = events; update.conf = conf; update.stats = &stats; update.in = in; now.tv_sec += conf->stats.interval; now.tv_usec = 0; fr_event_insert(events, rs_stats_process, (void *) &update, &now, NULL); } ret = fr_event_loop(events); /* Enter the main event loop */ } INFO("Done sniffing"); finish: if (filter_tree) { rbtree_free(filter_tree); } INFO("Exiting..."); /* * Free all the things! This also closes all the sockets and file descriptors */ talloc_free(conf); return ret; }
static void rs_packet_process(uint64_t count, rs_event_t *event, struct pcap_pkthdr const *header, uint8_t const *data) { rs_stats_t *stats = event->stats; struct timeval elapsed; struct timeval latency; /* * Pointers into the packet data we just received */ size_t len; uint8_t const *p = data; struct ip_header const *ip = NULL; /* The IP header */ struct ip_header6 const *ip6 = NULL; /* The IPv6 header */ struct udp_header const *udp; /* The UDP header */ uint8_t version; /* IP header version */ bool response; /* Was it a response code */ decode_fail_t reason; /* Why we failed decoding the packet */ static uint64_t captured = 0; RADIUS_PACKET *current; /* Current packet were processing */ rs_request_t *original; if (!start_pcap.tv_sec) { start_pcap = header->ts; } if (header->caplen <= 5) { INFO("Packet too small, captured %i bytes", header->caplen); return; } /* * Loopback header */ if ((p[0] == 2) && (p[1] == 0) && (p[2] == 0) && (p[3] == 0)) { p += 4; /* * Ethernet header */ } else { p += sizeof(struct ethernet_header); } version = (p[0] & 0xf0) >> 4; switch (version) { case 4: ip = (struct ip_header const *)p; len = (0x0f & ip->ip_vhl) * 4; /* ip_hl specifies length in 32bit words */ p += len; break; case 6: ip6 = (struct ip_header6 const *)p; p += sizeof(struct ip_header6); break; default: DEBUG("IP version invalid %i", version); return; } /* * End of variable length bits, do basic check now to see if packet looks long enough */ len = (p - data) + sizeof(struct udp_header) + (sizeof(radius_packet_t) - 1); /* length value */ if (len > header->caplen) { DEBUG("Packet too small, we require at least %zu bytes, captured %i bytes", (size_t) len, header->caplen); return; } udp = (struct udp_header const *)p; p += sizeof(struct udp_header); /* * With artificial talloc memory limits there's a good chance we can * recover once some requests timeout, so make an effort to deal * with allocation failures gracefully. */ current = rad_alloc(conf, 0); if (!current) { ERROR("Failed allocating memory to hold decoded packet"); rs_tv_add_ms(&header->ts, conf->stats.timeout, &stats->quiet); return; } current->timestamp = header->ts; current->data_len = header->caplen - (p - data); memcpy(¤t->data, &p, sizeof(current->data)); /* * Populate IP/UDP fields from PCAP data */ if (ip) { current->src_ipaddr.af = AF_INET; current->src_ipaddr.ipaddr.ip4addr.s_addr = ip->ip_src.s_addr; current->dst_ipaddr.af = AF_INET; current->dst_ipaddr.ipaddr.ip4addr.s_addr = ip->ip_dst.s_addr; } else { current->src_ipaddr.af = AF_INET6; memcpy(¤t->src_ipaddr.ipaddr.ip6addr.s6_addr, &ip6->ip_src.s6_addr, sizeof(current->src_ipaddr.ipaddr.ip6addr.s6_addr)); current->dst_ipaddr.af = AF_INET6; memcpy(¤t->dst_ipaddr.ipaddr.ip6addr.s6_addr, &ip6->ip_dst.s6_addr, sizeof(current->dst_ipaddr.ipaddr.ip6addr.s6_addr)); } current->src_port = ntohs(udp->udp_sport); current->dst_port = ntohs(udp->udp_dport); if (!rad_packet_ok(current, 0, &reason)) { RIDEBUG("(%" PRIu64 ") ** %s **", count, fr_strerror()); RIDEBUG("(%" PRIu64 ") %s Id %i %s:%s:%d -> %s:%d\t+%u.%03u", count, fr_packet_codes[current->code], current->id, event->in->name, fr_inet_ntop(current->src_ipaddr.af, ¤t->src_ipaddr.ipaddr), current->src_port, fr_inet_ntop(current->dst_ipaddr.af, ¤t->dst_ipaddr.ipaddr), current->dst_port, (unsigned int) elapsed.tv_sec, ((unsigned int) elapsed.tv_usec / 1000)); rad_free(¤t); return; } switch (current->code) { case PW_CODE_ACCOUNTING_RESPONSE: case PW_CODE_AUTHENTICATION_REJECT: case PW_CODE_AUTHENTICATION_ACK: case PW_CODE_COA_NAK: case PW_CODE_COA_ACK: case PW_CODE_DISCONNECT_NAK: case PW_CODE_DISCONNECT_ACK: case PW_CODE_STATUS_CLIENT: { rs_request_t search; struct timeval when; rs_tv_add_ms(&header->ts, conf->stats.timeout, &when); /* look for a matching request and use it for decoding */ search.packet = current; original = rbtree_finddata(request_tree, &search); /* * Only decode attributes if we want to print them or filter on them * rad_packet_ok does checks to verify the packet is actually valid. */ if (filter_vps || conf->print_packet) { if (rad_decode(current, original ? original->packet : NULL, conf->radius_secret) != 0) { rad_free(¤t); fr_perror("decode"); return; } } /* * Check if we've managed to link it to a request */ if (original) { /* * Is this a retransmit? */ if (!original->linked) { original->stats_rsp = &stats->exchange[current->code]; } else { RDEBUG("(%" PRIu64 ") ** RETRANSMISSION **", count); original->rt_rsp++; rad_free(&original->linked); fr_event_delete(event->list, &original->event); } original->linked = talloc_steal(original, current); /* * Some RADIUS servers and proxy servers may not cache * Accounting-Responses (and possibly other code), * and may immediately re-use a RADIUS packet src * port/id combination on receipt of a response. */ if (conf->dequeue[current->code]) { fr_event_delete(event->list, &original->event); rbtree_deletebydata(request_tree, original); } else { if (!fr_event_insert(event->list, rs_packet_cleanup, original, &when, &original->event)) { ERROR("Failed inserting new event"); /* * Delete the original request/event, it's no longer valid * for statistics. */ original->forced_cleanup = true; fr_event_delete(event->list, &original->event); rbtree_deletebydata(request_tree, original); return; } } /* * No request seen, or request was dropped by attribute filter */ } else { /* * If filter_vps are set assume the original request was dropped, * the alternative is maintaining another 'filter', but that adds * complexity, reduces max capture rate, and is generally a PITA. */ if (filter_vps) { rad_free(¤t); RDEBUG2("(%" PRIu64 ") Dropped by attribute filter", count); return; } RDEBUG("(%" PRIu64 ") ** UNLINKED **", count); stats->exchange[current->code].interval.unlinked_total++; } response = true; } break; case PW_CODE_ACCOUNTING_REQUEST: case PW_CODE_AUTHENTICATION_REQUEST: case PW_CODE_COA_REQUEST: case PW_CODE_DISCONNECT_REQUEST: case PW_CODE_STATUS_SERVER: { rs_request_t search; struct timeval when; /* * Only decode attributes if we want to print them or filter on them * rad_packet_ok does checks to verify the packet is actually valid. */ if (filter_vps || conf->print_packet) { if (rad_decode(current, NULL, conf->radius_secret) != 0) { rad_free(¤t); fr_perror("decode"); return; } } /* * Now verify the packet passes the attribute filter */ if (filter_vps && !pairvalidate_relaxed(filter_vps, current->vps)) { rad_free(¤t); RDEBUG2("(%" PRIu64 ") Dropped by attribute filter", count); return; } /* * save the request for later matching */ search.packet = rad_alloc_reply(conf, current); if (!search.packet) { ERROR("Failed allocating memory to hold expected reply"); rs_tv_add_ms(&header->ts, conf->stats.timeout, &stats->quiet); rad_free(¤t); return; } search.packet->code = current->code; rs_tv_add_ms(&header->ts, conf->stats.timeout, &when); original = rbtree_finddata(request_tree, &search); /* * Upstream device re-used src/dst ip/port id without waiting * for the timeout period to expire, or a response. */ if (original && memcmp(original->packet->vector, current->vector, sizeof(original->packet->vector) != 0)) { RDEBUG2("(%" PRIu64 ") ** PREMATURE ID RE-USE **", count); stats->exchange[current->code].interval.reused_total++; original->forced_cleanup = true; fr_event_delete(event->list, &original->event); rbtree_deletebydata(request_tree, original); original = NULL; } if (original) { RDEBUG("(%" PRIu64 ") ** RETRANSMISSION **", count); original->rt_req++; rad_free(&original->packet); original->packet = talloc_steal(original, search.packet); /* We may of seen the response, but it may of been lost upstream */ rad_free(&original->linked); fr_event_delete(event->list, &original->event); } else { original = talloc_zero(conf, rs_request_t); talloc_set_destructor(original, _request_free); original->id = count; original->in = event->in; original->stats_req = &stats->exchange[current->code]; original->packet = talloc_steal(original, search.packet); rbtree_insert(request_tree, original); } /* update the timestamp in either case */ original->packet->timestamp = header->ts; if (!fr_event_insert(event->list, rs_packet_cleanup, original, &when, &original->event)) { ERROR("Failed inserting new event"); rbtree_deletebydata(request_tree, original); return; } response = false; } break; default: RDEBUG("** Unsupported code %i **", current->code); rad_free(¤t); return; } if (event->out) { pcap_dump((void *) (event->out->dumper), header, data); } rs_tv_sub(&header->ts, &start_pcap, &elapsed); /* * Increase received count */ stats->exchange[current->code].interval.received_total++; /* * It's a linked response */ if (original && original->linked) { rs_tv_sub(¤t->timestamp, &original->packet->timestamp, &latency); /* * Update stats for both the request and response types. * * This isn't useful for things like Access-Requests, but will be useful for * CoA and Disconnect Messages, as we get the average latency across both * response types. * * It also justifies allocating 255 instances rs_latency_t. */ rs_stats_update_latency(&stats->exchange[current->code], &latency); rs_stats_update_latency(&stats->exchange[original->packet->code], &latency); /* * Print info about the request/response. */ RIDEBUG("(%" PRIu64 ") %s Id %i %s:%s:%d %s %s:%d\t+%u.%03u\t+%u.%03u", count, fr_packet_codes[current->code], current->id, event->in->name, fr_inet_ntop(current->src_ipaddr.af, ¤t->src_ipaddr.ipaddr), current->src_port, response ? "<-" : "->", fr_inet_ntop(current->dst_ipaddr.af, ¤t->dst_ipaddr.ipaddr), current->dst_port, (unsigned int) elapsed.tv_sec, ((unsigned int) elapsed.tv_usec / 1000), (unsigned int) latency.tv_sec, ((unsigned int) latency.tv_usec / 1000)); /* * It's the original request */ } else { /* * Print info about the request */ RIDEBUG("(%" PRIu64 ") %s Id %i %s:%s:%d %s %s:%d\t+%u.%03u", count, fr_packet_codes[current->code], current->id, event->in->name, fr_inet_ntop(current->src_ipaddr.af, ¤t->src_ipaddr.ipaddr), current->src_port, response ? "<-" : "->", fr_inet_ntop(current->dst_ipaddr.af, ¤t->dst_ipaddr.ipaddr), current->dst_port, (unsigned int) elapsed.tv_sec, ((unsigned int) elapsed.tv_usec / 1000)); } if (conf->print_packet && (fr_debug_flag > 1) && current->vps) { pairsort(¤t->vps, true); vp_printlist(log_dst, current->vps); pairfree(¤t->vps); } if (!conf->to_stdout && (fr_debug_flag > 4)) { rad_print_hex(current); } fflush(log_dst); /* * If it's a request, a duplicate of the packet will of already been stored. * If it's a unlinked response, we need to free it explicitly, as it will * not be done by the event queue. */ if (!response || !original) { rad_free(¤t); } captured++; /* * We've hit our capture limit, break out of the event loop */ if ((conf->limit > 0) && (captured >= conf->limit)) { INFO("Captured %" PRIu64 " packets, exiting...", captured); fr_event_loop_exit(events, 1); } }