WinPcapExtension ー ActionScriptからWinPcapを使えるようにする


■■ WinPcapExtension

 ActionScriptというかAdobe AIRからWinPcapが使いたかったので、WinPcapのActionScript Native Extensionを書きました。
  WinPcapExtension: https://github.com/ryujimiya/WinPcapExtension/

■■ 使用方法

1. ネットワークインターフェースの取得

 WinPcapExtensionでキャプチャーするネットワークアダプタを選択します。

1.1. 初期化
 WinPcapExtensionのインスタンスを new WinPcapExtension()で生成しています。

import flash.utils.ByteArray;
import livefan.packet.PacketUtil;
import livefan.winpcap.PcapDefine;
import livefan.winpcap.PcapHandle;
import livefan.winpcap.PcapIf;
import livefan.winpcap.PcapPktHdr;
import livefan.winpcap.ResultAllDevs;
import livefan.winpcap.ResultGetArrival;
import livefan.winpcap.ResultPcap;
import livefan.winpcap.Timeval;
import livefan.winpcap.WinPcapExtension;
import livefan.winpcap.WinPcapExtensionEvent;


public class NetworkSettingsWin extends NetworkSettingsWinView 
{
    /**
     * WinPcapネイティブ拡張インスタンス
     */
    private var _extension:WinPcapExtension = null;
    /**
     * ネットワークインターフェースインスタンス
     */
    private var _pcapIf:PcapIf = null;

    /**
     * コンストラクタ
     */
    public function NetworkSettingsWin()
    {
        super();
        addEventListener(AIREvent.WINDOW_COMPLETE, initWin);
        addEventListener(Event.CLOSING, closing);
    }

    /**
     * ウィンドウが初期化された
     * @param e
     */
    private function initWin(e:AIREvent):void 
    {
        // WinPcap拡張インスタンスの生成
        _extension = new WinPcapExtension();

        listNetworkIf.addEventListener(IndexChangeEvent.CHANGE, listNetworkIfIndexChange);
        setupNetworkIfList();
    }
}

1.2. ネットワークインターフェースの列挙
 ネットワークインターフェースを取得するにはextension.pcapFindAllDevsEx(source, null);を使います。ここでsourceはPcapDefine.PCAP_SRC_IF_STRINGを指定します。
PCAP_SRC_IF_STRINGは "rpcap://"と定義されています。
pcapFindAllDevsExの戻り値はResultAllDevsで、そのpcapIfsというプロパティでネットワークインターフェースのリスト(の先頭)を取り出せます。あとはpcapIf = pcapIfsとし、pcapIf->nextで列挙していけば一覧を取得できます。

    /**
     * ネットワークインターフェース一覧をセットアップ
     */
    private function setupNetworkIfList():void
    {
        /*
        if (_pcapHandle != null)
        {
            return;
        }
        */
        var source:String = PcapDefine.PCAP_SRC_IF_STRING;

        // ネットワークインターフェースの一覧を取得
        var resAllDevs:ResultAllDevs = _extension.pcapFindAllDevsEx(source, null);
        if (resAllDevs.retVal != 0)
        {
            Alert.show(resAllDevs.errBuf, "", Alert.OK, this);
            return;
        }

        // ネットワークインターフェースをリストに登録
        var dataProvider:ArrayCollection = new ArrayCollection();
        var pcapIfs:PcapIf = resAllDevs.pcapIfs;
        for (var pcapIf:PcapIf = pcapIfs; pcapIf != null; pcapIf = pcapIf.next)
        {
            var name:String = pcapIf.name;
            var description:String = pcapIf.description;
            var addresses:PcapAddr = pcapIf.addresses;
            var labelStr:String = description;
            dataProvider.addItem( { label: labelStr , data:pcapIf } );
        }
        listNetworkIf.dataProvider = dataProvider;

        // インターフェース一覧を解放
        resAllDevs.dispose();
    }

2. パケットキャプチャー

 では実際にパケットをキャプチャーする手順です。
キャプチャーの方法はWinPcapでいうpcap_loopを使う方法とpcap_next_exを使う方法の2種類あり、WinPcapExtensionはどちらにも対応しています。ここではpcap_loopを使う方法について説明します。

2.1. 初期化
 WinPcapExtensionインスタンスを生成するところはネットワークインターフェース一覧取得の時と同じです。同じクラスでやるならネットワーク一覧取得のWinPcapExtensionインスタンスをそのまま使えばいいと思います。
