使用Go实现traceroute工具

traceroute是一种用于诊断网络连接问题的实用程序,它可以确定两台计算机之间的网络路径和网络时延。traceroute工具在网络工程、系统管理和网络安全中都有广泛的应用。

traceroute工具也是使用了ICMP这种Internet控制消息协议,它可以让用户探测到目标主机与本地主机之间的网络路径和路由器(或网关)的数量。traceroute工具会向目标主机发送一系列UDP或ICMP报文,每个报文的Time To Live (TTL)值逐渐增加,直到达到设定的最大值,如果到达目标主机,则目标主机可能返回一个ICMP DestinationUnreachable包,否则返回一个 ICMP TimeExceeded包。通过分析响应包中的IP地址和时间信息,traceroute可以确定网络中的路由器和每个路由器的延迟时间。通过多次执行traceroute,可以帮助用户更好地理解网络的拓扑结构和性能瓶颈,以便优化网络连接。

最主要的, traceroute利用IP协议中的TTL的作用。在 IP 协议中,TTL(Time to Live)是一个8位字段,代表着一个 IP 数据包在网络中最多可以经过的路由器数量,也就是生存时间。每经过一个路由器,TTL 的值就会被减一,当 TTL 的值变成 0 时,该数据包会被路由器丢弃,并向源主机发送一个 ICMP 时间超时消息。

TTL 的作用是为了防止 IP 数据包在网络中无限制地循环,也就是防止出现数据包在网络中无限制地跳转,浪费网络资源。通过设定 TTL 的值,可以让数据包在网络中跳转一定的次数后被丢弃,从而避免网络中的拥塞和不必要的负荷。使用 traceroute 工具时,就是通过逐步减小 TTL 的值,依次向距离越来越远的路由器发送 ICMP 消息,从而获取到路由路径信息。

Linux中的traceroute是一个功能强大的工具,有很多的参数:

1
2
3
4
5
6
7
traceroute [-46dFITUnreAV] [-f first_ttl] [-g gate,...]
[-i device] [-m max_ttl] [-p port] [-s src_addr]
[-q nqueries] [-N squeries] [-t tos]
[-l flow_label] [-w waittimes] [-z sendwait] [-UL] [-D]
[-P proto] [--sport=port] [-M method] [-O mod_options]
[--mtu] [--back]
host [packet_len]

这篇文章主要介绍traceroute底层的实现原理,所以不会完全复刻Linux自带的traceroute所有的参数的功能,否则会有大段的代码处理这些参数的逻辑,本文只是实现一个最基本的功能。

注意traceroute工具发送设置了TTL的IP包时,可以使用ICMP、UDP、ICMP甚至其他的IP支持的协议,Linux支持UDP、TCP、ICMP这三种协议, MacOS使用UDP协议,不过TTL为0后返回的还是ICMP协议。Apple公司的traceroute.c是一个很好的学习traceroute的代码,虽然它支持发送UDP协议的包,不过这次我们使用Go语言介绍如何实现traceroute。

说起协议了,有些人可能会问了,为啥不直接使用ICMP包,而是还要实现UDP和TCP的发送包呢?这个物理网络实际的网络设备的处理是有关的。在同一个层级的节点中,比如北京联通的网络出口上,并不会只有一台网络设备,否则这台设备挂了,或者这台设备的带宽不够了,就会导致网络丢包或者不通,所以一般会部署多台设备,那么对于一个网络流来说,一般会使用他们的源目地址和源目端口做哈希,以便把同一个session的数据流发送到同一台设备上,所以使用 UDP或者TCP可以固定五元组,让探测流总是经过同一台设备,以便检查固定的链路是不是有问题。当然这也不是绝对的,有可能同一个五元组也会经过不同的设备。

比如下面的traceroute,在第9跳的时候就经过了三台设备(其他跳中也有经过多台设备的情况)

