/* * Copyright (c) 2000 Paul Herman * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $Id: tcpstat.c,v 1.61 2002/07/27 00:42:05 pherman Exp $ */ #include "tcpstat.h" #define KIL (1024) #define MEG (1024*1024) #define GIG (1024*1024*1024) #define Double double /* FP accuracy */ #define DEFAULT_INTERVAL 5 #define DEFAULT_FORMAT "Time:%S\tn=%n\tavg=%a\tstddev=%d\tbps=%b\n" #define SETUID_WARNING \ "\n \ \n \ \n*** Warning!! tcpstat is running setuid! This is not recommended. \ \n*** I will disable the ability to write to arbitrary file descriptors. \ \n \ " enum { ACTION_INTERVAL, ACTION_BANDWIDTH, ACTION_ACCOUNT, ACTION_LOAD }; /** ** GLOBALS - mostly options to be set on the command line **/ char action = ACTION_INTERVAL; char *filterexpr = NULL; char *outputformat = NULL; Double interval; Double relative_seconds = 0.0; Double bandwidth = 0.0; Double peakbandwidth = 0.0; int get_tcp_flags = 0; /* flags to pass to get_dump_data */ int flush_stdout = 0; /* flushes all fds when set */ int smart_bandwidth = 0; /* when set, use smart bandwidth mode */ int empty_intervals = 1; /* by default print empty intervals */ int is_setuid = 0; /* refuse to use fdescs when setuid */ int print_immediately = 0; /* flag set when SIGUSR1 received */ Double capture_seconds = -1.0; /* -1.0 means infinite */ /* * This is the main structure that keeps track of all the statistics */ typedef struct statistics { struct { Double ts_bigbang; Double total_time; Double total_time_over_bps; Double total_bytes; } global; struct { int packets; int arp; int ip; #ifdef INET6 int ip6; #endif int tcp; int udp; int icmp; } count; int max_packetsize; /* maximum packet size over the interval */ int min_packetsize; /* mininum packet size over the interval */ Double ts_interval_begin; /* timestamp of the first packet in the interval */ Double ts_now; /* timestamp of last packet received */ Double sum; /* sum of of the packet sizes */ Double sum2; /* sum of the squares of the packet sizes */ Double smartb_sum; /* bytes carried over from the last interval */ Double load; /* bps averaged over 1 minute */ } statistics; /* * Handy routine to return a string of only numbers. */ char *my_get_nr(char *f) { static char s[BUF_SIZ]; char *p; int i = 0; if (f == NULL) return NULL; p = f; while (*p && isdigit(*p) ) { i++; p++; } i = (i > (BUF_SIZ-1)) ? BUF_SIZ - 1 : i; /* min(BUF_SIZ,i) */ *s = 0; strncat(s, f, i); return s; } /* * Handy routine to print bytes in a human readable format. */ char *wr_bytes(Double b) { static char s[BUF_SIZ]; if (0) { snprintf(s, BUF_SIZ, "%12.0f Bytes", b); return s; } if ( b > GIG ) { snprintf(s, BUF_SIZ, "%8.1f GB", b/GIG); return s; } if ( b > MEG ) { snprintf(s, BUF_SIZ, "%8.1f MB", b/MEG); return s; } if ( b > KIL ) { snprintf(s, BUF_SIZ, "%8.1f KB", b/KIL); return s; } snprintf(s, BUF_SIZ, "%8.1f B", b); return s; } void catch_signal(int a) { switch (a) { case SIGUSR1: print_immediately = 1; break; default: break; } } /* * Parses the output format, and displays appropriate statistics */ void show_interval(statistics *s, Double dt) { char *f, *fd_str; Double avg, stddev, t_bar, delta_t; /* Used in the PRINTVAL macro which follows */ char printval_str[BUF_SIZ]; int filedesc = 1; /* Handy macro for writing bytes to arbitrary file descriptors */ #define PRINTVAL(x,y) { \ snprintf(printval_str, BUF_SIZ, (x),(y)); \ write(filedesc, printval_str, strlen(printval_str)); \ } if (action != ACTION_INTERVAL) return; if (s->count.packets == 0) { avg = 0; stddev = 0; } else { avg = s->sum/(Double)s->count.packets; stddev = sqrt(s->sum2/(Double)s->count.packets - avg*avg); } delta_t = (interval == -1)? (s->ts_now - s->ts_interval_begin) : dt; t_bar = s->ts_interval_begin + delta_t/2.0; f = outputformat; while (*f) { if (*f == '%') { switch (*(++f)) { case 'A': PRINTVAL("%d", s->count.arp); break; case 'a': PRINTVAL("%.2f", avg); break; case 'B': PRINTVAL("%.2f", s->sum/delta_t); break; case 'b': PRINTVAL("%.2f", s->sum/delta_t * 8.0); break; case 'C': PRINTVAL("%d", s->count.icmp); break; case 'd': PRINTVAL("%.2f", stddev); break; case 'I': PRINTVAL("%d", s->count.ip); break; case 'l': PRINTVAL("%.2f", (peakbandwidth == 0.0)? 1.0 : s->load/peakbandwidth); break; case 'n': PRINTVAL("%d", s->count.packets); break; case 'M': PRINTVAL("%d", s->max_packetsize); break; case 'm': PRINTVAL("%d", s->min_packetsize); break; case 'N': PRINTVAL("%.0f", s->sum); break; case 'p': PRINTVAL("%.2f", (Double)s->count.packets/delta_t); break; case 'S': PRINTVAL("%d", (int) (t_bar + 0.5 - relative_seconds) ); break; case 's': PRINTVAL("%.6f", t_bar - relative_seconds); break; case 'R': PRINTVAL("%d", (int) (t_bar + 0.5 - (relative_seconds + s->global.ts_bigbang) ) ); break; case 'r': PRINTVAL("%.6f", t_bar - (relative_seconds + s->global.ts_bigbang)); break; case 'T': PRINTVAL("%d", s->count.tcp); break; case 'U': PRINTVAL("%d", s->count.udp); break; #ifdef INET6 case 'V': PRINTVAL("%d", s->count.ip6); break; #endif default: /* Check for file descriptor change */ if (isdigit(*f)) { fd_str = my_get_nr(f); #ifndef NO_FILEDESC if (!is_setuid) { filedesc = strtol(fd_str, NULL, 10); if (filedesc>256) filedesc = 1; } #endif /* NO_FILEDESC */ f += strlen(fd_str) - 1; } else PRINTVAL("%%%c", *f); break; } /* switch */ } /* if % */ else if (*f == '\\') { switch (*(++f)) { /* XXX: This could be written better */ case 'a': PRINTVAL("%s", "\a"); break; case 'b': PRINTVAL("%s", "\b"); break; case 'f': PRINTVAL("%s", "\f"); break; case 'n': PRINTVAL("%s", "\n"); break; case 'r': PRINTVAL("%s", "\r"); break; case 't': PRINTVAL("%s", "\t"); break; case 'v': PRINTVAL("%s", "\v"); break; default: PRINTVAL("%c", *f); break; } } else PRINTVAL("%c", *f); f++; } if (flush_stdout) fflush(NULL); } /* * Stores zeros in the given statistics structure */ void reset_counters(statistics *sp) { sp->count.packets = 0; sp->sum = 0.0; sp->sum2 = 0.0; sp->smartb_sum = 0.0; /* Zero all counters */ bzero(&sp->count, sizeof(sp->count)); sp->max_packetsize = 0; sp->min_packetsize = 0; } /* This function updates all internal parameters * (peakbandwidth, load, etc.) and prints intervals * if neccesary */ int do_the_printing(statistics *sp, Double elapsed, Double ts) { statistics zero; Double x, bps; /* if this is a normal interval, i.e. one where the alarm * timer ran up, just take the theoretical interval length. * Otherwise, our best guess is the time elapsed between two * packets. */ if (!print_immediately && interval > 0.0) elapsed = interval; /* Plain Bandwidth */ bps = (sp->sum / elapsed) * 8.0; if (bps > peakbandwidth) peakbandwidth = bps; /* calculate the bandwidth load, if any */ if (sp->load == -1.0) sp->load = bps; sp->load += (bps - sp->load) * (1.0 - exp(-elapsed/60.0)); /* Smart bandwidth, if any */ sp->sum += sp->smartb_sum; bps = (sp->sum / elapsed) * 8.0; if (bps > bandwidth) sp->global.total_time_over_bps += elapsed; /* show interval and reset stats */ if (empty_intervals || sp->count.packets > 0) show_interval(sp, elapsed); sp->ts_interval_begin += elapsed; sp->global.total_time += elapsed; /* Now, pad the output with zeros, if neccesary */ reset_counters(&zero); zero.global.ts_bigbang = sp->global.ts_bigbang; zero.load = sp->load; for (x = sp->ts_interval_begin + interval; xts_interval_begin += interval; sp->global.total_time += interval; zero.load -= zero.load * (1.0 - exp(-interval/60.0)); if (empty_intervals) show_interval(&zero, interval); } sp->load = zero.load; reset_counters(sp); if (smart_bandwidth && action == ACTION_BANDWIDTH && bps > bandwidth) { /* Transfer the rest of the data over to * the next interval. This doesn't work * for the packet count, but that's * OK in bandwidth mode. */ sp->smartb_sum = (bps - bandwidth) * elapsed / 8.0; } /* Reset signal state */ print_immediately = 0; return 0; } /* * This fuction is called by pcap_loop (not really, but OK.) It adds * a particular packet to the total statistical data. */ void my_hook(packet_data *pd, void **args) { statistics *sp; Double ts, elapsed; sp = (statistics *) args[0]; ts = (Double)pd->timestamp.tv_sec + (Double)pd->timestamp.tv_usec/1.0e6; if (sp->ts_interval_begin == 0.0) { /* initialize stats if called for the first time */ reset_counters(sp); sp->ts_interval_begin = ts; sp->global.ts_bigbang = ts; } /* Before we add this packet to the statistics, * we need to check if we are actually done counting * packets. This should only happen (and be useful) * when reading data from a file. */ if (capture_seconds > 0.0 && ts - sp->global.ts_bigbang > capture_seconds) return; /* After that, we need to see if there is an interval waiting * to be printed. */ elapsed = ts - sp->ts_interval_begin; if ( (elapsed > interval && interval != -1.0) || (print_immediately && elapsed > 0.0) ) { /* We got a packet past the boundry of the last interval, * so update stats, and print out the last interval that * has already expired. There might be a packet handed to * this function at this point, but we can't include it in * the current statistic. */ do_the_printing(sp, elapsed, ts); } sp->ts_now = ts; sp->count.packets++; sp->global.total_bytes += (Double)pd->packet_len; sp->sum += (Double)pd->packet_len; sp->sum2 += (Double)pd->packet_len * (Double)pd->packet_len; /* sp->sum2 = sp->sum * sp->sum; */ if (is_ethernetarp_packet(pd)) (sp->count.arp)++; else { if (is_ip_packet(pd)) (sp->count.ip)++; #ifdef INET6 if (is_ip6_packet(pd)) (sp->count.ip6)++; #endif if (is_ip_tcp_packet(pd)) sp->count.tcp++; if (is_ip_udp_packet(pd)) sp->count.udp++; if (is_ip_icmp_packet(pd)) sp->count.icmp++; } if ((int)pd->packet_len > sp->max_packetsize) sp->max_packetsize = pd->packet_len; if ((int)pd->packet_len < sp->min_packetsize || sp->min_packetsize == 0) sp->min_packetsize = pd->packet_len; } /* * process_file() gets the data, and then displays the statistics */ void process_file(char *fname, u_int unused) { void *argv[2]; statistics stats; Double x; signal(SIGUSR1, catch_signal); reset_counters(&stats); stats.ts_interval_begin = 0.0; stats.load = -1.0; stats.global.ts_bigbang = 0.0; stats.global.total_bytes = 0.0; stats.global.total_time = 0.0; stats.global.total_time_over_bps = 0.0; argv[0] = (void *) &stats; /* Main Loop */ if (get_dump_data(fname, filterexpr, get_tcp_flags, capture_seconds, my_hook, argv)) return; /* The last "interval" still needs to be computed. * Most of the work has been done. Now do... */ x = (interval <= 0.0)? stats.ts_now - stats.global.ts_bigbang : interval; stats.global.total_time += x; do_the_printing(&stats, x, stats.ts_now); switch (action) { case ACTION_INTERVAL: /* If capture_seconds specified, do zero * padding. This is only useful when * reading data from a file. */ if (capture_seconds > 0.0) { while (stats.ts_now + x - stats.global.ts_bigbang < capture_seconds) { do_the_printing(&stats, x, stats.ts_now); stats.global.total_time += x; stats.ts_now += x; } } break; case ACTION_BANDWIDTH: printf( "%sTime exceeding %.2f bps:\t%.2f sec.\t(%.2f%% of the time)\n", (smart_bandwidth)? "" : "(Raw) ", bandwidth, stats.global.total_time_over_bps, 100 * stats.global.total_time_over_bps / stats.global.total_time ); printf("Peak Bandwidth: %.2f bps\n", peakbandwidth); break; case ACTION_ACCOUNT: if (stats.global.total_time != 0.0) x = stats.global.total_bytes/stats.global.total_time; else x = stats.global.total_bytes/interval; printf("Bytes/sec\t= %s\n", wr_bytes(x)); printf("Bytes/minute\t= %s\n", wr_bytes(x*60)); printf("Bytes/hour\t= %s\n", wr_bytes(x*60*60)); printf("Bytes/day\t= %s\n", wr_bytes(x*60*60*24)); printf("Bytes/month\t= %s\n", wr_bytes(x*60*60*24*30)); break; default: break; } } #define USAGE \ "usage: %s [-?haeFlp] [-b bps] [-B bps] [-f filter expr] [-i interface] \ \n [-o output] [-R seconds] [-r filename] [-s seconds] [interval] \ \n -?, -h - display this help \ \n -a - accounting mode \ \n -b bps - bandwidth mode, where bps is bits/second \ \n -B bps - dumb bandwidth mode, where bps is bits/second \ \n -e - do not print empty intervals \ \n -F - flush stdout after printing each line \ \n -f filter expr - packet filter expression (like in tcpdump) \ \n -i interface - do live capture on [interface], not from file \ \n -l - include linklayer in packet size calc \ \n -o output - format for the output of stats (see manpage) \ \n -p - non-promiscuous mode when doing live capture \ \n -R seconds - display time relative to [seconds] \ \n -r filename - read data from [filename] \ \n -s seconds - capture only [seconds] long (-1 is infinite) \ \n \ \n interval - time interval (seconds) for taking samples \ \n" int Usage(int r, char *prog) { fprintf(stderr, "%s version %s\n", PACKAGE, VERSION); fprintf(stderr, USAGE, my_basename(prog)); return r; } void error(char *s) { fprintf(stderr, "error: %s\n", s); Usage(-1, "tcpstat"); exit(-1); } int main(int argc, char **argv) { char *filename; int ch; if (getuid() != geteuid()) is_setuid = 1; #ifndef NO_FILEDESC if (is_setuid) fprintf(stderr, SETUID_WARNING); #endif /* NO_FILEDESC */ /* Default promisc, like tcpdump(1) */ interval = 0.0; get_tcp_flags = GET_TCPD_DO_LIVE|GET_TCPD_DO_LIVE_PROMISC; filename = NULL; my_safe_strcpy(&filterexpr, " "); my_safe_strcpy(&outputformat, DEFAULT_FORMAT); while ( (ch = getopt(argc, argv, "h?aeFlp1B:b:f:i:o:R:r:s:")) != -1) { switch(ch) { case 'h': case '?': return Usage(1, argv[0]); break; case 'a': action = ACTION_ACCOUNT; break; case 'l': get_tcp_flags |= GET_TCPD_COUNT_LINKSIZE; break; case 'p': get_tcp_flags &= ~GET_TCPD_DO_LIVE_PROMISC; break; case 'B': case 'b': bandwidth = atof(optarg); if (bandwidth <= 0) return Usage(1, argv[0]); action = ACTION_BANDWIDTH; if (ch == 'b') smart_bandwidth = 1; break; case 'e': empty_intervals = 0; break; case 'F': flush_stdout = 1; break; case 'f': my_safe_strcpy(&filterexpr, optarg); break; case 'i': get_tcp_flags |= GET_TCPD_DO_LIVE; my_safe_strcpy(&filename, optarg); break; case 'o': my_safe_strcpy(&outputformat, optarg); break; case 'R': relative_seconds = atof(optarg); if (relative_seconds < 0) return Usage(1, argv[0]); break; case 'r': get_tcp_flags &= ~GET_TCPD_DO_LIVE; my_safe_strcpy(&filename, optarg); break; case 's': capture_seconds = atof(optarg); if (capture_seconds <= 0 && capture_seconds != -1) return Usage(1, argv[0]); break; case '1': interval = -1.0; break; default: break; } } if (interval == 0.0) { if ((argc - optind) < 1) interval = DEFAULT_INTERVAL; else { interval = atof(argv[optind]); if (interval <= 0 && interval != -1) { fprintf(stderr, "error: Bad interval value\n"); return Usage(1, argv[0]); } } } /* Sanity checks */ if ( ! (get_tcp_flags & (GET_TCPD_DO_LIVE|GET_TCPD_DO_LIVE_PROMISC)) ) error("-p option not compatible with -r"); /* If we are doing a limited capture, make sure that * the interval isn't longer than the time that we * are capturing. */ if (capture_seconds > 0.0 && capture_seconds < interval) interval = -1.0; if (filename == NULL) { /* Default read from interface */ get_tcp_flags |= GET_TCPD_DO_LIVE; my_safe_strcpy(&filename, "auto"); } process_file(filename, 0); if (filename) free(filename); if (filterexpr) free(filterexpr); if (outputformat) free(outputformat); return 0; }