Implement WireGuard multi-site mesh networking with automatic routing and failover

Advanced 45 min Apr 26, 2026 719 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Deploy a scalable WireGuard mesh network across multiple sites with automatic routing, failover mechanisms, and centralized management for high-availability site-to-site connectivity.

Prerequisites

  • Multiple servers with public IP addresses
  • Root access on all mesh nodes
  • Basic understanding of networking and routing
  • Firewall configuration knowledge

What this solves

Traditional site-to-site VPN setups create single points of failure with hub-and-spoke architectures. This tutorial implements a WireGuard mesh network where multiple sites can communicate directly with automatic routing and failover. You get encrypted inter-site connectivity that automatically routes around failed nodes, perfect for distributed infrastructure, branch offices, or multi-datacenter deployments.

Step-by-step installation

Install WireGuard on all nodes

Install WireGuard on each server that will participate in the mesh network. These will be your site gateway nodes.

sudo apt update
sudo apt install -y wireguard iptables-persistent bird2
sudo dnf update -y
sudo dnf install -y wireguard-tools iptables-services bird

Enable IP forwarding system-wide

Configure each node to forward packets between network interfaces for mesh routing.

net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
net.core.netdev_max_backlog = 2000
net.ipv4.tcp_congestion_control = bbr
sudo sysctl --system

Generate key pairs for each site

Create unique cryptographic keys for each mesh node. Store these securely as they authenticate your sites.

sudo mkdir -p /etc/wireguard/keys
sudo chmod 700 /etc/wireguard/keys
sudo wg genkey | sudo tee /etc/wireguard/keys/private.key | wg pubkey | sudo tee /etc/wireguard/keys/public.key
sudo chmod 600 /etc/wireguard/keys/private.key
sudo chmod 644 /etc/wireguard/keys/public.key

Plan your mesh topology and IP allocation

Design your network addressing scheme. Use a dedicated subnet for the mesh overlay and plan your site networks.

# Mesh Network Plan
# Overlay Network: 10.100.0.0/16
# Site A (London): Gateway 10.100.1.1/24, LAN 192.168.1.0/24
# Site B (Berlin): Gateway 10.100.2.1/24, LAN 192.168.2.0/24
# Site C (Paris): Gateway 10.100.3.1/24, LAN 192.168.3.0/24
</code><h2><code>Site D (Madrid): Gateway 10.100.4.1/24, LAN 192.168.4.0/24</code></h2></pre>
</div>

<div class="step">
### Configure WireGuard mesh interface for Site A
<p>Create the WireGuard configuration for the first site. This node will connect to all other sites directly.</p>
<pre class="terminal"><code>[Interface]
PrivateKey = $(sudo cat /etc/wireguard/keys/private.key)
Address = 10.100.1.1/24
ListenPort = 51820
PostUp = iptables -t nat -A POSTROUTING -s 10.100.0.0/16 -o eth0 -j MASQUERADE
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -s 10.100.0.0/16 -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -o wg0 -j ACCEPT

# Site B (Berlin)
[Peer]
PublicKey = SITE_B_PUBLIC_KEY_HERE
Endpoint = 203.0.113.20:51820
AllowedIPs = 10.100.2.0/24, 192.168.2.0/24
PersistentKeepalive = 25

# Site C (Paris)
[Peer]
PublicKey = SITE_C_PUBLIC_KEY_HERE
Endpoint = 203.0.113.30:51820
AllowedIPs = 10.100.3.0/24, 192.168.3.0/24
PersistentKeepalive = 25

# Site D (Madrid)
[Peer]
PublicKey = SITE_D_PUBLIC_KEY_HERE
Endpoint = 203.0.113.40:51820
AllowedIPs = 10.100.4.0/24, 192.168.4.0/24
PersistentKeepalive = 25

Configure dynamic routing with BIRD

Set up BIRD routing daemon for automatic route discovery and failover between mesh nodes.

log syslog all;
router id 10.100.1.1;

protocol kernel {
    learn;
    persist;
    scan time 20;
    import all;
    export all;
}

protocol device {
    scan time 10;
}

protocol direct {
    interface "wg0", "eth0";
}

protocol ospf v2 mesh {
    tick 2;
    rfc1583compat yes;
    
    area 0.0.0.0 {
        interface "wg0" {
            cost 10;
            hello 5;
            dead 20;
            type pointopoint;
        };
        
        interface "eth0" {
            cost 100;
            hello 10;
            dead 40;
            stub yes;
        };
    };
}

Create mesh management scripts

Build automation scripts for mesh operations, health monitoring, and failover detection.

#!/bin/bash

# WireGuard Mesh Health Monitor

