#!/bin/bash # Minimum Bash version check if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then echo "ERROR: Bash >= 4.0 required (current: ${BASH_VERSION})" >&2; exit 1 fi # ============================================================================== # AmneziaWG 2.0 installation and configuration script for Ubuntu/Debian servers # Author: @bivlked # Version: 5.7.5 # Date: 2026-03-20 # Repository: https://github.com/bivlked/amneziawg-installer # ============================================================================== # --- Safe mode and Constants --- set -o pipefail SCRIPT_VERSION="5.7.5" AWG_DIR="/root/awg" CONFIG_FILE="$AWG_DIR/awgsetup_cfg.init" STATE_FILE="$AWG_DIR/setup_state" LOG_FILE="$AWG_DIR/install_amneziawg.log" KEYS_DIR="$AWG_DIR/keys" SERVER_CONF_FILE="/etc/amnezia/amneziawg/awg0.conf" AWG_BRANCH="${AWG_BRANCH:-v${SCRIPT_VERSION}}" COMMON_SCRIPT_URL="https://raw.githubusercontent.com/bivlked/amneziawg-installer/${AWG_BRANCH}/awg_common_en.sh" COMMON_SCRIPT_PATH="$AWG_DIR/awg_common.sh" MANAGE_SCRIPT_URL="https://raw.githubusercontent.com/bivlked/amneziawg-installer/${AWG_BRANCH}/manage_amneziawg_en.sh" MANAGE_SCRIPT_PATH="$AWG_DIR/manage_amneziawg.sh" # CLI flags UNINSTALL=0; HELP=0; DIAGNOSTIC=0; VERBOSE=0; NO_COLOR=0; AUTO_YES=0; NO_TWEAKS=0 _APT_UPDATED=0 CLI_PORT=""; CLI_SUBNET=""; CLI_DISABLE_IPV6="default" CLI_ROUTING_MODE="default"; CLI_CUSTOM_ROUTES=""; CLI_ENDPOINT=""; CLI_NO_TWEAKS=0 # --- Auto-cleanup of temporary files --- _install_temp_files=() _install_cleanup() { local f for f in "${_install_temp_files[@]}"; do [[ -f "$f" ]] && rm -f "$f"; done # Clean up temporary files from awg_common.sh (if already sourced) type _awg_cleanup &>/dev/null && _awg_cleanup } trap _install_cleanup EXIT INT TERM # --- Argument processing --- while [[ $# -gt 0 ]]; do case $1 in --uninstall) UNINSTALL=1 ;; --help|-h) HELP=1 ;; --diagnostic) DIAGNOSTIC=1 ;; --verbose|-v) VERBOSE=1 ;; --no-color) NO_COLOR=1 ;; --port=*) CLI_PORT="${1#*=}" ;; --subnet=*) CLI_SUBNET="${1#*=}" ;; --allow-ipv6) CLI_DISABLE_IPV6=0 ;; --disallow-ipv6) CLI_DISABLE_IPV6=1 ;; --route-all) CLI_ROUTING_MODE=1 ;; --route-amnezia) CLI_ROUTING_MODE=2 ;; --route-custom=*) CLI_ROUTING_MODE=3; CLI_CUSTOM_ROUTES="${1#*=}" ;; --endpoint=*) CLI_ENDPOINT="${1#*=}" ;; --yes|-y) AUTO_YES=1 ;; --no-tweaks) NO_TWEAKS=1; CLI_NO_TWEAKS=1 ;; *) echo "Unknown argument: $1"; HELP=1 ;; esac shift done # ============================================================================== # Logging functions # ============================================================================== log_msg() { local type="$1" msg="$2" local ts ts=$(date +'%F %T') local safe_msg safe_msg="${msg//%/%%}" local entry="[$ts] $type: $safe_msg" local color_start="" color_end="" if [[ "$NO_COLOR" -eq 0 ]]; then color_end="\033[0m" case "$type" in INFO) color_start="\033[0;32m" ;; WARN) color_start="\033[0;33m" ;; ERROR) color_start="\033[1;31m" ;; DEBUG) color_start="\033[0;36m" ;; *) color_start=""; color_end="" ;; esac fi if ! mkdir -p "$(dirname "$LOG_FILE")" || ! echo "$entry" >> "$LOG_FILE"; then echo "[$ts] ERROR: Log write error $LOG_FILE" >&2 fi if [[ "$type" == "ERROR" || "$type" == "WARN" ]]; then printf "${color_start}%s${color_end}\n" "$entry" >&2 elif [[ "$type" == "DEBUG" && "$VERBOSE" -eq 1 ]]; then printf "${color_start}%s${color_end}\n" "$entry" >&2 elif [[ "$type" == "INFO" ]]; then printf "${color_start}%s${color_end}\n" "$entry" elif [[ "$type" != "DEBUG" ]]; then printf "${color_start}%s${color_end}\n" "$entry" fi } log() { log_msg "INFO" "$1"; } log_warn() { log_msg "WARN" "$1"; } log_error() { log_msg "ERROR" "$1"; } log_debug() { if [[ "$VERBOSE" -eq 1 ]]; then log_msg "DEBUG" "$1"; fi; } die() { log_error "CRITICAL ERROR: $1"; log_error "Installation aborted. Log: $LOG_FILE"; exit 1; } # ============================================================================== # Help # ============================================================================== show_help() { cat << 'EOF' Usage: sudo bash install_amneziawg_en.sh [OPTIONS] Script for installation and configuration of AmneziaWG 2.0 on Ubuntu (24.04 / 25.10) and Debian (12 / 13). Options: -h, --help Show this help and exit --uninstall Uninstall AmneziaWG and all its configurations --diagnostic Generate diagnostic report and exit -v, --verbose Verbose output for debugging (including DEBUG) --no-color Disable colored terminal output --port=NUMBER Set UDP port (1024-65535) non-interactively --subnet=SUBNET Set tunnel subnet (x.x.x.x/yy) non-interactively --allow-ipv6 Keep IPv6 enabled non-interactively --disallow-ipv6 Force-disable IPv6 non-interactively --route-all Use 'All traffic' mode non-interactively --route-amnezia Use 'Amnezia' mode non-interactively --route-custom=NETS Use 'Custom' mode non-interactively --endpoint=IP Specify external server IP (for servers behind NAT) -y, --yes Auto-confirm (reboots, UFW, etc.) --no-tweaks Skip hardening/optimization (no UFW, Fail2Ban, sysctl tweaks) Examples: sudo bash install_amneziawg_en.sh # Interactive installation sudo bash install_amneziawg_en.sh --port=51820 --route-all # Non-interactive sudo bash install_amneziawg_en.sh --route-amnezia --yes # Fully automated sudo bash install_amneziawg_en.sh --uninstall # Uninstall sudo bash install_amneziawg_en.sh --diagnostic # Diagnostics Repository: https://github.com/bivlked/amneziawg-installer EOF exit 0 } # ============================================================================== # Utilities and validation # ============================================================================== update_state() { local next_step=$1 mkdir -p "$(dirname "$STATE_FILE")" # Atomic write with flock to prevent race condition ( flock -x 200 echo "$next_step" > "$STATE_FILE" ) 200>"${STATE_FILE}.lock" || die "Failed to write state" log "State: next step - $next_step" } request_reboot() { local next_step=$1 update_state "$next_step" echo "" >> "$LOG_FILE" log_warn "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" log_warn "!!! SYSTEM REBOOT REQUIRED !!!" log_warn "!!! After reboot, run the script again: !!!" log_warn "!!! sudo bash $0 [with the same parameters, if any] !!!" log_warn "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" echo "" >> "$LOG_FILE" local confirm="y" if [[ "$AUTO_YES" -eq 0 ]]; then read -rp "Reboot now? [y/N]: " confirm < /dev/tty else log "Auto-confirming reboot (--yes)." fi if [[ "$confirm" =~ ^[Yy]$ ]]; then log "Reboot initiated..." sleep 5 if ! reboot; then die "Reboot command failed."; fi exit 1 else log "Reboot cancelled. Reboot manually and run the script again." exit 1 fi } check_os_version() { log "Checking OS..." # Detection via /etc/os-release (universal for Ubuntu and Debian) OS_ID="" OS_VERSION="" OS_CODENAME="" if [[ -f /etc/os-release ]]; then # shellcheck source=/dev/null source /etc/os-release OS_ID="$ID" OS_VERSION="$VERSION_ID" OS_CODENAME="$VERSION_CODENAME" elif command -v lsb_release &>/dev/null; then OS_ID=$(lsb_release -si | tr '[:upper:]' '[:lower:]') OS_VERSION=$(lsb_release -sr) OS_CODENAME=$(lsb_release -sc) else log_warn "Cannot detect OS (/etc/os-release and lsb_release not found)." return 0 fi export OS_ID OS_VERSION OS_CODENAME # Supported OS local supported=0 case "$OS_ID" in ubuntu) if [[ "$OS_VERSION" == "24.04" || "$OS_VERSION" == "25.10" ]]; then supported=1 fi ;; debian) if [[ "$OS_VERSION" == "12" || "$OS_VERSION" == "13" ]]; then supported=1 fi ;; esac if [[ "$supported" -eq 1 ]]; then log "OS: ${OS_ID^} $OS_VERSION ($OS_CODENAME) — supported" else log_warn "Detected $OS_ID $OS_VERSION ($OS_CODENAME). Script tested on Ubuntu 24.04/25.10 and Debian 12/13." if [[ "$AUTO_YES" -eq 0 ]]; then read -rp "Continue? [y/N]: " confirm < /dev/tty if ! [[ "$confirm" =~ ^[Yy]$ ]]; then die "Cancelled."; fi else log "Continuing on $OS_ID $OS_VERSION (--yes)." fi fi } check_free_space() { log "Checking disk space..." local req=2048 local avail avail=$(df -m / | awk 'NR==2 {print $4}') if [[ -z "$avail" ]]; then log_warn "Failed to determine free space." return 0 fi if [ "$avail" -lt "$req" ]; then log_warn "Available $avail MB. Recommended >= $req MB." if [[ "$AUTO_YES" -eq 0 ]]; then read -rp "Continue? [y/N]: " confirm < /dev/tty if ! [[ "$confirm" =~ ^[Yy]$ ]]; then die "Cancelled."; fi else log "Continuing with $avail MB (--yes)." fi else log "Free: $avail MB (OK)" fi } check_port_availability() { local port=$1 log "Checking port $port..." local proc proc=$(ss -lunp | grep -P ":${port}\s") if [[ -n "$proc" ]]; then log_error "Port ${port}/udp already in use! Process: $proc" return 1 else log "Port $port/udp is free." return 0 fi } install_packages() { local packages=("$@") local to_install=() local pkg log "Checking packages: ${packages[*]}..." for pkg in "${packages[@]}"; do if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "ok installed"; then to_install+=("$pkg") fi done if [ ${#to_install[@]} -eq 0 ]; then log "All packages already installed." return 0 fi log "Installing: ${to_install[*]}..." if [[ "${_APT_UPDATED:-0}" -eq 0 ]]; then apt update -y || log_warn "Failed to update apt." _APT_UPDATED=1 fi DEBIAN_FRONTEND=noninteractive apt install -y "${to_install[@]}" || die "Package installation error." log "Packages installed." } cleanup_apt() { log "Cleaning apt..." apt-get clean || log_warn "apt-get clean error" rm -rf /var/lib/apt/lists/* || log_warn "rm /var/lib/apt/lists/* error" log "apt cache cleared." } configure_ipv6() { if [[ "$CLI_DISABLE_IPV6" != "default" ]]; then DISABLE_IPV6=$CLI_DISABLE_IPV6 log "IPv6 from CLI: $DISABLE_IPV6" elif [[ "$AUTO_YES" -eq 1 ]]; then DISABLE_IPV6=1 log "IPv6 disabled (--yes, default)." else read -rp "Disable IPv6 (recommended)? [Y/n]: " dis_ipv6 < /dev/tty if [[ "$dis_ipv6" =~ ^[Nn]$ ]]; then DISABLE_IPV6=0 else DISABLE_IPV6=1 fi fi export DISABLE_IPV6 log "IPv6 disable: $(if [ "$DISABLE_IPV6" -eq 1 ]; then echo 'Yes'; else echo 'No'; fi)" } # Safe configuration loader (whitelist parser, no source/eval) safe_load_config() { local config_file="${1:-$CONFIG_FILE}" if [[ ! -f "$config_file" ]]; then return 1; fi local line key value while IFS= read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^[[:space:]]*# ]] && continue [[ -z "${line// /}" ]] && continue line="${line#export }" if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then key="${BASH_REMATCH[1]}" value="${BASH_REMATCH[2]}" if [[ "$value" == \'*\' ]]; then value="${value#\'}" value="${value%\'}" fi case "$key" in OS_ID|OS_VERSION|OS_CODENAME|AWG_PORT|AWG_TUNNEL_SUBNET|\ DISABLE_IPV6|ALLOWED_IPS_MODE|ALLOWED_IPS|AWG_ENDPOINT|\ AWG_Jc|AWG_Jmin|AWG_Jmax|AWG_S1|AWG_S2|AWG_S3|AWG_S4|\ AWG_H1|AWG_H2|AWG_H3|AWG_H4|AWG_I1|NO_TWEAKS) export "$key=$value" ;; esac fi done < "$config_file" } # Read a single key from config (for point queries) safe_read_config_key() { local key="$1" config_file="${2:-$CONFIG_FILE}" local line while IFS= read -r line || [[ -n "$line" ]]; do line="${line#export }" if [[ "$line" =~ ^${key}=(.*)$ ]]; then local value="${BASH_REMATCH[1]}" if [[ "$value" == \'*\' ]]; then value="${value#\'}" value="${value%\'}" fi echo "$value" return 0 fi done < "$config_file" return 1 } validate_port() { local port="$1" if ! [[ "$port" =~ ^[0-9]+$ ]] || [[ "$port" -lt 1024 ]] || [[ "$port" -gt 65535 ]]; then die "Invalid port: '$port'. Allowed range: 1024-65535." fi } validate_subnet() { local subnet="$1" if ! [[ "$subnet" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/24$ ]] \ || [[ "${BASH_REMATCH[1]}" -gt 255 ]] || [[ "${BASH_REMATCH[2]}" -gt 255 ]] \ || [[ "${BASH_REMATCH[3]}" -gt 255 ]] || [[ "${BASH_REMATCH[4]}" -gt 255 ]]; then die "Invalid subnet: '$subnet'. Only /24 is supported." fi if [[ "${BASH_REMATCH[4]}" -eq 0 ]] || [[ "${BASH_REMATCH[4]}" -eq 255 ]]; then die "Invalid subnet: '$subnet'. Last octet cannot be 0 (network address) or 255 (broadcast)." fi if [[ "${BASH_REMATCH[4]}" -ne 1 ]]; then die "Invalid subnet: '$subnet'. Last octet must be 1 (server address in subnet)." fi } validate_cidr_list() { local input="$1" cidr IFS=',' read -ra cidrs <<< "$input" for cidr in "${cidrs[@]}"; do cidr=$(echo "$cidr" | tr -d ' ') if ! [[ "$cidr" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$ ]] \ || [[ "${BASH_REMATCH[1]}" -gt 255 ]] || [[ "${BASH_REMATCH[2]}" -gt 255 ]] \ || [[ "${BASH_REMATCH[3]}" -gt 255 ]] || [[ "${BASH_REMATCH[4]}" -gt 255 ]] \ || [[ "${BASH_REMATCH[5]}" -gt 32 ]]; then return 1 fi done } configure_routing_mode() { if [[ "$CLI_ROUTING_MODE" != "default" ]]; then ALLOWED_IPS_MODE=$CLI_ROUTING_MODE if [[ "$CLI_ROUTING_MODE" -eq 3 ]]; then ALLOWED_IPS=$CLI_CUSTOM_ROUTES if [ -z "$ALLOWED_IPS" ]; then die "No networks specified for --route-custom."; fi fi log "Routing mode from CLI: $ALLOWED_IPS_MODE" elif [[ "$AUTO_YES" -eq 1 ]]; then ALLOWED_IPS_MODE=2 log "Routing mode: Amnezia+DNS (--yes, default)." else echo "" log "Select routing mode (client AllowedIPs):" echo " 1) All traffic (0.0.0.0/0) - Max privacy, may block LAN" echo " 2) Amnezia List+DNS (default) - Recommended for bypassing restrictions" echo " 3) Only specified networks (Split Tunneling)" read -rp "Your choice [2]: " r_mode < /dev/tty ALLOWED_IPS_MODE=${r_mode:-2} fi case "$ALLOWED_IPS_MODE" in 1) ALLOWED_IPS="0.0.0.0/0" log "Selected mode: All traffic." ;; 3) if [[ -z "$CLI_CUSTOM_ROUTES" ]]; then read -rp "Enter networks (a.b.c.d/xx,...): " ALLOWED_IPS < /dev/tty while ! validate_cidr_list "$ALLOWED_IPS"; do log_warn "Invalid CIDR format: '$ALLOWED_IPS'. Expected: x.x.x.x/y[,x.x.x.x/y]" read -rp "Try again: " ALLOWED_IPS < /dev/tty done else ALLOWED_IPS=$CLI_CUSTOM_ROUTES if ! validate_cidr_list "$ALLOWED_IPS"; then die "Invalid CIDR format: '$ALLOWED_IPS'. Expected: x.x.x.x/y[,x.x.x.x/y]" fi fi log "Selected mode: Custom ($ALLOWED_IPS)" ;; *) ALLOWED_IPS_MODE=2 ALLOWED_IPS="0.0.0.0/5, 8.0.0.0/7, 11.0.0.0/8, 12.0.0.0/6, 16.0.0.0/4, 32.0.0.0/3, 64.0.0.0/2, 128.0.0.0/3, 160.0.0.0/5, 168.0.0.0/6, 172.0.0.0/12, 172.32.0.0/11, 172.64.0.0/10, 172.128.0.0/9, 173.0.0.0/8, 174.0.0.0/7, 176.0.0.0/4, 192.0.0.0/9, 192.128.0.0/11, 192.160.0.0/13, 192.169.0.0/16, 192.170.0.0/15, 192.172.0.0/14, 192.176.0.0/12, 192.192.0.0/10, 193.0.0.0/8, 194.0.0.0/7, 196.0.0.0/6, 200.0.0.0/5, 208.0.0.0/4, 8.8.8.8/32, 1.1.1.1/32" log "Selected mode: Amnezia List+DNS." ;; esac if [ -z "$ALLOWED_IPS" ]; then die "Failed to determine AllowedIPs."; fi export ALLOWED_IPS_MODE ALLOWED_IPS } # ============================================================================== # AWG 2.0 parameter generation (inline — needed in step 0, before downloading awg_common.sh) # ============================================================================== # Random number [min, max] via /dev/urandom (uint32 support) rand_range() { local min=$1 max=$2 local range=$((max - min + 1)) local random_val random_val=$(od -An -tu4 -N4 /dev/urandom | tr -d ' ') if [[ -z "$random_val" || ! "$random_val" =~ ^[0-9]+$ ]]; then # Fallback: combining two $RANDOM for 30-bit range random_val=$(( (RANDOM << 15) | RANDOM )) fi echo $(( (random_val % range) + min )) } # Generate CPS string for I1 # Format: "" where N is the number of random bytes (32-256) generate_cps_i1() { local n n=$(rand_range 32 256) echo "" } # Generate all AWG 2.0 parameters generate_awg_params() { log "Generating AWG 2.0 parameters..." AWG_Jc=$(rand_range 4 8) AWG_Jmin=$(rand_range 40 89) AWG_Jmax=$(( AWG_Jmin + $(rand_range 100 500) )) AWG_S1=$(rand_range 15 150) AWG_S2=$(rand_range 15 150) # Critical kernel constraint: S1+56 != S2 # Prevents init and response messages from having the same size while [[ $((AWG_S1 + 56)) -eq $AWG_S2 ]]; do AWG_S2=$(rand_range 15 150) done AWG_S3=$(rand_range 8 55) AWG_S4=$(rand_range 4 27) # H1-H4: non-overlapping ranges (4 sectors in uint32 space) # AWG itself picks a random value from the range on each handshake AWG_H1="100000-800000" AWG_H2="1000000-8000000" AWG_H3="10000000-80000000" AWG_H4="100000000-800000000" # I1: CPS concealment AWG_I1=$(generate_cps_i1) export AWG_Jc AWG_Jmin AWG_Jmax AWG_S1 AWG_S2 AWG_S3 AWG_S4 export AWG_H1 AWG_H2 AWG_H3 AWG_H4 AWG_I1 log " Jc=$AWG_Jc, Jmin=$AWG_Jmin, Jmax=$AWG_Jmax" log " S1=$AWG_S1, S2=$AWG_S2, S3=$AWG_S3, S4=$AWG_S4" log " H1=$AWG_H1" log " H2=$AWG_H2" log " H3=$AWG_H3" log " H4=$AWG_H4" log " I1=$AWG_I1" log "AWG 2.0 parameters generated." } # ============================================================================== # System optimization (new in v5.0) # ============================================================================== # Detect hardware characteristics detect_hardware() { TOTAL_RAM_MB=$(awk '/MemTotal/ {printf "%.0f", $2/1024}' /proc/meminfo) CPU_CORES=$(nproc) MAIN_NIC=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1); exit}') log "Hardware: RAM=${TOTAL_RAM_MB}MB, CPU=${CPU_CORES} cores, NIC=${MAIN_NIC}" } # Remove unnecessary packages and services cleanup_system() { log "Cleaning system of unnecessary components..." # Packages to remove (safe for VPS) # snapd and lxd-agent-loader — Ubuntu only, not present on Debian local packages_to_remove=() local pkg local cleanup_list="modemmanager networkd-dispatcher unattended-upgrades packagekit udisks2" if [[ "${OS_ID:-ubuntu}" == "ubuntu" ]]; then cleanup_list="snapd $cleanup_list lxd-agent-loader" fi for pkg in $cleanup_list; do if dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "ok installed"; then packages_to_remove+=("$pkg") fi done if [ ${#packages_to_remove[@]} -gt 0 ]; then log "Removing: ${packages_to_remove[*]}" DEBIAN_FRONTEND=noninteractive apt-get purge -y "${packages_to_remove[@]}" || log_warn "Error removing some packages" fi # Cleaning snap artifacts (Ubuntu only) if [[ "${OS_ID:-ubuntu}" == "ubuntu" && -d /snap ]]; then log "Cleaning snap artifacts..." rm -rf /snap /var/snap /var/lib/snapd 2>/dev/null || log_warn "snap cleanup error" fi # cloud-init: remove only if NOT managing network # Conservative approach: check cloud-init markers first, then renderer if dpkg-query -W -f='${Status}' cloud-init 2>/dev/null | grep -q "ok installed"; then local cloud_manages_network=0 # Check cloud-init markers (priority — safety) if ls /etc/netplan/*cloud-init* &>/dev/null 2>&1; then cloud_manages_network=1 elif grep -rq "cloud-init" /etc/netplan/ 2>/dev/null; then cloud_manages_network=1 elif [[ -f /etc/network/interfaces ]] && grep -q "cloud-init" /etc/network/interfaces 2>/dev/null; then cloud_manages_network=1 fi if [[ $cloud_manages_network -eq 0 ]]; then log "Removing cloud-init (network doesn't depend on it)..." DEBIAN_FRONTEND=noninteractive apt-get purge -y cloud-init 2>/dev/null || log_warn "cloud-init removal error" rm -rf /etc/cloud /var/lib/cloud 2>/dev/null else log_warn "cloud-init manages network — skipping removal." fi fi apt-get autoremove -y 2>/dev/null || log_warn "autoremove error" log "System cleanup completed." } # Swap configuration optimize_swap() { log "Optimizing swap..." local target_swap_mb if [[ $TOTAL_RAM_MB -le 2048 ]]; then target_swap_mb=1024 else target_swap_mb=512 fi # Check current swap local current_swap_mb current_swap_mb=$(free -m | awk '/Swap:/ {print $2}') if [[ $current_swap_mb -ge $target_swap_mb ]]; then log "Swap is already sufficient: ${current_swap_mb}MB (target: ${target_swap_mb}MB)" else log "Creating swap file: ${target_swap_mb}MB" # Disable existing swap file if present if [[ -f /swapfile ]]; then swapoff /swapfile 2>/dev/null rm -f /swapfile fi dd if=/dev/zero of=/swapfile bs=1M count="$target_swap_mb" status=none 2>/dev/null || { log_warn "Error creating swap file" return 1 } chmod 600 /swapfile mkswap /swapfile >/dev/null 2>&1 || { log_warn "mkswap error"; return 1; } swapon /swapfile || { log_warn "swapon error"; return 1; } # Add to fstab if missing if ! grep -q '/swapfile' /etc/fstab; then echo '/swapfile none swap sw 0 0' >> /etc/fstab fi log "Swap file created: ${target_swap_mb}MB" fi # Setting swappiness sysctl -w vm.swappiness=10 >/dev/null 2>&1 } # Network interface optimization optimize_nic() { if [[ -z "$MAIN_NIC" ]]; then log_warn "Main NIC not detected, skipping optimization." return 1 fi if ! command -v ethtool &>/dev/null; then log_debug "ethtool not found, skipping NIC optimization." return 0 fi log "NIC optimization: $MAIN_NIC" # Disable GRO/GSO/TSO — may interfere with VPN traffic ethtool -K "$MAIN_NIC" gro off 2>/dev/null || log_debug "GRO: not supported/already off." ethtool -K "$MAIN_NIC" gso off 2>/dev/null || log_debug "GSO: not supported/already off." ethtool -K "$MAIN_NIC" tso off 2>/dev/null || log_debug "TSO: not supported/already off." log "NIC optimization completed." } # Full system optimization optimize_system() { log "Optimizing system for VPN server..." detect_hardware optimize_swap optimize_nic log "System optimization completed." } # ============================================================================== # Sysctl configuration (minimal, for --no-tweaks) # ============================================================================== setup_minimal_sysctl() { log "Configuring minimal sysctl (--no-tweaks)..." local f="/etc/sysctl.d/99-amneziawg-forwarding.conf" cat > "$f" << SYSEOF # AmneziaWG — minimal settings (--no-tweaks) net.ipv4.ip_forward = 1 SYSEOF if [[ "${DISABLE_IPV6:-1}" -eq 1 ]]; then cat >> "$f" << SYSEOF net.ipv6.conf.all.disable_ipv6 = 1 net.ipv6.conf.default.disable_ipv6 = 1 net.ipv6.conf.lo.disable_ipv6 = 1 SYSEOF else cat >> "$f" << SYSEOF net.ipv6.conf.all.forwarding = 1 SYSEOF fi sysctl -p "$f" >/dev/null 2>&1 || log_warn "sysctl -p error" log "Minimal sysctl configured." } # ============================================================================== # Sysctl configuration (extended) # ============================================================================== setup_advanced_sysctl() { log "Configuring sysctl..." local f="/etc/sysctl.d/99-amneziawg-security.conf" # Adaptive buffers based on RAM local rmem_max wmem_max netdev_backlog if [[ ${TOTAL_RAM_MB:-1024} -ge 2048 ]]; then rmem_max=16777216 # 16MB wmem_max=16777216 netdev_backlog=5000 else rmem_max=4194304 # 4MB wmem_max=4194304 netdev_backlog=2500 fi cat > "$f" << EOF # AmneziaWG 2.0 Security/Performance Settings - $(date) # Auto-generated by install_amneziawg_en.sh v${SCRIPT_VERSION} # --- IP Forwarding --- net.ipv4.ip_forward = 1 $(if [[ "${DISABLE_IPV6:-1}" -eq 1 ]]; then echo "net.ipv6.conf.all.disable_ipv6 = 1" echo "net.ipv6.conf.default.disable_ipv6 = 1" echo "net.ipv6.conf.lo.disable_ipv6 = 1" else echo "# IPv6 not disabled" echo "net.ipv6.conf.all.forwarding = 1" fi) # --- TCP/IP Hardening --- net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 net.ipv4.icmp_echo_ignore_broadcasts = 1 net.ipv4.icmp_ignore_bogus_error_responses = 1 net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_max_syn_backlog = 4096 net.ipv4.tcp_synack_retries = 2 net.ipv4.tcp_syn_retries = 5 net.ipv4.tcp_rfc1337 = 1 # --- Redirects --- net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.all.secure_redirects = 0 net.ipv4.conf.default.secure_redirects = 0 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.send_redirects = 0 net.ipv4.conf.all.accept_source_route = 0 net.ipv4.conf.default.accept_source_route = 0 $(if [[ "${DISABLE_IPV6:-1}" -ne 1 ]]; then echo "net.ipv6.conf.all.accept_redirects = 0" echo "net.ipv6.conf.default.accept_redirects = 0" fi) # --- BBR Congestion Control --- net.core.default_qdisc = fq net.ipv4.tcp_congestion_control = bbr # --- Network Buffers (adaptive) --- net.core.rmem_max = ${rmem_max} net.core.wmem_max = ${wmem_max} net.core.netdev_max_backlog = ${netdev_backlog} # --- Conntrack --- net.netfilter.nf_conntrack_max = 65536 # --- Security --- vm.swappiness = 10 kernel.sysrq = 0 EOF log "Applying sysctl..." if ! sysctl -p "$f" >/dev/null 2>&1; then # nf_conntrack may be unavailable before module is loaded log_warn "Some sysctl parameters did not apply (nf_conntrack will be available later)." sysctl -p "$f" 2>/dev/null || true fi } # ============================================================================== # Firewall and security # ============================================================================== setup_improved_firewall() { log "Configuring UFW..." if ! command -v ufw &>/dev/null; then install_packages ufw; fi if ufw status | grep -q inactive; then log "UFW is inactive. Configuring..." ufw default deny incoming ufw default allow outgoing ufw limit 22/tcp comment "SSH Rate Limit" ufw allow "${AWG_PORT}/udp" comment "AmneziaWG VPN" log "UFW rules added." log_warn "--- ENABLING UFW ---" log_warn "Verify SSH access!" local confirm_ufw="y" if [[ "$AUTO_YES" -eq 0 ]]; then sleep 5 read -rp "Enable UFW? [y/N]: " confirm_ufw < /dev/tty else log "Auto-enabling UFW (--yes)." fi if ! [[ "$confirm_ufw" =~ ^[Yy]$ ]]; then log_warn "UFW not enabled." return 1 fi if ! ufw enable <<< "y"; then die "UFW enable error."; fi log "UFW enabled." else log "UFW is active. Updating rules..." ufw limit 22/tcp comment "SSH Rate Limit" ufw allow "${AWG_PORT}/udp" comment "AmneziaWG VPN" ufw reload || log_warn "UFW reload error." log "Rules updated." fi log "UFW configured." log "$(ufw status verbose 2>&1)" return 0 } secure_files() { log "Setting secure file permissions..." chmod 700 "$AWG_DIR" 2>/dev/null chmod 700 /etc/amnezia 2>/dev/null chmod 700 /etc/amnezia/amneziawg 2>/dev/null chmod 600 /etc/amnezia/amneziawg/*.conf 2>/dev/null find "$AWG_DIR" -name "*.conf" -type f -exec chmod 600 {} \; 2>/dev/null find "$AWG_DIR" -name "*.key" -type f -exec chmod 600 {} \; 2>/dev/null find "$AWG_DIR" -name "*.png" -type f -exec chmod 600 {} \; 2>/dev/null if [[ -d "$KEYS_DIR" ]]; then chmod 700 "$KEYS_DIR" 2>/dev/null chmod 600 "$KEYS_DIR"/* 2>/dev/null fi [[ -f "$CONFIG_FILE" ]] && chmod 600 "$CONFIG_FILE" [[ -f "$LOG_FILE" ]] && chmod 640 "$LOG_FILE" [[ -f "$MANAGE_SCRIPT_PATH" ]] && chmod 700 "$MANAGE_SCRIPT_PATH" [[ -f "$COMMON_SCRIPT_PATH" ]] && chmod 700 "$COMMON_SCRIPT_PATH" log "File permissions set." } setup_fail2ban() { log "Configuring Fail2Ban..." if ! command -v fail2ban-client &>/dev/null; then install_packages fail2ban; fi if ! command -v fail2ban-client &>/dev/null; then log_warn "Fail2Ban not installed, skipping." return 1 fi mkdir -p /etc/fail2ban/jail.d 2>/dev/null cat > /etc/fail2ban/jail.d/amneziawg.conf << 'EOF' || { log_warn "jail.d/amneziawg.conf write error"; return 1; } # AmneziaWG — SSH protection (managed by amneziawg-installer) [sshd] enabled = true maxretry = 5 findtime = 10m bantime = 1h banaction = ufw EOF if systemctl restart fail2ban; then log "Fail2Ban configured and restarted." else log_warn "fail2ban restart error" fi return 0 } # ============================================================================== # Service status check # ============================================================================== check_service_status() { log "Checking service status..." local ok=1 if systemctl is-failed --quiet awg-quick@awg0; then log_error "Service FAILED!" ok=0 fi if ! ip addr show awg0 &>/dev/null; then log_error "Interface awg0 not found!" ok=0 fi if ! awg show 2>/dev/null | grep -q "interface: awg0"; then log_error "awg show cannot see interface!" ok=0 fi # Port check local port_check=${AWG_PORT:-0} if [[ "$port_check" -eq 0 ]] && [[ -f "$CONFIG_FILE" ]]; then # shellcheck source=/dev/null port_check=$(safe_read_config_key "AWG_PORT" "$CONFIG_FILE") port_check=${port_check:-0} fi if [[ "$port_check" -ne 0 ]]; then if ! ss -lunp | grep -qP ":${port_check}\s"; then log_error "Port $port_check/udp is not listening!" ok=0 fi fi # AWG 2.0 parameter check if awg show awg0 2>/dev/null | grep -q "jc:"; then log "AWG 2.0 parameters active." else log_warn "AWG 2.0 parameters not detected in awg show." fi if [[ "$ok" -eq 1 ]]; then log "Service and interface status OK." return 0 else return 1 fi } # ============================================================================== # Diagnostics # ============================================================================== create_diagnostic_report() { log "Creating diagnostics..." local rf rf="$AWG_DIR/diag_$(date +%F_%T).txt" { echo "=== AMNEZIAWG 2.0 DIAGNOSTIC REPORT ===" echo "Generated: $(date)" echo "Hostname: $(hostname)" echo "Installer: v${SCRIPT_VERSION}" echo "" echo "--- OS ---" lsb_release -ds 2>/dev/null || cat /etc/os-release uname -a echo "" echo "--- Hardware ---" echo "RAM: $(awk '/MemTotal/ {printf "%.0f MB", $2/1024}' /proc/meminfo)" echo "CPU: $(nproc) cores" echo "Swap: $(free -m | awk '/Swap:/ {print $2}') MB" echo "" echo "--- Configuration ($CONFIG_FILE) ---" cat "$CONFIG_FILE" 2>/dev/null || echo "File not found" echo "" echo "--- Server Config ($SERVER_CONF_FILE) ---" # Mask private key if [[ -f "$SERVER_CONF_FILE" ]]; then sed 's/PrivateKey = .*/PrivateKey = [HIDDEN]/' "$SERVER_CONF_FILE" else echo "File not found" fi echo "" echo "--- Service Status ---" systemctl status awg-quick@awg0 --no-pager -l 2>/dev/null || echo "Service not found" echo "" echo "--- AWG Status ---" awg show 2>/dev/null || echo "awg show failed" echo "" echo "--- AWG Version ---" awg --version 2>/dev/null || echo "awg --version failed" echo "" echo "--- Network Interfaces ---" ip a 2>/dev/null echo "" echo "--- Listening Ports ---" ss -lunp 2>/dev/null echo "" echo "--- Firewall Status ---" if command -v ufw &>/dev/null; then ufw status verbose; else echo "UFW N/A"; fi echo "" echo "--- Routing Table ---" ip route 2>/dev/null echo "" echo "--- Kernel Params ---" sysctl net.ipv4.ip_forward net.ipv6.conf.all.disable_ipv6 2>/dev/null echo "" echo "--- AWG Journal (last 50) ---" journalctl -u awg-quick@awg0 -n 50 --no-pager --output=cat 2>/dev/null || echo "N/A" echo "" echo "--- Client List ---" grep "^#_Name = " "$SERVER_CONF_FILE" 2>/dev/null | sed 's/^#_Name = //' || echo "N/A" echo "" echo "--- DKMS Status ---" dkms status 2>/dev/null || echo "N/A" echo "" echo "--- Module Info ---" modinfo amneziawg 2>/dev/null || echo "N/A" echo "" echo "=== END ===" } > "$rf" || log_error "Report write error." chmod 600 "$rf" || log_warn "Report chmod error." log "Report: $rf" } # ============================================================================== # Uninstall # ============================================================================== step_uninstall() { log "### AMNEZIAWG UNINSTALL ###" echo "" echo "WARNING! Complete removal of AmneziaWG and configurations." echo "This process is irreversible!" echo "" local confirm="" backup="Y" if [[ "$AUTO_YES" -eq 0 ]]; then read -rp "Are you sure? (type 'yes'): " confirm < /dev/tty if [[ "$confirm" != "yes" ]]; then log "Uninstall cancelled."; exit 1; fi read -rp "Create backup before removal? [Y/n]: " backup < /dev/tty else log "Auto-confirming uninstall (--yes)." fi if [[ -z "$backup" || "$backup" =~ ^[Yy]$ ]]; then local bf bf="$HOME/awg_uninstall_backup_$(date +%F_%T).tar.gz" log "Creating backup: $bf" tar -czf "$bf" -C / etc/amnezia "$AWG_DIR" --ignore-failed-read 2>/dev/null || log_warn "Backup creation error $bf" chmod 600 "$bf" || log_warn "Backup chmod error" log "Backup created: $bf" fi # Load --no-tweaks flag from saved configuration local saved_no_tweaks=0 if [[ -f "$CONFIG_FILE" ]]; then # shellcheck source=/dev/null saved_no_tweaks=$(safe_read_config_key "NO_TWEAKS" "$CONFIG_FILE" 2>/dev/null) || saved_no_tweaks=0 saved_no_tweaks=${saved_no_tweaks:-0} fi log "Stopping service..." systemctl stop awg-quick@awg0 2>/dev/null systemctl disable awg-quick@awg0 2>/dev/null modprobe -r amneziawg 2>/dev/null || true if [[ "$saved_no_tweaks" -eq 0 ]]; then log "Disabling UFW (priority — preserve SSH access)..." if command -v ufw &>/dev/null; then ufw --force disable 2>/dev/null local port_to_del if [[ -f "$CONFIG_FILE" ]]; then # shellcheck source=/dev/null port_to_del=$(safe_read_config_key "AWG_PORT" "$CONFIG_FILE") fi port_to_del=${port_to_del:-39743} ufw delete allow "${port_to_del}/udp" 2>/dev/null fi log "Removing Fail2Ban bans..." if command -v fail2ban-client &>/dev/null; then fail2ban-client unban --all 2>/dev/null || true systemctl stop fail2ban 2>/dev/null fi else log "Skipping UFW/Fail2Ban (installed with --no-tweaks)." fi log "Removing packages..." if [[ "$saved_no_tweaks" -eq 0 ]]; then DEBIAN_FRONTEND=noninteractive apt-get purge -y amneziawg-dkms amneziawg-tools fail2ban qrencode 2>/dev/null || log_warn "Purge error." else DEBIAN_FRONTEND=noninteractive apt-get purge -y amneziawg-dkms amneziawg-tools qrencode 2>/dev/null || log_warn "Purge error." fi DEBIAN_FRONTEND=noninteractive apt-get autoremove -y 2>/dev/null || log_warn "Autoremove error." log "Removing PPA and files..." rm -f /etc/apt/sources.list.d/amnezia-ppa.sources \ /etc/apt/sources.list.d/amnezia-ppa.list \ /etc/apt/sources.list.d/amnezia-ubuntu-ppa-*.list \ /etc/apt/sources.list.d/amnezia-ubuntu-ppa-*.sources \ /etc/apt/keyrings/amnezia-ppa.gpg 2>/dev/null rm -rf /etc/amnezia \ /etc/modules-load.d/amneziawg.conf \ /etc/sysctl.d/99-amneziawg-security.conf \ /etc/sysctl.d/99-amneziawg-forwarding.conf \ /etc/logrotate.d/amneziawg* || log_warn "File removal error." if [[ "$saved_no_tweaks" -eq 0 ]]; then # Remove only our fail2ban artifacts rm -f /etc/fail2ban/jail.d/amneziawg.conf 2>/dev/null # Backward compatibility: remove jail.local if created by our installer if [[ -f /etc/fail2ban/jail.local ]] && grep -q "banaction = ufw" /etc/fail2ban/jail.local 2>/dev/null; then rm -f /etc/fail2ban/jail.local fi fi log "Removing DKMS..." rm -rf /var/lib/dkms/amneziawg* || log_warn "DKMS removal error." log "Restoring sysctl..." if grep -q "disable_ipv6" /etc/sysctl.conf 2>/dev/null; then sed -i '/disable_ipv6/d' /etc/sysctl.conf || log_warn "sed sysctl.conf error" fi sysctl -p --system 2>/dev/null rm -f /etc/apt/sources.list.d/*.bak-* "$AWG_DIR"/ubuntu.sources.bak-* 2>/dev/null || true log "Removing cron and scripts..." rm -f /etc/cron.d/awg-expiry 2>/dev/null log "=== UNINSTALL COMPLETED ===" # Copy log and remove working directory cp "$LOG_FILE" "$HOME/awg_uninstall.log" 2>/dev/null || true rm -rf "$AWG_DIR" 2>/dev/null || true exit 0 } # ============================================================================== # STEP 0: Initialization # ============================================================================== initialize_setup() { mkdir -p "$AWG_DIR" || die "Error creating $AWG_DIR" chown root:root "$AWG_DIR" touch "$LOG_FILE" || die "Failed to create log file $LOG_FILE" chmod 640 "$LOG_FILE" log "--- STARTING AmneziaWG 2.0 INSTALLATION (v${SCRIPT_VERSION}) ---" log "### STEP 0: Initialization and parameter check ###" if [ "$(id -u)" -ne 0 ]; then die "Run the script as root (sudo bash $0)."; fi cd "$AWG_DIR" || die "Error changing to $AWG_DIR" log "Working directory: $AWG_DIR" log "Log file: $LOG_FILE" check_os_version check_free_space local default_port=39743 local default_subnet="10.9.9.1/24" local config_exists=0 # Variable initialization AWG_PORT=$default_port AWG_TUNNEL_SUBNET=$default_subnet DISABLE_IPV6="default" ALLOWED_IPS_MODE="default" ALLOWED_IPS="" AWG_ENDPOINT="" # Load config if [[ -f "$CONFIG_FILE" ]]; then log "Configuration file found $CONFIG_FILE. Loading settings..." config_exists=1 # shellcheck source=/dev/null safe_load_config "$CONFIG_FILE" || log_warn "Failed to fully load settings from $CONFIG_FILE." AWG_PORT=${AWG_PORT:-$default_port} AWG_TUNNEL_SUBNET=${AWG_TUNNEL_SUBNET:-$default_subnet} DISABLE_IPV6=${DISABLE_IPV6:-"default"} ALLOWED_IPS_MODE=${ALLOWED_IPS_MODE:-"default"} ALLOWED_IPS=${ALLOWED_IPS:-""} AWG_ENDPOINT=${AWG_ENDPOINT:-""} log "Settings loaded from file." else log "Configuration file $CONFIG_FILE not found." fi # CLI override AWG_PORT=${CLI_PORT:-$AWG_PORT} AWG_TUNNEL_SUBNET=${CLI_SUBNET:-$AWG_TUNNEL_SUBNET} if [[ "$CLI_DISABLE_IPV6" != "default" ]]; then DISABLE_IPV6=$CLI_DISABLE_IPV6; fi if [[ "$CLI_ROUTING_MODE" != "default" ]]; then ALLOWED_IPS_MODE=$CLI_ROUTING_MODE if [[ "$CLI_ROUTING_MODE" -eq 3 ]]; then ALLOWED_IPS=$CLI_CUSTOM_ROUTES; fi fi if [[ -n "$CLI_ENDPOINT" ]]; then AWG_ENDPOINT=$CLI_ENDPOINT; fi if [[ "$CLI_NO_TWEAKS" -eq 1 ]]; then NO_TWEAKS=1; fi # Validate after CLI override validate_port "$AWG_PORT" validate_subnet "$AWG_TUNNEL_SUBNET" # Request settings from user only on first run if [[ "$config_exists" -eq 0 ]]; then log "Requesting settings from user (first run)." if [[ "$AUTO_YES" -eq 0 ]]; then read -rp "Enter AmneziaWG UDP port (1024-65535) [${AWG_PORT}]: " input_port < /dev/tty if [[ -n "$input_port" ]]; then AWG_PORT=$input_port; fi fi validate_port "$AWG_PORT" if [[ "$AUTO_YES" -eq 0 ]]; then read -rp "Enter tunnel subnet [${AWG_TUNNEL_SUBNET}]: " input_subnet < /dev/tty if [[ -n "$input_subnet" ]]; then AWG_TUNNEL_SUBNET=$input_subnet; fi fi validate_subnet "$AWG_TUNNEL_SUBNET" if [[ "$DISABLE_IPV6" == "default" ]]; then configure_ipv6; fi if [[ "$ALLOWED_IPS_MODE" == "default" ]]; then configure_routing_mode; fi else log "Using settings from $CONFIG_FILE." if [[ "$ALLOWED_IPS_MODE" == "3" ]] && [[ -n "$ALLOWED_IPS" ]]; then if ! validate_cidr_list "$ALLOWED_IPS"; then die "Invalid ALLOWED_IPS in config: '$ALLOWED_IPS'. Delete $CONFIG_FILE and re-run the installer." fi fi fi # Default values if [[ "$DISABLE_IPV6" == "default" ]]; then DISABLE_IPV6=1; fi if [[ "$ALLOWED_IPS_MODE" == "default" ]]; then ALLOWED_IPS_MODE=2; fi if [[ -z "$ALLOWED_IPS" ]]; then configure_routing_mode; fi # Port check (skip if AWG service is already listening on this port) if ! systemctl is-active --quiet awg-quick@awg0 2>/dev/null; then check_port_availability "$AWG_PORT" || die "Port $AWG_PORT/udp is occupied." else log "AWG service is active — skipping port check." fi # AWG 2.0 parameter generation (first run only) if [[ -z "${AWG_Jc:-}" ]]; then generate_awg_params else log "AWG 2.0 parameters already set from config." fi # Save configuration log "Saving settings to $CONFIG_FILE..." local temp_conf temp_conf=$(mktemp) || die "mktemp error." _install_temp_files+=("$temp_conf") cat > "$temp_conf" << EOF # AmneziaWG 2.0 installation configuration (Auto-generated) # Used by installation and management scripts export OS_ID='${OS_ID:-ubuntu}' export OS_VERSION='${OS_VERSION:-}' export OS_CODENAME='${OS_CODENAME:-}' export AWG_PORT=${AWG_PORT} export AWG_TUNNEL_SUBNET='${AWG_TUNNEL_SUBNET}' export DISABLE_IPV6=${DISABLE_IPV6} export ALLOWED_IPS_MODE=${ALLOWED_IPS_MODE} export ALLOWED_IPS='$(echo "$ALLOWED_IPS" | sed 's/\\,/,/g')' export AWG_ENDPOINT='${AWG_ENDPOINT}' # AWG 2.0 Parameters export AWG_Jc=${AWG_Jc} export AWG_Jmin=${AWG_Jmin} export AWG_Jmax=${AWG_Jmax} export AWG_S1=${AWG_S1} export AWG_S2=${AWG_S2} export AWG_S3=${AWG_S3} export AWG_S4=${AWG_S4} export AWG_H1='${AWG_H1}' export AWG_H2='${AWG_H2}' export AWG_H3='${AWG_H3}' export AWG_H4='${AWG_H4}' export AWG_I1='${AWG_I1}' export NO_TWEAKS=${NO_TWEAKS} EOF if ! mv "$temp_conf" "$CONFIG_FILE"; then rm -f "$temp_conf" die "Error saving $CONFIG_FILE" fi chmod 600 "$CONFIG_FILE" || log_warn "chmod $CONFIG_FILE error" log "Settings saved." export AWG_PORT AWG_TUNNEL_SUBNET DISABLE_IPV6 ALLOWED_IPS_MODE ALLOWED_IPS AWG_ENDPOINT log "Port: ${AWG_PORT}/udp" log "Subnet: ${AWG_TUNNEL_SUBNET}" log "IPv6 disable: $DISABLE_IPV6" log "AllowedIPs mode: $ALLOWED_IPS_MODE" # Loading state if [[ -f "$STATE_FILE" ]]; then current_step=$(cat "$STATE_FILE") if ! [[ "$current_step" =~ ^[0-9]+$ ]]; then log_warn "$STATE_FILE corrupted." current_step=1 update_state 1 else log "Resuming from step $current_step." fi else current_step=1 log "Starting from step 1." update_state 1 fi log "Step 0 completed." } # ============================================================================== # STEP 1: System update, cleanup, and optimization # ============================================================================== step1_update_and_optimize() { update_state 1 log "### STEP 1: System update, cleanup, and optimization ###" # Clean unnecessary components (BEFORE update to save bandwidth/time) if [[ "$NO_TWEAKS" -eq 0 ]]; then cleanup_system else log "Skipping system cleanup (--no-tweaks)." fi log "Updating package lists..." apt update -y || die "apt update error." log "Unlocking dpkg..." if ! apt-get check &>/dev/null; then log_warn "dpkg locked or corrupted, fixing..." DEBIAN_FRONTEND=noninteractive dpkg --configure -a || log_warn "dpkg --configure -a." fi log "Updating system..." DEBIAN_FRONTEND=noninteractive apt full-upgrade -y || die "apt full-upgrade error." log "System updated." install_packages curl wget gpg sudo ethtool if [[ "$NO_TWEAKS" -eq 0 ]]; then # System optimization optimize_system # Sysctl configuration setup_advanced_sysctl else log "Skipping optimization and hardening (--no-tweaks)." setup_minimal_sysctl fi log "Step 1 completed successfully." request_reboot 2 } # ============================================================================== # STEP 2: Installing AmneziaWG and dependencies # ============================================================================== step2_install_amnezia() { update_state 2 log "### STEP 2: Installing AmneziaWG and dependencies ###" _APT_UPDATED=0 # Reset: new sources will be added in this step # Enabling deb-src (Ubuntu only — Ubuntu uses ubuntu.sources) local sources_file="/etc/apt/sources.list.d/ubuntu.sources" if [[ "${OS_ID:-ubuntu}" == "ubuntu" ]]; then log "Checking/enabling deb-src..." if [[ -f "$sources_file" ]]; then if grep -q "^Types: deb$" "$sources_file"; then log "Enabling deb-src..." local bak bak="${AWG_DIR}/ubuntu.sources.bak-$(date +%F_%H%M%S)" cp "$sources_file" "$bak" || log_warn "Backup error" local tmp_sed tmp_sed=$(mktemp) _install_temp_files+=("$tmp_sed") sed '/^Types: deb$/s/Types: deb/Types: deb deb-src/' "$sources_file" > "$tmp_sed" || { rm -f "$tmp_sed"; die "sed error." } if ! mv "$tmp_sed" "$sources_file"; then rm -f "$tmp_sed"; die "mv $sources_file error" fi apt update -y || die "apt update error." else apt update -y fi else log_warn "$sources_file not found, skipping deb-src." apt update -y fi else # Debian: deb-src is usually already configured or not needed log "Debian: skipping deb-src configuration." apt update -y fi # PPA Amnezia (without software-properties-common) log "Adding Amnezia PPA..." # Determine codename for PPA # On Debian, map to nearest Ubuntu codename since PPA is Launchpad (Ubuntu) # Debian 12 (bookworm) → focal, Debian 13 (trixie) → noble local codename ppa_codename codename="${OS_CODENAME:-$(lsb_release -sc 2>/dev/null || echo "noble")}" case "${OS_ID:-ubuntu}" in debian) case "$codename" in bookworm) ppa_codename="focal" ;; trixie) ppa_codename="noble" ;; *) ppa_codename="noble" ;; esac log "Debian ($codename) → PPA codename: $ppa_codename" ;; *) ppa_codename="$codename" ;; esac local keyring_dir="/etc/apt/keyrings" local keyring_file="${keyring_dir}/amnezia-ppa.gpg" local ppa_sources="/etc/apt/sources.list.d/amnezia-ppa.sources" local ppa_list="/etc/apt/sources.list.d/amnezia-ppa.list" # Check for legacy files (from add-apt-repository of previous versions) local legacy_list="/etc/apt/sources.list.d/amnezia-ubuntu-ppa-${codename}.list" local legacy_sources="/etc/apt/sources.list.d/amnezia-ubuntu-ppa-${codename}.sources" if [[ -f "$legacy_list" ]] || [[ -f "$legacy_sources" ]]; then log "PPA already added (legacy format)." elif [[ -f "$ppa_sources" ]] || [[ -f "$ppa_list" ]]; then log "PPA already added." else mkdir -p "$keyring_dir" log "Importing Amnezia PPA GPG key..." curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x57290828" \ | gpg --dearmor -o "$keyring_file" \ || die "Amnezia PPA GPG key import error." chmod 644 "$keyring_file" # Debian 12 uses traditional .list format, Debian 13+ and Ubuntu 24.04+ use DEB822 .sources if [[ "${OS_ID:-ubuntu}" == "debian" && "${OS_VERSION}" == "12" ]]; then log "Debian 12: using traditional .list format" echo "deb [signed-by=${keyring_file}] https://ppa.launchpadcontent.net/amnezia/ppa/ubuntu ${ppa_codename} main" \ > "$ppa_list" || die "Failed to create $ppa_list" chmod 644 "$ppa_list" else cat > "$ppa_sources" </dev/null || apt-cache show "$current_headers" &>/dev/null 2>&1; then packages+=("$current_headers") else log_warn "No headers for $(uname -r), installing generic package..." if [[ "${OS_ID:-ubuntu}" == "debian" ]]; then # On Debian: linux-headers-amd64 (or linux-headers-$(dpkg --print-architecture)) local arch_pkg arch_pkg="linux-headers-$(dpkg --print-architecture 2>/dev/null || echo "amd64")" packages+=("$arch_pkg") else packages+=("linux-headers-generic") fi fi install_packages "${packages[@]}" # DKMS status log "Checking DKMS status..." local dkms_stat dkms_stat=$(dkms status 2>&1) if ! echo "$dkms_stat" | grep -q 'amneziawg.*installed'; then log_warn "DKMS status not OK." log_msg "WARN" "$dkms_stat" else log "DKMS status OK." fi log "Step 2 completed." request_reboot 3 } # ============================================================================== # STEP 3: Kernel module check # ============================================================================== step3_check_module() { update_state 3 log "### STEP 3: Kernel module check ###" sleep 2 if ! lsmod | grep -q -w amneziawg; then log "Module not loaded. Loading..." modprobe amneziawg || die "modprobe amneziawg error." log "Module loaded." local mf="/etc/modules-load.d/amneziawg.conf" mkdir -p "$(dirname "$mf")" if ! grep -qxF 'amneziawg' "$mf" 2>/dev/null; then echo "amneziawg" > "$mf" || log_warn "Write error $mf" log "Added to $mf." fi else log "amneziawg module loaded." fi log "Module information:" modinfo amneziawg | grep -E "filename|version|vermagic|srcversion" | while IFS= read -r line; do log " $line" done local cv kr cv=$(modinfo amneziawg 2>/dev/null | grep vermagic | awk '{print $2}') || cv="?" kr=$(uname -r) if [[ "$cv" != "$kr" ]]; then log_warn "VerMagic MISMATCH: Module($cv) != Kernel($kr)!" else log "VerMagic matches." fi # Check awg version if command -v awg &>/dev/null; then local awg_ver awg_ver=$(awg --version 2>/dev/null || echo "unknown") log "awg version: $awg_ver" else log_warn "awg command not found!" fi log "Step 3 completed." update_state 4 } # ============================================================================== # STEP 4: Firewall configuration # ============================================================================== step4_setup_firewall() { update_state 4 if [[ "$NO_TWEAKS" -eq 0 ]]; then log "### STEP 4: UFW firewall configuration ###" install_packages ufw setup_improved_firewall || die "UFW configuration error." log "Step 4 completed." else log "### STEP 4: Skipping UFW configuration (--no-tweaks) ###" fi update_state 5 } # ============================================================================== # STEP 5: Downloading scripts (NO Python!) # ============================================================================== step5_download_scripts() { update_state 5 log "### STEP 5: Downloading management scripts ###" cd "$AWG_DIR" || die "Error changing to $AWG_DIR" # Downloading awg_common.sh log "Downloading $COMMON_SCRIPT_PATH..." if curl -fLso "$COMMON_SCRIPT_PATH" --max-time 60 --retry 2 "$COMMON_SCRIPT_URL"; then chmod 700 "$COMMON_SCRIPT_PATH" || die "chmod awg_common.sh error" log "awg_common.sh downloaded." else die "awg_common.sh download error" fi # Downloading manage_amneziawg.sh log "Downloading $MANAGE_SCRIPT_PATH..." if curl -fLso "$MANAGE_SCRIPT_PATH" --max-time 60 --retry 2 "$MANAGE_SCRIPT_URL"; then chmod 700 "$MANAGE_SCRIPT_PATH" || die "chmod manage_amneziawg.sh error" log "manage_amneziawg.sh downloaded." else die "manage_amneziawg.sh download error" fi log "Step 5 completed." update_state 6 } # ============================================================================== # STEP 6: Config generation (native, without awgcfg.py) # ============================================================================== step6_generate_configs() { update_state 6 log "### STEP 6: AWG 2.0 config generation ###" cd "$AWG_DIR" || die "cd $AWG_DIR error" # Load shared library if [[ ! -f "$COMMON_SCRIPT_PATH" ]]; then die "awg_common.sh not found. Step 5 not completed?" fi # shellcheck source=/dev/null source "$COMMON_SCRIPT_PATH" # Create key directory mkdir -p "$KEYS_DIR" || die "Error creating $KEYS_DIR" # Generate server keys (if not yet present) if [[ ! -f "$AWG_DIR/server_private.key" ]]; then log "Generating server keys..." generate_server_keys || die "Server key generation error." else log "Server keys already exist." fi # Backup existing server config BEFORE overwriting if [[ -f "$SERVER_CONF_FILE" ]]; then local s_bak s_bak="${SERVER_CONF_FILE}.bak-$(date +%F_%H%M%S)" cp "$SERVER_CONF_FILE" "$s_bak" || log_warn "Backup error $s_bak" log "Server config backup: $s_bak" fi # Create AWG 2.0 server config log "Creating server config..." render_server_config || die "Server config creation error." # Generate default clients log "Creating default clients..." local client_name for client_name in my_phone my_laptop; do if grep -qxF "#_Name = ${client_name}" "$SERVER_CONF_FILE" 2>/dev/null; then log "Client '$client_name' already exists." else log "Creating client '$client_name'..." generate_client "$client_name" || log_warn "Client creation error '$client_name'" fi done # Config validation validate_awg_config || log_warn "Config validation found issues." # Set file permissions secure_files log "Configuration files in $AWG_DIR:" ls -la "$AWG_DIR"/*.conf "$AWG_DIR"/*.png 2>/dev/null | while IFS= read -r line; do log " $line" done log "Step 6 completed." update_state 7 } # ============================================================================== # STEP 7: Service startup # ============================================================================== step7_start_service() { update_state 7 log "### STEP 7: Service startup and security configuration ###" log "Enabling and starting awg-quick@awg0..." systemctl enable --now awg-quick@awg0 || die "enable --now error." log "Service enabled and started." log "Checking service status..." local _attempt for _attempt in 1 2 3 4 5; do sleep 1 check_service_status 2>/dev/null && break [[ $_attempt -lt 5 ]] && log_debug "Waiting for service startup... (attempt $_attempt/5)" done check_service_status || die "Service status check failed." # Fail2Ban if [[ "$NO_TWEAKS" -eq 0 ]]; then setup_fail2ban else log "Skipping Fail2Ban (--no-tweaks)." fi log "Step 7 completed successfully." update_state 99 } # ============================================================================== # STEP 99: Completion # ============================================================================== step99_finish() { log "### INSTALLATION COMPLETE ###" log "==============================================================================" log "AmneziaWG 2.0 installation and configuration COMPLETED SUCCESSFULLY!" log " " log "CLIENT FILES:" log " Configs (.conf) and QR codes (.png) in: $AWG_DIR" log " Copy them securely." log " Example (on your PC):" log " scp root@:$AWG_DIR/*.conf ./" log " " log "USEFUL COMMANDS:" log " sudo bash $MANAGE_SCRIPT_PATH help # Client management" log " systemctl status awg-quick@awg0 # VPN status" log " awg show # AmneziaWG status" log " ufw status verbose # Firewall status" log " " log "IMPORTANT: Use Amnezia VPN client >= 4.8.12.7 to connect" log " with AWG 2.0 protocol support" log " " cleanup_apt log " " # Final checks if [[ -f "$CONFIG_FILE" ]]; then log "Settings file $CONFIG_FILE: OK" else log_error "Settings file $CONFIG_FILE MISSING!" fi # Remove state file log "Removing installation state file..." rm -f "$STATE_FILE" "${STATE_FILE}.lock" || log_warn "Failed to remove $STATE_FILE" log "Installation fully completed. Log: $LOG_FILE" log "==============================================================================" } # ============================================================================== # Main execution loop # ============================================================================== if [[ "$HELP" -eq 1 ]]; then show_help; fi if [[ "$UNINSTALL" -eq 1 ]]; then step_uninstall; fi if [[ "$DIAGNOSTIC" -eq 1 ]]; then create_diagnostic_report; exit 0; fi if [[ "$VERBOSE" -eq 1 ]]; then set -x; fi initialize_setup while (( current_step < 99 )); do log "Executing step $current_step..." case $current_step in 1) step1_update_and_optimize ;; 2) step2_install_amnezia ;; 3) step3_check_module; current_step=4 ;; 4) step4_setup_firewall; current_step=5 ;; 5) step5_download_scripts; current_step=6 ;; 6) step6_generate_configs; current_step=7 ;; 7) step7_start_service; current_step=99 ;; *) die "Error: Unknown step $current_step." ;; esac done if (( current_step == 99 )); then step99_finish; fi exit 0