ipnauth.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package ipnauth controls access to the LocalAPI.
  4. package ipnauth
  5. import (
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net"
  10. "os"
  11. "os/user"
  12. "runtime"
  13. "strconv"
  14. "tailscale.com/envknob"
  15. "tailscale.com/feature/buildfeatures"
  16. "tailscale.com/ipn"
  17. "tailscale.com/safesocket"
  18. "tailscale.com/types/logger"
  19. "tailscale.com/util/clientmetric"
  20. "tailscale.com/util/groupmember"
  21. "tailscale.com/util/winutil"
  22. "tailscale.com/version/distro"
  23. )
  24. // ErrNotImplemented is returned by ConnIdentity.WindowsToken when it is not
  25. // implemented for the current GOOS.
  26. var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
  27. // WindowsToken represents the current security context of a Windows user.
  28. type WindowsToken interface {
  29. io.Closer
  30. // EqualUIDs reports whether other refers to the same user ID as the receiver.
  31. EqualUIDs(other WindowsToken) bool
  32. // IsAdministrator reports whether the receiver is a member of the built-in
  33. // Administrators group, or else an error. Use IsElevated to determine whether
  34. // the receiver is actually utilizing administrative rights.
  35. IsAdministrator() (bool, error)
  36. // IsUID reports whether the receiver's user ID matches uid.
  37. IsUID(uid ipn.WindowsUserID) bool
  38. // UID returns the ipn.WindowsUserID associated with the receiver, or else
  39. // an error.
  40. UID() (ipn.WindowsUserID, error)
  41. // IsElevated reports whether the receiver is currently executing as an
  42. // elevated administrative user.
  43. IsElevated() bool
  44. // IsLocalSystem reports whether the receiver is the built-in SYSTEM user.
  45. IsLocalSystem() bool
  46. // UserDir returns the special directory identified by folderID as associated
  47. // with the receiver. folderID must be one of the KNOWNFOLDERID values from
  48. // the x/sys/windows package, serialized as a stringified GUID.
  49. UserDir(folderID string) (string, error)
  50. // Username returns the user name associated with the receiver.
  51. Username() (string, error)
  52. }
  53. // ConnIdentity represents the owner of a localhost TCP or unix socket connection
  54. // connecting to the LocalAPI.
  55. type ConnIdentity struct {
  56. conn net.Conn
  57. notWindows bool // runtime.GOOS != "windows"
  58. // Fields used when NotWindows:
  59. isUnixSock bool // Conn is a *net.UnixConn
  60. creds PeerCreds // or nil if peercred.Get was not implemented on this OS
  61. // Used on Windows:
  62. // TODO(bradfitz): merge these into the peercreds package and
  63. // use that for all.
  64. pid int
  65. }
  66. // WindowsUserID returns the local machine's userid of the connection
  67. // if it's on Windows. Otherwise it returns the empty string.
  68. //
  69. // It's suitable for passing to LookupUserFromID (os/user.LookupId) on any
  70. // operating system.
  71. func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID {
  72. if !buildfeatures.HasDebug && runtime.GOOS != "windows" {
  73. // This function is only implemented on non-Windows for simulating
  74. // Windows in tests. But that test (per comments below) is broken
  75. // anyway. So disable this testing path in non-debug builds
  76. // and just do the thing that optimizes away.
  77. return ""
  78. }
  79. if envknob.GOOS() != "windows" {
  80. return ""
  81. }
  82. if tok, err := ci.WindowsToken(); err == nil {
  83. defer tok.Close()
  84. if uid, err := tok.UID(); err == nil {
  85. return uid
  86. }
  87. }
  88. // For Linux tests running as Windows:
  89. const isBroken = true // TODO(bradfitz,maisem): fix tests; this doesn't work yet
  90. if ci.creds != nil && !isBroken {
  91. if uid, ok := ci.creds.UserID(); ok {
  92. return ipn.WindowsUserID(uid)
  93. }
  94. }
  95. return ""
  96. }
  97. func (ci *ConnIdentity) Pid() int { return ci.pid }
  98. func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock }
  99. func (ci *ConnIdentity) Creds() PeerCreds { return ci.creds }
  100. // PeerCreds is the interface for a github.com/tailscale/peercred.Creds,
  101. // if linked into the binary.
  102. //
  103. // (It's not used on some platforms, or if ts_omit_unixsocketidentity is set.)
  104. type PeerCreds interface {
  105. UserID() (uid string, ok bool)
  106. PID() (pid int, ok bool)
  107. }
  108. var metricIssue869Workaround = clientmetric.NewCounter("issue_869_workaround")
  109. // LookupUserFromID is a wrapper around os/user.LookupId that works around some
  110. // issues on Windows. On non-Windows platforms it's identical to user.LookupId.
  111. func LookupUserFromID(logf logger.Logf, uid string) (*user.User, error) {
  112. u, err := user.LookupId(uid)
  113. if err != nil && runtime.GOOS == "windows" {
  114. // See if uid resolves as a pseudo-user. Temporary workaround until
  115. // https://github.com/golang/go/issues/49509 resolves and ships.
  116. if u, err := winutil.LookupPseudoUser(uid); err == nil {
  117. return u, nil
  118. }
  119. // TODO(aaron): With LookupPseudoUser in place, I don't expect us to reach
  120. // this point anymore. Leaving the below workaround in for now to confirm
  121. // that pseudo-user resolution sufficiently handles this problem.
  122. // The below workaround is only applicable when uid represents a
  123. // valid security principal. Omitting this check causes us to succeed
  124. // even when uid represents a deleted user.
  125. if !winutil.IsSIDValidPrincipal(uid) {
  126. return nil, err
  127. }
  128. metricIssue869Workaround.Add(1)
  129. logf("[warning] issue 869: os/user.LookupId failed; ignoring")
  130. // Work around https://github.com/tailscale/tailscale/issues/869 for
  131. // now. We don't strictly need the username. It's just a nice-to-have.
  132. // So make up a *user.User if their machine is broken in this way.
  133. return &user.User{
  134. Uid: uid,
  135. Username: "unknown-user-" + uid,
  136. Name: "unknown user " + uid,
  137. }, nil
  138. }
  139. return u, err
  140. }
  141. // IsReadonlyConn reports whether the connection should be considered read-only,
  142. // meaning it's not allowed to change the state of the node.
  143. //
  144. // Read-only also means it's not allowed to access sensitive information, which
  145. // admittedly doesn't follow from the name. Consider this "IsUnprivileged".
  146. // Also, Windows doesn't use this. For Windows it always returns false.
  147. //
  148. // TODO(bradfitz): rename it? Also make Windows use this.
  149. func (ci *ConnIdentity) IsReadonlyConn(operatorUID string, logf logger.Logf) bool {
  150. if runtime.GOOS == "windows" {
  151. // Windows doesn't need/use this mechanism, at least yet. It
  152. // has a different last-user-wins auth model.
  153. return false
  154. }
  155. const ro = true
  156. const rw = false
  157. if !safesocket.PlatformUsesPeerCreds() {
  158. return rw
  159. }
  160. creds := ci.creds
  161. if creds == nil {
  162. logf("connection from unknown peer; read-only")
  163. return ro
  164. }
  165. uid, ok := creds.UserID()
  166. if !ok {
  167. logf("connection from peer with unknown userid; read-only")
  168. return ro
  169. }
  170. if uid == "0" {
  171. logf("connection from userid %v; root has access", uid)
  172. return rw
  173. }
  174. if selfUID := os.Getuid(); selfUID != 0 && uid == strconv.Itoa(selfUID) {
  175. logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
  176. return rw
  177. }
  178. if operatorUID != "" && uid == operatorUID {
  179. logf("connection from userid %v; is configured operator", uid)
  180. return rw
  181. }
  182. if yes, err := isLocalAdmin(uid); err != nil {
  183. logf("connection from userid %v; read-only; %v", uid, err)
  184. return ro
  185. } else if yes {
  186. logf("connection from userid %v; is local admin, has access", uid)
  187. return rw
  188. }
  189. logf("connection from userid %v; read-only", uid)
  190. return ro
  191. }
  192. func isLocalAdmin(uid string) (bool, error) {
  193. u, err := user.LookupId(uid)
  194. if err != nil {
  195. return false, err
  196. }
  197. var adminGroup string
  198. switch {
  199. case runtime.GOOS == "darwin":
  200. adminGroup = "admin"
  201. case distro.Get() == distro.QNAP:
  202. adminGroup = "administrators"
  203. default:
  204. return false, fmt.Errorf("no system admin group found")
  205. }
  206. return groupmember.IsMemberOfGroup(adminGroup, u.Username)
  207. }