在Linux中,默认情况下,traceroute使用的是UDP协议,目的端口从起始值为33434开始,每个TTL值加1,最大值为65535。这是因为当TTL值为1时,数据包到达第一个路由器,如果该路由器启用了ICMP错误消息的生成功能,它会将一个ICMP TTL过期消息返回给traceroute。为了避免端口被旁路其他应用程序占用,traceroute将目标端口号加上TTL值作为UDP包的目的端口。这样每个TTL的数据包都会使用不同的目的端口号,保证traceroute能够得到正确的TTL值。

使用UDP包探测 (raw socket)

首先,我们使用UDP包进行探测,然后处理返回的ICMP包。

这里有几个技术点:

  • 如何设置TTL?
  • 如果处理不同的IP protocol?
  • 如果匹配ICMP包和traceroute的探测UDP包?

第一种方式是我们使用raw socket,利用gopacket生成探测包,设置TTL, 创建一个syscall.Socket用来发送UDP包,再创建一个icmp.PacketConn用来接收ICMP包。

rawsocket的生成使用下面的方法:

1
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)

然后调用Sendto系统调用发送IP+UDP的包:

1
err = syscall.Sendto(fd, data, 0, dstAddr)

读取ICMP消息理论也可以使用这个socket读取,不过这里我们使用下面的方法专门接收icmp的包:

1
rconn, err := icmp.ListenPacket("ip4:icmp", local)

这个专门读取ICMP的rconn尝试读取ICMP包:

1
replyBytes := make([]byte, 1500)

正常情况下会读取到ICMP的返回包,也可能读取到其他traceroute和ping的返回的包,所以先解析出ICMP message,还要进一步的根据源目IP和ID、Seq等进行判断。
一个设备返回ICMP TimeExceeded包时,会把IP Header以及之后的8个字节的数据返回。对于UDP来说,IP header中包含源目IP,UDP前4个字节正好是源目端口,基本上我们使用这四元组可以将返回的包和请求包匹配在一起,但是为了进一步避免误判,我们还可以设置IP Header中的id,把它设置成进程id,这样再增加一个匹配项,基本可以避免误判。注意这里我们目的端口每次ttl加一它也会加一,你也可以目的端口固定, “任从你心”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
te, ok := replyMsg.Body.(*icmp.TimeExceeded)
if !ok {
continue
}
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
if ipAndPayload.Dst != dst || ipAndPayload.Src != local || ipAndPayload.SrcPort != *sport || ipAndPayload.DstPort != *dport || ipAndPayload.ID != id {
continue
}
fmt.Printf("%d: %v %v\n", ttl, peer, time.Since(start))
if peer.String() == dst {
return
}
continue loop_ttl
}

