Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
package controlclient
import ( "context" "errors" "fmt" "net/http" "sync" "sync/atomic" "time"
"tailscale.com/net/sockstats" "tailscale.com/tailcfg" "tailscale.com/tstime" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/types/netmap" "tailscale.com/types/persist" "tailscale.com/types/structs" "tailscale.com/types/views" "tailscale.com/util/backoff" "tailscale.com/util/clientmetric" "tailscale.com/util/execqueue" "tailscale.com/util/testenv" "tailscale.com/wgengine/filter")
type LoginGoal struct { _ structs.Incomparable flags LoginFlags // flags to use when logging in url string // auth url that needs to be visited}
var _ Client = (*Auto)(nil)
// maxRetryWindow defines the upper bound on how long control is allowed// to tell the client to wait before retrying a request.const maxRetryWindow = 5 * time.Minute
// waitUnpause waits until either the client is unpaused or the Auto client is// shut down. It reports whether the client should keep running (i.e. it's not// closed).func (c *Auto) waitUnpause(routineLogName string) (keepRunning bool) { c.mu.Lock() if !c.paused || c.closed { defer c.mu.Unlock() return !c.closed } unpaused := c.unpausedChanLocked() c.mu.Unlock()
c.logf("%s: awaiting unpause", routineLogName) return <-unpaused}
// updateRoutine is responsible for informing the server of worthy changes to// our local state. It runs in its own goroutine.func (c *Auto) updateRoutine() { defer close(c.updateDone) bo := backoff.NewBackoff("updateRoutine", c.logf, 30*time.Second)
// lastUpdateGenInformed is the value of lastUpdateAt that we've successfully // informed the server of. var lastUpdateGenInformed updateGen
for { if !c.waitUnpause("updateRoutine") { c.logf("updateRoutine: exiting") return } c.mu.Lock() gen := c.lastUpdateGen ctx := c.mapCtx needUpdate := gen > 0 && gen != lastUpdateGenInformed && c.loggedIn c.mu.Unlock()
if !needUpdate { // Nothing to do, wait for a signal. select { case <-ctx.Done(): continue case <-c.updateCh: continue } }
t0 := c.clock.Now() err := c.direct.SendUpdate(ctx) d := time.Since(t0).Round(time.Millisecond) if err != nil { if ctx.Err() == nil { c.direct.logf("lite map update error after %v: %v", d, err) } if rle, rateLimited := errors.AsType[*rateLimitError](err); rateLimited { c.waitRetryAfter(ctx, "updateRoutine", rle) } else { bo.BackOff(ctx, err) } continue } bo.Reset() c.direct.logf("[v1] successful lite map update in %v", d)
lastUpdateGenInformed = gen }}
// atomicGen is an atomic int64 generator. It is used to generate monotonically// increasing numbers for updateGen.var atomicGen atomic.Int64
func nextUpdateGen() updateGen { return updateGen(atomicGen.Add(1))}
// updateGen is a monotonically increasing number that represents a particular// update to the local state.type updateGen int64
// Auto connects to a tailcontrol server for a node.// It's a concrete implementation of the Client interface.type Auto struct { direct *Direct // our interface to the server APIs clock tstime.Clock logf logger.Logf closed bool updateCh chan struct{} // readable when we should inform the server of a change observer Observer // if non-nil, called to update Client status observerQueue execqueue.ExecQueue shutdownFn func() // to be called prior to shutdown or nil
mu sync.Mutex // mutex guards the following fields
started bool // whether [Auto.Start] has been called wantLoggedIn bool // whether the user wants to be logged in per last method call urlToVisit string // the last url we were told to visit expiry time.Time
// lastUpdateGen is the gen of last update we had an update worth sending to // the server. lastUpdateGen updateGen
lastStatus atomic.Pointer[Status]
paused bool // whether we should stop making HTTP requests unpauseWaiters []chan bool // chans that gets sent true (once) on wake, or false on Shutdown loggedIn bool // true if currently logged in loginGoal *LoginGoal // non-nil if some login activity is desired inMapPoll bool // true once we get the first MapResponse in a stream; false when HTTP response ends
authCtx context.Context // context used for auth requests mapCtx context.Context // context used for netmap and update requests authCancel func() // cancel authCtx mapCancel func() // cancel mapCtx authDone chan struct{} // when closed, authRoutine is done mapDone chan struct{} // when closed, mapRoutine is done updateDone chan struct{} // when closed, updateRoutine is done}
// New creates and starts a new Auto.func New(opts Options) (*Auto, error) { c, err := newNoStart(opts) if err != nil { return nil, err } if opts.StartPaused { c.SetPaused(true) } if !opts.SkipStartForTests { c.start() } return c, err}
// newNoStart creates a new Auto, but without calling Start on it.func newNoStart(opts Options) (_ *Auto, err error) { direct, err := NewDirect(opts) if err != nil { return nil, err } defer func() { if err != nil { direct.Close() } }()
if opts.Logf == nil { opts.Logf = func(fmt string, args ...any) {} } if opts.Clock == nil { opts.Clock = tstime.StdClock{} } c := &Auto{ direct: direct, clock: opts.Clock, logf: opts.Logf, updateCh: make(chan struct{}, 1), authDone: make(chan struct{}), mapDone: make(chan struct{}), updateDone: make(chan struct{}), observer: opts.Observer, shutdownFn: opts.Shutdown, }
c.authCtx, c.authCancel = context.WithCancel(context.Background()) c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, opts.Logf)
c.mapCtx, c.mapCancel = context.WithCancel(context.Background()) c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, opts.Logf)
return c, nil}
// SetPaused controls whether HTTP activity should be paused.//// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.func (c *Auto) SetPaused(paused bool) { c.mu.Lock() defer c.mu.Unlock() if paused == c.paused || c.closed { return } c.logf("setPaused(%v)", paused) c.paused = paused if paused { c.cancelMapCtxLocked() c.cancelAuthCtxLocked() return } for _, ch := range c.unpauseWaiters { ch <- true } c.unpauseWaiters = nil}
// StartForTest starts the client's goroutines.//// It should only be called for clients created with [Options.SkipStartForTests].func (c *Auto) StartForTest() { testenv.AssertInTest() c.start()}
func (c *Auto) start() { c.mu.Lock() defer c.mu.Unlock() if c.started { return } c.started = true go c.authRoutine() go c.mapRoutine() go c.updateRoutine()}
// updateControl sends a new OmitPeers, non-streaming map request (to just send// Hostinfo/Netinfo/Endpoints info, while keeping an existing streaming response// open).//// It should be called whenever there's something new to tell the server.func (c *Auto) updateControl() { gen := nextUpdateGen() c.mu.Lock() if gen < c.lastUpdateGen { // This update is out of date. c.mu.Unlock() return } c.lastUpdateGen = gen c.mu.Unlock()
select { case c.updateCh <- struct{}{}: default: }}
// cancelAuthCtxLocked is like cancelAuthCtx, but assumes the caller holds c.mu.func (c *Auto) cancelAuthCtxLocked() { if c.authCancel != nil { c.authCancel() } if !c.closed { c.authCtx, c.authCancel = context.WithCancel(context.Background()) c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, c.logf) }}
// cancelMapCtxLocked is like cancelMapCtx, but assumes the caller holds c.mu.func (c *Auto) cancelMapCtxLocked() { if c.mapCancel != nil { c.mapCancel() } if !c.closed { c.mapCtx, c.mapCancel = context.WithCancel(context.Background()) c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, c.logf) }}
// restartMap cancels the existing mapPoll and liteUpdates, and then starts a// new one.func (c *Auto) restartMap() { c.mu.Lock() c.cancelMapCtxLocked() synced := c.inMapPoll c.mu.Unlock()
c.logf("[v1] restartMap: synced=%v", synced) c.updateControl()}
func (c *Auto) authRoutine() { defer close(c.authDone) bo := backoff.NewBackoff("authRoutine", c.logf, 30*time.Second)
for { if !c.waitUnpause("authRoutine") { c.logf("authRoutine: exiting") return } c.mu.Lock() goal := c.loginGoal ctx := c.authCtx loggedIn := c.loggedIn if goal != nil { c.logf("[v1] authRoutine: loggedIn=%v; wantLoggedIn=%v", loggedIn, true) } else { c.logf("[v1] authRoutine: loggedIn=%v; goal=nil paused=%v", loggedIn, c.paused) } c.mu.Unlock()
report := func(err error, msg string) { c.logf("[v1] %s: %v", msg, err) // don't send status updates for context errors, // since context cancelation is always on purpose. if ctx.Err() == nil { c.sendStatus("authRoutine-report", err, "", nil) } }
if goal == nil { c.direct.health.SetAuthRoutineInError(nil) // Wait for user to Login or Logout. <-ctx.Done() c.logf("[v1] authRoutine: context done.") continue }
c.mu.Lock() c.urlToVisit = goal.url c.mu.Unlock()
var url string var err error var f string if goal.url != "" { url, err = c.direct.WaitLoginURL(ctx, goal.url) f = "WaitLoginURL" } else { url, err = c.direct.TryLogin(ctx, goal.flags) f = "TryLogin" } if err != nil { c.direct.health.SetAuthRoutineInError(err) report(err, f) if rle, ok := errors.AsType[*rateLimitError](err); ok { c.waitRetryAfter(ctx, "authRoutine", rle) } else { bo.BackOff(ctx, err) } continue } if url != "" { // goal.url ought to be empty here. However, not all control servers // get this right, and logging about it here just generates noise. // // TODO(bradfitz): I don't follow that comment. Our own testcontrol // used by tstest/integration hits this path, in fact. if c.direct.panicOnUse { panic("tainted client") } c.mu.Lock() c.urlToVisit = url // Only store the URL follow-up goal if no concurrent Login() call has // replaced the goal we were processing while our control plane request // was in flight. Otherwise, the intent from the more recent goal gets // lost. if c.loginGoal == goal { c.loginGoal = &LoginGoal{ flags: LoginDefault, url: url, } } c.mu.Unlock()
c.sendStatus("authRoutine-url", err, url, nil) if goal.url == url { // The server sent us the same URL we already tried, // backoff to avoid a busy loop. bo.BackOff(ctx, errors.New("login URL not changing")) } else { bo.Reset() } continue }
// success c.direct.health.SetAuthRoutineInError(nil) c.mu.Lock() // Only commit the login success if no concurrent Login() // call has reset the goal and no Logout() has moved on // while our control plane request was in flight. In the // first case, clearing the goal would prevent the next // iteration from picking it up and running with it. In // the second case, we would record that we're loggedIn // even though we're logged out. goalStillCurrentGoal := c.loginGoal == goal if goalStillCurrentGoal { c.urlToVisit = "" c.loggedIn = true c.loginGoal = nil } c.mu.Unlock()
if goalStillCurrentGoal { c.sendStatus("authRoutine-success", nil, "", nil) c.restartMap() } bo.Reset() }}
// ExpiryForTests returns the credential expiration time, or the zero value if// the expiration time isn't known. It's used in tests only.func (c *Auto) ExpiryForTests() time.Time { c.mu.Lock() defer c.mu.Unlock() return c.expiry}
// DirectForTest returns the underlying direct client object.// It's used in tests only.func (c *Auto) DirectForTest() *Direct { return c.direct}
// unpausedChanLocked returns a new channel that gets sent// either a true when unpaused or false on Auto.Shutdown.//// c.mu must be heldfunc (c *Auto) unpausedChanLocked() <-chan bool { unpaused := make(chan bool, 1) c.unpauseWaiters = append(c.unpauseWaiters, unpaused) return unpaused}
// ClientID returns the ClientID of the direct controlClientfunc (c *Auto) ClientID() int64 { return c.direct.ClientID()}
// mapRoutineState is the state of Auto.mapRoutine while it's running.type mapRoutineState struct { c *Auto bo *backoff.Backoff}
var _ NetmapDeltaUpdater = mapRoutineState{}
func (mrs mapRoutineState) UpdateFullNetmap(nm *netmap.NetworkMap) { c := mrs.c
c.mu.Lock() c.inMapPoll = true c.expiry = nm.SelfKeyExpiry() stillAuthed := c.loggedIn c.logf("[v1] mapRoutine: netmap received: loggedIn=%v inMapPoll=true", stillAuthed)
// Reset the backoff timer if we got a netmap. mrs.bo.Reset() c.mu.Unlock()
if stillAuthed { c.sendStatus("mapRoutine-got-netmap", nil, "", nm) }}
func (mrs mapRoutineState) UpdateNetmapDelta(muts []netmap.NodeMutation) bool { c := mrs.c
c.mu.Lock() goodState := c.loggedIn && c.inMapPoll ndu, canDelta := c.observer.(NetmapDeltaUpdater) mapCtx := c.mapCtx c.mu.Unlock()
if !goodState || !canDelta { return false }
ctx, cancel := context.WithTimeout(mapCtx, 2*time.Second) defer cancel()
ch := make(chan bool, 1) c.observerQueue.Add(func() { ch <- ndu.UpdateNetmapDelta(muts) }) select { case ok := <-ch: return ok case <-ctx.Done(): return false }}
var ( _ PacketFilterUpdater = mapRoutineState{} _ UserProfileUpdater = mapRoutineState{})
// UpdatePacketFilter implements [PacketFilterUpdater] by forwarding to// [Auto.observer] if it implements [PacketFilterUpdater]. It returns// false (signaling fall back to a full netmap rebuild) if the// downstream observer doesn't implement [PacketFilterUpdater] or isn't// in a state to accept updates.func (mrs mapRoutineState) UpdatePacketFilter(rules views.Slice[tailcfg.FilterRule], parsed []filter.Match) bool { c := mrs.c c.mu.Lock() goodState := c.loggedIn && c.inMapPoll pfu, ok := c.observer.(PacketFilterUpdater) mapCtx := c.mapCtx c.mu.Unlock() if !goodState || !ok { return false } ctx, cancel := context.WithTimeout(mapCtx, 2*time.Second) defer cancel() ch := make(chan bool, 1) c.observerQueue.Add(func() { ch <- pfu.UpdatePacketFilter(rules, parsed) }) select { case applied := <-ch: return applied case <-ctx.Done(): return false }}
// UpdateUserProfiles implements [UserProfileUpdater] by forwarding to// [Auto.observer] if it implements [UserProfileUpdater]. It returns// false (signaling fall back to a full netmap rebuild) if the// downstream observer doesn't implement [UserProfileUpdater] or isn't// in a state to accept updates.func (mrs mapRoutineState) UpdateUserProfiles(profiles map[tailcfg.UserID]tailcfg.UserProfileView) bool { c := mrs.c c.mu.Lock() goodState := c.loggedIn && c.inMapPoll upu, ok := c.observer.(UserProfileUpdater) mapCtx := c.mapCtx c.mu.Unlock() if !goodState || !ok { return false } ctx, cancel := context.WithTimeout(mapCtx, 2*time.Second) defer cancel() ch := make(chan bool, 1) c.observerQueue.Add(func() { ch <- upu.UpdateUserProfiles(profiles) }) select { case applied := <-ch: return applied case <-ctx.Done(): return false }}
// mapRoutine is responsible for keeping a read-only streaming connection to the// control server, and keeping the netmap up to date.func (c *Auto) mapRoutine() { defer close(c.mapDone) mrs := mapRoutineState{ c: c, bo: backoff.NewBackoff("mapRoutine", c.logf, 30*time.Second), }
for { if !c.waitUnpause("mapRoutine") { c.logf("mapRoutine: exiting") return }
c.mu.Lock() loggedIn := c.loggedIn c.logf("[v1] mapRoutine: loggedIn=%v", loggedIn) ctx := c.mapCtx c.mu.Unlock()
report := func(err error, msg string) { c.logf("[v1] %s: %v", msg, err) err = fmt.Errorf("%s: %w", msg, err) // don't send status updates for context errors, // since context cancelation is always on purpose. if ctx.Err() == nil { c.sendStatus("mapRoutine1", err, "", nil) } }
if !loggedIn { // Wait for something interesting to happen c.mu.Lock() c.inMapPoll = false c.mu.Unlock()
<-ctx.Done() c.logf("[v1] mapRoutine: context done.") continue } c.direct.health.SetOutOfPollNetMap()
err := c.direct.PollNetMap(ctx, mrs)
c.direct.health.SetOutOfPollNetMap() c.mu.Lock() c.inMapPoll = false paused := c.paused c.mu.Unlock()
rle, rateLimited := errors.AsType[*rateLimitError](err)
if paused { mrs.bo.Reset() } else if !rateLimited { mrs.bo.BackOff(ctx, err) }
if paused { c.logf("mapRoutine: paused") } else { report(err, "PollNetMap") }
if rateLimited { c.waitRetryAfter(ctx, "mapRoutine", rle) } }}
// waitRetryAfter sleeps for the delay the server requested in rle, capped at// [maxRetryWindow] or until ctx is done, whichever comes first.func (c *Auto) waitRetryAfter(ctx context.Context, routine string, rle *rateLimitError) { if rle.retryAfter > maxRetryWindow { rle.retryAfter = maxRetryWindow }
c.logf("%s: %s", routine, rle)
t, ch := c.clock.NewTimer(rle.retryAfter) defer t.Stop()
select { case <-ctx.Done(): case <-ch: }}
func (c *Auto) AuthCantContinue() bool { if c == nil { return true } c.mu.Lock() defer c.mu.Unlock()
return !c.loggedIn && (c.loginGoal == nil || c.loginGoal.url != "")}
func (c *Auto) SetHostinfo(hi *tailcfg.Hostinfo) { if hi == nil { panic("nil Hostinfo") } if !c.direct.SetHostinfo(hi) { // No changes. Don't log. return }
// Send new Hostinfo to server c.updateControl()}
func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) { if ni == nil { panic("nil NetInfo") } if !c.direct.SetNetInfo(ni) { return }
// Send new NetInfo to server c.updateControl()}
// SetTKAHead updates the TKA head hash that map-request infrastructure sends.func (c *Auto) SetTKAHead(headHash string) { if !c.direct.SetTKAHead(headHash) { return }
// Send new TKAHead to server c.updateControl()}
// sendStatus can not be called with the c.mu held.func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) { c.mu.Lock() if c.closed { c.mu.Unlock() return } loggedIn := c.loggedIn inMapPoll := c.inMapPoll loginGoal := c.loginGoal c.mu.Unlock()
c.logf("[v1] sendStatus: %s: loggedIn=%v inMapPoll=%v", who, loggedIn, inMapPoll)
var p persist.PersistView if nm != nil && loggedIn && inMapPoll { p = c.direct.GetPersist() } else { // don't send netmap status, as it's misleading when we're // not logged in. nm = nil } newSt := &Status{ URL: url, Persist: p, NetMap: nm, Err: err, LoggedIn: loggedIn && loginGoal == nil, InMapPoll: inMapPoll, }
if c.observer == nil { return }
c.lastStatus.Store(newSt)
// Launch a new goroutine to avoid blocking the caller while the observer // does its thing, which may result in a call back into the client. metricQueued.Add(1) c.observerQueue.Add(func() { c.mu.Lock() closed := c.closed c.mu.Unlock() if closed { return }
if canSkipStatus(newSt, c.lastStatus.Load()) { metricSkippable.Add(1) if !c.direct.controlKnobs.DisableSkipStatusQueue.Load() { metricSkipped.Add(1) return } } c.observer.SetControlClientStatus(c, *newSt)
// Best effort stop retaining the memory now that we've sent it to the // observer (LocalBackend). We CAS here because the caller goroutine is // doing a Store which we want to win a race. This is only a memory // optimization and is not for correctness. // // If the CAS fails, that means somebody else's Store replaced our // pointer (so mission accomplished: our netmap is no longer retained in // any case) and that Store caller will be responsible for removing // their own netmap (or losing their race too, down the chain). // Eventually the last caller will win this CAS and zero lastStatus. c.lastStatus.CompareAndSwap(newSt, nil) })}
var ( metricQueued = clientmetric.NewCounter("controlclient_auto_status_queued") metricSkippable = clientmetric.NewCounter("controlclient_auto_status_queue_skippable") metricSkipped = clientmetric.NewCounter("controlclient_auto_status_queue_skipped"))
// canSkipStatus reports whether we can skip sending s1, knowing// that s2 is enqueued sometime in the future after s1.//// s1 must be non-nil. s2 may be nil.func canSkipStatus(s1, s2 *Status) bool { if s2 == nil { // Nothing in the future. return false } if s1 == s2 { // If the last item in the queue is the same as s1, // we can't skip it. return false } if s1.Err != nil || s1.URL != "" { // If s1 has an error or an URL, we shouldn't skip it, lest the error go // away in s2 or in-between. We want to make sure all the subsystems see // it. Plus there aren't many of these, so not worth skipping. return false } if !s1.Persist.Equals(s2.Persist) || s1.LoggedIn != s2.LoggedIn || s1.InMapPoll != s2.InMapPoll || s1.URL != s2.URL { // If s1 has a different Persist, has changed login state, changed map // poll state, or has a new login URL, don't skip it. We only care about // skipping the typical entries where the only difference is the NetMap. return false } // If nothing above precludes it, and both s1 and s2 have NetMaps, then // we can skip it, because s2's NetMap is a newer version and we can // jump straight from whatever state we had before to s2's state, // without passing through s1's state first. A NetMap is regrettably a // full snapshot of the state, not an incremental delta. We're slowly // moving towards passing around only deltas around internally at all // layers, but this is explicitly the case where we didn't have a delta // path for the message we received over the wire and had to resort // to the legacy full NetMap path. And then we can get behind processing // these full NetMap snapshots in LocalBackend/wgengine/magicsock/netstack // and this path (when it returns true) lets us skip over useless work // and not get behind in the queue. This matters in particular for tailnets // that are both very large + very churny. return s1.NetMap != nil && s2.NetMap != nil}
func (c *Auto) Login(flags LoginFlags) { c.logf("client.Login(%v)", flags)
c.mu.Lock() defer c.mu.Unlock() if c.closed { return } if c.direct != nil && c.direct.panicOnUse { panic("tainted client") } c.wantLoggedIn = true c.loginGoal = &LoginGoal{ flags: flags, } c.cancelMapCtxLocked() c.cancelAuthCtxLocked()}
var ErrClientClosed = errors.New("client closed")
func (c *Auto) Logout(ctx context.Context) error { c.logf("client.Logout()") c.mu.Lock() c.wantLoggedIn = false c.loginGoal = nil closed := c.closed if c.direct != nil && c.direct.panicOnUse { panic("tainted client") } c.mu.Unlock()
if closed { return ErrClientClosed }
if err := c.direct.TryLogout(ctx); err != nil { return err } c.mu.Lock() c.loggedIn = false c.cancelAuthCtxLocked() c.cancelMapCtxLocked() c.mu.Unlock()
c.sendStatus("authRoutine-wantout", nil, "", nil) return nil}
func (c *Auto) SetExpirySooner(ctx context.Context, expiry time.Time) error { return c.direct.SetExpirySooner(ctx, expiry)}
// UpdateEndpoints sets the client's discovered endpoints and sends// them to the control server if they've changed.//// It does not retain the provided slice.func (c *Auto) UpdateEndpoints(endpoints []tailcfg.Endpoint) { changed := c.direct.SetEndpoints(endpoints) if changed { c.updateControl() }}
// SetDiscoPublicKey sets the client's Disco public to key and sends the change// to the control server.func (c *Auto) SetDiscoPublicKey(key key.DiscoPublic) { c.direct.SetDiscoPublicKey(key) c.updateControl()}
// SetIPForwardingBroken updates the IP forwarding broken state and sends// a control update if the value changed.func (c *Auto) SetIPForwardingBroken(v bool) { if !c.direct.SetIPForwardingBroken(v) { return } c.updateControl()}
func (c *Auto) Shutdown() { c.mu.Lock() if c.closed { c.mu.Unlock() return } c.logf("client.Shutdown ...") shutdownFn := c.shutdownFn
direct := c.direct c.closed = true c.observerQueue.Shutdown() c.cancelAuthCtxLocked() c.cancelMapCtxLocked() for _, w := range c.unpauseWaiters { w <- false } c.unpauseWaiters = nil c.mu.Unlock()
if shutdownFn != nil { shutdownFn() }
<-c.authDone <-c.mapDone <-c.updateDone if direct != nil { direct.Close() } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() c.observerQueue.Wait(ctx) c.logf("Client.Shutdown done.")}
// NodePublicKey returns the node public key currently in use. This is// used exclusively in tests.func (c *Auto) TestOnlyNodePublicKey() key.NodePublic { priv := c.direct.GetPersist() return priv.PrivateNodeKey().Public()}
func (c *Auto) TestOnlySetAuthKey(authkey string) { c.direct.mu.Lock() defer c.direct.mu.Unlock() c.direct.authKey = authkey}
func (c *Auto) TestOnlyTimeNow() time.Time { return c.clock.Now()}
// SetDNS sends the SetDNSRequest request to the control plane server,// requesting a DNS record be created or updated.func (c *Auto) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error { return c.direct.SetDNS(ctx, req)}
func (c *Auto) DoNoiseRequest(req *http.Request) (*http.Response, error) { return c.direct.DoNoiseRequest(req)}