netbirdio/netbird · error

delete dnat rule: %w

Error message

delete dnat rule: %w

What it means

DeleteDNATRule issues an immediate NFT_MSG_DELRULE for the cached _dnat rule (the redirect in netbird-rt-redirect). This error wraps the kernel's negative ACK and is accumulated into a multierror together with the snat deletion and flush; on failure the map entries survive and the forwarding reference is kept for a later retry.

Source

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

	if err := r.refreshRulesMap(); err != nil {
		return fmt.Errorf(refreshRulesMapError, err)
	}

	_, hadDNAT := r.rules[ruleKey+dnatSuffix]
	_, hadSNAT := r.rules[ruleKey+snatSuffix]
	if !hadDNAT && !hadSNAT {
		return nil
	}

	var merr *multierror.Error
	var needsFlush bool

	if dnatRule, exists := r.rules[ruleKey+dnatSuffix]; exists {
		if dnatRule.Handle == 0 {
			log.Warnf("dnat rule %s has no handle, removing stale entry", ruleKey+dnatSuffix)
			delete(r.rules, ruleKey+dnatSuffix)
		} else if err := r.conn.DelRule(dnatRule); err != nil {
			merr = multierror.Append(merr, fmt.Errorf("delete dnat rule: %w", err))
		} else {
			needsFlush = true
		}
	}

	if masqRule, exists := r.rules[ruleKey+snatSuffix]; exists {
		if masqRule.Handle == 0 {
			log.Warnf("snat rule %s has no handle, removing stale entry", ruleKey+snatSuffix)
			delete(r.rules, ruleKey+snatSuffix)
		} else if err := r.conn.DelRule(masqRule); err != nil {
			merr = multierror.Append(merr, fmt.Errorf("delete snat rule: %w", err))
		} else {
			needsFlush = true
		}
	}

	if needsFlush {
		if err := r.conn.Flush(); err != nil {

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Confirm with 'sudo nft list chain <table> netbird-rt-redirect' whether the rule is already gone; ENOENT then means success.
  2. Retry DeleteDNATRule: refreshRulesMap re-reads handles and the second attempt deletes what remains.
  3. Ensure the agent has root or CAP_NET_ADMIN.
  4. Stop external tooling from rewriting the netbird tables while routes exist.

Example fix

// before
} else if err := r.conn.DelRule(dnatRule); err != nil {
    merr = multierror.Append(merr, fmt.Errorf("delete dnat rule: %w", err))
}

// after: a vanished rule satisfies the deletion intent
} else if err := r.conn.DelRule(dnatRule); err != nil {
    if errors.Is(err, unix.ENOENT) {
        log.Warnf("dnat rule %s already absent", ruleKey+dnatSuffix)
    } else {
        merr = multierror.Append(merr, fmt.Errorf("delete dnat rule: %w", err))
    }
}
Defensive patterns

Strategy: try-catch

Try / catch

} else if err := r.conn.DelRule(dnatRule); err != nil {
    if errors.Is(err, unix.ENOENT) {
        log.Warnf("dnat rule %s already absent", ruleKey+dnatSuffix)
    } else {
        merr = multierror.Append(merr, fmt.Errorf("delete dnat rule: %w", err))
    }
}

Prevention

When it happens

Trigger: The cached handle is stale because the rule was already removed externally (ENOENT is the common case), the netlink socket fails, or the process lacks CAP_NET_ADMIN.

Common situations: External ruleset flushes between refreshRulesMap and DelRule; concurrent deletions of the same forward rule; containerized agents without NET_ADMIN.

Related errors


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