完整的代码如下,关键行上我加上了注释:

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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
package main
import (
"encoding/binary"
"flag"
"fmt"
"log"
"net"
"os"
"syscall"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
const (
protocolICMP = 1
maxHops = 64
)
var (
sport = flag.Int("sport", 12345, "source port")
dport = flag.Int("p", 33434, "destination port")
)
func main() {
flag.Parse()
if len(os.Args) != 2 {
log.Fatalf("usage: %s host", os.Args[0])
}
dst := os.Args[1]
timeout := 3 * time.Second
dstAddr := &syscall.SockaddrInet4{}
copy(dstAddr.Addr[:], net.ParseIP(dst).To4())
// 得到本机的地址
local := localAddr()
// 生成一个icmp conn, 用来读取ICMP回包
rconn, err := icmp.ListenPacket("ip4:icmp", local)
if err != nil {
log.Fatalf("Failed to create ICMP listener: %v", err)
}
defer rconn.Close()
// 得到进程ID
id := uint16(os.Getpid() & 0xffff)
// 生成一个用来写udp的raw socket,这里使用syscall.IPPROTO_RAW,因为我们需要自己设置IP Header
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
fmt.Println(err)
return
}
defer syscall.Close(fd)
// 设置此项,我们自己手工组装IP header
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
if err != nil {
fmt.Println(err)
return
}
// TTL递增探测
loop_ttl:
for ttl := 1; ttl <= maxHops; ttl++ {
*dport++
// 拼装一个IP+UDP的包, IP header使用指定的id和ttl, udp 的payload使用一段字符串
data, err := encodeUDPPacket(local, dst, id, uint8(ttl), []byte("Hello, are you there?"))
if err != nil {
log.Printf("%d: %v", ttl, err)
continue
}
// 发送UDP包
start := time.Now()
err = syscall.Sendto(fd, data, 0, dstAddr)
if err != nil {
log.Printf("%d: %v", ttl, err)
continue
}
// listen for the reply
replyBytes := make([]byte, 1500)
if err := rconn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
log.Fatalf("Failed to set read deadline: %v", err)
}
// 尝试读取3次
// 你也可以使用死循环+一个超时来控制
for i := 0; i < 3; i++ {
n, peer, err := rconn.ReadFrom(replyBytes)
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
fmt.Printf("%d: *\n", ttl)
continue loop_ttl
} else {
log.Printf("%d: Failed to parse ICMP message: %v", ttl, err)
}
continue
}
// 解析 ICMP message
replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n])
if err != nil {
log.Printf("%d: Failed to parse ICMP message: %v", ttl, err)
continue
}
// 如果是 DestinationUnreachable,说明探测到了目的主机
if replyMsg.Type == ipv4.ICMPTypeDestinationUnreachable {
te, ok := replyMsg.Body.(*icmp.DstUnreach)
if !ok {
continue
}
// 抽取匹配项
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// 判断这个回包是否是本次请求匹配?
if ipAndPayload.Dst != dst || ipAndPayload.Src != local || ipAndPayload.SrcPort != *sport || ipAndPayload.DstPort != *dport || ipAndPayload.ID != id {
continue
}
// 如果匹配,这已经到达目的主机了,把时延打印出来,返回
fmt.Printf("%d: %v %v\n", ttl, peer, time.Since(start))
return
}
// 如果是中间设备而回包
if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
te, ok := replyMsg.Body.(*icmp.TimeExceeded)
if !ok {
continue
}
// 抽取匹配项
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// 判断这个回包是否是本次请求匹配?
if ipAndPayload.Dst != dst || ipAndPayload.Src != local || ipAndPayload.SrcPort != *sport || ipAndPayload.DstPort != *dport || ipAndPayload.ID != id {
continue
}
// 打印中间设备IP和时延
fmt.Printf("%d: %v %v\n", ttl, peer, time.Since(start))
if peer.String() == dst {
return
}
continue loop_ttl
}
}
}
}
// 构造IP包和UDP包
func encodeUDPPacket(localIP, dstIP string, id uint16, ttl uint8, payload []byte) ([]byte, error) {
ip := &layers.IPv4{
Id: uint16(id), // ID
SrcIP: net.ParseIP(localIP),
DstIP: net.ParseIP(dstIP),
Version: 4,
TTL: ttl, // ttl
Protocol: layers.IPProtocolUDP,
}
udp := &layers.UDP{
SrcPort: layers.UDPPort(*sport),
DstPort: layers.UDPPort(*dport),
}
udp.SetNetworkLayerForChecksum(ip)
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
err := gopacket.SerializeLayers(buf, opts, ip, udp, gopacket.Payload(payload))
return buf.Bytes(), err
}
type ipAndPayload struct {
Src string
Dst string
SrcPort int
DstPort int
ID uint16
TTL int
Payload []byte
}
// 抽取匹配项
func extractIPAndPayload(body []byte) (*ipAndPayload, error) {
if len(body) < ipv4.HeaderLen {
return nil, fmt.Errorf("ICMP packet too short: %d bytes", len(body))
}
ipHeader, payload := body[:ipv4.HeaderLen], body[ipv4.HeaderLen:] // 抽取ip header和payload(UDP packet的前8个字节)
iph, err := ipv4.ParseHeader(ipHeader)
if err != nil {
return nil, fmt.Errorf("Error parsing IP header: %s", err)
}
srcPort := binary.BigEndian.Uint16(payload[0:2]) // 前两个字节是源端口
dstPort := binary.BigEndian.Uint16(payload[2:4]) // 接下来两个字节是目的端口
return &ipAndPayload{
Src: iph.Src.String(),
Dst: iph.Dst.String(),
SrcPort: int(srcPort),
DstPort: int(dstPort),
ID: uint16(iph.ID),
TTL: iph.TTL,
Payload: payload,
}, nil
}
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 {
return ipNet.IP.String()
}
}
}
panic("no local IP address found")
}


