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:
Brad Fitzpatrick
2026-05-27 14:12:48 -07:00
committed by Brad Fitzpatrick
parent 80dc7a8d07
commit 364b952d62
15 changed files with 508 additions and 176 deletions
+22 -23
View File
@@ -27,7 +27,7 @@ import (
"tailscale.com/kube/egressservices"
"tailscale.com/kube/kubeclient"
"tailscale.com/kube/kubetypes"
"tailscale.com/types/netmap"
"tailscale.com/types/views"
"tailscale.com/util/httpm"
"tailscale.com/util/linuxfw"
"tailscale.com/util/mak"
@@ -55,7 +55,7 @@ type egressProxy struct {
tsClient *local.Client // never nil
netmapChan chan *netmap.NetworkMap // chan to receive netmap updates on
netmapChan chan netmapState // chan to receive netmap state updates on
podIPv4 string // never empty string, currently only IPv4 is supported
@@ -87,7 +87,7 @@ type httpClient interface {
// - the mounted egress config has changed
// - the proxy's tailnet IP addresses have changed
// - tailnet IPs have changed for any backend targets specified by tailnet FQDN
func (ep *egressProxy) run(ctx context.Context, nm *netmap.NetworkMap, opts egressProxyRunOpts) error {
func (ep *egressProxy) run(ctx context.Context, nm netmapState, opts egressProxyRunOpts) error {
ep.configure(opts)
var tickChan <-chan time.Time
var eventChan <-chan fsnotify.Event
@@ -136,7 +136,7 @@ type egressProxyRunOpts struct {
kc kubeclient.Client
tsClient *local.Client
stateSecret string
netmapChan chan *netmap.NetworkMap
netmapChan chan netmapState
podIPv4 string
tailnetAddrs []netip.Prefix
}
@@ -165,7 +165,7 @@ func (ep *egressProxy) configure(opts egressProxyRunOpts) {
// any firewall rules need to be updated. Currently using status in state Secret as a reference for what is the current
// firewall configuration is good enough because - the status is keyed by the Pod IP - we crash the Pod on errors such
// as failed firewall update
func (ep *egressProxy) sync(ctx context.Context, nm *netmap.NetworkMap) error {
func (ep *egressProxy) sync(ctx context.Context, nm netmapState) error {
cfgs, err := ep.getConfigs()
if err != nil {
return fmt.Errorf("error retrieving egress service configs: %w", err)
@@ -186,16 +186,15 @@ func (ep *egressProxy) sync(ctx context.Context, nm *netmap.NetworkMap) error {
return nil
}
// addrsHaveChanged returns true if the provided netmap update contains tailnet address change for this proxy node.
// Netmap must not be nil.
func (ep *egressProxy) addrsHaveChanged(nm *netmap.NetworkMap) bool {
return !reflect.DeepEqual(ep.tailnetAddrs, nm.SelfNode.Addresses())
// addrsHaveChanged returns true if the provided netmap state contains tailnet address change for this proxy node.
func (ep *egressProxy) addrsHaveChanged(nm netmapState) bool {
return !views.SliceEqual(views.SliceOf(ep.tailnetAddrs), nm.self.Addresses())
}
// syncEgressConfigs adds and deletes firewall rules to match the desired
// configuration. It uses the provided status to determine what is currently
// applied and updates the status after a successful sync.
func (ep *egressProxy) syncEgressConfigs(cfgs egressservices.Configs, status *egressservices.Status, nm *netmap.NetworkMap) (*egressservices.Status, error) {
func (ep *egressProxy) syncEgressConfigs(cfgs egressservices.Configs, status *egressservices.Status, nm netmapState) (*egressservices.Status, error) {
if !(wantsServicesConfigured(cfgs) || hasServicesConfigured(status)) {
return nil, nil
}
@@ -234,7 +233,7 @@ func (ep *egressProxy) syncEgressConfigs(cfgs egressservices.Configs, status *eg
// family.
for _, t := range tailnetTargetIPs {
var local netip.Addr
for _, pfx := range nm.SelfNode.Addresses().All() {
for _, pfx := range nm.self.Addresses().All() {
if !pfx.IsSingleIP() {
continue
}
@@ -424,7 +423,7 @@ func (ep *egressProxy) getStatus(ctx context.Context) (*egressservices.Status, e
// setStatus writes egress proxy's currently configured firewall to the state
// Secret and updates proxy's tailnet addresses.
func (ep *egressProxy) setStatus(ctx context.Context, status *egressservices.Status, nm *netmap.NetworkMap) error {
func (ep *egressProxy) setStatus(ctx context.Context, status *egressservices.Status, nm netmapState) error {
// Pod IP is used to determine if a stored status applies to THIS proxy Pod.
if status == nil {
status = &egressservices.Status{}
@@ -447,7 +446,7 @@ func (ep *egressProxy) setStatus(ctx context.Context, status *egressservices.Sta
if err := ep.kc.JSONPatchResource(ctx, ep.stateSecret, kubeclient.TypeSecrets, []kubeclient.JSONPatch{patch}); err != nil {
return fmt.Errorf("error patching state Secret: %w", err)
}
ep.tailnetAddrs = nm.SelfNode.Addresses().AsSlice()
ep.tailnetAddrs = nm.self.Addresses().AsSlice()
return nil
}
@@ -457,7 +456,7 @@ func (ep *egressProxy) setStatus(ctx context.Context, status *egressservices.Sta
// FQDN, resolve the FQDN and return the resolved IPs. It checks if the
// netfilter runner supports IPv6 NAT and skips any IPv6 addresses if it
// doesn't.
func (ep *egressProxy) tailnetTargetIPsForSvc(svc egressservices.Config, nm *netmap.NetworkMap) (addrs []netip.Addr, err error) {
func (ep *egressProxy) tailnetTargetIPsForSvc(svc egressservices.Config, nm netmapState) (addrs []netip.Addr, err error) {
if svc.TailnetTarget.IP != "" {
addr, err := netip.ParseAddr(svc.TailnetTarget.IP)
if err != nil {
@@ -473,8 +472,8 @@ func (ep *egressProxy) tailnetTargetIPsForSvc(svc egressservices.Config, nm *net
if svc.TailnetTarget.FQDN == "" {
return nil, errors.New("unexpected egress service config- neither tailnet target IP nor FQDN is set")
}
if nm == nil {
log.Printf("netmap is not available, unable to determine backend addresses for %s", svc.TailnetTarget.FQDN)
if !nm.self.Valid() {
log.Printf("netmap state is not available, unable to determine backend addresses for %s", svc.TailnetTarget.FQDN)
return addrs, nil
}
egressAddrs, err := resolveTailnetFQDN(nm, svc.TailnetTarget.FQDN)
@@ -501,26 +500,26 @@ func (ep *egressProxy) tailnetTargetIPsForSvc(svc egressservices.Config, nm *net
return addrs, nil
}
// shouldResync parses netmap update and returns true if the update contains
// shouldResync parses netmap state update and returns true if the update contains
// changes for which the egress proxy's firewall should be reconfigured.
func (ep *egressProxy) shouldResync(nm *netmap.NetworkMap) bool {
if nm == nil {
func (ep *egressProxy) shouldResync(nm netmapState) bool {
if !nm.self.Valid() {
return false
}
// If proxy's tailnet addresses have changed, resync.
if !reflect.DeepEqual(nm.SelfNode.Addresses().AsSlice(), ep.tailnetAddrs) {
if !views.SliceEqual(nm.self.Addresses(), views.SliceOf(ep.tailnetAddrs)) {
log.Printf("node addresses have changed, trigger egress config resync")
ep.tailnetAddrs = nm.SelfNode.Addresses().AsSlice()
ep.tailnetAddrs = nm.self.Addresses().AsSlice()
return true
}
// If the IPs for any of the egress services configured via FQDN have
// changed, resync.
for fqdn, ips := range ep.targetFQDNs {
for _, nn := range nm.Peers {
for nn := range nm.peers() {
if equalFQDNs(nn.Name(), fqdn) {
if !reflect.DeepEqual(ips, nn.Addresses().AsSlice()) {
if !views.SliceEqual(views.SliceOf(ips), nn.Addresses()) {
log.Printf("backend addresses for egress target %q have changed old IPs %v, new IPs %v trigger egress config resync", nn.Name(), ips, nn.Addresses().AsSlice())
return true
}