actor.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package ipnserver
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "net"
  9. "os/exec"
  10. "runtime"
  11. "time"
  12. "tailscale.com/ipn"
  13. "tailscale.com/ipn/ipnauth"
  14. "tailscale.com/types/logger"
  15. "tailscale.com/util/ctxkey"
  16. "tailscale.com/util/osuser"
  17. "tailscale.com/util/syspolicy"
  18. "tailscale.com/version"
  19. )
  20. var _ ipnauth.Actor = (*actor)(nil)
  21. // actor implements [ipnauth.Actor] and provides additional functionality that is
  22. // specific to the current (as of 2024-08-27) permission model.
  23. //
  24. // Deprecated: this type exists for compatibility reasons and will be removed as
  25. // we progress on tailscale/corp#18342.
  26. type actor struct {
  27. logf logger.Logf
  28. ci *ipnauth.ConnIdentity
  29. clientID ipnauth.ClientID
  30. isLocalSystem bool // whether the actor is the Windows' Local System identity.
  31. }
  32. func newActor(logf logger.Logf, c net.Conn) (*actor, error) {
  33. ci, err := ipnauth.GetConnIdentity(logf, c)
  34. if err != nil {
  35. return nil, err
  36. }
  37. var clientID ipnauth.ClientID
  38. if pid := ci.Pid(); pid != 0 {
  39. // Derive [ipnauth.ClientID] from the PID of the connected client process.
  40. // TODO(nickkhyl): This is transient and will be re-worked as we
  41. // progress on tailscale/corp#18342. At minimum, we should use a 2-tuple
  42. // (PID + StartTime) or a 3-tuple (PID + StartTime + UID) to identify
  43. // the client process. This helps prevent security issues where a
  44. // terminated client process's PID could be reused by a different
  45. // process. This is not currently an issue as we allow only one user to
  46. // connect anyway.
  47. // Additionally, we should consider caching authentication results since
  48. // operations like retrieving a username by SID might require network
  49. // connectivity on domain-joined devices and/or be slow.
  50. clientID = ipnauth.ClientIDFrom(pid)
  51. }
  52. return &actor{logf: logf, ci: ci, clientID: clientID, isLocalSystem: connIsLocalSystem(ci)}, nil
  53. }
  54. // CheckProfileAccess implements [ipnauth.Actor].
  55. func (a *actor) CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ipnauth.ProfileAccess) error {
  56. if profile.LocalUserID() != a.UserID() {
  57. return errors.New("the target profile does not belong to the user")
  58. }
  59. switch requestedAccess {
  60. case ipnauth.Disconnect:
  61. if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); alwaysOn {
  62. // TODO(nickkhyl): check if disconnecting with justifications is allowed
  63. // and whether a justification is included in the request.
  64. return errors.New("profile access denied: always-on mode is enabled")
  65. }
  66. return nil
  67. default:
  68. return errors.New("the requested operation is not allowed")
  69. }
  70. }
  71. // IsLocalSystem implements [ipnauth.Actor].
  72. func (a *actor) IsLocalSystem() bool {
  73. return a.isLocalSystem
  74. }
  75. // IsLocalAdmin implements [ipnauth.Actor].
  76. func (a *actor) IsLocalAdmin(operatorUID string) bool {
  77. return a.isLocalSystem || connIsLocalAdmin(a.logf, a.ci, operatorUID)
  78. }
  79. // UserID implements [ipnauth.Actor].
  80. func (a *actor) UserID() ipn.WindowsUserID {
  81. return a.ci.WindowsUserID()
  82. }
  83. func (a *actor) pid() int {
  84. return a.ci.Pid()
  85. }
  86. // ClientID implements [ipnauth.Actor].
  87. func (a *actor) ClientID() (_ ipnauth.ClientID, ok bool) {
  88. return a.clientID, a.clientID != ipnauth.NoClientID
  89. }
  90. // Username implements [ipnauth.Actor].
  91. func (a *actor) Username() (string, error) {
  92. if a.ci == nil {
  93. a.logf("[unexpected] missing ConnIdentity in ipnserver.actor")
  94. return "", errors.New("missing ConnIdentity")
  95. }
  96. switch runtime.GOOS {
  97. case "windows":
  98. tok, err := a.ci.WindowsToken()
  99. if err != nil {
  100. return "", fmt.Errorf("get windows token: %w", err)
  101. }
  102. defer tok.Close()
  103. return tok.Username()
  104. case "darwin", "linux", "illumos", "solaris":
  105. uid, ok := a.ci.Creds().UserID()
  106. if !ok {
  107. return "", errors.New("missing user ID")
  108. }
  109. u, err := osuser.LookupByUID(uid)
  110. if err != nil {
  111. return "", fmt.Errorf("lookup user: %w", err)
  112. }
  113. return u.Username, nil
  114. default:
  115. return "", errors.New("unsupported OS")
  116. }
  117. }
  118. type actorOrError struct {
  119. actor ipnauth.Actor
  120. err error
  121. }
  122. func (a actorOrError) unwrap() (ipnauth.Actor, error) {
  123. return a.actor, a.err
  124. }
  125. var errNoActor = errors.New("connection actor not available")
  126. var actorKey = ctxkey.New("ipnserver.actor", actorOrError{err: errNoActor})
  127. // contextWithActor returns a new context that carries the identity of the actor
  128. // owning the other end of the [net.Conn]. It can be retrieved with [actorFromContext].
  129. func contextWithActor(ctx context.Context, logf logger.Logf, c net.Conn) context.Context {
  130. actor, err := newActor(logf, c)
  131. return actorKey.WithValue(ctx, actorOrError{actor: actor, err: err})
  132. }
  133. // actorFromContext returns an [ipnauth.Actor] associated with ctx,
  134. // or an error if the context does not carry an actor's identity.
  135. func actorFromContext(ctx context.Context) (ipnauth.Actor, error) {
  136. return actorKey.Value(ctx).unwrap()
  137. }
  138. func connIsLocalSystem(ci *ipnauth.ConnIdentity) bool {
  139. token, err := ci.WindowsToken()
  140. return err == nil && token.IsLocalSystem()
  141. }
  142. // connIsLocalAdmin reports whether the connected client has administrative
  143. // access to the local machine, for whatever that means with respect to the
  144. // current OS.
  145. //
  146. // This is useful because tailscaled itself always runs with elevated rights:
  147. // we want to avoid privilege escalation for certain mutative operations.
  148. func connIsLocalAdmin(logf logger.Logf, ci *ipnauth.ConnIdentity, operatorUID string) bool {
  149. if ci == nil {
  150. logf("[unexpected] missing ConnIdentity in LocalAPI Handler")
  151. return false
  152. }
  153. switch runtime.GOOS {
  154. case "windows":
  155. tok, err := ci.WindowsToken()
  156. if err != nil {
  157. if !errors.Is(err, ipnauth.ErrNotImplemented) {
  158. logf("ipnauth.ConnIdentity.WindowsToken() error: %v", err)
  159. }
  160. return false
  161. }
  162. defer tok.Close()
  163. return tok.IsElevated()
  164. case "darwin":
  165. // Unknown, or at least unchecked on sandboxed macOS variants. Err on
  166. // the side of less permissions.
  167. //
  168. // authorizeServeConfigForGOOSAndUserContext should not call
  169. // connIsLocalAdmin on sandboxed variants anyway.
  170. if version.IsSandboxedMacOS() {
  171. return false
  172. }
  173. // This is a standalone tailscaled setup, use the same logic as on
  174. // Linux.
  175. fallthrough
  176. case "linux":
  177. uid, ok := ci.Creds().UserID()
  178. if !ok {
  179. return false
  180. }
  181. // root is always admin.
  182. if uid == "0" {
  183. return true
  184. }
  185. // if non-root, must be operator AND able to execute "sudo tailscale".
  186. if operatorUID != "" && uid != operatorUID {
  187. return false
  188. }
  189. u, err := osuser.LookupByUID(uid)
  190. if err != nil {
  191. return false
  192. }
  193. // Short timeout just in case sudo hangs for some reason.
  194. ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
  195. defer cancel()
  196. if err := exec.CommandContext(ctx, "sudo", "--other-user="+u.Name, "--list", "tailscale").Run(); err != nil {
  197. return false
  198. }
  199. return true
  200. default:
  201. return false
  202. }
  203. }