gonetworkingnetwork-programmingping

Writing raw IP packet in Go using Conn.Write


I have a native ping implementation in which I generate a raw IP packet (IP header + ICMP echo request packet as payload) and send it using Conn.Write.

This is what the final byte stream looks like, the first 20 bytes form the IP header and the following 8 bytes form the payload(icmp packet).

45 00 00 1c 00 00 00 00 40 01 26 a0 c0 a8 00 64 68 f4 2a 41 08 00 f7 ff 00 00 00 00
                                                           |----------icmp---------|
|--------------------ip header-----------------------------|

The thing is, I don't receive an echo reply when I send this byte stream. Wireshark shows a malformed packet in which the bytes are misaligned.

However, if I put the ICMP bytes ahead of the IP header bytes (i.e. 08 00 f7 ff 00 00 00 00 45 00 00 1c 00 00 00 00 40 01 26 e0 c0 a8 00 64 68 f4 2a 01) I do receive the echo reply.

Here's a small executable that demonstrates what I'm talking about.

package main

import (
    "log"
    "net"
    "bytes"
    "fmt"
)

func main() {
    conn, err := net.Dial("ip4:icmp", "104.244.42.129")
    if err != nil {
        log.Fatal(err)
    }

    // this does not work although byte order is correct
    packet := []byte{0x45, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x40, 0x01, 0x26, 0xa0, 0xc0, 0xa8, 0x00, 0x64, 0x68, 0xf4, 0x2a, 0x41, 0x08, 0x00, 0xf7, 0xff, 0x00, 0x00, 0x00, 0x00} 

    // this works although byte order is wrong
    // packet := []byte{0x08, 0x00, 0xf7, 0xff, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x40, 0x01, 0x26, 0xa0, 0xc0, 0xa8, 0x00, 0x64, 0x68, 0xf4, 0x2a, 0x41} 

    _, err = conn.Write(packet)
    if err != nil {
        log.Fatal(err)
    }

    reply := make([]byte, 1048)

    _, err = conn.Read(reply)
    if err != nil {
        log.Fatal(err)
    }
    reply = bytes.Trim(reply, "\x00")

    fmt.Println(reply)
}

I've been trying to reason why this is happening and cannot come up with any. Some thoughts,

func (p *Packet) Serialize() ([]byte, error) {
    buf := new(bytes.Buffer)
    headerSerialized, err := p.Header.Serialize()
    if err != nil {
        return nil, errors.Wrapf(err, "error serializing IPv4 packet header")
    }
    // buf.Write(p.Payload) // writing payload before header seems wrong, but it works
    buf.Write(headerSerialized)
    buf.Write(p.Payload) // this seems right, but doesn't work

    return buf.Bytes(), nil
}

Some help and insights would be of great help.


Solution

  • The line:

    conn, err := net.Dial("ip4:icmp", "104.244.42.129")
    

    doesn't create a raw socket, so, much as you suspected, whenever you call conn.Write an IP header gets pre-pended to your packet. So in this case you are expected to supply only a valid ICMP packet, not an full IP packet. An IP packet is not a valid ICMP packet so it will be dropped. However as you saw, reordering the data and putting ICMP part first will work.

    To visualize, what you are trying will generate a package that looks like this:

    |IP|IP|ICMP|
    

    Which is invalid, but your reordering will instead generate a package that looks like this:

    |IP|ICMP|IP|
    

    Which is valid.

    If you want to create a raw socket you'll probably need to use socket syscalls instead:

    fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)