tailscale/types/netmap/nodemut.go
Jordan Whited 583f740c0b
Revert "types/netmap,wgengine/magicsock: propagate CapVer to magicsock.endpoint (#16244)" (#16322)
This reverts commit 6a93b17c8cafc1d8e1c52e133511e52ed9086355.

The reverted commit added more complexity than it was worth at the
current stage. Handling delta CapVer changes requires extensive changes
to relayManager datastructures in order to also support delta updates of
relay servers.

Updates tailscale/corp#27502

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2025-06-19 09:47:06 -07:00

182 lines
5.2 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package netmap
import (
"cmp"
"net/netip"
"reflect"
"slices"
"sync"
"time"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
)
// NodeMutation is the common interface for types that describe
// the change of a node's state.
type NodeMutation interface {
NodeIDBeingMutated() tailcfg.NodeID
Apply(*tailcfg.Node)
}
type mutatingNodeID tailcfg.NodeID
func (m mutatingNodeID) NodeIDBeingMutated() tailcfg.NodeID { return tailcfg.NodeID(m) }
// NodeMutationDERPHome is a NodeMutation that says a node
// has changed its DERP home region.
type NodeMutationDERPHome struct {
mutatingNodeID
DERPRegion int
}
func (m NodeMutationDERPHome) Apply(n *tailcfg.Node) {
n.HomeDERP = m.DERPRegion
}
// NodeMutationEndpoints is a NodeMutation that says a node's endpoints have changed.
type NodeMutationEndpoints struct {
mutatingNodeID
Endpoints []netip.AddrPort
}
func (m NodeMutationEndpoints) Apply(n *tailcfg.Node) {
n.Endpoints = slices.Clone(m.Endpoints)
}
// NodeMutationOnline is a NodeMutation that says a node is now online or
// offline.
type NodeMutationOnline struct {
mutatingNodeID
Online bool
}
func (m NodeMutationOnline) Apply(n *tailcfg.Node) {
n.Online = ptr.To(m.Online)
}
// NodeMutationLastSeen is a NodeMutation that says a node's LastSeen
// value should be set to the current time.
type NodeMutationLastSeen struct {
mutatingNodeID
LastSeen time.Time
}
func (m NodeMutationLastSeen) Apply(n *tailcfg.Node) {
n.LastSeen = ptr.To(m.LastSeen)
}
var peerChangeFields = sync.OnceValue(func() []reflect.StructField {
var fields []reflect.StructField
rt := reflect.TypeFor[tailcfg.PeerChange]()
for i := range rt.NumField() {
fields = append(fields, rt.Field(i))
}
return fields
})
// NodeMutationsFromPatch returns the NodeMutations that
// p describes. If p describes something not yet supported
// by a specific NodeMutation type, it returns (nil, false).
func NodeMutationsFromPatch(p *tailcfg.PeerChange) (_ []NodeMutation, ok bool) {
if p == nil || p.NodeID == 0 {
return nil, false
}
var ret []NodeMutation
rv := reflect.ValueOf(p).Elem()
for i, sf := range peerChangeFields() {
if rv.Field(i).IsZero() {
continue
}
switch sf.Name {
default:
// Unhandled field.
return nil, false
case "NodeID":
continue
case "DERPRegion":
ret = append(ret, NodeMutationDERPHome{mutatingNodeID(p.NodeID), p.DERPRegion})
case "Endpoints":
ret = append(ret, NodeMutationEndpoints{mutatingNodeID(p.NodeID), slices.Clone(p.Endpoints)})
case "Online":
ret = append(ret, NodeMutationOnline{mutatingNodeID(p.NodeID), *p.Online})
case "LastSeen":
ret = append(ret, NodeMutationLastSeen{mutatingNodeID(p.NodeID), *p.LastSeen})
}
}
return ret, true
}
// MutationsFromMapResponse returns all the discrete node mutations described
// by res. It returns ok=false if res contains any non-patch field as defined
// by mapResponseContainsNonPatchFields.
func MutationsFromMapResponse(res *tailcfg.MapResponse, now time.Time) (ret []NodeMutation, ok bool) {
if now.IsZero() {
now = time.Now()
}
if mapResponseContainsNonPatchFields(res) {
return nil, false
}
// All that remains is PeersChangedPatch, OnlineChange, and LastSeenChange.
for _, p := range res.PeersChangedPatch {
deltas, ok := NodeMutationsFromPatch(p)
if !ok {
return nil, false
}
ret = append(ret, deltas...)
}
for nid, v := range res.OnlineChange {
ret = append(ret, NodeMutationOnline{mutatingNodeID(nid), v})
}
for nid, v := range res.PeerSeenChange {
if v {
ret = append(ret, NodeMutationLastSeen{mutatingNodeID(nid), now})
}
}
slices.SortStableFunc(ret, func(a, b NodeMutation) int {
return cmp.Compare(a.NodeIDBeingMutated(), b.NodeIDBeingMutated())
})
return ret, true
}
// mapResponseContainsNonPatchFields reports whether res contains only "patch"
// fields set (PeersChangedPatch primarily, but also including the legacy
// PeerSeenChange and OnlineChange fields).
//
// It ignores any of the meta fields that are handled by PollNetMap before the
// peer change handling gets involved.
//
// The purpose of this function is to ask whether this is a tricky enough
// MapResponse to warrant a full netmap update. When this returns false, it
// means the response can be handled incrementally, patching up the local state.
func mapResponseContainsNonPatchFields(res *tailcfg.MapResponse) bool {
return res.Node != nil ||
res.DERPMap != nil ||
res.DNSConfig != nil ||
res.Domain != "" ||
res.CollectServices != "" ||
res.PacketFilter != nil ||
res.PacketFilters != nil ||
res.UserProfiles != nil ||
res.Health != nil ||
res.DisplayMessages != nil ||
res.SSHPolicy != nil ||
res.TKAInfo != nil ||
res.DomainDataPlaneAuditLogID != "" ||
res.Debug != nil ||
res.ControlDialPlan != nil ||
res.ClientVersion != nil ||
res.Peers != nil ||
res.PeersRemoved != nil ||
// PeersChanged is too coarse to be considered a patch. Also, we convert
// PeersChanged to PeersChangedPatch in patchifyPeersChanged before this
// function is called, so it should never be set anyway. But for
// completedness, and for tests, check it too:
res.PeersChanged != nil ||
res.DefaultAutoUpdate != ""
}