juanfont/headscale · error

renaming node: %w

Error message

renaming node: %w

What it means

RenameNode validates the new GivenName with dnsname.ValidLabel before touching the database. This error means the proposed name is not a valid DNS label: empty, longer than 63 bytes, or containing characters outside the RFC 1035 subset. The comment states validation is expected to live in the state layer, so reaching this DB-layer check means the caller skipped it.

Source

Thrown at hscontrol/db/node.go:190

	return hsdb.Write(func(tx *gorm.DB) error {
		return SetLastSeen(tx, nodeID, lastSeen)
	})
}

// SetLastSeen sets a node's last seen field indicating that we
// have recently communicating with this node.
func SetLastSeen(tx *gorm.DB, nodeID types.NodeID, lastSeen time.Time) error {
	return tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("last_seen", lastSeen).Error
}

// RenameNode takes a [types.Node] struct and a new [types.Node.GivenName] for the nodes
// and renames it. Validation should be done in the state layer before calling this function.
func RenameNode(tx *gorm.DB,
	nodeID types.NodeID, newName string,
) error {
	err := dnsname.ValidLabel(newName)
	if err != nil {
		return fmt.Errorf("renaming node: %w", err)
	}

	// Check if the new name is unique
	var count int64

	if err := tx.Model(&types.Node{}).Where("given_name = ? AND id != ?", newName, nodeID).Count(&count).Error; err != nil { //nolint:noinlineerr
		return fmt.Errorf("checking name uniqueness: %w", err)
	}

	if count > 0 {
		return ErrNodeNameNotUnique
	}

	if err := tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("given_name", newName).Error; err != nil { //nolint:noinlineerr
		return fmt.Errorf("renaming node in database: %w", err)
	}

	return nil

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Sanitize with dnsname.SanitizeHostname before calling RenameNode
  2. Reject or truncate names to 63 bytes at the API boundary
  3. Return a 400-style validation error to the client instead of surfacing the DB error

Example fix

// before
err := db.RenameNode(tx, nodeID, newName) // newName = "my server #1"

// after
name := dnsname.SanitizeHostname(newName)
if name == "" {
	return fmt.Errorf("invalid node name: %q", newName)
}
err := db.RenameNode(tx, nodeID, name)
Defensive patterns

Strategy: validation

Validate before calling

name := dnsname.SanitizeHostname(newName)
if name == "" || len(name) > 63 {
	return fmt.Errorf("rejecting node name %q", newName)
}
err := db.RenameNode(tx, nodeID, name)

Type guard

func isValidNodeLabel(s string) bool {
	return dnsname.ValidLabel(s) == nil
}

Prevention

When it happens

Trigger: Passing a raw user-supplied hostname with spaces, dots, or UTF-8 characters; a name of 64+ bytes; an empty string after trimming.

Common situations: gRPC/API rename call with unvalidated input; scripts or automation feeding hostnames straight from cloud instance metadata.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/296115ed24a14ef6. Report an issue: GitHub.