mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-20 21:51:42 +00:00
ipn/ipnauth: start splitting ipnserver into new ipnauth package
We're trying to gut 90% of the ipnserver package. A lot will get deleted, some will move to LocalBackend, and a lot is being moved into this new ipn/ipnauth package which will be leaf-y and testable. This is a baby step towards moving some stuff to ipnauth. Update #6417 Updates tailscale/corp#8051 Change-Id: I28bc2126764f46597d92a2d72565009dc6927ee0 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
5f6fec0eba
commit
7bff7345cc
@ -173,7 +173,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from tailscale.com/net/tstun+
|
gvisor.dev/gvisor/pkg/tcpip/transport/udp from tailscale.com/net/tstun+
|
||||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
inet.af/peercred from tailscale.com/ipn/ipnauth
|
||||||
W 💣 inet.af/wf from tailscale.com/wf
|
W 💣 inet.af/wf from tailscale.com/wf
|
||||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||||
@ -198,6 +198,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
||||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||||
tailscale.com/ipn from tailscale.com/ipn/ipnlocal+
|
tailscale.com/ipn from tailscale.com/ipn/ipnlocal+
|
||||||
|
tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnserver
|
||||||
tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+
|
tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+
|
||||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||||
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
|
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
|
||||||
@ -228,7 +229,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
|
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
|
||||||
tailscale.com/net/netknob from tailscale.com/net/netns+
|
tailscale.com/net/netknob from tailscale.com/net/netns+
|
||||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver+
|
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnauth+
|
||||||
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
|
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
|
||||||
tailscale.com/net/packet from tailscale.com/net/tstun+
|
tailscale.com/net/packet from tailscale.com/net/tstun+
|
||||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||||
@ -281,13 +282,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||||
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
||||||
tailscale.com/util/goroutines from tailscale.com/control/controlclient+
|
tailscale.com/util/goroutines from tailscale.com/control/controlclient+
|
||||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
tailscale.com/util/groupmember from tailscale.com/ipn/ipnauth
|
||||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||||
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
||||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
||||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth
|
||||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||||
tailscale.com/util/strs from tailscale.com/hostinfo+
|
tailscale.com/util/strs from tailscale.com/hostinfo+
|
||||||
|
204
ipn/ipnauth/ipnauth.go
Normal file
204
ipn/ipnauth/ipnauth.go
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package ipnauth controls access to the LocalAPI.
|
||||||
|
package ipnauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"inet.af/peercred"
|
||||||
|
"tailscale.com/net/netstat"
|
||||||
|
"tailscale.com/safesocket"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/util/groupmember"
|
||||||
|
"tailscale.com/util/pidowner"
|
||||||
|
"tailscale.com/util/winutil"
|
||||||
|
"tailscale.com/version/distro"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnIdentity represents the owner of a localhost TCP or unix socket connection
|
||||||
|
// connecting to the LocalAPI.
|
||||||
|
type ConnIdentity struct {
|
||||||
|
conn net.Conn
|
||||||
|
notWindows bool // runtime.GOOS != "windows"
|
||||||
|
|
||||||
|
// Fields used when NotWindows:
|
||||||
|
isUnixSock bool // Conn is a *net.UnixConn
|
||||||
|
creds *peercred.Creds // or nil
|
||||||
|
|
||||||
|
// Used on Windows:
|
||||||
|
// TODO(bradfitz): merge these into the peercreds package and
|
||||||
|
// use that for all.
|
||||||
|
pid int
|
||||||
|
userID string
|
||||||
|
user *user.User
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ci *ConnIdentity) UserID() string { return ci.userID }
|
||||||
|
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) NotWindows() bool { return ci.notWindows }
|
||||||
|
func (ci *ConnIdentity) Creds() *peercred.Creds { return ci.creds }
|
||||||
|
|
||||||
|
// GetConnIdentity returns the localhost TCP connection's identity information
|
||||||
|
// (pid, userid, user). If it's not Windows (for now), it returns a nil error
|
||||||
|
// and a ConnIdentity with NotWindows set true. It's only an error if we expected
|
||||||
|
// to be able to map it and couldn't.
|
||||||
|
func GetConnIdentity(logf logger.Logf, c net.Conn) (ci *ConnIdentity, err error) {
|
||||||
|
ci = &ConnIdentity{conn: c}
|
||||||
|
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
|
||||||
|
ci.notWindows = true
|
||||||
|
_, ci.isUnixSock = c.(*net.UnixConn)
|
||||||
|
ci.creds, _ = peercred.Get(c)
|
||||||
|
return ci, nil
|
||||||
|
}
|
||||||
|
la, err := netip.ParseAddrPort(c.LocalAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
return ci, fmt.Errorf("parsing local address: %w", err)
|
||||||
|
}
|
||||||
|
ra, err := netip.ParseAddrPort(c.RemoteAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
return ci, fmt.Errorf("parsing local remote: %w", err)
|
||||||
|
}
|
||||||
|
if !la.Addr().IsLoopback() || !ra.Addr().IsLoopback() {
|
||||||
|
return ci, errors.New("non-loopback connection")
|
||||||
|
}
|
||||||
|
tab, err := netstat.Get()
|
||||||
|
if err != nil {
|
||||||
|
return ci, fmt.Errorf("failed to get local connection table: %w", err)
|
||||||
|
}
|
||||||
|
pid := peerPid(tab.Entries, la, ra)
|
||||||
|
if pid == 0 {
|
||||||
|
return ci, errors.New("no local process found matching localhost connection")
|
||||||
|
}
|
||||||
|
ci.pid = pid
|
||||||
|
uid, err := pidowner.OwnerOfPID(pid)
|
||||||
|
if err != nil {
|
||||||
|
var hint string
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
hint = " (WSL?)"
|
||||||
|
}
|
||||||
|
return ci, fmt.Errorf("failed to map connection's pid to a user%s: %w", hint, err)
|
||||||
|
}
|
||||||
|
ci.userID = uid
|
||||||
|
u, err := LookupUserFromID(logf, uid)
|
||||||
|
if err != nil {
|
||||||
|
return ci, fmt.Errorf("failed to look up user from userid: %w", err)
|
||||||
|
}
|
||||||
|
ci.user = u
|
||||||
|
return ci, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupUserFromID is a wrapper around os/user.LookupId that works around some
|
||||||
|
// issues on Windows. On non-Windows platforms it's identical to user.LookupId.
|
||||||
|
func LookupUserFromID(logf logger.Logf, uid string) (*user.User, error) {
|
||||||
|
u, err := user.LookupId(uid)
|
||||||
|
if err != nil && runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(0x534)) {
|
||||||
|
// The below workaround is only applicable when uid represents a
|
||||||
|
// valid security principal. Omitting this check causes us to succeed
|
||||||
|
// even when uid represents a deleted user.
|
||||||
|
if !winutil.IsSIDValidPrincipal(uid) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logf("[warning] issue 869: os/user.LookupId failed; ignoring")
|
||||||
|
// Work around https://github.com/tailscale/tailscale/issues/869 for
|
||||||
|
// now. We don't strictly need the username. It's just a nice-to-have.
|
||||||
|
// So make up a *user.User if their machine is broken in this way.
|
||||||
|
return &user.User{
|
||||||
|
Uid: uid,
|
||||||
|
Username: "unknown-user-" + uid,
|
||||||
|
Name: "unknown user " + uid,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsReadonlyConn reports whether the connection should be considered read-only,
|
||||||
|
// meaning it's not allowed to change the state of the node.
|
||||||
|
//
|
||||||
|
// Read-only also means it's not allowed to access sensitive information, which
|
||||||
|
// admittedly doesn't follow from the name. Consider this "IsUnprivileged".
|
||||||
|
// Also, Windows doesn't use this. For Windows it always returns false.
|
||||||
|
//
|
||||||
|
// TODO(bradfitz): rename it? Also make Windows use this.
|
||||||
|
func (ci *ConnIdentity) IsReadonlyConn(operatorUID string, logf logger.Logf) bool {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// Windows doesn't need/use this mechanism, at least yet. It
|
||||||
|
// has a different last-user-wins auth model.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const ro = true
|
||||||
|
const rw = false
|
||||||
|
if !safesocket.PlatformUsesPeerCreds() {
|
||||||
|
return rw
|
||||||
|
}
|
||||||
|
creds := ci.creds
|
||||||
|
if creds == nil {
|
||||||
|
logf("connection from unknown peer; read-only")
|
||||||
|
return ro
|
||||||
|
}
|
||||||
|
uid, ok := creds.UserID()
|
||||||
|
if !ok {
|
||||||
|
logf("connection from peer with unknown userid; read-only")
|
||||||
|
return ro
|
||||||
|
}
|
||||||
|
if uid == "0" {
|
||||||
|
logf("connection from userid %v; root has access", uid)
|
||||||
|
return rw
|
||||||
|
}
|
||||||
|
if selfUID := os.Getuid(); selfUID != 0 && uid == strconv.Itoa(selfUID) {
|
||||||
|
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
|
||||||
|
return rw
|
||||||
|
}
|
||||||
|
if operatorUID != "" && uid == operatorUID {
|
||||||
|
logf("connection from userid %v; is configured operator", uid)
|
||||||
|
return rw
|
||||||
|
}
|
||||||
|
if yes, err := isLocalAdmin(uid); err != nil {
|
||||||
|
logf("connection from userid %v; read-only; %v", uid, err)
|
||||||
|
return ro
|
||||||
|
} else if yes {
|
||||||
|
logf("connection from userid %v; is local admin, has access", uid)
|
||||||
|
return rw
|
||||||
|
}
|
||||||
|
logf("connection from userid %v; read-only", uid)
|
||||||
|
return ro
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLocalAdmin(uid string) (bool, error) {
|
||||||
|
u, err := user.LookupId(uid)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
var adminGroup string
|
||||||
|
switch {
|
||||||
|
case runtime.GOOS == "darwin":
|
||||||
|
adminGroup = "admin"
|
||||||
|
case distro.Get() == distro.QNAP:
|
||||||
|
adminGroup = "administrators"
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("no system admin group found")
|
||||||
|
}
|
||||||
|
return groupmember.IsMemberOfGroup(adminGroup, u.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func peerPid(entries []netstat.Entry, la, ra netip.AddrPort) int {
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.Local == ra && e.Remote == la {
|
||||||
|
return e.Pid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
@ -15,14 +15,12 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
@ -30,24 +28,20 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"go4.org/mem"
|
"go4.org/mem"
|
||||||
"inet.af/peercred"
|
|
||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/ipnauth"
|
||||||
"tailscale.com/ipn/ipnlocal"
|
"tailscale.com/ipn/ipnlocal"
|
||||||
"tailscale.com/ipn/localapi"
|
"tailscale.com/ipn/localapi"
|
||||||
"tailscale.com/logtail/backoff"
|
"tailscale.com/logtail/backoff"
|
||||||
"tailscale.com/net/dnsfallback"
|
"tailscale.com/net/dnsfallback"
|
||||||
"tailscale.com/net/netstat"
|
|
||||||
"tailscale.com/net/netutil"
|
"tailscale.com/net/netutil"
|
||||||
"tailscale.com/net/tsdial"
|
"tailscale.com/net/tsdial"
|
||||||
"tailscale.com/safesocket"
|
"tailscale.com/safesocket"
|
||||||
"tailscale.com/smallzstd"
|
"tailscale.com/smallzstd"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/util/groupmember"
|
|
||||||
"tailscale.com/util/pidowner"
|
|
||||||
"tailscale.com/util/systemd"
|
"tailscale.com/util/systemd"
|
||||||
"tailscale.com/util/winutil"
|
|
||||||
"tailscale.com/version"
|
"tailscale.com/version"
|
||||||
"tailscale.com/version/distro"
|
"tailscale.com/version/distro"
|
||||||
"tailscale.com/wgengine"
|
"tailscale.com/wgengine"
|
||||||
@ -107,109 +101,20 @@ type Server struct {
|
|||||||
bs *ipn.BackendServer
|
bs *ipn.BackendServer
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
serverModeUser *user.User // or nil if not in server mode
|
serverModeUser *user.User // or nil if not in server mode
|
||||||
lastUserID string // tracks last userid; on change, Reset state for paranoia
|
lastUserID string // tracks last userid; on change, Reset state for paranoia
|
||||||
allClients map[net.Conn]connIdentity // HTTP or IPN
|
allClients map[net.Conn]*ipnauth.ConnIdentity // HTTP or IPN
|
||||||
clients map[net.Conn]bool // subset of allClients; only IPN protocol
|
clients map[net.Conn]bool // subset of allClients; only IPN protocol
|
||||||
disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects
|
disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalBackend returns the server's LocalBackend.
|
// LocalBackend returns the server's LocalBackend.
|
||||||
func (s *Server) LocalBackend() *ipnlocal.LocalBackend { return s.b }
|
func (s *Server) LocalBackend() *ipnlocal.LocalBackend { return s.b }
|
||||||
|
|
||||||
// connIdentity represents the owner of a localhost TCP or unix socket connection.
|
|
||||||
type connIdentity struct {
|
|
||||||
Conn net.Conn
|
|
||||||
NotWindows bool // runtime.GOOS != "windows"
|
|
||||||
|
|
||||||
// Fields used when NotWindows:
|
|
||||||
IsUnixSock bool // Conn is a *net.UnixConn
|
|
||||||
Creds *peercred.Creds // or nil
|
|
||||||
|
|
||||||
// Used on Windows:
|
|
||||||
// TODO(bradfitz): merge these into the peercreds package and
|
|
||||||
// use that for all.
|
|
||||||
Pid int
|
|
||||||
UserID string
|
|
||||||
User *user.User
|
|
||||||
}
|
|
||||||
|
|
||||||
// getConnIdentity returns the localhost TCP connection's identity information
|
|
||||||
// (pid, userid, user). If it's not Windows (for now), it returns a nil error
|
|
||||||
// and a ConnIdentity with NotWindows set true. It's only an error if we expected
|
|
||||||
// to be able to map it and couldn't.
|
|
||||||
func (s *Server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
|
||||||
ci = connIdentity{Conn: c}
|
|
||||||
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
|
|
||||||
ci.NotWindows = true
|
|
||||||
_, ci.IsUnixSock = c.(*net.UnixConn)
|
|
||||||
ci.Creds, _ = peercred.Get(c)
|
|
||||||
return ci, nil
|
|
||||||
}
|
|
||||||
la, err := netip.ParseAddrPort(c.LocalAddr().String())
|
|
||||||
if err != nil {
|
|
||||||
return ci, fmt.Errorf("parsing local address: %w", err)
|
|
||||||
}
|
|
||||||
ra, err := netip.ParseAddrPort(c.RemoteAddr().String())
|
|
||||||
if err != nil {
|
|
||||||
return ci, fmt.Errorf("parsing local remote: %w", err)
|
|
||||||
}
|
|
||||||
if !la.Addr().IsLoopback() || !ra.Addr().IsLoopback() {
|
|
||||||
return ci, errors.New("non-loopback connection")
|
|
||||||
}
|
|
||||||
tab, err := netstat.Get()
|
|
||||||
if err != nil {
|
|
||||||
return ci, fmt.Errorf("failed to get local connection table: %w", err)
|
|
||||||
}
|
|
||||||
pid := peerPid(tab.Entries, la, ra)
|
|
||||||
if pid == 0 {
|
|
||||||
return ci, errors.New("no local process found matching localhost connection")
|
|
||||||
}
|
|
||||||
ci.Pid = pid
|
|
||||||
uid, err := pidowner.OwnerOfPID(pid)
|
|
||||||
if err != nil {
|
|
||||||
var hint string
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
hint = " (WSL?)"
|
|
||||||
}
|
|
||||||
return ci, fmt.Errorf("failed to map connection's pid to a user%s: %w", hint, err)
|
|
||||||
}
|
|
||||||
ci.UserID = uid
|
|
||||||
u, err := lookupUserFromID(s.logf, uid)
|
|
||||||
if err != nil {
|
|
||||||
return ci, fmt.Errorf("failed to look up user from userid: %w", err)
|
|
||||||
}
|
|
||||||
ci.User = u
|
|
||||||
return ci, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupUserFromID(logf logger.Logf, uid string) (*user.User, error) {
|
|
||||||
u, err := user.LookupId(uid)
|
|
||||||
if err != nil && runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(0x534)) {
|
|
||||||
// The below workaround is only applicable when uid represents a
|
|
||||||
// valid security principal. Omitting this check causes us to succeed
|
|
||||||
// even when uid represents a deleted user.
|
|
||||||
if !winutil.IsSIDValidPrincipal(uid) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logf("[warning] issue 869: os/user.LookupId failed; ignoring")
|
|
||||||
// Work around https://github.com/tailscale/tailscale/issues/869 for
|
|
||||||
// now. We don't strictly need the username. It's just a nice-to-have.
|
|
||||||
// So make up a *user.User if their machine is broken in this way.
|
|
||||||
return &user.User{
|
|
||||||
Uid: uid,
|
|
||||||
Username: "unknown-user-" + uid,
|
|
||||||
Name: "unknown user " + uid,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return u, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// blockWhileInUse blocks while until either a Read from conn fails
|
// blockWhileInUse blocks while until either a Read from conn fails
|
||||||
// (i.e. it's closed) or until the server is able to accept ci as a
|
// (i.e. it's closed) or until the server is able to accept ci as a
|
||||||
// user.
|
// user.
|
||||||
func (s *Server) blockWhileInUse(conn io.Reader, ci connIdentity) {
|
func (s *Server) blockWhileInUse(conn io.Reader, ci *ipnauth.ConnIdentity) {
|
||||||
s.logf("blocking client while server in use; connIdentity=%v", ci)
|
s.logf("blocking client while server in use; connIdentity=%v", ci)
|
||||||
connDone := make(chan struct{})
|
connDone := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
@ -296,7 +201,7 @@ func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tell the LocalBackend about the identity we're now running as.
|
// Tell the LocalBackend about the identity we're now running as.
|
||||||
s.b.SetCurrentUserID(ci.UserID)
|
s.b.SetCurrentUserID(ci.UserID())
|
||||||
|
|
||||||
if isHTTPReq {
|
if isHTTPReq {
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
@ -318,7 +223,7 @@ func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
|||||||
defer s.removeAndCloseConn(c)
|
defer s.removeAndCloseConn(c)
|
||||||
logf("[v1] incoming control connection")
|
logf("[v1] incoming control connection")
|
||||||
|
|
||||||
if isReadonlyConn(ci, s.b.OperatorUserID(), logf) {
|
if ci.IsReadonlyConn(s.b.OperatorUserID(), logf) {
|
||||||
ctx = ipn.ReadonlyContextOf(ctx)
|
ctx = ipn.ReadonlyContextOf(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,67 +249,6 @@ func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isReadonlyConn(ci connIdentity, operatorUID string, logf logger.Logf) bool {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// Windows doesn't need/use this mechanism, at least yet. It
|
|
||||||
// has a different last-user-wins auth model.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const ro = true
|
|
||||||
const rw = false
|
|
||||||
if !safesocket.PlatformUsesPeerCreds() {
|
|
||||||
return rw
|
|
||||||
}
|
|
||||||
creds := ci.Creds
|
|
||||||
if creds == nil {
|
|
||||||
logf("connection from unknown peer; read-only")
|
|
||||||
return ro
|
|
||||||
}
|
|
||||||
uid, ok := creds.UserID()
|
|
||||||
if !ok {
|
|
||||||
logf("connection from peer with unknown userid; read-only")
|
|
||||||
return ro
|
|
||||||
}
|
|
||||||
if uid == "0" {
|
|
||||||
logf("connection from userid %v; root has access", uid)
|
|
||||||
return rw
|
|
||||||
}
|
|
||||||
if selfUID := os.Getuid(); selfUID != 0 && uid == strconv.Itoa(selfUID) {
|
|
||||||
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
|
|
||||||
return rw
|
|
||||||
}
|
|
||||||
if operatorUID != "" && uid == operatorUID {
|
|
||||||
logf("connection from userid %v; is configured operator", uid)
|
|
||||||
return rw
|
|
||||||
}
|
|
||||||
if yes, err := isLocalAdmin(uid); err != nil {
|
|
||||||
logf("connection from userid %v; read-only; %v", uid, err)
|
|
||||||
return ro
|
|
||||||
} else if yes {
|
|
||||||
logf("connection from userid %v; is local admin, has access", uid)
|
|
||||||
return rw
|
|
||||||
}
|
|
||||||
logf("connection from userid %v; read-only", uid)
|
|
||||||
return ro
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLocalAdmin(uid string) (bool, error) {
|
|
||||||
u, err := user.LookupId(uid)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
var adminGroup string
|
|
||||||
switch {
|
|
||||||
case runtime.GOOS == "darwin":
|
|
||||||
adminGroup = "admin"
|
|
||||||
case distro.Get() == distro.QNAP:
|
|
||||||
adminGroup = "administrators"
|
|
||||||
default:
|
|
||||||
return false, fmt.Errorf("no system admin group found")
|
|
||||||
}
|
|
||||||
return groupmember.IsMemberOfGroup(adminGroup, u.Username)
|
|
||||||
}
|
|
||||||
|
|
||||||
// inUseOtherUserError is the error type for when the server is in use
|
// inUseOtherUserError is the error type for when the server is in use
|
||||||
// by a different local user.
|
// by a different local user.
|
||||||
type inUseOtherUserError struct{ error }
|
type inUseOtherUserError struct{ error }
|
||||||
@ -417,19 +261,19 @@ func (e inUseOtherUserError) Unwrap() error { return e.error }
|
|||||||
// The returned error, when non-nil, will be of type inUseOtherUserError.
|
// The returned error, when non-nil, will be of type inUseOtherUserError.
|
||||||
//
|
//
|
||||||
// s.mu must be held.
|
// s.mu must be held.
|
||||||
func (s *Server) checkConnIdentityLocked(ci connIdentity) error {
|
func (s *Server) checkConnIdentityLocked(ci *ipnauth.ConnIdentity) error {
|
||||||
// If clients are already connected, verify they're the same user.
|
// If clients are already connected, verify they're the same user.
|
||||||
// This mostly matters on Windows at the moment.
|
// This mostly matters on Windows at the moment.
|
||||||
if len(s.allClients) > 0 {
|
if len(s.allClients) > 0 {
|
||||||
var active connIdentity
|
var active *ipnauth.ConnIdentity
|
||||||
for _, active = range s.allClients {
|
for _, active = range s.allClients {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if ci.UserID != active.UserID {
|
if active != nil && ci.UserID() != active.UserID() {
|
||||||
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s, pid %d", active.User.Username, active.Pid)}
|
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s, pid %d", active.User().Username, active.Pid())}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if su := s.serverModeUser; su != nil && ci.UserID != su.Uid {
|
if su := s.serverModeUser; su != nil && ci.UserID() != su.Uid {
|
||||||
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s", su.Username)}
|
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s", su.Username)}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -439,7 +283,7 @@ func (s *Server) checkConnIdentityLocked(ci connIdentity) error {
|
|||||||
// the Tailscale local daemon API.
|
// the Tailscale local daemon API.
|
||||||
//
|
//
|
||||||
// s.mu must not be held.
|
// s.mu must not be held.
|
||||||
func (s *Server) localAPIPermissions(ci connIdentity) (read, write bool) {
|
func (s *Server) localAPIPermissions(ci *ipnauth.ConnIdentity) (read, write bool) {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "windows":
|
case "windows":
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@ -451,8 +295,8 @@ func (s *Server) localAPIPermissions(ci connIdentity) (read, write bool) {
|
|||||||
case "js":
|
case "js":
|
||||||
return true, true
|
return true, true
|
||||||
}
|
}
|
||||||
if ci.IsUnixSock {
|
if ci.IsUnixSock() {
|
||||||
return true, !isReadonlyConn(ci, s.b.OperatorUserID(), logger.Discard)
|
return true, !ci.IsReadonlyConn(s.b.OperatorUserID(), logger.Discard)
|
||||||
}
|
}
|
||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
@ -490,9 +334,9 @@ func isAllDigit(s string) bool {
|
|||||||
// TS_PERMIT_CERT_UID is set the to the userid of the peer
|
// TS_PERMIT_CERT_UID is set the to the userid of the peer
|
||||||
// connection. It's intended to give your non-root webserver access
|
// connection. It's intended to give your non-root webserver access
|
||||||
// (www-data, caddy, nginx, etc) to certs.
|
// (www-data, caddy, nginx, etc) to certs.
|
||||||
func (s *Server) connCanFetchCerts(ci connIdentity) bool {
|
func (s *Server) connCanFetchCerts(ci *ipnauth.ConnIdentity) bool {
|
||||||
if ci.IsUnixSock && ci.Creds != nil {
|
if ci.IsUnixSock() && ci.Creds() != nil {
|
||||||
connUID, ok := ci.Creds.UserID()
|
connUID, ok := ci.Creds().UserID()
|
||||||
if ok && connUID == userIDFromString(envknob.String("TS_PERMIT_CERT_UID")) {
|
if ok && connUID == userIDFromString(envknob.String("TS_PERMIT_CERT_UID")) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -520,8 +364,8 @@ func (s *Server) registerDisconnectSub(ch chan<- struct{}, add bool) {
|
|||||||
//
|
//
|
||||||
// If the returned error is of type inUseOtherUserError then the
|
// If the returned error is of type inUseOtherUserError then the
|
||||||
// returned connIdentity is also valid.
|
// returned connIdentity is also valid.
|
||||||
func (s *Server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
func (s *Server) addConn(c net.Conn, isHTTP bool) (ci *ipnauth.ConnIdentity, err error) {
|
||||||
ci, err = s.getConnIdentity(c)
|
ci, err = ipnauth.GetConnIdentity(s.logf, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -543,7 +387,7 @@ func (s *Server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
|||||||
s.clients = map[net.Conn]bool{}
|
s.clients = map[net.Conn]bool{}
|
||||||
}
|
}
|
||||||
if s.allClients == nil {
|
if s.allClients == nil {
|
||||||
s.allClients = map[net.Conn]connIdentity{}
|
s.allClients = map[net.Conn]*ipnauth.ConnIdentity{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.checkConnIdentityLocked(ci); err != nil {
|
if err := s.checkConnIdentityLocked(ci); err != nil {
|
||||||
@ -555,11 +399,11 @@ func (s *Server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
|||||||
}
|
}
|
||||||
s.allClients[c] = ci
|
s.allClients[c] = ci
|
||||||
|
|
||||||
if s.lastUserID != ci.UserID {
|
if s.lastUserID != ci.UserID() {
|
||||||
if s.lastUserID != "" {
|
if s.lastUserID != "" {
|
||||||
doReset = true
|
doReset = true
|
||||||
}
|
}
|
||||||
s.lastUserID = ci.UserID
|
s.lastUserID = ci.UserID()
|
||||||
}
|
}
|
||||||
return ci, nil
|
return ci, nil
|
||||||
}
|
}
|
||||||
@ -602,7 +446,7 @@ func (s *Server) stopAll() {
|
|||||||
//
|
//
|
||||||
// s.mu must be held
|
// s.mu must be held
|
||||||
func (s *Server) setServerModeUserLocked() {
|
func (s *Server) setServerModeUserLocked() {
|
||||||
var ci connIdentity
|
var ci *ipnauth.ConnIdentity
|
||||||
var ok bool
|
var ok bool
|
||||||
for _, ci = range s.allClients {
|
for _, ci = range s.allClients {
|
||||||
ok = true
|
ok = true
|
||||||
@ -612,12 +456,12 @@ func (s *Server) setServerModeUserLocked() {
|
|||||||
s.logf("ipnserver: [unexpected] now in server mode, but no connected client")
|
s.logf("ipnserver: [unexpected] now in server mode, but no connected client")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ci.NotWindows {
|
if ci.NotWindows() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ci.User != nil {
|
if ci.User() != nil {
|
||||||
s.logf("ipnserver: now in server mode; user=%v", ci.User.Username)
|
s.logf("ipnserver: now in server mode; user=%v", ci.User().Username)
|
||||||
s.serverModeUser = ci.User
|
s.serverModeUser = ci.User()
|
||||||
} else {
|
} else {
|
||||||
s.logf("ipnserver: [unexpected] now in server mode, but nil User")
|
s.logf("ipnserver: [unexpected] now in server mode, but nil User")
|
||||||
}
|
}
|
||||||
@ -772,7 +616,7 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
|
|||||||
|
|
||||||
var serverModeUser *user.User
|
var serverModeUser *user.User
|
||||||
if uid := b.CurrentUser(); uid != "" {
|
if uid := b.CurrentUser(); uid != "" {
|
||||||
u, err := lookupUserFromID(logf, uid)
|
u, err := ipnauth.LookupUserFromID(logf, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logf("ipnserver: found server mode auto-start key; failed to load user: %v", err)
|
logf("ipnserver: found server mode auto-start key; failed to load user: %v", err)
|
||||||
} else {
|
} else {
|
||||||
@ -1007,7 +851,7 @@ func (psc *protoSwitchConn) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) localhostHandler(ci connIdentity) http.Handler {
|
func (s *Server) localhostHandler(ci *ipnauth.ConnIdentity) http.Handler {
|
||||||
lah := localapi.NewHandler(s.b, s.logf, s.backendLogID)
|
lah := localapi.NewHandler(s.b, s.logf, s.backendLogID)
|
||||||
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
|
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
|
||||||
lah.PermitCert = s.connCanFetchCerts(ci)
|
lah.PermitCert = s.connCanFetchCerts(ci)
|
||||||
@ -1017,7 +861,7 @@ func (s *Server) localhostHandler(ci connIdentity) http.Handler {
|
|||||||
lah.ServeHTTP(w, r)
|
lah.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ci.NotWindows {
|
if ci.NotWindows() {
|
||||||
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
|
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1045,15 +889,6 @@ func (s *Server) ServeHTMLStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
st.WriteHTML(w)
|
st.WriteHTML(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func peerPid(entries []netstat.Entry, la, ra netip.AddrPort) int {
|
|
||||||
for _, e := range entries {
|
|
||||||
if e.Local == ra && e.Remote == la {
|
|
||||||
return e.Pid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// jsonNotifier returns a notify-writer func that writes ipn.Notify
|
// jsonNotifier returns a notify-writer func that writes ipn.Notify
|
||||||
// messages to w.
|
// messages to w.
|
||||||
func jsonNotifier(w io.Writer, logf logger.Logf) func(ipn.Notify) {
|
func jsonNotifier(w io.Writer, logf logger.Logf) func(ipn.Notify) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user