注意使用root权限或者给程序加cap_net_raw,当然最简单的就是使用root进行测试了。

使用UDP包探测

Go标准库是支持发送UDP包的,所以我们也可以使用标准库来发送探测包,使用相同的icmp包处理返回的ICMP消息。

可以为什么我们不一开始就介绍这种方式呢?

这是因为标准库为我们封装的太好了,所以我们基本上只能发送UDP包,很难设置IP Header,所以每办法设置ip header中的ID (ttl可以使用net扩展包中的ipv4来设置),这样就少了一项匹配项,只能通过源目IP和原木端口进行判断了。

使用标准库下面的方法创建发送的net.PacketConn:

1
wconn, err := net.ListenPacket("ip4:udp", local)

因为我们没有办法设置IP header中的ttl,还需要创建一个ipv4.PacketConn来设置TTL:

1
pconn := ipv4.NewPacketConn(wconn)

还是使用rconn来读取icmp包:

1
rconn, err := icmp.ListenPacket("ip4:icmp", local)

完整代码如下:

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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package main
import (
"encoding/binary"
"flag"
"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
maxHops = 64
)
var (
sport = flag.Int("sport", 12345, "source port")
dport = flag.Int("p", 33434, "destination port")
)
func main() {
flag.Parse()
if len(os.Args) != 2 {
log.Fatalf("usage: %s host", os.Args[0])
}
dst := os.Args[1]
dstAddr, err := net.ResolveIPAddr("ip4", dst)
if err != nil {
log.Fatalf("failed to resolve IP address for %s: %v", dst, err)
}
timeout := 3 * time.Second
local := localAddr()
// 使用net.PacketConn发送udp请求,发送的数据只是udp layer
wconn, err := net.ListenPacket("ip4:udp", local)
if err != nil {
log.Fatalf("failed to listen packet: %v", err)
}
defer wconn.Close()
pconn := ipv4.NewPacketConn(wconn) // 用来设置ttl
// 此net.PacketConn处理返回的icmp的包
rconn, err := icmp.ListenPacket("ip4:icmp", local)
if err != nil {
log.Fatalf("Failed to create ICMP listener: %v", err)
}
defer rconn.Close()
loop_ttl:
for ttl := 1; ttl <= maxHops; ttl++ {
pconn.SetTTL(ttl) // 设置ttl
*dport++
data, err := encodeUDPPacket(local, dst, uint8(ttl), []byte("Hello, are you there?"))
if err != nil {
log.Printf("%d: %v", ttl, err)
continue
}
// 写入udp探测包
start := time.Now()
_, err = wconn.WriteTo(data, dstAddr)
if err != nil {
log.Printf("%d: %v", ttl, err)
continue
}
// listen for the reply
replyBytes := make([]byte, 1500)
if err := rconn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
log.Fatalf("Failed to set read deadline: %v", err)
}
for i := 0; i < 3; i++ {
// 读取icmp包
n, peer, err := rconn.ReadFrom(replyBytes)
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
fmt.Printf("%d: *\n", ttl)
continue loop_ttl
} else {
log.Printf("%d: Failed to parse ICMP message: %v", ttl, err)
}
continue
}
// 解析 ICMP message
replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n])
if err != nil {
log.Printf("%d: Failed to parse ICMP message: %v", ttl, err)
continue
}
if replyMsg.Type == ipv4.ICMPTypeDestinationUnreachable {
te, ok := replyMsg.Body.(*icmp.DstUnreach)
if !ok {
continue
}
// 抽取匹配项
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// 根据四元组做匹配检查
if ipAndPayload.Dst != dst || ipAndPayload.Src != local || ipAndPayload.SrcPort != *sport || ipAndPayload.DstPort != *dport {
continue
}
fmt.Printf("%d: %v %v\n", ttl, peer, time.Since(start))
return
}
if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
te, ok := replyMsg.Body.(*icmp.TimeExceeded)
if !ok {
continue
}
// 抽取匹配项
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// 根据四元组做匹配检查
if ipAndPayload.Dst != dst || ipAndPayload.Src != local || ipAndPayload.SrcPort != *sport || ipAndPayload.DstPort != *dport {
continue
}
fmt.Printf("%d: %v %v\n", ttl, peer, time.Since(start))
if peer.String() == dst {
return
}
continue loop_ttl
}
}
}
}
func encodeUDPPacket(localIP, dstIP string, id uint16, ttl uint8, payload []byte) ([]byte, error) {
ip := ......
udp := ......
udp.SetNetworkLayerForChecksum(ip)
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
// 注意这里我们只使用udp和payload做序列化,并没有使用ip layer
err := gopacket.SerializeLayers(buf, opts, udp, gopacket.Payload(payload))
return buf.Bytes(), err
}
type ipAndPayload struct {
Src string
Dst string
SrcPort int
DstPort int
ID uint16
TTL int
Payload []byte
}
func extractIPAndPayload(body []byte) (*ipAndPayload, error) {
......
}
func localAddr() string {
......
}

