この文書はCQ出版の 月刊誌オープンデザイン 2001年03月号に 掲載された同名記事の原稿を、編集部の許可をいただいて公開しているものです。 掲載当時とくらべRubyは進化を続けていますが当文書は追従しておりません。 そのため内容が古くなっている箇所があります。ご注意下さい。 代わりといってはなんですが、 この記事を大幅に書き換えた書籍がこの春にCQ出版より発売予定です。

Copyright 2000-2002 IPR Writers. All rights reserved.

Rubyではじめるインターネットプログラミング

第8回
Internet Control Message Protocol
後藤謙太郎/後藤裕蔵/高橋征義/渡辺哲也 (50音順)

はじめに

前号でもこのページで触れた『Perl/Rubyカンファレンス2000』の中で,「ワンライナーのすすめ + Perl/Rubyの鉄人」というコーナーがあった.この後半では,PerlとRubyの「鉄人」によるワンライナー作成の実演が行われた.これは,主催者側が用意した食材,ではなく課題(「.htmとなっているファイルを.htmlに直せ」や「1000までの素数を求めよ」など)を, PerlとRubyの「鉄人」がその場で「料理」あする,というものだ. PerlやRubyをまったく使用しないワンライナーが回答として提出されたり,解説者として呼ばれたまつもとゆきひろさんが手づから課題を「料理」する場面もあるなど,なかなか楽しめる企画になっていた.

ワンライナーとはたった一行の,コマンドラインだけで書けるスクリプトのことだ.Perlはこれが得意な言語で,そのために用意されたと言わんばかりのオプションや組み込み変数もある.Rubyも,この辺りの機能を Perlから譲り受けており,とりわけワンライナーは良くも悪くも Perlそっくりというか,Python好きは眉をひそめるようなコードが書けてしまう.

もっとも,さすがにネットワークプログラミングをワンライナーで済ますのは難しい.それでも,今回出題されたものの中には「Web上のコンテンツから必要な情報を抽出する」というのもあり,これに対してRubyの鉄人の一人,やまだあきらさんが, Net::HTTPを使ったワンライナーを披露されていた.

ワンライナーの魅力は,「目的のためには手段を選ばず」と言いたくなるような,徹底的な実用性の追及にある.時には非常に場当たり的な解決でも,短時間に書けるのであればそれでいい,という割り切りが小気味よい.そして,その場にあるものを使って,今,そこにある課題をこなしていく.こなしたあとは,スクリプトは捨ててしまっても構わない.必要なときにはまた書けばいいのだから.

「あるものを巧みに使いこなし,問題を解決する」という態度は,何もワンライナーに限ったことではない.それは今回扱うICMPを利用したアプリケーション,pingやtracerouteも同様だ.制御情報を扱うICMPを,ネットワークの遅延の測定や経路の解析に使う,という発想はなかなか出てくるものではないだろう.かといって,そのために新しいプロトコルを開発する,というのではコストもかかる.それは,ちょっとしたテクニックを使えばワンライナーで解ける問題のために,わざわざクラス定義から行うようなものだ.

Rubyでは,Perlなどとの差別化を行うためもあって,ちゃんとした「プログラム」の重要性が強調されることもないではない.実際,コマンドラインオプションについては,Ruby本[MI99]でも『Rubyプログラミング入門』[Har00]でもあまり詳しく書かれていない.たしかにワンライナーに凝り出すと,無意味に読みにくいスクリプトを書いてしまったり,「ワンライナーで書かねば」と手段が目的となりがちな側面もある.とはいえ,日々のツールとしてRubyを使いこなすには,これらの技術も身につけておくと,便利なことも多い.手作業で定型的な仕事をしている際には,「これはRubyのワンライナーでできないか?」とちょっと考えてみることもいいだろう.ワンライナーもまた,Rubyの魅力の一つなのだから.

今回の流れ

前号まで取り上げてきたのは,基本的にTCPを使ったアプリケーションだった.インターネットプログラミングといえば, TCPまたはUDPを使うものがほとんどだが,それ以外のプロトコルを利用するものもある.そこで,今回はこれまでとは趣向を替えてICMPを紹介する.ICMPは直接それを操作するプログラムを書く機会は少ないものの,インターネットの機構を支える重要なプロトコルである.

まずはICMPそのものについて解説した後,ICMPを使うための重要な仕組みとしてraw socketとこれをRubyから使用するためのモジュールICMPModuleを紹介し,ICMPを使った代表的なアプリケーションであるpingtraceroute をRubyを使って実装してみる.さらに,任意のICMPのメッセージを観察するためのツール,watchicmpを紹介する.

ネットワークプログラミングをそれなりにこなしている方の中でも, ICMPに直接触れる機会はほとんどないという方もいるだろう.そのような方は,ぜひ本稿を参考にして,ICMPの基本的な使い方を習得していただきたい.

Inernet Control Message Protocol

ICMPの役割

Internet Control Message Protocol(ICMP)はインターネットプロトコルレベルの制御情報やエラーに関する情報などを通知するためのプロトコルで,その規格はRFC792で与えられる.ICMPは以下のような場面で使用される.

インターネットプロトコルは必ずしも接続性を保証しないアーキテクチャではあるが,これらの情報によってデータの流量や通信経路を制御しながら各ノード同士が相互にフィードバックを行なっている.インターネットプロトコルは「最善努力型」(best effort)と言うだけのことはあって,このような仕組みが提供されている.当然のことながらICMPパケットの到達性も保証の限りではないため,ネットワークの切断などにより,お互いのエラーを通知し合うICMPのパケットでネットワークが溢れてしまうことのないように,ICMPのエラーに対して,ICMPメッセージは発行しないように決められている.

一般に,ICMPのメッセージはTCP/IPの機能の一部としてプロトコルスタックによって処理されるため,TCP等の上位のプロトコルを使用するプログラムはICMPに触れる必要はないし.実際,ICMPを操作することができるのはUNIXではroot権限を持つものに限られている. ICMPを積極的に使用するアプリケーションとしてping が挙げられるが,ls -lで見れば,オーナーである rootにset user idされているのがわかる.

