Pingプログラムを作ろう ■0x01.) はじめに  何故pingが必要なのか? それは、インターネットが宛先にパケットを届ける ことを保証しないからです。さらに、インターネットはパケットを送信した順番 に相手に届けることさえも保証しません(ただしこれはTCPを利用することで解決 していますが)。インターネットではデータはパケット単位で分割されて送信さ れます。つまり一見いっぺんにデータを送信したように見えても、実は内部的に はいくつかのパケットに分割されて送信されているわけです。  さてさて、例えばパケットを宛先に送信したとしましょう。しかし、ネットワ ーク機器のトラブルや混雑などで宛先にパケットが正しく届かなかった、もしく は途中でパケットデータが壊れてしまったとします。そのとき、そのパケットは 破棄されます。送信元にはなんの通知もありません。なんの通知もなく、ただパ ケットだけを破棄、消滅させる、それがインターネットです。  私が、恋人に手紙を送ったとします(恋人いませんけどね T_T;)。その手紙は 郵便局へ渡り、さらに別の郵便局へ渡り、どんなに道が混んでいようと、どんな に道が破壊されていようと、どんなにトラブルにみまわれようと、郵便局の人は 必ず手紙を届けてくれるでしょう。しかし、インターネットはそうではないので す。インターネットの場合、道が混んでたら「メンドクサ」という言葉を吐き捨 て、その場で手紙を破り捨ててしまいます。ものすごい自分勝手なやつです(メ ンドクサとは言いませんけどね^^;)。  しかし、これじゃまずいので、ある人がpingというプログラムを作りました。 このプログラムから送られたパケットに限っては、もし、なにかのエラーに見ま われたときにもパケットを破棄して知らん顔ではなく、ちゃんとその原因を送信 元に教えてくれます。  もし、道が混んでいたら、「ごめん、道が渋滞しててさぁ、お前の手紙送れな かったよ」といって戻ってきます。道が壊れてたら、「わりっ、国道4号線が工事 中だったのでお前の手紙遅れなかったのよ」といって戻ってきます。いやー、と ても便利になりましたね。  ということで、今回はこのpingプログラムを作ってみることにします。 ■0x02.) ping(Packet INternet Groper)とは  インターネットやイントラネットなどのTCP/IPネットワークを診断するプログ ラム。接続されているかどうか調べたいコンピュータのIPアドレスを指定すると、 ICMPを使って通常32バイト程度のデータを送信し、相手のコンピュータから返信 があるかどうか、返信がある場合はどのくらい時間がかかっているか、などのデ ータを元にネットワークを診断する。  IT用語辞書e-wordsより(http://e-words.jp/w/ping.html)  pingコマンドは、Linuxはもちろん、Windowsにもデフォルトでインストールさ れています。コマンドプロンプトに以下のように入力するとこで、引数に渡した ホストまでのネットワーク状況が正常であるかどうかを判断できます。 ----- コマンドプロンプト C:\>ping -n 1 yahoo.co.jp Pinging yahoo.co.jp [211.14.15.5] with 32 bytes of data: Reply from 211.14.15.5: bytes=32 time=101ms TTL=244 Ping statistics for 211.14.15.5: Packets: Sent = 1, Received = 1, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 101ms, Maximum = 101ms, Average = 101ms C:\> ----- ■0x03.) ICMP(Internet Control Message Protocol)とは  IPのエラーメッセージや制御メッセージを転送するプロトコル。 TCP/IPで接続 されたコンピュータやネットワーク機器間で、互いの状態を確認するために用い られる。ネットワーク診断プログラムpingが使う。  IT用語辞書e-wordsより(http://e-words.jp/w/ICMP.html) ■0x04.) ICMP Header Format ----- Echo or Echo Reply Message 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Type | Code | Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identifier | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data ... +-+-+-+-+- (RFC792より) -----  最初の4バイトであるType以外はTypeの値によって可変します。詳細はRFC 792 をみてください。 ----- Type 8(Echo Request) echo要求 Type 0(Echo Reply) echo応答 -----  接続されているかどうか調べたいコンピュータへTypeを8にしたICMPパケットを 送信すると、そのコンピュータからTypeを0にしたパケットが返答されます。これ がpingの仕組みです。  Typeにはパケットのタイプが指定されます。pingには基本的にEcho Requestと Echo Replyを利用しますが、パケットタイプは他にも多々あります。  Codeにはエラーコードが入れられます。エラーがない場合は0が入れられていま すが、エラーの場合は0以外のエラーに対応した値が入れられます。送信側は基本 的に0を指定しておけば問題ないでしょう。  Checksumはデータが壊れているかどうかを判断するフィールドです。どうして たった2バイトで判断できるのかは割愛させていただきます。  Identifierとはいわゆる識別子ですね。識別子とはつまり、識別するための値 です。はい、そのままですね(ぉぃ。つまり、送信元がまず識別子を適当な値に 設定して宛先にデータを送信します。宛先はこの識別子を変更せずに送信元へ応 答データを返信します。すると送信元はどの送信パケットに対しての応答パケッ トであるかが分かるわけです。どのパケットに対しての答えであるのか、を明確 にするために、識別子は用いられます。  Sequence Numberはよくわかりません(^^;。RFC792には以下のように書かれてあ ります。 ----- rfc792引用 Sequence Number If code = 0, a sequence number to aid in matching echos and replies, may be zero. Description The data received in the echo message must be returned in the echo reply message. The identifier and sequence number may be used by the echo sender to aid in matching the replies with the echo requests. For example, the identifier might be used like a port in TCP or UDP to identify a session, and the sequence number might be incremented on each echo request sent. The echoer returns these same values in the echo reply. Code 0 may be received from a gateway or a host. -----  というようにあります。要するに「エコーの送信側が応答とエコー要求との対 応付けの手がかりとして使用可能である。」とありますので、適当な値で構わな いということでしょう。 ■0x05.) ソースコード  Windows Ping のソースコードは以下です。「Visual C++.NET」でコンパイルを 確認しています。BCCではコンパイルできません。いや、おそらくmax関数とmin関 数がありません、と怒られるので、自作しなければなりません。もし自作するのが 嫌な方は「TCP/IPの教科書」を買ってください(^^;。この本には、BCCでもコンパ イルできるカタチでCD-ROMに同封されています。 http://ruffnex.oc.to/kenji/socket/win_ping.cpp  では早速解説を始めます。まずは関数の説明をします。 ----- USHORT ip_checksum(USHORT *data, int size) { unsigned long sum = 0; while (size > 1) { sum += *(data++); size -= 2; } if (size > 0) { sum += (*data) & 0xff00; } sum = (sum & 0xffff) + (sum >> 16); return ((~(USHORT)((sum >> 16) + (sum & 0xffff)))); } -----  チェックサムを計算する関数です。これはそのままコピペすれば、別のプログ ラムでも利用できます。といっても「チェックサム」で検索すれば、結構同じよ うなプログラムがでてきます。この関数についての詳細は 「http://ruffnex.oc.to/kenji/xrea/checksum.txt」などをご覧ください。 ----- int host2ip(char *hostname, sockaddr_in &dest) { UINT addr; if((addr = inet_addr(hostname)) == INADDR_NONE){ struct hostent *hp; if ((hp = gethostbyname(hostname)) == NULL) { return -1; } memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length); dest.sin_family = hp->h_addrtype; }else{ dest.sin_addr.s_addr = addr; dest.sin_family = AF_INET; } return 0; } -----  ホスト名、もしくはIPアドレスからネットワークバイトオーダーに変換する関 数です。まずはIPアドレスだと仮定して、inet_addrで取得しています。もしエラ ーがでるようならば、おそらくホスト名だろうということで、gethostbynameで再 チャレンジします。これでもダメならエラーということで-1を戻り値として返し ます。 ----- void init_ping_packet(ICMPHeader *icmp_hdr, int packet_size, int seq_no) { icmp_hdr->type = ICMP_ECHO_REQUEST; icmp_hdr->code = 0; icmp_hdr->checksum = 0; icmp_hdr->id = (USHORT)GetCurrentProcessId(); icmp_hdr->seq = seq_no; icmp_hdr->timestamp = (ULONG)GetTickCount(); const unsigned long int deadmeat = 0xDEADBEEF; char *datapart = (char *)icmp_hdr + sizeof(ICMPHeader); int bytes_left = packet_size - sizeof(ICMPHeader); while (bytes_left > 0) { memcpy(datapart, &deadmeat, min(int(sizeof(deadmeat)), bytes_left)); bytes_left -= sizeof(deadmeat); datapart += sizeof(deadmeat); } icmp_hdr->checksum = ip_checksum((USHORT *)icmp_hdr, packet_size); } -----  pingパケットを作成しています。ソースコードの最初の方に ----- struct ICMPHeader { BYTE type; BYTE code; USHORT checksum; USHORT id; USHORT seq; ULONG timestamp; }; -----  というようにICMPHeaderが定義されてますので、それぞれICMPヘッダフォーマ ットと照らし合わせて調べてみてください。BYTEは1バイトの変数を定義します。 USHORTは2バイトですね。そしてULONGは4バイトです。timestampはデータ部にい れられていますが、これはパケットが返信してくる時間測定のために利用するデ ータです。  init_ping_packet関数はパケットの長さを引数にとります。このプログラムで は、パケットの長さ、つまりどれだけ大きなデータを相手に送信するのかをユー ザーが決めることができるので、その長さになるまでICMPパケットに適当なデー タでパディングしなければなりません。そのパディングするデータが 「deadmeat = 0xDEADBEEF」ですね。  whileでまわしてmemcpyでパディングしています。そして、最後にチェックサム を計算して、ICMPパケット完成となります。 ----- int send_ping(SOCKET sd, const sockaddr_in& dest, ICMPHeader *send_buf, int packet_size) { cout << "Sending " << packet_size << " bytes to " << inet_ntoa(dest.sin_addr) << "..." << flush; int bwrote = sendto(sd, (char *)send_buf, packet_size, 0, (sockaddr *)&dest, sizeof(dest)); if (bwrote == SOCKET_ERROR) { cerr << "send failed: " << WSAGetLastError() << endl; return -1; }else if (bwrote < packet_size) { cout << "sent " << bwrote << " bytes..." << flush; } return 0; } -----  関数名をみれば分かりますが、これはパケットを送信する関数です。送信すべ きパケットのデータをcoutで出力して、sendtoで送信しています。ここは特に説 明の必要は無いと思います。 ----- int recv_ping(SOCKET sd, sockaddr_in &source, IPHeader *recv_buf, int packet_size) { int fromlen = sizeof(source); int bread = recvfrom(sd, (char *)recv_buf, packet_size + sizeof(IPHeader), 0, (sockaddr *)&source, &fromlen); if (bread == SOCKET_ERROR) { cerr << "read failed: "; if (WSAGetLastError() == WSAEMSGSIZE) { cerr << "buffer too small" << endl; }else { cerr << "error #" << WSAGetLastError() << endl; } return -1; } return 0; } -----  返答の受信パケットです。recvfromを呼んでいるだけですね。エラーの場合は WSAGetLastErrorに適切なエラーコードが入れられるので、それを表示しています。 ----- void die(ICMPHeader *&send_buf, IPHeader *&recv_buf) { if (send_buf != NULL) { delete[]send_buf; } if (recv_buf != NULL) { delete[]recv_buf; } WSACleanup(); exit(1); } -----  dieはそのままプログラムを終了する関数です。main関数でメモリを確保してい るので、終了プログラムにメモリの開放をしています。この関数はエラーにより 終了した場合などに呼び出されます。 ----- int decode_reply(IPHeader *reply, int bytes, sockaddr_in *from) { USHORT header_len = reply->h_len * 4; ICMPHeader *icmphdr = (ICMPHeader *)((char *)reply + header_len); if (bytes < header_len + ICMP_MIN) { cerr << "too few bytes from " << inet_ntoa(from->sin_addr) << endl; return -1; } -----  decode_replyは、受け取ったパケット(つまりサーバからの返答パケット)を 加工する関数です。header_lenには、reply->h_len * 4が代入されますが、これ は、IPヘッダの長さをあらわしています。そのIPヘッダの長さをパケットのポイ ンタに加算するとICMPヘッダの位置になります。それをicmphdrに代入しています。 ----- if (icmphdr->type != ICMP_ECHO_REPLY && icmphdr->type != ICMP_TTL_EXPIRE) { cerr << "Unknown ICMP packet type " << (int)(icmphdr->type) << " received" << endl; return -1; } if (icmphdr->id != (USHORT)GetCurrentProcessId()) { return -2; } -----  次はTypeを検査しています。ICMP_ECHO_REPLY、つまりTypeが0ではない、かつ ICMP_TTL_EXPIREでもないならば、エラーとみなし「ICMPパケットのタイプでは無 い」というような出力をします。  GetCurrentProcessIdはこのプログラムのプロセスIDです。init_ping_packetで ICMPの識別子にはプロセスIDをいれるようになっていますので、もし識別子が違 うようならば、これもエラーとみなします。 ----- cout << endl << bytes << " bytes from "; cout << inet_ntoa(from->sin_addr) << ", icmp_seq " << icmphdr->seq << ", "; if (icmphdr->type == ICMP_TTL_EXPIRE) { cout << "TTL expired." << endl; } else { cout << nHops << " hop" << (nHops == 1 ? "" : "s"); cout << ", time: " << (GetTickCount() - icmphdr->timestamp) << " ms." << endl; } return 0; } -----  ここは、取得したパケットデータ解析後に、出力する部分です。返答にどれだ け時間がかかったのかなども表示しています。  では、いよいよmain関数に入ります。 ----- int main(int argc, char *argv[]) { ICMPHeader *send_buf = NULL; IPHeader *recv_buf = NULL; if (argc < 2) { cerr << "usage: " << argv[0] << " [data_size] [ttl]" << endl; cerr << "data_size can be up to " << MAX_PING_DATA_SIZE << " bytes. "; cerr << "Default is " << DEFAULT_PACKET_SIZE << "." << endl; cerr << "ttl should be 255 or lower. "; cerr << "Default is " << DEFAULT_TTL << "." << endl; return 1; } -----  メチャクチャお決まりのソースですけど、プログラムが引数にホストとパケッ トのサイズと、ttlの値をとります。ホストのみ必須ですので、2以下ならばエラ ーとし、終了します。 ----- int packet_size = DEFAULT_PACKET_SIZE; if (argc > 2) { packet_size = atoi(argv[2]); } int ttl = DEFAULT_TTL; if (argc > 3) { ttl = atoi(argv[3]); } -----  もしパケットサイズが指定されなかったら、DEFAULT_PACKET_SIZEに設定します。 同じく、ttlも設定されなかったら、DEFAULT_TTLに設定します。 ----- WSAData wsaData; if (WSAStartup(MAKEWORD(2, 1), &wsaData) != 0) { cerr << "Failed to find Winsock 2.1 or better." << endl; return 1; } -----  Windowsネットワークプログラムには必須の項目です。おまじないとかそういう 感じでいいでしょう。ちなみにバージョン2.1を利用します。 ----- SOCKET sd; if ((sd = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, 0, 0, 0)) == INVALID_SOCKET) { cerr << "Failed to create raw socket: " << WSAGetLastError() << endl; die(send_buf, recv_buf); } -----  rawソケットを使います。Windowsって少し前までrawソケットが使えなかったよ うですね。それ、かなり終わってるやん。正直Hackingもなにもない。そう考える と、いまのWindowsってだいぶ進化してるなぁ。結構、安定してるし。と、まぁ関 係ない話でした(^^;。 ----- if (setsockopt(sd, IPPROTO_IP, IP_TTL, (const char *)&ttl, sizeof(ttl)) == SOCKET_ERROR) { cerr << "TTL setsockopt failed: " << WSAGetLastError() << endl; die(send_buf, recv_buf); } -----  ここは、ttlのためだけに書かれています。ttlは残念ながらICMPヘッダではな く、IPヘッダにあるので、IPヘッダのTTLだけ設定するためにsetsockoptを使って いるわけです。ちなみにttlとは、パケットの寿命です。ルータを通るたびにこの 値が1ずつ減算されていって、0になるとパケットが破棄されます。TTLは1バイト なので、最大で255までしか設定できませんが、そんな数のルータを通ることなん て、意図的に構築しない限りありえませんので、まず問題ありません。 ----- sockaddr_in dest, source; memset(&dest, 0, sizeof(dest)); if (host2ip(argv[1], dest) < 0) { cerr << "Failed to resolve " << argv[1] << endl; } packet_size = max(sizeof(ICMPHeader), min(MAX_PING_DATA_SIZE, (UINT)packet_size)); -----  まずは宛先の設定ですね。住所を書いてるわけです。そして、パケットのサイ ズを決めます。 ----- if ((send_buf = (ICMPHeader *)new char[packet_size]) == 0) { cerr << "Failed to allocate output buffer." << endl; die(send_buf, recv_buf); } if ((recv_buf = (IPHeader *)new char[MAX_PING_PACKET_SIZE]) == 0) { cerr << "Failed to allocate output buffer." << endl; die(send_buf, recv_buf); } -----  ここはメモリ管理です。まぁみればわかるので、説明不要ですね。Cならmalloc やcallocなどを使いますが、C++なのでnewでやっています。 ----- int seq_no = 0; init_ping_packet(send_buf, packet_size, seq_no); if (send_ping(sd, dest, send_buf, packet_size) < 0) { cerr << "send_ping function error." << endl; die(send_buf, recv_buf); } -----  送信処理です。それぞれの関数にデータを渡して、送信処理をしています。 ----- while (1) { if (recv_ping(sd, source, recv_buf, MAX_PING_PACKET_SIZE) < 0) { unsigned short header_len = recv_buf->h_len * 4; ICMPHeader *icmphdr = (ICMPHeader *)((char *)recv_buf + header_len); if (icmphdr->seq == seq_no) { break; } cerr << "bad sequence number!" << endl; continue; } if (decode_reply(recv_buf, packet_size, &source) != -2) { break; } } -----  受信処理ですね。まず応答パケットを受信して、そのデータをdecode_replyに 渡して、コンソールへ出力しています。 ----- delete[]send_buf; delete[]recv_buf; WSACleanup(); return 0; } -----  最後にメモリを開放して終了となります。  では、コンパイルして実行してみてください。 ----- コマンドプロンプト C:\>myping yahoo.co.jp Sending 32 bytes to 203.141.35.113... 32 bytes from 203.141.35.113, icmp_seq 0, 12 hops, time: 30 ms. C:\> C:\>myping yahoo.com 64 Sending 64 bytes to 66.218.71.198... 64 bytes from 66.218.71.198, icmp_seq 0, 17 hops, time: 151 ms. C:\> -----  こんな感じですね。無事、送受信ができています。 ■0x06.) さいごに  さて、いかがだったでしょうか。Windowsも派手なGUIなどだけではなく、地味 にrawソケットに対応させたり、OS自体の安定性をあげたりと頑張ってるみたいで す。「でも、安定性とか、rawソケットとか、それって普通OSとして当たり前の機 能じゃないのか?」という素朴な疑問は置いておいて、Windowsも悪く無いなぁと 思う今日この頃です。ロングホーンも当分先の話っぽいので、気長にXPでも使っ ていようかなと思います。実は、今回使ったプログラムを書くときに「Winsock Programmer's FAQ」というサイトに非常にお世話になりました。というかこのサ イトにあるpingのサンプルソースコードをC++風に書き直しただけだったりするの が、今回の私が書いたサンプルソースなのですが(^^;。Winsockに関して詳しく知 りたい方にはお勧めのサイトです。さて、最後になりましたがここまで付き合っ て読んでくださった方本当に有り難うございました。  では、また会う日まで... ■0x07.) 参考ソース  Winsock Programmer's FAQ  http://www.kt.rim.or.jp/~ksk/wskfaq-ja/index.html