整体代码和上一节的代码类似,只不过我们没有办法设置ip header了。只能通过ipv4.PacketConn设置一下ttl。

使用TCP包探测

和上面的UDP方法类似,我们也可以发送TCP的包进行探测。

我们只会发送TCP的PSH包 (syn包也可以), 中间设备会返回ICMP TimeExceeded包,目的主机极大可能认为这是一个非法的包,直接把这个包丢弃,而不是返回一个ICMP DestinationUnreachable,所以你可能需要等待最大TTL探测完。

发送这个探测包理论不会对目标主机造成影响,因为TTL已经为0了。

发送我们使用下面的wconn:

1
wconn, err := net.ListenPacket("ip4:tcp", local)

接收icmp包我们还是使用下面的rconn:

1
rconn, err := icmp.ListenPacket("ip4:icmp", local)

每次构造一个TCP PSH包进行探测,这里我们的PSH包的payload没有设置,如有需要你也可以加上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pconn.SetTTL(ttl)
seq++
data, err := encodeTCPPacket(local, dst, id, uint8(ttl), seq)
if err != nil {
log.Printf("%d: %v", ttl, err)
continue
}
start := time.Now()
_, err = wconn.WriteTo(data, dstAddr)
if err != nil {
log.Printf("%d: %v", ttl, err)
continue
}

处理 ICMP回包的方法基本和上面类似。

上面发送UDP包的时候不是没有办法设置IP header的ID么?TCP探测包有了新的途径。TCP 包中的前两个字节是源端口,接下来两个字节是目的端口,再接下来四个字节是ID,我们正好可以使用这个id做匹配。
所以抽取匹配项的时候我们把这个id抽取出来了,当然发送的时候也使用探测段的进程id进行了设置。

这里我们还尝试把设备的IP地址转换成域名,更方便的检查中间设备所在的区域。

