netbirdio/netbird · error

delete inbound DNAT rule %s: %w

Error message

delete inbound DNAT rule %s: %w

What it means

RemoveInboundDNAT issues an immediate NFT_MSG_DELRULE for the cached inbound-dnat rule after refreshing handles. This error wraps the kernel's negative ACK. The Handle==0 stale-entry case is handled separately above it, so this path means a real handle was sent and the kernel refused to delete it.

Source

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

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

	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,

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Check 'sudo nft list chain <table> netbird-rt-redirect': an absent rule means ENOENT and success.
  2. Call RemoveInboundDNAT again; the refresh at its top re-reads handles.
  3. Verify root/CAP_NET_ADMIN.
  4. Prevent external tooling from rewriting the netbird tables during teardown.

Example fix

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

// after: treat an already-deleted rule as success
if err := r.conn.DelRule(rule); err != nil {
    if errors.Is(err, unix.ENOENT) {
        log.Warnf("inbound DNAT rule %s already absent", ruleID)
        delete(r.rules, ruleID)
        return nil
    }
    return fmt.Errorf("delete inbound DNAT rule %s: %w", ruleID, err)
}
Defensive patterns

Strategy: try-catch

Try / catch

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

Prevention

When it happens

Trigger: The handle went stale between refreshRulesMap and DelRule (rule removed externally, ENOENT), netlink socket failure, or missing CAP_NET_ADMIN.

Common situations: External nft flush or firewalld reload racing the removal; second cleanup caller deleting the same rule; containerized agent without NET_ADMIN.

Related errors


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