OpenNHP/opennhp · warning
received stale packet
Error message
received stale packet
What it means
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.
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.
Example fix
// before: sender clock drifting (no NTP) $ timedatectl // NTP: off, 12 min behind // after $ sudo timedatectl set-ntp true // keep skew < 600s tolerance
Defensive patterns
Strategy: validation
Validate before calling
// client-side: refuse to send if local clock is clearly wrong
func checkClockSkew() error {
resp, err := http.Get(ntpOrServerTimeURL)
if err != nil { return err }
serverMs, _ := strconv.ParseInt(strings.TrimSpace(readBody(resp)), 10, 64)
if abs(time.Now().UnixMilli()-serverMs) > 300_000 { // 5 min headroom vs 10 min limit
return fmt.Errorf("clock skew %dms exceeds safe tolerance; sync NTP", time.Now().UnixMilli()-serverMs)
}
return nil
} Try / catch
if err := sendKnock(pkt); err != nil && strings.Contains(err.Error(), "stale packet") {
// resync time, rebuild packet with current timestamp, retry
syncSystemTime()
err = sendKnock(buildPacket(time.Now().UnixMilli()))
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- received replay packet
- invalid resource ID: potential path traversal attack
- invalid file name
- unexpected signing method
- attestation verification failed
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/9ff2893c70012f6f.
Report an issue: GitHub.
Appendix: source
Thrown at nhp/core/responder.go:609
// block source address
ppd.ConnData.SendBlockSignal()
}
err = fmt.Errorf("received flood packet")
return err
}
}
if remoteSendTime < (ppd.LocalInitTime - 600*int64(time.Second)) {
// send remote timestamp is too old than receive local time, drop
// note there might be time calibration error between remote and local devices
log.Critical("received stale packet from %s, drop packet", ppd.ConnData.RemoteAddr.String())
threat := atomic.AddInt32(&ppd.ConnData.RecvThreatCount, 1)
if threat > ThreatCountBeforeBlock && !ppd.ConnData.IsClosed() {
// clamp threat count to avoid overflow
atomic.StoreInt32(&ppd.ConnData.RecvThreatCount, ThreatCountBeforeBlock)
// block source address
ppd.ConnData.SendBlockSignal()
}
err = fmt.Errorf("received stale packet")
return err
}
// update remote last send time
atomic.StoreInt64(&ppd.ConnData.LastRemoteSendTime, remoteSendTime)
// clear threat
atomic.StoreInt32(&ppd.ConnData.RecvThreatCount, 0)
// handle knock packet at overload before going into body decryption.
// sendCookie derives the cookie statelessly from the device's signing
// key and the remote ip:port + time window, so there is nothing to
// pre-generate or store per connection.
if ppd.device.deviceType == NHP_SERVER && ppd.Overload && (ppd.HeaderType == NHP_KNK || ppd.HeaderType == DHP_KNK) {
ppd.sendCookie()
err = ErrServerRejectWithCookie
return err
}
View on GitHub (pinned to 6e04ca5ff0)