netbirdio/netbird · error

delete snat rule: %w

Error message

delete snat rule: %w

What it means

DeleteDNATRule issues an immediate NFT_MSG_DELRULE for the cached _snat rule (the masquerade rule in netbird-rt-postrouting). This error wraps the kernel's negative ACK, is appended to the same multierror as the dnat deletion and flush, and on failure the map entry and forwarding reference are intentionally kept so a retry completes the teardown.

Source

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

	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 {
			merr = multierror.Append(merr, fmt.Errorf(flushError, err))
		}
	}

	// Release the refcount only once the rules are gone from the kernel. On
	// failure (including the refreshRulesMap error above) the rules and their
	// map entries remain, keeping forwarding on until a retry removes them.
	if merr == nil {
		delete(r.rules, ruleKey+dnatSuffix)
		delete(r.rules, ruleKey+snatSuffix)

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Check 'sudo nft list chain <table> netbird-rt-postrouting' for the masquerade rule; if absent, ENOENT is benign.
  2. Retry DeleteDNATRule so refreshRulesMap picks up current handles for both halves.
  3. Verify root/CAP_NET_ADMIN.
  4. After a successful retry, confirm forwarding was released (the refcount drop happens only when merr is nil).

Example fix

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

// after: tolerate an externally-removed masquerade rule
} else if err := r.conn.DelRule(masqRule); err != nil {
    if errors.Is(err, unix.ENOENT) {
        log.Warnf("snat rule %s already absent", ruleKey+snatSuffix)
    } else {
        merr = multierror.Append(merr, fmt.Errorf("delete snat rule: %w", err))
    }
}
Defensive patterns

Strategy: try-catch

Try / catch

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

Prevention

When it happens

Trigger: Stale handle (the masquerade rule was removed externally, ENOENT), netlink socket failure, or missing CAP_NET_ADMIN.

Common situations: External nftables rewrites racing the deletion; the dnat rule deletion succeeded but the snat half failed halfway through teardown; unprivileged containers.

Related errors


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