DEV Community

José David Ureña Torres
José David Ureña Torres

Posted on

Understanding ICMP: How to Send and Receive ICMP Messages in Go

Introduction

If you like video games you have probably heard the term ping. In gaming, ping measures the round-trip time (RTT) it takes for a packet to travel from your device to the game server and back. This is a key component of network latency. When playing an online game, higher latency means that it takes longer for messages between your computer and and the other players' computers, resulting in bad gameplay performance Schnutzel, 2021. Additionally, we can use ping to check whether we can reach a machine or if it is accessible from our own computer.

Have you wondered how it works? Ping relies on ICMP, a protocol used for network diagnostics and error reporting. Actually, you can try it out by opening a terminal and typing the following command:

bash-3.2$ ping www.google.com

PING www.google.com (142.250.64.132): 56 data bytes
64 bytes from 142.250.64.132: icmp_seq=0 ttl=114 time=54.802 ms
64 bytes from 142.250.64.132: icmp_seq=1 ttl=114 time=51.040 ms
64 bytes from 142.250.64.132: icmp_seq=2 ttl=114 time=48.910 ms
Enter fullscreen mode Exit fullscreen mode

Note the time value in each message, indicating the latency between my computer and the destination machine. The output also includes useful information like the number of bytes sent and how many hops the packet can take before being discarded (TTL).

However, if we try to reach a host that does not exist or it is not available we get the following response:

ping 127.0.0.2                                                                                                        2 х │ 10:01:29 PM
PING 127.0.0.2 (127.0.0.2): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
Request timeout for icmp_seq 2
Enter fullscreen mode Exit fullscreen mode

This article demonstrates how to send and receive ICMP messages using Go. For simplicity, I decided to cover only Echo and Echo Reply. If you want to learn more about other ICMP types and codes I encourage you to read the RFC 792 standard, the official specification for ICMP.

Why Go?

Other languages like C, C++ or even Rust might be preferred under certain circumstances. However, Go is still a solid choice for this type of applications. It offers an easy-to-read syntax, with the advantages of statically typed languages. Moreover, Go is efficient in terms of speed and executable size because it is a compiled language.

The Go icmp package provides structs for ICMP messages, allowing us to avoid crafting the packets manually. We will be using that package through this guide.

Understanding ICMP

The Internet Control Message Protocol (ICMP) is a fundamental component of the Internet Protocol Suite, primarily used for diagnostic and error-reporting purposes. Unlike TCP or UDP, ICMP is not used for transmitting application data but rather for sending control messages that help manage network behavior.

ICMP messages are commonly associated with network utilities like ping and traceroute, which rely on it to test connectivity and trace the route of packets across a network. These messages can indicate unreachable destinations, packet loss, or excessive delays, making ICMP essential for network troubleshooting and performance monitoring.

Despite its usefulness, ICMP can also be exploited for network attacks, such as ICMP flood (ping flood) or smurf attacks, leading to its restriction or filtering in certain environments. However, when used correctly, ICMP remains a vital tool for understanding and maintaining network health.

ICMP packet structure


   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 ...
   +-+-+-+-+-
Enter fullscreen mode Exit fullscreen mode

This is the structure of an ICMP Echo packet, commonly used for ping requests and replies. Here's a breakdown of each field:

  • Type (8 bits): Specifies the message type. Echo Request = 8, Echo Reply = 0.
  • Code (8 bits): Provides additional information. For Echo messages, this is always 0.
  • Checksum (16 bits): Used for error-checking the packet.
  • Identifier (16 bits): Helps match requests with replies.
  • Sequence Number (16 bits): Tracks the order of packets.
  • Data (Variable length): Contains the payload, e.g: timestamps.

List of ICMP Messages

Type Message Name Code Description
0 Echo Reply 0 Response to an ICMP Echo Request (ping response).
3 Destination Unreachable 0-15 Indicates that a packet could not reach its destination. Different codes specify the reason.
4 Source Quench 0 Deprecated. Previously used to indicate network congestion.
5 Redirect 0-3 Suggests a better route for the packet.
8 Echo Request 0 Used for ping tests to check connectivity.
9 Router Advertisement 0 Sent by routers to advertise their presence.
10 Router Solicitation 0 Sent by hosts to request router advertisements.
11 Time Exceeded 0-1 Packet exceeded its lifetime (TTL expired or reassembly timeout).
12 Parameter Problem 0-2 Packet contains an invalid header field.
13 Timestamp Request 0 Requests a timestamp from the destination.
14 Timestamp Reply 0 Response to a Timestamp Request.
15 Information Request 0 Deprecated. Previously used for address assignments.
16 Information Reply 0 Response to an Information Request.
17 Address Mask Request 0 Used to request the subnet mask of a network.
18 Address Mask Reply 0 Response to an Address Mask Request.

