本文は 糖菓・部落 に同時に公開されています。
これは開発に関する雑談に過ぎず、実質的な技術内容は含まれていません。また、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 上で Docker を使って 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
に変更して作成しました。特に技術はありません。
スレッド最適化#
開発中に一つの問題に直面しました:ルートトレースは連続的でブロッキングなプロセスであり、トレースのフロー関数をメインスレッドに置くと、結果が表示されるまでメインスレッドのレンダリングがブロックされ、プログラムのインタラクションがカクつき、システムがプログラムが応答していないと警告し、ウィンドウのドラッグ操作ができなくなります。
Qt はこのような状況に対処するために、QThread クラスを設計してバックグラウンドスレッドのタスクを簡単に管理できるようにしました。QThread を継承したクラスを設計し、ブロックされる操作を run () 関数に配置し、メインスレッドから start () 関数を呼び出すことで起動できます。
注意が必要なのは、サブスレッドが UI の変更操作を呼び出すことはできず、結果をメインスレッドに emit するために signals スロットを通じて処理する必要があることです。
スケーリング最適化#
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 クラス
- Qt におけるマルチスレッドの使用
- libmaxminddb - MaxMind DB ファイルを扱うためのライブラリ
- Linux C で libmaxminddb を使用して GeoIP2 MMDB から IP の地理位置を取得する
- GeoIP2 を使用して IP の地理位置を取得する
- Qt でウィンドウをページの拡大に合わせて大きくする方法
- Qt プログラムのパッケージ化の詳細(Windows プラットフォーム用)