moved time duration measurement of ping from scheduler to pingers
[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             protocol, 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     LogPrefix("IcmpPinger")
100 {
101     // Create "unique" identifier
102     boost::uuids::random_generator random_gen;
103     boost::uuids::uuid random_tag = random_gen();
104
105     BOOST_ASSERT( sizeof(Identifier) <= random_tag.size() );
106     memcpy( &Identifier, random_tag.data, sizeof(Identifier) );
107
108     LogPrefix = "IPing(no IP yet): ";
109 }
110
111 /**
112  * @brief Destructor.
113  */
114 IcmpPinger::~IcmpPinger()
115 {
116 }
117
118 /**
119  * @brief Ping a destination address from an available local source.
120  *
121  * @param destination_ip The address of the host to ping.
122  * @param destination_port The port at the destination host to ping.
123  * @param done_handler Done handler will be called on successful ping or timeout.
124  *
125  * @return void.
126  */
127 void IcmpPinger::ping(
128         const address &destination_ip,
129         const uint16_t /*destination_port*/, // the ICMP protocol does not use ports
130         function<void(PingStatus,long)> ping_done_callback
131 )
132 {
133     PingDoneCallback = ping_done_callback;
134
135     // Prepare ping
136     set_ping_status( PingStatus_NotSent );
137
138     set_destination_endpoint( destination_ip );
139
140     start_send();
141 }
142
143 void IcmpPinger::stop_pinging()
144 {
145     GlobalLogger.debug() << LogPrefix << "stop_pinging" << endl;
146
147     GlobalLogger.debug() << LogPrefix << "cancel timer" << endl;
148     IcmpPacketReceiveTimer.cancel();
149
150     GlobalLogger.debug() << LogPrefix << "unregister" << endl;
151
152     IcmpPingerItem icmp_item = boost::static_pointer_cast<IcmpPinger>( get_myself().lock() );
153     if ( icmp_item )
154     {
155         PacketDistributor->unregister_pinger( icmp_item );
156     } else
157     {
158         GlobalLogger.warning() << LogPrefix
159             << "weak pointer to pinger broken is empty. Huh?" << endl;
160     }
161 }
162
163
164 void IcmpPinger::set_destination_endpoint( const address &destination_ip )
165 {
166     uint16_t port = 0;
167     DestinationEndpoint = icmp::endpoint( destination_ip, port );
168
169     // update LogPrefix
170     std::stringstream temp;
171     temp << "IPing(" << DestinationEndpoint.address().to_string() << "): ";
172     LogPrefix = temp.str();
173 }
174
175 bool IcmpPinger::start_send()
176 {
177     ++SequenceNumber;
178
179     IcmpPacketItem icmp_packet_echo_request = IcmpPacketFactory::create_icmp_packet_echo_request(
180             Protocol, Identifier, SequenceNumber );
181
182     BOOST_ASSERT( PingerStatus == PingStatus_NotSent );
183     return send_echo_request( icmp_packet_echo_request );
184 }
185
186 bool IcmpPinger::send_echo_request( const IcmpPacketItem icmp_packet )
187 {
188     boost::asio::streambuf request_buffer;
189     ostream os( &request_buffer );
190     if ( !icmp_packet->write( os ) )
191     {
192         GlobalLogger.error() << LogPrefix << "fail writing ping data." << endl;
193     }
194
195     TimeSent = microsec_clock::universal_time();
196
197     string dest_address_string = DestinationEndpoint.address().to_string();
198     BOOST_ASSERT( !dest_address_string.empty() );
199
200     // Send the request
201     size_t bytes_sent = 0;
202     try
203     {
204         GlobalLogger.info() << LogPrefix << "sending ping" << endl;
205         const_buffers_1 data = request_buffer.data();
206
207         // Block until send the data
208         bytes_sent = PacketDistributor->get_socket()->send_to( data, DestinationEndpoint );
209         if ( bytes_sent != buffer_size( data ) )
210         {
211             GlobalLogger.error() << LogPrefix << "fail sending ping data."
212                                  << endl;
213         }
214     }
215     catch ( const exception &ex )
216     {
217         GlobalLogger.error() << LogPrefix << "fail sending ping data. "
218                              << ex.what() << endl;
219     }
220
221     ReplyReceived = false;
222     schedule_timeout_echo_reply();
223
224     return (bytes_sent > 0);
225 }
226
227 void IcmpPinger::schedule_timeout_echo_reply()
228 {
229     // Wait up to N seconds for a reply.
230     (void) IcmpPacketReceiveTimer.expires_at(
231             TimeSent + seconds( EchoReplyTimeoutInSec )
232     );
233     IcmpPacketReceiveTimer.async_wait(
234             boost::bind( &IcmpPinger::handle_timeout, this, boost::asio::placeholders::error )
235     );
236 }
237
238 /**
239  * @brief Gets called when the ping is finished: Either on timeout or on ping reply
240  *
241  * @return void (but calls PingDoneCallback)
242  **/
243 void IcmpPinger::handle_timeout(const boost::system::error_code& error)
244 {
245     if (error)
246     {
247         if ( error ==  boost::asio::error::operation_aborted )
248         {
249             if (! ReplyReceived)
250             {
251                 GlobalLogger.notice() << LogPrefix
252                     << "Timer waiting for ICMP echo reply was cancelled!"
253                     << endl;
254                 set_ping_status( PingStatus_FailureAsyncCancel );
255             }
256             // otherwise probably called by IcmpPacketReceiveTimer.cancel in
257             // handle_receive_icmp_packet!
258         }
259         else
260         {
261             GlobalLogger.notice() << LogPrefix << "Error " << error
262                 << " waiting for ICMP echo reply!" << endl;
263             set_ping_status( PingStatus_FailureAsyncError );
264         }
265
266         // Still continue with rest of function, so PingStatus is updated and Callback executed
267         //   when timer was cancelled
268     }
269     else if ( !ReplyReceived )
270     {    // Check ReplyReceived since the timer handler is also called by Timer.cancel();
271         GlobalLogger.info() << LogPrefix << "Request timed out" << endl;
272
273         set_ping_status( PingStatus_FailureTimeout );
274     }
275
276     // Call ping-done handler
277     PingDoneCallback( PingerStatus,
278                      (microsec_clock::universal_time()
279                                              - TimeSent).total_microseconds() );
280 }
281
282
283 /**
284  * @brief Receive ICMP packets
285  * @param bytes_transferred Number of bytes transferred.
286  * @return true if packet matches a request from this pinger, false otherwise
287  **/
288 bool IcmpPinger::handle_receive_icmp_packet(const IcmpPacketItem icmp_packet,
289                                             const size_t bytes_transferred )
290 {
291     bool does_match = false;
292
293     if ( ReplyReceived )
294     {
295         // continue, might be an old packet
296         // or return false right away, do not want packet anyway...
297         return does_match;
298     }
299     else if ( DestinationEndpoint.address() == address() )
300     {   // we have no IP set yet
301         return does_match;
302     }
303
304     // We can receive all ICMP packets received by the host, so we need to
305     // filter out only the echo replies that match our identifier,
306     // expected sequence number, and destination host address (receive just
307     // the ICMP packets from the host we had ping).
308
309     try
310     {
311         if ( icmp_packet->match_echo_reply(
312                                 Identifier, SequenceNumber,
313                                 DestinationEndpoint.address() ) )
314         {
315             GlobalLogger.info() << LogPrefix << "Received reply" << endl;
316
317             ReplyReceived = true;
318             does_match = true;
319
320             icmp_packet->print( bytes_transferred, TimeSent );
321
322             set_ping_status( PingStatus_SuccessReply );
323
324             IcmpPacketReceiveTimer.cancel();                            //lint !e534
325         }
326         else if ( icmp_packet->match_destination_unreachable(
327                                      Identifier, SequenceNumber,
328                                      DestinationEndpoint.address() ) )
329         {
330             GlobalLogger.info() << LogPrefix
331                                 << "Received destination unreachable" << endl;
332
333             ReplyReceived = true;
334             does_match = true;
335
336             icmp_packet->print( bytes_transferred, TimeSent );
337
338             set_ping_status( PingStatus_FailureDestinationUnreachable );
339
340             IcmpPacketReceiveTimer.cancel();                            //lint !e534
341         }
342         else if ( icmp_packet->match_time_exceeded(
343                                      Identifier, SequenceNumber,
344                                      DestinationEndpoint.address() ) )
345         {
346             GlobalLogger.info() << LogPrefix
347                                 << "Received time exceeded" << endl;
348
349             ReplyReceived = true;
350             does_match = true;
351
352             icmp_packet->print( bytes_transferred, TimeSent );
353
354             set_ping_status( PingStatus_FailureDestinationUnreachable );
355
356             IcmpPacketReceiveTimer.cancel();                            //lint !e534
357         }
358         else
359         {
360             GlobalLogger.debug() << LogPrefix
361                << "Received packet that does not match or has wrong seq.nr"
362                << endl;
363         }
364     }
365     catch ( std::exception &exc)
366     {
367         GlobalLogger.warning() << LogPrefix
368             << "Caught exception in packet interpretation: " << exc.what()
369             << std::endl;
370         if ( IcmpPacketFactory::PacketDumpMode == DUMP_ALWAYS ||
371              IcmpPacketFactory::PacketDumpMode == DUMP_IF_ERROR )
372             IcmpPacketFactory::dump_packet(*icmp_packet);
373         does_match = true;   // avoid the same procedure in all other pingers
374     }
375     catch ( ... )
376     {
377         GlobalLogger.warning() << LogPrefix
378             << "Caught unspecified exception in packet interpretation!"
379             << std::endl;
380         if ( IcmpPacketFactory::PacketDumpMode == DUMP_ALWAYS ||
381              IcmpPacketFactory::PacketDumpMode == DUMP_IF_ERROR )
382             IcmpPacketFactory::dump_packet(*icmp_packet);
383         does_match = true;   // avoid the same procedure in all other pingers
384     }
385
386     return does_match;
387 }
388
389 void IcmpPinger::set_ping_status( PingStatus ping_status )
390 {
391     PingerStatus = ping_status;
392 }
393
394 //------------------------------------------------------------------------
395 // IcmpPacketDistributor
396 //------------------------------------------------------------------------
397
398 static const std::size_t SOCKET_BUFFER_SIZE = 65536;   // 64kB
399
400 typedef std::set<IcmpPingerItem>::iterator PingerListIterator;
401
402
403 bool IcmpPacketDistributor::InstanceIdentifierComparator::operator() (
404                 const IcmpPacketDistributor::DistributorInstanceIdentifier &a,
405                 const IcmpPacketDistributor::DistributorInstanceIdentifier &b )
406                                                                           const
407 {
408     if ( a.first == boost::asio::ip::icmp::v4() )
409     {
410         if ( b.first == boost::asio::ip::icmp::v4() )
411             return a.second < b.second;   // v4 == v4
412         else
413             BOOST_ASSERT( b.first == boost::asio::ip::icmp::v6() );
414             return true;    // a(v4) < b(b6)
415     }
416     else
417     {
418         BOOST_ASSERT( a.first == boost::asio::ip::icmp::v6() );
419
420         if ( b.first == boost::asio::ip::icmp::v4() )
421             return false;   // a(v6) > b(v4)
422         else
423             BOOST_ASSERT( b.first == boost::asio::ip::icmp::v6() );
424             return a.second < b.second;    // v6 == v6
425     }
426 }
427
428 //-----------------------------------------------------------------------------
429 // Definition of IcmpPacketDistributor
430 //-----------------------------------------------------------------------------
431
432 IcmpPacketDistributor::map_type IcmpPacketDistributor::Instances; // initialize
433
434
435 IcmpPacketDistributorItem IcmpPacketDistributor::get_distributor(
436         const icmp::socket::protocol_type &protocol,
437         const std::string &network_interface,
438         const IoServiceItem io_serv )
439 {
440     IcmpPacketDistributor::DistributorInstanceIdentifier identifier(
441                                                   protocol, network_interface);
442
443     // check if there is an instance for this protocol and interface
444     if ( Instances.count(identifier) == 0 )
445     {   // need to create an instance for this protocol and network interface
446         std::string protocol_str;
447         if (protocol == icmp::v4())
448             protocol_str = "ICMPv4";
449         else if (protocol == icmp::v6())
450             protocol_str = "ICMPv6";
451         else
452             protocol_str = "unknown protocol!";
453
454         GlobalLogger.info() << "Creating IcmpPacketDistributor for interface "
455                             << network_interface << " and protocol "
456                             << protocol_str << std::endl;
457         IcmpPacketDistributorItem new_instance( new IcmpPacketDistributor(
458                     protocol, network_interface, io_serv ) );
459         Instances[identifier] = new_instance;
460     }
461
462     BOOST_ASSERT( Instances.count(identifier) == 1 );
463
464     // return the one instance for this protocol and interface
465     return Instances[identifier];
466 }
467
468
469 IcmpPacketDistributorItem IcmpPacketDistributor::get_distributor(
470         const icmp::socket::protocol_type &protocol,
471         const std::string &network_interface )
472 {
473     IcmpPacketDistributor::DistributorInstanceIdentifier identifier(
474                                                   protocol, network_interface);
475
476     BOOST_ASSERT( Instances.count(identifier) == 1 );
477
478     // return the one instance for this protocol and interface
479     return Instances[identifier];
480 }
481
482
483 IcmpPacketDistributor::IcmpPacketDistributor(
484             const icmp::socket::protocol_type &protocol,
485             const std::string &network_interface,
486             const IoServiceItem io_serv ):
487     Protocol( protocol ),
488     Socket( new icmp::socket(*io_serv, protocol) ),
489     ReplyBuffer(),
490     PingerList()
491 {
492     // set TTL for testing
493     //const boost::asio::ip::unicast::hops option( 3 );
494     //Socket->set_option(option);
495
496     NetworkInterface<icmp::socket, boost::asio::ip::icmp>
497                   NetInterface( network_interface, *Socket );
498
499     if ( !NetInterface.bind() )
500     {
501         GlobalLogger.error()
502            << "Trouble creating IcmpPacketDistributor for interface "
503            << network_interface// << " and protocol " << protocol
504            << ": could not bind the socket with the local interface. "
505            << ::strerror( errno )  << std::endl;
506     }
507
508     register_receive_handler();
509 }
510
511
512 void IcmpPacketDistributor::register_receive_handler()
513 {
514     // wait for reply, prepare buffer to receive up to SOCKET_BUFFER_SIZE bytes
515     Socket->async_receive(
516             ReplyBuffer.prepare( SOCKET_BUFFER_SIZE ),
517             boost::bind( &IcmpPacketDistributor::handle_receive, this,
518                          boost::asio::placeholders::error,
519                          boost::asio::placeholders::bytes_transferred )
520     );
521 }
522
523 void IcmpPacketDistributor::handle_receive(
524                                         const boost::system::error_code &error,
525                                         const size_t &bytes_transferred )
526 {
527     if ( error )
528     {
529         GlobalLogger.warning()
530            << ": Received error " << error
531            << " in ICMP packet distributor; end handler and schedule another.";
532         register_receive_handler();
533         return;
534     }
535
536     // The actual number of bytes received is committed to the buffer so that we
537     // can extract it using a std::istream object.
538     ReplyBuffer.commit( bytes_transferred );
539
540     GlobalLogger.info() << "received packet in distributor" << std::endl;
541
542     std::istream is( &ReplyBuffer );
543     if ( !is )
544     {
545         GlobalLogger.error() << "Can't handle ReplyBuffer" << std::endl;
546         return;
547     }
548
549     // Decode the reply packet.
550     IcmpPacketItem icmp_packet = IcmpPacketFactory::create_icmp_packet(
551                                                              Protocol, is );
552     if ( !icmp_packet )
553     {
554         GlobalLogger.warning() << "Ignoring broken ICMP packet"
555                                << std::endl;
556     }
557     else
558     {
559         GlobalLogger.debug() << "Succesfully parsed ICMP packet"
560                              << std::endl;
561
562         // check which pinger wants this packet
563         bool packet_matches = false;
564         BOOST_FOREACH( const IcmpPingerItem &pinger, PingerList )
565         {
566             packet_matches = pinger->handle_receive_icmp_packet(
567                                             icmp_packet, bytes_transferred);
568             if (packet_matches)
569                 break;
570         }
571         if (!packet_matches)
572             GlobalLogger.info() << "Packet did not match any pinger"
573                                 << std::endl;
574     }
575
576     // re-register receive handler
577     register_receive_handler();
578 }
579
580 bool IcmpPacketDistributor::register_pinger( const IcmpPingerItem &new_pinger )
581 {
582     std::pair<PingerListIterator, bool> result = PingerList.insert(new_pinger);
583     bool was_new = result.second;
584     if (was_new)
585         GlobalLogger.info() << "Register new pinger with IcmpPacketDistributor"
586                             << std::endl;
587     else
588         GlobalLogger.warning()
589             << "Pinger to register was already known in IcmpPacketDistributor"
590             << std::endl;
591     return was_new;
592 }
593
594
595 bool IcmpPacketDistributor::unregister_pinger( const IcmpPingerItem &old_pinger )
596 {
597     int n_erased = PingerList.erase(old_pinger);
598     bool was_erased = n_erased > 0;
599     if (was_erased)
600         GlobalLogger.info() << "Removed pinger from IcmpPacketDistributor"
601                             << std::endl;
602     else
603         GlobalLogger.warning()
604             << "Could not find pinger to remove from IcmpPacketDistributor"
605             << std::endl;
606     return was_erased;
607 }
608
609 /**
610  * @brief for all instances: close sockets, unregister all pingers
611  */
612 void IcmpPacketDistributor::clean_up_all()
613 {
614     BOOST_FOREACH( IcmpPacketDistributor::map_type::value_type &instance,
615                                                                     Instances )
616     {
617         instance.second->clean_up();
618     }
619
620     Instances.clear();
621 }
622
623 void IcmpPacketDistributor::clean_up()
624 {
625     if (PingerList.size() == 0)
626         GlobalLogger.info() << "All IcmpPingers have de-registered"
627                             << std::endl;
628     else
629         GlobalLogger.warning() << "There were still " << PingerList.size()
630             << " pingers registered in IcmpPacketDistributor!" << std::endl;
631     PingerList.clear();
632
633     boost::system::error_code error;
634     //Socket->shutdown(icmp::socket::shutdown_both, error);  //both=send&receive
635     //if ( error )
636     //    GlobalLogger.warning() << "Received error " << error
637     //                           << " when shutting down ICMP socket";
638     // always gave an error system:9 (probably EBADF: Bad file descriptor)
639
640     Socket->close(error);
641     if ( error )
642         GlobalLogger.warning() << "Received error " << error
643                                << " when closing ICMP socket";
644 }
645
646 IcmpPacketDistributor::~IcmpPacketDistributor()
647 {
648     GlobalLogger.info() << "Destroying IcmpPacketDistributor" << std::endl;
649 }
650
651 SocketItem IcmpPacketDistributor::get_socket() const
652 {
653     return Socket;
654 }