netbirdio/netbird · error

flush delete inbound DNAT rule: %w

Error message

flush delete inbound DNAT rule: %w

What it means

RemoveInboundDNAT commits the queued rule deletion with conn.Flush(); 'flush delete inbound DNAT rule' wraps that commit failure. The map entry is deleted only after a successful flush, so on failure the entry survives and a retry re-attempts the whole removal.

Source

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

	ruleID := fmt.Sprintf("inbound-dnat-%s-%s-%d-%d", localAddr.String(), protocol, originalPort, translatedPort)

	rule, exists := r.rules[ruleID]
	if !exists {
		return nil
	}

	if rule.Handle == 0 {
		log.Warnf("inbound DNAT rule %s has no handle, removing stale entry", ruleID)
		delete(r.rules, ruleID)
		return nil
	}

	if err := r.conn.DelRule(rule); err != nil {
		return fmt.Errorf("delete inbound DNAT rule %s: %w", ruleID, err)
	}
	if err := r.conn.Flush(); err != nil {
		return fmt.Errorf("flush delete inbound DNAT rule: %w", err)
	}
	delete(r.rules, ruleID)

	return nil
}

// ensureNATOutputChain lazily creates the OUTPUT NAT chain on first use.
func (r *router) ensureNATOutputChain() error {
	if _, exists := r.chains[chainNameNATOutput]; exists {
		return nil
	}

	r.chains[chainNameNATOutput] = r.conn.AddChain(&nftables.Chain{
		Name:     chainNameNATOutput,
		Table:    r.workTable,
		Hooknum:  nftables.ChainHookOutput,
		Priority: nftables.ChainPriorityNATDest,
		Type:     nftables.ChainTypeNAT,

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Read the wrapped errno to separate privilege issues from state skew.
  2. Retry RemoveInboundDNAT; the surviving map entry makes it idempotent.
  3. Confirm with 'sudo nft list chain <table> netbird-rt-redirect' whether the rule is actually gone despite the error.
  4. Check for seccomp or LSM policies blocking netlink sendmsg.

Example fix

// before
if err := r.conn.Flush(); err != nil {
    return fmt.Errorf("flush delete inbound DNAT rule: %w", err)
}

// after: downgrade ENOENT (rule already gone) and keep the entry only for real failures
if err := r.conn.Flush(); err != nil {
    if errors.Is(err, unix.ENOENT) {
        log.Warnf("inbound DNAT delete flush: rule already absent")
    } else {
        return fmt.Errorf("flush delete inbound DNAT rule: %w", err)
    }
}
Defensive patterns

Strategy: retry

Try / catch

if err := r.conn.Flush(); err != nil {
    if errors.Is(err, unix.ENOENT) {
        log.Warnf("inbound DNAT delete flush: rule already absent")
        delete(r.rules, ruleID)
        return nil
    }
    return fmt.Errorf("flush delete inbound DNAT rule: %w", err)
}

Prevention

When it happens

Trigger: The kernel rejects the delete inside the batch (stale handle, ENOENT), netlink exchange failure at commit time, or missing CAP_NET_ADMIN.

Common situations: Ruleset churn between DelRule and commit; netlink pressure on hosts with many rules; unprivileged environments.

Related errors


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