MESH_CONFIG="/etc/wireguard/wg0.conf"
LOG_FILE="/var/log/wireguard-mesh.log"
EMAIL_ALERT="admin@example.com"

log_message() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE"
}

check_peer_connectivity() {
    local peer_ip="$1"
    local peer_name="$2"
    
    if ping -c 3 -W 2 "$peer_ip" >/dev/null 2>&1; then
        log_message "HEALTHY: $peer_name ($peer_ip) is reachable"
        return 0
    else
        log_message "ALERT: $peer_name ($peer_ip) is unreachable"
        return 1
    fi
}

check_routing_table() {
    local expected_routes=$(grep -c "AllowedIPs" "$MESH_CONFIG")
    local active_routes=$(ip route | grep -c "dev wg0")
    
    if [ "$active_routes" -ge "$expected_routes" ]; then
        log_message "ROUTING: $active_routes routes active (expected >= $expected_routes)"
    else
        log_message "ALERT: Only $active_routes routes active (expected >= $expected_routes)"
        systemctl restart bird
    fi
}

# Monitor peer connectivity
check_peer_connectivity "10.100.2.1" "Berlin"
check_peer_connectivity "10.100.3.1" "Paris" 
check_peer_connectivity "10.100.4.1" "Madrid"

# Verify routing
check_routing_table

# Check WireGuard interface status
if ! wg show wg0 >/dev/null 2>&1; then
    log_message "ALERT: WireGuard interface wg0 is down"
    systemctl restart wg-quick@wg0
fi
sudo chmod 755 /usr/local/bin/wg-mesh-monitor.sh

Configure automatic failover script

Create a failover mechanism that reroutes traffic when primary paths fail.

#!/bin/bash

# WireGuard Mesh Failover Handler

BIRD_SOCKET="/var/run/bird/bird.ctl"
LOG_FILE="/var/log/wireguard-mesh.log"

log_message() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') FAILOVER: $1" >> "$LOG_FILE"
}

trigger_route_update() {
    local failed_peer="$1"
    log_message "Updating routes due to $failed_peer failure"
    
    # Recalculate OSPF routes
    echo "configure\nprotocol ospf mesh { disabled; }\nprotocol ospf mesh { disabled no; }\nexit" | birdc
    
    # Wait for convergence
    sleep 10
    
    # Verify new routes
    ip route show table main | grep "dev wg0" | while read route; do
        log_message "Active route: $route"
    done
}

handle_peer_failure() {
    local peer_ip="$1"
    local peer_name="$2"
    
    # Check if peer is consistently down
    local failures=0
    for i in {1..3}; do
        if ! ping -c 1 -W 2 "$peer_ip" >/dev/null 2>&1; then
            ((failures++))
        fi
        sleep 2
    done
    
    if [ "$failures" -eq 3 ]; then
        log_message "Peer $peer_name ($peer_ip) confirmed down, initiating failover"
        trigger_route_update "$peer_name"
        
        # Send alert
        echo "WireGuard mesh peer $peer_name failed. Automatic failover activated." | 
            mail -s "Mesh Network Alert: $peer_name Down" admin@example.com
    fi
}

# Check each peer and trigger failover if needed
handle_peer_failure "10.100.2.1" "Berlin"
handle_peer_failure "10.100.3.1" "Paris"
handle_peer_failure "10.100.4.1" "Madrid"
sudo chmod 755 /usr/local/bin/wg-mesh-failover.sh

Set up automated monitoring with systemd timers

Configure regular health checks and automatic failover detection using systemd timers.

[Unit]
Description=WireGuard Mesh Network Monitor
After=wg-quick@wg0.service bird.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/wg-mesh-monitor.sh
User=root
StandardOutput=journal
StandardError=journal
[Unit]
Description=Run WireGuard Mesh Monitor every 2 minutes
Requires=wg-mesh-monitor.service

[Timer]
OnCalendar=*:*:00/120
Persistent=true

[Install]
WantedBy=timers.target
[Unit]
Description=WireGuard Mesh Failover Handler
After=wg-quick@wg0.service bird.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/wg-mesh-failover.sh
User=root
StandardOutput=journal
StandardError=journal
[Unit]
Description=Run WireGuard Mesh Failover Check every 5 minutes
Requires=wg-mesh-failover.service

[Timer]
OnCalendar=*:*:00/300
Persistent=true

[Install]
WantedBy=timers.target

Configure firewall rules for mesh traffic

Open the necessary ports and configure packet filtering for secure mesh communication.

# Allow WireGuard traffic
sudo iptables -I INPUT -p udp --dport 51820 -j ACCEPT

