SwiftとLibpcapによるパケットモニターの作成
1 はじめに
今まで、Windows上でWinPcapというライブラリを使用して、パケットを扱うプログラムをよく書いていました。 最近、Macで作業することが多くなり、それじゃということで Swiftでも書いてみようと思ったのですが・・・
残念ながら、Swift で libpcap を扱うまとまった資料は、なかなか見つかりませんでした。そこで、今回作成したパケットモニター(簡易版)の作業手順をここに紹介させて頂くことにしました。
2 パケットモニター(簡易版)
下記は、今回作成したパケットモニター(簡易版)実行状況です。(画像をクリックしてください)
最初に、モニターの対象とするネットワークインターフェース(NIC)を選択してモニターが開始されます。 簡易的に、Ether、IPv4、IPv6、TCP、UDP、ARP、ICMPのヘッダだけをデコードしてみました。 途中で、pingコマンドを打つと、ICMPのエコー・リプライがモニターできているのを見ることできます。
また、コードは、githubに置きましたので、説明不足なところは、こちらをご参照ください。
サンプルコード(https://github.com/furuya02/sniffer-sample)
3 環境構築
Swiftでlibpcapを使用したプログラムを作成するためには、概ね次の準備が必要です。
- libpcapライブラリの追加
- ブリッジファイルの追加
- rootでのデバッグ
以下、それぞれについて解説します。
(1) libpcapライブラリの追加
まずは、新しいプロジェクトの作成を行います。 「OS X」の「Application」から「Command Line Tool」を選択し、なお、プロジェクト名は「sniffer-sample」としました。
続いて、libpcapライブラリをプロジェクトに追加します。
「TARGET」で対象プロジェクトを選択し、「Build Phases」タブを開いて「Link Binary With Libraries」セクションで「+」をクリックします。
表示されたダイアログで、「libpcap」と入力して検索し、「libpcap.tbd」を「Add」ボタンで追加します。
(2) ブリッジファイルの追加
Swiftからlibpcapライプラリを参照するためには、ブリッジとなるヘッダファイルが必要です。 SwiftのプロジェクトにObjective-Cのソースファイルを追加すると、自動的に「プロジェクト名-Bridging-Header.h」と言う名前で、ブリッジファイルを登録させることができるので、今回は、その機能を使用しました。
まずは、プロジェクトに「Objective-C File」を追加します。
名前は、「dmy」(何でも良い)としました。
「Next」のボタンを押すと、次のような確認が表示されますので、「Create Bridging Header」を選択します。
追加が完了すると、プロジェクトに「dmy.m」と「sniffer-sample-Bridging-Header.h」が追加されていることが確認できます。
これでブリッジファイルの追加は完了です。「dmy.m」は、削除してしまって構いません。
「Build Settings」の「Objective-C Bridging Header」に先ほどのヘッダファイルが登録されているのが確認できます。
この後、libpcapライブラリを使用する際に必要となるヘッダ情報は、この「sniffer-sample-Bridging-Header.h」に記述すればよいことになります。
下記のコードは、いくつかのヘッダを追加しものです。libpcapのヘッダファイルと、後々使用することになるネットワーク関連のいくつかのヘッダを追加しました。
// 必要なヘッダをここに追加する #import <pcap.h> #import <net/ethernet.h> #import <netinet/in.h> #import <arpa/inet.h>
(3) rootでのデバッグ
ネットワークインターフェースをプロミスキャスモードに変更するためには、root権限が必要です。(プロミスキャスで利用する場合のみ) デバッグ中にこの権限を付与するために、設定が必要です。
メニューから「Product」ー「Scheme」ー「Edit Scheme」を選択
開いたダイアログで「Run」「info」「Debug Process As」で「Me」を「root」に変更する
※定期的にパスワードを要求される
これで、libpcapを使用したプログラム作成の準備は完了です。
4 RawDataクラス
Swiftでは、型の違うものどうしの代入はできません。
しかし、パケットを扱うプログラムでは、あるデータ列の特定のオフセットから違った型(構造体)のポインタをあててデータを解釈するといった作業が必要になります。
このような動作は、Swiftでも記述可能ですが、後々コードの見通しが良くなるように、この辺の機能をクラスとして定義してみました。
RawDataクラスは、取得したパケットを丸ごと与えて初期化し、内部にNSDataとして保持します。 そして、特定のオフセットと型を指定して、そのポインタやデータ(実体)を受け取れるようにした[ memory<T>(offset)、ptr<T>(offset) ]と、特定のオフセットから指定したバイト数だけUnsafePointer<Int8>で受け取る[ ptr(offset,length) ]を用意しました。
また、静的メソッドとして初期化せず型変換だけを行う[ direct<T>() ]も定義しています。
class RawData:NSObject{ var _data:NSData // ポインタと長さで初期化 // Unsafepointer<Void>で指定された領域のデータをNSDataで保持する init(_ data:UnsafePointer<Void>, length:Int ) { self._data = NSData(bytes: data, length: length) } // 型を指定してデータ(実体)を取得 func memory<T>(offset: Int) -> T { //指定されたoffset以降のポインタを得る let p = self._data.subdataWithRange(NSMakeRange(offset, sizeof(T))).bytes // 指定された型にキャストする return UnsafePointer<T>(p).memory } // 型を指定してポインタを取得 func ptr<T>(offset: Int) -> UnsafePointer<T> { //指定されたoffset以降のポインタを得る let p = self._data.subdataWithRange(NSMakeRange(offset, sizeof(T))).bytes // 指定された型にキャストする return UnsafePointer<T>(p) } // サイズを指定してUnsafePointer<UInt8>を取得する func ptr(offset: Int, length:Int) -> UnsafePointer<UInt8> { //指定されたoffset以降のポインタを得る let p = self._data.subdataWithRange(NSMakeRange(offset, length)).bytes // UInt8へのポインタにキャストする return UnsafePointer<UInt8>(p) } // 型を指定して直接キャストする(クラスメソッド) class func direct<T>(data:UnsafePointer<Void>) -> T{ let d = NSData(bytes: data, length: sizeof(T)) return UnsafePointer<T>(d.bytes).memory } }
プログラムでは、libpcapで受信したパケットデータをそのまま、このクラスで飲み込んで、後の処理(各ヘッダの解釈)を進めています。
5 ネットワークインターフェースの一覧取得
libpcapでパケットを採取するためには、対象とするネットワークインターフェースの名前を指定する必要があります。 libpcapでは、pcap_findalldevs()という関数でインターフェースの列挙が可能です。(getifaddrs()でも列挙可能です) pcap_findalldevs()には、pcap_if構造体へのポインタ配列の先頭アドレスを引数に取りま すが、このpcap_if構造体は、次のようになっています。
struct pcap_if * next // 次のpcap_ifへのポインタ(NULLで終了) char * name // インターフェース名 char * description // 詳細情報ですが、MACでは取れません struct pcap_addr * addresses // アドレス u_int flags // フラグ
パケットキャプチャーで必要なのは、name(インターフェース名)だけなのですが、選択のための情報として、通常IPアドレスを確認します。
ここでのアドレス情報である、pcap_addr は、複数のpcap_addr構造体配列へのポインタとなっています。
pcap_addr * next // 次のpcap_addrへのポインタ(NULLで終了) sockaddr * addr // IPアドレス sockaddr * netmask // ネットマスク sockaddr * broadaddr // ブロードキャストアドレス sockaddr * dstaddr // ポイントツーポイントのインターフェースの場合、宛先アドレス
なお、sockaddrは、AF_INET(IPv4)の場合sockaddr_in、AF_INET6(IPv6)の場合sockaddr_in6に、それぞれキャストすることでIPアドレスを読み出すことができます。
ここで、先ほど紹介したRawDataクラスが活躍するわけです。
以下のコードは、pcap_findalldevsを使用して、インターフェースの名前とIPアドレスを列挙して表示しているものです。
/////////////////////////////////////////////////// // デバイス列挙 /////////////////////////////////////////////////// var errbuf = [Int8](count:Int(PCAP_ERRBUF_SIZE),repeatedValue:0) // エラー情報を受け取るためのバッファ var allDevs = UnsafeMutablePointer<pcap_if>()//.alloc(1) // デバイス情報を受け取るためのポインタ var buf = [Int8](count:128,repeatedValue:0) // IPアドレスを文字列化するためのバッファ var devs = Array<String>() // 名前の一覧を作成する var no = 0 // 選択番号 if ( pcap_findalldevs(&allDevs,UnsafeMutablePointer<CChar>(errbuf)) != -1 ){ for(var dev = allDevs.memory ;; dev = dev.next.memory){ // デバイス名の表示 var name = String.fromCString(dev.name)! devs.append(name) // 選択番号とデバイス名を表示する print("[\(no++). \(name)]") // 選択を補助するための、設定されているIPアドレスを表示する for(var p = dev.addresses ;; p = p.memory.next){ let famiry = Int32(p.memory.addr.memory.sa_family) if(famiry == AF_INET){ //IPv4 var sockaddrIn = RawData.direct(p.memory.addr) as sockaddr_in inet_ntop(AF_INET,&sockaddrIn.sin_addr,&buf,128) print(" " + String.fromCString(buf)!) }else if(famiry == AF_INET6){//IPv6 var sockaddrIn6 = RawData.direct(p.memory.addr) as sockaddr_in6 inet_ntop(AF_INET6,&sockaddrIn6.sin6_addr,&buf,128) print(" " + String.fromCString(buf)!) }else{ //Other } // 次の情報(pcap_addr)が無い場合は、ループを終了する if(p.memory.next == nil){ break } } // 次のデバイス情報(pcap_if)が無い場合は、ループを終了する if(dev.next == nil){ break } } } pcap_freealldevs(allDevs);
6 パケットの取得
pcap_open_live()関数にデバイス名を指定して、ハンドルを取得します。 後は、このハンドルを使用してpcap_next_ex()をコールするだけです。 pcap_next_ex()には、ヘッダ情報(pkt_hdr)とデータ(pkt_data)取得用のバッファを引数として渡します。
pcap_next_ex()の戻り値が、0以上の時、パケットが取得できていますので、後は、これを処理していくことになります。
let MAX_RECV_SIZE:Int32 = 65535 // 受信バッファのサイズ let Promisc:Int32 = 0 let timeout:Int32 = 500 var handle = pcap_open_live(devs[devNo], MAX_RECV_SIZE, Promisc, timeout,&errbuf); if (handle == nil) { NSLog("Can not open device \(String.fromCString(errbuf)!)"); exit(2); } var pkt_hdr = UnsafeMutablePointer<pcap_pkthdr>() // pcapヘッダ情報取得用バッファ var pkt_data = UnsafePointer<u_char>() // パケットデータ取得用バッファ var frameNo = 0 while(true){ var res = pcap_next_ex(handle, &pkt_hdr, &pkt_data) if(res <= 0){ if(res < 0 ){ print("ERROR!") }else{ // res=0 の場合は、受信データがなくタイムアウトしただけなので処理なし } continue; } // 取得したパケットは、ここで処理する }
7 パケットのデコード
取得したパケットは、単純なバイト配列です。 ここからは、ネットワークのプロトコルに従って、これをデコードする必要があります。
下記は、パケットの先頭であるEtherヘッダをデコードしている様子です。 取得したデータは、RawDataクラスに格納されていますので、オフセットをずらしながら、該当する型にキャストして解釈を進めています。
具体的には、先頭から6オクテットが宛先のMACアドレス、次の6オクテットが送信元MACアドレス、そして次の2オクテットがEtherパケットのタイプです。
var DestnationAddress = "" var SourceAddress = "" var Type:UInt16 = 0 override init(_ rawData:RawData,offset:Int){ super.init(rawData,offset: offset) var p = _offset let dstAddr:UnsafePointer<ether_addr> = _rawData.ptr(p) DestnationAddress = String.fromCString(ether_ntoa(dstAddr))! p += 6 let srcAddr:UnsafePointer<ether_addr> = _rawData.ptr(p) SourceAddress = String.fromCString(ether_ntoa(srcAddr))! p += 6 Type = (_rawData.memory(p) as UInt16).bigEndian p += sizeof(UInt16) }
その他のプロトコルは、それぞれ次のようになっています。
Etherヘッダ/ IPヘッダ/ IPv6ヘッダ/ TCPヘッダ/ UDPヘッダ/ ARPヘッダ/ ICMPヘッダ
本来は、すべてのプロトコルを解釈する必要があるのですが、これはあまりに膨大な作業になるため、今回は一部の主要プロトコルのデコードだけとなっています。
8 まとめ
今回は、SwiftでLibpcapを使用した簡易的なパケットモニターの作成してみました。 Cのライブラリを使用するには、やはり、Objective-Cの方が相性が良いようですが、ポインタの扱いさえ整理できれば、Swiftでも十分書けそうです。
9 参考リンク
TCPDUMP&libpcap
Swift - UnsafePointerと上手に付き合う
Swift & C: What I Learned Building Swift Bindings to libgit2
SwiftでC言語互換のポインタ操作が面倒
SwiftでTCPサーバーを作ってみる