Implement traceroute tool in Go

Original link: https://colobu.com/2023/05/03/write-the-traceroute-tool-in-Go/

traceroute is a utility for diagnosing network connectivity problems by determining the network path and network latency between two computers. The traceroute tool is widely used in network engineering, system management and network security.

The traceroute tool also uses ICMP, an Internet control message protocol, which allows users to detect the number of network paths and routers (or gateways) between the target host and the local host. The traceroute tool will send a series of UDP or ICMP messages to the target host, and the Time To Live (TTL) value of each message will gradually increase until it reaches the set maximum value. If it reaches the target host, the target host may return an ICMP DestinationUnreachable package, otherwise return an ICMP TimeExceeded package. By analyzing the IP address and time information in the response packet, traceroute can determine the routers in the network and the delay time of each router. Executing traceroute multiple times can help users better understand the network topology and performance bottlenecks, so as to optimize network connections.

Most importantly, traceroute utilizes the role of TTL in the IP protocol. In the IP protocol, TTL (Time to Live) is an 8-bit field that represents the maximum number of routers an IP packet can pass through in the network, that is, the time to live. Every time a router passes through, the TTL value will be reduced by one. When the TTL value becomes 0, the data packet will be discarded by the router, and an ICMP time-out message will be sent to the source host.

The role of TTL is to prevent IP data packets from circulating indefinitely in the network, that is, preventing data packets from jumping indefinitely in the network and wasting network resources. By setting the value of TTL, the data packet can be discarded after a certain number of hops in the network, thereby avoiding congestion and unnecessary load in the network. When using the traceroute tool, the value of TTL is gradually reduced, and ICMP messages are sent to routers with farther and farther distances successively, so as to obtain routing path information.

traceroute in Linux is a powerful tool with many parameters:


1
2
3
4
5
6
7

traceroute [-46 dFITUnreAV] [-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]

This article mainly introduces the underlying implementation principle of traceroute, so it will not completely reproduce the functions of all parameters of traceroute that comes with Linux, otherwise there will be a large section of code to process the logic of these parameters, this article only implements a most basic function.

Note that when the traceroute tool sends an IP packet with TTL set, it can use ICMP, UDP, ICMP or other IP-supported protocols. Linux supports UDP, TCP, and ICMP. MacOS uses the UDP protocol, but returns after the TTL is 0 The most important thing is the ICMP protocol. Apple’s traceroute.c is a good code for learning traceroute. Although it supports sending UDP protocol packets, this time we use Go language to introduce how to implement traceroute.

Speaking of protocols, some people may ask, why not use ICMP packets directly, but implement UDP and TCP packets? The actual handling of network devices on the physical network is relevant. Among the nodes at the same level, for example, there is not only one network device on the network outlet of Beijing Unicom, otherwise this device will hang up, or the bandwidth of this device will not be enough, which will lead to network packet loss or failure. Therefore, multiple devices are generally deployed. For a network flow, their source-destination addresses and source-destination ports are generally used for hashing, so as to send the data stream of the same session to the same device, so UDP is used. Or TCP can fix the five-tuple, so that the detection flow always passes through the same device, so as to check whether there is a problem with the fixed link. Of course, this is not absolute. It is possible that the same quintuple will pass through different devices.

For example, the traceroute below passes through three devices at the 9th hop (there are also multiple devices in other hops)

In Linux, by default, traceroute uses the UDP protocol. The destination port starts from the initial value of 33434, adds 1 to each TTL value, and the maximum value is 65535. This is because when the TTL value is 1, the packet arrives at the first router, and if that router has ICMP error message generation enabled, it will return an ICMP TTL Expired message to traceroute. In order to prevent the port from being occupied by other applications, traceroute uses the destination port number plus the TTL value as the destination port of the UDP packet. In this way, each TTL packet will use a different destination port number to ensure that traceroute can get the correct TTL value.

Use UDP packet detection (raw socket)

First, we probe with UDP packets, and then process the returned ICMP packets.

Here are a few technical points:

  • How to set TTL?
  • How to deal with different IP protocols?
  • How to match ICMP packets and traceroute probe UDP packets?

The first way is that we use raw socket, use gopacket to generate detection packets, set TTL, create a syscall.Socket to send UDP packets, and create an icmp.PacketConn to receive ICMP packets.

The generation of rawsocket uses the following method:


1

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

Then call the Sendto system call to send the IP+UDP packet:


1

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

The theory of reading ICMP messages can also be read using this socket, but here we use the following method to specifically receive icmp packets:


1

rconn, err := icmp.ListenPacket( “ip4:icmp” , local)

