ping is a network tool that is widely used to test the quality and stability of network connections. When we want to know if our computer is able to communicate with other devices or servers, ping is our best friend. The ping tool is also often used to measure the connectivity and network quality between networks when we want to detect them, as it is a small but powerful network diagnostic tool that often comes with the operating system.

ping was originally developed by Mike Muuss in 1983 for Unix systems. It got its name from the sonar system of a Navy submarine, which determines the location of a target by sending a sound wave and measuring its return time. ping works similarly by sending a small packet of data to a target device and then waiting for that device to return a response to measure its response time and latency.

When we use Ping to test a network connection, it can tell us two important metrics: latency and packet loss. Latency is the time it takes from sending a ping request to receiving a response, usually measured in milliseconds. Packet loss rate is the percentage of packets lost between the ping request and the response. If the packet loss rate is too high, it indicates that there may be problems with the network connection, resulting in unstable or even unreachable data transmission.

In addition to the basic ping command, there are many other ping commands and options available. For example, you can use the “-c” option to specify the number of ping requests to send, and the “-i” option to specify the time interval between ping requests. In addition, the “-s” option can be used to specify the packet size for sending ping requests.

Although ping is a very useful tool, it has some limitations. ping test results can be affected by many factors, such as network congestion, firewalls, router drops, and so on. In addition, some devices or servers may have disabled responses to ping requests, so the results of the ping test are not available.

Despite its limitations, it is still one of the must-have tools for network administrators and users.

Principle of implementation

The ping tool is based on rfc 792 (ICMP protocol). It is a document called “Internet Control Message Protocol (ICMP) Specification”, published by Jon Postel and J. Reynolds in September 1981. This document defines the ICMP protocol, which is an important part of the TCP/IP network protocol suite.

The ICMP protocol is a network layer protocol that is used to transport messages related to network control and error handling. The protocol is often used in conjunction with the IP protocol to exchange information over the Internet. rfc 792 details the different message types in the ICMP protocol and their uses. ping is implemented using sending an Echo request to get an Echo Reply.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Echo or Echo Reply Message

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type      |     Code      |          Checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Identifier          |        Sequence Number        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Data ...
   +-+-+-+-+-

where:

  • type: 8 for echo message, 0 for echo reply message
  • code: always 0
  • checksum: checksum of the whole message
  • Identifier: used to match echo and reply, we often use process ID
  • Sequence Number: is also used to match echo and reply, for example, the same process ID, different sequence number represents different echo
  • Data: payload value. So we can use a certain size of data when we use ping to test MTU or whatever

ICMP is encapsulated in IP packets for transmission.

We will also introduce ICMP when we introduce the traceroute tool implementation in the next article.

For ping, it is simply sending an echo message, calculating the time delay after receiving the corresponding echo reply message, and calculating a packet loss if the reply is not received after the timeout.

For example, we often use the ping command, which displays packet receipt and delay, you can specify the total number of packets sent, and there is a statistic at the end.

ping

The introduction and examples in this article are for IPV4, IPv6 is similar but a bit different.

If you search ping.c, you can easily find a ping tool implemented in C. If you use Go, there are also several ways to implement it.

“Cheat” method

The easiest way to implement this is to call the ping tool that comes with the operating system.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main
import (
    "fmt"
    "os"
    "os/exec"
)
func main() {
    host := os.Args[1]
    output, err := exec.Command("ping", "-c", "3", host).CombinedOutput()
    if err != nil {
        panic(err)
    }
    fmt.Println(string(output))
}

A few simple lines of code.

Using golang.org/x/net/icmp

Go’s net extension library specifically implements the icmp protocol. We can use it to implement pings.

If you use SOCK_RAW for pinging, you need cap_net_raw permission, you can set it with the following command.

1
setcap cap_net_raw=+ep /path/to/your/compiled/binary

In Linux 3.0 new implementation there is a socket way to enable normal users to execute ping commands as well.

1
socket(PF_INET, SOCK_DGRAM, IPPROTO_ICMP)

But you also need to set the following.

1
sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"

