juanfont/headscale · error

handling existing node: %w

Error message

handling existing node: %w

What it means

Wraps a failure of handleLogout when called on an existing node during registration (hscontrol/auth.go:103). This branch handles a RegisterRequest with Auth=nil and a non-zero Expiry for a node already known by node key; handleLogout either deletes an ephemeral node or sets its expiry to now. Any state/DB error from DeleteNode or SetNodeExpiry is re-wrapped with this message.

Source

Thrown at hscontrol/auth.go:103

			// the Noise session's machine key matches the cached node.
			// Without this check anyone holding a target's NodeKey could
			// open a Noise session with a throwaway machine key and read
			// the owner's User/Login back through [nodeToRegisterResponse].
			// [Headscale.handleLogout] enforces the same check on its own path.
			err := machineKeyMismatch(node, machineKey)
			if err != nil {
				return nil, err
			}

			// When tailscaled restarts, it sends [tailcfg.RegisterRequest] with Auth=nil and Expiry=zero.
			// Return the current node state without modification.
			if req.Expiry.IsZero() && !node.IsExpired() {
				return nodeToRegisterResponse(node), nil
			}

			resp, err := h.handleLogout(node, req, machineKey)
			if err != nil {
				return nil, fmt.Errorf("handling existing node: %w", err)
			}

			// If resp is not nil, we have a response to return to the node.
			// If resp is nil, we should proceed and see if the node is trying to re-auth.
			if resp != nil {
				return resp, nil
			}
		} else {
			// If the register request is not attempting to register a node, and
			// we cannot match it with an existing node, we consider that unexpected
			// as only register nodes should attempt to log out.
			log.Debug().
				Str("node.key", req.NodeKey.ShortString()).
				Str("machine.key", machineKey.ShortString()).
				Bool("unexpected", true).
				Msg("received register request with no auth, and no existing node")
		}
	}

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Read the wrapped error in logs to identify the failing operation (delete vs expiry update) and the DB-level cause.
  2. Stabilize the database: for SQLite avoid concurrent writers and check file permissions; for PostgreSQL check connection limits and restart policies.
  3. Retry the operation from the client — logout expiry updates are idempotent, a retry after DB recovery succeeds.
  4. If caused by ephemeral-node deletion races, upgrade headscale (state-layer locking improvements land regularly) before deep-diving.
Defensive patterns

Strategy: retry

Try / catch

resp, err := h.handleRegister(req, mk)
if err != nil && strings.Contains(err.Error(), "handling existing node") {
    // logout of a known node failed at the state layer; retry after DB recovery or expire via CLI
}

Prevention

When it happens

Trigger: tailscaled sends a register request with an expiry (logout) for an existing node while the database errors: ephemeral deletion fails (FK/constraint/lock) or SetNodeExpiry fails (connection lost, SQLite lock timeout).

Common situations: Nodes logging out during DB maintenance windows; ephemeral nodes being deleted while their peers stream updates (lock contention on SQLite); DB connection pool exhaustion under load.

Related errors


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