netbirdio/netbird · error

add elements to set %s: %w

Error message

add elements to set %s: %w

What it means

UpdateSet converts the prefixes to set elements and adds them with SetAddElements, which writes NFT_MSG_NEWSETELEM to the kernel immediately. Failures include ENOENT (the set vanished between GetSetByName and the add), netlink message-size limits with very large element batches (the package documents that about 1638 prefixes start to fail and caps usage at maxPrefixesSet=1500), and permission errors.

Source

Thrown at client/firewall/nftables/router_linux.go:1862

		delete(r.rules, ruleKey+snatSuffix)

		if err := r.ipFwdState.ReleaseForwarding(r.af.tableFamily == nftables.TableFamilyIPv6); err != nil {
			log.Errorf("%v", err)
		}
	}

	return nberrors.FormatErrorOrNil(merr)
}

func (r *router) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
	nfset, err := r.conn.GetSetByName(r.workTable, set.HashedName())
	if err != nil {
		return fmt.Errorf("get set %s: %w", set.HashedName(), err)
	}

	elements := r.convertPrefixesToSet(prefixes)
	if err := r.conn.SetAddElements(nfset, elements); err != nil {
		return fmt.Errorf("add elements to set %s: %w", set.HashedName(), err)
	}

	if err := r.conn.Flush(); err != nil {
		return fmt.Errorf(flushError, err)
	}

	log.Debugf("updated set %s with prefixes %v", set.HashedName(), prefixes)

	return nil
}

// AddInboundDNAT adds an inbound DNAT rule redirecting traffic from NetBird peers to local services.
func (r *router) AddInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
	ruleID := fmt.Sprintf("inbound-dnat-%s-%s-%d-%d", localAddr.String(), protocol, originalPort, translatedPort)

	if _, exists := r.rules[ruleID]; exists {
		return nil
	}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Chunk element additions below the netlink limit (the codebase treats 1500 as the safe bound).
  2. Retry after confirming the set still exists with 'sudo nft list set <table> <name>'.
  3. Prefer adding only new elements (diff) over re-adding the full prefix list each update.
  4. Verify CAP_NET_ADMIN when the error is not size-related.

Example fix

// before
elements := r.convertPrefixesToSet(prefixes)
if err := r.conn.SetAddElements(nfset, elements); err != nil {
    return fmt.Errorf("add elements to set %s: %w", set.HashedName(), err)
}

// after: chunk to stay under netlink message limits
const chunkSize = 512
elements := r.convertPrefixesToSet(prefixes)
for i := 0; i < len(elements); i += chunkSize {
    end := min(i+chunkSize, len(elements))
    if err := r.conn.SetAddElements(nfset, elements[i:end]); err != nil {
        return fmt.Errorf("add elements to set %s (chunk at %d): %w", set.HashedName(), i, err)
    }
}
Defensive patterns

Strategy: validation

Validate before calling

// Stay under the netlink element limit before adding
const maxPrefixesSet = 1500 // ~1638 starts to fail in practice

func prefixBatchSafe(n int) error {
    if n > maxPrefixesSet {
        return fmt.Errorf("%d prefixes exceeds the safe netlink batch limit %d; split the update", n, maxPrefixesSet)
    }
    return nil
}

Try / catch

elements := r.convertPrefixesToSet(prefixes)
if err := r.conn.SetAddElements(nfset, elements); err != nil {
    if errors.Is(err, unix.EMSGSIZE) || errors.Is(err, unix.ENOMEM) {
        // batch too large: caller should chunk and retry
        return fmt.Errorf("add elements to set %s: batch of %d too large: %w", set.HashedName(), len(elements), err)
    }
    return fmt.Errorf("add elements to set %s: %w", set.HashedName(), err)
}

Prevention

When it happens

Trigger: One UpdateSet call carrying hundreds or thousands of prefixes; the set deleted concurrently mid-update; unprivileged netlink access.

Common situations: Large network routes (big CIDR lists) pushed in a single update; races with external nft flush; hosts with small netlink buffers.

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/01c31708a77c9662. Report an issue: GitHub.