2007年05月10日

TAP-Win32 でネットワークパケットと戯れる (前編)

はてなブックマークに登録

■ はじめに

coLinuxOpenVPN を使ったことのある方なら仮想ネットワークアダプタ「TAP-Win32」の名前をご存知でしょう。
TAP-Win32は CIPE-Win32 というプロジェクトによる GPL のオープンソースソフトウェアで、実体は Windows のカーネルモード下で動作するネットワークドライバです。
TAP は本物のネットワークデバイスのように振る舞うので、これを使って Windows 上に独立した仮想ネットワーク環境を設置することが可能です。
TAP-Win32 を自作コードから利用するための開発者向けの情報は現時点ではあまり多くありませんが、面白そうなので扱い方を調べてみました。
データリンク層以降の生のパケットデータをユーザモードのプログラムから直接操作できるためアイディア次第で応用がききそうです。
今回は手始めに TAP デバイスからデータを読み出す方法をご紹介します。

■ TAP-Win32 ドライバのダウンロード

TAP-Win32 ドライバは coLinux や OpenVPN のパッケージに含まれていますが、機能的に独立しているため単体でインストールすることが可能です。 現在の最新版バイナリは下記のページなどからダウンロードできます。
TAP-Win32 Adapter V8 (coLinux)
※筆者は手持ちの Windows XP SP2 および Vista 環境に Version 8.4 をインストールし正常に動作することを確認しましたが、 実験はご自身の判断と責任で行って下さい。 (トラブルの例)

■ TAP-Win32 ドライバのインストール

アーカイブに含まれる一式を適当なディレクトリへコピーし、
[コントロールパネル]-[ハードウェアの追加]〜(略)〜「一覧から選択したハードウェアをインストールする」- 「ネットワークアダプタ」-「ディスク使用」から、展開ずみの *.sys および *.inf の存在するパスを指定して下さい。

■ 実験のための設定

[設定]-[ネットワーク接続] からインストールずみの TAP-Win32 デバイスを探し、適当な名前に変更しましょう。
ここでは「TAP01」としておきます。
併せて、プロパティ画面で現在の環境と衝突しない IP アドレスを設定します。
ここでは、192.168.0.1 サブネットマスク 255.255.255.0 としておきます。
これで TAP 側の準備は終わりです。

■ 「ネットワークケーブルが接続されていません」?

[ネットワーク接続] を覗いてみると TAP-Win32 デバイスのアイコンに×印がついています。ネットワークケーブル未接続の状態と表示されておりどこかで何かを間違えたのかとしばらく悩みましたが、ソースを読んでみるとこれは TAP デバイスに 接触中のプロセスが存在していないためとわかりました。なるほど、確かに意味的には「オフライン」ですね。
聞くところによると Windows 側の設定で常時オンライン状態にする方法もあるらしいのですが、まあここはあまり気にせずにおきましょう。

■ TAP-Win32 にアクセスしてみる

プログラムから TAP-Win32 とのやり取りを行うための手順は以下の通りです。
1) TAP デバイスをオープンする
2) TAP デバイスをアクティブ状態にする
3) TAP デバイスとパケットデータ入出力を行う
4) TAP デバイスをクローズする

1) TAP デバイスをオープンする
TAP デバイスの GUID(Global Unique ID) を指定し、CreateFile() API を呼び出します。
ネットワークデバイスの GUID はレジストリに記録されています。レジストリエディタで以下のキーを先頭に「TAP01」を検索することで参照が可能です。

[HKLM\SYSTEM\CurrentControlSet\Control\Network]

上記キー配下の \{xxxxx}\{yyyyy}\Connection\Name が「TAP01」なエントリの {yyyyy} 部分が GUID です。
なお、末尾のサンプルコードには TAP デバイスの表示名から GUID を自動的に検索する例を掲載しています。
CreateFile() 呼び出しの際のデバイス名は次の形式で指定します。
"\\.\Global\{yyyyy}.tap"

