ググっていると、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 件のコメント:
コメントを投稿