golang版的traceroute實現

前言

之前看<<TCP/IP詳解卷一>>的時候,發現能夠根據IP報文中的TTL字段追蹤數據包的路由詳情,以爲頗有意思。後來知作別人早就把它實現出來了,就是linux下的traceroute命令(windows 的tracert),學了golang後也想實現一個go版本的,但中間都給種種事情耽擱了,最近把工做辭了,恰好有點時間,就想着把它作出來,順便看成我的項目去面試。linux

應用場景

在分析traceroute以前,先介紹一下它的應用場景。不知道大家有沒有遇到過這樣狀況,就是買了個國外的服務器,用ssh鏈接的時候發現很慢,而後你就會忍不住ping一下看延遲多少,若是出來300的延遲你會忍不住吐槽一句:什麼破服務器,延遲這麼高。而後你確定想知道緣由,爲何這破服務器這麼卡。git

而這時候traceroute就能夠派上用場了,你用traceroute測一下就知道,它會能夠追蹤數據包的路由詳情,能夠知道從你的電腦到服務器之間通過了多少跳的路由,若是是數據包通過不少跳路由最終纔到服務器,天然就很卡。github

下面我用 vultr.com域名測試,先ping一下golang

Pinging vultr.com [108.61.13.174] with 32 bytes of data:
Reply from 108.61.13.174: bytes=32 time=234ms TTL=50
Reply from 108.61.13.174: bytes=32 time=233ms TTL=49
Reply from 108.61.13.174: bytes=32 time=247ms TTL=49
Reply from 108.61.13.174: bytes=32 time=233ms TTL=49

Ping statistics for 108.61.13.174:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 233ms, Maximum = 247ms, Average = 236ms

200多的延遲,而後咱們再用tracert(windows下的traceroute)測一下:面試

Tracing route to vultr.com [108.61.13.174]
over a maximum of 30 hops:

  1     1 ms     2 ms     2 ms  192.168.0.1 [192.168.0.1]
  2     2 ms     1 ms     1 ms  192.168.1.1 [192.168.1.1]
  3     4 ms     3 ms     3 ms  xxx.xx.xx.x
  4    15 ms    40 ms     4 ms  xxx.xx.xx.xx
  5     8 ms    17 ms    18 ms  xxx.xx.xx.xx
  6     9 ms     7 ms     7 ms  202.97.90.162  廣州
  7    17 ms    17 ms    16 ms  202.97.38.166  昆明
  8   185 ms   192 ms   184 ms  202.97.51.94   上海
  9   164 ms   167 ms   165 ms  202.97.90.118
 10   191 ms   170 ms   183 ms  9-1-9.ear1.LosAngeles1.Level3.net [4.78.200.1]
 11     *        *        *     Request timed out.
 12   235 ms   239 ms   247 ms  214.213.15.4.in-addr.arpa [4.15.213.214]
 13     *        *        *     Request timed out.
 14     *        *        *     Request timed out.
 15   246 ms   248 ms   237 ms  174.13.61.108.in-addr.arpa [108.61.13.174]

能夠看到通過了15跳的路由,若是你分別查一下這些ip對應地方,會發現它從廣州繞到昆明,再繞到上海最後纔去了美國,繞了中國大半圈,延遲不高才怪呢。算法

原理分析

下面來分析一下traceroute背後的原理,首先先介紹一個數據包在傳輸過程當中的一個特性,就是IP報文首部的TTL字段在每通過一跳路由的時候,TTL的值都會給路由器減1。就這樣每通過一跳路由就減1,當TTL的值減到0的時候,路由器將再也不轉發這個數據包,而是將其丟棄,然會返回一個ICMP報文到信源端。shell

這個特性有什麼用呢?你想啊,若是我手動把數據包TTL的值設爲1,發給目的地,而後IP數據報到下一條路由的時候就給丟棄了,並且還會收到下一跳路由的ICMP報文(裏面有該路由器的IP)。而後我再把TTL的值設爲2,數據包在第二條路由的時候又給丟棄了,又返回第二跳路由的ICMP報文,這樣我又能夠知道第二跳路由的IP了。就這樣經過投石問路的方式,不停地給目的地發送數據報,直到數據報到達目的地,就能夠把每一跳路由的IP給摸清楚了。windows

這裏有張圖,或許能夠方便理解服務器

d1900102224993c8.png

抓包分析

好了,原理分析講完了,下面來運行tracert並抓包分析來驗證一下個人觀點。ssh

