ググっていると、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でラッパーを作ります。あまり深入りはし過ぎないように、ほどほどの作りこみにしておきましょう。
※私が勝手に呼んでいるだけです。
コンストラクタ
/// <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はネットワークケーブルが差し込まれたと認識します。
パケット送信
/// <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()); }
パケット受信
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ですからね。深入りは厳禁です。
デストラクタ/ファイナライザ
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プロパティがそれに相当します。ですのでこのようなプログラムでどうでしょうか。
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すべきです。
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#バージョンと参照が必要です。
送信は超簡単です。
using(var tap = new Tap(tapnic.Id)) { tap.SendData(ping); }
Tap.NETから送ったpingパケットをWiresharkで無事拾えました。
4. ダウンロード
例によってNugetに置いておきました。現時点ではひとまずプレリリース扱いにしております。Tap.NET 0.1.0-beta1
0 件のコメント:
コメントを投稿