← All articles
NETWORKING Site-to-Site VPN with WireGuard Between Home Labs 2026-02-09 · 6 min read · wireguard · vpn · networking

Site-to-Site VPN with WireGuard Between Home Labs

Networking 2026-02-09 · 6 min read wireguard vpn networking site-to-site routing

If you have homelab gear in two locations — your house and a friend's, a main lab and a colocation, or a home network and a cloud VPC — you probably want those networks to talk to each other as if they were one. A site-to-site VPN does exactly that: it connects two entire subnets through an encrypted tunnel, so devices on one side can reach devices on the other side transparently.

WireGuard is the best tool for this. It's fast, simple, built into the Linux kernel, and the configuration is measured in lines, not pages. Unlike OpenVPN or IPsec, there's no certificate authority to manage, no complex phase negotiations, and the performance overhead is minimal — WireGuard typically saturates your internet uplink before it becomes a bottleneck itself.

WireGuard logo

Network Layout

For this guide, we'll connect two sites:

After setup, a device at 192.168.1.50 (Site A) will be able to reach 192.168.2.100 (Site B) directly, and vice versa.

Installing WireGuard

On Debian/Ubuntu:

sudo apt update
sudo apt install -y wireguard

On Fedora:

sudo dnf install -y wireguard-tools

WireGuard is in the kernel since Linux 5.6. If you're on an older kernel, the wireguard-dkms package provides the module. Modern distros should have it built in.

Generating Keys

On each site's gateway, generate a key pair:

wg genkey | tee privatekey | wg pubkey > publickey

This creates two files: privatekey and publickey. You'll need Site A's public key on Site B's config, and vice versa. Keep the private keys secure.

# Read the keys
cat privatekey
cat publickey

Do this on both Site A and Site B gateways.

Configuring Site A

Create /etc/wireguard/wg0.conf on the Site A gateway:

[Interface]
Address = 10.0.0.1/30
PrivateKey = <Site A private key>
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
PublicKey = <Site B public key>
Endpoint = 198.51.100.20:51820
AllowedIPs = 10.0.0.2/32, 192.168.2.0/24
PersistentKeepalive = 25

Key details:

Configuring Site B

Create /etc/wireguard/wg0.conf on the Site B gateway:

[Interface]
Address = 10.0.0.2/30
PrivateKey = <Site B private key>
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
PublicKey = <Site A public key>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.0.0.1/32, 192.168.1.0/24
PersistentKeepalive = 25

It's a mirror of Site A's config with the IPs and keys swapped.

Enabling IP Forwarding

Both gateways need to forward packets between interfaces. Enable it:

# Enable immediately
sudo sysctl -w net.ipv4.ip_forward=1

# Make it persistent
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system

Starting the Tunnel

On both sites:

sudo wg-quick up wg0

Enable it at boot:

sudo systemctl enable wg-quick@wg0

Verify the tunnel is up:

sudo wg show

You should see the peer listed with a recent handshake timestamp:

interface: wg0
  public key: <your public key>
  private key: (hidden)
  listening port: 51820

peer: <remote public key>
  endpoint: 198.51.100.20:51820
  allowed ips: 10.0.0.2/32, 192.168.2.0/24
  latest handshake: 12 seconds ago
  transfer: 1.24 KiB received, 1.08 KiB sent

Test connectivity:

# From Site A, ping Site B's tunnel IP
ping 10.0.0.2

# From Site A, ping a device on Site B's LAN
ping 192.168.2.100

Routing for Other Devices

The tunnel is up between the two gateways, but other devices on each LAN don't know about the remote subnet yet. There are three approaches:

Option 1: Static Routes on Your Router (Recommended)

On your Site A router, add a static route:

Destination: 192.168.2.0/24
Gateway: 192.168.1.1 (your WireGuard gateway's LAN IP)

Do the same on Site B's router for 192.168.1.0/24 via 192.168.2.1. Now every device on both LANs can reach the other side without any per-device configuration.

Option 2: Static Routes on Individual Devices

If you can't modify your router:

# On a Linux device at Site A
sudo ip route add 192.168.2.0/24 via 192.168.1.1

# On Windows at Site A
route add 192.168.2.0 mask 255.255.255.0 192.168.1.1

Option 3: NAT/Masquerade (Already Configured)

The MASQUERADE rule in our PostUp commands means traffic from remote devices appears to come from the gateway's LAN IP. This works without any routing changes on other devices, but remote devices can't initiate connections back — they only see the gateway's IP, not individual remote devices. This is fine for accessing services but breaks things that need true bidirectional connectivity.

For a proper site-to-site setup, Option 1 (router static routes) is the way to go.

Port Forwarding

Both sites need UDP port 51820 forwarded from the public IP to the WireGuard gateway. If your gateway IS the router (running pfSense, OPNsense, etc.), no port forward is needed — just open the port in the firewall.

If one side has no static IP, use dynamic DNS. WireGuard will resolve the Endpoint hostname periodically:

[Peer]
Endpoint = mylab.duckdns.org:51820

If both sides are behind CGNAT (no public IP at all), you'll need a relay. A cheap VPS in the middle can act as a WireGuard hub that both sites connect to. Each site configures the VPS as its peer, and the VPS routes between them.

Dynamic DNS and Endpoint Refresh

WireGuard resolves the Endpoint hostname only at startup. If a dynamic IP changes, the tunnel breaks until you restart. Fix this with a cron job:

# /etc/cron.d/wireguard-reresolve
*/5 * * * * root /usr/bin/wg set wg0 peer <PEER_PUBLIC_KEY> endpoint $(dig +short mylab.duckdns.org):51820

Or use the reresolve-dns.sh script that ships with wireguard-tools:

*/5 * * * * root /usr/share/wireguard-tools/examples/reresolve-dns/reresolve-dns.sh wg0

Firewall Considerations

If you're running a firewall on the gateways (you should be), make sure these rules are in place:

# Allow WireGuard UDP port
sudo ufw allow 51820/udp

# Allow forwarding between wg0 and your LAN interface
sudo ufw route allow in on wg0 out on eth0
sudo ufw route allow in on eth0 out on wg0

If using firewalld:

sudo firewall-cmd --permanent --add-port=51820/udp
sudo firewall-cmd --permanent --add-masquerade
sudo firewall-cmd --reload

Monitoring the Tunnel

WireGuard doesn't have built-in monitoring, but you can script it:

#!/bin/bash
# Check if the latest handshake was within the last 3 minutes
LAST_HANDSHAKE=$(sudo wg show wg0 latest-handshakes | awk '{print $2}')
NOW=$(date +%s)
DIFF=$((NOW - LAST_HANDSHAKE))

if [ $DIFF -gt 180 ]; then
    echo "WireGuard tunnel stale - last handshake ${DIFF}s ago"
    # Optionally restart: systemctl restart wg-quick@wg0
fi

Run this from cron or a monitoring system. You can also export WireGuard metrics to Prometheus with wireguard_exporter.

Performance

WireGuard's overhead is minimal. On modern hardware, expect:

A Raspberry Pi can handle a WireGuard site-to-site tunnel at 300+ Mbps. A modest x86 box won't even notice it's there.

WireGuard site-to-site is one of those setups where the configuration time is measured in minutes but the utility lasts years. Once it's up, your two networks just work as one, and you stop thinking about which site a service is on.