首先先打開wireshark,而後運行tracert (tracert www.baidu.com),固然你會在wireshark上面看到一堆密密麻麻的數據包,因此須要過濾一下,在綠色的選框那裏輸入icmp便可,由於只有icmp數據包纔是咱們想要的,你會看到相似輸出:

圖片描述

我已經分別用紅色和藍色的框標記起來了,能夠看到,tracert連續發送了3條TTL爲1的ICMP報文 (紅色框)到目的地,而後收到下一跳路由的ICMP報文(藍色框),內容爲TTL超時。

而後tracert繼續發送三個TTL2的ICMP報文到目的地:

圖片描述

仍是收到一樣的答覆,TTL超時

就這樣,每發送完一輪後,TTL加1,直到收到目的地的回覆才中止,如圖(我用藍框標記出來了):

圖片描述

看來我不是瞎猜的,上面的就是證據。
既然跟咱們預料中的同樣,那接下來是否是能夠寫代碼了?別急,還差一步,就是咱們剛纔只分析tracert發送的過程,只是一個大體的過程。但在寫代碼的時候,"差很少"是不行的,你須要精確地知道報文的格式和裏面的參數才能夠。

好比要發送ICMP報文到目的地時,ICMP的報文中的type要改8,code要改成0,表明的是回顯。如圖:

圖片描述

若是你熟悉ICMP報文的話,你會發現traceroute本質上就是一個ping,區別只是在於修改了一下IP首部的TTL字段而已

而後你會收到type爲11,code爲0的ICMP回覆,表明TTL超時

圖片描述

或者若是到達了目的地,會收到type爲0,code爲0的回覆。表明Echo Reply。就跟你平時ping某臺主機後所獲得的回覆是同樣的

圖片描述

關於ICMP報文格式,能夠參考wiki 或者百度也行

具體實現

實現過程

traceroute本質上就是一個ping,只是修改了一下IP首部的TTL字段而已,我一開始覺得是件很簡單的事,可是實現過程一波三折。

我一開始先google一下,看有沒有人已經實現過golang版的traceroute了,免得我處處查API。結果然的有,點這裏

我滿懷好奇地點了進去看了下源碼,看思路是否和我是同樣的,而後發現他用的syscall這個庫來建立socket,不禁自主地感嘆了這老哥的強悍。syscall是在系統提供給的API上封裝的,這麼底層的東西,須要對底層有足夠的瞭解才能駕馭。

看了一會,而後把代碼複製下來跑一下,發現報了這個錯:

..\traceroute.go:198:72: undefined: syscall.IPPROTO_ICMP
..\traceroute.go:211:61: undefined: syscall.SO_RCVTIMEO

就是windows不支持這個系統調用,而後我看了一下項目的README,纔看注意到:Must be run as sudo on OS X
並且也有個在windows上開發的人也遇到一樣的問題,做者表示無能爲力,或者是懶得弄,在這個issue

而後想着既然做者用syscall實現的版本沒法在windows上運行,那我乾脆本身實現一個好了,而後我就去官網的標準庫查API,可是看了發現標準庫提供的函數不支持修改IP首部的TTL

而後我又google了一下,發現官方提供的 golang.org/x/net/ipv4的包居然支持修改TTL,我滿心歡喜地安裝了這個包,可是在實現過程當中發現,這個包的某些函數也是不支持windows的,若是你查看他的源碼會發現,他尚未實現,只是在裏面寫了個TODO標籤。別人也遇到一樣的問題,並提交到這個issue

我本覺得修改TTL只是查一下標準庫函數就能搞定了,沒想到不只標準庫不支持,並且官方提供的包和封裝底層系統調用的syscall都不支持windows,這時候我彷佛知道他們都用linux的緣由了,並且這種平臺的差別性已經不是我能搞定的了。應該仍是有辦法的,但我如今也不打算花時間糾結這個了,本着實現一個linux版本的好了的心態,打算動手開幹。

可是我發現官方提供的demo裏就有traceroute的實現,並且寫得還很精緻,既然官方例子已經實現出來了,我就沒有必要再去折騰了。
我看了一下源碼,思路跟的我差很少。怎麼說呢,我以爲到這裏,我也算是把traceroute給實現出來了把,雖然我不是去查API從零開始實現的。

代碼分析

下面我摘抄一部分核心代碼並分析以下:

好比ICMP報文的封裝:

