Karolis Tamutis

UniFi Site-to-Site WireGuard Setup

| 5 min read

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.

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:

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:

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:

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:

  1. 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.
  2. 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.
  3. 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.

Tags:

Share this post: