File Coverage

blib/lib/Net/Async/Ping/ICMP.pm
Criterion Covered Total %
statement 127 165 76.9
branch 28 72 38.8
condition 5 15 33.3
subroutine 27 29 93.1
pod 2 2 100.0
total 189 283 66.7


line stmt bran cond sub pod time code
1             package Net::Async::Ping::ICMP;
2             $Net::Async::Ping::ICMP::VERSION = '0.004001';
3 1     1   1418 use Moo;
  1         6682  
  1         10  
4 1     1   1254 use warnings NONFATAL => 'all';
  1         2  
  1         62  
5              
6 1     1   590 use Future;
  1         7733  
  1         56  
7 1     1   10 use Time::HiRes;
  1         2  
  1         6  
8 1     1   106 use Carp qw( croak );
  1         3  
  1         66  
9 1     1   1725 use Net::Ping qw();
  1         14810  
  1         43  
10 1     1   12 use IO::Socket;
  1         2  
  1         17  
11 1     1   1293 use IO::Async::Socket;
  1         9371  
  1         34  
12 1     1   8 use Scalar::Util qw/blessed/;
  1         3  
  1         50  
13 1         80 use Socket qw(
14             SOCK_RAW SOCK_DGRAM AF_INET IPPROTO_ICMP NI_NUMERICHOST NIx_NOSERV
15             inet_aton pack_sockaddr_in unpack_sockaddr_in getnameinfo inet_ntop
16 1     1   5 );
  1         2  
17 1     1   476 use Net::Frame::Layer::IPv4 qw(:consts);
  1         77988  
  1         203  
18              
19 1     1   9 use constant ICMP_ECHOREPLY => 0; # ICMP packet types
  1         3  
  1         52  
20 1     1   6 use constant ICMP_UNREACHABLE => 3; # ICMP packet types
  1         2  
  1         35  
21 1     1   5 use constant ICMP_ECHO => 8;
  1         1  
  1         41  
22 1     1   6 use constant ICMP_TIME_EXCEEDED => 11; # ICMP packet types
  1         2  
  1         49  
23 1     1   5 use constant ICMP_STRUCT => "C2 n3 A"; # Structure of a minimal ICMP packet
  1         2  
  1         35  
24 1     1   4 use constant SUBCODE => 0; # No ICMP subcode for ECHO and ECHOREPLY
  1         2  
  1         44  
25 1     1   6 use constant ICMP_FLAGS => 0; # No special flags for send or recv
  1         2  
  1         55  
26              
27             extends 'IO::Async::Notifier';
28              
29 1     1   6 use namespace::clean;
  1         2  
  1         17  
