Raw sockets in Go: Link layer
Summary
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.
Receive
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.
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
fd, _ := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_ICMP)
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd %d", fd))
for {
buf := make([]byte, 1024)
numRead, err := f.Read(buf)
if err != nil {
fmt.Println(err)
}
fmt.Printf("% X\n", buf[:numRead])
}
}
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 isAF_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 Programming 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.
Send
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.
package main
import (
"log"
"net"
"syscall"
)
func main() {
var err error
fd, _ := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
addr := syscall.SockaddrInet4{
Port: 0,
Addr: [4]byte{127, 0, 0, 1},
}
p := pkt()
err = syscall.Sendto(fd, p, 0, &addr)
if err != nil {
log.Fatal("Sendto:", err)
}
}
func pkt() []byte {
h := Header{
Version: 4,
Len: 20,
TotalLen: 20 + 10, // 20 bytes for IP, 10 for ICMP
TTL: 64,
Protocol: 1, // ICMP
Dst: net.IPv4(127, 0, 0, 1),
// ID, Src and Checksum will be set for us by the kernel
}
icmp := []byte{
8, // type: echo request
0, // code: not used by echo request
0, // checksum (16 bit), we fill in below
0,
0, // identifier (16 bit). zero allowed.
0,
0, // sequence number (16 bit). zero allowed.
0,
0xC0, // Optional data. ping puts time packet sent here
0xDE,
}
cs := csum(icmp)
icmp[2] = byte(cs)
icmp[3] = byte(cs >> 8)
out, err := h.Marshal()
if err != nil {
log.Fatal(err)
}
return append(out, icmp...)
}
func csum(b []byte) uint16 {
var s uint32
for i := 0; i < len(b); i += 2 {
s += uint32(b[i+1])<<8 | uint32(b[i])
}
// add back the carry
s = s>>16 + s&0xffff
s = s + s>>16
return uint16(^s)
}
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.