ワークステーションが多数ある組織のセキュリティ分析プロジェクトの1つでは、独自の「ピンガー」を開発する必要がありました。
技術仕様の要件は次のとおりです。
- 同時に応答するノードの数は多くする必要があります(複数のサブネット)。
- ポートの数はユーザーが設定します(65535の場合があります)。
- Pingerは、プロセッサのすべての時間を「食べる」べきではありません。
- ピンガーは高速でなければなりません。
pingメソッドはユーザーによって設定され、さまざまなメソッドが利用可能です(ICMP ping、TCPポートping、名前解決)。 当然、最初の考えは既製のソリューション、たとえばnmapを使用することでしたが、そのようなノード(ポート)の範囲では重く非生産的です。
結果がToRに対応するには、実行されるすべての操作が非同期であり、スレッドの単一プールを使用する必要があります。
後者の状況では、必要なすべての非同期プリミティブが含まれているため、Boost.Asioライブラリを開発ツールとして選択するよう促されました。
ピンガーの実装
ピンガーの仕事では、次の階層が実装されます。
Pingクラスは、pingの操作、名前の取得、タスクの完了後、コールバックが開始され、結果が送信されます。 Pingerクラスはping操作を作成し、初期化し、新しいリクエストをキューに入れ、スレッドの数と同時に開くソケットの数を制御し、ローカルポートの可用性を決定します。
待機中のソケットの数、したがって同時にPing可能なポートの数が数千に達する可能性があるという事実を考慮する必要がありますが、アクセスできないノード(ポート)がPingされた場合、プロセッサの負荷は最小限になります。
一方、使用可能なノード(ポート)がpingされると、数百のアクティブソケットがプロセッサの負荷を大幅に増加させます。 アクティブなソケットの数に対するプロセッサの負荷の依存性は非線形であることがわかります。
CPUリソースとping時間のバランスをとるために、アクティブなソケットの数が制御されるプロセッサ負荷が使用されます。
ポートの可用性
pingマシンでは、ファイアウォールによってポートがブロックされる可能性があるため、ピンガーではローカルポートの可用性を判断するメカニズムを実装する必要がありました。 ポートの可用性を判断するために、無効なアドレスへの接続を試みます。成功した場合、ポートはファイアウォールによってエミュレートされます。
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); }
pingプロセスでは、多くの場合、ホストのネットワーク名を取得する必要がありますが、残念ながら、 getnameinfoの非同期バージョンはそのように欠落しています。
Boost.Asioでは、 boost :: asio :: io_serviceオブジェクトにバインドされたバックグラウンドスレッドで非同期の名前の取得が行われます 。 したがって、名前を取得するバックグラウンド操作の数は、 boost :: asio_io_serviceオブジェクトの数と等しくなります 。 一般に名前とpingの受信速度を上げるために、プール内のスレッドの数に応じてboost :: asio :: io_serviceオブジェクトを作成し、各ping操作は独自のオブジェクトによって処理されます。
ping操作の実装
ICMP ping
すべてが非常に簡単です。生のソケットが使用されます。 boost.orgのサンプルの実装に基づいています。 コードは非常にシンプルで、特別な説明は必要ありません。
TCP ping
これは、範囲内の各ポートに対してリモートホストとのTCP接続を確立しようとする試みです。 リモートホストの少なくとも1つのポートへの接続が成功した場合、ホストは利用可能と見なされます。 ポートとの接続を確立できなかった場合、非同期操作の数はゼロに等しくなり、pingオブジェクトは破棄されます。 この場合、pingデストラクタでpingの結果を考慮してコールバックが実行されます。
shared_from_this()ポインターがそれぞれに渡されるため、ping操作オブジェクトは少なくとも1つの非同期操作が実行される限り存在します。
TCP pingプロセスを開始するコード:
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 )); }
コールバック:
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 }
名前解決
ブーストリゾルバーは、渡された引数のタイプに応じて、関数getaddrinfoまたはgetnameinfoを実行します(それぞれ、以下の最初と2番目のコード例)。
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名を確認します。 2番目の例のコードは、IPが既知の場合にノードのFQDNを取得するために使用されます。
ピンガーロジック
実際には、別の抽象化で作成されます。 そして、これにはいくつかの理由があります。
- ソケット操作をピンガーロジックから分離する必要があります。
- 将来のピンガーの仕事の過程でいくつかの戦略を使用する可能性を考慮する必要があります。
- ユニットでカバーするための条件の実装は、個別のエンティティとしてピンガー操作のロジック全体をテストします。
ping操作を実装するクラスは、ロジックを実装するクラスから継承されます。
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); } }
今日は以上です。 ご清聴ありがとうございました! 次の記事では、ネットワークpingプロセスの範囲と、ピンガーのロジックをユニットテストでカバーします。 お楽しみに。
Posted by Sergey Karnaukhov、シニアプログラマー、Positive Technologies( CLRN )。