次にイベントハンドラを設定します。
  WinPcapExtensionEvent.CAPTURETHREAD_PACKETARRIVAL
  WinPcapExtensionEvent.CAPTURETHREAD_THREADFUNCFINISHED
これらは、pcap_loop使用時に必要なハンドラで、pcap_next_exを利用した場合は必要ありません。
また、networkNameは前項で取得したpcapIf.nameを指定します。

public class CaptureThreadDemoWin extends CaptureThreadDemoWinView
{
    private var _extension:WinPcapExtension = null;
    private var _pcapHandle:PcapHandle = null;
    private var _pcapIf:PcapIf = null;
    private var _networkName:String = "";

    public function CaptureThreadDemoWin(networkName:String)
    {
        super();
        addEventListener(AIREvent.WINDOW_COMPLETE, initWin);
        addEventListener(Event.CLOSING, closing);
        _networkName = networkName;
    }

    private function initWin(e:AIREvent):void 
    {
        // WinPcap拡張インスタンスの生成
        _extension = new WinPcapExtension();
        _extension.addEventListener(WinPcapExtensionEvent.CAPTURETHREAD_PACKETARRIVAL, onCaptureThreadPacketArrival);
        _extension.addEventListener(WinPcapExtensionEvent.CAPTURETHREAD_THREADFUNCFINISHED, onCaptureThreadFinished);

        textAreaDump.editable = false;

        _pcapIf = getNetworkAdapter();
        startCapture(_pcapIf.name);
    }


    private function onCaptureThreadPacketArrival(e:WinPcapExtensionEvent):void 
    {

        // 実装する

    }



    private function onCaptureThreadFinished(e:WinPcapExtensionEvent):void
    {

        // 実装する
    }
}

2.2. キャプチャーの開始、停止
 キャプチャーを開始するにはまず_extension.pcapOpenでpcapHandleを取得する必要があります。
 つぎに_pcapHandle.startCaptureThread()でキャプチャースレッドを起動します。そうすると、先ほど設定したイベントハンドラonCaptureThreadPacketArrivalにパケット到達時に通知されます。
 キャプチャーを終了するには_pcapHandle.stopCaptureThread()を使用します。また、再開の必要がなければ、pcalHandleもクローズします。

    private function startCapture(adapterName:String):Boolean
    {
        var resPcap:ResultPcap = _extension.pcapOpen(adapterName, 65535, PcapDefine.PCAP_OPENFLAG_PROMISCUOUS, 20, null);
        if (resPcap.retVal != 0)
        {
            Alert.show(resPcap.errBuf, "", Alert.OK, this);
            return false;
        }
        this._pcapHandle = resPcap.pcapHandle;

        this._pcapHandle.startCaptureThread(); // CaptureThreadを起動する
        return true;
    }

    private function stopCapture():void
    {
        if (_pcapHandle == null)
        {
            return;
        }
        _pcapHandle.stopCaptureThread(); // CaptureThreadを終了する
        _pcapHandle.pcapClose();
        _pcapHandle.dispose();
        _pcapHandle = null;
    }
}

2.3. フィルタリング
 キャプチャーするパケットにフィルタリングをかけることもできます。
次のようにpcapSetFilterを使います。この設定はstartCaptureThreadを呼び出す前に行います。

        this._pcapHandle.pcapSetFilter("src port >= 80 and src port <= 99", 1, 0xffffff);

3. パケットの取得

 onCaptureThreadPacketArrivalに実装します。
 理想はこのイベントが発生したとき到達パケット数は1であってほしかったのですが、パケット到達数が多いとそうもいかないようなので、まずパケット数を_pcapHandle.getArrivalPacketCount()で取得します。
