I recently ran into a very specific but frustrating UniFi routing issue.
I have a UniFi Cloud Gateway Fiber with several local networks, a built-in WireGuard VPN server for remote clients, and outbound WireGuard VPN client tunnels. UniFi’s Policy-Based Routing works well for normal local clients, but I discovered that remote WireGuard clients were not being treated the same way as local LAN clients.
The goal was simple:
iPhone on WireGuard VPN
→ connects back to my UniFi gateway
→ traffic enters through the UniFi WireGuard server
→ UniFi Policy-Based Routing evaluates the traffic
→ selected traffic exits through an outbound WireGuard client tunnel
In my case, this was useful for things like Xfinity Stream, Netflix, and other location-sensitive streaming or app use cases where I may want certain traffic from my phone to appear as though it is coming from a specific home, proxy, or remote network location.
This post documents what I found, how I tested it, the one-line fix that worked in my environment, and how I made it persistent with a small systemd timer/watchdog.
Environment Tested
This fix was tested on:
Device: UniFi Cloud Gateway Fiber / UCG Fiber
UniFi OS: 5.1.11
UniFi Network Application: 10.4.57
Release Channel: Early Access
Auto Update: Off
Kernel: Linux 5.4.213-ui-ipq9574
Architecture: aarch64
Base OS: Debian GNU/Linux 11 (bullseye)
Date tested: May 18, 2026
This matters because UniFi OS and the UniFi Network application can change how interfaces, ipsets, firewall rules, and policy-based routing are generated.
A Note on How This Was Developed
This fix was developed and tested with the help of ChatGPT while working through the actual SSH output from my UniFi Cloud Gateway Fiber.
I would not recommend blindly copying these commands without first confirming your own network details. In my case, the fix came down to one simple command:
ipset add UBIOS4local_network 10.10.100.0/24 -exist
But that command only makes sense because my UniFi WireGuard remote-access subnet is:
10.10.100.0/24
and my UniFi PBR rules were using:
UBIOS4local_network
as part of their source matching.
Your interface names, WireGuard subnet, route tables, marks, and ipset names may be different. Before applying anything persistently, use the diagnostic commands below to confirm how your own UniFi gateway is configured.
This was one of those projects where ChatGPT was genuinely useful as a troubleshooting partner: not by guessing the answer, but by helping interpret the live UniFi routing tables, iptables rules, ipsets, WireGuard interfaces, and route tests until the actual missing piece became obvious.
The Use Case
I wanted remote WireGuard clients, such as an iPhone, to behave more like local network clients.
For example:
Remote iPhone
→ WireGuard VPN back to UniFi
→ UniFi PBR decides where traffic should go
→ some traffic exits normal WAN
→ some traffic exits through wgclt2
This is different from a basic VPN setup where all remote traffic simply exits the home WAN.
The specific goal was:
Remote WireGuard client traffic should be eligible for the same UniFi GUI Policy-Based Routing rules as local LAN traffic.
That way I could keep using the UniFi Network application to define destination-based or domain-based PBR rules, instead of manually recreating all routing logic in iptables.
Important WireGuard Terminology in This Setup
There are two very different WireGuard roles on the UniFi gateway in this setup:
wgsrv1 = WireGuard SERVER interface on the UniFi gateway
wgclt1 = WireGuard CLIENT interface from the UniFi gateway to another endpoint
wgclt2 = another WireGuard CLIENT interface from the UniFi gateway to another endpoint
That distinction matters.
wgsrv1: Remote Clients Connecting Into the UniFi Gateway
wgsrv1 is the UniFi gateway acting as a WireGuard server.
This is what my iPhone, laptop, or other remote device connects into when I am away from home.
In my setup:
wgsrv1 = 10.10.100.1/24
Remote WireGuard clients = 10.10.100.x
So the inbound path looks like this:
iPhone / remote device
→ WireGuard tunnel over the internet
→ UniFi gateway
→ wgsrv1
→ source IP such as 10.10.100.2
This is the “road warrior” or remote-access VPN side.
wgclt1 / wgclt2: The UniFi Gateway Connecting Out to Other WireGuard Endpoints
wgclt1 and wgclt2 are different. These are the UniFi gateway acting as a WireGuard client.
That means the UDM / UCG itself initiates a WireGuard tunnel out to another endpoint, such as:
another UniFi gateway
a WireGuard server at another location
a VPS
a proxy endpoint
a commercial VPN provider
another home network
In my setup, the outbound tunnel I wanted to use for selected traffic was:
wgclt2
So the outbound path looks like this:
UniFi gateway
→ wgclt2
→ remote WireGuard endpoint / proxy / other UDM
→ internet or remote network
The Full Hairpin Path
The full goal was to combine both sides:
iPhone away from home
→ connects into UniFi using WireGuard
→ enters the gateway on wgsrv1
→ UniFi PBR evaluates the traffic
→ selected traffic exits through wgclt2
→ remote WireGuard endpoint / proxy / other UDM
→ internet
So this is not simply:
Phone → home internet
It is more like:
Remote WireGuard client
→ home UniFi gateway
→ UniFi Policy-Based Routing
→ outbound WireGuard client tunnel
That is why the distinction between wgsrv1 and wgclt2 is so important.
My Network Layout
On my UniFi gateway, WireGuard had three relevant interfaces:
Inbound WireGuard server:
wgsrv1 = 10.10.100.1/24
Outbound WireGuard client tunnels:
wgclt1 = UniFi gateway connecting out to another WireGuard endpoint
wgclt2 = UniFi gateway connecting out to another WireGuard endpoint
The important distinction is:
wgsrv1 receives remote VPN clients.
wgclt1 and wgclt2 are outbound VPN tunnels created by the UniFi gateway itself.
My remote WireGuard clients connect into wgsrv1 and receive IPs like:
10.10.100.2, 10.10.100.3, 10.10.100.4, 10.10.100.5, etc.
My desired outbound VPN path for selected traffic was:
wgclt2
So the full desired path was:
Remote iPhone / laptop
→ connects to UniFi WireGuard server
→ enters UniFi on wgsrv1
→ source is 10.10.100.x
→ UniFi PBR rules evaluate the destination
→ matching traffic exits through wgclt2
→ remote WireGuard endpoint / proxy / other UDM
In other words, wgsrv1 is the entrance tunnel, and wgclt2 is the selected exit tunnel.
Before You Copy This
The important concept is not the exact subnet I used. The concept is:
Find the WireGuard remote-access subnet
→ confirm it is not included in UniFi's local-network ipset
→ add that subnet to the IPv4 local-network ipset
→ verify that UniFi GUI PBR rules now apply
→ only then make it persistent
In my case:
WireGuard server interface: wgsrv1
WireGuard server IP: 10.10.100.1/24
Remote WG client subnet: 10.10.100.0/24
Outbound WG client tunnel: wgclt2
Target route table: 178.wgclt2
If your WireGuard server subnet is different, change the script variables accordingly:
WG_SUBNET="10.10.100.0/24"
TEST_IP="10.10.100.2"
SET_NAME="UBIOS4local_network"
The Problem
UniFi had already created the PBR rules correctly for local clients.
The relevant UniFi PBR rules looked conceptually like this:
If source is in UBIOS_local_network
AND destination matches one of the UniFi traffic-route destination sets
THEN mark the packet for wgclt2
The problem was that the remote WireGuard client subnet:
10.10.100.0/24
was not considered part of UniFi’s local network ipset.
So traffic from a remote WireGuard client entered the gateway, but it did not match the normal UniFi GUI Policy-Based Routing source condition.
In other words:
Local LAN clients matched PBR.
Remote WireGuard clients did not.
Discovering the WireGuard Interfaces
SSH into the UniFi gateway:
ssh root@<gateway-ip>
Then list network interfaces:
ls /sys/class/net
Useful WireGuard status command:
wg show
In my case, I saw:
wgclt1
wgclt2
wgsrv1
Where:
wgsrv1 = UniFi WireGuard server for remote clients
wgclt2 = outbound WireGuard client tunnel
I also checked interface addressing:
ip -br addr
Relevant output:
wgclt1 192.168.131.3/32
wgclt2 192.168.234.3/32
wgsrv1 10.10.100.1/24
That confirmed the remote WireGuard server subnet was:
10.10.100.0/24
Checking UniFi’s Routing Tables
These commands helped reveal how UniFi was routing marked traffic:
ip rule show
ip route show table all | grep -Ei 'default|wgclt|wgsrv|eth|table'
The key result was:
default dev wgclt2 table 178.wgclt2
And the policy rule showed that marked traffic could be routed to that table.
Testing a forwarded packet entering from the WireGuard server interface confirmed the route:
ip route get 1.1.1.1 from 10.10.100.2 iif wgsrv1 mark 0x6a0000
Expected result:
1.1.1.1 from 10.10.100.2 dev wgclt2 table 178.wgclt2 mark 0x6a0000
cache iif wgsrv1
That proved the route itself worked.
The missing piece was getting the remote WireGuard clients to match UniFi’s PBR source rules.
Checking UniFi’s Local Network ipset
UniFi uses ipsets as part of its firewall and PBR logic.
I checked the parent local-network set:
ipset list UBIOS_local_network
It showed:
Name: UBIOS_local_network
Type: list:set
Members:
UBIOS4local_network
UBIOS6local_network
So IPv4 local networks are actually in:
UBIOS4local_network
I tested whether my remote WireGuard clients were considered local:
ipset test UBIOS4local_network 10.10.100.2
ipset test UBIOS4local_network 10.10.100.3
ipset test UBIOS4local_network 10.10.100.5
They were not. That explained why PBR did not apply.
The One-Line Fix in My Environment
After confirming the routing and ipset behavior, the actual fix in my environment was:
ipset add UBIOS4local_network 10.10.100.0/24 -exist
This added my WireGuard remote-access subnet to UniFi’s IPv4 local-network ipset.
That made remote WireGuard clients eligible for the same UniFi GUI Policy-Based Routing rules as my local LAN clients.
Again, do not assume 10.10.100.0/24 is correct for your setup. Confirm your WireGuard server subnet first.
After running that command, I verified:
ipset test UBIOS4local_network 10.10.100.2
Expected result:
10.10.100.2 is in set UBIOS4local_network.
Then I tested routing again:
ip route get 1.1.1.1 from 10.10.100.2 iif wgsrv1 mark 0x6a0000
Expected:
1.1.1.1 from 10.10.100.2 dev wgclt2 table 178.wgclt2 mark 0x6a0000
cache iif wgsrv1
At that point, the remote WireGuard client traffic began following my existing UniFi GUI PBR rules.
Why This Works
UniFi’s PBR rules were already doing most of the work.
The relevant flow became:
Remote WireGuard client
→ source 10.10.100.x
→ now included in UBIOS4local_network
→ matches UniFi PBR source condition
→ destination matches UniFi traffic-route ipset
→ packet gets marked
→ marked packet routes to wgclt2
→ UniFi NAT masquerades traffic out wgclt2
The beauty of this approach is that I did not have to recreate all of my PBR rules manually.
UniFi still manages:
destination IP sets
domain-derived IP sets
packet marking
routing table selection
NAT out wgclt+
MSS clamping
The only manual change is making the remote WireGuard subnet count as a local IPv4 source for PBR.
Testing Live Traffic
To watch PBR counters:
iptables -t mangle -L UBIOS_PREROUTING_PBR -v -n
Then generate traffic from the remote WireGuard client to a destination covered by a PBR rule.
Run the counter command again:
iptables -t mangle -L UBIOS_PREROUTING_PBR -v -n
You should see counters increase on rules involving:
UBIOS_trafficroute_ip_*
UBIOS_trafficroute_dn_*
You can also watch packets live.
In one SSH window:
tcpdump -ni wgsrv1 host 10.10.100.2
In another SSH window:
tcpdump -ni wgclt2
Expected behavior:
Traffic enters on wgsrv1 from the remote client.
Matched PBR traffic exits on wgclt2.
Monitoring and Debugging Live Traffic
One thing that made this harder to troubleshoot is that the UniFi Network UI did not clearly show when these PBR rules were being triggered.
In the past, UniFi’s traffic route / PBR display seemed to show rule activity more clearly. In this case, the UI did not reliably show that the PBR rules were being matched, even though the routing was actually working. That may be a UniFi bug, a limitation of the current Network application version, or just a gap in how the UI reports traffic from WireGuard remote-access clients.
The good news is that you can confirm what is happening directly over SSH.
Watch the UniFi PBR Counters
This command shows packet and byte counters for UniFi’s PBR mangle chain:
iptables -t mangle -L UBIOS_PREROUTING_PBR -v -n
Before testing from the remote WireGuard client, run:
iptables -t mangle -L UBIOS_PREROUTING_PBR -v -n
Then generate traffic from the remote client, such as opening the app or website that should match your PBR rule.
Then run it again:
iptables -t mangle -L UBIOS_PREROUTING_PBR -v -n
You are looking for increasing packet and byte counters on rules involving:
UBIOS_trafficroute_ip_*
UBIOS_trafficroute_dn_*
Those are the UniFi-created destination sets for traffic routes / policy-based routing.
Watch Counters Continuously
To watch the counters update in near real time:
watch -n 1 'iptables -t mangle -L UBIOS_PREROUTING_PBR -v -n | head -100'
Then use the remote WireGuard client and watch for counters to increase.
This was more useful than the UniFi UI because it showed whether the firewall/PBR rules were actually being hit.
Monitor Traffic Entering from the Remote WireGuard Client
In one SSH window, watch traffic entering from the remote WireGuard client on the WireGuard server interface:
tcpdump -ni wgsrv1 host 10.10.100.2
Replace 10.10.100.2 with the IP of your remote WireGuard client.
This confirms that traffic from the remote client is entering the UniFi gateway through wgsrv1.
Monitor Traffic Exiting the Outbound WireGuard Client Tunnel
In a second SSH window, watch traffic exiting the outbound WireGuard client tunnel:
tcpdump -ni wgclt2
Then generate traffic from the remote WireGuard client to a destination covered by your PBR rule.
Expected behavior:
Traffic enters on wgsrv1 from 10.10.100.2.
Matched PBR traffic exits through wgclt2.
That proves the full path:
Remote WireGuard client
→ wgsrv1
→ UniFi PBR match
→ wgclt2
Confirm the Route for Marked Traffic
This command simulates a forwarded packet from the remote WireGuard client entering on wgsrv1 with the UniFi PBR mark:
ip route get 1.1.1.1 from 10.10.100.2 iif wgsrv1 mark 0x6a0000
In my case, the expected result was:
1.1.1.1 from 10.10.100.2 dev wgclt2 table 178.wgclt2 mark 0x6a0000
cache iif wgsrv1
That output confirms that marked traffic from the remote WireGuard client will use the wgclt2 route table.
Confirm the WireGuard Subnet Is Still in the Local ipset
Since the entire fix depends on the remote WireGuard subnet being included in UniFi’s IPv4 local-network ipset, this is the quick check:
ipset test UBIOS4local_network 10.10.100.2
Expected:
10.10.100.2 is in set UBIOS4local_network.
If it is not in the set, re-run the fix:
ipset add UBIOS4local_network 10.10.100.0/24 -exist
or start the persistent service:
systemctl start wg-pbr-fix.service
Full Debugging Block
This is a useful all-in-one check:
echo "=== Timer status ==="
systemctl is-active wg-pbr-fix.timer
systemctl is-enabled wg-pbr-fix.timer
echo
echo "=== Is remote WG client treated as local? ==="
ipset test UBIOS4local_network 10.10.100.2 2>&1
echo
echo "=== Does marked traffic route out wgclt2? ==="
ip route get 1.1.1.1 from 10.10.100.2 iif wgsrv1 mark 0x6a0000
echo
echo "=== UniFi PBR counters ==="
iptables -t mangle -L UBIOS_PREROUTING_PBR -v -n | head -100
Expected high-level result:
Timer is active/enabled.
10.10.100.2 is in UBIOS4local_network.
Marked traffic from wgsrv1 routes out wgclt2.
PBR counters increase when matching traffic is generated.
This debugging is especially helpful because the UniFi Network UI may not accurately show that a traffic route has been triggered, even when the underlying iptables and routing behavior is working correctly.
Making It Persistent
The one-line fix works, but it will not necessarily survive:
reboot
UniFi Network restart
firewall rule regeneration
PBR changes
VPN changes
firmware updates
So I created a small script and systemd timer.
The timer checks every five minutes and re-adds the subnet only if UniFi has removed it.
Create the Script
mkdir -p /data/scripts
cat > /data/scripts/fix-wg-pbr-local.sh <<'EOF'
#!/bin/sh
WG_SUBNET="10.10.100.0/24"
TEST_IP="10.10.100.2"
SET_NAME="UBIOS4local_network"
# Exit quietly if UniFi has not created the ipset yet.
ipset list "$SET_NAME" >/dev/null 2>&1 || exit 0
# If the test IP is already covered, do nothing.
if ipset test "$SET_NAME" "$TEST_IP" >/dev/null 2>&1; then
exit 0
fi
# Otherwise re-add the whole WireGuard remote-access subnet.
ipset add "$SET_NAME" "$WG_SUBNET" -exist 2>/dev/null
logger -t wg-pbr-fix "Added $WG_SUBNET to $SET_NAME"
exit 0
EOF
chmod +x /data/scripts/fix-wg-pbr-local.sh
Create the systemd Service
cat > /etc/systemd/system/wg-pbr-fix.service <<'EOF'
[Unit]
Description=Ensure WireGuard remote-access subnet is eligible for UniFi PBR
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/data/scripts/fix-wg-pbr-local.sh
EOF
Create the 5-Minute Timer
cat > /etc/systemd/system/wg-pbr-fix.timer <<'EOF'
[Unit]
Description=Periodically check WireGuard remote-access subnet in UniFi PBR local ipset
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Unit=wg-pbr-fix.service
[Install]
WantedBy=timers.target
EOF
Enable the Timer
systemctl daemon-reload
systemctl enable --now wg-pbr-fix.timer
systemctl start wg-pbr-fix.service
Verifying the Timer
Check that the timer is active:
systemctl is-active wg-pbr-fix.timer
systemctl is-enabled wg-pbr-fix.timer
Expected:
active
enabled
See the schedule:
systemctl list-timers | grep wg-pbr
Check the last service run:
systemctl status wg-pbr-fix.service
For a oneshot service, this is normal:
Active: inactive (dead)
code=exited, status=0/SUCCESS
TriggeredBy: wg-pbr-fix.timer
It runs, exits, and waits for the timer to call it again.
Verify the actual fix:
ipset test UBIOS4local_network 10.10.100.2
Expected:
10.10.100.2 is in set UBIOS4local_network.
Verify routing:
ip route get 1.1.1.1 from 10.10.100.2 iif wgsrv1 mark 0x6a0000
Expected:
1.1.1.1 from 10.10.100.2 dev wgclt2 table 178.wgclt2 mark 0x6a0000
cache iif wgsrv1
Proof the Watchdog Works
After creating the service and timer, I verified the timer was active:
systemctl is-active wg-pbr-fix.timer
systemctl is-enabled wg-pbr-fix.timer
Expected:
active
enabled
I also checked the timer schedule:
systemctl list-timers | grep wg-pbr
Example output:
Mon 2026-05-18 13:38:39 EDT 3min 10s left Mon 2026-05-18 13:33:39 EDT 1min 49s ago wg-pbr-fix.timer wg-pbr-fix.service
Then I manually removed the WireGuard subnet from UniFi’s IPv4 local-network ipset:
ipset del UBIOS4local_network 10.10.100.0/24
Confirmed it was gone:
ipset test UBIOS4local_network 10.10.100.2
Expected:
10.10.100.2 is NOT in set UBIOS4local_network.
Then I manually started the service:
systemctl start wg-pbr-fix.service
And confirmed the subnet was re-added:
ipset test UBIOS4local_network 10.10.100.2
Expected:
10.10.100.2 is in set UBIOS4local_network.
Finally, I confirmed that marked traffic from the remote WireGuard client would route out the outbound WireGuard client tunnel:
ip route get 1.1.1.1 from 10.10.100.2 iif wgsrv1 mark 0x6a0000
Expected:
1.1.1.1 from 10.10.100.2 dev wgclt2 table 178.wgclt2 mark 0x6a0000
cache iif wgsrv1
Final Health Check
After reboot or UniFi updates, these are the quick checks:
systemctl is-active wg-pbr-fix.timer
ipset test UBIOS4local_network 10.10.100.2
ip route get 1.1.1.1 from 10.10.100.2 iif wgsrv1 mark 0x6a0000
Expected results:
active
10.10.100.2 is in set UBIOS4local_network.
1.1.1.1 from 10.10.100.2 dev wgclt2 table 178.wgclt2 mark 0x6a0000
cache iif wgsrv1
Rollback
To remove the persistent fix:
systemctl disable --now wg-pbr-fix.timer
rm -f /etc/systemd/system/wg-pbr-fix.timer
rm -f /etc/systemd/system/wg-pbr-fix.service
rm -f /data/scripts/fix-wg-pbr-local.sh
systemctl daemon-reload
Remove the ipset entry:
ipset del UBIOS4local_network 10.10.100.0/24 2>/dev/null
Important Caveats
This is an SSH-level workaround. It is not an official UniFi feature.
A few important notes:
This may break after UniFi OS or Network application updates.
Your interface names may differ.
Your WireGuard subnet may differ.
Your PBR mark/table may differ.
You should test before making it persistent.
This was tested on UniFi OS 5.1.11 with UniFi Network 10.4.57 on the Early Access channel. Future official or early-access releases may change the ipset names, firewall chains, routing table names, or PBR marks. Verify your own environment before applying the persistent service.
Do not blindly copy my subnet unless your WireGuard server is also using:
10.10.100.0/24
Adjust these values for your environment:
WG_SUBNET="10.10.100.0/24"
TEST_IP="10.10.100.2"
SET_NAME="UBIOS4local_network"
Conclusion
This ended up being a very clean fix.
The issue was not that UniFi could not route the traffic.
The issue was that remote WireGuard clients were not included in the local-network ipset that UniFi’s PBR rules use for source matching.
By adding:
ipset add UBIOS4local_network 10.10.100.0/24 -exist
remote WireGuard clients became eligible for the same GUI-created PBR rules as local clients.
The final working path is:
iPhone / remote WireGuard client
→ connects into UniFi WireGuard server
→ enters on wgsrv1
→ source IP is 10.10.100.x
→ source is now included in UBIOS4local_network
→ UniFi GUI PBR destination rules apply
→ matching traffic is marked for wgclt2
→ traffic exits through the UniFi gateway's outbound WireGuard client tunnel
→ remote WireGuard endpoint / proxy / other UDM
The key idea is that wgsrv1 and wgclt2 are opposite sides of the flow:
wgsrv1 = remote clients come in
wgclt2 = selected traffic goes back out
That allows remote clients to come back through the UniFi gateway and still benefit from selective Policy-Based Routing, including use cases like Xfinity Stream, Netflix, and other location-sensitive traffic.



































































