WireGuard Mesh Network Configuration
WireGuard is a modern VPN protocol that builds fast, cryptographically secure tunnels with minimal configuration. This guide covers building a full mesh network connecting multiple Linux servers using WireGuard, including peer configuration, routing tables, DNS resolution across sites, and auto-configuration scripts.
Prerequisites
- Ubuntu 20.04/22.04 or CentOS/Rocky Linux 8+ on each node
- Root or sudo access
- Each server must have a public IP or be reachable from the other nodes
- UDP port 51820 open on all firewalls
Install WireGuard
# Ubuntu 20.04+
sudo apt update && sudo apt install -y wireguard
# CentOS/Rocky Linux 8+
sudo dnf install -y epel-release
sudo dnf install -y wireguard-tools
# Verify installation
wg --version
Generate Key Pairs
Each node needs its own public/private key pair:
# Generate keys for each node (run on each server)
wg genkey | sudo tee /etc/wireguard/private.key
sudo chmod 600 /etc/wireguard/private.key
# Derive the public key from the private key
sudo cat /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key
# Print keys for configuration
echo "Private key: $(sudo cat /etc/wireguard/private.key)"
echo "Public key: $(sudo cat /etc/wireguard/public.key)"
Collect the public key from each node — you'll need them all for the mesh configuration.
Mesh Network Design
A mesh topology connects every node directly to every other node:
Node A (10.10.0.1) ─────── Node B (10.10.0.2)
│ │
└──────── Node C (10.10.0.3)
| Node | Public IP | WireGuard IP | Hostname |
|---|---|---|---|
| Node A | 1.2.3.4 | 10.10.0.1/24 | node-a |
| Node B | 5.6.7.8 | 10.10.0.2/24 | node-b |
| Node C | 9.10.11.12 | 10.10.0.3/24 | node-c |
Configure Each Node
Node A configuration:
# /etc/wireguard/wg0.conf on Node A
sudo tee /etc/wireguard/wg0.conf > /dev/null <<'EOF'
[Interface]
PrivateKey = <NODE_A_PRIVATE_KEY>
Address = 10.10.0.1/24
ListenPort = 51820
# Enable IP forwarding for routing
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
# Node B
[Peer]
PublicKey = <NODE_B_PUBLIC_KEY>
Endpoint = 5.6.7.8:51820
AllowedIPs = 10.10.0.2/32
PersistentKeepalive = 25
# Node C
[Peer]
PublicKey = <NODE_C_PUBLIC_KEY>
Endpoint = 9.10.11.12:51820
AllowedIPs = 10.10.0.3/32
PersistentKeepalive = 25
EOF
sudo chmod 600 /etc/wireguard/wg0.conf
Node B configuration:
sudo tee /etc/wireguard/wg0.conf > /dev/null <<'EOF'
[Interface]
PrivateKey = <NODE_B_PRIVATE_KEY>
Address = 10.10.0.2/24
ListenPort = 51820
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
# Node A
[Peer]
PublicKey = <NODE_A_PUBLIC_KEY>
Endpoint = 1.2.3.4:51820
AllowedIPs = 10.10.0.1/32
PersistentKeepalive = 25
# Node C
[Peer]
PublicKey = <NODE_C_PUBLIC_KEY>
Endpoint = 9.10.11.12:51820
AllowedIPs = 10.10.0.3/32
PersistentKeepalive = 25
EOF
Node C configuration:
sudo tee /etc/wireguard/wg0.conf > /dev/null <<'EOF'
[Interface]
PrivateKey = <NODE_C_PRIVATE_KEY>
Address = 10.10.0.3/24
ListenPort = 51820
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
# Node A
[Peer]
PublicKey = <NODE_A_PUBLIC_KEY>
Endpoint = 1.2.3.4:51820
AllowedIPs = 10.10.0.1/32
PersistentKeepalive = 25
# Node B
[Peer]
PublicKey = <NODE_B_PUBLIC_KEY>
Endpoint = 5.6.7.8:51820
AllowedIPs = 10.10.0.2/32
PersistentKeepalive = 25
EOF
Start WireGuard on all nodes:
# Enable and start wg0 interface
sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
# Verify the interface is up
sudo wg show wg0
ip addr show wg0
# Test connectivity
ping 10.10.0.2 # From Node A to Node B
ping 10.10.0.3 # From Node A to Node C
Routing Between Sites
To route private subnets behind each node through the mesh:
# Each node has a private LAN subnet:
# Node A: 192.168.1.0/24
# Node B: 192.168.2.0/24
# Node C: 192.168.3.0/24
# On Node A, add the remote subnets to AllowedIPs for each peer
# Edit /etc/wireguard/wg0.conf:
[Peer] # Node B
PublicKey = <NODE_B_PUBLIC_KEY>
Endpoint = 5.6.7.8:51820
AllowedIPs = 10.10.0.2/32,192.168.2.0/24 # WireGuard IP + LAN subnet
[Peer] # Node C
PublicKey = <NODE_C_PUBLIC_KEY>
Endpoint = 9.10.11.12:51820
AllowedIPs = 10.10.0.3/32,192.168.3.0/24
# Apply changes without restarting
sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
# Add static routes (if not using AllowedIPs for this)
sudo ip route add 192.168.2.0/24 via 10.10.0.2 dev wg0
sudo ip route add 192.168.3.0/24 via 10.10.0.3 dev wg0
# Make routing persistent (Ubuntu with Netplan):
# Add PostUp commands to wg0.conf:
PostUp = ip route add 192.168.2.0/24 via 10.10.0.2 dev wg0
PostUp = ip route add 192.168.3.0/24 via 10.10.0.3 dev wg0
PostDown = ip route del 192.168.2.0/24
PostDown = ip route del 192.168.3.0/24
DNS Resolution Across the Mesh
Configure a split-horizon DNS so hostnames resolve across the mesh:
# On each node, add mesh node hostnames to /etc/hosts
sudo tee -a /etc/hosts > /dev/null <<'EOF'
10.10.0.1 node-a node-a.mesh.internal
10.10.0.2 node-b node-b.mesh.internal
10.10.0.3 node-c node-c.mesh.internal
EOF
# Test resolution
ping node-b
ping node-c.mesh.internal
For a proper DNS setup using dnsmasq:
# Install dnsmasq on Node A (DNS server for the mesh)
sudo apt install -y dnsmasq
sudo tee /etc/dnsmasq.d/wireguard-mesh.conf > /dev/null <<'EOF'
# Only listen on the WireGuard interface
interface=wg0
bind-interfaces
# Serve local hostnames for mesh nodes
address=/node-a.mesh.internal/10.10.0.1
address=/node-b.mesh.internal/10.10.0.2
address=/node-c.mesh.internal/10.10.0.3
EOF
sudo systemctl restart dnsmasq
# On other nodes, use Node A as DNS
# Edit /etc/systemd/resolved.conf or /etc/resolv.conf:
echo "nameserver 10.10.0.1" | sudo tee /etc/resolv.conf
Auto-Configuration Script
#!/bin/bash
# generate-wg-config.sh
# Usage: ./generate-wg-config.sh <node-name> <wg-ip> <public-ip>
NODE_NAME=$1
WG_IP=$2
PUBLIC_IP=$3
WG_PORT=51820
# Generate keys
PRIVATE_KEY=$(wg genkey)
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | wg pubkey)
echo "=== Node: $NODE_NAME ==="
echo "Public Key: $PUBLIC_KEY"
echo ""
mkdir -p /etc/wireguard
cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
PrivateKey = $PRIVATE_KEY
Address = $WG_IP/24
ListenPort = $WG_PORT
PostUp = sysctl -w net.ipv4.ip_forward=1
EOF
echo "Config written to /etc/wireguard/wg0.conf"
echo "Add the following peer block to all other nodes:"
echo ""
echo "[Peer]"
echo "# $NODE_NAME"
echo "PublicKey = $PUBLIC_KEY"
echo "Endpoint = $PUBLIC_IP:$WG_PORT"
echo "AllowedIPs = $WG_IP/32"
echo "PersistentKeepalive = 25"
Monitoring the Mesh
# Show all peer status
sudo wg show
# Show latest handshake times for each peer
sudo wg show wg0 latest-handshakes
# Show data transfer per peer
sudo wg show wg0 transfer
# Monitor in real time
watch -n 5 sudo wg show
# Check interface statistics
ip -s link show wg0
# Test latency across the mesh
ping -c 10 10.10.0.2
ping -c 10 10.10.0.3
Troubleshooting
Peer shows no handshake:
# Check WireGuard is listening
sudo ss -ulnp | grep 51820
# Verify the public endpoint is correct
sudo wg show wg0 endpoints
# Test UDP connectivity to the peer
nc -uz 5.6.7.8 51820 && echo "UDP reachable"
# Verify firewall allows UDP 51820
sudo ufw status | grep 51820
sudo iptables -L INPUT -n | grep 51820
Handshake succeeds but no ping:
# Check that AllowedIPs covers the destination
sudo wg show wg0 allowed-ips
# Verify IP forwarding is enabled
sysctl net.ipv4.ip_forward
# Check routing table
ip route show table main | grep 10.10.0
Connection drops periodically:
# PersistentKeepalive = 25 should prevent this
# Verify it's set:
sudo wg show wg0 | grep keepalive
# Check for NAT timeout issues (keepalive should be < 25s for most NATs)
Conclusion
WireGuard mesh networks provide secure, high-performance connectivity between servers with minimal configuration overhead. Each node maintains direct encrypted tunnels to every other node, eliminating hub-and-spoke bottlenecks. The AllowedIPs mechanism doubles as both a routing filter and an access control list, making WireGuard meshes simple to reason about and audit.


