この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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のヘッダファイルと、後々使用することになるネットワーク関連のいくつかのヘッダを追加しました。
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」を選択
開いたダイアログで「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>() ]も定義しています。
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サーバーを作ってみる