$ ls -l `which ping`
-r-sr-xr-x  1 root  wheel  229052 Aug 27 02:32 /sbin/ping

実際のところアプリケーションプログラマはICMPの詳細について知る必要は無いのかもしれないが,障害解析などの場面では知っておくといいことがあるのも事実なので,一通り学んでおいたほうがいいだろう.

メッセージフォーマット

RFC792には各ICMPメッセージのレイアウトが示されている.図1はDestination Unreach MessageとEchoおよびEcho Reply Messageを抜粋したものである.

--------------------------------------------------------------------
図1 ICMPメッセージフォーマットの例

Destination Unreachable 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             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                             unused                            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |      Internet Header + 64 bits of Original Data Datagram      |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

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 ...
   +-+-+-+-+-
--------------------------------------------------------------------

ここで,先頭の4バイトは共通のレイアウト(Type,Code, Checksum)を持つことに注目して欲しい.TypeフィールドにはICMPメッセージの種別を表す定数値が格納され,さらにその詳細を示す値がCodeフィールドに格納される.図1のDestination Unrechable Messageの場合,Typeフィールドには3が格納される.Codeフィールドには宛先のネットワークに到達できない場合には0,ホストに到達できない場合には1,また,指定したポートが開かれていない場合には3,というように原因に応じた値が格納される. Checksumフィールドは以降に続くデータ部分を含めたパケット全体のチェックサムである.具体的な計算方法は後ほど紹介するプログラムの解説の中で扱うことにする.

主なICMPメッセージの一覧を表1にまとめておくが,やはり詳細はRFCをあたって欲しい.また,C言語ではICMPを扱うための構造体が/usr/include/netinet/ip_icmp.hで提供されている.ここにはTypeやCodeの値が一通り定義されているので.リファレンスとしてはこれを参照するのもいいだろう.

表1 ICMPタイプ
┌──────────┬─┬────┬───────────────────┐
│ICMPタイプ          │値│RFC#    │概要                                  │
┝━━━━━━━━━━┿━┿━━━━┿━━━━━━━━━━━━━━━━━━━┥
│Echo Reply          │ 0│RFC792  │Echoに対する応答.                    │
├──────────┼─┼────┼───────────────────┤
│Destination         │ 3│RFC792  │パケットの宛先であるネットワーク,    │
│Unreachable         │  │        │ホスト,ポートなどに到達できない場合  │
│                    │  │        │にゲートウェイやホストから発行される.│
├──────────┼─┼────┼───────────────────┤
│Source Quench       │ 4│RFC792  │パケットを処理しきれない場合に,ゲー  │
│                    │  │RFC1812 │トウェイやホストから発行される.      │
├──────────┼─┼────┼───────────────────┤
│Redirect            │ 5│RFC792  │より近いゲートウェイを指定するために,│
│                    │  │        │ゲートウェイから発行される.          │
├──────────┼─┼────┼───────────────────┤
│Echo                │ 8│RFC792  │エコー要求.                          │
├──────────┼─┼────┼───────────────────┤
│Router Advertisement│ 9│RFC1256 │自分がゲートウェイであることを示すた  │
│                    │  │        │めに,ゲートウェイから発行される.    │
├──────────┼─┼────┼───────────────────┤
│Router Solicitation │10│RFC1256 │Router Advertisementを要求するために,│
│                    │  │        │ホストから発行される.                │
├──────────┼─┼────┼───────────────────┤
│Time Exceeded       │11│RFC792  │IPヘッダに設定されたTTLが0になってパ  │
│                    │  │        │ケットが相手先に到達できない場合に,  │
│                    │  │        │ゲートウェイから発行される.          │
├──────────┼─┼────┼───────────────────┤
│Parameter Problem   │12│RFC792  │IPヘッダの値に誤りがある場合などに,  │
│                    │  │RFC1108 │ゲートウェイやホストから発行される.  │
├──────────┼─┼────┼───────────────────┤
│Information Requst  │13│RFC792  │情報要求.                            │
├──────────┼─┼────┼───────────────────┤
│Information Reply   │14│RFC792  │Information Requestに対する応答.     │
├──────────┼─┼────┼───────────────────┤
│Timestamp Requst    │15│RFC792  │Timestamp要求.                       │
├──────────┼─┼────┼───────────────────┤
│Timestamp Reply     │16│RFC792  │Timestampに対する応答で,パケットの   │
│                    │  │        │受信時刻と送信時刻が加えられる.      │
├──────────┼─┼────┼───────────────────┤
│Address Mask Request│17│RFC950  │サブネットマスクを問い合わせるために,│
│                    │  │        │ホストから発行される.                │
├──────────┼─┼────┼───────────────────┤
│Address Mask Reply  │18│RFC950  │Address Mask Requestに対する応答.    │
└──────────┴─┴────┴───────────────────┘

raw socket

raw socket は,特権ユーザが直接プロトコルにアクセスするためのものである.特権ユーザとは,UNIX では root になる. ICMP メッセージを扱うためにはこの raw socket を使用することになる. raw socket としては,ICMP メッセージだけではなく汎用的な仕組みを提供している.

raw socket の目的は,次のようになる.

ということに使用できる.ここでのユーザプロセスとは,カーネル以外のプロセスという意味になる. raw socket は,直接プログラミングの対象になることはすくないと思うが,ネットワークを利用しているシステム内では活躍している.

raw socket のできること

raw socket は,通常の TCP や UDP というプロトコルの上で作成しているアプリケーションではなく,直接プロトコルを扱いたい場合に使用する.これをカーネルではなくユーザプロセスとして使用できるように用意されている.

次のようなアプリケーションが,raw socket を用いて実装されている.

このうちいくつかは,ネットワークに興味を持っているみなさんなら使用されているのではないだろうか.

