March 2, 2014

Raw sockets in Go: IP layer

Posted in Software, Uncategorized at 00:41 by graham

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.


Let’s read the first ICMP packet on localhost:

package main

import (

func main() {
    protocol := "icmp"
    netaddr, _ := net.ResolveIPAddr("ip4", "")
    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.


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

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)


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


  1. Nivetha said,

    September 21, 2020 at 11:29

    In the line ListenIP(“ipv4:”+protocol,netaddr) im getting error.same program showing me goroutine runtime panic.How to resolve this issue. Thanks

  2. michael said,

    December 16, 2016 at 21:36

    @No Hat: With buffio you can’t control the byte order like you can in the binary package.

  3. ron minnich said,

    December 3, 2014 at 01:25

    Graham, I’m pretty interested in the link layer bits … I’m trying to write a client dhcp packet on the link layer, but failing miserably :-)

    If you’ve got that link layer code ready I’m interested.

    This is for my u-root

    and, yes, on that project, we need all the help we can get. I’m going to need a wpa2 supplicant soon …

    Hints welcome.

  4. No Hat said,

    September 27, 2014 at 06:58

    Why don’t you use buffio ?

Leave a Comment

Note: Your comment will only appear on the site once I approve it manually. This can take a day or two. Thanks for taking the time to comment.