mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-14 00:36:13 +02:00
lanscaping: drop more ipnauth
-rwxr-xr-x@ 1 bradfitz staff 9871042 Jan 11 14:21 /Users/bradfitz/bin/tailscaled.min -rwxr-xr-x@ 1 bradfitz staff 9765016 Jan 11 14:21 /Users/bradfitz/bin/tailscaled.minlinux Change-Id: I558009a377d699e51c585f8f397e1580434bdddd Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
c781951fdd
commit
b9745a5375
@ -38,7 +38,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/ipn/conffile from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
|
||||
@ -89,7 +88,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/views from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/ctxkey from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/util/ctxkey from tailscale.com/types/logger
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
tailscale.com/util/execqueue from tailscale.com/control/controlclient
|
||||
@ -113,7 +112,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/testenv from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/vizerror from tailscale.com/tailcfg+
|
||||
tailscale.com/util/winutil from tailscale.com/ipn/ipnauth
|
||||
tailscale.com/util/zstdframe from tailscale.com/control/controlclient
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscaled+
|
||||
@ -228,7 +226,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
os from crypto/rand+
|
||||
os/exec from tailscale.com/cmd/tailscaled+
|
||||
os/signal from tailscale.com/cmd/tailscaled
|
||||
os/user from tailscale.com/ipn/ipnauth+
|
||||
os/user from tailscale.com/ipn/ipnserver
|
||||
path from io/fs+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
|
||||
@ -42,7 +42,6 @@ import (
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/conffile"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/ipset"
|
||||
"tailscale.com/net/netcheck"
|
||||
@ -88,6 +87,8 @@ import (
|
||||
"tailscale.com/wgengine/wgcfg/nmcfg"
|
||||
)
|
||||
|
||||
type ipnauthActor interface{}
|
||||
|
||||
var controlDebugFlags = getControlDebugFlags()
|
||||
|
||||
func getControlDebugFlags() []string {
|
||||
@ -124,11 +125,11 @@ func RegisterNewSSHServer(fn newSSHServerFunc) {
|
||||
}
|
||||
|
||||
// watchSession represents a WatchNotifications channel,
|
||||
// an [ipnauth.Actor] that owns it (e.g., a connected GUI/CLI),
|
||||
// an [ipnauthActor] that owns it (e.g., a connected GUI/CLI),
|
||||
// and sessionID as required to close targeted buses.
|
||||
type watchSession struct {
|
||||
ch chan *ipn.Notify
|
||||
owner ipnauth.Actor // or nil
|
||||
owner ipnauthActor // or nil
|
||||
sessionID string
|
||||
cancel context.CancelFunc // to shut down the session
|
||||
}
|
||||
@ -235,9 +236,9 @@ type LocalBackend struct {
|
||||
endpoints []tailcfg.Endpoint
|
||||
blocked bool
|
||||
keyExpired bool
|
||||
authURL string // non-empty if not Running
|
||||
authURLTime time.Time // when the authURL was received from the control server
|
||||
authActor ipnauth.Actor // an actor who called [LocalBackend.StartLoginInteractive] last, or nil
|
||||
authURL string // non-empty if not Running
|
||||
authURLTime time.Time // when the authURL was received from the control server
|
||||
authActor ipnauthActor // an actor who called [LocalBackend.StartLoginInteractive] last, or nil
|
||||
egg bool
|
||||
prevIfState *netmon.State
|
||||
loginFlags controlclient.LoginFlags
|
||||
@ -260,7 +261,7 @@ type LocalBackend struct {
|
||||
componentLogUntil map[string]componentLogState
|
||||
// c2nUpdateStatus is the status of c2n-triggered client update.
|
||||
c2nUpdateStatus updateStatus
|
||||
currentUser ipnauth.Actor
|
||||
currentUser ipnauthActor
|
||||
selfUpdateProgress []ipnstate.UpdateProgress
|
||||
lastSelfUpdateState ipnstate.SelfUpdateStatus
|
||||
// capForcedNetfilter is the netfilter that control instructs Linux clients
|
||||
@ -2249,10 +2250,10 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
|
||||
b.WatchNotificationsAs(ctx, nil, mask, onWatchAdded, fn)
|
||||
}
|
||||
|
||||
// WatchNotificationsAs is like WatchNotifications but takes an [ipnauth.Actor]
|
||||
// WatchNotificationsAs is like WatchNotifications but takes an [ipnauthActor]
|
||||
// as an additional parameter. If non-nil, the specified callback is invoked
|
||||
// only for notifications relevant to this actor.
|
||||
func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.Actor, mask ipn.NotifyWatchOpt, onWatchAdded func(), fn func(roNotify *ipn.Notify) (keepGoing bool)) {
|
||||
func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauthActor, mask ipn.NotifyWatchOpt, onWatchAdded func(), fn func(roNotify *ipn.Notify) (keepGoing bool)) {
|
||||
ch := make(chan *ipn.Notify, 128)
|
||||
sessionID := rands.HexString(16)
|
||||
origFn := fn
|
||||
@ -2442,10 +2443,6 @@ type notificationTarget struct {
|
||||
// TODO(nickkhyl): make this field cross-platform rather
|
||||
// than Windows-specific.
|
||||
userID ipn.WindowsUserID
|
||||
// clientID identifies a client that should be the exclusive recipient
|
||||
// of the notification. A zero value indicates that notification should
|
||||
// be sent to all sessions of the specified user.
|
||||
clientID ipnauth.ClientID
|
||||
}
|
||||
|
||||
var allClients = notificationTarget{} // broadcast to all connected clients
|
||||
@ -2454,11 +2451,10 @@ var allClients = notificationTarget{} // broadcast to all connected clients
|
||||
// representing the same user as the specified actor. If the actor represents
|
||||
// a specific connected client, the [ipnauth.ClientID] must also match.
|
||||
// If the actor is nil, the [notificationTarget] matches all actors.
|
||||
func toNotificationTarget(actor ipnauth.Actor) notificationTarget {
|
||||
func toNotificationTarget(actor ipnauthActor) notificationTarget {
|
||||
t := notificationTarget{}
|
||||
if actor != nil {
|
||||
t.userID = actor.UserID()
|
||||
t.clientID, _ = actor.ClientID()
|
||||
// lanscaping
|
||||
}
|
||||
return t
|
||||
}
|
||||
@ -2466,22 +2462,8 @@ func toNotificationTarget(actor ipnauth.Actor) notificationTarget {
|
||||
// match reports whether the specified actor should receive notifications
|
||||
// targeting t. If the actor is nil, it should only receive notifications
|
||||
// intended for all users.
|
||||
func (t notificationTarget) match(actor ipnauth.Actor) bool {
|
||||
if t == allClients {
|
||||
return true
|
||||
}
|
||||
if actor == nil {
|
||||
return false
|
||||
}
|
||||
if t.userID != "" && t.userID != actor.UserID() {
|
||||
return false
|
||||
}
|
||||
if t.clientID != ipnauth.NoClientID {
|
||||
clientID, ok := actor.ClientID()
|
||||
if !ok || clientID != t.clientID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
func (t notificationTarget) match(actor ipnauthActor) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -2520,7 +2502,7 @@ func (b *LocalBackend) sendToLocked(n ipn.Notify, recipient notificationTarget)
|
||||
// If url is "", it is equivalent to calling [LocalBackend.resetAuthURLLocked] with b.mu held.
|
||||
func (b *LocalBackend) setAuthURL(url string) {
|
||||
var popBrowser, keyExpired bool
|
||||
var recipient ipnauth.Actor
|
||||
var recipient ipnauthActor
|
||||
|
||||
b.mu.Lock()
|
||||
switch {
|
||||
@ -2555,7 +2537,7 @@ func (b *LocalBackend) setAuthURL(url string) {
|
||||
// keyExpired is the value of b.keyExpired upon entry and indicates
|
||||
// whether the node's key has expired.
|
||||
// It must not be called with b.mu held.
|
||||
func (b *LocalBackend) popBrowserAuthNow(url string, keyExpired bool, recipient ipnauth.Actor) {
|
||||
func (b *LocalBackend) popBrowserAuthNow(url string, keyExpired bool, recipient ipnauthActor) {
|
||||
b.logf("popBrowserAuthNow(%q): url=%v, key-expired=%v, seamless-key-renewal=%v", maybeUsernameOf(recipient), url != "", keyExpired, b.seamlessRenewalEnabled())
|
||||
|
||||
// Deconfigure the local network data plane if:
|
||||
@ -2836,45 +2818,10 @@ func (b *LocalBackend) InServerMode() bool {
|
||||
// Currently (as of 2024-08-26), this is only used on Windows.
|
||||
// We plan to remove it as part of the multi-user and unattended mode improvements
|
||||
// as we progress on tailscale/corp#18342.
|
||||
func (b *LocalBackend) CheckIPNConnectionAllowed(actor ipnauth.Actor) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
serverModeUid := b.pm.CurrentUserID()
|
||||
if serverModeUid == "" {
|
||||
// Either this platform isn't a "multi-user" platform or we're not yet
|
||||
// running as one.
|
||||
return nil
|
||||
}
|
||||
if !b.pm.CurrentPrefs().ForceDaemon() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Always allow Windows SYSTEM user to connect,
|
||||
// even if Tailscale is currently being used by another user.
|
||||
if actor.IsLocalSystem() {
|
||||
return nil
|
||||
}
|
||||
|
||||
uid := actor.UserID()
|
||||
if uid == "" {
|
||||
return errors.New("empty user uid in connection identity")
|
||||
}
|
||||
if uid != serverModeUid {
|
||||
return fmt.Errorf("Tailscale running in server mode (%q); connection from %q not allowed", b.tryLookupUserName(string(serverModeUid)), b.tryLookupUserName(string(uid)))
|
||||
}
|
||||
func (b *LocalBackend) CheckIPNConnectionAllowed(actor ipnauthActor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryLookupUserName tries to look up the username for the uid.
|
||||
// It returns the username on success, or the UID on failure.
|
||||
func (b *LocalBackend) tryLookupUserName(uid string) string {
|
||||
u, err := ipnauth.LookupUserFromID(b.logf, uid)
|
||||
if err != nil {
|
||||
return uid
|
||||
}
|
||||
return u.Username
|
||||
}
|
||||
|
||||
// StartLoginInteractive requests a new interactive login from controlclient,
|
||||
// unless such a flow is already in progress, in which case
|
||||
// StartLoginInteractive attempts to pick up the in-progress flow where it left
|
||||
@ -2883,12 +2830,12 @@ func (b *LocalBackend) StartLoginInteractive(ctx context.Context) error {
|
||||
return b.StartLoginInteractiveAs(ctx, nil)
|
||||
}
|
||||
|
||||
// StartLoginInteractiveAs is like StartLoginInteractive but takes an [ipnauth.Actor]
|
||||
// StartLoginInteractiveAs is like StartLoginInteractive but takes an [ipnauthActor]
|
||||
// as an additional parameter. If non-nil, the specified user is expected to complete
|
||||
// the interactive login, and therefore will receive the BrowseToURL notification once
|
||||
// the control plane sends us one. Otherwise, the notification will be delivered to all
|
||||
// active [watchSession]s.
|
||||
func (b *LocalBackend) StartLoginInteractiveAs(ctx context.Context, user ipnauth.Actor) error {
|
||||
func (b *LocalBackend) StartLoginInteractiveAs(ctx context.Context, user ipnauthActor) error {
|
||||
b.mu.Lock()
|
||||
if b.cc == nil {
|
||||
panic("LocalBackend.assertClient: b.cc == nil")
|
||||
@ -3037,46 +2984,6 @@ func (b *LocalBackend) shouldUploadServices() bool {
|
||||
return !p.ShieldsUp() && b.netMap.CollectServices
|
||||
}
|
||||
|
||||
// SetCurrentUser is used to implement support for multi-user systems (only
|
||||
// Windows 2022-11-25). On such systems, the uid is used to determine which
|
||||
// user's state should be used. The current user is maintained by active
|
||||
// connections open to the backend.
|
||||
//
|
||||
// When the backend initially starts it will typically start with no user. Then,
|
||||
// the first connection to the backend from the GUI frontend will set the
|
||||
// current user. Once set, the current user cannot be changed until all previous
|
||||
// connections are closed. The user is also used to determine which
|
||||
// LoginProfiles are accessible.
|
||||
//
|
||||
// In unattended mode, the backend will start with the user which enabled
|
||||
// unattended mode. The user must disable unattended mode before the user can be
|
||||
// changed.
|
||||
//
|
||||
// On non-multi-user systems, the user should be set to nil.
|
||||
//
|
||||
// SetCurrentUser returns the ipn.WindowsUserID associated with the user
|
||||
// when successful.
|
||||
func (b *LocalBackend) SetCurrentUser(actor ipnauth.Actor) (ipn.WindowsUserID, error) {
|
||||
var uid ipn.WindowsUserID
|
||||
if actor != nil {
|
||||
uid = actor.UserID()
|
||||
}
|
||||
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
if b.pm.CurrentUserID() == uid {
|
||||
return uid, nil
|
||||
}
|
||||
b.pm.SetCurrentUserID(uid)
|
||||
if c, ok := b.currentUser.(ipnauth.ActorCloser); ok {
|
||||
c.Close()
|
||||
}
|
||||
b.currentUser = actor
|
||||
b.resetForProfileChangeLockedOnEntry(unlock)
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) CheckPrefs(p *ipn.Prefs) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
@ -3989,12 +3896,6 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
|
||||
b.setNetMapLocked(nil)
|
||||
b.pm.Reset()
|
||||
if b.currentUser != nil {
|
||||
if c, ok := b.currentUser.(ipnauth.ActorCloser); ok {
|
||||
c.Close()
|
||||
}
|
||||
b.currentUser = nil
|
||||
}
|
||||
b.keyExpired = false
|
||||
b.resetAuthURLLocked()
|
||||
b.activeLogin = ""
|
||||
@ -5149,12 +5050,8 @@ func (b *LocalBackend) srcIPHasCapForFilter(srcIP netip.Addr, cap tailcfg.NodeCa
|
||||
|
||||
// maybeUsernameOf returns the actor's username if the actor
|
||||
// is non-nil and its username can be resolved.
|
||||
func maybeUsernameOf(actor ipnauth.Actor) string {
|
||||
var username string
|
||||
if actor != nil {
|
||||
username, _ = actor.Username()
|
||||
}
|
||||
return username
|
||||
func maybeUsernameOf(actor ipnauthActor) string {
|
||||
return "root"
|
||||
}
|
||||
|
||||
// VIPServices returns the list of tailnet services that this node
|
||||
|
||||
@ -1,176 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/ctxkey"
|
||||
"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
|
||||
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, isLocalSystem: connIsLocalSystem(ci)}, nil
|
||||
}
|
||||
|
||||
// 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.ci.WindowsUserID()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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":
|
||||
return "root", nil
|
||||
default:
|
||||
return "", errors.New("unsupported OS")
|
||||
}
|
||||
}
|
||||
|
||||
type actorOrError struct {
|
||||
actor *actor
|
||||
err error
|
||||
}
|
||||
|
||||
func (a actorOrError) unwrap() (*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})
|
||||
}
|
||||
|
||||
// actorFromContext returns an [actor] associated with ctx,
|
||||
// or an error if the context does not carry an actor's identity.
|
||||
func actorFromContext(ctx context.Context) (*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":
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -8,8 +8,6 @@ package ipnserver
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -27,7 +25,6 @@ import (
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
@ -49,9 +46,8 @@ type Server struct {
|
||||
// lock order: mu, then LocalBackend.mu
|
||||
mu sync.Mutex
|
||||
lastUserID ipn.WindowsUserID // tracks last userid; on change, Reset state for paranoia
|
||||
activeReqs map[*http.Request]*actor
|
||||
backendWaiter waiterSet // of LocalBackend waiters
|
||||
zeroReqWaiter waiterSet // of blockUntilZeroConnections waiters
|
||||
backendWaiter waiterSet // of LocalBackend waiters
|
||||
zeroReqWaiter waiterSet // of blockUntilZeroConnections waiters
|
||||
}
|
||||
|
||||
func (s *Server) mustBackend() *ipnlocal.LocalBackend {
|
||||
@ -170,21 +166,10 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ci, err := actorFromContext(r.Context())
|
||||
if err != nil {
|
||||
if errors.Is(err, errNoActor) {
|
||||
http.Error(w, "internal error: "+err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
onDone, err := s.addActiveHTTPRequest(r, ci)
|
||||
onDone, err := s.addActiveHTTPRequest(r)
|
||||
if err != nil {
|
||||
if ou, ok := err.(inUseOtherUserError); ok && localapi.InUseOtherUserIPNStream(w, r, ou.Unwrap()) {
|
||||
w.(http.Flusher).Flush()
|
||||
s.blockWhileIdentityInUse(ctx, ci)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
@ -194,8 +179,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/") {
|
||||
lah := localapi.NewHandler(lb, s.logf, s.backendLogID)
|
||||
lah.PermitRead, lah.PermitWrite = ci.Permissions(lb.OperatorUserID())
|
||||
lah.Actor = ci
|
||||
lah.PermitRead, lah.PermitWrite = true, true
|
||||
lah.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@ -228,88 +212,11 @@ func (e inUseOtherUserError) Unwrap() error { return e.error }
|
||||
// The returned error, when non-nil, will be of type inUseOtherUserError.
|
||||
//
|
||||
// s.mu must be held.
|
||||
func (s *Server) checkConnIdentityLocked(ci *actor) error {
|
||||
// If clients are already connected, verify they're the same user.
|
||||
// This mostly matters on Windows at the moment.
|
||||
if len(s.activeReqs) > 0 {
|
||||
var active *actor
|
||||
for _, active = range s.activeReqs {
|
||||
break
|
||||
}
|
||||
if active != nil {
|
||||
// Always allow Windows SYSTEM user to connect,
|
||||
// even if Tailscale is currently being used by another user.
|
||||
if ci.IsLocalSystem() {
|
||||
return nil
|
||||
}
|
||||
func (s *Server) checkConnIdentityLocked() error {
|
||||
|
||||
if ci.UserID() != active.UserID() {
|
||||
var b strings.Builder
|
||||
b.WriteString("Tailscale already in use")
|
||||
if username, err := active.Username(); err == nil {
|
||||
fmt.Fprintf(&b, " by %s", username)
|
||||
}
|
||||
fmt.Fprintf(&b, ", pid %d", active.pid())
|
||||
return inUseOtherUserError{errors.New(b.String())}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := s.mustBackend().CheckIPNConnectionAllowed(ci); err != nil {
|
||||
return inUseOtherUserError{err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// blockWhileIdentityInUse blocks while ci can't connect to the server because
|
||||
// the server is in use by a different user.
|
||||
//
|
||||
// This is primarily used for the Windows GUI, to block until one user's done
|
||||
// controlling the tailscaled process.
|
||||
func (s *Server) blockWhileIdentityInUse(ctx context.Context, actor *actor) error {
|
||||
inUse := func() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
_, ok := s.checkConnIdentityLocked(actor).(inUseOtherUserError)
|
||||
return ok
|
||||
}
|
||||
for inUse() {
|
||||
// Check whenever the connection count drops down to zero.
|
||||
ready, cleanup := s.zeroReqWaiter.add(&s.mu, ctx)
|
||||
<-ready
|
||||
cleanup()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Permissions returns the actor's permissions for accessing
|
||||
// the Tailscale local daemon API. The operatorUID is only used on
|
||||
// Unix-like platforms and specifies the ID of a local user
|
||||
// (in the os/user.User.Uid string form) who is allowed
|
||||
// to operate tailscaled without being root or using sudo.
|
||||
func (a *actor) Permissions(operatorUID string) (read, write bool) {
|
||||
switch envknob.GOOS() {
|
||||
case "windows":
|
||||
// As of 2024-08-27, according to the current permission model,
|
||||
// Windows users always have read/write access to the local API if
|
||||
// they're allowed to connect. Whether a user is allowed to connect
|
||||
// is determined by [Server.checkConnIdentityLocked] when adding a
|
||||
// new connection in [Server.addActiveHTTPRequest]. Therefore, it's
|
||||
// acceptable to permit read and write access without any additional
|
||||
// checks here. Note that this permission model is being changed in
|
||||
// tailscale/corp#18342.
|
||||
return true, true
|
||||
case "js":
|
||||
return true, true
|
||||
}
|
||||
if a.ci.IsUnixSock() {
|
||||
return true, !a.ci.IsReadonlyConn(operatorUID, logger.Discard)
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
// userIDFromString maps from either a numeric user id in string form
|
||||
// ("998") or username ("caddy") to its string userid ("998").
|
||||
// It returns the empty string on error.
|
||||
@ -339,10 +246,7 @@ func isAllDigit(s string) bool {
|
||||
// The returned error may be of type [inUseOtherUserError].
|
||||
//
|
||||
// onDone must be called when the HTTP request is done.
|
||||
func (s *Server) addActiveHTTPRequest(req *http.Request, actor *actor) (onDone func(), err error) {
|
||||
if actor == nil {
|
||||
return nil, errors.New("internal error: nil actor")
|
||||
}
|
||||
func (s *Server) addActiveHTTPRequest(req *http.Request) (onDone func(), err error) {
|
||||
|
||||
lb := s.mustBackend()
|
||||
|
||||
@ -359,51 +263,12 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, actor *actor) (onDone f
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if err := s.checkConnIdentityLocked(actor); err != nil {
|
||||
if err := s.checkConnIdentityLocked(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mak.Set(&s.activeReqs, req, actor)
|
||||
|
||||
if len(s.activeReqs) == 1 {
|
||||
if envknob.GOOS() == "windows" && !actor.IsLocalSystem() {
|
||||
// Tell the LocalBackend about the identity we're now running as,
|
||||
// unless its the SYSTEM user. That user is not a real account and
|
||||
// doesn't have a home directory.
|
||||
uid, err := lb.SetCurrentUser(actor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.lastUserID != uid {
|
||||
if s.lastUserID != "" {
|
||||
doReset = true
|
||||
}
|
||||
s.lastUserID = uid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDone = func() {
|
||||
s.mu.Lock()
|
||||
delete(s.activeReqs, req)
|
||||
remain := len(s.activeReqs)
|
||||
s.mu.Unlock()
|
||||
|
||||
if remain == 0 && s.resetOnZero {
|
||||
if lb.InServerMode() {
|
||||
s.logf("client disconnected; staying alive in server mode")
|
||||
} else {
|
||||
s.logf("client disconnected; stopping server")
|
||||
lb.ResetForClientDisconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Wake up callers waiting for the server to be idle:
|
||||
if remain == 0 {
|
||||
s.mu.Lock()
|
||||
s.zeroReqWaiter.wakeAll()
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
return onDone, nil
|
||||
@ -476,10 +341,7 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error {
|
||||
hs := &http.Server{
|
||||
Handler: http.HandlerFunc(s.serveHTTP),
|
||||
BaseContext: func(_ net.Listener) context.Context { return ctx },
|
||||
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
|
||||
return contextWithActor(ctx, s.logf, c)
|
||||
},
|
||||
ErrorLog: logger.StdLogger(logger.WithPrefix(s.logf, "ipnserver: ")),
|
||||
ErrorLog: logger.StdLogger(logger.WithPrefix(s.logf, "ipnserver: ")),
|
||||
}
|
||||
if err := hs.Serve(ln); err != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
|
||||
@ -22,7 +22,6 @@ import (
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
@ -94,9 +93,6 @@ type Handler struct {
|
||||
// (operator user).
|
||||
PermitWrite bool
|
||||
|
||||
// Actor is the identity of the client connected to the Handler.
|
||||
Actor ipnauth.Actor
|
||||
|
||||
b *ipnlocal.LocalBackend
|
||||
logf logger.Logf
|
||||
backendLogID logid.PublicID
|
||||
@ -361,9 +357,6 @@ func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeC
|
||||
if !configIn.HasPathHandler() {
|
||||
return nil
|
||||
}
|
||||
if h.Actor.IsLocalAdmin(h.b.OperatorUserID()) {
|
||||
return nil
|
||||
}
|
||||
switch goos {
|
||||
case "windows":
|
||||
return errors.New("must be a Windows local admin to serve a path")
|
||||
@ -452,7 +445,7 @@ func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
ctx := r.Context()
|
||||
enc := json.NewEncoder(w)
|
||||
h.b.WatchNotificationsAs(ctx, h.Actor, mask, f.Flush, func(roNotify *ipn.Notify) (keepGoing bool) {
|
||||
h.b.WatchNotificationsAs(ctx, nil, mask, f.Flush, func(roNotify *ipn.Notify) (keepGoing bool) {
|
||||
err := enc.Encode(roNotify)
|
||||
if err != nil {
|
||||
h.logf("json.Encode: %v", err)
|
||||
@ -472,7 +465,7 @@ func (h *Handler) serveLoginInteractive(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h.b.StartLoginInteractiveAs(r.Context(), h.Actor)
|
||||
h.b.StartLoginInteractiveAs(r.Context(), nil)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user