[GH-ISSUE #6166] [client] feat: --skip-native-firewall flag to initialize firewall manager without writing iptables/nftables #12918

Closed
opened 2026-08-05 02:07:00 -04:00 by saavagebueno · 3 comments
Owner

Originally created by @ld-873233 on GitHub (May 16, 2026).
Original GitHub issue: https://github.com/netbirdio/netbird/issues/6166

Summary

In NetBird agent v0.71.0, --disable-firewall is a single all-or-nothing switch that gates the entire firewall.NewFirewall() factory. Toggling it has two coupled effects:

  1. Whether NetBird writes native iptables/nftables rules on the host
  2. Whether the userspace firewall manager (uspfilter) gets initialized — which is the component that routes decrypted WireGuard packets to the wt0 interface

Request: add a separate flag --skip-native-firewall that lets operators initialize the firewall manager (so packet forwarding works) WITHOUT modifying the host's native iptables/nftables.

Motivation — concrete scenario where the coupling is painful

We deploy NetBird on hosts that already have other software actively managing iptables/nftables: kernel WireGuard (wg0) with custom PostUp rules, Rosenpass, Tailscale break-glass, edge security tooling. Historically we passed --disable-firewall to prevent NetBird from clobbering these.

On 0.70.5 this worked. On 0.71 we observed in-mesh ICMP/TCP go to 100% loss between certain peers even though:

  • WireGuard handshake completes
  • wg show wt0 shows real ephemeral ports
  • Keepalive/PSK exchange bytes flow bidirectionally (peer latency measured at ~1ms)
  • netbird status -d reports Connection type: P2P, Last WireGuard handshake: <seconds> ago

The smoking gun was netbird debug trace:

$ netbird debug trace out 100.81.14.42 100.81.126.1 -p icmp --icmp-type 8 --icmp-code 0
Error: trace failed: firewall manager not initialized

Toggling DisableFirewall: true → false in /var/lib/netbird/default.json and restarting the daemon made ping work immediately:

PING 100.81.126.1 (100.81.126.1) 56(84) bytes of data.
5 packets transmitted, 5 received, 0% packet loss, time 1202ms
rtt min/avg/max/mdev = 0.927/1.005/1.128/0.067 ms

…but at the cost of NetBird adding 32 iptables rules on a host where we explicitly didn't want it managing iptables.

Current code behavior (verified)

In client/firewall/create_linux.go, NewFirewall() is the single factory. When DisableFirewall=true, it isn't called at all → no firewall manager, no uspfilter.Manager, no packet forwarding.

There's no environment variable that decouples these. NB_FORCE_USERSPACE_FIREWALL exists but only takes effect when WireGuard runs in userspace mode (iface.IsUserspaceBind() returns true). Hosts running kernel WireGuard can't use it.

Proposed change — minimum patch

A single new boolean threaded through CLI → daemon → factory:

  1. client/cmd/up.go: add --skip-native-firewall flag, persist via setupSetConfigReqSetConfigRequest
  2. client/proto/daemon.proto: add optional bool skip_native_firewall = N;
  3. client/internal/profilemanager/config.go: add SkipNativeFirewall bool to Config and ConfigInput
  4. client/server/server.go: read from SetConfigRequest, persist to profile, forward to ConnectClient
  5. client/internal/connect.go: pass the value to NewFirewall()
  6. client/firewall/create_linux.go: in NewFirewall(), after the disableFirewall short-circuit, add:
if skipNativeFirewall {
    log.Info("initializing userspace firewall without native rules (--skip-native-firewall)")
    return createUserspaceFirewall(iface, nil, disableServerRoutes, flowLogger, mtu)
}

createUserspaceFirewall already accepts a nil native firewall manager and uses uspfilter.Manager standalone — the wiring is already there.

  1. client/server/setconfig_test.go: add "skip-native-firewall" to the flagToField map for test coverage.

Total: ~50-100 lines of code + 1 proto regen. The runtime behavior is purely additive — existing users of --disable-firewall are unaffected.

Use cases

  • Edge/IoT fleets where another component owns the firewall (industrial gateways, Jetson edge devices with hardware-managed netfilter, etc.)
  • Multi-VPN hosts (NetBird + WireGuard kernel + Tailscale on the same node) where each shouldn't trample the others' iptables
  • Strict-iptables environments running firewalld, ufw, or fleet-managed netfilter where operators want the cleanest possible footprint
  • Diagnostic mode — operators investigating an issue can quickly toggle iptables-writing off without losing connectivity

What I'd want to confirm before opening a PR

Happy to write the patch myself with CLA signed, but a quick maintainer ack on these would speed things up:

  1. Is the field name skip_native_firewall / --skip-native-firewall acceptable, or do you prefer a different naming (e.g., --firewall-mode=userspace-only enum)?
  2. Should it be an env var as well (e.g., NB_SKIP_NATIVE_FIREWALL) for parity with NB_FORCE_USERSPACE_FIREWALL?
  3. Does the management server need to gain the matching capability so this can be policy-set rather than client-set, or is the client-side flag sufficient?

Environment for the reproducer above

  • NetBird agent: v0.71.0
  • NetBird management: v0.71.0
  • NetBird relay: v0.71.0
  • OS: NVIDIA Jetson Orin (Ubuntu 22.04, aarch64) for one peer; Debian 12 (x86_64) for the other
  • WireGuard: kernel mode (not userspace)
  • Other network managers on hosts: Rosenpass (separate PQC mesh on wg0), occasional Tailscale break-glass
  • Topology: behind double NAT (MikroTik on-prem + AWS VPC for the EKS side)

Happy to provide debug bundles or test on a development build.

Originally created by @ld-873233 on GitHub (May 16, 2026). Original GitHub issue: https://github.com/netbirdio/netbird/issues/6166 ## Summary In NetBird agent v0.71.0, `--disable-firewall` is a single all-or-nothing switch that gates the entire `firewall.NewFirewall()` factory. Toggling it has two coupled effects: 1. Whether NetBird writes native `iptables`/`nftables` rules on the host 2. Whether the userspace firewall manager (uspfilter) gets initialized — which is the component that routes decrypted WireGuard packets to the wt0 interface **Request: add a separate flag `--skip-native-firewall` that lets operators initialize the firewall manager (so packet forwarding works) WITHOUT modifying the host's native iptables/nftables.** ## Motivation — concrete scenario where the coupling is painful We deploy NetBird on hosts that already have other software actively managing iptables/nftables: kernel WireGuard (`wg0`) with custom PostUp rules, Rosenpass, Tailscale break-glass, edge security tooling. Historically we passed `--disable-firewall` to prevent NetBird from clobbering these. On 0.70.5 this worked. On 0.71 we observed in-mesh ICMP/TCP go to 100% loss between certain peers even though: - WireGuard handshake completes - `wg show wt0` shows real ephemeral ports - Keepalive/PSK exchange bytes flow bidirectionally (peer latency measured at ~1ms) - `netbird status -d` reports `Connection type: P2P, Last WireGuard handshake: <seconds> ago` The smoking gun was `netbird debug trace`: ``` $ netbird debug trace out 100.81.14.42 100.81.126.1 -p icmp --icmp-type 8 --icmp-code 0 Error: trace failed: firewall manager not initialized ``` Toggling `DisableFirewall: true → false` in `/var/lib/netbird/default.json` and restarting the daemon made ping work immediately: ``` PING 100.81.126.1 (100.81.126.1) 56(84) bytes of data. 5 packets transmitted, 5 received, 0% packet loss, time 1202ms rtt min/avg/max/mdev = 0.927/1.005/1.128/0.067 ms ``` …but at the cost of NetBird adding 32 iptables rules on a host where we explicitly didn't want it managing iptables. ## Current code behavior (verified) In `client/firewall/create_linux.go`, `NewFirewall()` is the single factory. When `DisableFirewall=true`, it isn't called at all → no firewall manager, no `uspfilter.Manager`, no packet forwarding. There's no environment variable that decouples these. `NB_FORCE_USERSPACE_FIREWALL` exists but only takes effect when WireGuard runs in userspace mode (`iface.IsUserspaceBind()` returns true). Hosts running kernel WireGuard can't use it. ## Proposed change — minimum patch A single new boolean threaded through CLI → daemon → factory: 1. `client/cmd/up.go`: add `--skip-native-firewall` flag, persist via `setupSetConfigReq` → `SetConfigRequest` 2. `client/proto/daemon.proto`: add `optional bool skip_native_firewall = N;` 3. `client/internal/profilemanager/config.go`: add `SkipNativeFirewall bool` to `Config` and `ConfigInput` 4. `client/server/server.go`: read from `SetConfigRequest`, persist to profile, forward to `ConnectClient` 5. `client/internal/connect.go`: pass the value to `NewFirewall()` 6. `client/firewall/create_linux.go`: in `NewFirewall()`, after the `disableFirewall` short-circuit, add: ```go if skipNativeFirewall { log.Info("initializing userspace firewall without native rules (--skip-native-firewall)") return createUserspaceFirewall(iface, nil, disableServerRoutes, flowLogger, mtu) } ``` `createUserspaceFirewall` already accepts a `nil` native firewall manager and uses `uspfilter.Manager` standalone — the wiring is already there. 7. `client/server/setconfig_test.go`: add `"skip-native-firewall"` to the `flagToField` map for test coverage. Total: ~50-100 lines of code + 1 proto regen. The runtime behavior is purely additive — existing users of `--disable-firewall` are unaffected. ## Use cases - **Edge/IoT fleets where another component owns the firewall** (industrial gateways, Jetson edge devices with hardware-managed netfilter, etc.) - **Multi-VPN hosts** (NetBird + WireGuard kernel + Tailscale on the same node) where each shouldn't trample the others' iptables - **Strict-iptables environments** running firewalld, ufw, or fleet-managed netfilter where operators want the cleanest possible footprint - **Diagnostic mode** — operators investigating an issue can quickly toggle iptables-writing off without losing connectivity ## What I'd want to confirm before opening a PR Happy to write the patch myself with CLA signed, but a quick maintainer ack on these would speed things up: 1. Is the field name `skip_native_firewall` / `--skip-native-firewall` acceptable, or do you prefer a different naming (e.g., `--firewall-mode=userspace-only` enum)? 2. Should it be an env var as well (e.g., `NB_SKIP_NATIVE_FIREWALL`) for parity with `NB_FORCE_USERSPACE_FIREWALL`? 3. Does the management server need to gain the matching capability so this can be policy-set rather than client-set, or is the client-side flag sufficient? ## Environment for the reproducer above - NetBird agent: v0.71.0 - NetBird management: v0.71.0 - NetBird relay: v0.71.0 - OS: NVIDIA Jetson Orin (Ubuntu 22.04, aarch64) for one peer; Debian 12 (x86_64) for the other - WireGuard: kernel mode (not userspace) - Other network managers on hosts: Rosenpass (separate PQC mesh on wg0), occasional Tailscale break-glass - Topology: behind double NAT (MikroTik on-prem + AWS VPC for the EKS side) Happy to provide debug bundles or test on a development build.
Author
Owner

@linear-code[bot] commented on GitHub (May 16, 2026):

NET-1183

<!-- gh-comment-id:4465801141 --> @linear-code[bot] commented on GitHub (May 16, 2026): <!-- linear-linkback --> <p><a href="https://linear.app/netbird/issue/NET-1183">NET-1183</a></p>
Author
Owner

@lixmal commented on GitHub (May 16, 2026):

What exactly should --skip-native-firewall achieve compared to --disable-firewall? Kernel mode cannot use this one:

createUserspaceFirewall already accepts a nil native firewall manager and uses uspfilter.Manager standalone — the wiring is already there.

nor can it use the packet tracer:

$ netbird debug trace out 100.81.14.42 100.81.126.1 -p icmp --icmp-type 8 --icmp-code 0
Error: trace failed: firewall manager not initialized

The reason is simple: the traffic flows outside of the netbird process, so it cannot be observed or filtered from there.

<!-- gh-comment-id:4466158348 --> @lixmal commented on GitHub (May 16, 2026): What exactly should `--skip-native-firewall` achieve compared to `--disable-firewall`? Kernel mode cannot use this one: > createUserspaceFirewall already accepts a nil native firewall manager and uses uspfilter.Manager standalone — the wiring is already there. nor can it use the packet tracer: > $ netbird debug trace out 100.81.14.42 100.81.126.1 -p icmp --icmp-type 8 --icmp-code 0 Error: trace failed: firewall manager not initialized The reason is simple: the traffic flows outside of the netbird process, so it cannot be observed or filtered from there.
Author
Owner

@jnfrati commented on GitHub (May 19, 2026):

Moving this to a discussion as per the new issue triage flow https://github.com/netbirdio/netbird/discussions/6074

<!-- gh-comment-id:4486167855 --> @jnfrati commented on GitHub (May 19, 2026): Moving this to a discussion as per the new issue triage flow https://github.com/netbirdio/netbird/discussions/6074
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#12918