Graham King

Solvitas perambulum

Raw sockets in Go: IP layer

software
Summary
At the IP layer, you can work with protocols beyond TCP and UDP and craft your own packets. To read the first ICMP packet on localhost in Go, use the `net` package to listen on a raw socket. You'll need root privileges to do this. When you run the program, you can ping localhost and observe the raw bytes of ICMP packets. If you switch to TCP, you’ll see the packet headers instead. To send a TCP packet, define a TCP header structure, marshal it, calculate the checksum, and use a connection to send the data. Working with binary data in Go involves using the `encoding/binary` package and utilizing bitwise operations. In the next part, I'll explore the link layer for reading and writing IP packets.

In the Internet protocol suite we usually work at the transport layer, with TCP or UDP. Go (golang) has good support for working with lower layers. This post is about working one layer down, at the IP layer.

If you want to use protocols other than TCP or UDP, or craft your own packets, you need to connect at the IP layer.

Receive

Let’s read the first ICMP packet on localhost:

package main

import (
    "fmt"
    "net"
)

func main() {
    protocol := "icmp"
    netaddr, _ := net.ResolveIPAddr("ip4", "127.0.0.1")
    conn, _ := net.ListenIP("ip4:"+protocol, netaddr)

    buf := make([]byte, 1024)
    numRead, _, _ := conn.ReadFrom(buf)
    fmt.Printf("% X\n", buf[:numRead])
}

Build it go build test.go and run it as root sudo ./test. You need to be root to do raw sockets, because (from man 7 raw):

Only processes with an effective user ID of 0 or the CAP_NET_RAW capability are allowed to open raw sockets.

In a different window, ping localhost. You should see the raw bytes of your ICMP packets. Next, change protocol to tcp, and wget localhost.

We usually work at the TCP / UDP layer, which gives us the content of TCP or UDP message. Here, because we are at the IP layer, we get the contents of the IP message, so we can see the ICMP or TCP header.

In the ICMP (ping) case the first byte should be 8, an Echo Request.

In the TCP (wget) case, the first four bytes are the source and destination port. The source port will be a high number, an ephemeral port the OS chose for you. The destination port (bytes 3 and 4) will be 00 50, which is hex for 80, the HTTP port. Byte 14 will be 02, telling you this is a SYN packet, the opening message of the TCP three-way handshake. This handshake is normally hidden from you, when you work at the TCP layer.

Send

To send packets, you need to make them. There’s a Go TCP header and example usage in github.com/grahamking/latency.

A full program would be too long to show here, but if you get tcp.go from the link above, you could send a TCP packet like this:

packet := TCPHeader{
    Source: 0xaa47, // Random ephemeral port
    Destination: 80,
    SeqNum: rand.Uint32(),
    AckNum: 0,
    DataOffset: 5, // 4 bits
    Reserved: 0, // 3 bits
    ECN: 0, // 3 bits
    Ctrl: 2, // 6 bits (000010, SYN bit set)
    Window: 0xaaaa, // size of your receive window
    Checksum: 0, // Kernel will set this if it's 0
    Urgent: 0,
    Options: []TCPOption{},
}

data := packet.Marshal()
packet.Checksum = csum(data, to4byte(laddr), to4byte(raddr))
data = packet.Marshal()

conn, err := net.Dial("ip4:tcp", raddr)
if err != nil {
    log.Fatalf("Dial: %s\n", err)
}

conn.Write(data)

Making your own packets means working in binary, and several things help when working with binary in Go:

  • The encoding/binary package.
  • Conversions truncate without complaining. If you have a := 0xFFFF and you only need the lower 8 bits, byte(a) does it.
  • The usual shift <<, >> operators, and boolean logic | and &.

In part 2, we go to the link layer to read and write IP packets: Raw sockets in Go: Link layer