From 2a906cd15e7d3d4ee4829021c59fb2add699b0e2 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 18 Aug 2025 20:24:53 +0200 Subject: [PATCH] derp Signed-off-by: Kristoffer Dalby --- hscontrol/state/state.go | 347 ++++----------------------------------- 1 file changed, 33 insertions(+), 314 deletions(-) diff --git a/hscontrol/state/state.go b/hscontrol/state/state.go index 51725d71..5315ac84 100644 --- a/hscontrol/state/state.go +++ b/hscontrol/state/state.go @@ -249,11 +249,10 @@ func (s *State) CreateUser(user types.User) (*types.User, change.ChangeSet, erro // Even if the policy manager doesn't detect a filter change, SSH policies // might now be resolvable when they weren't before. If there are existing // nodes, we should send a policy change to ensure they get updated SSH policies. + // TODO(kradalby): detect this, or rebuild all SSH policies so we can determine + // this upstream. if c.Empty() { - nodes := s.ListNodes() - if nodes.Len() > 0 { - c = change.PolicyChange() - } + c = change.PolicyChange() } log.Info().Str("user", user.Name).Bool("policyChanged", !c.Empty()).Msg("User created, policy manager updated") @@ -444,7 +443,7 @@ func (s *State) Connect(id types.NodeID) change.ChangeSet { n.IsOnline = ptr.To(true) n.LastSeen = ptr.To(now) }) - + // Also persist the last seen time to the database // Note: IsOnline is managed only in NodeStore (marked with gorm:"-"), not persisted to database _, err := s.updateNodeTx(id, func(tx *gorm.DB) error { @@ -1064,32 +1063,33 @@ func (s *State) HandleNodeFromAuthPath( nodeToRegister := regEntry.Node nodeToRegister.RegisterMethod = registrationMethod - // Custom update function for existing nodes - updateFunc := func(node *types.Node) { - node.NodeKey = nodeToRegister.NodeKey - node.DiscoKey = nodeToRegister.DiscoKey - node.Hostname = nodeToRegister.Hostname - - // Preserve NetInfo from existing node to maintain DERP connectivity during relogin - // Registration requests typically don't include NetInfo with PreferredDERP, - // but we need to preserve it to avoid nodes appearing as disconnected - if nodeToRegister.Hostinfo != nil && node.Hostinfo != nil && - nodeToRegister.Hostinfo.NetInfo == nil && node.Hostinfo.NetInfo != nil { - log.Debug(). - Uint64("node.id", node.ID.Uint64()). - Int("preferredDERP", node.Hostinfo.NetInfo.PreferredDERP). - Msg("preserving NetInfo during node re-registration") - - // Create a copy of the new Hostinfo and preserve the existing NetInfo - newHostinfo := *nodeToRegister.Hostinfo - newHostinfo.NetInfo = node.Hostinfo.NetInfo - node.Hostinfo = &newHostinfo - } else { - node.Hostinfo = nodeToRegister.Hostinfo + // Handle IP allocation + var ipv4, ipv6 *netip.Addr + if existingMachineNode != nil && existingMachineNode.UserID == uint(userID) { + // Reuse existing IPs and properties + nodeToRegister.ID = existingMachineNode.ID + nodeToRegister.GivenName = existingMachineNode.GivenName + nodeToRegister.ApprovedRoutes = existingMachineNode.ApprovedRoutes + ipv4 = existingMachineNode.IPv4 + ipv6 = existingMachineNode.IPv6 + } else { + // Allocate new IPs + ipv4, ipv6, err = s.ipAlloc.Next() + if err != nil { + return types.NodeView{}, change.EmptySet, fmt.Errorf("allocating IPs: %w", err) } - - node.Endpoints = nodeToRegister.Endpoints - node.RegisterMethod = nodeToRegister.RegisterMethod + } + + nodeToRegister.IPv4 = ipv4 + nodeToRegister.IPv6 = ipv6 + + // Ensure unique given name if not set + if nodeToRegister.GivenName == "" { + givenName, err := hsdb.EnsureUniqueGivenName(s.db.DB, nodeToRegister.Hostname) + if err != nil { + return types.NodeView{}, change.EmptySet, fmt.Errorf("failed to ensure unique given name: %w", err) + } + nodeToRegister.GivenName = givenName } savedNode, err := s.registerOrUpdateNode(nodeRegistrationHelper{ @@ -1169,97 +1169,10 @@ func (s *State) HandleNodeFromPreAuthKey( Str("user", pak.User.Username()). Msg("Refreshing existing node registration with pre-auth key") - // Update NodeStore first with the new expiry and other fields - s.nodeStore.UpdateNode(existingNodeView.ID(), func(node *types.Node) { - if !regReq.Expiry.IsZero() { - expiry := regReq.Expiry - node.Expiry = &expiry - } - // Update machine key if it changed - node.MachineKey = machineKey - - // Preserve NetInfo from existing node to maintain DERP connectivity during relogin - // Registration requests typically don't include NetInfo with PreferredDERP, - // but we need to preserve it to avoid nodes appearing as disconnected - if regReq.Hostinfo != nil && node.Hostinfo != nil && - regReq.Hostinfo.NetInfo == nil && node.Hostinfo.NetInfo != nil { - log.Debug(). - Uint64("node.id", node.ID.Uint64()). - Int("preferredDERP", node.Hostinfo.NetInfo.PreferredDERP). - Msg("preserving NetInfo during pre-auth key re-registration") - - // Create a copy of the new Hostinfo and preserve the existing NetInfo - newHostinfo := *regReq.Hostinfo - newHostinfo.NetInfo = node.Hostinfo.NetInfo - node.Hostinfo = &newHostinfo - } else { - node.Hostinfo = regReq.Hostinfo - } - - // Node is re-registering, so it's coming online - node.IsOnline = ptr.To(true) - node.LastSeen = ptr.To(time.Now()) - // Update auth key association - node.AuthKey = pak - node.AuthKeyID = &pak.ID - // Update forced tags from the pre-auth key - node.ForcedTags = pak.Proto().GetAclTags() - }) - - // Save to database - _, err = hsdb.Write(s.db.DB, func(tx *gorm.DB) (*types.Node, error) { - // Update the node in database - node := existingNodeView.AsStruct() - if !regReq.Expiry.IsZero() { - err := hsdb.NodeSetExpiry(tx, existingNodeView.ID(), regReq.Expiry) - if err != nil { - return nil, err - } - } - // Update machine key if changed - if node.MachineKey != machineKey { - err := hsdb.NodeSetMachineKey(tx, node, machineKey) - if err != nil { - return nil, err - } - } - // Update tags - err := hsdb.SetTags(tx, existingNodeView.ID(), pak.Proto().GetAclTags()) - if err != nil { - return nil, err - } - // Update last seen - err = hsdb.SetLastSeen(tx, existingNodeView.ID(), time.Now()) - if err != nil { - return nil, err - } - // Mark the pre-auth key as used if not reusable - if !pak.Reusable { - err = hsdb.UsePreAuthKey(tx, pak) - if err != nil { - return nil, err - } - } - // Return the node to satisfy the Write signature - return hsdb.GetNodeByID(tx, existingNodeView.ID()) - }) - if err != nil { - return types.NodeView{}, change.EmptySet, fmt.Errorf("failed to update node: %w", err) + return types.NodeView{}, c, nil } - // Get updated node from NodeStore - updatedNode, _ := s.nodeStore.GetNode(existingNodeView.ID()) - - // Check if policy manager needs updating - c, err := s.updatePolicyManagerNodes() - if err != nil { - return updatedNode, change.EmptySet, fmt.Errorf("failed to update policy manager after node update: %w", err) - } - if !c.IsFull() { - c = change.KeyExpiry(existingNodeView.ID()) - } - - return updatedNode, c, nil + return types.NodeView{}, change.EmptySet, nil } log.Debug(). @@ -1284,38 +1197,7 @@ func (s *State) HandleNodeFromPreAuthKey( var expiry *time.Time if !regReq.Expiry.IsZero() { - expiry = ®Req.Expiry - nodeToRegister.Expiry = expiry - } - - // Custom update function for existing nodes - updateFunc := func(node *types.Node) { - node.NodeKey = nodeToRegister.NodeKey - node.Hostname = nodeToRegister.Hostname - - // Preserve NetInfo from existing node to maintain DERP connectivity during relogin - // Registration requests typically don't include NetInfo with PreferredDERP, - // but we need to preserve it to avoid nodes appearing as disconnected - if nodeToRegister.Hostinfo != nil && node.Hostinfo != nil && - nodeToRegister.Hostinfo.NetInfo == nil && node.Hostinfo.NetInfo != nil { - log.Debug(). - Uint64("node.id", node.ID.Uint64()). - Int("preferredDERP", node.Hostinfo.NetInfo.PreferredDERP). - Msg("preserving NetInfo during pre-auth key node registration") - - // Create a copy of the new Hostinfo and preserve the existing NetInfo - newHostinfo := *nodeToRegister.Hostinfo - newHostinfo.NetInfo = node.Hostinfo.NetInfo - node.Hostinfo = &newHostinfo - } else { - node.Hostinfo = nodeToRegister.Hostinfo - } - - node.Endpoints = nodeToRegister.Endpoints - node.RegisterMethod = nodeToRegister.RegisterMethod - node.ForcedTags = nodeToRegister.ForcedTags - node.AuthKey = nodeToRegister.AuthKey - node.AuthKeyID = nodeToRegister.AuthKeyID + nodeToRegister.Expiry = ®Req.Expiry } // Post-save callback to use the pre-auth key @@ -1651,166 +1533,3 @@ func peerChangeEmpty(peerChange tailcfg.PeerChange) bool { peerChange.LastSeen == nil && peerChange.KeyExpiry == nil } - -// nodeRegistrationHelper contains common logic for registering or updating nodes. -// It handles IP allocation, given name generation, and the NodeStore vs Database update pattern. -type nodeRegistrationHelper struct { - node *types.Node - userID types.UserID - user *types.User - expiry *time.Time - updateExistingNode func(*types.Node) - postSaveCallback func(tx *gorm.DB, savedNode *types.Node) error -} - -// registerOrUpdateNode is a common helper for node registration that both HandleNodeFromAuthPath -// and HandleNodeFromPreAuthKey can use. It encapsulates the complex logic of handling -// existing vs new nodes, IP allocation, and the NodeStore/Database update pattern. -func (s *State) registerOrUpdateNode(helper nodeRegistrationHelper) (*types.Node, error) { - // Check if node exists with same machine key - var existingNode *types.Node - if nv, exists := s.nodeStore.GetNodeByMachineKey(helper.node.MachineKey); exists && nv.Valid() { - existingNode = nv.AsStruct() - } - - // Check for different user registration - if existingNode != nil && existingNode.UserID != uint(helper.userID) { - return nil, hsdb.ErrDifferentRegisteredUser - } - - // Handle IP allocation and existing node properties - var ipv4, ipv6 *netip.Addr - if existingNode != nil && existingNode.UserID == uint(helper.userID) { - // Reuse existing node properties - helper.node.ID = existingNode.ID - helper.node.GivenName = existingNode.GivenName - helper.node.ApprovedRoutes = existingNode.ApprovedRoutes - ipv4 = existingNode.IPv4 - ipv6 = existingNode.IPv6 - } else { - // Allocate new IPs - var err error - ipv4, ipv6, err = s.ipAlloc.Next() - if err != nil { - return nil, fmt.Errorf("allocating IPs: %w", err) - } - } - - helper.node.IPv4 = ipv4 - helper.node.IPv6 = ipv6 - helper.node.UserID = uint(helper.userID) - helper.node.User = *helper.user - if helper.expiry != nil { - helper.node.Expiry = helper.expiry - } - - // Ensure unique given name if not set - if helper.node.GivenName == "" { - givenName, err := hsdb.EnsureUniqueGivenName(s.db.DB, helper.node.Hostname) - if err != nil { - return nil, fmt.Errorf("failed to ensure unique given name: %w", err) - } - helper.node.GivenName = givenName - } - - var savedNode *types.Node - var err error - if existingNode != nil && existingNode.UserID == uint(helper.userID) { - // Update existing node - NodeStore first, then database - s.nodeStore.UpdateNode(existingNode.ID, func(node *types.Node) { - // Apply common updates - node.UserID = helper.node.UserID - node.User = helper.node.User - node.IPv4 = helper.node.IPv4 - node.IPv6 = helper.node.IPv6 - node.IsOnline = ptr.To(false) - node.LastSeen = ptr.To(time.Now()) - if helper.expiry != nil { - node.Expiry = helper.expiry - } - - // Apply custom updates from caller - if helper.updateExistingNode != nil { - helper.updateExistingNode(node) - } - }) - - // Get the updated node from NodeStore to save to database - // This ensures any changes made by updateExistingNode (like preserving NetInfo) - // are persisted to the database - updatedNodeView, exists := s.nodeStore.GetNode(existingNode.ID) - if !exists || !updatedNodeView.Valid() { - return nil, fmt.Errorf("failed to get updated node from NodeStore after update") - } - nodeToSave := updatedNodeView.AsStruct() - - // Save to database - savedNode, err = hsdb.Write(s.db.DB, func(tx *gorm.DB) (*types.Node, error) { - if err := tx.Save(nodeToSave).Error; err != nil { - return nil, fmt.Errorf("failed to save node: %w", err) - } - - // Run post-save callback if provided - if helper.postSaveCallback != nil { - if err := helper.postSaveCallback(tx, nodeToSave); err != nil { - return nil, err - } - } - - return nodeToSave, nil - }) - if err != nil { - return nil, err - } - } else { - // New node - database first to get ID, then NodeStore - savedNode, err = hsdb.Write(s.db.DB, func(tx *gorm.DB) (*types.Node, error) { - if err := tx.Save(helper.node).Error; err != nil { - return nil, fmt.Errorf("failed to save node: %w", err) - } - - // Run post-save callback if provided - if helper.postSaveCallback != nil { - if err := helper.postSaveCallback(tx, helper.node); err != nil { - return nil, err - } - } - - return helper.node, nil - }) - if err != nil { - return nil, err - } - - // Add to NodeStore after database creates the ID - s.nodeStore.PutNode(*savedNode) - } - - return savedNode, nil -} - -// finalizeNodeRegistration handles the common final steps after node registration: -// updating policy managers and generating the appropriate change set. -func (s *State) finalizeNodeRegistration(savedNode *types.Node) (change.ChangeSet, error) { - // Update policy managers - usersChange, err := s.updatePolicyManagerUsers() - if err != nil { - log.Error().Err(err).Msg("failed to update policy manager users after node registration") - // Don't fail the registration, just log the error - } - - nodesChange, err := s.updatePolicyManagerNodes() - if err != nil { - log.Error().Err(err).Msg("failed to update policy manager nodes after node registration") - // Don't fail the registration, just log the error - } - - var c change.ChangeSet - if !usersChange.Empty() || !nodesChange.Empty() { - c = change.PolicyChange() - } else { - c = change.NodeAdded(savedNode.ID) - } - - return c, nil -}