// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause // Package ipnauth controls access to the LocalAPI. package ipnauth import ( "errors" "fmt" "io" "net" "os" "os/user" "runtime" "strconv" "github.com/tailscale/peercred" "tailscale.com/envknob" "tailscale.com/ipn" "tailscale.com/safesocket" "tailscale.com/types/logger" "tailscale.com/util/clientmetric" "tailscale.com/util/groupmember" "tailscale.com/util/winutil" "tailscale.com/version/distro" ) // ErrNotImplemented is returned by ConnIdentity.WindowsToken when it is not // implemented for the current GOOS. var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS) // WindowsToken represents the current security context of a Windows user. type WindowsToken interface { io.Closer // EqualUIDs reports whether other refers to the same user ID as the receiver. EqualUIDs(other WindowsToken) bool // IsAdministrator reports whether the receiver is a member of the built-in // Administrators group, or else an error. Use IsElevated to determine whether // the receiver is actually utilizing administrative rights. IsAdministrator() (bool, error) // IsUID reports whether the receiver's user ID matches uid. IsUID(uid ipn.WindowsUserID) bool // UID returns the ipn.WindowsUserID associated with the receiver, or else // an error. UID() (ipn.WindowsUserID, error) // IsElevated reports whether the receiver is currently executing as an // elevated administrative user. IsElevated() bool // IsLocalSystem reports whether the receiver is the built-in SYSTEM user. IsLocalSystem() bool // UserDir returns the special directory identified by folderID as associated // with the receiver. folderID must be one of the KNOWNFOLDERID values from // the x/sys/windows package, serialized as a stringified GUID. UserDir(folderID string) (string, error) // Username returns the user name associated with the receiver. Username() (string, error) } // ConnIdentity represents the owner of a localhost TCP or unix socket connection // connecting to the LocalAPI. type ConnIdentity struct { conn net.Conn notWindows bool // runtime.GOOS != "windows" // Fields used when NotWindows: isUnixSock bool // Conn is a *net.UnixConn creds *peercred.Creds // or nil // Used on Windows: // TODO(bradfitz): merge these into the peercreds package and // use that for all. pid int } // WindowsUserID returns the local machine's userid of the connection // if it's on Windows. Otherwise it returns the empty string. // // It's suitable for passing to LookupUserFromID (os/user.LookupId) on any // operating system. func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID { if envknob.GOOS() != "windows" { return "" } if tok, err := ci.WindowsToken(); err == nil { defer tok.Close() if uid, err := tok.UID(); err == nil { return uid } } // For Linux tests running as Windows: const isBroken = true // TODO(bradfitz,maisem): fix tests; this doesn't work yet if ci.creds != nil && !isBroken { if uid, ok := ci.creds.UserID(); ok { return ipn.WindowsUserID(uid) } } return "" } func (ci *ConnIdentity) Pid() int { return ci.pid } func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock } func (ci *ConnIdentity) Creds() *peercred.Creds { return ci.creds } var metricIssue869Workaround = clientmetric.NewCounter("issue_869_workaround") // LookupUserFromID is a wrapper around os/user.LookupId that works around some // issues on Windows. On non-Windows platforms it's identical to user.LookupId. func LookupUserFromID(logf logger.Logf, uid string) (*user.User, error) { u, err := user.LookupId(uid) if err != nil && runtime.GOOS == "windows" { // See if uid resolves as a pseudo-user. Temporary workaround until // https://github.com/golang/go/issues/49509 resolves and ships. if u, err := winutil.LookupPseudoUser(uid); err == nil { return u, nil } // TODO(aaron): With LookupPseudoUser in place, I don't expect us to reach // this point anymore. Leaving the below workaround in for now to confirm // that pseudo-user resolution sufficiently handles this problem. // The below workaround is only applicable when uid represents a // valid security principal. Omitting this check causes us to succeed // even when uid represents a deleted user. if !winutil.IsSIDValidPrincipal(uid) { return nil, err } metricIssue869Workaround.Add(1) logf("[warning] issue 869: os/user.LookupId failed; ignoring") // Work around https://github.com/tailscale/tailscale/issues/869 for // now. We don't strictly need the username. It's just a nice-to-have. // So make up a *user.User if their machine is broken in this way. return &user.User{ Uid: uid, Username: "unknown-user-" + uid, Name: "unknown user " + uid, }, nil } return u, err } // IsReadonlyConn reports whether the connection should be considered read-only, // meaning it's not allowed to change the state of the node. // // Read-only also means it's not allowed to access sensitive information, which // admittedly doesn't follow from the name. Consider this "IsUnprivileged". // Also, Windows doesn't use this. For Windows it always returns false. // // TODO(bradfitz): rename it? Also make Windows use this. func (ci *ConnIdentity) IsReadonlyConn(operatorUID string, logf logger.Logf) bool { if runtime.GOOS == "windows" { // Windows doesn't need/use this mechanism, at least yet. It // has a different last-user-wins auth model. return false } const ro = true const rw = false if !safesocket.PlatformUsesPeerCreds() { return rw } creds := ci.creds if creds == nil { logf("connection from unknown peer; read-only") return ro } uid, ok := creds.UserID() if !ok { logf("connection from peer with unknown userid; read-only") return ro } if uid == "0" { logf("connection from userid %v; root has access", uid) return rw } if selfUID := os.Getuid(); selfUID != 0 && uid == strconv.Itoa(selfUID) { logf("connection from userid %v; connection from non-root user matching daemon has access", uid) return rw } if operatorUID != "" && uid == operatorUID { logf("connection from userid %v; is configured operator", uid) return rw } if yes, err := isLocalAdmin(uid); err != nil { logf("connection from userid %v; read-only; %v", uid, err) return ro } else if yes { logf("connection from userid %v; is local admin, has access", uid) return rw } logf("connection from userid %v; read-only", uid) return ro } func isLocalAdmin(uid string) (bool, error) { u, err := user.LookupId(uid) if err != nil { return false, err } var adminGroup string switch { case runtime.GOOS == "darwin": adminGroup = "admin" case distro.Get() == distro.QNAP: adminGroup = "administrators" default: return false, fmt.Errorf("no system admin group found") } return groupmember.IsMemberOfGroup(adminGroup, u.Username) }