「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。2017-02-13高速にリモートホストのポートがListenしているかを調べるプログラミングhb.matsumoto-r.jp以下のエントリは一部誤認が含まれていたので、上記エントリにその旨をまとめましたので御覧ください。とある事情でミドルウェア上から高速にリモートホストのポートのListenチェックをしたくなりました。ローカルホストのポートであれば、/procやnetlinkなどを使って素早くチェックする方法がありますが、今回は対象がリモートホストなのでソケットでなんとかする必要があります。そこで、誰もがまず思いつくのは、connect()システムコールによってリモートホストのポートに接続しにいって、connectできればOK、できなければNGと判定する方法があり得るでしょう。(高負荷時に接続できないパターンはListenしていないと判定してよい)そこで一旦、最低限socket()システムコールとconnect()システムコールで接続する時のパケットをtcpdumpで眺めてみます。0ふむむむ、connect成功時は9パケットのパケット送受信が発生していますね。これを見ると、ポートのチェックだけをしたいのに9パケットって、6パケット分ぐらいは無駄に感じますよね。TCPの3wayhandshake的には、synパケット送ってsyn+ackがかえってこればそれで基本はListenしているとみなせるのだから、synパケット送信→syn+ackパケット受信→rstパケット送信、の3パケットで十分なわけです。それだったら、ユーザランドに簡易TCPスタックを自作して、synパケットを作ってRAWソケットを介して戻りのパケットを受信してやれば良いわけですね。2017年2月13日による指摘の通り、実際は普通にconnectしてcloseすると以下のようなtcpdumpでした。全部で普通は6パケットですね。0ユーザランドに簡易TCPスタックを作るIPヘッダやTCPヘッダを構造体で定義するために、最初は以下のような構造体を自前で作ってパケットを作っていました。<netinet/tcp.h>あたりにIPヘッダやTCPヘッダの構造体が既に定義済みで、それを使った方がだいぶ楽だということが分かったので、それらのヘッダファイルを利用します。以下のように、RAWソケットを作って、IPヘッダやTCPヘッダを自前で定義してパケットを送りつけます。それによって、ユーザランドで送受信するデータにもTCP/IPヘッダが含まれるようになります。概ね以下のように書くことになります。}IPヘッダとTCPヘッダの構造体およびヘッダサイズに気をつけながらpacketバッファ上のポインタの位置を移動させ、IPおよびTCPヘッダを定義していきます。今回はリモートホストが80ポートをListenしているかをチェックするために、それらの情報とsynフラグを立てたTCPヘッダを作りました。IPヘッダおよびTCPヘッダも以下のように計算します。チェックサムについては今回は流れを説明するため詳細を省略します。ググると沢山でてきますので。(answer);}そして、パケットを作ることができたら、それをsendto()システムコールによって送信します。これで、リモートホストに対してsynフラグのたったパケットをユーザランドで作って送ることができます。実際に上記のようなパケットの作り方でtcpdumpを見ると以下のようになります。0ちゃんとsynパケットを送れていますね。後はsynとackのフラグが立ったパケットを同様にrecvfrom()すれば良いだけ....となりますが、ここで大きな問題が発生します。カーネルのTCPスタックに邪魔されるRAWソケットを介して、ユーザランドで自前で作ったパケットを送信し、その後リモートホストから返りのパケットを受信する場合、ユーザランドのTCPスタックだけでなくカーネルのTCPスタックも通過することになります。すると、カーネルのTCPスタックはこんな返りのパケット知らない!といって、不要なパケットとみなしrstフラグのたったパケットを返していしまいます。実際にRAWソケットでsynパケットを送った後のtcpdumpの出力しては以下のようになります。0これは、ユーザランド側でTCP/IPヘッダを含むパケットをrecvfrom()しなくても発生します。いうのも、カーネルのTCPスタックが先にrstパケットを送ってしまっているためです。なると、リモートホストは通信が終了したことになり、その後にユーザランドのTCPスタックからrecvfromしたとしても接続は切れていると判定(返り値0)され、パケットを受信できなくなります。結果、ユーザランドTCPスタック上でsyn+ackパケットが返ってきたことが判定できません。そこで、ユーザランドでもそれを知る方法として以下の2つの戦略がありえます。カーネルのTCPスタックがrstを送るよりも速くユーザランドでパケットを受信するユーザランドのTCPスタックでパケットを処理しながらBPF経由でパケットをモニタリングするでは、それぞれの方針で実装を進めてみましょう。カーネルのTCPスタックがrstを送るよりも速くユーザランドでパケットを受信するパケットがカーネルのTCPスタックを登る前に、RAWソケットとしてはユーザランド側にもコピーされるはずなので、カーネルのTCPスタックの処理よりも先にユーザランド側でパケットを受信する必要があります。そこで、ユーザランドのTCPスタックで作ったsynパケットを送信する前に、あらかじめ受信専用のスレッドを作って受信処理をループさせて待機しておき、synパケット送信後、返りのパケットをできるだけ速く受信できるように試みてみます。その場合、以下のような実装になります。NULL);流れとしては、上記のようにsendto前から受信スレッドを走らせておき、スレッドの処理は以下のように実装しておきます。close(socket_raw);}その上で、運良くカーネルがrstを送りきるより速くsyn+ackパケットを受信できた場合に、それのパケットを表示するようにします。0;}これで、理屈上はうまくいきそうです。TCPスタック同士の処理速度では勝負にならないはずなので、TCPスタックの処理が無視できるほどのレイテンシーの低いリモートホストであれば、rstがカーネルからリモートホストに届く(1往復半)よりも速く、ユーザランドでsyn+ackを受信(1往復)できる可能性は高くなりそうです。ユーザランドのTCPスタックでパケットを処理しながらBPF経由でパケットをモニタリングするもう一つの戦略はBPF経由でパケットをモニタリングする方法です。一般的にはlibpcapなどを使えばよいので、比較的簡単に実装できるでしょう。ユーザランドでやっていると言っていいのか不明ですが、とりあえず実装してみます。これも、一つ目の戦略と同様にパケットを見失わないように予めスレッドを動かして、パケットを監視するようにします。同様に以下のようにスレッドを作ります。NULL);recvfromのループ戦略と同様ですね。スレッド上でBPFによるパケットモニタリングを実装します。}}pcapでパケットをフィルタしつつ、pcap_loopでモニタリングし続けます。その上で、パケットを受信したらそのパケットのフラグを解析し、syn+ackなパケットであれば、pcap_loopを抜けるためにpcap_breakloopを実行します。このあたりはlibeventにも似たインターフェイスになっていますね。このコードのfound_syn_ack_packetはrecvfromによる受信スレッドのものと同一です。}}実際に試してみましょう実際にこれらのコードを全てまとめたものが以下になります。0;}これを以下のようにビルドします。tcp_port_check実行は、RAWソケットを扱うためCAP_NET_ADMINの特権が必要となるので、sudoで実行します。80これにより、カーネルよりも速くユーザランドのTCPスタックでやりたいことができたらパケットのダンプが表示されるはず。0やったーーーーー!connectで9パケット送る必要無いし、9個も送ると遅くなるから、ユーザランドに簡易TCPスタックを作って、スレッドを駆使してカーネルがrst送りきるよりも速くsyn+ackパケット受信したり、BPFでパケットモニタリングすることによってパケット6個も減らして、全部で3パケットでポートのListenチェックできる処理が実現できたぞーーーーーー!!あれ?まとめということで、結論としては高速にリモートホストのポートがListenしているかをチェックするには特権もいらないconnectで十分であることが分かりました(connectできなかった場合はたったの2パケット)。パケットを3つに抑えた所で、今回のような実装だと実際のチェックコストは下手したら1000倍ぐらいかかっており、パケット6個を減らすためにユーザランドTCPスタックの実装やパケットの自前実装、BPFによるフィルタリング等、色々頑張りましたが、ミドルウェアのようなソフトウェアから高速にリモートホストのポートのListen状態をチェックするという観点ではノーバリューでフィニッシュでした。2017年2月13日による指摘の通り、実際は普通にconnectしてcloseすると以下のようなtcpdumpでした。全部で普通は6パケットでした。2017年2月13日この方法により、カーネルのTCPスタックを使っても4パケットでフィニッシュ可能そうです。0;}これだと、以下のようなtcpdump結果となりました。4パケットでフィニッシュしているので、カーネルのTCPのスタックを使う場合の最小パケット数と思われます。はてなブログPro研究者と技術者をやっています。詳しくはリンクの「研究・開発業績ページ」を御覧ください。さくらインターネットに入社して1年が経ちました非厳密計算で確率的に解釈するコンピューティングへの流れラストオーサーとしての国際会議投稿やジャーナル執筆のサポートについてviを:wqや:q!、あるいはZZで終了するのとではどちらが効率的か自分の研究開発に自信を持つためには月別アーカイブリンク研究・開発業績ページ旧ブログ検索はてなブログをはじめよう!matsumoto_rさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。