# Allow mesh subnet traffic
sudo iptables -I INPUT -s 10.100.0.0/16 -j ACCEPT
sudo iptables -I FORWARD -s 10.100.0.0/16 -j ACCEPT
sudo iptables -I FORWARD -d 10.100.0.0/16 -j ACCEPT

# Allow OSPF protocol
sudo iptables -I INPUT -p ospf -j ACCEPT
sudo iptables -I OUTPUT -p ospf -j ACCEPT

</code><h2><code>Save rules</code></h2></pre>
<div class="distro-tabs">
<div class="tab-bar">


</div>
<div>
<pre class="terminal"><code>sudo netfilter-persistent save
sudo service iptables save

Start and enable all services

Activate the WireGuard interface, routing daemon, and monitoring services.

# Enable WireGuard interface
sudo systemctl enable --now wg-quick@wg0

# Enable BIRD routing daemon
sudo systemctl enable --now bird

# Enable monitoring timers
sudo systemctl enable --now wg-mesh-monitor.timer
sudo systemctl enable --now wg-mesh-failover.timer

# Check service status
sudo systemctl status wg-quick@wg0 bird wg-mesh-monitor.timer

Replicate configuration for remaining sites

Deploy similar configurations to Sites B, C, and D with appropriate IP addresses and peer configurations.

[Interface]
PrivateKey = SITE_B_PRIVATE_KEY
Address = 10.100.2.1/24
ListenPort = 51820
PostUp = iptables -t nat -A POSTROUTING -s 10.100.0.0/16 -o eth0 -j MASQUERADE
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -s 10.100.0.0/16 -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -o wg0 -j ACCEPT

# Site A (London)
[Peer]
PublicKey = SITE_A_PUBLIC_KEY
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.100.1.0/24, 192.168.1.0/24
PersistentKeepalive = 25

# Site C (Paris)
[Peer]
PublicKey = SITE_C_PUBLIC_KEY
Endpoint = 203.0.113.30:51820
AllowedIPs = 10.100.3.0/24, 192.168.3.0/24
PersistentKeepalive = 25

# Site D (Madrid)
[Peer]
PublicKey = SITE_D_PUBLIC_KEY
Endpoint = 203.0.113.40:51820
AllowedIPs = 10.100.4.0/24, 192.168.4.0/24
PersistentKeepalive = 25
Note: Update the router ID in /etc/bird/bird.conf to match each site's gateway IP (10.100.2.1 for Site B, 10.100.3.1 for Site C, etc.)

Verify your setup

Test mesh connectivity and verify automatic routing between all sites.

# Check WireGuard interface status
sudo wg show wg0

# Verify all peers are connected
sudo wg show wg0 allowed-ips

# Test connectivity to all mesh nodes
ping -c 3 10.100.2.1  # Site B
ping -c 3 10.100.3.1  # Site C
ping -c 3 10.100.4.1  # Site D

# Check OSPF routing table
sudo birdc show route

# Verify site-to-site connectivity
ping -c 3 192.168.2.10  # Test host at Site B
ping -c 3 192.168.3.10  # Test host at Site C

# Check monitoring logs
sudo journalctl -u wg-mesh-monitor.service --since "1 hour ago"
tail -f /var/log/wireguard-mesh.log

# Test failover (disable one peer temporarily)
sudo wg-quick down wg0
sudo wg-quick up wg0

Site-to-site connectivity testing

Perform comprehensive testing to verify mesh functionality and failover behavior.

#!/bin/bash

# Comprehensive mesh connectivity test

SITES=("10.100.1.1:London" "10.100.2.1:Berlin" "10.100.3.1:Paris" "10.100.4.1:Madrid")
LAN_SUBNETS=("192.168.1.0/24" "192.168.2.0/24" "192.168.3.0/24" "192.168.4.0/24")

echo "=== WireGuard Mesh Connectivity Test ==="
echo "Started: $(date)"
echo

# Test mesh node connectivity
echo "Testing mesh node connectivity:"
for site in "${SITES[@]}"; do
    ip="${site%%:*}"
    name="${site##*:}"
    
    if ping -c 3 -W 2 "$ip" >/dev/null 2>&1; then
        echo "✓ $name ($ip) - REACHABLE"
    else
        echo "✗ $name ($ip) - UNREACHABLE"
    fi
done
echo

# Test cross-site routing
echo "Testing cross-site LAN routing:"
for subnet in "${LAN_SUBNETS[@]}"; do
    gateway="${subnet%.*}.1"
    if ping -c 2 -W 2 "$gateway" >/dev/null 2>&1; then
        echo "✓ $subnet via $gateway - ROUTABLE"
    else
        echo "✗ $subnet via $gateway - NOT ROUTABLE"
    fi
done
echo