2) TAP デバイスをアクティブ状態にする
CreateFile() で取得したデバイスハンドルに対し、TAP_IOCTL_SET_MEDIA_STATUS コードを送出することで TAP デバイスがアクティブになります。コードの送出には DeviceIoControl() API を使用します。MSDN: DeviceIoControl リファレンス
ちなみに、TAP_IOCTL_SET_MEDIA_STATUS 定数は TAP-Win32 本体に定義されています。

3) TAP デバイスとのパケットデータ入出力
オープンずみのデバイスハンドルに対し ReadFile(), WriteFile() を行うことでパケットデータの入出力が可能です。

4) TAP デバイスをクローズする
CloseHandle() でデバイスハンドルをクローズします。

■ サンプルプログラム

最後に、TAP デバイスをオープンし受信パケットの内容をダンプする C 言語のサンプルコードを掲載します。これを sample.c の名前で保存し、 コンパイル〜リンクしてお試し下さい。 本コードは Microsoft Visual C++ .NET 2003 および、Microsoft Visual C++ 2005 (無償)でのビルドを確認しています。 また、フリーのMinGW (Minimalist GNU for Win32) でもビルド可能です。
C:\temp>cl sample.c
Microsoft(R) 32-bit C/C++ Standard Compiler Version XXXX
Copyright (C) Microsoft Corporation 1984-XXXX. All rights reserved.
 
sample.c
Microsoft (R) Incremental Linker Version XXXX
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:sample.exe
sample.obj
TAP デバイスの表示名を引数として sample.exe を実行するとコンソールに受信パケットの内容がダンプ出力されます。
C:\temp>sample TAP01
TAP-Win32: [TAP01] GUID = {B0DD3441-B92F-4F93-889A-XXXXXXXXXXXX}
[Press CTRL+C to exit]
<< dump: 42 bytes >>
FF FF FF FF FF FF 00 FF  B0 DD 34 41 08 06 00 01
08 00 06 04 00 01 00 FF  B0 DD 34 41 C0 A8 00 01
00 00 00 00 00 00 C0 A8  00 05
<< dump: 215 bytes >>
FF FF FF FF FF FF 00 FF  B0 DD 34 41 08 00 45 00
00 C9 32 41 00 00 80 11  85 92 C0 A8 00 01 C0 A8
	:
生データのままではパケット内容を解析しにくいためパケットモニタプログラムを併用すると良いでしょう。Microsoft Network Monitor 3

#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
 
#pragma warning(disable: 4996)
 
#pragma comment(lib, "Advapi32.lib")
 
#define DEVICE_PATH_FMT "\\\\.\\Global\\%s.tap"
 
#define TAP_CONTROL_CODE(request,method) \
  CTL_CODE (FILE_DEVICE_UNKNOWN, request, method, FILE_ANY_ACCESS)
 
#define TAP_IOCTL_SET_MEDIA_STATUS \
  TAP_CONTROL_CODE (6, METHOD_BUFFERED)
 
#define BUFMAX 2048
 
static HANDLE hTap = NULL;
static HANDLE hEvent = NULL;
 
VOID dump(UCHAR *, int);
BOOL ExitHandler(DWORD);
CHAR *GetNetWorkDeviceGuid(CONST CHAR *, CHAR *, DWORD);
 
