“implementing the ping tool”

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

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. When we want to detect the connectivity and network quality between networks, we often use the ping tool to measure, because it is a network diagnostic tool often included in the operating system, which is small but powerful.

ping was originally developed by Mike Muuss in 1983 for Unix systems. Its name comes from the sonar systems on Navy submarines, which determine a target’s location by sending out a sound wave and measuring when it returns. Ping works similarly, it sends a small packet of data to the target device, then waits for the device to return a response to measure its response time and latency.

When we use Ping to test network connections, it can tell us two important indicators: latency and packet loss rate. Latency is the time between sending a ping request and receiving a response, usually measured in milliseconds. The packet loss rate refers to the percentage of packets lost between the ping request and the response. If the packet loss rate is too high, there may be a problem with the network connection, resulting in unstable data transmission or even an inability to connect.

Besides 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 times to send ping requests, and the “-i” option to specify the time interval between Ping requests. In addition, you can use the “-s” option to specify the packet size for sending ping requests.

Although ping is a very useful tool, it has some limitations. The results of a ping test can be affected by many factors such as network congestion, firewalls, router drops, and more. Also, some devices or servers may have disabled responses to ping requests, so ping test results cannot be obtained.

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

Realization principle

The ping tool is implemented based on rfc 792 (ICMP protocol) . It is a document called “Internet Control Message Protocol (ICMP) Specification”, published in September 1981 by Jon Postel and J. Reynolds. 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 used to transmit messages related to network control and error handling. This protocol is often used together with the IP protocol for exchanging information on the Internet. RFC 792 details the different message types and their uses in the ICMP protocol. Ping is implemented by sending an Echo request to get an Echo Reply. (Currently ICMP is encapsulated in IP packets for transmission)

in:

  • type: 8 means echo message, 0 means echo reply message
  • code: always 0
  • checksum: checksum of the entire message
  • Identifier: used to match echo and reply, we often use process ID
  • Sequence Number: The serial number is also used to match echo and reply, such as the same process ID, different serial numbers represent different echoes
  • Data: payload value. So when we use ping, we can use a certain size of data to test MTU or something

When the next article introduces the implementation of the traceroute tool, we will also introduce ICMP.

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

For example, our commonly used ping command can display the packet receiving status and delay, you can specify the total number of sending, and there will be a statistical information at the end:

The introduction and examples in this article are all for IPv4, and IPv6 is similar but somewhat different.

If you search for ping.c on the Internet, you can easily find a ping tool implemented in C language. If you use Go language, there are several implementation methods.

The “cheating” way

The easiest way to implement it 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
15
16
17
18

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))
}

Just a few lines of code.

use golang.org/x/net/icmp

Go’s net extension library specifically implements the icmp protocol. We can use this to achieve ping.

Insert a knowledge point.
If you use SOCK_RAW to implement ping, you need cap_net_raw permission, you can set it by the following command:


1

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

In Linux 3.0, a new Socket method is implemented , which allows ordinary users to execute the ping command:
socket(PF_INET, SOCK_DGRAM, IPPROTO_ICMP)


1
2

But you also need to set:

shell
sudo sysctl -w net.ipv4.ping_group_range=”0 2147483647″


1
2
3
4

First of all, we implement the `n on-privileged ping` method of ping, and the icmp package encapsulates it for us, so we don’t have to use the underlying ` socket` , but directly use `i cmp.ListenPacket( “udp4” , “ 0.0.0.0′ ) ` to achieve.
The complete code is as follows:

go
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]

 // 使用icmp得到一个*packetconn,注意这里的network我们设置的`udp4` c , err := icmp. ListenPacket ( "udp4" , "0.0.0.0" ) if err != nil { log. Fatal (err) } defer c . Close () // 生成一个Echo消息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) } // 发送,注意这里必须是一个UDP地址start := time. Now () if _ , err := c . WriteTo (wb, &net. UDPAddr { IP : net. ParseIP (host)}); err != nil { log. Fatal (err) } // 读取回包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) // 得到的回包是一个ICMP消息,先解析出来msg, err = icmp. ParseMessage ( protocol ICMP , reply [: n ]) if err != nil { log. Fatal (err) } // 打印结果switch msg. Type { case ipv4. ICMPTypeEchoReply : // 如果是Echo Reply消息echoReply, ok := msg. Body .(*icmp. Echo ) // 消息体是Echo类型if !ok { log. Fatal ( "invalid ICMP Echo Reply message" ) return } // 这里可以通过ID, Seq、远程地址来进行判断,下面这个只使用了两个判断条件,是有风险的// 如果此时有其他程序也发送了ICMP Echo,序列号一样,那么就可能是别的程序的回包,只不过这个几率比较小而已// 如果再加上ID的判断,就精确了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 ) }

}