あとはこのパケット数分を _pcapHandle.getArrivalPacket()を使ってパケットを取得すればいいです。

    private function onCaptureThreadPacketArrival(e:WinPcapExtensionEvent):void 
    {
        if (_pcapHandle == null)
        {
            return;
        }

        var packetCnt:int = _pcapHandle.getArrivalPacketCount();
        //trace("packet = " + packetCnt);
        for (var i:int = 0; i < packetCnt; i++)
        {
            // 到達パケットを取得する
            var resGetArrival:ResultGetArrival = _pcapHandle.getArrivalPacket();
            var ret:int = resGetArrival.retVal;
            var header:PcapPktHdr = resGetArrival.header as PcapPktHdr;
            var data:ByteArray = resGetArrival.data as ByteArray;
            resGetArrival.dispose(); // 結果を破棄する (取得したheaderはnativeで使用不可となる)
            if (ret != 1)
            {
                continue;
            }
            //trace(PacketUtil.hexDump(data));

            // 表示
            var newlineStr:String = "\r\n";
            var text:String = "";
            if (header != null)
            {
                var ts:Timeval = header.ts;
                var date:Date = new Date(ts.tvSec * 1000);
                text += date.toLocaleString();
                text += " capLen:" + header.capLen.toString() + newlineStr;
            }
            text += PacketUtil.hexDump(data);
            textAreaDump.text = text;
        }
    }

4. パケットの解析

 パケットの解析にはPacketLibを使います。PacketLibはActionScriptのみで書いたパケット解析ライブラリです。
 PacketLib: https://github.com/ryujimiya/PacketLib

 次の関数parsePacketの引数dataには、data:ByteArray = resGetArrival.data as ByteArrayで取得したdataがセットされると思ってください。
 Packet.parsePacketでパケットの生データをパケットオブジェクトに変換します。
以下の例ではlastPacket.payloadByteAry.lengthとペイロード長を取得するためにしか使っていませんが、実際パケット解析するときは、lastPacket.payloadByteAry自体が重要で、パケットのペイロードをByteArrayで取得できます。

    private function parsePacket(data:ByteArray):String 
    {
        var retStr:String = "";
        //var newlineStr:String = "\r\n";

        // パケットデータをパースする
        var dlt:int = _pcapHandle.pcapDataLink();
        var packet:Packet = Packet.parsePacket(dlt, data);
        if (packet == null)
        {
            return retStr;
        }
        // 一番内側のパケットを取得する
        var lastPacket:Packet = packet.getLastPacket();
        // TcpPacketを指定して取得する
        //var lastPacket:Packet = packet.extractPacket(TcpPacket);

        // パケット情報を取得する
        retStr += getQualifiedClassName(lastPacket) + " ";
        if (lastPacket is TcpPacket || lastPacket is UdpPacket)
        {
            // TcpPacketかUdpPacketの場合

            var srcAddrAsString:String = "";
            var dstAddrAsString:String = "";
            var srcPort:uint = 0;
            var dstPort:uint = 0;
            var ipPacket:IpPacket = lastPacket.parentPacket as IpPacket;
            if (ipPacket != null)
            {
                if (ipPacket is Ipv4Packet)
                {
                    var ipv4Packet:Ipv4Packet = ipPacket as Ipv4Packet;
                    srcAddrAsString = _extension.IpAddrByteArrayToString(SockAddr.AF_INET, ipv4Packet.srcAddress);
                    dstAddrAsString = _extension.IpAddrByteArrayToString(SockAddr.AF_INET, ipv4Packet.dstAddress);
                }
                else if (ipPacket is Ipv6Packet)
                {
                    var ipv6Packet:Ipv6Packet = ipPacket as Ipv6Packet;
                    srcAddrAsString = "[" + _extension.IpAddrByteArrayToString(SockAddr.AF_INET6, ipv6Packet.srcAddress) + "]";
                    dstAddrAsString = "[" + _extension.IpAddrByteArrayToString(SockAddr.AF_INET6, ipv6Packet.dstAddress) + "]";
                }
                else
                {
                    // invalid
                }
            }

            if (lastPacket is TcpPacket)
            {
                var tcpPacket:TcpPacket = lastPacket as TcpPacket;
                srcPort = tcpPacket.srcPort;
                dstPort = tcpPacket.dstPort;
            }
            else if (lastPacket is UdpPacket)
            {
                var udpPacket:UdpPacket = lastPacket as UdpPacket;
                srcPort = udpPacket.srcPort;
                dstPort = udpPacket.dstPort;
            }
            retStr += 
                srcAddrAsString
                + ":"
                + srcPort.toString()
                + "->"
                + dstAddrAsString
                + ":"
                + dstPort.toString()
                + " payload length = " + lastPacket.payloadByteAry.length.toString();
        }
        else
        {
            // その他
        }
        return retStr;
    }

■■サンプルソースコード