wm := icmp.Message{
        Type: ipv4.ICMPTypeEcho, Code: 0,
        Body: &icmp.Echo{
            ID:   os.Getpid() & 0xffff,
            Data: []byte("HELLO-R-U-THERE"),
        },
    }

echo的ICMP的報文格式應該是type:0,code:0,可是他已經定義並封裝好了。
還有這裏的ID是進程號,用於區分不一樣的程序,由於這個字段在報文中是16位的,因此和0xffff作了與運算

wm.Body.(*icmp.Echo).Seq = i

這裏是ICMP報文中的序列號,用於區分發送的第幾個ICMP數據報

if err := p.SetTTL(i); err != nil {
    log.Fatal(err)
}

這裏是設置每次發送的TTL,封裝得太完全了,一行就搞定

switch rm.Type {
case ipv4.ICMPTypeTimeExceeded:
    names, _ := net.LookupAddr(peer.String())
    fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
case ipv4.ICMPTypeEchoReply:
    names, _ := net.LookupAddr(peer.String())
    fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
    return
default:
    log.Printf("unknown ICMP message: %+v\n", rm)
}

最後是根據這段代碼來判斷數據報是否已經到目的地的,能夠看到若是收到的是TTL超時報文會繼續發送,若是收到的是正常的回顯,則說明已經到達目的地,函數退出。

因爲這個庫封裝了底層的一些東西,好比不用考慮ICMP校驗和字段,IP首部校驗和算法的實現,因此實現起來代碼量很少,包註釋也就100行

完整的代碼以下:

package main

import (
    "fmt"
    "golang.org/x/net/icmp"
    "golang.org/x/net/ipv4"
    "log"
    "net"
    "os"
    "time"
)

func main() {
    // Tracing an IP packet route to www.baidu.com.

    const host = "www.baidu.com"
    ips, err := net.LookupIP(host)
    if err != nil {
        log.Fatal(err)
    }
    var dst net.IPAddr
    for _, ip := range ips {
        if ip.To4() != nil {
            dst.IP = ip
            fmt.Printf("using %v for tracing an IP packet route to %s\n", dst.IP, host)
            break
        }
    }
    if dst.IP == nil {
        log.Fatal("no A record found")
    }

    c, err := net.ListenPacket("ip4:1", "0.0.0.0") // ICMP for IPv4
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()
    p := ipv4.NewPacketConn(c)

    if err := p.SetControlMessage(ipv4.FlagTTL|ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface, true); err != nil {
        log.Fatal(err)
    }
    wm := icmp.Message{
        Type: ipv4.ICMPTypeEcho, Code: 0,
        Body: &icmp.Echo{
            ID:   os.Getpid() & 0xffff,
            Data: []byte("HELLO-R-U-THERE"),
        },
    }

    rb := make([]byte, 1500)
    for i := 1; i <= 64; i++ { // up to 64 hops
        wm.Body.(*icmp.Echo).Seq = i
        wb, err := wm.Marshal(nil)
        if err != nil {
            log.Fatal(err)
        }
        if err := p.SetTTL(i); err != nil {
            log.Fatal(err)
        }

        // In the real world usually there are several
        // multiple traffic-engineered paths for each hop.
        // You may need to probe a few times to each hop.
        begin := time.Now()
        if _, err := p.WriteTo(wb, nil, &dst); err != nil {
            log.Fatal(err)
        }
        if err := p.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {
            log.Fatal(err)
        }
        n, cm, peer, err := p.ReadFrom(rb)
        if err != nil {
            if err, ok := err.(net.Error); ok && err.Timeout() {
                fmt.Printf("%v\t*\n", i)
                continue
            }
            log.Fatal(err)
        }
        rm, err := icmp.ParseMessage(1, rb[:n])
        if err != nil {
            log.Fatal(err)
        }
        rtt := time.Since(begin)

        // In the real world you need to determine whether the
        // received message is yours using ControlMessage.Src,
        // ControlMessage.Dst, icmp.Echo.ID and icmp.Echo.Seq.
        switch rm.Type {
        case ipv4.ICMPTypeTimeExceeded:
            names, _ := net.LookupAddr(peer.String())
            fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
        case ipv4.ICMPTypeEchoReply:
            names, _ := net.LookupAddr(peer.String())
            fmt.Printf("%d\t%v %+v %v\n\t%+v\n", i, peer, names, rtt, cm)
            return
        default:
            log.Printf("unknown ICMP message: %+v\n", rm)
        }
    }
}
相關文章
相關標籤/搜索