First, we implement the non-privileged ping method of pinging. The icmp package does the wrapping for us, so we don’t have to use the underlying socket, but use icmp.ListenPacket("udp4", "0.0.0.0") to do it directly.

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
package main
import (
    "fmt"
    "log"
    "net"
    "os"
    "time"
    "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]
    // Use icmp to get a *packetconn, note that we set `udp4` for network here
    c, err := icmp.ListenPacket("udp4", "0.0.0.0")
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()
    // Generate an Echo message
    msg := &icmp.Message{
        Type: ipv4.ICMPTypeEcho,
        Code: 0,
        Body: &icmp.Echo{
            ID:   os.Getpid() & 0xffff,
            Seq:  1,
            Data: []byte("Hello, are you there!"),
        },
    }
    wb, err := msg.Marshal(nil)
    if err != nil {
        log.Fatal(err)
    }
    // Send, note that here it must be a UDP address
    start := time.Now()
    if _, err := c.WriteTo(wb, &net.UDPAddr{IP: net.ParseIP(host)}); err != nil {
        log.Fatal(err)
    }
    // Read the reply package
    reply := make([]byte, 1500)
    err = c.SetReadDeadline(time.Now().Add(5 * time.Second))
    if err != nil {
        log.Fatal(err)
    }
    n, peer, err := c.ReadFrom(reply)
    if err != nil {
        log.Fatal(err)
    }
    duration := time.Since(start)
    // The reply packet is an ICMP message, parsed first
    msg, err = icmp.ParseMessage(protocolICMP, reply[:n])
    if err != nil {
        log.Fatal(err)
    }
    // Print Results
    switch msg.Type {
    case ipv4.ICMPTypeEchoReply: // If it is an Echo Reply message
        echoReply, ok := msg.Body.(*icmp.Echo) // The message body is of type Echo
        if !ok {
            log.Fatal("invalid ICMP Echo Reply message")
            return
        }
        // Here you can judge by ID, Seq, remote address, the following one only uses two judgment conditions, it is risky
        // If another program also sends ICMP Echo with the same serial number, then it may be a reply packet from another program, but the chance of this is relatively small
        // If you add the ID judgment, it is accurate
        if peer.(*net.UDPAddr).IP.String() == host && echoReply.Seq == 1 {
            fmt.Printf("Reply from %s: seq=%d time=%v\n", host, msg.Body.(*icmp.Echo).Seq, duration)
            return
        }
    default:
        fmt.Printf("Unexpected ICMP message type: %v\n", msg.Type)
    }
}

The key codes are commented out, mainly paying attention to the parsing of reply packets and the judgment of reply packets. Especially the reply packet judgment, we need to pay particular attention to this point when implementing traceroute in the next chapter.

Implementing with ip4:icmp

Even if we want to implement privileged ping, we don’t need to use raw socket directly, we still use icmp packets.

In this scenario, our network needs to be ip4:icmp, capable of sending ICMP packets, not udp4 as above.

 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
package main
import (
    "fmt"
    "log"
    "net"
    "os"
    "time"
    "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)
    }
    // Creating an ICMP connection
    conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    // Constructing ICMP messages
    msg := &icmp.Message{
        Type: ipv4.ICMPTypeEcho,
        Code: 0,
        Body: &icmp.Echo{
            ID:   os.Getpid() & 0xffff,
            Seq:  1,
            Data: []byte("Hello, are you there!"),
        },
    }
    msgBytes, err := msg.Marshal(nil)
    if err != nil {
        log.Fatal(err)
    }
    // Send ICMP packets
    start := time.Now()
    _, err = conn.WriteTo(msgBytes, dst)
    if err != nil {
        log.Fatal(err)
    }
    // Receiving ICMP messages
    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)
        }
        n, peer, err := conn.ReadFrom(reply)
        if err != nil {
            log.Fatal(err)
        }
        duration := time.Since(start)
        // Parsing ICMP packets
        msg, err = icmp.ParseMessage(protocolICMP, reply[:n])
        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 peer.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)
                return
            }
        default:
            fmt.Printf("unexpected ICMP message type: %v\n", msg.Type)
        }
    }
}

