ping- 歸來後- 請告訴我你經過的路由
在使用 Go 實現 ping 工具 [1] 那一篇文章中,我們自己實現了 ping 工具的基本功能,我們也瞭解了 ping 底層實現的原理。讀者 @ZerOne 提出一個問題:能不能實現單向跟蹤路由的功能,類似 ping -R一樣,從 A 端 tracert B 端,同時顯示 B 端到 A 端的路徑?
Record Route 背景知識
我先前沒有用過 ping 的-R參數,ZerOne 這麼一問,我感覺很有趣。因爲最近兩年我一直在做網絡相關的工作,特別是網絡監控的能力,如果如 ZerOne 說的,ping 如果有這個能力,可以很好地用在我們網絡監控上,所以我迅速在網上搜索這相關的信息,並使用 Go 實現類似的功能, 最終形成了這一篇文章,太長不看的話:這個問題的答案是,行,但不現實。
這也算是 Go 高級網絡編程系列中插播的一篇吧,另外一個讀者評論批量發送包的時候提出了 sendmmsg 和 sendv(應該是想說 writv) 的區別,我會在下一篇文章介紹,這兩篇算是插播的知識點。歡迎大家一起探討高級網絡編程的知識,私信我或者加入 Go 高級編程研討羣進行交流。
首先man ping看它的幫助文檔中對-R參數的介紹:
-R Record route. Includes the RECORD_ROUTE option in the ECHO_REQUEST packet and displays the route buffer on returned packets. Note that the IP header is only large enough for nine such routes. Many hosts ignore or discard this option.
-R 記錄路由。在 Echo 請求包中包含
RECORD_ROUTE選項,在返回的包中顯示緩存的路由。注意 IPV4 的頭部只能最多存 9 個這樣的路由地址。很多節點會忽略或者丟棄這個選項。
Mac OS 的 ping 工具已經把這個選項標記爲棄用了,即使你添加了這個參數,也相當於一個 no-op 操作。
RR 的功能是利用 ipv4 header 的 option 實現的。
普通的 ipv4 header 是 20 個字節 (圖中每一行是 4 個字節, 32bit), 但是協議還設置了 Option, 可以擴展它的基本功能。
IHL 是 4 個 bit, 代表 IP 頭的長度 (行數),因爲 4 個 bit 最大也就是 15,所以 ipv4 的 header 最大也就是 15 * 4byte = 60 byte,留給 option 的也只有 40 byte (60 byte - 40 byte)。
Option 的格式採用 TLV 格式定義,第一個 byte 是 Type, 第二個 byte 是此 Option 的長度 Length,剩餘的字節包含 Option 的值 Value:
rfc791[2] 中定義了 IPv4 的幾個選項:
其中 record route(RR) 就是本文要討論的對象。
IPv4 record route(RR)選項指示路由器在數據包中要記錄它們的 IP 地址。RR 是 Internet 協議的標準部分,可以在任何數據包上啓用。與 traceroute 類似,RR 記錄和報告從源到目的地沿着 Internet 路徑的 IP 地址,但它比 traceroute 具有幾個優點。例如,RR 可以逐跳地拼接回到目的地的反向路徑,這對於 traceroute 和其他傳統技術來說是不可見的;並且它可以發現一些不響應 traceroute 探測的跳數。
但是這也帶來了安全問題,尤其是雲服務當道的今天。如果從雲機房的 IP 包開啓了這個選項,那麼它經過的雲機房路由就會到雲機房外,就會把雲服務商的網絡架構暴露出來,甚至黑客還可以利用其它選項,讓流走特定的設備實現攻擊,同時每個數據包還額外增加了一些數據多佔帶寬,喫力不討好,所以你的機器是購買的雲虛機的我話,ping -R大概率都被丟棄掉了,可能在源機房,也可能在目的機房。比如我在騰訊雲的雲虛機做測試,8.8.8.8忽略了這個選項,1.1.1.1和github.com丟棄了請求,127.0.0.1返回了這個選項:
但是論文 The record route option is an option![3] 卻提出一個新的觀點,認爲 RR 非常具有網絡測量的潛力,並且可以與 traceroute 結合使用來增強對網絡拓撲結構的理解。這篇論文呼籲重新評估 RR 選項潛力,並探索新的使用方式。
以上就是Record Route選項的背景知識。接下來我們就在原來我們實現的 ping 程序上加上 RR 的功能
使用 Go 實現 RR 功能
解析 RR 選項
首先我們先做一個準備工作,如果我們收到一個 IPv4 包,我們需要解析出它的選項。可能包括多個選項,我們需要按照 TLV 的格式把它們都解析出來。
解析選項我們還是使用的 gopacket 包, 首先解析 T, 根據 T 的類型決定解析 L 和 V:
func parseOptions(data []byte) ([]layers.IPv4Option, error) {
options := make([]layers.IPv4Option, 0, 4)
for len(data) > 0 {
opt := layers.IPv4Option{OptionType: data[0]}
switch opt.OptionType {
case 0: // End of options
opt.OptionLength = 1
options = append(options, opt)
return options, nil
case 1: // 1 byte padding, no-op
opt.OptionLength = 1
data = data[1:]
default:
if len(data) < 2 {
return options, fmt.Errorf("invalid ip4 option length. Length %d less than 2", len(data))
}
opt.OptionLength = data[1]
if len(data) < int(opt.OptionLength) {
return options, fmt.Errorf("IP option length exceeds remaining IP header size, option type %v length %v", opt.OptionType, opt.OptionLength)
}
if opt.OptionLength <= 2 {
return options, fmt.Errorf("invalid IP option type %v length %d. Must be greater than 2", opt.OptionType, opt.OptionLength)
}
opt.OptionData = data[2:opt.OptionLength]
data = data[opt.OptionLength:]
options = append(options, opt)
}
}
return options, nil
}
我們只關注 RR 選項,它的 T 是0x07, 它的第三個 byte 是一個從 1 開始的指針,指向存放路由 IP 地址的下一個字節,初始是 4,也就是它後面的那個字節,我們需要解析出它已存放的 IP 地址:
type RecordRouteOption struct {
layers.IPv4Option
IPs []net.IP
}
func parseRecordRouteOption(rr layers.IPv4Option) *RecordRouteOption {
if rr.OptionType != 0x07 {
return nil
}
ptr := int(rr.OptionData[0] - 3)
var ips []net.IP
for i := 1; i+4 <= ptr; i += 4 {
ips = append(ips, net.IP(rr.OptionData[i:i+4]))
}
return &RecordRouteOption{
IPv4Option: rr,
IPs: ips,
}
}
func (rr RecordRouteOption) IPString(prefix, suffix string) string {
var ips []string
for _, ip := range rr.IPs {
ips = append(ips, prefix+ip.String()+suffix)
}
return strings.Join(ips, "")
}
解析 option 的代碼已經完成,接下來的工作就是在發送 icmp Echo 請求的時候,把 RR 設置上。並且有幸收到回包的話,你需要把 RR 中的路由列表打印出來。
實現帶 RR 功能的 ping
和先前我們實現的 ping 工具不同,我們需要再深入一下。因爲先前的 ping 實現我們直接發送 ICMP 的包,不需要 IPv4 這一層,現在既然我們需要設置 IPv4 的 option, 我們就得手工構造 IPv4 的包,發送 IPv4 的包有多重途徑,我本來在這個系列中就要講如何發送 IPv4 的包,因爲插入了這一篇介紹 RR 的文章,所以在這篇文章我就介紹其中一種方式,其他方式請關注鳥窩聊技術微信公衆號。
我們可以使用pc, err := ipv4.NewRawConn(conn),把net.PacketConn轉換成 *ipvr.RawConn ,就可以發送和接收帶 IPv4 header 的包了。
重點是構造 IP header, 設置 RR 選項最多支持 9 個路由 (32 byte + ptr), ptr 的初始值設置爲 4:
ip := layers.IPv4{
Version: 4,
SrcIP: net.ParseIP(local),
DstIP: dst.IP,
Protocol: layers.IPProtocolICMPv4,
TTL: 64,
Flags: layers.IPv4DontFragment,
}
// 準備 Record Route 選項
recordRouteOption := layers.IPv4Option{
OptionType: 0x07,
OptionLength: 39,
OptionData: make([]byte, 37),
}
recordRouteOption.OptionData[0] = 4
// 添加選項
ip.Options = append(ip.Options, layers.IPv4Option{OptionType: 0x01}, recordRouteOption)
我們使用下面的語句收取回包數據:
iph, payload, _, err := pc.ReadFrom(reply)
它可以讀取 ip header 以及 payload 數據 (ICMP 回包)。
如果檢查收到的數據是對應的 icmp 回包,我們就可以使用一開始我們準備的解析數據,解析出 option:
opts, _ := parseOptions(iph.Options)
for _, opt := range opts {
if opt.OptionType != 0x07 {
continue
}
rr := parseRecordRouteOption(opt)
if rr != nil {
fmt.Println("\nRR:" + rr.IPString("\t", "\n"))
}
}
完整的代碼如下:
package main
import (
"fmt"
"log"
"net"
"os"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
const (
protocolICMP = 1
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "usage: %s host\n", os.Args[0])
os.Exit(1)
}
host := os.Args[1]
// 解析目標主機的 IP 地址
dst, err := net.ResolveIPAddr("ip", host)
if err != nil {
log.Fatal(err)
}
local := localAddr()
// 創建 ICMP 連接
conn, err := net.ListenPacket("ip4:icmp", local)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
pc, err := ipv4.NewRawConn(conn)
if err != nil {
log.Fatal(err)
}
// 構造 IP 層
ip := layers.IPv4{
Version: 4,
SrcIP: net.ParseIP(local),
DstIP: dst.IP,
Protocol: layers.IPProtocolICMPv4,
TTL: 64,
Flags: layers.IPv4DontFragment,
}
// 準備 Record Route 選項
recordRouteOption := layers.IPv4Option{
OptionType: 0x07,
OptionLength: 39,
OptionData: make([]byte, 37),
}
recordRouteOption.OptionData[0] = 4
// 添加選項
ip.Options = append(ip.Options, layers.IPv4Option{OptionType: 0x01}, recordRouteOption)
// 構造 ICMP 層
icmpLayer := layers.ICMPv4{
TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, 0),
Id: uint16(os.Getpid() & 0xffff),
Seq: 1,
}
// 添加 ICMP 數據
payload := []byte("ping!")
// 封裝到 gopacket.Packet
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
err = gopacket.SerializeLayers(buf, opts, &ip, &icmpLayer, gopacket.Payload(payload))
if err != nil {
panic(err)
}
// 發送 ICMP 報文
start := time.Now()
_, err = pc.WriteToIP(buf.Bytes(), dst)
if err != nil {
log.Fatal(err)
}
// 接收 ICMP 報文
reply := make([]byte, 1500)
for i := 0; i < 3; i++ {
err = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
if err != nil {
log.Fatal(err)
}
iph, payload, _, err := pc.ReadFrom(reply)
if err != nil {
log.Fatal(err)
}
duration := time.Since(start)
// 解析 ICMP 報文
msg, err := icmp.ParseMessage(protocolICMP, payload)
if err != nil {
log.Fatal(err)
}
// 打印結果
switch msg.Type {
case ipv4.ICMPTypeEchoReply:
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok {
log.Fatal("invalid ICMP Echo Reply message")
return
}
if iph.Src.String() == host && echoReply.ID == os.Getpid()&0xffff && echoReply.Seq == 1 {
fmt.Printf("reply from %s: seq=%d time=%v\n", dst.String(), msg.Body.(*icmp.Echo).Seq, duration)
opts, _ := parseOptions(iph.Options)
for _, opt := range opts {
if opt.OptionType != 0x07 {
continue
}
rr := parseRecordRouteOption(opt)
if rr != nil {
fmt.Println("\nRR:" + rr.IPString("\t", "\n"))
}
}
return
}
default:
fmt.Printf("unexpected ICMP message type: %v\n", msg.Type)
}
}
}
func localAddr() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
panic(err)
}
for _, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
if ipNet.IP.To4() != nil && ipNet.IP.To4()[0] != 192 {
return ipNet.IP.String()
}
}
}
panic("no local IP address found")
}
測試一下, 可以從回包中收到 RR 信息:
參考資料
[1]
使用 Go 實現 ping 工具: https://colobu.com/2023/04/26/write-the-ping-tool-in-Go/
[2]
rfc791: https://www.rfc-editor.org/rfc/rfc791
[3]
The record route option is an option!: https://dl.acm.org/doi/10.1145/3131365.3131392
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Zc0wKiQ_kJaO9IS5yis2lw