Non-hassle, Easy, And Simple Ping Tool In Go
We all know that one of the best tools for networking is ping. We ping servers, edge devices, and anything in a network testing connectivity. How does it work though? Let’s begin by understanding what an ICMP packet contains, and then continue to build a simple ping tool in Go.
Understanding ICMP Packets
Starting with the basics, let’s look at the contents of the “ping” we send. The ping is essentially an ICMP packet that is 64 bytes at minimum. I say minimum as on top of that minimum, we have the data/payload attached to it which varies. This is likely one of the reasons we can adjust the packet sizes in the original tool. We can check roundtrip (send a message from and receive) time to determine whether we meet the requirements. The packet contains the type, code, checksum, and extended header. Below are the sizes of each.
- Type – 8 bits
- Code – 8 bits
- Checksum – 16 bits
- Extended header – 32 bits
- Data/Payload (Variable Length)
ICMP packet contents
Now let’s take a look at the contents of each of the items in the packet. It’s important to remember that all numbers shown in each packet content are represented in bits of size shown in the list above.
Types of messages can be either errors or ways of requesting and responding. An Echo-Request will receive an Echo-Reply if successful. If the TTL(time to live) value reaches zero, the response is 11, corresponding to the type Time Exceeded. Here is a list of values and their corresponding meanings:
- 0 – Echo Reply
- 3 – Destination Unreachable
- 5 – Redirect Message
- 8 – Echo Request
- 11 – Time Exceeded
- 12 – Parameter Problem
The code is an 8-bit packet format. It carries additional information about the error message and type.
The checksum is a 16-bit field. It’s used to check the number of bits of the message and to enable the ICMP tool to ensure the data is complete. The core responsibility is to validate types and codes.
The extended header is 32 bits and points out problems in IP messages. Byte locations are identifiers of where the problem lies.
Data/payload varies in length. Contains the data being transmitted. This can be anything, as we expect to check the roundtrip time or whether we can reach a given destination.
The table below contains a set of combinations of type-code-descriptions. These are valid options that a developer can choose to either send or respond to.
Type | Code | Description |
---|---|---|
0 – Echo Reply | 0 | Echo Reply |
3 – Destination Unreachable | 0 | Destination Network Unreachable |
1 | Destination Host Unreachable | |
2 | Destination Protocol Unreachable | |
3 | Destination Port Unreachable | |
4 | Fragmentation is needed and the DF flag set | |
5 | Service Route Failed | |
5 – Redirect Message | 0 | Redirect the datagram for the network |
1 | Redirect datagram for the host | |
2 | Redirect datagram for the Type of Service and network | |
3 | Redirect datagram for the service and host | |
8 – Echo Request | 0 | Echo Request |
9 – Router Advertisement | 0 | Use to discover the addresses of operational routers |
10 – Router Solicitation | 0 | |
11 – Time Exceeded | 0 | Time to live exceeded in transit |
12- Parameter Problem | 0 | Pointer indicates an error |
1 | Missing required option | |
2 | Bad length | |
13 – Timestamp | 0 | Used for time synchronization |
14 – Timestamp Reply | 0 | Reply to timestamp message |
Coding of Ping Tool in Go
N ow it’s time to move on to the process. I’ve used Go to provide an implementation that takes in the destination address and a subset of options from the original tool. The options I have implemented are count (-c), wait between pings (-i), packet size (-s), and timeout duration of the sent packet (-t).
Step by step, using the information provided up to this point, let’s build a ping tool in Go.
CLI Flags and Arguments
First of all, we need to process the command line arguments. Although there are powerful tools Cobra and Viper for go, I chose the builtin flag package as it would be overkill to use the others. Other languages have their packages to get the argumenta and flags such as Python have argumentparser.
type PingCLIOptions struct {
Count int `default:"-1"`
Wait int `default:"1"`
PacketSize int `default:"0"`
Timeout int `default:"3"`
}
func parseFlags() *PingCLIOptions {
var count, wait, packetSize, timeout int
flag.IntVar(&count, "c", -1, "Number of pings to send")
flag.IntVar(&wait, "i", 1, "Time to wait between pings in seconds")
flag.IntVar(&packetSize, "s", 8, "Packet size to deliver")
flag.IntVar(&timeout, "t", 3, "Timeout before ping exits in seconds")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Options:\\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "dst\\n the destination IP address\\n")
}
flag.Parse()
return NewPingCLIOptions(count, wait, packetSize, timeout)
}
func main() {
var dst *net.IPAddr
timeTotal := time.Duration(0) // used to calculate statistics
cliOptions := parseFlags()
ctx := context.Background()
if flag.NArg() != 1 {
flag.Usage()
os.Exit(1)
}
}
Here the PingCLIOptions
is a way to store the default and set options of the tool. The parseFlags
function defines the flags and the Usage
function. Then immediately it will parse the flags. The section above is only a part of the main function.
Pinging using the socket
The second step is to create a packet and send it to the destination. A message object is created using the package “x/net/icmp” in Go. This will contain the type, code, and message body which will also contain the payload. Here is a sample of the ping
function called later on in the main function.
We’ve already created a raw socket connection where we read and write packets. We write the message and send it to the desired destination using the socket. At this point, we also start a timer to measure roundtrip time and wait to read a message. We await a response because reading from a socket is a blocking call.
An important detail here is to set the read deadline flag for each read message. This is to not wait i definitely for a response and accept the packet is either lost or that it simply took too long. Nobody wants their machines sitting idle waiting for a single client to respond.
We process this and save the outcome whether it is a success or not when a response is read. Statistics can be provided to the end user.
func ping(ctx context.Context, stats *PingStats) time.Duration {
icmpPacket := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Seq: stats.Seq,
Data: make([]byte, stats.PacketSize-8),
},
}
wb, err := icmpPacket.Marshal(nil)
if err != nil {
fmt.Print(err)
}
socket := ctx.Value(socketKey).(*icmp.PacketConn)
timeoutDuration := ctx.Value(timeoutDurationKey).(time.Duration)
dst := ctx.Value(dstKey).(net.Addr)
var msg string
startTime := time.Now()
// Set deadline to read the incoming message
socket.SetReadDeadline(time.Now().Add(time.Duration(timeoutDuration)))
// Send ping to client
if _, err = socket.WriteTo(wb, dst); err != nil {
msg = fmt.Sprintf("%s", err)
}
var b []byte
_, _, err = socket.ReadFrom(b)
elapsedTime := time.Since(startTime)
if err != nil {
msg = fmt.Sprintf("%s", err)
}
if msg != "" {
stats.Failures++
fmt.Printf("%s ttl=%d icmp_seq=%d: %s\\n",
ctx.Value(dstKey), stats.TTL, stats.Seq, msg)
} else {
stats.Successes++
msg := "%d bytes transferred from %s ttl=%d icmp_seq=%d roundtrip time %s\\n"
fmt.Printf(msg, stats.PacketSize, ctx.Value(dstKey), stats.TTL, stats.Seq,
elapsedTime.Truncate(time.Microsecond))
}
return elapsedTime
}
The program does the following in the exact order:
- Create an ICMP packet which is called Message, and set the type to
Echo
- Run
Marshal
to convert the packet into a byte slice (array) - Get the socket, timeout duration, and the destination address from the context
- Save the start time and set the read deadline using the timeout duration variable
- The packet is sent using the socket
- Wait for the response to arrive and read it into
b
of type byte slice, and get the time it took for the round trip - Update the stats object The stats object is defined as the following code block. This is passed to the function as a pointer.
- Return the roundtrip time and print the outcome
type PingStats struct {
Successes int
Failures int
PacketSize int
TTL int
Seq int
}
Completing the main function
The main function needs addition. The flags are processed, now a socket needs to be opened, and a PingStats
object to be created, populate the context to pass information to the ping function and finally run ping depending on the count variable.
func main() {
// .
// .
// .
// Previously mentioned code above
dst, err := net.ResolveIPAddr("ip4", os.Args[flag.NFlag()+2])
if err != nil {
fmt.Println("Invalid IP address format.")
os.Exit(1)
}
ctx = context.WithValue(ctx, dstKey, dst)
socket, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
fmt.Print(err)
}
ctx = context.WithValue(ctx, socketKey, socket)
defer socket.Close()
ttl, err := socket.IPv4PacketConn().TTL()
if err != nil {
fmt.Print(err)
}
stats := &PingStats{
PacketSize: cliOptions.PacketSize,
TTL: ttl,
Seq: 0,
Successes: 0,
Failures: 0,
}
timeoutDuration := time.Duration(cliOptions.Timeout) * time.Second
waitDuration := time.Duration(cliOptions.Wait) * time.Second
ctx = context.WithValue(ctx, timeoutDurationKey, timeoutDuration)
ctx = context.WithValue(ctx, waitDurationKey, waitDuration)
if cliOptions.Count == -1 {
for {
timeTotal += ping(ctx, stats)
// Sleep until next ping
time.Sleep(waitDuration)
stats.Seq++
}
} else {
count := 0
for count < cliOptions.Count {
timeTotal += ping(ctx, stats)
// Sleep until next ping
time.Sleep(waitDuration)
count++
stats.Seq++
}
}
displayStats(stats, timeTotal)
}
Displaying statistics of the operation
Now we can call the ping function, run using the CLI options, and collect the statistics of success count. The only section that hasn’t been defined is the displayStats
function. It is shown below.
func displayStats(stats *PingStats, totalTime time.Duration) {
aveTime := totalTime / time.Duration(stats.Seq)
// Add one to prevent from divide by zero
packetLossRate := (1 - (float32(stats.Successes) / float32(stats.Seq))) * 100.0
fmt.Printf("\\n---------- Ping Statistics ----------\\n")
fmt.Printf("Average %s, Success: %d, Failure: %d, Paket Loss %.2f %%\\n",
aveTime.String(), stats.Successes, stats.Failures, packetLossRate)
}
This is how a packet is sent to a given destination. The ping process is simply an ICMP packet that is sent and receives a reply. The roundtrip time is captured and displayed. It is fairly simple and widely used for debugging purposes in networking.
Thanks for reading this post!
If you’d like to contact me about anything, send feedback, or want to chat feel free to:
Send an email: andy@serra.us
Archives
Calendar
M | T | W | T | F | S | S |
---|---|---|---|---|---|---|
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 |
Leave a Reply