#!/usr/bin/perl -w

#
# ifinfo v0.90 by Joseph Landman (landman@scalableinformatics.com)
# copyright 2002,2003 by Scalable Informatics LLC
# 
# License:  Artistic License used in Perl (see the enclosed LICENSE file)
#
# Changelog:
#
# v0.25
#	Get the basic idea down and working.  Add a little option parsing so as
#	to get the information back in the needed format
# v0.40
#       Add the mask conversion stuff, fix some formatting issues, and try to
#       fix the spurious "something or the other not defined" bug in comparisons.
#       (the -w and strict options are wonderful!)
# v0.50
#       Fixed a bug.  Incorrectly defined Gigabyte.  Correct
#       definition now in place.
# v0.60 
#	Added name service query option (--ns) to report the name(s) of the
#	interface(s)
#
# v0.75 
#	Added routing bits
#
# v0.85
#       Added hardware MAC address, irq, etc
#
# v0.90
#       fixed insecure path, device sorting, and headers
#
# todo:
#	1) make portable (e.g. abstract data gathering) to all *nix
#	2) creeping featuritis ... nah
#	3) test corner cases (multiple odd aliases, things not well represented in the 
#	   current set of probed files/structures)


	use strict;
	use Net::netent qw(:FIELDS);
	use Net::hostent;
	use Socket;
	use IO::File;
	use Carp;
	use Data::Dumper;
	use Getopt::Long;
	

#
# constants
#
	
	use constant kilobyte => 1024;
	use constant megabyte => 1024 * kilobyte;
	use constant gigabyte => 1024 * megabyte;

	use constant true  => (1==1);
	use constant false => (1==0);
	

#
#  Program paths and options:  change these as needed
#
	use constant netstat_binary => '/bin/netstat -nre';
	use constant ifconfig_binary => '/sbin/ifconfig ';

#
# variables and run time setup
#

 #
 # fix the path so that it is not wide open ... delete un-needed environment bits
 #
	$ENV{'PATH'} = '/bin:/usr/bin';
        delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

	my $fh = new IO::File;
	my $net;
	my @proc = qw(
	               rx_bytes rx_packets rx_errors rx_drop rx_fifo
		       rx_frame rx_compressed rx_multicast tx_bytes 
		       tx_packets tx_errors tx_drop tx_fifo tx_frame
		       tx_compressed tx_multicast HWaddr name
		     );
        my @include = qw(Link_encap inet_addr Broadcast Mask MTU Metric dev);
	my @dir_service = qw(name);
	my @options = (@proc,@include,@dir_service);
	my (%route,$run_netstat,$tmp_y);
	my (@info,%ifname,@bytes,@lines,$long,$key,$value,$ns,$h,$name);
	my ($s,%status,@vector,$mask,$addr_and_mask,$field_size,$route);
	my $sep = ",";
	my $format;
	my (@ints,@all,$interfaces,$request,$state,$help,@params,$paramstring);
	my ($mac,$run_ifconfig,$irq,$tmp,$no_headers,$macnc);

	$mac=false;
	$macnc=false;
	$no_headers=false;
#
# Parse command line options
#
	my $result = GetOptions (
				 "info=s" => \$request,
				 "ifname=s" =>\$interfaces, 
				 "sep=s"=>\$sep, 
				 "format=s" =>\$format,
				 "mask=s" => \$mask,				
				 "help" => \$help,
				 "ns"	=> \$ns,
				 "mac"	=> \$mac,
				 "irq"	=> \$irq,
				 "route"=> \$route,
				 "noheaders"=> \$no_headers
				);
	if (defined($help)) { &print_help_and_exit() ; }
	if (defined($format)) { $format .= "\n";  } # append a new-line on fixed formats
	if (defined($request) && ($request =~ /name/)) { $ns = 1; }	 
	# make sure name service is on
	# if we request info by name

