summaryrefslogtreecommitdiffhomepage
path: root/types/netmap/nodemut.go
blob: 901296b1fc337c578ec8a9a2ba0c0703fed45ad6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package netmap

import (
	"cmp"
	"net/netip"
	"reflect"
	"slices"
	"sync"
	"time"

	"tailscale.com/tailcfg"
)

// 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 = new(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 = new(m.LastSeen)
}

var peerChangeFields = sync.OnceValue(func() []reflect.StructField {
	var fields []reflect.StructField
	rt := reflect.TypeFor[tailcfg.PeerChange]()
	for field := range rt.Fields() {
		fields = append(fields, field)
	}
	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.DeprecatedDefaultAutoUpdate != ""
}