{"record":{"id":"4b46a8ebd2c44294","repo":"juanfont/headscale","slug":"errsshdstnodenotfound","errorCode":"ErrSSHDstNodeNotFound","errorMessage":"dst node not found","messagePattern":"dst node not found","errorType":"http","errorClass":"HTTPError","httpStatus":404,"severity":"warning","filePath":"hscontrol/noise.go","lineNumber":402,"sourceCode":"\t\t\terr,\n\t\t))\n\n\t\treturn\n\t}\n\n\t// Authenticate the Noise session: the destination node is the\n\t// tailscaled instance asking us whether to permit an incoming SSH\n\t// connection, so its Noise session must belong to dst. Without this\n\t// check any unauthenticated client could open a Noise tunnel with a\n\t// throwaway machine key and pollute lastSSHAuth for arbitrary\n\t// (src, dst) pairs, defeating SSH check-mode's stolen-key\n\t// protections.\n\tdstNode, ok := ns.headscale.state.GetNodeByID(dstNodeID)\n\tif !ok {\n\t\thttpError(writer, NewHTTPError(\n\t\t\thttp.StatusNotFound,\n\t\t\t\"dst node not found\",\n\t\t\tfmt.Errorf(\"%w: %d\", ErrSSHDstNodeNotFound, dstNodeID),\n\t\t))\n\n\t\treturn\n\t}\n\n\tif dstNode.MachineKey() != ns.machineKey {\n\t\thttpError(writer, NewHTTPError(\n\t\t\thttp.StatusUnauthorized,\n\t\t\t\"machine key does not match dst node\",\n\t\t\tfmt.Errorf(\n\t\t\t\t\"%w: machine key %s, dst node %d\",\n\t\t\t\tErrSSHMachineKeyMismatch, ns.machineKey.ShortString(), dstNodeID,\n\t\t\t),\n\t\t))\n\n\t\treturn\n\t}\n","sourceCodeStart":384,"sourceCodeEnd":420,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/noise.go#L384-L420","documentation":"Returned by the SSH action handler when the dst_node_id in the URL does not match any node in the state store (GetNodeByID miss). The dst node is the tailscaled that would accept the incoming SSH connection; if it does not exist, no SSH verdict can be produced.","triggerScenarios":"An SSH check-mode request naming a dst node that was deleted, expired, or never existed; a stale SSHAction URL from a client cached across node removal.","commonSituations":"Node removed with 'headscale nodes delete' while an SSH check session was pending; node IDs reused/shifted after database changes; crafted requests probing the endpoint.","solutions":["Verify the dst node still exists: headscale nodes list","If it was deleted, let the client re-run 'tailscale ssh' to start a fresh session","Check for ID confusion between src and dst when reading logs"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if _, ok := h.state.GetNodeByID(dstNodeID); !ok {\n    return fmt.Errorf(\"dst node %d does not exist; it may have been deleted — retry the SSH session\", dstNodeID)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat node deletion as invalidating in-flight SSH check sessions","In clients, retry the SSH connection on 404 dst-not-found instead of reusing the old action URL"],"tags":["ssh","node-id","not-found","check-mode"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}