📜 ⬆️ ⬇️

Asynchronous ping with Boost.Asio

One of the steps to scan a node for vulnerabilities is to determine its network availability. As you know, this can be done in several ways, including through the ping command.

For one of the security analysis projects in an organization with a large number of workstations, we needed to develop our own pinger.

The requirements of the technical specifications were as follows:
')
  1. The number of simultaneously ping nodes should be large (several subnets).
  2. The number of ports is set by the user (maybe 65535).
  3. Pinger should not “eat” all the time of the processor.
  4. Pinger must have high speed.

The ping method is set by the user, various methods are available (ICMP ping, TCP port ping and Resolve name). Naturally, the first thought was to use a ready-made solution, for example, nmap, but it is heavy and unproductive on such ranges of nodes (ports).

In order for the result to correspond to the TOR, all performed operations must be asynchronous and use a single pool of threads.

The latter circumstance prompted us to choose the Boost.Asio library as a development tool, since it contains all the necessary asynchronous primitives.

Pinger implementation


The following hierarchy is implemented in the pinger’s work:
The Ping class performs ping operations, getting a name, after the execution of tasks, a callback is initiated, to which the result is sent. The Pinger class creates ping operations, initializes, places new requests in a queue, controls the number of threads and the number of simultaneously opened sockets, determines the availability of local ports.

It is necessary to take into account the fact that the number of waiting sockets, and therefore simultaneously pinged ports, can go up to several thousand, while the processor load may be minimal if unavailable nodes (ports) are pinged.

On the other hand, if available nodes (ports) are pinged, then several hundred active sockets significantly increase the load on the processor. It turns out that the dependence of the processor load on the number of active sockets is nonlinear.

For a balance between CPU resources and ping time, CPU utilization is used, based on which the number of active sockets is controlled.

Port Availability

On a machine that performs ping, the ports can be blocked by a firewall, so our pinger needed to implement a mechanism for determining the availability of local ports. To determine the availability of the port, we are trying to connect to an invalid address: if successful, the port is emulated by a firewall.

typename PortState::Enum GetPortState(const Ports::value_type port) { boost::recursive_mutex::scoped_lock lock(m_PortsMutex); PortState::Enum& state = m_EnabledPorts[port]; if (state == PortState::Unknown) { state = PortState::Pending; const std::size_t service = GetNextService(); const SocketPtr socket(new TCPSocket(GetService(service))); const TimerPtr timer(new Timer(GetService(service))); socket->async_connect( Tcp::endpoint(Address(INVALID_IP), port), boost::bind( &PingerImpl::GetPortStateCallback, this, ba::placeholders::error, port, socket, timer ) ); timer->expires_from_now(boost::posix_time::seconds(1)); timer->async_wait(boost::bind(&PingerImpl::CancelConnect, this, socket)); } return state; } void GetPortStateCallback(const boost::system::error_code& e, const Ports::value_type port, const SocketPtr, const TimerPtr) { boost::recursive_mutex::scoped_lock lock(m_PortsMutex); m_EnabledPorts[port] = e ? PortState::Enabled : PortState::Disabled; } void CancelConnect(const SocketPtr socket) { boost::system::error_code e; socket->close(e); } 

During the ping process, it is often necessary to get the network host name, but unfortunately, the asynchronous version of getnameinfo is missing as such.

In Boost.Asio, asynchronous name retrieval takes place in a background thread that is bound to the object boost :: asio :: io_service . Thus, the number of background operations to get the name is equal to the number of objects boost :: asio_io_service . To increase the speed of receiving names and ping as a whole, we create objects boost :: asio :: io_service by the number of threads in the pool, with each ping operation processed by its object.

Ping operation


ICMP ping

It's pretty simple: raw sockets are used. Based on the implementation of examples boost.org . The code is quite simple and does not require special explanation.

TCP ping

It is an attempt to establish a TCP connection with a remote node for each port in the range. If the connection attempt with at least one port of the remote node is successful, the node is considered available. If it is not possible to establish a connection with any port, the number of asynchronous operations becomes zero and the ping object is destroyed. In this case, the callback is executed in the ping destructor based on the results of the ping.

The ping operation object exists as long as at least one asynchronous operation is performed, since the pointer shared_from_this () is passed to each of them.

