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
- Verify the packet source is legitimate; if replays are malicious, keep the automatic block (threat counter triggers SendBlockSignal) and investigate the capturing attacker path.
- 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.
- Check for network duplication (bonded interfaces, misconfigured switches, VPN tunnels multiplying packets) and fix at the network layer.
- 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
- Regenerate the packet (fresh timestamp and nonce) for every send attempt
- Check client retry logic never resends byte-identical payloads
- Investigate network paths that duplicate UDP packets (bonds, VPNs, load balancers)
- Treat repeated replay errors as a possible active attack and review packet capture exposure
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
- peer does not match its previous address on this connection
- received flood packet
- received stale packet
- invalid resource ID: potential path traversal attack
- invalid file name
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)