Working TCP pinger
authorGuilherme Maciel Ferreira <guilherme.maciel.ferreira@gmail.com>
Fri, 22 Jul 2011 03:47:22 +0000 (00:47 -0300)
committerGuilherme Maciel Ferreira <guilherme.maciel.ferreira@gmail.com>
Fri, 22 Jul 2011 03:47:22 +0000 (00:47 -0300)
- Needs to fix the source IP address used in the pseudo IP header

src/tcp/tcppinger.cpp
src/tcp/tcppinger.h

index 3da92bb..b681660 100644 (file)
@@ -19,13 +19,32 @@ on this file might be covered by the GNU General Public License.
 */
 #include "tcp/tcppinger.h"
 
+#include <istream>
+#include <ostream>
+
 #include <boost/assert.hpp>
+#include <boost/bind.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/date_time/posix_time/posix_time_types.hpp>
+#include <boost/uuid/uuid.hpp>
+#include <boost/uuid/uuid_generators.hpp>
+
+#include <logfunc.hpp>
 
+#include "ip/ipv4header.h"
 #include "tcp/tcpheader.h"
 
 using namespace std;
+using boost::asio::const_buffers_1;
 using boost::asio::io_service;
+using boost::asio::ip::address;
+using boost::asio::ip::tcp_raw_protocol;
+using boost::date_time::time_resolution_traits_adapted64_impl;
 using boost::function;
+using boost::posix_time::microsec_clock;
+using boost::posix_time::ptime;
+using boost::posix_time::seconds;
+using I2n::Logger::GlobalLogger;
 
 //-----------------------------------------------------------------------------
 // TcpPinger
@@ -35,18 +54,243 @@ TcpPinger::TcpPinger(
         io_service &io_serv,
         const string &source_network_interface,
         const int echo_reply_timeout_in_sec
-)
+) :
+    IoService( io_serv ),
+    DestinationEndpoint(),
+    Socket( IoService, tcp_raw_protocol::v4() ),
+    TcpSegmentReceiveTimer( IoService ),
+    Identifier( 0 ),
+    SequenceNumber( 0 ),
+    TimeSent( microsec_clock::universal_time() ),
+    ReplyBuffer(),
+    ReceivedReply( false ),
+    EchoReplyTimeoutInSec( echo_reply_timeout_in_sec ),
+    PingerStatus( PingStatus_NotSent ),
+    PingDoneCallback()
 {
+    if ( !source_network_interface.empty() &&
+         !select_source_network_interface( source_network_interface ) )
+    {
+        GlobalLogger.error() << "Error: could not bind the socket "
+                "with the local interface." << ::strerror( errno )  << endl;
+    }
+
+    // Create "unique" identifier
+    boost::uuids::random_generator random_gen;
+    boost::uuids::uuid random_tag = random_gen();
+
+    BOOST_ASSERT( sizeof(Identifier) <= random_tag.size() );
+    memcpy( &Identifier, random_tag.data, sizeof(Identifier) );
 }
 
 TcpPinger::~TcpPinger()
 {
 }
 
