tailscale/tailscale · error

exit node cannot be changed: %w

Error message

exit node cannot be changed: %w

What it means

checkPrefs: the user tried to change exit node prefs (ExitNodeID/ExitNodeIP/AutoExitNode) that are managed by syspolicy, and either the AllowExitNodeOverride policy did not permit the override or the user attempted to disable exit node usage entirely. A policy-enforced rejection.

Source

Thrown at ipn/ipnlocal/local.go:5224

	// Prevent users from changing exit node preferences
	// when exit node usage is managed by policy.
	if mp.ExitNodeIDSet || mp.ExitNodeIPSet || mp.AutoExitNodeSet {
		isManaged, err := b.polc.HasAnyOf(pkey.ExitNodeID, pkey.ExitNodeIP)
		if err != nil {
			err = fmt.Errorf("policy check failed: %w", err)
		} else if isManaged {
			// Allow users to override ExitNode policy settings and select an exit node manually
			// if permitted by [pkey.AllowExitNodeOverride].
			//
			// Disabling exit node usage entirely is not allowed.
			allowExitNodeOverride, _ := b.polc.GetBoolean(pkey.AllowExitNodeOverride, false)
			if !allowExitNodeOverride || b.changeDisablesExitNodeLocked(prefs, mp) {
				err = errManagedByPolicy
			}
		}
		if err != nil {
			errs = append(errs, fmt.Errorf("exit node cannot be changed: %w", err))
		}
	}

	return errors.Join(errs...)
}

// changeDisablesExitNodeLocked reports whether applying the change
// to the given prefs would disable exit node usage.
//
// In other words, it returns true if prefs.ExitNodeID is non-empty
// initially, but would become empty after applying the given change.
//
// It applies the same adjustments and resolves the exit node in the prefs
// as done during actual edits. While not optimal performance-wise,
// changing the exit node via LocalAPI isn't a hot path, and reusing
// the same logic ensures consistency and simplifies maintenance.
//
// b.mu must be held.

View on GitHub (pinned to 5201273aec)

Solutions

  1. Disconnect or adjust the conflicting setting before changing the exit node.
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at ipn/ipnlocal/local.go:5218 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of tailscale/tailscale@5201273aec (2026-08-18). Data as JSON: /api/errors/b25daaa5f5e74c1a. Report an issue: GitHub.