WireGuard Site-to-Site VPN: wg0.conf Configuration Example - 夜莺博客

WireGuard Site-to-Site VPN: wg0.conf Configuration Example

WireGuard has become the default answer for site-to-site VPNs between Linux gateways because a complete tunnel configuration fits in a few lines and the crypto is modern (Curve25519, ChaCha20-Poly1305). This tutorial walks through a real site-to-site WireGuard VPN between two networks, covering key generation, the wg0.conf format, AllowedIPs routing semantics, and the operational details (MTU, keepalives, NAT) that decide whether the tunnel actually survives in production.

How Site-to-Site WireGuard Works

Each site has a gateway with a public IP. The gateways form a UDP tunnel (default port 51820), and each LAN routes to the other LAN's subnet through the tunnel interface. Unlike IPsec, there are no phases, no SA lifetimes and no policy databases - every peer simply lists which IP ranges it is allowed to claim (AllowedIPs), which doubles as the routing table entry for the tunnel.

That last point is the key mental model shift. In IPsec you negotiate a tunnel and then separately define which traffic is interesting; in WireGuard the peer's AllowedIPs is the route, the selector and the anti-spoofing filter all at once. Two consequences follow immediately:

  • Routing is declarative. wg-quick reads the configuration and installs a route for each prefix in AllowedIPs pointing at the tunnel interface. No ip route add needed for the tunnel itself.
  • AllowedIPs is also a filter. A packet arriving on the tunnel whose source address is outside the peer's AllowedIPs is dropped. This is what makes the design safe by default: a peer cannot claim addresses it was not given.

WireGuard is also silent - it only sends traffic when there is traffic to send. There is no handshake until a packet needs the tunnel, which is why PersistentKeepalive exists and why the first ping after a quiet period may take an extra moment.

Step 1: Generate Keys on Each Gateway

Keys are generated on the gateway itself and the private key never leaves that machine:

sudo apt install wireguard
wg genkey | sudo tee /etc/wireguard/wgA.key
sudo cat /etc/wireguard/wgA.key | wg pubkey | sudo tee /etc/wireguard/wgA.pub
sudo chmod 600 /etc/wireguard/wgA.key

Only the .pub file is exchanged with the remote site. If a private key is ever copied across the network or appears in shell history, treat it as compromised and regenerate.

A cleaner pattern for anything beyond a scratch lab is to generate keys with correct permissions in one step and derive the public key without re-reading the private file into a pipeline that could log it:

cd /etc/wireguard
umask 077
wg genkey | tee wgA.key | wg pubkey > wgA.pub
chmod 600 wgA.key
cat wgA.pub          # this is the only value you share with the peer
rm -f ~/.bash_history 2>/dev/null || true   # if you typed the key at a prompt

Two operational habits are worth adopting from the start. First, keep the private key in a file that the VPN configuration references (PrivateKey = /etc/wireguard/wgA.key style indirection, as shown below with PostUp) rather than pasting it into a config that might get committed to Git. Second, generate a separate key pair per peer - never reuse a key across sites, because revoking one peer then means rebuilding every tunnel that shared it.

Step 2: Write wg0.conf on Both Gateways

Site A (10.10.10.0/24) and Site B (10.10.11.0/24) share the /31 tunnel network 10.10.9.0/31. On gateway A:

[Interface]
Address = 10.10.9.0/31
ListenPort = 51000
PostUp = wg set %i private-key /etc/wireguard/%i.key
MTU = 1420

[Peer]          # beta site
PublicKey = <contents of /etc/wireguard/wgB.pub>
AllowedIPs = 10.10.11.0/24, 10.10.9.0/31
Endpoint = <beta-gw-public-ip>:51000
PersistentKeepalive = 25

