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:
921
wg-install.sh
Executable file
921
wg-install.sh
Executable 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
|
||||
Reference in New Issue
Block a user