Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

NetBIOSネームサービスでネットワーク内の端末をリアルタイムに列挙する

NBNSプロトコルの解釈とパケットの送受信

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

ダウンロード 実行ファイル (17.2 KB)
ダウンロード ソースファイル (31.9 KB)

本稿では、nbtstatコマンドで使用されているNBNSパケットを生成し、リアルタイムでネットワーク内のコンピュータを列挙するプログラムを作成します。また、サンプルを通してプロトコル仕様に従ったパケットの作成および、解釈についても紹介します。

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

はじめに

 本稿では、nbtstatコマンドで使用されているNBNSパケットを生成し、リアルタイムでネットワーク内のコンピュータを列挙するプログラムを作成します。また、サンプルを通してプロトコル仕様に従ったパケットの作成および、解釈についても紹介します。

対象読者

  1. .NET Frameworkを用いてWindowsアプリケーションを開発している方。
  2. ネットワーク関連のプログラムを作成される方。

必要な環境

 サンプルはVisual Studio .NET 2003で作成し、.NET Framework 1.1で動作確認をしています。

最新を表示しないネットワークコンピュータの一覧

 デスクトップ上のネットワークコンピュータを開くと、ネットワークにつながったWindows端末の一覧を表示することができます。しかし、この一覧では、表示されているのにアクセスできない端末や、逆に、居るはずなのに表示されない端末があることを皆さんは体験的によく知っていると思います。これは、ネットワークコンピュータで表示する一覧が、現在の状態を検索しているのではなく、その時点でマスターブラウザが保持している「ブラウズ・リスト」を表示していることが原因です。「ブラウズ・リスト」は、独自の時間間隔で情報を更新しており、リアルタイムで最新状態を反映できていないため、このような症状が発生しているのです。

図1 ネットワークコンピュータによるワークグループ内の端末表示
図1 ネットワークコンピュータによるワークグループ内の端末表示
ブラウズ・リストの更新のしくみ
 「ブラウズ・リスト」は、マスタブラウザが保持する端末の一覧です。それぞれの端末が、起動・終了時に自らの名前をマスタブラウザに通知することで更新されています。また、12分おきに、自分の存在を確認してもらうためマスタブラウザに通知をしています。したがって、端末が異常終了して終了の通知をしなかった場合などは、12分後の確認時間まで居なくなった事を知るすべがなく放置され、ブラウズ・リストから消えることはありません。
 また、マスタブラウザは、自動的に割り当てられるため、マスタブラウザとなっている端末が急に居なくなった場合も、混乱を生じます。
 なお、このNetBIOSの名前解決は、同一ネットワーク内での動作が基本となっているため、複数のネットワークにまたがったワークグループなどの更新は、その整合のためにさらに時間を要することになります。
 詳しくは、たかはしもとのぶ氏の下記のページで解説されていますので、紹介しておきます。

NetBIOSネームサービス

 ネットワークコンピュータの一覧に表示されていなくても、直接その端末を指定して開いたり、「コンピュータの検索」を使用した場合にアクセスできる事があります。これは、これらの動作が先の「ブラウズ・リスト」に頼っていないからです。そして、この動作で使用されているのが、NetBIOSネームサービス(以降、「NBNS」と表記)というプロトコルです。

nbtstatコマンド

 WindowsでNBNSをコマンドとして実装しているのがnbtstatです。nbtstatコマンドは、次のように使用することができます。

nbtstatコマンドの実行
C:\>nbtstat -A 192.168.1.211

ローカル エリア接続:
Node IpAddress: [192.168.1.211] Scope Id: []

           NetBIOS Remote Machine Name Table

       Name               Type         Status
    ---------------------------------------------
    WS1            <00>  UNIQUE      Registered
    WORKGROUP      <00>  GROUP       Registered
    WS1            <20>  UNIQUE      Registered
    WORKGROUP      <1E>  GROUP       Registered
    WORKGROUP      <1D>  UNIQUE      Registered
    ..__MSBROWSE__.<01>  GROUP       Registered

    MAC Address = 00-09-46-89-F9-ED

 出力から分かるとおり、IPアドレスを頼りに、その端末のコンピュータ名や所属しているワークグループ名などをリアルタイムで取得することができます。

