Асинхронный пинг с помощью Boost.Asio

в 8:20, , рубрики: boost.asio, c++, Блог компании Positive Technologies, пингер, разработка, метки: , ,

Асинхронный пинг с помощью Boost.AsioОдним из этапов сканирования узла на наличие уязвимостей является определение его сетевой доступности. Как известно, сделать это можно несколькими способами, в том числе и посредством команды ping.

Для одного из проектов по анализу защищенности в организации с большим количеством рабочих станций нам понадобилось разработать собственный «пингер».

Требования технического задания были следующие:

  1. Количество одновременно пингуемых узлов должно быть велико (несколько подсетей).
  2. Количество портов задается пользователем (может быть 65535).
  3. Пингер не должен «съедать» все время процессора.
  4. Пингер должен обладать высоким быстродействием.

Способ пинга задается пользователем, доступны различные способы (ICMP ping, TCP port ping и Resolve name). Естественно, первой мыслью было использовать готовое решение, например, nmap, но он тяжеловат и непроизводителен на таких диапазонах узлов (портов).

Чтобы результат соответствовал ТЗ, все выполняемые операции должны быть асинхронными и использовать единый пул потоков.

Последнее обстоятельство подтолкнуло нас к выбору библиотеки Boost.Asio в качестве средства разработки, поскольку она содержит все необходимые асинхронные примитивы.

Реализация пингера

В работе пингера реализована следующая иерархия:
Класс Ping выполняет операции пинга, получения имени, после выполнения заданий инициируется обратный вызов (callback), в который передается результат. Класс Pinger создает операции пинга, инициализирует, помещает новые запросы в очередь, управляет количеством потоков и количеством одновременно открытых сокетов, определяет доступность локальных портов.

Необходимо принимать во внимание тот факт, что количество ожидающих сокетов, а значит и одновременно пингуемых портов, может доходить до нескольких тысяч, при этом загрузка процессора может быть минимальна, в случае если пингуются недоступные узлы (порты).

С другой стороны, если пингуются доступные узлы (порты), то несколько сотен активных сокетов значительно увеличивают нагрузку на процессор. Получается, что зависимость загрузки процессора от количества активных сокетов нелинейна.

Для баланса между ресурсами ЦП и временем пинга используется загрузка процессора, исходя из которой контролируется количество активных сокетов.

Доступность портов

На машине, выполняющей пинг, порты могут быть заблокированы межсетевым экраном, поэтому в нашем пингере необходимо было реализовать механизм определения доступности локальных портов. Чтобы определить доступность порта пытаемся осуществить соединение с невалидным адресом: если удалось — порт эмулирован межсетевым экраном.

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);
	}

В процессе пинга довольно часто приходится получать сетевое имя узла, но к сожалению, асинхронная версия getnameinfo отсутствует как таковая.

В Boost.Asio асинхронное получение имен проходит в фоновом потоке, который привязан к объекту boost::asio::io_service. Таким образом количество фоновых операций получения имени равно количеству объектов boost::asio_io_service. Чтобы повысить быстродействие получения имен и пинга в целом, создаем объекты boost::asio::io_service по числу потоков в пуле, при этом каждая операция пинга обрабатывается своим объектом.

Реализация операции пинга

ICMP ping

Все довольно просто: используются сырые сокеты. За основу взята реализация из примеров boost.org. Код достаточно прост и не требует особых пояснений.

TCP ping

Представляет собой попытки установления TCP-соединения с удаленным узлом для каждого порта из диапазона. В случае если попытка соединения хотя бы с одним портом удаленного узла успешна — узел считается доступным. Если же соединение ни с одним портом установить не удалось, количество асинхронных операций становится равным нулю и объект пинга уничтожается. В этом случае в деструкторе пинга выполняется callback c учетом результатов пинга.

Объект операции пинга существует, пока выполняется хотя бы одна асинхронная операция, поскольку в каждую из них передается указатель shared_from_this().

Код, запускающий процесс TCP-пинга:

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));
		}

Код, запускающий асинхронное соединение:

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);
		}		

Соответствующие обработчики:

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

Бустовый резолвер в зависимости от типа переданного аргумента выполняет функции getaddrinfo или getnameinfo (первый и второй примеры кода ниже соответственно).

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
			));
		}		

Первый пример кода используется для получения IP-адреса; аналогичный код используется для проверки NetBIOS-имени. Код из второго примера используется для получения FQDN узла, в случае если его IP уже известен.

Логика пингера

Собственно, она вынесена в отдельную абстракцию. И у нас для этого есть несколько причин.

  1. Необходимо отделить выполнение операций с сокетами от логики пингера.
  2. Нужно предусмотреть возможность использования в будущем нескольких стратегий в ходе работы пингера.
  3. Реализация условий для покрытия юнит-тестами всей логики работы пингера как отдельной сущности.

Класс, реализующий операцию пинга, унаследован от класса, реализующего логику:

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

При этом в классе Ping переопределяются соответствующие виртуальные методы:

//! 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;

Детально реализацию класса PingerLogic описывать не будем, приведем лишь примеры кода, которые говорят сами за себя.

//! 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);
		}
	}

На сегодня все. Спасибо за внимание! В следующей статье мы расскажем о покрытии процесса сетевого пинга и логики нашего пингера модульным тестированием. Следите за обновлениями.

Автор: Сергей Карнаухов, старший программист Positive Technologies (CLRN).

Автор: ptsecurity

Источник

Поделиться

* - обязательные к заполнению поля