File Coverage

blib/lib/CHI/Driver/Ping.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1              
2             package CHI::Driver::Ping;
3              
4 1     1   27184 use strict;
  1         3  
  1         40  
5 1     1   6 use warnings;
  1         2  
  1         30  
6              
7 1     1   478 use Moose;
  0            
  0            
8             use Moose::Util::TypeConstraints;
9              
10             use Fcntl;
11             use Errno;
12             use FileHandle;
13             use IO::Handle;
14             use Socket;
15             use Time::HiRes;
16             use POSIX;
17              
18             use Fcntl qw(:flock SEEK_END);
19              
20              
21             use Carp 'croak';
22              
23             extends 'CHI::Driver';
24              
25             use 5.006;
26             our $VERSION = '0.00000001';
27              
28             use constant ICMP_ECHOREPLY => 0; # ICMP packet types
29             use constant ICMP_UNREACHABLE => 3; # ICMP packet types
30             use constant ICMP_ECHO => 8;
31             use constant ICMP_STRUCT => "C2 n3 A"; # Structure of a minimal ICMP packet
32             use constant SUBCODE => 0; # No ICMP subcode for ECHO and ECHOREPLY
33             use constant ICMP_FLAGS => 0; # No special flags for send or recv
34             use constant ICMP_PORT => 0; # No port with ICMP
35              
36             =head1 NAME
37              
38             CHI::Driver::Ping - Cache data in the Ether.
39              
40             =head1 SYNOPSIS
41              
42             use CHI;
43              
44             $< == 0 or exec 'sudo', $0, @ARGV; # sending ICMPs requires root priv
45              
46             system 'sysctl', '-w', 'net.ipv4.icmp_ratelimit=100000';
47              
48             my $cache = CHI->new( driver => 'Ping', ip => 74.125.73.105 ); # google IP
49              
50             =head1 DESCRIPTION
51              
52             Tap into the Ether. Optimize for CPU or storage? Fuck that.
53              
54             If you thought the Cloud was awesome, just wait until you try
55             storing your data in the Ether.
56              
57             Inspired by Delay Line Memory, L,
58             this modules stores data by transmitting it through a medium known to have a
59             delay and waiting for it to come back again, whereupon it both returns it and
60             retransmits it out again.
61              
62             It seems rather pointless and silly to bother with spinning metal oxide
63             covered platters or billions of tiny capacitors when data can be stored
64             in the air between the Earth and sattelites, in ordinary copper wire,
65             and in easy to extrude lengths of glass fiber.
66              
67             =head1 ATTRIBUTES
68              
69             =over
70              
71             =item ip
72              
73             Who to send all of the ICMP ECHOPINGs to.
74              
75             =item namespace
76              
77             Not currently used (XXX).
78              
79             =back
80              
81             =head1 TODO
82              
83             CIDR block of hosts to use, or a list, or something. Even better, scan the network
84             for hosts that are up and build this dynamically. For extra points, find hosts with
85             a lot of hops to them.
86              
87             namespace. XXX.
88              
89             remove. XXX.
90              
91             purge. XXX.
92              
93             =head1 BUGS
94              
95             =item 0.00000001
96              
97             Initial; github dev version.
98             Requires root privilege.
99              
100             =head1 Authors
101              
102             L by Scott Walters (scott@slowass.net) with suggestions from
103             Brock Wilcox (awwaiid@thelackthereof.org).
104              
105             Uses code stolen from L by bbb@cpan.org (Rob Brown), colinm@cpan.org (Colin McMillen),
106             bronson@trestle.com (Scott Bronson), karrer@bernina.ethz.ch (Andreas Karrer),
107             pmarquess@bfsec.bt.co.uk (Paul Marquess), and mose@ns.ccsn.edu (Russell Mosemann).
108             These folks shall remain blameless for my actions.
109              
110             =head1 COPYRIGHT & LICENSE
111              
112             Copyright (c) Scott Walters (scrottie) 2011
113              
114             This program is free software; you can redistribute it and/or modify it under
115             the same terms as Perl itself.
116              
117             =cut
118              
119             has 'table_prefix' => ( is => 'rw', isa => 'Str', default => 'chi_', );
120              
121             has 'proto_num' => ( is => 'rw' );
122              
123             has 'pid' => ( is => 'rw' ); # not currently used; Net::Ping looked at this PID returned with the ECHOPING; we don't, yet XXX
124              
125             has 'fh' => ( is => 'rw' );
126              
127             has 'seq' => ( is => 'rw', default => 0 );
128              
129             has 'ip' => ( is => 'rw', default => '127.0.0.1' );
130              
131             # has 'daemon_pid' => ( is => 'rw', );
132             sub daemon_pid {
133             my $self = shift;
134             our $daemon_pid; # process global
135             $daemon_pid = shift if @_;
136             $daemon_pid;
137             }
138              
139             has 'i_am_daemon' => ( is => 'rw', );
140              
141             __PACKAGE__->meta->make_immutable;
142              
143             sub BUILD {
144             my $self = shift;
145             croak("icmp ping requires root privilege") if ($> and $^O ne 'VMS' and $^O ne 'cygwin');
146             $self->proto_num( (getprotobyname('icmp'))[2] || croak("Can't get icmp protocol by name") );
147             $self->pid( $$ & 0xffff ); # Save lower 16 bits of pid
148             $self->fh( FileHandle->new() );
149             socket($self->fh, PF_INET, SOCK_RAW, $self->proto_num) or croak "icmp socket error - $!";
150             # $SIG{CHLD} = sub { wait };
151             $SIG{CHLD} = 'IGNORE';
152             $self->launch_daemon;
153             }
154              
155             sub DEMOLISH {
156             my $self = shift;
157             kill 9, $self->daemon_pid if $self->daemon_pid;
158             $self->daemon_pid( 0 );
159             }
160              
161             sub remove {
162             my ( $self, $key, ) = @_;
163             $self->store( $key, 'delete' );
164             return;
165             }
166              
167             sub clear {
168             my $self = shift;
169             # XXX this will be a fun one; clear the entire cache
170             return;
171             }
172              
173             sub get_keys {
174             my ( $self ) = @_;
175             # XXX this will be a fun one
176             }
177              
178             sub get_namespaces { croak 'not supported' }
179              
180             sub store {
181              
182             my $self = shift;
183             my $key = shift;
184             my $value = shift;
185              
186             my $ip = $self->ip();
187              
188             # warn "ip: $ip";
189              
190             my ($saddr, # sockaddr_in with port and ip
191             $msg, # ICMP packet to send
192             );
193              
194             # XXXXXXXXXXX
195             # # at construction instead XXX on the other hand, it's possible that we were sharing a daemon with another process and it exited and they killed it
196             # if( ! $self->i_am_daemon and ( ! $self->daemon_pid or ! kill 0, $self->daemon_pid ) ) {
197             # $self->launch_daemon;
198             # }
199              
200             my $data = join '', $key, chr(0), $value;
201              
202             $self->seq( ( $self->seq() + 1) % 65536 ); # Increment sequence
203             my $checksum = 0;
204             $msg = pack( ICMP_STRUCT . length( $data ), ICMP_ECHO, SUBCODE, $checksum, $self->pid, $self->seq, $data );
205             $checksum = $self->checksum($msg);
206             $msg = pack( ICMP_STRUCT . length( $data ), ICMP_ECHO, SUBCODE, $checksum, $self->pid, $self->seq, $data );
207             $saddr = sockaddr_in(ICMP_PORT, inet_aton( $self->ip ) );
208             send($self->fh, $msg, ICMP_FLAGS, $saddr); # Send the message
209              
210             }
211              
212             sub fetch {
213              
214             my $self = shift;
215             my $key = shift;
216             my $mode = shift() || 0;
217              
218             local $SIG{USR1} = sub { use Carp; Carp::cluck "USR1"; };
219            
220             #warn "XXX got mode: $mode";
221             my $delete_mode = 1 if $mode eq 'delete'; # don't retransmit this packet once we see it
222             my $forever_mode = 1 if $mode eq 'forever'; # daemonize; don't return
223            
224             # at construction instead; XXX on the other hand as above
225             # 30589 pts/1 R+ 0:13 t/CHIDriverTests-Ping.t: perl echo ping daemon: perl echo ping daemon: perl echo ping daemon: perl echo ping daemon... why the fuck am I seeing this?
226              
227             # XXXXXXXX
228             # if( ! $self->daemon_pid and ! $forever_mode and ! $self->i_am_daemon ) {
229             # # ^--- launch_daemon calls this back again in turn; don't call them back or we'll loop forever
230             # $self->launch_daemon;
231             # }
232              
233             if( ! $forever_mode and $self->daemon_pid ) {
234             # XXX this is dangerous; a semaphore would be better; STOPing them, there's a risk that we've stopped them while
235             # they're in middle of processing the packet that we want; try to work around that
236             # XXX also, a semaphore is not adequate; multiple processes might be accessing the same cache; need multiple-up-multiple-down
237             kill SIGSTOP, $self->daemon_pid; # XXX test result
238             }
239            
240             fcntl($self->fh, F_SETFL, fcntl($self->fh, F_GETFL, 0) | O_NONBLOCK) or die "fcntl: $!";
241            
242             my $start_time = Time::HiRes::time;
243             # warn "start_time $start_time";
244             my $return_value;
245            
246             while(1) {
247             if( ! $forever_mode and Time::HiRes::time - $start_time > 2) {
248             # ^------ here is also where we exit in failure
249             kill SIGCONT, $self->daemon_pid if $self->daemon_pid;
250             return;
251             }
252             my $recv_msg = "";
253             my $from_pid = -1;
254             my $from_seq = -1;
255             my $from_saddr = recv($self->fh, $recv_msg, 1500, ICMP_FLAGS); # sockaddr_in of sender
256             if( $! == Errno::EAGAIN ) {
257             kill SIGCONT, $self->daemon_pid if $self->daemon_pid; # just in case they're in middle of processing the packet we want; XXX test result
258             Time::HiRes::sleep(0.2);
259             kill SIGSTOP, $self->daemon_pid if $self->daemon_pid; # XXX test result
260             next;
261             }
262             my $from_port; # Port packet was sent from
263             my $from_ip; # Packed IP of sender
264             ($from_port, $from_ip) = sockaddr_in($from_saddr);
265             (my $from_type, my $from_subcode) = unpack("C2", substr($recv_msg, 20, 2));
266             if ($from_type == ICMP_ECHOREPLY) {
267             ($from_pid, $from_seq) = unpack("n3", substr($recv_msg, 24, 4, ''));
268             if( length $recv_msg >= 28 ) {
269             # warn "raw message: $recv_msg";
270             substr $recv_msg, 0, 24, '';
271             my $i = index $recv_msg, chr(0);
272             my $key2 = substr $recv_msg, 0, $i;
273             my $value = substr $recv_msg, $i+1;
274             $return_value = $value if ! $forever_mode and $key eq $key2; # don't return yet but remember what to return
275             $self->store( $key2, $value ) unless $delete_mode;
276             if( $return_value ) {
277             # ^----- only ever gets set if we aren't in $forever_mode
278             if( $self->daemon_pid ) {
279             kill SIGCONT, $self->daemon_pid;
280             }
281             return $return_value if $return_value; # <----- here is where we return successfully
282             }
283             # warn "found it: $value";
284             # return ($key, $value); # XXXX
285             }
286             }
287             }
288             }
289              
290             sub checksum {
291              
292             my ($class,
293             $msg # The message to checksum
294             ) = @_;
295             my ($len_msg, # Length of the message
296             $num_short, # The number of short words in the message
297             $short, # One short word
298             $chk # The checksum
299             );
300            
301             $len_msg = length($msg);
302             $num_short = int($len_msg / 2);
303             $chk = 0;
304             foreach $short (unpack("n$num_short", $msg))
305             {
306             $chk += $short;
307             } # Add the odd byte in
308             $chk += (unpack("C", substr($msg, $len_msg - 1, 1)) << 8) if $len_msg % 2;
309             $chk = ($chk >> 16) + ($chk & 0xffff); # Fold high into low
310             return(~(($chk >> 16) + $chk) & 0xffff); # Again and complement
311             }
312              
313             sub launch_daemon {
314             my $self = shift;
315              
316             our $launch_daemon_lock;
317             return if $launch_daemon_lock;
318             $launch_daemon_lock++;
319              
320             if( $self->i_am_daemon ) {
321             warn "that's odd; I am the daemon but somewhere decided that I should launch a daemon";
322             $launch_daemon_lock--;
323             return;
324             }
325              
326             # XXXXXXXXXXXX
327             # we only need one of these fuckers
328             open my $fh, '<', '/var/lock/chi-driver-ping-pid';
329             flock($fh, LOCK_EX) or die "Cannot lock PID file";
330              
331             if( $fh ) {
332             my $pid = readline $fh;
333             if( $pid ) {
334             chomp $pid;
335             if( $pid =~ m/^\d+$/ and kill 0, $pid ) {
336             warn "XXX daemon process already exists? why don't we know it's pid? (it's $pid, by the way)";
337             $self->daemon_pid( $pid ); # update this with what we've learned
338             open my $fh, '>', '/var/lock/chi-driver-ping-pid'; # but don't die
339             if( $fh ) {
340             $fh->print($pid);
341             }
342             close $fh;
343             $launch_daemon_lock--;
344             return;
345             }
346             }
347             }
348             close $fh;
349              
350             if( my $pid = fork ) {
351             $self->daemon_pid( $pid ); # parent
352             open my $fh, '>', '/var/lock/chi-driver-ping-pid'; # but don't die
353             if( $fh ) {
354             $fh->print($pid);
355             }
356             close $fh;
357             } else {
358             # child
359             warn "XXX daemon pid started up as $$";
360             open STDIN, '
361             #open STDOUT, '>/dev/null' or die "Can't open STDOUT to /dev/null: [$!]\n"; # XXX
362             #open STDERR, '>&STDOUT' or die "Can't open STDERR to STDOUT: [$!]\n";
363             # Change to root dir to avoid locking a mounted file system
364             chdir '/' or die "Can't chdir to \"/\": [$!]";
365             # Turn process into session leader, and ensure no controlling terminal
366             # POSIX::setsid(); # no; die with the parent
367             $0 = "$0: perl echo ping daemon";
368             $self->i_am_daemon( 1 );
369             while(1) {
370             $self->fetch( undef, 'forever' ); # fetch receives and re-transmits; do this forever
371             }
372             }
373              
374             $launch_daemon_lock--;
375             }
376              
377              
378            
379             1;
380              
381             __END__