開発の動機
HTTP(80)/HTTPS(443)のポートが開いていて、ICMPが閉じられているサーバーに対して、外形監視を行いたいという要望があり、ICMPが閉じられているためpingコマンドは利用できないので、hping3やngingを利用してみたところ、以下ような結果が返ってきました。
$ sudo hping3 監視対象ホスト -p 80 -S -c 5
HPING 監視対象ホスト (eth0 123.123.123.123): S set, 40 headers + 0 data bytes
len=46 ip=123.123.123.123 ttl=51 DF id=0 sport=80 flags=SA seq=0 win=64240 rtt=19.3 ms
len=46 ip=123.123.123.123 ttl=51 DF id=0 sport=80 flags=SA seq=1 win=64240 rtt=19.6 ms
len=46 ip=123.123.123.123 ttl=51 DF id=0 sport=80 flags=SA seq=2 win=64240 rtt=19.3 ms
len=52 ip=123.123.123.123 ttl=51 DF id=0 sport=80 flags=SA seq=0 win=64240 rtt=0.0 ms
len=46 ip=123.123.123.123 ttl=51 DF id=29975 sport=80 flags=A seq=0 win=251 rtt=0.0 ms
--- 監視対象ホスト hping statistic ---
3 packets transmitted, 5 packets received, -66% packet loss
round-trip min/avg/max = 19.3/19.4/19.6 ms
「-c 5」の指定によって5回のパケット送信を行うように指示していますが、3回目までは正常なレスポンスで、4回目と5回目のレスポンスが0ミリ秒で返ってきている表示になっており、パケットロス率が「-66%」のようにマイナス値になっています。
パケットロス率は以下のような計算式になります。
(実行回数 - 成功回数) / 実行回数 * 100 = パケットロス率(%)
5回実行して3回成功した場合のパケットロス率は「40%」です。
(5 - 3) / 5 * 100 = 40%
上記のマイナス値の場合は3回しか実行していないのに5回成功しているような状況なので「-66%」となっています。
(3 - 5) / 3 * 100 = −66.66%
上記のような現象が発生する原因
- hping3を実行した監視元サーバーでは、hping3とは別のプロセスで監視対象サーバーの80番ポートからのパケットを受信している。
- hping3はパケットを受信する際に、hping3コマンド実行時の送信元ポート番号宛か否かをチェックしておらず、また、受信時のACK番号が「hping3コマンド実行時のシーケンス番号 + 1」になっているかについてもチェックしていないため、関係ないパケットを「受信成功」と判断している。
このような状況を踏まえて、hping3やnpingでは監視対象のサーバーの外形監視が正しく行えないとの判断になり、「自分で実装するか」という結論に至りました。
もちろん、MackerelやDatadogなどの機能を利用することでHTTP/HTTPSの監視は可能なのですが、「監視元をどこから行うのか」といった点にもこだわりたい部分があり、監視元サーバーについては「複数のクラウド事業者」を利用し、かつ、「複数のロケーションから監視を行いたい」という要望にも応えたかったので、今回は自前実装で対応しています。
※こういった条件を満たした監視元サーバーの候補が運用中のサーバーの中に既に存在しているので、そのサーバー上で監視ツールを動かしたいという要件がありました。
開発言語の選択
監視元サーバーはLinux環境になります。
今回開発するツールの実行にあたり、監視元サーバーに対して追加でパッケージをインストールするようなことは極力避けたいと考えていました。
社内でのプログラミング言語の利用比率を考慮するとJavaを採用するのですが、Javaのプログラムを動かすには当然その実行環境を準備する必要があり、「追加でパッケージ等をインストールすることなく」という点を踏まえて、ひとまずJavaを選択肢から外しました。
Linux環境に追加でパッケージ等をインストールすることなくこういったコマンドラインツールを実装する場合はbashかPythonになりそうですが、bashは仮に実装できるとしても、できればIDEの力を借りて実装を進めたいので少々開発がつらそうです。
そうなるとPythonになるのですが、社内での利用比率が高くなく運用を開始した後に自分しかメンテナンスできない状況になるのは運用体制的に好ましくありません。
※Pythonのsocketを利用することで実装はできそうです。
そこで次に選択肢として上がったのがGoです。社内でも利用実績があり、バイナリファイルを置くだけで実行可能なためポータビリティが高く、今回の要件にマッチしています。
また、私自身がGoの実装経験がなかったので「勉強するのに丁度よい」という面もありました。
主な仕様
- pingライクなインターフェースを提供する。
- 監視対象の「ホスト名(-h)」と「ポート番号(-p)」、「実行回数(-c)」を指定できる。
- パケットの送受信毎にRTT(Round Trip Time)を表示する。
- パケットの送受信が完了したら、RTTの最小時間(min)、最大時間(max)、平均時間(avg)、および、パケットロス率(%)を表示する。
- SYNフラグのパケットを送信して、SYN/ACKのパケットを受信したら成功とする。
- パケット受信時の「ACK番号」がパケット送信時の「シーケンス番号 + 1」になっていることを確認する。
- パケット送信時の送信元IPアドレス、送信元ポートがパケット受信時の送信先IPアドレス、送信先ポートと一致していることを確認する。
- パケット送信時の送信先IPアドレス、送信先ポートがパケット受信時の送信元IPアドレス、送信元ポートと一致していることを確認する。
開発したツール
ここまでの説明を踏まえて、今回「Synpack」というツールを開発しました。
今のところ、Ubuntu 22.04の環境のみでしか動作確認が取れていませんが、前述の仕様を満たした動きになっています。
※Mac環境だとパケットの送信に失敗するようなので現在調査中です。
Go言語による実装が初めてということもあり、ChatGPTとやり取りしながらなんとか動くところまでもってきた感じになっているため、改善の余地は大いにあるかと思いますが、ひとまずGo言語デビューは果たせたかなと。
$ sudo ./synpack -h github.com -p 80 -c 3
Synpack 送信元デバイス名 (192.168.0.1) -> github.com (20.27.177.113)
len=4096 ip=20.27.177.113 port=80 seq=1308764033 rtt=28.95ms
len=4096 ip=20.27.177.113 port=80 seq=2426553645 rtt=29.15ms
len=4096 ip=20.27.177.113 port=80 seq=3435491915 rtt=30.74ms
--- github.com Synpack statistic ---
3 packets transmitted, 3 packets received, 0.00% packet loss
round-trip min/avg/max = 28.95ms/29.62ms/30.74ms