#
# start
#	
	
	# open the /proc/net/dev file and read it all in ...
	if (!($fh->open("< /proc/net/dev")))
	   {
	     croak "Could not open /proc/net/dev for reading\n";
	   }
	@all=$fh->getlines();
	$fh->close;

	# ... drop first two lines, and remove \n from ends
	# of all of the lines.
	shift @all;
	shift @all;
	chomp @all;
	
	# grab routing table if requested...
	&get_routing if ($route);
	
	if (defined($interfaces))
	   {
	     @ints=split(/[:,\,,\s+]/,$interfaces); # allow : , and whitespace to 
	     					    # seperate the names						    
	   }

        #
	# check info parameters against allowable parameters
	#
	if (defined($request))
	   {
	     @params=split(/[:,\,,\s+]/,$request); # allow : , and whitespace to 
	     					# seperate the parameters
	     # if no format string has been defined, build one out
	     # of the parameters and a seperator.  Assume all parameters
	     # are strings (not really, but this can work for the moment)						
	     if (!defined($format))
	        {
		  my $tmpfmt="%s".$sep;
		  $format=$tmpfmt x ($#params);
		  $format .="%s\n";
		}							     			    
	   }   


	# parse the remaining lines ...
	foreach my $line (@all)
	  { 
	    $state=1;
            $line =~ s/^\s+//g; # ... trim leading white space
            $line =~ s/:/ /g; # ... turn ":" into white space
	    $line =~ s/\s+/\ /g;# ... turn multiple white spaces 
	                        #     into a single white space       
	    @info = split (/[:,\s]/,$line);  # split on : or white space
	    #print "info: ",join(":",@info),"\n";
	    map($state &&= ($info[0] ne $_),@ints);  # "AND" the comparisons together
	    #print "state,if=>",$state," ",$info[0],"\n";
	    if (@ints) {next if ($state);}      # skip if the interface wasnt on the command line
	    $ifname{$info[0]} =
    				{
				  'rx_bytes' => $info[1],
				  'rx_packets' => $info[2],
				  'rx_errors' => $info[3],
				  'rx_drop' => $info[4],
				  'rx_fifo' => $info[5],
				  'rx_frame' => $info[6],
				  'rx_compressed' => $info[7],
				  'rx_multicast' => $info[8],
				  'tx_bytes' => $info[9],
				  'tx_packets' => $info[10],
				  'tx_errors' => $info[11],
				  'tx_drop' => $info[12],
				  'tx_fifo' => $info[13],
				  'tx_frame' => $info[14],
				  'tx_compressed' => $info[15],
				  'tx_multicast' => $info[16],
				  'dev' => $info[0]			  
				};
	    # now grab the information from ifconfig if possible
	    $run_ifconfig = ifconfig_binary . " " . $info[0] ." |";
	    open($fh,$run_ifconfig) or next;
	    @lines=<$fh>; # grab all lines in file
	    close($fh);
	    $long=join("",@lines); # make one big line
	    $long =~ s/\n/ /g; # remove newlines
	    if ($long =~ /HWaddr\s(\S+)\s+/)
	       {
	         $ifname{$info[0]}->{'HWaddr'}=$1;
	       }
	    if ($long =~ /Interrupt:(\S+)\s+/)
	       {
	         $ifname{$info[0]}->{'irq'}=$1;
	       }
	    foreach my $elt (split(/\s{2,}/,$long)) # split on 2 or more white spaces
	      {
        	next if ($elt eq $info[0]); #skip the interface name
		if (!($elt =~ m/HWaddr/))
		   {
		     ($key,$value)=split(":",$elt,2);
		     $key =~ s/\s/\_/g;
		   }
		  else
		   {
		     ($key,$value)=split(" ",$elt,2);	   
		   }
		if (map(($key =~ m/$_/),@include))  # only add the keys in the include array
		   {
		     $ifname{$info[0]}->{$key}=$value;
		   }
	      }
	  }
	
	if (!$no_headers)
	   {
	    if ( 
		(!$format)		&& 
		(!$ns)			&&
		(!$route)		&&
		(!$mac)		
	       )
	       {
		 printf "device:\taddress/netmask\t\t\tMTU\t   Tx (MB)\t   Rx (MB)\n";
	       }
	    if ( 
		(!$format)		&& 
		(!$ns)			&&
		(!$route)		&&
		($mac)		
	       )
	       {
		 printf "device:\tMAC\t\taddress/netmask\t\t\tMTU\t   Tx (MB)\t   Rx (MB)\n";
	       }
	    if ( 
		(!$format)		&& 
		(!$ns)			&&
		($route)		 
	       )
	       {
		 printf "device:\taddress/netmask\t\t\t    destination/gw\n";
	       }
	    if ( 
		(!$format)		&& 
		($ns)			&&
		(!$route)		&&
		(!$mac)	
	       )
	       {
		 printf "device:\tname from resolver\t\tMTU\t   Tx (MB)\t   Rx (MB)\n";
	       }
	    if ( 
		(!$format)		&& 
		($ns)			&&
		(!$route)		&&
		($mac)	
	       )
	       {
		 printf "device:\tMAC\t\tname from resolver\t\tMTU\t   Tx (MB)\t   Rx (MB)\n";
	       }
	   }
	foreach  my $interface  (sort { lc($a) cmp lc($b) } keys %ifname)
	  {
	    
	    %status=%{$ifname{$interface}};
	    $field_size=31;
	    #print Dumper(\%status),"\n";
	    
	    if ((!exists($status{'tx_bytes'})) || (!($status{'tx_bytes'})))
	       {
        	 $status{'tx_bytes'} = 0;
	       }
	    if ((!exists($status{'HWaddr'})) || (!($status{'HWaddr'})))
	       {
        	 $status{'HWaddr'} = "00:00:00:00:00:00";
	       }
	    if ((!exists($status{'irq'})) || (!($status{'irq'})))
	       {
        	 $status{'irq'} = "-";
	       }
	    if ((!exists($status{'rx_bytes'})) ||(!($status{'rx_bytes'})))
	       {
        	 $status{'rx_bytes'} = 0;
	       }
	    if ((!exists($status{'inet_addr'})) ||(!($status{'inet_addr'})))
	       {
        	 $status{'inet_addr'} = 'addr not set';
		 $status{'name'} = 'name not set';
	       }
	      else
	       {
	         if ((defined($ns)) || (defined($status{'name'})))
		    {
		      if ($h=gethost($status{'inet_addr'}))
		         {
			   $status{'name'}=$h->name;
			 }
			else
			 {
			   $status{'name'}="Unknown";
			 }
		    }
	       }
	    if ((!exists($status{'Mask'})) ||(!($status{'Mask'})))
	       {
        	 $status{'Mask'} = 'mask not set';
	       }
	    elsif ((defined($mask)) && ($mask eq "hex"))
	       {
	         $tmp=&quad_to_hex($status{'Mask'});
		 $status{'Mask'}="0x".$tmp;$field_size=31;
	       }
	    elsif ((defined($mask)) && ($mask eq "range"))
	       {
	         $tmp=&count_bits($status{'Mask'});
		 $status{'Mask'}=$tmp;$field_size=31;
	       }   
	    elsif ((defined($mask)) && ($mask eq "bits"))
	       {
	         my $tmp=&quad_to_bits($status{'Mask'});
		 $status{'Mask'}=$tmp;
		 $field_size=47;
	       }   
	    if (!($format))
	       {
	       
	         $addr_and_mask = sprintf "%s/%s",
		    $status{'inet_addr'},
		    $status{'Mask'};
		 $addr_and_mask .= " " x $field_size;
		 if (!$ns)
		   { 
		     if (!defined($route))
		        {
			  if (!($mac))
			     {		 		    
			       printf "%s:\t%s\t%i\t%10.3f\t%10.3f\n",
			       $interface,
			       substr($addr_and_mask,0,$field_size),
			       $status{'MTU'},
			       $status{'tx_bytes'}/megabyte,
			       $status{'rx_bytes'}/megabyte; 
			     }
			    else
			     {
			       $status{'HWaddr'} =~ s/://g;
			       printf "%s:\t%s\t%s\t%i\t%10.3f\t%10.3f\n",
			       $interface,$status{'HWaddr'},
			       substr($addr_and_mask,0,$field_size),
			       $status{'MTU'},
			       $status{'tx_bytes'}/megabyte,
			       $status{'rx_bytes'}/megabyte; 			     
			     }
		        }
		       else
		        {
			  my @routes;
			  #printf "-----P\nDump=%s\n",Dumper(\%route);
			  map {
			        if (($route{$_}->{'dev'}) eq $interface)
				   {
				     $tmp_y = "";
				     $tmp_y = sprintf '%s/%s',
				              $_,
					      $route{$_}->{'genmask'};
				     $tmp_y .= " " x 32;				     
				     push @routes, sprintf '%s -> %s',
				     substr($tmp_y, 0, 32),
				     $route{$_}->{'gateway'};
				   }
			      } keys %route;
			  my $paths=join("\n\t",@routes);
			  printf "%s:\t%s\n",
			  $interface,$paths;
			   

			}
		   }
		  else
		   {
		     my $space=" " x 80;
		     if (!($mac))
		        {
			  printf "%s:\t%s\t%i\t%10.3f\t%10.3f\n",
			  $interface,
			  substr($status{'name'}.$space,0,$field_size),
			  $status{'MTU'},
			  $status{'tx_bytes'}/megabyte,
			  $status{'rx_bytes'}/megabyte; 		  
		        }
		      else
		        {
			  $status{'HWaddr'} =~ s/://g;			 
			  printf "%s:\t%s\t%s\t%i\t%10.3f\t%10.3f\n",
			  $interface,$status{'HWaddr'},
			  substr($status{'name'}.$space,0,$field_size),
			  $status{'MTU'},
			  $status{'tx_bytes'}/megabyte,
			  $status{'rx_bytes'}/megabyte; 		  			  
			} 
		   }   	
	       }
	    if ($format)
	       {
		 printf $format,map($status{$_} || "_not_set_",@params);    	
	       }
	     
	  }

sub print_help_and_exit
    {
       exec "man ifinfo";
       exit;
    }
    
sub quad_to_hex
    {
       my $quad=shift;
       my @y=split(/\./,$quad);
       my $sc=256; 
       my $s=1; 
       my $sum = 0; 
       foreach my $elt (@y) { $sum += $s*$elt; $s *= $sc ;} 
       $s=unpack("H*",pack("L",$sum));
       return $s;
    }
sub quad_to_bits
    {
       my $quad=shift;
       my @y=split(/\./,$quad);
       my $sc=256; 
       my $s=1; 
       my $sum = 0; 
       foreach my $elt (@y) { $sum += $s*$elt; $s *= $sc ;} 
       $s=unpack("B*",pack("L",$sum));
       return $s;
    }
sub count_bits
    {
       my $quad=shift;
       my $bits=&quad_to_bits($quad);     
       my @exploded=split(/ */,$bits);
       #print join(":",@exploded),"\n";
       my $sum=0;
       foreach my $s (@exploded) { $sum++ if $s; }
       return $sum;
    }

sub get_routing
    {
       my @lines;
       $run_netstat = netstat_binary . " |";
       return if (!(open($fh,$run_netstat) ));
       @lines=<$fh>; # grab all lines in file
       close($fh);
       shift @lines;
       shift @lines; # drop first two lines of text
       foreach my $line (@lines) # loop over routing table
	 {
          $line =~ m/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/;
          #$line =~ m/^((\S+)\s+){8}$/;
	  $route{$1}->{'gateway'}=$2;
	  $route{$1}->{'genmask'}=$3;
	  $route{$1}->{'flags'}=$4;
	  $route{$1}->{'metric'}=$5;
	  $route{$1}->{'ref'}=$6;
	  $route{$1}->{'use'}=$7;
	  $route{$1}->{'dev'}=$8;
	 }
}

sub parse
    {
      my $line=shift;
      my $in=shift;
      my %ret;
      if ($line =~ /Link encap:(\w+)\s+/)
         {
	   $ret{'Link_encap'}=$1;
	 }
      if ($line =~ /HWaddr\s+(\S+)\s+/)
         {
	   $ret{'HWaddr'}=$1;
	 }
      if ($line =~ /HWaddr\s+(\S+)\s+/)
         {
	   $ret{'HWaddr'}=$1;
	 }



    }
    
__END__

=head1 NAME

ifinfo - a tool to get network interface information

=head1 SYNOPSIS

B<ifinfo [options for ifinfo] >

=head1 DESCRIPTION

B<ifinfo> is a simple commandline network inquiry/formatting tool 
specifically designed to output some useful information about your
network connection. It works by querying the relevant tables
in I</proc> as well as the output of various Unix commands. Its
entire purpose in life is to tell you what you want to
know, hopefully in a format that is useful and compact. 

=head1 OPTIONS

=over 4

=item B<--ifname=>I<interface_1>[I<,interface_2>][I<,interface_3>]...

Choose which interfaces to display.  Defaults to all interfaces.  You 
may use as many interfaces as you wish.  Repeated interface names will
not generate multiple output lines.  Missing interfaces will be silently 
ignored.

=item B<--info=>I<parameter_1>[I<,parameter_2>][I<,parameter_3>]...

Choose which parameters to display.  Defaults to a summary of all
interfaces with IP address/netmask, maximum transmit unit, bytes 
transmitted and received.  The column headers in the below example
below are not part of the output.

 Ifname	 IP/netmask                    MTU     Tx (MB)      Rx (MB)

 lo:     127.0.0.1/255.0.0.0         16436    210.524 MB   210.524 MB
 eth0:   192.168.1.16/255.255.255.0   1500   1787.516 MB   186.974 MB

The parameters you may use for this are as follows:

=over 2

=item B<rx_bytes>: number of received bytes

=item B<rx_packets>: number of received packets

=item B<rx_errors>: number of receive errors

=item B<rx_drop>: number of receive packets dropped

=item B<rx_fifo>: 

=item B<rx_frame>: 

=item B<rx_compressed>: 

=item B<rx_multicast>:  number of received multicast packets

=item B<tx_bytes>: number of transmitted bytes

=item B<tx_packets>: number of transmitted packets

=item B<tx_errors>: number of transmit errors

=item B<tx_drop>: number of transmit packets dropped

=item B<tx_fifo>: 

=item B<tx_frame>: 

=item B<tx_compressed>: 

=item B<tx_multicast>: number of transmitted multicast packets

=item B<HWaddr>: the hardware MAC address of the interface

=item B<name>: the name of this interface according to the resolver

=item B<irq>: the interrupt associated with the interface.  A returned value of
"-" means that no interrupt has been assigned to the interface.

=back

Note: current versions of the Linux kernel have a network byte counter
overflow at 4 GB, so that if you are transfering more than 4 GB of 
data, you will find that the counters overflow and wrap around.



=item B<--sep=>I<output separator character>

This is the character that is used to separate fields in the output.  

 ifinfo --sep="," --ifname=eth0,lo --info=dev,HWaddr,irq,inet_addr,name

 lo,00:00:00:00:00:00,-,127.0.0.1,localhost.localdomain
 eth0,00:40:05:0B:9F:01,5,192.168.1.16,squash.scalableinformatics.com


=item B<--format=>"I<sprintf format specifier>"

This gives much finer grain control over the output of the program.  You
may use this to encode an arbitrary format string.  It is possible to 
confuse the program with an incorrect format specifier.  

 ifinfo --format="[%s],%s+%s" --ifname=eth0,lo --info=dev,HWaddr,inet_addr

 [lo],00:00:00:00:00:00+127.0.0.1
 [eth0],00:40:05:0B:9F:01+192.168.1.16

ifinfo --format="<tr><td>%s</td><td>%s</td><td>%s</td>" \
           --ifname=eth0,lo --info=dev,HWaddr,inet_addr
	   
 <tr><td>lo</td><td>00:00:00:00:00:00</td><td>127.0.0.1</td>
 <tr><td>eth0</td><td>00:40:05:0B:9F:01</td><td>192.168.1.16</td>

We recommend simply using B<%s> as the format specifier for a particular
field.  Wrap the field with the text you need.  The above example shows
how this could be used as part of a status display for a web based
machine. 


=item B<--mask=>[I<hex>|I<range>|I<bits>]

This option controls how the netmask is printed.  The usual method is in
terms of the integer quads I<xxx.yyy.zzz.ttt>.  The I<mask> option allows
you to change that.  

 ifinfo --mask=hex --ifname=eth0,lo

 lo:     127.0.0.1/0xff000000            16436   219.678 MB      219.678 MB
 eth0:   192.168.1.16/0xffffff00         1500    1793.610 MB     189.103 MB


 ifinfo --mask=range --ifname=eth0,lo

 lo:     127.0.0.1/8                     16436   219.685 MB      219.685 MB
 eth0:   192.168.1.16/24                 1500    1793.614 MB     189.108 MB


 ifinfo --mask=bits --ifname=eth0,lo

 lo:     127.0.0.1/11111111000000000000000000000000      16436   219.691 MB	219.691 MB
 eth0:   192.168.1.16/11111111111111111111111100000000   1500    1793.620 MB	189.114 MB


=item B<--help> 

The man page.

=item B<--ns>

This option queries your default resolver service to return the name
of the particular interface.  It is a synonym for the parameter I<name>
which can be used in the I<--info=...> option.

 ifinfo --ifname=eth0,lo --ns

 lo:     localhost.localdomain           16436   220.802 MB      220.802 MB
 eth0:   squash.scalableinformatics.com  1500    1794.392 MB     189.379 MB


 ifinfo --ifname=eth0,lo --info=dev,name

 lo,localhost.localdomain
 eth0,squash.scalableinformatics.com


=item B<--irq>

This option reports the interrupt assigned to the interface.  It is a 
synonym for the parameter I<irq> which can be used in the 
I<--info=...> option.  A minus sign (B<->) indicates that this interface
does not have an interrupt assigned.  This might be the case for various
network interfaces which do not have hardware drivers attached to them.


 ifinfo --ifname=eth0,lo --info=dev,name,irq

 lo,localhost.localdomain,-
 eth0,squash.scalableinformatics.com,5


=item B<--route>

This option returns a simple representation of the current routing 
tables.


 ifinfo --ifname=eth0,lo --route

 lo:     127.0.0.0/255.0.0.0              -> 0.0.0.0
 eth0:   0.0.0.0/0.0.0.0                  -> 192.168.1.254
         192.168.1.0/255.255.255.0        -> 0.0.0.0
         169.254.0.0/255.255.0.0          -> 0.0.0.0


The route I<0.0.0.0/0.0.0.0> address pointing to a particular IP address
represents the default route.  The masking bits above do not currently
work with this option.  


=head1 FILES

=over 4

/usr/local/bin/ifinfo

=back

=head1 DIAGNOSTICS

B<ifinfo> will emit warning messages  for incorrect parameters.  

=head1 REQUIRES

Perl 5.6.0 or higher, Getopt::Long, POSIX.

=head1 SEE ALSO

ifconfig(8), route(8), netstat(8)

=head1 LICENSE

This code is licensed under GPL version 2.0.  
See http://www.gnu.org/copyleft/gpl.html#SEC1 for specific
details.

=head1 AUTHOR

 Joe Landman 
 landman@scalableinformatics.com
 
L<http://scalableinformatics.com>

=cut
