ping, when you come back, please tell me the address of the router you passed

Original link: https://colobu.com/2023/05/07/ping-and-record-route/

In the article on using Go to implement the ping tool , we implemented the basic functions of the ping tool ourselves, and we also learned the principle of the underlying implementation of ping. Reader @ZerOne asks a question: Can the function of one-way trace routing be implemented, similar to ping -R , to tracert B from A and display the path from B to A at the same time?

Record Route background knowledge

I haven’t used the -R parameter of ping before, and I find it interesting that ZerOne asks this question. Because I have been doing network-related work in the past two years, especially the ability of network monitoring. If, as ZerOne said, if ping has this ability, it can be very well used in our network monitoring, so I quickly searched for this on the Internet. Related information, and using Go to achieve similar functions, finally formed this article, if it is too long to read: the answer to this question is yes, but it is not realistic.

This can be regarded as an interlude in the Go advanced network programming series. Another reader mentioned the difference between sendmmsg and sendv (should mean writv) when commenting on sending packets in batches. I will introduce them in the next article. It can be regarded as a knowledge point for insertion. Welcome everyone to discuss the knowledge of advanced network programming, private message me or join the Go advanced programming seminar group for communication.

First, man ping to see the introduction of -R parameter in its help documentation:

-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 Log routes. Include the RECORD_ROUTE option in the Echo request packet, and display the cached route in the returned packet. Note that the IPV4 header can only store up to 9 such routing addresses. Many nodes ignore or discard this option.

The ping tool of Mac OS has marked this option as deprecated, even if you add this parameter, it is equivalent to a no-op operation.

The function of RR is realized by using the option of ipv4 header.

The normal ipv4 header is 20 bytes (each row in the figure is 4 bytes, 32bit), but the protocol also sets Option, which can expand its basic functions.

IHL is 4 bits, representing the length of the IP header (the number of lines), because the maximum of 4 bits is 15, so the maximum header of ipv4 is 15 * 4byte = 60 byte, and only 40 bytes are left for the option (60 byte – 40 bytes).

The format of the Option is defined in the TLV format. The first byte is the Type, the second byte is the length of the Option, and the remaining bytes contain the value of the Option:

Several options for IPv4 are defined in rfc791 :

Among them, record route (RR) is the object to be discussed in this article.

The IPv4 record route (RR) option instructs routers to record their IP addresses in packets. RR is a standard part of the Internet protocol and can be enabled on any packet. Similar to traceroute, RR records and reports IP addresses along the Internet path from source to destination, but it has several advantages over traceroute. For example, RR can splice the reverse path back to the destination hop by hop, which is invisible to traceroute and other traditional techniques; and it can discover some hops that do not respond to traceroute probes.

But this also brings security issues, especially today when cloud services are in power. If this option is enabled from the IP packet of the cloud computer room, the route it passes through the cloud computer room will go outside the cloud computer room, which will expose the network architecture of the cloud service provider, and even hackers can use other options to let the flow go away. At the same time, each data packet also adds some additional data to take up more bandwidth, which is thankless, so if your machine is a cloud virtual machine purchased, ping -R will be discarded with a high probability, maybe In the source computer room, it may also be in the destination computer room.
For example, I was testing on the cloud virtual machine of Tencent Cloud, 8.8.8.8 ignored this option, 1.1.1.1 and github.com discarded the request, and 127.0.0.1 returned this option:

But the paper The record route option is an option! put forward a new point of view, that RR has great potential for network measurement, and can be used in combination with traceroute to enhance the understanding of network topology. This paper calls for a reassessment of the potential of RR options and exploration of new ways of using them.

The above is the background knowledge of the Record Route option. Next, we will add the RR function to the original ping program we implemented.

Using Go to implement RR functions

Parsing RR options

First of all, we do a preparatory work. If we receive an IPv4 packet, we need to parse out its options. It may include multiple options, and we need to parse them all according to the TLV format.

