Skip to content

Menu

  • Home
  • About
  • Resume
  • Contact

Archives

  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • May 2023
  • April 2023
  • March 2023
  • September 2022
  • April 2022
  • March 2022

Calendar

March 2024
M T W T F S S
 123
45678910
11121314151617
18192021222324
25262728293031
« May   Apr »

Categories

  • Analysis
  • Personal View
  • Ping Pong Tracker – Embedded Camera System
  • Projects
  • Service Metrics
  • Tutorial
  • Web Scrapers

Copyright Andrew Serra 2025 | Theme by ThemeinProgress | Proudly powered by WordPress

Andrew Serra
  • Home
  • About
  • Resume
  • Contact
Written by Andrew SerraMarch 13, 2024

Non-hassle, Easy, And Simple Ping Tool In Go

Projects . Tutorial Article

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

Read moreGuide to Develop A Simple Web Scraper Using Scrapy

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
Read moreService Metrics Project, A Step-by-Step Journey to Analytics Development

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.

Read moreDesigning the data flow of a Ping Pong Tracker on a Embedded Device

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.

Read moreSnickerdoodle FPGA Project: Image Processing and Position Visualization using Glasgow Linux

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.

TypeCodeDescription
0 – Echo Reply0Echo Reply
3 – Destination Unreachable0Destination Network Unreachable
1Destination Host Unreachable
2Destination Protocol Unreachable
3Destination Port Unreachable
4Fragmentation is needed and the DF flag set
5Service Route Failed
5 – Redirect Message0Redirect the datagram for the network
1Redirect datagram for the host
2Redirect datagram for the Type of Service and network
3Redirect datagram for the service and host
8 – Echo Request0Echo Request
9 – Router Advertisement0Use to discover the addresses of operational routers
10 – Router Solicitation0
11 – Time Exceeded0Time to live exceeded in transit
12- Parameter Problem0Pointer indicates an error
1Missing required option
2Bad length
13 – Timestamp0Used for time synchronization
14 – Timestamp Reply0Reply 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).

Read moreSimplified Credit Card Validator API in Go

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.

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
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)
    }
}
Read moreWord Counter Using Producer-Consumer Pattern

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.

Read moreDocker Containers for React Deployment

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.

Read moreHow To Minimize Docker Container Sizes

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.

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
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:

  1. Create an ICMP packet which is called Message, and set the type to Echo
  2. Run Marshal to convert the packet into a byte slice (array)
  3. Get the socket, timeout duration, and the destination address from the context
  4. Save the start time and set the read deadline using the timeout duration variable
  5. The packet is sent using the socket
  6. 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
  7. Update the stats object The stats object is defined as the following code block. This is passed to the function as a pointer.
  8. Return the roundtrip time and print the outcome
1
2
3
4
5
6
7
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.

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
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.

1
2
3
4
5
6
7
8
9
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

Contact Me

  • LinkedIn
  • GitHub
  • Twitter
  • Instagram
Tags: cli tool, code, go, golang, icmp, networking, ping

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Archives

  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • May 2023
  • April 2023
  • March 2023
  • September 2022
  • April 2022
  • March 2022

Calendar

March 2024
M T W T F S S
 123
45678910
11121314151617
18192021222324
25262728293031
« May   Apr »

Categories

  • Analysis
  • Personal View
  • Ping Pong Tracker – Embedded Camera System
  • Projects
  • Service Metrics
  • Tutorial
  • Web Scrapers

Copyright Andrew Serra 2025 | Theme by ThemeinProgress | Proudly powered by WordPress