1#!/usr/bin/perl -w
2#
3# iftop - display top interface counters in real time.
4#         (a quick hack based on if-counters.pl by Simon Leinen)
5#
6# $Id: iftop,v 1.2 2003-12-14 15:00:00 leinen Exp $
7# Dave Plonka, 21-Nov-2003
8#
9######################################################################
10### Observe interface counters in real time.
11######################################################################
12### Copyright (c) 1995-2000, Simon Leinen.
13###
14### This program is free software; you can redistribute it under the
15### "Artistic License" included in this distribution (file "Artistic").
16######################################################################
17### Author:       Simon Leinen  <simon@switch.ch>
18### Date Created: 21-Feb-1999
19###
20### Real-time full-screen display of the octet and (Cisco-specific)
21### CRC error counters on interfaces of an SNMP-capable node
22###
23### Description:
24###
25### Call this script with "-h" to learn about command usage.
26###
27### The script will poll the RFC 1213 ifTable at specified intervals
28### (default is every five seconds).
29###
30### For each interface except for those that are down, a line is
31### written to the terminal which lists the interfaces name (ifDescr),
32### well as the input and output transfer rates, as computed from the
33### deltas of the respective octet counts since the last sample.
34###
35### "Alarms"
36###
37### When an interface is found to have had CRC errors in the last
38### sampling interval, or only output, but no input traffic, it is
39### shown in inverse video.  In addition, when a link changes state
40### (from normal to inverse or vice versa), a bell character is sent
41### to the terminal.
42###
43### Miscellaneous
44###
45### Note that on the very first display, the actual SNMP counter
46### values are displayed.  THOSE ABSOLUTE COUNTER VALUES HAVE NO
47### DEFINED SEMANTICS WHATSOEVER.  However, in some versions of
48### Cisco's software, the values seem to correspond to the total
49### number of counted items since system boot (modulo 2^32).  This can
50### be useful for certain kinds of slowly advancing counters (such as
51### CRC errors, hopefully).
52###
53### The topmost screen line shows the name of the managed node, as
54### well as a few hard-to-explain items I found useful while debugging
55### the script.
56###
57### Please send any patches and suggestions for improvement to the
58### author (see e-mail address above).  Hope you find this useful!
59###
60### Original Purpose:
61###
62### This script should serve as an example of how to "correctly"
63### traverse the rows of a table.  This functionality is implemented in
64### the map_table() subroutine.  The example script displays a few
65### columns of the RFC 1213 interface table and Cisco's locIfTable.  The
66### tables share the same index, so they can be handled by a single
67### invocation of map_table().
68###
69require 5.003;
70
71use strict;
72
73use BER;
74use SNMP_Session "0.67";	# requires map_table_4
75use POSIX;			# for exact time
76use Curses;
77use Math::BigInt;
78
79### Forward declarations
80sub out_interface ($$$$$$@);
81sub pretty_bps ($$);
82sub usage ($ );
83
84my $version = '1';
85
86my $desired_interval = 10.0;
87
88my $all_p = 0;
89
90my $port = 161;
91
92my $max_repetitions = 0;
93
94my $suppress_output = 0;
95
96my $debug = 0;
97
98my $show_out_discards = 0;
99
100my $opt_P = 0;
101
102my $cisco_p = 0;
103
104my $counter64_p = 0;
105
106my $host;
107
108my $community;
109
110while (defined $ARGV[0]) {
111    if ($ARGV[0] =~ /^-v/) {
112	if ($ARGV[0] eq '-v') {
113	    shift @ARGV;
114	    usage (1) unless defined $ARGV[0];
115	} else {
116	    $ARGV[0] = substr($ARGV[0], 2);
117	}
118	if ($ARGV[0] eq '1') {
119	    $version = '1';
120	} elsif ($ARGV[0] eq '2c') {
121	    $version = '2c';
122	} else {
123	    usage (1);
124	}
125    } elsif ($ARGV[0] =~ /^-m/) {
126	if ($ARGV[0] eq '-m') {
127	    shift @ARGV;
128	    usage (1) unless defined $ARGV[0];
129	} else {
130	    $ARGV[0] = substr($ARGV[0], 2);
131	}
132	if ($ARGV[0] =~ /^[0-9]+$/) {
133	    $max_repetitions = $ARGV[0];
134	} else {
135	    usage (1);
136	}
137    } elsif ($ARGV[0] =~ /^-p/) {
138	if ($ARGV[0] eq '-p') {
139	    shift @ARGV;
140	    usage (1) unless defined $ARGV[0];
141	} else {
142	    $ARGV[0] = substr($ARGV[0], 2);
143	}
144	if ($ARGV[0] =~ /^[0-9]+$/) {
145	    $port = $ARGV[0];
146	} else {
147	    usage (1);
148	}
149    } elsif ($ARGV[0] =~ /^-t/) {
150	if ($ARGV[0] eq '-t') {
151	    shift @ARGV;
152	    usage (1) unless defined $ARGV[0];
153	} else {
154	    $ARGV[0] = substr($ARGV[0], 2);
155	}
156	if ($ARGV[0] =~ /^[0-9]+(\.[0-9]+)?$/) {
157	    $desired_interval = $ARGV[0];
158	} else {
159	    usage (1);
160	}
161    } elsif ($ARGV[0] eq '-a') {
162	$all_p = 1;
163    } elsif ($ARGV[0] eq '-c') {
164	$cisco_p = 1;
165    } elsif ($ARGV[0] eq '-l') {
166	$counter64_p = 1;
167    } elsif ($ARGV[0] eq '-n') {
168	$suppress_output = 1;
169    } elsif ($ARGV[0] eq '-d') {
170	$suppress_output = 1;
171	$debug = 1;
172    } elsif ($ARGV[0] eq '-D') {
173	$show_out_discards = 1;
174    } elsif ($ARGV[0] eq '-P') {
175	$opt_P = 1;
176    } elsif ($ARGV[0] eq '-h') {
177	usage (0);
178	exit 0;
179    } elsif ($ARGV[0] =~ /^-/) {
180	usage (1);
181    } else {
182	if (!defined $host) {
183	    $host = $ARGV[0];
184	} elsif (!defined $community) {
185	    $community = $ARGV[0];
186	} else {
187	    usage (1);
188	}
189    }
190    shift @ARGV;
191}
192defined $host or usage (1);
193defined $community or $community = 'public';
194usage (1) if $#ARGV >= $[;
195
196my $ifDescr = [1,3,6,1,2,1,2,2,1,2];
197my $ifAdminStatus = [1,3,6,1,2,1,2,2,1,7];
198my $ifOperStatus = [1,3,6,1,2,1,2,2,1,8];
199my $ifInOctets = [1,3,6,1,2,1,2,2,1,10];
200my $ifOutOctets = [1,3,6,1,2,1,2,2,1,16];
201my $ifInUcastPkts = [1,3,6,1,2,1,2,2,1,11];
202my $ifOutUcastPkts = [1,3,6,1,2,1,2,2,1,17];
203my $ifOutDiscards = [1,3,6,1,2,1,2,2,1,19];
204my $ifAlias = [1,3,6,1,2,1,31,1,1,1,18];
205## Counter64 variants
206my $ifHCInOctets = [1,3,6,1,2,1,31,1,1,1,6];
207my $ifHCInUcastPkts = [1,3,6,1,2,1,31,1,1,1,7];
208my $ifHCOutOctets = [1,3,6,1,2,1,31,1,1,1,10];
209my $ifHCOutUcastPkts = [1,3,6,1,2,1,31,1,1,1,11];
210## Cisco-specific variables enabled by `-c' option
211my $locIfInCRC = [1,3,6,1,4,1,9,2,2,1,1,12];
212my $locIfOutCRC = [1,3,6,1,4,1,9,2,2,1,1,12];
213
214my $clock_ticks = POSIX::sysconf( &POSIX::_SC_CLK_TCK );
215
216my $win = new Curses
217    unless $suppress_output;
218
219my %old;
220my @ifactive;
221my $sleep_interval = $desired_interval + 0.0;
222my $interval;
223my $linecount;
224
225sub rate_32 ($$$@) {
226    my ($old, $new, $interval, $multiplier) = @_;
227    $multiplier = 1 unless defined $multiplier;
228    my $diff = $new-$old;
229    if ($diff < 0) {
230	$diff += (2**32);
231    }
232    return $diff / $interval * $multiplier;
233}
234
235sub rate_64 ($$$@) {
236    my ($old, $new, $interval, $multiplier) = @_;
237    $multiplier = 1 unless defined $multiplier;
238    return 0 if $old == $new;
239    my $diff = Math::BigInt->new ($new-$old);
240    if ($diff < 0) {
241	$diff = $diff->add (2**64);
242    }
243    ## hrm.  Why is this so complicated?
244    ## I want a real programming language (such as Lisp).
245    my $result = $diff->bnorm () / $interval * $multiplier;
246    return $result;
247}
248
249sub rate ($$$$@) {
250    my ($old, $new, $interval, $counter64_p, $multiplier) = @_;
251    $multiplier = 1 unless defined $multiplier;
252    return $counter64_p
253	? rate_64 ($old, $new, $interval, $multiplier)
254	: rate_32 ($old, $new, $interval, $multiplier);
255}
256
257sub rate_or_0 ($$$$$) {
258    my ($old, $new, $interval, $counter64_p, $multiplier) = @_;
259    return defined $new
260	? rate ($old, $new, $interval, $counter64_p, $multiplier)
261	: 0;
262}
263
264sub handle_interface ($$$$$$@) {
265    my ($index, $descr, $admin, $oper, $in, $out);
266    my ($crc, $comment);
267    my ($drops);
268    my ($clock) = POSIX::times();
269    my $alarm = 0;
270
271    ($index, $descr, $admin, $oper, $in, $out, $comment, @_) = @_;
272    ($crc, @_) = @_ if $cisco_p;
273    ($drops, @_) = @_ if $show_out_discards;
274
275    grep (defined $_ && ($_=pretty_print $_),
276	  ($descr, $admin, $oper, $in, $out, $crc, $comment, $drops));
277    $win->clrtoeol ()
278	unless $suppress_output;
279    return unless $all_p || defined $oper && $oper == 1;	# up
280    return unless defined $in && defined $out;
281    ## Suppress interfaces called "unrouted VLAN..."
282    return if $descr =~ /^unrouted VLAN/;
283    if (!defined $old{$index}) {
284	$win->addstr ($linecount, 0,
285		      sprintf ("%2d  %-24s %10s %10s",
286			       $index,
287			       defined $descr ? $descr : '',
288			       defined $in ? $in : '-',
289			       defined $out ? $out : '-'))
290	    unless $suppress_output;
291	if ($show_out_discards) {
292	    $win->addstr (sprintf (" %8s",
293				   defined $drops ? $drops : '-'))
294		unless $suppress_output;
295	}
296	if ($cisco_p) {
297	    $win->addstr (sprintf (" %10s",
298				   defined $crc ? $crc : '-'))
299		unless $suppress_output;
300	}
301	$win->addstr (sprintf (" %s",
302			       defined $comment ? $comment : ''))
303	    unless $suppress_output;
304        ++$linecount;
305        $win->refresh ()
306	    unless $suppress_output;
307        $old{$index} = {'in' => $in,
308		        'out' => $out,
309		        'crc' => $crc,
310		        'drops' => $drops,
311		        'clock' => $clock,
312		        'alarm' => $alarm};
313    } else {
314	my $old = $old{$index};
315
316	$interval = ($clock-$old->{'clock'}) * 1.0 / $clock_ticks;
317	my $d_in = rate_or_0 ($old->{'in'}, $in, $interval, $counter64_p, $opt_P? 1 : 8);
318	my $d_out = rate_or_0 ($old->{'out'}, $out, $interval, $counter64_p, $opt_P? 1 : 8);
319	my $d_drops = rate_or_0 ($old->{'drops'}, $drops, $interval, 0, 1);
320	my $d_crc = rate_or_0 ($old->{'crc'}, $crc, $interval, 0, 1);
321	$alarm = ($d_crc != 0)
322	    || 0 && ($d_out > 0 && $d_in == 0);
323
324        $old{$index} = {'in' => $in,
325		        'out' => $out,
326		        'crc' => $crc,
327		        'drops' => $drops,
328		        'clock' => $clock,
329		        'alarm' => $alarm,
330                        'descr' => $descr,   # needed for display
331                        'd_in' => $d_in,     # needed for display
332                        'd_out' => $d_out,   # needed for display
333                        'drops' => $d_drops, # needed for display
334                        'crc' => $d_crc,     # needed for display
335                        'comment' => $comment, # needed for display
336                       };
337        push(@ifactive, $index);
338    }
339}
340
341sub out_interface ($$$$$$@) {
342    my ($index, $descr, $admin, $oper, $in, $out);
343    my ($crc, $comment);
344    my ($drops);
345    my ($clock) = POSIX::times();
346    my $alarm = 0;
347
348    ($index, $descr, $admin, $oper, $in, $out, $comment, @_) = @_;
349    ($crc, @_) = @_ if $cisco_p;
350    ($drops, @_) = @_ if $show_out_discards;
351
352    grep (defined $_ && ($_=pretty_print $_),
353	  ($descr, $admin, $oper, $in, $out, $crc, $comment, $drops));
354    $win->clrtoeol ()
355	unless $suppress_output;
356    return unless $all_p || defined $oper && $oper == 1;	# up
357    return unless defined $in && defined $out;
358    ## Suppress interfaces called "unrouted VLAN..."
359    return if $descr =~ /^unrouted VLAN/;
360    if (!defined $old{$index}) {
361	$win->addstr ($linecount, 0,
362		      sprintf ("%2d  %-24s %10s %10s",
363			       $index,
364			       defined $descr ? $descr : '',
365			       defined $in ? $in : '-',
366			       defined $out ? $out : '-'))
367	    unless $suppress_output;
368	if ($show_out_discards) {
369	    $win->addstr (sprintf (" %8s",
370				   defined $drops ? $drops : '-'))
371		unless $suppress_output;
372	}
373	if ($cisco_p) {
374	    $win->addstr (sprintf (" %10s",
375				   defined $crc ? $crc : '-'))
376		unless $suppress_output;
377	}
378	$win->addstr (sprintf (" %s",
379			       defined $comment ? $comment : ''))
380	    unless $suppress_output;
381    } else {
382	my $old = $old{$index};
383
384	$interval = ($clock-$old->{'clock'}) * 1.0 / $clock_ticks;
385	my $d_in = rate_or_0 ($old->{'in'}, $in, $interval, $counter64_p, $opt_P? 1 : 8);
386	my $d_out = rate_or_0 ($old->{'out'}, $out, $interval, $counter64_p, $opt_P? 1 : 8);
387	my $d_drops = rate_or_0 ($old->{'drops'}, $drops, $interval, 0, 1);
388	my $d_crc = rate_or_0 ($old->{'crc'}, $crc, $interval, 0, 1);
389	$alarm = ($d_crc != 0)
390	    || 0 && ($d_out > 0 && $d_in == 0);
391	print STDERR "\007" if $alarm && !$old->{'alarm'};
392	print STDERR "\007" if !$alarm && $old->{'alarm'};
393	$win->standout() if $alarm && !$suppress_output;
394	$win->addstr ($linecount, 0,
395		      sprintf ("%2d  %-24s %s %s",
396			       $index,
397			       defined $descr ? $descr : '',
398			       pretty_bps ($in, $d_in),
399			       pretty_bps ($out, $d_out)))
400	    unless $suppress_output;
401	if ($show_out_discards) {
402	    $win->addstr (sprintf (" %8.1f %s",
403				   defined $drops ? $d_drops : 0))
404		unless $suppress_output;
405	}
406	if ($cisco_p) {
407	    $win->addstr (sprintf (" %10.1f",
408				   defined $crc ? $d_crc : 0))
409		unless $suppress_output;
410	}
411	$win->addstr (sprintf (" %s",
412			       defined $comment ? $comment : ''))
413	    unless $suppress_output;
414	$win->standend() if $alarm && !$suppress_output;
415    }
416    $old{$index} = {'in' => $in,
417		    'out' => $out,
418		    'crc' => $crc,
419		    'drops' => $drops,
420		    'clock' => $clock,
421		    'alarm' => $alarm};
422    ++$linecount;
423    $win->refresh ()
424	unless $suppress_output;
425}
426
427sub update_interfaces () {
428   foreach my $index (sort { $old{$b}->{d_in} <=> $old{$a}->{d_in} or
429                             $old{$b}->{d_out} <=> $old{$a}->{d_out} } @ifactive) {
430        my $cur = $old{$index};
431	$win->addstr ($linecount, 0,
432		      sprintf ("%2d  %-24s %s %s",
433			       $index,
434			       defined $cur->{descr} ? $cur->{descr} : '',
435			       pretty_bps ($cur->{in}, $cur->{d_in}),
436			       pretty_bps ($cur->{out}, $cur->{d_out})))
437	    unless $suppress_output;
438	if ($show_out_discards) {
439	    $win->addstr (sprintf (" %8.1f %s",
440				   defined $cur->{drops} ? $cur->{d_drops} : 0))
441		unless $suppress_output;
442	}
443	if ($cisco_p) {
444	    $win->addstr (sprintf (" %10.1f",
445				   defined $cur->{crc} ? $cur->{d_crc} : 0))
446		unless $suppress_output;
447	}
448	$win->addstr (sprintf (" %-80.80s",
449			       defined $cur->{comment} ? $cur->{comment} : ''))
450	    unless $suppress_output;
451        ++$linecount;
452        $win->refresh ()
453           unless $suppress_output;
454    }
455}
456
457sub pretty_bps ($$) {
458    my ($count, $bps) = @_;
459    if (! defined $count) {
460	return '      -   ';
461    } elsif ($bps > 1000000) {
462	return sprintf ("%8.4f M", $bps/1000000);
463    } elsif ($bps > 1000) {
464	return sprintf ("%9.1fk", $bps/1000);
465    } else {
466	return sprintf ("%10.0f", $bps);
467    }
468}
469
470$win->erase ()
471    unless $suppress_output;
472my $session =
473    ($version eq '1' ? SNMPv1_Session->open ($host, $community, $port)
474     : $version eq '2c' ? SNMPv2c_Session->open ($host, $community, $port)
475     : die "Unknown SNMP version $version")
476  || die "Opening SNMP_Session";
477$session->debug (1) if $debug;
478
479### max_repetitions:
480###
481### We try to be smart about the value of $max_repetitions.  Starting
482### with the session default, we use the number of rows in the table
483### (returned from map_table_4) to compute the next value.  It should
484### be one more than the number of rows in the table, because
485### map_table needs an extra set of bindings to detect the end of the
486### table.
487###
488$max_repetitions = $session->default_max_repetitions
489    unless $max_repetitions;
490while (1) {
491    unless ($suppress_output) {
492	$win->addstr (0, 0, sprintf ("%-20s interval %4.1fs %d reps",
493				     $host,
494				     $interval || $desired_interval,
495				     $max_repetitions));
496	$win->standout();
497        if ($opt_P) {
498	    $win->addstr (1, 0,
499		          sprintf (("%2s  %-24s %10s %10s"),
500			           "ix", "name",
501			           "pkts/s", "pkts/s"));
502        } else {
503	    $win->addstr (1, 0,
504		          sprintf (("%2s  %-24s %10s %10s"),
505			           "ix", "name",
506			           "bits/s", "bits/s"));
507        }
508	if ($show_out_discards) {
509	    $win->addstr (sprintf ((" %8s"),
510				   "drops/s"));
511	}
512	if ($cisco_p) {
513	    $win->addstr (sprintf ((" %10s"), "pkts/s"));
514	}
515	$win->addstr (sprintf ((" %s"), "description"));
516	$win->addstr (2, 0,
517		      sprintf (("%2s  %-24s %10s %10s"),
518			       "", "",
519			       "in", "out"));
520	if ($show_out_discards) {
521	    $win->addstr (sprintf ((" %8s"),
522				   ""));
523	}
524	if ($cisco_p) {
525	    $win->addstr (2, 0,
526			  sprintf ((" %10s %s"),
527				   "CRC",
528				   ""));
529	}
530	$win->clrtoeol ();
531	$win->standend();
532    }
533    $linecount = 3;
534    my @oids = ($ifDescr,$ifAdminStatus,$ifOperStatus);
535    if ($counter64_p) {
536	if ($opt_P) {
537	    @oids = (@oids,$ifHCInUcastPkts,$ifHCOutUcastPkts);
538	} else {
539	    @oids = (@oids,$ifHCInOctets,$ifHCOutOctets);
540	}
541    } else {
542	if ($opt_P) {
543	    @oids = (@oids,$ifInUcastPkts,$ifOutUcastPkts);
544	} else {
545	    @oids = (@oids,$ifInOctets,$ifOutOctets);
546	}
547    }
548    @oids = (@oids,$ifAlias);
549    if ($cisco_p) {
550	push @oids, $locIfInCRC;
551    }
552    if ($show_out_discards) {
553	push @oids, $ifOutDiscards;
554    }
555    @ifactive = ();
556    my $calls = $session->map_table_4
557	(\@oids, \&handle_interface, $max_repetitions);
558    update_interfaces() if @ifactive;
559    $max_repetitions = $calls + 1
560	if $calls > 0;
561    $sleep_interval -= ($interval - $desired_interval)
562	if defined $interval;
563    select (undef, undef, undef, $sleep_interval);
564}
5651;
566
567sub usage ($) {
568    warn <<EOM;
569Usage: $0 [-t secs] [-v (1|2c)] [-c] [-l] [-m max] [-p port] host [community]
570       $0 -h
571
572  -h           print this usage message and exit.
573
574  -c           also use Cisco-specific variables (locIfInCrc)
575
576  -P           poll interface unicast packet counters (rather than octets)
577
578  -l           use 64-bit counters (requires SNMPv2 or higher)
579
580  -t secs      specifies the sampling interval.  Defaults to 10 seconds.
581
582  -v version   can be used to select the SNMP version.  The default
583   	       is SNMPv1, which is what most devices support.  If your box
584   	       supports SNMPv2c, you should enable this by passing "-v 2c"
585   	       to the script.  SNMPv2c is much more efficient for walking
586   	       tables, which is what this tool does.
587
588  -m max       specifies the maxRepetitions value to use in getBulk requests
589               (only relevant for SNMPv2c).
590
591  -m port      can be used to specify a non-standard UDP port of the SNMP
592               agent (the default is UDP port 161).
593
594  host         hostname or IP address of a router
595
596  community    SNMP community string to use.  Defaults to "public".
597EOM
598    exit (1) if $_[0];
599}
600