Parsing options We still use the gopacket package, first parse T, and decide to parse L and V according to the type of T:


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

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
}

We only pay attention to the RR option, its T is 0x07 , its third byte is a pointer starting from 1, pointing to the next byte storing the routing IP address, the initial value is 4, which is the byte behind it, We need to parse out its stored IP address:


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

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, “” )
}

The code for parsing the option has been completed, and the next job is to set the RR when sending the icmp Echo request. And if you are lucky enough to receive a reply packet, you need to print out the routing list in RR.

Realize ping with RR function

Unlike the ping tool we implemented earlier, we need to go a little deeper. Because the previous implementation of ping directly sent ICMP packets without the IPv4 layer. Now that we need to set the IPv4 option, we have to manually construct IPv4 packets. There are multiple ways to send IPv4 packets. I was originally in this series In this article, we will talk about how to send IPv4 packets. Because this article introducing RR is inserted, I will introduce one of the methods in this article. For other methods, please pay attention to the WeChat official account of Bird’s Nest Chat Technology .

We can use pc, err := ipv4.NewRawConn(conn) to convert net.PacketConn to *ipvr.RawConn , and then we can send and receive packets with IPv4 header.

The key point is to construct the IP header, set the RR option to support up to 9 routes (32 byte + ptr), and the initial value of ptr is set to 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,
}
// Prepare Record Route options
recordRouteOption := layers.IPv4Option{
OptionType: 0x07 ,
OptionLength: 39 ,
OptionData: make ([] byte , 37 ),
}
recordRouteOption.OptionData [0 ] = 4
// add options
ip.Options = append (ip.Options, layers.IPv4Option{OptionType: 0x01 }, recordRouteOption)

We use the following statement to receive the return packet data:


1

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

It can read ip header as well as payload data (ICMP return packet).

If the received data is the corresponding icmp return packet, we can use the parsed data we prepared at the beginning to parse out the 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” ))
}
}

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

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 ]
// resolve the IP address of the target host
dst, err := net.ResolveIPAddr( “ip” , ​​host)
if err != nil {
log. Fatal(err)
}
local := localAddr()
// Create an ICMP connection
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)
}
// Construct IP layer
ip := layers.IPv4{
Version: 4 ,
SrcIP: net.ParseIP(local),
DstIP: dst.IP,
Protocol: layers.IPProtocolICMPv4,
TTL: 64 ,
Flags: layers.IPv4DontFragment,
}
// Prepare Record Route options
recordRouteOption := layers.IPv4Option{
OptionType: 0x07 ,
OptionLength: 39 ,
OptionData: make ([] byte , 37 ),
}
recordRouteOption.OptionData [0 ] = 4
// add options
ip.Options = append (ip.Options, layers.IPv4Option{OptionType: 0x01 }, recordRouteOption)
// Construct ICMP layer
icmpLayer := layers.ICMPv4{
TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, 0 ),
Id: uint16 (os. Getpid() & 0xffff ),
Seq: 1 ,
}
// add ICMP data
payload := [] byte ( “ping!” )
// Encapsulate to gopacket.Packet
buf := gopacket. NewSerializeBuffer()
opts := gopacket.SerializeOptions{
Compute Checksums: true ,
FixLengths: true ,
}
err = gopacket.SerializeLayers(buf, opts, &ip, &icmpLayer, gopacket.Payload(payload))
if err != nil {
panic (err)
}
// Send ICMP message
start := time. Now()
_, err = pc.WriteToIP(buf.Bytes(), dst)
if err != nil {
log. Fatal(err)
}
// Receive ICMP message
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)
// parse ICMP message
msg, err := icmp. ParseMessage(protocolICMP, payload)
if err != nil {
log. Fatal(err)
}
// print the result
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” )
}

Test it, you can receive RR information from the return packet:

This article is transferred from: https://colobu.com/2023/05/07/ping-and-record-route/
This site is only for collection, and the copyright belongs to the original author.