Shoeisha Technology Media

CodeZine(コードジン)

記事種別から探す

Whoisクライアントの作成と国際化ドメイン名への対応

ドメイン名やIPアドレスから所有者の情報を取得するプログラムの作成

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/10/13 12:00

本稿では、Whoisサービスの概要を紹介し、プロトコル仕様(RFC3912)に基づいたWhoisクライアントのサンプルプログラムを作成します。また、同サンプルを国際化ドメイン名に対応させるため、「アプリケーションのドメイン名国際化(RFC3490)」についてもその内容および実装方法を紹介します。

サンプルプログラムの実行例
サンプルプログラムの実行例

はじめに

 本稿では、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サービスで交際されている情報をWeb上で簡単に検索できるサービスがいくつか展開されています。実装している機能は、今回紹介したサンプルとほぼ同じですが、ブラウザで軽易に利用できるという点で非常に優れています。

Whoisプロトコルについて

 Whoisサービスで使用されているプロトコル仕様はRFC3912で定義されています。Whoisプロトコルは、TCPの43番ポートを使用した非常にシンプルなプロトコルであり、クライアントはテキストで検索したい文字列を送り、サーバからのレスポンスもまたすべてテキストで返されます。クライアントからの要求はASCIIのCRLFで終了し、応答での改行にもCRFLが使用されます。なお、サーバ側から接続を閉じることでプロトコルは完結します。

 サンプルとして、『whois.nic.milにあるWhoisサーバに「Smith」の要求をした場合』に回線上を流れるパケットを表現したものがRFCにあります。

Whoisプロトコルのパケットの例(RFC3912より)
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クライアントは非常に間単に実装することが可能です。

簡単な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サーバからの対象サーバの検索
// 指定ドメインの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にその内容を管理しています。

「Whois.conf」から抜粋
// [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種類を使用しています。

「Whois.conf」内でのIPアドレス範囲の表現例
192.168.0.1/24 ''(←ビットマスクによる表現)''
192.168.0.1-192.168.0.100 ''(←範囲による表現)''

 WhoisDbクラスでは、正規表現のマッチングにより、この表現を発見した場合は、その表現する範囲の開始アドレスと終了アドレスを計算し、startIpおよびendIpという変数で管理しています。なお、このIPアドレスの情報は、データベース内では32ビットの変数(UInt32)で保持されていますが、大小比較で容易に検索できるようにビックエンディアン形式に変換にしています。

IPアドレスの開始と終了を取得する
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から返されるレスポンスで下記のようなものもあります。

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コードとして認識されています。

JISコードの変換
StreamReader sr = new StreamReader(tcp.GetStream(),
                                   Encoding.GetEncoding(50222));//JIS

国際化ドメイン名(IDN)技術

 みなさんは、「生茶.jp」というドメイン名を聞いたことがあるでしょうか? 著名な企業がキャンペーンに使用したことで非常に印象深いものがありました。この日本語で表現されたドメイン名は、http://生茶.jp/のようにアクセスできます。

 これは、国際化ドメイン名(Internationalized Domain Name:以降「IDN」と表記)技術を利用したものであり、漢字やひらがなをドメイン名として使用できるようになっています。

国際化ドメイン名に対応したブラウザでアクセスした様子
国際化ドメイン名に対応したブラウザでアクセスした様子
http://生茶.jp/
 ブラウザが日本語ドメイン名に対応していない場合、このようなアクセスはできません。現在、Netscape・Firefox・Operaなどは、そのままで対応可能ですが、InternetExplorerの場合、「i-Nav」や「JWord」などのプラグインが必要になります。

アプリケーションのドメイン名国際化(RFC3490)

 ドメイン名で使用可能な文字は、英数字とハイフンの37文字だけのはずが、どうして2バイト文字をドメイン名に利用できるのでしょうか。実は、人間が認識している2バイト文字を、プログラムの中で英数字とハイフンに置き換える仕組みがここで働いているのです。

 サンプルプログラムで「生茶.jp」を検索した時のイメージを見ていただくと分かるとおり、実際にWhoisサーバにリクエストされているのは、「XN--9MYO41A.JP」であり、このドメイン名がDNSに登録されているのです。

