mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	Updates #5794 Change-Id: I7b05cd29ec02085cb503bbcd0beb61bf455002ac Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			169 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			169 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| // Package osuser implements OS user lookup. It's a wrapper around os/user that
 | |
| // works on non-cgo builds.
 | |
| package osuser
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"log"
 | |
| 	"os/exec"
 | |
| 	"os/user"
 | |
| 	"runtime"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 	"unicode/utf8"
 | |
| 
 | |
| 	"tailscale.com/version/distro"
 | |
| )
 | |
| 
 | |
| // LookupByUIDWithShell is like os/user.LookupId but handles a few edge cases
 | |
| // like gokrazy and non-cgo lookups, and returns the user shell. The user shell
 | |
| // lookup is best-effort and may be empty.
 | |
| func LookupByUIDWithShell(uid string) (u *user.User, shell string, err error) {
 | |
| 	return lookup(uid, user.LookupId, true)
 | |
| }
 | |
| 
 | |
| // LookupByUsernameWithShell is like os/user.Lookup but handles a few edge
 | |
| // cases like gokrazy and non-cgo lookups, and returns the user shell. The user
 | |
| // shell lookup is best-effort and may be empty.
 | |
| func LookupByUsernameWithShell(username string) (u *user.User, shell string, err error) {
 | |
| 	return lookup(username, user.Lookup, true)
 | |
| }
 | |
| 
 | |
| // LookupByUID is like os/user.LookupId but handles a few edge cases like
 | |
| // gokrazy and non-cgo lookups.
 | |
| func LookupByUID(uid string) (*user.User, error) {
 | |
| 	u, _, err := lookup(uid, user.LookupId, false)
 | |
| 	return u, err
 | |
| }
 | |
| 
 | |
| // LookupByUsername is like os/user.Lookup but handles a few edge cases like
 | |
| // gokrazy and non-cgo lookups.
 | |
| func LookupByUsername(username string) (*user.User, error) {
 | |
| 	u, _, err := lookup(username, user.Lookup, false)
 | |
| 	return u, err
 | |
| }
 | |
| 
 | |
| // lookupStd is either user.Lookup or user.LookupId.
 | |
| type lookupStd func(string) (*user.User, error)
 | |
| 
 | |
| func lookup(usernameOrUID string, std lookupStd, wantShell bool) (*user.User, string, error) {
 | |
| 	// Skip getent entirely on Non-Unix platforms that won't ever have it.
 | |
| 	// (Using HasPrefix for "wasip1", anticipating that WASI support will
 | |
| 	// move beyond "preview 1" some day.)
 | |
| 	if runtime.GOOS == "windows" || runtime.GOOS == "js" || runtime.GOARCH == "wasm" || runtime.GOOS == "plan9" {
 | |
| 		var shell string
 | |
| 		if wantShell && runtime.GOOS == "plan9" {
 | |
| 			shell = "/bin/rc"
 | |
| 		}
 | |
| 		if runtime.GOOS == "plan9" {
 | |
| 			if u, err := user.Current(); err == nil {
 | |
| 				return u, shell, nil
 | |
| 			}
 | |
| 		}
 | |
| 		u, err := std(usernameOrUID)
 | |
| 		return u, shell, err
 | |
| 	}
 | |
| 
 | |
| 	// No getent on Gokrazy. So hard-code the login shell.
 | |
| 	if distro.Get() == distro.Gokrazy {
 | |
| 		var shell string
 | |
| 		if wantShell {
 | |
| 			shell = "/tmp/serial-busybox/ash"
 | |
| 		}
 | |
| 		u, err := std(usernameOrUID)
 | |
| 		if err != nil {
 | |
| 			return &user.User{
 | |
| 				Uid:      "0",
 | |
| 				Gid:      "0",
 | |
| 				Username: "root",
 | |
| 				Name:     "Gokrazy",
 | |
| 				HomeDir:  "/",
 | |
| 			}, shell, nil
 | |
| 		}
 | |
| 		return u, shell, nil
 | |
| 	}
 | |
| 
 | |
| 	if runtime.GOOS == "plan9" {
 | |
| 		return &user.User{
 | |
| 			Uid:      "0",
 | |
| 			Gid:      "0",
 | |
| 			Username: "glenda",
 | |
| 			Name:     "Glenda",
 | |
| 			HomeDir:  "/",
 | |
| 		}, "/bin/rc", nil
 | |
| 	}
 | |
| 
 | |
| 	// Start with getent if caller wants to get the user shell.
 | |
| 	if wantShell {
 | |
| 		return userLookupGetent(usernameOrUID, std)
 | |
| 	}
 | |
| 	// If shell is not required, try os/user.Lookup* first and only use getent
 | |
| 	// if that fails. This avoids spawning a child process when os/user lookup
 | |
| 	// succeeds.
 | |
| 	if u, err := std(usernameOrUID); err == nil {
 | |
| 		return u, "", nil
 | |
| 	}
 | |
| 	return userLookupGetent(usernameOrUID, std)
 | |
| }
 | |
