mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-26 02:04:53 +09:00
8973347d22
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
0.29 pins an older tailscale where Hostinfo.NetInfo.PreferredDERP is an int,
so the DERPRegionID types are int here.
(cherry picked from commit 59f3ff12a7)
198 lines
6.6 KiB
Go
198 lines
6.6 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 int
|
|
|
|
// 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", d.oldDERP).
|
|
Int("derp.new", 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()).
|
|
Int(zf.DERP, currentHostinfo.NetInfo.PreferredDERP).
|
|
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) int {
|
|
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)
|
|
}
|