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