This rconn, which specializes in reading ICMP, tries to read ICMP packets:


1

replyBytes := make ([] byte , 1500 )

Under normal circumstances, ICMP return packets will be read, and other traceroute and ping return packets may also be read, so the ICMP message must be parsed first, and further judgments must be made based on the source and destination IP, ID, Seq, etc.
When a device returns an ICMP TimeExceeded packet, it will return the IP Header and the following 8 bytes of data. For UDP, the IP header contains the source and destination IP, and the first 4 bytes of UDP are exactly the source and destination port. Basically, we can use these four-tuples to match the returned packet with the request packet, but in order to further avoid errors We can also set the id in the IP Header and set it as the process id, so that adding another matching item can basically avoid misjudgment. Note here that every time our destination port is incremented by one, it will also be incremented by one, you can also fix the destination port, “do whatever you want”:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two

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
}

The complete code is as follows, and I added comments on the key lines:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
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())
// Get the address of the machine
local := localAddr()
// Generate an icmp conn to read ICMP return packets
rconn, err := icmp.ListenPacket( “ip4:icmp” , local)
if err != nil {
log.Fatalf( “Failed to create ICMP listener: %v” , err)
}
defer rconn.Close()
// get the process ID
id := uint16 (os. Getpid() & 0xffff )
// Generate a raw socket for writing udp, use syscall.IPPROTO_RAW here, because we need to set the IP Header ourselves
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
fmt.Println(err)
return
}
defer syscall.Close(fd)
// Set this item, we manually assemble the IP header by ourselves
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1 )
if err != nil {
fmt.Println(err)
return
}
// TTL increment detection
loop_ttl:
for ttl := 1 ; ttl <= maxHops; ttl++ {
*dport++
// Assemble an IP+UDP packet, the IP header uses the specified id and ttl, and the udp payload uses a string
data, err := encodeUDPPacket(local, dst, id, uint8 (ttl), [] byte ( “Hello, are you there?” ))
if err != nil {
log.Printf( “%d: %v” , ttl, err)
continue
}
// send UDP packet
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)
}
// try to read 3 times
// You can also use an infinite loop + a timeout to control
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
}
// parse 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 it is DestinationUnreachable, it means that the destination host has been detected
if replyMsg.Type == ipv4.ICMPTypeDestinationUnreachable {
te, ok := replyMsg.Body.(*icmp.DstUnreach)
if !ok {
continue
}
// extract matches
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// Determine whether this return packet matches this request?
if ipAndPayload.Dst != dst || ipAndPayload.Src != local || ipAndPayload.SrcPort != *sport || ipAndPayload.DstPort != *dport || ipAndPayload.ID != id {
continue
}
// If it matches, this has reached the destination host, print out the delay and return
fmt.Printf( “%d: %v %v\n” , ttl, peer, time.Since(start))
return
}
// If it is an intermediate device, return the packet
if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
te, ok := replyMsg.Body.(*icmp.TimeExceeded)
if !ok {
continue
}
// extract matches
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// Determine whether this return packet matches this request?
if ipAndPayload.Dst != dst || ipAndPayload.Src != local || ipAndPayload.SrcPort != *sport || ipAndPayload.DstPort != *dport || ipAndPayload.ID != id {
continue
}
// print the intermediate device IP and delay
fmt.Printf( “%d: %v %v\n” , ttl, peer, time.Since(start))
if peer. String() == dst {
return
}
continue loop_ttl
}
}
}
}
// Construct IP packets and UDP packets
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{
Compute Checksums: 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
}
// extract matches
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:] // Extract ip header and payload (the first 8 bytes of UDP packet)
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 ]) // The first two bytes are the source port
dstPort := binary.BigEndian.Uint16(payload [2 :4 ]) // The next two bytes are the destination port
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” )
}


Pay attention to use root authority or add cap_net_raw to the program. Of course, the easiest way is to use root for testing.

Use UDP packet detection

The Go standard library supports sending UDP packets, so we can also use the standard library to send probe packets, and use the same icmp packet to process the returned ICMP messages.

Why didn’t we introduce this way from the beginning?

This is because the standard library encapsulates it for us so well, so we can basically only send UDP packets, and it is difficult to set the IP Header, so every way to set the ID in the ip header (ttl can be set using ipv4 in the net extension package ), so there is one less matching item, which can only be judged by the source and destination IP and log port.

Create a net.PacketConn to send using the following method from the standard library:


1

wconn, err := net.ListenPacket( “ip4:udp” , local)

Because we have no way to set the ttl in the IP header, we also need to create an ipv4.PacketConn to set the TTL:


1

pconn := ipv4.NewPacketConn(wconn)

