Namespaces and Go Part 1
Linux provides the following namespaces and we will see how we can demonstrate these with Go
Namespace Constant Isolates Cgroup CLONE_NEWCGROUP Cgroup root directory IPC CLONE_NEWIPC System V IPC, POSIX message queues Network CLONE_NEWNETNetwork devices, stacks, ports, etc. Mount CLONE_NEWNS Mount points PID CLONE_NEWPID Process IDs User CLONE_NEWUSER User and group IDs UTS CLONE_NEWUTS Hostname and NIS domain name
This post is part one which explains the how we can implement User namespace
Namespaces were introduced in Linux to isolate a process .This is the fundamental idea which evolved to Linux containers (Docker,LXC etc..)
Lets start with a program user_namesapce_demo.go to execute a binary – here we will start a shell
package main import ( "os" "os/exec" ) func main() { cmd := exec.Command("/bin/sh") cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Run() }
Compile and execute it
[email protected]:~/.../Golang-Tutor> go build user_namesapce_demo.go [email protected]:~/.../Golang-Tutor> ./user_namesapce_demo sh-4.3$ id uid=1000(linxlabs) gid=100(users) groups=100(users) sh-4.3$
We can see the user id is same as the parent shell as expected.
How can we change this behavior so that the newly spawned process will get a new user ID instead of its parent user ID
To accomplish that ,we will use USER Namespace
Lets rewrite the program to include user namespace
package main import ( "os" "os/exec" "syscall" // For SysProcAttr to pass clone flag CLONE_NEWUSER ) func main() { cmd := exec.Command("/bin/sh") cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUSER, } cmd.Run() }
Compile and execute it to see the user id
[email protected]:~/.../Golang-Tutor> go build user_namesapce_demo.go [email protected]:~/.../Golang-Tutor> ./user_namesapce_demo sh-4.3$ id uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup) sh-4.3$
Now you can see “nobody” instead of “linxlabs”
We didn’t define any mappings in our program ,so Go will assign overflow ID 65534
So lets see what is UID/GID mappings
We will get a user id from parent process which will be called as “HostID”
In our “containerized” process we need a custom user ID which will be called as “ContainerID”
You may read more detailed implementation in this LKML article.
Below copied snipp is from the same article.
Normally, one of the first steps after creating a new user namespace is to define the mappings used for the user and group IDs of the processes that will be created in that namespace.
This is done by writing mapping information to the /proc/PID/uid_map and /proc/PID/gid_map files corresponding to one of the processes in the user namespace. (Initially, these two files are empty.)
This information consists of one or more lines, each of which contains three values separated by white space:
ID-inside-ns ID-outside-ns length
Together, the ID-inside-ns and length values define a range of IDs inside the namespace that are to be mapped to an ID range of the same length outside the namespace.
The ID-outside-ns value specifies the starting point of the outside range.
How ID-outside-ns is interpreted depends on the whether the process opening the file /proc/PID/uid_map (or /proc/PID/gid_map) is in the same user namespace as the process PID:
As per the documentation of SysProcAttr, we can add UID/GID mappings with below two structure members
UidMappings []SysProcIDMap // User ID mappings for user namespaces. GidMappings []SysProcIDMap // Group ID mappings for user namespaces.
SysProcIDMap structure have three members which matches with the kernel documentation (ID-inside-ns ,ID-outside-ns and length)
type SysProcIDMap struct { ContainerID int // Container ID. HostID int // Host ID. Size int // Size. }
So lets add few lines of codes to map both UID and GID
package main import ( "os" "os/exec" "syscall" ) func main() { cmd := exec.Command("/bin/sh") cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUSER, UidMappings: []syscall.SysProcIDMap{ { ContainerID: 0, HostID: os.Getuid(), Size: 1, }, }, GidMappings: []syscall.SysProcIDMap{ { ContainerID: 0, HostID: os.Getgid(), Size: 1, }, }, } cmd.Run() }
Lets compile and execute the program
[email protected]:~/.../Golang-Tutor> go build user_namesapce_demo.go [email protected]:~/.../Golang-Tutor> ./user_namesapce_demo sh-4.3# sh-4.3# id uid=0(root) gid=0(root) groups=0(root) sh-4.3#
Now the shell shows root instead of nobody as we did the UID/GID mappings to “0” which is root
But that’s not all . Even Though the shell shows root , we will not be able to execute any privileged commands
For example , changing hostname whill be denied with an error shown below
sh-4.3# hostname test hostname: you must be root to change the host name sh-4.3#
This is because we only used one namespace and there is more name spaces to initialize which will allow the program to execute privileged commands in their own isolated place
The hostname command modifies a kernel paramter which is part of UTS namespace and we will see that implementation and demo in next article . Stay tunen
If you like this article , please subscribe and follow us on social media