本文は 糖菓・部落 に同時に公開されています。
これは開発に関する雑談に過ぎず、実質的な技術内容は含まれていません。また、NyaTrace プロジェクトは大幅に改良され、多くの期待される目標が達成されたため、記事の内容はやや古くなっている可能性があります。コードや実行可能なプログラムをお求めの場合は、nyatrace.app に移動してください。
GeoIP2 を購入した後の 3 つ目のプロジェクトとして(前の 2 つはそれぞれ喵窝のログイン位置表示と NyaSpeed の実際の位置表示です)、今回は長い間未解決だった願いをかなえることを望んでいます:可視化された IP の詳細情報を伴うルートトレースプログラムを書くことです。
インスピレーションの源#
17monipdb.exe というツールやその後継の Best Trace について聞いたことがあるかもしれません。これは私がルートトレース作業に使用していた唯一の選択肢でした。しかし、開発者の IPIP.NET が徐々にエコシステムを閉じた商業化に移行するにつれて(すべての製品がコンサルティング価格の企業モデル)、本能的な拒絶感から代替の解決策を探し始めました。
その後、新しいツール WorstTrace が登場しました(おそらく Best Trace に対抗するためでしょう)が、Electron でパッケージ化されているため、サイズが大きく、UI はより現代的ですが、私には良い解決策とは思えませんでした。
さらに、上記の 2 つのツールはすべてクローズドソース製品であり、コードのセキュリティ監査は不可能であったため、長い間、実際にはシステムに付属のtracert
とHE BGP ToolkitおよびCensys Searchを組み合わせて使用していました。
しかし、これは長期的な解決策ではありません。手動で操作する必要があり、リンクの状況を迅速に判断するには適していません。また、HE と Censys の接続状況に依存しており、特定の状況では必要なデータを取得できないため、ローカルの実行環境でルートトレースを実行するタスクに依存することが生まれました。最近、MaxMind の GeoIP2 City と ISP の 1 か月のサブスクリプションを購入し、これら 2 つのデータベースをうまく活用できないかと考えました。
開発の進行#
開発作業の第一歩はニーズの分析ですので、3 つのモジュールに分けました:
- ルートトレース
- グラフィカルインターフェース
- IP データベースの読み取り
ルートトレース#
参考の検索#
最初のステップで壁にぶつかりました。route trace open source
を検索すると、最初に出てきたのは Open Visual Traceroute という Java で開発されたツールです。Java に偏見があるかもしれませんが、私はその開発されたソフトウェアが重く、環境に高度に依存していると常に考えています。ルートトレースという小さな機能を実現するために、すべてのユーザーが巨大なハードディスクを消費するものをインストールする必要があると考えると、悲しみが心に湧き上がります。その後、中国語で开源 路由追踪
を検索し、NextTraceを見つけましたが、喜んで実行しようとしたところ、Windows をサポートしていないことがわかり、心が冷えました。
その間に、golang 版の traceroute 実装を見つけ、golang.org/x/net/ipv4
というパッケージが言及されているのを見ましたが、Windows 機能をサポートしていないことがわかり、TraceRoute のサンプルに基づいて Docker イメージを作成して Windows 上で Linux の実装を実現しようと考えましたが、あまりにも複雑で、頭を振って否定しました。
いつそれを検索したのかは忘れましたが、TraceRoute の実装(Windows 下 C/C++ 基于原始套接字)というブログを見つけたことだけは覚えています。私が考えていることを理解している記事を見つけたとき、思わず泣きそうになりました。話を戻すと、このブログは私が必要としているルートトレース機能の低レベルのソケット実装について説明しており(外部コンポーネントに依存せず、完全に低レベルのシステムインタラクションに依存しています)、その実装方法の説明を注意深く読んだ後、まずコードをダウンロードしてテストすることに決めました。
結果は、喜びと悲しみで表現できるものでした。喜びは、このプログラムが実行でき、他の日常的に見つけた無関係なエラーメッセージの地獄のコードとはまったく異なるものでした。悲しみは、その結果が期待通りではなく、最後のホップで IP を取得できる以外はすべてのパケットがタイムアウトを示したことです。
WireShark を開いてパケットをキャプチャしましたが、明らかに返答のあるTime-to-live exceeded
パケットが多く存在しましたが、ソケットの recvFrom では取得できませんでした。
そのため、引数の問題だと思い、しばらく探しましたが、成果はありませんでした。また、Windows 11 が低レベルのソケット設定を変更したのではないかと思い(参考にした記事は 2020 年のものでした)、関連資料を探しても全く何も得られませんでした。途方に暮れているとき、別の言語を試してみることにしました。
Python を思いつき、環境パッケージを大きくすることはできるので、使えないわけではありません。ちょうど Python にはルートトレースをサポートする操作ライブラリがあり、それは Scapy です。しかし、残念ながら、さまざまな文書やブログを探しましたが、人々は公式のあいまいな文書を中国語に翻訳して貼り付けるのが好きで、文脈のないコードの断片を貼り付けるだけでしたが、このルートトレース機能をうまく使用して何かを達成する方法については、価値のある情報を見つけることができず、諦めざるを得ませんでした。
その後、nodejs-tracerouteというライブラリを見つけ、システムに付属の tracert 機能を呼び出してその戻り値を使用して結果を構築する巧妙なテクニックを使用していることに気付きました。そのとき、私は無効な情報の海に翻弄されている状態で、あまり考えずに、できるだけ早くこのタスクを完了させたいと思っていました。しかし、なぜこの実装方法を選ばなかったのかは、後で言及するグラフィカルインターフェースに関係しているので、後で読んでください~
パケットタイムアウト問題の解決#
とにかく、翌日、ぼんやりと検索していると、rust で実装されたtracertの Windows ユーザーへのヒントを見つけました:
ICMP Time-to-live Exceeded
およびICMP Destination (Port) Unreachable
パケットを受信できるようにファイアウォールルールを設定する必要があるかもしれません。
netsh
の例netsh advfirewall firewall add rule name="All ICMP v4" dir=in action=allow protocol=icmpv4:any,any netsh advfirewall firewall add rule name="All ICMP v6" dir=in action=allow protocol=icmpv6:any,any
当時、ファイアウォールがこれらの受信リクエストパケットをブロックするとは全く考えていませんでした。WireShark がキャプチャできたのは、WinCap を使用してさらにレベルを下げたため、ネットワークカード上の純粋なデータパケットをキャプチャできたからかもしれません。試してみる気持ちで、上記のコードを実行しました(管理者権限が必要です)。結果は驚きで表現できました:
Windows に付属の tracert がなぜこの制限を回避できるのかは、記事の最後に言及されている WinMTR を研究する必要があります。 システムが提供するダイナミックリンクライブラリインターフェースを呼び出して実現しているため、手動でリクエストパケットを構築するのではありません。NyaTrace はルートトレースアルゴリズムを更新し、今ではファイアウォールルールを追加する必要がなくなりました ♥
グラフィカルインターフェース#
グラフィカルインターフェースライブラリの選択#
基本機能の実験が成功した後、次のモジュールに進むことになります:グラフィカルインターフェース。最初に成功したのは NodeJS ベースのパッケージだったので、それを基に試してみることにしました。Electron のリソース使用が不満だったため(ルートトレースを実行するのは簡単ではありません!)、nodeguiを選択し、その React ラッパーであるReact NodeGuiを試してみることにしました。しかし、サンプルプロジェクトを初期化しようとしたときにコンパイラがエラーを示したため、結局、基本的な使い方を確認することにしました~
そこで、最初の nodegui の使い方に戻り、実際には Qt エンジンライブラリを呼び出していることがわかりました。そのため、Qt の操作に似た部分があります。しばらく試行錯誤した結果、基本機能が比較的完全なウィンドウインターフェースを成功裏に組み立てました:
実行成功!勢いに乗って、トレースと内容の充填ロジックを書き、実行ボタンをクリックし、アドレスを入力し、開始ボタンを押しました ——
友情の小舟はひっくり返りました。
この道が通じないことに気づいた後、私は他の解決策を研究し続け、ファイアウォールによるパケットタイムアウト問題を解決した後、C++ を主要な開発言語として選択しました。
この時点で次の議題に入ります:C++ の GUI ライブラリはたくさんありますが、どれを選ぶのが良いでしょうか?
学生時代に C++ を多く書いたため、MFC、MSVC、Qt の 3 つのクラシックなグラフィカルインターフェースライブラリに少し触れたことがあります。このプロジェクトの開発の主な目標は Windows プラットフォームですが、将来的には他のプラットフォーム(Linux や macOS など)にすべての開発環境を移行する可能性があります。そのため、将来の互換性を確保するために、Qt をグラフィックライブラリとして選択しました。また、Qt は UI を手動で作成できるため、怠けたい開発者にとって非常に優しいです。
しかし、Qt 自体は友好的ではありません。なぜなら、非常に高価な商業ソリューションだからです(企業版とプロフェッショナル版の 2 つの有料リースプランしかなく、プロフェッショナル版は企業版よりも 8%安いだけで、企業版は395 USD 毎月
です)。無料で使用できるコミュニティ版は基本的な機能とリソースしかなく、オープンソースライセンスの制限を受けます。しかし、私にとっては機能を実現することが重要であり、オープンソースの問題を心配する必要はありません(このプロジェクトは元々オープンソースにするつもりで、私が書くものは基本的にオープンソースです)ので、これらの悩みはありません。
Qt 6 がオープンソースコミュニティを実験場として扱う行動が予期しない問題を引き起こす可能性を懸念し、私は 5 LTS バージョンを使用しています。
実際、この決定は非常に賢明でした。なぜなら、Qt 6 は QtLocation や QtPositioning などの地図関連コンポーネントの移行作業がまだ完了していないため、もし当初 Qt 6 を選んでいたら、現在の地図機能を追加することができなかったでしょう。
簡単に UI を組み立て、いくつかのバージョンを繰り返した結果、執筆時点ではこのようになっています:
依然としてミニマリストスタイルを追求し、関係する機能コンポーネントを配置するだけです。将来的には地図機能を追加するかもしれませんが、今はこれで十分です。
LOGO は Nucleo アイコンライブラリから選択したworld-marker
アイコンを使用し、ピンの色を赤から私たちの象徴的な青色62b6e7
に変更しました。特に技術的なことはありません。
スレッドの最適化#
開発中に 1 つの問題に直面しました:ルートトレースは連続的かつブロッキングなプロセスであり、トレースのフローチャンクをメインスレッドに置くと、結果が表示されるまでメインスレッドのレンダリングがブロックされ続け、プログラムのインタラクションがカクカクし、システムがプログラムが応答していないと警告し、ウィンドウのドラッグなどの操作ができなくなります。
Qt はこのような状況に対処するために、QThread クラスを設計してバックグラウンドスレッドのタスクを簡単に管理できるようにしました。QThread を継承したクラスを設計し、ブロックされる操作を run () 関数に置くことで、メインスレッドから start () 関数を呼び出すだけで開始できます。
注意が必要なのは、サブスレッドは UI の変更操作を呼び出すことができず、処理結果をメインスレッドに emit するために signals スロットを使用して、メインスレッドが UI の変更を実行する必要があることです。
スケーリングの最適化#
Qt のデフォルトのインターフェース配置モードでは、ウィンドウを拡大縮小すると、その中のコンポーネントが変化しないため、非常に醜くなります。
配置モードをグリッドモード(Grid)に設定したところ、スケーリングの問題が自動的に解決され、とても快適になりました。
IP データベースの読み取り#
MaxMind の他の言語(nodejs、go など)のクライアント SDK は非常に良く封装されており、C++ のクライアントも便利で使いやすいと思っていましたが、C++ には存在しないパッケージ管理システムの問題を見落としていました。
公式が提供するサンプルコードの例は C# で、NuGet を使用してパッケージ管理を行っています。しかし、C/C++ では同じように簡単に使用できないため、非常に困ったことになります。
面白いことに、実際には公式が C 操作のクライアントを開発しており、GeoIP2 and GeoLite2 Database Documentation の Official API Clients セクションにリストされています。それはlibmaxminddbですが、どうやら構築してインストールする必要があるようで、Windows プラットフォームに非常に優しいわけではないようです。
そのため、万能の検索エンジンに助けを求めましたが、やはり収穫はなく、得られた情報は Linux 上での構築、インストール、開発操作に関するもので、非常に困惑しました。
実際、この時点でかなり疲れており、諦めかけていましたが、死馬を生き馬にするつもりで、プロジェクトリポジトリのコードファイルとヘッダーファイルを NyaTrace プロジェクトに無思考で追加しました。おそらく、開発者がもともとマルチプラットフォーム互換の方法で開発していたため、直接使用してもエラーが出ず、動的リンクライブラリをコンパイルして接続し、パッケージ化する手間を省くことができました。これには非常に興奮し、気がつくとすでに深夜であることを忘れていました。
しかし、喜びが冷める間もなく、新たな問題が発生しました:私はどのようにその操作関数を呼び出すべきか?いくつかの中国語の資料を調べましたが、マッチした IP アドレスのすべての情報を標準出力に可視化して印刷することがほとんどであり、これは厳密には私のニーズに合致しないため、再び公式文書に助けを求めました。
幸い、公式文書はデータの読み取り操作の呼び出し方法を比較的詳細に説明しており、最初に完全な Map Object を取得し、次に階層 K-V を使用して必要なキーを選択します。
文書やさまざまな資料の dump の使用法に従って、すべてのデータを取得しました:
データは長いので、ここでは少しだけ列挙します。
その中のキー階層の順序に従って、MMDB_get_value
関数を使用して読み取り、最後に NULL を入力する必要があります(なぜかはよくわかりませんが、入力しないと取得できません):
必要なフィールドを取得しました。すぐに新たな問題に直面しました。これらの文字列自体は\0
を末尾に使用していないため、取得した文字列が超長になり、多くの無効なデータを含んでいました。
正しく印刷できるMMDB_dump_entry_data_list
関数に助けを求めました —— そのコードを読んでみると、data_size
を使用してフィールドの長さを指定し、データを抽出する際に新しいスペースを作成し、完全な文字列をコピーして末尾に 0 を埋めて返すことがわかりました。
同じ考え方に基づいて、この操作を含むヘッダーファイルを呼び出しましたが、C++ ではポインタタイプの定義が C よりも厳格で、元々正常に実行されていた関数がタイプ不一致のエラーを示しました。さらに悪いことに、Windows 上ではこの文字列処理関数が実装されていないようです(見逃した可能性もあります)。他に方法がないので、コピーしてポインタに強制的に型変換を行い、独立したツール関数として存在させることにしました。
この時点でコードは混沌としていましたが、各モジュールがそれぞれの責任を果たしており、機能は正常に動作していたため、急いで混ぜ合わせてパッケージを提出しました。その後、いくつかの最適化処理を実行し、IP 読み取りの呼び出しを IPDB クラスに封装し、トレーススレッドが起動する際にこのクラスを構築し、実行中にオブジェクトレベルの呼び出しを実行できるようにしました。これにより、将来的な操作のアップグレードやインターフェースの分離が容易になります。
この時点で、NyaTrace の基本機能はほぼ整理されましたので、この投稿が生まれました:
ビルドとパッケージ化#
この部分は標準的なプロセスです:
- Qt の左下隅のモード選択を Release(リリース)モードに切り替えます。
- 🔨ボタンをクリックして実行可能プログラムパッケージをビルドします。
- ビルドされた実行可能プログラムパッケージを見つけます(通常、プロジェクトの上位ディレクトリにあり、
build-プロジェクト名-ビルド環境-Release
という名前の作業環境があり、その中の release サブディレクトリにビルドされた.exe ファイルがあります。これを空のディレクトリに移動します)。 - スタートメニューで、対応するパッケージ環境名のコンソール(例えば、MSVC でビルドされた場合は MSVC、MinGW でビルドされた場合は MinGW)を見つけてクリックします。
- ドライブ操作と cd コマンドを使用して、先ほど.exe ファイルを置いた空のディレクトリに移動し、
windeployqt 実行可能ファイル名.exe
コマンドを実行して、必要な動的リンクライブラリなどのファイルをコピーさせます。たくさんのものが必要で、本来は小さなプログラムが一気に多くの実行環境を必要とします(しかし、Electron や Java よりは軽いです)。 - この時点でプログラムを実行できるようになります!
注意が必要なのは、GeoIP2 をクエリ依存として使用するため、リリース時に
mmdb
という名前の空のディレクトリを作成して、ユーザーがデータベースを配置して使用できるように指示するのが最善です(MaxMind のユーザー契約では、ソフトウェアパッケージに彼らのデータベース製品を含めることは許可されておらず、データベースの有効性を考慮すると、ユーザーが最新のものを自分でダウンロードする方が良いです)。
後記#
Best Trace がなぜそんなに速いのか#
それは非同期の並行パッケージ送信の考え方を使用しているため、ここで実装されている同期順序のパッケージ送信ではなく、対応する結果がすぐに得られ、タイムアウト部分はせいぜい一回だけトリガーされます。
tracert がなぜそんなに遅いのか#
それは、同期順序のパッケージ送信を使用しているだけでなく、各ホップで 3 つのパッケージを送信し、さらに不明な理由から、たとえ 3 つの成功したパッケージであっても数秒待つことになります。応答できない中継があると、3 回連続でリクエストがタイムアウトになるため、一ホップで 3 * 3 = 9 秒を消費することになり、自然と遅くなります。
ただし、パッケージを繰り返し送信することには利点もあります。時々、中継は完全にパッケージを返さないわけではなく、ちょうどその時にパッケージを返すことができれば、その IP アドレスを取得できることがあります。
他に解決策はあるのか#
開発が完了した後、偶然WinMTR (Redux)というプロジェクトを見つけました。これはルートトレースのコア機能を開発するための参考として使用できると思います。
古いですが、使いやすいです。Windows の強力な互換性は確かに(逃げ)
また、ファイアウォールのルールを無視できるようですので、さらに深く研究する価値があります!
参考資料#
- 図解 | 9 分で traceroute(ルートトレース)の原理と実装を理解する
- TraceRoute の実装(Windows 下 C/C++ 基于原始套接字)
- QThread Class
- Qt におけるマルチスレッドの使用
- libmaxminddb - MaxMind DB ファイルを扱うためのライブラリ
- Linux C で libmaxminddb を使用して GeoIP2 MMDB から IP の地理位置を取得する
- GeoIP2 を使用して IP の地理位置を取得する
- Qt でウィンドウをページの拡大に合わせて大きくする方法
- Qt プログラムのパッケージ化の詳細(Windows プラットフォーム向け)