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)
NodePublic IPWireGuard IPHostname
Node A1.2.3.410.10.0.1/24node-a
Node B5.6.7.810.10.0.2/24node-b
Node C9.10.11.1210.10.0.3/24node-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.