netbirdio/netbird · error

add output DNAT rule: %w

Error message

add output DNAT rule: %w

What it means

Returned by router.AddOutputDNAT (client/firewall/nftables/router_linux.go:2074) when r.conn.Flush() fails after AddRule queued the OUTPUT-chain DNAT rule. Flush submits all buffered netlink messages to the kernel at once, so the error is the kernel's verdict on the rule: EPERM without CAP_NET_ADMIN, ENOENT when the target table/chain vanished between creation and flush, EINVAL for malformed expressions, or ENODEV/EOPNOTSUPP when the nftables kernel module is unavailable (old kernels, restricted containers).

Source

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

		},
		&expr.NAT{
			Type:        expr.NATTypeDestNAT,
			Family:      uint32(r.af.tableFamily),
			RegAddrMin:  1,
			RegProtoMin: 2,
		},
	)

	dnatRule := &nftables.Rule{
		Table:    r.workTable,
		Chain:    r.chains[chainNameNATOutput],
		Exprs:    exprs,
		UserData: []byte(ruleID),
	}
	r.conn.AddRule(dnatRule)

	if err := r.conn.Flush(); err != nil {
		return fmt.Errorf("add output DNAT rule: %w", err)
	}

	r.rules[ruleID] = dnatRule

	return nil
}

// RemoveOutputDNAT removes an OUTPUT chain DNAT rule.
func (r *router) RemoveOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
	if err := r.refreshRulesMap(); err != nil {
		return fmt.Errorf(refreshRulesMapError, err)
	}

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

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

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Run the agent as root (or with CAP_NET_ADMIN) in an environment that exposes nftables; verify with 'nft list ruleset' as the same user
  2. Check for concurrent nftables managers (firewalld, docker) flushing the table, and prefer a dedicated netbird chain/table policy
  3. Recreate the nftables manager (firewall factory Reset/Create) and retry AddOutputDNAT once, since a vanished table is recreated by ensureNATOutputChain
  4. Inspect the exact errno via strace on the netlink sendmsg to distinguish permission (EPERM) from missing-objects (ENOENT) errors

Example fix

// before
if err := r.conn.Flush(); err != nil {
    return fmt.Errorf("add output DNAT rule: %w", err)
}
// after - recreate dependent objects once when the kernel reports them missing
if err := r.conn.Flush(); err != nil {
    if isNotExistErr(err) {
        if rerr := r.ensureNATOutputChain(); rerr == nil {
            r.conn.AddRule(dnatRule)
            err = r.conn.Flush()
        }
    }
    if err != nil {
        return fmt.Errorf("add output DNAT rule: %w", err)
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// before enabling DNAT, confirm nftables is writable as this user
cmd := exec.Command("nft", "list", "ruleset")
if err := cmd.Run(); err != nil {
    return fmt.Errorf("nftables unavailable: %w", err)
}

Try / catch

err := router.AddOutputDNAT(addr, proto, o, t)
if err != nil {
    if isNetlinkNotExist(err) || isNetlinkBusy(err) {
        firewall.Reset() // recreates table/chains
        err = router.AddOutputDNAT(addr, proto, o, t)
    }
    if err != nil {
        log.Errorf("output DNAT unavailable, local port forwarding disabled: %v", err)
    }
}

Prevention

When it happens

Trigger: Running the agent unprivileged or in a container without NET_ADMIN; another nftables client (firewalld, docker, a manual 'nft flush ruleset') deleting the netbird table between ensureNATOutputChain and Flush; an nftables/expr version mismatch producing an expression the kernel rejects.

Common situations: Agent started without root or in a Docker/LXC environment lacking nftables support; parallel firewall tooling flushing the ruleset; kernel built without CONFIG_NF_TABLES; upgrading the nftables Go library to a version emitting syntax the running kernel rejects.

Related errors


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