| 
 | |
| func checkGetentInput(usernameOrUID string) bool {
 | |
| 	maxUid := 32
 | |
| 	if runtime.GOOS == "linux" {
 | |
| 		maxUid = 256
 | |
| 	}
 | |
| 	if len(usernameOrUID) > maxUid || len(usernameOrUID) == 0 {
 | |
| 		return false
 | |
| 	}
 | |
| 	for _, r := range usernameOrUID {
 | |
| 		if r < ' ' || r == 0x7f || r == utf8.RuneError { // TODO(bradfitz): more?
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // userLookupGetent uses "getent" to look up users so that even with static
 | |
| // tailscaled binaries without cgo (as we distribute), we can still look up
 | |
| // PAM/NSS users which the standard library's os/user without cgo won't get
 | |
| // (because of no libc hooks). If "getent" fails, userLookupGetent falls back
 | |
| // to the standard library.
 | |
| func userLookupGetent(usernameOrUID string, std lookupStd) (*user.User, string, error) {
 | |
| 	// Do some basic validation before passing this string to "getent", even though
 | |
| 	// getent should do its own validation.
 | |
| 	if !checkGetentInput(usernameOrUID) {
 | |
| 		return nil, "", errors.New("invalid username or UID")
 | |
| 	}
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 | |
| 	defer cancel()
 | |
| 	out, err := exec.CommandContext(ctx, "getent", "passwd", usernameOrUID).Output()
 | |
| 	if err != nil {
 | |
| 		log.Printf("error calling getent for user %q: %v", usernameOrUID, err)
 | |
| 		u, err := std(usernameOrUID)
 | |
| 		return u, "", err
 | |
| 	}
 | |
| 	// output is "alice:x:1001:1001:Alice Smith,,,:/home/alice:/bin/bash"
 | |
| 	f := strings.SplitN(strings.TrimSpace(string(out)), ":", 10)
 | |
| 	for len(f) < 7 {
 | |
| 		f = append(f, "")
 | |
| 	}
 | |
| 	var mandatoryFields = []int{0, 2, 3, 5}
 | |
| 	for _, v := range mandatoryFields {
 | |
| 		if f[v] == "" {
 | |
| 			log.Printf("getent for user %q returned invalid output: %q", usernameOrUID, out)
 | |
| 			u, err := std(usernameOrUID)
 | |
| 			return u, "", err
 | |
| 		}
 | |
| 	}
 | |
| 	return &user.User{
 | |
| 		Username: f[0],
 | |
| 		Uid:      f[2],
 | |
| 		Gid:      f[3],
 | |
| 		Name:     f[4],
 | |
| 		HomeDir:  f[5],
 | |
| 	}, f[6], nil
 | |
| }
 |