raw socket に詳しく書かれている文献はすくない. raw socket の解説やアプリケーションの実装については,[Ste98]を参照してほしい.これ以上のドキュメントは実際に実装されているソースコードだけだろう.

raw socket からユーザプロセスが受けとることができるのは次のようになる.

ICMP メッセージについては,カーネルが処理したあとに渡される.ユーザプロセスに渡されるかどうかについて細かな部分は,カーネルの実装(BSD 系列や Linux)により異なる.詳細はカーネルのソースコードを参照ということになるが,実装レベルでの確認としてサンプルを用意している.プロセスが受けとったすべての ICMP メッセージを出力する後述している watchicmp.rb を参照してほしい.

raw socket は,ネットワークプログラミングとしては,特別なものになるかもしれない.これを直接利用することはすくないが,動作原理の確認などには有効だ.

残念ながら,悪意が持ったユーザがこれらを使い問題を起こすこともある.くれぐれも実験は閉じられた環境で行ってほしい.

ICMPModule

ここでは,ICMPをRubyから使用するためのモジュールである ICMPModuleを紹介する.ICMPModuleはRAAに登録してあるので,最新版はこちらから入手して欲しい. ICMPModuleに含まれるクラスや定数を紹介しておこう.

ICMPSocketクラス

ICMPSocketを使用するには,次のようにすればよい.

| require 'icmp'
| icmp = ICMPModule::ICMPSocket.new

このクラスはSocketのサブクラスのため.入出力は, ICMPSocket#recvICMPSocket#sendを使用して行なうことができる.

ICMPを使用するには,RAWソケットを作成する必要があることは前に述べた.C言語のsocket関数では,以下のような呼び出しでICMPを利用できるようになる.

| #include <sys/socket.h>
| #include <netinet/in.h>
|
| icmp = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);

RubyのSocket.newはほとんどsocket関数への直接のインタフェースであるため,

| require 'socket'
| include Socket::Constants
|
| icmp = Socket.new(AF_INET, SOCK_RAW, 1)

とすることで,ICMPを利用できるようになる. ICMPSochet.newは単に内部でこれを行なっているだけである. Socketクラスを使えば,その他のOSに依存するようなソケット(例えば,FreeBSDのdivert(4)等)に付いても,適切な定数値を与えてやればRubyから利用することが可能である.

ICMPクラス

ICMPを扱う上で面倒なのはパケットの編集や参照である. C言語でICMPパケットを操作するための構造体は, /usr/include/netinet/ip_icmp.hstruct icmp として与えられるのが通常である.この構造体を見てもらえれば分かると思うが,共用体が使われていてなんとも複雑である.ICMPModuleでは,ICMPパケットを編集しやすくするためのICMPクラスを提供し,また,実際のコーディングで必要となるICMPのタイプとコードに対する定数値を定義している.

ICMPのパケットはそのメッセージのタイプによって長さが異なるので,これを調整する仕組みがあれば便利である.また,ICMPはさすがにRAW(生)ソケットというだけのことはあって,通常はOSが自動的に行なってくれるチェックサムの計算などは送信に先立って自前で行なわなければならない. (逆に言えばどんなに間違った物でも送信することができる.) サイズの調整を行ない,チェックサムの計算を行なうためのメソッドICMP#setupを用意している.チェックサムについては,コラム「チェックサムの計算について」を参照して欲しい.

ICMPパケットの送信までの流れはおおむね次のようになる.

| require 'icmp'
| include ICMPModule
|
| icmp = ICMPSocket.new         # ソケットの作成
| packet = ICMP.new             # ICMPパケットの作成
| packet.icmp_type = ICMP_ECHO  # ICMPタイプその他
| packet.icmp_code = 0          #   必要な項目の設定
| ...
| packet.setup                  # パケットを調整する
| icmp.send packet              # 送信

受信の際の取り扱いについては次に挙げるIPクラスとともに紹介する.

IPクラス

