OpenNHP/opennhp · critical

relay forward cap exceeded

Error message

relay forward cap exceeded

What it means

Each relay address is limited to MaxConnectionsPerRelay forwarded client connections. When a relay exceeds that cap the forward is dropped and this error returned, preventing one compromised or busy relay from monopolizing the server connection table.

Solutions

  1. Raise MaxConnectionsPerRelay for high-capacity relays
  2. Distribute clients across multiple relay addresses
  3. Check the relay for leaked/stale forwards that never release slots
  4. Clean up relayConnCount entries when relay connections close

Example fix

// before
const MaxConnectionsPerRelay = 64
// after
const MaxConnectionsPerRelay = 512 // sized per relay load
Defensive patterns

Strategy: fallback

Validate before calling

if relayConnCount[relayAddr] >= maxConnectionsPerRelay {
    return fmt.Errorf("relay %s at per-relay cap", relayAddr)
}

Try / catch

if err := forwardViaRelay(pkt); err != nil {
    if strings.Contains(err.Error(), "relay forward cap exceeded") {
        return tryOtherRelay(pkt) // fail over to another relay
    }
    return err
}

Prevention

When it happens

Trigger: relayConnCount[relayAddrStr] would exceed MaxConnectionsPerRelay after this forward and no stale slot could be transferred to the new client.

Common situations: Many clients NATed behind a single relay IP; relay leaking forwards; per-relay cap set too low relative to relay subscriber count.

Related errors


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

Appendix: source

Thrown at endpoints/server/msghandler.go:973

			//
			// In practice the OLD <= MaxConnectionsPerRelay invariant
			// means this branch is unreachable for the current
			// constant cap; it becomes reachable if
			// MaxConnectionsPerRelay is ever hot-reloaded to a lower
			// value mid-flight. The fix-up is cheap so do it
			// unconditionally.
			if transferred && curr > 0 {
				s.relayConnCount[relayAddrStr]--
				if s.relayConnCount[relayAddrStr] == 0 {
					delete(s.relayConnCount, relayAddrStr)
				}
			}
			s.relayConnCountMutex.Unlock()
			s.remoteConnectionMapMutex.Unlock()
			s.device.ReleasePoolPacket(innerPkt)
			log.Critical("server-relay[HandleRelayForward] relay %s exceeded MaxConnectionsPerRelay (%d), dropping forward",
				relayAddrStr, MaxConnectionsPerRelay)
			return fmt.Errorf("relay forward cap exceeded")
		}
		if !transferred {
			s.relayConnCount[relayAddrStr]++
		}
		s.relayConnCountMutex.Unlock()

		conn = &UdpConn{mapKey: connKey}
		conn.ConnData = &core.ConnectionData{
			InitTime:          recvTime,
			LastLocalRecvTime: recvTime,
			Device:            s.device,
			LocalAddr:         s.listenAddr,
			RemoteAddr:        relayAddr,
			RealRemoteAddr:    realAddr,
			// CookieStore omitted: see udpserver.go for rationale.
			RemoteTransactionMap: make(map[uint64]*core.RemoteTransaction),
			TimeoutMs:            DefaultAgentConnectionTimeoutMs,
			SendQueue:            make(chan *core.Packet, PacketQueueSizePerConnection),

View on GitHub (pinned to 6e04ca5ff0)