+/**
+ * @brief Ping a destination address from an available local source.
+ *
+ * @param destination_ip The address of the host to ping.
+ * @param done_handler Done handler will be called on successful ping or timeout
+ */
 void TcpPinger::ping(
         const string &destination_ip,
-        function<void(bool)> /*ping_done_callback*/
+        function<void(bool)> ping_done_callback
 )
 {
     BOOST_ASSERT( !destination_ip.empty() );
+
+    PingDoneCallback = ping_done_callback;
+
+    // Prepare ping
+    set_ping_status( PingStatus_NotSent );
+
+    set_destination_endpoint( destination_ip );
+
+    start_send();
+    start_receive();
+}
+
+uint32_t TcpPinger::get_source_address() const
+{
+    return 0xC0A80166; // TODO 192.168.1.102
+}
+
+uint32_t TcpPinger::get_destination_address() const
+{
+    return ((unsigned int) DestinationEndpoint.data()->sa_data[2] & 0xFF) << 24 |
+           ((unsigned int) DestinationEndpoint.data()->sa_data[3] & 0xFF) << 16 |
+           ((unsigned int) DestinationEndpoint.data()->sa_data[4] & 0xFF) << 8 |
+           ((unsigned int) DestinationEndpoint.data()->sa_data[5] & 0xFF);
+}
+
+void TcpPinger::set_destination_endpoint( const string &destination_ip )
+{
+    BOOST_ASSERT( !destination_ip.empty() );
+
+    uint16_t port = 0;
+    address destination_address = address::from_string( destination_ip );
+    DestinationEndpoint = tcp_raw_protocol::endpoint( destination_address, port );
+}
+
+void TcpPinger::start_send()
+{
+    ++SequenceNumber;
+
+    // Create an TCP header for an ACK request.
+    TcpHeader tcp_header;
+    uint16_t port = ( random() % 16383 ) + 49152; // same as random() % 65536;
+    tcp_header.source_port( port ); // assign an random ephemeral port number
+    tcp_header.destination_port( 80 );
+    tcp_header.sequence_number( ++SequenceNumber );
+    tcp_header.header_length( 5 ); // header size in units of 32 bits (5 words of 32 bits = 5 * 4 bytes = 20 bytes)
+    tcp_header.acknowledgment( true );
+    tcp_header.window_size( 32768 ); // window size
+
+    // TODO isolate this part, so it can change without break the rest of the code
+    uint32_t src_addr = get_source_address();
+    uint32_t dest_addr = get_destination_address();
+
+    uint16_t cksum = tcp_header.calculate_tcp_checksum( src_addr, dest_addr, NULL, 0 );
+    tcp_header.checksum( cksum );
+
+    // Encode the request packet.
+    boost::asio::streambuf request_buffer;
+    ostream os( &request_buffer );
+    os << tcp_header;
+
+    // Send the request.
+    TimeSent = microsec_clock::universal_time();
+
+    string dest_address_string = DestinationEndpoint.address().to_string();
+    BOOST_ASSERT( !dest_address_string.empty() );
+
+    // Send the request
+    try
+    {
+        const_buffers_1 data = request_buffer.data();
+        // Block until send the data
+        size_t bytes_sent = Socket.send_to( data, DestinationEndpoint );
+        if ( bytes_sent != buffer_size( data ) )
+        {
+            GlobalLogger.error() << "Error: fail sending ping data."
+                    << endl;
+        }
+    }
+    catch ( const exception &ex )
+    {
+        GlobalLogger.error() << "Error: fail sending ping data. "
+                << ex.what() << endl;
+    }
+
+    // Wait up to N seconds for a reply.
+    ReceivedReply = false;
+    (void) TcpSegmentReceiveTimer.expires_at(
+            TimeSent + seconds( EchoReplyTimeoutInSec )
+    );
+    TcpSegmentReceiveTimer.async_wait(
+            boost::bind( &TcpPinger::handle_timeout, this )
+    );
+}
+
+void TcpPinger::handle_timeout()
+{
+    ReceivedReply = false;
+    TcpSegmentReceiveTimer.expires_at(
+            TimeSent + seconds( EchoReplyTimeoutInSec )
+    );
+    TcpSegmentReceiveTimer.async_wait(
+            boost::bind( &TcpPinger::start_send, this )
+    );
+}
+
+void TcpPinger::start_receive()
+{
+    // Discard any data already in the buffer.
+    ReplyBuffer.consume( ReplyBuffer.size() );
+
+    // Waiting for a reply. We prepare the buffer to receive up to 64KB.
+    Socket.async_receive(
+            ReplyBuffer.prepare( 65536 ),
+            boost::bind( &TcpPinger::handle_receive_tcp_segment, this, _2 )
+    );
+}
+
+void TcpPinger::handle_receive_tcp_segment( const size_t &bytes_transferred )
+{
+    std::cerr << "handle_receive( " << bytes_transferred << ")" << std::endl; // TODO
+
+    // The actual number of bytes received is committed to the buffer so that we
+    // can extract it using a std::istream object.
+    ReplyBuffer.commit( bytes_transferred );
+
+    istream is( &ReplyBuffer );
+    if ( !is )
+    {
+        GlobalLogger.error() << "Error: can't handle ReplyBuffer" << endl;
+        return;
+    }
+
+    Ipv4Header ipv4_hdr;
+    TcpHeader tcp_hdr;
+    is >> ipv4_hdr >> tcp_hdr;
+
+    // filter out only the TCP reset (RST) replies that match the our expected
+    // sequence number.
+    if ( is &&
+         tcp_hdr.reset() &&
+         tcp_hdr.sequence_number() == SequenceNumber &&
+         ipv4_hdr.get_source_address() == DestinationEndpoint.address() )
+    {
+        ReceivedReply = true;
+
+        // TODO print_rst_xxx
+        // Print out some information about the reply packet.
+        // RST from 74.125.234.17: seq=4279499795 ttl=56 time=1707165.000s
+        ptime now = microsec_clock::universal_time();
+        GlobalLogger.notice() << " RST from " << ipv4_hdr.get_source_address()
+            << ": tcp_seq=" << tcp_hdr.sequence_number()
+            << ", ttl=" << ipv4_hdr.get_time_to_live()
+            << " time=" << (now - TimeSent).total_milliseconds() << " ms" << endl;
+
+        TcpSegmentReceiveTimer.cancel();
+    }
+
+    start_receive();
+}
+
+
+void TcpPinger::set_ping_status( TcpPinger::PingStatus ping_status )
+{
+    PingerStatus = ping_status;
+}
+
+/**
+ * Avoid the socket to drop to another network interface if the destination
+ * is unreachable through the binded interface. Packets are sent only from
+ * this interface.
+ *
+ * @param source_network_interface The network interface to bind the pinger.
+ *
+ * @return false if the bind failed.
+ */
+bool TcpPinger::select_source_network_interface(
+        const string &source_network_interface
+)
+{
+    BOOST_ASSERT( !source_network_interface.empty() );
+
+    int ret = ::setsockopt(
+            Socket.native(),
+            SOL_SOCKET,
+            SO_BINDTODEVICE,
+            source_network_interface.c_str(),
+            source_network_interface.size()
+    );
+    if ( ret == -1 )
+    {
+        return false;
+    }
+
+    return true;
 }
