UniFi Site-to-Site WireGuard Setup
Imagine you have two homes with UniFi gear and you want devices at both locations to see each other as if they were on the same network. No NAT, no port forwarding, just flat routing between the two LANs over WireGuard. Sounds simple, right?
I spent an embarrassing amount of time getting this to work. Most WireGuard + UniFi guides either cover road-warrior (single client) setups, rely on UniFi’s Policy-Based Routes (which are not real routes, more on that later), or just break the moment you want both LANs to talk to each other symmetrically. This is the version that actually works and survives reboots. Documenting it here, mostly for my future self.
The topology
I have two locations, each running a UniFi gateway (UCG/UDM-class). Site A is the “main” site with a static public IP, Site B is the “remote” behind some ISP router.
- Site A: LAN
10.1.1.0/24, gateway10.1.1.1, public IP203.0.113.10 - Site B: LAN
10.1.3.0/24, gateway10.1.3.1, behind NAT (doesn’t matter)
I’m using a dedicated 10.1.2.0/24 “transit” network just for the WireGuard link between the two gateways. Site A gets 10.1.2.1, Site B gets 10.1.2.2.
The goal: devices on 10.1.1.0/24 can reach 10.1.3.0/24 and vice-versa. Both sides can ping the other’s gateway, switches, cameras, whatever. No NAT between sites, real IPs everywhere.
WireGuard config
Site A acts as the WireGuard server. You can set this up via UniFi’s UI, but here’s what the config effectively looks like:
# Site A: /etc/wireguard/wg-site-a.conf
[Interface]
PrivateKey = <SITE_A_PRIVATE_KEY>
Address = 10.1.2.1/24
ListenPort = 51820
# Site B peer
[Peer]
PublicKey = <SITE_B_PUBLIC_KEY>
AllowedIPs = 10.1.2.0/24, 10.1.3.0/24
The important bit in AllowedIPs is that we’re telling Site A to accept traffic from the WG transit network and the entire Site B LAN through the tunnel.
Site B is the client:
# Site B: /etc/wireguard/wg-site-b.conf
[Interface]
PrivateKey = <SITE_B_PRIVATE_KEY>
Address = 10.1.2.2/32
DNS = 10.1.2.1 # optional
[Peer]
PublicKey = <SITE_A_PUBLIC_KEY>
AllowedIPs = 10.1.2.0/24, 10.1.1.0/24
Endpoint = 203.0.113.10:51820
PersistentKeepalive = 25
Mirror image. Site B’s AllowedIPs includes the transit network and Site A’s LAN. This is what lets Site B send traffic to 10.1.1.0/24 through the tunnel at all. PersistentKeepalive is set because Site B is behind NAT and needs to keep the tunnel alive.
The UniFi routing trap
This is where I lost the most time. When you set up WireGuard through UniFi’s wizard, it creates what look like routes in the UI. They show up under Policy-Based Routing with entries like “Send 10.1.3.0/24 over wg-site-b”.
The problem? These are not kernel routes. They’re firewall match rules. If you SSH into your gateway and run ip route, you won’t see any routes for the remote LAN. Return traffic has no idea how to get back into the tunnel.
The fix is to change those Policy-Based Route entries to actual Static Routes.
On Site B (remote site), go to Settings > Routing & Firewall > Policy-Based Routing. Find the entry that was “sending to 10.1.1.0/24” and change it to:
- Policy Type:
Static Route - Action:
Route - Destination:
10.1.1.0/24 - Interface / Tunnel:
wg-site-b(the WireGuard client) - Source / Zones: leave at
Any/-
This effectively creates a real kernel route:
ip route add 10.1.1.0/24 dev wgclt1
Do the mirror on Site A for the Site B LAN:
- Policy Type:
Static Route - Destination:
10.1.3.0/24 - Interface / Tunnel:
wg-site-a(WireGuard server)
Giving you:
ip route add 10.1.3.0/24 dev wgsrv1
This is the crucial step. Without these real routes, everything else is pain. I wish someone had told me this upfront instead of me staring at tcpdump output for hours wondering why return packets were vanishing.
Firewall rules (the weird part)
Here’s something that isn’t obvious at all: on UniFi UCG/UDM devices, WireGuard traffic is treated more like “WAN ingress” than “LAN”. This means:
- WAN IN rules affect forwarded traffic from WG to your LAN hosts
- Internet Local rules affect traffic from WG to the gateway itself
- LAN IN rules, where you’d intuitively put this, live in a sub-chain that WG traffic never hits
So on each side, you need two firewall rules. I’ll show Site A’s rules, then you just mirror them on Site B with the subnets swapped.
On Site A, create an Internet Local rule to allow Site B LAN (10.1.3.0/24) to talk to the Site A gateway. Accept, source 10.1.3.0/24, destination Any, all protocols. This covers Site B reaching 10.1.2.1 (WG IP), 10.1.1.1 (LAN IP), and any services on the gateway itself.
Then create a WAN IN rule to allow Site B LAN to reach Site A LAN hosts. Accept, source 10.1.3.0/24, destination 10.1.1.0/24, all protocols. Make sure to check “Apply before predefined rules”. This is the rule that effectively becomes:
iptables -I UBIOS_FORWARD_IN_USER \
-i wgsrv1 -s 10.1.3.0/24 -d 10.1.1.0/24 -j ACCEPT
On Site B, do the exact mirror: Internet Local allowing 10.1.1.0/24 to the gateway, WAN IN allowing 10.1.1.0/24 to reach 10.1.3.0/24.
With all four rules in place, traffic flows symmetrically in both directions with no NAT.
Sanity checks
From a device on Site A LAN:
ping 10.1.3.1 # Site B gateway
ping 10.1.3.2 # Site B switch
ping 10.1.3.165 # Site B camera, etc.
From Site B gateway:
ping 10.1.1.1 # Site A gateway
ping 10.1.1.x # some Site A LAN host
On each gateway, verify the routes are actually there:
ip route | grep 10.1.1.0
ip route | grep 10.1.3.0
If something is off, tcpdump on the WG interfaces is your friend:
# On Site A
tcpdump -nnpi wgsrv1 icmp
# On Site B
tcpdump -nnpi wgclt1 icmp
What I learned
The three things that weren’t obvious and cost me the most time:
- UniFi’s Policy-Based Routing entries from the WireGuard wizard are not kernel routes. For site-to-site, you must convert them to Static Route type policies bound to the WG tunnel.
- WireGuard on UniFi gateways behaves like a virtual WAN interface, not LAN. Use WAN IN for forwarded traffic and Internet Local for gateway-destined traffic.
- For full mesh between sites, you need three pieces per side: WG config with proper
AllowedIPs, real routes to the remote LAN via the WG interface, and firewall rules (WAN IN + Internet Local) to allow that traffic.
Once those are in place, the whole thing becomes boring and just keeps working.