Using SO_PEERCRED in Go

At this year’s GopherCon, Gabbi Fisher (@gabbifish) of CloudFlare made a great presentation introducing her audience to the complexities of network socket options in Go (archived video of her presentation here). In her talk, Gabbi details how to use the network socket option SO_REUSEADDR to allow multiple processes on the same server to listen on the same network port. Gabbi closes by mentioning the breadth of socket options that are available beyond just her example. Inspired by her talk, I’ve decided to write about the SO_PEERCRED socket option and Go.

Sockets and Socket Options

What are sockets in the context of network programming? In essence, sockets are a special case of file descriptors used for network connections. They behave in many ways similar to general file descriptors – for instance, you can call Read(), Write() and Close() on them just like when operating on files. However, sockets also have operations such as Accept() and Listen() that provide the network specific behavior for a socket. For those new to socket programming, I would suggest watching Gabbi’s presentation as she provides a good overview of sockets with a focus of how they are used within Go.

Sockets also have numerous options that can be used when creating or interacting with them. The SO_REUSEADDR option is one such option. It allows more than one socket to bind to the same address, something normally that would result in an error, while providing clear rules for routing traffic to correct socket. Likewise, SO_PEERCRED is a socket option specific to Unix domain sockets that provides the server (ie. listening socket) with credential information (user ID, group ID and process ID) of any connected client.

While restricted to local connections (Unix domain sockets are not accessible across the network), SO_PEERCRED can be a useful tool for daemons to authenticate connections without requiring additional means of authentication – for example, a user specific dameon that only should be accessible by other processes launched by the same user.

The remainder of this article will show an example of SO_PEERCRED being used within a simple Go program.

An Echo Server in Go

First lets begin with a basic echo server that doesn’t include SO_PEERCRED. The main() function, below, creates a socket at the path /tmp/echo.sock, listens for connecting clients and passes every client connection to its own goroutine running the function handleConn().

package main

import (
	"log"
	"net"
	"os"
)

const sockAddr = "/tmp/echo.sock"

func main() {
	// Make sure no stale sockets present
	os.Remove(sockAddr)

	// Create new Unix domain socket
	server, err := net.Listen("unix", sockAddr)
	if err != nil {
		log.Fatal(err)
	}
	defer server.Close()

	// Loop to process client connections
	for {
		client, err := server.Accept()
		if err != nil {
			log.Printf("Accept() failed: %s", err)
			continue
		}

		go handleConn(client)
	}
}

The handleConn() function referenced above is just a simple line-based echo – each line read will immediately be sent back to the client via Write().

package main

import (
	"bufio"
	"net"
)

func handleConn(c net.Conn) {
	b := bufio.NewReader(c)
	for {
		line, err := b.ReadBytes('\n')
		if err != nil {
			break
		}
		c.Write([]byte("> "))
		c.Write(line)
	}

	c.Close()
}

After launching our go program in another terminal (ie. go run *.go), we can easily test it with the network utility nc (aka netcat) using the -U option to indicate a Unix domain socket:

$ nc -U /tmp/echo.sock
this is a line
> this is a line
^C
$

But what if we wanted to only allow whatever user who launched the echo server to be able to interact with it? This is where we use SO_PEERCRED.

Echo Server with SO_PEERCRED

In her presentation, Gabbi showed how to use a net.ListenerConfig struct to assign a callback that would set SO_REUSEADDR prior to creating a new socket with a call to Listen() or ListenPacket(). This is essential for SO_REUSEADDR as it is a creation-time option of sockets. However, for our case, SO_PEERCRED is used on already established connections and, thus, needs to be be handled differently.

To access the credential information via SO_PEERCRED I have created a function called readCreds() which I will explain in further detail below:

package main

import (
	"fmt"
	"net"

	"golang.org/x/sys/unix"
)