and the above example, mainly the logic of sending is not the same, much the same, send the amount of content are ICMP Echo message, only this time to send the failure, the address is not a UDP address, but an IP address.

Using go-ping

Although the Go net extension library provides icmp packages to facilitate our pinging ability, the code is still a bit on the bottom side of processing. There is a go-ping/ping library on the web that is still used a lot and provides a more advanced or foolproof approach.

Three years on, the impact of the epidemic on the world has been subtle enough to affect the Internet, affecting the open source community. I see a lot of open source projects are no longer maintained for some reason, including this go-ping project, just rely on the author with love can not do lasting, a bit of regret, but at least it has been more mature, we use in the project no problem. prometheus community based on this project, maintain a new project: pro-bing.

The examples in its README document have explained its usage well, you can use it to achieve a similar function as the ping tool, if you want to achieve the ping function in large quantities, this library is not suitable.

The following code is a basic function of ping, nothing to say, ping 3 times to get the result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ping and collect the results
pinger, err := probing.NewPinger("github.com")
if err != nil {
    panic(err)
}
   // Number of pings
pinger.Count = 3
err = pinger.Run() // Blocking until completion or timeout
if err != nil {
    panic(err)
}
stats := pinger.Statistics() // Get statistical results
pretty.Println(stats)

If you want to implement ping under Linux, you can be a little more complex.

 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
pinger, err = probing.NewPinger("github.com")
if err != nil {
    panic(err)
}
// Listen for Ctrl-C.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
    for _ = range c {
        pinger.Stop()
    }
}()
pinger.OnRecv = func(pkt *probing.Packet) {
    fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n",
        pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt)
}
pinger.OnDuplicateRecv = func(pkt *probing.Packet) {
    fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v ttl=%v (DUP!)\n",
        pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt, pkt.TTL)
}
pinger.OnFinish = func(stats *probing.Statistics) {
    fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr)
    fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n",
        stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss)
    fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n",
        stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt)
}
fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr())
err = pinger.Run()
if err != nil {
    panic(err)
}

As mentioned earlier, processing the returned messages and matching them with the sent requests is a technical point, so how is go-ping implemented? The main thing is the following code.

 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
switch pkt := m.Body.(type) {
case *icmp.Echo:
    if !p.matchID(pkt.ID) {
        return nil
    }
    if len(pkt.Data) < timeSliceLength+trackerLength {
        return fmt.Errorf("insufficient data received; got: %d %v",
            len(pkt.Data), pkt.Data)
    }
    pktUUID, err := p.getPacketUUID(pkt.Data)
    if err != nil || pktUUID == nil {
        return err
    }
    timestamp := bytesToTime(pkt.Data[:timeSliceLength])
    inPkt.Rtt = receivedAt.Sub(timestamp)
    inPkt.Seq = pkt.Seq
    // Check for duplicate packets received
    if _, inflight := p.awaitingSequences[*pktUUID][pkt.Seq]; !inflight {
        p.PacketsRecvDuplicates++
        if p.OnDuplicateRecv != nil {
            p.OnDuplicateRecv(inPkt)
        }
        return nil
    }
    // Returned results have been obtained
    delete(p.awaitingSequences[*pktUUID], pkt.Seq)
    p.updateStatistics(inPkt)
default:
    // Very bad, not sure how this can happen
    return fmt.Errorf("invalid ICMP echo reply; type: '%T', '%v'", pkt, pkt)
}

First check the body must be *icmp.Echo type, this is the basic operation. Then check the pkt.ID, this one filters the ICMP echo reply packet which is not from this program.

Here it also adds its own uuid and time stamp of sending in the sent payload.

Here also deal with duplicate packets, uuid+seq identify the same Echo request.

Through these examples, you should understand the underlying implementation of the ping tool, collect them and return to check them when you encounter related problems.

Next, we’ll talk about the implementation of the traceroute tool, which is more complex than ping, but is related to the ICMP protocol.

Ref

  • https://colobu.com/2023/04/26/write-the-ping-tool-in-Go/