diff --git a/control/controlknobs/controlknobs.go b/control/controlknobs/controlknobs.go index 77a496349..175e16402 100644 --- a/control/controlknobs/controlknobs.go +++ b/control/controlknobs/controlknobs.go @@ -110,6 +110,11 @@ type Knobs struct { // See https://github.com/tailscale/tailscale/issues/15404. // TODO(bradfitz): remove this a few releases after 2026-02-16. ForceRegisterMagicDNSIPv4Only atomic.Bool + + // CacheNetworkMaps is whether the node should persistently cache network + // maps and use them to establish peer connectivity on start, if doing so + // is supported by the client and storage is available. + CacheNetworkMaps atomic.Bool } // UpdateFromNodeAttributes updates k (if non-nil) based on the provided self @@ -139,6 +144,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) { disableSkipStatusQueue = has(tailcfg.NodeAttrDisableSkipStatusQueue) disableHostsFileUpdates = has(tailcfg.NodeAttrDisableHostsFileUpdates) forceRegisterMagicDNSIPv4Only = has(tailcfg.NodeAttrForceRegisterMagicDNSIPv4Only) + cacheNetworkMaps = has(tailcfg.NodeAttrCacheNetworkMaps) ) if has(tailcfg.NodeAttrOneCGNATEnable) { @@ -166,6 +172,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) { k.DisableSkipStatusQueue.Store(disableSkipStatusQueue) k.DisableHostsFileUpdates.Store(disableHostsFileUpdates) k.ForceRegisterMagicDNSIPv4Only.Store(forceRegisterMagicDNSIPv4Only) + k.CacheNetworkMaps.Store(cacheNetworkMaps) } // AsDebugJSON returns k as something that can be marshalled with json.Marshal diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 9720f57cd..8a1bebca4 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -4403,8 +4403,10 @@ type NewDiscoKeyAvailable struct { // maybeSendTSMPDiscoAdvert conditionally emits an event indicating that we // should send our DiscoKey to the first node address of the magicksock endpoint. -// The event is only emitted if we are not already communicating directly and -// more than 60 seconds has passed since the last DiscoKey was sent. +// +// The event is suppressed if we are already communicating directly or +// less than 60 seconds has passed since the last DiscoKey was sent, or netmap +// caching is disabled on this node. // // We do not need the Conn to be locked, but the endpoint should be. func (c *Conn) maybeSendTSMPDiscoAdvert(de *endpoint) { @@ -4412,6 +4414,17 @@ func (c *Conn) maybeSendTSMPDiscoAdvert(de *endpoint) { return } + // Disable TSMP disco advert by default, unless network map caching is + // enabled for the local node. Caching network maps on the remote node is + // what really matters in terms of handling a TSMP disco advert and applying + // it in a useful way, but the TSMP disco advert implementation as it exists + // here has pathological behaviors. Therefore, it should be disabled for + // almost all tailnets, and we lean on the network map caching control knob + // for this purpose. See #20081. + if c.controlKnobs == nil || !c.controlKnobs.CacheNetworkMaps.Load() { + return + } + de.mu.Lock() defer de.mu.Unlock() diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index 3552ecc19..b2309a6a7 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -4436,6 +4436,10 @@ func TestSendingTSMPDiscoTimer(t *testing.T) { tw := eventbustest.NewWatcher(t, conn.eventBus) t.Cleanup(func() { conn.Close() }) + // maybeSendTSMPDiscoAdvert only advertises when netmap caching is enabled. + conn.controlKnobs = new(controlknobs.Knobs) + conn.controlKnobs.CacheNetworkMaps.Store(true) + peerKey := key.NewNode().Public() ep := &endpoint{ nodeID: 1, @@ -4497,3 +4501,43 @@ func TestSendingTSMPDiscoTimer(t *testing.T) { t.Errorf("expected only one event, got: %s", err) } } + +// TestSendingTSMPDiscoCachingDisabled verifies that maybeSendTSMPDiscoAdvert +// early-returns (sends no advert) when netmap caching is not enabled via the +// CacheNetworkMaps control knob, including when no knobs are present at all. +func TestSendingTSMPDiscoCachingDisabled(t *testing.T) { + tests := []struct { + name string + knobs *controlknobs.Knobs + }{ + {name: "no-knobs", knobs: nil}, + // Knobs present but CacheNetworkMaps left at its false default. + {name: "caching-disabled", knobs: new(controlknobs.Knobs)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conn := newTestConn(t) + t.Cleanup(func() { conn.Close() }) + conn.controlKnobs = tt.knobs + + ep := &endpoint{ + nodeID: 1, + publicKey: key.NewNode().Public(), + nodeAddr: netip.MustParseAddr("100.64.0.1"), + } + ep.c = conn + + // A fresh endpoint with a zero lastDiscoKeyAdvertisement and no + // direct bestAddr would otherwise advertise; the only thing + // suppressing it here is the disabled caching knob. On early + // return the timestamp is left untouched (zero). + conn.maybeSendTSMPDiscoAdvert(ep) + + ep.mu.Lock() + defer ep.mu.Unlock() + if !ep.lastDiscoKeyAdvertisement.IsZero() { + t.Errorf("lastDiscoKeyAdvertisement = %v; want zero (advert should have been suppressed)", ep.lastDiscoKeyAdvertisement) + } + }) + } +}