This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+169 -31
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package controlclient
+187 -30
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package controlclient
+2 -4
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package controlclient