#!/bin/bash #================================================================ # Project: V2Ray All-in-One Deployment Script # Author: Gemini & User # Version: 3.0 (Fully Non-Interactive & Documented) # Description: Automates the deployment of V2Ray with WebSocket, # TLS, Nginx, and generates a Clash configuration. # This script is designed for Debian/Ubuntu systems. #================================================================ # # --- User Guide --- # # This script supports two modes of operation: # # 1. Interactive Mode (Default): # Simply run the script with sudo, and it will prompt you for all # necessary information. # $ sudo ./deploy_v2ray.sh # # 2. Non-Interactive Mode (for Automation): # Set the required configuration as environment variables before running # the script. This is ideal for use with secrets management tools like # 1Password CLI (op), Doppler, or in CI/CD pipelines. # # Example with 1Password CLI: # $ op run --env-file=.env -- sudo ./deploy_v2ray.sh # # --- Environment Variables --- # # To run in non-interactive mode, set the following variables. # For yes/no questions, 'y' means yes, anything else means no. # # [Core Configuration] # V2RAY_DOMAIN # Your domain name (e.g., v2.example.com). Required. # V2RAY_EMAIL # Your email for SSL certificates. Required. # V2RAY_UUID # Your V2Ray UUID. Optional, will be generated if not set. # # [Cloudflare DNS Automation] # V2RAY_USE_CF_DNS # Set to 'y' to enable. If not set, will ask interactively. # CF_API_TOKEN # Your Cloudflare API Token. Required if V2RAY_USE_CF_DNS=y. # CF_ZONE_ID # Your Cloudflare Zone ID. Required if V2RAY_USE_CF_DNS=y. # # [GitHub Gist Subscription] # V2RAY_USE_GIST # Set to 'y' to enable. If not set, will ask interactively. # GITHUB_USER # Your GitHub username. Required if V2RAY_USE_GIST=y. # GITHUB_TOKEN # Your GitHub Personal Access Token (with 'gist' scope). # # Required if V2RAY_USE_GIST=y. # #================================================================ # --- Color Codes --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # --- State File --- STATE_FILE="/root/.v2ray_deployment_state" # --- Script functions --- # Function to print error messages and exit error_exit() { echo -e "${RED}Error: $1${NC}" >&2 exit 1 } # Function to check if running as root check_root() { if [ "$(id -u)" -ne 0 ]; then error_exit "This script must be run as root. Please use sudo." fi } # Function to run pre-flight checks and install essential tools pre_flight_checks() { echo -e "${BLUE}Running pre-flight checks and installing essential tools...${NC}" apt-get update apt-get install -y curl wget jq socat unzip || error_exit "Failed to install essential tools. Please check your network and apt sources." } # Function to get user input get_user_input() { # Load existing state if available if [ -f "${STATE_FILE}" ]; then source "${STATE_FILE}" fi echo -e "${BLUE}--- V2Ray Deployment Setup ---${NC}" # Check for Domain from env if [ -n "$V2RAY_DOMAIN" ]; then echo -e "${GREEN}Domain found in environment variables: ${V2RAY_DOMAIN}${NC}" DOMAIN="$V2RAY_DOMAIN" else read -p "Enter your domain name (e.g., v2.example.com): " DOMAIN fi if [ -z "${DOMAIN}" ]; then error_exit "Domain name cannot be empty." fi # Check for Email from env if [ -n "$V2RAY_EMAIL" ]; then echo -e "${GREEN}Email found in environment variables: ${V2RAY_EMAIL}${NC}" EMAIL="$V2RAY_EMAIL" else read -p "Enter your email for SSL certificate (e.g., admin@example.com): " EMAIL fi if [ -z "${EMAIL}" ]; then error_exit "Email cannot be empty." fi echo "" if [ -z "$V2RAY_USE_CF_DNS" ]; then read -p "Do you want to automatically configure Cloudflare DNS? (y/n): " USE_CF_DNS else echo -e "${GREEN}Cloudflare DNS configuration is set by V2RAY_USE_CF_DNS environment variable.${NC}" USE_CF_DNS=$V2RAY_USE_CF_DNS fi if [[ "$USE_CF_DNS" =~ ^[Yy]$ ]]; then if [ -n "$CF_API_TOKEN" ]; then echo -e "${GREEN}Cloudflare API Token found in environment variables.${NC}" else read -p "Enter your Cloudflare API Token: " CF_API_TOKEN fi if [ -n "$CF_ZONE_ID" ]; then echo -e "${GREEN}Cloudflare Zone ID found in environment variables.${NC}" else read -p "Enter your Cloudflare Zone ID: " CF_ZONE_ID fi if [ -z "${CF_API_TOKEN}" ] || [ -z "${CF_ZONE_ID}" ]; then error_exit "Cloudflare API Token and Zone ID are required for this feature." fi fi echo "" if [ -z "$V2RAY_USE_GIST" ]; then read -p "Do you want to create a GitHub Gist subscription link? (y/n): " USE_GIST else echo -e "${GREEN}GitHub Gist creation is set by V2RAY_USE_GIST environment variable.${NC}" USE_GIST=$V2RAY_USE_GIST fi if [[ "$USE_GIST" =~ ^[Yy]$ ]]; then if [ -n "$GITHUB_USER" ]; then echo -e "${GREEN}GitHub Username found in environment variables: ${GITHUB_USER}${NC}" else read -p "Enter your GitHub Username: " GITHUB_USER fi if [ -n "$GITHUB_TOKEN" ]; then echo -e "${GREEN}GitHub Personal Access Token found in environment variables.${NC}" else read -s -p "Enter your GitHub Personal Access Token (with 'gist' scope): " GITHUB_TOKEN echo fi if [ -z "${GITHUB_USER}" ] || [ -z "${GITHUB_TOKEN}" ]; then error_exit "GitHub Username and Token are required for this feature." fi fi # Check for UUID from env if [ -n "$V2RAY_UUID" ]; then echo -e "${GREEN}UUID found in environment variables.${NC}" UUID="$V2RAY_UUID" else read -p "Enter your V2Ray UUID (or press Enter to generate one): " UUID fi if [ -z "${UUID}" ]; then UUID=$(cat /proc/sys/kernel/random/uuid) echo -e "${YELLOW}Generated UUID: ${UUID}${NC}" fi # Generate a random path for WebSocket WS_PATH="/$(head -n 10 /dev/urandom | md5sum | head -c 8)-ws" echo -e "${YELLOW}Generated WebSocket Path: ${WS_PATH}${NC}" echo -e "${BLUE}--- Configuration Summary ---" echo -e "Domain: ${GREEN}${DOMAIN}${NC}" echo -e "Email: ${GREEN}${EMAIL}${NC}" echo -e "UUID: ${GREEN}${UUID}${NC}" echo -e "WS Path: ${GREEN}${WS_PATH}${NC}" echo -e "Auto DNS: " $([[ "$USE_CF_DNS" =~ ^[Yy]$ ]] && echo -e "${GREEN}Enabled${NC}" || echo -e "${YELLOW}Disabled${NC}") echo -e "Gist Link: " $([[ "$USE_GIST" =~ ^[Yy]$ ]] && echo -e "${GREEN}Enabled${NC}" || echo -e "${YELLOW}Disabled${NC}") echo -e "----------------------------${NC}" read -p "Press Enter to continue, or Ctrl+C to cancel..." } # Function to update system and install dependencies install_dependencies() { echo -e "${BLUE}Updating system and installing dependencies...${NC}" # System update and upgrade, with options to handle config file conflicts automatically export DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade || error_exit "System upgrade failed." # Install main applications apt-get install -y nginx certbot python3-certbot-nginx || error_exit "Failed to install Nginx or Certbot." # Install V2Ray echo -e "${BLUE}Installing V2Ray...${NC}" bash <(curl -L https://raw.githubusercontent.com/v2fly/fhs-install-v2ray/master/install-release.sh) || error_exit "V2Ray core installation failed." bash <(curl -L https://raw.githubusercontent.com/v2fly/fhs-install-v2ray/master/install-dat-release.sh) || error_exit "V2Ray dat files installation failed." systemctl enable --now v2ray || error_exit "Failed to enable V2Ray service." systemctl enable --now nginx || error_exit "Failed to enable Nginx service." echo -e "${GREEN}Dependencies installed successfully.${NC}" } # Function to configure Cloudflare DNS setup_cloudflare_dns() { if [[ ! "$USE_CF_DNS" =~ ^[Yy]$ ]]; then return fi echo -e "${BLUE}Verifying domain with Cloudflare Zone...${NC}" # Get the zone name from the provided Zone ID to verify domain ownership ZONE_DETAILS_RESPONSE=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json") SUCCESS=$(echo "${ZONE_DETAILS_RESPONSE}" | jq -r '.success') if [ "${SUCCESS}" != "true" ]; then ERRORS=$(echo "${ZONE_DETAILS_RESPONSE}" | jq -r '.errors[0].message') error_exit "Cloudflare API call to get zone details failed: ${ERRORS}. Please check your Zone ID and API Token." fi ZONE_NAME=$(echo "${ZONE_DETAILS_RESPONSE}" | jq -r '.result.name') # Check if the user-provided domain is part of the fetched zone if ! [[ "${DOMAIN}" == "${ZONE_NAME}" || "${DOMAIN}" == *".${ZONE_NAME}" ]]; then error_exit "Domain mismatch: The domain '${DOMAIN}' does not belong to the Cloudflare zone '${ZONE_NAME}' associated with your Zone ID." fi echo -e "${GREEN}Domain '${DOMAIN}' successfully verified against Zone '${ZONE_NAME}'.${NC}" echo -e "${BLUE}Configuring Cloudflare DNS record for ${DOMAIN}...${NC}" PUBLIC_IP=$(curl -s https://api.ipify.org) if [ -z "${PUBLIC_IP}" ]; then error_exit "Failed to get public IP address." fi echo "Public IP detected: ${PUBLIC_IP}" # Check if DNS record already exists DNS_RECORD_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=A&name=${DOMAIN}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" | jq -r '.result[0].id') if [ "$DNS_RECORD_ID" != "null" ] && [ ! -z "$DNS_RECORD_ID" ]; then echo -e "${YELLOW}DNS record for ${DOMAIN} already exists. Updating it...${NC}" CF_API_RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${DNS_RECORD_ID}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data "{\"type\":\"A\",\"name\":\"${DOMAIN}\",\"content\":\"${PUBLIC_IP}\",\"ttl\":120,\"proxied\":false}") else echo -e "${BLUE}Creating new DNS A record for ${DOMAIN}...${NC}" CF_API_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ --data "{\"type\":\"A\",\"name\":\"${DOMAIN}\",\"content\":\"${PUBLIC_IP}\",\"ttl\":120,\"proxied\":false}") fi SUCCESS=$(echo "${CF_API_RESPONSE}" | jq -r '.success') if [ "${SUCCESS}" != "true" ]; then ERRORS=$(echo "${CF_API_RESPONSE}" | jq -r '.errors[0].message') error_exit "Cloudflare API call failed: ${ERRORS}" fi echo -e "${GREEN}Cloudflare DNS record configured successfully.${NC}" echo -e "${YELLOW}Waiting 30 seconds for DNS to propagate...${NC}" sleep 30 } # Function to configure Nginx and get SSL certificate configure_nginx_and_ssl() { echo -e "${BLUE}Configuring Nginx and obtaining SSL certificate...${NC}" # --- Pre-emptive Cleanup --- # Remove any stray .bak files from previous failed runs in sites-enabled rm -f /etc/nginx/sites-enabled/*.bak* # Remove any broken symlinks in sites-enabled find /etc/nginx/sites-enabled/ -xtype l -delete # Create a directory for the fake site mkdir -p /var/www/${DOMAIN} || error_exit "Failed to create web directory." echo "