30              
31             has default_timeout => (
32             is => 'ro',
33             default => 5,
34             );
35              
36             has bind => ( is => 'rw' );
37              
38             has _is_raw_socket_setup_done => (
39             is => 'rw',
40             default => 0,
41             );
42              
43             has _raw_socket => (
44             is => 'lazy',
45             );
46              
47             sub _build__raw_socket {
48 2     2   43 my $self = shift;
49              
50 2         43 my $fh = IO::Socket->new;
51 2 50       380 $fh->socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) ||
52             croak("Unable to create raw socket ($!). Are you running as root?"
53             ." If not, and your system supports ping sockets, try setting"
54             ." /proc/sys/net/ipv4/ping_group_range");
55              
56 2 50       182 if ($self->bind) {
57 0 0       0 $fh->bind(pack_sockaddr_in 0, inet_aton $self->bind)
58             or croak "Failed to bind to ".$self->bind;
59             }
60              
61             my $on_recv = $self->_capture_weakself(sub {
62 8 50   8   6332 my $self = shift or return; # weakref, may have disappeared
63 8         22 my ( undef, $recv_msg, $from_saddr ) = @_;
64              
65 8         39 my $from_data = $self->_parse_icmp_packet($recv_msg, $from_saddr, 20);
66             return
67 8 100 66     68 unless defined $from_data && ref $from_data eq 'HASH';
68              
69             # ignore received packets which are not a response to one of
70             # our echo requests
71 4         26 my $f = $self->_raw_socket_queue->{$from_data->{ip}};
72             return
73             unless defined $f
74             && $from_data->{id} == $self->_pid
75 4 50 33     133 && $from_data->{seq} == $self->seq;
      33        
76              
77 4 50       74 if ($from_data->{type} == ICMP_ECHOREPLY) {
    0          
    0          
78 4         85 $f->done;
79             }
80             elsif ($from_data->{type} == ICMP_UNREACHABLE) {
81 0         0 $f->fail('ICMP Unreachable');
82             }
83             elsif ($from_data->{type} == ICMP_TIME_EXCEEDED) {
84 0         0 $f->fail('ICMP Timeout');
85             }
86 2         42 });
87              
88             my $socket = IO::Async::Socket->new(
89             handle => $fh,
90             on_send_error => sub {
91 0     0   0 my ( $self, $errno ) = @_;
92 0         0 warn "Send error: $errno\n";
93             },
94             on_recv_error => sub {
95 0     0   0 my ( $self, $errno ) = @_;
96 0         0 warn "Receive error: $errno\n";
97             },
98 2         92 on_recv => $on_recv,
99             );
100              
101 2         577 return $socket;
102             }
103              
104             has _raw_socket_queue => (
105             is => 'rw',
106             default => sub { {} },
107             );
108              
109             has _pid => (
110             is => 'lazy',
111             );
112              
113             sub _build__pid
114 2     2   33 { my $self = shift;
115 2         24 $$ & 0xffff;
116             }
117              
118             has seq => (
119             is => 'ro',
120             default => 1,
121             );
122              
123             # Whether to try and use ping sockets. This option used in tests
124             # to force normal ping to be used
125             has use_ping_socket => (
126             is => 'ro',
127             default => 1,
128             );
129              
130             sub _parse_icmp_packet {
131 8     8   28 my ( $self, $recv_msg, $from_saddr, $offset ) = @_;
132 8 50       24 $offset = 0
133             unless defined $offset;
134              
135 8         15 my $from_ip = -1;
136 8         15 my $from_pid = -1;
137 8         12 my $from_seq = -1;
138              
139             # ping sockets only return the ICMP packet
140             # raw sockets return the IPv4 packet containing the ICMP
141             # packet
142 8         32 my ($from_type, $from_subcode) =
143             unpack("C2", substr($recv_msg, $offset, 2));
144              
145             # extract source ip, identifier and sequence depending on
146             # packet type
147 8 100       34 if ($from_type == ICMP_ECHOREPLY) {
    50          
148 4         33 (my $err, $from_ip) = getnameinfo($from_saddr,
149             NI_NUMERICHOST, NIx_NOSERV);
150 4 50       15 croak "getnameinfo: $err"
151             if $err;
152 4 50       25 ($from_pid, $from_seq) =
153             unpack("n2", substr($recv_msg, $offset + 4, 4))
154             if length $recv_msg >= $offset + 8;
155             }
156             # an ICMPv4 error message includes the original header
157             # IPv4 + ICMPv4 + ICMPv4::Echo
158             elsif ($from_type == ICMP_UNREACHABLE) {
159 0         0 my $ipv4 = Net::Frame::Layer::IPv4->new(
160             # 8 byte is the length of the ICMP Destination
161             # unreachable header
162             raw => substr($recv_msg, $offset + 8)
163             )->unpack;
164             # skip if contained packet isn't an icmp packet
165             return
166 0 0       0 if $ipv4->protocol != NF_IPv4_PROTOCOL_ICMPv4;
167              
168             # skip if contained packet isn't an icmp echo request packet
169 0         0 my ($to_type, $to_subcode) =
170             unpack("C2", substr($ipv4->payload, 0, 2));
171             return
172 0 0       0 if $to_type != ICMP_ECHO;
173              
174 0         0 $from_ip = $ipv4->dst;
175 0         0 ($from_pid, $from_seq) =
176             unpack("n2", substr($ipv4->payload, 4, 4));
177             }
178             # no packet we care about, raw sockets receive broadcasts,
179             # multicasts etc, ours is only limited to IPv4 containing ICMP
180             else {
181 4         18 return;
182             }
183              
184             return {
185 4         40 type => $from_type,
186             ip => $from_ip,
187             id => $from_pid,
188             seq => $from_seq,
189             };
190             }
191              
192             # Overrides method in IO::Async::Notifier to allow specific options in this class
193             sub configure_unknown
194 2     2 1 1899 { my $self = shift;
195 2         8 my %params = @_;
196 2         14 delete $params{$_} foreach qw/default_timeout bind seq use_ping_socket/;
197 2 50       13 return unless keys %params;
198 0         0 my $class = ref $self;
199 0         0 croak "Unrecognised configuration keys for $class - " . join( " ", keys %params );
200              
201             }
202              
203             sub ping {
204 8     8 1 172288 my $self = shift;
205             # Maintain compat with old API
206 8 100       91 my $legacy = blessed $_[0] and $_[0]->isa('IO::Async::Loop');
207 8 100       56 my $loop = $legacy ? shift : $self->loop;
208              
209 8         47 my ($host, $timeout) = @_;
210 8   33     126 $timeout //= $self->default_timeout;
211              
212 8         42 my $t0 = [Time::HiRes::gettimeofday];
213              
214             $loop->resolver->getaddrinfo(
215             host => $host,
216             protocol => IPPROTO_ICMP,
217             family => AF_INET,
218             )->then( sub {
219 6     6   56504 my $saddr = $_[0]->{addr};
220 6         79 my ($err, $dst_ip) = getnameinfo($saddr, NI_NUMERICHOST, NIx_NOSERV);
221 6 50       34 croak "getnameinfo: $err"
222             if $err;
223 6         36 my $f = $loop->new_future;
224              
225             # Let's try a ping socket (unprivileged ping) first. See
226             # https://lwn.net/Articles/422330/
227 6         183 my ($socket, $ping_socket, $ident);
228 6 50       138 if ($self->use_ping_socket) {
229 0         0 my $ping_fh = IO::Socket->new;
230 0 0       0 if ($ping_fh->socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)) {
231 0         0 ($ident) = unpack_sockaddr_in getsockname($ping_fh);
232              
233 0 0       0 if ($self->bind) {
234 0 0       0 $ping_fh->bind(pack_sockaddr_in 0, inet_aton $self->bind)
235             or croak "Failed to bind to ".$self->bind;
236             }
237              
238             my $on_recv = $self->_capture_weakself(
239             sub {
240 0 0       0 my $self = shift or return; # weakref, may have disappeared
241 0         0 my ( undef, $recv_msg, $from_saddr ) = @_;
242              
243 0         0 my $from_data = $self->_parse_icmp_packet($recv_msg,
244             $from_saddr);
245              
246             # ignore received packets which are not a response to one of
247             # our echo requests
248             return
249             unless $from_data->{ip} eq $dst_ip
250 0 0 0     0 && $from_data->{seq} == $self->seq;
251              
252 0 0       0 if ($from_data->{type} == ICMP_ECHOREPLY) {
    0          
    0          
253 0         0 $f->done;
254             }
255             elsif ($from_data->{type} == ICMP_UNREACHABLE) {
256 0         0 $f->fail('ICMP Unreachable');
257             }
258             elsif ($from_data->{type} == ICMP_TIME_EXCEEDED) {
259 0         0 $f->fail('ICMP Timeout');
260             }
261             },
262 0         0 );
263              
264             $socket = IO::Async::Socket->new(
265             handle => $ping_fh,
266             on_send_error => sub {
267 0         0 my ( $self, $errno ) = @_;
268 0         0 $f->fail("Send error: $errno");
269             },
270             on_recv_error => sub {
271 0         0 my ( $self, $errno ) = @_;
272 0         0 $f->fail("Receive error: $errno");
273             },
274 0         0 on_recv => $on_recv,
275             );
276 0 0       0 $legacy ? $loop->add($socket) : $self->add_child($socket);
277 0         0 $ping_socket = 1;
278             }
279             }
280              
281             # fallback to raw socket or if no ping socket was requested
282 6 50       21 if (not defined $socket) {
283 6         235 $socket = $self->_raw_socket;
284 6         261 $ident = $self->_pid;
285 6 100       65 if (!$self->_is_raw_socket_setup_done) {
286 2 100       18 $legacy ? $loop->add($socket) : $self->add_child($socket);
287 2         861 $self->_is_raw_socket_setup_done(1);
288             }
289             }
290              
291             # remember raw socket requests
292 6 50       23 if (!$ping_socket) {
293 6 50       29 if (exists $self->_raw_socket_queue->{$dst_ip}) {
294 0         0 warn "$dst_ip already in raw queue, $host probably duplicate\n";
295             }
296 6         27 $self->_raw_socket_queue->{$dst_ip} = $f;
297             }
298 6         24 $socket->send( $self->_msg($ident), ICMP_FLAGS, $saddr );
299              
300             Future->wait_any(
301             $f,
302             $loop->timeout_future(after => $timeout)
303             )
304             ->then( sub {
305 4         1038 Future->done(Time::HiRes::tv_interval($t0));
306             })
307             ->followed_by( sub {
308 6         2004509 my $f = shift;
309              
310 6 50       20 if ($ping_socket) {
311 0         0 $socket->remove_from_parent;
312             }
313             else {
314             # remove from raw socket queue
315 6         34 delete $self->_raw_socket_queue->{$dst_ip};
316             }
317              
318 6         17 return $f;
319             })
320 8         78 });
  6         1229  
321             }
322              
323             sub _msg {
324 6     6   19 my ($self, $ident) = @_;
325              
326             # data_size to be implemented later
327 6         15 my $data_size = 0;
328 6         12 my $data = '';
329 6         13 my $checksum = 0;
330 6         80 my $msg = pack(ICMP_STRUCT . $data_size, ICMP_ECHO, SUBCODE,
331             $checksum, $ident, $self->seq, $data);
332 6         85 $checksum = Net::Ping->checksum($msg);
333 6         218 $msg = pack(ICMP_STRUCT . $data_size, ICMP_ECHO, SUBCODE,
334             $checksum, $ident, $self->seq, $data);
335 6         46 return $msg;
336             }
337              
338             1;
339              
340             __END__