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

  1. Synchronize the sending host's clock with NTP/chrony so skew stays well under the 10-minute tolerance, then re-run the flow.
  2. Check the sender for suspend/resume or snapshot-restore events that freeze the clock, and force a time resync after resume.
  3. Verify there is no long-lived queuing/proxy between peer and responder holding packets beyond the freshness window.
  4. 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

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


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)