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 {
|
||||
|
||||
+4
-7
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package controlclient implements the client for the Tailscale
|
||||
@@ -87,13 +87,10 @@ type Client interface {
|
||||
// future map requests. This should be called after rotating the discovery key.
|
||||
// Note: the auto client uploads the new key to control immediately.
|
||||
SetDiscoPublicKey(key.DiscoPublic)
|
||||
// SetIPForwardingBroken updates the IP forwarding broken state
|
||||
// and sends a control update if the value changed.
|
||||
SetIPForwardingBroken(bool)
|
||||
// ClientID returns the ClientID of a client. This ID is meant to
|
||||
// distinguish one client from another.
|
||||
ClientID() int64
|
||||
}
|
||||
|
||||
// UserVisibleError is an error that should be shown to users.
|
||||
type UserVisibleError string
|
||||
|
||||
func (e UserVisibleError) Error() string { return string(e) }
|
||||
func (e UserVisibleError) UserVisibleError() string { return string(e) }
|
||||
+268
-98
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package controlclient
|
||||
@@ -9,12 +9,15 @@ import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -22,6 +25,7 @@ import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -45,49 +49,53 @@ import (
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailcfg/nodecap"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/httpbody"
|
||||
"tailscale.com/util/singleflight"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/util/vizerror"
|
||||
"tailscale.com/util/zstdframe"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
type Direct struct {
|
||||
httpc *http.Client // HTTP client used to do TLS requests to control (just https://controlplane.tailscale.com/key?v=123)
|
||||
interceptedDial *atomic.Bool // if non-nil, pointer to bool whether ScreenTime intercepted our dial
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
controlKnobs *controlknobs.Knobs // always non-nil
|
||||
serverURL string // URL of the tailcontrol server
|
||||
clock tstime.Clock
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // non-nil
|
||||
health *health.Tracker
|
||||
busClient *eventbus.Client
|
||||
clientVersionPub *eventbus.Publisher[tailcfg.ClientVersion]
|
||||
autoUpdatePub *eventbus.Publisher[AutoUpdate]
|
||||
controlTimePub *eventbus.Publisher[ControlTime]
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
debugFlags []string
|
||||
skipIPForwardingCheck bool
|
||||
pinger Pinger
|
||||
popBrowser func(url string) // or nil
|
||||
polc policyclient.Client // always non-nil
|
||||
c2nHandler http.Handler // or nil
|
||||
panicOnUse bool // if true, panic if client is used (for testing)
|
||||
closedCtx context.Context // alive until Direct.Close is called
|
||||
closeCtx context.CancelFunc // cancels closedCtx
|
||||
httpc *http.Client // HTTP client used to do TLS requests to control (just https://controlplane.tailscale.com/key?v=123)
|
||||
interceptedDial *atomic.Bool // if non-nil, pointer to bool whether ScreenTime intercepted our dial
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
controlKnobs *controlknobs.Knobs // always non-nil
|
||||
serverURL string // URL of the tailcontrol server
|
||||
clock tstime.Clock
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // non-nil
|
||||
health *health.Tracker
|
||||
extraRootCAs *x509.CertPool // additional trusted root CAs; or nil
|
||||
busClient *eventbus.Client
|
||||
clientVersionPub *eventbus.Publisher[tailcfg.ClientVersion]
|
||||
autoUpdatePub *eventbus.Publisher[AutoUpdate]
|
||||
controlTimePub *eventbus.Publisher[ControlTime]
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
debugFlags []string
|
||||
pinger Pinger
|
||||
popBrowser func(url string) // or nil
|
||||
polc policyclient.Client // always non-nil
|
||||
c2nHandler http.Handler // or nil
|
||||
panicOnUse bool // if true, panic if client is used (for testing)
|
||||
closedCtx context.Context // alive until Direct.Close is called
|
||||
closeCtx context.CancelFunc // cancels closedCtx
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
@@ -95,6 +103,7 @@ type Direct struct {
|
||||
serverLegacyKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key; only used for signRegisterRequest on Windows now
|
||||
serverNoiseKey key.MachinePublic
|
||||
discoPubKey key.DiscoPublic // protected by mu; can be updated via [SetDiscoPublicKey]
|
||||
ipForwardBroken bool // protected by mu; can be updated via [SetIPForwardingBroken]
|
||||
|
||||
sfGroup singleflight.Group[struct{}, *ts2021.Client] // protects noiseClient creation.
|
||||
noiseClient *ts2021.Client // also protected by mu
|
||||
@@ -140,6 +149,7 @@ type Options struct {
|
||||
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
HealthTracker *health.Tracker
|
||||
ExtraRootCAs *x509.CertPool // additional trusted root CAs; or nil
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
Dialer *tsdial.Dialer // non-nil
|
||||
C2NHandler http.Handler // or nil
|
||||
@@ -159,11 +169,6 @@ type Options struct {
|
||||
// If nil, no status updates are reported.
|
||||
Observer Observer
|
||||
|
||||
// SkipIPForwardingCheck declares that the host's IP
|
||||
// forwarding works and should not be double-checked by the
|
||||
// controlclient package.
|
||||
SkipIPForwardingCheck bool
|
||||
|
||||
// Pinger optionally specifies the Pinger to use to satisfy
|
||||
// MapResponse.PingRequest queries from the control plane.
|
||||
// If nil, PingRequest queries are not answered.
|
||||
@@ -224,6 +229,9 @@ type NetmapUpdater interface {
|
||||
// rather than just full updates.
|
||||
type NetmapDeltaUpdater interface {
|
||||
// UpdateNetmapDelta is called with discrete changes to the network map.
|
||||
// The mutation slice may contain [netmap.NodeMutationUpsert] entries when
|
||||
// peers are inserted or replaced, and [netmap.NodeMutationRemove] entries
|
||||
// when peers are removed, alongside per-field patches.
|
||||
//
|
||||
// The ok result is whether the implementation was able to apply the
|
||||
// mutations. It might return false if its internal state doesn't
|
||||
@@ -232,6 +240,52 @@ type NetmapDeltaUpdater interface {
|
||||
UpdateNetmapDelta([]netmap.NodeMutation) (ok bool)
|
||||
}
|
||||
|
||||
// PacketFilterUpdater is an optional interface that can be implemented by
|
||||
// NetmapUpdater implementations to receive incremental packet-filter updates
|
||||
// without a full netmap rebuild.
|
||||
//
|
||||
// It exists because the packet filter currently changes on every peer
|
||||
// addition, so a MapResponse carrying PeersChanged almost always also carries
|
||||
// PacketFilter (or PacketFilters). Handling the filter narrowly keeps peer
|
||||
// churn O(1) on the controlclient side.
|
||||
type PacketFilterUpdater interface {
|
||||
// UpdatePacketFilter is called when a MapResponse's PacketFilter (or
|
||||
// PacketFilters) changed. rules is the already-merged concatenation of
|
||||
// the session's named packet filter chunks; parsed is the parsed form.
|
||||
//
|
||||
// It returns false to signal the caller to fall back to a full
|
||||
// netmap rebuild. Proxy/forwarder implementations return false when
|
||||
// their downstream destination doesn't implement
|
||||
// [PacketFilterUpdater]; concrete implementations return true on
|
||||
// successful apply.
|
||||
UpdatePacketFilter(rules views.Slice[tailcfg.FilterRule], parsed []filter.Match) bool
|
||||
}
|
||||
|
||||
// UserProfileUpdater is an optional interface that can be implemented by
|
||||
// NetmapUpdater implementations to receive incremental UserProfile updates
|
||||
// without a full netmap rebuild.
|
||||
//
|
||||
// It exists so consumers of [ipn.Notify.UserProfiles] can be told about
|
||||
// new or updated UserProfiles before (or with) the [ipn.Notify.PeersChanged]
|
||||
// or [ipn.Notify.PeerChangedPatch] entry that references the corresponding
|
||||
// UserID.
|
||||
type UserProfileUpdater interface {
|
||||
// UpdateUserProfiles is called when a MapResponse carries UserProfiles
|
||||
// entries. profiles is the new/updated subset (NOT the full map);
|
||||
// implementations should merge with whatever they already know.
|
||||
//
|
||||
// The values are [tailcfg.UserProfileView]s sharing backing memory
|
||||
// with the caller's tracking map; implementations may store them
|
||||
// directly without copying.
|
||||
//
|
||||
// It returns false to signal the caller to fall back to a full
|
||||
// netmap rebuild. Proxy/forwarder implementations return false when
|
||||
// their downstream destination doesn't implement
|
||||
// [UserProfileUpdater]; concrete implementations return true on
|
||||
// successful apply.
|
||||
UpdateUserProfiles(profiles map[tailcfg.UserID]tailcfg.UserProfileView) bool
|
||||
}
|
||||
|
||||
var nextControlClientID atomic.Int64
|
||||
|
||||
// NewDirect returns a new Direct client.
|
||||
@@ -285,13 +339,19 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
}
|
||||
var interceptedDial *atomic.Bool
|
||||
if httpc == nil {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr := netutil.NewDefaultTransport()
|
||||
if buildfeatures.HasUseProxy {
|
||||
tr.Proxy = feature.HookProxyFromEnvironment.GetOrNil()
|
||||
if f, ok := feature.HookProxySetTransportGetProxyConnectHeader.GetOk(); ok {
|
||||
f(tr)
|
||||
}
|
||||
}
|
||||
if opts.ExtraRootCAs != nil {
|
||||
if tr.TLSClientConfig == nil {
|
||||
tr.TLSClientConfig = &tls.Config{}
|
||||
}
|
||||
tr.TLSClientConfig.RootCAs = opts.ExtraRootCAs
|
||||
}
|
||||
tr.TLSClientConfig = tlsdial.Config(opts.HealthTracker, tr.TLSClientConfig)
|
||||
var dialFunc netx.DialFunc
|
||||
dialFunc, interceptedDial = makeScreenTimeDetectingDialFunc(opts.Dialer.SystemDial)
|
||||
@@ -307,26 +367,26 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
}
|
||||
|
||||
c := &Direct{
|
||||
httpc: httpc,
|
||||
interceptedDial: interceptedDial,
|
||||
controlKnobs: opts.ControlKnobs,
|
||||
getMachinePrivKey: opts.GetMachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
clock: opts.Clock,
|
||||
logf: opts.Logf,
|
||||
persist: opts.Persist.View(),
|
||||
authKey: opts.AuthKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
netMon: netMon,
|
||||
health: opts.HealthTracker,
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
polc: cmp.Or(opts.PolicyClient, policyclient.Client(policyclient.NoPolicyClient{})),
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dnsCache: dnsCache,
|
||||
dialPlan: opts.DialPlan,
|
||||
httpc: httpc,
|
||||
interceptedDial: interceptedDial,
|
||||
controlKnobs: opts.ControlKnobs,
|
||||
getMachinePrivKey: opts.GetMachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
clock: opts.Clock,
|
||||
logf: opts.Logf,
|
||||
persist: opts.Persist.View(),
|
||||
authKey: opts.AuthKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
netMon: netMon,
|
||||
health: opts.HealthTracker,
|
||||
extraRootCAs: opts.ExtraRootCAs,
|
||||
pinger: opts.Pinger,
|
||||
polc: cmp.Or(opts.PolicyClient, policyclient.Client(policyclient.NoPolicyClient{})),
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dnsCache: dnsCache,
|
||||
dialPlan: opts.DialPlan,
|
||||
}
|
||||
c.discoPubKey = opts.DiscoPublicKey
|
||||
c.closedCtx, c.closeCtx = context.WithCancel(context.Background())
|
||||
@@ -382,7 +442,7 @@ func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
|
||||
if hi == nil {
|
||||
panic("nil Hostinfo")
|
||||
}
|
||||
hi = ptr.To(*hi)
|
||||
hi = new(*hi)
|
||||
hi.NetInfo = nil
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -528,6 +588,49 @@ var macOSScreenTime = health.Register(&health.Warnable{
|
||||
ImpactsConnectivity: true,
|
||||
})
|
||||
|
||||
type rateLimitError struct {
|
||||
msg string
|
||||
retryAfter time.Duration
|
||||
}
|
||||
|
||||
func (e *rateLimitError) Error() string {
|
||||
return fmt.Sprintf("rate limited: %s (retry after %v)", e.msg, e.retryAfter)
|
||||
}
|
||||
|
||||
func parseRateLimitError(res *http.Response) *rateLimitError {
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
|
||||
ret := &rateLimitError{
|
||||
msg: strings.TrimSpace(string(msg)),
|
||||
}
|
||||
|
||||
v := res.Header.Get("Retry-After")
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
ret.retryAfter = time.Duration(i) * time.Second
|
||||
} else if t, err := http.ParseTime(v); err == nil {
|
||||
ret.retryAfter = time.Until(t)
|
||||
}
|
||||
|
||||
// If the server didn't give us a valid Retry-After, default to 10s.
|
||||
if ret.retryAfter <= 0 || ret.retryAfter > time.Hour {
|
||||
ret.retryAfter = 5*time.Second + rand.N(5*time.Second)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// isRateLimitedResponse reports whether a non-200 response should be
|
||||
// treated as a rate limit rather than a generic failure subject to backoff.
|
||||
func isRateLimitedResponse(res *http.Response) bool {
|
||||
switch res.StatusCode {
|
||||
case http.StatusTooManyRequests:
|
||||
return true
|
||||
case http.StatusServiceUnavailable:
|
||||
return res.Header.Get("Retry-After") != ""
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, newURL string, nks tkatype.MarshaledSignature, err error) {
|
||||
if c.panicOnUse {
|
||||
panic("tainted client")
|
||||
@@ -722,6 +825,12 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, fmt.Errorf("register request: %w", err)
|
||||
}
|
||||
// Handle 429 Too Many Requests with a specific error type that includes the retry-after duration.
|
||||
if isRateLimitedResponse(res) {
|
||||
rle := parseRateLimitError(res)
|
||||
msg := fmt.Sprintf("node registration rate limited; will retry after %v", rle.retryAfter)
|
||||
return false, "", nil, vizerror.WrapWithMessage(rle, msg)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
@@ -743,7 +852,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
|
||||
|
||||
if resp.Error != "" {
|
||||
return false, "", nil, UserVisibleError(resp.Error)
|
||||
return false, "", nil, vizerror.New(resp.Error)
|
||||
}
|
||||
if len(resp.NodeKeySignature) > 0 {
|
||||
return true, "", resp.NodeKeySignature, nil
|
||||
@@ -828,21 +937,34 @@ func (c *Direct) PollNetMap(ctx context.Context, nu NetmapUpdater) error {
|
||||
return c.sendMapRequest(ctx, true, nu)
|
||||
}
|
||||
|
||||
// rememberLastNetmapUpdater is a container that remembers the last netmap
|
||||
// update it observed. It is used by tests and [NetmapFromMapResponseForDebug].
|
||||
// It will report only the first netmap seen.
|
||||
type rememberLastNetmapUpdater struct {
|
||||
last *netmap.NetworkMap
|
||||
done chan any
|
||||
}
|
||||
|
||||
func (nu *rememberLastNetmapUpdater) UpdateFullNetmap(nm *netmap.NetworkMap) {
|
||||
nu.last = nm
|
||||
select {
|
||||
case nu.done <- nil:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// FetchNetMapForTest fetches the netmap once.
|
||||
func (c *Direct) FetchNetMapForTest(ctx context.Context) (*netmap.NetworkMap, error) {
|
||||
var nu rememberLastNetmapUpdater
|
||||
nu.done = make(chan any, 1)
|
||||
err := c.sendMapRequest(ctx, false, &nu)
|
||||
if err == nil && nu.last == nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nu.last == nil {
|
||||
return nil, errors.New("[unexpected] sendMapRequest success without callback")
|
||||
}
|
||||
<-nu.done
|
||||
return nu.last, err
|
||||
}
|
||||
|
||||
@@ -861,6 +983,18 @@ func (c *Direct) SetDiscoPublicKey(key key.DiscoPublic) {
|
||||
c.discoPubKey = key
|
||||
}
|
||||
|
||||
// SetIPForwardingBroken updates the IP forwarding broken state.
|
||||
// It reports whether the value changed.
|
||||
func (c *Direct) SetIPForwardingBroken(v bool) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.ipForwardBroken == v {
|
||||
return false
|
||||
}
|
||||
c.ipForwardBroken = v
|
||||
return true
|
||||
}
|
||||
|
||||
// ClientID returns the controlClientID of the controlClient.
|
||||
func (c *Direct) ClientID() int64 {
|
||||
return c.controlClientID
|
||||
@@ -991,10 +1125,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
}
|
||||
|
||||
var extraDebugFlags []string
|
||||
if buildfeatures.HasAdvertiseRoutes && hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
|
||||
ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
|
||||
}
|
||||
if c.health.RouterHealth() != nil {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-router-unhealthy")
|
||||
}
|
||||
@@ -1017,6 +1147,15 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// The map response body is a stream of size-prefixed messages. Each
|
||||
// message is capped by maxCompressedMapResponseSize and
|
||||
// maxDecodedMapResponseSize, but a streaming session can carry an
|
||||
// unbounded number of messages over one body, so the body itself must
|
||||
// not be subject to ts2021.Client.Do's default response size cap.
|
||||
// This must happen before the watchdog goroutine below captures ctx:
|
||||
// reassigning ctx after the goroutine exists is a data race.
|
||||
ctx = httpbody.WithMaxSize(ctx, 0)
|
||||
|
||||
machinePubKey := machinePrivKey.Public()
|
||||
t0 := c.clock.Now()
|
||||
|
||||
@@ -1064,6 +1203,13 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
}
|
||||
vlogf("netmap: Do = %v after %v", res.StatusCode, time.Since(t0).Round(time.Millisecond))
|
||||
if res.StatusCode != 200 {
|
||||
// The body is an error message rather than a message stream, so
|
||||
// cap it: the map body itself is uncapped per the comment above.
|
||||
httpbody.LimitSizeTo(res, httpbody.DefaultMaxSize)
|
||||
if isRateLimitedResponse(res) {
|
||||
rle := parseRateLimitError(res)
|
||||
return fmt.Errorf("initial fetch failed %d: %w", res.StatusCode, rle)
|
||||
}
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return fmt.Errorf("initial fetch failed %d: %.200s",
|
||||
@@ -1120,19 +1266,22 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
for mapResIdx := 0; mapResIdx == 0 || isStreaming; mapResIdx++ {
|
||||
watchdogTimer.Reset(watchdogTimeout)
|
||||
vlogf("netmap: starting size read after %v (poll %v)", time.Since(t0).Round(time.Millisecond), mapResIdx)
|
||||
var siz [4]byte
|
||||
if _, err := io.ReadFull(res.Body, siz[:]); err != nil {
|
||||
vlogf("netmap: size read error after %v: %v", time.Since(t0).Round(time.Millisecond), err)
|
||||
msg, err = readMapResponseMessage(res.Body, msg)
|
||||
if err != nil {
|
||||
// If the read failed because the poll's context was
|
||||
// canceled, report that instead of the underlying
|
||||
// transport error. Which error the transport returns for
|
||||
// a read interrupted by cancellation varies by transport
|
||||
// and Go version (Go 1.27's http2 returns the underlying
|
||||
// "use of closed network connection" where earlier
|
||||
// versions returned the context error).
|
||||
if ctx.Err() != nil {
|
||||
err = ctx.Err()
|
||||
}
|
||||
vlogf("netmap: message read error after %v: %v", time.Since(t0).Round(time.Millisecond), err)
|
||||
return err
|
||||
}
|
||||
size := binary.LittleEndian.Uint32(siz[:])
|
||||
vlogf("netmap: read size %v after %v", size, time.Since(t0).Round(time.Millisecond))
|
||||
msg = append(msg[:0], make([]byte, size)...)
|
||||
if _, err := io.ReadFull(res.Body, msg); err != nil {
|
||||
vlogf("netmap: body read error: %v", err)
|
||||
return err
|
||||
}
|
||||
vlogf("netmap: read body after %v", time.Since(t0).Round(time.Millisecond))
|
||||
vlogf("netmap: read message of %d bytes after %v", len(msg), time.Since(t0).Round(time.Millisecond))
|
||||
|
||||
var resp tailcfg.MapResponse
|
||||
if err := sess.decodeMsg(msg, &resp); err != nil {
|
||||
@@ -1187,12 +1336,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
|
||||
// DefaultAutoUpdate in its CapMap and deprecated top-level field forms.
|
||||
if self := resp.Node; self != nil {
|
||||
for _, v := range self.CapMap[tailcfg.NodeAttrDefaultAutoUpdate] {
|
||||
for _, v := range self.CapMap[nodecap.DefaultAutoUpdate] {
|
||||
switch v {
|
||||
case "true", "false":
|
||||
c.autoUpdatePub.Publish(AutoUpdate{c.controlClientID, v == "true"})
|
||||
default:
|
||||
c.logf("netmap: [unexpected] unknown %s in CapMap: %q", tailcfg.NodeAttrDefaultAutoUpdate, v)
|
||||
c.logf("netmap: [unexpected] unknown %s in CapMap: %q", nodecap.DefaultAutoUpdate, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1230,8 +1379,11 @@ func NetmapFromMapResponseForDebug(ctx context.Context, pr persist.PersistView,
|
||||
if resp.Node == nil {
|
||||
return nil, errors.New("MapResponse lacks Node")
|
||||
}
|
||||
if !pr.Valid() {
|
||||
return nil, errors.New("PersistView invalid")
|
||||
}
|
||||
|
||||
nu := &rememberLastNetmapUpdater{}
|
||||
nu := &rememberLastNetmapUpdater{done: make(chan any, 1)}
|
||||
sess := newMapSession(pr.PrivateNodeKey(), nu, nil)
|
||||
defer sess.Close()
|
||||
|
||||
@@ -1239,6 +1391,7 @@ func NetmapFromMapResponseForDebug(ctx context.Context, pr persist.PersistView,
|
||||
return nil, fmt.Errorf("HandleNonKeepAliveMapResponse: %w", err)
|
||||
}
|
||||
|
||||
<-nu.done
|
||||
return sess.netmap(), nil
|
||||
}
|
||||
|
||||
@@ -1298,20 +1451,53 @@ var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
const justKeepAliveStr = `{"KeepAlive":true}`
|
||||
|
||||
// maxCompressedMapResponseSize bounds the on-the-wire size of a single map
|
||||
// response message. The size prefix is a little-endian uint32 the control
|
||||
// server chooses, so without a cap a malicious control server can make us
|
||||
// allocate up to 4 GiB before reading any body bytes. Real production traffic
|
||||
// has hit a 16 MB cap before, so the cap stays far above any plausible
|
||||
// legitimate size.
|
||||
const maxCompressedMapResponseSize = 256 << 20
|
||||
|
||||
// maxDecodedMapResponseSize bounds the decompressed size of a single map
|
||||
// response message, so a malicious control server can't expand a small zstd
|
||||
// frame into an unbounded amount of JSON.
|
||||
const maxDecodedMapResponseSize = 1 << 30
|
||||
|
||||
// readMapResponseMessage reads a single map response message from r: a
|
||||
// little-endian uint32 size followed by that many bytes. The returned message
|
||||
// reuses msg's backing storage. Sizes beyond [maxCompressedMapResponseSize]
|
||||
// are rejected before any allocation.
|
||||
func readMapResponseMessage(r io.Reader, msg []byte) ([]byte, error) {
|
||||
var siz [4]byte
|
||||
if _, err := io.ReadFull(r, siz[:]); err != nil {
|
||||
return msg, err
|
||||
}
|
||||
size := binary.LittleEndian.Uint32(siz[:])
|
||||
if size > maxCompressedMapResponseSize {
|
||||
return msg, fmt.Errorf("map response message size %d exceeds max %d", size, maxCompressedMapResponseSize)
|
||||
}
|
||||
msg = append(msg[:0], make([]byte, size)...)
|
||||
if _, err := io.ReadFull(r, msg); err != nil {
|
||||
return msg, err
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// decodeMsg is responsible for uncompressing msg and unmarshaling into v.
|
||||
func (sess *mapSession) decodeMsg(compressedMsg []byte, v *tailcfg.MapResponse) error {
|
||||
func (ms *mapSession) decodeMsg(compressedMsg []byte, v *tailcfg.MapResponse) error {
|
||||
// Fast path for common case of keep-alive message.
|
||||
// See tailscale/tailscale#17343.
|
||||
if sess.keepAliveZ != nil && bytes.Equal(compressedMsg, sess.keepAliveZ) {
|
||||
if ms.keepAliveZ != nil && bytes.Equal(compressedMsg, ms.keepAliveZ) {
|
||||
v.KeepAlive = true
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := zstdframe.AppendDecode(nil, compressedMsg)
|
||||
b, err := zstdframe.AppendDecode(nil, compressedMsg, zstdframe.MaxDecodedSize(maxDecodedMapResponseSize))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sess.ztdDecodesForTest++
|
||||
ms.ztdDecodesForTest++
|
||||
|
||||
if DevKnob.DumpNetMaps() {
|
||||
var buf bytes.Buffer
|
||||
@@ -1326,7 +1512,7 @@ func (sess *mapSession) decodeMsg(compressedMsg []byte, v *tailcfg.MapResponse)
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
if v.KeepAlive && string(b) == justKeepAliveStr {
|
||||
sess.keepAliveZ = compressedMsg
|
||||
ms.keepAliveZ = compressedMsg
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1357,7 +1543,8 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
|
||||
return nil, fmt.Errorf("fetch control key: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := io.ReadAll(io.LimitReader(res.Body, 64<<10))
|
||||
httpbody.LimitSizeTo(res, 64<<10)
|
||||
b, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch control key response: %v", err)
|
||||
}
|
||||
@@ -1410,24 +1597,6 @@ func initDevKnob() devKnobs {
|
||||
|
||||
var clock tstime.Clock = tstime.StdClock{}
|
||||
|
||||
// ipForwardingBroken reports whether the system's IP forwarding is disabled
|
||||
// and will definitely not work for the routes provided.
|
||||
//
|
||||
// It should not return false positives.
|
||||
//
|
||||
// TODO(bradfitz): Change controlclient.Options.SkipIPForwardingCheck into a
|
||||
// func([]netip.Prefix) error signature instead.
|
||||
func ipForwardingBroken(routes []netip.Prefix, state *netmon.State) bool {
|
||||
warn, err := netutil.CheckIPForwarding(routes, state)
|
||||
if err != nil {
|
||||
// Oh well, we tried. This is just for debugging.
|
||||
// We don't want false positives.
|
||||
// TODO: maybe we want a different warning for inability to check?
|
||||
return false
|
||||
}
|
||||
return warn != nil
|
||||
}
|
||||
|
||||
// isUniquePingRequest reports whether pr contains a new PingRequest.URL
|
||||
// not already handled, noting its value when returning true.
|
||||
func (c *Direct) isUniquePingRequest(pr *tailcfg.PingRequest) bool {
|
||||
@@ -1481,7 +1650,7 @@ func (c *Direct) answerPing(pr *tailcfg.PingRequest) {
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, t := range strings.Split(pr.Types, ",") {
|
||||
for t := range strings.SplitSeq(pr.Types, ",") {
|
||||
switch pt := tailcfg.PingType(t); pt {
|
||||
case tailcfg.PingTSMP, tailcfg.PingDisco, tailcfg.PingICMP, tailcfg.PingPeerAPI:
|
||||
go doPingerPing(c.logf, httpc, pr, c.pinger, pt)
|
||||
@@ -1567,6 +1736,7 @@ func (c *Direct) getNoiseClient() (*ts2021.Client, error) {
|
||||
Logf: c.logf,
|
||||
NetMon: c.netMon,
|
||||
HealthTracker: c.health,
|
||||
ExtraRootCAs: c.extraRootCAs,
|
||||
DialPlan: dp,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package controlclient
|
||||
|
||||
+187
-30
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package controlclient
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
@@ -24,11 +26,11 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailcfg/nodecap"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -80,8 +82,7 @@ type mapSession struct {
|
||||
// Fields storing state over the course of multiple MapResponses.
|
||||
lastPrintMap time.Time
|
||||
lastNode tailcfg.NodeView
|
||||
lastCapSet set.Set[tailcfg.NodeCapability]
|
||||
peers map[tailcfg.NodeID]tailcfg.NodeView
|
||||
lastCapSet set.Set[nodecap.Cap]
|
||||
lastDNSConfig *tailcfg.DNSConfig
|
||||
lastDERPMap *tailcfg.DERPMap
|
||||
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfileView
|
||||
@@ -97,6 +98,10 @@ type mapSession struct {
|
||||
lastPopBrowserURL string
|
||||
lastTKAInfo *tailcfg.TKAInfo
|
||||
lastNetmapSummary string // from NetworkMap.VeryConcise
|
||||
|
||||
// mu protects the peers map.
|
||||
peersMu sync.RWMutex
|
||||
peers map[tailcfg.NodeID]tailcfg.NodeView
|
||||
}
|
||||
|
||||
// newMapSession returns a mostly unconfigured new mapSession.
|
||||
@@ -146,12 +151,17 @@ func (ms *mapSession) Close() {
|
||||
ms.sessionAliveCtxClose()
|
||||
}
|
||||
|
||||
var ErrChangeQueueClosed = errors.New("change queue closed")
|
||||
|
||||
// HandleNonKeepAliveMapResponse handles a non-KeepAlive MapResponse (full or
|
||||
// incremental).
|
||||
//
|
||||
// All fields that are valid on a KeepAlive MapResponse have already been
|
||||
// handled.
|
||||
//
|
||||
// Debug messages are handled first, followed by pushing the response onto a
|
||||
// queue for new updates handled sequentially.
|
||||
//
|
||||
// TODO(bradfitz): make this handle all fields later. For now (2023-08-20) this
|
||||
// is [re]factoring progress enough.
|
||||
func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *tailcfg.MapResponse) error {
|
||||
@@ -160,7 +170,6 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if DevKnob.StripEndpoints() {
|
||||
for _, p := range resp.Peers {
|
||||
p.Endpoints = nil
|
||||
@@ -203,9 +212,11 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
|
||||
ms.updateStateFromResponse(resp)
|
||||
|
||||
if ms.tryHandleIncrementally(resp) {
|
||||
metricMapResponseHandledIncrementally.Add(1)
|
||||
ms.occasionallyPrintSummary(ms.lastNetmapSummary)
|
||||
return nil
|
||||
}
|
||||
metricMapResponseHandledFullRebuild.Add(1)
|
||||
|
||||
// We have to rebuild the whole netmap (lots of garbage & work downstream of
|
||||
// our UpdateFullNetmap call). This is the part we tried to avoid but
|
||||
@@ -242,7 +253,7 @@ func upgradeNode(n *tailcfg.Node) {
|
||||
if ip == tailcfg.DerpMagicIP && err == nil {
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err == nil {
|
||||
n.HomeDERP = port
|
||||
n.HomeDERP = tailcfg.DERPRegionID(port)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,6 +266,12 @@ func upgradeNode(n *tailcfg.Node) {
|
||||
if n.AllowedIPs == nil {
|
||||
n.AllowedIPs = slices.Clone(n.Addresses)
|
||||
}
|
||||
// Unsigned peers aren't covered by tailnet lock, so a (possibly malicious)
|
||||
// control server must not grant them network access via advertised routes.
|
||||
// Strip any AllowedIPs beyond their own addresses.
|
||||
if n.UnsignedPeerAPIOnly && !slices.Equal(n.AllowedIPs, n.Addresses) {
|
||||
n.AllowedIPs = slices.Clone(n.Addresses)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *mapSession) tryHandleIncrementally(res *tailcfg.MapResponse) bool {
|
||||
@@ -265,11 +282,76 @@ func (ms *mapSession) tryHandleIncrementally(res *tailcfg.MapResponse) bool {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
mutations, ok := netmap.MutationsFromMapResponse(res, time.Now())
|
||||
if ok && len(mutations) > 0 {
|
||||
// If the response carries a new packet filter, the updater must
|
||||
// support pushing it narrowly; otherwise fall back to a full netmap
|
||||
// rebuild. PacketFilter/PacketFilters are no longer in
|
||||
// mapResponseContainsNonPatchFields, so MutationsFromMapResponse will
|
||||
// happily return mutations alongside a filter change — we need to
|
||||
// deliver the filter separately before those mutations land.
|
||||
if res.PacketFilter != nil || res.PacketFilters != nil {
|
||||
pfu, ok := ms.netmapUpdater.(PacketFilterUpdater)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !pfu.UpdatePacketFilter(ms.lastPacketFilterRules, ms.lastParsedPacketFilter) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
mutations, mutationsOK := netmap.MutationsFromMapResponse(res, time.Now())
|
||||
|
||||
// Same shape for UserProfiles: deliver profiles before the peer
|
||||
// mutations that may reference them, so bus consumers never see a
|
||||
// UserID for which a profile hasn't been published.
|
||||
//
|
||||
// Besides the new/updated profiles carried by the response, also
|
||||
// replay the profiles of upserted peers' users from
|
||||
// ms.lastUserProfile. A full netmap keeps only the profiles of users
|
||||
// with a currently visible peer (see [mapSession.addUserProfile]), so
|
||||
// a peer returning via delta upsert can reference a user the updater
|
||||
// has since dropped, and control (mapver 5+) does not resend
|
||||
// unchanged profiles. Without the replay, WhoIs on the returned peer
|
||||
// fails at the user profile lookup until the next full netmap.
|
||||
//
|
||||
// The values are read from ms.lastUserProfile (just populated by
|
||||
// updateStateFromResponse) so views are shared with mapSession's
|
||||
// store; downstream consumers can use [UserProfileView.Equal] for
|
||||
// dedup without copying.
|
||||
var profiles map[tailcfg.UserID]tailcfg.UserProfileView
|
||||
addProfile := func(id tailcfg.UserID) {
|
||||
if id == 0 {
|
||||
return
|
||||
}
|
||||
if up, ok := ms.lastUserProfile[id]; ok {
|
||||
mak.Set(&profiles, id, up)
|
||||
}
|
||||
}
|
||||
for _, up := range res.UserProfiles {
|
||||
addProfile(up.ID)
|
||||
}
|
||||
if mutationsOK {
|
||||
for _, m := range mutations {
|
||||
if up, ok := m.(netmap.NodeMutationUpsert); ok {
|
||||
addProfile(up.Node.User())
|
||||
addProfile(up.Node.Sharer())
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(profiles) > 0 {
|
||||
upu, ok := ms.netmapUpdater.(UserProfileUpdater)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !upu.UpdateUserProfiles(profiles) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !mutationsOK {
|
||||
return false
|
||||
}
|
||||
if len(mutations) > 0 {
|
||||
return nud.UpdateNetmapDelta(mutations)
|
||||
}
|
||||
return ok
|
||||
return true
|
||||
}
|
||||
|
||||
// updateStats are some stats from updateStateFromResponse, primarily for
|
||||
@@ -289,7 +371,7 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
|
||||
if resp.Node != nil {
|
||||
ms.lastNode = resp.Node.View()
|
||||
|
||||
capSet := set.Set[tailcfg.NodeCapability]{}
|
||||
capSet := set.Set[nodecap.Cap]{}
|
||||
for _, c := range resp.Node.Capabilities {
|
||||
capSet.Add(c)
|
||||
}
|
||||
@@ -323,7 +405,7 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
|
||||
// really the control plane should pick this. This is only a fallback.
|
||||
if hostinfo.IsInVM86() {
|
||||
numCanMeasure := 0
|
||||
lowest := 0
|
||||
var lowest tailcfg.DERPRegionID
|
||||
for rid, r := range dm.Regions {
|
||||
if !r.NoMeasureNoHome {
|
||||
numCanMeasure++
|
||||
@@ -450,11 +532,25 @@ var (
|
||||
|
||||
patchifiedPeer = clientmetric.NewCounter("controlclient_patchified_peer")
|
||||
patchifiedPeerEqual = clientmetric.NewCounter("controlclient_patchified_peer_equal")
|
||||
|
||||
// metricMapResponseHandledIncrementally counts non-keepalive MapResponses
|
||||
// that were processed via [mapSession.tryHandleIncrementally] (i.e. the
|
||||
// "fast" delta path that avoids rebuilding the full netmap).
|
||||
metricMapResponseHandledIncrementally = clientmetric.NewCounter("controlclient_map_response_handled_incrementally")
|
||||
|
||||
// metricMapResponseHandledFullRebuild counts non-keepalive MapResponses
|
||||
// that fell through to the full netmap rebuild path because they
|
||||
// carried a field that the incremental path can't handle. See
|
||||
// [netmap.mapResponseContainsNonPatchFields].
|
||||
metricMapResponseHandledFullRebuild = clientmetric.NewCounter("controlclient_map_response_handled_full_rebuild")
|
||||
)
|
||||
|
||||
// updatePeersStateFromResponseres updates ms.peers from resp.
|
||||
// It takes ownership of resp.
|
||||
func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (stats updateStats) {
|
||||
ms.peersMu.Lock()
|
||||
defer ms.peersMu.Unlock()
|
||||
|
||||
if ms.peers == nil {
|
||||
ms.peers = make(map[tailcfg.NodeID]tailcfg.NodeView)
|
||||
}
|
||||
@@ -504,7 +600,7 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
|
||||
if vp, ok := ms.peers[nodeID]; ok {
|
||||
mut := vp.AsStruct()
|
||||
if seen {
|
||||
mut.LastSeen = ptr.To(clock.Now())
|
||||
mut.LastSeen = new(clock.Now())
|
||||
} else {
|
||||
mut.LastSeen = nil
|
||||
}
|
||||
@@ -516,7 +612,7 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
|
||||
for nodeID, online := range resp.OnlineChange {
|
||||
if vp, ok := ms.peers[nodeID]; ok {
|
||||
mut := vp.AsStruct()
|
||||
mut.Online = ptr.To(online)
|
||||
mut.Online = new(online)
|
||||
ms.peers[nodeID] = mut.View()
|
||||
stats.changed++
|
||||
}
|
||||
@@ -550,11 +646,11 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
|
||||
patchDiscoKey.Add(1)
|
||||
}
|
||||
if v := pc.Online; v != nil {
|
||||
mut.Online = ptr.To(*v)
|
||||
mut.Online = new(*v)
|
||||
patchOnline.Add(1)
|
||||
}
|
||||
if v := pc.LastSeen; v != nil {
|
||||
mut.LastSeen = ptr.To(*v)
|
||||
mut.LastSeen = new(*v)
|
||||
patchLastSeen.Add(1)
|
||||
}
|
||||
if v := pc.KeyExpiry; v != nil {
|
||||
@@ -589,13 +685,22 @@ func (ms *mapSession) addUserProfile(nm *netmap.NetworkMap, userID tailcfg.UserI
|
||||
}
|
||||
|
||||
var debugPatchifyPeer = envknob.RegisterBool("TS_DEBUG_PATCHIFY_PEER")
|
||||
var debugPatchifyPeerMiss = envknob.RegisterBool("TS_DEBUG_PATCHIFY_PEER_MISS")
|
||||
|
||||
// patchifyMissOnFalse, if non-nil, is called with the field name when
|
||||
// patchifyPeer fails. It is set by an init func in map_debug.go.
|
||||
var patchifyMissOnFalse func(string)
|
||||
|
||||
// patchifyPeersChanged mutates resp to promote PeersChanged entries to PeersChangedPatch
|
||||
// when possible.
|
||||
func (ms *mapSession) patchifyPeersChanged(resp *tailcfg.MapResponse) {
|
||||
var onFalse func(string)
|
||||
if debugPatchifyPeerMiss() {
|
||||
onFalse = patchifyMissOnFalse
|
||||
}
|
||||
filtered := resp.PeersChanged[:0]
|
||||
for _, n := range resp.PeersChanged {
|
||||
if p, ok := ms.patchifyPeer(n); ok {
|
||||
if p, ok := ms.patchifyPeer(n, onFalse); ok {
|
||||
patchifiedPeer.Add(1)
|
||||
if debugPatchifyPeer() {
|
||||
patchj, _ := json.Marshal(p)
|
||||
@@ -618,12 +723,12 @@ func (ms *mapSession) patchifyPeersChanged(resp *tailcfg.MapResponse) {
|
||||
|
||||
var nodeFields = sync.OnceValue(getNodeFields)
|
||||
|
||||
// getNodeFields returns the fails of tailcfg.Node.
|
||||
// getNodeFields returns the fields of tailcfg.Node.
|
||||
func getNodeFields() []string {
|
||||
rt := reflect.TypeFor[tailcfg.Node]()
|
||||
ret := make([]string, rt.NumField())
|
||||
for i := range rt.NumField() {
|
||||
ret[i] = rt.Field(i).Name
|
||||
ret := make([]string, 0, rt.NumField())
|
||||
for f := range rt.Fields() {
|
||||
ret = append(ret, f.Name)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -633,18 +738,27 @@ func getNodeFields() []string {
|
||||
//
|
||||
// It returns ok=false if a patch can't be made, (V, ok) on a delta, or (nil,
|
||||
// true) if all the fields were identical (a zero change).
|
||||
func (ms *mapSession) patchifyPeer(n *tailcfg.Node) (_ *tailcfg.PeerChange, ok bool) {
|
||||
func (ms *mapSession) patchifyPeer(n *tailcfg.Node, onFalse func(string)) (_ *tailcfg.PeerChange, ok bool) {
|
||||
ms.peersMu.RLock()
|
||||
defer ms.peersMu.RUnlock()
|
||||
|
||||
was, ok := ms.peers[n.ID]
|
||||
if !ok {
|
||||
if onFalse != nil {
|
||||
onFalse("peer_not_found")
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
return peerChangeDiff(was, n)
|
||||
return peerChangeDiff(was, n, onFalse)
|
||||
}
|
||||
|
||||
// peerChangeDiff returns the difference from 'was' to 'n', if possible.
|
||||
//
|
||||
// It returns (nil, true) if the fields were identical.
|
||||
func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChange, ok bool) {
|
||||
func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node, onFalse func(string)) (_ *tailcfg.PeerChange, ok bool) {
|
||||
if onFalse == nil {
|
||||
onFalse = func(string) {}
|
||||
}
|
||||
var ret *tailcfg.PeerChange
|
||||
pc := func() *tailcfg.PeerChange {
|
||||
if ret == nil {
|
||||
@@ -661,38 +775,43 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
case "computedHostIfDifferent", "ComputedName", "ComputedNameWithHost":
|
||||
// Caller's responsibility to have populated these.
|
||||
continue
|
||||
case "DataPlaneAuditLogID":
|
||||
// Not sent for peers.
|
||||
case "DataPlaneAuditLogID", "StableTailnetID":
|
||||
// Not sent for peers.
|
||||
case "Capabilities":
|
||||
// Deprecated; see https://github.com/tailscale/tailscale/issues/11508
|
||||
// And it was never sent by any known control server.
|
||||
case "ID":
|
||||
if was.ID() != n.ID {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "StableID":
|
||||
if was.StableID() != n.StableID {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "Name":
|
||||
if was.Name() != n.Name {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "User":
|
||||
if was.User() != n.User {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "Sharer":
|
||||
if was.Sharer() != n.Sharer {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "Key":
|
||||
if was.Key() != n.Key {
|
||||
pc().Key = ptr.To(n.Key)
|
||||
pc().Key = new(n.Key)
|
||||
}
|
||||
case "KeyExpiry":
|
||||
if !was.KeyExpiry().Equal(n.KeyExpiry) {
|
||||
pc().KeyExpiry = ptr.To(n.KeyExpiry)
|
||||
pc().KeyExpiry = new(n.KeyExpiry)
|
||||
}
|
||||
case "KeySignature":
|
||||
if !was.KeySignature().Equal(n.KeySignature) {
|
||||
@@ -700,18 +819,21 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
}
|
||||
case "Machine":
|
||||
if was.Machine() != n.Machine {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "DiscoKey":
|
||||
if was.DiscoKey() != n.DiscoKey {
|
||||
pc().DiscoKey = ptr.To(n.DiscoKey)
|
||||
pc().DiscoKey = new(n.DiscoKey)
|
||||
}
|
||||
case "Addresses":
|
||||
if !views.SliceEqual(was.Addresses(), views.SliceOf(n.Addresses)) {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "AllowedIPs":
|
||||
if !views.SliceEqual(was.AllowedIPs(), views.SliceOf(n.AllowedIPs)) {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "Endpoints":
|
||||
@@ -731,13 +853,16 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
continue
|
||||
}
|
||||
if !was.Hostinfo().Valid() || !n.Hostinfo.Valid() {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
if !was.Hostinfo().Equal(n.Hostinfo) {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "Created":
|
||||
if !was.Created().Equal(n.Created) {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "Cap":
|
||||
@@ -765,38 +890,45 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
}
|
||||
case "Tags":
|
||||
if !views.SliceEqual(was.Tags(), views.SliceOf(n.Tags)) {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "PrimaryRoutes":
|
||||
if !views.SliceEqual(was.PrimaryRoutes(), views.SliceOf(n.PrimaryRoutes)) {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "Online":
|
||||
if wasOnline, ok := was.Online().GetOk(); ok && n.Online != nil && *n.Online != wasOnline {
|
||||
pc().Online = ptr.To(*n.Online)
|
||||
pc().Online = new(*n.Online)
|
||||
}
|
||||
case "LastSeen":
|
||||
if wasSeen, ok := was.LastSeen().GetOk(); ok && n.LastSeen != nil && !wasSeen.Equal(*n.LastSeen) {
|
||||
pc().LastSeen = ptr.To(*n.LastSeen)
|
||||
pc().LastSeen = new(*n.LastSeen)
|
||||
}
|
||||
case "MachineAuthorized":
|
||||
if was.MachineAuthorized() != n.MachineAuthorized {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "UnsignedPeerAPIOnly":
|
||||
if was.UnsignedPeerAPIOnly() != n.UnsignedPeerAPIOnly {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "IsWireGuardOnly":
|
||||
if was.IsWireGuardOnly() != n.IsWireGuardOnly {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "IsJailed":
|
||||
if was.IsJailed() != n.IsJailed {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "Expired":
|
||||
if was.Expired() != n.Expired {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "SelfNodeV4MasqAddrForThisPeer":
|
||||
@@ -805,6 +937,7 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
continue
|
||||
}
|
||||
if va, ok := va.GetOk(); !ok || vb == nil || va != *vb {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "SelfNodeV6MasqAddrForThisPeer":
|
||||
@@ -813,17 +946,20 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
continue
|
||||
}
|
||||
if va, ok := va.GetOk(); !ok || vb == nil || va != *vb {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
case "ExitNodeDNSResolvers":
|
||||
va, vb := was.ExitNodeDNSResolvers(), views.SliceOfViews(n.ExitNodeDNSResolvers)
|
||||
|
||||
if va.Len() != vb.Len() {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for i := range va.Len() {
|
||||
if !va.At(i).Equal(vb.At(i)) {
|
||||
onFalse(field)
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
@@ -836,7 +972,28 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// PeerIDAndKeyByTailscaleIP returns the node ID and node Key from the peers
|
||||
// map without touching the netmap itself. The implementation mirrors the
|
||||
// implementation of [netmap.PeerByTailscaleIP].
|
||||
func (ms *mapSession) PeerIDAndKeyByTailscaleIP(ip netip.Addr) (tailcfg.NodeID, key.NodePublic, bool) {
|
||||
ms.peersMu.RLock()
|
||||
defer ms.peersMu.RUnlock()
|
||||
for _, n := range ms.peers {
|
||||
ad := n.Addresses()
|
||||
for i := range ad.Len() {
|
||||
a := ad.At(i)
|
||||
if a.Addr() == ip {
|
||||
return n.ID(), n.Key(), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, key.NodePublic{}, false
|
||||
}
|
||||
|
||||
func (ms *mapSession) sortedPeers() []tailcfg.NodeView {
|
||||
ms.peersMu.RLock()
|
||||
defer ms.peersMu.RUnlock()
|
||||
|
||||
ret := slicesx.MapValues(ms.peers)
|
||||
slices.SortFunc(ret, func(a, b tailcfg.NodeView) int {
|
||||
return cmp.Compare(a.ID(), b.ID())
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_debug
|
||||
|
||||
package controlclient
|
||||
|
||||
import "tailscale.com/metrics"
|
||||
|
||||
var patchifyMissStats = metrics.NewLabelMap("counter_patchify_miss", "why")
|
||||
|
||||
func init() {
|
||||
patchifyMissOnFalse = func(field string) {
|
||||
patchifyMissStats.Add(field, 1)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package controlclient
|
||||
|
||||
+2
-4
@@ -1,9 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build windows
|
||||
|
||||
// darwin,cgo is also supported by certstore but untested, so it is not enabled.
|
||||
//go:build windows || (darwin && !ios && cgo)
|
||||
|
||||
package controlclient
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows
|
||||
//go:build (!windows && !(darwin && cgo)) || ios
|
||||
|
||||
package controlclient
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package controlclient
|
||||
|
||||
Reference in new issue
Block a user