netbirdio/netbird · warning

output DNAT not supported without native firewall

Error message

output DNAT not supported without native firewall

What it means

AddOutputDNAT on the uspfilter Manager only works when a native (kernel) firewall manager was injected at construction time. uspfilter filters traffic in userspace and cannot rewrite the destination of locally originated connections before they leave the host, so output DNAT is delegated to iptables/nftables/pf. When the Manager was built without a native firewall (netstack mode, embedded/WASM clients, platforms where the native manager failed or is unsupported), the call fails fast instead of silently doing nothing.

Source

Thrown at client/firewall/uspfilter/nat.go:573

// RemoveInboundDNAT removes an inbound DNAT rule.
func (m *Manager) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
	var layerType gopacket.LayerType
	switch protocol {
	case firewall.ProtocolTCP:
		layerType = layers.LayerTypeTCP
	case firewall.ProtocolUDP:
		layerType = layers.LayerTypeUDP
	default:
		return fmt.Errorf("unsupported protocol: %s", protocol)
	}

	return m.removePortRedirection(localAddr, layerType, originalPort, translatedPort)
}

// AddOutputDNAT delegates to the native firewall if available.
func (m *Manager) AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
	if m.nativeFirewall == nil {
		return fmt.Errorf("output DNAT not supported without native firewall")
	}
	return m.nativeFirewall.AddOutputDNAT(localAddr, protocol, originalPort, translatedPort)
}

// RemoveOutputDNAT delegates to the native firewall if available.
func (m *Manager) RemoveOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
	if m.nativeFirewall == nil {
		return nil
	}
	return m.nativeFirewall.RemoveOutputDNAT(localAddr, protocol, originalPort, translatedPort)
}

// translateInboundPortDNAT applies port-specific DNAT translation to inbound packets.
func (m *Manager) translateInboundPortDNAT(packetData []byte, d *decoder, srcIP, dstIP netip.Addr) bool {
	if !m.portDNATEnabled.Load() {
		return false
	}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Do not call AddOutputDNAT on deployments without a kernel firewall; feature-detect before using it
  2. Run the agent where a native firewall backend is available (Linux with nftables/iptables and NET_ADMIN, macOS pf, Windows WFP) and verify that backend initialized at startup
  3. If you control Manager construction, pass a native firewall manager or expose a capability flag (HasOutputDNAT) so callers can branch
  4. Treat the error as a hard signal: do not fall back to pretending redirection happened

Example fix

// before
if err := m.AddOutputDNAT(addr, proto, 80, 8080); err != nil {
    return err
}

// after
if err := m.AddOutputDNAT(addr, proto, 80, 8080); err != nil {
    if strings.Contains(err.Error(), "without native firewall") {
        log.Warn("output DNAT unavailable in this mode; skipping")
        return nil
    }
    return err
}
Defensive patterns

Strategy: validation

Validate before calling

// surface capability at your boundary before calling the daemon/firewall API
if runtime.GOOS == "js" || embeddedMode {
    return errors.New("output DNAT requires a native firewall backend")
}
if err := m.AddOutputDNAT(addr, proto, orig, translated); err != nil { ... }

Type guard

// if you control Manager construction, expose the capability
func (m *Manager) SupportsOutputDNAT() bool { return m.nativeFirewall != nil }

Try / catch

if err := m.AddOutputDNAT(addr, proto, orig, translated); err != nil {
    if strings.Contains(err.Error(), "without native firewall") {
        log.Warnf("output DNAT unsupported here; feature disabled")
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: Running the agent in netstack/embedded mode (no TUN, no kernel firewall) and invoking AddOutputDNAT; constructing the uspfilter Manager on a platform whose native firewall initialization failed and fell through with a nil nativeFirewall; calling the API before the native manager was attached.

Common situations: Enabling a feature that requires outbound port redirection (e.g. local SSH-over-relay or DNS redirect setups) on iOS, in the embedded client, or in a container without NET_ADMIN/nftables; misconfigured deployment expecting kernel NAT in a pure userspace deployment.

Related errors


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