Refresh Net::Ping to 2.02
Russell Mosemann [Tue, 24 Dec 1996 04:08:16 +0000 (16:08 +1200)]
lib/Net/Ping.pm

index 3ba88d5..91077dd 100644 (file)
 package Net::Ping;
 
-# Authors: karrer@bernina.ethz.ch (Andreas Karrer)
-#          pmarquess@bfsec.bt.co.uk (Paul Marquess)
-
-require 5.002 ;
+# Author:   mose@ccsn.edu (Russell Mosemann)
+#
+# Authors of the original pingecho():
+#           karrer@bernina.ethz.ch (Andreas Karrer)
+#           pmarquess@bfsec.bt.co.uk (Paul Marquess)
+#
+# Copyright (c) 1996 Russell Mosemann.  All rights reserved.  This
+# program is free software; you may redistribute it and/or modify it
+# under the same terms as Perl itself.
+
+require 5.002;
 require Exporter;
 
-use strict ;
-use vars qw(@ISA @EXPORT $VERSION $tcp_proto $echo_port) ;
+use strict;
+use vars qw(@ISA @EXPORT $VERSION
+            $def_timeout $def_proto $max_datasize);
+use FileHandle;
+use Socket qw( SOCK_DGRAM SOCK_STREAM SOCK_RAW PF_INET
+               inet_aton sockaddr_in );
+use Carp;
 
 @ISA = qw(Exporter);
-@EXPORT = qw(ping pingecho);
-$VERSION = 1.01;
-
-use Socket 'PF_INET', 'AF_INET', 'SOCK_STREAM';
-use Carp ;
-
-$tcp_proto = (getprotobyname('tcp'))[2];
-$echo_port = (getservbyname('echo', 'tcp'))[2];
-
-sub ping {
-    croak "ping not implemented yet. Use pingecho()";
-}
+@EXPORT = qw(pingecho);
+$VERSION = 2.02;
 
+# Constants
 
