mirror of
https://github.com/tailscale/tailscale.git
synced 2025-06-22 16:18:39 +00:00

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>
182 lines
5.2 KiB
Go
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 != ""
|
|
}
|