生茶.jpのWhois情報を検索
生茶.jpのWhois情報を検索

 このように、IDNをASCII文字だけに変換するメカニズムが、アプリケーションのドメイン名国際化(Internationalized Domain Name in Applications:以降「IDNA」と表記)技術です。IDNAは、ドメイン名という識別子を国際化するための技術標準であり、IETFによって技術仕様を規定したRFC3490が発行されています。IDNA技術は完全にアプリケーションの中にあるもので、クライアント・サーバやピアトゥーピアのプロトコルではありません。

 IDNAにおいて、IDNを変換するステップは、おおむね次のとおりです。

  1. 入力されたドメイン名を.(ドット)で区切ってラベルに分解する
  2. ASCII表記でない場合、[RFC3491]名前前処理(NAMEPREP)を行う
  3. RFC3492]プニコード(PUNYCODE)によるACEラベルへの変換
  4. ACEラベルへACEプレフィックス(xn--)を追加
  5. 分解された各ラベル(ACEラベル)からドメイン名を再構築する

 RFC3490に記述されている変換ステップをフローチャートで表現すると、次のようになります。

RFC3490をフローチャートに表現
RFC3490をフローチャートに表現

 IDNAを実装するアプリケーションは、上記の工程を実装しなくてはなりません。サンプルプログラムでは、クラスIdna(「Idna.cs」)のToAsciiで、この内容を実装しています。

名前前処理(NAMEPREP):RFC3491

 NAMEPREPは、上記のIDNAの中で使用されるUNICODE文字列の標準化のための処理であり、RFC3491に定義されています。

 UNICODEでは、さまざまな場面での互換性を考慮して、ディスプレイ上では同一に見えるものも内部的には複数のコードを持っている場合があります。しかし、識別のためのドメイン名として使用するのに、これでは不都合を生じてしまいます。そこで、このNAMEPREPでは、IDNAで使用されることを前提にUNICODEの各種変換を定義しています。

 NAMEPREPにおいて処理される内容は、おおむね次のとおりです。

  1. 文字範囲の確認
  2. 文字種の統一(大文字を小文字に変更するなど)
  3. 互換文字の統一や合成用文字の合成(カタカナの半角を全角に変更)
  4. 出力禁止文字の削除(空白や制御文字)

 具体的には、上記の処理は、RFC3454で定義されているテーブル(「A.1」「B.1」など)を使用して、対象文字を削除したりマッピングしたりする処理となります。

  1. 文字範囲の確認(「A.1」で定義されるUnicode 3.2に該当していないかどうかの確認)
  2. 文字種の統一(「B.2」で定義されるテーブルで置き換え)
  3. 互換文字の統一や合成用文字の合成(「B.2」で定義されるテーブルで置き換え)
  4. 出力禁止文字の削除(「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)のためのコードも同クラスに記述しました。

国際化ドメイン名対応ツールキット(idnkit)
 既存のアプリケーションを国際化ドメイン名に対応させるひとつの方法として、日本語ドメイン名協会(JDNA)で配布されているツールキット(idnkit)をインストールする方法があります。ただし、このツールはWindows環境において「Winsock.DLL」の動作にラッピングする形で動作するものであり、Winsockの名前解決を利用している部分のみの対応となります。従って、今回作成しているようなWhoisクライアントなど、直接国際化ドメイン名が必要になる場合には、利用できません。

まとめ

 Whoisクライアントの作成は、非常にシンプルなものです。しかし、それが取得する情報は膨大なものであり、また有意義なものも多数含まれています。この機能を利用したアプリケーションも、色々考えられるのではないでしょうか。

 また、本稿で紹介したとおり、IDNA技術は、それぞれのアプリケーションが対応する技術です。ネットワーク関連のプログラムで、その機能にドメイン名を処理することが含まれている場合、今回紹介した国際化ドメイン名への対応が必要ないかどうかをぜひ検討してみてください。

 最後になりますが、ソースファイルには、IDNへのエンコードだけでなく、サンプルプログラムで利用されていないIDNからのデコードも機能としてプログラムしました。興味のある方は、ぜひ参照してみてください。

参考資料

  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 平内 真一(ヒラウチ シンイチ)

     クラスメソッド株式会社 モバイルアプリサービス部所属  仕事では、iOSアプリの開発を行っております。 会社ブログ 個人ブログ

All contents copyright © 2005-2017 Shoeisha Co., Ltd. All rights reserved. ver.1.5