{"record":{"id":"9ff2893c70012f6f","repo":"OpenNHP/opennhp","slug":"received-stale-packet","errorCode":null,"errorMessage":"received stale packet","messagePattern":"received stale packet","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"nhp/core/responder.go","lineNumber":609,"sourceCode":"\t\t\t\t// block source address\n\t\t\t\tppd.ConnData.SendBlockSignal()\n\t\t\t}\n\t\t\terr = fmt.Errorf(\"received flood packet\")\n\t\t\treturn err\n\t\t}\n\t}\n\tif remoteSendTime < (ppd.LocalInitTime - 600*int64(time.Second)) {\n\t\t// send remote timestamp is too old than receive local time, drop\n\t\t// note there might be time calibration error between remote and local devices\n\t\tlog.Critical(\"received stale packet from %s, drop packet\", ppd.ConnData.RemoteAddr.String())\n\t\tthreat := atomic.AddInt32(&ppd.ConnData.RecvThreatCount, 1)\n\t\tif threat > ThreatCountBeforeBlock && !ppd.ConnData.IsClosed() {\n\t\t\t// clamp threat count to avoid overflow\n\t\t\tatomic.StoreInt32(&ppd.ConnData.RecvThreatCount, ThreatCountBeforeBlock)\n\t\t\t// block source address\n\t\t\tppd.ConnData.SendBlockSignal()\n\t\t}\n\t\terr = fmt.Errorf(\"received stale packet\")\n\t\treturn err\n\t}\n\n\t// update remote last send time\n\tatomic.StoreInt64(&ppd.ConnData.LastRemoteSendTime, remoteSendTime)\n\t// clear threat\n\tatomic.StoreInt32(&ppd.ConnData.RecvThreatCount, 0)\n\n\t// handle knock packet at overload before going into body decryption.\n\t// sendCookie derives the cookie statelessly from the device's signing\n\t// key and the remote ip:port + time window, so there is nothing to\n\t// pre-generate or store per connection.\n\tif ppd.device.deviceType == NHP_SERVER && ppd.Overload && (ppd.HeaderType == NHP_KNK || ppd.HeaderType == DHP_KNK) {\n\t\tppd.sendCookie()\n\t\terr = ErrServerRejectWithCookie\n\t\treturn err\n\t}\n","sourceCodeStart":591,"sourceCodeEnd":627,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/nhp/core/responder.go#L591-L627","documentation":"validatePeer rejects packets whose remote send timestamp is older than the local receive time minus 600 seconds (10 minutes). Stale packets are dropped because NHP treats timestamps as freshness proof — a packet older than the tolerance window could be a replay from long ago or produced by a badly desynchronized clock. Like the other threat paths, repeated offenses raise the threat count and can trigger an address block.","triggerScenarios":"A packet arrives whose remoteSendTime is more than 600 seconds behind ppd.LocalInitTime: caused by clock skew greater than 10 minutes on the sending device, a queue/backlog delaying delivery beyond the window, or replay of an old captured packet.","commonSituations":"VMs or embedded devices without NTP whose clocks drift by minutes/hours, containers resuming from snapshot with a stale clock, cold-standby nodes replaying queued packets, or attackers replaying old traffic (deliberately stale).","solutions":["Synchronize the sending host's clock with NTP/chrony so skew stays well under the 10-minute tolerance, then re-run the flow.","Check the sender for suspend/resume or snapshot-restore events that freeze the clock, and force a time resync after resume.","Verify there is no long-lived queuing/proxy between peer and responder holding packets beyond the freshness window.","If the source was auto-blocked after repeated stale packets, clear the block after clocks are corrected."],"exampleFix":"// before: sender clock drifting (no NTP)\n$ timedatectl // NTP: off, 12 min behind\n\n// after\n$ sudo timedatectl set-ntp true // keep skew < 600s tolerance","handlingStrategy":"validation","validationCode":"// client-side: refuse to send if local clock is clearly wrong\nfunc checkClockSkew() error {\n    resp, err := http.Get(ntpOrServerTimeURL)\n    if err != nil { return err }\n    serverMs, _ := strconv.ParseInt(strings.TrimSpace(readBody(resp)), 10, 64)\n    if abs(time.Now().UnixMilli()-serverMs) > 300_000 { // 5 min headroom vs 10 min limit\n        return fmt.Errorf(\"clock skew %dms exceeds safe tolerance; sync NTP\", time.Now().UnixMilli()-serverMs)\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"if err := sendKnock(pkt); err != nil && strings.Contains(err.Error(), \"stale packet\") {\n    // resync time, rebuild packet with current timestamp, retry\n    syncSystemTime()\n    err = sendKnock(buildPacket(time.Now().UnixMilli()))\n}","preventionTips":["Run NTP/chrony on every host that sends NHP packets","Force a time resync after VM resume, snapshot restore, or container restart","Keep system clock skew well under the 600-second freshness window","Alert on 'stale packet' log entries as an early sign of clock drift"],"tags":["clock-skew","ntp","packet-validation","security"],"backgroundTag":"invalid-date-format","analyzedSha":"6e04ca5ff03222a699c24205cd4bf8fee9af7ffe","analyzedAt":"2026-09-07T15:44:59.941Z","contentChangedAt":"2026-09-07T15:44:59.941Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}