ちょっと話題の記事

tracerouteの高速版をSwiftで書いてみた

2015.11.22

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

1 はじめに

tracerouteは、ネットワークの経路を確認するコマンドですが、このコマンド、ちょっと待ち時間が長くてイライラすることは無いでしょうか。

今回は、このコマンドの高速版を作成してみました。

2 動作のおさらい

ちょっとここで、簡単に動作をおさらいします。

tracerouteは、pingと同じようにICMPのエコー要求パケットを投げるだけですが、この時、TTL値を1から順に増やしながら試します。 TTL値はルータを越えるごとに1づつ減らされるため、TTLが0になった時点で、ルータは「これ以上行けない」という意味で返事(Time Exceeded)を返します。 最終的に宛先ホストまで到着した時点で、経路上のすべてのルータが判明するという事です。

006

3 ちょっとイライラする

経路上のルータが、全部さっさと返事をすれば、特に問題は無いのですが、返事をしないルータがよくいます。 このようなルータがあると、tracerouteはタイムアウトするまで次の動作に移れないため、ここで待たされる事になり、ちょっとイライラするのです。

図のアスタリクスの状態ですね・・・(デフォルトで3回試行される)

001

4 高速化プログラム(Swift)

イライラする原因は、タイムアウト待ちが発生することにありますので、タイムアウトの無いプログラムを書いてみました。

(1) 多重化

最初にTTL値が1〜30のICMPパケットを一気に送信しています。 ここでのmaxは、現在30となってます。とりあえず、30ホップぐらいで到達できるだろうという予想です。

main.swift

let ping = Ping()
ping.delegate = self
// 多重スレッドでTTLの違うICMPを送信する
for i in 0 ... max {
    dispatch_async(queue, {
        ping.send(ipAddr, id: i, ttl: UInt32(i+1))
    })
}
// type=0のパケットを受け取るまで待機
while(!isFinish){
    sleep(0)
}

(2) 受信処理

返された返事は、到着順に関係なくTTL値の順で格納します。 そして、エコー応答のパケット(宛先ホストの返事)が到着したら、未返事のルータに100msだけ余裕をやって終了としています。

main.swift

// 受信完了
func recved(recvBytes: Int, srcAddr: String, icmpType: UInt8, msec: NSTimeInterval, id: Int) {
        
    // 結果取得用の配列に格納する
    var host = NetName.toHost(srcAddr) // IPアドレスから名前解決
    results[id].hostName = host[0] // ホスト名
    results[id].srcAddr = srcAddr // IPアドレス
    results[id].icmpType = icmpType // 応答の内容
    results[id].msec = msec // 経過時間
        
    if (icmpType == 0){ //エコー応答の場合
        // 到達のパケットを受けてから100msだけ待機する
        usleep(100000)
        isFinish = true // 終了フラグを立てる
    }
}

(3) 結果表示

最終的な結果の表示は次のような感じです。 TTL値1から順に表示しますが、「エコー応答」があった場合は、それで終わりです。 結果を受け取っていないルータは、アスタリクスで表示します。

main.swift

for i in 0 ... max {
    let r = results[i]
    if(r.srcAddr=="*"){
        print(String(format: "%d *",i+1))
    }else{
        print(String(format: "%d %@ (%@) %.3f ms",i+1,r.hostName,r.srcAddr,r.msec*1000))
    }
    if(r.icmpType==0){
        break
     }
}

(4) ICMPパケット

ICMPパケット用のIPPROTO_ICMPは、 SOCK_RAWで使用されているサンプルをよく見ることがありますが、ここでは、SOCK_DGRAMを使用しています。そのため、コマンド実行にroot権限は必要ありません。 また、TTL値は、setsockoptで設定しています。

Ping.swift

var icmpHdr:IcmpHeader = IcmpHeader.init(type: 8, code: 0, sum: 0, id: UInt16(id), seq: UInt16(id))
icmpHdr.sum = (self.checksum(Cast.ptr(&icmpHdr,len: IcmpHeaderLen) as UnsafePointer<UInt8>, start:0,len:IcmpHeaderLen)).bigEndian

let sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
if (sock < 0) {
    delegate?.err("socket() faild. sock=\(sock)")
    return -1
}

var t = Int32(ttl)
if( 0 != setsockopt(sock,IPPROTO_IP,IP_TTL,&t,4)){
    delegate?.err("error setsockopt")
    return -1
}

完成したプログラムを使用しているようすです。(画像をクリックしてください)

tra

※早送りしていません

途中、返事をしないルータがいくつあっても、瞬時に結果を表示することができます。

5 まとめ

今回は、tracerouteの高速版を書いてみました。 多重化することで、到達時間等の測定精度は、かなり悪いと思います。 また、宛先ホストより非常に遅いレスポンスを返すルータがいた場合は、無視されることになります。 単純に、経路を概観するだけのプログラムということでご了承ください。

作成したプログラムは、下記に置きました。

githubコード(https://github.com/furuya02/tra)