mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-19 14:54:53 +09:00
state: classify map requests before broadcasting
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
This commit is contained in:
@@ -6,10 +6,78 @@ 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(
|
||||
@@ -26,8 +94,8 @@ func netInfoFromMapRequest(
|
||||
if currentHostinfo != nil && currentHostinfo.NetInfo != nil {
|
||||
log.Debug().
|
||||
Caller().
|
||||
Uint64("node.id", nodeID.Uint64()).
|
||||
Int64("preferredDERP", currentHostinfo.NetInfo.PreferredDERP.Int64()).
|
||||
Uint64(zf.NodeID, nodeID.Uint64()).
|
||||
Int64(zf.DERP, currentHostinfo.NetInfo.PreferredDERP.Int64()).
|
||||
Msg("using NetInfo from previous Hostinfo in MapRequest")
|
||||
|
||||
return currentHostinfo.NetInfo
|
||||
@@ -43,9 +111,87 @@ func netInfoFromMapRequest(
|
||||
|
||||
log.Debug().
|
||||
Caller().
|
||||
Uint64("node.id", nodeID.Uint64()).
|
||||
Str("node.hostname", hostname).
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user