File Coverage

blib/lib/Net/Async/Ping/ICMP.pm
Criterion Covered Total %
statement 111 121 91.7
branch 22 38 57.8
condition 5 12 41.6
subroutine 24 24 100.0
pod 1 2 50.0
total 163 197 82.7


line stmt bran cond sub pod time code
1             package Net::Async::Ping::ICMP;
2             $Net::Async::Ping::ICMP::VERSION = '0.001001';
3 1     1   1231 use Moo;
  1         8  
  1         30  
4 1     1   552 use warnings NONFATAL => 'all';
  1         2  
  1         91  
5              
6 1     1   8 use Future;
  1         8  
  1         42  
7 1     1   6 use POSIX 'ECONNREFUSED';
  1         2  
  1         26  
8 1     1   150 use Time::HiRes;
  1         2  
  1         19  
9 1     1   193 use Carp;
  1         1  
  1         94  
10 1     1   1378 use Net::Ping;
  1         17538  
  1         91  
11 1     1   886 use IO::Async::Socket;
  1         1479  
  1         54  
12              
13 1     1   8 use Socket qw( SOCK_RAW SOCK_DGRAM AF_INET NI_NUMERICHOST inet_aton pack_sockaddr_in unpack_sockaddr_in getnameinfo inet_ntop);
  1         2  
  1         145  
14              
15 1     1   7 use constant ICMP_ECHOREPLY => 0; # ICMP packet types
  1         8  
  1         85  
16 1     1   6 use constant ICMP_UNREACHABLE => 3; # ICMP packet types
  1         2  
  1         59  
17 1     1   6 use constant ICMP_ECHO => 8;
  1         2  
  1         58  
18 1     1   7 use constant ICMP_TIME_EXCEEDED => 11; # ICMP packet types
  1         2  
  1         57  
19 1     1   12 use constant ICMP_PARAMETER_PROBLEM => 12; # ICMP packet types
  1         3  
  1         63  
20 1     1   5 use constant ICMP_STRUCT => "C2 n3 A"; # Structure of a minimal ICMP packet
  1         10  
  1         64  
21 1     1   5 use constant SUBCODE => 0; # No ICMP subcode for ECHO and ECHOREPLY
  1         2  
  1         64  
22 1     1   5 use constant ICMP_FLAGS => 0; # No special flags for send or recv
  1         2  
  1         63  
23 1     1   11 use constant ICMP_PORT => 0; # No port with ICMP
  1         2  
  1         79  
24              
25             extends 'IO::Async::Notifier';
26              
27 1     1   13 use namespace::clean;
  1         2  
  1         26  
