2025-07-28 11:15:53 +02:00
package mapper
import (
2025-07-05 23:30:47 +02:00
"errors"
2025-07-28 11:15:53 +02:00
"fmt"
"time"
"github.com/juanfont/headscale/hscontrol/state"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/types/change"
2025-11-13 13:38:49 -06:00
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
2025-07-28 11:15:53 +02:00
"github.com/puzpuzpuz/xsync/v4"
2025-09-05 16:32:46 +02:00
"github.com/rs/zerolog/log"
2025-07-28 11:15:53 +02:00
"tailscale.com/tailcfg"
)
2025-12-15 14:36:21 +00:00
var mapResponseGenerated = promauto . NewCounterVec ( prometheus . CounterOpts {
Namespace : "headscale" ,
Name : "mapresponse_generated_total" ,
Help : "total count of mapresponses generated by response type" ,
} , [ ] string { "response_type" } )
2025-11-13 13:38:49 -06:00
2025-07-28 11:15:53 +02:00
type batcherFunc func ( cfg * types . Config , state * state . State ) Batcher
// Batcher defines the common interface for all batcher implementations.
type Batcher interface {
Start ( )
Close ( )
2025-07-05 23:30:47 +02:00
AddNode ( id types . NodeID , c chan <- * tailcfg . MapResponse , version tailcfg . CapabilityVersion ) error
RemoveNode ( id types . NodeID , c chan <- * tailcfg . MapResponse ) bool
2025-07-28 11:15:53 +02:00
IsConnected ( id types . NodeID ) bool
ConnectedMap ( ) * xsync . Map [ types . NodeID , bool ]
2025-12-15 14:36:21 +00:00
AddWork ( r ... change . Change )
MapResponseFromChange ( id types . NodeID , r change . Change ) ( * tailcfg . MapResponse , error )
2025-08-27 17:09:13 +02:00
DebugMapResponses ( ) ( map [ types . NodeID ] [ ] tailcfg . MapResponse , error )
2025-07-28 11:15:53 +02:00
}
func NewBatcher ( batchTime time . Duration , workers int , mapper * mapper ) * LockFreeBatcher {
return & LockFreeBatcher {
mapper : mapper ,
workers : workers ,
tick : time . NewTicker ( batchTime ) ,
// The size of this channel is arbitrary chosen, the sizing should be revisited.
workCh : make ( chan work , workers * 200 ) ,
2025-09-05 16:32:46 +02:00
nodes : xsync . NewMap [ types . NodeID , * multiChannelNodeConn ] ( ) ,
2025-07-28 11:15:53 +02:00
connected : xsync . NewMap [ types . NodeID , * time . Time ] ( ) ,
2025-12-15 14:36:21 +00:00
pendingChanges : xsync . NewMap [ types . NodeID , [ ] change . Change ] ( ) ,
2025-07-28 11:15:53 +02:00
}
}
// NewBatcherAndMapper creates a Batcher implementation.
func NewBatcherAndMapper ( cfg * types . Config , state * state . State ) Batcher {
m := newMapper ( cfg , state )
b := NewBatcher ( cfg . Tuning . BatchChangeDelay , cfg . Tuning . BatcherWorkers , m )
m . batcher = b
2025-09-05 16:32:46 +02:00
2025-07-28 11:15:53 +02:00
return b
}
// nodeConnection interface for different connection implementations.
type nodeConnection interface {
nodeID ( ) types . NodeID
version ( ) tailcfg . CapabilityVersion
send ( data * tailcfg . MapResponse ) error
2025-12-15 14:36:21 +00:00
// computePeerDiff returns peers that were previously sent but are no longer in the current list.
computePeerDiff ( currentPeers [ ] tailcfg . NodeID ) ( removed [ ] tailcfg . NodeID )
// updateSentPeers updates the tracking of which peers have been sent to this node.
updateSentPeers ( resp * tailcfg . MapResponse )
2025-07-28 11:15:53 +02:00
}
2025-12-15 14:36:21 +00:00
// generateMapResponse generates a [tailcfg.MapResponse] for the given NodeID based on the provided [change.Change].
func generateMapResponse ( nc nodeConnection , mapper * mapper , r change . Change ) ( * tailcfg . MapResponse , error ) {
nodeID := nc . nodeID ( )
version := nc . version ( )
if r . IsEmpty ( ) {
return nil , nil //nolint:nilnil // Empty response means nothing to send
2025-07-28 11:15:53 +02:00
}
if nodeID == 0 {
return nil , fmt . Errorf ( "invalid nodeID: %d" , nodeID )
}
if mapper == nil {
return nil , fmt . Errorf ( "mapper is nil for nodeID %d" , nodeID )
}
2025-12-15 14:36:21 +00:00
// Handle self-only responses
if r . IsSelfOnly ( ) && r . TargetNode != nodeID {
return nil , nil //nolint:nilnil // No response needed for other nodes when self-only
}
2025-09-05 16:32:46 +02:00
var (
2025-12-15 14:36:21 +00:00
mapResp * tailcfg . MapResponse
err error
2025-09-05 16:32:46 +02:00
)
2025-07-28 11:15:53 +02:00
2025-12-15 14:36:21 +00:00
// Track metric using categorized type, not free-form reason
mapResponseGenerated . WithLabelValues ( r . Type ( ) ) . Inc ( )
2025-11-13 13:38:49 -06:00
2025-12-15 14:36:21 +00:00
// Check if this requires runtime peer visibility computation (e.g., policy changes)
if r . RequiresRuntimePeerComputation {
currentPeers := mapper . state . ListPeers ( nodeID )
2025-11-13 13:38:49 -06:00
2025-12-15 14:36:21 +00:00
currentPeerIDs := make ( [ ] tailcfg . NodeID , 0 , currentPeers . Len ( ) )
for _ , peer := range currentPeers . All ( ) {
currentPeerIDs = append ( currentPeerIDs , peer . ID ( ) . NodeID ( ) )
2025-11-13 13:38:49 -06:00
}
2025-12-15 14:36:21 +00:00
removedPeers := nc . computePeerDiff ( currentPeerIDs )
mapResp , err = mapper . policyChangeResponse ( nodeID , version , removedPeers , currentPeers )
} else {
mapResp , err = mapper . buildFromChange ( nodeID , version , & r )
2025-07-28 11:15:53 +02:00
}
if err != nil {
return nil , fmt . Errorf ( "generating map response for nodeID %d: %w" , nodeID , err )
}
return mapResp , nil
}
2025-12-15 14:36:21 +00:00
// handleNodeChange generates and sends a [tailcfg.MapResponse] for a given node and [change.Change].
func handleNodeChange ( nc nodeConnection , mapper * mapper , r change . Change ) error {
2025-07-28 11:15:53 +02:00
if nc == nil {
2025-07-05 23:30:47 +02:00
return errors . New ( "nodeConnection is nil" )
2025-07-28 11:15:53 +02:00
}
nodeID := nc . nodeID ( )
2025-09-05 16:32:46 +02:00
2025-12-15 14:36:21 +00:00
log . Debug ( ) . Caller ( ) . Uint64 ( "node.id" , nodeID . Uint64 ( ) ) . Str ( "reason" , r . Reason ) . Msg ( "Node change processing started because change notification received" )
2025-09-05 16:32:46 +02:00
2025-12-15 14:36:21 +00:00
data , err := generateMapResponse ( nc , mapper , r )
2025-07-28 11:15:53 +02:00
if err != nil {
return fmt . Errorf ( "generating map response for node %d: %w" , nodeID , err )
}
if data == nil {
2025-12-15 14:36:21 +00:00
// No data to send is valid for some response types
2025-07-28 11:15:53 +02:00
return nil
}
// Send the map response
2025-09-05 16:32:46 +02:00
err = nc . send ( data )
if err != nil {
2025-07-28 11:15:53 +02:00
return fmt . Errorf ( "sending map response to node %d: %w" , nodeID , err )
}
2025-12-15 14:36:21 +00:00
// Update peer tracking after successful send
nc . updateSentPeers ( data )
2025-07-28 11:15:53 +02:00
return nil
}
// workResult represents the result of processing a change.
type workResult struct {
mapResponse * tailcfg . MapResponse
err error
}
// work represents a unit of work to be processed by workers.
type work struct {
2025-12-17 10:35:48 +00:00
c change . Change
2025-07-28 11:15:53 +02:00
nodeID types . NodeID
resultCh chan <- workResult // optional channel for synchronous operations
}