int main(int ac, char *av[])
{
  DWORD dwLen;
  ULONG status = TRUE;
  OVERLAPPED ovl;
  UCHAR Buf[BUFMAX];
  CHAR szDevicePath[256];
 
  if (ac < 2) {
    printf("Usage: %s <display name of TAP device>\n", av[0]);
    return 0;
  }
 
  // 指定された表示名から TAP の GUID を得る
  if (!GetNetWorkDeviceGuid(av[1], Buf, BUFMAX)) {
    printf("TAP-Win32: [%s] GUID is not found\n", av[1]);
    return -1;
  }
  printf("TAP-Win32: [%s] GUID = %s\n", av[1], Buf);
  sprintf(szDevicePath, DEVICE_PATH_FMT, Buf);
 
  // TAP デバイスを開く
  hTap = CreateFile (szDevicePath, GENERIC_READ | GENERIC_WRITE,
      0, 0, OPEN_EXISTING,
      FILE_ATTRIBUTE_SYSTEM | FILE_FLAG_OVERLAPPED, 0);
 
  if (hTap == INVALID_HANDLE_VALUE) {
    printf("TAP-Win32: Failed to open [%s]", szDevicePath);
    return -1;
  }
 
  memset(&ovl, 0, sizeof(OVERLAPPED));
  ovl.hEvent = hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
  ovl.Offset = 0;
  ovl.OffsetHigh = 0;
 
  // Ctrl+C ハンドラを設定
  SetConsoleCtrlHandler((PHANDLER_ROUTINE)ExitHandler, TRUE);
 
  // TAP デバイスをアクティブに
  status = TRUE;
  if (!DeviceIoControl(hTap,TAP_IOCTL_SET_MEDIA_STATUS,
        &status, sizeof(status), &status, sizeof(status),
        &dwLen, NULL)) {
    printf("TAP-Win32: TAP_IOCTL_SET_MEDIA_STATUS err\n");
    CloseHandle(hTap);
    return -1;
  }
 
  printf("[Press CTRL+C to exit]\n");
 
  // パケット read ループ
  while (1) {
    if (!ReadFile(hTap, Buf, sizeof(Buf), &dwLen, &ovl)) {
      DWORD err = GetLastError();
      if (err == ERROR_IO_PENDING) {
        WaitForSingleObject(hEvent, INFINITE); // 受信完了待ち
        GetOverlappedResult(hTap, &ovl, &dwLen, FALSE);
        dump(Buf, dwLen);
      } else {
        printf("TAP-Win32: ReadFile err=0x%08X\n", err);
        CloseHandle(hTap);
        return -1;
      }
    } else {
      dump(Buf, dwLen);
    }
  }
  return 0;
}
 
 
//-------------------------------------------------
 
// パケットダンプ
void dump(UCHAR *buffer, int len)
{
  UCHAR buf[80] = "\0";
  int i, cnt = 0;
  printf("<< dump: %d bytes >>\n", len);
  for (i = 0; i < len; i++) {
    sprintf(&buf[cnt], "%02X ", buffer[i]);
    cnt += 3;
    if ((i+1) % 8 == 0) {
      strcat(buf, " ");
      cnt++;
    }
    if ((i+1) % 16 == 0) {
      printf("%s\n", buf);
      cnt = 0;
      buf[0] = '\0';
    }
  }
  if (buf[0] != '\0') {
    printf("%s\n", buf);
  }
}
 
// 終了時ハンドラ
BOOL ExitHandler(DWORD dwCtrlType)
{
  if (dwCtrlType == CTRL_C_EVENT) { // CTRL+C 押下
    if (hTap != NULL) {
      CloseHandle(hTap);
      CloseHandle(hEvent);
      printf("TAP-Win32 is closed\n");
    }
    ExitProcess(0);
  }
  return FALSE;
}
 
