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