netbirdio/netbird · error

add return masquerade rule: %v

Error message

add return masquerade rule: %v

What it means

Second rule of addPostroutingRules(): appending the return-traffic masquerade rule (match PreroutingFwmarkMasqueradeReturn, out the NetBird interface, MASQUERADE) to NETBIRD-RT-NAT. Failure aborts createContainers() with 'add static nat rules'; note rule1 may already be installed, leaving partially programmed state that only a later Reset cleans up.

Source

Thrown at client/firewall/iptables/router_linux.go:555

	// First rule for outbound masquerade
	rule1 := []string{
		"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkMasquerade),
		"!", "-o", "lo",
		"-j", routingFinalNatJump,
	}
	if err := r.iptablesClient.Append(tableNat, chainRTNAT, rule1...); err != nil {
		return fmt.Errorf("add outbound masquerade rule: %v", err)
	}
	r.rules["static-nat-outbound"] = rule1

	// Second rule for return traffic masquerade
	rule2 := []string{
		"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkMasqueradeReturn),
		"-o", r.wgIface.Name(),
		"-j", routingFinalNatJump,
	}
	if err := r.iptablesClient.Append(tableNat, chainRTNAT, rule2...); err != nil {
		return fmt.Errorf("add return masquerade rule: %v", err)
	}
	r.rules["static-nat-return"] = rule2

	return nil
}

// addMSSClampingRules adds MSS clamping rules to prevent fragmentation for forwarded traffic.
func (r *router) addMSSClampingRules() error {
	overhead := uint16(ipv4TCPHeaderSize)
	if r.v6 {
		overhead = ipv6TCPHeaderSize
	}
	mss := r.mtu - overhead

	// Add jump rule from FORWARD chain in mangle table to our custom chain
	jumpRule := []string{
		"-j", chainRTMSSCLAMP,
	}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Check the agent log whether the outbound rule succeeded just before (if yes, suspect the interface-name or transient-lock angle)
  2. Reproduce: `iptables -t nat -A NETBIRD-RT-NAT -m mark --mark 0x... -o wt0 -j MASQUERADE`
  3. `modprobe iptable_nat xt_MASQUERADE xt_mark` and verify ip6table_nat for dual-stack
  4. Run `netbird down` (cleans the half-applied state) before retrying `netbird up`
  5. If the interface name changed, restart the daemon so wgIface.Name() is current

Example fix

// before: rule1 already installed; on rule2 failure the half-state persists
if err := r.iptablesClient.Append(tableNat, chainRTNAT, rule2...); err != nil {
    return fmt.Errorf("add return masquerade rule: %v", err)
}

// after: roll rule1 back so setup fails atomically
if err := r.iptablesClient.Append(tableNat, chainRTNAT, rule2...); err != nil {
    if delErr := r.iptablesClient.DeleteIfExists(tableNat, chainRTNAT, rule1...); delErr != nil {
        log.Warnf("rollback outbound masquerade rule: %v", delErr)
    }
    delete(r.rules, "static-nat-outbound")
    return fmt.Errorf("add return masquerade rule: %w", err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

func ifaceExists(name string) bool {
    _, err := net.InterfaceByName(name)
    return err == nil
}

Try / catch

On rule2 failure, roll back rule1 (DeleteIfExists on the stored spec) before returning, keeping the nat chain either fully programmed or untouched.

Prevention

When it happens

Trigger: `iptables -t nat -A NETBIRD-RT-NAT -m mark --mark 0x... -o wt0 -j MASQUERADE` failing when the NetBird interface name just changed/disappeared (e.g., interface recreated between operations), or the same missing-module/permission/lock causes as the outbound rule.

Common situations: Interface name churn after a quick down/up cycle; IPv6 pairs where ip6table_nat is missing while IPv4 works; systems that resolved the first append but lost the xtables lock to a concurrent docker restart mid-setup.

Related errors


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