tailscale/ipn/ipnlocal/local.go

5048 lines
145 KiB
Go
Raw Normal View History

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnlocal
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"net/netip"
"net/url"
"os"
"os/user"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"go4.org/mem"
"go4.org/netipx"
"golang.org/x/exp/slices"
"gvisor.dev/gvisor/pkg/tcpip"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
"tailscale.com/doctor"
"tailscale.com/doctor/permissions"
"tailscale.com/doctor/routetable"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/health/healthmsg"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
"tailscale.com/log/sockstatlog"
"tailscale.com/logpolicy"
"tailscale.com/net/dns"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/net/netutil"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/paths"
"tailscale.com/portlist"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/tsd"
"tailscale.com/types/dnstype"
"tailscale.com/types/empty"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
"tailscale.com/util/cmpx"
"tailscale.com/util/deephash"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
"tailscale.com/util/multierr"
"tailscale.com/util/osshare"
"tailscale.com/util/set"
ipn/ipnserver: enable systemd-notify support Addresses #964 Still to be done: - Figure out the correct logging lines in util/systemd - Figure out if we need to slip the systemd.Status function anywhere else - Log util/systemd errors? (most of the errors are of the "you cannot do anything about this, but it might be a bad idea to crash the program if it errors" kind) Assistance in getting this over the finish line would help a lot. Signed-off-by: Christine Dodrill <me@christine.website> util/systemd: rename the nonlinux file to appease the magic Signed-off-by: Christine Dodrill <me@christine.website> util/systemd: fix package name Signed-off-by: Christine Dodrill <me@christine.website> util/systemd: fix review feedback from @mdlayher Signed-off-by: Christine Dodrill <me@christine.website> cmd/tailscale{,d}: update depaware manifests Signed-off-by: Christine Dodrill <me@christine.website> util/systemd: use sync.Once instead of func init Signed-off-by: Christine Dodrill <me@christine.website> control/controlclient: minor review feedback fixes Signed-off-by: Christine Dodrill <me@christine.website> {control,ipn,systemd}: fix review feedback Signed-off-by: Christine Dodrill <me@christine.website> review feedback fixes Signed-off-by: Christine Dodrill <me@christine.website> ipn: fix sprintf call Signed-off-by: Christine Dodrill <me@christine.website> ipn: make staticcheck less sad Signed-off-by: Christine Dodrill <me@christine.website> ipn: print IP address in connected status Signed-off-by: Christine Dodrill <me@christine.website> ipn: review feedback Signed-off-by: Christine Dodrill <me@christine.website> final fixups Signed-off-by: Christine Dodrill <me@christine.website>
2020-11-24 23:35:04 +00:00
"tailscale.com/util/systemd"
"tailscale.com/util/uniq"
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/wgcfg"
"tailscale.com/wgengine/wgcfg/nmcfg"
)
var controlDebugFlags = getControlDebugFlags()
func getControlDebugFlags() []string {
if e := envknob.String("TS_DEBUG_CONTROL_FLAGS"); e != "" {
return strings.Split(e, ",")
}
return nil
}
// SSHServer is the interface of the conditionally linked ssh/tailssh.server.
type SSHServer interface {
HandleSSHConn(net.Conn) error
// OnPolicyChange is called when the SSH access policy changes,
// so that existing sessions can be re-evaluated for validity
// and closed if they'd no longer be accepted.
OnPolicyChange()
// Shutdown is called when tailscaled is shutting down.
Shutdown()
}
type newSSHServerFunc func(logger.Logf, *LocalBackend) (SSHServer, error)
var newSSHServer newSSHServerFunc // or nil
// RegisterNewSSHServer lets the conditionally linked ssh/tailssh package register itself.
func RegisterNewSSHServer(fn newSSHServerFunc) {
newSSHServer = fn
}
// LocalBackend is the glue between the major pieces of the Tailscale
// network software: the cloud control plane (via controlclient), the
// network data plane (via wgengine), and the user-facing UIs and CLIs
// (collectively called "frontends", via LocalBackend's implementation
// of the Backend interface).
//
// LocalBackend implements the overall state machine for the Tailscale
// application. Frontends, controlclient and wgengine can feed events
// into LocalBackend to advance the state machine, and advancing the
// state machine generates events back out to zero or more components.
type LocalBackend struct {
// Elements that are thread-safe or constant after construction.
ctx context.Context // canceled by Close
ctxCancel context.CancelFunc // cancels ctx
logf logger.Logf // general logging
keyLogf logger.Logf // for printing list of peers on change
statsLogf logger.Logf // for printing peers stats on change
sys *tsd.System
e wgengine.Engine // non-nil; TODO(bradfitz): remove; use sys
pm *profileManager
store ipn.StateStore // non-nil; TODO(bradfitz): remove; use sys
dialer *tsdial.Dialer // non-nil; TODO(bradfitz): remove; use sys
backendLogID logid.PublicID
unregisterNetMon func()
unregisterHealthWatch func()
portpoll *portlist.Poller // may be nil
portpollOnce sync.Once // guards starting readPoller
gotPortPollRes chan struct{} // closed upon first readPoller result
newDecompressor func() (controlclient.Decompressor, error)
varRoot string // or empty if SetVarRoot never called
logFlushFunc func() // or nil if SetLogFlusher wasn't called
em *expiryManager // non-nil
sshAtomicBool atomic.Bool
shutdownCalled bool // if Shutdown has been called
debugSink *capture.Sink
sockstatLogger *sockstatlog.Logger
// getTCPHandlerForFunnelFlow returns a handler for an incoming TCP flow for
// the provided srcAddr and dstPort if one exists.
//
// srcAddr is the source address of the flow, not the address of the Funnel
// node relaying the flow.
// dstPort is the destination port of the flow.
//
// It returns nil if there is no known handler for this flow.
//
// This is specifically used to handle TCP flows for Funnel connections to tsnet
// servers.
//
// It is set once during initialization, and can be nil if SetTCPHandlerForFunnelFlow
// is never called.
getTCPHandlerForFunnelFlow func(srcAddr netip.AddrPort, dstPort uint16) (handler func(net.Conn))
// lastProfileID tracks the last profile we've seen from the ProfileManager.
// It's used to detect when the user has changed their profile.
lastProfileID ipn.ProfileID
filterAtomic atomic.Pointer[filter.Filter]
containsViaIPFuncAtomic syncs.AtomicValue[func(netip.Addr) bool]
shouldInterceptTCPPortAtomic syncs.AtomicValue[func(uint16) bool]
numClientStatusCalls atomic.Uint32
// The mutex protects the following elements.
mu sync.Mutex
filterHash deephash.Sum
httpTestClient *http.Client // for controlclient. nil by default, used by tests.
ccGen clientGen // function for producing controlclient; lazily populated
sshServer SSHServer // or nil, initialized lazily.
notify func(ipn.Notify)
cc controlclient.Client
ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto
machinePrivKey key.MachinePrivate
tka *tkaState
state ipn.State
capFileSharing bool // whether netMap contains the file sharing capability
capTailnetLock bool // whether netMap contains the tailnet lock capability
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo
// netMap is not mutated in-place once set.
netMap *netmap.NetworkMap
nmExpiryTimer *time.Timer // for updating netMap on node expiry; can be nil
nodeByAddr map[netip.Addr]*tailcfg.Node
activeLogin string // last logged LoginName from netMap
engineStatus ipn.EngineStatus
endpoints []tailcfg.Endpoint
blocked bool
keyExpired bool
authURL string // cleared on Notify
authURLSticky string // not cleared on Notify
interact bool
egg bool
prevIfState *interfaces.State
peerAPIServer *peerAPIServer // or nil
peerAPIListeners []*peerAPIListener
loginFlags controlclient.LoginFlags
incomingFiles map[*incomingFile]bool
fileWaiters set.HandleSet[context.CancelFunc] // of wake-up funcs
notifyWatchers set.HandleSet[chan *ipn.Notify]
lastStatusTime time.Time // status.AsOf value of the last processed status update
// directFileRoot, if non-empty, means to write received files
// directly to this directory, without staging them in an
// intermediate buffered directory for "pick-up" later. If
// empty, the files are received in a daemon-owned location
// and the localapi is used to enumerate, download, and delete
// them. This is used on macOS where the GUI lifetime is the
// same as the Network Extension lifetime and we can thus avoid
// double-copying files by writing them to the right location
// immediately.
// It's also used on several NAS platforms (Synology, TrueNAS, etc)
// but in that case DoFinalRename is also set true, which moves the
// *.partial file to its final name on completion.
directFileRoot string
directFileDoFinalRename bool // false on macOS, true on several NAS platforms
componentLogUntil map[string]componentLogState
// ServeConfig fields. (also guarded by mu)
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
serveConfig ipn.ServeConfigView // or !Valid if none
serveListeners map[netip.AddrPort]*serveListener // addrPort => serveListener
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *httputil.ReverseProxy
// statusLock must be held before calling statusChanged.Wait() or
// statusChanged.Broadcast().
statusLock sync.Mutex
statusChanged *sync.Cond
// dialPlan is any dial plan that we've received from the control
// server during a previous connection; it is cleared on logout.
dialPlan atomic.Pointer[tailcfg.ControlDialPlan]
// tkaSyncLock is used to make tkaSyncIfNeeded an exclusive
// section. This is needed to stop two map-responses in quick succession
// from racing each other through TKA sync logic / RPCs.
//
// tkaSyncLock MUST be taken before mu (or inversely, mu must not be held
// at the moment that tkaSyncLock is taken).
tkaSyncLock sync.Mutex
}
// clientGen is a func that creates a control plane client.
// It's the type used by LocalBackend.SetControlClientGetterForTesting.
type clientGen func(controlclient.Options) (controlclient.Client, error)
// NewLocalBackend returns a new LocalBackend that is ready to run,
// but is not actually running.
//
// If dialer is nil, a new one is made.
func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, loginFlags controlclient.LoginFlags) (*LocalBackend, error) {
e := sys.Engine.Get()
store := sys.StateStore.Get()
dialer := sys.Dialer.Get()
pm, err := newProfileManager(store, logf)
if err != nil {
return nil, err
}
if sds, ok := store.(ipn.StateStoreDialerSetter); ok {
sds.SetDialer(dialer.SystemDial)
}
hi := hostinfo.New()
logf.JSON(1, "Hostinfo", hi)
envknob.LogCurrent(logf)
if dialer == nil {
dialer = &tsdial.Dialer{Logf: logf}
}
osshare.SetFileSharingEnabled(false, logf)
ctx, cancel := context.WithCancel(context.Background())
portpoll := new(portlist.Poller)
b := &LocalBackend{
ctx: ctx,
ctxCancel: cancel,
logf: logf,
keyLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
sys: sys,
e: e,
dialer: dialer,
store: store,
pm: pm,
backendLogID: logID,
state: ipn.NoState,
portpoll: portpoll,
em: newExpiryManager(logf),
gotPortPollRes: make(chan struct{}),
loginFlags: loginFlags,
}
netMon := sys.NetMon.Get()
b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID, netMon)
if err != nil {
log.Printf("error setting up sockstat logger: %v", err)
}
// Enable sockstats logs only on unstable builds
if version.IsUnstableBuild() && b.sockstatLogger != nil {
b.sockstatLogger.SetLoggingEnabled(true)
}
// Default filter blocks everything and logs nothing, until Start() is called.
b.setFilter(filter.NewAllowNone(logf, &netipx.IPSet{}))
b.setTCPPortsIntercepted(nil)
b.statusChanged = sync.NewCond(&b.statusLock)
b.e.SetStatusCallback(b.setWgengineStatus)
b.prevIfState = netMon.InterfaceState()
// Call our linkChange code once with the current state, and
// then also whenever it changes:
b.linkChange(false, netMon.InterfaceState())
b.unregisterNetMon = netMon.RegisterChangeCallback(b.linkChange)
b.unregisterHealthWatch = health.RegisterWatcher(b.onHealthChange)
if tunWrap, ok := b.sys.Tun.GetOK(); ok {
tunWrap.PeerAPIPort = b.GetPeerAPIPort
} else {
b.logf("[unexpected] failed to wire up PeerAPI port for engine %T", e)
}
for _, component := range debuggableComponents {
key := componentStateKey(component)
if ut, err := ipn.ReadStoreInt(pm.Store(), key); err == nil {
if until := time.Unix(ut, 0); until.After(time.Now()) {
// conditional to avoid log spam at start when off
b.SetComponentDebugLogging(component, until)
}
}
}
return b, nil
}
type componentLogState struct {
until time.Time
timer *time.Timer // if non-nil, the AfterFunc to disable it
}
var debuggableComponents = []string{
"magicsock",
"sockstats",
}
func componentStateKey(component string) ipn.StateKey {
return ipn.StateKey("_debug_" + component + "_until")
}
// SetComponentDebugLogging sets component's debug logging enabled until the until time.
// If until is in the past, the component's debug logging is disabled.
//
// The following components are recognized:
//
// - magicsock
// - sockstats
func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Time) error {
b.mu.Lock()
defer b.mu.Unlock()
var setEnabled func(bool)
switch component {
case "magicsock":
mc, err := b.magicConn()
if err != nil {
return err
}
setEnabled = mc.SetDebugLoggingEnabled
case "sockstats":
if b.sockstatLogger != nil {
setEnabled = func(v bool) {
b.sockstatLogger.SetLoggingEnabled(v)
// Flush (and thus upload) logs when the enabled period ends,
// so that the logs are available for debugging.
if !v {
b.sockstatLogger.Flush()
}
}
}
}
if setEnabled == nil || !slices.Contains(debuggableComponents, component) {
return fmt.Errorf("unknown component %q", component)
}
timeUnixOrZero := func(t time.Time) int64 {
if t.IsZero() {
return 0
}
return t.Unix()
}
ipn.PutStoreInt(b.store, componentStateKey(component), timeUnixOrZero(until))
now := time.Now()
on := now.Before(until)
setEnabled(on)
var onFor time.Duration
if on {
onFor = until.Sub(now)
b.logf("debugging logging for component %q enabled for %v (until %v)", component, onFor.Round(time.Second), until.UTC().Format(time.RFC3339))
} else {
b.logf("debugging logging for component %q disabled", component)
}
if oldSt, ok := b.componentLogUntil[component]; ok && oldSt.timer != nil {
oldSt.timer.Stop()
}
newSt := componentLogState{until: until}
if on {
newSt.timer = time.AfterFunc(onFor, func() {
// Turn off logging after the timer fires, as long as the state is
// unchanged when the timer actually fires.
b.mu.Lock()
defer b.mu.Unlock()
if ls := b.componentLogUntil[component]; ls.until.Equal(until) {
setEnabled(false)
b.logf("debugging logging for component %q disabled (by timer)", component)
}
})
}
mak.Set(&b.componentLogUntil, component, newSt)
return nil
}
// GetComponentDebugLogging gets the time that component's debug logging is
// enabled until, or the zero time if component's time is not currently
// enabled.
func (b *LocalBackend) GetComponentDebugLogging(component string) time.Time {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
ls := b.componentLogUntil[component]
if ls.until.IsZero() || ls.until.Before(now) {
return time.Time{}
}
return ls.until
}
// Dialer returns the backend's dialer.
// It is always non-nil.
func (b *LocalBackend) Dialer() *tsdial.Dialer {
return b.dialer
}
// SetDirectFileRoot sets the directory to download files to directly,
// without buffering them through an intermediate daemon-owned
// tailcfg.UserID-specific directory.
//
// This must be called before the LocalBackend starts being used.
func (b *LocalBackend) SetDirectFileRoot(dir string) {
b.mu.Lock()
defer b.mu.Unlock()
b.directFileRoot = dir
}
// SetDirectFileDoFinalRename sets whether the peerapi file server should rename
// a received "name.partial" file to "name" when the download is complete.
//
// This only applies when SetDirectFileRoot is non-empty.
// The default is false.
func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) {
b.mu.Lock()
defer b.mu.Unlock()
b.directFileDoFinalRename = v
}
// b.mu must be held.
func (b *LocalBackend) maybePauseControlClientLocked() {
if b.cc == nil {
return
}
networkUp := b.prevIfState.AnyInterfaceUp()
b.cc.SetPaused((b.state == ipn.Stopped && b.netMap != nil) || !networkUp)
}
// linkChange is our network monitor callback, called whenever the network changes.
// major is whether ifst is different than earlier.
func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
b.mu.Lock()
defer b.mu.Unlock()
hadPAC := b.prevIfState.HasPAC()
b.prevIfState = ifst
b.maybePauseControlClientLocked()
// If the PAC-ness of the network changed, reconfig wireguard+route to
// add/remove subnets.
if hadPAC != ifst.HasPAC() {
b.logf("linkChange: in state %v; PAC changed from %v->%v", b.state, hadPAC, ifst.HasPAC())
switch b.state {
case ipn.NoState, ipn.Stopped:
// Do nothing.
default:
go b.authReconfig()
}
}
// If the local network configuration has changed, our filter may
// need updating to tweak default routes.
b.updateFilterLocked(b.netMap, b.pm.CurrentPrefs())
if peerAPIListenAsync && b.netMap != nil && b.state == ipn.Running {
want := len(b.netMap.Addresses)
if len(b.peerAPIListeners) < want {
b.logf("linkChange: peerAPIListeners too low; trying again")
go b.initPeerAPIListener()
}
}
}
func (b *LocalBackend) onHealthChange(sys health.Subsystem, err error) {
if err == nil {
b.logf("health(%q): ok", sys)
} else {
b.logf("health(%q): error: %v", sys, err)
}
}
// Shutdown halts the backend and all its sub-components. The backend
// can no longer be used after Shutdown returns.
func (b *LocalBackend) Shutdown() {
b.mu.Lock()
if b.shutdownCalled {
b.mu.Unlock()
return
}
b.shutdownCalled = true
if b.loginFlags&controlclient.LoginEphemeral != 0 {
b.mu.Unlock()
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
defer cancel()
b.LogoutSync(ctx) // best effort
b.mu.Lock()
}
cc := b.cc
if b.sshServer != nil {
b.sshServer.Shutdown()
b.sshServer = nil
}
b.closePeerAPIListenersLocked()
if b.debugSink != nil {
b.e.InstallCaptureHook(nil)
b.debugSink.Close()
b.debugSink = nil
}
b.mu.Unlock()
if b.sockstatLogger != nil {
b.sockstatLogger.Shutdown()
}
b.unregisterNetMon()
b.unregisterHealthWatch()
if cc != nil {
cc.Shutdown()
}
b.ctxCancel()
b.e.Close()
b.e.Wait()
}
func stripKeysFromPrefs(p ipn.PrefsView) ipn.PrefsView {
if !p.Valid() || !p.Persist().Valid() {
return p
}
p2 := p.AsStruct()
p2.Persist.LegacyFrontendPrivateMachineKey = key.MachinePrivate{}
p2.Persist.PrivateNodeKey = key.NodePrivate{}
p2.Persist.OldPrivateNodeKey = key.NodePrivate{}
p2.Persist.NetworkLockKey = key.NLPrivate{}
return p2.View()
}
// Prefs returns a copy of b's current prefs, with any private keys removed.
func (b *LocalBackend) Prefs() ipn.PrefsView {
b.mu.Lock()
defer b.mu.Unlock()
return b.sanitizedPrefsLocked()
}
func (b *LocalBackend) sanitizedPrefsLocked() ipn.PrefsView {
return stripKeysFromPrefs(b.pm.CurrentPrefs())
}
// Status returns the latest status of the backend and its
// sub-components.
func (b *LocalBackend) Status() *ipnstate.Status {
sb := &ipnstate.StatusBuilder{WantPeers: true}
b.UpdateStatus(sb)
return sb.Status()
}
// StatusWithoutPeers is like Status but omits any details
// of peers.
func (b *LocalBackend) StatusWithoutPeers() *ipnstate.Status {
sb := &ipnstate.StatusBuilder{WantPeers: false}
b.UpdateStatus(sb)
return sb.Status()
}
// UpdateStatus implements ipnstate.StatusUpdater.
func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
b.e.UpdateStatus(sb)
var extraLocked func(*ipnstate.StatusBuilder)
if sb.WantPeers {
extraLocked = b.populatePeerStatusLocked
}
b.updateStatus(sb, extraLocked)
}
// updateStatus populates sb with status.
//
// extraLocked, if non-nil, is called while b.mu is still held.
func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func(*ipnstate.StatusBuilder)) {
b.mu.Lock()
defer b.mu.Unlock()
sb.MutateStatus(func(s *ipnstate.Status) {
s.Version = version.Long()
s.TUN = !b.sys.IsNetstack()
s.BackendState = b.state.String()
s.AuthURL = b.authURLSticky
if err := health.OverallError(); err != nil {
switch e := err.(type) {
case multierr.Error:
for _, err := range e.Errors() {
s.Health = append(s.Health, err.Error())
}
default:
s.Health = append(s.Health, err.Error())
}
}
if m := b.sshOnButUnusableHealthCheckMessageLocked(); m != "" {
s.Health = append(s.Health, m)
}
if version.IsUnstableBuild() {
s.Health = append(s.Health, "This is an unstable (development) version of Tailscale; frequent updates and bugs are likely")
}
if b.netMap != nil {
s.CertDomains = append([]string(nil), b.netMap.DNS.CertDomains...)
s.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
if s.CurrentTailnet == nil {
s.CurrentTailnet = &ipnstate.TailnetStatus{}
}
s.CurrentTailnet.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
s.CurrentTailnet.MagicDNSEnabled = b.netMap.DNS.Proxied
s.CurrentTailnet.Name = b.netMap.Domain
if prefs := b.pm.CurrentPrefs(); prefs.Valid() {
if !prefs.RouteAll() && b.netMap.AnyPeersAdvertiseRoutes() {
s.Health = append(s.Health, healthmsg.WarnAcceptRoutesOff)
}
if !prefs.ExitNodeID().IsZero() {
if exitPeer, ok := b.netMap.PeerWithStableID(prefs.ExitNodeID()); ok {
var online = false
if exitPeer.Online != nil {
online = *exitPeer.Online
}
s.ExitNodeStatus = &ipnstate.ExitNodeStatus{
ID: prefs.ExitNodeID(),
Online: online,
TailscaleIPs: exitPeer.Addresses,
}
}
}
}
}
})
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
ss.Online = health.GetInPollNetMap()
if b.netMap != nil {
ss.InNetworkMap = true
ss.HostName = b.netMap.Hostinfo.Hostname
ss.DNSName = b.netMap.Name
ss.UserID = b.netMap.User
if sn := b.netMap.SelfNode; sn != nil {
peerStatusFromNode(ss, sn)
if c := sn.Capabilities; len(c) > 0 {
ss.Capabilities = append([]string(nil), c...)
}
}
} else {
ss.HostName, _ = os.Hostname()
}
for _, pln := range b.peerAPIListeners {
ss.PeerAPIURL = append(ss.PeerAPIURL, pln.urlStr)
}
})
// TODO: hostinfo, and its networkinfo
// TODO: EngineStatus copy (and deprecate it?)
if extraLocked != nil {
extraLocked(sb)
}
}
func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
if b.netMap == nil {
return
}
for id, up := range b.netMap.UserProfiles {
sb.AddUser(id, up)
}
exitNodeID := b.pm.CurrentPrefs().ExitNodeID()
for _, p := range b.netMap.Peers {
var lastSeen time.Time
if p.LastSeen != nil {
lastSeen = *p.LastSeen
}
var tailscaleIPs = make([]netip.Addr, 0, len(p.Addresses))
for _, addr := range p.Addresses {
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
tailscaleIPs = append(tailscaleIPs, addr.Addr())
}
}
ps := &ipnstate.PeerStatus{
InNetworkMap: true,
UserID: p.User,
TailscaleIPs: tailscaleIPs,
HostName: p.Hostinfo.Hostname(),
DNSName: p.Name,
OS: p.Hostinfo.OS(),
LastSeen: lastSeen,
Online: p.Online != nil && *p.Online,
ShareeNode: p.Hostinfo.ShareeNode(),
ExitNode: p.StableID != "" && p.StableID == exitNodeID,
SSH_HostKeys: p.Hostinfo.SSH_HostKeys().AsSlice(),
Location: p.Hostinfo.Location(),
}
peerStatusFromNode(ps, p)
p4, p6 := peerAPIPorts(p)
if u := peerAPIURL(nodeIP(p, netip.Addr.Is4), p4); u != "" {
ps.PeerAPIURL = append(ps.PeerAPIURL, u)
}
if u := peerAPIURL(nodeIP(p, netip.Addr.Is6), p6); u != "" {
ps.PeerAPIURL = append(ps.PeerAPIURL, u)
}
sb.AddPeer(p.Key, ps)
}
}
// peerStatusFromNode copies fields that exist in the Node struct for
// current node and peers into the provided PeerStatus.
func peerStatusFromNode(ps *ipnstate.PeerStatus, n *tailcfg.Node) {
ps.ID = n.StableID
ps.Created = n.Created
ps.ExitNodeOption = tsaddr.ContainsExitRoutes(n.AllowedIPs)
if n.Tags != nil {
v := views.SliceOf(n.Tags)
ps.Tags = &v
}
if n.PrimaryRoutes != nil {
v := views.IPPrefixSliceOf(n.PrimaryRoutes)
ps.PrimaryRoutes = &v
}
if n.Expired {
ps.Expired = true
}
if t := n.KeyExpiry; !t.IsZero() {
t = t.Round(time.Second)
ps.KeyExpiry = &t
}
}
// WhoIs reports the node and user who owns the node with the given IP:port.
// If the IP address is a Tailscale IP, the provided port may be 0.
// If ok == true, n and u are valid.
func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
b.mu.Lock()
defer b.mu.Unlock()
n, ok = b.nodeByAddr[ipp.Addr()]
if !ok {
var ip netip.Addr
if ipp.Port() != 0 {
ip, ok = b.e.WhoIsIPPort(ipp)
}
if !ok {
return nil, u, false
}
n, ok = b.nodeByAddr[ip]
if !ok {
return nil, u, false
}
}
u, ok = b.netMap.UserProfiles[n.User]
if !ok {
return nil, u, false
}
return n, u, true
}
// PeerCaps returns the capabilities that remote src IP has to
// ths current node.
func (b *LocalBackend) PeerCaps(src netip.Addr) tailcfg.PeerCapMap {
b.mu.Lock()
defer b.mu.Unlock()
return b.peerCapsLocked(src)
}
func (b *LocalBackend) peerCapsLocked(src netip.Addr) tailcfg.PeerCapMap {
if b.netMap == nil {
return nil
}
filt := b.filterAtomic.Load()
if filt == nil {
return nil
}
for _, a := range b.netMap.Addresses {
if !a.IsSingleIP() {
continue
}
dst := a.Addr()
if dst.BitLen() == src.BitLen() { // match on family
return filt.CapsWithValues(src, dst)
}
}
return nil
}
// SetDecompressor sets a decompression function, which must be a zstd
// reader.
//
// This exists because the iOS/Mac NetworkExtension is very resource
// constrained, and the zstd package is too heavy to fit in the
// constrained RSS limit.
func (b *LocalBackend) SetDecompressor(fn func() (controlclient.Decompressor, error)) {
b.newDecompressor = fn
}
// setClientStatus is the callback invoked by the control client whenever it posts a new status.
// Among other things, this is where we update the netmap, packet filters, DNS and DERP maps.
func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// The following do not depend on any data for which we need to lock b.
if st.Err != nil {
// TODO(crawshaw): display in the UI.
if errors.Is(st.Err, io.EOF) {
b.logf("[v1] Received error: EOF")
return
}
b.logf("Received error: %v", st.Err)
var uerr controlclient.UserVisibleError
if errors.As(st.Err, &uerr) {
s := uerr.UserVisibleError()
b.send(ipn.Notify{ErrMessage: &s})
}
return
}
// Track the number of calls
currCall := b.numClientStatusCalls.Add(1)
b.mu.Lock()
// Handle node expiry in the netmap
if st.NetMap != nil {
now := time.Now()
b.em.flagExpiredPeers(st.NetMap, now)
// Always stop the existing netmap timer if we have a netmap;
// it's possible that we have no nodes expiring, so we should
// always cancel the timer and then possibly restart it below.
if b.nmExpiryTimer != nil {
// Ignore if we can't stop; the atomic check in the
// AfterFunc (below) will skip running.
b.nmExpiryTimer.Stop()
// Nil so we don't attempt to stop on the next netmap
b.nmExpiryTimer = nil
}
// Figure out when the next node in the netmap is expiring so we can
// start a timer to reconfigure at that point.
nextExpiry := b.em.nextPeerExpiry(st.NetMap, now)
if !nextExpiry.IsZero() {
tmrDuration := nextExpiry.Sub(now) + 10*time.Second
b.nmExpiryTimer = time.AfterFunc(tmrDuration, func() {
// Skip if the world has moved on past the
// saved call (e.g. if we race stopping this
// timer).
if b.numClientStatusCalls.Load() != currCall {
return
}
b.logf("setClientStatus: netmap expiry timer triggered after %v", tmrDuration)
// Call ourselves with the current status again; the logic in
// setClientStatus will take care of updating the expired field
// of peers in the netmap.
b.setClientStatus(st)
})
}
}
wasBlocked := b.blocked
keyExpiryExtended := false
if st.NetMap != nil {
wasExpired := b.keyExpired
isExpired := !st.NetMap.Expiry.IsZero() && st.NetMap.Expiry.Before(time.Now())
if wasExpired && !isExpired {
keyExpiryExtended = true
}
b.keyExpired = isExpired
}
b.mu.Unlock()
if keyExpiryExtended && wasBlocked {
// Key extended, unblock the engine
b.blockEngineUpdates(false)
}
if st.LoginFinished != nil && wasBlocked {
// Auth completed, unblock the engine
b.blockEngineUpdates(false)
b.authReconfig()
b.send(ipn.Notify{LoginFinished: &empty.Message{}})
}
// Lock b once and do only the things that require locking.
b.mu.Lock()
if st.LogoutFinished != nil {
if p := b.pm.CurrentPrefs(); !p.Persist().Valid() || p.Persist().LoginName() == "" {
b.mu.Unlock()
return
}
if err := b.pm.DeleteProfile(b.pm.CurrentProfile().ID); err != nil {
b.logf("error deleting profile: %v", err)
}
if err := b.resetForProfileChangeLockedOnEntry(); err != nil {
b.logf("resetForProfileChangeLockedOnEntry err: %v", err)
}
return
}
prefsChanged := false
prefs := b.pm.CurrentPrefs().AsStruct()
netMap := b.netMap
interact := b.interact
ipn{,/ipnlocal}, cmd/tailscale/cli: don't check pref reverts on initial up The ipn.NewPrefs func returns a populated ipn.Prefs for historical reasons. It's not used or as important as it once was, but it hasn't yet been removed. Meanwhile, it contains some default values that are used on some platforms. Notably, for this bug (#1725), Windows/Mac use its Prefs.RouteAll true value (to accept subnets), but Linux users have always gotten a "false" value for that, because that's what cmd/tailscale's CLI default flag is _for all operating systems_. That meant that "tailscale up" was rightfully reporting that the user was changing an implicit setting: RouteAll was changing from true with false with the user explicitly saying so. An obvious fix might be to change ipn.NewPrefs to return Prefs.RouteAll == false on some platforms, but the logic is complicated by darwin: we want RouteAll true on windows, android, ios, and the GUI mac app, but not the CLI tailscaled-on-macOS mode. But even if we used build tags (e.g. the "redo" build tag) to determine what the default is, that then means we have duplicated and differing "defaults" between both the CLI up flags and ipn.NewPrefs. Furthering that complication didn't seem like a good idea. So, changing the NewPrefs defaults is too invasive at this stage of the release, as is removing the NewPrefs func entirely. Instead, tweak slightly the semantics of the ipn.Prefs.ControlURL field. This now defines that a ControlURL of the empty string means both "we're uninitialized" and also "just use the default". Then, once we have the "empty-string-means-unintialized" semantics, use that to suppress "tailscale up"'s recent implicit-setting-revert checking safety net, if we've never initialized Tailscale yet. And update/add tests. Fixes #1725 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-18 06:50:58 +01:00
if prefs.ControlURL == "" {
// Once we get a message from the control plane, set
// our ControlURL pref explicitly. This causes a
// future "tailscale up" to start checking for
// implicit setting reverts, which it doesn't do when
// ControlURL is blank.
prefs.ControlURL = prefs.ControlURLOrDefault()
prefsChanged = true
}
if st.Persist != nil && st.Persist.Valid() {
if !prefs.Persist.View().Equals(*st.Persist) {
prefsChanged = true
prefs.Persist = st.Persist.AsStruct()
}
}
if st.URL != "" {
b.authURL = st.URL
b.authURLSticky = st.URL
}
if wasBlocked && st.LoginFinished != nil {
// Interactive login finished successfully (URL visited).
// After an interactive login, the user always wants
// WantRunning.
if !prefs.WantRunning || prefs.LoggedOut {
prefsChanged = true
}
prefs.WantRunning = true
prefs.LoggedOut = false
}
if findExitNodeIDLocked(prefs, st.NetMap) {
prefsChanged = true
}
// Perform all mutations of prefs based on the netmap here.
if st.NetMap != nil {
if b.updatePersistFromNetMapLocked(st.NetMap, prefs) {
prefsChanged = true
}
}
// Prefs will be written out if stale; this is not safe unless locked or cloned.
if prefsChanged {
if err := b.pm.SetPrefs(prefs.View()); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
}
}
// initTKALocked is dependent on CurrentProfile.ID, which is initialized
// (for new profiles) on the first call to b.pm.SetPrefs.
if err := b.initTKALocked(); err != nil {
b.logf("initTKALocked: %v", err)
}
// Perform all reconfiguration based on the netmap here.
if st.NetMap != nil {
b.capTailnetLock = hasCapability(st.NetMap, tailcfg.CapabilityTailnetLock)
b.mu.Unlock() // respect locking rules for tkaSyncIfNeeded
if err := b.tkaSyncIfNeeded(st.NetMap, prefs.View()); err != nil {
b.logf("[v1] TKA sync error: %v", err)
}
b.mu.Lock()
if b.tka != nil {
head, err := b.tka.authority.Head().MarshalText()
if err != nil {
b.logf("[v1] error marshalling tka head: %v", err)
} else {
b.cc.SetTKAHead(string(head))
}
} else {
b.cc.SetTKAHead("")
}
if !envknob.TKASkipSignatureCheck() {
b.tkaFilterNetmapLocked(st.NetMap)
}
b.setNetMapLocked(st.NetMap)
b.updateFilterLocked(st.NetMap, prefs.View())
}
b.mu.Unlock()
// Now complete the lock-free parts of what we started while locked.
if prefsChanged {
b.send(ipn.Notify{Prefs: ptr.To(prefs.View())})
}
if st.NetMap != nil {
if envknob.NoLogsNoSupport() && hasCapability(st.NetMap, tailcfg.CapabilityDataPlaneAuditLogs) {
msg := "tailnet requires logging to be enabled. Remove --no-logs-no-support from tailscaled command line."
health.SetLocalLogConfigHealth(errors.New(msg))
// Connecting to this tailnet without logging is forbidden; boot us outta here.
b.mu.Lock()
prefs.WantRunning = false
p := prefs.View()
if err := b.pm.SetPrefs(p); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
}
b.mu.Unlock()
b.send(ipn.Notify{ErrMessage: &msg, Prefs: &p})
return
}
if netMap != nil {
diff := st.NetMap.ConciseDiffFrom(netMap)
if strings.TrimSpace(diff) == "" {
b.logf("[v1] netmap diff: (none)")
} else {
b.logf("[v1] netmap diff:\n%v", diff)
}
}
b.e.SetNetworkMap(st.NetMap)
b.e.SetDERPMap(st.NetMap.DERPMap)
// Update our cached DERP map
dnsfallback.UpdateCache(st.NetMap.DERPMap, b.logf)
b.send(ipn.Notify{NetMap: st.NetMap})
}
if st.URL != "" {
b.logf("Received auth URL: %.20v...", st.URL)
2020-10-27 20:57:10 +00:00
if interact {
b.popBrowserAuthNow()
}
}
b.stateMachine()
// This is currently (2020-07-28) necessary; conditionally disabling it is fragile!
// This is where netmap information gets propagated to router and magicsock.
b.authReconfig()
}
// findExitNodeIDLocked updates prefs to reference an exit node by ID, rather
// than by IP. It returns whether prefs was mutated.
func findExitNodeIDLocked(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool) {
if nm == nil {
// No netmap, can't resolve anything.
return false
}
// If we have a desired IP on file, try to find the corresponding
// node.
if !prefs.ExitNodeIP.IsValid() {
return false
}
// IP takes precedence over ID, so if both are set, clear ID.
if prefs.ExitNodeID != "" {
prefs.ExitNodeID = ""
prefsChanged = true
}
for _, peer := range nm.Peers {
for _, addr := range peer.Addresses {
if !addr.IsSingleIP() || addr.Addr() != prefs.ExitNodeIP {
continue
}
// Found the node being referenced, upgrade prefs to
// reference it directly for next time.
prefs.ExitNodeID = peer.StableID
prefs.ExitNodeIP = netip.Addr{}
return true
}
}
return prefsChanged
}
// setWgengineStatus is the callback by the wireguard engine whenever it posts a new status.
// This updates the endpoints both in the backend and in the control client.
func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
if err != nil {
b.logf("wgengine status error: %v", err)
b.broadcastStatusChanged()
return
}
if s == nil {
b.logf("[unexpected] non-error wgengine update with status=nil: %v", s)
b.broadcastStatusChanged()
return
}
b.mu.Lock()
if s.AsOf.Before(b.lastStatusTime) {
// Don't process a status update that is older than the one we have
// already processed. (corp#2579)
b.mu.Unlock()
return
}
b.lastStatusTime = s.AsOf
es := b.parseWgStatusLocked(s)
cc := b.cc
b.engineStatus = es
needUpdateEndpoints := !endpointsEqual(s.LocalAddrs, b.endpoints)
if needUpdateEndpoints {
b.endpoints = append([]tailcfg.Endpoint{}, s.LocalAddrs...)
}
b.mu.Unlock()
if cc != nil {
if needUpdateEndpoints {
cc.UpdateEndpoints(s.LocalAddrs)
}
b.stateMachine()
}
b.broadcastStatusChanged()
b.send(ipn.Notify{Engine: &es})
}
func (b *LocalBackend) broadcastStatusChanged() {
// The sync.Cond docs say: "It is allowed but not required for the caller to hold c.L during the call."
// In this particular case, we must acquire b.statusLock. Otherwise we might broadcast before
// the waiter (in requestEngineStatusAndWait) starts to wait, in which case
// the waiter can get stuck indefinitely. See PR 2865.
b.statusLock.Lock()
b.statusChanged.Broadcast()
b.statusLock.Unlock()
}
func endpointsEqual(x, y []tailcfg.Endpoint) bool {
if len(x) != len(y) {
return false
}
for i := range x {
if x[i] != y[i] {
return false
}
}
return true
}
func (b *LocalBackend) SetNotifyCallback(notify func(ipn.Notify)) {
b.mu.Lock()
defer b.mu.Unlock()
b.notify = notify
}
// SetHTTPTestClient sets an alternate HTTP client to use with
// connections to the coordination server. It exists for
// testing. Using nil means to use the default.
func (b *LocalBackend) SetHTTPTestClient(c *http.Client) {
b.mu.Lock()
defer b.mu.Unlock()
b.httpTestClient = c
}
// SetControlClientGetterForTesting sets the func that creates a
// control plane client. It can be called at most once, before Start.
func (b *LocalBackend) SetControlClientGetterForTesting(newControlClient func(controlclient.Options) (controlclient.Client, error)) {
b.mu.Lock()
defer b.mu.Unlock()
if b.ccGen != nil {
panic("invalid use of SetControlClientGetterForTesting after Start")
}
b.ccGen = newControlClient
}
func (b *LocalBackend) getNewControlClientFunc() clientGen {
b.mu.Lock()
defer b.mu.Unlock()
if b.ccGen == nil {
// Initialize it rather than just returning the
// default to make any future call to
// SetControlClientGetterForTesting panic.
b.ccGen = func(opts controlclient.Options) (controlclient.Client, error) {
return controlclient.New(opts)
}
}
return b.ccGen