ググっていると、OpenVPNに付属している仮想ネットワークドライバ「TAP-Windows」というものを見つけました。本来はVPNで使用するための仮想ネットワークドライバのようですが、Win32APIを介してパケットのやり取りができるようです。
というわけで、この.NETラッパーを作ってC#等から使えるようにしてみました。
1. TAP-Windowsのインストール
まずはTAP-Windowsをインストールしなければ始まりません。Download - OpenVPN.netこの中からOpenVPNのWindows用インストーラをダウンロードしインストールをします。OpenVPN本体自体は不要ですので、インストールにて他のチェックボックスは極力外しておきましょう。
インストールが終わったらネットワークと共有センターを開いてちゃんとインストールできているか確認しておきましょう。名前をわかりやすいものに変更しておくといいかもしれません。
余談ですが、Wiresharkがパケットキャプチャで使っているWinPcapサービスは起動時に存在するネットワークアダプタをスキャンしているようです。なので、WinPcapからTAP-Windowsを見るためには一旦パソコンを再起動する必要があります。
2. ラッパーを作る
TAP-WindowsにアクセスするにはWin32APIのCreateFileやReadFile、WriteFile関数などを呼び出します。概ねファイルの読み書きと同様にできると言っていいでしょう。C#からP/Invokeで頑張るのもありかもしれませんが、だんだんわけがわからなくなってきたり、結局定数の中身を除くために延々とwindows.hを辿ったり、unsafeを強要されそうになってしまったり…というお約束のルートが見えています。ですので、プログラミング界の黒魔術(※)と呼ばれているC++/CLIでラッパーを作ります。あまり深入りはし過ぎないように、ほどほどの作りこみにしておきましょう。
※私が勝手に呼んでいるだけです。
コンストラクタ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /// <summary> /// コンストラクタ /// </summary> /// <param name="guid">TAPデバイスのGUID</param> TapDotNet::Tap::Tap(String ^ guid) { String^ path = "\\\\.\\Global\\" + guid + ".tap" ; pin_ptr< const wchar_t > pszPath = PtrToStringChars(path); ULONG status = TRUE; DWORD dwLen; //TAPデバイスを開く hTap = CreateFileW(pszPath, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, 0); if (hTap == INVALID_HANDLE_VALUE) throw gcnew InvalidOperationException( "Cannot open the device" ); //TAPデバイスをアクティブに status = TRUE; if (!DeviceIoControl(hTap, CTL_CODE(FILE_DEVICE_UNKNOWN, 6, METHOD_BUFFERED, FILE_ANY_ACCESS), &status, sizeof (status), &status, sizeof (status), &dwLen, NULL)) { CloseHandle(hTap); hTap = NULL; throw gcnew InvalidOperationException( "Cannot activate device" ); } } |
CreateFileのファイル名を指定するパラメーターには"\\.\Global\{GUID}.tap"を渡します。GUIDの取得方法はまた後程。その他のパラメーターはなんとなくわかると思います。
デバイスを開いたら次はDeviceIoControl関数を呼び出してデバイスをアクティブにします。この時点でWindowsはネットワークケーブルが差し込まれたと認識します。
パケット送信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /// <summary> /// データを送信するメソッド /// </summary> /// <param name="data">パケット</param> void TapDotNet::Tap::SendData(array<Byte>^ data) { if (data == nullptr) throw gcnew ArgumentNullException( "data" ); if (hTap == NULL) throw gcnew ObjectDisposedException( "Tap" ); pin_ptr<Byte> pinned = &data[0]; DWORD dwWritten; if (!WriteFile(hTap, pinned, data->Length * sizeof (Byte), &dwWritten, NULL)) throw gcnew InvalidOperationException( "WriteFile Error: " + GetLastError().ToString()); } |
パケット受信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | void TapDotNet::Tap::StartWatching() { if (!ReceiveWatching) { WatcherThread = gcnew Thread(gcnew ThreadStart( this , &TapDotNet::Tap::WatchReceive)); WatcherThread->Start(); } } void TapDotNet::Tap::StopWatching() { if (ReceiveWatching) { WatcherThread->Abort(); WatcherThread->Join(); WatcherThread = nullptr; } } void TapDotNet::Tap::WatchReceive() { try { UCHAR Buf[1518] = { 0 }; DWORD dwLen = 0; while ( true ) { if (ReadFile(hTap, Buf, sizeof (Buf), &dwLen, NULL)) if (Filter(Buf, dwLen)) RaiseReceiveEvent(Buf, dwLen); else { DWORD lasterr = GetLastError(); if (lasterr != 0) { RaiseErrorEvent( "ReadFile Error" , lasterr); break ; } } } } catch (ThreadAbortException^) { } } |
えっ、今の時代Thread直作りは流行らないって?C++/CLI自体が流行らないので許してください…。
Filter関数は受信したパケットにフィルターを掛けるものです。全てのパケットをイベントでRaiseしていてはイベントの嵐になってしまいますので、そういうのはできるだけ根っこで止めるということで、受信直後にフィルターを掛ける機能を実装しました。
ただ、ここで受信するのはイーサネットフレームで、そのヘッダで得られる情報は発信元と宛先のMACアドレスと、IPv4などのネットワーク層水準のプロトコルの種類くらいです。
トランスポート層のプロトコル種類(TCPとかUDPとか)やIPアドレスなどを判別する機能も付けたいところではありますが、じゃあ同じようにAppleTalkのフィルターも作るのかと言ったらそんなの作る気にもなれず、ひとまずそのレベルのフィルターでとどめておくことにしました。
C++/CLIですからね。深入りは厳禁です。
デストラクタ/ファイナライザ
1 2 3 4 5 6 7 8 9 10 11 12 13 | TapDotNet::Tap::~Tap() { StopWatching(); this ->!Tap(); } TapDotNet::Tap::!Tap() { if (hTap != NULL) { CloseHandle(hTap); hTap = NULL; } } |
マネージドリソース(今回はThread)はデストラクタ、アンマネージドリソース(CreateFileのハンドル)はファイナライザにて解放処理を行います。
3. 実際に使う
やっとC++/CLIの呪縛から逃れました。前述のTAP-WindowsのGUIDを求める方法は超簡単です。NetworkInterfaceクラスのIdプロパティがそれに相当します。ですのでこのようなプログラムでどうでしょうか。
1 2 3 4 5 6 7 8 9 | var tapnic = NetworkInterface.GetAllNetworkInterfaces().SingleOrDefault(p => p.Description == "TAP-Windows Adapter V9" ); using (var tap = new Tap(tapnic.Id)) { tap.DataReceived += Tap_DataReceived; tap.ReceiveWatching = true ; Task.Delay(TimeSpan.FromSeconds(10)).Wait(); tap.ReceiveWatching = false ; tap.DataReceived -= Tap_DataReceived; } |
また、これはコンソールアプリケーションで作ったので、Task.Delay().Wait();ができますが、GUIアプリケーションに移植する際は気を付けてください。あくまでもTaskはasync/awaitすべきです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | private static void Tap_DataReceived( object sender, DataReceivedEventArgs e) { Console.WriteLine($ "======== {e.Data.Length} bytes ========" ); const int linecnt = 16; const int separatedcnt = 8; foreach (var (list, index) in e.Data.Buffer(linecnt).Select((list, index) => (list, index))) { var line = new StringBuilder(); line.Append($ "{index * linecnt:X4} " ); line.Append( string .Join( " " , list .Select(p => p.ToString( "X2" )) .Concat(Enumerable.Repeat( " " , linecnt - list.Count)) .Buffer(separatedcnt) .Select(p => string .Join( " " , p)))); line.Append( " " ); line.Append( string .Join( " " , list .Select(p => ( char )p) .Select(p=> char .IsControl(p) ? '.' : p) .Concat(Enumerable.Repeat( ' ' , linecnt - list.Count)) .Buffer(separatedcnt) .Select(p => new string (p.ToArray())))); Console.WriteLine(line); } Console.WriteLine(); } |
ValueTupleとIxのBuffer()を使っているので、相応のC#バージョンと参照が必要です。
送信は超簡単です。
1 2 3 | using (var tap = new Tap(tapnic.Id)) { tap.SendData(ping); } |
Tap.NETから送ったpingパケットをWiresharkで無事拾えました。
4. ダウンロード
例によってNugetに置いておきました。現時点ではひとまずプレリリース扱いにしております。Tap.NET 0.1.0-beta1
0 件のコメント:
コメントを投稿