tailscale / tailscale/tailscale
NetfilterMode=0 (off) not respected on daemon startup — nft rules created before prefs loaded
- Dominant language
- Go
- Stars
- 36.5k
- Forks
- 3.2k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 123
Description
## Environment
- Tailscale 1.96.4 (latest stable as of 2026-03-31)
- Fedora 43 (Bazzite immutable, based on Fedora Atomic)
- nftables backend (firewalld manages base rules, ~621 lines)
- SELinux enforcing
- Headless home server (SSH/SMB/NFS/HTTP services on LAN)
## Steps to reproduce
1. `tailscale set --netfilter-mode=off` — persists `NetfilterMode: 0` in state
2. Verify: `tailscale debug prefs` shows `NetfilterMode: 0` / `nf=off`
3. `sudo systemctl stop tailscaled`
4. `sudo nft list ruleset | wc -l` → baseline (e.g., 621 lines from firewalld)
5. `sudo systemctl start tailscaled`
6. Immediately check: `sudo nft list ruleset | wc -l` → significantly higher (800+)
7. `sudo nft list ruleset | grep ts-` → ts-input, ts-forward, ts-postrouting chains with active rules
## Expected behavior
With `NetfilterMode=0` persisted in the state file, `tailscaled` should NOT create any nftables rules on startup.
## Actual behavior
`tailscaled` creates nft rules using the **default** `NetfilterMode=2` (On) before loading persisted prefs from the state file. The rules block all inbound LAN TCP traffic (SSH, SMB, NFS, HTTP services — everything).
On a **warm restart** (daemon already running, prefs in memory), `NetfilterMode=0` IS respected — no rules created. The bug only manifests on **cold start** (fresh daemon process).
## Impact
- All inbound LAN connections blocked (SSH, SMB, NFS, web services)
- Server becomes completely unreachable from LAN
- Only outbound connections work
- Requires physical console access to recover (`systemctl stop tailscaled`)
- Makes tailscaled unusable on headless servers that depend on LAN services
## Root cause analysis
From source analysis (`wgengine/router/osrouter/router_linux.go`, `ipn/prefs.go`):
1. `NewPrefs()` creates prefs with default `NetfilterMode = 2` (On)
2. Router receives initial config with `NetfilterOn` → creates nft rules immediately
3. `LocalBackend` loads saved prefs from state file **asynchronously**
4. By the time `NetfilterMode=0` is applied, rules already exist
5. The cleanup path for transitioning from On→Off appears to be incomplete
This is the same root cause architecture identified in #8779 (netfilter subsystem initializes before prefs are loaded), but a different symptom: #8779 is about tailscaled crashing when no iptables binary exists, while this bug is about unwanted nft rules being created despite `NetfilterMode=0`.
## Related issues
- #8779 — same root cause (netfilter init before prefs loaded), different symptom (crash vs unwanted rules)
- #11504 — confirms async startup pattern exists (`systemd.Ready()` fires before async work completes)
- #7170 / #7708 — prior discussion about `netfilter-mode=off` behavior and iptables dependency
## Workarounds attempted (all failed or insufficient)
1. `tailscale set --netfilter-mode=off` — persists correctly but not read before rules are created
2. `ExecStartPost` script to flush ts-* rules — works but fragile; if it fails, can crash-loop the service
3. Delayed start (60s timer) — race happens whenever daemon starts, not just early boot
4. `TS_DEBUG_FIREWALL_MODE` env var — only controls iptables vs nftables backend selection, has no "off" value
5. There is no `tailscaled` daemon flag for netfilter mode (only available via CLI `tailscale set/up`)
## Suggested fix
**Option A (preferred):** Load `NetfilterMode` from persisted state synchronously during startup, **before** initializing the router/nftables subsystem. The state file read for firewall-related prefs should not be deferred to the async path.
**Option B:** Add a `--netfilter-mode` flag to `tailscaled` itself (not just the `tailscale` CLI), so it can be set via `FLAGS` in `/etc/default/tailscaled` and applied before any router initialization.
Contributor guide
Assessment
This issue has not been assessed yet.