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