-sub pingecho {
+$def_timeout = 5;           # Default timeout to wait for a reply
+$def_proto = "udp";         # Default protocol to use for pinging
+$max_datasize = 1024;       # Maximum data bytes in a packet
 
-    croak "usage: pingecho host [timeout]" 
-        unless @_ == 1 or @_ == 2 ;
+# Description:  The pingecho() subroutine is provided for backward
+# compatibility with the original Net::Ping.  It accepts a host
+# name/IP and an optional timeout in seconds.  Create a tcp ping
+# object and try pinging the host.  The result of the ping is returned.
 
-    my ($host, $timeout) = @_;
-    my ($saddr, $ip);
-    my ($ret) ;
-    local (*PINGSOCK);
+sub pingecho
+{
+    my ($host,              # Name or IP number of host to ping
+        $timeout            # Optional timeout in seconds
+        ) = @_;
+    my ($p);                # A ping object
 
-    # check if $host is alive by connecting to its echo port, within $timeout
-    # (default 5) seconds. returns 1 if OK, 0 if no answer, 0 if host not found
+    $p = Net::Ping->new("tcp", $timeout);
+    $p->ping($host);        # Going out of scope closes the connection
+}
 
-    $timeout = 5 unless $timeout;
+# Description:  The new() method creates a new ping object.  Optional
+# parameters may be specified for the protocol to use, the timeout in
+# seconds and the size in bytes of additional data which should be
+# included in the packet.
+#   After the optional parameters are checked, the data is constructed
+# and a socket is opened if appropriate.  The object is returned.
+
+sub new
+{
+    my ($this,
+        $proto,             # Optional protocol to use for pinging
+        $timeout,           # Optional timeout in seconds
+        $data_size          # Optional additional bytes of data
+        ) = @_;
+    my  $class = ref($this) || $this;
+    my  $self = {};
+    my ($cnt,               # Count through data bytes
+        $min_datasize       # Minimum data bytes required
+        );
+
+    bless($self, $class);
+
+    $proto = $def_proto unless $proto;          # Determine the protocol
+    croak("Protocol for ping must be \"tcp\", \"udp\" or \"icmp\"")
+        unless $proto =~ m/^(tcp|udp|icmp)$/;
+    $self->{"proto"} = $proto;
+
+    $timeout = $def_timeout unless $timeout;    # Determine the timeout
+    croak("Default timeout for ping must be greater than 0 seconds")
+        if $timeout <= 0;
+    $self->{"timeout"} = $timeout;
+
+    $min_datasize = ($proto eq "udp") ? 1 : 0;  # Determine data size
+    $data_size = $min_datasize unless defined($data_size) && $proto ne "tcp";
+    croak("Data for ping must be from $min_datasize to $max_datasize bytes")
+        if ($data_size < $min_datasize) || ($data_size > $max_datasize);
+    $data_size-- if $self->{"proto"} eq "udp";  # We provide the first byte
+    $self->{"data_size"} = $data_size;
+
+    $self->{"data"} = "";                       # Construct data bytes
+    for ($cnt = 0; $cnt < $self->{"data_size"}; $cnt++)
+    {
+        $self->{"data"} .= chr($cnt % 256);
+    }
+
+    $self->{"seq"} = 0;                         # For counting packets
+    if ($self->{"proto"} eq "udp")              # Open a socket
+    {
+        $self->{"proto_num"} = (getprotobyname('udp'))[2] ||
+            croak("Can't udp protocol by name");
+        $self->{"port_num"} = (getservbyname('echo', 'udp'))[2] ||
+            croak("Can't get udp echo port by name");
+        $self->{"fh"} = FileHandle->new();
+        socket($self->{"fh"}, &PF_INET(), &SOCK_DGRAM(),
+               $self->{"proto_num"}) ||
+            croak("udp socket error - $!");
+    }
+    elsif ($self->{"proto"} eq "icmp")
+    {
+        croak("icmp ping requires root privilege") if $>;
+        $self->{"proto_num"} = (getprotobyname('icmp'))[2] ||
+                    croak("Can't get icmp protocol by name");
+        $self->{"pid"} = $$ & 0xffff;           # Save lower 16 bits of pid
+        $self->{"fh"} = FileHandle->new();
+        socket($self->{"fh"}, &PF_INET(), &SOCK_RAW(), $self->{"proto_num"}) ||
+            croak("icmp socket error - $!");
+    }
+    elsif ($self->{"proto"} eq "tcp")           # Just a file handle for now
+    {
+        $self->{"proto_num"} = (getprotobyname('tcp'))[2] ||
+            croak("Can't get tcp protocol by name");
+        $self->{"port_num"} = (getservbyname('echo', 'tcp'))[2] ||
+            croak("Can't get tcp echo port by name");
+        $self->{"fh"} = FileHandle->new();
+    }
+
+
+    return($self);
+}
 
-    if ($host =~ /^\s*((\d+\.){3}\d+)\s*$/)
-      { $ip = pack ('C4', split (/\./, $1)) }
+# Description: Ping a host name or IP number with an optional timeout.
+# First lookup the host, and return undef if it is not found.  Otherwise
+# perform the specific ping method based on the protocol.  Return the 
+# result of the ping.
+
+sub ping
+{
+    my ($self,
+        $host,              # Name or IP number of host to ping
+        $timeout            # Seconds after which ping times out
+        ) = @_;
+    my ($ip,                # Packed IP number of $host
+        $ret                # The return value
+        );
+
+    croak("Usage: \$p->ping(\$host [, \$timeout])") unless @_ == 2 || @_ == 3;
+    $timeout = $self->{"timeout"} unless $timeout;
+    croak("Timeout must be greater than 0 seconds") if $timeout <= 0;
+
+    $ip = inet_aton($host);
+    return(undef) unless defined($ip);      # Does host exist?
+
+    if ($self->{"proto"} eq "udp")
+    {
+        $ret = $self->ping_udp($ip, $timeout);
+    }
+    elsif ($self->{"proto"} eq "icmp")
+    {
+        $ret = $self->ping_icmp($ip, $timeout);
+    }
+    elsif ($self->{"proto"} eq "tcp")
+    {
+        $ret = $self->ping_tcp($ip, $timeout);
+    }
     else
-      { $ip = (gethostbyname($host))[4] }
-
-    return 0 unless $ip;               # "no such host"
+    {
+        croak("Unknown protocol \"$self->{proto}\" in ping()");
+    }
+    return($ret);
+}
 
-    $saddr = pack('S n a4 x8', AF_INET, $echo_port, $ip);
-    $SIG{'ALRM'} = sub { die } ;
-    alarm($timeout);
-    
+sub ping_icmp
+{
+    my ($self,
+        $ip,                # Packed IP number of the host
+        $timeout            # Seconds after which ping times out
+        ) = @_;
+
+    my $ICMP_ECHOREPLY = 0; # ICMP packet types
+    my $ICMP_ECHO = 8;
+    my $icmp_struct = "C2 S3 A";  # Structure of a minimal ICMP packet
+    my $subcode = 0;        # No ICMP subcode for ECHO and ECHOREPLY
+    my $flags = 0;          # No special flags when opening a socket
+    my $port = 0;           # No port with ICMP
+
+    my ($saddr,             # sockaddr_in with port and ip
+        $checksum,          # Checksum of ICMP packet
+        $msg,               # ICMP packet to send
+        $len_msg,           # Length of $msg
+        $rbits,             # Read bits, filehandles for reading
+        $nfound,            # Number of ready filehandles found
+        $finish_time,       # Time ping should be finished
+        $done,              # set to 1 when we are done
+        $ret,               # Return value
+        $recv_msg,          # Received message including IP header
+        $from_saddr,        # sockaddr_in of sender
+        $from_port,         # Port packet was sent from
+        $from_ip,           # Packed IP of sender
+        $from_type,         # ICMP type
+        $from_subcode,      # ICMP subcode
+        $from_chk,          # ICMP packet checksum
+        $from_pid,          # ICMP packet id
+        $from_seq,          # ICMP packet sequence
+        $from_msg           # ICMP message
+        );
+
+    $self->{"seq"} = ($self->{"seq"} + 1) % 65536; # Increment sequence
+    $checksum = 0;                          # No checksum for starters
+    $msg = pack($icmp_struct . $self->{"data_size"}, $ICMP_ECHO, $subcode,
+                $checksum, $self->{"pid"}, $self->{"seq"}, $self->{"data"});
+    $checksum = Net::Ping->checksum($msg);
+    $msg = pack($icmp_struct . $self->{"data_size"}, $ICMP_ECHO, $subcode,
+                $checksum, $self->{"pid"}, $self->{"seq"}, $self->{"data"});
+    $len_msg = length($msg);
+    $saddr = sockaddr_in($port, $ip);
+    send($self->{"fh"}, $msg, $flags, $saddr); # Send the message
+
+    $rbits = "";
+    vec($rbits, $self->{"fh"}->fileno(), 1) = 1;
     $ret = 0;
+    $done = 0;
+    $finish_time = time() + $timeout;       # Must be done by this time
+    while (!$done && $timeout > 0)          # Keep trying if we have time
+    {
+        $nfound = select($rbits, undef, undef, $timeout); # Wait for packet
+        $timeout = $finish_time - time();   # Get remaining time
+        if (!defined($nfound))              # Hmm, a strange error
+        {
+            $ret = undef;
+            $done = 1;
+        }
+        elsif ($nfound)                     # Got a packet from somewhere
+        {
+            $recv_msg = "";
+            $from_saddr = recv($self->{"fh"}, $recv_msg, 1500, $flags);
+            ($from_port, $from_ip) = sockaddr_in($from_saddr);
+            ($from_type, $from_subcode, $from_chk,
+             $from_pid, $from_seq, $from_msg) =
+                unpack($icmp_struct . $self->{"data_size"},
+                       substr($recv_msg, length($recv_msg) - $len_msg,
+                              $len_msg));
+            if (($from_type == $ICMP_ECHOREPLY) &&
+                ($from_ip eq $ip) &&
+                ($from_pid == $self->{"pid"}) && # Does the packet check out?
+                ($from_seq == $self->{"seq"}))
+            {
+                $ret = 1;                   # It's a winner
+                $done = 1;
+            }
+        }
+        else                                # Oops, timed out
+        {
+            $done = 1;
+        }
+    }
+    return($ret)
+}
+
+# Description:  Do a checksum on the message.  Basically sum all of
+# the short words and fold the high order bits into the low order bits.
+
+sub checksum
+{
+    my ($class,
+        $msg            # The message to checksum
+        ) = @_;
+    my ($len_msg,       # Length of the message
+        $num_short,     # The number of short words in the message
+        $short,         # One short word
+        $chk            # The checksum
+        );
+
+    $len_msg = length($msg);
+    $num_short = $len_msg / 2;
+    $chk = 0;
+    foreach $short (unpack("S$num_short", $msg))
+    {
+        $chk += $short;
+    }                                           # Add the odd byte in
+    $chk += unpack("C", substr($msg, $len_msg - 1, 1)) if $len_msg % 2;
+    $chk = ($chk >> 16) + ($chk & 0xffff);      # Fold high into low
+    return(~(($chk >> 16) + $chk) & 0xffff);    # Again and complement
+}
+
+# Description:  Perform a tcp echo ping.  Since a tcp connection is
+# host specific, we have to open and close each connection here.  We
+# can't just leave a socket open.  Because of the robust nature of
+# tcp, it will take a while before it gives up trying to establish a
+# connection.  Therefore, we have to set the alarm to break out of the
+# connection sooner if the timeout expires.  No data bytes are actually
+# sent since the successful establishment of a connection is proof
+# enough of the reachability of the remote host.  Also, tcp is
+# expensive and doesn't need our help to add to the overhead.
+
+sub ping_tcp
+{
+    my ($self,
+        $ip,                # Packed IP number of the host
+        $timeout            # Seconds after which ping times out
+        ) = @_;
+    my ($saddr,             # sockaddr_in with port and ip
+        $ret                # The return value
+        );
+                            
+    socket($self->{"fh"}, &PF_INET(), &SOCK_STREAM(), $self->{"proto_num"}) ||
+        croak("tcp socket error - $!");
+    $saddr = sockaddr_in($self->{"port_num"}, $ip);
+
+    $SIG{'ALRM'} = sub { die };
+    alarm($timeout);        # Interrupt connect() if we have to
+            
+    $ret = 0;               # Default to unreachable
     eval <<'EOM' ;
-    return unless socket(PINGSOCK, PF_INET, SOCK_STREAM, $tcp_proto) ;
-    return unless connect(PINGSOCK, $saddr) ;
-    $ret=1 ;
+        return unless connect($self->{"fh"}, $saddr);
+        $ret = 1;
 EOM
     alarm(0);
-    close(PINGSOCK);
-    $ret;
+    $self->{"fh"}->close();
+    return($ret);
+}
+
+# Description:  Perform a udp echo ping.  Construct a message of
+# at least the one-byte sequence number and any additional data bytes.
+# Send the message out and wait for a message to come back.  If we
+# get a message, make sure all of its parts match.  If they do, we are
+# done.  Otherwise go back and wait for the message until we run out
+# of time.  Return the result of our efforts.
+
+sub ping_udp
+{
+    my ($self,
+        $ip,                # Packed IP number of the host
+        $timeout            # Seconds after which ping times out
+        ) = @_;
+
+    my $flags = 0;          # Nothing special on open
+
+    my ($saddr,             # sockaddr_in with port and ip
+        $ret,               # The return value
+        $msg,               # Message to be echoed
+        $finish_time,       # Time ping should be finished
+        $done,              # Set to 1 when we are done pinging
+        $rbits,             # Read bits, filehandles for reading
+        $nfound,            # Number of ready filehandles found
+        $from_saddr,        # sockaddr_in of sender
+        $from_msg,          # Characters echoed by $host
+        $from_port,         # Port message was echoed from
+        $from_ip            # Packed IP number of sender
+        );
+
+    $saddr = sockaddr_in($self->{"port_num"}, $ip);
+    $self->{"seq"} = ($self->{"seq"} + 1) % 256;    # Increment sequence
+    $msg = chr($self->{"seq"}) . $self->{"data"};   # Add data if any
+    send($self->{"fh"}, $msg, $flags, $saddr);      # Send it
+
+    $rbits = "";
+    vec($rbits, $self->{"fh"}->fileno(), 1) = 1;
+    $ret = 0;                   # Default to unreachable
+    $done = 0;
+    $finish_time = time() + $timeout;       # Ping needs to be done by then
+    while (!$done && $timeout > 0)
+    {
+        $nfound = select($rbits, undef, undef, $timeout); # Wait for response
+        $timeout = $finish_time - time();   # Get remaining time
+
+        if (!defined($nfound))  # Hmm, a strange error
+        {
+            $ret = undef;
+            $done = 1;
+        }
+        elsif ($nfound)         # A packet is waiting
+        {
+            $from_msg = "";
+            $from_saddr = recv($self->{"fh"}, $from_msg, 1500, $flags);
+            ($from_port, $from_ip) = sockaddr_in($from_saddr);
+            if (($from_ip eq $ip) &&        # Does the packet check out?
+                ($from_port == $self->{"port_num"}) &&
+                ($from_msg eq $msg))
+            {
+                $ret = 1;       # It's a winner
+                $done = 1;
+            }
+        }
+        else                    # Oops, timed out
+        {
+            $done = 1;
+        }
+    }
+    return($ret);
 }   
 
+# Description:  Close the connection unless we are using the tcp
+# protocol, since it will already be closed.
+
+sub close
+{
+    my ($self) = @_;
+
+    $self->{"fh"}->close() unless $self->{"proto"} eq "tcp";
+}
+
+
 1;
 __END__
 
-=cut
-
 =head1 NAME
 
-Net::Ping, pingecho - check a host for upness
+Net::Ping - check a remote host for reachability
 
 =head1 SYNOPSIS
 
     use Net::Ping;
-    print "'jimmy' is alive and kicking\n" if pingecho('jimmy', 10) ;
-
-=head1 DESCRIPTION
 
-This module contains routines to test for the reachability of remote hosts.
-Currently the only routine implemented is pingecho(). 
+    $p = Net::Ping->new();
+    print "$host is alive.\n" if $p->ping($host);
+    $p->close();
+
+    $p = Net::Ping->new("icmp");
+    foreach $host (@host_array)
+    {
+        print "$host is ";
+        print "NOT " unless $p->ping($host, 2);
+        print "reachable.\n";
+        sleep(1);
+    }
+    $p->close();
+    
+    $p = Net::Ping->new("tcp", 2);
+    while ($stop_time > time())
+    {
+        print "$host not reachable ", scalar(localtime()), "\n"
+            unless $p->ping($host);
+        sleep(300);
+    }
+    undef($p);
+    
+    # For backward compatibility
+    print "$host is alive.\n" if pingecho($host);
 
-pingecho() uses a TCP echo (I<not> an ICMP one) to determine if the
-remote host is reachable. This is usually adequate to tell that a remote
-host is available to rsh(1), ftp(1), or telnet(1) onto.
+=head1 DESCRIPTION
 
-=head2 Parameters
+This module contains methods to test the reachability of remote
+hosts on a network.  A ping object is first created with optional
+parameters, a variable number of hosts may be pinged multiple
+times and then the connection is closed.
+
+You may choose one of three different protocols to use for the ping.
+With the "tcp" protocol the ping() method attempts to establish a
+connection to the remote host's echo port.  If the connection is
+successfully established, the remote host is considered reachable.  No
+data is actually echoed.  This protocol does not require any special
+privileges but has higher overhead than the other two protocols.
+
+Specifying the "udp" protocol causes the ping() method to send a udp
+packet to the remote host's echo port.  If the echoed packet is
+received from the remote host and the received packet contains the
+same data as the packet that was sent, the remote host is considered
+reachable.  This protocol does not require any special privileges.
+
+If the "icmp" protocol is specified, the ping() method sends an icmp
+echo message to the remote host, which is what the UNIX ping program
+does.  If the echoed message is received from the remote host and
+the echoed information is correct, the remote host is considered
+reachable.  Specifying the "icmp" protocol requires that the program
+be run as root or that the program be setuid to root.
+
+=head2 Functions
+
+=over 4
+
+=item Net::Ping->new([$proto [, $def_timeout [, $bytes]]]);
+
+Create a new ping object.  All of the parameters are optional.  $proto
+specifies the protocol to use when doing a ping.  The current choices
+are "tcp", "udp" or "icmp".  The default is "udp".
+
+If a default timeout ($def_timeout) in seconds is provided, it is used
+when a timeout is not given to the ping() method (below).  The timeout
+must be greater than 0 and the default, if not specified, is 5 seconds.
+
+If the number of data bytes ($bytes) is given, that many data bytes
+are included in the ping packet sent to the remote host. The number of
+data bytes is ignored if the protocol is "tcp".  The minimum (and
+default) number of data bytes is 1 if the protocol is "udp" and 0
+otherwise.  The maximum number of data bytes that can be specified is
+1024.
+
+=item $p->ping($host [, $timeout]);
+
+Ping the remote host and wait for a response.  $host can be either the
+hostname or the IP number of the remote host.  The optional timeout
+must be greater than 0 seconds and defaults to whatever was specified
+when the ping object was created.  If the hostname cannot be found or
+there is a problem with the IP number, undef is returned.  Otherwise,
+1 is returned if the host is reachable and 0 if it is not.  For all
+practical purposes, undef and 0 and can be treated as the same case.
+
+=item $p->close();
+
+Close the network connection for this ping object.  The network
+connection is also closed by "undef $p".  The network connection is
+automatically closed if the ping object goes out of scope (e.g. $p is
+local to a subroutine and you leave the subroutine).
+
+=item pingecho($host [, $timeout]);
+
+To provide backward compatibility with the previous version of
+Net::Ping, a pingecho() subroutine is available with the same
+functionality as before.  pingecho() uses the tcp protocol.  The
+return values and parameters are the same as described for the ping()
+method.  This subroutine is obsolete and may be removed in a future
+version of Net::Ping.
 
-=over 5
+=back
 
-=item hostname
+=head1 WARNING
 
-The remote host to check, specified either as a hostname or as an IP address.
+pingecho() or a ping object with the tcp protocol use alarm() to
+implement the timeout.  So, don't use alarm() in your program while
+you are using pingecho() or a ping object with the tcp protocol.  The
+udp and icmp protocols do not use alarm() to implement the timeout.
 
-=item timeout
+=head1 NOTES
 
-The timeout in seconds. If not specified it will default to 5 seconds.
+There will be less network overhead (and some efficiency in your
+program) if you specify either the udp or the icmp protocol.  The tcp
+protocol will generate 2.5 times or more traffic for each ping than
+either udp or icmp.  If many hosts are pinged frequently, you may wish
+to implement a small wait (e.g. 25ms or more) between each ping to
+avoid flooding your network with packets.
 
-=back
+The icmp protocol requires that the program be run as root or that it
+be setuid to root.  The tcp and udp protocols do not require special
+privileges, but not all network devices implement the echo protocol
+for tcp or udp.
 
-=head1 WARNING
+Local hosts should normally respond to pings within milliseconds.
+However, on a very congested network it may take up to 3 seconds or
+longer to receive an echo packet from the remote host.  If the timeout
+is set too low under these conditions, it will appear that the remote
+host is not reachable (which is almost the truth).
 
-pingecho() uses alarm to implement the timeout, so don't set another alarm
-while you are using it.
+Reachability doesn't necessarily mean that the remote host is actually
+functioning beyond its ability to echo packets.
 
+Because of a lack of anything better, this module uses its own
+routines to pack and unpack ICMP packets.  It would be better for a
+separate module to be written which understands all of the different
+kinds of ICMP packets.
 
+=cut