Namespaces in Go - Basics

Ed King
5 min readDec 11, 2016

--

In the previous article we dipped our toes in the namespace waters with the unshare command. unshare is great for simple scripting around namespaces but it's not so well suited for when we need more fine-grained and precise control, as is the case with containers. For this use case it's much better to have the support of a fully fledged programming language.

Go has emerged as the container implementation language of choice. This is due in part to the fact that Docker was, and still is, written in Go. Docker is one of the most successful open source Go projects to date (37,680 GitHub ⭐️s at time of writing) and it showed the world that Go was a language to be taken seriously.

The Docker developers have previously outlined the reasons they chose to write Docker in Go. Some of the top reasons include static compilation, good asynchronous primitives, low-level interfaces, a full development environment and strong cross compilation support.

For me personally the real beauty of Go is in its apparent simplicity. Containers are hard! And by using a ‘simple’ language it makes it much easier to reason about what exactly is going on under the hood. There is a great talk by Rob Pike, “Simplicity is Complicated”, in which he discusses how simplicity is part of Go’s design. It’s definitely worth a watch if you’re interested.

👉 Let’s Go

The aim for this series of articles is to provide an understanding of how to work with Linux namespaces inside Go programs. To achieve this, we will be building out a sample application named ns-process.

ns-process will be fairly simple to begin with - it will create a /bin/sh process in a new set of namespaces. Over the course of the next few articles it will evolve in to something much more exciting - a program capable of creating unprivileged containers! Don’t worry if you’re not sure what “unprivileged” means in this context, all will be explaining along the way.

The code for ns-process is available on GitHub and I highly recommend cloning the repo so you can follow along at home.

# Git repo: https://github.com/teddyking/ns-process
# Git tag: 1.0
# Filename: ns_process.go
package main

import (
"fmt"
"os"
"os/exec"
"syscall"
)

func main() {
cmd := exec.Command("/bin/sh")

cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

cmd.Env = []string{"PS1=-[ns-process]- # "}

cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS,
}

if err := cmd.Run(); err != nil {
fmt.Printf("Error running the /bin/sh command - %s\n", err)
os.Exit(1)
}
}

As you can see, there’s nothing particularly complicated here. We’re simply creating a *exec.Cmd, piping through stdin/out/err from the calling process and setting the PS1 environment variable on the new process (this just makes it easier to identify the namespaced shell when executing the program).

The interesting part is cmd.SysProcAttr, but before understanding SysProcAttr we need to take a deeper look at the underlying system calls that make up the namespaces API.

📝 The namespaces API

The namespaces(7) man page tells us there are 3 system calls that make up the API:

  1. clone(2) - creates a new process
  2. setns(2) - allows the calling process to join an existing namespace
  3. unshare(2) - moves the calling process to a new namespace

unshare() may look familiar from the previous article. This is the system call that gets invoked when running the unshare command. The call we're interested in this time is clone(), as clone() gets called as part of Go’s exec.Run().

When calling clone() it's possible to pass one or more CLONE_* flags. Each namespace has a corresponding CLONE flag - CLONE_NEWNS, CLONE_NEWUTS, CLONE_NEWIPC, CLONE_NEWPID, CLONE_NEWNET, CLONE_NEWUSER and CLONE_NEWCGROUP. The execution context of the cloned process is, in part, defined by the flags passed in.

Back up to Go land and SysProcAttr, SysProcAttr allows us to set attributes on the *exec.Cmd. By specifying the Cloneflags attribute, we're telling Go to pass the corresponding CLONE_* flags through to system calls to clone(). And thus we can control which namespaces we'd like our process to be executed in.

Compile and run the program and you will be dropped into a /bin/sh process that's running in a new UTS namespace. Note that the program must be run as the root user.

💁 The following has been tested on Ubuntu 16.04 Xenial with Go 1.7.1

$ go build
$ sudo ./ns-process
-[ns-process]- #

Great! We’ve been dropped into a new shell that’s supposedly running in a new UTS namespace. Let’s confirm that this is the case.

-[ns-process]- # readlink /proc/self/ns/uts
uts:[4026532410]
-[ns-process]- # exit
$ readlink /proc/self/ns/uts
uts:[4026531838]

The contents of /proc/self/ns/uts include the namespace type (uts) and the inode number of the namespace. The fact that the inode number is different inside the ns-process shell compared to outside it implies that these two processes are indeed running in different UTS namespaces.

Not bad at all! But, we can do better. At the moment we’re only requesting a single new namespace for the process. Let’s throw in a few more to spice things up a little. This can be achieved by adding additional flags to Cloneflags, as follows.

# Git repo: https://github.com/teddyking/ns-process
# Git tag: 1.1
# Filename: ns_process.go
...
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWNS |
syscall.CLONE_NEWUTS |
syscall.CLONE_NEWIPC |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNET |
syscall.CLONE_NEWUSER,
}
...

Compile and run the program again, and this time you’ll be dropped into a /bin/sh process that's running in a new Mount, UTS, IPC, PID, Network and User namespace.

💡 When requesting a new User namespace alongside other namespaces, the User namespace will be created first. User namespaces can be created without root permissions, which means we can now drop the sudo and run our program as a non-root user! I’ll go into more detail about the user namespace in a later article.

This is all well and good, and at a basic level does allow us to run processes in new namespaces from Go. However, IRL it’s not really all that useful … We’re missing a lot of setup required to fully initialise and configure the namespaces. For example:

  • We’ve requested a new Mount namespace (CLONE_NEWNS) but are currently piggybacking off the host's mounts and rootfs
  • We’ve requested a new PID namespace (CLONE_NEWPID) but haven't mounted a new /proc filesystem
  • We’ve requested a new Network namespace (CLONE_NEWNET) but haven't setup any interfaces inside the namespace
  • We’ve requested a new User namespace (CLONE_NEWUSER) but have failed to provide a UID/GID mapping

And so it appears that we’ve still got plenty of work cut out for us.

📺 On the next…

We’ve seen how to run a process in a new set of namespaces using Go, but how do we configure and initialise the namespaces so they are ready for use? The answer to this and plenty more coming up, stay tuned…

Update: Part 3, “Namespaces in Go - User” has been published and is available here.

--

--

Ed King

A Software Engineer currently working on and with Kubernetes.