tailscale/tailscale · error

add match subnet route mark rule v4: %w

Error message

add match subnet route mark rule v4: %w

What it means

addBase4 failed to install the mark-match accept rule into IPv4 ts-forward. The rule matches packets whose mark, masked with the Tailscale fwmark mask, equals the subnet-route mark, then accepts them; construction (createMatchSubnetRouteMarkRule) cannot fail, so the wrapped error is the helper's conn.Flush() netlink commit error.

Source

Thrown at util/linuxfw/nftables_runner.go:1742

	inputChain, err := getChainFromTable(conn, n.nft4.Filter, chainNameInput)
	if err != nil {
		return fmt.Errorf("get input chain v4: %v", err)
	}
	if err = addAcceptIncomingPacketRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
		return fmt.Errorf("add accept incoming packet rule v4: %w", err)
	}

	forwardChain, err := getChainFromTable(conn, n.nft4.Filter, chainNameForward)
	if err != nil {
		return fmt.Errorf("get forward chain v4: %v", err)
	}

	if err = addSetSubnetRouteMarkRule(conn, n.nft4.Filter, forwardChain, tunname); err != nil {
		return fmt.Errorf("add set subnet route mark rule v4: %w", err)
	}

	if err = addMatchSubnetRouteMarkRule(conn, n.nft4.Filter, forwardChain, Accept); err != nil {
		return fmt.Errorf("add match subnet route mark rule v4: %w", err)
	}

	if err = addDropOutgoingPacketFromCGNATRangeRuleWithTunname(conn, n.nft4.Filter, forwardChain, tunname); err != nil {
		return fmt.Errorf("add drop outgoing packet from cgnat range rule v4: %w", err)
	}

	if err = addAcceptOutgoingPacketRule(conn, n.nft4.Filter, forwardChain, tunname); err != nil {
		return fmt.Errorf("add accept outgoing packet rule v4: %w", err)
	}

	if err = conn.Flush(); err != nil {
		return fmt.Errorf("flush base v4: %w", err)
	}

	return nil
}

// addBase6 adds some basic IPv6 processing rules.

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Verify CAP_NET_ADMIN and retry after AddChains() on ENOENT
  2. Classify errno and switch firewall mode on EOPNOTSUPP
  3. Remove concurrent ruleset flushes
  4. Check Conn lifecycle
Defensive patterns

Strategy: retry

Validate before calling

// same pre-flight as the other flush failures: probe + re-ensure chains
conn, err := nftables.New()
if err != nil { return err }
defer conn.Close()
if _, err := conn.ListTables(); err != nil { return err }
if err := fw.AddChains(); err != nil { return err }

Type guard

func isFlushCommitErr(err error) bool {
	return errors.Is(err, unix.EPERM) || errors.Is(err, unix.ENOENT) ||
		errors.Is(err, unix.EOPNOTSUPP) || errors.Is(err, unix.ENOTSUPP)
}

Try / catch

if err := fw.AddBase(tun); err != nil {
	if errors.Is(err, unix.ENOENT) {
		if fw.AddChains() == nil {
			return fw.AddBase(tun)
		}
	}
	return err
}

Prevention

When it happens

Trigger: EPERM at commit without CAP_NET_ADMIN; ENOENT when ts-forward/filter was flushed concurrently; EOPNOTSUPP/EINVAL on kernels lacking the needed nft expressions; closed nftables.Conn.

Common situations: Same family as the other base-rule flush failures: restricted containers, unsupported kernels, racing firewall managers.

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/bc8d80f53a69360f. Report an issue: GitHub.