[GH-ISSUE #5327] FreeBSD: masquerade not functional on userspace routing peers — source IP not rewritten #11183

Open
opened 2026-08-05 01:28:48 -04:00 by saavagebueno · 3 comments
Owner

Originally created by @crisap94 on GitHub (Feb 14, 2026).
Original GitHub issue: https://github.com/netbirdio/netbird/issues/5327

Describe the problem

Masquerade is not functional on FreeBSD/OPNsense routing peers. Despite being enabled in the network router configuration, the source IP of forwarded packets is never rewritten — the original NetBird IP (100.x.x.x) is preserved when packets are forwarded to the destination network.

On Linux routing peers with kernel WireGuard, masquerade correctly rewrites the source IP to the routing peer's local interface address. On FreeBSD routing peers with userspace WireGuard, this does not happen.

This was confirmed by inspecting the PF state table on the routing peer while a client accessed a routed resource:

all tcp 10.2.0.250:443 <- 100.x.x.x:45612       FIN_WAIT_2:FIN_WAIT_2

The source 100.x.x.x is the client's NetBird IP. With working masquerade, this should have been rewritten to the routing peer's local IP (e.g., 10.0.0.1).

As additional evidence: Tailscale is installed on the same FreeBSD hosts and its masquerade works correctly — the same destinations are reachable via Tailscale but not via NetBird. This confirms the issue is in NetBird's userspace forwarding implementation, not a FreeBSD/PF limitation.

Consequences

The broken masquerade causes two problems:

1. Services on the routing peer itself are unreachable

When the destination IP belongs to the routing peer itself (e.g., HAProxy listening on a CARP VIP), the NetBird daemon's internal firewall drops the packet before it reaches the kernel. Workaround: disabling the NetBird firewall on the routing peer (Disable Firewall = true) allows these packets through to the kernel, where PF accepts them normally.

2. Asymmetric routing to hosts that also run NetBird (no workaround)

When the destination is another host that also runs NetBird (e.g., a second OPNsense in an HA pair), the response is lost:

Client (100.x.x.x) → NetBird tunnel → fw-primary (wt0)
    → fw-primary forwards to LAN (source IP still 100.x.x.x, NOT masqueraded)
    → fw-secondary receives packet from 100.x.x.x
    → fw-secondary looks up route for 100.x.x.x
    → fw-secondary has its OWN wt0 with route for 100.x.0.0/16
    → response exits through fw-secondary's tunnel instead of returning via LAN to fw-primary
    → client never receives the response → timeout

Hosts that do NOT run NetBird (NAS, VMs, containers) work fine — they have no wt0 interface, so they respond to their default gateway (the routing peer), which routes the response back correctly.

To Reproduce

Environment:

  • 2x OPNsense firewalls in CARP HA (fw-primary: 10.0.0.1, fw-secondary: 10.0.0.254)
  • Both run NetBird as routing peers in the same peer group
  • A Linux client with a route to 10.0.0.0/24 via fw-primary
  • A NAS at 10.1.0.2 on a different VLAN (does NOT run NetBird)

Steps:

  1. Configure a Network with a /24 subnet resource and a router using the OPNsense peer group (masquerade: enabled)
  2. From the Linux client:
    • curl https://10.0.0.1works (routing peer's own IP, requires Disable Firewall = true)
    • curl https://10.0.0.254timeout (fw-secondary, also runs NetBird)
    • curl https://10.1.0.2works (NAS, does NOT run NetBird)
  3. Disconnect NetBird → curl https://10.0.0.254works (direct LAN, confirms fw-secondary is healthy)

Expected behavior

With masquerade enabled, the routing peer should rewrite the source IP before forwarding, the same way Linux routing peers do:

Client (100.x.x.x) → NetBird tunnel → fw-primary (wt0)
    → fw-primary masquerades: rewrites source to 10.0.0.1
    → fw-primary forwards to LAN (source is now 10.0.0.1)
    → fw-secondary receives packet from 10.0.0.1
    → fw-secondary responds to 10.0.0.1 via LAN
    → fw-primary receives response, routes back through wt0 to client

Are you using NetBird Cloud?

Self-hosted (management server, signal, and TURN relay).

NetBird version

  • Routing peers (OPNsense FreeBSD 14.3): 0.60.7 (latest in OPNsense pkg repo)
  • Linux client: 0.65.0
  • Interface type on FreeBSD: Userspace
  • Interface type on Linux client: Kernel

Is any other VPN software installed?

Tailscale on the same OPNsense hosts (M2M use). Tailscale's masquerade works correctly on these hosts.

Debug output

netbird status -d on fw-primary:

Daemon version: 0.60.7
CLI version: 0.60.7
Interface type: Userspace
Networks: 10.0.0.0/24, 10.1.0.0/24, 10.2.0.0/24, [multiple /24 and /32 routes]

netbird status -d on the client showing fw-primary:

fw-primary.netbird.selfhosted:
  NetBird IP: 100.x.y.z
  Status: Connected
  Connection type: P2P
  Networks: 10.0.0.0/24, 10.1.0.0/24, 10.2.0.0/24, [all routes]

fw-secondary shows Networks: - (no routes advertised, only fw-primary is actively routing).

Screenshots

N/A

Additional context

  • Related issues: #4542 (masquerade toggle not respected on non-Linux), #4898 (CARP failover), #2200 (FreeBSD routing support via FakeBSD patch)
  • The OPNsense NetBird package uses a patch to report FreeBSD as Linux to the management server (#2200), enabling routing peer functionality. The masquerade code path may not be implemented for FreeBSD's userspace forwarder.

Have you tried these troubleshooting steps?

  • Reviewed client troubleshooting (if applicable)
  • Checked for newer NetBird versions
  • Searched for similar issues on GitHub (including closed ones)
  • Restarted the NetBird client
  • Disabled other VPN software
  • Checked firewall settings
Originally created by @crisap94 on GitHub (Feb 14, 2026). Original GitHub issue: https://github.com/netbirdio/netbird/issues/5327 **Describe the problem** Masquerade is not functional on FreeBSD/OPNsense routing peers. Despite being enabled in the network router configuration, the source IP of forwarded packets is **never rewritten** — the original NetBird IP (100.x.x.x) is preserved when packets are forwarded to the destination network. On Linux routing peers with kernel WireGuard, masquerade correctly rewrites the source IP to the routing peer's local interface address. On FreeBSD routing peers with userspace WireGuard, this does not happen. This was confirmed by inspecting the PF state table on the routing peer while a client accessed a routed resource: ``` all tcp 10.2.0.250:443 <- 100.x.x.x:45612 FIN_WAIT_2:FIN_WAIT_2 ``` The source `100.x.x.x` is the client's NetBird IP. With working masquerade, this should have been rewritten to the routing peer's local IP (e.g., `10.0.0.1`). As additional evidence: Tailscale is installed on the same FreeBSD hosts and its masquerade works correctly — the same destinations are reachable via Tailscale but not via NetBird. This confirms the issue is in NetBird's userspace forwarding implementation, not a FreeBSD/PF limitation. **Consequences** The broken masquerade causes two problems: **1. Services on the routing peer itself are unreachable** When the destination IP belongs to the routing peer itself (e.g., HAProxy listening on a CARP VIP), the NetBird daemon's internal firewall drops the packet before it reaches the kernel. **Workaround:** disabling the NetBird firewall on the routing peer (`Disable Firewall = true`) allows these packets through to the kernel, where PF accepts them normally. **2. Asymmetric routing to hosts that also run NetBird (no workaround)** When the destination is another host that also runs NetBird (e.g., a second OPNsense in an HA pair), the response is lost: ``` Client (100.x.x.x) → NetBird tunnel → fw-primary (wt0) → fw-primary forwards to LAN (source IP still 100.x.x.x, NOT masqueraded) → fw-secondary receives packet from 100.x.x.x → fw-secondary looks up route for 100.x.x.x → fw-secondary has its OWN wt0 with route for 100.x.0.0/16 → response exits through fw-secondary's tunnel instead of returning via LAN to fw-primary → client never receives the response → timeout ``` Hosts that do NOT run NetBird (NAS, VMs, containers) work fine — they have no `wt0` interface, so they respond to their default gateway (the routing peer), which routes the response back correctly. **To Reproduce** Environment: - 2x OPNsense firewalls in CARP HA (fw-primary: `10.0.0.1`, fw-secondary: `10.0.0.254`) - Both run NetBird as routing peers in the same peer group - A Linux client with a route to `10.0.0.0/24` via fw-primary - A NAS at `10.1.0.2` on a different VLAN (does NOT run NetBird) Steps: 1. Configure a Network with a `/24` subnet resource and a router using the OPNsense peer group (masquerade: enabled) 2. From the Linux client: - `curl https://10.0.0.1` → **works** (routing peer's own IP, requires `Disable Firewall = true`) - `curl https://10.0.0.254` → **timeout** (fw-secondary, also runs NetBird) - `curl https://10.1.0.2` → **works** (NAS, does NOT run NetBird) 3. Disconnect NetBird → `curl https://10.0.0.254` → **works** (direct LAN, confirms fw-secondary is healthy) **Expected behavior** With masquerade enabled, the routing peer should rewrite the source IP before forwarding, the same way Linux routing peers do: ``` Client (100.x.x.x) → NetBird tunnel → fw-primary (wt0) → fw-primary masquerades: rewrites source to 10.0.0.1 → fw-primary forwards to LAN (source is now 10.0.0.1) → fw-secondary receives packet from 10.0.0.1 → fw-secondary responds to 10.0.0.1 via LAN → fw-primary receives response, routes back through wt0 to client ``` **Are you using NetBird Cloud?** Self-hosted (management server, signal, and TURN relay). **NetBird version** - Routing peers (OPNsense FreeBSD 14.3): `0.60.7` (latest in OPNsense pkg repo) - Linux client: `0.65.0` - Interface type on FreeBSD: **Userspace** - Interface type on Linux client: **Kernel** **Is any other VPN software installed?** Tailscale on the same OPNsense hosts (M2M use). Tailscale's masquerade works correctly on these hosts. **Debug output** `netbird status -d` on fw-primary: ``` Daemon version: 0.60.7 CLI version: 0.60.7 Interface type: Userspace Networks: 10.0.0.0/24, 10.1.0.0/24, 10.2.0.0/24, [multiple /24 and /32 routes] ``` `netbird status -d` on the client showing fw-primary: ``` fw-primary.netbird.selfhosted: NetBird IP: 100.x.y.z Status: Connected Connection type: P2P Networks: 10.0.0.0/24, 10.1.0.0/24, 10.2.0.0/24, [all routes] ``` fw-secondary shows `Networks: -` (no routes advertised, only fw-primary is actively routing). **Screenshots** N/A **Additional context** - Related issues: #4542 (masquerade toggle not respected on non-Linux), #4898 (CARP failover), #2200 (FreeBSD routing support via FakeBSD patch) - The OPNsense NetBird package uses a patch to report FreeBSD as Linux to the management server (#2200), enabling routing peer functionality. The masquerade code path may not be implemented for FreeBSD's userspace forwarder. **Have you tried these troubleshooting steps?** - [x] Reviewed [client troubleshooting](https://docs.netbird.io/how-to/troubleshooting-client) (if applicable) - [x] Checked for newer NetBird versions - [x] Searched for similar issues on GitHub (including closed ones) - [x] Restarted the NetBird client - [ ] Disabled other VPN software - [x] Checked firewall settings
saavagebueno added the triage-needed label 2026-08-05 01:28:48 -04:00
Author
Owner

@lixmal commented on GitHub (Feb 14, 2026):

If you turn off the NetBird firewall then there's no NAT done by NetBird, you'll have to manage that yourself.

With the firewall on you'll also need a policy that allows access to the routing peer itself.

I don't know what this has to do with Tailscale. NetBird is not a clone

<!-- gh-comment-id:3903081911 --> @lixmal commented on GitHub (Feb 14, 2026): If you turn off the NetBird firewall then there's no NAT done by NetBird, you'll have to manage that yourself. With the firewall on you'll also need a policy that allows access to the routing peer itself. I don't know what this has to do with Tailscale. NetBird is not a clone
Author
Owner

@crisap94 commented on GitHub (Feb 14, 2026):

Thanks @lixmal for the clarification.

We've since enabled the firewall on both routing peers (netbird up --disable-firewall=false) and routing works — Networks are populated and peers receive routes correctly. The original workaround of disabling the firewall was to reach services on the routing peer itself (HAProxy CARP VIPs), but as you note, that breaks NAT and the userspace forwarder entirely.

Regarding the policy for the routing peer itself — we don't currently have one. Could you clarify: does this mean we need a policy where the routing peer's own IP (e.g., the OPNsense CARP VIP) is a destination resource, so that the firewall module allows forwarded packets destined for the peer to pass through? That would explain why services hosted on the routing peer were unreachable with the firewall enabled.

The Tailscale reference was only to demonstrate that FreeBSD/PF can handle masquerade correctly — it's not a feature comparison.

To be clear: the masquerade bug reported in this issue is still present. Enabling the firewall fixed routing but did not fix source IP rewriting. Services behind the routing peer work only because they don't run NetBird themselves. The asymmetric routing problem (consequence #2) is still reproducible — any destination host that also runs NetBird will route the response through its own tunnel instead of returning via LAN to the routing peer.

<!-- gh-comment-id:3903210986 --> @crisap94 commented on GitHub (Feb 14, 2026): Thanks @lixmal for the clarification. We've since enabled the firewall on both routing peers (netbird up --disable-firewall=false) and routing works — Networks are populated and peers receive routes correctly. The original workaround of disabling the firewall was to reach services on the routing peer itself (HAProxy CARP VIPs), but as you note, that breaks NAT and the userspace forwarder entirely. Regarding the policy for the routing peer itself — we don't currently have one. Could you clarify: does this mean we need a policy where the routing peer's own IP (e.g., the OPNsense CARP VIP) is a destination resource, so that the firewall module allows forwarded packets destined for the peer to pass through? That would explain why services hosted on the routing peer were unreachable with the firewall enabled. The Tailscale reference was only to demonstrate that FreeBSD/PF can handle masquerade correctly — it's not a feature comparison. To be clear: the masquerade bug reported in this issue is still present. Enabling the firewall fixed routing but did not fix source IP rewriting. Services behind the routing peer work only because they don't run NetBird themselves. The asymmetric routing problem (consequence #2) is still reproducible — any destination host that also runs NetBird will route the response through its own tunnel instead of returning via LAN to the routing peer.
Author
Owner

@davidlfe commented on GitHub (Feb 18, 2026):

Hi, any update? I’m facing the same issue. Initially, it works the ping on one side, but on the other side I get a “TTL expired” error. After rebooting pfSense, I only receive “Request timed out” from clients behind the LAN.

<!-- gh-comment-id:3923062101 --> @davidlfe commented on GitHub (Feb 18, 2026): Hi, any update? I’m facing the same issue. Initially, it works the ping on one side, but on the other side I get a “TTL expired” error. After rebooting pfSense, I only receive “Request timed out” from clients behind the LAN.
Sign in to join this conversation.
No Label triage-needed
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#11183