如果我们能结合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
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package main
import (
"encoding/binary"
"flag"
"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
maxHops = 64
)
var (
sport = flag.Int("sport", 12345, "source port")
dport = flag.Int("p", 33433, "destination port")
)
// 使用IP packet的ID检查
func main() {
flag.Parse()
if len(os.Args) != 2 && len(os.Args) != 4 {
log.Fatalf("usage: %s host", os.Args[0])
}
dst := os.Args[1]
timeout := time.Second
dstAddr, err := net.ResolveIPAddr("ip4", dst)
if err != nil {
log.Fatalf("failed to resolve IP address for %s: %v", dst, err)
}
// 发送tcp的net.PacketConn
local := localAddr()
wconn, err := net.ListenPacket("ip4:tcp", local)
if err != nil {
log.Fatalf("failed to listen packet: %v", err)
}
defer wconn.Close()
pconn := ipv4.NewPacketConn(wconn) // 用来设置tos
// 读取icmp的net.PacketConn
rconn, err := icmp.ListenPacket("ip4:icmp", local)
if err != nil {
log.Fatalf("Failed to create ICMP listener: %v", err)
}
defer rconn.Close()
// ID, 这里还增加了一个seq, 使用id+seq来设置tcp 的id
id := uint16(os.Getpid() & 0xffff)
seq := uint32(0)
loop_ttl:
for ttl := 1; ttl <= maxHops; ttl++ {
pconn.SetTTL(ttl)
seq++
// 构造一个tcp psh包
data, err := encodeTCPPacket(local, dst, id, uint8(ttl), seq)
if err != nil {
log.Printf("%d: %v", ttl, err)
continue
}
// 发送
start := time.Now()
_, err = wconn.WriteTo(data, dstAddr)
if err != nil {
log.Printf("%d: %v", ttl, err)
continue
}
replyBytes := make([]byte, 1500)
for i := 0; i < 3; i++ {
if err := rconn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
log.Fatalf("Failed to set read deadline: %v", err)
}
// 读取icmp包
n, peer, err := rconn.ReadFrom(replyBytes)
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
fmt.Printf("%d: *\n", ttl)
continue loop_ttl
} else {
log.Printf("%d: Failed to parse ICMP message: %v", ttl, err)
}
continue
}
rconn.SetReadDeadline(time.Time{})
// 解析 ICMP message
replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n])
if err != nil {
log.Printf("%d: Failed to parse ICMP message: %v", ttl, err)
continue
}
if replyMsg.Type == ipv4.ICMPTypeDestinationUnreachable { // 其实无用
te, ok := replyMsg.Body.(*icmp.DstUnreach)
if !ok {
continue
}
// 抽取匹配项
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// 检查是否和探测包匹配
if ipAndPayload.Dst != dst || ipAndPayload.Src != local || ipAndPayload.SrcPort != *sport || ipAndPayload.DstPort != *dport || ipAndPayload.ID != int(id)+int(seq) {
continue
}
fmt.Printf("%d: %v %v\n", ttl, peer, time.Since(start))
return
}
if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
te, ok := replyMsg.Body.(*icmp.TimeExceeded)
if !ok {
continue
}
// 抽取匹配项
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// 检查是否和探测包匹配
if ipAndPayload.Dst != dst || ipAndPayload.Src != local || ipAndPayload.SrcPort != *sport || ipAndPayload.DstPort != *dport || ipAndPayload.ID != int(id)+int(seq) {
continue
}
fmt.Printf("%d: %v %v\n", ttl, peer, time.Since(start))
if peer.String() == dst {
return
}
continue loop_ttl
}
}
}
}
func encodeTCPPacket(localIP, dstIP string, id uint16, ttl uint8, seq uint32) ([]byte, error) {
ip := &layers.IPv4{
SrcIP: net.ParseIP(localIP),
DstIP: net.ParseIP(dstIP),
Version: 4,
TTL: ttl,
Protocol: layers.IPProtocolTCP,
}
tcp := &layers.TCP{
SrcPort: layers.TCPPort(*sport),
DstPort: layers.TCPPort(*dport),
Seq: uint32(id) + seq,
PSH: true,
}
tcp.SetNetworkLayerForChecksum(ip)
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
err := gopacket.SerializeLayers(buf, opts, tcp)
return buf.Bytes(), err
}
type ipAndPayload struct {
Src string
Dst string
SrcPort int
DstPort int
ID int
Payload []byte
}
func extractIPAndPayload(body []byte) (*ipAndPayload, error) {
if len(body) < ipv4.HeaderLen {
return nil, fmt.Errorf("ICMP packet too short: %d bytes", len(body))
}
ipHeader, payload := body[:ipv4.HeaderLen], body[ipv4.HeaderLen:]
iph, err := ipv4.ParseHeader(ipHeader)
if err != nil {
return nil, fmt.Errorf("Error parsing IP header: %s", err)
}
srcPort := binary.BigEndian.Uint16(payload[0:2])
dstPort := binary.BigEndian.Uint16(payload[2:4])
id := binary.BigEndian.Uint32(payload[4:8])
return &ipAndPayload{
Src: iph.Src.String(),
Dst: iph.Dst.String(),
SrcPort: int(srcPort),
DstPort: int(dstPort),
ID: int(id),
Payload: payload,
}, nil
}
func localAddr() string {
......
}

