#3432·headscale

[Bug] GivenName is not resynced when a node re-registers with a changed hostname

Author: Moep90Created Aug 21, 2026Updated Sep 1, 2026

Is this a support request?

  • This is not a support request

Is there an existing issue for this?

  • I have searched the existing issues

Current Behavior

GivenName is resynced from a changed client hostname on the MapRequest path, but not on the register/reauth path. Because the reauth path already overwrites node.Hostinfo, the later MapRequest never observes a hostinfo change, so the resync that exists never runs. The node keeps its old DNS label indefinitely.

hscontrol/state/state.go, UpdateNodeFromMapRequest — the resync exists and is correctly guarded against clobbering an admin rename:

go
if hostinfoChanged {
    currentNode.Hostinfo = req.Hostinfo
    if req.Hostinfo != nil && req.Hostinfo.Hostname != "" {
        // Preserve an admin-renamed GivenName: only auto-derive when the
        // current GivenName is still what SanitizeHostname of the old
        // Hostname would produce (possibly with a "-N" collision bump).
        autoDerived := isAutoDerivedGivenName(currentNode.GivenName, currentNode.Hostname)

        currentNode.Hostname = req.Hostinfo.Hostname
        if autoDerived {
            currentNode.GivenName = dnsname.SanitizeHostname(req.Hostinfo.Hostname)
        }
    }
    ...

hscontrol/state/state.go, applyAuthNodeUpdate — same field, no resync:

go
updatedNodeView, ok := s.nodeStore.UpdateNode(params.ExistingNode.ID(), func(node *types.Node) {
    node.NodeKey = regData.NodeKey
    node.DiscoKey = regData.DiscoKey
    node.Hostname = params.Hostname          // <-- hostname updated

    node.Hostinfo = params.ValidHostinfo     // <-- hostinfo updated too
    ...
    // GivenName is never considered
})

The second assignment is what makes this permanent rather than merely delayed. UpdateNodeFromMapRequest computes hostinfoChanged by comparing the incoming Hostinfo against the stored one. Reauth has already stored the new Hostinfo, so the first MapRequest after reauth sees no change, takes the early-return branch, and the autoDerived block is never reached.

Net effect: node.Hostname and node.GivenName diverge permanently for any node that re-registers under a new hostname. MagicDNS keeps serving the old label (GetFQDN() uses GivenName), so the name a device answers to and the name it reports are different, with no warning surfaced anywhere.

This is self-latching. Once the two fields diverge, isAutoDerivedGivenName(GivenName, Hostname) returns false forever, so even a subsequent genuine MapRequest hostname change will no longer resync. The anti-clobber guard cannot distinguish "admin renamed this" from "reauth desynced this", and treats the desync as an intentional rename.

Expected Behavior

applyAuthNodeUpdate applies the same isAutoDerivedGivenName guard that UpdateNodeFromMapRequest already applies: when the stored GivenName is still auto-derived from the stored Hostname, re-derive it from the incoming hostname; when it has been admin-renamed, leave it alone.

Both paths mutate the same two fields for the same reason, so they should agree on the policy.

Steps To Reproduce

  1. Register a node with hostname host-a. Hostname and GivenName are both host-a.
  2. Change the client's hostname to host-b and re-register with an auth key (tailscale up --force-reauth, or logout/login). This is the normal path for automated (re)provisioning, where a device is re-imaged or renamed and re-enrolled with the same machine key and user.
  3. headscale nodes list shows Hostname=host-b, GivenName=host-a.
  4. host-a.<base_domain> still resolves; host-b.<base_domain> does not.
  5. Wait for any number of MapRequests. The state never converges — step 2 already wrote the new Hostinfo, so hostinfoChanged is false from then on.

Environment

markdown
- Headscale version: v0.29.3 (also present on main @ 3390-line hscontrol/state/state.go)
- Tailscale version: multiple, 1.60-1.80

Runtime environment

  • Headscale is behind a (reverse) proxy
  • Headscale runs in a container

Anything else?

Proposed fix — mirror the existing MapRequest logic in applyAuthNodeUpdate:

go
updatedNodeView, ok := s.nodeStore.UpdateNode(params.ExistingNode.ID(), func(node *types.Node) {
    node.NodeKey = regData.NodeKey
    node.DiscoKey = regData.DiscoKey

    // Keep GivenName in sync with a client-side hostname change, using the
    // same rule as UpdateNodeFromMapRequest: only re-derive when the current
    // GivenName is still auto-derived, so an admin rename is preserved.
    if params.Hostname != "" && params.Hostname != node.Hostname {
        if isAutoDerivedGivenName(node.GivenName, node.Hostname) {
            node.GivenName = dnsname.SanitizeHostname(params.Hostname)
            // NodeStore.UpdateNode auto-bumps GivenName on collision.
        }
    }

    node.Hostname = params.Hostname
    ...

NodeStore.UpdateNode already bumps on collision via resolveGivenName, so the uniqueness constraint on GivenName is handled by the existing machinery.

Note on existing deployments. Nodes that have already desynced cannot be repaired by this fix alone, because the guard now reads them as admin-renamed. A detection helper would let operators find them:

go
// Nodes where GivenName is neither auto-derived from the current Hostname
// nor from any plausible earlier one are indistinguishable from an admin
// rename, so surfacing them is the most that can be done automatically.

Two options that would each be useful on their own:

  1. Extend the existing hscontrol/state/node_health.go checks with a givenNameMatchesHostname warning, alongside the current givenNameMapsToValidFQDN. Purely advisory, no behaviour change, and it makes the drift visible without an operator having to diff two columns by hand across the whole tailnet.
  2. Add a --reset-given-name flag to headscale nodes rename (or a distinct subcommand) that re-derives GivenName from the current Hostname, so the repair does not require the operator to retype a name that headscale already knows.

Happy to open a PR for the applyAuthNodeUpdate change plus a regression test asserting that reauth with a changed hostname updates GivenName when auto-derived and preserves it after an explicit rename. Let me know if the health-check or CLI additions are wanted in the same PR or separately.