82962525512b8a2e0b56e3ca7e0513e68f3dcfbd
[pingcheck] / src / icmp / icmppinger.cpp
1 // Boost pinger (c) 2011 by Guilherme Maciel Ferreira / Intra2net AG
2 // Based upon work copyright (c) 2003-2010 Christopher M. Kohlhoff (ping.cpp)
3 //
4 // Distributed under the Boost Software License, Version 1.0.
5 //    (See accompanying file LICENSE_1_0.txt or copy at
6 //          http://www.boost.org/LICENSE_1_0.txt)
7 #include "icmp/icmppinger.h"
8
9 #include <errno.h>
10
11 #include <ostream>
12
13 #include <boost/bind.hpp>
14 #include <boost/date_time/posix_time/posix_time.hpp>
15 #include <boost/date_time/posix_time/posix_time_types.hpp>
16 #include <boost/uuid/uuid.hpp>
17 #include <boost/uuid/uuid_generators.hpp>
18 #include <boost/foreach.hpp>
19
20 #include <logfunc.hpp>
21
22 #include "boost_assert_handler.h"
23 #include "icmp/icmppacketfactory.h"
24 #include "host/networkinterface.hpp"
25
26 using namespace std;
27 using boost::asio::const_buffers_1;
28 using boost::asio::io_service;
29 using boost::asio::ip::address;
30 using boost::asio::ip::icmp;
31 using boost::function;
32 using boost::posix_time::microsec_clock;
33 using boost::posix_time::seconds;
34 using boost::shared_ptr;
35 using I2n::Logger::GlobalLogger;
36
37 using boost::asio::ip::icmp;
38
39 //-----------------------------------------------------------------------------
40 // IcmpPinger
41 //-----------------------------------------------------------------------------
42
43 /**
44  * @brief factory function for IcmpPingers, ensures that set_myself is set
45  *
46  * @returns a shared pointer to a Pinger
47  */
48 PingerItem IcmpPinger::create(
49         const IoServiceItem io_serv,
50         const icmp::socket::protocol_type &protocol,
51         const string &source_network_interface,
52         const int echo_reply_timeout_in_sec )
53 {
54     // get distributor
55     IcmpPacketDistributorItem distributor = IcmpPacketDistributor::get_distributor(
56             icmp::v4(), source_network_interface, io_serv);
57
58     // create pinger
59     IcmpPinger *ptr = new IcmpPinger(io_serv, protocol, echo_reply_timeout_in_sec, distributor);
60     IcmpPingerItem shared_ptr_(ptr);
61
62     // keep weak pointer to self
63     //shared_ptr_->set_myself( weak_ptr ); //Error: Pinger::set_myself is protected
64     ptr->set_myself( shared_ptr_ );
65
66     // register in distributor
67     distributor->register_pinger(shared_ptr_);
68
69     // done, return shared ptr
70     return shared_ptr_;
71 }
72
73 /**
74  * @brief Parameterized constructor.
75  *
76  * @param io_serv The one @c io_service object that controls async processing
77  * @param protocol The network layer protocol to use.
78  * @param source_network_interface The network interface name from where to
79  * send the packets.
80  * @param echo_reply_timeout_in_sec The amount of time to wait for a reply.
81  */
82 IcmpPinger::IcmpPinger(
83         const IoServiceItem io_serv,
84         const icmp::socket::protocol_type &protocol,
85         const int echo_reply_timeout_in_sec,
86         const IcmpPacketDistributorItem distributor
87 ) :
88     PacketDistributor( distributor ),
89     DestinationEndpoint(),
90     Protocol( protocol ),
91     IcmpPacketReceiveTimer( *io_serv ),
92     Identifier( 0 ),
93     SequenceNumber( 0 ),
94     TimeSent( microsec_clock::universal_time() ),
95     ReplyReceived( false ),
96     EchoReplyTimeoutInSec( echo_reply_timeout_in_sec ),
97     PingerStatus( PingStatus_NotSent ),
98     PingDoneCallback()
99 {
100     // Create "unique" identifier
101     boost::uuids::random_generator random_gen;
102     boost::uuids::uuid random_tag = random_gen();
103
104     BOOST_ASSERT( sizeof(Identifier) <= random_tag.size() );
105     memcpy( &Identifier, random_tag.data, sizeof(Identifier) );
106 }
107
108 /**
109  * @brief Destructor.
110  */
111 IcmpPinger::~IcmpPinger()
112 {
113 }
114
115 /**
116  * @brief Ping a destination address from an available local source.
117  *
118  * @param destination_ip The address of the host to ping.
119  * @param destination_port The port at the destination host to ping.
120  * @param done_handler Done handler will be called on successful ping or timeout.
121  *
122  * @return void.
123  */
124 void IcmpPinger::ping(
125         const string &destination_ip,
126         const uint16_t /*destination_port*/, // the ICMP protocol does not use ports
127         function<void(bool)> ping_done_callback
128 )
129 {
130     BOOST_ASSERT( !destination_ip.empty() );
131
132     PingDoneCallback = ping_done_callback;
133
134     // Prepare ping
135     set_ping_status( PingStatus_NotSent );
136
137     set_destination_endpoint( destination_ip );
138
139     start_send();
140 }
141
142 void IcmpPinger::stop_pinging()
143 {
144     GlobalLogger.debug()
145            << DestinationEndpoint.address().to_string()
146            << ": stop_pinging" << endl;
147
148     GlobalLogger.debug()
149            << DestinationEndpoint.address().to_string()
150            << ": cancel timer" << endl;
151     IcmpPacketReceiveTimer.cancel();
152
153     GlobalLogger.debug()
154            << DestinationEndpoint.address().to_string()
155            << ": unregister" << endl;
156
157     IcmpPingerItem icmp_item = boost::static_pointer_cast<IcmpPinger>( get_myself().lock() );
158     if ( icmp_item )
159     {
160         PacketDistributor->unregister_pinger( icmp_item );
161     } else
162     {
163         GlobalLogger.warning()
164             << DestinationEndpoint.address().to_string()
165             << ": weak pointer to pinger broken is empty. Huh?" << endl;
166     }
167 }
168
169
170 void IcmpPinger::set_destination_endpoint( const string &destination_ip )
171 {
172     BOOST_ASSERT( !destination_ip.empty() );
173
174     uint16_t port = 0;
175     address destination_address = address::from_string( destination_ip );
176     DestinationEndpoint = icmp::endpoint( destination_address, port );
177 }
178
179 bool IcmpPinger::start_send()
180 {
181     ++SequenceNumber;
182
183     IcmpPacketItem icmp_packet_echo_request = IcmpPacketFactory::create_icmp_packet_echo_request(
184             Protocol, Identifier, SequenceNumber );
185
186     BOOST_ASSERT( PingerStatus == PingStatus_NotSent );
187     return send_echo_request( icmp_packet_echo_request );
188 }
189
190 bool IcmpPinger::send_echo_request( const IcmpPacketItem icmp_packet )
191 {
192     boost::asio::streambuf request_buffer;
193     ostream os( &request_buffer );
194     if ( !icmp_packet->write( os ) )
195     {
196         GlobalLogger.error()
197            << DestinationEndpoint.address().to_string()
198            << ": fail writing ping data." << endl;
199     }
200
201     TimeSent = microsec_clock::universal_time();
202
203     string dest_address_string = DestinationEndpoint.address().to_string();
204     BOOST_ASSERT( !dest_address_string.empty() );
205
206     // Send the request
207     size_t bytes_sent = 0;
208     try
209     {
210         GlobalLogger.info()
211                     << DestinationEndpoint.address().to_string()
212                     << ": sending ping" << endl;
213         const_buffers_1 data = request_buffer.data();
214
215         // Block until send the data
216         bytes_sent = PacketDistributor->get_socket()->send_to( data, DestinationEndpoint );
217         if ( bytes_sent != buffer_size( data ) )
218         {
219             GlobalLogger.error()
220                    << DestinationEndpoint.address().to_string()
221                    << ": fail sending ping data." << endl;
222         }
223     }
224     catch ( const exception &ex )
225     {
226         GlobalLogger.error()
227                    << DestinationEndpoint.address().to_string()
228                    << ": fail sending ping data. " << ex.what() << endl;
229     }
230
231     ReplyReceived = false;
232     schedule_timeout_echo_reply();
233
234     return (bytes_sent > 0);
235 }
236
237 void IcmpPinger::schedule_timeout_echo_reply()
238 {
239     // Wait up to N seconds for a reply.
240     (void) IcmpPacketReceiveTimer.expires_at(
241             TimeSent + seconds( EchoReplyTimeoutInSec )
242     );
243     IcmpPacketReceiveTimer.async_wait(
244             boost::bind( &IcmpPinger::handle_timeout, this, boost::asio::placeholders::error )
245     );
246 }
247
248 /**
249  * @brief Gets called when the ping is finished: Either on timeout or on ping reply
250  *
251  * @return void
252  **/
253 void IcmpPinger::handle_timeout(const boost::system::error_code& error)
254 {
255     if (error)
256     {
257         if ( error ==  boost::asio::error::operation_aborted )
258         {
259             if (! ReplyReceived)
260                 GlobalLogger.notice() << "Timer waiting for ICMP echo reply was cancelled!" << endl;
261             // otherwise probably called by IcmpPacketReceiveTimer.cancel in handle_receive_icmp_packet!
262         }
263         else
264             GlobalLogger.notice() << "Error " << error << " waiting for ICMP echo reply!" << endl;
265
266         // Still continue with rest of function, so PingStatus is updated and Callback executed
267         //   when timer was cancelled
268     }
269
270     // Check ReplyReceived as the timer handler
271     // is also called by Timer.cancel();
272     if ( !ReplyReceived )
273     {
274         GlobalLogger.info()
275                    << DestinationEndpoint.address().to_string()
276                    << ": Request timed out" << endl;
277
278         set_ping_status( PingStatus_FailureTimeout );
279     }
280
281     // Call ping-done handler
282     bool ping_success = (PingerStatus == PingStatus_SuccessReply);
283     PingDoneCallback( ping_success );
284 }
285
286
287 /**
288  * @brief Receive ICMP packets
289  * @param bytes_transferred Number of bytes transferred.
290  * @return true if packet matches a request from this pinger, false otherwise
291  **/
292 bool IcmpPinger::handle_receive_icmp_packet(const IcmpPacketItem icmp_packet,
293                                             const size_t bytes_transferred )
294 {
295     bool does_match = false;
296
297     if ( ReplyReceived )
298     {
299         // continue, might be an old packet
300         // or return false right away, do not want packet anyway...
301         GlobalLogger.debug()
302            << DestinationEndpoint.address().to_string()
303            << ": Not interested in packets since we already got a reply"
304            << endl;
305         return does_match;
306     }
307
308     // We can receive all ICMP packets received by the host, so we need to
309     // filter out only the echo replies that match our identifier,
310     // expected sequence number, and destination host address (receive just
311     // the ICMP packets from the host we had ping).
312
313     if ( icmp_packet->match_echo_reply(
314                             Identifier, SequenceNumber,
315                             DestinationEndpoint.address() ) )
316     {
317         GlobalLogger.info()
318            << DestinationEndpoint.address().to_string()
319            << ": Received reply" << endl;
320
321         ReplyReceived = true;
322         does_match = true;
323
324         icmp_packet->print( bytes_transferred, TimeSent );
325
326         set_ping_status( PingStatus_SuccessReply );
327
328         IcmpPacketReceiveTimer.cancel();                            //lint !e534
329     }
330     else if ( icmp_packet->match_destination_unreachable(
331                                  Identifier, SequenceNumber,
332                                  DestinationEndpoint.address() ) )
333     {
334         GlobalLogger.info()
335            << DestinationEndpoint.address().to_string()
336            << ": Received destination unreachable" << endl;
337
338         ReplyReceived = true;
339         does_match = true;
340
341         icmp_packet->print( bytes_transferred, TimeSent );
342
343         set_ping_status( PingStatus_FailureDestinationUnreachable );
344
345         IcmpPacketReceiveTimer.cancel();                            //lint !e534
346     }
347     else if ( icmp_packet->match_time_exceeded(
348                                  Identifier, SequenceNumber,
349                                  DestinationEndpoint.address() ) )
350     {
351         GlobalLogger.info()
352            << DestinationEndpoint.address().to_string()
353            << ": Received time exceeded" << endl;
354
355         ReplyReceived = true;
356         does_match = true;
357
358         icmp_packet->print( bytes_transferred, TimeSent );
359
360         set_ping_status( PingStatus_FailureDestinationUnreachable );
361
362         IcmpPacketReceiveTimer.cancel();                            //lint !e534
363     }
364     else
365     {
366         GlobalLogger.debug()
367            << DestinationEndpoint.address().to_string()
368            << ": Received packet that does not match or has wrong seq.nr"
369            << endl;
370     }
371
372     return does_match;
373 }
374
375 void IcmpPinger::set_ping_status( PingStatus ping_status )
376 {
377     PingerStatus = ping_status;
378 }
379
380 //------------------------------------------------------------------------
381 // IcmpPacketDistributor
382 //------------------------------------------------------------------------
383
384 static const std::size_t SOCKET_BUFFER_SIZE = 65536;   // 64kB
385
386 typedef std::set<IcmpPingerItem>::iterator PingerListIterator;
387
388
389 bool IcmpPacketDistributor::InstanceIdentifierComparator::operator() (
390                 const IcmpPacketDistributor::DistributorInstanceIdentifier &a,
391                 const IcmpPacketDistributor::DistributorInstanceIdentifier &b )
392                                                                           const
393 {
394     if ( a.first == boost::asio::ip::icmp::v4() )
395     {
396         if ( b.first == boost::asio::ip::icmp::v4() )
397             return a.second < b.second;   // v4 == v4
398         else
399             BOOST_ASSERT( b.first == boost::asio::ip::icmp::v6() );
400             return true;    // a(v4) < b(b6)
401     }
402     else
403     {
404         BOOST_ASSERT( a.first == boost::asio::ip::icmp::v6() );
405
406         if ( b.first == boost::asio::ip::icmp::v4() )
407             return false;   // a(v6) > b(v4)
408         else
409             BOOST_ASSERT( b.first == boost::asio::ip::icmp::v6() );
410             return a.second < b.second;    // v6 == v6
411     }
412 }
413
414 //-----------------------------------------------------------------------------
415 // Definition of IcmpPacketDistributor
416 //-----------------------------------------------------------------------------
417
418 IcmpPacketDistributor::map_type IcmpPacketDistributor::Instances; // initialize
419
420
421 IcmpPacketDistributorItem IcmpPacketDistributor::get_distributor(
422         const icmp::socket::protocol_type &protocol,
423         const std::string &network_interface,
424         const IoServiceItem io_serv )
425 {
426     IcmpPacketDistributor::DistributorInstanceIdentifier identifier(
427                                                   protocol, network_interface);
428
429     // check if there is an instance for this protocol and interface
430     if ( Instances.count(identifier) == 0 )
431     {   // need to create an instance for this protocol and network interface
432         GlobalLogger.info() << "Creating IcmpPacketDistributor for interface "
433                             << network_interface << std::endl;
434         IcmpPacketDistributorItem new_instance( new IcmpPacketDistributor(
435                     protocol, network_interface, io_serv ) );
436         Instances[identifier] = new_instance;
437     }
438
439     BOOST_ASSERT( Instances.count(identifier) == 1 );
440
441     // return the one instance for this protocol and interface
442     return Instances[identifier];
443 }
444
445
446 IcmpPacketDistributorItem IcmpPacketDistributor::get_distributor(
447         const icmp::socket::protocol_type &protocol,
448         const std::string &network_interface )
449 {
450     IcmpPacketDistributor::DistributorInstanceIdentifier identifier(
451                                                   protocol, network_interface);
452
453     BOOST_ASSERT( Instances.count(identifier) == 1 );
454
455     // return the one instance for this protocol and interface
456     return Instances[identifier];
457 }
458
459
460 IcmpPacketDistributor::IcmpPacketDistributor(
461             const icmp::socket::protocol_type &protocol,
462             const std::string &network_interface,
463             const IoServiceItem io_serv ):
464     Protocol( protocol ),
465     Socket( new icmp::socket(*io_serv, protocol) ),
466     ReplyBuffer(),
467     PingerList()
468 {
469     // set TTL for testing
470     //const boost::asio::ip::unicast::hops option( 3 );
471     //Socket->set_option(option);
472
473     NetworkInterface<icmp::socket, boost::asio::ip::icmp>
474                   NetInterface( network_interface, *Socket );
475
476     if ( !NetInterface.bind() )
477     {
478         GlobalLogger.error()
479            << "Trouble creating IcmpPacketDistributor for interface "
480            << network_interface// << " and protocol " << protocol
481            << ": could not bind the socket with the local interface. "
482            << ::strerror( errno )  << std::endl;
483     }
484
485     register_receive_handler();
486 }
487
488
489 void IcmpPacketDistributor::register_receive_handler()
490 {
491     // wait for reply, prepare buffer to receive up to SOCKET_BUFFER_SIZE bytes
492     Socket->async_receive(
493             ReplyBuffer.prepare( SOCKET_BUFFER_SIZE ),
494             boost::bind( &IcmpPacketDistributor::handle_receive, this,
495                          boost::asio::placeholders::error,
496                          boost::asio::placeholders::bytes_transferred )
497     );
498 }
499
500 void IcmpPacketDistributor::handle_receive(
501                                         const boost::system::error_code &error,
502                                         const size_t &bytes_transferred )
503 {
504     if ( error )
505     {
506         GlobalLogger.warning()
507            << ": Received error " << error
508            << " in ICMP packet distributor; end handler and schedule another.";
509         register_receive_handler();
510         return;
511     }
512
513     // The actual number of bytes received is committed to the buffer so that we
514     // can extract it using a std::istream object.
515     ReplyBuffer.commit( bytes_transferred );
516
517     GlobalLogger.info() << "received packet in distributor" << std::endl;
518
519     std::istream is( &ReplyBuffer );
520     if ( !is )
521     {
522         GlobalLogger.error() << "Can't handle ReplyBuffer" << std::endl;
523         return;
524     }
525
526     // Decode the reply packet.
527     IcmpPacketItem icmp_packet = IcmpPacketFactory::create_icmp_packet(
528                                                              Protocol, is );
529     if ( !icmp_packet )
530     {
531         GlobalLogger.warning() << "Ignoring broken ICMP packet"
532                                << std::endl;
533     }
534     else
535     {
536         GlobalLogger.debug() << "Succesfully parsed ICMP packet"
537                              << std::endl;
538
539         // check which pinger wants this packet
540         bool packet_matches = false;
541         BOOST_FOREACH( const IcmpPingerItem &pinger, PingerList )
542         {
543             packet_matches = pinger->handle_receive_icmp_packet(
544                                             icmp_packet, bytes_transferred);
545             if (packet_matches)
546                 break;
547         }
548         if (!packet_matches)
549             GlobalLogger.info() << "Packet did not match any pinger"
550                                    << std::endl;
551     }
552
553     // re-register receive handler
554     register_receive_handler();
555 }
556
557 bool IcmpPacketDistributor::register_pinger( const IcmpPingerItem &new_pinger )
558 {
559     std::pair<PingerListIterator, bool> result = PingerList.insert(new_pinger);
560     bool was_new = result.second;
561     if (was_new)
562         GlobalLogger.info() << "Register new pinger with IcmpPacketDistributor"
563                             << std::endl;
564     else
565         GlobalLogger.warning()
566             << "Pinger to register was already known in IcmpPacketDistributor"
567             << std::endl;
568     return was_new;
569 }
570
571
572 bool IcmpPacketDistributor::unregister_pinger( const IcmpPingerItem &old_pinger )
573 {
574     int n_erased = PingerList.erase(old_pinger);
575     bool was_erased = n_erased > 0;
576     if (was_erased)
577         GlobalLogger.info() << "Removed pinger from IcmpPacketDistributor"
578                             << std::endl;
579     else
580         GlobalLogger.warning()
581             << "Could not find pinger to remove from IcmpPacketDistributor"
582             << std::endl;
583     return was_erased;
584 }
585
586 /**
587  * @brief for all instances: close sockets, unregister all pingers
588  */
589 void IcmpPacketDistributor::clean_up_all()
590 {
591     BOOST_FOREACH( IcmpPacketDistributor::map_type::value_type &instance,
592                                                                     Instances )
593     {
594         instance.second->clean_up();
595     }
596
597     Instances.clear();
598 }
599
600 void IcmpPacketDistributor::clean_up()
601 {
602     if (PingerList.size() > 0)
603         GlobalLogger.warning() << "There were still " << PingerList.size()
604             << " pingers registered in IcmpPacketDistributor!" << std::endl;
605     PingerList.clear();
606
607     boost::system::error_code error;
608     //Socket->shutdown(icmp::socket::shutdown_both, error);  //both=send&receive
609     //if ( error )
610     //    GlobalLogger.warning() << "Received error " << error
611     //                           << " when shutting down ICMP socket";
612     // always gave an error system:9 (probably EBADF: Bad file descriptor)
613
614     Socket->close(error);
615     if ( error )
616         GlobalLogger.warning() << "Received error " << error
617                                << " when closing ICMP socket";
618 }
619
620 IcmpPacketDistributor::~IcmpPacketDistributor()
621 {
622     GlobalLogger.info() << "Destroying IcmpPacketDistributor" << std::endl;
623 }
624
625 SocketItem IcmpPacketDistributor::get_socket() const
626 {
627     return Socket;
628 }