netbirdio/netbird · error

flush delete output DNAT rule: %w

Error message

flush delete output DNAT rule: %w

What it means

Returned by RemoveOutputDNAT (client/firewall/nftables/router_linux.go:2105) when the conn.Flush() following DelRule fails. DelRule only buffers the delete netlink message; Flush commits it, so this is the kernel's asynchronous verdict on the deletion: EPERM, ENOENT (rule handle already gone), or a netlink send failure (buffer full, socket closed).

Source

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

	ruleID := fmt.Sprintf("output-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("output 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 output DNAT rule %s: %w", ruleID, err)
	}
	if err := r.conn.Flush(); err != nil {
		return fmt.Errorf("flush delete output DNAT rule: %w", err)
	}
	delete(r.rules, ruleID)

	return nil
}

// applyNetwork generates nftables expressions for networks (CIDR) or sets
func (r *router) applyNetwork(
	network firewall.Network,
	setPrefixes []netip.Prefix,
	isSource bool,
) ([]expr.Any, error) {
	if network.IsSet() {
		exprs, err := r.getIpSet(network.Set, setPrefixes, isSource)
		if err != nil {
			return nil, fmt.Errorf("source: %w", err)
		}
		return exprs, nil

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Retry the removal after a fresh refreshRulesMap when the kernel reports ENOENT - the rule is gone and the map just needs resyncing
  2. Recreate the nftables manager if the socket is dead, then retry; the re-created table contains no stale DNAT rule so the removal goal is already met
  3. Keep delete batches small or flush incrementally to avoid ENOBUFS on netlink
  4. Verify the process retains CAP_NET_ADMIN for the whole daemon lifetime, not just at start

Example fix

// before
if err := r.conn.Flush(); err != nil {
    return fmt.Errorf("flush delete output DNAT rule: %w", err)
}
// after
if err := r.conn.Flush(); err != nil {
    if isNotExistErr(err) {
        delete(r.rules, ruleID)
        return nil
    }
    return fmt.Errorf("flush delete output DNAT rule: %w", err)
}
delete(r.rules, ruleID)
Defensive patterns

Strategy: fallback

Try / catch

if err := router.RemoveOutputDNAT(...); err != nil {
    switch {
    case isNotExistErr(rootCause(err)):
        return nil
    case isPermissionErr(rootCause(err)):
        return fmt.Errorf("need CAP_NET_ADMIN to remove DNAT: %w", err)
    default:
        // socket may be dead; a recreated manager has no stale rule to remove
        firewall.Reset()
        return nil
    }
}

Prevention

When it happens

Trigger: The same rule deleted externally between DelRule and Flush; netlink socket invalidated (network namespace teardown, daemon fork); too many buffered operations in one batch exceeding netlink buffer limits; missing capabilities.

Common situations: Container teardown where the network namespace is being destroyed concurrently; large batch removals of DNAT rules; agent shutdown racing with system nftables cleanup scripts.

Related errors


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