Let's rewrite ping in Go to see what we can learn

How ping works:

  1. Send a host a ICMP packet
  2. Wait for host to respond with ICMP reply
  3. End with aggregating data from a few round trips
Sample terminal output of `ping -c 5 www.example.com`
link

What ping tells you:

  • (Implicitly) if a host is reachable
  • How long each round trip took
  • Packet loss
  • Round-trip min/avg/max/standard deviation

Concepts we need to understand:

  • What is ICMP
  • What is a Echo Request packet?
  • What is a Echo Reply packet?
  • Bonus: Ping flood Ping of doom, Ping sweep

What is ICMP

Internet Control Message Protocol is…protocol… used by network devices…to send error messages and operational information indicating success or failure when communicating with another IP address Wikipedia

What is a echo request packet?

This is the packet ping will send that matches a standard format.

Packet

What is a echo reply packet?

This is the response expected from an echo request that should match the request packet.

The execution

  • Don't worry too much about the boilerplate of where files live in this project, if you have any question lmk. Here's the source code.
  • see the makefile for details on running the specific commands
  • We are focusing on IPv4, but IPv6 would be very similar with different packet layouts
// /cmd/main.go

/**
* Parse the CLI input and translate it to our ping package
*/


func main() {
	if len(os.Args) != 2 {
		fmt.Println("Usage: sg-ping hostname")
	}

	// TODO: could make this a CLI arg
	defaultCount := 5

	ping.Ping(os.Args[1], defaultCount)
}
// /pkg/ping/ping.go

func Ping(host string, count int) {
	for i := 0; i < count; i++ {
		fmt.Println("Ping", host, i)
		ping(host, i)
	}
}

func ping(host string, seq int) {
	// Note: IPv4 only
	conn, err := net.Dial("ip4:icmp", host)
	if err != nil {
		fmt.Println("Request failed to connect")
	} else {
		// will cause SIGSEGV when trying to close if there's an err
		defer conn.Close()
	}

	start := time.Now()
	packet := makePacket(seq)
	conn.Write(packet)

	conn.SetReadDeadline(time.Now().Add(time.Second))
	size, err := conn.Read(packet)
	if err != nil {
		fmt.Println("Request timed out")
	}

	rtt := time.Since(start)
	fmt.Println("Size", size)
	fmt.Println("RTT", rtt)
}

func makePacket(seq int) []byte {
	// TODO: explain packet better
	packet := make([]byte, 32)
	packet[0] = 8
	packet[1] = 0
	packet[2] = 0 // checksum
	packet[3] = 0
	packet[4] = 0
	packet[5] = 13
	packet[6] = 0 // seq
	packet[7] = 37

	cs := checksum(packet)
	packet[2] = byte(cs >> 8)
	packet[3] = byte(cs & 255)

	return packet
}

func checksum(data []byte) uint16 {
	// TODO: explain checksum
	var sum uint32
	n := len(data)
	for i := 0; i < n-1; i += 2 {
		sum += uint32(data[i])<<8 + uint32(data[i+1])
	}

	if n%2 == 1 {
		sum += uint32(data[n-1])
	}

	sum = (sum >> 16) + (sum & 0xffff)
	sum = sum + (sum >> 16)
	return uint16(^sum)
}
// /pkg/ping/ping_test.go

/**
* TODO: Write a test
*/

Wrap-up

Why does this matter? It's like a meditation for me to understand networking a little better and demystify the tools I use every day.

Also, this could hypothetically be extended to write tools to do ping sweeps or ping of doom attacks. Which maybe we'll do in a follow up post.

Til next time, Sg.

TODOS:

  • Better explain packet in code
  • Better explain checksum
  • Explain the need for sudo in makefile
  • Improve print in cli
  • attach repo
  • Write a test