首頁 > 軟體

如何使用golang實現traceroute

2023-09-12 18:01:28

Traceroute 概念

traceroute是一種網路診斷工具,通過traceroute可以診斷出本機到目的地IP之間的路由情況,例如路由跳數、延遲、是否可達等資訊。該工具在linux環境下的命令是traceroute或者tracepath,在windows下命令是tracert

工作原理

traceroute在linux系列的作業系統,預設通過傳送UDP請求到目的地IP,UDP的埠使用的是33434到33545之間。除了UDP的協定,可選用ICMP或者TCP(TCP SYN包)。使用33434到33534之間到埠是因為大部分linux系統的該範圍內的埠是不可用的。正常情況下如果我們對一個目的地主機發起UDP請求,並且該埠不存在就會直接返回埠或者主機不可大的資訊,這樣是無法獲取到中途的路由節點。此時需要引入一個TTL的概念。

TTL即Time-To-Live,更多的被理解為路由跳數,該值存於IP頭,經過路由轉發時會將該值減1,當ttl值為0時,路由就會回覆一個ICMP訊息"Time Exceeded",表示跳數已經達到最大值,無法進行轉發。

TTL在ipv4和ipv6頭有不同的定義,在ipv4頭用8位元來存該數值,且命名為“Time to Live”,而在ipv6的頭則叫做“Hop Limit”。

不管是Time to Live還是Hop Limit,其實都是相同的邏輯,路由轉發一次就減1,並且該值為0時則無法轉發。

我們來看一下traceroute的發包過程:

第一步:主機A往目的主機B傳送UDP包,包頭需要設定TTL=1,並且設定目的埠為33434。
第二步:主機A的最近的路由A收到UDP包以後,將TTL減1,此時TTL=0,路由A就將該包丟棄,並且回覆主機A一條ICMP資訊:“Time Exceeded”。
第三步:主機A收到ICMP的訊息以後即可記錄ICMP傳送主機的地址,該地址就是路由IP,並且主機A設定TTL=2,再次傳送UDP包到目的主機B的33434埠。
第四步:以此類推,直到TTL超過設定的最大值或者收到目的主機返回的訊息時停止發包,這樣就得到了一個路由地址列表,同時也能拿到傳送到路由之間的訊息延遲,如果路由超過設定的時間內沒有相應,則置該跳數的路由地址為“*”。

traceroute-go程式碼實現

由於go語言是高階語言,將udp以及tcp的包頭都封裝完整,無法客製化設定ttl。好在golang提供了syscall庫,該庫提供依稀了linux下的函數呼叫,因此可以利用該包的方法達到設定ttl的目的。在1.4之前可以使用標準庫syscall,但因為該庫已經被棄用,可以使用golang.org/x/sys庫,該庫是syscall的擴充套件,提供更加豐富的系統呼叫方法。
有庫的支援,我們則需要了解一下C語言的知識,即用C語言傳送udp包和接受icmp的資訊,因此這裡需要涉及到幾個函數:

socket函數,建立一個socke的檔案描述,用於傳送udp以及接收icmp的訊息,golang對應的函數為func Socket(domain, typ, proto int) (fd int, err error)setsockopt函數,該函數可以用於設定IP的頭資訊,我們要設定TTL就是利用該函數,同時該函數可以設定socket的請求或者接收訊息的超時時間,golang對應的函數為func SetsockoptInt(fd, level, opt int, value int) (err error)sendto函數,用於傳送udp訊息,golang對應的函數為func Sendto(fd int, p []byte, flags int, to Sockaddr) (err error)recvfrom函數,用於接收icmp訊息,golang對應的函數為func Recvfrom(fd int, p []byte, flags int) (n int, from Sockaddr, err error)

函數準備好以後就可以開工編寫golang版本的traceroute庫了。

首先,建立sendSocket,用於傳送UDP包,注意內部的引數 unix.IPPROTO_UDP表示使用ipv4的udp協定,這個與ipv6協定是有區別的,可以通過命令man socket檢視函數說明,然後建立一個recvSocket的socket檔案描述符,用於接收ICMP的訊息,這裡呼叫了函數SetsockoptTimeval,用於設定接收訊息的超時時間。

然後在for迴圈內迴圈傳送udp訊息並且接收icmp訊息:

程式碼中SetsockoptInt函數設定ipv4的頭TTL,初始化ttl=1,通過Sendto函數將訊息傳送到目的地址和目的埠,這裡目的埠從33434開始,會在33434到33534區間內迴圈。

傳送訊息以後,通過Recvfrom接收訊息,此時會判斷接收訊息是否報錯,如果報錯則直接退出迴圈並結束traceroute操作;如果沒有報錯,則需要解析返回的ICMP訊息,由於ipv4的Header包頭長度最小是20位元組,最大是60位元組,會出現浮動,因此需要拿到實際的ipv4頭長度,這裡使用ipv4庫的ParseHeader函數解析拿到ipv4的包頭結構,然後將收到的訊息擷取ipHeader.Len長度就得到我們的ICMP訊息結構體,拿到ICMP訊息結構以後既可以根據Type判定訊息型別,由於我們只關注ICMPTypeTimeExceededICMPTypeDestinationUnreachable型別的訊息,因此其他訊息我們都會丟棄,並且如果收到的是ICMPTypeTimeExceeded,則需要將傳送方的地址(路由地址)存下來,並且將ttl+1,然後再次迴圈傳送udp訊息到目的地。
如果收到的ICMP訊息型別是ICMPTypeDestinationUnreachable或者ttl超過了最大的ttl設定或者接受的的ICMP訊息來自於目的地址,則結束髮包,並輸出結果。

當然,如果接收到報錯的訊息,該訊息可能是路由不通或者發包超時,因此我們需要將該跳的路由地址設定為“*”,同時判定重試次數,以及是否超過了最大TTL。
最後每次迴圈都將目的埠值+1,並且超過了最大的埠33534是又從最小埠開始,保障埠範圍一直在33434到33534之間。

結果輸出:
以下是我們自己的程式結果輸出:

以下是系統自帶的traceroute輸出:

總結

traceroute工具原理不難,但要實現這個過程需要涉及到一些基本知識,如ip的報文組成、udp、icmp協定的一些基本知識,另外就是需要知道路由跳數的基本原理,通過實現這個過程也可以加深這些基礎知識,同時是對這些知識的運用。
完整程式碼已經上傳到github,地址為:https://github.com/Kseleven/traceroute-go,歡迎大家star,當然如有紕漏或者講解不正確的地方,歡迎指正。

參考文獻

  1. ipv4 rfc 791
  2. ipv6 rfc 2460
  3. icmp rfc 792
  4. traceroute rfc 1393
  5. linux man page-traceroute
  6. traceroute wiki
  7. icmp wiki
  8. golang sys庫

到此這篇關於如何使用golang實現traceroute的文章就介紹到這了,更多相關golang實現traceroute內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com