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

  1. Reduce client RKN send frequency or add client-side backoff/jitter
  2. Scale the server or raise MaxConcurrentConnection so overload mode clears
  3. Inspect rknLimiter configuration for a per-IP rate appropriate for NATed clients
  4. 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

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


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)