For the purpose of this reading pay attention to the Echo and Echo Reply messages, which have types 8 and 0 respectively.

How to use

First, you must start the docker containers. The repo includes two containers icmp-client and icmp-server, with a client and server application respectively. Docker will take care of installing the necessary tools and compiling both applications. See the docker-compose.yml.

docker compose up --build
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can run them separately:

docker compose run icmp-client
docker compose run icmp-server
Enter fullscreen mode Exit fullscreen mode

Run client

We need to connect to the client container and start the client application.

docker exec -it icmp-client /bin/sh
bin/client icmp-server
Enter fullscreen mode Exit fullscreen mode

The program will print a output similar to the following:

~/code # bin/client icmp-server
21 bytes from 172.22.0.3: pid=921, icmp_type=echo reply, icmp_seq=0, data=hello from client, time:590μs
21 bytes from 172.22.0.3: pid=921, icmp_type=echo reply, icmp_seq=1, data=hello from client, time:506μs
21 bytes from 172.22.0.3: pid=921, icmp_type=echo reply, icmp_seq=2, data=hello from client, time:505μs
Enter fullscreen mode Exit fullscreen mode

But, who is responding to the message if we have not started the server yet?

An ICMP request does not require any specialized server software to handle it, because the operating system running on the machine has built-in functionality for handling ICMP messages. Specifically, the OS is set up to recognize and respond to ICMP Echo Requests by automatically sending back an ICMP Echo Reply. This built-in mechanism is part of the network stack in the operating system, which manages network communication. The client determines the latency by calculating the elapsed time between the echo and the echo reply.

However, it does not mean that we can send ICMP requests to any server. Routers and firewalls could prevent or filter those messages. Others will limit the rate to prevent flooding.

Note: I used microseconds instead of milliseconds just for convenience. The elapsed time is very short in this case to use milliseconds.

Run server

We can run our own ICMP handler if we want. Our handler will process the packet and send it back to the sender, but changing the type to echo reply. We are not covering other special cases in this article.

Now that the machines are running, connect to the server:

docker exec -it icmp-server /bin/sh
bin/server
Enter fullscreen mode Exit fullscreen mode

Once you start the client you will see a similar output:

~/code # bin/server
21 bytes from 172.22.0.2: pid=927, icmp_type=echo, icmp_seq=0, data=hello from client
21 bytes from 172.22.0.2: pid=927, icmp_type=echo, icmp_seq=1, data=hello from client
21 bytes from 172.22.0.2: pid=927, icmp_type=echo, icmp_seq=2, data=hello from client
Enter fullscreen mode Exit fullscreen mode
~/code # bin/client icmp-server
21 bytes from 172.22.0.3: pid=927, icmp_type=echo reply, icmp_seq=0, data=hello from client, time:1017μs
21 bytes from 172.22.0.3: pid=927, icmp_type=echo reply, icmp_seq=0, data=hello from client, time:660μs
21 bytes from 172.22.0.3: pid=927, icmp_type=echo reply, icmp_seq=1, data=hello from client, time:525μs
Enter fullscreen mode Exit fullscreen mode

As you can see, the server is receiving the packets and echoing the message back to the sender. The same way as before the client can determine the latency by calculating the elapsed time between the echo and the echo reply. Also note that send request is of type echo and the answer from the server is of type echo reply.

Implementation details

If you want to see the full code, navigate to the Github repository.

Here is the code to send Echo requests to a server:

func Ping(host string, attempts int, delay time.Duration) error {

    raddr, err := net.ResolveIPAddr("ip4", host)
    if err != nil {
        return fmt.Errorf("failed to resolve target address: %w", err)
    }

    conn, err := net.DialIP("ip4:icmp", nil, raddr)
    if err != nil {
        return fmt.Errorf("failed to create ICMP connection: %w", err)
    }
    defer conn.Close()

    data := []byte("hello from client")
    for i := 0; i < attempts; i++ {
        echoReq := icmp.Message{
            Type: ipv4.ICMPTypeEcho,
            Code: 0,
            Body: &icmp.Echo{
                ID:   os.Getpid() & 0xffff,
                Seq:  i,
                Data: data[:],
            },
        }

        msgBytes, err := echoReq.Marshal(nil)

        if err != nil {
            return fmt.Errorf("failed to marshal ICMP message: %w", err)
        }

        if err := conn.SetReadDeadline(time.Now().Add(1 * time.Second)); err != nil {
            return fmt.Errorf("failed to set read deadline: %w", err)
        }

        timeStart := time.Now()
        if _, err := conn.Write(msgBytes); err != nil {
            return fmt.Errorf("failed to send ICMP message: %w", err)
        }

        resp := make([]byte, 512)
        n, peer, err := conn.ReadFrom(resp)
        timeEnd := time.Now()
        if err != nil {
            return fmt.Errorf("failed to read ICMP response: %w", err)
        }

        parsedMsg, err := icmp.ParseMessage(1, resp[:n])
        if err != nil {
            return fmt.Errorf("failed to parse ICMP message: %w", err)
        }

        echoType := parsedMsg.Type
        body := parsedMsg.Body.(*icmp.Echo)
        proto := parsedMsg.Type.Protocol()

        switch parsedMsg.Type {
        case ipv4.ICMPTypeEchoReply:
            elapsed := timeEnd.Sub(timeStart)
            fmt.Printf("%d bytes from %s: pid=%d, icmp_type=%v, icmp_seq=%d, data=%s, time:%dμs\n", body.Len(proto), peer, body.ID, echoType, body.Seq, string(body.Data), elapsed.Microseconds())
        default:
            fmt.Printf("received unexpected message from %s: pid=%d, icmp_type=%v, icmp_seq=%d, data=%s\n", peer, body.ID, echoType, body.Seq, string(body.Data))
        }
        time.Sleep(1 * time.Second)
    }
    return nil
}

Enter fullscreen mode Exit fullscreen mode

Additionally, this is the code for receiving those ICMP requests and sending them back to the sender:

func Pong() error {
    addr := "0.0.0.0"
    raddr, err := net.ResolveIPAddr("ip4", addr)

    if err != nil {
        return fmt.Errorf("error resolving ip: %w", err)
    }

    conn, err := net.ListenIP("ip4:icmp", raddr)

    if err != nil {
        return fmt.Errorf("error listening package: %w", err)
    }

    buf := make([]byte, 512)

    for {
        numRead, from, _ := conn.ReadFrom(buf)

        echoReq, err := icmp.ParseMessage(1, buf[:numRead])

        if err != nil {
            fmt.Printf("error parsing message: %s", err)
            continue
        }

        body := echoReq.Body.(*icmp.Echo)
        echoType := echoReq.Type
        proto := echoReq.Type.Protocol()
        if echoType != ipv4.ICMPTypeEcho {
            fmt.Printf("received unexpected message from %s: pid=%d, icmp_type=%v, icmp_seq=%d, data=%s\n", from, body.ID, echoType, body.Seq, string(body.Data))
            continue
        }

        fmt.Printf("%d bytes from %s: pid=%d, icmp_type=%v, icmp_seq=%d, data=%s\n", body.Len(proto), from, body.ID, echoType, body.Seq, string(body.Data))

        reply := icmp.Message{
            Type: ipv4.ICMPTypeEchoReply,
            Code: 0,
            Body: &icmp.Echo{
                ID:   body.ID,
                Seq:  body.Seq,
                Data: body.Data,
            },
        }

        replyBytes, err := reply.Marshal(nil)

        if err != nil {
            fmt.Printf("error serializing reply: %s", err)
            continue
        }

        _, err = conn.WriteTo(replyBytes, from)

        if err != nil {
            fmt.Printf("error sending reply: %s", err)
            continue
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Conclusions

The Internet Control Message Protocol (ICMP) is a fundamental part of networking, providing useful information regarding the communication between devices. The concept of "ping," often associated with online gaming and other real-time applications, relies heavily on ICMP Echo requests and replies to determine network latency. Understanding and using ICMP correctly can help diagnose network issues, measure latency, and optimize connectivity in both personal and professional environments.

Bibliography

Top comments (0)