func readCreds(c net.Conn) (*unix.Ucred, error) {

	var cred *unix.Ucred

	// net.Conn is an interface. Expect only *net.UnixConn types
	uc, ok := c.(*net.UnixConn)
	if !ok {
		return nil, fmt.Errorf("unexpected socket type")
	}

	// Fetches raw network connection from UnixConn
	raw, err := uc.SyscallConn()
	if err != nil {
		return nil, fmt.Errorf("error opening raw connection: %s", err)
	}

	// The raw.Control() callback does not return an error directly.
	// In order to capture errors, we wrap already defined variable
	// 'err' within the closure. 'err2' is then the error returned
	// by Control() itself.
	err2 := raw.Control(func(fd uintptr) {
		cred, err = unix.GetsockoptUcred(int(fd),
			unix.SOL_SOCKET,
			unix.SO_PEERCRED)
	})

	if err != nil {
		return nil, fmt.Errorf("GetsockoptUcred() error: %s", err)
	}

	if err2 != nil {
		return nil, fmt.Errorf("Control() error: %s", err2)
	}

	return cred, nil
}

Before we can use SO_PEERCRED we need to get access to the Control() function of the raw socket. The Accept() call in our main() function returns a net.Conn interface which is passed to our function. However, but we need to access the real type, *net.UnixConn, directly to get to the raw socket so we use a standard Go type assertion to get the underlying concrete type as variable uc.

uc, ok := c.(*net.UnixConn)

Next we use the method SyscallConn() to return a syscall.RawConn interface containing the necessary Control() method:

raw, err := uc.SyscallConn()

The Control() method allows one to run a callback function (with the function signature of func(fd int)) against the raw socket. Here we implement a function closure that allows us to execute the syscall unix.GetsockoptUcred() while retaining the returned values in the enclosed variables, cred and err.

err2 := raw.Control(func(fd uintptr) {
    cred, err = unix.GetsockoptUcred(int(fd),
        unix.SOL_SOCKET,
        unix.SO_PEERCRED)
})

After handling the errors (both the one returned by Control() and the error returned within the closure), we can return a *unix.Ucred struct with the following fields:

type Ucred struct {
    Pid int32
    Uid uint32
    Gid uint32
}

From here we can make a few additions to our original main(). First, at startup we get the current user ID and convert to an int value:

uidStr, err := user.Current()
if err != nil {
    log.Fatal(err)
}

uid, err := strconv.Atoi(uidStr.Uid)
if err != nil {
    log.Fatal(err)
}

Next we add a call to readCreds() after our Accept() call to check the UID of the connecting client to that of the running daemon:

creds, err := readCreds(client)
if err != nil {
    log.Printf("Error reading credentials: %s", err)
    continue
}

if creds.Uid != uint32(uid) {
    log.Printf("UID mismatch (%d != %d). Closing connection.\n", creds.Uid, uid)
    client.Write([]byte("Unauthorized access\n"))
    client.Close()
    continue
}

(The fully modified main.go file can be found here along with handle.go and cred.go that make up this example.)

Now, once again, we can use nc to test our daemon:

$ nc -U /tmp/echo.sock
this is a line
> this is a line
^C
$

Works the same as before. However, if we use sudo to change to another user before running the command we get a different result:

$ sudo -u guest nc -U /tmp/echo.sock
Unauthorized access
$

If we look in the terminal running our go program, we can see it disallowed the connection based on the user ID:

$ go run *.go
2019/09/09 13:22:36 UID mismatch (1001 != 1000). Closing connection.

Now our echo server is secured from anyone else attempting to connect to it other than ourselves!

Other Uses

Saavy Unix users will note that setting the read/write permissions on the socket itself would be an easier way to restrict access without having to modify the server. Indeed, I had to run chmod on my socket in the example above to allow the guest user to write to it. But Unix file permissions might not cover all cases and what about the superuser, root? root can write to any socket but with our modifications, even root is not authorized unless root started the program:

$ sudo nc -U /tmp/echo.sock
Unauthorized access
$

And in our go program’s terminal:

2019/09/09 13:24:32 UID mismatch (0 != 1000). Closing connection.

Thus SO_PEERCRED provides us with security that even file permissions cannot. But this only scratches the surface of possibilities. We could also use SO_PEERCRED to do things like:

All told, there are many interesting possibilities opened up with using SO_PEERCRED. That said there are some limitations:

Wrapping Up

I hope this article proves useful in further understanding socket options in Go and the SO_PEERCRED option in particular.

Thanks again to Gabbi for the presentation and the inspiration for this article.