OpenNHP/opennhp · warning

received replay packet

Error message

received replay packet

What it means

validatePeer detects that the packet's remote send timestamp equals one already seen (the connection's replay window/last-send-time tracking), meaning this exact packet was received before. The responder rejects it as a replay attack defense: an attacker capturing and retransmitting a legitimate encrypted NHP packet must not be able to replay knock or ack messages.

Solutions

  1. Verify the packet source is legitimate; if replays are malicious, keep the automatic block (threat counter triggers SendBlockSignal) and investigate the capturing attacker path.
  2. On the client, ensure each send uses a monotonically increasing timestamp and never retransmits the exact same serialized packet; regenerate a fresh packet per attempt.
  3. Check for network duplication (bonded interfaces, misconfigured switches, VPN tunnels multiplying packets) and fix at the network layer.
  4. If a custom integration is the source, review its packet construction so send time and nonce are updated for every transmission.

Example fix

// before: client retransmits the same serialized packet on timeout
retry(packetBytes)

// after: rebuild packet with fresh timestamp/nonce each attempt
newPacket := buildPacket(nowMillis())
send(newPacket)
Defensive patterns

Strategy: retry

Validate before calling

// client-side: never retransmit an identical serialized packet
var lastSentTs int64
func beforeSend(pkt *Packet) error {
    if pkt.SendTimeMs <= atomic.LoadInt64(&lastSentTs) {
        return errors.New("timestamp must strictly increase per packet")
    }
    atomic.StoreInt64(&lastSentTs, pkt.SendTimeMs)
    return nil
}

Try / catch

if err := send(pkt); err != nil && strings.Contains(err.Error(), "replay packet") {
    // rebuild with a fresh timestamp/nonce and retry once
    pkt = buildPacket(time.Now().UnixMilli())
    err = send(pkt)
}

Prevention

When it happens

Trigger: A UDP packet whose signed/encrypted remoteSendTime is identical to a previously processed packet on the same connection arrives again — either a genuine network duplicate (retransmission at UDP level), or an active replay attack, or a peer whose clock/stamp logic is broken and emits identical timestamps.

Common situations: Aggressive UDP retransmit settings in the client, packet duplication by network equipment, an attacker replaying captured knock packets to reopen firewall rules, or a buggy custom client reusing the same timestamp for multiple sends.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/7a1024d607217201. Report an issue: GitHub.

Appendix: source

Thrown at nhp/core/responder.go:580

	}

	remoteSendTime := int64(binary.BigEndian.Uint64(tsBytes[:]))

	if shouldCheckRecvAttack(ppd.device.deviceType, peerDeviceType, ppd.HeaderType) {
		// block remote if threat level is reached
		if remoteSendTime < ppd.ConnData.LastRemoteSendTime {
			// replay packet, drop
			log.Critical("received replay packet from %s, drop packet", ppd.ConnData.RemoteAddr.String())
			// threat plus 1
			threat := atomic.AddInt32(&ppd.ConnData.RecvThreatCount, 1)
			// with high queue number, the device may use ConnData channels when conn is already closed
			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 replay packet")
			return err
		}
		if remoteSendTime < ppd.ConnData.LastRemoteSendTime+MinimalRecvIntervalMs*int64(time.Millisecond) {
			// flood packet, drop
			log.Critical("received flood packet from %s, drop packet", ppd.ConnData.RemoteAddr.String())
			// threat plus 1
			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 flood packet")
			return err
		}
	}
	if remoteSendTime < (ppd.LocalInitTime - 600*int64(time.Second)) {

View on GitHub (pinned to 6e04ca5ff0)