ping, 回来后,请告诉我你经过的路由器地址

使用Go实现ping工具那一篇文章中,我们自己实现了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中定义了IPv4的几个选项:

其中 record route(RR)就是本文要讨论的对象。

IPv4 record route(RR)选项指示路由器在数据包中要记录它们的IP地址。 RR是Internet协议的标准部分,可以在任何数据包上启用。与traceroute类似,RR记录和报告从源到目的地沿着Internet路径的IP地址,但它比traceroute具有几个优点。例如,RR可以逐跳地拼接回到目的地的反向路径,这对于traceroute和其他传统技术来说是不可见的;并且它可以发现一些不响应traceroute探测的跳数。

但是这也带来了安全问题,尤其是云服务当道的今天。如果从云机房的IP包开启了这个选项,那么它经过的云机房路由就会到云机房外,就会把云服务商的网络架构暴露出来,甚至黑客还可以利用其它选项,让流走特定的设备实现攻击,同时每个数据包还额外增加了一些数据多占带宽,吃力不讨好,所以你的机器是购买的云虚机的我话,ping -R大概率都被丢弃掉了,可能在源机房,也可能在目的机房。

特别是,RFC 1704强调了在互联网协议中的选项字段可能被滥用的风险。RFC 1704认为,记录路由选项可能会被用于追踪信息的来源和目的地,因此需要采取一些措施,以确保该选项不被滥用。该文档建议对记录路由选项进行限制和过滤,同时提高对它的安全意识和理解。

比如我在腾讯云的云虚机做测试,8.8.8.8忽略了这个选项,1.1.1.1github.com丢弃了请求,127.0.0.1返回了这个选项:

但是论文The record route option is an option!却提出一个新的观点,认为RR非常具有网络测量的潜力,并且可以与traceroute结合使用来增强对网络拓扑结构的理解。这篇论文呼吁重新评估RR选项潜力,并探索新的使用方式。

以上就是Record Route选项的背景知识。接下来我们就在原来我们实现的ping程序上加上RR的功能

使用 Go实现 RR 功能

解析 RR 选项

首先我们先做一个准备工作,如果我们收到一个IPv4包,我们需要解析出它的选项。可能包括多个选项,我们需要按照TLV的格式把它们都解析出来。

解析选项我们还是使用的gopacket包,首先解析T, 根据T的类型决定解析L和V:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)

我们使用下面的语句收取回包数据:

1
iph, payload, _, err := pc.ReadFrom(reply)

它可以读取ip header以及payload数据(ICMP回包)。

如果检查收到的数据是对应的icmp回包,我们就可以使用一开始我们准备的解析数据,解析出option:

1
2
3
4
5
6
7
8
9
10
11
12
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"))
}
}

完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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信息: