slackhq/nebula · error

ErrMessageCounterExhausted

ErrMessageCounterExhausted

Error message

message counter exhausted

What it means

EncryptDanger refuses to encrypt once the message nonce reaches RejectAfterMessages (MaxUint64 minus RejectHeadroom of 2^40). Beyond that point the remaining nonce space is reserved for out-of-order receive window headroom, so further encryption would break replay protection; the connection must rekey.

Source

Thrown at noiseutil/cipher_state.go:18

package noiseutil

import (
	"errors"
	"fmt"
	"math"

	"github.com/flynn/noise"
)

// RejectHeadroom is the wrap gap for senders racing the counter, sized large enough for any routine count.
const RejectHeadroom = uint64(1) << 40

// RejectAfterMessages is the nonce ceiling: encrypting stops RejectHeadroom short of the wrap.
const RejectAfterMessages = math.MaxUint64 - RejectHeadroom

// ErrMessageCounterExhausted is returned by EncryptDanger once the nonce reaches RejectAfterMessages.
var ErrMessageCounterExhausted = errors.New("message counter exhausted")

// CipherState is the post-handshake AEAD cipher used for the data plane.
// Each supported cipher has its own concrete implementation in this package with the nonce endianness hardcoded,
// so the encrypt/decrypt fast path avoids interface dispatch on the byte order.
type CipherState interface {
	// EncryptDanger encrypts and authenticates a given payload.
	//
	// out is a destination slice to hold the output of the EncryptDanger operation.
	//   - ad is additional data, which will be authenticated and appended to out, but not encrypted.
	//   - plaintext is encrypted, authenticated and appended to out.
	//   - n is a nonce value which must never be re-used with this key.
	//   - nb is a scratch buffer used to assemble the nonce.
	EncryptDanger(out, ad, plaintext []byte, n uint64, nb []byte) ([]byte, error)

	// DecryptDanger authenticates and decrypts a given payload, with the same argument shape as EncryptDanger.
	DecryptDanger(out, ad, ciphertext []byte, n uint64, nb []byte) ([]byte, error)

	// Overhead returns the AEAD tag size, or 0 if the receiver is nil.

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Force a handshake/rekey to establish fresh session keys before the nonce ceiling is reached.
  2. Check that the automatic rekey logic (key reload/rotate timers) is enabled and functioning.
  3. If this appears early in a connection's life, reset the nonce counter to 0 with the new session key.
Defensive patterns

Strategy: try-catch

Validate before calling

if n >= noiseutil.RejectAfterMessages {
    return errors.New("nonce ceiling reached; rekey required")
}

Try / catch

out, err := cs.EncryptDanger(out, ad, plaintext, n, nb)
if errors.Is(err, noiseutil.ErrMessageCounterExhausted) {
    // initiate new handshake and retry on the fresh session
}

Prevention

When it happens

Trigger: A long-lived tunnel sends more than ~2^64-2^40 messages on the same session key and the nonce counter hits RejectAfterMessages; exercised by TestEncryptRejectsExhaustedCounter.

Common situations: Extremely long-lived connections without rekeying, embedded devices that never rotate keys, or test rigs manually passing enormous nonce values.

Related errors


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/750ebbc43927d223. Report an issue: GitHub.