はじめに
本稿では、Whoisサービスの概要を紹介し、プロトコル仕様(RFC3912)に基づいたWhoisクライアントのサンプルプログラムを作成します。また、同サンプルを国際化ドメイン名に対応させるため、「アプリケーションのドメイン名国際化(RFC3490)」についてもその内容および実装方法を紹介します。
対象読者
- .NET Frameworkを用いてWindowsアプリケーションを開発している方。
- ネットワーク関連のプログラムを作成される方。
必要な環境
サンプルはVisual Studio .NET 2003で作成し、.NET Framework 1.1で動作確認をしています。
「IPアドレス」および「ドメイン名」について
インターネットに接続された全てのコンピューターには、それぞれを識別するためにIPアドレスが割り振られています。実際の通信は、このIPアドレスを使用して行われますが、数字の羅列であるIPアドレスは、人間が利用するのに困難であるため、分かりやすい名前(ドメイン名)を付けて利用されています。
IPアドレスやドメイン名の割り振りや管理を行っているのは、「レジストラ」という機関です。過去には、これらの資源管理は、com、net、orgの3つのトップレベルドメインをNetwork Solutions社、それ以外をIANAという組織が行ってきました。しかし、インターネットの国際化に伴い、1998年10月以降、ICANNという組織がこれを引き継いでいます。ICANNの下部組織には、ARIN(アメリカ大陸、カリブ海周辺、サハラ砂漠以南のアフリカ大陸)、APNIC(アジア・太平洋地域)、RIPE-NCC(ヨーロッパやその周辺地域)の3つの団体があり、それぞれが管轄するIPアドレスの割り振りを行っています。また、併せてその地域内の各国ドメインを担当するレジストラの統括も行っています。
各レジストラは、Whoisサービスを使用し、現在の割り当て状態を公開しており、ドメイン名やIPアドレスからいつでも最新の所有(利用)者名を検索できるようになっています。
今回作成したWhoisクライアントは、このWhoisサービスを通じてこれらの公開情報を取得するプログラムです。
Whoisプロトコルについて
Whoisサービスで使用されているプロトコル仕様はRFC3912で定義されています。Whoisプロトコルは、TCPの43番ポートを使用した非常にシンプルなプロトコルであり、クライアントはテキストで検索したい文字列を送り、サーバからのレスポンスもまたすべてテキストで返されます。クライアントからの要求はASCIIのCRLFで終了し、応答での改行にもCRFLが使用されます。なお、サーバ側から接続を閉じることでプロトコルは完結します。
サンプルとして、『whois.nic.milにあるWhoisサーバに「Smith」の要求をした場合』に回線上を流れるパケットを表現したものがRFCにあります。
client server at whois.nic.mil open TCP ---- (SYN) ------------------------------> <---- (SYN+ACK) ------------------------- send query ---- "Smith<CR><LF>" --------------------> get answer <---- "Info about Smith<CR><LF>" --------- <---- "More info about Smith<CR><LF>" ---- close <---- (FIN) ------------------------------ ----- (FIN) ----------------------------->
Whoisクライアントの実装例
.NETでは、TCP通信を簡単に行うためのクラス(TcpClient)が用意されています。このクラスを利用することで、Whoisクライアントは非常に間単に実装することが可能です。
String server="whois.jp"; // 例としてwhois.jpに接続する Int32 port=43; //デフォルトポートは43番 TcpClient tcp = new TcpClient();// TcpClientの生成 try{ tcp.Connect(server,port); // 接続 NetworkStream ns = tcp.GetStream(); //JISでエンコードして取得する StreamReader sr = new StreamReader(tcp.GetStream(), Encoding.GetEncoding(50222)); // 例として"google.c.jp"を検索する string Data = "google.co.jp" + "\r\n"; // バイトストリームを作成 byte[] szData = Encoding.ASCII.GetBytes(Data.ToCharArray()); ns.Write(szData, 0, szData.Length);// リクエストを送信する while (true){ string s = sr.ReadLine(); // 1行受信 if(s==null) // ReadLineでnullが返されたとき、 // 相手から切断されていると判断する break; Console.Write(s+"\r\n"); // 取得した1行を表示する } tcp.Close(); }catch{ // エラー発生 }
例からも分かるとおり、検索したい情報を持っているWhoisサーバに接続し、検索したいドメイン名(IPアドレス)を1行送信するだけです。問題になるのは、「検索したい情報を持っているWhoisサーバがどこにあるのか?」と言うことだけなので、接続すべきサーバさえ分かってしまえば何の問題もなくWhoisクライアントは完成というわけです。
なお、Whoisクライアントの完全な実装例は、サンプルコードの「Whois.cs」を参照してください。
Whoisサーバの一覧データベース
各ドメインのWhoisサーバは、通常、各ドメインを管理しているレジストラが提供しています。
サンプルプログラムでは、最初に入力された文字列からIPAddress
を生成し、成功した場合には、IPアドレスが指定されたものとしてサーバの検索を行い、失敗した場合には、ドメイン名が入力されたものとして同様に検索を行っています。
// 指定ドメインのWhoisサーバ及びポート番号を取得する public bool Get(String target,ref String server,ref Int32 port) { try{ IPAddress ipAddress = IPAddress.Parse(target); // 成功した場合、IPアドレスが指定されている if(GetServer(ipAddress,ref server,ref port)) return true; // 検索成功 return false; }catch{ // 失敗した場合、ホスト名が指定されている } String domain = target; while(true){ int i; // 最初に.が現れた位置を取得する if(-1==(i = domain.IndexOf("."))) return false; // ドメイン名をもう取得できない // .の位置以降の文字列をドメイン名として取得する domain = domain.Substring(i+1); // データベースにドメインが存在するかどうかを検索する if(GetServer(domain,ref server,ref port)) return true; // 検索成功 } }
サンプルプログラムで、Whoisサーバの一覧データベースはWhoisDb
クラスで表現されています。WhoisDb
クラスは、コンストラクタで初期化ファイル「Whois.conf」を読み込み、ArrayList
にその内容を管理しています。
// [CIDR] IPアドレスを検索する場合に使用するwhoisサーバ名のリスト 201.0.0.0/8 whois.lacnic.net 203.0.0.0/8 whois.apnic.net 210.0.0.0/8 whois.apnic.net 211.0.0.0/8 whois.apnic.net 212.0.0.0-217.255.255.255 whois.ripe.net 213.0.0.0/8 whois.ripe.net // [DOMAIN] ドメイン名を検索する場合に使用するwhoisサーバ名のリスト com whois.internic.net jp whois.jp net whois.internic.net
IPアドレスからの検索の場合
IPアドレスが入力された場合の接続先もまた、「Whois.conf」から構築されるデータベースで検索していますが、こちらは、やや複雑な形式になっています。これは、IPアドレスが範囲を表す形式であるため、単純に文字列の比較では検索できないためです。
「Whois.conf」でIPアドレスは、「ビットマスクでの表現」と「範囲による表現」の2種類を使用しています。
192.168.0.1/24 ''(←ビットマスクによる表現)'' 192.168.0.1-192.168.0.100 ''(←範囲による表現)''
WhoisDb
クラスでは、正規表現のマッチングにより、この表現を発見した場合は、その表現する範囲の開始アドレスと終了アドレスを計算し、startIp
およびendIp
という変数で管理しています。なお、このIPアドレスの情報は、データベース内では32ビットの変数(UInt32)で保持されていますが、大小比較で容易に検索できるようにビックエンディアン形式に変換にしています。
static bool Function(ref UInt32 startIp,ref UInt32 endIp,String str){ MatchCollection matches; // 正規表現によるCIDRかどうかの判断 [例:192.168.0.1/24] matches = Regex.Matches(str, @"^([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3})/([0-9]{1,3})$"); if(matches.Count>=1){ try{ // IPアドレス表現部分("192.168.0.1")をstrAddrにコピーする String strAddr = matches[0].Groups[1].Value; // マスク表現部分("24")をstrMaskにコピーする String strMask = matches[0].Groups[2].Value; // IPアドレス文字列からIPAddressクラスを生成し、 // バイト配列を取得する // バイトの並びはリトルエンディアンになっている // 0xC0,0xA8,0x00,0x01 byte [] byteAddr = IPAddress.Parse(strAddr).GetAddressBytes(); // バイト配列をUInt32に変換する // 0xC0,0xA8,0x00,0x01 -> 0x0100A8C0 ("1.0.168.192") UInt32 uintAddr = BitConverter.ToUInt32(byteAddr,0); // リトルエンディアンをビックエンディアンに変換する // 0x0100A8C0->0xC0A80001 ("192.168.0.1") uintAddr = Convert.ToUInt32(IPAddress.NetworkToHostOrder( (int)uintAddr) & 0xFFFFFFFF); // マスク数("24")のバイト配列を作成する // 24 の場合, // (11111111111111111111111100000000) 0xFFFFFF00となる UInt32 mask=0; int max = Int32.Parse(strMask); for(int i=0;i<32;i++){ mask = mask << 1; if(i<max) mask += 1; } // マスクのバイト配列を使用して // ブロードキャスト作成用のバイト配列を作成する // 0xFFFFFF00 の場合 0x000000FFとなる UInt32 broadcast = 0xFFFFFFFF ^ mask; // IPアドレスとネットマスクでANDを取って // 開始アドレスを作成する // 192.168.0.1 ,0xFFFFFF00 の場合 192.168.0.0となる startIp = Convert.ToUInt32(uintAddr & mask); // IPアドレスとブロードキャスト作成用のORを取って // 終了アドレスを作成する // 192.168.0.1 ,0x000000FF の場合 192.168.0.255となる endIp = Convert.ToUInt32(uintAddr | broadcast); return true; }catch{ return false; } } // 正規表現によるCIDRかどうかの判断 [例:192.168.0.1-192.168.0.100] matches = Regex.Matches(str, @"^([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3})- ([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3})$"); if(matches.Count>=1){ try{ byte [] byteAddr; UInt32 uintAddr; String strAddr; // 開始アドレスの取得 // 最初のアドレス部分文字列を取得する "192.168.0.1" strAddr = matches[0].Groups[1].Value; // IPアドレス文字列からIPAddressクラスを生成し、 // バイト配列を取得する // バイトの並びはリトルエンディアンになっている // 0xC0,0xA8,0x00,0x01 byteAddr = IPAddress.Parse(strAddr).GetAddressBytes(); // バイト配列をUInt32に変換する // 0xC0,0xA8,0x00,0x01 -> 0x0100A8C0 ("1.0.168.192") uintAddr = BitConverter.ToUInt32(byteAddr,0); // リトルエンディアンをビックエンディアンに変換する // 0x0100A8C0->0xC0A80001 ("192.168.0.1") startIp = Convert.ToUInt32(IPAddress. NetworkToHostOrder((int)uintAddr) & 0xFFFFFFFF); // 終了アドレスの取得 // 最初のアドレス部分文字列を取得する "192.168.0.100" strAddr = matches[0].Groups[2].Value; // IPアドレス文字列からIPAddressクラスを生成し、 // バイト配列を取得する // バイトの並びはリトルエンディアンになっている // 0xC0,0xA8,0x00,0x64 byteAddr = IPAddress.Parse(strAddr).GetAddressBytes(); // バイト配列をUInt32に変換する // 0xC0,0xA8,0x00,0x64 -> 0x6400A8C0 ("100.0.168.192") uintAddr = BitConverter.ToUInt32(byteAddr,0); // リトルエンディアンをビックエンディアンに変換する // 0x6400A8C0->0xC0A80064 ("192.168.0.100") endIp = Convert.ToUInt32(IPAddress. NetworkToHostOrder((int)uintAddr) & 0xFFFFFFFF); return true; }catch{ return false; } } return false; }
2段階検索
IPアドレスからの検索の場合、サーバから次のようなレスポンスが返ってくる場合があります。
OrgName: Asia Pacific Network Information Centre OrgID: APNIC Address: PO Box 2131 City: Milton StateProv: QLD PostalCode: 4064 Country: AU ReferralServer: whois://whois.apnic.net NetRange: 218.0.0.0 - 218.255.255.255 CIDR: 218.0.0.0/8 NetName: APNIC4 NetHandle: NET-218-0-0-0-1 Parent: NetType: Allocated to APNIC
これは、更なる照会サーバがあることを示しています。そこで本サンプルプログラムでは、受信したレスポンスの中に「ReferralServer:」の行を見つけた場合、接続先を指定されたものに変更して、再度検索を実施しています。
また、whois.arin.netから返されるレスポンスで下記のようなものもあります。
Comcast Cable Communications, IP Services EASTERNSHORE-1 (NET-24-0-0-0-1) 24.0.0.0 - 24.15.255.255 Comcast Cable Communications TEXAS-8 (NET-24-0-0-0-2) 24.0.0.0 - 24.1.255.255
これは、当該サブネットは、指定したネット名(NET-24-0-0-0-1など)で検索できることを示しています。そこでサンプルプログラムでは、この場合も再び同じサーバに接続し、検索文字列を当該ネット名に変更して再検索を行うようにしています。
文字コードの問題
非常に簡単に実装できるWhoisクライアントですが、実は1つの問題を抱えています。それは、文字コードの問題です。Whoisプロトコルの仕様では、使用する文字コードを制限していないため、サーバから返される文字コードは保障されていません。このため、クライアント側で表示する際に適切なコード変換を行わないと、正常に表示できない場合があるということです。
この問題の身近なものとして、日本ドメイン(JP)の情報を提供しているWHOIS.JPがあります。このサーバでは、Whois情報をJISコードで返答します。従って、Whoisクライアントの側で適切なコード変換をしないと、日本語が正常に表示できないという結果になります。本来であれば、どのような文字コードが返されても、それを判別して確実に変換するようにプログラムしなければなりませんが、現実には、ほとんどのWhoisサーバがASCIIコードで返答するため、本サンプルではJISコードのみに対応して実装を行いました。
最初の「簡単なWhoisクライアントの実装例」では、下記の部分がその対応になっています。接続ストリームはオープン時にEncoding.GetEncoding
を使用して、JISコードとして認識されています。
StreamReader sr = new StreamReader(tcp.GetStream(), Encoding.GetEncoding(50222));//JIS
国際化ドメイン名(IDN)技術
みなさんは、「生茶.jp」というドメイン名を聞いたことがあるでしょうか? 著名な企業がキャンペーンに使用したことで非常に印象深いものがありました。この日本語で表現されたドメイン名は、http://生茶.jp/のようにアクセスできます。
これは、国際化ドメイン名(Internationalized Domain Name:以降「IDN」と表記)技術を利用したものであり、漢字やひらがなをドメイン名として使用できるようになっています。
アプリケーションのドメイン名国際化(RFC3490)
ドメイン名で使用可能な文字は、英数字とハイフンの37文字だけのはずが、どうして2バイト文字をドメイン名に利用できるのでしょうか。実は、人間が認識している2バイト文字を、プログラムの中で英数字とハイフンに置き換える仕組みがここで働いているのです。
サンプルプログラムで「生茶.jp」を検索した時のイメージを見ていただくと分かるとおり、実際にWhoisサーバにリクエストされているのは、「XN--9MYO41A.JP」であり、このドメイン名がDNSに登録されているのです。
このように、IDNをASCII文字だけに変換するメカニズムが、アプリケーションのドメイン名国際化(Internationalized Domain Name in Applications:以降「IDNA」と表記)技術です。IDNAは、ドメイン名という識別子を国際化するための技術標準であり、IETFによって技術仕様を規定したRFC3490が発行されています。IDNA技術は完全にアプリケーションの中にあるもので、クライアント・サーバやピアトゥーピアのプロトコルではありません。
IDNAにおいて、IDNを変換するステップは、おおむね次のとおりです。
- 入力されたドメイン名を.(ドット)で区切ってラベルに分解する
- ASCII表記でない場合、[RFC3491]名前前処理(NAMEPREP)を行う
- [RFC3492]プニコード(PUNYCODE)によるACEラベルへの変換
- ACEラベルへACEプレフィックス(xn--)を追加
- 分解された各ラベル(ACEラベル)からドメイン名を再構築する
RFC3490に記述されている変換ステップをフローチャートで表現すると、次のようになります。
IDNAを実装するアプリケーションは、上記の工程を実装しなくてはなりません。サンプルプログラムでは、クラスIdna
(「Idna.cs」)のToAscii
で、この内容を実装しています。
名前前処理(NAMEPREP):RFC3491
NAMEPREPは、上記のIDNAの中で使用されるUNICODE文字列の標準化のための処理であり、RFC3491に定義されています。
UNICODEでは、さまざまな場面での互換性を考慮して、ディスプレイ上では同一に見えるものも内部的には複数のコードを持っている場合があります。しかし、識別のためのドメイン名として使用するのに、これでは不都合を生じてしまいます。そこで、このNAMEPREPでは、IDNAで使用されることを前提にUNICODEの各種変換を定義しています。
NAMEPREPにおいて処理される内容は、おおむね次のとおりです。
- 文字範囲の確認
- 文字種の統一(大文字を小文字に変更するなど)
- 互換文字の統一や合成用文字の合成(カタカナの半角を全角に変更)
- 出力禁止文字の削除(空白や制御文字)
具体的には、上記の処理は、RFC3454で定義されているテーブル(「A.1」「B.1」など)を使用して、対象文字を削除したりマッピングしたりする処理となります。
- 文字範囲の確認(「A.1」で定義されるUnicode 3.2に該当していないかどうかの確認)
- 文字種の統一(「B.2」で定義されるテーブルで置き換え)
- 互換文字の統一や合成用文字の合成(「B.2」で定義されるテーブルで置き換え)
- 出力禁止文字の削除(「C.1.1」「C.2.2」「C3」「C4」「C5」「C6」「C7」「C8」「C9」で定義される文字を削除)
サンプルプログラムでは、クラスStringprepProfile
(「StringprepProfile.cs」)のNameprep
でこの内容を実装しています。
プニコード処理(PUNYCODE):RFC3492
PUNYCODEもまた、上記のIDNAの中で使用される処理であり、RFC3492で定義されています。PUNYCODEは、IDNを従来のASCIIドメイン名と互換性のある形式にエンコードおよびデコードするために特化した、UNICODEの符号化方式の1つです。PUNYCODE処理において、UNICODEを符号変換する際、比較的複雑な計算方法をとっていますが、『いますぐ使える国際化ドメイン名の理論と実践 ~アプリケーションとネットワークのIDNへの対応~」』(@IT)で簡略化した説明が公開されておりますので紹介させて頂きます。
サンプルプログラムでは、クラスPunycode
(「Punycode.cs」)で、この内容を実装していますが、このコードはRFCの中で記述されているC言語のサンプルコード(『C. Punycode sample implementation』)をそのままC#に書き直したものです。なお、同サンプルでは、エンコード(Encode)のみを使用していますが、参考のためデコード(Decode)のためのコードも同クラスに記述しました。
まとめ
Whoisクライアントの作成は、非常にシンプルなものです。しかし、それが取得する情報は膨大なものであり、また有意義なものも多数含まれています。この機能を利用したアプリケーションも、色々考えられるのではないでしょうか。
また、本稿で紹介したとおり、IDNA技術は、それぞれのアプリケーションが対応する技術です。ネットワーク関連のプログラムで、その機能にドメイン名を処理することが含まれている場合、今回紹介した国際化ドメイン名への対応が必要ないかどうかをぜひ検討してみてください。
最後になりますが、ソースファイルには、IDNへのエンコードだけでなく、サンプルプログラムで利用されていないIDNからのデコードも機能としてプログラムしました。興味のある方は、ぜひ参照してみてください。
参考資料
- RFC 3912 WHOISプロトコル仕様
- RFC 3490 アプリケーションのドメイン名国際化(IDNA)
- RFC 3491 名前前処理:国際化のドメイン名のための文字列前処理プロフィール(IDN)
- RFC 3492 プニコード:アプリケーションの国際化ドメイン名(IDNA)のためのユニコードブースティングコーディング
- RFC 3454 国際化文字列の準備("文字列準備")
- idnkit 国際化ドメイン名ツールキット(JPNIC)
- 日本レジストリサービス
- 日本語ドメイン名協会
- JDNA 『各種ブラウザ日本語ドメイン名対応環境の設定方法』
- @IT 『いますぐ使える国際化ドメイン名の理論と実践 ~アプリケーションとネットワークのIDNへの対応~』 米谷嘉朗 著、2003年2月