OpenNHP/opennhp · warning
rkn rate limit exceeded
Error message
rkn rate limit exceeded
What it means
When the server is in overload state, inner NHP_RKN (knock-continue) packets arriving via relay are additionally throttled per real client IP by s.rknLimiter. Packets exceeding the per-IP rate are dropped with this error; it is drop-only and never block-lists the client.
Solutions
- Reduce client RKN send frequency or add client-side backoff/jitter
- Scale the server or raise MaxConcurrentConnection so overload mode clears
- Inspect rknLimiter configuration for a per-IP rate appropriate for NATed clients
- Retry the knock after the rate window elapses
Example fix
// before: tight retry loop on client
for { sendRKN() }
// after
if !lastSent.IsZero() && time.Since(lastSent) < rknInterval { time.Sleep(rknInterval) }
sendRKN() Defensive patterns
Strategy: retry
Try / catch
err := forwardViaRelay(pkt)
if err != nil && strings.Contains(err.Error(), "rate limit") {
time.Sleep(backoff) // exponential backoff with jitter, then retry
return forwardViaRelay(pkt)
} Prevention
- Add client-side backoff/jitter for RKN retries
- Size the server so overload mode rarely triggers
- Account for NAT aggregation when setting per-IP rates
When it happens
Trigger: Device IsOverload() is true (total connections above OverloadConnectionThreshold) and a relayed NHP_RKN arrives from the same real client IP faster than rknLimiter's per-IP allowance.
Common situations: Client retry loops hammering RKN during a load spike; relay aggregating many clients behind one IP hitting a single per-IP bucket; benchmark or load test via relay.
Related errors
- relay overloaded
- relay source address
- inner packet too large
- server connection table full
- relay forward cap exceeded
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/f5bc52266bc4416b.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/server/msghandler.go:847
}
innerPkt.HeaderType = innerType
log.Info("server-relay[HandleRelayForward] inner [%s] from real client %s via relay %s",
core.HeaderTypeToString(innerType), realAddr, relayAddrStr)
// Same RKN-under-overload gate as the direct-UDP path
// (recvPacketRoutine), but keyed on the REAL client IP rather than the
// relay's: a relay legitimately fans out many clients, so keying on
// the relay address would let one busy relay's honest traffic throttle
// itself while doing nothing to isolate a single abusive client. The
// inner RKN reaches the same cookie-verify ECDH (via
// ForwardInboundPacket -> connectionRoutine -> RecvPacketToMsg), so it
// needs the same pre-ECDH throttle. Dropped-only, no block-listing.
if innerType == core.NHP_RKN && s.device.IsOverload() {
if !s.rknLimiter.allow(realAddr, time.Now().UnixNano()) {
s.device.ReleasePoolPacket(innerPkt)
log.Warning("server-relay[HandleRelayForward] inner RKN from real client %s (via relay %s) dropped: per-IP rate limit exceeded under overload",
realAddr, relayAddrStr)
return fmt.Errorf("rkn rate limit exceeded")
}
}
// Build or reuse a connection keyed on "relay|<relayAddr>|<realClientAddr>".
// This avoids collisions with the relay's own NHP_RLY connection (which is
// already keyed on relayAddrStr) and isolates per-client anti-replay state.
// RemoteAddr must be the relay's UDP address so that response packets
// (ACK/COK) are sent back to the relay — the relay then forwards them
// to the browser over HTTP. The real client address is used only for
// auth/logging purposes.
//
// The '|' separator is required, not cosmetic: an IPv6 address renders
// as "[2001:db8::1]:80", so a ':'-delimited key could not be reliably
// split from the right. See relayConnKeySep for the full reasoning.
relayAddr := ppd.ConnData.RemoteAddr
connKey := relayConnKeyPrefix + relayAddrStr + relayConnKeySep + realAddr.String()
recvTime := time.Now().UnixNano()
View on GitHub (pinned to 6e04ca5ff0)