Browse code

Add utility/support package for user namespace support

The `pkg/idtools` package supports the creation of user(s) for
retrieving /etc/sub{u,g}id ranges and creation of the UID/GID mappings
provided to clone() to add support for user namespaces in Docker.

Docker-DCO-1.1-Signed-off-by: Phil Estes <estesp@linux.vnet.ibm.com> (github: estesp)

Phil Estes authored on 2015/10/09 00:46:10
Showing 3 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,207 @@
0
+package idtools
1
+
2
+import (
3
+	"bufio"
4
+	"fmt"
5
+	"os"
6
+	"sort"
7
+	"strconv"
8
+	"strings"
9
+
10
+	"github.com/docker/docker/pkg/system"
11
+)
12
+
13
+// IDMap contains a single entry for user namespace range remapping. An array
14
+// of IDMap entries represents the structure that will be provided to the Linux
15
+// kernel for creating a user namespace.
16
+type IDMap struct {
17
+	ContainerID int `json:"container_id"`
18
+	HostID      int `json:"host_id"`
19
+	Size        int `json:"size"`
20
+}
21
+
22
+type subIDRange struct {
23
+	Start  int
24
+	Length int
25
+}
26
+
27
+type ranges []subIDRange
28
+
29
+func (e ranges) Len() int           { return len(e) }
30
+func (e ranges) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
31
+func (e ranges) Less(i, j int) bool { return e[i].Start < e[j].Start }
32
+
33
+const (
34
+	subuidFileName string = "/etc/subuid"
35
+	subgidFileName string = "/etc/subgid"
36
+)
37
+
38
+// MkdirAllAs creates a directory (include any along the path) and then modifies
39
+// ownership to the requested uid/gid.  If the directory already exists, this
40
+// function will still change ownership to the requested uid/gid pair.
41
+func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error {
42
+	return mkdirAs(path, mode, ownerUID, ownerGID, true)
43
+}
44
+
45
+// MkdirAs creates a directory and then modifies ownership to the requested uid/gid.
46
+// If the directory already exists, this function still changes ownership
47
+func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error {
48
+	return mkdirAs(path, mode, ownerUID, ownerGID, false)
49
+}
50
+
51
+func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll bool) error {
52
+	if mkAll {
53
+		if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) {
54
+			return err
55
+		}
56
+	} else {
57
+		if err := os.Mkdir(path, mode); err != nil && !os.IsExist(err) {
58
+			return err
59
+		}
60
+	}
61
+	// even if it existed, we will chown to change ownership as requested
62
+	if err := os.Chown(path, ownerUID, ownerGID); err != nil {
63
+		return err
64
+	}
65
+	return nil
66
+}
67
+
68
+// GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps.
69
+// If the maps are empty, then the root uid/gid will default to "real" 0/0
70
+func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) {
71
+	var uid, gid int
72
+
73
+	if uidMap != nil {
74
+		xUID, err := ToHost(0, uidMap)
75
+		if err != nil {
76
+			return -1, -1, err
77
+		}
78
+		uid = xUID
79
+	}
80
+	if gidMap != nil {
81
+		xGID, err := ToHost(0, gidMap)
82
+		if err != nil {
83
+			return -1, -1, err
84
+		}
85
+		gid = xGID
86
+	}
87
+	return uid, gid, nil
88
+}
89
+
90
+// ToContainer takes an id mapping, and uses it to translate a
91
+// host ID to the remapped ID. If no map is provided, then the translation
92
+// assumes a 1-to-1 mapping and returns the passed in id
93
+func ToContainer(hostID int, idMap []IDMap) (int, error) {
94
+	if idMap == nil {
95
+		return hostID, nil
96
+	}
97
+	for _, m := range idMap {
98
+		if (hostID >= m.HostID) && (hostID <= (m.HostID + m.Size - 1)) {
99
+			contID := m.ContainerID + (hostID - m.HostID)
100
+			return contID, nil
101
+		}
102
+	}
103
+	return -1, fmt.Errorf("Host ID %d cannot be mapped to a container ID", hostID)
104
+}
105
+
106
+// ToHost takes an id mapping and a remapped ID, and translates the
107
+// ID to the mapped host ID. If no map is provided, then the translation
108
+// assumes a 1-to-1 mapping and returns the passed in id #
109
+func ToHost(contID int, idMap []IDMap) (int, error) {
110
+	if idMap == nil {
111
+		return contID, nil
112
+	}
113
+	for _, m := range idMap {
114
+		if (contID >= m.ContainerID) && (contID <= (m.ContainerID + m.Size - 1)) {
115
+			hostID := m.HostID + (contID - m.ContainerID)
116
+			return hostID, nil
117
+		}
118
+	}
119
+	return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID)
120
+}
121
+
122
+// CreateIDMappings takes a requested user and group name and
123
+// using the data from /etc/sub{uid,gid} ranges, creates the
124
+// proper uid and gid remapping ranges for that user/group pair
125
+func CreateIDMappings(username, groupname string) ([]IDMap, []IDMap, error) {
126
+	subuidRanges, err := parseSubuid(username)
127
+	if err != nil {
128
+		return nil, nil, err
129
+	}
130
+	subgidRanges, err := parseSubgid(groupname)
131
+	if err != nil {
132
+		return nil, nil, err
133
+	}
134
+	if len(subuidRanges) == 0 {
135
+		return nil, nil, fmt.Errorf("No subuid ranges found for user %q", username)
136
+	}
137
+	if len(subgidRanges) == 0 {
138
+		return nil, nil, fmt.Errorf("No subgid ranges found for group %q", groupname)
139
+	}
140
+
141
+	return createIDMap(subuidRanges), createIDMap(subgidRanges), nil
142
+}
143
+
144
+func createIDMap(subidRanges ranges) []IDMap {
145
+	idMap := []IDMap{}
146
+
147
+	// sort the ranges by lowest ID first
148
+	sort.Sort(subidRanges)
149
+	containerID := 0
150
+	for _, idrange := range subidRanges {
151
+		idMap = append(idMap, IDMap{
152
+			ContainerID: containerID,
153
+			HostID:      idrange.Start,
154
+			Size:        idrange.Length,
155
+		})
156
+		containerID = containerID + idrange.Length
157
+	}
158
+	return idMap
159
+}
160
+
161
+func parseSubuid(username string) (ranges, error) {
162
+	return parseSubidFile(subuidFileName, username)
163
+}
164
+
165
+func parseSubgid(username string) (ranges, error) {
166
+	return parseSubidFile(subgidFileName, username)
167
+}
168
+
169
+func parseSubidFile(path, username string) (ranges, error) {
170
+	var rangeList ranges
171
+
172
+	subidFile, err := os.Open(path)
173
+	if err != nil {
174
+		return rangeList, err
175
+	}
176
+	defer subidFile.Close()
177
+
178
+	s := bufio.NewScanner(subidFile)
179
+	for s.Scan() {
180
+		if err := s.Err(); err != nil {
181
+			return rangeList, err
182
+		}
183
+
184
+		text := strings.TrimSpace(s.Text())
185
+		if text == "" {
186
+			continue
187
+		}
188
+		parts := strings.Split(text, ":")
189
+		if len(parts) != 3 {
190
+			return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path)
191
+		}
192
+		if parts[0] == username {
193
+			// return the first entry for a user; ignores potential for multiple ranges per user
194
+			startid, err := strconv.Atoi(parts[1])
195
+			if err != nil {
196
+				return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
197
+			}
198
+			length, err := strconv.Atoi(parts[2])
199
+			if err != nil {
200
+				return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
201
+			}
202
+			rangeList = append(rangeList, subIDRange{startid, length})
203
+		}
204
+	}
205
+	return rangeList, nil
206
+}
0 207
new file mode 100644
... ...
@@ -0,0 +1,155 @@
0
+package idtools
1
+
2
+import (
3
+	"fmt"
4
+	"os/exec"
5
+	"path/filepath"
6
+	"strings"
7
+	"syscall"
8
+)
9
+
10
+// add a user and/or group to Linux /etc/passwd, /etc/group using standard
11
+// Linux distribution commands:
12
+// adduser --uid <id> --shell /bin/login --no-create-home --disabled-login --ingroup <groupname> <username>
13
+// useradd -M -u <id> -s /bin/nologin -N -g <groupname> <username>
14
+// addgroup --gid <id> <groupname>
15
+// groupadd -g <id> <groupname>
16
+
17
+const baseUID int = 10000
18
+const baseGID int = 10000
19
+const idMAX int = 65534
20
+
21
+var (
22
+	userCommand  string
23
+	groupCommand string
24
+
25
+	cmdTemplates = map[string]string{
26
+		"adduser":  "--uid %d --shell /bin/false --no-create-home --disabled-login --ingroup %s %s",
27
+		"useradd":  "-M -u %d -s /bin/false -N -g %s %s",
28
+		"addgroup": "--gid %d %s",
29
+		"groupadd": "-g %d %s",
30
+	}
31
+)
32
+
33
+func init() {
34
+	// set up which commands are used for adding users/groups dependent on distro
35
+	if _, err := resolveBinary("adduser"); err == nil {
36
+		userCommand = "adduser"
37
+	} else if _, err := resolveBinary("useradd"); err == nil {
38
+		userCommand = "useradd"
39
+	}
40
+	if _, err := resolveBinary("addgroup"); err == nil {
41
+		groupCommand = "addgroup"
42
+	} else if _, err := resolveBinary("groupadd"); err == nil {
43
+		groupCommand = "groupadd"
44
+	}
45
+}
46
+
47
+func resolveBinary(binname string) (string, error) {
48
+	binaryPath, err := exec.LookPath(binname)
49
+	if err != nil {
50
+		return "", err
51
+	}
52
+	resolvedPath, err := filepath.EvalSymlinks(binaryPath)
53
+	if err != nil {
54
+		return "", err
55
+	}
56
+	//only return no error if the final resolved binary basename
57
+	//matches what was searched for
58
+	if filepath.Base(resolvedPath) == binname {
59
+		return resolvedPath, nil
60
+	}
61
+	return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath)
62
+}
63
+
64
+// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair
65
+// and calls the appropriate helper function to add the group and then
66
+// the user to the group in /etc/group and /etc/passwd respectively.
67
+// This new user's /etc/sub{uid,gid} ranges will be used for user namespace
68
+// mapping ranges in containers.
69
+func AddNamespaceRangesUser(name string) (int, int, error) {
70
+	// Find unused uid, gid pair
71
+	uid, err := findUnusedUID(baseUID)
72
+	if err != nil {
73
+		return -1, -1, fmt.Errorf("Unable to find unused UID: %v", err)
74
+	}
75
+	gid, err := findUnusedGID(baseGID)
76
+	if err != nil {
77
+		return -1, -1, fmt.Errorf("Unable to find unused GID: %v", err)
78
+	}
79
+
80
+	// First add the group that we will use
81
+	if err := addGroup(name, gid); err != nil {
82
+		return -1, -1, fmt.Errorf("Error adding group %q: %v", name, err)
83
+	}
84
+	// Add the user as a member of the group
85
+	if err := addUser(name, uid, name); err != nil {
86
+		return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err)
87
+	}
88
+	return uid, gid, nil
89
+}
90
+
91
+func addUser(userName string, uid int, groupName string) error {
92
+
93
+	if userCommand == "" {
94
+		return fmt.Errorf("Cannot add user; no useradd/adduser binary found")
95
+	}
96
+	args := fmt.Sprintf(cmdTemplates[userCommand], uid, groupName, userName)
97
+	return execAddCmd(userCommand, args)
98
+}
99
+
100
+func addGroup(groupName string, gid int) error {
101
+
102
+	if groupCommand == "" {
103
+		return fmt.Errorf("Cannot add group; no groupadd/addgroup binary found")
104
+	}
105
+	args := fmt.Sprintf(cmdTemplates[groupCommand], gid, groupName)
106
+	// only error out if the error isn't that the group already exists
107
+	// if the group exists then our needs are already met
108
+	if err := execAddCmd(groupCommand, args); err != nil && !strings.Contains(err.Error(), "already exists") {
109
+		return err
110
+	}
111
+	return nil
112
+}
113
+
114
+func execAddCmd(cmd, args string) error {
115
+	execCmd := exec.Command(cmd, strings.Split(args, " ")...)
116
+	out, err := execCmd.CombinedOutput()
117
+	if err != nil {
118
+		return fmt.Errorf("Failed to add user/group with error: %v; output: %q", err, string(out))
119
+	}
120
+	return nil
121
+}
122
+
123
+func findUnusedUID(startUID int) (int, error) {
124
+	return findUnused("passwd", startUID)
125
+}
126
+
127
+func findUnusedGID(startGID int) (int, error) {
128
+	return findUnused("group", startGID)
129
+}
130
+
131
+func findUnused(file string, id int) (int, error) {
132
+	for {
133
+		cmdStr := fmt.Sprintf("cat /etc/%s | cut -d: -f3 | grep '^%d$'", file, id)
134
+		cmd := exec.Command("sh", "-c", cmdStr)
135
+		if err := cmd.Run(); err != nil {
136
+			// if a non-zero return code occurs, then we know the ID was not found
137
+			// and is usable
138
+			if exiterr, ok := err.(*exec.ExitError); ok {
139
+				// The program has exited with an exit code != 0
140
+				if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
141
+					if status.ExitStatus() == 1 {
142
+						//no match, we can use this ID
143
+						return id, nil
144
+					}
145
+				}
146
+			}
147
+			return -1, fmt.Errorf("Error looking in /etc/%s for unused ID: %v", file, err)
148
+		}
149
+		id++
150
+		if id > idMAX {
151
+			return -1, fmt.Errorf("Maximum id in %q reached with finding unused numeric ID", file)
152
+		}
153
+	}
154
+}
0 155
new file mode 100644
... ...
@@ -0,0 +1,12 @@
0
+// +build !linux
1
+
2
+package idtools
3
+
4
+import "fmt"
5
+
6
+// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair
7
+// and calls the appropriate helper function to add the group and then
8
+// the user to the group in /etc/group and /etc/passwd respectively.
9
+func AddNamespaceRangesUser(name string) (int, int, error) {
10
+	return -1, -1, fmt.Errorf("No support for adding users or groups on this OS")
11
+}