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)
| 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 |
+} |