Overview
I have been working on a side project which is essentially a network troubleshooting tool. My intent is to deepen my understanding of networking fundamentals and get comfortable using troubleshooting tools offerred by the OS.
It’s a CLI app, that would take the hostname and try to diagnose issues(if any). The plan was to implement ping and traceroute first and progressively implement other tools depending on my level of comfort.
However, my ping implmentation is not accurate in the sense that, the IPv4 packets are malformed. That’s what wireshark had to say.
1 0.000000 192.168.0.100 142.250.195.132 ICMP 300 Unknown ICMP (obsolete or malformed?)
Code
Here’s how I have implemented ping
package ping
import (
"encoding/json"
"net"
"github.com/pkg/errors"
)
var (
IcmpProtocolNumber uint8 = 1
IPv4Version uint8 = 4
IPv4IHL uint8 = 5
ICMPHeaderType uint8 = 8
ICMPHeaderSubtype uint8 = 0
)
type NativePinger struct {
SourceIP string
DestIP string
}
type ICMPHeader struct {
Type uint8
Code uint8
Checksum uint16
}
type ICMPPacket struct {
Header ICMPHeader
Payload interface{}
}
type IPv4Header struct {
SourceIP string
DestinationIP string
Length uint16
Identification uint16
FlagsAndOffset uint16
Checksum uint16
VersionIHL uint8
DSCPAndECN uint8
TTL uint8
Protocol uint8
}
type IPv4Packet struct {
Header IPv4Header
Payload *ICMPPacket
}
func (p *NativePinger) createIPv4Packet() (*IPv4Packet, error) {
versionIHL := (IPv4Version << 4) | IPv4IHL
icmpPacket := &ICMPPacket{
Header: ICMPHeader{
Type: ICMPHeaderType,
Code: ICMPHeaderSubtype,
},
}
ipv4Packet := &IPv4Packet{
Header: IPv4Header{
VersionIHL: versionIHL,
DSCPAndECN: 0,
Identification: 0,
FlagsAndOffset: 0,
TTL: 64,
Protocol: IcmpProtocolNumber,
SourceIP: p.SourceIP,
DestinationIP: p.DestIP,
},
Payload: icmpPacket,
}
ipv4Packet.Header.Length = 40
bytes, err := json.Marshal(icmpPacket)
if err != nil {
return nil, errors.Wrapf(err, "error converting ICMP packet to bytes")
}
icmpPacket.Header.Checksum = calculateChecksum(bytes)
bytes, err = json.Marshal(ipv4Packet)
if err != nil {
return nil, errors.Wrapf(err, "error converting IPv4 packet to bytes")
}
ipv4Packet.Header.Checksum = calculateChecksum(bytes)
return ipv4Packet, nil
}
func calculateChecksum(data []byte) uint16 {
sum := uint32(0)
// creating 16 bit words
for i := 0; i < len(data)-1; i++ {
word := uint32(data[i])<<8 | uint32(data[i+1])
sum += word
}
if len(data)%2 == 1 {
sum += uint32(data[len(data)-1])
}
// adding carry bits with lower 16 bits
for (sum >> 16) > 0 {
sum = (sum & 0xffff) + (sum >> 16)
}
// taking one's compliment
checksum := ^sum
return uint16(checksum)
}
func (p *NativePinger) ResolveAddress(dest string) error {
ips, err := net.LookupIP(dest)
if err != nil {
return errors.Wrapf(err, "error resolving address of remote host")
}
for _, ip := range ips {
if ipv4 := ip.To4(); ipv4 != nil {
p.DestIP = ipv4.String()
}
}
// The destination address does not need to exist as unlike tcp, udp does not require a handshake.
// The goal here is to retrieve the outbound IP. Source: https://stackoverflow.com/a/37382208/3728336
//
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return errors.Wrapf(err, "error resolving outbound ip address of local machine")
}
defer conn.Close()
p.SourceIP = conn.LocalAddr().(*net.UDPAddr).IP.String()
return nil
}
func (p *NativePinger) Ping(host string) error {
if err := p.ResolveAddress(host); err != nil {
return errors.Wrapf(err, "error resolving source/destination addresses")
}
packet, err := p.createIPv4Packet()
if err != nil {
return errors.Wrapf(err, "error creating IPv4Packet")
}
conn, err := net.Dial("ip4:icmp", packet.Header.DestinationIP)
if err != nil {
return errors.Wrapf(err, "error eshtablishing connection with %s", host)
}
defer conn.Close()
bytes, err := json.Marshal(packet)
if err != nil {
return errors.Wrapf(err, "error converting IPv4 packet into bytes")
}
_, err = conn.Write(bytes)
if err != nil {
return errors.Wrapf(err, "error sending ICMP echo request")
}
buff := make([]byte, 2048)
_, err = conn.Read(buff) // The implementation doesn't proceed beyond this point
if err != nil {
return errors.Wrapf(err, "error receiving ICMP echo response")
}
return nil
}
Upon introspection
I'm uncertain whether the packet's malformation is due to a single reason or multiple reasons. I feel the problem lies in either(or both?) of these two places:
- Incorrect calculation of header length I have manually calculated the length to be
40 bytes (wordsize = 4 bytes)
. Wrote the struct fields in an order that would prevent a maligned struct. I referred this source to know the sizes of various types.
// 1 word (4 bytes)
type ICMPHeader struct {
Type uint8 // 8 bit
Code uint8 // 8 bit
Checksum uint16 // 16 bit
}
// 3 words (3*4 = 12 bytes)
type ICMPPacket struct {
Header ICMPHeader // 1 word
Payload interface{} // 2 words
}
// 7 words (7*4 = 28 bytes)
type IPv4Header struct {
// Below group takes 4 words (each string takes 2 words)
SourceIP string
DestinationIP string
// Following group takes 2 words (each 16 bits)
Length uint16
Identification uint16
FlagsAndOffset uint16
Checksum uint16
// Below group takes 1 word (each takes 8 bits)
VersionIHL uint8
DSCPAndECN uint8
TTL uint8
Protocol uint8
}
// 10 words (40 bytes)
type IPv4Packet struct {
Header IPv4Header // 7 words as calculated above
Payload ICMPPacket // 3 words as calculated above
}
- Incorrect checksum calculation I implemented the internet checksum algorithm. Please let me know if that's not what I was supposed to do here.
There are missing parts in the implementation such as configuring count, assigning sequence numbers to packets, etc, but before that the basic implementation needs to be fixed i.e. receiving a response for the ICMP ECHO packet. It would be great to know where I'm making an error.
Thanks!
Update 24th Aug '23
I've updated the code considering the suggestions I got in the comments i.e. fixing byte ordering and using raw bytes for source, dest addresses. However, this alone does not solve the problem, the packet is still malformed so there must be other things going wrong.