1
2
3
4
5
6
7
8

The key codes are all commented, mainly paying attention to the analysis of the return package and the judgment of the return package. Especially the judgment of the return packet, we need to pay special attention to this when implementing traceroute in the next chapter.
## Use ip4:icmp to achieve
Even if we want to achieve privileged ping, we don’t need to use raw socket directly, or use icmp package.
In this scenario, our network needs to be `ip4:icmp` , capable of sending ICMP packets, instead of `udp4` above.

go
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]

 // 解析目标主机的IP 地址dst, err := net.ResolveIPAddr( "ip" , host) if err != nil { log .Fatal(err) } // 创建ICMP 连接conn, err := icmp.ListenPacket( "ip4:icmp" , "0.0.0.0" ) if err != nil { log .Fatal(err) } defer conn.Close() // 构造ICMP 报文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) } // 发送ICMP 报文start := time .Now() _, err = conn.WriteTo(msgBytes, dst) if err != nil { log .Fatal(err) } // 接收ICMP 报文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) // 解析ICMP 报文msg, err = icmp.ParseMessage(protocolICMP, reply[:n]) if err != nil { log .Fatal(err) } // 打印结果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) } }

}


1
2
3
4
5
6
7
8
9
10
11
12
13
14

Compared with the above example, the main reason is that the logic of sending is different, and the content of the sending quota is ICMP Echo message, but this sending is invalid, and the address is not a UDP address, but an IP address.
## use go-ping
Although the Go net extension library provides the icmp package, which is convenient for us to realize the ping capability, the code is still a bit low-level. There is a [ go-ping/ping ] ( https://ift.tt/UDVEnY5 ) library on the Internet, which is still used Use a lot, providing more advanced or more foolish methods.
In the past three years, the impact of the epidemic on the world has subtly affected the Internet and the open source community. I have seen that many open source projects are no longer maintained due to some reasons, including this `go-ping` project. It is a pity that the author can’t make it last with love, but it is already relatively mature. Our project There is no problem in using it. Based on this project, the prometheus community maintains a new project: [ pro-bing ]( https://ift.tt/QN058M2 ).
The examples in its README document have seldom explained its usage. You can use it to implement a function similar to the ping tool. If you want to implement the ping function in large quantities, this library is not suitable.
The following code is a basic function of ping. There is nothing to say. Ping 3 times to get the result:

go
// ping and collect results
pinger, err := probing. NewPinger(“github.com”)
if err != nil {
panic(err)
}
// The number of pings
pinger. Count = 3
err = pinger.Run() // block until complete or timeout
if err != nil {
panic(err)
}
stats := pinger.Statistics() // Get statistical results
pretty. Println(stats)


1
2

If you want to implement the ping function under Linux, it can be a little more complicated:

go
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) }

1
2

As mentioned earlier, processing the returned message and matching it with the sending request is a technical point, so how is go- ping implemented? Mainly the following code:

go
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 // 检查是否收到重复的包if _, inflight := p.awaitingSequences[*pktUUID][pkt.Seq]; !inflight { p.PacketsRecvDuplicates++ if p.OnDuplicateRecv != nil { p.OnDuplicateRecv(inPkt) } return nil } // 已经得到返回结果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 that the body must be of type *icmp.Echo , which is the basic operation. Then check the pkt.ID, which filters out the non-program ICMP echo reply packets.

Here it also adds its own uuid and sent timestamp to the sent payload.

Duplicate packets are also processed here, uuid+seq identifies the same Echo request.

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

In the next article, we will introduce the implementation of the traceroute tool, which is more complicated than ping, but it is all related to the ICMP protocol. If you have an online topic you are interested in, “click the original text” for discussion.

Go Advanced Network Programming Series

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