Skip to content

Instantly share code, notes, and snippets.

@jhthorsen
Created February 20, 2015 11:05
Show Gist options
  • Save jhthorsen/41ba7d99000505d76152 to your computer and use it in GitHub Desktop.
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
#!/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