diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index 61bf89f0b..4915ceafc 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -2,11 +2,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus filippo.io/edwards25519/field from filippo.io/edwards25519 - W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket - W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio - W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio - W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs - W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+ W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+ W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy @@ -43,6 +38,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs + W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket + W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio + W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio + W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs + W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+ L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink L github.com/vishvananda/netns from github.com/tailscale/netlink+ @@ -111,7 +111,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa 💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+ tailscale.com/net/wsconn from tailscale.com/cmd/derper+ tailscale.com/paths from tailscale.com/client/tailscale - tailscale.com/safesocket from tailscale.com/client/tailscale + 💣 tailscale.com/safesocket from tailscale.com/client/tailscale tailscale.com/syncs from tailscale.com/cmd/derper+ tailscale.com/tailcfg from tailscale.com/client/tailscale+ tailscale.com/tka from tailscale.com/client/tailscale+ diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 5908685c6..7225470f2 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -2,11 +2,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus filippo.io/edwards25519/field from filippo.io/edwards25519 - W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket - W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio - W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio - W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs - W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+ W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+ W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy @@ -47,6 +42,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+ github.com/skip2/go-qrcode/reedsolomon from github.com/skip2/go-qrcode + W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket + W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio + W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio + W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs + W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+ github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+ github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+ @@ -116,7 +116,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep 💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+ tailscale.com/net/wsconn from tailscale.com/control/controlhttp+ tailscale.com/paths from tailscale.com/cmd/tailscale/cli+ - tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+ + 💣 tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+ tailscale.com/syncs from tailscale.com/net/netcheck+ tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+ tailscale.com/tka from tailscale.com/client/tailscale+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 3ae8c6bef..2a8a5b838 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -2,11 +2,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus filippo.io/edwards25519/field from filippo.io/edwards25519 - W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket - W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio - W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio - W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs - W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+ W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+ W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy @@ -136,6 +131,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de LD github.com/pkg/sftp from tailscale.com/ssh/tailssh LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient + W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket + W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio + W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio + W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs + W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+ github.com/tailscale/golang-x-crypto/acme from tailscale.com/ipn/ipnlocal LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh LD 💣 github.com/tailscale/golang-x-crypto/internal/alias from github.com/tailscale/golang-x-crypto/chacha20 @@ -297,7 +297,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de 💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal tailscale.com/posture from tailscale.com/ipn/ipnlocal tailscale.com/proxymap from tailscale.com/tsd+ - tailscale.com/safesocket from tailscale.com/client/tailscale+ + 💣 tailscale.com/safesocket from tailscale.com/client/tailscale+ tailscale.com/smallzstd from tailscale.com/control/controlclient+ LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled tailscale.com/syncs from tailscale.com/net/netcheck+ @@ -352,7 +352,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de 💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+ W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+ - W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth tailscale.com/util/race from tailscale.com/net/dns/resolver tailscale.com/util/racebuild from tailscale.com/logpolicy tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+ diff --git a/go.mod b/go.mod index c34694db4..34b5bd7c5 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.21 require ( filippo.io/mkcert v1.4.4 - github.com/Microsoft/go-winio v0.6.1 github.com/akutz/memconn v0.1.0 github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa github.com/andybalholm/brotli v1.0.5 @@ -104,6 +103,7 @@ require ( ) require ( + github.com/Microsoft/go-winio v0.6.1 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/gorilla/securecookie v1.1.1 // indirect ) @@ -321,6 +321,7 @@ require ( github.com/stretchr/testify v1.8.4 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect + github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 github.com/tdakkota/asciicheck v0.2.0 // indirect github.com/tetafro/godot v1.4.11 // indirect github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect diff --git a/go.sum b/go.sum index 1c117802a..562315a02 100644 --- a/go.sum +++ b/go.sum @@ -868,6 +868,8 @@ github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff h1:vnxdYZUJb github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HPjrSuJYEkdZ+0ItmGQAQ75cRHIiftIyE= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns= github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ= github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e h1:JyeJF/HuSwvxWtsR1c0oKX1lzaSH5Wh4aX+MgiStaGQ= diff --git a/ipn/ipnauth/ipnauth.go b/ipn/ipnauth/ipnauth.go index a83ca459b..5dc2e2768 100644 --- a/ipn/ipnauth/ipnauth.go +++ b/ipn/ipnauth/ipnauth.go @@ -5,7 +5,9 @@ package ipnauth import ( + "errors" "fmt" + "io" "net" "net/netip" "os" @@ -25,6 +27,35 @@ import ( "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 + // 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 { @@ -38,9 +69,7 @@ type ConnIdentity struct { // Used on Windows: // TODO(bradfitz): merge these into the peercreds package and // use that for all. - pid int - userID ipn.WindowsUserID - user *user.User + pid int } // WindowsUserID returns the local machine's userid of the connection @@ -52,8 +81,11 @@ func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID { if envknob.GOOS() != "windows" { return "" } - if ci.userID != "" { - return ci.userID + 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 @@ -65,7 +97,6 @@ func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID { return "" } -func (ci *ConnIdentity) User() *user.User { return ci.user } func (ci *ConnIdentity) Pid() int { return ci.pid } func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock } func (ci *ConnIdentity) Creds() *peercred.Creds { return ci.creds } diff --git a/ipn/ipnauth/ipnauth_notwindows.go b/ipn/ipnauth/ipnauth_notwindows.go index 0a6275e65..135ab3674 100644 --- a/ipn/ipnauth/ipnauth_notwindows.go +++ b/ipn/ipnauth/ipnauth_notwindows.go @@ -21,3 +21,9 @@ func GetConnIdentity(_ logger.Logf, c net.Conn) (ci *ConnIdentity, err error) { ci.creds, _ = peercred.Get(c) return ci, nil } + +// WindowsToken is unsupported when GOOS != windows and always returns +// ErrNotImplemented. +func (ci *ConnIdentity) WindowsToken() (WindowsToken, error) { + return nil, ErrNotImplemented +} diff --git a/ipn/ipnauth/ipnauth_windows.go b/ipn/ipnauth/ipnauth_windows.go index abf795832..86cfd7969 100644 --- a/ipn/ipnauth/ipnauth_windows.go +++ b/ipn/ipnauth/ipnauth_windows.go @@ -6,53 +6,157 @@ package ipnauth import ( "fmt" "net" - "syscall" + "runtime" "unsafe" "golang.org/x/sys/windows" "tailscale.com/ipn" + "tailscale.com/safesocket" "tailscale.com/types/logger" - "tailscale.com/util/pidowner" ) -var ( - kernel32 = syscall.NewLazyDLL("kernel32.dll") - procGetNamedPipeClientProcessId = kernel32.NewProc("GetNamedPipeClientProcessId") -) - -func getNamedPipeClientProcessId(h windows.Handle) (pid uint32, err error) { - r1, _, err := procGetNamedPipeClientProcessId.Call(uintptr(h), uintptr(unsafe.Pointer(&pid))) - if r1 > 0 { - return pid, nil - } - return 0, err -} - // GetConnIdentity extracts the identity information from the connection // based on the user who owns the other end of the connection. // If c is not backed by a named pipe, an error is returned. func GetConnIdentity(logf logger.Logf, c net.Conn) (ci *ConnIdentity, err error) { ci = &ConnIdentity{conn: c} - h, ok := c.(interface { - Fd() uintptr - }) + wcc, ok := c.(*safesocket.WindowsClientConn) if !ok { - return ci, fmt.Errorf("not a windows handle: %T", c) + return nil, fmt.Errorf("not a WindowsClientConn: %T", c) } - pid, err := getNamedPipeClientProcessId(windows.Handle(h.Fd())) + ci.pid, err = wcc.ClientPID() if err != nil { - return ci, fmt.Errorf("getNamedPipeClientProcessId: %v", err) + return nil, err } - ci.pid = int(pid) - uid, err := pidowner.OwnerOfPID(ci.pid) + return ci, nil +} + +type token struct { + t windows.Token +} + +func (t *token) UID() (ipn.WindowsUserID, error) { + sid, err := t.uid() if err != nil { - return ci, fmt.Errorf("failed to map connection's pid to a user (WSL?): %w", err) + return "", fmt.Errorf("failed to look up user from token: %w", err) } - ci.userID = ipn.WindowsUserID(uid) - u, err := LookupUserFromID(logf, uid) + + return ipn.WindowsUserID(sid.String()), nil +} + +func (t *token) Username() (string, error) { + sid, err := t.uid() if err != nil { - return ci, fmt.Errorf("failed to look up user from userid: %w", err) + return "", fmt.Errorf("failed to look up user from token: %w", err) } - ci.user = u - return ci, nil + + username, domain, _, err := sid.LookupAccount("") + if err != nil { + return "", fmt.Errorf("failed to look up username from SID: %w", err) + } + + return fmt.Sprintf(`%s\%s`, domain, username), nil +} + +func (t *token) IsAdministrator() (bool, error) { + baSID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid) + if err != nil { + return false, err + } + + return t.t.IsMember(baSID) +} + +func (t *token) IsElevated() bool { + return t.t.IsElevated() +} + +func (t *token) UserDir(folderID string) (string, error) { + guid, err := windows.GUIDFromString(folderID) + if err != nil { + return "", err + } + + return t.t.KnownFolderPath((*windows.KNOWNFOLDERID)(unsafe.Pointer(&guid)), 0) +} + +func (t *token) Close() error { + if t.t == 0 { + return nil + } + if err := t.t.Close(); err != nil { + return err + } + t.t = 0 + runtime.SetFinalizer(t, nil) + return nil +} + +func (t *token) EqualUIDs(other WindowsToken) bool { + if t != nil && other == nil || t == nil && other != nil { + return false + } + ot, ok := other.(*token) + if !ok { + return false + } + if t == ot { + return true + } + uid, err := t.uid() + if err != nil { + return false + } + oUID, err := ot.uid() + if err != nil { + return false + } + return uid.Equals(oUID) +} + +func (t *token) uid() (*windows.SID, error) { + tu, err := t.t.GetTokenUser() + if err != nil { + return nil, err + } + + return tu.User.Sid, nil +} + +func (t *token) IsUID(uid ipn.WindowsUserID) bool { + tUID, err := t.UID() + if err != nil { + return false + } + + return tUID == uid +} + +// WindowsToken returns the WindowsToken representing the security context +// of the connection's client. +func (ci *ConnIdentity) WindowsToken() (WindowsToken, error) { + var wcc *safesocket.WindowsClientConn + var ok bool + if wcc, ok = ci.conn.(*safesocket.WindowsClientConn); !ok { + return nil, fmt.Errorf("not a WindowsClientConn: %T", ci.conn) + } + + // We duplicate the token's handle so that the WindowsToken we return may have + // a lifetime independent from the original connection. + var h windows.Handle + if err := windows.DuplicateHandle( + windows.CurrentProcess(), + windows.Handle(wcc.Token()), + windows.CurrentProcess(), + &h, + 0, + false, + windows.DUPLICATE_SAME_ACCESS, + ); err != nil { + return nil, err + } + + result := &token{t: windows.Token(h)} + runtime.SetFinalizer(result, func(t *token) { t.Close() }) + return result, nil } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index ad36fd37b..d9a0f3a78 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -261,6 +261,7 @@ type LocalBackend struct { componentLogUntil map[string]componentLogState // c2nUpdateStatus is the status of c2n-triggered client update. c2nUpdateStatus updateStatus + currentUser ipnauth.WindowsToken // ServeConfig fields. (also guarded by mu) lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig @@ -2722,7 +2723,7 @@ func (b *LocalBackend) shouldUploadServices() bool { return !p.ShieldsUp() && b.netMap.CollectServices } -// SetCurrentUserID is used to implement support for multi-user systems (only +// 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. @@ -2737,18 +2738,35 @@ func (b *LocalBackend) shouldUploadServices() bool { // unattended mode. The user must disable unattended mode before the user can be // changed. // -// On non-multi-user systems, the uid should be set to empty string. -func (b *LocalBackend) SetCurrentUserID(uid ipn.WindowsUserID) { +// On non-multi-user systems, the token should be set to nil. +// +// SetCurrentUser returns the ipn.WindowsUserID associated with token +// when successful. +func (b *LocalBackend) SetCurrentUser(token ipnauth.WindowsToken) (ipn.WindowsUserID, error) { + var uid ipn.WindowsUserID + if token != nil { + var err error + uid, err = token.UID() + if err != nil { + return "", err + } + } + b.mu.Lock() if b.pm.CurrentUserID() == uid { b.mu.Unlock() - return + return uid, nil } if err := b.pm.SetCurrentUserID(uid); err != nil { b.mu.Unlock() - return + return uid, nil } + if b.currentUser != nil { + b.currentUser.Close() + } + b.currentUser = token b.resetForProfileChangeLockedOnEntry() + return uid, nil } func (b *LocalBackend) CheckPrefs(p *ipn.Prefs) error { @@ -4112,6 +4130,10 @@ func (b *LocalBackend) ResetForClientDisconnect() { b.setNetMapLocked(nil) b.pm.Reset() + if b.currentUser != nil { + b.currentUser.Close() + b.currentUser = nil + } b.keyExpired = false b.authURL = "" b.authURLSticky = "" diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 705c01016..755919275 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -202,6 +202,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) { lah := localapi.NewHandler(lb, s.logf, s.netMon, s.backendLogID) lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci) lah.PermitCert = s.connCanFetchCerts(ci) + lah.CallerIsLocalAdmin = s.connIsLocalAdmin(ci) lah.ServeHTTP(w, r) return } @@ -242,8 +243,30 @@ func (s *Server) checkConnIdentityLocked(ci *ipnauth.ConnIdentity) error { for _, active = range s.activeReqs { break } - if active != nil && ci.WindowsUserID() != active.WindowsUserID() { - return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s, pid %d", active.User().Username, active.Pid())} + if active != nil { + chkTok, err := ci.WindowsToken() + if err == nil { + defer chkTok.Close() + } else if !errors.Is(err, ipnauth.ErrNotImplemented) { + return err + } + + activeTok, err := active.WindowsToken() + if err == nil { + defer activeTok.Close() + } else if !errors.Is(err, ipnauth.ErrNotImplemented) { + return err + } + + if chkTok != nil && !chkTok.EqualUIDs(activeTok) { + var b strings.Builder + b.WriteString("Tailscale already in use") + if username, err := activeTok.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 { @@ -341,6 +364,31 @@ func (s *Server) connCanFetchCerts(ci *ipnauth.ConnIdentity) bool { return false } +// connIsLocalAdmin reports whether ci has administrative access to the local +// machine, for whatever that means with respect to the current OS. +// +// This returns true only on Windows machines when the client user is a +// member of the built-in Administrators group (but not necessarily elevated). +// This is useful because, on Windows, tailscaled itself always runs with +// elevated rights: we want to avoid privilege escalation for certain mutative operations. +func (s *Server) connIsLocalAdmin(ci *ipnauth.ConnIdentity) bool { + tok, err := ci.WindowsToken() + if err != nil { + if !errors.Is(err, ipnauth.ErrNotImplemented) { + s.logf("ipnauth.ConnIdentity.WindowsToken() error: %v", err) + } + return false + } + defer tok.Close() + + isAdmin, err := tok.IsAdministrator() + if err != nil { + s.logf("ipnauth.WindowsToken.IsAdministrator() error: %v", err) + return false + } + return isAdmin +} + // addActiveHTTPRequest adds c to the server's list of active HTTP requests. // // If the returned error may be of type inUseOtherUserError. @@ -372,14 +420,25 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit mak.Set(&s.activeReqs, req, ci) - if uid := ci.WindowsUserID(); uid != "" && len(s.activeReqs) == 1 { - // Tell the LocalBackend about the identity we're now running as. - lb.SetCurrentUserID(uid) - if s.lastUserID != uid { - if s.lastUserID != "" { - doReset = true + if len(s.activeReqs) == 1 { + token, err := ci.WindowsToken() + if err != nil { + if !errors.Is(err, ipnauth.ErrNotImplemented) { + s.logf("error obtaining access token: %v", err) + } + } else { + // Tell the LocalBackend about the identity we're now running as. + uid, err := lb.SetCurrentUser(token) + if err != nil { + token.Close() + return nil, err + } + if s.lastUserID != uid { + if s.lastUserID != "" { + doReset = true + } + s.lastUserID = uid } - s.lastUserID = uid } } diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 4483efdd3..9671c2554 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -157,6 +157,17 @@ type Handler struct { // cert fetching access. PermitCert bool + // CallerIsLocalAdmin is whether the this handler is being invoked as a + // result of a LocalAPI call from a user who is a local admin of the current + // machine. + // + // As of 2023-10-26 it is only populated on Windows. + // + // It can be used to to restrict some LocalAPI operations which should only + // be run by an admin and not unprivileged users in a computing environment + // managed by IT admins. + CallerIsLocalAdmin bool + b *ipnlocal.LocalBackend logf logger.Logf netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand diff --git a/safesocket/pipe_windows.go b/safesocket/pipe_windows.go index a110f5f2b..999929120 100644 --- a/safesocket/pipe_windows.go +++ b/safesocket/pipe_windows.go @@ -3,16 +3,27 @@ package safesocket +//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go pipe_windows.go + import ( + "context" "fmt" "net" + "runtime" "syscall" + "time" - "github.com/Microsoft/go-winio" + "github.com/tailscale/go-winio" + "golang.org/x/sys/windows" ) func connect(s *ConnectionStrategy) (net.Conn, error) { - return winio.DialPipe(s.path, nil) + dl := time.Now().Add(20 * time.Second) + ctx, cancel := context.WithDeadline(context.Background(), dl) + defer cancel() + // We use the identification impersonation level so that tailscaled may + // obtain information about our token for access control purposes. + return winio.DialPipeAccessImpLevel(ctx, s.path, windows.GENERIC_READ|windows.GENERIC_WRITE, winio.PipeImpLevelIdentification) } func setFlags(network, address string, c syscall.RawConn) error { @@ -39,5 +50,109 @@ func listen(path string) (net.Listener, error) { if err != nil { return nil, fmt.Errorf("namedpipe.Listen: %w", err) } - return lc, nil + return &winIOPipeListener{Listener: lc}, nil +} + +// WindowsClientConn is an implementation of net.Conn that permits retrieval of +// the Windows access token associated with the connection's client. The +// embedded net.Conn must be a go-winio PipeConn. +type WindowsClientConn struct { + net.Conn + token windows.Token +} + +// winioPipeHandle is fulfilled by the underlying code implementing go-winio's +// PipeConn interface. +type winioPipeHandle interface { + // Fd returns the Windows handle associated with the connection. + Fd() uintptr +} + +func resolvePipeHandle(c net.Conn) windows.Handle { + wph, ok := c.(winioPipeHandle) + if !ok { + return 0 + } + return windows.Handle(wph.Fd()) +} + +func (conn *WindowsClientConn) handle() windows.Handle { + return resolvePipeHandle(conn.Conn) +} + +// ClientPID returns the pid of conn's client, or else an error. +func (conn *WindowsClientConn) ClientPID() (int, error) { + var pid uint32 + if err := getNamedPipeClientProcessId(conn.handle(), &pid); err != nil { + return -1, fmt.Errorf("GetNamedPipeClientProcessId: %w", err) + } + return int(pid), nil +} + +// Token returns the Windows access token of the client user. +func (conn *WindowsClientConn) Token() windows.Token { + return conn.token +} + +func (conn *WindowsClientConn) Close() error { + if conn.token != 0 { + conn.token.Close() + conn.token = 0 + } + return conn.Conn.Close() +} + +type winIOPipeListener struct { + net.Listener +} + +func (lw *winIOPipeListener) Accept() (net.Conn, error) { + conn, err := lw.Listener.Accept() + if err != nil { + return nil, err + } + + token, err := clientUserAccessToken(conn) + if err != nil { + conn.Close() + return nil, err + } + + return &WindowsClientConn{ + Conn: conn, + token: token, + }, nil } + +func clientUserAccessToken(c net.Conn) (windows.Token, error) { + h := resolvePipeHandle(c) + if h == 0 { + return 0, fmt.Errorf("not a windows handle: %T", c) + } + + // Impersonation touches thread-local state, so we need to lock until the + // client access token has been extracted. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if err := impersonateNamedPipeClient(h); err != nil { + return 0, err + } + defer func() { + // Revert the current thread's impersonation. + if err := windows.RevertToSelf(); err != nil { + panic(fmt.Errorf("could not revert impersonation: %w", err)) + } + }() + + // Extract the client's access token from the thread-local state. + var token windows.Token + if err := windows.OpenThreadToken(windows.CurrentThread(), windows.TOKEN_DUPLICATE|windows.TOKEN_QUERY, true, &token); err != nil { + return 0, err + } + + return token, nil +} + +//sys getNamedPipeClientProcessId(h windows.Handle, clientPid *uint32) (err error) [int32(failretval)==0] = kernel32.GetNamedPipeClientProcessId +//sys impersonateNamedPipeClient(h windows.Handle) (err error) [int32(failretval)==0] = advapi32.ImpersonateNamedPipeClient diff --git a/safesocket/zsyscall_windows.go b/safesocket/zsyscall_windows.go new file mode 100644 index 000000000..db22d7386 --- /dev/null +++ b/safesocket/zsyscall_windows.go @@ -0,0 +1,62 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package safesocket + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + + procImpersonateNamedPipeClient = modadvapi32.NewProc("ImpersonateNamedPipeClient") + procGetNamedPipeClientProcessId = modkernel32.NewProc("GetNamedPipeClientProcessId") +) + +func impersonateNamedPipeClient(h windows.Handle) (err error) { + r1, _, e1 := syscall.Syscall(procImpersonateNamedPipeClient.Addr(), 1, uintptr(h), 0, 0) + if int32(r1) == 0 { + err = errnoErr(e1) + } + return +} + +func getNamedPipeClientProcessId(h windows.Handle, clientPid *uint32) (err error) { + r1, _, e1 := syscall.Syscall(procGetNamedPipeClientProcessId.Addr(), 2, uintptr(h), uintptr(unsafe.Pointer(clientPid)), 0) + if int32(r1) == 0 { + err = errnoErr(e1) + } + return +}