Or use rconn to read icmp packets:


1

rconn, err := icmp.ListenPacket( “ip4:icmp” , local)

The complete code is as follows:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
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()
// Use net.PacketConn to send udp request, the data sent is just 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) // used to set ttl
// This net.PacketConn handles the returned icmp package
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) // set ttl
*dport++
data, err := encodeUDPPacket(local, dst, uint8 (ttl), [] byte ( “Hello, are you there?” ))
if err != nil {
log.Printf( “%d: %v” , ttl, err)
continue
}
// Write udp detection packet
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++ {
// read icmp packet
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
}
// parse 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
}
// extract matches
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// Do a match check based on the quaternion
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
}
// extract matches
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// Do a match check based on the quaternion
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{
Compute Checksums: true ,
FixLengths: true ,
}
// Note that here we only use udp and payload for serialization, and do not use 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 {
}

The overall code is similar to the code in the previous section, except that we have no way to set the ip header. You can only set ttl through ipv4.PacketConn.

Use TCP packet detection

Similar to the UDP method above, we can also send TCP packets for detection.

We will only send TCP PSH packets (syn packets are also available), and the intermediate device will return ICMP TimeExceeded packets. The destination host will most likely think that this is an illegal packet, and directly discard this packet instead of returning an ICMP DestinationUnreachable, so You may need to wait for the maximum TTL to be detected.

Sending this probe packet theoretically will not affect the target host, because the TTL is already 0.

To send us use the following wconn:


1

wconn, err := net.ListenPacket( “ip4:tcp” , local)

To receive icmp packets we still use the following rconn:


1

rconn, err := icmp.ListenPacket( “ip4:icmp” , local)

Each time a TCP PSH packet is constructed for detection, the payload of our PSH packet is not set here, you can also add it if necessary:


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
}

The method of processing ICMP return packets is basically similar to the above.

Isn’t there no way to set the ID of the IP header when the UDP packet is sent above? There is a new way for TCP probe packets. The first two bytes in the TCP packet are the source port, the next two bytes are the destination port, and the next four bytes are the ID, which we can just use for matching.
So when we extract the matching item, we extract the id, and of course, we also use the process id of the detection segment to set it when sending.

Here we also try to convert the IP address of the device into a domain name, which makes it easier to check the area where the intermediate device is located.

If we can combine the IP address geographic location library, we can also display the country, city, service provider, etc. where the device is located.

The completed code is as follows:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
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” )
)
// Use the ID check of the IP packet
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)
}
// send net.PacketConn of tcp
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) // used to set tos
// Read icmp’s 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, here also adds a seq, use id+seq to set the id of tcp
id := uint16 (os. Getpid() & 0xffff )
seq := uint32 (0 )
loop_ttl:
for ttl := 1 ; ttl <= maxHops; ttl++ {
pconn. SetTTL(ttl)
seq++
// Construct a tcp psh package
data, err := encodeTCPPacket(local, dst, id, uint8 (ttl), seq)
if err != nil {
log.Printf( “%d: %v” , ttl, err)
continue
}
// send
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)
}
// read icmp packet
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{})
// parse 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 { // Actually useless
te, ok := replyMsg.Body.(*icmp.DstUnreach)
if !ok {
continue
}
// extract matches
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// Check if it matches the probe packet
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
}
// extract matches
ipAndPayload, err := extractIPAndPayload(te.Data)
if err != nil {
continue
}
// Check if it matches the probe packet
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{
Compute Checksums: 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 {
}

Use ICMP packet detection

Finally, if there is no special requirement, we can use simple ICMP packets as probe request packets.

The advantage of using icmp detection is that we can use an icmp PacketConn to send and read. The second advantage is that we can use the ID and seq in the Echo message in icmp to match.

Here we don’t need to extract the matching items ourselves, just try to parse the returned result into an Echo message to check the matching items:


1
2
3
4

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

The complete code is as follows:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
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
}
}
}
}

Let’s use this program to explore the machines of github.com. I use the machines of Alibaba Cloud. The message reaches Singapore through Alibaba Cloud’s Beijing computer room intranet, Beijing Telecom, Hangzhou Telecom, China Telecom’s Hong Kong node, Japan node, and Singapore node. engine room.

In the next article, if the number of likes is even, we will introduce unicast, multicast and broadcast. If the number of likes is odd, we will introduce how to send IP packets. If the number of likes is 0, this series will stop and we will update it. A series on Go concurrency and Rust concurrency.

This article is transferred from: https://colobu.com/2023/05/03/write-the-traceroute-tool-in-Go/
This site is only for collection, and the copyright belongs to the original author.