NBNSのパケット

 図2は、nbtstatコマンドを実行した際に、どのようなパケットの送受で取得されたのかをパケットモニターで取得した様子です。

図2 nbtstat -Aをモニターした様子
図2 nbtstat -Aをモニターした様子

 モニタした結果、NBNSのメッセージは、50バイトの要求パケットと、約200バイトの返答パケットがUDPのポート137(送受)でやり取りされている事が分かります。NBNSは、DNSプロトコルフォーマットの拡張という形で仕様化されており、ほとんどDNSのクエリーと同じと考えて問題ありません。

 DNSクエリーメッセージと、NBNSメッセージの違いは、フラグフィールドの12ビット目に「ブロードキャストフラグ」が追加されているだけです。詳しくは、次の図(図3)を参照してください。

図3 DNSクエリーメッセージとNBNSの構造
図3 DNSクエリーメッセージとNBNSの構造

 図3で示される、質問エントリーおよび応答・権威・追加情報の各セクションで使用されるリソースレコードのフォーマットは、図4のようになっています。

図4 質問エントリーおよび各セクションで使用されるリソースレコードのフォーマット
図4 質問エントリーおよび各セクションで使用されるリソースレコードのフォーマット

 本来は、ドメイン名の部分は長さを指定して可変長で表現できるのですが、NBNSの場合は、0x16(32オクテット)に固定されています。

要求パケット

 「nbtstat -A」を実行したときに送信される要求パケットでは、ヘッダ部分の質問数に1がセットされ、その後に質問エントリーが1つだけ含まれたメッセージが送信されます(応答などの各セクションはありません)。

 この質問エントリのドメイン名部分には「* <00>」を表現する「CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00」が挿入され、この後に続くタイプには、\x21(NetBIOS Node Status)、クラスには\x01(インターネット)が入ります。

 パケットのサイズは、ヘッダ(12)+ドメイン名(32+2 サイズ・終端を含む)+タイプ(2)+ クラス (2) の合計で常に50オクテットとなります。

ニブル変換によるDNS名の作成
 NBNSパケットの中では、ドメイン名部分に挿入されるNetBIOS名は、DNS名との互換のために符号化されています。
 符号化の手順は、下記のとおりです。
  1. 例としてNetBIOS名[ABC]を変換します
  2. ABC      <00>
  3. 名前を16進数で表現する(空白は0x20になります)
  4. 41-42-43-20-20-20-20-20-20-20-20-20-20-20-20-00
  5. 4ビット単位で分解する
  6. 4-1-4-2-4-3-2-0-2-0-2-0-2-0-2-0-2-0-2-0-2-0-2-0-2-0-2-0-2-0-0-0
  7. 符号化ASCII文字で変換する(0はA、1はB、...、FはPとなる)
  8. AECEDCACACACACACACACACACACACAAA
 上記と同様の変換で[*<00>]は、「CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA」になっています。

応答パケット

 一方、応答メッセージでは、ヘッダ部分の応答セクション数に1がセットされ、その後の応答セクション部分にリソースレコードが1つだけ含まれたメッセージが返されます(質問エントリやその他のセクションはありません)。

 リソースレコードでは、ドメイン名、タイプ、クラスの部分に、要求メッセージ内の質問エントリで送ったものが、そのまま格納され、その後に生存時間、データ長、データ部が続きます。

 データ部には、nbtstatで本来取得したかった、各種のNetBIOS名情報が格納されています。データ部の細部のフォーマットは、図5のとおりです。

図5 データ部のフォーマット
図5 データ部のフォーマット

 図5から分かる通り、最初の1バイトには、これから続く各種のNetBIOS名情報の数が格納されています。そしてその後には、その数分だけ固定長18バイトのNetBIOS名情報が続き、最後にMACアドレスが格納されています。

 固定長18バイトのNetBIOS名情報は、「NetBIOS名」「タイプ」「ステータス」の3つで構成されています。

