mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-12 11:32:02 +09:00
59f3ff12a7
Each request is reduced to the narrowest change it justifies, so a keepalive or endpoint bump no longer resends the whole node to peers. Updates #3417
198 lines
6.7 KiB
Go
198 lines
6.7 KiB
Go
// Package state provides pure functions for processing [tailcfg.MapRequest] data.
|
|
// These functions are extracted from [State.UpdateNodeFromMapRequest] to improve
|
|
// testability and maintainability.
|
|
|
|
package state
|
|
|
|
import (
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/log"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
// mapRequestDelta carries the classified facts extracted from one MapRequest
|
|
// against the currently-stored node. It separates the raw wire-level peer
|
|
// change (what the client actually sent) from the broadcast/persist/policy
|
|
// decisions made from it, so that each downstream decision (broadcast, persist,
|
|
// policy refresh, relation rebuild) sees only its own input.
|
|
//
|
|
// Construction happens once per MapRequest inside the NodeStore write callback
|
|
// (so comparisons run against serialized current state); classification happens
|
|
// after the write succeeds. Splitting these avoids redoing comparisons and
|
|
// avoids having the broadcast classifier depend on incidental branch order.
|
|
type mapRequestDelta struct {
|
|
// peerChange is the wire-level delta produced by
|
|
// [types.Node.PeerChangeFromMapRequest] - LastSeen is always stamped.
|
|
peerChange tailcfg.PeerChange
|
|
|
|
// hostinfoChanged reports whether anything in Hostinfo changed apart
|
|
// from PreferredDERP (tracked by derpChanged) and DERP latency jitter.
|
|
// It gates storing and persisting the new Hostinfo.
|
|
hostinfoChanged bool
|
|
|
|
// peerHostinfoChanged reports whether a Hostinfo field that peers read
|
|
// changed (see [peerHostinfo]). Only that forces a whole-node resend.
|
|
peerHostinfoChanged bool
|
|
|
|
// routesChanged reports whether announced routes (RoutableIPs)
|
|
// changed. Routes are policy and election inputs, so they are tracked
|
|
// on their own.
|
|
routesChanged bool
|
|
|
|
// derpChanged reports whether PreferredDERP changed. oldDERP/newDERP
|
|
// carry the values; DERP zero means "unchanged" on the wire, so
|
|
// clearing DERP to zero cannot be sent as a patch and forces a
|
|
// whole-peer update.
|
|
derpChanged bool
|
|
oldDERP, newDERP tailcfg.DERPRegionID
|
|
|
|
// endpointBroadcast reports whether the endpoint delta is worth fanning
|
|
// out (i.e., a newly-added useful non-STUN endpoint). Storage still
|
|
// happens regardless; this gates only broadcast.
|
|
endpointBroadcast bool
|
|
|
|
// keyChanged and discoKeyChanged report whether the node's wire keys
|
|
// changed. Key patches already carry the resulting endpoints/expiry,
|
|
// so a key change subsumes endpoint/DERP patches.
|
|
keyChanged bool
|
|
discoKeyChanged bool
|
|
|
|
// persistWorthy reports whether the request carries data that should
|
|
// hit the database. LastSeen-only updates are not persist-worthy.
|
|
persistWorthy bool
|
|
}
|
|
|
|
// MarshalZerologObject implements [zerolog.LogObjectMarshaler].
|
|
func (d mapRequestDelta) MarshalZerologObject(e *zerolog.Event) {
|
|
e.Bool("hostinfo.changed", d.hostinfoChanged).
|
|
Bool("hostinfo.peer_changed", d.peerHostinfoChanged).
|
|
Bool("routes.changed", d.routesChanged).
|
|
Bool("derp.changed", d.derpChanged).
|
|
Int("derp.old", int(d.oldDERP)).
|
|
Int("derp.new", int(d.newDERP)).
|
|
Bool("endpoint.broadcast", d.endpointBroadcast).
|
|
Bool("key.changed", d.keyChanged).
|
|
Bool("disco_key.changed", d.discoKeyChanged).
|
|
Bool("persist", d.persistWorthy)
|
|
}
|
|
|
|
// netInfoFromMapRequest determines the correct [tailcfg.NetInfo] to use.
|
|
// Returns the [tailcfg.NetInfo] that should be used for this request.
|
|
func netInfoFromMapRequest(
|
|
nodeID types.NodeID,
|
|
currentHostinfo *tailcfg.Hostinfo,
|
|
reqHostinfo *tailcfg.Hostinfo,
|
|
) *tailcfg.NetInfo {
|
|
// If request has [tailcfg.NetInfo], use it
|
|
if reqHostinfo != nil && reqHostinfo.NetInfo != nil {
|
|
return reqHostinfo.NetInfo
|
|
}
|
|
|
|
// Otherwise, use current [tailcfg.NetInfo] if available
|
|
if currentHostinfo != nil && currentHostinfo.NetInfo != nil {
|
|
log.Debug().
|
|
Caller().
|
|
Uint64(zf.NodeID, nodeID.Uint64()).
|
|
Int64(zf.DERP, currentHostinfo.NetInfo.PreferredDERP.Int64()).
|
|
Msg("using NetInfo from previous Hostinfo in MapRequest")
|
|
|
|
return currentHostinfo.NetInfo
|
|
}
|
|
|
|
// No [tailcfg.NetInfo] available anywhere - log for debugging
|
|
var hostname string
|
|
if reqHostinfo != nil {
|
|
hostname = reqHostinfo.Hostname
|
|
} else if currentHostinfo != nil {
|
|
hostname = currentHostinfo.Hostname
|
|
}
|
|
|
|
log.Debug().
|
|
Caller().
|
|
Uint64(zf.NodeID, nodeID.Uint64()).
|
|
Str(zf.Hostname, hostname).
|
|
Msg("node sent update but has no NetInfo in request or database")
|
|
|
|
return nil
|
|
}
|
|
|
|
// hostinfoDERP returns the PreferredDERP value from a Hostinfo, or 0 when
|
|
// either pointer is nil. 0 is the wire "unchanged" sentinel.
|
|
func hostinfoDERP(hi *tailcfg.Hostinfo) tailcfg.DERPRegionID {
|
|
if hi == nil || hi.NetInfo == nil {
|
|
return 0
|
|
}
|
|
|
|
return hi.NetInfo.PreferredDERP
|
|
}
|
|
|
|
// hostinfoEqual reports whether two Hostinfo values are the same apart from
|
|
// PreferredDERP, which the caller tracks on its own, and DERP latency
|
|
// jitter, which [tailcfg.NetInfo.BasicallyEqual] skips.
|
|
func hostinfoEqual(oldHI, newHI *tailcfg.Hostinfo) bool {
|
|
if oldHI == nil || newHI == nil {
|
|
return oldHI == newHI
|
|
}
|
|
|
|
if !netInfoEqualIgnoringDERP(oldHI.NetInfo, newHI.NetInfo) {
|
|
return false
|
|
}
|
|
|
|
// NetInfo is compared above; drop it so nil-vs-empty inside it does
|
|
// not leak through reflect.DeepEqual.
|
|
oldCopy := *oldHI
|
|
oldCopy.NetInfo = nil
|
|
newCopy := *newHI
|
|
newCopy.NetInfo = nil
|
|
|
|
return oldCopy.Equal(&newCopy)
|
|
}
|
|
|
|
// peerHostinfo keeps the Hostinfo fields another node's client reads from
|
|
// a peer: what tailscale status shows, PeerAPI services, SSH known hosts,
|
|
// exit-node location, app-connector eligibility, and the routes this server
|
|
// feeds into policy. Everything else is stored but never fanned out, and a
|
|
// peer's NetInfo is never read at all.
|
|
func peerHostinfo(hi *tailcfg.Hostinfo) *tailcfg.Hostinfo {
|
|
if hi == nil {
|
|
return nil
|
|
}
|
|
|
|
return &tailcfg.Hostinfo{
|
|
Hostname: hi.Hostname,
|
|
OS: hi.OS,
|
|
Services: hi.Services,
|
|
SSH_HostKeys: hi.SSH_HostKeys,
|
|
Location: hi.Location,
|
|
AppConnector: hi.AppConnector,
|
|
RoutableIPs: hi.RoutableIPs,
|
|
}
|
|
}
|
|
|
|
// peerHostinfoEqual reports whether the fields peers read are unchanged.
|
|
func peerHostinfoEqual(oldHI, newHI *tailcfg.Hostinfo) bool {
|
|
return peerHostinfo(oldHI).Equal(peerHostinfo(newHI))
|
|
}
|
|
|
|
// netInfoEqualIgnoringDERP compares two NetInfo values via
|
|
// [tailcfg.NetInfo.BasicallyEqual] after zeroing PreferredDERP on both, so
|
|
// that DERP-only changes do not appear here (they are tracked separately).
|
|
func netInfoEqualIgnoringDERP(old, current *tailcfg.NetInfo) bool {
|
|
if old == nil && current == nil {
|
|
return true
|
|
}
|
|
|
|
if (old == nil) != (current == nil) {
|
|
return false
|
|
}
|
|
|
|
oldCopy := *old
|
|
oldCopy.PreferredDERP = 0
|
|
currentCopy := *current
|
|
currentCopy.PreferredDERP = 0
|
|
|
|
return oldCopy.BasicallyEqual(¤tCopy)
|
|
}
|