
Asynchronous Ping Using Boost.Asio

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:
- The number of simultaneously responding nodes should be large (several subnets).
- The number of ports is set by the user (may be 65535).
- Pinger should not "eat" all the time the processor.
- 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).
For the result to correspond to the ToR, all operations performed 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 work of the pinger:
The Ping class performs the operations of ping, getting the name, and after the tasks are completed, a callback is initiated, into which the result is transmitted. The Pinger class creates ping operations, initializes, queues new requests, controls the number of threads and the number of simultaneously open 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 pingable ports, can reach several thousand, while the processor load can be minimal if inaccessible 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.
To balance between CPU resources and ping time, processor loading is used, based on which the number of active sockets is controlled.
Port availability
On a ping machine, ports can be blocked by a firewall, so in our pinger it was necessary to implement a mechanism for determining the availability of local ports. To determine the availability of the port, we try to connect to an invalid address: if it succeeds, 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);
}
In the process of ping, you often have to get the network name of the host, but unfortunately, the asynchronous version of getnameinfo is missing as such.
In Boost.Asio, asynchronous name acquisition takes place in a background thread that is bound to the boost :: asio :: io_service object . Thus, the number of background operations to get the name is equal to the number of boost :: asio_io_service objects . To increase the speed of receiving names and ping in general, we create boost :: asio :: io_service objects by the number of threads in the pool, and each ping operation is processed by its own object.
Ping operation implementation
ICMP ping
Everything is quite simple: raw sockets are used. Based on the implementation of examples from boost.org . The code is quite simple and requires no special explanation.
TCP ping
This is an attempt to establish a TCP connection with a remote host for each port in the range. If the attempt to connect to at least one port of the remote host is successful, the host is considered available. If it was not possible to establish a connection with any port, the number of asynchronous operations becomes equal to zero and the ping object is destroyed. In this case, a callback is performed in the ping destructor taking into account the ping results.
The ping operation object exists as long as at least one asynchronous operation is performed, since the shared_from_this () pointer is passed to each of them .
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));
}
Code that starts an 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(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::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 the 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 obtain an IP address; similar code is used to verify the NetBIOS name. The code from the second example is used to obtain the FQDN of the node if its IP is already known.
Pinger logic
Actually, it is made in a separate abstraction. And we have several reasons for this.
- It is necessary to separate socket operations from pinger logic.
- It is necessary to provide for the possibility of using several strategies in the course of the pinger's work in the future.
- Implementation of conditions for covering with unit tests the whole logic of pinger operation 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, 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 code examples 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 cover the coverage of the network ping process and the logic of our pinger with unit testing. Keep for updates.
Posted by Sergey Karnaukhov, Senior Programmer, Positive Technologies ( CLRN ).