# Check routing table
echo "Active WireGuard routes:"
ip route | grep "dev wg0" | while read route; do
    echo "  $route"
done
echo

# Test bandwidth between sites
echo "Network performance test (if iperf3 available):"
if command -v iperf3 >/dev/null 2>&1; then
    for site in "${SITES[@]}"; do
        ip="${site%%:*}"
        name="${site##*:}"
        
        if [ "$ip" != "$(hostname -I | awk '{print $1}')" ]; then
            echo "Testing bandwidth to $name ($ip):"
            timeout 10 iperf3 -c "$ip" -t 5 2>/dev/null || echo "  Bandwidth test failed or iperf3 server not running"
        fi
    done
else
    echo "  iperf3 not installed, skipping bandwidth tests"
fi

echo "Test completed: $(date)"
sudo chmod 755 /usr/local/bin/mesh-connectivity-test.sh
sudo /usr/local/bin/mesh-connectivity-test.sh

Common issues

SymptomCauseFix
Peers show as connected but can't pingFirewall blocking mesh trafficsudo iptables -I INPUT -s 10.100.0.0/16 -j ACCEPT
Some sites unreachable after failoverBIRD routing convergence delayCheck sudo birdc show route and wait 30 seconds
Handshake fails between peersIncorrect public keys in configVerify public keys with sudo wg show wg0 peers
Monitoring scripts not runningSystemd timers not enabledsudo systemctl enable --now wg-mesh-monitor.timer
High CPU usage from BIRDToo frequent OSPF hello intervalsIncrease hello timer to 10 seconds in BIRD config
NAT not working for site LANsMissing MASQUERADE rulessudo iptables -t nat -A POSTROUTING -s 10.100.0.0/16 -o eth0 -j MASQUERADE

Monitoring and maintenance

Set up comprehensive monitoring for your mesh network with automated alerts and performance tracking.

#!/bin/bash

# Generate mesh network health report

REPORT_FILE="/tmp/mesh-health-$(date +%Y%m%d-%H%M).txt"

echo "WireGuard Mesh Network Health Report" > "$REPORT_FILE"
echo "Generated: $(date)" >> "$REPORT_FILE"
echo "====================================" >> "$REPORT_FILE"
echo >> "$REPORT_FILE"

# Interface status
echo "WireGuard Interface Status:" >> "$REPORT_FILE"
sudo wg show wg0 >> "$REPORT_FILE"
echo >> "$REPORT_FILE"

# Peer statistics
echo "Peer Connection Statistics:" >> "$REPORT_FILE"
sudo wg show wg0 transfer >> "$REPORT_FILE"
echo >> "$REPORT_FILE"

# Routing table
echo "Active Routes:" >> "$REPORT_FILE"
ip route | grep "dev wg0" >> "$REPORT_FILE"
echo >> "$REPORT_FILE"

# BIRD protocol status
echo "OSPF Protocol Status:" >> "$REPORT_FILE"
sudo birdc show protocols >> "$REPORT_FILE"
echo >> "$REPORT_FILE"

# System resources
echo "System Resource Usage:" >> "$REPORT_FILE"
echo "CPU: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | sed 's/%us,//')" >> "$REPORT_FILE"
echo "Memory: $(free -m | awk 'NR==2{printf "%.1f%%", $3*100/$2}')" >> "$REPORT_FILE"
echo "Disk: $(df -h / | awk 'NR==2{print $5}')" >> "$REPORT_FILE"
echo >> "$REPORT_FILE"

# Recent log entries
echo "Recent Mesh Events:" >> "$REPORT_FILE"
tail -20 /var/log/wireguard-mesh.log >> "$REPORT_FILE"

echo "Report saved to: $REPORT_FILE"

# Email report if configured
if command -v mail >/dev/null 2>&1; then
    mail -s "WireGuard Mesh Health Report - $(hostname)" admin@example.com < "$REPORT_FILE"
fi
sudo chmod 755 /usr/local/bin/mesh-health-report.sh

# Run weekly health reports
echo "0 8 * * 1 /usr/local/bin/mesh-health-report.sh" | sudo crontab -
Pro tip: Monitor WireGuard VPN server with Prometheus and Grafana dashboards for advanced metrics collection and alerting integration.

Next steps

Automated install script

Run this to automate the entire setup

Sie möchten das nicht selbst verwalten?

Wir betreiben Infrastruktur für Unternehmen, die auf Verfügbarkeit angewiesen sind. Vollständig verwaltet, mit einem festen Ansprechpartner, der Ihre Umgebung kennt.

Sie erhalten einen festen Ansprechpartner, der Ihr Setup kennt

Rotterdam 12:57 · erreichbar per Nachricht, kein Ticketformular