Create Ping in Go

By Hideki Ishiguro at

The other day, I create Ping in Go programming langueage.

To be honest, I wanted to create it without relying on external packages because I wanted to know deeply about how Ping works.
But fortunately (and unfortunately), there are useful packages (like "net/icmp", "net/ipv4" or "net/ipv6") for sending ICMP packets in Go. So I decided to use them.

The code of this repository is the enhanced version of the code in here. Please check it out if you're interested in.


1. Create a New Go Project

First off, make a new folder and create a new project using "go mod init" command.
Then create a new file e.g. "main.go".

mkdir myping
cd myping
go mod init example/myping
touch main.go

2. Import External Packages

Open your favorite code editor, and start writing code in the "main.go".
Import some packages which used for receiving/sending ICMP packets and common ones for showing results.

package main

import (
    "fmt"
    "log"

    "golang.org/x/net/icmp"
    "golang.org/x/net/ipv4"
    "golang.org/x/net/ipv6"
)

3. Create an ICMP Packet Receiver

To check if packets are coming back from the destination, you need to make a listener and wait for getting the packets.
Here I use IPv4 for the network structure so the network value of the argument of "icmp.ListenPacket" is "ip4:icmp" or "ip4:1". If you use IPv6, this value must be "ip6:ipv6-icmp" or "ip6:58".

package main

...

func main() {

    packetconn, err := icmp.ListenPacket("ip4:1", "0.0.0.0")
    if err != nil {
        log.Fatal(err)
    }
    defer packetconn.Close()

}

4. Prepare ICMP Message

Next, create an ICMP message which contains Code, Identifier, Data, and so on.
As I said above, I use Ipv4 so set the Type field into ipv4.ICMPType.
Set 0 (echo reply) into the Code field for ping.

...

func main() {

    ...

    msg := &icmp.Message{
        Type: ipv4.ICMPType,
        Code: 0,
        Body: &icmp.Echo{
            ID: os.Getpid() & 0xffff,
            Seq: 0,
            Data: []byte("hello"),
        }
    }

    wb, err := msg.Marshal(nil)
    if err != nil {
        log.Fatal(err)
    }
}

5. Implement to Send Packets

WriteTo function writes the ICMP message to the destination.
As an example this time, set Google DNS (8.8.8.8) as the destination.

...

func main() {

    ...

    if _, err := packetconn.WriteTo(wb, "8.8.8.8"); err != nil {
        log.Fatal(err)
    }
}

6. Implement to Receive Message

The packet from the destination is processed in here.

...

func main() {

    ...

    rb := make([]byte, 1500)
    n, peer, err := packetconn.ReadFrom(rb)
    if err != nil {
        log.Fatal(err)
    }

    rm, err != icmp.ParseMessage(1, rb[:n])
    if err !=nil {
        log.Fatal(err)
    }
}

7. Implement to Display the Result

At the end, display the information about the received packet or failed.

...

func main() {

    ...

    switch rm.Type {
        case ipv4.ICMPTypeEchoReply:
            fmt.Printf("received from %v", peer)
        default:
            fmt.Printf("Failed: %+v\n", rm)
    }
}

Build & Run

After implementing, check if our ping works correctly.

go mod tidy
go build
./myping

Conclusion

As above, we can send ICMP packets too easily if we use some useful packages.
Below is the entire code.

package main

import (
    "fmt"
    "log"

    "golang.org/x/net/icmp"
    "golang.org/x/net/ipv4"
    "golang.org/x/net/ipv6"
)

func main() {
    packetconn, err := icmp.ListenPacket("ip4:1", "0.0.0.0")
    if err != nil {
        log.Fatal(err)
    }
    defer packetconn.Close()

    msg := &icmp.Message{
        Type: ipv4.ICMPType,
        Code: 0,
        Body: &icmp.Echo{
            ID: os.Getpid() & 0xffff,
            Seq: 0,
            Data: []byte("hello"),
        }
    }

    wb, err := msg.Marshal(nil)
    if err != nil {
        log.Fatal(err)
    }

    if _, err := packetconn.WriteTo(wb, "8.8.8.8"); err != nil {
        log.Fatal(err)
    }

    rb := make([]byte, 1500)
    n, peer, err := packetconn.ReadFrom(rb)
    if err != nil {
        log.Fatal(err)
    }

    rm, err != icmp.ParseMessage(1, rb[:n])
    if err !=nil {
        log.Fatal(err)
    }

    switch rm.Type {
        case ipv4.ICMPTypeEchoReply:
            fmt.Printf("received from %v", peer)
        default:
            fmt.Printf("Failed: %+v\n", rm)
    }
}

Actually I wanted to make it while examining the internal parts more, but I couldn't pursue it deeply because the packages are so useful. I would like to try this again later if I feel like it.