使用ICMP包探测

最终,如果没有特殊的需求,我们可以使用简单的ICMP包作为探测请求包。

使用icmp探测的好处就是我们可以使用一个icmp的PacketConn来进行发送和读取,第二个好处就是我们可以使用icmp中的Echo消息中的ID和seq进行匹配。

这里我们没有必要自己进行匹配项的抽取了,直接尝试把返回的结果解析成Echo消息进行匹配项检查即可:

1
2
3
4
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok || echoReply.ID != id || echoReply.Seq != seq {
continue
}

完整的代码如下:

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
package main
import (
"fmt"
"log"
"net"
"os"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
const (
protocolICMP = 1
maxHops = 64
)
func main() {
if len(os.Args) != 2 {
log.Fatalf("Usage: %s host", os.Args[0])
}
dst := os.Args[1]
timeout := time.Second * 3
// resolve the host name to an IP address
ipAddr, err := net.ResolveIPAddr("ip4", dst)
if err != nil {
log.Fatalf("Failed to resolve IP address for %s: %v", dst, err)
}
// create a socket to listen for incoming ICMP packets
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
log.Fatalf("Failed to create ICMP listener: %v", err)
}
defer conn.Close()
id := os.Getpid() & 0xffff
seq := 0
loop_ttl:
for ttl := 1; ttl <= maxHops; ttl++ {
// set the TTL on the socket
if err := conn.IPv4PacketConn().SetTTL(ttl); err != nil {
log.Fatalf("Failed to set TTL: %v", err)
}
seq++
// create an ICMP message
msg := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: id,
Seq: seq,
Data: []byte("hello, are you there?"),
},
}
// serialize the ICMP message
msgBytes, err := msg.Marshal(nil)
if err != nil {
log.Fatalf("Failed to serialize ICMP message: %v", err)
}
// send the ICMP message
start := time.Now()
if _, err := conn.WriteTo(msgBytes, ipAddr); err != nil {
log.Printf("%d: %v", ttl, err)
continue loop_ttl
}
// listen for the reply
replyBytes := make([]byte, 1500)
if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
log.Fatalf("Failed to set read deadline: %v", err)
}
for i := 0; i < 3; i++ {
n, peer, err := conn.ReadFrom(replyBytes)
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
fmt.Printf("%d: *\n", ttl)
continue loop_ttl
} else {
log.Printf("%d: Failed to parse ICMP message: %v", ttl, err)
}
continue
}
// parse the ICMP message
replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n])
if err != nil {
log.Printf("%d: Failed to parse ICMP message: %v", ttl, err)
continue
}
// check if the reply is an echo reply
if replyMsg.Type == ipv4.ICMPTypeEchoReply {
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok || echoReply.ID != id || echoReply.Seq != seq {
continue
}
fmt.Printf("%d: %v %v\n", ttl, peer, time.Since(start))
break loop_ttl
}
if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok || echoReply.ID != id || echoReply.Seq != seq {
continue
}
var raddr = peer.String()
names, _ := net.LookupAddr(raddr)
if len(names) > 0 {
raddr = names[0] + " (" + raddr + ")"
} else {
raddr = raddr + " (" + raddr + ")"
}
fmt.Printf("%d: %v %v\n", ttl, raddr, time.Since(start))
continue loop_ttl
}
}
}
}

我们使用这个程序探索一下github.com的机器,我使用的是阿里云的机器,消息经过了阿里云北京机房内网、北京电信、杭州电信、中国电信香港节点、日本节点、新加坡节点达到了新加坡机房。

下一篇,点赞数如果是偶数,我们介绍单播、组播和广播,点赞数如果是奇数,我们介绍如何发送IP包,如果点赞数为0,本系列停更,我们去更新Go并发和Rust并发的系列。