The code that starts the TCP ping process:

 virtual void StartTCPPing(std::size_t timeout) override { boost::mutex::scoped_lock lock(m_DataMutex); if (PingerLogic::IsCompleted() || m_Ports2Ping.empty()) return; Ports::const_iterator it = m_Ports2Ping.begin(); const Ports::const_iterator itEnd = m_Ports2Ping.end(); for (; it != itEnd; ) { const PortState::Enum state = m_Owner.GetPortState(*it); //      —  if (state == PortState::Disabled) { it = m_Ports2Ping.erase(it); continue; } else if (state == PortState::Pending) //  ,      { ++it; continue; } if (m_Owner.CanAddSocket()) // ,        { PingPort(*it); it = m_Ports2Ping.erase(it); if (m_Ports2Ping.empty()) break; } else { break; } } if (!m_Ports2Ping.empty()) { //   ,     m_RestartPingTimer.expires_from_now(boost::posix_time::milliseconds(DELAY_IF_MAX_SOCKETS_REACHED)); m_RestartPingTimer.async_wait(boost::bind( &Ping::StartTCPPing, shared_from_this(), timeout )); } //           m_StartTime = boost::posix_time::microsec_clock().local_time(); m_PingTimer.expires_from_now(boost::posix_time::seconds(timeout)); m_PingTimer.async_wait(boost::bind(&Ping::OnTimeout, shared_from_this(), ba::placeholders::error, timeout)); } 

The code that starts the asynchronous connection:

 void PingPort(const Ports::value_type port) { const Tcp::endpoint ep(m_Address, port); const SocketPtr socket(new TCPSocket(m_Owner.GetService(m_ServiceIndex))); m_Sockets.push_back(socket); m_Owner.OnSocketCreated(); //     socket->async_connect(ep, boost::bind( &Ping::TCPConnectCallback, shared_from_this(), boost::asio::placeholders::error, socket )); } 

Callback:

 void TCPConnectCallback(const boost::system::error_code& e, const SocketPtr socket) { m_Owner.OnSocketClosed(); //     if (!e) TCPPingSucceeded(socket); else TCPPingFailed(socket); } 

Relevant Handlers:

 void TCPPingSucceeded(const SocketPtr socket) { const boost::posix_time::time_duration td(boost::posix_time::microsec_clock::local_time() - m_StartTime); boost::system::error_code error; socket->shutdown(TCPSocket::shutdown_both, error); // pinged successfully, close all opened sockets boost::mutex::scoped_lock lock(m_DataMutex); CloseSockets(); PingerLogic::OnTcpSucceeded(static_cast<std::size_t>(td.total_milliseconds())); } void TCPPingFailed(const SocketPtr socket) { // ping on this port fails, close this socket boost::system::error_code error; socket->close(error); boost::mutex::scoped_lock lock(m_DataMutex); const std::vector<SocketPtr>::const_iterator it = std::remove( m_Sockets.begin(), m_Sockets.end(), socket ); m_Sockets.erase(it, m_Sockets.end()); if (m_Sockets.empty()) m_PingTimer.cancel(); // all ports failed, cancel timer } 

Name resolving

The boost resolver, depending on the type of argument passed, performs the functions getaddrinfo or getnameinfo (the first and second code examples below, respectively).

 virtual void StartResolveIpByName(const std::string& name) override { const typename Resolver::query query(Tcp::v4(), name, ""); m_Resolver.async_resolve(query, boost::bind( &Ping::ResolveIpCallback, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::iterator )); } virtual void StartResolveNameByIp(unsigned long ip) override { const Tcp::endpoint ep(Address(ip), 0); m_Resolver.async_resolve(ep, boost::bind( &Ping::ResolveFQDNCallback, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::iterator )); } 

The first code example is used to get the IP address; similar code is used to verify the NetBIOS name. The code from the second example is used to get the host's FQDN, in case its IP is already known.

Pinger logic

Actually, it is in a separate abstraction. And we have several reasons for this.

  1. It is necessary to separate the execution of operations with sockets from the pinger logic.
  2. It is necessary to provide for the possibility of using several strategies in the course of the pinger’s work in the future.
  3. The implementation of the conditions for the unit tests to cover the entire logic of the pinger's work as a separate entity.

The class that implements the ping operation is inherited from the class that implements the logic:

 class Ping : public boost::enable_shared_from_this<Ping>, public PingerLogic 

At the same time, the corresponding virtual methods are redefined in the Ping class:
 //! Init ports virtual void InitPorts(const std::string& ports) = 0; //! Resolve ip virtual bool ResolveIP(const std::string& name) = 0; //! Start resolve callback virtual void StartResolveNameByIp(unsigned long ip) = 0; //! Start resolve callback virtual void StartResolveIpByName(const std::string& name) = 0; //! Start TCP ping callback virtual void StartTCPPing(std::size_t timeout) = 0; //! Start ICMP ping virtual void StartICMPPing(std::size_t timeout) = 0; //! Start get NetBios name virtual void StartGetNetBiosName(const std::string& name) = 0; //! Cancel all pending operations virtual void Cancel() = 0; 

We will not describe the implementation of the PingerLogic class in detail; we will only give examples of code that speak for themselves.

 //! On ping start void OnStart() { InitPorts(m_Request.m_Ports); const bool ipResolved = ResolveIP(m_Request.m_HostName); if (!ipResolved) StartResolveIpByName(m_Request.m_HostName); } //! On ip resolved void OnIpResolved(const unsigned long ip) { boost::recursive_mutex::scoped_lock lock(m_Mutex); m_Result.m_ResolvedIP = ip; if (m_Request.m_Flags & SCANMGR_PING_RESOLVE_HOSTNAME) { m_HasPendingResolve = true; StartResolveNameByIp(ip); } if (m_Request.m_Flags & SCANMGR_PING_ICMP) { // if tcp ping needed it will be invoked after icmp completes StartICMPPing(m_Request.m_TimeoutSec); return; } if (m_Request.m_Flags & SCANMGR_PING_TCP) { // in case of tcp ping only StartTCPPing(m_Request.m_TimeoutSec); } } 

That's all for today. Thanks for attention! In the next article we will talk about the coverage of the network ping process and the logic of our pinger with unit testing. Keep for updates.

Author: Sergey Karnaukhov, Senior Programmer, Positive Technologies ( CLRN ).

Source: https://habr.com/ru/post/156795/


All Articles