The Problem with Manual WireGuard Management
Managing WireGuard peers manually can be error-prone. After accidentally deleting an active peer with my old management script, I needed something better - a tool that would never overwrite existing configurations and always create backups before making changes. This makes adding new devices to my personal network much easier. Also it is worth mentioning that this allows you to set the parameters for you specific devices in accordance to the use case without the need to do this manually.
For example DNS traffic does not need to be passed through a device that is only operating in split tunnel mode. My mobile device is used for various remote management tasks but DNS is handled externally to the WireGuard interface this prevents issues like not being able to access the wider internet because 2 DNS servers are being called within the same network at the same time.
There can be only one primary DNS, for a more detailed explanation please review the sections below. I cannot overstate how frustrating this issue can be when you cannot identify the problem.
> Without WireGuard (Normal):
Primary Interface (WiFi/Ethernet)
├─ IP: 192.168.1.100
├─ Gateway: 192.168.1.1
└─ DNS: 192.168.1.1 or ISP DNS
└─ All DNS queries → Primary interface DNS
> With WireGuard DNS Configured:
Primary Interface (WiFi/Ethernet) WireGuard Interface (wg0)
├─ IP: 192.168.1.100 ├─ IP: 10.0.0.5
├─ Gateway: 192.168.1.1 ├─ Routes: 10.0.0.0/24 only
└─ DNS: 192.168.1.1 └─ DNS: 1.1.1.1, 1.0.0.1
└─ Tries to become PRIMARY DNS
Why DNS Configuration Breaks Split-Tunnel Mode
The Intent of Split-Tunnel
In split-tunnel mode, only traffic destined for your internal network (10.0.0.0/24) should route through the WireGuard VPN. All other traffic - web browsing, email, general internet use - should continue using your device’s normal network connection. This is controlled by the AllowedIPs = 10.0.0.0/24 setting in the client configuration.
What the DNS Setting Does
When you include DNS = 1.1.1.1, 1.0.0.1 in the WireGuard interface configuration, you’re instructing the operating system to:
- Use these DNS servers for all DNS lookups when the VPN is active
- Route DNS queries (UDP port 53) to these servers
- Reconfigure the system’s DNS resolver to prioritize these servers
The Routing Conflict
Here’s where the problem occurs:
- DNS servers are outside your tunnel range: 1.1.1.1 and 1.0.0.1 are public Cloudflare DNS servers, not part of 10.0.0.0/24
- Split-tunnel only routes 10.0.0.0/24: Your routing table says “only 10.0.0.x traffic goes through WireGuard”
- DNS queries need to reach 1.1.1.1: But 1.1.1.1 isn’t in the 10.0.0.0/24 range
- OS gets confused: The system tries to send DNS queries to servers it can’t route to through the tunnel
What Actually Happens
Different operating systems handle this conflict differently:
- Windows/macOS: May ignore the split-tunnel
AllowedIPsand start routing additional traffic through the VPN to reach the DNS servers, defeating the purpose of split-tunnel - Linux: May fail DNS resolution entirely because it can’t route to the specified DNS servers through the tunnel
- iOS/Android: Often try to use the VPN DNS for all queries, causing intermittent failures when those queries can’t complete
- All platforms: DNS lookups become unreliable - sometimes working (when using the VPN DNS), sometimes failing (when routing conflicts occur)
The Cascading Effect
Even though you only want to access 10.0.0.50:3389 (your RDP server), here’s what happens:
- You type a website like
google.comin your browser - Your device needs to resolve
google.comto an IP address - It tries to use 1.1.1.1 (the VPN DNS)
- Routing problem: Can’t reach 1.1.1.1 through the tunnel (not in 10.0.0.0/24)
- OS workaround: Tries to route more traffic through VPN or fails the lookup entirely
- Result: Internet access breaks or becomes unreliable
Why This Affects Infrastructure Use
For your use case (RDP, SSH, databases), you’re connecting to services by IP address:
- RDP:
mstsc 10.0.0.50:3389 - SSH:
ssh user@10.0.0.20 - Database:
mysql -h 10.0.0.30
These don’t require DNS lookups at all. But the moment your device tries to do anything else (check email, browse documentation, download updates), it needs DNS for those internet addresses, and the conflict triggers.
The Solution: Omit DNS in Split-Tunnel
By commenting out or removing the DNS line:
# DNS = 1.1.1.1, 1.0.0.1 # Omitted for split-tunnel
Your device behavior becomes:
- VPN DNS: Not configured, so system uses its existing DNS settings
- Routing table: Only 10.0.0.0/24 goes through VPN (as intended)
- Internet DNS: Uses your normal network connection (WiFi DNS, cellular DNS, etc.)
- Internal IPs: Still work perfectly (10.0.0.x doesn’t need DNS)
- No conflicts: Internet and VPN traffic stay completely separate
Real-World Impact
With DNS configured (broken):
- Mobile device connects to VPN for RDP access
- After connecting, internet stops working
- Apps fail to load
- Must disconnect VPN to browse web
- Reconnect VPN for RDP, disconnect for everything else
Without DNS configured (working):
- Mobile device connects to VPN for RDP access
- RDP works:
10.0.0.50:3389routes through tunnel - Internet still works: Uses cellular/WiFi DNS and routing
- Can simultaneously access internal servers AND browse web
- VPN stays connected, no conflicts
When You DO Want DNS in VPN Config
DNS should only be included for full-tunnel mode where AllowedIPs = 0.0.0.0/0 routes ALL traffic through the VPN:
- The VPN becomes your internet gateway
- DNS servers are reachable because all traffic goes through the tunnel
- You want privacy/security for all internet activity
- Use case: Remote workers, public WiFi protection, bypassing restrictions
Summary
Split-tunnel DNS conflicts occur because you’re telling the OS to use DNS servers (1.1.1.1) that exist outside your tunnel’s routing range (10.0.0.0/24), while simultaneously telling it to only route 10.0.0.0/24 through the tunnel. This creates irreconcilable routing conflicts that break internet connectivity. For infrastructure-only VPN connections, omit DNS entirely - your device’s existing DNS configuration handles internet lookups, while IP-based internal services work without any DNS requirements.
What I Built
I created a comprehensive WireGuard peer management script with several key safety features this is drop in an use no special configuration or setup so long as the path for the wg0.conf is /etc/wiregaurd you should be good to run the script, you can move the .sh file to the /bin directory for seamless execution but I like to be very deliberate when running script, this helps me remember where they all are:
- Automatic backups before every configuration change
- Append-only mode to prevent accidental peer deletion
- Split-tunnel by default for infrastructure-only connections
- Interactive mode for guided setup
- IP conflict detection across both client configs and server config
Key Features
Dual Operation Modes
Interactive Mode - Perfect for beginners or when you need guidance:
sudo ./wg-peer-manager.sh
Flag-Based Mode - Quick operations for power users:
sudo ./wg-peer-manager.sh -n server-backend -q
Split vs Full Tunnel
The script defaults to split-tunnel mode - routing only your internal network (10.0.0.0/24) through the VPN. This is perfect for point-to-point connections like RDP, SSH, or database access without interfering with internet traffic.
# Split-tunnel (default) - infrastructure only
sudo ./wg-peer-manager.sh -n rdp-server
# Full-tunnel - complete VPN
sudo ./wg-peer-manager.sh -n mobile-device -f
Why this matters: DNS settings in VPN configs can cause the operating system to route internet traffic through the tunnel even when you only want infrastructure access. Split-tunnel mode comments out the DNS line and restricts routing to your internal network only.
Automatic Backups
Every peer creation automatically creates a timestamped backup:
/etc/wireguard/wg0.conf.backup-20241119-143022
If something goes wrong, restoration is simple:
# Via script
sudo ./wg-peer-manager.sh -r /etc/wireguard/wg0.conf.backup-TIMESTAMP
# Or manually
sudo cp /etc/wireguard/wg0.conf.backup-TIMESTAMP /etc/wireguard/wg0.conf
sudo wg syncconf wg0 <(wg-quick strip wg0)
IP Conflict Prevention
The original version only checked client configs for IP assignments. If you had legacy peers in your main config without matching client files, the script would assign duplicate IPs.
The fixed version checks both locations:
/etc/wireguard/clients/*.conf- Client configurations/etc/wireguard/wg0.conf- Main server config
This prevents duplicate IP assignments across your entire infrastructure.
Installation
# Download the script
wget https://example.com/wg-peer-manager.sh
# Make it executable
chmod +x wg-peer-manager.sh
# Move to system path (optional)
sudo mv wg-peer-manager.sh /usr/local/bin/wg-peer-manager
Usage Examples
Creating Infrastructure Peers
# RDP server with auto-assigned IP
sudo ./wg-peer-manager.sh -n rdp-server
# Database with specific IP
sudo ./wg-peer-manager.sh -n database-primary -i 10.0.0.50
# Mobile device with QR code (split-tunnel)
sudo ./wg-peer-manager.sh -n ipad-admin -q
Creating Full VPN Peers
# Laptop needing complete VPN
sudo ./wg-peer-manager.sh -n john-laptop -f
# Mobile with QR code and internet routing
sudo ./wg-peer-manager.sh -n android-phone -f -q
Management Operations
# List all peers and their status
sudo ./wg-peer-manager.sh -l
# List all backups
sudo ./wg-peer-manager.sh -b
# Restore from backup
sudo ./wg-peer-manager.sh -r /etc/wireguard/wg0.conf.backup-TIMESTAMP
Interactive Mode Walkthrough
Running the script without arguments launches interactive mode:
$ sudo ./wg-peer-manager.sh
[INFO] === WireGuard Peer Manager - Interactive Mode ===
What would you like to do?
1) Create a new peer
2) List all peers
3) List backups
4) Restore from backup
5) Exit
Select option (1-5): 1
Enter peer name: office-device
Enter IP address (press Enter for auto-assign):
Select tunnel mode:
1) Split-tunnel (default) - Infrastructure only, no DNS
2) Full-tunnel - Complete VPN with internet routing
Select mode (1 or 2) [1]: 1
Generate QR code for mobile device? (y/n): y
[INFO] Auto-assigned IP: 10.0.0.5
[INFO] Summary:
Name: office-device
IP: 10.0.0.5
Mode: split-tunnel
QR Code: Yes
Create this peer? (y/n): y
Technical Implementation Details
Append-Only Design
The script uses >> (append) instead of > (overwrite) when adding peers:
cat >> "$WG_CONFIG" <<EOF
# Peer: $peer_name - $peer_ip
[Peer]
PublicKey = $peer_public_key
PresharedKey = $peer_preshared_key
AllowedIPs = $peer_ip/32
EOF
This ensures existing peers are never lost.
Non-Destructive Reloads
Instead of restarting WireGuard (which drops active connections), the script uses wg syncconf:
wg syncconf "$WG_INTERFACE" <(wg-quick strip "$WG_INTERFACE")
This applies configuration changes without interrupting existing connections.
Configuration Templates
Split-Tunnel Client Config:
[Interface]
PrivateKey = <generated>
Address = 10.0.0.x/32
# DNS = 1.1.1.1, 1.0.0.1 # Commented out
[Peer]
PublicKey = <server-key>
PresharedKey = <generated>
Endpoint = <public-ip>:51820
AllowedIPs = 10.0.0.0/24 # Only tunnel traffic
PersistentKeepalive = 25
Full-Tunnel Client Config:
[Interface]
PrivateKey = <generated>
Address = 10.0.0.x/32
DNS = 1.1.1.1, 1.0.0.1 # Active
[Peer]
PublicKey = <server-key>
PresharedKey = <generated>
Endpoint = <public-ip>:51820
AllowedIPs = 0.0.0.0/0, ::/0 # All traffic
PersistentKeepalive = 25
Lessons Learned
1. DNS Can Route Internet Traffic
Even with AllowedIPs set to only your internal network, having DNS configured in the client can cause the OS to route traffic through the tunnel. For infrastructure-only use cases, omit the DNS line entirely.
2. Check All Configuration Sources
Don’t assume all peers have matching client config files. Always validate IP assignments against both the client configs directory and the main server configuration.
3. Backups Are Essential
A single character difference (> vs >>) can wipe out your entire peer configuration. Automatic backups before every change saved me multiple times during development.
4. wg syncconf vs Restart
Use wg syncconf for adding/updating peers to avoid dropping active connections. However, when removing peers, you need a full restart:
sudo wg-quick down wg0 && sudo wg-quick up wg0
wg syncconf only adds and updates - it doesn’t remove peers from the running configuration.
Use Cases
Point-to-Point Infrastructure
Perfect for accessing internal services without routing internet traffic:
- RDP servers (3389)
- SSH access to internal servers
- Database connections
- Internal APIs and web services
- Service mesh between applications
Full VPN Solution
When you need complete privacy and security:
- Remote workers needing secure internet
- Public WiFi protection
- Geo-restriction bypass
- Complete traffic encryption
Safety Checklist
Before deploying in production:
Verify peer count before and after operations:
sudo grep -c "\[Peer\]" /etc/wireguard/wg0.confTest backup restoration in a dev environment first
Keep manual backups before major changes:
sudo cp /etc/wireguard/wg0.conf /etc/wireguard/wg0.conf.manual-$(date +%Y%m%d)Document IP assignments for your infrastructure
Monitor logs after changes:
sudo tail -f /var/log/wireguard-peers.log
Future Enhancements
Potential additions I’m considering:
- Peer removal with confirmation prompts
- Bulk peer creation from CSV
- Email notifications on peer creation
- Integration with monitoring systems
- Automatic backup rotation/cleanup
- Config validation before applying
Full Script
#!/bin/bash
#################################################################
# WireGuard Peer Management Script
# Manages peer creation, configuration generation, and QR codes
#################################################################
# Configuration
WG_INTERFACE="wg0"
WG_CONFIG="/etc/wireguard/${WG_INTERFACE}.conf"
WG_CLIENTS_DIR="/etc/wireguard/clients"
LOG_FILE="/var/log/wireguard-peers.log"
SERVER_IP="10.0.0.1"
SERVER_SUBNET="10.0.0.0/24"
SERVER_PORT="51820"
SERVER_PUBLIC_IP="" # Will be auto-detected if empty
# Default tunnel mode: split (infrastructure) or full (internet)
DEFAULT_TUNNEL_MODE="split" # Change to "full" for VPN routing
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to log messages
log_message() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $1" | tee -a "$LOG_FILE"
}
# Function to print colored output
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root"
exit 1
fi
}
# Check required dependencies
check_dependencies() {
local missing_deps=()
for cmd in wg wg-quick qrencode; do
if ! command -v "$cmd" &> /dev/null; then
missing_deps+=("$cmd")
fi
done
if [ ${#missing_deps[@]} -ne 0 ]; then
print_error "Missing required dependencies: ${missing_deps[*]}"
print_info "Install them with: apt install wireguard-tools qrencode"
exit 1
fi
}
# Get server public IP
get_server_public_ip() {
if [ -z "$SERVER_PUBLIC_IP" ]; then
SERVER_PUBLIC_IP=$(curl -s https://api.ipify.org)
if [ -z "$SERVER_PUBLIC_IP" ]; then
SERVER_PUBLIC_IP=$(curl -s https://ifconfig.me)
fi
if [ -z "$SERVER_PUBLIC_IP" ]; then
print_warning "Could not auto-detect public IP"
read -p "Enter server public IP address: " SERVER_PUBLIC_IP
fi
fi
}
# Get server public key
get_server_public_key() {
if [ ! -f "$WG_CONFIG" ]; then
print_error "WireGuard config not found at $WG_CONFIG"
exit 1
fi
SERVER_PRIVATE_KEY=$(grep PrivateKey "$WG_CONFIG" | awk '{print $3}')
SERVER_PUBLIC_KEY=$(echo "$SERVER_PRIVATE_KEY" | wg pubkey)
}
# Get next available IP
get_next_ip() {
mkdir -p "$WG_CLIENTS_DIR"
# Get all assigned IPs from client configs
local used_ips=()
if [ -d "$WG_CLIENTS_DIR" ]; then
while IFS= read -r file; do
if [ -f "$file" ]; then
ip=$(grep "Address" "$file" | awk '{print $3}' | cut -d'/' -f1)
if [ -n "$ip" ]; then
used_ips+=("$ip")
fi
fi
done < <(find "$WG_CLIENTS_DIR" -name "*.conf")
fi
# Also check main server config for existing peer IPs
if [ -f "$WG_CONFIG" ]; then
while IFS= read -r line; do
if [[ "$line" =~ AllowedIPs[[:space:]]*=[[:space:]]*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ]]; then
ip="${BASH_REMATCH[1]}"
if [[ "$ip" =~ ^10\.0\.0\. ]]; then
used_ips+=("$ip")
fi
fi
done < "$WG_CONFIG"
fi
# Find next available IP in range 10.0.0.2 - 10.0.0.254
for i in {2..254}; do
test_ip="10.0.0.$i"
if [[ ! " ${used_ips[@]} " =~ " ${test_ip} " ]]; then
echo "$test_ip"
return
fi
done
print_error "No available IP addresses in range"
exit 1
}
# Validate IP address
validate_ip() {
local ip=$1
local stat=1
if [[ $ip =~ ^10\.0\.0\.([0-9]{1,3})$ ]]; then
local octet=${BASH_REMATCH[1]}
if [[ $octet -ge 2 && $octet -le 254 ]]; then
stat=0
fi
fi
return $stat
}
# Check if IP is already in use
check_ip_in_use() {
local ip=$1
# Check client configs
if [ -d "$WG_CLIENTS_DIR" ]; then
while IFS= read -r file; do
if [ -f "$file" ]; then
existing_ip=$(grep "Address" "$file" | awk '{print $3}' | cut -d'/' -f1)
if [ "$existing_ip" == "$ip" ]; then
return 0 # IP is in use
fi
fi
done < <(find "$WG_CLIENTS_DIR" -name "*.conf")
fi
# Check main server config
if [ -f "$WG_CONFIG" ]; then
while IFS= read -r line; do
if [[ "$line" =~ AllowedIPs[[:space:]]*=[[:space:]]*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/32 ]]; then
existing_ip="${BASH_REMATCH[1]}"
if [ "$existing_ip" == "$ip" ]; then
return 0 # IP is in use
fi
fi
done < "$WG_CONFIG"
fi
return 1 # IP is not in use
}
# Create peer configuration
create_peer() {
local peer_name=$1
local peer_ip=$2
local show_qr=${3:-false}
local tunnel_mode=${4:-$DEFAULT_TUNNEL_MODE}
# Sanitize peer name
peer_name=$(echo "$peer_name" | tr ' ' '_' | tr -cd '[:alnum:]_-')
if [ -z "$peer_name" ]; then
print_error "Invalid peer name"
exit 1
fi
# Validate and check IP
if ! validate_ip "$peer_ip"; then
print_error "Invalid IP address. Must be in range 10.0.0.2 - 10.0.0.254"
exit 1
fi
if check_ip_in_use "$peer_ip"; then
print_error "IP address $peer_ip is already in use"
exit 1
fi
# Create clients directory
mkdir -p "$WG_CLIENTS_DIR"
# Check if peer already exists
if [ -f "$WG_CLIENTS_DIR/${peer_name}.conf" ]; then
print_error "Peer '$peer_name' already exists"
exit 1
fi
print_info "Creating peer: $peer_name with IP: $peer_ip (${tunnel_mode} tunnel)"
# Generate keys for peer
local peer_private_key=$(wg genkey)
local peer_public_key=$(echo "$peer_private_key" | wg pubkey)
local peer_preshared_key=$(wg genpsk)
# Determine AllowedIPs and DNS based on tunnel mode
local allowed_ips
local dns_line
if [ "$tunnel_mode" == "full" ]; then
allowed_ips="0.0.0.0/0, ::/0"
dns_line="DNS = 1.1.1.1, 1.0.0.1"
else
# Split tunnel - infrastructure only
allowed_ips="$SERVER_SUBNET"
dns_line="# DNS = 1.1.1.1, 1.0.0.1 # Commented out for split-tunnel"
fi
# Create client config file
cat > "$WG_CLIENTS_DIR/${peer_name}.conf" <<EOF
[Interface]
PrivateKey = $peer_private_key
Address = $peer_ip/32
$dns_line
[Peer]
PublicKey = $SERVER_PUBLIC_KEY
PresharedKey = $peer_preshared_key
Endpoint = $SERVER_PUBLIC_IP:$SERVER_PORT
AllowedIPs = $allowed_ips
PersistentKeepalive = 25
EOF
# Backup server config before modification
local backup_file="${WG_CONFIG}.backup-$(date +%Y%m%d-%H%M%S)"
print_info "Creating backup: $backup_file"
cp "$WG_CONFIG" "$backup_file"
# Add peer to server config
print_info "Adding peer to server configuration..."
cat >> "$WG_CONFIG" <<EOF
# Peer: $peer_name - $peer_ip
[Peer]
PublicKey = $peer_public_key
PresharedKey = $peer_preshared_key
AllowedIPs = $peer_ip/32
EOF
# Reload WireGuard
print_info "Reloading WireGuard interface..."
wg syncconf "$WG_INTERFACE" <(wg-quick strip "$WG_INTERFACE")
# Log the operation
log_message "Created peer: $peer_name | IP: $peer_ip | Public Key: $peer_public_key"
# Print success message
print_success "Peer '$peer_name' created successfully!"
echo ""
print_info "Configuration file: $WG_CLIENTS_DIR/${peer_name}.conf"
print_info "Peer IP: $peer_ip"
print_info "Tunnel Mode: $tunnel_mode"
print_info "Public Key: $peer_public_key"
print_info "Backup created: $backup_file"
# Show QR code if requested
if [ "$show_qr" == "true" ]; then
echo ""
print_info "QR Code for mobile device:"
echo ""
qrencode -t ansiutf8 < "$WG_CLIENTS_DIR/${peer_name}.conf"
echo ""
print_info "You can also generate QR code later with:"
print_info "qrencode -t ansiutf8 < $WG_CLIENTS_DIR/${peer_name}.conf"
fi
echo ""
print_info "To display configuration:"
echo "cat $WG_CLIENTS_DIR/${peer_name}.conf"
}
# List all peers
list_peers() {
print_info "Current WireGuard Peers:"
echo ""
if [ ! -d "$WG_CLIENTS_DIR" ] || [ -z "$(ls -A "$WG_CLIENTS_DIR" 2>/dev/null)" ]; then
print_warning "No peers found"
return
fi
printf "%-20s %-15s %-10s\n" "NAME" "IP ADDRESS" "STATUS"
printf "%-20s %-15s %-10s\n" "----" "----------" "------"
for conf_file in "$WG_CLIENTS_DIR"/*.conf; do
if [ -f "$conf_file" ]; then
peer_name=$(basename "$conf_file" .conf)
peer_ip=$(grep "Address" "$conf_file" | awk '{print $3}' | cut -d'/' -f1)
peer_pubkey=$(grep "PrivateKey" "$conf_file" | awk '{print $3}' | wg pubkey)
# Check if peer is active
if wg show "$WG_INTERFACE" | grep -q "$peer_pubkey"; then
status="${GREEN}Active${NC}"
else
status="${YELLOW}Inactive${NC}"
fi
printf "%-20s %-15s %-10b\n" "$peer_name" "$peer_ip" "$status"
fi
done
echo ""
}
# List available backups
list_backups() {
print_info "Available WireGuard configuration backups:"
echo ""
local backups=($(ls -t "${WG_CONFIG}.backup-"* 2>/dev/null))
if [ ${#backups[@]} -eq 0 ]; then
print_warning "No backups found"
return
fi
printf "%-30s %-20s %-10s\n" "BACKUP FILE" "DATE" "SIZE"
printf "%-30s %-20s %-10s\n" "-----------" "----" "----"
for backup in "${backups[@]}"; do
local filename=$(basename "$backup")
local filesize=$(du -h "$backup" | cut -f1)
local filedate=$(echo "$filename" | grep -oP '\d{8}-\d{6}' | sed 's/\([0-9]\{8\}\)-\([0-9]\{6\}\)/\1 \2/')
printf "%-30s %-20s %-10s\n" "$filename" "$filedate" "$filesize"
done
echo ""
print_info "To restore a backup:"
echo "sudo cp /etc/wireguard/wg0.conf.backup-TIMESTAMP /etc/wireguard/wg0.conf"
echo "sudo wg syncconf wg0 <(wg-quick strip wg0)"
echo ""
}
# Restore from backup
restore_backup() {
local backup_file=$1
if [ ! -f "$backup_file" ]; then
print_error "Backup file not found: $backup_file"
exit 1
fi
print_warning "This will restore WireGuard config from backup"
read -p "Are you sure? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
print_info "Restore cancelled"
exit 0
fi
# Create backup of current config before restoring
local safety_backup="${WG_CONFIG}.pre-restore-$(date +%Y%m%d-%H%M%S)"
cp "$WG_CONFIG" "$safety_backup"
print_info "Safety backup created: $safety_backup"
# Restore
cp "$backup_file" "$WG_CONFIG"
print_success "Config restored from: $backup_file"
# Reload
print_info "Reloading WireGuard..."
wg syncconf "$WG_INTERFACE" <(wg-quick strip "$WG_INTERFACE")
print_success "Restore complete!"
}
# Show usage
show_usage() {
cat <<EOF
WireGuard Peer Management Script
Usage: $0 [OPTIONS]
Interactive Mode:
Run without any options for guided menu-driven interface:
$0
Options:
-n, --name NAME Peer name (required)
-i, --ip IP IP address (optional, auto-assigned if not provided)
-q, --qr Generate QR code for mobile devices
-f, --full-tunnel Full tunnel mode (route all traffic through VPN)
Default: split-tunnel (infrastructure only)
-l, --list List all peers
-b, --backups List all configuration backups
-r, --restore FILE Restore from backup file
-h, --help Show this help message
Tunnel Modes:
Split-tunnel (default): Only routes 10.0.0.0/24 through VPN, no DNS
Perfect for point-to-point services (RDP, etc)
Full-tunnel (-f): Routes all traffic through VPN with DNS
Use for complete VPN internet access
Examples:
# Interactive mode (recommended for beginners)
$0
# Create infrastructure peer (split-tunnel, no DNS)
$0 -n server-backend
# Create infrastructure peer with QR code
$0 -n office-device -q
# Create full VPN peer with internet routing
$0 -n mobile-device -f -q
# Create peer with specific IP
$0 -n service-host -i 10.0.0.50
# List all peers
$0 -l
# List all backups
$0 -b
# Restore from backup
$0 -r /etc/wireguard/wg0.conf.backup-20231118-143022
Safety:
• Automatic backup before each peer creation
• Append-only mode (never overwrites existing peers)
• Non-destructive config reload
Log file: $LOG_FILE
Config directory: $WG_CLIENTS_DIR
Backups: ${WG_CONFIG}.backup-*
EOF
}
# Main script
main() {
check_root
check_dependencies
# Parse command line arguments
local peer_name=""
local peer_ip=""
local show_qr=false
local list_mode=false
local backups_mode=false
local restore_file=""
local tunnel_mode=$DEFAULT_TUNNEL_MODE
local interactive_mode=false
# Check if running with no arguments (interactive mode)
if [ $# -eq 0 ]; then
interactive_mode=true
fi
while [[ $# -gt 0 ]]; do
case $1 in
-n|--name)
peer_name="$2"
shift 2
;;
-i|--ip)
peer_ip="$2"
shift 2
;;
-q|--qr)
show_qr=true
shift
;;
-f|--full-tunnel)
tunnel_mode="full"
shift
;;
-l|--list)
list_mode=true
shift
;;
-b|--backups)
backups_mode=true
shift
;;
-r|--restore)
restore_file="$2"
shift 2
;;
-h|--help)
show_usage
exit 0
;;
*)
print_error "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Interactive mode
if [ "$interactive_mode" == true ]; then
echo ""
print_info "=== WireGuard Peer Manager - Interactive Mode ==="
echo ""
# Show menu
echo "What would you like to do?"
echo " 1) Create a new peer"
echo " 2) List all peers"
echo " 3) List backups"
echo " 4) Restore from backup"
echo " 5) Exit"
echo ""
read -p "Select option (1-5): " menu_choice
case $menu_choice in
1)
# Create peer - continue with prompts below
;;
2)
get_server_public_key
list_peers
exit 0
;;
3)
list_backups
exit 0
;;
4)
list_backups
echo ""
read -p "Enter backup file path to restore: " restore_file
if [ -n "$restore_file" ]; then
restore_backup "$restore_file"
fi
exit 0
;;
5)
print_info "Goodbye!"
exit 0
;;
*)
print_error "Invalid option"
exit 1
;;
esac
echo ""
print_info "=== Create New Peer ==="
echo ""
# Get peer name
while [ -z "$peer_name" ]; do
read -p "Enter peer name (e.g., office-device, john-phone): " peer_name
if [ -z "$peer_name" ]; then
print_error "Peer name cannot be empty"
fi
done
# Get IP address (note: actual auto-assign happens later after server info is loaded)
echo ""
read -p "Enter IP address (press Enter for auto-assign): " peer_ip
# Get tunnel mode
echo ""
echo "Select tunnel mode:"
echo " 1) Split-tunnel (default) - Infrastructure only, no DNS"
echo " Routes only 10.0.0.0/24 through VPN"
echo " Perfect for: RDP, SSH, databases, service mesh"
echo ""
echo " 2) Full-tunnel - Complete VPN with internet routing"
echo " Routes ALL traffic through VPN with DNS"
echo " Perfect for: Remote workers, secure browsing"
echo ""
read -p "Select mode (1 or 2) [1]: " mode_choice
case $mode_choice in
2)
tunnel_mode="full"
print_info "Selected: Full-tunnel mode"
;;
1|"")
tunnel_mode="split"
print_info "Selected: Split-tunnel mode"
;;
*)
print_warning "Invalid choice, using default: Split-tunnel"
tunnel_mode="split"
;;
esac
# Get QR code preference
echo ""
read -p "Generate QR code for mobile device? (y/n) [n]: " qr_choice
case $qr_choice in
y|Y|yes|Yes)
show_qr=true
;;
*)
show_qr=false
;;
esac
fi
# Backup list mode
if [ "$backups_mode" == true ]; then
list_backups
exit 0
fi
# Restore mode
if [ -n "$restore_file" ]; then
restore_backup "$restore_file"
exit 0
fi
# List mode
if [ "$list_mode" == true ]; then
get_server_public_key
list_peers
exit 0
fi
# Validate required parameters (only for flag-based mode)
if [ -z "$peer_name" ] && [ "$interactive_mode" == false ]; then
print_error "Peer name is required"
show_usage
exit 1
fi
# Auto-assign IP if not provided (for flag-based mode only)
if [ -z "$peer_ip" ] && [ "$interactive_mode" == false ]; then
peer_ip=$(get_next_ip)
print_info "Auto-assigned IP: $peer_ip"
fi
# Get server info
get_server_public_ip
get_server_public_key
# Auto-assign IP if not provided
if [ -z "$peer_ip" ]; then
peer_ip=$(get_next_ip)
if [ "$interactive_mode" == true ]; then
print_info "Auto-assigned IP: $peer_ip"
fi
fi
# Show summary in interactive mode before creating
if [ "$interactive_mode" == true ]; then
echo ""
print_info "Summary:"
echo " Name: $peer_name"
echo " IP: $peer_ip"
echo " Mode: $tunnel_mode-tunnel"
echo " QR Code: $([ "$show_qr" == true ] && echo "Yes" || echo "No")"
echo ""
read -p "Create this peer? (y/n) [y]: " confirm
case $confirm in
n|N|no|No)
print_info "Cancelled"
exit 0
;;
esac
fi
# Create the peer
create_peer "$peer_name" "$peer_ip" "$show_qr" "$tunnel_mode"
}
# Run main function
main "$@"
Conclusion
After accidentally deleting an active peer with my old script, building a safer alternative was essential. The key improvements - automatic backups, append-only operations, split-tunnel defaults, and interactive mode - make WireGuard peer management reliable and safe for infrastructure deployments. The script has been running in production for over a month managing point-to-point connections for RDP access, database connections, and service mesh networking across our infrastructure without incident. Do not forget that there is a QR code option for mobile device enrollment, so please do not try importing and exporting configs this is dangerous.

