A WireGuard site to site VPN configuration needs two gateways, one keypair each, and one [Peer] block per side whose AllowedIPs holds the remote LAN plus the remote tunnel address. At least one side needs an Endpoint, the reachable side needs UDP 51820 open, both need IP forwarding, and LAN hosts need a route back.
The examples use this layout throughout:
| Site A | Site B | |
|---|---|---|
| LAN | 192.168.10.0/24 | 192.168.20.0/24 |
| Gateway LAN IP | 192.168.10.1 | 192.168.20.1 |
| WAN | static 203.0.113.10 | dynamic, behind ISP NAT |
Tunnel IP on wg0 | 10.7.0.1/30 | 10.7.0.2/30 |
Both gateways run Linux with wg-quick and nftables, with wan0 and lan0 as interface names. The pfSense, OPNsense and MikroTik equivalents follow.
What has to be true before you write a config?
Three things: the two LANs must not overlap, at least one site must accept inbound UDP on its WAN, and you need to know which box routes for each LAN. Miss one and the tunnel can handshake cleanly while carrying nothing useful.
- Non-overlapping subnets. If both sites run the consumer-router default
192.168.1.0/24, fix that first; the FAQ covers the options. - One reachable side. The responder needs a public address, plus an explicit UDP 51820 forward if an ISP router sits in front; UPnP stays off. If both sites sit behind CGNAT, neither can accept a handshake: you need a public relay or a coordination layer, compared in WireGuard vs Tailscale.
- Where WireGuard runs. On the router, routing mostly takes care of itself. On a LAN box such as a Raspberry Pi, the site’s real router needs a static route, covered below.
The two wg-quick configs
Generate a keypair on each gateway. Private keys never leave the box; only public keys are exchanged.
umask 077
wg genkey | tee /etc/wireguard/wg0.key | wg pubkey > /etc/wireguard/wg0.pub
Site A, /etc/wireguard/wg0.conf:
[Interface]
Address = 10.7.0.1/30
ListenPort = 51820
PrivateKey = <site A private key>
[Peer]
# Site B
PublicKey = <site B public key>
AllowedIPs = 10.7.0.2/32, 192.168.20.0/24
Site B:
[Interface]
Address = 10.7.0.2/30
ListenPort = 51820
PrivateKey = <site B private key>
[Peer]
# Site A
PublicKey = <site A public key>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.7.0.1/32, 192.168.10.0/24
PersistentKeepalive = 25
Start each with systemctl enable --now wg-quick@wg0. Per wg-quick(8), it infers routes from every peer’s AllowedIPs and “automatically adds them to the system routing table,” so Site A gets 192.168.20.0/24 dev wg0 with no separate route command. Leave MTU unset and it derives one from the endpoint or default route.
AllowedIPs works in both directions: it is the route out and the source filter in. Site A accepts decrypted packets from Site B only if their source sits in 10.7.0.2/32 or 192.168.20.0/24. The tunnel /32 covers traffic a gateway originates itself, which is sourced from its tunnel IP; the Pro Custodibus site-to-site walkthrough adds it for that case. More in WireGuard Allowed IPs: what to set on each peer and WireGuard explained.
Which side needs Endpoint and PersistentKeepalive?
The side that can’t accept inbound connections initiates: it gets an Endpoint pointing at the reachable side, plus PersistentKeepalive = 25. The reachable side needs neither. Per wg(8), a peer’s endpoint “will be updated automatically” to the source of the latest authenticated packet, so Site A learns Site B’s current address from the handshake.
Keepalive is off by default. Without it, Site B’s NAT mapping times out during a quiet spell and Site A has no way back in until a host at B sends something. With it, B sends an authenticated empty packet every 25 seconds, the interval wg(8) uses in its own example and the one the OPNsense site-to-site how-to specifies for a site behind NAT. If both sites have static addresses, give both an Endpoint and skip keepalive; either side can then start the handshake.
Dynamic DNS needs one more piece. An Endpoint hostname is resolved when the peer is configured, not continuously, so a WAN address change strands the tunnel. OPNsense ships a cron job for this, “Renew DNS for WireGuard on stale connections.” On Linux, wireguard-tools includes a reresolve-dns script meant to run “every thirty seconds or so” from cron.
Forwarding, firewall rules and the return route
A handshake proves keys and endpoints, nothing more. LAN-to-LAN traffic also needs kernel forwarding, forward-chain rules for the two subnets, and a return path for LAN hosts. Enable forwarding persistently on both gateways; dual-stack sites add net.ipv6.conf.all.forwarding = 1 and the remote IPv6 prefix in AllowedIPs.
echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-wireguard.conf
sysctl --system
Site A’s ruleset, default-deny, in apply order. Site A’s LAN may reach all of Site B; Site B may reach only the NAS at 192.168.10.20 on TCP 443 and 445:
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iif "lo" accept
iifname "lan0" accept
iifname "wan0" udp dport 51820 accept
iifname "wg0" icmp type echo-request accept
}
chain forward {
type filter hook forward priority filter; policy drop;
oifname "wg0" tcp flags syn tcp option maxseg size set rt mtu
ct state established,related accept
iifname "lan0" oifname "wan0" accept
iifname "lan0" oifname "wg0" ip daddr 192.168.20.0/24 accept
iifname "wg0" oifname "lan0" ip saddr 192.168.20.0/24 ip daddr 192.168.10.20 tcp dport { 443, 445 } accept
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "wan0" masquerade
}
}
Site B runs the mirror image with the subnets swapped, plus anything its gateway serves to Site A, such as DNS on port 53 from 192.168.10.0/24 via wg0. Four details decide whether this works:
- The MSS rule sits first on purpose. It clamps SYNs entering the tunnel to a size derived from the route MTU, 1380 on a 1420-byte
wg0, the same ceiling the OPNsense guide gives. A SYN-ACK leavingwg0is alreadyestablishedto conntrack, so a clamp placed below that rule never sees it. - Shadowing. A blanket
iifname "wg0" acceptanywhere above the NAS rule hands Site B your whole LAN and turns the NAS rule into dead code. The pfSense recipe uses a pass-everything tunnel rule for brevity and labels it “not a secure practice.” - NAT scope. Masquerade only on
oifname "wan0". A masquerade rule with no interface match also rewrites tunnel traffic, and Site B’s per-host rules stop matching. - Input on the initiator. Site B starts every handshake, so replies from A ride
ct state established. If you add the UDP 51820 rule there anyway, pin it withip saddr 203.0.113.10.
If WireGuard runs on a LAN box, say a Pi at 192.168.10.5, hosts still send Site B traffic to their default gateway, which pushes it out the WAN. Add 192.168.20.0/24 via 192.168.10.5 on Site A’s router and the mirror at Site B; as Pro Custodibus notes, no WireGuard setting can install that route. Stateful routers that see only one direction of each flow may drop it; masquerading on the Pi avoids that but hides source addresses. See the Raspberry Pi setup guide.
The same tunnel on pfSense, OPNsense and MikroTik
The concepts map one-to-one; only the knobs move. Values come from the pfSense recipe, the OPNsense how-to and the RouterOS WireGuard page:
| pfSense | OPNsense | MikroTik RouterOS | |
|---|---|---|---|
| Transfer net in the example | /31 | /24 | /30 |
| Remote LAN routing | Allowed IPs plus a static route via the assigned WireGuard gateway | Peer Allowed IPs | allowed-address plus /ip/route with gateway=wireguard1 |
| WAN rule | Pass UDP 51820 to WAN address | Pass UDP 51820 from the other site’s public IP | chain=input accept UDP 13231 from the peer, above any drop |
| MTU / MSS | 1420 dual-stack, 1440 IPv4-only | MTU 1420 (1412 on PPPoE), MSS 1380 or lower | Default MTU 1420 |
| Watch for | Pin the default gateway to WAN before assigning the tunnel | Endpoint hostnames resolve only at tunnel start | Example port is 13231, not 51820 |
How do you test a site-to-site tunnel?
Test from a LAN host at each site, not only from the gateways. A ping from a gateway is sourced from its tunnel IP, skips its own forward chain, and may hit a different rule on the far side, so it proves little either way. Check the handshake, route, LAN reachability, MTU ceiling, throughput both ways, and DNS.
# on Site A's gateway
wg show wg0 # recent "latest handshake", rx/tx climbing
ip route get 192.168.20.10 # expect "dev wg0"
tcpdump -ni wan0 udp port 51820
# from 192.168.10.50 on Site A's LAN
ping -c 3 192.168.20.1
mtr -n -c 20 192.168.20.10 # 192.168.10.1, 10.7.0.2, target
ping -M do -s 1392 -c 3 192.168.20.10 # 1392 + 28 = 1420: must pass
ping -M do -s 1393 -c 3 192.168.20.10 # must fail: frag needed, mtu 1420
iperf3 -c 192.168.20.10 -t 30 # A to B (iperf3 -s on the target)
iperf3 -c 192.168.20.10 -t 30 -R # B to A
dig @192.168.20.1 nas.siteb.home.arpa
Read failures in order. No handshake: endpoint, keys or the WAN rule; tcpdump shows whether packets arrive. Handshake but no LAN ping: forwarding, the forward chain or the return route. Small pings pass but HTTPS or SMB stalls: MTU and MSS, covered in the WireGuard MTU calculator. On PPPoE, where wg-quick picks 1412, test with 1384 and 1385. A-to-B throughput can’t beat Site A’s upload or Site B’s download; judge iperf3 against those.
FAQ
does wireguard site to site need a static ip
No. Only one side must be reachable, and that side can use dynamic DNS instead of a static address. The unreachable side gets an Endpoint pointing at it plus PersistentKeepalive = 25, and the reachable side learns the return address from the handshake. If both sites sit behind CGNAT, add a relay with a public address.
how do i connect three sites with wireguard
Use hub-and-spoke or a full mesh. In hub-and-spoke, the hub has one peer per spoke with that spoke’s tunnel IP and LAN in AllowedIPs, each spoke’s single hub peer lists every other site’s LAN, and the hub forwards wg0 to wg0. A full mesh gives each site a peer for every other site and skips the hub hop.
can wireguard site to site work with overlapping subnets
Not directly. A prefix can’t be your local LAN and a route into the tunnel at the same time, so two sites on 192.168.1.0/24 can’t reach each other’s hosts. Renumbering one site is the clean fix. 1:1 NAT that maps the remote LAN onto an unused range works, but it complicates every firewall rule and log.
what port does wireguard site to site use
WireGuard uses one UDP port per interface, set with ListenPort. 51820 is the common convention, used in the pfSense and OPNsense guides; MikroTik’s example uses 13231. Per wg(8), an unset port is “chosen randomly”, which is fine on an initiator behind NAT but useless on the responder, whose port you must forward.