28              
29             has default_timeout => (
30             is => 'ro',
31             default => 5,
32             );
33              
34             has bind => ( is => 'rw' );
35              
36             has _pid => (
37             is => 'lazy',
38             );
39              
40             sub _build__pid
41 2     2   58 { my $self = shift;
42 2         13 $$ & 0xffff;
43             }
44              
45             has seq => (
46             is => 'ro',
47             default => 1,
48             );
49              
50             # Whether to try and use ping sockets. This option used in tests
51             # to force normal ping to be used
52             has use_ping_socket => (
53             is => 'ro',
54             default => 1,
55             );
56              
57             # Overrides method in IO::Async::Notifier to allow specific options in this class
58             sub configure_unknown
59 2     2 1 1649 { my $self = shift;
60 2         7 my %params = @_;
61 2         11 delete $params{$_} foreach qw/default_timeout bind seq use_ping_socket/;
62 2 50       15 return unless keys %params;
63 0         0 my $class = ref $self;
64 0         0 croak "Unrecognised configuration keys for $class - " . join( " ", keys %params );
65              
66             }
67              
68             sub ping {
69 6     6 0 2008771 my $self = shift;
70             # Maintain compat with old API
71 6         16 my $legacy = ref $_[0] eq 'IO::Async::Loop::Poll';
72 6 100       29 my $loop = $legacy ? shift : $self->loop;
73              
74 6         23 my ($host, $timeout) = @_;
75 6   33     46 $timeout //= $self->default_timeout;
76              
77 6         25 my $t0 = [Time::HiRes::gettimeofday];
78              
79 6         47 my $fh = IO::Socket->new;
80 6   33     827 my $proto_num = (getprotobyname('icmp'))[2] ||
81             croak("Can't get icmp protocol by name");
82             # Let's try a ping socket (unprivileged ping) first. See
83             # https://lwn.net/Articles/422330/
84 6         12 my ($ping_socket, $ident);
85 6 50 33     39 if ($self->use_ping_socket && socket($fh, AF_INET, SOCK_DGRAM, $proto_num))
86             {
87 0         0 $ping_socket = 1;
88 0         0 ($ident) = unpack_sockaddr_in getsockname($fh);
89             }
90             else {
91 6 50       146 socket($fh, AF_INET, SOCK_RAW, $proto_num) ||
92             croak("Unable to create ICMP socket ($!). Are you running as root?"
93             ." If not, and your system supports ping sockets, try setting"
94             ." /proc/sys/net/ipv4/ping_group_range");
95 6         229 $ident = $self->_pid;
96             }
97              
98 6 50       50 if ($self->bind)
99             {
100 0         0 my $bind = pack_sockaddr_in 0, inet_aton $self->bind;
101 0 0       0 bind $fh, $bind
102             or croak "Failed to bind to ".$self->bind;
103             }
104              
105             $loop->resolver->getaddrinfo(
106             host => $host,
107             protocol => $proto_num,
108             family => AF_INET,
109             )->then( sub {
110              
111 4     4   7202 my $saddr = $_[0]->{addr};
112 4         17 my $f = $loop->new_future;
113              
114             my $socket = IO::Async::Socket->new(
115             handle => $fh,
116             on_recv_error => sub {
117 0         0 my ( $self, $errno ) = @_;
118 0         0 $f->fail('Receive error');
119             },
120 4         121 );
121              
122             my $on_recv = $self->_capture_weakself(
123             sub {
124 6 50       2287 my $ping = shift or return; # weakref, may have disappeared
125 6         11 my ( $self, $recv_msg, $from_saddr ) = @_;
126              
127 6         11 my $from_pid = -1;
128 6         8 my $from_seq = -1;
129 6         24 my ($from_port, $from_ip) = unpack_sockaddr_in($from_saddr);
130 6 50       19 my $offset = $ping_socket ? 0 : 20; # No offset needed for ping sockets
131 6         23 my ($from_type, $from_subcode) = unpack("C2", substr($recv_msg, $offset, 2));
132              
133 6 100       17 if ($from_type == ICMP_ECHOREPLY) {
134 3 50       23 ($from_pid, $from_seq) = unpack("n3", substr($recv_msg, $offset + 4, 4))
135             if length $recv_msg >= $offset + 8;
136             } else {
137 3 50       10 ($from_pid, $from_seq) = unpack("n3", substr($recv_msg, $offset + 32, 4))
138             if length $recv_msg >= $offset + 36;
139             }
140              
141             # Not needed for ping socket - kernel handles this for us
142 6 100 66     305 return if !$ping_socket && $from_pid != $ping->_pid;
143 3 50       38 return if $from_seq != $ping->seq;
144 3 50       18 if ($from_type == ICMP_ECHOREPLY) {
    0          
    0          
145 3         12 my $ip = unpack_sockaddr_in($saddr);
146 3 100       37 return if inet_ntop(AF_INET, $from_ip) ne inet_ntop(AF_INET, $ip); # Does the packet check out?
147 2         16 $f->done;
148             } elsif ($from_type == ICMP_UNREACHABLE) {
149 0         0 $f->fail('ICMP Unreachable');
150             } elsif ($from_type == ICMP_TIME_EXCEEDED) {
151 0         0 $f->fail('ICMP Timeout');
152             }
153 2 100       1800 $legacy ? $loop->remove($socket) : $ping->remove_child($socket);
154             },
155 4         683 );
156              
157 4         42 $socket->configure(on_recv => $on_recv);
158 4 100       175 $legacy ? $loop->add($socket) : $self->add_child($socket);
159 4         807 $socket->send( $self->_msg($ident), ICMP_FLAGS, $saddr );
160              
161             Future->wait_any(
162             $f,
163             $loop->timeout_future(after => $timeout)
164             )
165             ->then(
166 2         311 sub { Future->done(Time::HiRes::tv_interval($t0)) }
167             )
168 6         30 });
  4         481  
169             }
170              
171             sub _msg
172 4     4   8 { my ($self, $ident) = @_;
173             # data_size to be implemented later
174 4         5 my $data_size = 0;
175 4         7 my $data = '';
176 4         5 my $checksum = 0;
177 4         31 my $msg = pack(ICMP_STRUCT . $data_size, ICMP_ECHO, SUBCODE,
178             $checksum, $ident, $self->seq, $data);
179 4         32 $checksum = Net::Ping->checksum($msg);
180 4         96 $msg = pack(ICMP_STRUCT . $data_size, ICMP_ECHO, SUBCODE,
181             $checksum, $ident, $self->seq, $data);
182 4         21 return $msg;
183             }
184              
185             1;
186              
187             __END__