Let's rewrite ping in Go to see what we can learn
How ping works:
- Send a host a ICMP packet
- Wait for host to respond with ICMP reply
- End with aggregating data from a few round trips
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.
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