Created
February 20, 2015 11:05
-
-
Save jhthorsen/41ba7d99000505d76152 to your computer and use it in GitHub Desktop.
This program can read a list of IP's from STDIN and ping them non-blocking
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/perl | |
# Usage: echo -en "1.1.0.1\n127.0.0.1\n" | sudo /usr/bin/perl stdinping | |
# A lot of this code is mostly copy/paste/modify from Net::Ping | |
BEGIN { | |
# Make sure we don't load code that is not owned by root | |
$^X eq "/usr/bin/perl" or die "Invalid perl: $^X\n"; | |
my @perl_stat = stat $^X; | |
unshift @INC, sub { | |
my ($code, $module) = @_; | |
for my $dir (@INC) { | |
my @lib_stat = stat "$dir/$module" or next; | |
$lib_stat[4] == $perl_stat[4] and next; | |
die "Cannot load alien library $dir/$module\n"; | |
} | |
return; | |
}; | |
} | |
use strict; | |
use warnings; | |
use Socket; | |
use Time::HiRes; | |
use constant DATA_SIZE => $ENV{STDINPING_DATA_SIZE} || 56; | |
use constant DEBUG => $ENV{STDINPING_DEBUG} || 0; | |
use constant ICMP_ECHO => 8; | |
use constant ICMP_ECHOREPLY => 0; # ICMP packet types | |
use constant ICMP_FLAGS => 0; # No special flags for send or recv | |
use constant ICMP_PORT => 0; # No port with ICMP | |
use constant ICMP_UNREACHABLE => 3; # ICMP packet types | |
use constant ICMP_TIME_EXCEEDED => 11; # ICMP packet types | |
use constant ICMP_PARAMETER_PROBLEM => 12; # ICMP packet types | |
use constant ICMP_STRUCT => 'C2 n3 A'; # Structure of a minimal ICMP packet | |
use constant SUBCODE => 0; # No ICMP subcode for ECHO and ECHOREPLY | |
use constant MONOTONIC => eval { !!Time::HiRes::clock_gettime(Time::HiRes::CLOCK_MONOTONIC()) }; | |
my $SOCK = create_icmp_socket(); | |
my $RIN_BITS = rin_bits(\*STDIN, $SOCK); | |
my $PID = $$ & 0xffff; | |
my $ICMP_DATA = join '', map { chr($_ % 256) } 0 .. DATA_SIZE - 1; | |
my $SEQ = 0; | |
my %IN_FLIGHT; | |
main(); | |
exit 0; | |
sub checksum { | |
my $len_msg = length $_[0]; | |
my $chk = 0; | |
$chk += $_ for unpack sprintf('n%d', $len_msg / 2), $_[0]; | |
$chk += unpack("C", substr($_[0], $len_msg - 1, 1)) << 8 if $len_msg % 2; | |
$chk = ($chk >> 16) + ($chk & 0xffff); | |
return ~(($chk >> 16) + $chk) & 0xffff; | |
} | |
sub create_icmp_socket { | |
my $icmp_num = (getprotobyname 'icmp')[2] or die "Could not find icmp proto number\n"; | |
socket my $SOCK, PF_INET, SOCK_RAW, $icmp_num or die "Socket error: $!\n"; | |
#setsockopt $SOCK, IPPROTO_IP, IP_TOS, pack('I*', $args{tos}) if $args{tos} =~ /^\d+$/; | |
#setsockopt $SOCK, IPPROTO_IP, IP_TTL, pack('I*', $args{ttl}) if $args{ttl} =~ /^\d+$/; | |
return $SOCK; | |
} | |
sub icmp_receive { | |
my $addr = recv($SOCK, my ($buf), 1500, ICMP_FLAGS); | |
my $now = steady_time(); | |
my ($port, $ip) = sockaddr_in $addr; | |
my ($type, $subcode) = unpack 'C2', substr($buf, 20, 2); | |
my ($pid, $seq, $args) = (-1, -1, undef); | |
$ip = inet_ntoa($ip); | |
$args = $IN_FLIGHT{$ip} or return; | |
if ($type == ICMP_ECHOREPLY) { | |
($pid, $seq) = unpack 'n3', substr($buf, 24, 4) if length $buf >= 28; | |
} | |
else { | |
($pid, $seq) = unpack 'n3', substr($buf, 52, 4) if length $buf >= 56; | |
} | |
if ($pid != $PID or $seq != $args->{seq}) { | |
warn "[$ip] !!! $pid == $PID, $seq == $args->{seq}\n" if DEBUG; | |
return; | |
} | |
delete $IN_FLIGHT{$ip}; | |
return printf qq({"id":"%s","rtt":%.6f}\n), $args->{id}, $now - $args->{start} if $type == ICMP_ECHOREPLY; | |
return printf qq({"id":"%s","err":"ICMP_TIME_EXCEEDED"}\n), $args->{id} if $type == ICMP_TIME_EXCEEDED; | |
return printf qq({"id":"%s","err":"ICMP_UNREACHABLE"}\n), $args->{id} if $type == ICMP_UNREACHABLE; | |
return printf qq({"id":"%s","err":"ICMP_PARAMETER_PROBLEM"}\n), $args->{id} if $type == ICMP_PARAMETER_PROBLEM; | |
return printf qq({"id":"%s","err":"UNKNOWN"}\n), $args->{id}; | |
} | |
sub icmp_send { | |
my $args = readline STDIN; | |
vec($RIN_BITS, fileno(STDIN), 1) = 0 if eof STDIN; # do not want to select() STDIN after end of file | |
chomp $args; | |
warn "[icmp_send] <<< $args\n" if DEBUG; | |
my @args = (ip => split /\|/, $args); | |
my %args; | |
return if @args % 2; # invalid hash | |
%args = @args; | |
return if $IN_FLIGHT{$args{ip}}; | |
eval { | |
$args{id} ||= $args{ip}; | |
$args{seq} = next_seq(); | |
$args{start} = steady_time(); | |
$args{timeout} ||= 1; | |
my $addr = sockaddr_in ICMP_PORT, inet_aton($args{ip}) or die "Invalid IP.\n"; | |
my $checksum = 0; | |
my $msg = pack ICMP_STRUCT . DATA_SIZE, ICMP_ECHO, SUBCODE, $checksum, $PID, $args{seq}, $ICMP_DATA; | |
$checksum = checksum($msg); | |
$msg = pack ICMP_STRUCT . DATA_SIZE, ICMP_ECHO, SUBCODE, $checksum, $PID, $args{seq}, $ICMP_DATA; | |
warn "[$args{ip}] ICMP_ECHO $args{seq}\n" if DEBUG; | |
send $SOCK, $msg, ICMP_FLAGS, $addr or die "Send: $!"; | |
$IN_FLIGHT{$args{ip}} = \%args; | |
} or do { | |
$@ =~ s! at \S+.*!! unless DEBUG; | |
$@ =~ s!"!'!g; | |
chomp $@; | |
print qq({"id":"$args{id}","err":"$@"}\n); | |
}; | |
} | |
sub main { | |
while (1) { | |
my $rout_bits; | |
if (0 < select(($rout_bits = $RIN_BITS), undef, undef, 0.2)) { | |
steady_time(); | |
icmp_receive() if vec($rout_bits, fileno($SOCK), 1); | |
icmp_send() if vec($rout_bits, fileno(STDIN), 1); | |
} | |
else { | |
my $now = steady_time(); | |
for my $id (keys %IN_FLIGHT) { | |
next if $IN_FLIGHT{$id}{start} + $IN_FLIGHT{$id}{timeout} > $now; | |
my $args = delete $IN_FLIGHT{$id}; | |
print qq({"id":"$args->{id}","err":"ICMP_TIME_EXCEEDED"}\n); | |
} | |
} | |
} | |
} | |
sub next_seq { | |
$SEQ = ($SEQ + 1) % 65536; | |
} | |
sub rin_bits { | |
my $bits = ''; | |
vec($bits, fileno($_), 1) = 1 for @_; | |
return $bits; | |
} | |
sub steady_time { | |
MONOTONIC ? Time::HiRes::clock_gettime(Time::HiRes::CLOCK_MONOTONIC()) : Time::HiRes::time; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment