{"record":{"id":"2417679459880a91","repo":"slackhq/nebula","slug":"errexistinghostinfo","errorCode":"ErrExistingHostInfo","errorMessage":"existing hostinfo","messagePattern":"existing hostinfo","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"info","filePath":"handshake_manager.go","lineNumber":414,"sourceCode":"\tif !doTrigger {\n\t\t// Add any calculated remotes, and trigger early handshake if one found\n\t\tdoTrigger = hm.lightHouse.addCalculatedRemotes(vpnAddr)\n\t}\n\n\tif doTrigger {\n\t\tselect {\n\t\tcase hm.trigger <- vpnAddr:\n\t\tdefault:\n\t\t}\n\t}\n\n\thm.Unlock()\n\thm.lightHouse.QueryServer(vpnAddr)\n\treturn hostinfo\n}\n\nvar (\n\tErrExistingHostInfo    = errors.New(\"existing hostinfo\")\n\tErrAlreadySeen         = errors.New(\"already seen\")\n\tErrLocalIndexCollision = errors.New(\"local index collision\")\n)\n\n// CheckAndComplete checks for any conflicts in the main and pending hostmap\n// before adding hostinfo to main. If err is nil, it was added. Otherwise err will be:\n//\n// ErrAlreadySeen if we already have an entry in the hostmap that has seen the\n// exact same handshake packet\n//\n// ErrExistingHostInfo if we already have an entry in the hostmap for this\n// VpnIp and the new handshake was older than the one we currently have\n//\n// ErrLocalIndexCollision if we already have an entry in the main or pending\n// hostmap for the hostinfo.localIndexId.\nfunc (hm *HandshakeManager) CheckAndComplete(hostinfo *HostInfo, handshakePacket uint8, f *Interface) (*HostInfo, error) {\n\thm.mainHostMap.Lock()\n\tdefer hm.mainHostMap.Unlock()","sourceCodeStart":396,"sourceCodeEnd":432,"githubUrl":"https://github.com/slackhq/nebula/blob/dd8f660c0ac37903ec4080ca4d3c861ba9342ceb/handshake_manager.go#L396-L432","documentation":"ErrExistingHostInfo is returned by CheckAndComplete when the main hostmap already contains a hostinfo entry for the same VPN address, and the existing entry is at least as new (its lastHandshakeTime is greater-or-equal and it is not the initiator). The completed handshake is redundant/stale, so the new hostinfo is rejected in favor of the existing one.","triggerScenarios":"Completing a handshake for a host whose existing main-hostmap entry has lastHandshakeTime >= the new hostinfo's and the existing entry is not the initiator (handshake_manager.go:448) — i.e. a stale or duplicated handshake completion raced with a newer established connection.","commonSituations":"Both peers initiating handshakes to each other simultaneously (race); retransmitted/late handshake responses arriving after a newer handshake completed; flapping networks causing rapid re-handshakes.","solutions":["This is usually benign — the manager logs 'Handshake too old' and keeps the existing connection; no action needed","If it recurs persistently, check for clock skew between hosts (lastHandshakeTime comparison) and symmetric initiation loops (both sides dialing each other)","Ensure lighthouse/roaming config isn't causing both sides to continuously re-initiate"],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":null,"typeGuard":"func isStaleHandshake(existing, incoming *HostInfo) bool {\n    return existing != nil &&\n        existing.lastHandshakeTime >= incoming.lastHandshakeTime &&\n        !existing.ConnectionState.initiator\n}","tryCatchPattern":"hi, err := hm.CheckAndComplete(hostinfo)\nif errors.Is(err, ErrExistingHostInfo) {\n    // benign: existing newer connection kept; log at info and move on\n    return hi, nil\n}","preventionTips":["NTP-sync all hosts to avoid lastHandshakeTime skew","Avoid symmetric initiation (both peers dialing simultaneously)","Don't treat this error as fatal; the existing connection is preserved"],"tags":["handshake","hostmap","race-condition"],"backgroundTag":"stale-handshake","analyzedSha":"dd8f660c0ac37903ec4080ca4d3c861ba9342ceb","analyzedAt":"2026-09-03T11:13:55.444Z","contentChangedAt":"2026-09-03T11:13:55.444Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}