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.

ipv4 header

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.

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.

ping -R

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.

RR message

Ref

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