Compare commits
6 Commits
deb6c3c6ab
...
99013d01d4
Author | SHA1 | Date |
---|---|---|
Nick Khyl | 99013d01d4 | |
Brad Fitzpatrick | 4dece0c359 | |
Brad Fitzpatrick | 7f587d0321 | |
Jonathan Nobels | 71e9258ad9 | |
Brad Fitzpatrick | 745931415c | |
Nick Khyl | 551d6ae0f3 |
|
@ -400,7 +400,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||
💣 tailscale.com/util/winutil from tailscale.com/clientupdate+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate+
|
||||
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
|
||||
W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo
|
||||
W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+
|
||||
tailscale.com/util/zstdframe from tailscale.com/control/controlclient+
|
||||
tailscale.com/version from tailscale.com/client/web+
|
||||
tailscale.com/version/distro from tailscale.com/client/web+
|
||||
|
|
|
@ -358,7 +358,7 @@ func run() (err error) {
|
|||
sys.Set(netMon)
|
||||
}
|
||||
|
||||
pol := logpolicy.New(logtail.CollectionNode, netMon, nil /* use log.Printf */)
|
||||
pol := logpolicy.New(logtail.CollectionNode, netMon, sys.HealthTracker(), nil /* use log.Printf */)
|
||||
pol.SetVerbosityLevel(args.verbose)
|
||||
logPol = pol
|
||||
defer func() {
|
||||
|
@ -677,7 +677,7 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
|
|||
// configuration being unavailable (from the noop
|
||||
// manager). More in Issue 4017.
|
||||
// TODO(bradfitz): add a Synology-specific DNS manager.
|
||||
conf.DNS, err = dns.NewOSConfigurator(logf, "") // empty interface name
|
||||
conf.DNS, err = dns.NewOSConfigurator(logf, sys.HealthTracker(), "") // empty interface name
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||
}
|
||||
|
@ -699,13 +699,13 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
|
|||
return false, err
|
||||
}
|
||||
|
||||
r, err := router.New(logf, dev, sys.NetMon.Get())
|
||||
r, err := router.New(logf, dev, sys.NetMon.Get(), sys.HealthTracker())
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return false, fmt.Errorf("creating router: %w", err)
|
||||
}
|
||||
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
d, err := dns.NewOSConfigurator(logf, sys.HealthTracker(), devName)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
r.Close()
|
||||
|
|
|
@ -104,9 +104,10 @@ func newIPN(jsConfig js.Value) map[string]any {
|
|||
sys.Set(store)
|
||||
dialer := &tsdial.Dialer{Logf: logf}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Dialer: dialer,
|
||||
SetSubsystem: sys.Set,
|
||||
ControlKnobs: sys.ControlKnobs(),
|
||||
Dialer: dialer,
|
||||
SetSubsystem: sys.Set,
|
||||
ControlKnobs: sys.ControlKnobs(),
|
||||
HealthTracker: sys.HealthTracker(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
|
@ -1453,14 +1453,15 @@ func (c *Direct) getNoiseClient() (*NoiseClient, error) {
|
|||
}
|
||||
c.logf("[v1] creating new noise client")
|
||||
nc, err := NewNoiseClient(NoiseOpts{
|
||||
PrivKey: k,
|
||||
ServerPubKey: serverNoiseKey,
|
||||
ServerURL: c.serverURL,
|
||||
Dialer: c.dialer,
|
||||
DNSCache: c.dnsCache,
|
||||
Logf: c.logf,
|
||||
NetMon: c.netMon,
|
||||
DialPlan: dp,
|
||||
PrivKey: k,
|
||||
ServerPubKey: serverNoiseKey,
|
||||
ServerURL: c.serverURL,
|
||||
Dialer: c.dialer,
|
||||
DNSCache: c.dnsCache,
|
||||
Logf: c.logf,
|
||||
NetMon: c.netMon,
|
||||
HealthTracker: c.health,
|
||||
DialPlan: dp,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"golang.org/x/net/http2"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
|
@ -174,6 +175,7 @@ type NoiseClient struct {
|
|||
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor
|
||||
health *health.Tracker
|
||||
|
||||
// mu only protects the following variables.
|
||||
mu sync.Mutex
|
||||
|
@ -204,6 +206,8 @@ type NoiseOpts struct {
|
|||
// network interface state. This field can be nil; if so, the current
|
||||
// state will be looked up dynamically.
|
||||
NetMon *netmon.Monitor
|
||||
// HealthTracker, if non-nil, is the health tracker to use.
|
||||
HealthTracker *health.Tracker
|
||||
// DialPlan, if set, is a function that should return an explicit plan
|
||||
// on how to connect to the server.
|
||||
DialPlan func() *tailcfg.ControlDialPlan
|
||||
|
@ -247,6 +251,7 @@ func NewNoiseClient(opts NoiseOpts) (*NoiseClient, error) {
|
|||
dialPlan: opts.DialPlan,
|
||||
logf: opts.Logf,
|
||||
netMon: opts.NetMon,
|
||||
health: opts.HealthTracker,
|
||||
}
|
||||
|
||||
// Create the HTTP/2 Transport using a net/http.Transport
|
||||
|
@ -453,6 +458,7 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
|
|||
DialPlan: dialPlan,
|
||||
Logf: nc.logf,
|
||||
NetMon: nc.netMon,
|
||||
HealthTracker: nc.health,
|
||||
Clock: tstime.StdClock{},
|
||||
}).Dial(ctx)
|
||||
if err != nil {
|
||||
|
|
|
@ -38,7 +38,6 @@ import (
|
|||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netutil"
|
||||
|
@ -434,7 +433,7 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
|
|||
// Disable HTTP2, since h2 can't do protocol switching.
|
||||
tr.TLSClientConfig.NextProtos = []string{}
|
||||
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
|
||||
tr.TLSClientConfig = tlsdial.Config(a.Hostname, health.Global, tr.TLSClientConfig)
|
||||
tr.TLSClientConfig = tlsdial.Config(a.Hostname, a.HealthTracker, tr.TLSClientConfig)
|
||||
if !tr.TLSClientConfig.InsecureSkipVerify {
|
||||
panic("unexpected") // should be set by tlsdial.Config
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/url"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/tailcfg"
|
||||
|
@ -79,6 +80,9 @@ type Dialer struct {
|
|||
|
||||
NetMon *netmon.Monitor
|
||||
|
||||
// HealthTracker, if non-nil, is the health tracker to use.
|
||||
HealthTracker *health.Tracker
|
||||
|
||||
// DialPlan, if set, contains instructions from the control server on
|
||||
// how to connect to it. If present, we will try the methods in this
|
||||
// plan before falling back to DNS.
|
||||
|
|
109
health/health.go
109
health/health.go
|
@ -30,17 +30,39 @@ var (
|
|||
debugHandler map[string]http.Handler
|
||||
)
|
||||
|
||||
// Global is a global health tracker for the process.
|
||||
//
|
||||
// TODO(bradfitz): finish moving all reference to this plumb it (ultimately out
|
||||
// from tsd.System) so a process can have multiple tsnet/etc instances with
|
||||
// their own health trackers. But for now (2024-04-25), the tsd.System value
|
||||
// given out is just this one, until that's the only remaining Global reference
|
||||
// remaining.
|
||||
var Global = new(Tracker)
|
||||
// ReceiveFunc is one of the three magicsock Receive funcs (IPv4, IPv6, or
|
||||
// DERP).
|
||||
type ReceiveFunc int
|
||||
|
||||
// ReceiveFunc indices for Tracker.MagicSockReceiveFuncs.
|
||||
const (
|
||||
ReceiveIPv4 ReceiveFunc = 0
|
||||
ReceiveIPv6 ReceiveFunc = 1
|
||||
ReceiveDERP ReceiveFunc = 2
|
||||
)
|
||||
|
||||
func (f ReceiveFunc) String() string {
|
||||
if f < 0 || int(f) >= len(receiveNames) {
|
||||
return fmt.Sprintf("ReceiveFunc(%d)", f)
|
||||
}
|
||||
return receiveNames[f]
|
||||
}
|
||||
|
||||
var receiveNames = []string{
|
||||
ReceiveIPv4: "ReceiveIPv4",
|
||||
ReceiveIPv6: "ReceiveIPv6",
|
||||
ReceiveDERP: "ReceiveDERP",
|
||||
}
|
||||
|
||||
// Tracker tracks the health of various Tailscale subsystems,
|
||||
// comparing each subsystems' state with each other to make sure
|
||||
// they're consistent based on the user's intended state.
|
||||
type Tracker struct {
|
||||
// mu guards everything in this var block.
|
||||
// MagicSockReceiveFuncs tracks the state of the three
|
||||
// magicsock receive functions: IPv4, IPv6, and DERP.
|
||||
MagicSockReceiveFuncs [3]ReceiveFuncStats // indexed by ReceiveFunc values
|
||||
|
||||
// mu guards everything that follows.
|
||||
mu sync.Mutex
|
||||
|
||||
warnables []*Warnable // keys ever set
|
||||
|
@ -530,7 +552,7 @@ func (t *Tracker) timerSelfCheck() {
|
|||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
checkReceiveFuncs()
|
||||
t.checkReceiveFuncsLocked()
|
||||
t.selfCheckLocked()
|
||||
if t.timer != nil {
|
||||
t.timer.Reset(time.Minute)
|
||||
|
@ -632,9 +654,10 @@ func (t *Tracker) overallErrorLocked() error {
|
|||
_ = t.lastMapRequestHeard
|
||||
|
||||
var errs []error
|
||||
for _, recv := range receiveFuncs {
|
||||
if recv.missing {
|
||||
errs = append(errs, fmt.Errorf("%s is not running", recv.name))
|
||||
for i := range t.MagicSockReceiveFuncs {
|
||||
f := &t.MagicSockReceiveFuncs[i]
|
||||
if f.missing {
|
||||
errs = append(errs, fmt.Errorf("%s is not running", f.name))
|
||||
}
|
||||
}
|
||||
for sys, err := range t.sysErr {
|
||||
|
@ -670,63 +693,69 @@ func (t *Tracker) overallErrorLocked() error {
|
|||
return multierr.New(errs...)
|
||||
}
|
||||
|
||||
var (
|
||||
ReceiveIPv4 = ReceiveFuncStats{name: "ReceiveIPv4"}
|
||||
ReceiveIPv6 = ReceiveFuncStats{name: "ReceiveIPv6"}
|
||||
ReceiveDERP = ReceiveFuncStats{name: "ReceiveDERP"}
|
||||
|
||||
receiveFuncs = []*ReceiveFuncStats{&ReceiveIPv4, &ReceiveIPv6, &ReceiveDERP}
|
||||
)
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS == "js" {
|
||||
receiveFuncs = receiveFuncs[2:] // ignore IPv4 and IPv6
|
||||
}
|
||||
}
|
||||
|
||||
// ReceiveFuncStats tracks the calls made to a wireguard-go receive func.
|
||||
type ReceiveFuncStats struct {
|
||||
// name is the name of the receive func.
|
||||
// It's lazily populated.
|
||||
name string
|
||||
// numCalls is the number of times the receive func has ever been called.
|
||||
// It is required because it is possible for a receive func's wireguard-go goroutine
|
||||
// to be active even though the receive func isn't.
|
||||
// The wireguard-go goroutine alternates between calling the receive func and
|
||||
// processing what the func returned.
|
||||
numCalls uint64 // accessed atomically
|
||||
numCalls atomic.Uint64
|
||||
// prevNumCalls is the value of numCalls last time the health check examined it.
|
||||
prevNumCalls uint64
|
||||
// inCall indicates whether the receive func is currently running.
|
||||
inCall uint32 // bool, accessed atomically
|
||||
inCall atomic.Bool
|
||||
// missing indicates whether the receive func is not running.
|
||||
missing bool
|
||||
}
|
||||
|
||||
func (s *ReceiveFuncStats) Enter() {
|
||||
atomic.AddUint64(&s.numCalls, 1)
|
||||
atomic.StoreUint32(&s.inCall, 1)
|
||||
s.numCalls.Add(1)
|
||||
s.inCall.Store(true)
|
||||
}
|
||||
|
||||
func (s *ReceiveFuncStats) Exit() {
|
||||
atomic.StoreUint32(&s.inCall, 0)
|
||||
s.inCall.Store(false)
|
||||
}
|
||||
|
||||
func checkReceiveFuncs() {
|
||||
for _, recv := range receiveFuncs {
|
||||
recv.missing = false
|
||||
prev := recv.prevNumCalls
|
||||
numCalls := atomic.LoadUint64(&recv.numCalls)
|
||||
recv.prevNumCalls = numCalls
|
||||
// ReceiveFuncStats returns the ReceiveFuncStats tracker for the given func
|
||||
// type.
|
||||
//
|
||||
// If t is nil, it returns nil.
|
||||
func (t *Tracker) ReceiveFuncStats(which ReceiveFunc) *ReceiveFuncStats {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return &t.MagicSockReceiveFuncs[which]
|
||||
}
|
||||
|
||||
func (t *Tracker) checkReceiveFuncsLocked() {
|
||||
for i := range t.MagicSockReceiveFuncs {
|
||||
f := &t.MagicSockReceiveFuncs[i]
|
||||
if f.name == "" {
|
||||
f.name = (ReceiveFunc(i)).String()
|
||||
}
|
||||
if runtime.GOOS == "js" && i < 2 {
|
||||
// Skip IPv4 and IPv6 on js.
|
||||
continue
|
||||
}
|
||||
f.missing = false
|
||||
prev := f.prevNumCalls
|
||||
numCalls := f.numCalls.Load()
|
||||
f.prevNumCalls = numCalls
|
||||
if numCalls > prev {
|
||||
// OK: the function has gotten called since last we checked
|
||||
continue
|
||||
}
|
||||
if atomic.LoadUint32(&recv.inCall) == 1 {
|
||||
if f.inCall.Load() {
|
||||
// OK: the function is active, probably blocked due to inactivity
|
||||
continue
|
||||
}
|
||||
// Not OK: The function is not active, and not accumulating new calls.
|
||||
// It is probably MIA.
|
||||
recv.missing = true
|
||||
f.missing = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer -type=Prefs,ServeConfig,TCPPortHandler,HTTPHandler,WebServerConfig
|
||||
//go:generate go run tailscale.com/cmd/viewer -type=LoginProfile,Prefs,ServeConfig,TCPPortHandler,HTTPHandler,WebServerConfig
|
||||
|
||||
// Package ipn implements the interactions between the Tailscale cloud
|
||||
// control plane and the local network stack.
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// AccessDeniedError is an error due to permissions.
|
||||
type AccessDeniedError struct {
|
||||
// Err is the underlying error.
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error returns error message.
|
||||
func (e *AccessDeniedError) Error() string { return e.Err.Error() }
|
||||
|
||||
// Unwrap returns an underlying error.
|
||||
func (e *AccessDeniedError) Unwrap() error { return e.Err }
|
||||
|
||||
// ToHTTPStatus returns http.StatusForbidden.
|
||||
func (e *AccessDeniedError) ToHTTPStatus() int { return http.StatusForbidden }
|
||||
|
||||
// NotFoundError is an error due to a missing resource.
|
||||
type NotFoundError struct {
|
||||
// Err is the underlying error.
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error returns error message.
|
||||
func (e *NotFoundError) Error() string { return e.Err.Error() }
|
||||
|
||||
// Unwrap returns an underlying error.
|
||||
func (e *NotFoundError) Unwrap() error { return e.Err }
|
||||
|
||||
// ToHTTPStatus returns http.StatusNotFound.
|
||||
func (e *NotFoundError) ToHTTPStatus() int { return http.StatusNotFound }
|
||||
|
||||
// BadArgsError is an error due to bad arguments.
|
||||
type BadArgsError struct {
|
||||
// Err is the underlying error.
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error returns error message.
|
||||
func (e *BadArgsError) Error() string { return e.Err.Error() }
|
||||
|
||||
// Unwrap returns an underlying error.
|
||||
func (e *BadArgsError) Unwrap() error { return e.Err }
|
||||
|
||||
// ToHTTPStatus returns http.StatusBadRequest.
|
||||
func (e *BadArgsError) ToHTTPStatus() int { return http.StatusBadRequest }
|
||||
|
||||
// ServiceUnavailableError is an error that can be represented by http.StatusServiceUnavailable.
|
||||
type ServiceUnavailableError struct {
|
||||
Err error // Err is the underlying error.
|
||||
}
|
||||
|
||||
// Error returns error message.
|
||||
func (e *ServiceUnavailableError) Error() string { return e.Err.Error() }
|
||||
|
||||
// Unwrap returns an underlying error.
|
||||
func (e *ServiceUnavailableError) Unwrap() error { return e.Err }
|
||||
|
||||
// ToHTTPStatus returns http.StatusServiceUnavailable.
|
||||
func (e *ServiceUnavailableError) ToHTTPStatus() int { return http.StatusServiceUnavailable }
|
||||
|
||||
// InternalServerError is an error that can be represented by http.StatusInternalServerError.
|
||||
type InternalServerError struct {
|
||||
Err error // Err is the underlying error.
|
||||
}
|
||||
|
||||
// Error returns error message.
|
||||
func (e *InternalServerError) Error() string { return e.Err.Error() }
|
||||
|
||||
// Unwrap returns an underlying error.
|
||||
func (e *InternalServerError) Unwrap() error { return e.Err }
|
||||
|
||||
// ToHTTPStatus returns http.StatusInternalServerError.
|
||||
func (e *InternalServerError) ToHTTPStatus() int { return http.StatusInternalServerError }
|
||||
|
||||
// NewAccessDeniedError returns a new AccessDeniedError with the specified text.
|
||||
func NewAccessDeniedError(text string) *AccessDeniedError {
|
||||
return &AccessDeniedError{errors.New(text)}
|
||||
}
|
||||
|
||||
// NewNotFoundError returns a new NotFoundError with the specified text.
|
||||
func NewNotFoundError(text string) *NotFoundError {
|
||||
return &NotFoundError{errors.New(text)}
|
||||
}
|
||||
|
||||
// NewBadArgsError returns a new BadArgsError with the specified text.
|
||||
func NewBadArgsError(text string) *BadArgsError {
|
||||
return &BadArgsError{errors.New(text)}
|
||||
}
|
||||
|
||||
// NewServiceUnavailableError returns a new ServiceUnavailableError with the specified text.
|
||||
func NewServiceUnavailableError(text string) *ServiceUnavailableError {
|
||||
return &ServiceUnavailableError{errors.New(text)}
|
||||
}
|
||||
|
||||
// NewInternalServerError returns a new InternalServerError with the specified text.
|
||||
func NewInternalServerError(text string) *InternalServerError {
|
||||
return &InternalServerError{errors.New(text)}
|
||||
}
|
|
@ -15,6 +15,29 @@ import (
|
|||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of LoginProfile.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *LoginProfile) Clone() *LoginProfile {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(LoginProfile)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _LoginProfileCloneNeedsRegeneration = LoginProfile(struct {
|
||||
ID ProfileID
|
||||
Name string
|
||||
NetworkProfile NetworkProfile
|
||||
Key StateKey
|
||||
UserProfile tailcfg.UserProfile
|
||||
NodeID tailcfg.StableNodeID
|
||||
LocalUserID WindowsUserID
|
||||
ControlURL string
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of Prefs.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Prefs) Clone() *Prefs {
|
||||
|
|
|
@ -17,7 +17,73 @@ import (
|
|||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Prefs,ServeConfig,TCPPortHandler,HTTPHandler,WebServerConfig
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=LoginProfile,Prefs,ServeConfig,TCPPortHandler,HTTPHandler,WebServerConfig
|
||||
|
||||
// View returns a readonly view of LoginProfile.
|
||||
func (p *LoginProfile) View() LoginProfileView {
|
||||
return LoginProfileView{ж: p}
|
||||
}
|
||||
|
||||
// LoginProfileView provides a read-only view over LoginProfile.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type LoginProfileView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *LoginProfile
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v LoginProfileView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v LoginProfileView) AsStruct() *LoginProfile {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v LoginProfileView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *LoginProfileView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x LoginProfile
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v LoginProfileView) ID() ProfileID { return v.ж.ID }
|
||||
func (v LoginProfileView) Name() string { return v.ж.Name }
|
||||
func (v LoginProfileView) NetworkProfile() NetworkProfile { return v.ж.NetworkProfile }
|
||||
func (v LoginProfileView) Key() StateKey { return v.ж.Key }
|
||||
func (v LoginProfileView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
|
||||
func (v LoginProfileView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
|
||||
func (v LoginProfileView) LocalUserID() WindowsUserID { return v.ж.LocalUserID }
|
||||
func (v LoginProfileView) ControlURL() string { return v.ж.ControlURL }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _LoginProfileViewNeedsRegeneration = LoginProfile(struct {
|
||||
ID ProfileID
|
||||
Name string
|
||||
NetworkProfile NetworkProfile
|
||||
Key StateKey
|
||||
UserProfile tailcfg.UserProfile
|
||||
NodeID tailcfg.StableNodeID
|
||||
LocalUserID WindowsUserID
|
||||
ControlURL string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of Prefs.
|
||||
func (p *Prefs) View() PrefsView {
|
||||
|
|
|
@ -0,0 +1,299 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"math/bits"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DeviceAccess is a bitmask representing the requested, required, or granted
|
||||
// access rights to a device.
|
||||
type DeviceAccess uint32
|
||||
|
||||
// ProfileAccess is a bitmask representing the requested, required, or granted
|
||||
// access rights to a Tailscale login profile.
|
||||
type ProfileAccess uint32
|
||||
|
||||
// Define access rights for general device management tasks and operations that affect all profiles.
|
||||
// They are allowed or denied based on the environment and user's role on the device,
|
||||
// rather than the currently active Tailscale profile.
|
||||
const (
|
||||
// ReadDeviceStatus is the access right required to read non-profile specific device statuses,
|
||||
// such as the IP forwarding status. It is a non-privileged access right generally available to all users.
|
||||
// It must not grant access to any sensitive or private information,
|
||||
// including Tailscale profile names, network devices, etc.
|
||||
ReadDeviceStatus DeviceAccess = 1 << iota
|
||||
// GenerateBugReport is the access right required to generate a bug report
|
||||
// (e.g. `tailscale bugreport` in CLI or Debug > Bug Report in GUI).
|
||||
// It is a non-privileged access right granted to all users.
|
||||
GenerateBugReport
|
||||
// CreateProfile is the access right required to create new Tailscale profiles on the device.
|
||||
// This operation is privileged on Unix-like platforms, including Linux,
|
||||
// but is available to all users on non-Server Windows devices.
|
||||
CreateProfile
|
||||
// Debug is required for debugging operations that could expose sensitive information.
|
||||
// Many such operations are accessible via `tailscale debug` subcommands.
|
||||
// It is considered privileged access on all platforms, requiring root access on Unix-like systems
|
||||
// and elevated admin access on Windows.
|
||||
Debug
|
||||
// InstallUpdates is required to initiate a Tailscale client self-update on platforms that support it.
|
||||
// It is available to all users on all platforms except for Windows Server,
|
||||
// where it requires admin rights.
|
||||
InstallUpdates
|
||||
// DeleteAllProfiles is required to log out from and delete all Tailscale profiles on a device.
|
||||
// It is considered a privileged operation, requiring root access on Unix-like systems
|
||||
// and elevated admin access on Windows.
|
||||
DeleteAllProfiles
|
||||
|
||||
// UnrestrictedDeviceAccess combines all possible device access rights.
|
||||
UnrestrictedDeviceAccess = ^DeviceAccess(0)
|
||||
)
|
||||
|
||||
var deviceAccessNames = map[DeviceAccess]string{
|
||||
CreateProfile: "CreateProfile",
|
||||
Debug: "Debug",
|
||||
DeleteAllProfiles: "DeleteAllProfiles",
|
||||
GenerateBugReport: "GenerateBugReport",
|
||||
InstallUpdates: "InstallUpdates",
|
||||
ReadDeviceStatus: "ReadDeviceStatus",
|
||||
}
|
||||
|
||||
// Define access rights that are specific to individual profiles,
|
||||
// granted or denied on a per-profile basis.
|
||||
const (
|
||||
// ReadProfileInfo is required to view a profile in the list of available profiles and
|
||||
// to read basic profile info like the user name and tailnet name.
|
||||
// It also allows to read profile/connection-specific status details, excluding information about peers,
|
||||
// but must not grant access to any sensitive information, such as private keys.
|
||||
//
|
||||
// This access right is granted to all users on Unix-like platforms.
|
||||
//
|
||||
// On Windows, any user should have access to their own profiles as well as profiles shared with them.
|
||||
// NOTE: As of 2024-04-08, the following are the only two ways to share a profile:
|
||||
// - Create a profile in local system's security context (e.g. via a GP/MDM/SCCM-deployed script);
|
||||
// - Enable Unattended Mode (ipn.Prefs.ForceDaemon) for the profile.
|
||||
// We'll reconsider this in tailscale/corp#18342 or subsequent tickets.
|
||||
// Additionally, Windows admins should be able to list all profiles when running elevated.
|
||||
//
|
||||
// If a user does not have ReadProfileInfo access to the current profile, its details will be masked.
|
||||
ReadProfileInfo ProfileAccess = 1 << iota
|
||||
// Connect is required to connect to and use a Tailscale profile.
|
||||
// It is considered a privileged operation on Unix-like platforms and Windows Server.
|
||||
// On Windows client devices, however, users have the Connect access right
|
||||
// to the profiles they can read.
|
||||
Connect
|
||||
// Disconnect is required to disconnect (or switch from) a Tailscale profile.
|
||||
// It is considered a privileged operation on Unix-like platforms and Windows Server.
|
||||
// On Windows Client and other platforms any user should be able to disconnect
|
||||
// from an active Tailnet.
|
||||
Disconnect
|
||||
// DeleteProfile is required to delete a local Tailscale profile.
|
||||
// Root (or operator) access is required on Unix-like platforms.
|
||||
// On Windows, profiles can be deleted by their owners. Additionally,
|
||||
// on Windows Server and managed Windows Client devices, elevated admins have the right
|
||||
// to delete any profile.
|
||||
DeleteProfile
|
||||
// ReauthProfile is required to re-authenticate a Tailscale profile.
|
||||
// Root (or operator) access is required on Unix-like platforms,
|
||||
// profile ownership or elevated admin rights is required on Windows.
|
||||
ReauthProfile
|
||||
// ListPeers is required to view peer users and devices.
|
||||
// It is granted to all users on Unix-like platform,
|
||||
// and to the same users as ReadProfileInfo on Windows.
|
||||
ListPeers
|
||||
|
||||
// ReadPrefs is required to read ipn.Prefs associated with a profile,
|
||||
// but must not grant access to any sensitive information, such as private keys.
|
||||
//
|
||||
// As a general rule, the same users who have ReadProfileInfo access to a profile
|
||||
// also have the ReadPrefs access right.
|
||||
ReadPrefs
|
||||
// ChangePrefs allows changing any preference in ipn.Prefs.
|
||||
// Root (or operator) access is required on Unix-like platforms.
|
||||
// Profile ownership or elevated admin rights are required on Windows.
|
||||
ChangePrefs
|
||||
// ChangeExitNode allows users without the full ChangePrefs access to select an exit node.
|
||||
// As of 2024-04-08, it is only used to allow users on non-server, non-managed Windows devices to
|
||||
// to change an exit node on admin-configured unattended profiles.
|
||||
ChangeExitNode
|
||||
|
||||
// ReadServe is required to read a serve config.
|
||||
ReadServe
|
||||
// ChangeServe allows to change a serve config, except for serving a path.
|
||||
ChangeServe
|
||||
// ServePath allows to serve an arbitrary path.
|
||||
// It is a privileged operation that is only available to users that have
|
||||
// administrative access to the local machine.
|
||||
ServePath
|
||||
|
||||
// SetDNS allows sending a SetDNSRequest request to the control plane server,
|
||||
// requesting a DNS record be created or updated.
|
||||
// Root (or operator) access is required on Unix-like platforms.
|
||||
// Profile ownership or elevated admin rights are required on Windows.
|
||||
SetDNS
|
||||
// FetchCerts allows to get an ipnlocal.TLSCertKeyPair for domain, either from cache or via the ACME process.
|
||||
// On Windows, it's available to the profile owner. On Unix-like platforms, it requires root or operator access,
|
||||
// or the TS_PERMIT_CERT_UID environment variable set to the userid.
|
||||
FetchCerts
|
||||
// ReadPrivateKeys allows reading node's private key.
|
||||
// Root (or operator) access is required on Unix-like platforms.
|
||||
// Profile ownership is required on Windows.
|
||||
ReadPrivateKeys
|
||||
|
||||
// ReadTKA allows reading tailnet key authority info.
|
||||
// Root (or operator) access is required on Unix-like platforms.
|
||||
// Profile ownership or elevated admin rights are required on Windows.
|
||||
ReadTKA
|
||||
// ManageTKA allows managing TKA for a profile.
|
||||
// Root (or operator) access is required on Unix-like platforms.
|
||||
// Profile ownership or elevated admin rights are required on Windows.
|
||||
ManageTKA
|
||||
|
||||
// ReceiveFiles allows to receive files via Taildrop.
|
||||
// Root (or operator) access is required on Unix-like platforms.
|
||||
// Profile ownership or elevated admin rights are required on Windows.
|
||||
ReceiveFiles
|
||||
|
||||
// UnrestrictedProfileAccess combines all possible profile access rights,
|
||||
// granting full access to a profile.
|
||||
UnrestrictedProfileAccess = ^ProfileAccess(0)
|
||||
)
|
||||
|
||||
// Placeholder values for clients to use when rendering the current ipn.LoginProfile
|
||||
// if the client's user does not have ipnauth.ReadProfileInfo access to the profile.
|
||||
// However, clients supporting this feature should use UserProfile.ID.IsZero() to determine
|
||||
// when profile information is not accessible, and render masked profiles
|
||||
// in a platform-specific, localizable way.
|
||||
// Clients should avoid checking against these constants, as they are subject to change.
|
||||
const (
|
||||
maskedLoginName = "Other User's Account"
|
||||
maskedDisplayName = "Other User"
|
||||
maskedProfilePicURL = ""
|
||||
maskedDomainName = ""
|
||||
)
|
||||
|
||||
var profileAccessNames = map[ProfileAccess]string{
|
||||
ChangeExitNode: "ChangeExitNode",
|
||||
ChangePrefs: "ChangePrefs",
|
||||
ChangeServe: "ChangeServe",
|
||||
Connect: "Connect",
|
||||
DeleteProfile: "DeleteProfile",
|
||||
Disconnect: "Disconnect",
|
||||
FetchCerts: "FetchCerts",
|
||||
ListPeers: "ListPeers",
|
||||
ManageTKA: "ManageTKA",
|
||||
ReadPrefs: "ReadPrefs",
|
||||
ReadPrivateKeys: "ReadPrivateKeys",
|
||||
ReadProfileInfo: "ReadProfileInfo",
|
||||
ReadServe: "ReadServe",
|
||||
ReadTKA: "ReadTKA",
|
||||
ReauthProfile: "ReauthProfile",
|
||||
ReceiveFiles: "ReceiveFiles",
|
||||
ServePath: "ServePath",
|
||||
SetDNS: "SetDNS",
|
||||
}
|
||||
|
||||
var (
|
||||
deviceAccessBitNames = make([]string, 32)
|
||||
profileAccessBitNames = make([]string, 32)
|
||||
)
|
||||
|
||||
func init() {
|
||||
for da, name := range deviceAccessNames {
|
||||
deviceAccessBitNames[bits.Len32(uint32(da))-1] = name
|
||||
}
|
||||
for pa, name := range profileAccessNames {
|
||||
profileAccessBitNames[bits.Len32(uint32(pa))-1] = name
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a to da.
|
||||
// It is a no-op if da already contains a.
|
||||
func (da *DeviceAccess) Add(a DeviceAccess) {
|
||||
*da |= a
|
||||
}
|
||||
|
||||
// Remove removes a from da.
|
||||
// It is a no-op if da does not contain a.
|
||||
func (da *DeviceAccess) Remove(a DeviceAccess) {
|
||||
*da &= ^a
|
||||
}
|
||||
|
||||
// ContainsAll reports whether da contains all access rights specified in a.
|
||||
func (da *DeviceAccess) ContainsAll(a DeviceAccess) bool {
|
||||
return (*da & a) == a
|
||||
}
|
||||
|
||||
// Overlaps reports whether da contains any of the access rights specified in a.
|
||||
func (da *DeviceAccess) Overlaps(a DeviceAccess) bool {
|
||||
return (*da & a) != 0
|
||||
}
|
||||
|
||||
// String returns a string representation of one or more access rights in da.
|
||||
// It returns (None) if da is zero.
|
||||
func (da *DeviceAccess) String() string {
|
||||
return formatAccessMask(uint32(*da), deviceAccessBitNames)
|
||||
}
|
||||
|
||||
// Add adds a to pa.
|
||||
// It is a no-op if pa already contains a.
|
||||
func (pa *ProfileAccess) Add(a ProfileAccess) {
|
||||
*pa |= a
|
||||
}
|
||||
|
||||
// Remove removes a from pa.
|
||||
// It is a no-op if pa does not contain a.
|
||||
func (pa *ProfileAccess) Remove(a ProfileAccess) {
|
||||
*pa &= ^a
|
||||
}
|
||||
|
||||
// Contains reports whether pa contains all access rights specified in a.
|
||||
func (pa *ProfileAccess) Contains(a ProfileAccess) bool {
|
||||
return (*pa & a) == a
|
||||
}
|
||||
|
||||
// Overlaps reports whether pa contains any of the access rights specified in a.
|
||||
func (pa *ProfileAccess) Overlaps(a ProfileAccess) bool {
|
||||
return (*pa & a) != 0
|
||||
}
|
||||
|
||||
// String returns a string representation of one or more access rights in pa.
|
||||
// It returns (None) if pa is zero.
|
||||
func (pa *ProfileAccess) String() string {
|
||||
return formatAccessMask(uint32(*pa), profileAccessBitNames)
|
||||
}
|
||||
|
||||
func formatAccessMask(v uint32, flagNames []string) string {
|
||||
switch {
|
||||
case v == 0:
|
||||
return "(None)"
|
||||
case v == ^uint32(0):
|
||||
return "(Unrestricted)"
|
||||
case (v & (v - 1)) == 0:
|
||||
return flagNames[bits.Len32(v)-1]
|
||||
default:
|
||||
return formatAccessMaskSlow(v, flagNames)
|
||||
}
|
||||
}
|
||||
|
||||
func formatAccessMaskSlow(v uint32, flagNames []string) string {
|
||||
var rem uint32
|
||||
flags := make([]string, 0, bits.OnesCount32(v))
|
||||
for i := 0; i < 32 && v != 0; i++ {
|
||||
if bf := uint32(1 << i); v&bf != 0 {
|
||||
if name := flagNames[i]; name != "" {
|
||||
flags = append(flags, name)
|
||||
} else {
|
||||
rem |= bf
|
||||
}
|
||||
v &= ^bf
|
||||
}
|
||||
}
|
||||
if rem != 0 {
|
||||
flags = append(flags, "0x"+strings.ToUpper(strconv.FormatUint(uint64(rem), 16)))
|
||||
}
|
||||
return strings.Join(flags, "|")
|
||||
}
|
|
@ -0,0 +1,355 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// errNotAllowed is an error returned when access is neither explicitly allowed,
|
||||
// nor denied with a more specific error.
|
||||
var errNotAllowed error = ipn.NewAccessDeniedError("the requested operation is not allowed")
|
||||
|
||||
// AccessCheckResult represents the result of an access check.
|
||||
// Its zero value is valid and indicates that the access request
|
||||
// has neither been explicitly allowed nor denied for a specific reason.
|
||||
//
|
||||
// Higher-level access control code should forward the AccessCheckResult
|
||||
// from lower-level access control mechanisms to the caller
|
||||
// immediately upon receiving a definitive result, as indicated
|
||||
// by the AccessCheckResult.HasResult() method returning true.
|
||||
//
|
||||
// Requested access that has not been explicitly allowed
|
||||
// or explicitly denied is implicitly denied. This is reflected
|
||||
// in the values returned by AccessCheckResult's Allowed, Denied, and Error methods.
|
||||
type AccessCheckResult struct {
|
||||
err error
|
||||
hasResult bool
|
||||
}
|
||||
|
||||
// AllowAccess returns a new AccessCheckResult indicating that
|
||||
// the requested access has been allowed.
|
||||
//
|
||||
// Access control implementations should return AllowAccess()
|
||||
// only when they are certain that further access checks
|
||||
// are unnecessary and the requested access is definitively allowed.
|
||||
//
|
||||
// This includes cases where a certain access right, that might
|
||||
// otherwise be denied based on the environment and normal user rights,
|
||||
// is explicitly allowed by a corporate admin through syspolicy (GP or MDM).
|
||||
// It also covers situations where access is not denied by
|
||||
// higher-level access control mechanisms, such as syspolicy,
|
||||
// and is granted based on the user's identity, following
|
||||
// platform and environment-specific rules.
|
||||
// (e.g., because they are root on Unix or a profile owner on a personal Windows device).
|
||||
func AllowAccess() AccessCheckResult {
|
||||
return AccessCheckResult{hasResult: true}
|
||||
}
|
||||
|
||||
// DenyAccess returns a new AccessCheckResult indicating that
|
||||
// the requested access has been denied with the specified err.
|
||||
//
|
||||
// Access control implementations should return DenyAccess()
|
||||
// as soon as the requested access has been denied, without calling
|
||||
// any subsequent lower-level access checking mechanisms, if any.
|
||||
//
|
||||
// Higher-level access control code should forward the AccessCheckResult
|
||||
// from any lower-level access check to the caller as soon as it receives
|
||||
// a definitive result as indicated by the HasResult() method returning true.
|
||||
// Therefore, if access is denied due to tailscaled config or syspolicy settings,
|
||||
// it will be immediately denied, regardless of the caller's identity.
|
||||
func DenyAccess(err error) AccessCheckResult {
|
||||
if err == nil {
|
||||
err = ipn.NewInternalServerError("access denied with a nil error")
|
||||
} else {
|
||||
err = &ipn.AccessDeniedError{Err: err}
|
||||
}
|
||||
return AccessCheckResult{err: err, hasResult: true}
|
||||
}
|
||||
|
||||
// ContinueCheck returns a new AccessCheckResult indicating that
|
||||
// the requested access has neither been allowed, nor denied,
|
||||
// and any further access checks should be performed to determine the result.
|
||||
//
|
||||
// An an example, a higher level access control code that denies
|
||||
// certain access rights based on syspolicy may return ContinueCheck()
|
||||
// to indicate that access is not denied by any applicable policies,
|
||||
// and lower-level access checks should be performed.
|
||||
//
|
||||
// Similarly, if a tailscaled config file is present and restricts certain ipn.Prefs fields
|
||||
// from being modified, its access checking mechanism should return ContinueCheck()
|
||||
// when a user tries to change only preferences that are not locked down.
|
||||
//
|
||||
// As a general rule, any higher-level access checking code should
|
||||
// continue calling lower-level access checking code, until it either receives
|
||||
// and forwards a definitive result from one of the lower-level mechanisms,
|
||||
// or until there are no additional checks to be performed.
|
||||
// In the latter case, it can also return ContinueCheck(),
|
||||
// resulting in the requested access being implicitly denied.
|
||||
func ContinueCheck() AccessCheckResult {
|
||||
return AccessCheckResult{}
|
||||
}
|
||||
|
||||
// HasResult reports whether a definitive access decision (either allowed or denied) has been made.
|
||||
func (r AccessCheckResult) HasResult() bool {
|
||||
return r.hasResult
|
||||
}
|
||||
|
||||
// Allowed reports whether the requested access has been allowed.
|
||||
func (r AccessCheckResult) Allowed() bool {
|
||||
return r.hasResult && r.err == nil
|
||||
}
|
||||
|
||||
// Denied reports whether the requested access should be denied.
|
||||
func (r AccessCheckResult) Denied() bool {
|
||||
return !r.hasResult || r.err != nil
|
||||
}
|
||||
|
||||
// Error returns an ipn.AccessDeniedError detailing why access was denied,
|
||||
// or nil if access has been allowed.
|
||||
func (r AccessCheckResult) Error() error {
|
||||
if !r.hasResult && r.err == nil {
|
||||
return errNotAllowed
|
||||
}
|
||||
return r.err
|
||||
}
|
||||
|
||||
// String returns a string representation of r.
|
||||
func (r AccessCheckResult) String() string {
|
||||
switch {
|
||||
case !r.hasResult:
|
||||
return "Implicit Deny"
|
||||
case r.err != nil:
|
||||
return "Deny: " + r.err.Error()
|
||||
default:
|
||||
return "Allow"
|
||||
}
|
||||
}
|
||||
|
||||
// accessChecker is a helper type that allows step-by-step granting or denying of access rights.
|
||||
type accessChecker[T ~uint32] struct {
|
||||
remain T // access rights that were requested but have not been granted yet.
|
||||
res AccessCheckResult
|
||||
}
|
||||
|
||||
// newAccessChecker returns a new accessChecker with the specified requested access.
|
||||
func newAccessChecker[T ~uint32](requested T) accessChecker[T] {
|
||||
return accessChecker[T]{remain: requested}
|
||||
}
|
||||
|
||||
// remaining returns the access rights that have been requested but not yet granted.
|
||||
func (ac *accessChecker[T]) remaining() T {
|
||||
return ac.remain
|
||||
}
|
||||
|
||||
// result determines if access is Allowed, Denied, or requires further evaluation.
|
||||
func (ac *accessChecker[T]) result() AccessCheckResult {
|
||||
if !ac.res.HasResult() && ac.remaining() == 0 {
|
||||
ac.res = AllowAccess()
|
||||
}
|
||||
return ac.res
|
||||
}
|
||||
|
||||
// grant unconditionally grants the specified rights, updating and returning an AccessCheckResult.
|
||||
func (ac *accessChecker[T]) grant(rights T) AccessCheckResult {
|
||||
ac.remain &= ^rights
|
||||
return ac.result()
|
||||
}
|
||||
|
||||
// deny unconditionally denies the specified rights, updating and returning an AccessCheckResult.
|
||||
// If the specified rights were not requested, it is a no-op.
|
||||
func (ac *accessChecker[T]) deny(rights T, err error) AccessCheckResult {
|
||||
if ac.remain&rights != 0 {
|
||||
ac.res = DenyAccess(err)
|
||||
}
|
||||
return ac.result()
|
||||
}
|
||||
|
||||
// tryGrant grants the specified rights and updates the result if those rights have been requested
|
||||
// and the check does not return an error.
|
||||
// Otherwise, it is a no-op.
|
||||
func (ac *accessChecker[T]) tryGrant(rights T, check func() error) AccessCheckResult {
|
||||
if ac.remain&rights != 0 && check() == nil {
|
||||
return ac.grant(rights)
|
||||
}
|
||||
return ac.result()
|
||||
}
|
||||
|
||||
// mustGrant attempts to grant specified rights if they have been requested.
|
||||
// If the check fails with an error, that error is used as the reason for access denial.
|
||||
// If the specified rights were not requested, it is a no-op.
|
||||
func (ac *accessChecker[T]) mustGrant(rights T, check func() error) AccessCheckResult {
|
||||
if ac.remain&rights != 0 {
|
||||
if err := check(); err != nil {
|
||||
return ac.deny(rights, err)
|
||||
}
|
||||
return ac.grant(rights)
|
||||
}
|
||||
return ac.result()
|
||||
}
|
||||
|
||||
// CheckAccess reports whether the caller is allowed or denied the desired access.
|
||||
func CheckAccess(caller Identity, desired DeviceAccess) AccessCheckResult {
|
||||
// Allow non-user originating changes, such as any changes requested by the control plane.
|
||||
// We don't want these to be affected by GP/MDM policies or any other restrictions.
|
||||
if IsUnrestricted(caller) {
|
||||
return AllowAccess()
|
||||
}
|
||||
|
||||
// TODO(nickkhyl): check syspolicy.
|
||||
|
||||
return caller.CheckAccess(desired)
|
||||
}
|
||||
|
||||
// CheckProfileAccess reports whether the caller is allowed or denied the desired access
|
||||
// to a specific profile and its prefs.
|
||||
func CheckProfileAccess(caller Identity, profile ipn.LoginProfileView, prefs ipn.PrefsGetter, requested ProfileAccess) AccessCheckResult {
|
||||
// TODO(nickkhyl): consider moving or copying OperatorUser from ipn.Prefs to ipn.LoginProfile,
|
||||
// as this is the main reason why we need to read prefs here.
|
||||
|
||||
// Allow non-user originating changes, such as any changes requested by the control plane.
|
||||
// We don't want these to be affected by GP/MDM policies or any other restrictions.
|
||||
if IsUnrestricted(caller) {
|
||||
return AllowAccess()
|
||||
}
|
||||
|
||||
// TODO(nickkhyl): check syspolicy.
|
||||
|
||||
return caller.CheckProfileAccess(profile, prefs, requested)
|
||||
}
|
||||
|
||||
// CheckEditProfile reports whether the caller has access to apply the specified changes to
|
||||
// the profile and prefs.
|
||||
func CheckEditProfile(caller Identity, profile ipn.LoginProfileView, prefs ipn.PrefsGetter, changes *ipn.MaskedPrefs) AccessCheckResult {
|
||||
if IsUnrestricted(caller) {
|
||||
return AllowAccess()
|
||||
}
|
||||
|
||||
requiredAccess := PrefsChangeRequiredAccess(changes)
|
||||
return CheckProfileAccess(caller, profile, prefs, requiredAccess)
|
||||
}
|
||||
|
||||
// FilterProfile returns the specified profile, filtering or masking out fields
|
||||
// inaccessible to the caller. The provided profile value is considered immutable,
|
||||
// and a new instance of ipn.LoginProfile will be returned if any filtering is necessary.
|
||||
func FilterProfile(caller Identity, profile ipn.LoginProfileView, prefs ipn.PrefsGetter) ipn.LoginProfileView {
|
||||
switch {
|
||||
case CheckProfileAccess(caller, profile, prefs, ReadProfileInfo).Allowed():
|
||||
return profile
|
||||
default:
|
||||
res := &ipn.LoginProfile{
|
||||
ID: profile.ID(),
|
||||
Key: profile.Key(),
|
||||
LocalUserID: profile.LocalUserID(),
|
||||
UserProfile: maskedUserProfile(profile),
|
||||
NetworkProfile: maskedNetworkProfile(profile),
|
||||
}
|
||||
res.Name = res.UserProfile.LoginName
|
||||
return res.View()
|
||||
}
|
||||
}
|
||||
|
||||
// maskedNetworkProfile returns a masked tailcfg.UserProfile for the specified profile.
|
||||
// The returned value is used by ipnauth.FilterProfile in place of the actual ipn.LoginProfile.UserProfile
|
||||
// when the caller does not have ipnauth.ReadProfileInfo access to the profile.
|
||||
//
|
||||
// Although CLI or GUI clients can render this value as is, it's not localizable, may lead to a suboptimal UX,
|
||||
// and is provided mainly for compatibility with existing clients.
|
||||
//
|
||||
// For an improved UX, CLI and GUI clients should use UserProfile.ID.IsZero() to check
|
||||
// whether profile information is inaccessible and then render such profiles
|
||||
// in a platform-specific and localizable way.
|
||||
func maskedUserProfile(ipn.LoginProfileView) tailcfg.UserProfile {
|
||||
return tailcfg.UserProfile{
|
||||
LoginName: maskedLoginName,
|
||||
DisplayName: maskedDisplayName,
|
||||
ProfilePicURL: maskedProfilePicURL,
|
||||
}
|
||||
}
|
||||
|
||||
// maskedNetworkProfile returns a masked ipn.NetworkProfile for the specified profile.
|
||||
// It is like maskedUserProfile, but for NetworkProfile.
|
||||
func maskedNetworkProfile(ipn.LoginProfileView) ipn.NetworkProfile {
|
||||
return ipn.NetworkProfile{
|
||||
DomainName: maskedDomainName,
|
||||
}
|
||||
}
|
||||
|
||||
// PrefsChangeRequiredAccess returns the access required to change prefs as requested by mp.
|
||||
func PrefsChangeRequiredAccess(mp *ipn.MaskedPrefs) ProfileAccess {
|
||||
masked := reflect.ValueOf(mp).Elem()
|
||||
return maskedPrefsFieldsAccess(&mp.Prefs, "", masked)
|
||||
}
|
||||
|
||||
// maskedPrefsFieldsAccess returns the access required to change preferences, whose
|
||||
// corresponding {FieldName}Set flags are set in masked, to the values specified in p.
|
||||
// The `path` represents a dot-separated path to masked from the ipn.MaskedPrefs root.
|
||||
func maskedPrefsFieldsAccess(p *ipn.Prefs, path string, masked reflect.Value) ProfileAccess {
|
||||
var access ProfileAccess
|
||||
for i := 0; i < masked.NumField(); i++ {
|
||||
fName := masked.Type().Field(i).Name
|
||||
if !strings.HasSuffix(fName, "Set") {
|
||||
continue
|
||||
}
|
||||
fName = strings.TrimSuffix(fName, "Set")
|
||||
fPath := path + fName
|
||||
fValue := masked.Field(i)
|
||||
|
||||
switch fKind := fValue.Kind(); fKind {
|
||||
case reflect.Bool:
|
||||
if fValue.Bool() {
|
||||
access |= prefsFieldRequiredAccess(p, fPath)
|
||||
}
|
||||
case reflect.Struct:
|
||||
access |= maskedPrefsFieldsAccess(p, fPath+".", fValue)
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported mask field kind %v", fKind))
|
||||
}
|
||||
}
|
||||
return access
|
||||
}
|
||||
|
||||
// prefsDefaultFieldAccess is the default ProfileAccess required to modify ipn.Prefs fields
|
||||
// that do not have access rights overrides.
|
||||
const prefsDefaultFieldAccess = ChangePrefs
|
||||
|
||||
var (
|
||||
// prefsStaticFieldAccessOverride allows to override ProfileAccess needed to modify ipn.Prefs fields.
|
||||
// The map uses dot-separated field paths as keys.
|
||||
prefsStaticFieldAccessOverride = map[string]ProfileAccess{
|
||||
"ExitNodeID": ChangeExitNode,
|
||||
"ExitNodeIP": ChangeExitNode,
|
||||
"ExitNodeAllowLANAccess": ChangeExitNode,
|
||||
}
|
||||
// prefsDynamicFieldAccessOverride is like prefsStaticFieldAccessOverride, but it maps field paths
|
||||
// to functions that dynamically determine ProfileAccess based on the target value to be set.
|
||||
prefsDynamicFieldAccessOverride = map[string]func(p *ipn.Prefs) ProfileAccess{
|
||||
"WantRunning": prefsWantRunningRequiredAccess,
|
||||
}
|
||||
)
|
||||
|
||||
// prefsFieldRequiredAccess returns the access required to change a prefs field
|
||||
// represented by its field path in ipn.MaskedPrefs to the corresponding value in p.
|
||||
func prefsFieldRequiredAccess(p *ipn.Prefs, path string) ProfileAccess {
|
||||
if access, ok := prefsStaticFieldAccessOverride[path]; ok {
|
||||
return access
|
||||
}
|
||||
if accessFn, ok := prefsDynamicFieldAccessOverride[path]; ok {
|
||||
return accessFn(p)
|
||||
}
|
||||
return prefsDefaultFieldAccess
|
||||
}
|
||||
|
||||
// prefsWantRunningRequiredAccess returns the access required to change WantRunning to the value in p.
|
||||
func prefsWantRunningRequiredAccess(p *ipn.Prefs) ProfileAccess {
|
||||
if p.WantRunning {
|
||||
return Connect
|
||||
}
|
||||
return Disconnect
|
||||
}
|
|
@ -0,0 +1,550 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func TestAccessCheckResult(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
res AccessCheckResult
|
||||
wantStr string
|
||||
wantHasResult bool
|
||||
wantAllow bool
|
||||
wantDeny bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "zero-value-implicit-deny",
|
||||
res: AccessCheckResult{},
|
||||
wantStr: "Implicit Deny",
|
||||
wantHasResult: false,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "continue-implicit-deny",
|
||||
res: ContinueCheck(),
|
||||
wantStr: "Implicit Deny",
|
||||
wantHasResult: false,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "explicit-deny",
|
||||
res: DenyAccess(errNotAllowed),
|
||||
wantStr: "Deny: " + errNotAllowed.Error(),
|
||||
wantHasResult: true,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "explicit-allow",
|
||||
res: AllowAccess(),
|
||||
wantStr: "Allow",
|
||||
wantHasResult: true,
|
||||
wantAllow: true,
|
||||
wantDeny: false,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotStr := tt.res.String(); gotStr != tt.wantStr {
|
||||
t.Errorf("got: %q, want: %q", gotStr, tt.wantStr)
|
||||
}
|
||||
if gotHasResult := tt.res.HasResult(); gotHasResult != tt.wantHasResult {
|
||||
t.Errorf("gotHasResult: %v, wantHasResult: %v", gotHasResult, tt.wantHasResult)
|
||||
}
|
||||
if gotAllow := tt.res.Allowed(); gotAllow != tt.wantAllow {
|
||||
t.Errorf("gotAllow: %v, wantAllow: %v", gotAllow, tt.wantAllow)
|
||||
}
|
||||
if gotDeny := tt.res.Denied(); gotDeny != tt.wantDeny {
|
||||
t.Errorf("gotDeny: %v, wantDeny: %v", gotDeny, tt.wantDeny)
|
||||
}
|
||||
|
||||
if gotErr := tt.res.Error(); tt.wantErr {
|
||||
if _, isAccessDenied := gotErr.(*ipn.AccessDeniedError); !isAccessDenied {
|
||||
t.Errorf("err: %v, wantErr: %v", gotErr, tt.wantErr)
|
||||
}
|
||||
} else if gotErr != nil {
|
||||
t.Errorf("err: %v, wantErr: %v", gotErr, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessCheckerGrant(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
requested ProfileAccess
|
||||
grant []ProfileAccess
|
||||
wantRemaining ProfileAccess
|
||||
wantHasResult bool
|
||||
wantAllow bool
|
||||
wantDeny bool
|
||||
}{
|
||||
{
|
||||
name: "grant-none",
|
||||
requested: ReadProfileInfo,
|
||||
grant: []ProfileAccess{},
|
||||
wantRemaining: ReadProfileInfo,
|
||||
wantHasResult: false,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
},
|
||||
{
|
||||
name: "grant-single",
|
||||
requested: ReadProfileInfo,
|
||||
grant: []ProfileAccess{ReadProfileInfo},
|
||||
wantRemaining: 0,
|
||||
wantHasResult: true,
|
||||
wantAllow: true,
|
||||
wantDeny: false,
|
||||
},
|
||||
{
|
||||
name: "grant-other",
|
||||
requested: ReadProfileInfo,
|
||||
grant: []ProfileAccess{Connect},
|
||||
wantRemaining: ReadProfileInfo,
|
||||
wantHasResult: false,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
},
|
||||
{
|
||||
name: "grant-some",
|
||||
requested: ReadProfileInfo | Connect,
|
||||
grant: []ProfileAccess{ReadProfileInfo},
|
||||
wantRemaining: Connect,
|
||||
wantHasResult: false,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
},
|
||||
{
|
||||
name: "grant-all",
|
||||
requested: ReadProfileInfo | Connect | Disconnect | ReadPrefs,
|
||||
grant: []ProfileAccess{ReadProfileInfo, Connect | Disconnect, ReadPrefs},
|
||||
wantRemaining: 0,
|
||||
wantHasResult: true,
|
||||
wantAllow: true,
|
||||
wantDeny: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
checker := newAccessChecker(tt.requested)
|
||||
for _, grant := range tt.grant {
|
||||
checker.grant(grant)
|
||||
}
|
||||
if gotRemaining := checker.remaining(); gotRemaining != tt.wantRemaining {
|
||||
t.Errorf("gotRemaining: %v, wantRemaining: %v", gotRemaining, tt.wantRemaining)
|
||||
}
|
||||
res := checker.result()
|
||||
if gotHasResult := res.HasResult(); gotHasResult != tt.wantHasResult {
|
||||
t.Errorf("gotHasResult: %v, wantHasResult: %v", gotHasResult, tt.wantHasResult)
|
||||
}
|
||||
if gotAllow := res.Allowed(); gotAllow != tt.wantAllow {
|
||||
t.Errorf("gotAllow: %v, wantAllow: %v", gotAllow, tt.wantAllow)
|
||||
}
|
||||
if gotDeny := res.Denied(); gotDeny != tt.wantDeny {
|
||||
t.Errorf("gotDeny: %v, wantDeny: %v", gotDeny, tt.wantDeny)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessCheckerConditionalGrant(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
requested ProfileAccess
|
||||
mustGrant bool
|
||||
grant ProfileAccess
|
||||
predicate func() error
|
||||
wantRemaining ProfileAccess
|
||||
wantHasResult bool
|
||||
wantAllow bool
|
||||
wantDeny bool
|
||||
}{
|
||||
{
|
||||
name: "try-grant",
|
||||
requested: ReadProfileInfo,
|
||||
grant: ReadProfileInfo,
|
||||
predicate: func() error { return nil },
|
||||
wantRemaining: 0,
|
||||
wantHasResult: true,
|
||||
wantAllow: true,
|
||||
wantDeny: false,
|
||||
},
|
||||
{
|
||||
name: "try-grant-err",
|
||||
requested: ReadProfileInfo,
|
||||
grant: ReadProfileInfo,
|
||||
predicate: func() error { return errNotAllowed },
|
||||
wantRemaining: ReadProfileInfo,
|
||||
wantHasResult: false,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
},
|
||||
{
|
||||
name: "must-grant",
|
||||
requested: ReadProfileInfo,
|
||||
mustGrant: true,
|
||||
grant: ReadProfileInfo,
|
||||
predicate: func() error { return nil },
|
||||
wantRemaining: 0,
|
||||
wantHasResult: true,
|
||||
wantAllow: true,
|
||||
wantDeny: false,
|
||||
},
|
||||
{
|
||||
name: "must-grant-err",
|
||||
requested: ReadProfileInfo,
|
||||
mustGrant: true,
|
||||
grant: ReadProfileInfo,
|
||||
predicate: func() error { return errNotAllowed },
|
||||
wantRemaining: ReadProfileInfo,
|
||||
wantHasResult: true,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
checker := newAccessChecker(tt.requested)
|
||||
|
||||
var res AccessCheckResult
|
||||
if tt.mustGrant {
|
||||
res = checker.mustGrant(tt.grant, tt.predicate)
|
||||
} else {
|
||||
res = checker.tryGrant(tt.grant, tt.predicate)
|
||||
}
|
||||
|
||||
if gotRemaining := checker.remaining(); gotRemaining != tt.wantRemaining {
|
||||
t.Errorf("gotRemaining: %v, wantRemaining: %v", gotRemaining, tt.wantRemaining)
|
||||
}
|
||||
if gotHasResult := res.HasResult(); gotHasResult != tt.wantHasResult {
|
||||
t.Errorf("gotHasResult: %v, wantHasResult: %v", gotHasResult, tt.wantHasResult)
|
||||
}
|
||||
if gotAllow := res.Allowed(); gotAllow != tt.wantAllow {
|
||||
t.Errorf("gotAllow: %v, wantAllow: %v", gotAllow, tt.wantAllow)
|
||||
}
|
||||
if gotDeny := res.Denied(); gotDeny != tt.wantDeny {
|
||||
t.Errorf("gotDeny: %v, wantDeny: %v", gotDeny, tt.wantDeny)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessCheckerDeny(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
requested ProfileAccess
|
||||
grant ProfileAccess
|
||||
deny ProfileAccess
|
||||
wantHasResult bool
|
||||
wantAllow bool
|
||||
wantDeny bool
|
||||
}{
|
||||
{
|
||||
name: "deny-single",
|
||||
requested: ReadProfileInfo,
|
||||
deny: ReadProfileInfo,
|
||||
wantHasResult: true,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
},
|
||||
{
|
||||
name: "deny-other",
|
||||
requested: ReadProfileInfo,
|
||||
deny: Connect,
|
||||
wantHasResult: false,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
},
|
||||
{
|
||||
name: "grant-some-then-deny",
|
||||
requested: ReadProfileInfo | Connect,
|
||||
grant: ReadProfileInfo,
|
||||
deny: Connect,
|
||||
wantHasResult: true,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
},
|
||||
{
|
||||
name: "deny-some",
|
||||
requested: ReadProfileInfo | Connect | Disconnect | ReadPrefs,
|
||||
deny: Connect,
|
||||
wantHasResult: true,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
},
|
||||
{
|
||||
name: "deny-all",
|
||||
requested: ReadProfileInfo | Connect | Disconnect | ReadPrefs,
|
||||
deny: ReadProfileInfo | Connect | Disconnect | ReadPrefs,
|
||||
wantHasResult: true,
|
||||
wantAllow: false,
|
||||
wantDeny: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
checker := newAccessChecker(tt.requested)
|
||||
res := checker.grant(tt.grant)
|
||||
if res.HasResult() {
|
||||
t.Fatalf("the result must not be ready yet")
|
||||
}
|
||||
res = checker.deny(tt.deny, errNotAllowed)
|
||||
if gotHasResult := res.HasResult(); gotHasResult != tt.wantHasResult {
|
||||
t.Errorf("gotHasResult: %v, wantHasResult: %v", gotHasResult, tt.wantHasResult)
|
||||
}
|
||||
if gotAllow := res.Allowed(); gotAllow != tt.wantAllow {
|
||||
t.Errorf("gotAllow: %v, wantAllow: %v", gotAllow, tt.wantAllow)
|
||||
}
|
||||
if gotDeny := res.Denied(); gotDeny != tt.wantDeny {
|
||||
t.Errorf("gotDeny: %v, wantDeny: %v", gotDeny, tt.wantDeny)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterProfile(t *testing.T) {
|
||||
profile := &ipn.LoginProfile{
|
||||
ID: "TEST",
|
||||
Key: "profile-TEST",
|
||||
Name: "user@example.com",
|
||||
NetworkProfile: ipn.NetworkProfile{
|
||||
MagicDNSName: "example.ts.net",
|
||||
DomainName: "example.ts.net",
|
||||
},
|
||||
UserProfile: tailcfg.UserProfile{
|
||||
ID: 123456789,
|
||||
LoginName: "user@example.com",
|
||||
DisplayName: "User",
|
||||
ProfilePicURL: "https://example.com/profile.png",
|
||||
},
|
||||
NodeID: "TEST-NODE-ID",
|
||||
LocalUserID: "S-1-5-21-1234567890-1234567890-1234567890-1001",
|
||||
ControlURL: "https://controlplane.tailscale.com",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
user Identity
|
||||
profile *ipn.LoginProfile
|
||||
wantProfile *ipn.LoginProfile
|
||||
}{
|
||||
{
|
||||
name: "filter-unreadable",
|
||||
user: &TestIdentity{ProfileAccess: 0},
|
||||
profile: profile,
|
||||
wantProfile: &ipn.LoginProfile{
|
||||
ID: profile.ID,
|
||||
Name: "Other User's Account",
|
||||
Key: profile.Key,
|
||||
LocalUserID: profile.LocalUserID,
|
||||
UserProfile: tailcfg.UserProfile{
|
||||
LoginName: "Other User's Account",
|
||||
DisplayName: "Other User",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "do-not-filter-readable",
|
||||
user: &TestIdentity{UID: string(profile.LocalUserID), ProfileAccess: ReadProfileInfo},
|
||||
profile: profile,
|
||||
wantProfile: profile,
|
||||
},
|
||||
{
|
||||
name: "do-not-filter-for-self",
|
||||
user: Self,
|
||||
profile: profile,
|
||||
wantProfile: profile,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
profile := FilterProfile(tt.user, tt.profile.View(), ipn.PrefsGetterFor(ipn.PrefsView{})).AsStruct()
|
||||
if !reflect.DeepEqual(profile, tt.wantProfile) {
|
||||
t.Errorf("got: %+v, want: %+v", profile, tt.wantProfile)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefsChangeRequiredAccess(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefs ipn.MaskedPrefs
|
||||
wantRequiredAccess ProfileAccess
|
||||
}{
|
||||
{
|
||||
name: "no-changes",
|
||||
prefs: ipn.MaskedPrefs{},
|
||||
wantRequiredAccess: 0,
|
||||
},
|
||||
{
|
||||
name: "connect",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{WantRunning: true},
|
||||
WantRunningSet: true,
|
||||
},
|
||||
wantRequiredAccess: Connect,
|
||||
},
|
||||
{
|
||||
name: "disconnect",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{WantRunning: false},
|
||||
WantRunningSet: true,
|
||||
},
|
||||
wantRequiredAccess: Disconnect,
|
||||
},
|
||||
{
|
||||
name: "change-exit-node-id",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
ExitNodeIDSet: true,
|
||||
},
|
||||
wantRequiredAccess: ChangeExitNode,
|
||||
},
|
||||
{
|
||||
name: "change-exit-node-ip",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
ExitNodeIPSet: true,
|
||||
},
|
||||
wantRequiredAccess: ChangeExitNode,
|
||||
},
|
||||
{
|
||||
name: "change-exit-node-lan-access",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
ExitNodeAllowLANAccessSet: true,
|
||||
},
|
||||
wantRequiredAccess: ChangeExitNode,
|
||||
},
|
||||
{
|
||||
name: "change-multiple",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{WantRunning: true},
|
||||
ExitNodeIDSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
wantRequiredAccess: Connect | ChangeExitNode,
|
||||
},
|
||||
{
|
||||
name: "change-other-single",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
ForceDaemonSet: true,
|
||||
},
|
||||
wantRequiredAccess: ChangePrefs,
|
||||
},
|
||||
{
|
||||
name: "change-other-multiple",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
ForceDaemonSet: true,
|
||||
RunSSHSet: true,
|
||||
},
|
||||
wantRequiredAccess: ChangePrefs,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotRequiredAccess := PrefsChangeRequiredAccess(&tt.prefs)
|
||||
if gotRequiredAccess != tt.wantRequiredAccess {
|
||||
t.Errorf("got: %v, want: %v", gotRequiredAccess, tt.wantRequiredAccess)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckEditProfile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefs ipn.MaskedPrefs
|
||||
user Identity
|
||||
wantAllow bool
|
||||
}{
|
||||
{
|
||||
name: "allow-connect",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{WantRunning: true},
|
||||
WantRunningSet: true,
|
||||
},
|
||||
user: &TestIdentity{ProfileAccess: Connect},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-connect",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{WantRunning: true},
|
||||
WantRunningSet: true,
|
||||
},
|
||||
user: &TestIdentity{ProfileAccess: ReadProfileInfo},
|
||||
wantAllow: false,
|
||||
},
|
||||
{
|
||||
name: "allow-change-exit-node",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
ExitNodeIDSet: true,
|
||||
},
|
||||
user: &TestIdentity{ProfileAccess: ChangeExitNode},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-change-prefs",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
ForceDaemonSet: true,
|
||||
RunSSHSet: true,
|
||||
},
|
||||
user: &TestIdentity{ProfileAccess: ChangePrefs},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-change-prefs",
|
||||
prefs: ipn.MaskedPrefs{
|
||||
ForceDaemonSet: true,
|
||||
RunSSHSet: true,
|
||||
},
|
||||
user: &TestIdentity{ProfileAccess: ChangeExitNode},
|
||||
wantAllow: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
profile, prefs := ipn.LoginProfile{}, ipn.NewPrefs()
|
||||
res := CheckEditProfile(tt.user, profile.View(), ipn.PrefsGetterFor(prefs.View()), &tt.prefs)
|
||||
if gotAllow := res.Allowed(); gotAllow != tt.wantAllow {
|
||||
t.Errorf("gotAllow: %v, wantAllow: %v", gotAllow, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDenyAccessWithNilError(t *testing.T) {
|
||||
res := DenyAccess(nil)
|
||||
if gotHasResult := res.HasResult(); !gotHasResult {
|
||||
t.Errorf("gotHasResult: %v, wantHasResult: true", gotHasResult)
|
||||
}
|
||||
if gotAllow := res.Allowed(); gotAllow {
|
||||
t.Errorf("gotAllow: %v, wantAllow: false", gotAllow)
|
||||
}
|
||||
if gotDeny := res.Denied(); !gotDeny {
|
||||
t.Errorf("gotDeny: %v, wantDeny: true", gotDeny)
|
||||
}
|
||||
gotErr := res.Error()
|
||||
if _, isInternalError := gotErr.(*ipn.InternalServerError); !isInternalError {
|
||||
t.Errorf("got %T: %v, want: *ipn.InternalServerError", gotErr, gotErr)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeviceAccessStringer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
access DeviceAccess
|
||||
wantStr string
|
||||
}{
|
||||
{
|
||||
name: "zero-access",
|
||||
access: 0,
|
||||
wantStr: "(None)",
|
||||
},
|
||||
{
|
||||
name: "unrestricted-access",
|
||||
access: ^DeviceAccess(0),
|
||||
wantStr: "(Unrestricted)",
|
||||
},
|
||||
{
|
||||
name: "single-access",
|
||||
access: ReadDeviceStatus,
|
||||
wantStr: "ReadDeviceStatus",
|
||||
},
|
||||
{
|
||||
name: "multi-access",
|
||||
access: ReadDeviceStatus | GenerateBugReport | DeleteAllProfiles,
|
||||
wantStr: "ReadDeviceStatus|GenerateBugReport|DeleteAllProfiles",
|
||||
},
|
||||
{
|
||||
name: "unknown-access",
|
||||
access: DeviceAccess(0xABCD0000),
|
||||
wantStr: "0xABCD0000",
|
||||
},
|
||||
{
|
||||
name: "multi-with-unknown-access",
|
||||
access: ReadDeviceStatus | DeviceAccess(0xABCD0000),
|
||||
wantStr: "ReadDeviceStatus|0xABCD0000",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotStr := tt.access.String()
|
||||
if gotStr != tt.wantStr {
|
||||
t.Errorf("got %v, want %v", gotStr, tt.wantStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileAccessStringer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
access ProfileAccess
|
||||
wantStr string
|
||||
}{
|
||||
{
|
||||
name: "zero-access",
|
||||
access: 0,
|
||||
wantStr: "(None)",
|
||||
},
|
||||
{
|
||||
name: "unrestricted-access",
|
||||
access: ^ProfileAccess(0),
|
||||
wantStr: "(Unrestricted)",
|
||||
},
|
||||
{
|
||||
name: "single-access",
|
||||
access: ReadProfileInfo,
|
||||
wantStr: "ReadProfileInfo",
|
||||
},
|
||||
{
|
||||
name: "multi-access",
|
||||
access: ReadProfileInfo | Connect | Disconnect,
|
||||
wantStr: "ReadProfileInfo|Connect|Disconnect",
|
||||
},
|
||||
{
|
||||
name: "unknown-access",
|
||||
access: ProfileAccess(0xFF000000),
|
||||
wantStr: "0xFF000000",
|
||||
},
|
||||
{
|
||||
name: "multi-with-unknown-access",
|
||||
access: ReadProfileInfo | ProfileAccess(0xFF000000),
|
||||
wantStr: "ReadProfileInfo|0xFF000000",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotStr := tt.access.String()
|
||||
if gotStr != tt.wantStr {
|
||||
t.Errorf("got %v, want %v", gotStr, tt.wantStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamedDeviceAccessFlagsArePowerOfTwo(t *testing.T) {
|
||||
for da, name := range deviceAccessNames {
|
||||
if (da & (da - 1)) != 0 {
|
||||
t.Errorf("DeviceAccess, %s: got 0x%x, want power of two", name, uint64(da))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamedProfileAccessFlagsArePowerOfTwo(t *testing.T) {
|
||||
for pa, name := range profileAccessNames {
|
||||
if (pa & (pa - 1)) != 0 {
|
||||
t.Errorf("ProfileAccess, %s: got 0x%x, want power of two", name, uint64(pa))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Identity is any caller identity.
|
||||
//
|
||||
// It typically represents a specific OS user, indicating that an operation
|
||||
// is performed on behalf of this user, should be evaluated against their
|
||||
// access rights, and performed in their security context when applicable.
|
||||
//
|
||||
// However, it can also represent an unrestricted identity (e.g. ipnauth.Self) when an operation
|
||||
// is executed on behalf of tailscaled itself, in response to a control plane request,
|
||||
// or when a user's access rights have been verified via other means.
|
||||
type Identity interface {
|
||||
// UserID returns an OS-specific UID of the user represented by the identity,
|
||||
// or "" if the receiver does not represent a specific user.
|
||||
// As of 2024-04-08, it is only used on Windows.
|
||||
UserID() ipn.WindowsUserID
|
||||
// Username returns the user name associated with the receiver,
|
||||
// or "" if the receiver does not represent a specific user.
|
||||
Username() (string, error)
|
||||
// CheckAccess reports whether the receiver is allowed or denied the requested device access.
|
||||
//
|
||||
// This method ignores environment factors, Group Policy, and MDM settings that might
|
||||
// override access permissions at a higher level than individual user identities.
|
||||
// Therefore, most callers should use ipnauth.CheckAccess instead.
|
||||
CheckAccess(requested DeviceAccess) AccessCheckResult
|
||||
// CheckProfileAccess reports whether the receiver is allowed or denied the requested access
|
||||
// to a specific profile and its prefs.
|
||||
//
|
||||
// This method ignores environment factors, Group Policy, and MDM settings that might
|
||||
// override access permissions at a higher level than individual user identities.
|
||||
// Therefore, most callers should use ipnauth.CheckProfileAccess instead.
|
||||
CheckProfileAccess(profile ipn.LoginProfileView, prefs ipn.PrefsGetter, requested ProfileAccess) AccessCheckResult
|
||||
}
|
||||
|
||||
type identityContextKey struct{}
|
||||
|
||||
var errNoSecContext = ipn.NewAccessDeniedError("security context not available")
|
||||
|
||||
// RequestIdentity returns a user identity associated with ctx,
|
||||
// or an error if the context does not carry a user's identity.
|
||||
func RequestIdentity(ctx context.Context) (Identity, error) {
|
||||
switch v := ctx.Value(identityContextKey{}).(type) {
|
||||
case Identity:
|
||||
return v, nil
|
||||
case error:
|
||||
return nil, v
|
||||
case nil:
|
||||
return nil, errNoSecContext
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
// ContextWithConnIdentity returns a new context that carries the identity of the user
|
||||
// owning the other end of the connection.
|
||||
func ContextWithConnIdentity(ctx context.Context, logf logger.Logf, c net.Conn) context.Context {
|
||||
ci, err := GetConnIdentity(logf, c)
|
||||
if err != nil {
|
||||
return context.WithValue(ctx, identityContextKey{}, err)
|
||||
}
|
||||
return context.WithValue(ctx, identityContextKey{}, ci)
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
var allGOOSes = []string{"linux", "darwin", "windows", "freebsd"}
|
||||
|
||||
type accessTest[Access ~uint32] struct {
|
||||
name string
|
||||
geese []string
|
||||
requestAccess []Access
|
||||
isLocalAdmin bool
|
||||
wantAllow bool
|
||||
}
|
||||
|
||||
func TestServeAccess(t *testing.T) {
|
||||
tests := []accessTest[ProfileAccess]{
|
||||
{
|
||||
name: "read-serve-not-admin",
|
||||
geese: allGOOSes,
|
||||
requestAccess: []ProfileAccess{ReadServe},
|
||||
isLocalAdmin: false,
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "change-serve-not-admin",
|
||||
geese: []string{"windows"},
|
||||
requestAccess: []ProfileAccess{ChangeServe},
|
||||
isLocalAdmin: false,
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "change-serve-not-admin",
|
||||
geese: []string{"linux", "darwin", "freebsd"},
|
||||
requestAccess: []ProfileAccess{ChangeServe},
|
||||
isLocalAdmin: false,
|
||||
wantAllow: false,
|
||||
},
|
||||
{
|
||||
name: "serve-path-not-admin",
|
||||
geese: allGOOSes,
|
||||
requestAccess: []ProfileAccess{ServePath},
|
||||
isLocalAdmin: false,
|
||||
wantAllow: false,
|
||||
},
|
||||
{
|
||||
name: "serve-path-admin",
|
||||
geese: allGOOSes,
|
||||
requestAccess: []ProfileAccess{ServePath},
|
||||
isLocalAdmin: true,
|
||||
wantAllow: true,
|
||||
},
|
||||
}
|
||||
runProfileAccessTests(t, tests)
|
||||
}
|
||||
|
||||
func runDeviceAccessTests(t *testing.T, tests []accessTest[DeviceAccess]) {
|
||||
t.Helper()
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, goos := range tt.geese {
|
||||
user := NewTestIdentityWithGOOS(goos, "test", tt.isLocalAdmin)
|
||||
for _, access := range tt.requestAccess {
|
||||
testName := goos + "-" + tt.name + "-" + access.String()
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
res := user.CheckAccess(access)
|
||||
if res.Allowed() != tt.wantAllow {
|
||||
t.Errorf("got result = %v, want allow %v", res, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runProfileAccessTests(t *testing.T, tests []accessTest[ProfileAccess]) {
|
||||
t.Helper()
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, goos := range tt.geese {
|
||||
user := NewTestIdentityWithGOOS(goos, "test", tt.isLocalAdmin)
|
||||
profile := &ipn.LoginProfile{LocalUserID: user.UserID()}
|
||||
prefs := func() (ipn.PrefsView, error) { return ipn.NewPrefs().View(), nil }
|
||||
|
||||
for _, access := range tt.requestAccess {
|
||||
testName := goos + "-" + tt.name + "-" + access.String()
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
res := user.CheckProfileAccess(profile.View(), prefs, access)
|
||||
if res.Allowed() != tt.wantAllow {
|
||||
t.Errorf("got result = %v, want allow %v", res, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +1,22 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package ipnauth controls access to the LocalAPI.
|
||||
// Package ipnauth controls access to the LocalAPI and LocalBackend.
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/tailscale/peercred"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/groupmember"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// ErrNotImplemented is returned by ConnIdentity.WindowsToken when it is not
|
||||
|
@ -153,57 +147,6 @@ func (ci *ConnIdentity) IsReadonlyConn(operatorUID string, logf logger.Logf) boo
|
|||
// 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)
|
||||
user := &unixIdentity{goos: runtime.GOOS, creds: ci.creds}
|
||||
return !user.isPrivileged(func() string { return operatorUID }, logf)
|
||||
}
|
||||
|
|
|
@ -9,9 +9,19 @@ import (
|
|||
"net"
|
||||
|
||||
"github.com/tailscale/peercred"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// GetIdentity extracts the identity information from the connection
|
||||
// based on the user who owns the other end of the connection.
|
||||
// TODO(nickkhyl): rename this to GetConnIdentity once we no longer need
|
||||
// the original GetConnIdentity.
|
||||
func GetIdentity(c net.Conn) (ci Identity, err error) {
|
||||
creds, _ := peercred.Get(c)
|
||||
return &unixIdentity{goos: envknob.GOOS(), creds: creds}, nil
|
||||
}
|
||||
|
||||
// GetConnIdentity extracts the identity information from the connection
|
||||
// based on the user who owns the other end of the connection.
|
||||
// and couldn't. The returned connIdentity has NotWindows set to true.
|
||||
|
|
|
@ -14,8 +14,30 @@ import (
|
|||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/util/winutil/winenv"
|
||||
)
|
||||
|
||||
// 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.
|
||||
// TODO(nickkhyl): rename this to GetConnIdentity once we no longer need
|
||||
// the original GetConnIdentity.
|
||||
func GetIdentity(c net.Conn) (ci Identity, err error) {
|
||||
wcc, ok := c.(*safesocket.WindowsClientConn)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not a WindowsClientConn: %T", c)
|
||||
}
|
||||
tok, err := windowsClientToken(wcc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newWindowsIdentity(tok, currentWindowsEnvironment()), nil
|
||||
}
|
||||
|
||||
func currentWindowsEnvironment() WindowsEnvironment {
|
||||
return WindowsEnvironment{IsManaged: winenv.IsManaged(), IsServer: winenv.IsWindowsServer()}
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
@ -168,7 +190,12 @@ func (ci *ConnIdentity) WindowsToken() (WindowsToken, error) {
|
|||
if wcc, ok = ci.conn.(*safesocket.WindowsClientConn); !ok {
|
||||
return nil, fmt.Errorf("not a WindowsClientConn: %T", ci.conn)
|
||||
}
|
||||
return windowsClientToken(wcc)
|
||||
}
|
||||
|
||||
// windowsClientToken returns the WindowsToken representing the security context
|
||||
// of the connection's client.
|
||||
func windowsClientToken(wcc *safesocket.WindowsClientConn) (WindowsToken, error) {
|
||||
// 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
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
var (
|
||||
unixGOOSes = []string{"linux", "darwin", "freebsd"}
|
||||
otherGOOSes = []string{"js"}
|
||||
)
|
||||
|
||||
func TestDeviceAccessUnix(t *testing.T) {
|
||||
tests := []accessTest[DeviceAccess]{
|
||||
{
|
||||
name: "allow-read-admin",
|
||||
geese: unixGOOSes,
|
||||
requestAccess: []DeviceAccess{ReadDeviceStatus, GenerateBugReport},
|
||||
isLocalAdmin: true,
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-read-non-admin",
|
||||
geese: unixGOOSes,
|
||||
requestAccess: []DeviceAccess{ReadDeviceStatus, GenerateBugReport},
|
||||
isLocalAdmin: false,
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-non-read-non-admin",
|
||||
geese: unixGOOSes,
|
||||
requestAccess: []DeviceAccess{^(ReadDeviceStatus | GenerateBugReport)},
|
||||
isLocalAdmin: false,
|
||||
wantAllow: false,
|
||||
},
|
||||
{
|
||||
name: "allow-all-access-admin",
|
||||
geese: unixGOOSes,
|
||||
requestAccess: []DeviceAccess{UnrestrictedDeviceAccess},
|
||||
isLocalAdmin: true,
|
||||
wantAllow: true,
|
||||
},
|
||||
}
|
||||
runDeviceAccessTests(t, tests)
|
||||
}
|
||||
|
||||
func TestDeviceAccessOther(t *testing.T) {
|
||||
tests := []accessTest[DeviceAccess]{
|
||||
{
|
||||
name: "allow-all-access-admin",
|
||||
geese: otherGOOSes,
|
||||
requestAccess: []DeviceAccess{UnrestrictedDeviceAccess},
|
||||
isLocalAdmin: true,
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-all-access-non-admin",
|
||||
geese: otherGOOSes,
|
||||
requestAccess: []DeviceAccess{UnrestrictedDeviceAccess},
|
||||
isLocalAdmin: false,
|
||||
wantAllow: true,
|
||||
},
|
||||
}
|
||||
runDeviceAccessTests(t, tests)
|
||||
}
|
||||
|
||||
func TestProfileAccessUnix(t *testing.T) {
|
||||
tests := []accessTest[ProfileAccess]{
|
||||
{
|
||||
name: "allow-read-admin",
|
||||
geese: unixGOOSes,
|
||||
requestAccess: []ProfileAccess{ReadProfileInfo, ReadPrefs, ReadServe, ListPeers},
|
||||
isLocalAdmin: true,
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-read-non-admin",
|
||||
geese: unixGOOSes,
|
||||
requestAccess: []ProfileAccess{ReadProfileInfo, ReadPrefs, ReadServe, ListPeers},
|
||||
isLocalAdmin: false,
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-non-read-non-admin",
|
||||
geese: unixGOOSes,
|
||||
requestAccess: []ProfileAccess{^(ReadProfileInfo | ReadPrefs | ReadServe | ListPeers)},
|
||||
isLocalAdmin: false,
|
||||
wantAllow: false,
|
||||
},
|
||||
{
|
||||
name: "allow-use-profile-admin",
|
||||
geese: unixGOOSes,
|
||||
requestAccess: []ProfileAccess{Connect, Disconnect, DeleteProfile, ReauthProfile, ChangePrefs, ChangeExitNode},
|
||||
isLocalAdmin: true,
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-use-profile-non-admin",
|
||||
geese: unixGOOSes,
|
||||
requestAccess: []ProfileAccess{Connect, Disconnect, DeleteProfile, ReauthProfile, ChangePrefs, ChangeExitNode},
|
||||
isLocalAdmin: false,
|
||||
wantAllow: false,
|
||||
},
|
||||
{
|
||||
name: "allow-all-access-admin",
|
||||
geese: unixGOOSes,
|
||||
requestAccess: []ProfileAccess{UnrestrictedProfileAccess},
|
||||
isLocalAdmin: true,
|
||||
wantAllow: true,
|
||||
},
|
||||
}
|
||||
runProfileAccessTests(t, tests)
|
||||
}
|
||||
|
||||
func TestFetchCertsAccessUnix(t *testing.T) {
|
||||
for _, goos := range unixGOOSes {
|
||||
t.Run(goos, func(t *testing.T) {
|
||||
user := NewTestIdentityWithGOOS(goos, "user", false)
|
||||
uid := *user.(*unixIdentity).forceForTest.uid
|
||||
envknob.Setenv("TS_PERMIT_CERT_UID", uid)
|
||||
defer envknob.Setenv("TS_PERMIT_CERT_UID", "")
|
||||
|
||||
profile := ipn.LoginProfile{}
|
||||
res := user.CheckProfileAccess(profile.View(), ipn.PrefsGetterFor(ipn.NewPrefs().View()), FetchCerts)
|
||||
if !res.Allowed() {
|
||||
t.Errorf("got result = %v, want allow", res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileAccessOther(t *testing.T) {
|
||||
tests := []accessTest[ProfileAccess]{
|
||||
{
|
||||
name: "allow-all-access-admin",
|
||||
geese: otherGOOSes,
|
||||
requestAccess: []ProfileAccess{UnrestrictedProfileAccess},
|
||||
isLocalAdmin: true,
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-all-access-non-admin",
|
||||
geese: otherGOOSes,
|
||||
requestAccess: []ProfileAccess{UnrestrictedProfileAccess},
|
||||
isLocalAdmin: false,
|
||||
wantAllow: true,
|
||||
},
|
||||
}
|
||||
runProfileAccessTests(t, tests)
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import "tailscale.com/ipn"
|
||||
|
||||
// Self is a caller identity that represents the tailscaled itself and therefore has unlimited access.
|
||||
//
|
||||
// It's typically used for operations performed by tailscaled on its own,
|
||||
// or upon a request from the control plane, rather on behalf of a specific user.
|
||||
var Self Identity = unrestricted{}
|
||||
|
||||
// IsUnrestricted reports whether the specified identity has unrestricted access to the LocalBackend,
|
||||
// including all user profiles and preferences, serving as a performance optimization
|
||||
// and ensuring that tailscaled operates correctly, unaffected by Group Policy, MDM, or similar restrictions.
|
||||
func IsUnrestricted(identity Identity) bool {
|
||||
if _, ok := identity.(unrestricted); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type unrestricted struct {
|
||||
}
|
||||
|
||||
// UserID returns an empty string.
|
||||
func (unrestricted) UserID() ipn.WindowsUserID {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Username returns an empty string.
|
||||
func (unrestricted) Username() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// CheckAccess always allows the requested access.
|
||||
func (unrestricted) CheckAccess(desired DeviceAccess) AccessCheckResult {
|
||||
return AllowAccess()
|
||||
}
|
||||
|
||||
// CheckProfileAccess always allows the requested profile access.
|
||||
func (unrestricted) CheckProfileAccess(profile ipn.LoginProfileView, prefs ipn.PrefsGetter, requested ProfileAccess) AccessCheckResult {
|
||||
return AllowAccess()
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
// TestIdentity is an identity with a predefined UID, Name and access rights.
|
||||
// It should only be used for testing purposes, and allows external packages
|
||||
// to test against a specific set of access rights.
|
||||
type TestIdentity struct {
|
||||
UID string // UID is an OS-specific user id of the test user.
|
||||
Name string // Name is the login name of the test user.
|
||||
DeviceAccess DeviceAccess // DeviceAccess is the test user's access rights on the device.
|
||||
ProfileAccess ProfileAccess // ProfileAccess is the test user's access rights to Tailscale profiles.
|
||||
AccessOthersProfiles bool // AccessOthersProfiles indicates whether the test user can access all profiles, regardless of their ownership.
|
||||
}
|
||||
|
||||
var (
|
||||
// TestAdmin is a test identity that has unrestricted access to the device
|
||||
// and all Tailscale profiles on it. It should only be used for testing purposes.
|
||||
TestAdmin = &TestIdentity{
|
||||
Name: "admin",
|
||||
DeviceAccess: UnrestrictedDeviceAccess,
|
||||
ProfileAccess: UnrestrictedProfileAccess,
|
||||
AccessOthersProfiles: true,
|
||||
}
|
||||
)
|
||||
|
||||
// NewTestIdentityWithGOOS returns a new test identity for the given GOOS,
|
||||
// with the specified user name and the isAdmin flag indicating
|
||||
// whether the user has administrative access on the local machine.
|
||||
//
|
||||
// When goos is windows, it returns an identity representing an elevated admin
|
||||
// or a regular user account on a non-managed non-server environment. Callers
|
||||
// that require fine-grained control over user's privileges or environment
|
||||
// should use NewWindowsIdentity instead.
|
||||
func NewTestIdentityWithGOOS(goos, name string, isAdmin bool) Identity {
|
||||
if goos == "windows" {
|
||||
token := &testToken{
|
||||
SID: ipn.WindowsUserID(name),
|
||||
Name: name,
|
||||
Admin: isAdmin,
|
||||
Elevated: isAdmin,
|
||||
}
|
||||
return newWindowsIdentity(token, WindowsEnvironment{})
|
||||
}
|
||||
identity := &unixIdentity{goos: goos}
|
||||
identity.forceForTest.username = ptr.To(name)
|
||||
identity.forceForTest.isAdmin = ptr.To(isAdmin)
|
||||
if isAdmin {
|
||||
identity.forceForTest.uid = ptr.To("0")
|
||||
} else {
|
||||
identity.forceForTest.uid = ptr.To("1000")
|
||||
}
|
||||
return identity
|
||||
}
|
||||
|
||||
// NewTestIdentity is like NewTestIdentityWithGOOS, but returns a test identity
|
||||
// for the current platform.
|
||||
func NewTestIdentity(name string, isAdmin bool) Identity {
|
||||
return NewTestIdentityWithGOOS(runtime.GOOS, name, isAdmin)
|
||||
}
|
||||
|
||||
// UserID returns t.ID.
|
||||
func (t *TestIdentity) UserID() ipn.WindowsUserID {
|
||||
return ipn.WindowsUserID(t.UID)
|
||||
}
|
||||
|
||||
// Username returns t.Name.
|
||||
func (t *TestIdentity) Username() (string, error) {
|
||||
return t.Name, nil
|
||||
}
|
||||
|
||||
// CheckAccess reports whether the requested access is allowed or denied
|
||||
// based on t.DeviceAccess.
|
||||
func (t *TestIdentity) CheckAccess(requested DeviceAccess) AccessCheckResult {
|
||||
if requested&t.DeviceAccess == requested {
|
||||
return AllowAccess()
|
||||
}
|
||||
return DenyAccess(errors.New("access denied"))
|
||||
}
|
||||
|
||||
// CheckProfileAccess reports whether the requested profile access is allowed or denied
|
||||
// based on t.ProfileAccess.
|
||||
func (t *TestIdentity) CheckProfileAccess(profile ipn.LoginProfileView, prefs ipn.PrefsGetter, requested ProfileAccess) AccessCheckResult {
|
||||
if !t.AccessOthersProfiles && profile.LocalUserID() != t.UserID() && profile.LocalUserID() != "" {
|
||||
return DenyAccess(errors.New("the requested profile is owned by another user"))
|
||||
}
|
||||
if t.ProfileAccess&requested == requested {
|
||||
return AllowAccess()
|
||||
}
|
||||
return DenyAccess(errors.New("access denied"))
|
||||
}
|
||||
|
||||
// testToken implements WindowsToken and should only be used for testing purposes.
|
||||
type testToken struct {
|
||||
SID ipn.WindowsUserID
|
||||
Name string
|
||||
Admin, Elevated bool
|
||||
LocalSystem bool
|
||||
}
|
||||
|
||||
// UID returns t's Security Identifier (SID).
|
||||
func (t *testToken) UID() (ipn.WindowsUserID, error) {
|
||||
return t.SID, nil
|
||||
}
|
||||
|
||||
// Username returns t's username.
|
||||
func (t *testToken) Username() (string, error) {
|
||||
return t.Name, nil
|
||||
}
|
||||
|
||||
// IsAdministrator reports whether t represents an admin's,
|
||||
// but not necessarily elevated, security context.
|
||||
func (t *testToken) IsAdministrator() (bool, error) {
|
||||
return t.Admin, nil
|
||||
}
|
||||
|
||||
// IsElevated reports whether t represents an elevated security context,
|
||||
// such as of LocalSystem or "Run as administrator".
|
||||
func (t *testToken) IsElevated() bool {
|
||||
return t.Elevated || t.IsLocalSystem()
|
||||
}
|
||||
|
||||
// IsLocalSystem reports whether t represents a LocalSystem's security context.
|
||||
func (t *testToken) IsLocalSystem() bool {
|
||||
return t.LocalSystem
|
||||
}
|
||||
|
||||
// UserDir is not implemented.
|
||||
func (t *testToken) UserDir(folderID string) (string, error) {
|
||||
return "", errors.New("Not implemented")
|
||||
}
|
||||
|
||||
// Close is a no-op.
|
||||
func (t *testToken) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// EqualUIDs reports whether two WindowsTokens have the same UIDs.
|
||||
func (t *testToken) EqualUIDs(other WindowsToken) bool {
|
||||
if t != nil && other == nil || t == nil && other != nil {
|
||||
return false
|
||||
}
|
||||
ot, ok := other.(*testToken)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return t == ot || t.SID == ot.SID
|
||||
}
|
||||
|
||||
// IsUID reports whether t has the specified UID.
|
||||
func (t *testToken) IsUID(uid ipn.WindowsUserID) bool {
|
||||
return t.SID == uid
|
||||
}
|
|
@ -0,0 +1,322 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/peercred"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/groupmember"
|
||||
"tailscale.com/util/osuser"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var (
|
||||
errMustBeRootOrOperator = ipn.NewAccessDeniedError("must be root or an operator")
|
||||
errMustBeRootOrSudoerOperator = ipn.NewAccessDeniedError("must be root, or be an operator and able to run 'sudo tailscale' to serve a path")
|
||||
)
|
||||
|
||||
var _ Identity = (*unixIdentity)(nil)
|
||||
|
||||
// unixIdentity is a non-Windows user identity.
|
||||
type unixIdentity struct {
|
||||
goos string
|
||||
creds *peercred.Creds // or nil
|
||||
|
||||
// forceForTest are fields used exclusively for testing purposes.
|
||||
// Only non-nil values within this struct are used.
|
||||
forceForTest struct {
|
||||
uid, username *string
|
||||
isAdmin *bool
|
||||
}
|
||||
}
|
||||
|
||||
// UserID returns the empty string; it exists only to implement ipnauth.Identity.
|
||||
func (id *unixIdentity) UserID() ipn.WindowsUserID {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Username returns the user name associated with the identity.
|
||||
func (id *unixIdentity) Username() (string, error) {
|
||||
if id.forceForTest.username != nil {
|
||||
return *id.forceForTest.username, nil
|
||||
}
|
||||
switch id.goos {
|
||||
case "darwin", "linux":
|
||||
uid, ok := id.creds.UserID()
|
||||
if !ok {
|
||||
return "", errors.New("missing user ID")
|
||||
}
|
||||
u, err := osuser.LookupByUID(uid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("lookup user: %w", err)
|
||||
}
|
||||
return u.Username, nil
|
||||
default:
|
||||
return "", errors.New("unsupported OS")
|
||||
}
|
||||
}
|
||||
|
||||
// CheckAccess reports whether user is allowed or denied the requested access.
|
||||
func (id *unixIdentity) CheckAccess(requested DeviceAccess) AccessCheckResult {
|
||||
if id.isPrivileged(nil, logger.Discard) {
|
||||
return AllowAccess()
|
||||
}
|
||||
allowed := GenerateBugReport | ReadDeviceStatus | InstallUpdates
|
||||
if requested&^allowed == 0 {
|
||||
return AllowAccess()
|
||||
}
|
||||
return DenyAccess(errMustBeRootOrOperator)
|
||||
}
|
||||
|
||||
// CheckProfileAccess reports whether user is allowed or denied the requested access to the profile.
|
||||
func (id *unixIdentity) CheckProfileAccess(profile ipn.LoginProfileView, prefs ipn.PrefsGetter, requested ProfileAccess) AccessCheckResult {
|
||||
operatorUID := operatorUIDFromPrefs(prefs)
|
||||
checker := newAccessChecker(requested)
|
||||
// Deny access immediately if ServePath was requested, unless the user is root,
|
||||
// or both a sudoer and an operator.
|
||||
if checker.remaining()&ServePath != 0 {
|
||||
if !id.canServePath(operatorUID) {
|
||||
return checker.deny(ServePath, errMustBeRootOrSudoerOperator)
|
||||
}
|
||||
if res := checker.grant(ServePath); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
// Grant non-privileges access to everyone.
|
||||
if res := checker.grant(ReadProfileInfo | ListPeers | ReadPrefs | ReadServe); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
// Grant all access to root, admins and the operator.
|
||||
if id.isPrivileged(operatorUID, logger.Discard) {
|
||||
if res := checker.grant(UnrestrictedProfileAccess); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
// Grant cert fetching access to the TS_PERMIT_CERT_UID user.
|
||||
if id.canFetchCerts() {
|
||||
if res := checker.grant(FetchCerts); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
// Deny any other access.
|
||||
return DenyAccess(errMustBeRootOrOperator)
|
||||
}
|
||||
|
||||
func operatorUIDFromPrefs(prefs ipn.PrefsGetter) func() string {
|
||||
return func() string {
|
||||
prefs, err := prefs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
opUserName := prefs.OperatorUser()
|
||||
if opUserName == "" {
|
||||
return ""
|
||||
}
|
||||
u, err := user.Lookup(opUserName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.Uid
|
||||
}
|
||||
}
|
||||
|
||||
// isPrivileged reports whether the identity should be considered privileged,
|
||||
// meaning it's allowed to change the state of the node and access sensitive information.
|
||||
func (id *unixIdentity) isPrivileged(operatorUID func() string, logf logger.Logf) bool {
|
||||
if logf == nil {
|
||||
logf = func(format string, args ...any) {
|
||||
fmt.Printf("%s", fmt.Sprintf(format, args...))
|
||||
}
|
||||
}
|
||||
const ro, rw = false, true
|
||||
if !safesocket.GOOSUsesPeerCreds(id.goos) {
|
||||
return rw
|
||||
}
|
||||
if id.forceForTest.isAdmin != nil {
|
||||
return *id.forceForTest.isAdmin
|
||||
}
|
||||
creds := id.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 != nil {
|
||||
if operatorUID := operatorUID(); 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
|
||||
}
|
||||
|
||||
// canFetchCerts reports whether id is allowed to fetch HTTPS
|
||||
// certs from this server when it wouldn't otherwise be able to.
|
||||
//
|
||||
// That is, this reports whether id should grant additional
|
||||
// capabilities over what the conn would otherwise be able to do.
|
||||
//
|
||||
// For now this only returns true on Unix machines when
|
||||
// TS_PERMIT_CERT_UID is set the to the userid of the peer
|
||||
// connection. It's intended to give your non-root webserver access
|
||||
// (www-data, caddy, nginx, etc) to certs.
|
||||
func (id *unixIdentity) canFetchCerts() bool {
|
||||
var uid string
|
||||
var hasUID bool
|
||||
if id.forceForTest.uid != nil {
|
||||
uid, hasUID = *id.forceForTest.uid, true
|
||||
} else if id.creds != nil {
|
||||
uid, hasUID = id.creds.UserID()
|
||||
}
|
||||
if hasUID && uid == userIDFromString(envknob.String("TS_PERMIT_CERT_UID")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (id *unixIdentity) canServePath(operatorUID func() string) bool {
|
||||
switch id.goos {
|
||||
case "linux", "darwin":
|
||||
// continue
|
||||
case "windows":
|
||||
panic("unreachable")
|
||||
default:
|
||||
return id.isPrivileged(operatorUID, logger.Discard)
|
||||
}
|
||||
// Only check for local admin on tailscaled-on-mac (based on "sudo"
|
||||
// permissions). On sandboxed variants (MacSys and AppStore), tailscaled
|
||||
// cannot serve files outside of the sandbox and this check is not
|
||||
// relevant.
|
||||
if id.goos == "darwin" && version.IsSandboxedMacOS() {
|
||||
return true
|
||||
}
|
||||
|
||||
return id.isLocalAdminForServe(operatorUID)
|
||||
}
|
||||
|
||||
// isLocalAdminForServe reports whether the identity representing a 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 (id *unixIdentity) isLocalAdminForServe(operatorUID func() string) bool {
|
||||
if id.forceForTest.isAdmin != nil {
|
||||
return *id.forceForTest.isAdmin
|
||||
}
|
||||
switch id.goos {
|
||||
case "darwin":
|
||||
// Unknown, or at least unchecked on sandboxed macOS variants. Err on
|
||||
// the side of less permissions.
|
||||
//
|
||||
// canSetServePath 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":
|
||||
uid, ok := id.creds.UserID()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// root is always admin.
|
||||
if uid == "0" {
|
||||
return true
|
||||
}
|
||||
// if non-root, must be operator AND able to execute "sudo tailscale".
|
||||
if operatorUID := operatorUID(); operatorUID != "" && uid != operatorUID {
|
||||
return false
|
||||
}
|
||||
u, err := osuser.LookupByUID(uid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Short timeout just in case sudo hangs for some reason.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := exec.CommandContext(ctx, "sudo", "--other-user="+u.Name, "--list", "tailscale").Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
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, errors.New("no system admin group found")
|
||||
}
|
||||
return groupmember.IsMemberOfGroup(adminGroup, u.Username)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func userIDFromString(v string) string {
|
||||
if v == "" || isAllDigit(v) {
|
||||
return v
|
||||
}
|
||||
u, err := user.Lookup(v)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.Uid
|
||||
}
|
||||
|
||||
func isAllDigit(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if b := s[i]; b < '0' || b > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
var _ Identity = (*windowsIdentity)(nil)
|
||||
|
||||
// windowsIdentity represents identity of a Windows user.
|
||||
type windowsIdentity struct {
|
||||
tok WindowsToken
|
||||
env WindowsEnvironment
|
||||
}
|
||||
|
||||
// newWindowsIdentity returns a new WindowsIdentity with the specified token and environment.
|
||||
func newWindowsIdentity(tok WindowsToken, env WindowsEnvironment) *windowsIdentity {
|
||||
identity := &windowsIdentity{tok, env}
|
||||
runtime.SetFinalizer(identity, func(i *windowsIdentity) { i.Close() })
|
||||
return identity
|
||||
}
|
||||
|
||||
// UserID returns SID of a Windows user account.
|
||||
func (wi *windowsIdentity) UserID() ipn.WindowsUserID {
|
||||
if uid, err := wi.tok.UID(); err == nil {
|
||||
return uid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// UserID returns SID of a Windows user account.
|
||||
func (wi *windowsIdentity) Username() (string, error) {
|
||||
return wi.tok.Username()
|
||||
}
|
||||
|
||||
// CheckAccess reports whether wi is allowed or denied the requested access.
|
||||
func (wi *windowsIdentity) CheckAccess(requested DeviceAccess) AccessCheckResult {
|
||||
checker := newAccessChecker(requested)
|
||||
|
||||
// Debug and ResetAllProfiles access rights can only be granted to elevated admins.
|
||||
if res := checker.mustGrant(DeleteAllProfiles|Debug, wi.checkElevatedAdmin); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
|
||||
if wi.env.IsServer {
|
||||
// Only admins can create new profiles or install client updates on Windows Server devices.
|
||||
// However, we should allow these operations from non-elevated contexts (e.g GUI).
|
||||
if res := checker.tryGrant(CreateProfile|InstallUpdates, wi.checkAdmin); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
} else {
|
||||
// But any user should be able to create a profile or initiate an update on non-server (e.g. Windows 10/11) devices.
|
||||
if res := checker.grant(CreateProfile | InstallUpdates); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
// Unconditionally grant ReadStatus and GenerateBugReport to all authenticated users, regardless of the environment.
|
||||
if res := checker.grant(ReadDeviceStatus | GenerateBugReport); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
|
||||
// Grant unrestricted device access to elevated admins.
|
||||
if res := checker.tryGrant(UnrestrictedDeviceAccess, wi.checkElevatedAdmin); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
|
||||
// Returns the final access check result, implicitly denying any access rights that have not been explicitly granted.
|
||||
return checker.result()
|
||||
}
|
||||
|
||||
// CheckProfileAccess reports whether wi is allowed or denied the requested access to the profile.
|
||||
func (wi *windowsIdentity) CheckProfileAccess(profile ipn.LoginProfileView, prefs ipn.PrefsGetter, requested ProfileAccess) AccessCheckResult {
|
||||
checker := newAccessChecker(requested)
|
||||
|
||||
// To avoid privilege escalation, the ServePath access right must only be granted to elevated admins.
|
||||
// The access request will be immediately denied if wi is not an elevated admin.
|
||||
if res := checker.mustGrant(ServePath, wi.checkElevatedAdmin); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
|
||||
// Profile owners have unrestricted access to their own profiles.
|
||||
if wi.isProfileOwner(profile) {
|
||||
if res := checker.grant(UnrestrictedProfileAccess); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
if isProfileShared(profile, prefs) {
|
||||
// Allow all users to read basic profile info (e.g. profile and tailnet name)
|
||||
// and list network device for shared profiles.
|
||||
// Profile is considered shared if it has unattended mode enabled
|
||||
// and/or is not owned by a specific user (e.g. created via MDM/GP).
|
||||
sharedProfileRights := ReadProfileInfo | ListPeers
|
||||
if !wi.env.IsServer && !wi.env.IsManaged {
|
||||
// Additionally, on non-managed Windows client devices we should allow users to
|
||||
// connect / disconnect, read preferences and select exit nodes.
|
||||
sharedProfileRights |= Connect | Disconnect | ReadPrefs | ChangeExitNode
|
||||
}
|
||||
if res := checker.grant(sharedProfileRights); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
if !wi.env.IsServer && !isProfileEnforced(profile, prefs) {
|
||||
// Allow any user to disconnect from non-enforced Tailnets on non-Windows Server devices.
|
||||
// TODO(nickkhyl): automatically disconnect from the current Tailnet
|
||||
// when a different user logs in or unlocks their Windows session,
|
||||
// unless the unattended mode is enabled. But in the meantime, we should allow users
|
||||
// to disconnect themselves.
|
||||
if res := checker.grant(Disconnect); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
if isAdmin, _ := wi.tok.IsAdministrator(); isAdmin {
|
||||
// Allow local admins to disconnect from any tailnet.
|
||||
localAdminRights := Disconnect
|
||||
if wi.tok.IsElevated() {
|
||||
// Allow elevated admins unrestricted access to all local profiles,
|
||||
// except for reading private keys.
|
||||
localAdminRights |= UnrestrictedProfileAccess & ^ReadPrivateKeys
|
||||
}
|
||||
if isProfileShared(profile, prefs) {
|
||||
// Allow all admins unrestricted access to shared profiles,
|
||||
// except for reading private keys.
|
||||
// This is to allow shared profiles created by others (admins or users)
|
||||
// to be managed from the GUI client.
|
||||
localAdminRights |= UnrestrictedProfileAccess & ^ReadPrivateKeys
|
||||
}
|
||||
if res := checker.grant(localAdminRights); res.HasResult() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
return checker.result()
|
||||
}
|
||||
|
||||
// Close implements io.Closer by releasing resources associated with the Windows user identity.
|
||||
func (wi *windowsIdentity) Close() error {
|
||||
if wi == nil || wi.tok == nil {
|
||||
return nil
|
||||
}
|
||||
if err := wi.tok.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.SetFinalizer(wi, nil)
|
||||
wi.tok = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wi *windowsIdentity) checkAdmin() error {
|
||||
isAdmin, err := wi.tok.IsAdministrator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isAdmin {
|
||||
return errors.New("the requested operation requires local admin rights")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wi *windowsIdentity) checkElevatedAdmin() error {
|
||||
if !wi.tok.IsElevated() {
|
||||
return errors.New("the requested operation requires elevation")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wi *windowsIdentity) isProfileOwner(profile ipn.LoginProfileView) bool {
|
||||
return wi.tok.IsUID(profile.LocalUserID())
|
||||
}
|
||||
|
||||
// isProfileShared reports whether the specified profile is considered shared,
|
||||
// meaning that all local users should have at least ReadProfileInfo and ListPeers
|
||||
// access to it, but may be granted additional access rights based on the environment
|
||||
// and their role on the device.
|
||||
func isProfileShared(profile ipn.LoginProfileView, prefs ipn.PrefsGetter) bool {
|
||||
if profile.LocalUserID() == "" {
|
||||
// Profiles created as LocalSystem (e.g. via MDM) can be used by everyone on the device.
|
||||
return true
|
||||
}
|
||||
if prefs, err := prefs(); err == nil {
|
||||
// Profiles that have unattended mode enabled can be used by everyone on the device.
|
||||
return prefs.ForceDaemon()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isProfileEnforced(ipn.LoginProfileView, ipn.PrefsGetter) bool {
|
||||
// TODO(nickkhyl): allow to mark profiles as enforced to prevent
|
||||
// regular users from disconnecting.
|
||||
return false
|
||||
}
|
||||
|
||||
// WindowsEnvironment describes the current Windows environment.
|
||||
type WindowsEnvironment struct {
|
||||
IsServer bool // whether running on a server edition of Windows
|
||||
IsManaged bool // whether the device is managed (domain-joined or MDM-enrolled)
|
||||
}
|
||||
|
||||
// String returns a string representation of the environment.
|
||||
func (env WindowsEnvironment) String() string {
|
||||
switch {
|
||||
case env.IsManaged && env.IsServer:
|
||||
return "Managed Server"
|
||||
case env.IsManaged && !env.IsServer:
|
||||
return "Managed Client"
|
||||
case !env.IsManaged && env.IsServer:
|
||||
return "Non-Managed Server"
|
||||
case !env.IsManaged && !env.IsServer:
|
||||
return "Non-Managed Client"
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
var (
|
||||
winServerEnvs = []WindowsEnvironment{
|
||||
{IsServer: true, IsManaged: false},
|
||||
{IsServer: true, IsManaged: true},
|
||||
}
|
||||
|
||||
winClientEnvs = []WindowsEnvironment{
|
||||
{IsServer: false, IsManaged: false},
|
||||
{IsServer: false, IsManaged: true},
|
||||
}
|
||||
|
||||
winManagedEnvs = []WindowsEnvironment{
|
||||
{IsServer: false, IsManaged: true},
|
||||
{IsServer: true, IsManaged: true},
|
||||
}
|
||||
|
||||
winAllEnvs = []WindowsEnvironment{
|
||||
{IsServer: false, IsManaged: false},
|
||||
{IsServer: false, IsManaged: true},
|
||||
{IsServer: true, IsManaged: false},
|
||||
{IsServer: true, IsManaged: true},
|
||||
}
|
||||
)
|
||||
|
||||
func TestDeviceAccessWindows(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
requestAccess []DeviceAccess
|
||||
envs []WindowsEnvironment
|
||||
tok WindowsToken
|
||||
wantAllow bool
|
||||
}{
|
||||
{
|
||||
name: "allow-all-access-elevated-admin",
|
||||
requestAccess: []DeviceAccess{UnrestrictedDeviceAccess},
|
||||
envs: winAllEnvs,
|
||||
tok: &testToken{Admin: true, Elevated: true},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-create-profile-non-elevated-admin",
|
||||
requestAccess: []DeviceAccess{CreateProfile},
|
||||
envs: winAllEnvs,
|
||||
tok: &testToken{Admin: true, Elevated: false},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-install-updates-non-elevated-admin",
|
||||
requestAccess: []DeviceAccess{InstallUpdates},
|
||||
envs: winAllEnvs,
|
||||
tok: &testToken{Admin: true, Elevated: false},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-privileged-access-non-elevated-admin",
|
||||
requestAccess: []DeviceAccess{Debug, DeleteAllProfiles},
|
||||
envs: winAllEnvs,
|
||||
tok: &testToken{Admin: true, Elevated: false},
|
||||
wantAllow: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "allow-read-access-user",
|
||||
requestAccess: []DeviceAccess{ReadDeviceStatus, GenerateBugReport},
|
||||
envs: winAllEnvs,
|
||||
tok: &testToken{Admin: false},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-privileged-access-user",
|
||||
requestAccess: []DeviceAccess{Debug, DeleteAllProfiles},
|
||||
envs: winAllEnvs,
|
||||
tok: &testToken{Admin: false},
|
||||
wantAllow: false,
|
||||
},
|
||||
{
|
||||
name: "allow-create-profile-non-server-user",
|
||||
requestAccess: []DeviceAccess{CreateProfile},
|
||||
envs: winClientEnvs,
|
||||
tok: &testToken{Admin: false},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-create-profile-server-user",
|
||||
requestAccess: []DeviceAccess{CreateProfile},
|
||||
envs: winServerEnvs,
|
||||
tok: &testToken{Admin: false},
|
||||
wantAllow: false,
|
||||
},
|
||||
{
|
||||
name: "allow-install-updates-non-server-user",
|
||||
requestAccess: []DeviceAccess{InstallUpdates},
|
||||
envs: winClientEnvs,
|
||||
tok: &testToken{Admin: false},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-install-updates-server-user",
|
||||
requestAccess: []DeviceAccess{InstallUpdates},
|
||||
envs: winServerEnvs,
|
||||
tok: &testToken{Admin: false},
|
||||
wantAllow: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, env := range tt.envs {
|
||||
user := newWindowsIdentity(tt.tok, env)
|
||||
for _, access := range tt.requestAccess {
|
||||
testName := tt.name + "-" + env.String() + "-" + access.String()
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
if res := user.CheckAccess(access); res.Allowed() != tt.wantAllow {
|
||||
t.Errorf("got result: %v, want allow: %v", res, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileAccessWindows(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tok WindowsToken
|
||||
profile ipn.LoginProfile
|
||||
prefs ipn.Prefs
|
||||
envs []WindowsEnvironment
|
||||
requestAccess []ProfileAccess
|
||||
wantAllow bool
|
||||
}{
|
||||
{
|
||||
name: "allow-users-access-to-own-profiles",
|
||||
tok: &testToken{Admin: false, SID: "User1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: "User1"},
|
||||
envs: winAllEnvs,
|
||||
requestAccess: []ProfileAccess{UnrestrictedProfileAccess & ^(ServePath)}, // ServePath requires elevated admin rights
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-users-disconnect-access-to-others-profiles-on-clients",
|
||||
tok: &testToken{Admin: false, SID: "User1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: "User2"},
|
||||
envs: winClientEnvs,
|
||||
requestAccess: []ProfileAccess{Disconnect},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-users-access-to-others-unattended-profiles-on-unmanaged-clients",
|
||||
tok: &testToken{Admin: false, SID: "User1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: "User2"},
|
||||
prefs: ipn.Prefs{ForceDaemon: true},
|
||||
envs: []WindowsEnvironment{{IsServer: false, IsManaged: false}},
|
||||
requestAccess: []ProfileAccess{ReadProfileInfo, Connect, Disconnect, ListPeers, ReadPrefs, ChangeExitNode},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-users-read-access-to-others-unattended-profiles-on-managed",
|
||||
tok: &testToken{Admin: false, SID: "User1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: "User2"},
|
||||
prefs: ipn.Prefs{ForceDaemon: true},
|
||||
envs: winManagedEnvs,
|
||||
requestAccess: []ProfileAccess{ReadProfileInfo, ListPeers},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-users-read-access-to-others-unattended-profiles-on-servers",
|
||||
tok: &testToken{Admin: false, SID: "User1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: "User2"},
|
||||
prefs: ipn.Prefs{ForceDaemon: true},
|
||||
envs: winServerEnvs,
|
||||
requestAccess: []ProfileAccess{ReadProfileInfo, ListPeers},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-users-access-to-non-unattended-others-profiles",
|
||||
tok: &testToken{Admin: false, SID: "User1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: "User2"},
|
||||
envs: winAllEnvs,
|
||||
requestAccess: []ProfileAccess{ReadProfileInfo, Connect, ListPeers, ReadPrefs, ChangePrefs, ChangeExitNode},
|
||||
wantAllow: false,
|
||||
},
|
||||
{
|
||||
name: "allow-elevated-admins-access-to-others-profiles",
|
||||
tok: &testToken{Admin: true, Elevated: true, SID: "Admin1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: "User1"},
|
||||
envs: winAllEnvs,
|
||||
requestAccess: []ProfileAccess{UnrestrictedProfileAccess & ^ReadPrivateKeys}, // ReadPrivateKeys is never allowed to others' profiles.
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-non-elevated-admins-access-to-shared-profiles",
|
||||
tok: &testToken{Admin: true, Elevated: true, SID: "Admin1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: ""},
|
||||
envs: winManagedEnvs,
|
||||
requestAccess: []ProfileAccess{UnrestrictedProfileAccess & ^(ReadPrivateKeys | ServePath)},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "allow-non-elevated-admins-access-to-unattended-profiles",
|
||||
tok: &testToken{Admin: true, Elevated: true, SID: "Admin1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: ""},
|
||||
prefs: ipn.Prefs{ForceDaemon: true},
|
||||
envs: winManagedEnvs,
|
||||
requestAccess: []ProfileAccess{UnrestrictedProfileAccess & ^(ReadPrivateKeys | ServePath)},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-non-elevated-admins-access-to-others-profiles",
|
||||
tok: &testToken{Admin: true, Elevated: false, SID: "Admin1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: "User1"},
|
||||
envs: winAllEnvs,
|
||||
requestAccess: []ProfileAccess{ReadProfileInfo, Connect, ListPeers, ReadPrefs, ChangePrefs, ChangeExitNode},
|
||||
wantAllow: false,
|
||||
},
|
||||
{
|
||||
name: "allow-elevated-admins-serve-path-for-own-profiles",
|
||||
tok: &testToken{Admin: true, Elevated: true, SID: "Admin1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: "Admin1"},
|
||||
envs: winAllEnvs,
|
||||
requestAccess: []ProfileAccess{ServePath},
|
||||
wantAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny-non-elevated-admins-serve-path-for-own-profiles",
|
||||
tok: &testToken{Admin: true, Elevated: false, SID: "Admin1"},
|
||||
profile: ipn.LoginProfile{LocalUserID: "Admin1"},
|
||||
envs: winAllEnvs,
|
||||
requestAccess: []ProfileAccess{ServePath},
|
||||
wantAllow: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, env := range tt.envs {
|
||||
user := newWindowsIdentity(tt.tok, env)
|
||||
for _, access := range tt.requestAccess {
|
||||
testName := tt.name + "-" + env.String() + "-" + access.String()
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
res := user.CheckProfileAccess(tt.profile.View(), ipn.PrefsGetterFor(tt.prefs.View()), access)
|
||||
if res.Allowed() != tt.wantAllow {
|
||||
t.Errorf("got result: %v, want allow: %v", res, tt.wantAllow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -415,7 +415,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
|||
}
|
||||
|
||||
netMon := sys.NetMon.Get()
|
||||
b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID, netMon)
|
||||
b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID, netMon, sys.HealthTracker())
|
||||
if err != nil {
|
||||
log.Printf("error setting up sockstat logger: %v", err)
|
||||
}
|
||||
|
@ -6253,6 +6253,7 @@ func mayDeref[T any](p *T) (v T) {
|
|||
}
|
||||
|
||||
var ErrNoPreferredDERP = errors.New("no preferred DERP, try again later")
|
||||
var ErrCannotSuggestExitNode = errors.New("unable to suggest an exit node, try again later")
|
||||
|
||||
// SuggestExitNode computes a suggestion based on the current netmap and last netcheck report. If
|
||||
// there are multiple equally good options, one is selected at random, so the result is not stable. To be
|
||||
|
@ -6266,6 +6267,9 @@ func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionRes
|
|||
lastReport := b.MagicConn().GetLastNetcheckReport(b.ctx)
|
||||
netMap := b.netMap
|
||||
b.mu.Unlock()
|
||||
if lastReport == nil || netMap == nil {
|
||||
return response, ErrCannotSuggestExitNode
|
||||
}
|
||||
seed := time.Now().UnixNano()
|
||||
r := rand.New(rand.NewSource(seed))
|
||||
return suggestExitNode(lastReport, netMap, r)
|
||||
|
|
13
ipn/prefs.go
13
ipn/prefs.go
|
@ -245,6 +245,19 @@ type Prefs struct {
|
|||
Persist *persist.Persist `json:"Config"`
|
||||
}
|
||||
|
||||
// PrefsGetter is any function that returns a PrefsView or an error.
|
||||
// It delays fetching of PrefsView from the StateStore until and unless it is needed.
|
||||
// This is primarily used when ipnauth needs to access Prefs.OperatorUser on Linux
|
||||
// or Prefs.ForceDaemon on Windows.
|
||||
// TODO(nickkhyl): consider moving / copying fields that are used in access checks
|
||||
// from ipn.Prefs to ipn.LoginProfile.
|
||||
type PrefsGetter func() (PrefsView, error)
|
||||
|
||||
// PrefsGetterFor returns a new PrefsGetter that always return the specified p.
|
||||
func PrefsGetterFor(p PrefsView) PrefsGetter {
|
||||
return func() (PrefsView, error) { return p, nil }
|
||||
}
|
||||
|
||||
// AutoUpdatePrefs are the auto update settings for the node agent.
|
||||
type AutoUpdatePrefs struct {
|
||||
// Check specifies whether background checks for updates are enabled. When
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/logtail/filch"
|
||||
|
@ -93,7 +94,7 @@ func SockstatLogID(logID logid.PublicID) logid.PrivateID {
|
|||
// The returned Logger is not yet enabled, and must be shut down with Shutdown when it is no longer needed.
|
||||
// Logs will be uploaded to the log server using a new log ID derived from the provided backend logID.
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) (*Logger, error) {
|
||||
func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor, health *health.Tracker) (*Logger, error) {
|
||||
if !sockstats.IsAvailable {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -113,7 +114,7 @@ func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID, netMon *ne
|
|||
logger := &Logger{
|
||||
logf: logf,
|
||||
filch: filch,
|
||||
tr: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon, logf),
|
||||
tr: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon, health, logf),
|
||||
}
|
||||
logger.logger = logtail.NewLogger(logtail.Config{
|
||||
BaseURL: logpolicy.LogURL(),
|
||||
|
|
|
@ -23,7 +23,7 @@ func TestResourceCleanup(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lg, err := NewLogger(td, logger.Discard, id.Public(), nil)
|
||||
lg, err := NewLogger(td, logger.Discard, id.Public(), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -453,13 +453,13 @@ func tryFixLogStateLocation(dir, cmdname string, logf logger.Logf) {
|
|||
// The logf parameter is optional; if non-nil, information logs (e.g. when
|
||||
// migrating state) are sent to that logger, and global changes to the log
|
||||
// package are avoided. If nil, logs will be printed using log.Printf.
|
||||
func New(collection string, netMon *netmon.Monitor, logf logger.Logf) *Policy {
|
||||
return NewWithConfigPath(collection, "", "", netMon, logf)
|
||||
func New(collection string, netMon *netmon.Monitor, health *health.Tracker, logf logger.Logf) *Policy {
|
||||
return NewWithConfigPath(collection, "", "", netMon, health, logf)
|
||||
}
|
||||
|
||||
// NewWithConfigPath is identical to New, but uses the specified directory and
|
||||
// command name. If either is empty, it derives them automatically.
|
||||
func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor, logf logger.Logf) *Policy {
|
||||
func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor, health *health.Tracker, logf logger.Logf) *Policy {
|
||||
var lflags int
|
||||
if term.IsTerminal(2) || runtime.GOOS == "windows" {
|
||||
lflags = 0
|
||||
|
@ -555,7 +555,7 @@ func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor,
|
|||
PrivateID: newc.PrivateID,
|
||||
Stderr: logWriter{console},
|
||||
CompressLogs: true,
|
||||
HTTPC: &http.Client{Transport: NewLogtailTransport(logtail.DefaultHost, netMon, logf)},
|
||||
HTTPC: &http.Client{Transport: NewLogtailTransport(logtail.DefaultHost, netMon, health, logf)},
|
||||
}
|
||||
if collection == logtail.CollectionNode {
|
||||
conf.MetricsDelta = clientmetric.EncodeLogTailMetricsDelta
|
||||
|
@ -570,7 +570,7 @@ func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor,
|
|||
logf("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
|
||||
conf.BaseURL = val
|
||||
u, _ := url.Parse(val)
|
||||
conf.HTTPC = &http.Client{Transport: NewLogtailTransport(u.Host, netMon, logf)}
|
||||
conf.HTTPC = &http.Client{Transport: NewLogtailTransport(u.Host, netMon, health, logf)}
|
||||
}
|
||||
|
||||
filchOptions := filch.Options{
|
||||
|
@ -742,7 +742,7 @@ func dialContext(ctx context.Context, netw, addr string, netMon *netmon.Monitor,
|
|||
//
|
||||
// The logf parameter is optional; if non-nil, logs are printed using the
|
||||
// provided function; if nil, log.Printf will be used instead.
|
||||
func NewLogtailTransport(host string, netMon *netmon.Monitor, logf logger.Logf) http.RoundTripper {
|
||||
func NewLogtailTransport(host string, netMon *netmon.Monitor, health *health.Tracker, logf logger.Logf) http.RoundTripper {
|
||||
if testenv.InTest() {
|
||||
return noopPretendSuccessTransport{}
|
||||
}
|
||||
|
@ -783,7 +783,7 @@ func NewLogtailTransport(host string, netMon *netmon.Monitor, logf logger.Logf)
|
|||
tr.TLSNextProto = map[string]func(authority string, c *tls.Conn) http.RoundTripper{}
|
||||
}
|
||||
|
||||
tr.TLSClientConfig = tlsdial.Config(host, health.Global, tr.TLSClientConfig)
|
||||
tr.TLSClientConfig = tlsdial.Config(host, health, tr.TLSClientConfig)
|
||||
|
||||
return tr
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dns/resolvconffile"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
|
@ -116,8 +117,9 @@ func restartResolved() error {
|
|||
// The caller must call Down before program shutdown
|
||||
// or as cleanup if the program terminates unexpectedly.
|
||||
type directManager struct {
|
||||
logf logger.Logf
|
||||
fs wholeFileFS
|
||||
logf logger.Logf
|
||||
health *health.Tracker
|
||||
fs wholeFileFS
|
||||
// renameBroken is set if fs.Rename to or from /etc/resolv.conf
|
||||
// fails. This can happen in some container runtimes, where
|
||||
// /etc/resolv.conf is bind-mounted from outside the container,
|
||||
|
@ -140,14 +142,15 @@ type directManager struct {
|
|||
}
|
||||
|
||||
//lint:ignore U1000 used in manager_{freebsd,openbsd}.go
|
||||
func newDirectManager(logf logger.Logf) *directManager {
|
||||
return newDirectManagerOnFS(logf, directFS{})
|
||||
func newDirectManager(logf logger.Logf, health *health.Tracker) *directManager {
|
||||
return newDirectManagerOnFS(logf, health, directFS{})
|
||||
}
|
||||
|
||||
func newDirectManagerOnFS(logf logger.Logf, fs wholeFileFS) *directManager {
|
||||
func newDirectManagerOnFS(logf logger.Logf, health *health.Tracker, fs wholeFileFS) *directManager {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m := &directManager{
|
||||
logf: logf,
|
||||
health: health,
|
||||
fs: fs,
|
||||
ctx: ctx,
|
||||
ctxClose: cancel,
|
||||
|
|
|
@ -78,7 +78,7 @@ func (m *directManager) checkForFileTrample() {
|
|||
return
|
||||
}
|
||||
if bytes.Equal(cur, want) {
|
||||
health.Global.SetWarnable(warnTrample, nil)
|
||||
m.health.SetWarnable(warnTrample, nil)
|
||||
if lastWarn != nil {
|
||||
m.mu.Lock()
|
||||
m.lastWarnContents = nil
|
||||
|
@ -101,7 +101,7 @@ func (m *directManager) checkForFileTrample() {
|
|||
show = show[:1024]
|
||||
}
|
||||
m.logf("trample: resolv.conf changed from what we expected. did some other program interfere? current contents: %q", show)
|
||||
health.Global.SetWarnable(warnTrample, errors.New("Linux DNS config not ideal. /etc/resolv.conf overwritten. See https://tailscale.com/s/dns-fight"))
|
||||
m.health.SetWarnable(warnTrample, errors.New("Linux DNS config not ideal. /etc/resolv.conf overwritten. See https://tailscale.com/s/dns-fight"))
|
||||
}
|
||||
|
||||
func (m *directManager) closeInotifyOnDone(ctx context.Context, in *gonotify.Inotify) {
|
||||
|
|
|
@ -42,7 +42,8 @@ const maxActiveQueries = 256
|
|||
|
||||
// Manager manages system DNS settings.
|
||||
type Manager struct {
|
||||
logf logger.Logf
|
||||
logf logger.Logf
|
||||
health *health.Tracker
|
||||
|
||||
activeQueriesAtomic int32
|
||||
|
||||
|
@ -55,7 +56,7 @@ type Manager struct {
|
|||
|
||||
// NewManagers created a new manager from the given config.
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func NewManager(logf logger.Logf, oscfg OSConfigurator, netMon *netmon.Monitor, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector, knobs *controlknobs.Knobs) *Manager {
|
||||
func NewManager(logf logger.Logf, oscfg OSConfigurator, netMon *netmon.Monitor, health *health.Tracker, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector, knobs *controlknobs.Knobs) *Manager {
|
||||
if dialer == nil {
|
||||
panic("nil Dialer")
|
||||
}
|
||||
|
@ -64,6 +65,7 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, netMon *netmon.Monitor,
|
|||
logf: logf,
|
||||
resolver: resolver.New(logf, netMon, linkSel, dialer, knobs),
|
||||
os: oscfg,
|
||||
health: health,
|
||||
}
|
||||
m.ctx, m.ctxCancel = context.WithCancel(context.Background())
|
||||
m.logf("using %T", m.os)
|
||||
|
@ -94,10 +96,10 @@ func (m *Manager) Set(cfg Config) error {
|
|||
return err
|
||||
}
|
||||
if err := m.os.SetDNS(ocfg); err != nil {
|
||||
health.Global.SetDNSOSHealth(err)
|
||||
m.health.SetDNSOSHealth(err)
|
||||
return err
|
||||
}
|
||||
health.Global.SetDNSOSHealth(nil)
|
||||
m.health.SetDNSOSHealth(nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -248,7 +250,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
|
|||
// This is currently (2022-10-13) expected on certain iOS and macOS
|
||||
// builds.
|
||||
} else {
|
||||
health.Global.SetDNSOSHealth(err)
|
||||
m.health.SetDNSOSHealth(err)
|
||||
return resolver.Config{}, OSConfig{}, err
|
||||
}
|
||||
}
|
||||
|
@ -453,12 +455,12 @@ func (m *Manager) FlushCaches() error {
|
|||
// in case the Tailscale daemon terminated without closing the router.
|
||||
// No other state needs to be instantiated before this runs.
|
||||
func CleanUp(logf logger.Logf, interfaceName string) {
|
||||
oscfg, err := NewOSConfigurator(logf, interfaceName)
|
||||
oscfg, err := NewOSConfigurator(logf, nil, interfaceName)
|
||||
if err != nil {
|
||||
logf("creating dns cleanup: %v", err)
|
||||
return
|
||||
}
|
||||
dns := NewManager(logf, oscfg, nil, &tsdial.Dialer{Logf: logf}, nil, nil)
|
||||
dns := NewManager(logf, oscfg, nil, nil, &tsdial.Dialer{Logf: logf}, nil, nil)
|
||||
if err := dns.Down(); err != nil {
|
||||
logf("dns down: %v", err)
|
||||
}
|
||||
|
|
|
@ -8,11 +8,12 @@ import (
|
|||
"os"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, ifName string) (OSConfigurator, error) {
|
||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, ifName string) (OSConfigurator, error) {
|
||||
return &darwinConfigurator{logf: logf, ifName: ifName}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
|
||||
package dns
|
||||
|
||||
import "tailscale.com/types/logger"
|
||||
import (
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func NewOSConfigurator(logger.Logf, string) (OSConfigurator, error) {
|
||||
// TODO(dmytro): on darwin, we should use a macOS-specific method such as scutil.
|
||||
// This is currently not implemented. Editing /etc/resolv.conf does not work,
|
||||
// as most applications use the system resolver, which disregards it.
|
||||
func NewOSConfigurator(logger.Logf, *health.Tracker, string) (OSConfigurator, error) {
|
||||
return NewNoopManager()
|
||||
}
|
||||
|
|
|
@ -7,13 +7,14 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ string) (OSConfigurator, error) {
|
||||
bs, err := os.ReadFile("/etc/resolv.conf")
|
||||
if os.IsNotExist(err) {
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
|
@ -23,16 +24,16 @@ func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
|||
case "resolvconf":
|
||||
switch resolvconfStyle() {
|
||||
case "":
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
case "debian":
|
||||
return newDebianResolvconfManager(logf)
|
||||
case "openresolv":
|
||||
return newOpenresolvManager(logf)
|
||||
default:
|
||||
logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", resolvconfStyle())
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
default:
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ func (kv kv) String() string {
|
|||
|
||||
var publishOnce sync.Once
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurator, err error) {
|
||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, interfaceName string) (ret OSConfigurator, err error) {
|
||||
env := newOSConfigEnv{
|
||||
fs: directFS{},
|
||||
dbusPing: dbusPing,
|
||||
|
@ -40,7 +40,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
|||
nmVersionBetween: nmVersionBetween,
|
||||
resolvconfStyle: resolvconfStyle,
|
||||
}
|
||||
mode, err := dnsMode(logf, env)
|
||||
mode, err := dnsMode(logf, health, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -52,9 +52,9 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
|||
logf("dns: using %q mode", mode)
|
||||
switch mode {
|
||||
case "direct":
|
||||
return newDirectManagerOnFS(logf, env.fs), nil
|
||||
return newDirectManagerOnFS(logf, health, env.fs), nil
|
||||
case "systemd-resolved":
|
||||
return newResolvedManager(logf, interfaceName)
|
||||
return newResolvedManager(logf, health, interfaceName)
|
||||
case "network-manager":
|
||||
return newNMManager(interfaceName)
|
||||
case "debian-resolvconf":
|
||||
|
@ -63,7 +63,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
|||
return newOpenresolvManager(logf)
|
||||
default:
|
||||
logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode)
|
||||
return newDirectManagerOnFS(logf, env.fs), nil
|
||||
return newDirectManagerOnFS(logf, health, env.fs), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ type newOSConfigEnv struct {
|
|||
resolvconfStyle func() string
|
||||
}
|
||||
|
||||
func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
func dnsMode(logf logger.Logf, health *health.Tracker, env newOSConfigEnv) (ret string, err error) {
|
||||
var debug []kv
|
||||
dbg := func(k, v string) {
|
||||
debug = append(debug, kv{k, v})
|
||||
|
@ -271,7 +271,7 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
|||
return "direct", nil
|
||||
}
|
||||
|
||||
health.Global.SetDNSManagerHealth(errors.New("systemd-resolved and NetworkManager are wired together incorrectly; MagicDNS will probably not work. For more info, see https://tailscale.com/s/resolved-nm"))
|
||||
health.SetDNSManagerHealth(errors.New("systemd-resolved and NetworkManager are wired together incorrectly; MagicDNS will probably not work. For more info, see https://tailscale.com/s/resolved-nm"))
|
||||
dbg("nm-safe", "no")
|
||||
return "systemd-resolved", nil
|
||||
default:
|
||||
|
|
|
@ -286,7 +286,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var logBuf tstest.MemLogger
|
||||
got, err := dnsMode(logBuf.Logf, tt.env)
|
||||
got, err := dnsMode(logBuf.Logf, nil, tt.env)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
|
@ -19,8 +20,8 @@ func (kv kv) String() string {
|
|||
return fmt.Sprintf("%s=%s", kv.k, kv.v)
|
||||
}
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) {
|
||||
return newOSConfigurator(logf, interfaceName,
|
||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, interfaceName string) (OSConfigurator, error) {
|
||||
return newOSConfigurator(logf, health, interfaceName,
|
||||
newOSConfigEnv{
|
||||
rcIsResolvd: rcIsResolvd,
|
||||
fs: directFS{},
|
||||
|
@ -33,7 +34,7 @@ type newOSConfigEnv struct {
|
|||
rcIsResolvd func(resolvConfContents []byte) bool
|
||||
}
|
||||
|
||||
func newOSConfigurator(logf logger.Logf, interfaceName string, env newOSConfigEnv) (ret OSConfigurator, err error) {
|
||||
func newOSConfigurator(logf logger.Logf, health *health.Tracker, interfaceName string, env newOSConfigEnv) (ret OSConfigurator, err error) {
|
||||
var debug []kv
|
||||
dbg := func(k, v string) {
|
||||
debug = append(debug, kv{k, v})
|
||||
|
@ -48,7 +49,7 @@ func newOSConfigurator(logf logger.Logf, interfaceName string, env newOSConfigEn
|
|||
bs, err := env.fs.ReadFile(resolvConf)
|
||||
if os.IsNotExist(err) {
|
||||
dbg("rc", "missing")
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
|
@ -60,7 +61,7 @@ func newOSConfigurator(logf logger.Logf, interfaceName string, env newOSConfigEn
|
|||
}
|
||||
|
||||
dbg("resolvd", "missing")
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
|
||||
func rcIsResolvd(resolvConfContents []byte) bool {
|
||||
|
|
|
@ -87,7 +87,7 @@ func TestDNSOverTCP(t *testing.T) {
|
|||
SearchDomains: fqdns("coffee.shop"),
|
||||
},
|
||||
}
|
||||
m := NewManager(t.Logf, &f, nil, new(tsdial.Dialer), nil, nil)
|
||||
m := NewManager(t.Logf, &f, nil, nil, new(tsdial.Dialer), nil, nil)
|
||||
m.resolver.TestOnlySetHook(f.SetResolver)
|
||||
m.Set(Config{
|
||||
Hosts: hosts(
|
||||
|
@ -172,7 +172,7 @@ func TestDNSOverTCP_TooLarge(t *testing.T) {
|
|||
SearchDomains: fqdns("coffee.shop"),
|
||||
},
|
||||
}
|
||||
m := NewManager(log, &f, nil, new(tsdial.Dialer), nil, nil)
|
||||
m := NewManager(log, &f, nil, nil, new(tsdial.Dialer), nil, nil)
|
||||
m.resolver.TestOnlySetHook(f.SetResolver)
|
||||
m.Set(Config{
|
||||
Hosts: hosts("andrew.ts.com.", "1.2.3.4"),
|
||||
|
|
|
@ -613,7 +613,7 @@ func TestManager(t *testing.T) {
|
|||
SplitDNS: test.split,
|
||||
BaseConfig: test.bs,
|
||||
}
|
||||
m := NewManager(t.Logf, &f, nil, new(tsdial.Dialer), nil, nil)
|
||||
m := NewManager(t.Logf, &f, nil, nil, new(tsdial.Dialer), nil, nil)
|
||||
m.resolver.TestOnlySetHook(f.SetResolver)
|
||||
|
||||
if err := m.Set(test.in); err != nil {
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/winutil"
|
||||
|
@ -44,11 +45,11 @@ type windowsManager struct {
|
|||
closing bool
|
||||
}
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) {
|
||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, interfaceName string) (OSConfigurator, error) {
|
||||
ret := &windowsManager{
|
||||
logf: logf,
|
||||
guid: interfaceName,
|
||||
wslManager: newWSLManager(logf),
|
||||
wslManager: newWSLManager(logf, health),
|
||||
}
|
||||
|
||||
if isWindows10OrBetter() {
|
||||
|
|
|
@ -84,7 +84,7 @@ func TestManagerWindowsGPCopy(t *testing.T) {
|
|||
}
|
||||
defer delIfKey()
|
||||
|
||||
cfg, err := NewOSConfigurator(logf, fakeInterface.String())
|
||||
cfg, err := NewOSConfigurator(logf, nil, fakeInterface.String())
|
||||
if err != nil {
|
||||
t.Fatalf("NewOSConfigurator: %v\n", err)
|
||||
}
|
||||
|
@ -213,7 +213,7 @@ func runTest(t *testing.T, isLocal bool) {
|
|||
}
|
||||
defer delIfKey()
|
||||
|
||||
cfg, err := NewOSConfigurator(logf, fakeInterface.String())
|
||||
cfg, err := NewOSConfigurator(logf, nil, fakeInterface.String())
|
||||
if err != nil {
|
||||
t.Fatalf("NewOSConfigurator: %v\n", err)
|
||||
}
|
||||
|
|
|
@ -63,13 +63,14 @@ type resolvedManager struct {
|
|||
ctx context.Context
|
||||
cancel func() // terminate the context, for close
|
||||
|
||||
logf logger.Logf
|
||||
ifidx int
|
||||
logf logger.Logf
|
||||
health *health.Tracker
|
||||
ifidx int
|
||||
|
||||
configCR chan changeRequest // tracks OSConfigs changes and error responses
|
||||
}
|
||||
|
||||
func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) {
|
||||
func newResolvedManager(logf logger.Logf, health *health.Tracker, interfaceName string) (*resolvedManager, error) {
|
||||
iface, err := net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -82,8 +83,9 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage
|
|||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
logf: logf,
|
||||
ifidx: iface.Index,
|
||||
logf: logf,
|
||||
health: health,
|
||||
ifidx: iface.Index,
|
||||
|
||||
configCR: make(chan changeRequest),
|
||||
}
|
||||
|
@ -163,7 +165,7 @@ func (m *resolvedManager) run(ctx context.Context) {
|
|||
|
||||
// Reset backoff and SetNSOSHealth after successful on reconnect.
|
||||
bo.BackOff(ctx, nil)
|
||||
health.Global.SetDNSOSHealth(nil)
|
||||
m.health.SetDNSOSHealth(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -241,7 +243,7 @@ func (m *resolvedManager) run(ctx context.Context) {
|
|||
// Set health while holding the lock, because this will
|
||||
// graciously serialize the resync's health outcome with a
|
||||
// concurrent SetDNS call.
|
||||
health.Global.SetDNSOSHealth(err)
|
||||
m.health.SetDNSOSHealth(err)
|
||||
if err != nil {
|
||||
m.logf("failed to configure systemd-resolved: %v", err)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
@ -54,12 +55,14 @@ func wslDistros() ([]string, error) {
|
|||
// wslManager is a DNS manager for WSL2 linux distributions.
|
||||
// It configures /etc/wsl.conf and /etc/resolv.conf.
|
||||
type wslManager struct {
|
||||
logf logger.Logf
|
||||
logf logger.Logf
|
||||
health *health.Tracker
|
||||
}
|
||||
|
||||
func newWSLManager(logf logger.Logf) *wslManager {
|
||||
func newWSLManager(logf logger.Logf, health *health.Tracker) *wslManager {
|
||||
m := &wslManager{
|
||||
logf: logf,
|
||||
logf: logf,
|
||||
health: health,
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
@ -73,7 +76,7 @@ func (wm *wslManager) SetDNS(cfg OSConfig) error {
|
|||
}
|
||||
managers := make(map[string]*directManager)
|
||||
for _, distro := range distros {
|
||||
managers[distro] = newDirectManagerOnFS(wm.logf, wslFS{
|
||||
managers[distro] = newDirectManagerOnFS(wm.logf, wm.health, wslFS{
|
||||
user: "root",
|
||||
distro: distro,
|
||||
})
|
||||
|
|
|
@ -6,6 +6,7 @@ package netutil
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
|
@ -145,8 +146,6 @@ func CheckIPForwarding(routes []netip.Prefix, state *interfaces.State) (warn, er
|
|||
// disabled or set to 'loose' mode for exit node functionality on any
|
||||
// interface.
|
||||
//
|
||||
// The state param can be nil, in which case interfaces.GetState is used.
|
||||
//
|
||||
// The routes should only be advertised routes, and should not contain the
|
||||
// node's Tailscale IPs.
|
||||
//
|
||||
|
@ -159,11 +158,7 @@ func CheckReversePathFiltering(state *interfaces.State) (warn []string, err erro
|
|||
}
|
||||
|
||||
if state == nil {
|
||||
var err error
|
||||
state, err = interfaces.GetState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.New("no link state")
|
||||
}
|
||||
|
||||
// The kernel uses the maximum value for rp_filter between the 'all'
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
)
|
||||
|
||||
type conn struct {
|
||||
|
@ -70,7 +72,13 @@ func TestCheckReversePathFiltering(t *testing.T) {
|
|||
if runtime.GOOS != "linux" {
|
||||
t.Skipf("skipping on %s", runtime.GOOS)
|
||||
}
|
||||
warn, err := CheckReversePathFiltering(nil)
|
||||
netMon, err := netmon.New(t.Logf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer netMon.Close()
|
||||
|
||||
warn, err := CheckReversePathFiltering(netMon.InterfaceState())
|
||||
t.Logf("err: %v", err)
|
||||
t.Logf("warnings: %v", warn)
|
||||
}
|
||||
|
|
|
@ -139,14 +139,6 @@ func (s *System) ProxyMapper() *proxymap.Mapper {
|
|||
|
||||
// HealthTracker returns the system health tracker.
|
||||
func (s *System) HealthTracker() *health.Tracker {
|
||||
// TODO(bradfitz): plumb the tsd.System.HealthTracker() value
|
||||
// everywhere and then then remove this use of the global
|
||||
// and remove health.Global entirely. But for now we keep
|
||||
// the two in sync during plumbing.
|
||||
const stillPlumbing = true
|
||||
if stillPlumbing {
|
||||
return health.Global
|
||||
}
|
||||
return &s.healthTracker
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
|
@ -504,7 +505,8 @@ func (s *Server) start() (reterr error) {
|
|||
return fmt.Errorf("%v is not a directory", s.rootPath)
|
||||
}
|
||||
|
||||
if err := s.startLogger(&closePool); err != nil {
|
||||
sys := new(tsd.System)
|
||||
if err := s.startLogger(&closePool, sys.HealthTracker()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -514,7 +516,6 @@ func (s *Server) start() (reterr error) {
|
|||
}
|
||||
closePool.add(s.netMon)
|
||||
|
||||
sys := new(tsd.System)
|
||||
s.dialer = &tsdial.Dialer{Logf: logf} // mutated below (before used)
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
ListenPort: s.Port,
|
||||
|
@ -627,7 +628,7 @@ func (s *Server) start() (reterr error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) startLogger(closePool *closeOnErrorPool) error {
|
||||
func (s *Server) startLogger(closePool *closeOnErrorPool, health *health.Tracker) error {
|
||||
if testenv.InTest() {
|
||||
return nil
|
||||
}
|
||||
|
@ -658,7 +659,7 @@ func (s *Server) startLogger(closePool *closeOnErrorPool) error {
|
|||
Stderr: io.Discard, // log everything to Buffer
|
||||
Buffer: s.logbuffer,
|
||||
CompressLogs: true,
|
||||
HTTPC: &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, s.netMon, s.logf)},
|
||||
HTTPC: &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, s.netMon, health, s.logf)},
|
||||
MetricsDelta: clientmetric.EncodeLogTailMetricsDelta,
|
||||
}
|
||||
s.logtail = logtail.NewLogger(c, s.logf)
|
||||
|
|
|
@ -681,8 +681,10 @@ func (c *Conn) runDerpWriter(ctx context.Context, dc *derphttp.Client, ch <-chan
|
|||
}
|
||||
|
||||
func (c *connBind) receiveDERP(buffs [][]byte, sizes []int, eps []conn.Endpoint) (int, error) {
|
||||
health.ReceiveDERP.Enter()
|
||||
defer health.ReceiveDERP.Exit()
|
||||
if s := c.Conn.health.ReceiveFuncStats(health.ReceiveDERP); s != nil {
|
||||
s.Enter()
|
||||
defer s.Exit()
|
||||
}
|
||||
|
||||
for dm := range c.derpRecvCh {
|
||||
if c.isClosed() {
|
||||
|
|
|
@ -1203,12 +1203,12 @@ func (c *Conn) putReceiveBatch(batch *receiveBatch) {
|
|||
|
||||
// receiveIPv4 creates an IPv4 ReceiveFunc reading from c.pconn4.
|
||||
func (c *Conn) receiveIPv4() conn.ReceiveFunc {
|
||||
return c.mkReceiveFunc(&c.pconn4, &health.ReceiveIPv4, metricRecvDataIPv4)
|
||||
return c.mkReceiveFunc(&c.pconn4, c.health.ReceiveFuncStats(health.ReceiveIPv4), metricRecvDataIPv4)
|
||||
}
|
||||
|
||||
// receiveIPv6 creates an IPv6 ReceiveFunc reading from c.pconn6.
|
||||
func (c *Conn) receiveIPv6() conn.ReceiveFunc {
|
||||
return c.mkReceiveFunc(&c.pconn6, &health.ReceiveIPv6, metricRecvDataIPv6)
|
||||
return c.mkReceiveFunc(&c.pconn6, c.health.ReceiveFuncStats(health.ReceiveIPv6), metricRecvDataIPv6)
|
||||
}
|
||||
|
||||
// mkReceiveFunc creates a ReceiveFunc reading from ruc.
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/connstats"
|
||||
|
@ -92,7 +93,7 @@ var testClient *http.Client
|
|||
// The IP protocol and source port are always zero.
|
||||
// The sock is used to populated the PhysicalTraffic field in Message.
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor) error {
|
||||
func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor, health *health.Tracker) error {
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
if nl.logger != nil {
|
||||
|
@ -101,7 +102,7 @@ func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID lo
|
|||
|
||||
// Startup a log stream to Tailscale's logging service.
|
||||
logf := log.Printf
|
||||
httpc := &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon, logf)}
|
||||
httpc := &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon, health, logf)}
|
||||
if testClient != nil {
|
||||
httpc = testClient
|
||||
}
|
||||
|
|
|
@ -237,7 +237,7 @@ func interfaceFromLUID(luid winipcfg.LUID, flags winipcfg.GAAFlags) (*winipcfg.I
|
|||
|
||||
var networkCategoryWarning = health.NewWarnable(health.WithMapDebugFlag("warn-network-category-unhealthy"))
|
||||
|
||||
func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
|
||||
func configureInterface(cfg *Config, tun *tun.NativeTun, health *health.Tracker) (retErr error) {
|
||||
var mtu = tstun.DefaultTUNMTU()
|
||||
luid := winipcfg.LUID(tun.LUID())
|
||||
iface, err := interfaceFromLUID(luid,
|
||||
|
@ -268,10 +268,10 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
|
|||
for i := range tries {
|
||||
found, err := setPrivateNetwork(luid)
|
||||
if err != nil {
|
||||
health.Global.SetWarnable(networkCategoryWarning, fmt.Errorf("set-network-category: %w", err))
|
||||
health.SetWarnable(networkCategoryWarning, fmt.Errorf("set-network-category: %w", err))
|
||||
log.Printf("setPrivateNetwork(try=%d): %v", i, err)
|
||||
} else {
|
||||
health.Global.SetWarnable(networkCategoryWarning, nil)
|
||||
health.SetWarnable(networkCategoryWarning, nil)
|
||||
if found {
|
||||
if i > 0 {
|
||||
log.Printf("setPrivateNetwork(try=%d): success", i)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"reflect"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
|
@ -44,9 +45,9 @@ type Router interface {
|
|||
//
|
||||
// If netMon is nil, it's not used. It's currently (2021-07-20) only
|
||||
// used on Linux in some situations.
|
||||
func New(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor) (Router, error) {
|
||||
func New(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
|
||||
logf = logger.WithPrefix(logf, "router: ")
|
||||
return newUserspaceRouter(logf, tundev, netMon)
|
||||
return newUserspaceRouter(logf, tundev, netMon, health)
|
||||
}
|
||||
|
||||
// CleanUp restores the system network configuration to its original state
|
||||
|
|
|
@ -5,12 +5,13 @@ package router
|
|||
|
||||
import (
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor) (Router, error) {
|
||||
return newUserspaceBSDRouter(logf, tundev, netMon)
|
||||
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
|
||||
return newUserspaceBSDRouter(logf, tundev, netMon, health)
|
||||
}
|
||||
|
||||
func cleanUp(logger.Logf, string) {
|
||||
|
|
|
@ -10,11 +10,12 @@ import (
|
|||
"runtime"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Monitor) (Router, error) {
|
||||
func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
|
||||
return nil, fmt.Errorf("unsupported OS %q", runtime.GOOS)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ package router
|
|||
|
||||
import (
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
@ -14,8 +15,8 @@ import (
|
|||
// Work is currently underway for an in-kernel FreeBSD implementation of wireguard
|
||||
// https://svnweb.freebsd.org/base?view=revision&revision=357986
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor) (Router, error) {
|
||||
return newUserspaceBSDRouter(logf, tundev, netMon)
|
||||
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
|
||||
return newUserspaceBSDRouter(logf, tundev, netMon, health)
|
||||
}
|
||||
|
||||
func cleanUp(logf logger.Logf, interfaceName string) {
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"golang.org/x/sys/unix"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
|
@ -69,7 +70,7 @@ type linuxRouter struct {
|
|||
magicsockPortV6 uint16
|
||||
}
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Monitor) (Router, error) {
|
||||
func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
|
||||
tunname, err := tunDev.Name()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -886,7 +886,7 @@ func newLinuxRootTest(t *testing.T) *linuxTest {
|
|||
mon.Start()
|
||||
lt.mon = mon
|
||||
|
||||
r, err := newUserspaceRouter(logf, lt.tun, mon)
|
||||
r, err := newUserspaceRouter(logf, lt.tun, mon, nil)
|
||||
if err != nil {
|
||||
lt.Close()
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"go4.org/netipx"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/set"
|
||||
|
@ -30,7 +31,7 @@ type openbsdRouter struct {
|
|||
routes set.Set[netip.Prefix]
|
||||
}
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor) (Router, error) {
|
||||
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
|
||||
tunname, err := tundev.Name()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"go4.org/netipx"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
|
@ -23,12 +24,13 @@ import (
|
|||
type userspaceBSDRouter struct {
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor
|
||||
health *health.Tracker
|
||||
tunname string
|
||||
local []netip.Prefix
|
||||
routes map[netip.Prefix]bool
|
||||
}
|
||||
|
||||
func newUserspaceBSDRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor) (Router, error) {
|
||||
func newUserspaceBSDRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
|
||||
tunname, err := tundev.Name()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -37,6 +39,7 @@ func newUserspaceBSDRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.M
|
|||
return &userspaceBSDRouter{
|
||||
logf: logf,
|
||||
netMon: netMon,
|
||||
health: health,
|
||||
tunname: tunname,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/tailscale/wireguard-go/tun"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netmon"
|
||||
|
@ -31,12 +32,13 @@ import (
|
|||
type winRouter struct {
|
||||
logf func(fmt string, args ...any)
|
||||
netMon *netmon.Monitor // may be nil
|
||||
health *health.Tracker
|
||||
nativeTun *tun.NativeTun
|
||||
routeChangeCallback *winipcfg.RouteChangeCallback
|
||||
firewall *firewallTweaker
|
||||
}
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor) (Router, error) {
|
||||
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
|
||||
nativeTun := tundev.(*tun.NativeTun)
|
||||
luid := winipcfg.LUID(nativeTun.LUID())
|
||||
guid, err := luid.GUID()
|
||||
|
@ -47,6 +49,7 @@ func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Moni
|
|||
return &winRouter{
|
||||
logf: logf,
|
||||
netMon: netMon,
|
||||
health: health,
|
||||
nativeTun: nativeTun,
|
||||
firewall: &firewallTweaker{
|
||||
logf: logger.WithPrefix(logf, "firewall: "),
|
||||
|
@ -80,7 +83,7 @@ func (r *winRouter) Set(cfg *Config) error {
|
|||
}
|
||||
r.firewall.set(localAddrs, cfg.Routes, cfg.LocalRoutes)
|
||||
|
||||
err := configureInterface(cfg, r.nativeTun)
|
||||
err := configureInterface(cfg, r.nativeTun, r.health)
|
||||
if err != nil {
|
||||
r.logf("ConfigureInterface: %v", err)
|
||||
return err
|
||||
|
|
|
@ -341,7 +341,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
|||
tunName, _ := conf.Tun.Name()
|
||||
conf.Dialer.SetTUNName(tunName)
|
||||
conf.Dialer.SetNetMon(e.netMon)
|
||||
e.dns = dns.NewManager(logf, conf.DNS, e.netMon, conf.Dialer, fwdDNSLinkSelector{e, tunName}, conf.ControlKnobs)
|
||||
e.dns = dns.NewManager(logf, conf.DNS, e.netMon, e.health, conf.Dialer, fwdDNSLinkSelector{e, tunName}, conf.ControlKnobs)
|
||||
|
||||
// TODO: there's probably a better place for this
|
||||
sockstats.SetNetMon(e.netMon)
|
||||
|
@ -966,7 +966,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
|
|||
nid := cfg.NetworkLogging.NodeID
|
||||
tid := cfg.NetworkLogging.DomainID
|
||||
e.logf("wgengine: Reconfig: starting up network logger (node:%s tailnet:%s)", nid.Public(), tid.Public())
|
||||
if err := e.networkLogger.Startup(cfg.NodeID, nid, tid, e.tundev, e.magicConn, e.netMon); err != nil {
|
||||
if err := e.networkLogger.Startup(cfg.NodeID, nid, tid, e.tundev, e.magicConn, e.netMon, e.health); err != nil {
|
||||
e.logf("wgengine: Reconfig: error starting up network logger: %v", err)
|
||||
}
|
||||
e.networkLogger.ReconfigRoutes(routerCfg)
|
||||
|
|
Loading…
Reference in New Issue