SwiftとLibpcapによるパケットモニターの作成

2015.11.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

今まで、Windows上でWinPcapというライブラリを使用して、パケットを扱うプログラムをよく書いていました。 最近、Macで作業することが多くなり、それじゃということで Swiftでも書いてみようと思ったのですが・・・

残念ながら、Swift で libpcap を扱うまとまった資料は、なかなか見つかりませんでした。そこで、今回作成したパケットモニター(簡易版)の作業手順をここに紹介させて頂くことにしました。

2 パケットモニター(簡易版)

下記は、今回作成したパケットモニター(簡易版)実行状況です。(画像をクリックしてください)

001

最初に、モニターの対象とするネットワークインターフェース(NIC)を選択してモニターが開始されます。 簡易的に、Ether、IPv4、IPv6、TCP、UDP、ARP、ICMPのヘッダだけをデコードしてみました。 途中で、pingコマンドを打つと、ICMPのエコー・リプライがモニターできているのを見ることできます。

また、コードは、githubに置きましたので、説明不足なところは、こちらをご参照ください。

githubサンプルコード(https://github.com/furuya02/sniffer-sample)

3 環境構築

Swiftでlibpcapを使用したプログラムを作成するためには、概ね次の準備が必要です。

  • libpcapライブラリの追加
  • ブリッジファイルの追加
  • rootでのデバッグ

以下、それぞれについて解説します。

(1) libpcapライブラリの追加

まずは、新しいプロジェクトの作成を行います。 「OS X」の「Application」から「Command Line Tool」を選択し、なお、プロジェクト名は「sniffer-sample」としました。

001

002

続いて、libpcapライブラリをプロジェクトに追加します。

「TARGET」で対象プロジェクトを選択し、「Build Phases」タブを開いて「Link Binary With Libraries」セクションで「+」をクリックします。

003

表示されたダイアログで、「libpcap」と入力して検索し、「libpcap.tbd」を「Add」ボタンで追加します。

004

005

(2) ブリッジファイルの追加

Swiftからlibpcapライプラリを参照するためには、ブリッジとなるヘッダファイルが必要です。 SwiftのプロジェクトにObjective-Cのソースファイルを追加すると、自動的に「プロジェクト名-Bridging-Header.h」と言う名前で、ブリッジファイルを登録させることができるので、今回は、その機能を使用しました。

まずは、プロジェクトに「Objective-C File」を追加します。

006

名前は、「dmy」(何でも良い)としました。

007

「Next」のボタンを押すと、次のような確認が表示されますので、「Create Bridging Header」を選択します。

008

追加が完了すると、プロジェクトに「dmy.m」と「sniffer-sample-Bridging-Header.h」が追加されていることが確認できます。

009

これでブリッジファイルの追加は完了です。「dmy.m」は、削除してしまって構いません。

「Build Settings」の「Objective-C Bridging Header」に先ほどのヘッダファイルが登録されているのが確認できます。

010

この後、libpcapライブラリを使用する際に必要となるヘッダ情報は、この「sniffer-sample-Bridging-Header.h」に記述すればよいことになります。

下記のコードは、いくつかのヘッダを追加しものです。libpcapのヘッダファイルと、後々使用することになるネットワーク関連のいくつかのヘッダを追加しました。

sniffer-sample-Bridging-Header.h

// 必要なヘッダをここに追加する
#import <pcap.h>
#import <net/ethernet.h>
#import <netinet/in.h>
#import <arpa/inet.h>

(3) rootでのデバッグ

ネットワークインターフェースをプロミスキャスモードに変更するためには、root権限が必要です。(プロミスキャスで利用する場合のみ) デバッグ中にこの権限を付与するために、設定が必要です。

メニューから「Product」ー「Scheme」ー「Edit Scheme」を選択

011

開いたダイアログで「Run」「info」「Debug Process As」で「Me」を「root」に変更する

012

※定期的にパスワードを要求される

これで、libpcapを使用したプログラム作成の準備は完了です。

4 RawDataクラス

Swiftでは、型の違うものどうしの代入はできません。

しかし、パケットを扱うプログラムでは、あるデータ列の特定のオフセットから違った型(構造体)のポインタをあててデータを解釈するといった作業が必要になります。

このような動作は、Swiftでも記述可能ですが、後々コードの見通しが良くなるように、この辺の機能をクラスとして定義してみました。

RawDataクラスは、取得したパケットを丸ごと与えて初期化し、内部にNSDataとして保持します。 そして、特定のオフセットと型を指定して、そのポインタやデータ(実体)を受け取れるようにした[ memory<T>(offset)、ptr<T>(offset) ]と、特定のオフセットから指定したバイト数だけUnsafePointer<Int8>で受け取る[ ptr(offset,length) ]を用意しました。

また、静的メソッドとして初期化せず型変換だけを行う[ direct<T>() ]も定義しています。

RawData.swift

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構造体は、次のようになっています。

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

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アドレスを列挙して表示しているものです。

main.swift

///////////////////////////////////////////////////
// デバイス列挙
///////////////////////////////////////////////////
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以上の時、パケットが取得できていますので、後は、これを処理していくことになります。

main.swift

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パケットのタイプです。

EtherHeader.swift

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サーバーを作ってみる