On gateway B the roles are reversed (Address = 10.10.9.1/31, peer AllowedIPs = 10.10.10.0/24, 10.10.9.0/31, Endpoint = alpha's public IP). Two details matter for site-to-site links:

  • MTU 1420: WireGuard adds roughly 60 bytes of overhead (IPv4 + UDP + WireGuard headers). On a 1500-byte path, 1420 keeps large packets from fragmenting or being silently dropped. On PPPoE links (1492 outer MTU) use 1432.
  • PersistentKeepalive 25: sends a keepalive every 25 s so NAT/firewall mappings on the path stay open. Required when either endpoint is behind NAT; harmless otherwise.

A few further details in that configuration repay attention:

  • Why a /31? A point-to-point tunnel needs exactly two addresses, and RFC 3021 allows a /31 with no network or broadcast address wasted. Using a /30 is also fine; the /31 simply avoids two unused addresses per tunnel.
  • Including the tunnel subnet in AllowedIPs. Listing 10.10.9.0/31 alongside the remote LAN means packets addressed to the peer's tunnel IP also route into the tunnel - useful for troubleshooting with pings to the far gateway.
  • ListenPort on both sides. Setting an explicit port makes firewall rules deterministic. If you leave it unset WireGuard picks a random high port, which is fine for roaming clients but awkward for a site-to-site link behind a static policy.
  • The PostUp private-key line. Pointing the config at a key file rather than embedding the key keeps the tunnel definition safe to store in version control.

Step 3: Enable Routing and Start the Tunnel

sudo sysctl -w net.ipv4.ip_forward=1        # persist in /etc/sysctl.conf
sudo systemctl enable --now wg-quick@wg0
sudo wg show                                 # handshake + transfer counters

If LAN clients must reach the remote LAN through the gateway, add forwarding plus NAT rules on each gateway (PostUp iptables lines are shown in the related example configurations). Verify with wg show (look for a recent latest handshake) and a ping from a LAN client to the far LAN gateway IP.

The forwarding and NAT rules are where most site-to-site builds are decided, so they are worth writing out explicitly:

# allow forwarding between the LAN and the tunnel
PostUp   = iptables -A FORWARD -i %i -j ACCEPT
PostUp   = iptables -A FORWARD -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -j ACCEPT
PostDown = iptables -D FORWARD -o %i -j ACCEPT

# if the peer is expected to reach THIS LAN and there is no return route,
# masquerade the tunnel source so replies find their way home
PostUp   = iptables -t nat -A POSTROUTING -o %i -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -o %i -j MASQUERADE

Prefer routing over masquerading when you can. If you add a static route for the remote prefix on the LAN's router - pointing at the gateway's LAN address - then hosts on both sides see real source addresses, your firewall logs stay meaningful, and per-host policy still works. Masquerading hides which host originated the traffic, which is convenient but costs you visibility; keep it as the fallback for cases where you cannot touch the remote routing table. For firewall-centric designs where the tunnel terminates on a security device rather than a Linux host, compare this model with route-based IPsec on the Juniper SRX.

Persistence matters too. sysctl -w is not persistent by itself; put net.ipv4.ip_forward=1 in /etc/sysctl.conf (or a file under /etc/sysctl.d/) so the setting survives a reboot. If the gateway reboots and forwarding is off, the tunnel comes up and passes nothing - a failure that looks like a routing bug but is a one-line configuration omission.

MTU, Perimeter and Fragmentation in Practice

MTU is the most common cause of "the tunnel works for pings but large transfers stall". WireGuard encapsulates an inner IP packet inside UDP inside the outer IP packet, so the usable payload shrinks by the size of the WireGuard header (32 bytes of message header plus 16 bytes of authentication tag), the UDP header (8 bytes) and the outer IP header (20 bytes) - roughly 76 bytes in the IPv4 case, and more when the outer transport includes extra encapsulation.

The correct MTU is therefore outer path MTU minus overhead, rounded down to a safe value:

Outer path Outer MTU Recommended wg MTU
Ethernet, no extra encapsulation 1500 1420
PPPoE DSL 1492 1412-1432
Outer IPsec or GRE 1500 minus outer overhead 1360 or lower

Do not guess - measure. A ping with the DF bit set finds the ceiling empirically: the largest size that returns without fragmentation error is the inner path MTU, and the tunnel MTU is that value plus the outer headers already accounted for. Test from both ends, because asymmetric paths (a mobile link on one side) can have different ceilings.

The failure mode to watch for is silent: when a packet exceeds MTU and ICMP "fragmentation needed" is filtered somewhere, the connection hangs rather than failing cleanly - small requests work, large responses never arrive. Setting the tunnel MTU conservatively on both ends is the durable fix; see MTU, MSS and PMTUD troubleshooting for the clamping techniques that help when you cannot change every end host, and WireGuard MTU and handshake failures for the narrower case of a tunnel that will not come up at all.

Verification and Troubleshooting

  • sudo wg show - peer endpoint, allowed IPs, latest handshake, RX/TX bytes. No recent handshake means the tunnel never established; check UDP 51820 reachability and firewall rules.
  • sudo wg show wg0 fwmark - the routing mark used by wg-quick.
  • AllowedIPs mismatch is the #1 routing bug: the remote LAN prefix must appear in the local peer's AllowedIPs list, otherwise wg-quick never installs the route and packets go out the default gateway.
  • If traffic passes but large transfers stall, re-check MTU on both ends (ping with DF bit and 1400/1420-byte payloads to find the ceiling).

When the tunnel is down, walk the layers in order rather than changing things randomly:

# 1. interface state - is wg0 up and does it have the address?
ip -br addr show wg0

# 2. configuration - do both sides list each other correctly?
sudo wg show

# 3. handshake - has the tunnel ever completed?
sudo wg show wg0 latest-handshakes

# 4. transport - is the UDP endpoint reachable at all?
sudo tcpdump -ni any udp port 51000 -c 20

# 5. routing - does the remote prefix point at the tunnel?
ip route get 10.10.11.10

# 6. forwarding and firewall - are there counters incrementing?
sudo iptables -vnL FORWARD | head

Each command eliminates one layer. An empty handshake with packets leaving but none arriving points at a firewall or NAT problem on the path; a handshake that exists but no traffic flowing points at routing or forwarding; traffic flowing and transfers stalling points at MTU. Because WireGuard is stateless from the operator's perspective, there is no session to inspect - the counters in sudo wg show and a tcpdump on the UDP port tell you nearly everything.

Three smaller habits prevent recurring tickets. Give the tunnel a fixed listen port and a matching firewall exception on both sides so the rules can be reviewed rather than inferred. Document which side owns the persistent tunnel with PersistentKeepalive - in a hub-and-spoke design the spokes keep it alive so the hub does not need to know every spoke's address. And record the tunnel subnet in your IP address management system alongside the two LANs, because a /31 that only appears in a config file is a /31 that gets reused by accident.

WireGuard is not the only tunnel option for connecting sites - compare it with route-based IPsec on the Juniper SRX for firewall-centric designs or GRE tunnels on Cisco IOS when you need multicast or a simpler L3 overlay, and see our SSH tunneling guide for quick single-service tunnels that need zero new software. If you want to reproduce the whole topology before touching production hardware, both gateways and the LAN behind them can be built as containers following Linux network namespaces and veth pairs.

原文链接:https://ubuntu.com/server/docs/how-to/wireguard-vpn/site-to-site/