// ネットワークデバイス表示名からデバイス GUID 文字列を検索
CHAR *GetNetWorkDeviceGuid(CONST CHAR *pDisplayName, 
                           CHAR *pszBuf, DWORD cbBuf)
{
  #define BUFSZ 256
  CONST CHAR *SUBKEY =
    "SYSTEM\\CurrentControlSet\\Control\\Network";
 
  // HKLM\SYSTEM\\CurrentControlSet\\Control\\Network\{id1]\{id2}\Connection\Name が
  // ネットワークデバイス名(ユニーク)の格納されたエントリであり、
  // {id2} がこのデバイスの GUID である
 
  HKEY hKey1, hKey2, hKey3;
  LONG nResult;
  DWORD dwIdx1, dwIdx2;
  CHAR szData[64], *pKeyName1, *pKeyName2, *pKeyName3, *pKeyName4; 
  DWORD dwSize, dwType = REG_SZ;
  BOOL bDone = FALSE;
  FILETIME ft;
 
  hKey1 = hKey2 = hKey3 = NULL;
  pKeyName1 = pKeyName2 = pKeyName3 = pKeyName4 = NULL;
 
  // 主キーのオープン
  nResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
                         SUBKEY, 0, KEY_ALL_ACCESS, &hKey1);
  if (nResult != ERROR_SUCCESS) {
    printf("GetNetWorkDeviceGuid: open key err HKLM%s\n", SUBKEY);
    return NULL;
  }
  pKeyName1 = (CHAR*)malloc(BUFSZ);
  pKeyName2 = (CHAR*)malloc(BUFSZ);
  pKeyName3 = (CHAR*)malloc(BUFSZ);
  pKeyName4 = (CHAR*)malloc(BUFSZ);
 
  dwIdx1 = 0;
  while (bDone != TRUE) { // {id1} を列挙するループ
 
    dwSize = BUFSZ;
    nResult = RegEnumKeyEx(hKey1, dwIdx1++, pKeyName1,
                          &dwSize, NULL, NULL, NULL, &ft);
    if (nResult == ERROR_NO_MORE_ITEMS) {
      break;
    }
 
    // SUBKEY\{id1} キーをオープン
    sprintf(pKeyName2, "%s\\%s", SUBKEY, pKeyName1);
    nResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, pKeyName2,
                          0, KEY_ALL_ACCESS, &hKey2);
    if (nResult != ERROR_SUCCESS) {
      continue;
    }
    dwIdx2 = 0;
    while (1) { // {id2} を列挙するループ
      dwSize = BUFSZ;
      nResult = RegEnumKeyEx(hKey2, dwIdx2++, pKeyName3,
                          &dwSize, NULL, NULL, NULL, &ft);
      if (nResult == ERROR_NO_MORE_ITEMS) {
        break;
      }
 
      if (nResult != ERROR_SUCCESS) {
        continue;
      }
 
      // SUBKEY\{id1}\{id2]\Connection キーをオープン
      sprintf(pKeyName4, "%s\\%s\\%s",
                      pKeyName2, pKeyName3, "Connection");
      nResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
                      pKeyName4, 0, KEY_ALL_ACCESS, &hKey3);
      if (nResult != ERROR_SUCCESS) {
        continue;
      }
 
      // SUBKEY\{id1}\{id2]\Connection\Name 値を取得
      dwSize = sizeof(szData);
      nResult = RegQueryValueEx(hKey3, "Name",
                      0, &dwType, (LPBYTE)szData, &dwSize);
 
      if (nResult == ERROR_SUCCESS) {
        if (stricmp(szData, pDisplayName) == 0) {
          strcpy(pszBuf, pKeyName3);
          bDone = TRUE;
          break;
        }
      }
      RegCloseKey(hKey3);
      hKey3 = NULL;
    }
    RegCloseKey(hKey2);
    hKey2 = NULL;
  }
 
  if (hKey1) { RegCloseKey(hKey1); }
  if (hKey2) { RegCloseKey(hKey2); }
  if (hKey3) { RegCloseKey(hKey3); }
 
  if (pKeyName1) { free(pKeyName1); }
  if (pKeyName2) { free(pKeyName2); }
  if (pKeyName3) { free(pKeyName3); }
  if (pKeyName4) { free(pKeyName4); }
 
  // GUID を発見できず
  if (bDone != TRUE) {
    return NULL;
  }
  return pszBuf;
}

次回は、今回の内容をふくらませて「実在するホストのフリをして ping に応答する仮想ホストプログラム」というものを考えてみます。

klab_gijutsu2 at 08:42│Comments(0)TrackBack(0)win 

トラックバックURL

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント
最新トラックバック
Archives