January 31, 2015

Raw sockets in Go: Link layer

Posted in Software at 21:55 by graham

Continuing our dive into the Internet Protocol Suite from Go (See part 1 Raw sockets in Go: IP layer), we are going to the link layer, so we can see the IP headers. This will also allow us to craft our own IP headers, or handle address families outside IP. We’ll send ping packets (ICMP echo request) and watch the kernel’s response.


This isn’t wrapped in Go, so we need a syscall. Otherwise it’s very similar to the IP layer in part 1, and pretty similar to the C equivalent.

On the first line of main we request the AF_INET family, meaning IPv4. We could ask for a different address family (AF_* constants) – here’s a list of address families. Most of the protocols in that list are rare (AF_IPX, AF_APPLETALK, etc). We’re in a IP world today.

Other useful address families:

  • AF_INET6 for IPv6.
  • AF_UNIX for unix domain sockets. It is used in net.DialUnix and net.ListenUnix. The POSIX name for AF_UNIX is AF_LOCAL, but Go largely sticks to AF_UNIX. They are equivalent.
  • An odd / interesting one is AF_NETLINK, which is for talking to the kernel. Read about it man 7 netlink or at Linux Journal. Docker has a netlink package.

The second parameter, SOCK_RAW is what makes this a raw socket, where we receive IP packets. SOCK_STREAM would give us TCP, SOCK_DGRAM would give UDP.

The third parameter filters packets so we only receive ICMP. You need a protocol here. As man 7 raw says “Receiving of all IP protocols via IPPROTO_RAW is not possible using raw sockets”. We’ll do that in the next post in this series, at the physical / device driver layer.

Build and run it as root (only root or CAP_NET_RAW can open raw sockets). In a different window ping localhost. You should see something like this:

45 00 00 3C EA FF 40 00 40 06 51 BA 7F 00 00 01 7F 00 00 01 …

This is the IP Header. First byte 45 is 4 for the IP version (IPv4), and 5 for length of this header (5 32-bit words), and so on. This is just like the receive example in the previous post except that we also see the IP header.

Try replacing IPPROTO_ICMP in the Socket call with IPPROTO_TCP, and wget localhost. The first 20 bytes will be similar (the IP header), then you should see a TCP packet, and finally HTTP.

If you have UNIX Network Programmings by Richard Stevens, you might be as confused as I was by section 28.4 which claims “received TCP packets are never passed to a raw socket.” That’s clearly not true. It’s a historic note related to BSD. man 7 raw says “Raw sockets may tap all IP protocols in Linux, even protocols like ICMP or TCP” but “This should not be relied upon in portable programs”.

That Stevens book is widely considered the best reference for unix network programming. It’s also very expensive. There’s a soft cover “International Student Edition” (meaning India) you can get at abebooks.com for much less.


There’s an IP header type in the go.net sub-repository, with Marshall and ParseHeader methods. That file doesn’t depend on anything else in there, so grab it wget https://raw.githubusercontent.com/golang/net/master/ipv4/header.go, change the package statement to main, and we’ll play with it.

We’re going to send an ICMP ping packet, creating the IP header ourselves.

Download that as send.go, and the header.go from net repo, and build it go build send.go header.go. Now run the receive in one window (remember to sudo), and send in another. Receive should output something like this:

45 00 00 1E 1C D4 00 00 40 01 60 09 7F 00 00 01 7F 00 00 01 08 00 37 21 00 00 00 00 C0 DE

45 00 00 1E 1C D5 00 00 40 01 60 08 7F 00 00 01 7F 00 00 01 00 00 3F 21 00 00 00 00 C0 DE

The first line is our ICMP Echo Request packet, with a few IP header fields filled in by the kernel. The second line is the kernel’s response! (yes I find that very exciting). Notice byte 21 is 0, instead of 8. That’s the only difference between an ICMP Echo message (ping) and an ICMP Echo Reply message (pong).

We use syscall.IPPROTO_RAW as the last argument to Socket because we will be including our own IP header. If you wanted to use syscall.IPPROTO_ICMP there, you also need to set the IP_HDRINCL socket option:

syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)

I think that last argument is only used so the kernel knows what protocol to put in the IP header’s Protocol field. If you’re building the header yourself, there’s not much point using anything except IPPROTO_RAW.

To build a basic ping client, combine send and receive into one program, put send timestamp in the data part (instead of 0xC0DE), and print the elapsed time when you get the Echo Reply. If you actually need to do ICMP (other than to play with raw sockets), the net repo has an icmp package.

In the final post I’ll write about the physical layer, and we’ll get to work with Ethernet packets.


  1. a said,

    June 27, 2019 at 17:15

    I tried the same with ipv6 https://stackoverflow.com/questions/56794906/sending-ipv6-packets-over-raw-sockets-in-golang

  2. mihaly said,

    February 2, 2017 at 22:43

    Awesome article, thank you! :)

    Could you point me to a doc that works with Ethernet headers and ICMP (just like you explained it here)? I want to create ICMP packets with custom Ethernet header, more specifically custom destination MAC address.

  3. Ian said,

    September 3, 2016 at 22:22

    Thanks for the post. “We’ll do that in the next post in this series, at the physical / device driver layer.” – yes please :)

  4. Peter said,

    March 26, 2015 at 19:59


    I tried this out. I have a question though, it appears that when you receive using raw sockets, the kernel stack still handles the packet. By that I mean if you ping your machine that is running the receive raw socket program you have, I still get an echo reply, even though my receive code does not write one back. Is there a way to bypass the kernel network stack completely (e.g. have the raw socket such that all packets that are ICMP go to the application and does not go to the kernel’s ICMP protocol?).



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.