Introduction
I have two locations, which I affectionately call SiteA and SiteB. Both run UniFi Dream Machines. Years ago, I drank the UniFi Kool-Aid, and now I'm so deeply invested in their ecosystem that migrating away would probably cost more than staying.
The original idea was simple: I wanted to access devices in either location as if they were part of one larger private network. An old-school extranet.
No Internet traffic tunneling, no commercial VPN service, and no additional infrastructure. Just access to the remote LAN.
SiteA has a UDM Pro Max and SiteB has a UDM Pro. Both sit behind ISP-provided routers, both their WAN interfaces have private IP addresses. The public IP addresses live on the ISP equipment, which forwards the necessary ports to the UDMs.
This is ordinary double NAT, not necessarily CGNAT, but it prevents some of UniFi's more magical site-to-site options from being useful in my configuration, and yes, I've called my ISP, and no, they would not budge.
About a year ago, I built a solution using WireGuard.
Actually, I built two solutions.
I configured a WireGuard server on SiteA with a client on SiteB. Then I configured another WireGuard server on SiteB with a client on SiteA.
Two tunnels. One for each direction.
Did it work? Yes.
Was it necessary? Apparently not, I'm sure there were reasons for it back then, and maybe they didn't make sense, but it worked.
Last week, the WireGuard client on SiteA stopped connecting to the server on SiteB. DNS was correct, the endpoints were reachable as far as I could verify, and the configuration hadn't intentionally changed.
UniFi reported that the server was not reachable.
Why did it stop working? I still don't know.
I could have spent another afternoon investigating UDP traffic, port forwarding, and WireGuard handshakes.
Instead, I started wondering why I needed two tunnels in the first place, it did not make any sense back then, and it didn't today.
Turns out I only needed one... today.
Objective
The network layout looks like this:
INTERNET
|
+--------+--------+
| |
ISP Router ISP Router
| |
UDM Pro Max UDM Pro
SiteA SiteB
| |
192.168.10.0/24 192.168.30.0/24
What I wanted:
- Devices on
192.168.10.0/24should reach192.168.30.0/24. - Devices on
192.168.30.0/24should reach192.168.10.0/24. - Every device should continue using its local Internet connection.
- Hosts should retain their original LAN IP addresses when communicating between locations.
That last requirement became rather important later.
A normal site-to-site routed network should behave exactly like this.
WireGuard OSS implementation supports bidirectional communication over a single tunnel, regardless of which peer initiates the connection.
The server/client distinction determines how the connection gets established. It doesn't dictate which direction traffic can travel.
I knew that.
Apparently, a year ago, I chose to ignore it, or it didn't work by any reason... moving on.
Prerequisites
For this configuration, I'm using:
| Component | SiteA | SiteB |
|---|---|---|
| Gateway | UniFi UDM Pro Max | UniFi UDM Pro |
| Local network | 192.168.10.0/24 |
192.168.30.0/24 |
| WireGuard role | Server | Client |
| WireGuard address | 10.77.0.1 |
10.77.0.2 |
| Internet | ISP A | ISP B |
| WAN | Private IP behind ISP router | Private IP behind ISP router |
You'll need two distinct local subnets, administrative access to both gateways, and a way for the remote WireGuard client to reach the server.
Because both locations use dynamic public addresses, I maintain their DNS records through a Python script and deSEC.
We've addressed this on post 012, Dynamic DNS: Python Script + deSEC.io DNS Service.
For WireGuard, you need one reachable server endpoint. In my case, the SiteA ISP router forwards UDP port 51820 to its UDM Pro Max.
If you're behind CGNAT and don't control any publicly reachable endpoint, this particular setup won't solve that problem.
Architecture
The resulting configuration is considerably simpler.
SiteA SiteB
192.168.10.0/24 192.168.30.0/24
| |
UDM Pro Max UDM Pro
| |
WG Server WG Client
10.77.0.1 10.77.0.2
| |
+========== WireGuard ==========+
One bidirectional tunnel
One tunnel. Two networks. No full Internet routing.
There are four pieces we need to configure:
- WireGuard routing on the SiteA server.
- Routing to SiteA through the SiteB client.
- Firewall permissions for connections initiated from SiteA.
- NAT exclusion so SiteB devices retain their original source IPs.
The first two establish the routing. The third permits connections in both directions. The fourth makes the resulting network behave like a proper routed network instead of hiding everyone behind the VPN gateway.
1. Configuring the WireGuard Server on SiteA
I already had a working WireGuard server on SiteA, so I kept it.
In UniFi Network, navigate to Settings → VPN → VPN Server and create or edit the WireGuard server.
My settings:
Name: WG_Server
Network: 10.77.0.0/24
Gateway: 10.77.0.1
Port: UDP 51820
Create a peer for SiteB.
Client Name: SiteB_WG_Client
Client IP: 10.77.0.2
Now comes the first interesting setting.
Open the client configuration and find Networks Behind Client.
Enable it and add:
192.168.30.0/24
This tells SiteA that the SiteB LAN is reachable through this WireGuard peer.
UniFi should automatically create the corresponding VPN route.
In my case, checking the routing table confirmed:
Destination: 192.168.30.0/24
Interface: WG_Server
Type: VPN
Without this, SiteA knows how to communicate with the WireGuard client, but not necessarily how to reach devices behind that client.
That's the first piece.
2. Configuring the WireGuard Client on SiteB
On SiteB, navigate to Settings → VPN → VPN Client.
Import the configuration generated by the SiteA WireGuard server.
The client configuration uses:
[Interface]
PrivateKey = <SiteB_PRIVATE_KEY>
Address = 10.77.0.2/32
[Peer]
PublicKey = <SiteA_PUBLIC_KEY>
Endpoint = <SiteA_DDNS_HOSTNAME>:51820
AllowedIPs = 192.168.10.0/24, 10.77.0.0/24
PersistentKeepalive = 25
This is an illustrative split-tunnel configuration, not a verbatim export of my client. The exact tunnel addresses and permitted peer routes should match your own server configuration.
The important part is that we do not need 0.0.0.0/0 for this purpose.
We're interested in reaching remote private networks, not redirecting all SiteB Internet traffic through SiteA.
My client is named SiteA_Server.
Once connected, UniFi reports the tunnel as Established.
Policy-Based Routing
On SiteB, I already had a policy named WG_Traffic.
It routes traffic destined for my SiteA networks through the WireGuard client.
The relevant settings:
Name: WG_Traffic
Interface: SiteA_Server
Source: Any
Destination: 192.168.10.0/24
Kill Switch: Enabled
I also have other private networks behind SiteA, so my actual policy includes additional destinations.
The critical point is that the route targets the remote LAN, not the entire Internet.
At this stage, devices at SiteB should be able to initiate connections toward SiteA, provided the SiteA firewall permits them.
Kill Switch is optional. I keep it enabled because if the tunnel fails, I don't want traffic intended for the remote LAN attempting to use another connection. There isn't much point in sending packets destined for a private subnet into the Internet anyway.
Which brings us to UniFi's firewall.
3. The Firewall Problem
This one was rather entertaining.
The tunnel was connected.
SiteA had the correct route to SiteB.
SiteB could reach devices on SiteA.
But from SiteA, I couldn't reach anything on the SiteB LAN.
For example:
ping -c 4 192.168.30.34
Result:
4 packets transmitted, 0 packets received
100.0% packet loss
The routing looked correct. The tunnel was up.
So what was missing?
Apparently, UniFi places WireGuard client interfaces in the External firewall zone.
Yes, External.
Alongside the WAN interfaces.
The WireGuard server interface on SiteA belongs to the VPN zone, but the WireGuard client on SiteB belongs to External.
Different firewall treatment for two sides of the same connection.
Fair enough, I suppose.
On SiteB, the relevant default firewall relationship was:
External → Internal
Allow Return
Block Invalid
Block All
That allows responses to established connections, but prevents new connections initiated from SiteA.
We need a firewall rule.
Creating the SiteB Firewall Policy
Under Settings → Policy Engine → Firewall, create:
Name: Allow_SiteA_to_SiteB
Action: Allow
Source Zone: External
Source IP: 192.168.10.0/24
Destination Zone: Internal
Destination: Main_Net_SiteB
(192.168.30.0/24)
IP Version: IPv4
Protocol: All
Connection State: All
Auto Allow Return: Enabled
Make sure this rule takes precedence over the default External → Internal block rule.
One security detail worth mentioning: the External zone also contains the actual WAN interfaces. Restricting the source to 192.168.10.0/24 is important, but I would prefer an additional incoming-interface restriction if UniFi exposes one in a future Network release.
Do not simply allow everything from External to Internal.
After applying the rule, I tested again.
PING 192.168.30.34
4 packets transmitted
4 packets received
0.0% packet loss
round-trip min/avg/max:
11.122/14.950/18.940 ms
Another SiteB host also responded successfully.
Brilliant.
SiteA could now initiate connections toward SiteB.
Interestingly, I still couldn't ping the SiteB gateway at 192.168.30.1.
That's because traffic targeting the UDM itself is subject to a different firewall relationship, External → Gateway.
I don't need to manage the gateway through its LAN address, so I left that alone.
Less access is perfectly acceptable when the access isn't necessary.
At this point, I thought we were finished.
I was wrong.
4. The NAT Masquerading Problem
One of the services I run at SiteA is a TrueNAS server named THISISTHENAS.
Its address is:
192.168.10.50
At SiteB, I have a Proxmox node named Virtual01:
192.168.30.60
Virtual01 mounts NFS shares exported by THISISTHENAS.
The TrueNAS exports permit clients from both networks:
192.168.10.0/24
192.168.30.0/24
Perfectly reasonable.
Except Virtual01 couldn't access the shares correctly.
The connection existed, and the hosts could communicate, but NFS authentication was failing.
Cue tcpdump.
Or how my grandpa used to say, "the what the heck is going on here"
On THISISTHENAS:
sudo tcpdump -ni any \
'host 192.168.30.60 or host 10.77.0.2'
And there was the problem.
10.77.0.2 → 192.168.10.50:2049
THISISTHENAS wasn't seeing Virtual01's original address.
It was seeing the SiteB WireGuard client address.
UniFi was masquerading traffic leaving through the WireGuard client interface.
That means every SiteB host could appear to SiteA as the same device:
10.77.0.2
Useful for certain VPN configurations.
Not particularly useful when the entire purpose is to connect two routed networks and preserve individual host identities.
The VPN worked, but it wasn't behaving the way I wanted.
Creating a NAT Exclusion
Back to the SiteB UDM.
Navigate to Settings → Policy Engine → NAT and create a policy.
My configuration:
Name: No_NAT_SiteB_to_SiteA
Type: Masquerade
Interface: SiteA_Server
Source: 192.168.30.0/24
Destination: 192.168.10.0/24
IP Version: IPv4
Protocol: All
Exclude: Enabled
The critical setting is Exclude.
Instead of masquerading traffic matching that source, destination, and interface combination, UniFi should preserve its original source address.
This exclusion also needs to take precedence over a matching masquerade rule.
After applying the policy, I tested NFS again.
This time Virtual01 could access THISISTHENAS's documents.
More importantly, packet captures confirmed the addresses were correct.
192.168.30.60 → 192.168.10.50:2049
192.168.10.50 → 192.168.30.60
Successful NFS requests and replies.
No masquerading.
No need to modify the TrueNAS export permissions to accommodate an artificial tunnel address.
This is exactly what I wanted, is this how UniFi should work by default? maybe, for my use case, I just wish the documentation was clearer.
5. Verifying Bidirectional Connectivity
I don't particularly trust an interface telling me something is connected.
Connected doesn't necessarily mean working, as this entire exercise demonstrated.
I prefer testing actual traffic.
From SiteA to SiteB
From a SiteA host:
ping -c 4 192.168.30.62
My results:
4 packets transmitted
4 packets received
0.0% packet loss
Average RTT: 14.950 ms
From SiteB to SiteA
From Virtual01:
ping -c 4 192.168.10.50
Then verify what the destination actually receives.
On THISISTHENAS
sudo tcpdump -ni any \
'tcp port 2049 and (net 192.168.30.0/24 or host 10.77.0.2)'
The expected result:
192.168.30.60 → 192.168.10.50
Not:
10.77.0.2 → 192.168.10.50
On Virtual01
I also captured traffic while pinging Virtual01 from THISISTHENAS.
The result:
192.168.10.50 → 192.168.30.60 ICMP echo request
192.168.30.60 → 192.168.10.50 ICMP echo reply
That confirmed both directions were operational, with original source addresses intact.
A proper routed network.
Finally.
6. Cleaning Up the Old Configuration
With the single tunnel working, I could remove the redundant pair:
- The old WireGuard client on SiteA, which connected toward SiteB.
- The old WireGuard server on SiteB, which accepted that connection.
The remaining configuration is:
SiteA:
WG_Server
|
+--- SiteB_WG_Client
Remote Network: 192.168.30.0/24
SiteB:
SiteA_Server
|
+--- WG_Traffic routing policy
+--- Allow_SiteA_to_SiteB firewall policy
+--- No_NAT_SiteB_to_SiteA NAT exclusion
Four operational concerns, one tunnel.
The old reverse tunnel wasn't providing anything that the existing WireGuard connection couldn't already accomplish.
7. Never forget again
WireGuard is inherently bidirectional. You don't need a separate server/client pair for every direction.
Routing and firewall permissions are different things. Having a route to a subnet doesn't mean the destination gateway permits the traffic.
A working tunnel doesn't guarantee preserved source addresses. If your remote services depend on client IPs, verify what they actually receive.
UniFi's VPN server and VPN client interfaces may belong to different firewall zones. That affects how new connections are handled.
Avoid broad NAT and firewall changes. A narrowly scoped exclusion is preferable to disabling masquerading globally.
Dynamic DNS and port forwarding are separate prerequisites. The WireGuard server must be reachable before any of the routing configuration matters.
And because UniFi occasionally rearranges its interface, keep a record of the actual settings instead of relying entirely on menu paths.
Conclusion
I started this exercise intending to repair a WireGuard tunnel that had stopped working.
Instead, I removed it.
What previously required two independent WireGuard connections now works through one, with both LANs accessible, independent Internet connections, and original IP addresses preserved.
The important configuration wasn't particularly exotic:
- Advertise the network behind the WireGuard client.
- Route remote subnets through the existing tunnel.
- Permit incoming traffic in the appropriate firewall zone.
- Exclude site-to-site traffic from source NAT.
All reasonably ordinary networking concepts.
The difficult part was finding where UniFi had placed them and understanding how its implementation treats WireGuard clients differently from servers.
I still don't know why the original tunnel stopped connecting.
At this point, I don't particularly care.
One less tunnel to maintain.
And for once, improving my homelab didn't require buying anything.
I'm not sure how I feel about that.