| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package ipnserver
- import (
- "context"
- "errors"
- "fmt"
- "net"
- "os/exec"
- "runtime"
- "time"
- "tailscale.com/feature/buildfeatures"
- "tailscale.com/ipn"
- "tailscale.com/ipn/ipnauth"
- "tailscale.com/types/logger"
- "tailscale.com/util/ctxkey"
- "tailscale.com/util/osuser"
- "tailscale.com/version"
- )
- var _ ipnauth.Actor = (*actor)(nil)
- // actor implements [ipnauth.Actor] and provides additional functionality that is
- // specific to the current (as of 2024-08-27) permission model.
- //
- // Deprecated: this type exists for compatibility reasons and will be removed as
- // we progress on tailscale/corp#18342.
- type actor struct {
- logf logger.Logf
- ci *ipnauth.ConnIdentity
- clientID ipnauth.ClientID
- userID ipn.WindowsUserID // cached Windows user ID of the connected client process.
- // accessOverrideReason specifies the reason for overriding certain access restrictions,
- // such as permitting a user to disconnect when the always-on mode is enabled,
- // provided that such justification is allowed by the policy.
- accessOverrideReason string
- isLocalSystem bool // whether the actor is the Windows' Local System identity.
- }
- func newActor(logf logger.Logf, c net.Conn) (*actor, error) {
- ci, err := ipnauth.GetConnIdentity(logf, c)
- if err != nil {
- return nil, err
- }
- var clientID ipnauth.ClientID
- if pid := ci.Pid(); pid != 0 {
- // Derive [ipnauth.ClientID] from the PID of the connected client process.
- // TODO(nickkhyl): This is transient and will be re-worked as we
- // progress on tailscale/corp#18342. At minimum, we should use a 2-tuple
- // (PID + StartTime) or a 3-tuple (PID + StartTime + UID) to identify
- // the client process. This helps prevent security issues where a
- // terminated client process's PID could be reused by a different
- // process. This is not currently an issue as we allow only one user to
- // connect anyway.
- // Additionally, we should consider caching authentication results since
- // operations like retrieving a username by SID might require network
- // connectivity on domain-joined devices and/or be slow.
- clientID = ipnauth.ClientIDFrom(pid)
- }
- return &actor{
- logf: logf,
- ci: ci,
- clientID: clientID,
- userID: ci.WindowsUserID(),
- isLocalSystem: connIsLocalSystem(ci),
- },
- nil
- }
- // actorWithAccessOverride returns a new actor that carries the specified
- // reason for overriding certain access restrictions, if permitted by the
- // policy. If the reason is "", it returns the base actor.
- func actorWithAccessOverride(baseActor *actor, reason string) *actor {
- if reason == "" {
- return baseActor
- }
- return &actor{
- logf: baseActor.logf,
- ci: baseActor.ci,
- clientID: baseActor.clientID,
- userID: baseActor.userID,
- accessOverrideReason: reason,
- isLocalSystem: baseActor.isLocalSystem,
- }
- }
- // CheckProfileAccess implements [ipnauth.Actor].
- func (a *actor) CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ipnauth.ProfileAccess, auditLogger ipnauth.AuditLogFunc) error {
- // TODO(nickkhyl): return errors of more specific types and have them
- // translated to the appropriate HTTP status codes in the API handler.
- if profile.LocalUserID() != a.UserID() {
- return errors.New("the target profile does not belong to the user")
- }
- switch requestedAccess {
- case ipnauth.Disconnect:
- // Disconnect is allowed if a user owns the profile and the policy permits it.
- return ipnauth.CheckDisconnectPolicy(a, profile, a.accessOverrideReason, auditLogger)
- default:
- return errors.New("the requested operation is not allowed")
- }
- }
- // IsLocalSystem implements [ipnauth.Actor].
- func (a *actor) IsLocalSystem() bool {
- return a.isLocalSystem
- }
- // IsLocalAdmin implements [ipnauth.Actor].
- func (a *actor) IsLocalAdmin(operatorUID string) bool {
- return a.isLocalSystem || connIsLocalAdmin(a.logf, a.ci, operatorUID)
- }
- // UserID implements [ipnauth.Actor].
- func (a *actor) UserID() ipn.WindowsUserID {
- return a.userID
- }
- func (a *actor) pid() int {
- return a.ci.Pid()
- }
- // ClientID implements [ipnauth.Actor].
- func (a *actor) ClientID() (_ ipnauth.ClientID, ok bool) {
- return a.clientID, a.clientID != ipnauth.NoClientID
- }
- // Context implements [ipnauth.Actor].
- func (a *actor) Context() context.Context { return context.Background() }
- // Username implements [ipnauth.Actor].
- func (a *actor) Username() (string, error) {
- if a.ci == nil {
- a.logf("[unexpected] missing ConnIdentity in ipnserver.actor")
- return "", errors.New("missing ConnIdentity")
- }
- switch runtime.GOOS {
- case "windows":
- tok, err := a.ci.WindowsToken()
- if err != nil {
- return "", fmt.Errorf("get windows token: %w", err)
- }
- defer tok.Close()
- return tok.Username()
- case "darwin", "linux", "illumos", "solaris", "openbsd":
- creds := a.ci.Creds()
- if creds == nil {
- return "", errors.New("peer credentials not implemented on this OS")
- }
- uid, ok := creds.UserID()
- if !ok {
- return "", errors.New("missing user ID")
- }
- u, err := osuser.LookupByUID(uid)
- if err != nil {
- return "", fmt.Errorf("lookup user: %w", err)
- }
- return u.Username, nil
- default:
- return "", errors.New("unsupported OS")
- }
- }
- type actorOrError struct {
- actor ipnauth.Actor
- err error
- }
- func (a actorOrError) unwrap() (ipnauth.Actor, error) {
- return a.actor, a.err
- }
- var errNoActor = errors.New("connection actor not available")
- var actorKey = ctxkey.New("ipnserver.actor", actorOrError{err: errNoActor})
- // contextWithActor returns a new context that carries the identity of the actor
- // owning the other end of the [net.Conn]. It can be retrieved with [actorFromContext].
- func contextWithActor(ctx context.Context, logf logger.Logf, c net.Conn) context.Context {
- actor, err := newActor(logf, c)
- return actorKey.WithValue(ctx, actorOrError{actor: actor, err: err})
- }
- // NewContextWithActorForTest returns a new context that carries the identity
- // of the specified actor. It is used in tests only.
- func NewContextWithActorForTest(ctx context.Context, actor ipnauth.Actor) context.Context {
- return actorKey.WithValue(ctx, actorOrError{actor: actor})
- }
- // actorFromContext returns an [ipnauth.Actor] associated with ctx,
- // or an error if the context does not carry an actor's identity.
- func actorFromContext(ctx context.Context) (ipnauth.Actor, error) {
- return actorKey.Value(ctx).unwrap()
- }
- func connIsLocalSystem(ci *ipnauth.ConnIdentity) bool {
- token, err := ci.WindowsToken()
- return err == nil && token.IsLocalSystem()
- }
- // connIsLocalAdmin reports whether the connected client has administrative
- // access to the local machine, for whatever that means with respect to the
- // current OS.
- //
- // This is useful because tailscaled itself always runs with elevated rights:
- // we want to avoid privilege escalation for certain mutative operations.
- func connIsLocalAdmin(logf logger.Logf, ci *ipnauth.ConnIdentity, operatorUID string) bool {
- if ci == nil {
- logf("[unexpected] missing ConnIdentity in LocalAPI Handler")
- return false
- }
- switch runtime.GOOS {
- case "windows":
- tok, err := ci.WindowsToken()
- if err != nil {
- if !errors.Is(err, ipnauth.ErrNotImplemented) {
- logf("ipnauth.ConnIdentity.WindowsToken() error: %v", err)
- }
- return false
- }
- defer tok.Close()
- return tok.IsElevated()
- case "darwin":
- // Unknown, or at least unchecked on sandboxed macOS variants. Err on
- // the side of less permissions.
- //
- // authorizeServeConfigForGOOSAndUserContext should not call
- // connIsLocalAdmin on sandboxed variants anyway.
- if version.IsSandboxedMacOS() {
- return false
- }
- // This is a standalone tailscaled setup, use the same logic as on
- // Linux.
- fallthrough
- case "linux":
- if !buildfeatures.HasUnixSocketIdentity {
- // Everybody is an admin if support for unix socket identities
- // is omitted for the build.
- return true
- }
- uid, ok := ci.Creds().UserID()
- if !ok {
- return false
- }
- // root is always admin.
- if uid == "0" {
- return true
- }
- // if non-root, must be operator AND able to execute "sudo tailscale".
- if operatorUID != "" && uid != operatorUID {
- return false
- }
- u, err := osuser.LookupByUID(uid)
- if err != nil {
- return false
- }
- // Short timeout just in case sudo hangs for some reason.
- ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
- defer cancel()
- if err := exec.CommandContext(ctx, "sudo", "--other-user="+u.Name, "--list", "tailscale").Run(); err != nil {
- return false
- }
- return true
- default:
- return false
- }
- }
|