ICMPソケットの特徴として,送信時にはICMPの部分だけを送 ればよいのに対して,受信時にはIPヘッダが付加されたままになっているという点が挙げられる.(図2

| 図2 ICMPソケットから受信したデータの構造
|
|                           icmp_type (Type)
| ip_v(IP Version)          │icmp_code (Code)
| │ip_hl(Header Length)    ││ icmp_cksum (Checksum)
| ↓↓                      ↓↓ ↓
|┏┯┯━━━━━━━━━━┳┯┯━┯━━━━━━━━━━━━━┓
|┃        IPヘッダ        ┃        ICMP メッセージ           ┃
|┗┷┷━━━━━━━━━━┻┷┷━┷━━━━━━━━━━━━━┛
|│←──────────→│
|        ip_hl×4バイト
|│←────────────────────────────→│ 
|                一回の recv で受信したデータ

もちろん,これは必要のためこうなっている.実際,ICMPを扱う多くのプログラムはIPヘッダの各フィールドを参照する必要があるため,ICMPModuleでは前述の ICMPクラスに加えて,IPヘッダを参照するために IPクラスを提供している.また,受信したメッセージをIPヘッダとICMPメッセージに分離するために ICMPModule::splitを用意している.

ICMPメッセージの送信までの流れはおおむね次のようになる.

| require 'icmp'
| include ICMPModule
|
| icmp = ICMPSocket.new       # ソケットの作成
| packet = icmp.recv          # メッセージの受信
| iph, msg = split(packet)    # パケットの分解 
| 
| case msg.icmp_type in       # ICMPタイプによる
| when ICMP_ECHO              #   処理の振り分けなど
|  ...
| when ICMP_ECHOREPLY
|  ...
| end

ICMPではポートによる送信先の振り分けなどができないため,そのホストで受信した全てのICMPパケットを受け取ってしまう点には注意が必要である.

ICMPModuleのまとめ

Rubyを使用する上で実効速度に関する問題が取り上げられることがある.ICMPMoudleはアーカイブに含まれている icmpmodule.cをコンパイルすることで,若干の性能の向上が見込まれるが,後に紹介するpingでは,OSに付属の物に比べてRTT(Round Trip Time: 要求から応答までの時間)に幾分差がでるようだ(Celeron 400MHzのマシンで 0.4ms程度).やはりシビアに計測を行なうべき場面ではC言語で記述を行なう必要があるという点は挙げておこう.

ICMPModuleに含まれるクラスのもつメソッド名や,『ICMP_...』で始まる定数名はC言語のヘッダファイルで定義されている名前と同じになっている.このモジュールを使用して得た知識を,C言語で必要になった場合にも活かして頂ければ幸いである.

ping の実装

ICMP モジュールのサンプルとして ping コマンドを実装する.ネットワークへ接続したときに最初にテストするのは,ping コマンドになると思う.みなさんもよく使っているのではないだろうか.

ping コマンドは,raw socket を使用し ICMP メッセージの ECHO REQUEST を相手に送り,ECHO REPLY を受けとる処理を行っている.このとき相手に送るデータ内に送ったプロセスを特定できる情報やパケットの往復の時間を確認できるようにしている.

リスト1(ping.rb)を参照してほしい.

------------------------------------------------------------------- ping.rb
┃  1 #!/usr/bin/env ruby
┃  2 
┃  3 require 'icmp'
┃  4 
┃  5 include ICMPModule
┃  6 include Socket::Constants
┃  7 
┃  8 host = ARGV.shift || 'localhost'
┃  9 ent = Socket::gethostbyname(host)
┃ 10 
┃ 11 ips = "%d.%d.%d.%d" %
┃ 12   [ent[3][0], ent[3][1], ent[3][2], ent[3][3]]
┃ 13 print "PING #{ent[0]} (#{ips})\n"
┃ 14 
┃ 15 # open ICMP socket.
┃ 16 sock = ICMPSocket.new
┃ 17 sockaddr = make_sockaddr_in(AF_INET, 0, ent[3])
┃ 18 
┃ 19 # make ICMP packet.
┃ 20 req = ICMP.new
┃ 21 req.icmp_type = ICMP_ECHO
┃ 22 req.icmp_code = 0
┃ 23 req.icmp_id = $$ & 0xffff
┃ 24 
┃ 25 Thread.start do
┃ 26   loop do
┃ 27     buf = sock.recv(65535)
┃ 28     recv_time = Time.now.to_f
┃ 29     iph, repl = ICMPModule.split(buf)
┃ 30 
┃ 31     if repl.icmp_type == ICMP_ECHOREPLY &&
┃ 32         repl.icmp_id == req.icmp_id
┃ 33       send_time = repl.icmp_data.unpack("d")[0]
┃ 34       rtt = (recv_time - send_time) * 1000
┃ 35       print "%d bytes from %s: icmp_seq=%d time=%.3f ms\n" %
┃ 36         [repl.size, iph.ip_src, repl.icmp_seq, rtt]
┃ 37     end
┃ 38   end
┃ 39 end
┃ 40 
┃ 41 1000.times { |i|
┃ 42   req.icmp_seq = i
┃ 43   req.icmp_data = [ Time.now.to_f ].pack("d")
┃ 44   req.setup # calc checksum and config packet length.
┃ 45 
┃ 46   sock.send(req, 0, sockaddr)
┃ 47 
┃ 48   sleep 1
┃ 49 }
---------------------------------------------------------------------------

処理はそれぞれ次のようになっている.

初期設定部分では,ICMP ライブラリを初期化し送受信の準備を行う. ICMPSocket.new で ICMP のための raw socket を準備する. ICMP.new で ICMP プロトコルの設定を行う.ここでは,ICMP メッセージの ECHO REQUEST ICMPModule::ICMP_ECHO を設定している.また ICMP#icmp_id では,他の ping コマンドなどと ICMP メッセージが混同したときに識別が行えるように設定を行っている. raw socket は,他の ICMP メッセージでも受けとってしまうことがあるためだ.

受信部分は,Thread を使用した.送られてくる ICMP パケットは非同期に送られてくる.送信は一秒間隔で送りだすが,受けとる部分は他の ICMP パケットも受けとる可能性があるため Thread を採用した.きれいなネットワークの場合には,送りだして受けとるというプログラムを書いても問題は起きない.なんらかの ICMP メッセージが送られてくるような環境では Thread が有効になる.受けとった ICMP メッセージは,ECHO REPLY であることと識別のための ID を確認している.また,受けとるまでにかかった経過時間を計算している.

送信部分は,送信用の ICMP メッセージ ECHO REQUEST に送信した順番を確認するための番号の設定,時間を代入している.受信時にこれらの情報をもとに結果を表示している.これらの準備を行いパケットを一秒間隔で送りだしている.

図3 は,ping.rb の実行例になる.

------------------------------ 図3 ping.rd の実行例
# ruby ping.rb
PING localhost.localdomain (127.0.0.1)
16 bytes from 127.0.0.1: icmp_seq=0 time=0.907 ms
16 bytes from 127.0.0.1: icmp_seq=1 time=0.645 ms
16 bytes from 127.0.0.1: icmp_seq=2 time=0.645 ms
------------------------------

ここでは localhost へ送った結果になっている.一般の ping コマンドは,オプションで指定できるがデータ部分がもっと大きいためパケットサイズが異なっている.

なお,RubyにはPingモジュール(ping.rb)が標準で含まれている.これはICMPを使用するのではなく,TCPのechoサービスのコネクションを生成してみることによって,接続性の確認のみを行なうためのものである.これはraw socketがroot権限でしか使用できないことから考えられた代替の方法である.上記の目的に置いては十分使用に耐えるのものなのだが,昨今のOSの標準のセットアップでは不要なポートは閉じられる傾向にあることから,使用できる場面が限られてきているのも事実である.

Traceroute の実装

ICMPを使用するアプリケーションでpingに次いでよく使われるのは,tracerouteだろう(Windowsでは tracertという名前になっている). tracerouteは,指定したホストとの間に存在するゲートウェイを調べるためのツールで,pingと同様にネットワークの状態を調べるのに便利なツールである.

Time To Live

先に述べたようにtracerouteもICMPを使用しているが, ICMPにはパケットの転送経路を取得するような仕組みは備えていないため,ちょっとした仕組みを使って実装されている. tracerouteはICMPだけでなくIPレベルの情報を操作することによって実現されている.

インターネットを流れるパケットのIPヘッダの中には TTL(Time To Live)という値が格納されている.ゲートウェイはパケットを受け取るとTTLの値を検査し,1を引いて0になった場合はパケットを破棄する.そうでなければ, TTL-1の値をTTLとして設定し直して,次のゲートウェイにパケットを転送する.つまりIPパケットは送信元のホストが設定したTTLの数を越えて転送されることはないのである. TTLは8ビットのため,以上の理由から256回以上の転送を必要とするホストとの通信はできないことになるが,現実には十分な大きさのようだ.

このTTLによる転送回数の制限は,ルーティング情報の設定ミスなどからループができてしまった場合などに,同じ経路をパケットがいつまでも行き来し続けることがないようにするためのものである.パケットを破棄したゲートウェイは送 信元のホストに対して,破棄したことを示すICMP Time Exceededメッセージを送信する.これによって送信元のホストはTTLが0になったために相手にパケットが届かなかったことを知ることができるようになっている.

ソケットライブラリでは,sesockopt関数を使用することによってTTLの値を変更することができる.Rubyでは Socket#setsockoptを通して,この機能を利用することができる.

Tracerouteの仕組み

tracerouteは.目的のホストに対してパケットを送信する際にTTLの値を1から順に大きくして行き,ICMP Time Exceededを監視することによって,途中に存在するゲートウェイを検出している.このようにTTLを積極的に操作するアプリケーションは珍しい.

では,TTLの値が十分に大きくなって指定のホストまでパケットが届いた場合は,どうなるだろう.この場合,ICMP Time Exceededが送られることは無いが,tracerouteの作者が目を付けたのが,ICMP Unreachメッセージである.ICMP Unreachにはいくつかの種類があり,それぞれに対応する値が,icmpパケットのicmp_codeフィールドに設定される. ICMP Unreachメッセージについて簡単に紹介しておくが,詳細はRFCや[Ste98]などを参照して欲しい.

net unreachable (icmp_code = 0)

送信先のホストが含まれるネットワークに到達することができないことを通知する.

host unreachable (icmp_code = 1)

送信先のホストに到達することができないことを通知する.

protocol unrechable (icmp_code = 2)

送信先のホストでそのプロトコルが使用できないことを通知する.

port unrechable (icmp_code = 3)

送信先のホストでそのポートが利用できないことを通知する.

fragmentation needed and DF set (icmp_code = 4)

パケットのフラグメントが必要なことを通知する.

source route failed (icmp_code = 5)

送信経路の指定オプションに誤りがあることを通知する.

tracerouteはあるポートに向けてUDPメッセージを送 信していて,このポートに対するメッセージが宛先のホストで処理されなければ,port unreachableが帰って来るので.ここで処理を終了する.このポートは一般に使用されないポートであればなんでもよい.オリジナルの実装では33434番が使用されているが,使用されないことが保証されたポート番号などというものは存在しない.とは言うものの,とりあえず,筆者の経験ではこのポートで問題になったことはないように思う.

Tracerouteの実装

例としてtracerouteの簡易版である traceroute.rbを作成した.簡易版というのは本物には存在するオプションを全てサポートしていないし,実際の tracerouteの機能をきちんと実装していない部分があるからである.まずは,リスト2を参照して欲しい.

------------------------------------------------------------- traceroute.rb
┃  1 #!/usr/bin/env ruby
┃  2 
┃  3 require 'icmp'
┃  4 require 'getopts'
┃  5 
┃  6 include Socket::Constants
┃  7 include ICMPModule
┃  8 
┃  9 IP_TTL = 4
┃ 10 
┃ 11 def traceroute(host, port, num_queries,
┃ 12                max_ttl, wait_time)
┃ 13 
┃ 14   icmp_sock = ICMPSocket.new
┃ 15   udp_sock = UDPSocket.new
┃ 16   ttl = 0
┃ 17 
┃ 18   while ttl < max_ttl
┃ 19     ttl += 1
┃ 20     printf "%3d ", ttl
┃ 21     udp_sock.setsockopt(IPPROTO_IP, IP_TTL, ttl)
┃ 22     send_time = Time.now
┃ 23 
┃ 24     reached = nil
┃ 25     num_queries.times{ |i|
┃ 26       udp_sock.send("\0\0\0\0", 0, host, port)
┃ 27       reached = recv_packet(icmp_sock,
┃ 28                     send_time, wait_time, i == 0)
┃ 29     }
┃ 30     print "\n"
┃ 31     break if reached
┃ 32   end
┃ 33 end
┃ 34 
┃ 35 def recv_packet(sock, send_time,
┃ 36                 wait_time, print_host)
┃ 37 
┃ 38   ary = select([sock], nil, nil, wait_time)
┃ 39   unless ary
┃ 40     print(" *")
┃ 41     return nil
┃ 42   end
┃ 43 
┃ 44   buf = sock.recv(65535)
┃ 45   recv_time = Time.now
┃ 46   iph, icmpp = ICMPModule::split(buf)
┃ 47 
┃ 48   if print_host
┃ 49     begin
┃ 50       name, = TCPSocket::gethostbyname(iph.ip_src)
┃ 51     rescue SocketError
┃ 52       name = iph.ip_src
┃ 53     end
┃ 54     printf("%s (%s) ", name, iph.ip_src)
┃ 55   end
┃ 56   rtt = recv_time.to_f - send_time.to_f
┃ 57   printf "%.3f ms ", rtt * 1000
┃ 58 
┃ 59   case icmpp.icmp_type
┃ 60   when ICMP_TIMXCEED
┃ 61   when ICMP_UNREACH
┃ 62     case icmpp.icmp_code
┃ 63     when ICMP_UNREACH_PORT
┃ 64       return :TR_REACHED
┃ 65     when ICMP_UNREACH_HOST
┃ 66       print(" !H")
┃ 67     when ICMP_UNREACH_NET
┃ 68       print(" !N")
┃ 69     when ICMP_UNREACH_PROTOCOL
┃ 70       print(" !P")
┃ 71     end
┃ 72   else
┃ 73     print(" ??")
┃ 74   end
┃ 75 end
┃ 76 
┃ 77 ##
┃ 78 ## Main routine
┃ 79 ##
┃ 80 getopts nil, "p:33434", "q:3", "m:30", "w:3"
┃ 81 udp_port    = $OPT_p.to_i
┃ 82 num_queries = $OPT_q.to_i
┃ 83 max_ttl     = $OPT_m.to_i
┃ 84 wait_time   = $OPT_w.to_i
┃ 85 
┃ 86 traceroute(ARGV[0], udp_port, num_queries,
┃ 87            max_ttl, wait_time)
---------------------------------------------------------------------------

11行目から始まるtracerouteメソッドがこのプログラムの本体である.このメソッドはTTLと各ホストへの問い合わせの回数の2重のループになっている.UDPを送出する際の TTLの設定は21行目でUDPSocket#setsockoptを呼び出すことで行なっている.

TTLを増加させる度にnum_queriesで指定された回数 UDPパケットを送出する.返答の受信および出力は recv_packetメソッドで行なう.recv_packetメソッドの最後の引数はホスト名を出力するかどうかを指定するための物である.最初の要求(i==0)の時のみホスト名を出力するように指定している.

recv_packetはICMPソケット,送信時刻,応答の待ち時間.前述のホスト名の出力フラグを引数として受け取る.受け取ったパケットがICMP Unreach Portの場合は,trueを返し,それ以外の場合にはnilを返す. tracerouteメソッドはrecv_packetの結果が trueの場合にはループを抜けて終了する.

実際のtracerotueコマンドは,ICMP UureachやICMP Time Exceededに含まれるオリジナルのIPヘッダおよびUDPヘッダを利用して,自分が送ったパケットに対する応答かどうかを判定するようになっているが,UDPヘッダのレイアウトを知る必要があるなど,あまり本質的ではないので,ここでは省略している.

ICMP メッセージを受けとる watchicmp.rb

ICMP の応用として ICMP メッセージを確認するツール watchicmp.rb を紹介しよう.これは,システムに送られてくる ICMP メッセージをできるだけ表示するというものになる.ユーザプロセスである watchicmp.rb は,システムに送られてきたすべての ICMP メッセージを表示することはできない.これは,カーネルが処理してしまいユーザプロセスには渡されない ICMP メッセージがあるからだ.ユーザプロセスとしてどういうものが受け取ることができるかの確認や,周囲からどのような ICMP メッセージが送られてくるかの確認にりようできる.システムが受けとった ICMP メッセージの統計的情報は netstat コマンドのオプション -s で確認できる(コラム netstat コマンド).以下に示す watchicmp.rb は,システムがユーザプロセスへどの程度 ICMP メッセージを渡すかにより動作が異なる.実行するシステムにより結果が異なる.今回の実行例は Linux で実施している例になっている.

watchicmp.rb

すべての ICMP メッセージを表示するために,すべての ICMP メッセージを受けとるようにしている.これは,Ruby の ICMP ライブラリのがめに記述的にはとても簡単だ.リスト( watchicmp.rb )を参照してほしい.

-------------------------------------------------------------- watchicmp.rb
┃  1 #! /usr/local/bin/ruby
┃  2 
┃  3 class String
┃  4   def to_hex
┃  5     str = self.unpack('C*').collect do |i|
┃  6       '%02x' %  i
┃  7     end.join(' ')
┃  8     47.step(str.size - 1, 48) do |i|
┃  9       str[i] = "\n"
┃ 10     end
┃ 11     str
┃ 12   end
┃ 13 end
┃ 14 
┃ 15 require 'icmp'
┃ 16 include ICMPModule
┃ 17 include Socket::Constants
┃ 18 
┃ 19 class Integer
┃ 20   def to_icmptype_string
┃ 21     case self
┃ 22     when ICMP_ECHOREPLY
┃ 23       'ICMP_ECHOREPLY'
┃ 24     when ICMP_UNREACH
┃ 25       'ICMP_UNREACH'
┃ 26     when ICMP_SOURCEQUENCH
┃ 27       'ICMP_SOURCEQUENCH'
┃ 28     when ICMP_REDIRECT
┃ 29       'ICMP_REDIRECT'
┃ 30     when ICMP_ECHO
┃ 31       'ICMP_ECHO'
┃ 32     when ICMP_ROUTERADVERT
┃ 33       'ICMP_ROUTERADVERT'
┃ 34     when ICMP_ROUTERSOLICIT
┃ 35       'ICMP_ROUTERSOLICIT'
┃ 36     when ICMP_TIMXCEED
┃ 37       'ICMP_TIMXCEED'
┃ 38     when ICMP_PARAMPROB
┃ 39       'ICMP_PARAMPROB'
┃ 40     when ICMP_TSTAMP
┃ 41       'ICMP_TSTAMP'
┃ 42     when ICMP_TSTAMPREPLY
┃ 43       'ICMP_TSTAMPREPLY'
┃ 44     when ICMP_IREQ
┃ 45       'ICMP_IREQ'
┃ 46     when ICMP_IREQREPLY
┃ 47       'ICMP_IREQREPLY'
┃ 48     when ICMP_MASKREQ
┃ 49         'ICMP_MASKREQ'
┃ 50     when ICMP_MASKREPLY
┃ 51       'ICMP_MASKREPLY'
┃ 52     else
┃ 53       'unknown ICMP?'
┃ 54     end
┃ 55   end
┃ 56 end
┃ 57 
┃ 58 STDOUT.sync = true
┃ 59 
┃ 60 icmp = ICMPSocket.new
┃ 61 
┃ 62 loop do
┃ 63   buf = icmp.recv(65535)
┃ 64   iph, repl = ICMPModule.split(buf)
┃ 65   
┃ 66   ipaddr = iph.ip_src
┃ 67   p Time.now
┃ 68   p TCPSocket.gethostbyname(ipaddr)
┃ 69   type =  repl.icmp_type.to_icmptype_string
┃ 70   
┃ 71   print ipaddr, ':',
┃ 72     type, ':',
┃ 73     iph.ip_len - 20,':',
┃ 74     repl.icmp_data.size, "\n"
┃ 75   print repl.icmp_data.to_hex, "\n\n"
┃ 76 end
---------------------------------------------------------------------------

最後の 10 数行ですべてを記述している.前半は表示するリストを見易くするためのサポート部分になっている.

watchicmp の使いかた

raw socket を扱うためには,root の権限が必要になる.実行は root で行ってほしい.図4は,相手がいない先に ping を実行した結果,ICMP UNREACH メッセージを受けとった例になる.

------------------------------図4 watchicmp.rb 実行例その 1
# ruby watchicmp.rb
Sat Dec 09 10:32:30 JST 2000
["", [""], 2, "192.168.1.1"]
192.168.1.1:ICMP_UNREACH:112:104
45 00 00 54 21 38 00 00 40 01 e7 f7 c0 a8 78 1e
c0 a8 78 0a 08 00 cb 49 5c 14 01 00 ac 8b 31 3a
f9 d8 0d 00 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13
14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23
24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33
34 35 36 37 83 08 83 08 3c 1c b2 12 83 08 83 08
df 24 df 24 df 24 df 24
------------------------------

図5は,localhost へ ping を実行した結果,ICMP ECHO/ECHOREPLY メッセージを受けとった例になる. ICMP#icmp_data のデータ部分をダンプ表示している.この部分では,ICMP ECHO メッセージで送られてきた内容を ICMP ECHOREPLY メッセージで送り返していることがわかる.

------------------------------図5 watchicmp.rb 実行例その 2
Sat Dec 09 10:41:51 JST 2000
["localhost.localdomain", ["localhost"], 2, "127.0.0.1"]
127.0.0.1:ICMP_ECHO:64:56
df 8d 31 3a d8 ca 03 00 08 09 0a 0b 0c 0d 0e 0f
10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f
30 31 32 33 34 35 36 37

Sat Dec 09 10:41:51 JST 2000
["localhost.localdomain", ["localhost"], 2, "127.0.0.1"]
127.0.0.1:ICMP_ECHOREPLY:64:56
df 8d 31 3a d8 ca 03 00 08 09 0a 0b 0c 0d 0e 0f
10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f
30 31 32 33 34 35 36 37
------------------------------

応用として

アプリケーションとしては,直接 ICMP メッセージを扱うことはまずない.けれど,UDP ソケットを扱うような場合非同期の ICMP エラーメッセージの受信が問題になる. UDP ソケットでは,IP パケット配送の問題については,非同期にエラーを受けとることになりアプリケーションとしての対応が難しい.複数の IP パケット配送のエラーなどが送られてきた場合にアプリケーション内部では,エラーが起きたタイミングで確認できない.次に動作を行うときに,エラーが起きているということがわかるだけだ.このような IP パケットの配送の問題を ICMP メッセージを確認することでいくらか補完できるのではないだろうか.これは,アプリケーションで直接 ICMP メッセージを受けとるということではなく,システムに一つ ICMP メッセージを監視する daemon の役目になると思う.この話題については [Ste98] に詳しい.

また,システムがネットワーク的にどのような状態になっているか,ということも確認できる. netstat コマンドにより統計的な情報は得られるが,どのタイミングでなにが起きているかというのはわからない. ICMP メッセージを確認することで,システムのネットワークの状態,アプリケーションが利用しているネットワークの状態が確認できる.これは,システム監視やシステム管理という意味でツールになるのではないだろうか.なにが起きているか,なにが起きたかという記録はシステム管理には重要な情報になる.

まとめ

ICMPの利用方法を取り上げるのが今回のテーマだったが,いかがだっただろうか.ICMPの仕組みや利用方法だけでなく, pingtraceroute等,普段何気なく使っているコマンドも自分で実際に作成できるということを感じていただければ幸いである.

とりわけUNIXの分野では,ほとんど全てのコマンドの完全なソースを入手することができるので,「あのコマンドに似たことがやりたいんだけど...」というような場合には,これらのソースにあたってみるのがいい.今回取り上げた tarcerouteのような仕組みは,なかなか思い付くようなものではないだろう.

さて,次号からは再びTCPの世界に戻り,数回に渡って電子メール関連のプロトコルを扱っていく.ひとまず,次号では SMTPおよびPOP3を取り上げることにする.メールシステムはその歴史が長いだけに非常に多くの資産のある分野でもある.そこで,プロトコルそのものに関する話題よりも,既存のライブラリをいかに利用するかに焦点をあてていく予定である.

参考文献

[Bak95]

F. Bake,``Requirements for IP Version 4 Routers'',RFC 1812 (June 1995)

[BBP88]

B.Braden, D.Borman, C. Partridge, ``Computing the Internet Checksum'', RFC 1071 (01 Sep 1988)

[Dee91]

S. Deering,``ICMP Router Discovery Messages.'',RFC 1256 (01 Sep 1991)

[Har00]

原信一郎 (まつもとゆきひろ監修), 『Rubyプログラミング入門』, オーム社 (2000)

[Ken91]

S. Kent, ``U.S. Department of Defense Security Options for the Internet Protocol.'',RFC 1108 (Nov 1991)

[MI99]

まつもとゆきひろ, 石塚圭樹, 『オブジェクト指向スクリプト言語Ruby』, アスキー (1999)

[MP85]

J.C. Mogul, J. Postel,``Internet Standard Subnetting Procedure'', RFC 950 (01 Aug 1985)

[Pos81]

J. Postel, ``INTERNET CONTROL MESSAGE PROTOCOL'', RFC 792 (01 Sep 1981)

[Ste98]

W.リチャード・スティーヴンス,『UNIX ネットワークプログラミング 第2版 Vol.1』, ピアソン,1999

コラム netstat コマンド

UNIX 系のコマンドに netstat というものがあります.ネットワークを使われているかたは,このコマンドをよく使われていると思います.このコマンドには,-s オプションがありますが,みなさんは使っているでしょうか.もし,使ったことがなかったらぜひ確認してみてください.

このオプションは,実行してみるとわかりますが,プロトコルレベルの統計情報を出力します. IP レベル,今回とりあげている ICMP に TCP や UDP についてです.それぞれのプロトコルで,パケットをやり取りを行ったときの情報を残しているのです.この中に ICMP メッセージの記録も残っています. ICMP の情報が残っているということは,ネットワーク上でなにかが起きているという結果が残っていることになります.確認できるものはシステムとして受けとった ICMP メッセージのタイプと数,送った ICMP のタイプと数になります.

ネットワークの状態を確認するときに,これらの情報も役立つものがあります.ぜひ活用ください.

コラム チェックサムの計算について

チェックサムの計算は,チェックサムフィールドを0にした状態でのパケットを16ビット整数の配列と見なし,全体の 1の補数和の1の補数を取ることによって求められます.詳しい手順はRFC1071である,``Computing the Internet Checksum'' を参照してください.1の補数和を計算することによる利点は,バイトオーダーを気にしなくてもよい点にあります.例として,

|┏━┯━┯━┯━┓
|┃ff│ff│00│01┃
|┗━┷━┷━┷━┛

という配列の16ビット整数の1の補数和を考えてみることにします.SPARCなどのCPUで扱われる,いわゆるビッグエンディアンと呼ばれる種族では0xffff + 0x0001の計算結果 (0x0000)に対して,繰り上がった桁を足し込むため,

0xffff +' 0x0001 = 0x0000 + 1 = 0x0001
(「+'」は1の補数和を表します)

となります.一方Intelのチップに代表されるリトルエンディアンと呼ばれる種族では,0xffff + 0x0100の計算結果 (0x00ff)に対して,繰り上がった桁を足し込むため,

0xffff +' 0x0100 = 0x00ff + 1 = 0x0100

となります.これらのバイト順は逆転しているので,バイナリイメージとしては同じ物が得られるという点が重要です.パケットにチェックサムを格納する際には,この結果の更に 1の補数を取ったものを書き込むことになっています.

ICMPModule#ICMPでは次のようなコードでこの機能を実現しています.毎回1の補数和を取るのではなく,桁溢れした部分は6行目と7行でまとめて足し込んでいます.

-------------------------------------- ICMP#set_cksum
| 1 def set_cksum
| 2   self.icmp_cksum = 0
| 3   sum = 0
| 4   self.unpack("n*").each{ |i| sum += i }
| 5   sum += (self[-1] << 8) if self.size % 2 == 1
| 6   sum = (sum >> 16) + (sum & 0xffff)
| 7   sum += (sum >> 16)
| 8   self.icmp_cksum = ~sum & 0xffff
| 9   self 
|10 end
-----------------------------------------------------

さっき「バイトオーダーを気にしなくてもいい」と言ったばかりにも係わらず,4行目でわざわざネットワークバイトオーダーでupackしています.これは8行目で行なっている icmp_cksumの更新に関連します.ICMP#icmp_cksum=は次のようになっています.

----------------------------------- ICMP#icmp_cksum=
| def icmp_cksum=v
|   self[2..3]=[v].pack("n")
| end
-----------------------------------------------------

ネットワークバイトオーダーでpackした結果を格納しているため,こうしなければ実際のバイト順が逆になってしまうのです.他のフィールドに付いてはネットワークバイトオーダーで評価しているのに,ここだけ変更するのも何か変です.

筆者も最初はunsigned shortとして評価するunpack("S*") の結果からチェックサムを計算していましたが当然うまくいくはずもなく,送出されていくパケットを眺めながらこの関連に気が付くまでかなりの時間を要してしまいました.単に注意が足りないだけですが,過去の経験ゆえにハマる悲しい例と言えなくもありません.Rubyでバイナリデータを編集するにはちょっとした慣れが必要かもしれません.

RFC1071の他,ASCIIより刊行されている「BSD magazine 2000 No.4」の72ページのコラム「IPチェックサムの秘密」にチェックサムに関する詳しい考察があります.

コラム 気をつけたいICMPの利用

ICMPはネットワークを管理する上で大変便利なものですが,迷惑のかかるような利用は考えものです.むかしからIRCサーバのような有名なサイトは大量の ICMPパケットを使った攻撃にさらされており,ICMPサービスを停止しているところも少なくありません.筆者の一人が所属する組織もこの夏からICMPの一部を通さなくしました.しかしこのサイトは某ネットワークの地域拠点となるサイトでもあるため下流サイトの管理者は不便な思いをすることも少なくないだろうと思います.たとえば,遠くのサイトとのtracerouteなどは実用上あまり必要でないと思いますが,近所とのIP接続性を確認していくことで障害の範囲を特定することは現実的に必要なことがしばしばあります.そんなときにICMP が使えないのは緊急車両が通れないようなものかも知れません.寒い話です.

いっぽうで,悪意を持たなくても日常的に使うものではありません.とくに管理に関係のない負荷測定のためにECHOなんかを流すのはご法度です.管理上必要でなければ公衆回線で利用するのは避けましょう.


[Ruby] 著者(50音順): 後藤謙太郎、後藤裕蔵、高橋征義、渡辺哲也
御意見,御感想,御批判の宛先: ipr-feedback@notwork.org