| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- // 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"
- "tailscale.com/envknob"
- "tailscale.com/feature/buildfeatures"
- "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 PeerCreds // or nil if peercred.Get was not implemented on this OS
- // 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 !buildfeatures.HasDebug && runtime.GOOS != "windows" {
- // This function is only implemented on non-Windows for simulating
- // Windows in tests. But that test (per comments below) is broken
- // anyway. So disable this testing path in non-debug builds
- // and just do the thing that optimizes away.
- return ""
- }
- 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() PeerCreds { return ci.creds }
- // PeerCreds is the interface for a github.com/tailscale/peercred.Creds,
- // if linked into the binary.
- //
- // (It's not used on some platforms, or if ts_omit_unixsocketidentity is set.)
- type PeerCreds interface {
- UserID() (uid string, ok bool)
- PID() (pid int, ok bool)
- }
- 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)
- }
|