NetBIOS名のサフィックス
 NetBIOS名は16文字に固定されていますが、Microsoftではその16バイト目を、機能を表すサフィックスとして扱っています。たとえば、「ABC<03>」のように表現される名前があった場合、最後の<03>が、このサフィックスです。
 nbtstatコマンドで表示される各種のNetBIOS名も、このサフィックスを見ることで、どういう機能の名前であるかを区別することができるようになっています。
 サンプルプログラムでも、このサフィックスを使用してコンピュータ名やワークグループ名を判断しています。
 サフィックスについての細部は、『NetBIOS サフィックス (NetBIOS 名の 16 番目の文字)』を参照してください。

ドメイン名圧縮

 各エントリーに出てくるドメイン名については、DNSのパケットでは、重複を避けるために「ドメイン名圧縮」が使用されています。しかし、NBNSのパケットでは、要求パケットで質問エントリが1つ、応答パケットでも応答セクションが1つだけという形式ですので、ドメイン名が1回以上出現する可能性がありません。そのため、サンプルプログラムでは、この「ドメイン名圧縮」については実装していません。

 一方、DNSパケットでは、ドメイン名の先頭部分が一致することがほとんどであり、この圧縮は非常に有効に働いています。DNSパケットの作成および解釈には、「ドメイン名圧縮」のアルゴリズムの実装は必須ですのでご注意ください。

パケットを操作するプログラム

 パケットを直接操作すると言っても、EtherヘッダやIPヘッダなどを操作する訳ではありませんので、送受信に関しては通常のソケットプログラムとなんら変わるところはありません。

 今回のサンプルプログラムでは、ソケットを扱う基本的ライブラリであるSystem.Net.socketクラスを使用しました。NBNSはUDPで動作するプロトコルであるため、ソケットはUDPで作成し、SendToメソッドで137番ポートに送信しています。

 なお、対象IPアドレスの端末が存在しない場合などに返事を待つタイムラグを解消するため、受信は非同期で行うようBeginReceiveFromメソッドを使用し、別スレッドで受信するようにしました。

UDPパケットの送受信処理
// 送信先の指定
IPEndPoint ep = new IPEndPoint(IPAddress.Parse(ipAddress),137);
// UDPソケット作成
Socket socket =
    new Socket(ep.AddressFamily,SocketType.Dgram,ProtocolType.Udp);
// 要求パケット送信
socket.SendTo(sendBuffer,0,sendBuffer.Length,SocketFlags.None,ep);
// パラメータ受け渡し用クラスを生成
StateObject so = new StateObject(socket,ep);
// 非同期受信スレッドの開始
socket.BeginReceiveFrom(so.recvBuffer,0,so.recvBuffer.Length,
    SocketFlags.None,ref so.ep,new AsyncCallback(ReciveCallback),so);

要求パケットの作成

 テキストデータを送信するのではなく、プロトコル仕様に整合したデータを作成するには、少しテクニックが必要です。通常このような場合、プロトコルのフォーマット仕様を構造体で表現し、その値を設定して送信データ用のバッファにコピーするという手法を使用します。

 要求パケットで必要な、NBNSのヘッダと質問エントリを表現した構造体は、次のようになります。

図3のNBNSメッセージヘッダ(固定長ヘッダ12オクテット)の部分を表現する構造体
//NBNSヘッダ構造体
[StructLayout(LayoutKind.Sequential,Pack=1)]
    struct NBTHeader{
    public ushort  identification; // 識別
    public ushort  flags;          // 各種フラグ
    public ushort  qd_count;       // 質問数
    public ushort  an_aount;       // 回答数
    public ushort  ns_count;       // オーソリティ数
    public ushort  ar_count;       // 追加情報数
}
図4の質問エントリを表現する構造体
//質問エントリ構造体
[StructLayout(LayoutKind.Sequential,Pack=1)]
    struct QNSection{  
    public byte name_len;  // 名前の長さ
    [MarshalAs(UnmanagedType.ByValTStr,SizeConst=33)]
    public string   name;          // 名前
    public ushort  qtype;          // タイプ
    public ushort  qclass;         // クラス
}

 メモリ上のイメージを構造体で表現するには、StructLayout属性を設定します。StructLayout属性には配置方法の指定が可能ですが、ここでLayoutKind列挙体のSequentialを指定すると、構造体で定義した順番で各メンバがメモリ上に配置されるようになります。

 StructLayoutはデフォルトでパッキングが8になっています。これはプログラムの効率化のために、各メンバを8バイト境界にあわせて配置するということになります。構造体の中でメンバがushortushortと並んだ場合などに、本当は4バイト、4バイトと並んでほしいのに最初の4バイトの後に隙間を作ってしまうことになります。そこで、StructLayoutの中でPack=1を指定して、1byte境界に配置するように指定しています。プロトコルフォーマットを構造体で表現する場合は、必ずパッキングの問題に注意が必要です。

 次に定義した構造体を使用して、送信データを作成する手順を示します。

