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

+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 {