Add WireGuard TUI implementation

- Add Go TUI with bubbletea for WireGuard management
- Implement client CRUD operations with QR code generation
- Add configuration and validation modules
- Install/update scripts for client setup
- Update Makefile to build binaries to bin/ directory
- Add .gitignore for Go projects
This commit is contained in:
Calmcacil
2026-01-12 19:03:35 +01:00
parent 5ac68db854
commit 26120b8bc2
37 changed files with 6330 additions and 97 deletions

921
wg-install.sh Executable file
View File

@@ -0,0 +1,921 @@
#!/usr/bin/env bash
#
# wg-install.sh - WireGuard VPN Server Installation Script
#
# This script handles the complete installation of a WireGuard VPN server on Debian 13.
# It includes dependency checks, package installation, firewall setup (nftables),
# server key generation, interface initialization, and systemd service setup.
#
# Settings can be provided via interactive prompts or environment variables prefixed with WGI_
# (e.g., WGI_SERVER_DOMAIN, WGI_WG_PORT, WGI_VPN_IPV4_RANGE, etc.)
set -euo pipefail
# Default Configuration (can be overridden by environment variables with WGI_ prefix)
WGI_SERVER_DOMAIN="${WGI_SERVER_DOMAIN:-}"
WGI_WG_PORT="${WGI_WG_PORT:-51820}"
WGI_VPN_IPV4_RANGE="${WGI_VPN_IPV4_RANGE:-10.10.69.0/24}"
WGI_VPN_IPV6_RANGE="${WGI_VPN_IPV6_RANGE:-fd69:dead:beef:69::/64}"
WGI_WG_INTERFACE="${WGI_WG_INTERFACE:-wg0}"
WGI_DNS_SERVERS="${WGI_DNS_SERVERS:-8.8.8.8, 8.8.4.4}"
WGI_LOG_FILE="${WGI_LOG_FILE:-/var/log/wg-admin-install.log}"
WGI_MIN_DISK_SPACE_MB=100
# Derived paths
CONF_D_DIR="/etc/wireguard/conf.d"
SERVER_CONF="${CONF_D_DIR}/server.conf"
CLIENT_OUTPUT_DIR="/etc/wireguard/clients"
WG_CONFIG="/etc/wireguard/${WGI_WG_INTERFACE}.conf"
BACKUP_DIR="/etc/wg-admin/backups"
# Global variables for cleanup and rollback
TEMP_DIR=""
ROLLBACK_BACKUP_DIR=""
ROLLBACK_NEEDED=false
PUBLIC_INTERFACE=""
SERVER_PRIVATE_KEY=""
SERVER_PUBLIC_KEY=""
# ============================================================================
# Logging Functions
# ============================================================================
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[${timestamp}] [${level}] ${message}" | tee -a "${WGI_LOG_FILE}"
}
log_info() {
log "INFO" "$@"
}
log_error() {
log "ERROR" "$@" >&2
}
log_warn() {
log "WARN" "$@"
}
# ============================================================================
# Cleanup and Error Handling
# ============================================================================
cleanup_handler() {
local exit_code=$?
# Remove temporary directories
if [[ -n "${TEMP_DIR}" ]] && [[ -d "${TEMP_DIR}" ]]; then
log_info "Cleaning up temporary directory: ${TEMP_DIR}"
rm -rf "${TEMP_DIR}"
fi
# Rollback on failure if needed
if [[ ${ROLLBACK_NEEDED} == true ]] && [[ ${exit_code} -ne 0 ]]; then
log_error "Installation failed, attempting rollback..."
rollback_installation
fi
exit ${exit_code}
}
# Set up traps for cleanup
trap cleanup_handler EXIT INT TERM HUP
# ============================================================================
# Input Validation Functions
# ============================================================================
validate_dns_servers() {
local dns="$1"
if [[ -z "$dns" ]]; then
return 0 # Empty DNS is allowed
fi
# Split by comma and validate each DNS server
IFS=',' read -ra dns_array <<< "$dns"
for dns_server in "${dns_array[@]}"; do
dns_server=$(echo "$dns_server" | xargs) # Trim whitespace
if [[ ! "$dns_server" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
log_error "Invalid DNS server format: ${dns_server}"
return 1
fi
done
return 0
}
validate_port_range() {
local port="$1"
if [[ -z "$port" ]]; then
log_error "Port cannot be empty"
return 1
fi
if [[ ! "$port" =~ ^[0-9]+$ ]]; then
log_error "Port must be a number"
return 1
fi
if [[ "$port" -lt 1 ]] || [[ "$port" -gt 65535 ]]; then
log_error "Port must be between 1 and 65535"
return 1
fi
return 0
}
validate_cidr() {
local cidr="$1"
local is_ipv6="$2"
if [[ -z "$cidr" ]]; then
log_error "CIDR cannot be empty"
return 1
fi
if [[ "$is_ipv6" == "true" ]]; then
# Basic IPv6 CIDR validation
if [[ ! "$cidr" =~ ^[0-9a-fA-F:]+/[0-9]+$ ]]; then
log_error "Invalid IPv6 CIDR format: ${cidr}"
return 1
fi
local prefix="${cidr#*/}"
if [[ "$prefix" -lt 0 ]] || [[ "$prefix" -gt 128 ]]; then
log_error "Invalid IPv6 prefix length: ${prefix}"
return 1
fi
else
# IPv4 CIDR validation
if [[ ! "$cidr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]]; then
log_error "Invalid IPv4 CIDR format: ${cidr}"
return 1
fi
local ip="${cidr%/*}"
local prefix="${cidr#*/}"
# Validate each octet
IFS='.' read -ra octets <<< "$ip"
for octet in "${octets[@]}"; do
if [[ "$octet" -lt 0 ]] || [[ "$octet" -gt 255 ]]; then
log_error "Invalid IPv4 octet: ${octet} in ${cidr}"
return 1
fi
done
if [[ "$prefix" -lt 0 ]] || [[ "$prefix" -gt 32 ]]; then
log_error "Invalid IPv4 prefix length: ${prefix}"
return 1
fi
fi
return 0
}
validate_server_domain() {
local domain="$1"
if [[ -z "$domain" ]]; then
log_error "Server domain cannot be empty"
return 1
fi
# Basic domain validation (alphanumeric, hyphens, dots)
if [[ ! "$domain" =~ ^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
log_error "Invalid server domain format: ${domain}"
return 1
fi
return 0
}
# ============================================================================
# Pre-installation Validation
# ============================================================================
pre_install_validation() {
log_info "Running pre-installation validation..."
# Check root privileges
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
# Check disk space (at least MIN_DISK_SPACE_MB free)
local free_space_kb=$(df / | awk 'NR==2 {print $4}')
local free_space_mb=$((free_space_kb / 1024))
if [[ ${free_space_mb} -lt ${WGI_MIN_DISK_SPACE_MB} ]]; then
log_error "Insufficient disk space (${free_space_mb}MB free, ${WGI_MIN_DISK_SPACE_MB}MB required)"
exit 1
fi
log_info "Disk space validation passed (${free_space_mb}MB free)"
# Check port availability
if ss -ulnp 2>/dev/null | grep -q ":${WGI_WG_PORT}"; then
log_error "Port ${WGI_WG_PORT} is already in use"
echo "ERROR: WireGuard port ${WGI_WG_PORT} is already in use."
echo "Action: Stop the service using port ${WGI_WG_PORT} or change the port."
echo "To find what's using the port: 'sudo ss -tulnp | grep ${WGI_WG_PORT}'"
exit 1
fi
log_info "Port ${WGI_WG_PORT} is available"
log_info "Pre-installation validation passed"
}
# ============================================================================
# Backup Functions
# ============================================================================
backup_config() {
local operation="${1:-manual}"
local timestamp=$(date +"%Y%m%d_%H%M%S")
local backup_name="wg-backup-${operation}-${timestamp}"
local backup_path="${BACKUP_DIR}/${backup_name}"
log_info "Creating configuration backup: ${backup_name}"
# Create backup directory if it doesn't exist
mkdir -p "${BACKUP_DIR}"
chmod 700 "${BACKUP_DIR}"
# Create backup directory
mkdir -p "${backup_path}"
# Backup WireGuard configurations
if [[ -d "/etc/wireguard" ]]; then
cp -a /etc/wireguard "${backup_path}/" 2>/dev/null || true
fi
# Backup nftables configurations
if [[ -f "/etc/nftables.conf" ]]; then
cp -a /etc/nftables.conf "${backup_path}/" 2>/dev/null || true
fi
if [[ -d "/etc/nftables.d" ]]; then
cp -a /etc/nftables.d "${backup_path}/" 2>/dev/null || true
fi
# Backup sysctl configuration
if [[ -f "/etc/sysctl.d/99-wireguard.conf" ]]; then
cp -a /etc/sysctl.d/99-wireguard.conf "${backup_path}/" 2>/dev/null || true
fi
# Create backup metadata
cat > "${backup_path}/backup-info.txt" <<EOF
Backup created: ${timestamp}
Operation: ${operation}
Hostname: $(hostname)
WireGuard interface: ${WGI_WG_INTERFACE}
EOF
# Set restrictive permissions
chmod -R 600 "${backup_path}"
# Apply retention policy - keep only last 10 backups
apply_retention_policy
log_info "Backup created successfully: ${backup_path}"
echo "${backup_path}"
}
apply_retention_policy() {
if [[ ! -d "${BACKUP_DIR}" ]]; then
return
fi
# List all backups sorted by modification time (oldest first)
local backups=($(ls -t "${BACKUP_DIR}" 2>/dev/null | grep -E '^wg-backup-'))
# If we have more than 10 backups, remove oldest backups
if [[ ${#backups[@]} -gt 10 ]]; then
log_info "Applying retention policy (keeping last 10 backups)..."
local to_remove=(${backups[@]:10})
for backup in "${to_remove[@]}"; do
log_info "Removing old backup: ${backup}"
rm -rf "${BACKUP_DIR}/${backup}"
done
fi
}
# ============================================================================
# Rollback Functions
# ============================================================================
rollback_installation() {
log_warn "Rolling back installation..."
# Stop services
systemctl stop wg-quick@${WGI_WG_INTERFACE}.service 2>/dev/null || true
systemctl disable wg-quick@${WGI_WG_INTERFACE}.service 2>/dev/null || true
# Restore from backup if exists
if [[ -d "${ROLLBACK_BACKUP_DIR}" ]]; then
log_info "Restoring from backup: ${ROLLBACK_BACKUP_DIR}"
if [[ -f "${ROLLBACK_BACKUP_DIR}/wireguard.conf" ]]; then
cp "${ROLLBACK_BACKUP_DIR}/wireguard.conf" "${WG_CONFIG}"
fi
if [[ -f "${ROLLBACK_BACKUP_DIR}/nftables.conf" ]]; then
cp "${ROLLBACK_BACKUP_DIR}/nftables.conf" /etc/nftables.conf
nft -f /etc/nftables.conf 2>/dev/null || true
fi
if [[ -d "${ROLLBACK_BACKUP_DIR}/conf.d" ]]; then
rm -rf "${CONF_D_DIR}"
cp -r "${ROLLBACK_BACKUP_DIR}/conf.d" "${CONF_D_DIR}"
chmod 700 "${CONF_D_DIR}"
fi
fi
# Bring down interface
wg-quick down ${WGI_WG_INTERFACE} 2>/dev/null || true
log_warn "Rollback complete. Please review the logs and try again."
}
# ============================================================================
# Interactive Configuration
# ============================================================================
prompt_configuration() {
echo ""
echo "=== WireGuard VPN Server Configuration ==="
echo ""
echo "Press Enter to accept the default value (shown in brackets)"
echo ""
# Server Domain
if [[ -z "${WGI_SERVER_DOMAIN}" ]]; then
read -p "Server Domain [e.g., vpn.example.com]: " input_domain
WGI_SERVER_DOMAIN="${input_domain}"
else
echo "Server Domain: ${WGI_SERVER_DOMAIN}"
fi
# Port
if [[ -z "${WGI_WG_PORT}" ]] || [[ "${WGI_WG_PORT}" == "51820" ]]; then
read -p "WireGuard Port [${WGI_WG_PORT}]: " input_port
if [[ -n "$input_port" ]]; then
WGI_WG_PORT="$input_port"
fi
else
echo "WireGuard Port: ${WGI_WG_PORT}"
fi
# IPv4 Range
if [[ -z "${WGI_VPN_IPV4_RANGE}" ]] || [[ "${WGI_VPN_IPV4_RANGE}" == "10.10.69.0/24" ]]; then
read -p "VPN IPv4 Range [${WGI_VPN_IPV4_RANGE}]: " input_ipv4
if [[ -n "$input_ipv4" ]]; then
WGI_VPN_IPV4_RANGE="$input_ipv4"
fi
else
echo "VPN IPv4 Range: ${WGI_VPN_IPV4_RANGE}"
fi
# IPv6 Range
if [[ -z "${WGI_VPN_IPV6_RANGE}" ]] || [[ "${WGI_VPN_IPV6_RANGE}" == "fd69:dead:beef:69::/64" ]]; then
read -p "VPN IPv6 Range [${WGI_VPN_IPV6_RANGE}]: " input_ipv6
if [[ -n "$input_ipv6" ]]; then
WGI_VPN_IPV6_RANGE="$input_ipv6"
fi
else
echo "VPN IPv6 Range: ${WGI_VPN_IPV6_RANGE}"
fi
# DNS Servers
if [[ -z "${WGI_DNS_SERVERS}" ]] || [[ "${WGI_DNS_SERVERS}" == "8.8.8.8, 8.8.4.4" ]]; then
read -p "DNS Servers [${WGI_DNS_SERVERS}]: " input_dns
if [[ -n "$input_dns" ]]; then
WGI_DNS_SERVERS="$input_dns"
fi
else
echo "DNS Servers: ${WGI_DNS_SERVERS}"
fi
# Interface Name
if [[ -z "${WGI_WG_INTERFACE}" ]] || [[ "${WGI_WG_INTERFACE}" == "wg0" ]]; then
read -p "WireGuard Interface [${WGI_WG_INTERFACE}]: " input_interface
if [[ -n "$input_interface" ]]; then
WGI_WG_INTERFACE="$input_interface"
# Update derived paths
WG_CONFIG="/etc/wireguard/${WGI_WG_INTERFACE}.conf"
fi
else
echo "WireGuard Interface: ${WGI_WG_INTERFACE}"
fi
echo ""
echo "=== Configuration Summary ==="
echo "Server Domain: ${WGI_SERVER_DOMAIN}"
echo "WireGuard Port: ${WGI_WG_PORT}"
echo "VPN IPv4 Range: ${WGI_VPN_IPV4_RANGE}"
echo "VPN IPv6 Range: ${WGI_VPN_IPV6_RANGE}"
echo "DNS Servers: ${WGI_DNS_SERVERS}"
echo "WireGuard Interface: ${WGI_WG_INTERFACE}"
echo ""
read -p "Proceed with installation? (yes/no): " confirm
if [[ "${confirm}" != "yes" ]]; then
log_info "Installation cancelled by user"
exit 0
fi
}
# ============================================================================
# Package Installation
# ============================================================================
install_packages() {
log_info "Installing packages..."
echo "Updating package lists..."
apt-get update -qq
echo "Installing WireGuard and dependencies..."
apt-get install -y wireguard wireguard-tools qrencode nftables
log_info "Package installation complete"
}
# ============================================================================
# Cleanup Existing Installation
# ============================================================================
cleanup_existing_installation() {
log_info "Checking for existing WireGuard installation..."
# Stop and disable WireGuard service
if systemctl is-enabled --quiet wg-quick@${WGI_WG_INTERFACE}.service 2>/dev/null || systemctl list-unit-files | grep -q wg-quick@${WGI_WG_INTERFACE}.service; then
log_info "Stopping WireGuard service..."
echo "Stopping WireGuard service..."
systemctl stop wg-quick@${WGI_WG_INTERFACE}.service 2>/dev/null || true
echo "Disabling WireGuard service..."
systemctl disable wg-quick@${WGI_WG_INTERFACE}.service 2>/dev/null || true
fi
# Stop and disable old client loader service
if systemctl is-enabled --quiet wg-load-clients.service 2>/dev/null || systemctl list-unit-files | grep -q wg-load-clients.service; then
echo "Removing old WireGuard client loader service..."
systemctl disable wg-load-clients.service 2>/dev/null || true
systemctl stop wg-load-clients.service 2>/dev/null || true
rm -f /etc/systemd/system/wg-load-clients.service
systemctl daemon-reload 2>/dev/null || true
fi
# Bring down interface if running
if wg show ${WGI_WG_INTERFACE} &>/dev/null; then
echo "WireGuard interface is active, bringing down..."
wg-quick down ${WGI_WG_INTERFACE} 2>/dev/null || true
fi
# Remove existing configuration directories
if [[ -d "/etc/wireguard" ]]; then
echo "Removing existing WireGuard configuration..."
rm -rf /etc/wireguard
fi
if [[ -d "/etc/clients.d" ]]; then
echo "Removing old client directory..."
rm -rf /etc/clients.d
fi
if [[ -d "/etc/wireguard/conf.d" ]]; then
echo "Removing existing config.d directory..."
rm -rf /etc/wireguard/conf.d
fi
if [[ -d "/etc/wireguard/peer.d" ]]; then
echo "Removing old peer.d directory..."
rm -rf /etc/wireguard/peer.d
fi
if [[ -d "/root/wireguard-clients" ]]; then
echo "Removing old client config directory..."
rm -rf /root/wireguard-clients
fi
# Flush nftables rules
if command -v nft &> /dev/null; then
echo "Flushing nftables rules..."
nft flush ruleset 2>/dev/null || true
fi
echo "Cleanup complete."
}
# ============================================================================
# Network Configuration
# ============================================================================
detect_public_interface() {
PUBLIC_INTERFACE=$(ip route get 8.8.8.8 | grep -oP 'dev \K\S+' | head -1)
log_info "Public interface detected: ${PUBLIC_INTERFACE}"
echo "Public interface: ${PUBLIC_INTERFACE}"
}
enable_ip_forwarding() {
log_info "Enabling IP forwarding..."
echo "Enabling IP forwarding..."
cat > /etc/sysctl.d/99-wireguard.conf <<EOF
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sysctl -p /etc/sysctl.d/99-wireguard.conf
log_info "IP forwarding enabled"
}
configure_nftables() {
log_info "Configuring nftables firewall..."
echo "Configuring nftables firewall..."
mkdir -p /etc/nftables.d
# Create wireguard config in /etc/nftables.d
cat > /etc/nftables.d/wireguard.conf <<EOF
#!/usr/sbin/nft -f
# nftables configuration for WireGuard VPN
flush ruleset
table inet wireguard {
chain prerouting {
type filter hook prerouting priority -150;
# Rate limiting for SSH (3 connections per minute, burst 5)
tcp dport 22 ct state new limit rate 3/minute burst 5 packets accept
tcp dport 22 ct state new limit rate 3/minute burst 5 packets drop
# Rate limiting for WireGuard (${WGI_WG_PORT})
udp dport ${WGI_WG_PORT} limit rate 10/second burst 20 packets accept
udp dport ${WGI_WG_PORT} limit rate 10/second burst 20 packets drop
}
chain input {
type filter hook input priority 0; policy drop;
iifname lo accept
# Connection tracking bypass for WireGuard UDP traffic
iifname "${PUBLIC_INTERFACE}" udp dport ${WGI_WG_PORT} notrack
ct state established,related accept
ct state invalid drop
# Allow SSH
tcp dport 22 accept
# Allow WireGuard UDP traffic (already tracked via notrack)
udp dport ${WGI_WG_PORT} accept
# ICMPv4
icmp type { echo-request, echo-reply } accept
# ICMPv6 - ensure neighbor discovery is allowed
icmpv6 type { echo-request, echo-reply, nd-neighbor-solicit, nd-neighbor-advert, nd-router-solicit, nd-router-advert } accept
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
iifname ${WGI_WG_INTERFACE} accept
oifname ${WGI_WG_INTERFACE} accept
}
chain output {
type filter hook output priority 0; policy accept;
# Connection tracking bypass for WireGuard UDP traffic
oifname "${PUBLIC_INTERFACE}" udp dport ${WGI_WG_PORT} notrack
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
# TCP MSS clamping for MTU issues (clamp to 1360)
oifname "${PUBLIC_INTERFACE}" tcp flags syn tcp option maxseg size set 1360
oifname "${PUBLIC_INTERFACE}" ip saddr ${WGI_VPN_IPV4_RANGE} masquerade
}
}
table ip6 nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
# TCP MSS clamping for IPv6 MTU issues
oifname "${PUBLIC_INTERFACE}" tcp flags syn tcp option maxseg size set 1360
oifname "${PUBLIC_INTERFACE}" ip6 saddr ${WGI_VPN_IPV6_RANGE} masquerade
}
}
EOF
# Create /etc/nftables.conf that includes wireguard.conf
cat > /etc/nftables.conf <<EOF
#!/usr/sbin/nft -f
# nftables configuration automatically generated by wg-install.sh
flush ruleset
include "/etc/nftables.d/wireguard.conf"
EOF
chmod 600 /etc/nftables.conf
chmod 600 /etc/nftables.d/wireguard.conf
# Validate nftables configuration before applying
echo "Validating nftables configuration..."
if ! nft check -f /etc/nftables.conf; then
log_error "nftables configuration validation failed"
exit 1
fi
echo "nftables configuration is valid"
# Apply nftables rules
nft -f /etc/nftables.conf
# Enable and start nftables service
echo "Enabling nftables service..."
systemctl enable nftables.service
systemctl start nftables.service
# Verify nftables is running
if ! systemctl is-active --quiet nftables.service; then
log_error "nftables service failed to start"
echo "=== Service Status ==="
systemctl status nftables.service
exit 1
fi
echo "nftables service started successfully"
log_info "nftables configuration complete"
}
# ============================================================================
# Server Key Generation
# ============================================================================
generate_server_keys() {
log_info "Generating server keys..."
echo "Generating server keys..."
mkdir -p /etc/wireguard
cd /etc/wireguard
# Generate keys with atomic write
local temp_private=$(mktemp)
local temp_public=$(mktemp)
wg genkey > "$temp_private"
wg pubkey < "$temp_private" > "$temp_public"
SERVER_PRIVATE_KEY=$(cat "$temp_private")
SERVER_PUBLIC_KEY=$(cat "$temp_public")
# Move to final location with proper permissions
mv "$temp_private" server_private.key
mv "$temp_public" server_public.key
chmod 600 server_private.key
chmod 644 server_public.key
log_info "Server keys generated and secured"
}
# ============================================================================
# WireGuard Configuration
# ============================================================================
configure_wireguard() {
log_info "Configuring WireGuard interface..."
echo "Configuring WireGuard interface..."
# Create config.d directory
mkdir -p "${CONF_D_DIR}"
# Create server.conf in conf.d directory
cat > "${SERVER_CONF}" <<EOF
[Interface]
PrivateKey = ${SERVER_PRIVATE_KEY}
Address = ${WGI_VPN_IPV4_RANGE%.*}.1/${WGI_VPN_IPV4_RANGE#*/}, ${WGI_VPN_IPV6_RANGE%:*}:1/${WGI_VPN_IPV6_RANGE#*/}
ListenPort = ${WGI_WG_PORT}
EOF
# Set permissions
chmod 600 "${SERVER_CONF}"
chmod 700 "${CONF_D_DIR}"
chmod 600 /etc/wireguard/server_private.key
log_info "WireGuard configuration created"
}
# ============================================================================
# Systemd Service Setup
# ============================================================================
setup_systemd_service() {
log_info "Setting up systemd service..."
echo "Setting up WireGuard systemd service..."
# Create main config file (will be populated by load-clients)
cat > "${WG_CONFIG}" <<EOF
[Interface]
PrivateKey = ${SERVER_PRIVATE_KEY}
Address = ${WGI_VPN_IPV4_RANGE%.*}.1/${WGI_VPN_IPV4_RANGE#*/}, ${WGI_VPN_IPV6_RANGE%:*}:1/${WGI_VPN_IPV6_RANGE#*/}
ListenPort = ${WGI_WG_PORT}
EOF
chmod 600 "${WG_CONFIG}"
# Enable WireGuard service
systemctl enable wg-quick@${WGI_WG_INTERFACE}.service
# Start WireGuard service
echo ""
echo "Starting WireGuard..."
# Ensure interface is down before starting
if wg show ${WGI_WG_INTERFACE} &>/dev/null; then
wg-quick down ${WGI_WG_INTERFACE} 2>/dev/null || true
fi
systemctl start wg-quick@${WGI_WG_INTERFACE}.service
# Wait for WireGuard to initialize
sleep 2
# Verify WireGuard is running
if ! systemctl is-active --quiet wg-quick@${WGI_WG_INTERFACE}.service; then
log_error "WireGuard service failed to start"
echo "=== Service Status ==="
systemctl status wg-quick@${WGI_WG_INTERFACE}.service
exit 1
fi
# Verify correct listening port
ACTUAL_PORT=$(wg show ${WGI_WG_INTERFACE} listen-port)
if [[ "$ACTUAL_PORT" != "$WGI_WG_PORT" ]]; then
log_error "WireGuard listening on port $ACTUAL_PORT instead of $WGI_WG_PORT"
echo "=== Config File ==="
grep ListenPort "${WG_CONFIG}"
echo "=== Running Interface ==="
wg show ${WGI_WG_INTERFACE}
exit 1
fi
echo "WireGuard started successfully on port $ACTUAL_PORT"
log_info "WireGuard service started successfully"
}
# ============================================================================
# Main Installation Function
# ============================================================================
main() {
log_info "=== WireGuard VPN Installation for Debian 13 ==="
# Validate configuration
validate_server_domain "${WGI_SERVER_DOMAIN}"
validate_port_range "${WGI_WG_PORT}"
validate_cidr "${WGI_VPN_IPV4_RANGE}" false
validate_cidr "${WGI_VPN_IPV6_RANGE}" true
validate_dns_servers "${WGI_DNS_SERVERS}"
# Interactive configuration if not all values set
if [[ -z "${WGI_SERVER_DOMAIN}" ]] || \
[[ -z "${WGI_WG_PORT}" ]] || \
[[ -z "${WGI_VPN_IPV4_RANGE}" ]] || \
[[ -z "${WGI_VPN_IPV6_RANGE}" ]]; then
prompt_configuration
fi
# Print configuration
echo ""
echo "=== WireGuard VPN Installation for Debian 13 ==="
echo "Server: ${WGI_SERVER_DOMAIN}"
echo "IPv4 VPN range: ${WGI_VPN_IPV4_RANGE}"
echo "IPv6 VPN range: ${WGI_VPN_IPV6_RANGE}"
echo "Port: ${WGI_WG_PORT}"
echo "Interface: ${WGI_WG_INTERFACE}"
echo ""
# Pre-installation validation
pre_install_validation
# Auto-backup before install (only if config exists)
if [[ -f "${WG_CONFIG}" ]] || [[ -f "/etc/nftables.conf" ]]; then
backup_config "install-pre"
fi
# Enable rollback flag
ROLLBACK_NEEDED=true
# Create backup directory for potential rollback
ROLLBACK_BACKUP_DIR=$(mktemp -d)
log_info "Created rollback backup directory: ${ROLLBACK_BACKUP_DIR}"
# Backup existing configs if they exist
if [[ -f "${WG_CONFIG}" ]]; then
cp "${WG_CONFIG}" "${ROLLBACK_BACKUP_DIR}/wireguard.conf"
log_info "Backed up existing WireGuard config"
fi
if [[ -f "/etc/nftables.conf" ]]; then
cp /etc/nftables.conf "${ROLLBACK_BACKUP_DIR}/nftables.conf"
log_info "Backed up existing nftables config"
fi
if [[ -d "${CONF_D_DIR}" ]]; then
cp -r "${CONF_D_DIR}" "${ROLLBACK_BACKUP_DIR}/conf.d"
log_info "Backed up existing client configs"
fi
# Cleanup existing installation
cleanup_existing_installation
# Install packages
install_packages
# Detect public interface
detect_public_interface
# Enable IP forwarding
enable_ip_forwarding
# Configure nftables firewall
configure_nftables
# Generate server keys
generate_server_keys
# Configure WireGuard
configure_wireguard
# Setup systemd service
setup_systemd_service
# Disable rollback flag - installation successful
ROLLBACK_NEEDED=false
# Clean up rollback backup directory
if [[ -d "${ROLLBACK_BACKUP_DIR}" ]]; then
rm -rf "${ROLLBACK_BACKUP_DIR}"
log_info "Cleaned up rollback backup directory"
fi
# Installation complete
echo ""
log_info "=== Installation Complete ==="
echo "=== Installation Complete ==="
echo ""
echo "Server Public Key: ${SERVER_PUBLIC_KEY}"
echo "Endpoint: ${WGI_SERVER_DOMAIN}:${WGI_WG_PORT}"
echo "VPN IPv4 Range: ${WGI_VPN_IPV4_RANGE}"
echo "VPN IPv6 Range: ${WGI_VPN_IPV6_RANGE}"
echo ""
echo "Use 'wireguard.sh add <name> [--psk]' to add clients"
echo "Use 'wireguard.sh list' to list clients"
echo "Configs will be merged from: ${CONF_D_DIR}"
echo " - ${SERVER_CONF} (server interface)"
echo " - ${CONF_D_DIR}/client-*.conf (client peers)"
echo ""
echo "Check status: wg show"
echo "System status: systemctl status wg-quick@${WGI_WG_INTERFACE}"
echo ""
log_info "Installation completed successfully"
}
# ============================================================================
# Entry Point
# ============================================================================
# Only run main if script is executed directly (not sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
# Show usage if help requested
if [[ "${1:-}" == "-h" ]] || [[ "${1:-}" == "--help" ]] || [[ "${1:-}" == "help" ]]; then
echo "Usage: $0 [options]"
echo ""
echo "Environment Variables (prefix with WGI_):"
echo " WGI_SERVER_DOMAIN Server domain (e.g., vpn.example.com)"
echo " WGI_WG_PORT WireGuard UDP port (default: 51820)"
echo " WGI_VPN_IPV4_RANGE IPv4 VPN range (default: 10.10.69.0/24)"
echo " WGI_VPN_IPV6_RANGE IPv6 VPN range (default: fd69:dead:beef:69::/64)"
echo " WGI_DNS_SERVERS DNS servers (default: 8.8.8.8, 8.8.4.4)"
echo " WGI_WG_INTERFACE WireGuard interface name (default: wg0)"
echo ""
echo "Examples:"
echo " # Interactive installation"
echo " sudo $0"
echo ""
echo " # Non-interactive with environment variables"
echo " sudo WGI_SERVER_DOMAIN=vpn.example.com $0"
echo ""
echo " # Custom port and VPN range"
echo " sudo WGI_SERVER_DOMAIN=vpn.example.com WGI_WG_PORT=443 $0"
exit 0
fi
# Run main installation
main
fi