要求パケットの作成
// ヘッダ
NBTHeader nbtHeader = new NBTHeader();
nbtHeader.identification = NetUtil.htons(identification); // 識別子
nbtHeader.qd_count       = NetUtil.htons(1);// 質問数
int nbtHeaderSize = Marshal.SizeOf(nbtHeader);
// 質問エントリ
// ・名前
QNSection qnSection = new QNSection();
qnSection.name   = "CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
// ・名前のサイズ
qnSection.name_len = Convert.ToByte(qnSection.name.Length);
// ・タイプ(0x21 NBNS Node Status)
qnSection.qtype  = NetUtil.htons(0x21);
// ・クラス(0x01 Internet)
qnSection.qclass = NetUtil.htons(0x01);
int qnSectionSize = Marshal.SizeOf(qnSection);

// 要求パケット作成
byte [] sendBuffer = new byte[nbtHeaderSize+qnSectionSize];
unsafe{
    fixed(byte *p = sendBuffer) {
        // ヘッダのコピー
        Marshal.StructureToPtr(nbtHeader,(IntPtr)(p),true);
        // 質問エントリのコピー
        Marshal.StructureToPtr(qnSection,
            (IntPtr)(p+nbtHeaderSize),true);
    }
}

 C#では、ローカル変数を生成するとすべてのメンバが0で初期化されているため、ヘッダ構造体と質問エントリ構造体を生成した後、0でないメンバだけの値を代入しています。

 完成した構造体を、実際の送信データにコピーする段階では、unsafeを使用してポインタによるメモリコピーを使用しています。unsafeは、メモリ管理がすべてプログラマの責任にゆだねられますのでコピー先のバッファサイズを確実に確保する必要があります。サンプルプログラムでは、ヘッダ構造体と質問エントリ構造体のサイズを足した分だけ送信バッファを確保しています。

 サンプル内で使用されているNetUtil.htonsは、バイトオーダをネットワーク形式に変更するために、独自に作成したメソッドです。.NET Framework では、System.Net.IPAddress.HostToNetworkOrderで同様の処理が可能ですが、プログラムを簡素に書けるように「NetUtil.cs」で用意したものです。

応答パケットの解釈

 応答パケットの解釈は、送信パケット作成の逆順と考えてください。まずは、応答パケットの解釈に必要なフィールドを構造体で表現します。

図4のリソースレコードを表現する構造体
//リソースレコード構造体
[StructLayout(LayoutKind.Sequential,Pack=1)]
    struct QNSection{  
    public byte name_len;  // 名前の長さ
    [MarshalAs(UnmanagedType.ByValTStr,SizeConst=33)]
    public string   name;            // 名前
    public ushort  qtype;          // タイプ
    public ushort  qclass;         // クラス
}

 必要なフィールドを表現する構造体が揃ったら、これに受信したデータをコピーしていきます。

 まずは、NBNSヘッダを解釈し、質問エントリおよび各セクションのレコード数を確認します。続いて、その数だけフィールドを読み取っていくことになります。この際、先にも書いたとおり、unsafeの中でのメモリ操作には細心の注意が必要です。

 受信したパケットは外部からの直接入力であり、常に悪意がある可能性があることを念頭におく必要があります。サンプルプログラムでは、コピーの前に必要なサイズが確実にあるかどうかを確認し、問題ある場合は例外を発生させるようにしました。

