File Coverage

blib/lib/Net/Async/Ping/ICMPv6.pm
Criterion Covered Total %
statement 108 165 65.4
branch 13 70 18.5
condition 1 15 6.6
subroutine 26 29 89.6
pod 2 2 100.0
total 150 281 53.3


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