index bb64169..4795f7f 100644 (file)
@@ -23,6 +23,7 @@ on this file might be covered by the GNU General Public License.
 #include <string>
 
 #include <boost/asio.hpp>
+#include <boost/asio/ip/tcp_raw_protocol.hpp>
 #include <boost/function.hpp>
 
 #include "host/pinger.h"
@@ -31,6 +32,10 @@ on this file might be covered by the GNU General Public License.
 // TcpPinger
 //-----------------------------------------------------------------------------
 
+/**
+ * @brief This class performs a TCP ping to host using Boost Asio.
+ * Scope: one object per host.
+ */
 class TcpPinger : public Pinger
 {
 public:
@@ -46,6 +51,59 @@ public:
             boost::function<void(bool)> ping_done_callback
     );
 
+private:
+    enum PingStatus
+    {
+        PingStatus_NotSent,
+        PingStatus_SuccessReply,
+        PingStatus_FailureTimeout,
+        PingStatus_FailureDestinationUnreachable
+    };
+
+private:
+    uint32_t get_source_address() const;
+    uint32_t get_destination_address() const;
+    void set_destination_endpoint( const std::string &destination_ip );
+
+    void start_send();
+    void handle_timeout();
+
+    void start_receive();
+    void handle_receive_tcp_segment( const std::size_t &bytes_transferred );
+
+    void set_ping_status( TcpPinger::PingStatus ping_status );
+
+    bool select_source_network_interface(
+            const std::string &source_network_interface
+    );
+
+private:
+    /// io service object, which has the loop event
+    boost::asio::io_service &IoService;
+    /// the destination host
+    boost::asio::ip::tcp_raw_protocol::endpoint DestinationEndpoint;
+    /// the socket object
+    boost::asio::ip::tcp_raw_protocol::socket Socket;
+    /// the timer of TCP segment receive, triggers the timeout to avoid infinite
+    /// wait
+    boost::asio::deadline_timer TcpSegmentReceiveTimer;
+    /// ICMP packet identifier
+    uint16_t Identifier;
+    /// ICMP packet sequence_number
+    uint16_t SequenceNumber;
+    /// the time when the last ICMP packet was sent
+    boost::posix_time::ptime TimeSent;
+    /// the buffer where the data received will be placed
+    boost::asio::streambuf ReplyBuffer;
+    /// Flag to indicate if we got a reply or not
+    /// number of replies to the ICMP echo request
+    bool ReceivedReply;
+    /// the amount of time to wait for the reply
+    int EchoReplyTimeoutInSec;
+    /// the status of the pinger
+    TcpPinger::PingStatus PingerStatus;
+    /// Callback to notify when the ping is done (got reply/timeout)
+    boost::function< void(bool) > PingDoneCallback;
 };
 
 #endif /* TCPPINGER_H */