応答パケットの解釈
int len=0; // 受信サイズ
if(0<(len = so.socket.EndReceiveFrom(ar,ref so.ep))){
    // ヘッダ
    NBTHeader nbtHeader = new NBTHeader();
    int nbtHeaderSize = Marshal.SizeOf(nbtHeader);
    // 質問フィールド
    QNSection qnSection = new QNSection();
    int qnSectionSize = Marshal.SizeOf(qnSection);
    // リソースレコード
    ResourceRecord resourceRecord = new ResourceRecord();
    int resourceRecordSize = Marshal.SizeOf(resourceRecord);
    // リソースデータ
    byte [] rdata=null;
    unsafe{
        int offSet=0;
        fixed(byte *p = so.recvBuffer) {
            // ヘッダのコピー
            // 受信バイト数超過(例外処理)
            if(offSet+nbtHeaderSize>len)
                throw new Exception();
            nbtHeader = (NBTHeader)Marshal.PtrToStructure(
                (IntPtr)(p),typeof(NBTHeader));
            offSet+=nbtHeaderSize;
            // RCodeおよびQRの確認
            int flags = NetUtil.htons(nbtHeader.flags);
            int RCode = flags & 0x000F;
            int QR = flags>>15;
            if(RCode!=0) // RCodeがNOERROR(0)でない場合(例外処理)
                throw new Exception();
            if(QR!=1) // QRが応答(1)でない場合(例外処理)
                throw new Exception();
            // 回答セクションがない場合(例外処理)
            if(1>NetUtil.htons(nbtHeader.an_aount))
                throw new Exception();
            // 質問セクションのコピー(通常は0)
            for(int i=0;i<nbtHeader.qd_count;i++){
            // 受信バイト数超過(例外処理)
                if(offSet+qnSectionSize>len)
                    throw new Exception();
                qnSection = (QNSection)Marshal.PtrToStructure(
                    (IntPtr)(p+offSet),typeof(QNSection));
                offSet+=qnSectionSize;
            }
            // 回答セクションのコピー(1セクションのみ)
            // 受信バイト数超過(例外処理)
            if(offSet+resourceRecordSize>len)
                throw new Exception();
            resourceRecord = (ResourceRecord)Marshal.PtrToStructure(
                (IntPtr)(p+offSet),typeof(ResourceRecord));
            offSet+=resourceRecordSize;
            int rdata_len = NetUtil.htons(resourceRecord.rdata_len);
            if(offSet+rdata_len>len) // 受信バイト数超過(例外処理)
                throw new Exception();
            rdata = new byte[rdata_len];
            Marshal.Copy((IntPtr)(p+offSet),rdata,0,rdata_len);
            // オーソリティおよび追加情報は読み飛ばす
        }
    }
    // この時点でデータ部は、rdataに確保されている
    // rdataの処理
}

 今回紹介しているNBNSパケットの応答のパケットには、質問エントリーは存在せず、応答セクションに1つのリソースレコードだけが含まれています。しかし、プロトコル上は、それぞれにレコードを挿入することが可能になっているため、正当にレコード数を確認し、順番に読み込みを行っています。ただし、応答セクションを1レコード読み込んだ後は、どのようなデータが入っているかを判断せずに、そのまま取得完了としています。

nbtstatコマンドを使用するとネットワーク外の端末のMACアドレスを取得できる
 MACアドレスは、ブロードキャストドメイン内の通信にのみ使用されているため、ネットワーク外(ルータの外)に存在する端末のMACアドレスがパケットに現れることはありません。そのことから、ネットワーク外の端末のMACアドレスを知りたくても、なんらかの上位プロトコルにそのデータがない限り取得は不可能ということになります。
 今回紹介したNBNSには、応答パケットにMACアドレス情報が入っていますので、このような用途に使用することが可能です。

まとめ

 本稿では、NBNSパケットの送受信を通してプロトコル仕様に従ったパケットの作成・解釈の一例を紹介しました。わずかながらでも、ネットワーク関連のプログラム作成に参考になれば幸いです。

参考資料

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

著者プロフィール

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

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

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