Skip to content

[client] Use native firewall for peer ACLs in userspace WireGuard mode#5668

Merged
lixmal merged 4 commits intomainfrom
fix/userspace-native-firewall
Apr 10, 2026
Merged

[client] Use native firewall for peer ACLs in userspace WireGuard mode#5668
lixmal merged 4 commits intomainfrom
fix/userspace-native-firewall

Conversation

@lixmal
Copy link
Copy Markdown
Collaborator

@lixmal lixmal commented Mar 23, 2026

Describe your changes

When WireGuard runs in userspace mode, peer ACLs were handled by USPFilter while route ACLs used nftables/iptables. This desync meant Docker containers with published ports (bridge mode, DNAT) could not be reached even when allowed by policy, because the nftables mangle prerouting marks were never set for peer ACL rules.

  • Use native nftables/iptables for peer ACLs in userspace mode when available (the TUN interface is identical from netfilter's perspective)
  • Fall back to USPFilter when native firewall is unavailable
  • Add NB_FORCE_USERSPACE_FIREWALL env var to force USPFilter for peer ACLs (skips native firewall entirely)

To run with no kernel netfilter rules at all, all three env vars are needed:

NB_WG_KERNEL_DISABLED=true
NB_FORCE_USERSPACE_FIREWALL=true
NB_FORCE_USERSPACE_ROUTER=true

Issue ticket number and link

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

New env var is for internal testing/debugging only.

Summary by CodeRabbit

  • New Features

    • Added NB_FORCE_USERSPACE_FIREWALL environment variable to force userspace firewall mode.
  • Improvements

    • Initialization prefers userspace when forced and falls back to userspace if native setup fails.
    • Peer-filtering/allow rules are applied consistently across firewall backends.
  • Tests

    • Tests updated to exercise forced userspace firewall behavior.
  • Breaking Changes

    • Userspace-bind state is no longer persisted or exposed in shutdown state.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: edd668a5-b122-4fb4-9bf2-9072ae317f30

📥 Commits

Reviewing files that changed from the base of the PR and between 0adec63 and 4d75797.

📒 Files selected for processing (2)
  • client/firewall/iptables/manager_linux.go
  • client/firewall/nftables/manager_linux.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/firewall/iptables/manager_linux.go

📝 Walkthrough

Walkthrough

Adds NB_FORCE_USERSPACE_FIREWALL, changes firewall creation to honor a force-userspace flag and fallback behavior in userspace-bind mode, removes UserspaceBind/IsUserspaceBind() from iface/state and iFaceMapper, and makes AllowNetbird always apply peer-filtering; tests set the new env flag.

Changes

Cohort / File(s) Summary
Firewall Initialization
client/firewall/create_linux.go
Add parsing of NB_FORCE_USERSPACE_FIREWALL; alter NewFirewall control flow to return userspace immediately when forced, otherwise try native first and fallback to userspace in userspace-bind mode on native failure; kernel-mode remains native-only.
Interface constant
client/firewall/iface.go
Add exported constant EnvForceUserspaceFirewall = "NB_FORCE_USERSPACE_FIREWALL".
iptables Manager & State
client/firewall/iptables/manager_linux.go, client/firewall/iptables/state_linux.go
Remove IsUserspaceBind() from iFaceMapper and drop UserspaceBind from InterfaceState; Init() no longer persists userspace-bind; AllowNetbird() always invokes peer-filtering/blanket-accept rule logic.
iptables Tests
client/firewall/iptables/manager_linux_test.go
Remove IsUserspaceBind() from the test mock.
nftables Manager & State
client/firewall/nftables/manager_linux.go, client/firewall/nftables/state_linux.go
Remove IsUserspaceBind() from iFaceMapper and drop UserspaceBind from InterfaceState; Init() stops persisting userspace-bind; AllowNetbird() always creates default allow rules and flushes allow-input under lock.
nftables Tests
client/firewall/nftables/manager_linux_test.go
Remove IsUserspaceBind() from the test mock.
ACL Manager Tests
client/internal/acl/manager_test.go
Set firewall.EnvForceUserspaceFirewall = "true" in multiple tests to force userspace firewall during test runs.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant Env as Environment (NB_FORCE_USERSPACE_FIREWALL)
    participant Iface as iface (bind mode)
    participant Native as NativeFirewall
    participant Userspace as UserspaceFirewall

    Caller->>Iface: request firewall creation
    Iface-->>Caller: reports bind mode (userspace or kernel)

    alt Interface userspace
        Caller->>Env: read NB_FORCE_USERSPACE_FIREWALL
        Env-->>Caller: flag value
        alt Flag true
            Caller->>Userspace: create userspace (native = nil)
            Userspace-->>Caller: return userspace instance
        else Flag false
            Caller->>Native: attempt create native
            alt Native creation fails
                Native-->>Caller: error
                Caller->>Userspace: create userspace (fallback)
                Userspace-->>Caller: return userspace instance
            else Native creation succeeds
                Native-->>Caller: return native instance
            end
        end
    else Interface kernel
        Caller->>Native: create native (no fallback)
        Native-->>Caller: return result/error
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • pappz
  • crn4

Poem

🐰 I hop where flags and firewalls meet,

A tiny switch makes userspace complete,
Native tries first, but if it slips away,
Userspace hops in to keep rules at play,
I thump a drum and nibble a carrot for the day.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: using native firewall for peer ACLs when WireGuard runs in userspace mode, which aligns with the core problem and solution in the PR.
Description check ✅ Passed The description covers the problem, solution, and new environment variable with clear examples. However, the issue ticket link is missing and tests created are not mentioned.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/userspace-native-firewall

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/firewall/create_linux.go`:
- Around line 45-59: When createNativeFirewall(iface, ...) succeeds in userspace
bind mode (iface.IsUserspaceBind() true and err == nil) you must call the
firewall's AllowNetbird() initialization before returning; update the successful
native path (the fm returned from createNativeFirewall) to call
fm.AllowNetbird(), handle and return any error from that call (or log and
propagate) just like the userspace path does, so iptables/nftables
implementations are initialized consistently; keep the existing fallback
behavior for err != nil unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32fe975d-d2d8-4481-8dd3-9c5c9cd3822a

📥 Commits

Reviewing files that changed from the base of the PR and between 9f41367 and 0adec63.

📒 Files selected for processing (2)
  • client/firewall/create_linux.go
  • client/firewall/iface.go
✅ Files skipped from review due to trivial changes (1)
  • client/firewall/iface.go

Comment thread client/firewall/create_linux.go
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Apr 8, 2026

@lixmal lixmal merged commit d2cdc0e into main Apr 10, 2026
42 of 43 checks passed
@lixmal lixmal deleted the fix/userspace-native-firewall branch April 10, 2026 01:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants