cmd/containerboot: track peers from IPN bus updates, stop using netmap.NetworkMap
Some tests in another repo were broken by tailscale/tailscale#19607. This fixes them, by finishing off the rest of the migration away from netmap.NetworkMap on the IPN bus in containerboot. Containerboot used to rebuild a full NetworkMap-shaped view while reacting to IPN bus notifications. Now it insteads has its own netmapState type (immutable) of exactly what it needs to track, and sends those immutable values around, making cheap edits of new immutable values when an IPN bus edit arrives. This should make cmd/containerboot scale to much larger tailnets now too. Fixes #19852 Fixes tailscale/corp#42347 Updates #12542 Change-Id: I88adaf061f85f677f954a764935e6654329d75a6 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
committed by
Brad Fitzpatrick
parent
80dc7a8d07
commit
364b952d62
+21
-8
@@ -5,6 +5,7 @@ package ipn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -35,15 +36,27 @@ const (
|
||||
// ID tokens used by the Android client.
|
||||
const GoogleIDTokenType = "ts_android_google_login"
|
||||
|
||||
var stateStrings = [...]string{
|
||||
"NoState",
|
||||
"InUseOtherUser",
|
||||
"NeedsLogin",
|
||||
"NeedsMachineAuth",
|
||||
"Stopped",
|
||||
"Starting",
|
||||
"Running",
|
||||
}
|
||||
|
||||
func (s State) String() string {
|
||||
return [...]string{
|
||||
"NoState",
|
||||
"InUseOtherUser",
|
||||
"NeedsLogin",
|
||||
"NeedsMachineAuth",
|
||||
"Stopped",
|
||||
"Starting",
|
||||
"Running"}[s]
|
||||
return stateStrings[s]
|
||||
}
|
||||
|
||||
// StateFromString parses s as a State string value.
|
||||
func StateFromString(s string) (_ State, ok bool) {
|
||||
i := slices.Index(stateStrings[:], s)
|
||||
if i == -1 {
|
||||
return NoState, false
|
||||
}
|
||||
return State(i), true
|
||||
}
|
||||
|
||||
// EngineStatus contains WireGuard engine stats.
|
||||
|
||||
+34
-2
@@ -1417,6 +1417,7 @@ func (b *LocalBackend) updateStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
}
|
||||
if nm != nil {
|
||||
s.CertDomains = append([]string(nil), nm.DNS.CertDomains...)
|
||||
s.ExtraRecords = append([]tailcfg.DNSRecord(nil), nm.DNS.ExtraRecords...)
|
||||
s.MagicDNSSuffix = nm.MagicDNSSuffix()
|
||||
if s.CurrentTailnet == nil {
|
||||
s.CurrentTailnet = &ipnstate.TailnetStatus{}
|
||||
@@ -2325,6 +2326,7 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
||||
defer b.mu.Unlock()
|
||||
|
||||
cn := b.currentNode()
|
||||
needsAuthReconfig := netmapDeltaNeedsAuthReconfig(cn, muts)
|
||||
cn.UpdateNetmapDelta(muts)
|
||||
|
||||
// Dispatch Upsert/Remove per-peer to magicsock, and any per-field
|
||||
@@ -2348,6 +2350,9 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
||||
}
|
||||
}
|
||||
ms.UpdateNetmapDelta(muts)
|
||||
if needsAuthReconfig {
|
||||
b.authReconfigLocked()
|
||||
}
|
||||
|
||||
// If auto exit nodes are enabled and our exit node went offline,
|
||||
// we need to schedule picking a new one.
|
||||
@@ -2411,6 +2416,33 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
||||
return true
|
||||
}
|
||||
|
||||
func netmapDeltaNeedsAuthReconfig(cn *nodeBackend, muts []netmap.NodeMutation) bool {
|
||||
for _, m := range muts {
|
||||
switch m := m.(type) {
|
||||
case netmap.NodeMutationUpsert:
|
||||
old, ok := cn.NodeByID(m.Node.ID())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if peerRouteConfigChanged(old, m.Node) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func peerRouteConfigChanged(old, new tailcfg.NodeView) bool {
|
||||
return old.Key() != new.Key() ||
|
||||
old.DiscoKey() != new.DiscoKey() ||
|
||||
!views.SliceEqual(old.AllowedIPs(), new.AllowedIPs()) ||
|
||||
old.Expired() != new.Expired() ||
|
||||
old.IsJailed() != new.IsJailed() ||
|
||||
old.IsWireGuardOnly() != new.IsWireGuardOnly() ||
|
||||
old.SelfNodeV4MasqAddrForThisPeer() != new.SelfNodeV4MasqAddrForThisPeer() ||
|
||||
old.SelfNodeV6MasqAddrForThisPeer() != new.SelfNodeV6MasqAddrForThisPeer()
|
||||
}
|
||||
|
||||
// UpdatePacketFilter implements [controlclient.PacketFilterUpdater].
|
||||
//
|
||||
// It is called by the controlclient when a MapResponse carries a new packet
|
||||
@@ -2506,7 +2538,7 @@ func mutationsAreWorthyOfRecalculatingSuggestedExitNode(muts []netmap.NodeMutati
|
||||
// [tailcfg.PeerChange] for use in [ipn.Notify.PeerChangedPatch]. Multiple
|
||||
// mutations against the same node are merged into a single PeerChange.
|
||||
//
|
||||
// Add/Remove mutations are skipped (they ride
|
||||
// Upsert/Remove mutations are skipped (they ride
|
||||
// [ipn.Notify.PeersChanged]/[ipn.Notify.PeersRemoved]). Any other mutation
|
||||
// type that doesn't fit a [tailcfg.PeerChange] causes ok=false; the caller
|
||||
// should fall back to a full netmap rebuild.
|
||||
@@ -5662,7 +5694,7 @@ func (b *LocalBackend) authReconfigLocked() {
|
||||
|
||||
cn := b.currentNode()
|
||||
|
||||
nm := cn.NetMap()
|
||||
nm := cn.netMapWithPeers()
|
||||
if nm == nil {
|
||||
b.logf("[v1] authReconfig: netmap not yet valid. Skipping.")
|
||||
return
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -1573,6 +1574,48 @@ func TestEngineReconfigOnStateChange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineReconfigOnPeerRouteDelta(t *testing.T) {
|
||||
connect := &ipn.MaskedPrefs{Prefs: ipn.Prefs{WantRunning: true}, WantRunningSet: true}
|
||||
peerAddr := netip.MustParsePrefix("100.64.1.1/32")
|
||||
vipAddr := netip.MustParsePrefix("100.99.99.99/32")
|
||||
|
||||
peer := makePeer(1, withName("node-1"), withAddresses(peerAddr))
|
||||
peerStruct := peer.AsStruct()
|
||||
peerStruct.AllowedIPs = []netip.Prefix{peerAddr}
|
||||
peer = peerStruct.View()
|
||||
|
||||
nm := buildNetmapWithPeers(
|
||||
makePeer(2, withName("node-2"), withAddresses(netip.MustParsePrefix("100.64.1.2/32"))),
|
||||
peer,
|
||||
)
|
||||
|
||||
lb, engine, cc := newLocalBackendWithMockEngineAndControl(t, false)
|
||||
mustDo(t)(lb.Start(ipn.Options{}))
|
||||
mustDo2(t)(lb.EditPrefs(connect))
|
||||
cc().authenticated(nm)
|
||||
|
||||
replacement := nm.Peers[0].AsStruct()
|
||||
replacement.AllowedIPs = append(replacement.AllowedIPs, vipAddr)
|
||||
if !lb.UpdateNetmapDelta([]netmap.NodeMutation{netmap.NodeMutationUpsert{Node: replacement.View()}}) {
|
||||
t.Fatal("UpdateNetmapDelta = false, want true")
|
||||
}
|
||||
|
||||
cfg := engine.Config()
|
||||
if cfg == nil {
|
||||
t.Fatal("engine config is nil")
|
||||
}
|
||||
for _, peer := range cfg.Peers {
|
||||
if peer.PublicKey != replacement.Key {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(peer.AllowedIPs, vipAddr) {
|
||||
t.Fatalf("peer AllowedIPs = %v; want %v", peer.AllowedIPs, vipAddr)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatalf("engine config missing peer %v", replacement.Key.ShortString())
|
||||
}
|
||||
|
||||
// TestSendPreservesAuthURL tests that wgengine updates arriving in the middle of
|
||||
// processing an auth URL doesn't result in the auth URL being cleared.
|
||||
func TestSendPreservesAuthURL(t *testing.T) {
|
||||
|
||||
@@ -73,6 +73,9 @@ type Status struct {
|
||||
// trailing periods, and without any "_acme-challenge." prefix.
|
||||
CertDomains []string
|
||||
|
||||
// ExtraRecords contains extra DNS records to add to the DNS resolver.
|
||||
ExtraRecords []tailcfg.DNSRecord
|
||||
|
||||
// Peer is the state of each peer, keyed by each peer's current public key.
|
||||
Peer map[key.NodePublic]*PeerStatus
|
||||
|
||||
|
||||
Reference in New Issue
Block a user