In the previous article, we implemented the basic functionality of the ping tool ourselves, and we learned about the principles of the underlying ping implementation. A reader asked a question: Is it possible to implement a one-way traceroute feature, similar to ping -R, that tracert B from A and show 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 it’s interesting to me that the reader asked this. Since I’ve been doing network-related work for the last two years, especially the ability to monitor the network, if ping has this ability, as the reader said, it can be very good for our network monitoring, so I quickly searched the Internet for this related information and used Go to implement similar functions, and finally formed this article, in short: the answer to this question is, OK, but not realistic.
First man ping
look at its help documentation for the -R
parameter.
-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.
The Mac OS ping utility already marks this option as deprecated, so even if you add this parameter, it is equivalent to a no-op operation.
The RR function is implemented using the ipv4 header option.
The normal ipv4 header is 20 bytes (each line in the diagram is 4 bytes, 32 bits), but the protocol also sets Option, which can extend its basic functions.
IHL is 4 bits, representing the length of the IP header (number of lines), and since the maximum of 4 bits is 15, the maximum of the ipv4 header is 15 * 4byte = 60 byte, leaving only 40 byte (60 byte - 40 byte) for the option.
The format of Option is defined in TLV format, the first byte is the Type, the second byte is the length of the Option, and the remaining byte contains the value of the Option.
Several options for IPv4 are defined in rfc791.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
CLASS NUMBER LENGTH DESCRIPTION
----- ------ ------ -----------
0 0 - End of Option list. This option occupies only
1 octet; it has no length octet.
0 1 - No Operation. This option occupies only 1
octet; it has no length octet.
0 2 11 Security. Used to carry Security,
Compartmentation, User Group (TCC), and
Handling Restriction Codes compatible with DOD
requirements.
0 3 var. Loose Source Routing. Used to route the
internet datagram based on information
supplied by the source.
0 9 var. Strict Source Routing. Used to route the
internet datagram based on information
supplied by the source.
0 7 var. Record Route. Used to trace the route an
internet datagram takes.
0 8 4 Stream ID. Used to carry the stream
identifier.
2 4 var. Internet Timestamp.
|
One of them, record route(RR), is the object of discussion 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 stitch the reverse path back to the destination on a hop-by-hop basis, which is not visible to traceroute and other legacy 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 the norm. If the IP packet from the cloud room turns on this option, then it passes through the cloud room route to the cloud room outside, it will expose the cloud service provider’s network architecture, and even hackers can also use other options to let the traffic go specific devices to achieve the attack, while each packet also adds some additional data to account for more bandwidth, eating the effort, so your machine is purchased if the cloud virtual machine, ping -R probability are discarded, may be in the source room, may also be in the destination machine room.
In particular, RFC 1704 highlights the risk of possible misuse of the option field in the Internet Protocol. RFC 1704 argues that the record route option may be used to track the source and destination of information, and that measures need to be taken to ensure that the option is not misused. The document recommends restricting and filtering the record routing option, as well as increasing awareness and understanding of its security.
For example, I did a test on Tencent’s cloud virtual machine, 8.8.8.8
ignored this option, 1.1.1.1
and github.com
dropped the request, and 127.0.0.1
returned this option.
But the paper The record route option is an option! presents a new view that RR has great potential for network measurement and can be used in conjunction with traceroute to enhance understanding of network topology. This paper calls for re-evaluating the potential of the RR option and exploring new ways to use it.
This is the background to the Record Route option. Next we will add RR to the original ping program we implemented
Implementing RR functionality with Go
Parsing RR options
Let’s start with a preparation. If we receive an IPv4 packet, we need to parse out its options. This may include multiple options, and we need to parse them all in TLV format.
We still use the gopacket package for parsing options, first parsing T, then L and V depending on the type of T.
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
|
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 focus on the RR option, its T is 0x07
, its third byte is a pointer starting from 1, pointing to the next byte that holds the IP address of the route, initially 4, which is the byte after it, we need to parse out the IP address it has stored.
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
|
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 to parse the option is done, the next job is to set up the RR when you send the icmp Echo request. And if you are lucky enough to receive a packet back, you need to print out the list of routes in the RR.
Implementing ping with RR functionality
Unlike our previous implementation of the ping tool, we need to go a little deeper. Since the previous ping implementation sent ICMP packets directly without the IPv4 layer, now we need to set the IPv4 option, so we have to construct the IPv4 packets manually. There are multiple ways to send IPv4 packets, I was going to talk about how to send IPv4 packets in this series, but since I inserted this article about RR, I will introduce one of them in this article.
We can use pc, err := ipv4.NewRawConn(conn)
to convert net.PacketConn
to *ipvr.RawConn
to send and receive packets with IPv4 header.
The point is to construct the IP header, set the RR option to support up to 9 routes (32 byte + ptr), and set the initial value of ptr to 4.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
ip := layers.IPv4{
Version: 4,
SrcIP: net.ParseIP(local),
DstIP: dst.IP,
Protocol: layers.IPProtocolICMPv4,
TTL: 64,
Flags: layers.IPv4DontFragment,
}
// Preparing 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 reply packet data.
1
|
iph, payload, _, err := pc.ReadFrom(reply)
|
It can read ip header as well as payload data (ICMP reply packet).
If the data received by the check is the corresponding icmp reply 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
|
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
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
|
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()
// Creating 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)
}
// Constructing the IP layer
ip := layers.IPv4{
Version: 4,
SrcIP: net.ParseIP(local),
DstIP: dst.IP,
Protocol: layers.IPProtocolICMPv4,
TTL: 64,
Flags: layers.IPv4DontFragment,
}
// Preparing 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)
// Constructing the ICMP layer
icmpLayer := layers.ICMPv4{
TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, 0),
Id: uint16(os.Getpid() & 0xffff),
Seq: 1,
}
// Adding ICMP Data
payload := []byte("ping!")
// Encapsulation to 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)
}
// Send ICMP packets
start := time.Now()
_, err = pc.WriteToIP(buf.Bytes(), dst)
if err != nil {
log.Fatal(err)
}
// Receiving ICMP packets
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)
// Parsing ICMP packets
msg, err := icmp.ParseMessage(protocolICMP, payload)
if err != nil {
log.Fatal(err)
}
// Print Results
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 the RR message from the reply packet.
Ref
https://colobu.com/2023/05/07/ping-and-record-route/