Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+169
-31
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package controlclient
|
||||
@@ -20,10 +20,12 @@ import (
|
||||
"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 {
|
||||
@@ -34,6 +36,10 @@ type LoginGoal struct {
|
||||
|
||||
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).
|
||||
@@ -88,10 +94,14 @@ func (c *Auto) updateRoutine() {
|
||||
if ctx.Err() == nil {
|
||||
c.direct.logf("lite map update error after %v: %v", d, err)
|
||||
}
|
||||
bo.BackOff(ctx, err)
|
||||
if rle, rateLimited := errors.AsType[*rateLimitError](err); rateLimited {
|
||||
c.waitRetryAfter(ctx, "updateRoutine", rle)
|
||||
} else {
|
||||
bo.BackOff(ctx, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
bo.BackOff(ctx, nil)
|
||||
bo.Reset()
|
||||
c.direct.logf("[v1] successful lite map update in %v", d)
|
||||
|
||||
lastUpdateGenInformed = gen
|
||||
@@ -356,7 +366,11 @@ func (c *Auto) authRoutine() {
|
||||
if err != nil {
|
||||
c.direct.health.SetAuthRoutineInError(err)
|
||||
report(err, f)
|
||||
bo.BackOff(ctx, err)
|
||||
if rle, ok := errors.AsType[*rateLimitError](err); ok {
|
||||
c.waitRetryAfter(ctx, "authRoutine", rle)
|
||||
} else {
|
||||
bo.BackOff(ctx, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if url != "" {
|
||||
@@ -370,9 +384,15 @@ func (c *Auto) authRoutine() {
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.urlToVisit = url
|
||||
c.loginGoal = &LoginGoal{
|
||||
flags: LoginDefault,
|
||||
url: 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()
|
||||
|
||||
@@ -382,7 +402,7 @@ func (c *Auto) authRoutine() {
|
||||
// backoff to avoid a busy loop.
|
||||
bo.BackOff(ctx, errors.New("login URL not changing"))
|
||||
} else {
|
||||
bo.BackOff(ctx, nil)
|
||||
bo.Reset()
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -390,14 +410,26 @@ func (c *Auto) authRoutine() {
|
||||
// success
|
||||
c.direct.health.SetAuthRoutineInError(nil)
|
||||
c.mu.Lock()
|
||||
c.urlToVisit = ""
|
||||
c.loggedIn = true
|
||||
c.loginGoal = nil
|
||||
// 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()
|
||||
|
||||
c.sendStatus("authRoutine-success", nil, "", nil)
|
||||
c.restartMap()
|
||||
bo.BackOff(ctx, nil)
|
||||
if goalStillCurrentGoal {
|
||||
c.sendStatus("authRoutine-success", nil, "", nil)
|
||||
c.restartMap()
|
||||
}
|
||||
bo.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,13 +478,14 @@ func (mrs mapRoutineState) UpdateFullNetmap(nm *netmap.NetworkMap) {
|
||||
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)
|
||||
}
|
||||
// Reset the backoff timer if we got a netmap.
|
||||
mrs.bo.Reset()
|
||||
}
|
||||
|
||||
func (mrs mapRoutineState) UpdateNetmapDelta(muts []netmap.NodeMutation) bool {
|
||||
@@ -461,20 +494,89 @@ func (mrs mapRoutineState) UpdateNetmapDelta(muts []netmap.NodeMutation) bool {
|
||||
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(c.mapCtx, 2*time.Second)
|
||||
ctx, cancel := context.WithTimeout(mapCtx, 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var ok bool
|
||||
err := c.observerQueue.RunSync(ctx, func() {
|
||||
ok = ndu.UpdateNetmapDelta(muts)
|
||||
ch := make(chan bool, 1)
|
||||
c.observerQueue.Add(func() {
|
||||
ch <- ndu.UpdateNetmapDelta(muts)
|
||||
})
|
||||
return err == nil && ok
|
||||
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
|
||||
@@ -528,13 +630,41 @@ func (c *Auto) mapRoutine() {
|
||||
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 {
|
||||
mrs.bo.BackOff(ctx, nil)
|
||||
c.logf("mapRoutine: paused")
|
||||
} else {
|
||||
mrs.bo.BackOff(ctx, err)
|
||||
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:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,17 +804,16 @@ func canSkipStatus(s1, s2 *Status) bool {
|
||||
// we can't skip it.
|
||||
return false
|
||||
}
|
||||
if s1.Err != nil || s1.URL != "" || s1.LoggedIn {
|
||||
// If s1 has an error, a URL, or LoginFinished set, 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.
|
||||
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, LoginFinished, Synced, or URL than s2,
|
||||
// don't skip it. We only care about skipping the typical
|
||||
// entries where the only difference is the NetMap.
|
||||
// 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
|
||||
@@ -774,6 +903,15 @@ func (c *Auto) SetDiscoPublicKey(key key.DiscoPublic) {
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user