AWS管理脚本:一键部署 Wavelength 环境 & 全能 EC2 换 IP 神器 (Zsh版)

如果你经常使用 AWS EC2,尤其是涉及到 AWS Wavelength (5G 频段) 或者是需要频繁更换实例公网 IP 的业务,你一定遇到过以下痛点:

  1. Wavelength 配置极繁琐:要启用 Zone Group、创建 Carrier Gateway、配置路由表、反复重试寻找不冲突的子网 CIDR……手动搞一次至少 10 分钟。

  2. 换 IP 手速慢:想要换个 IP,得手动解绑 EIP、释放 EIP、申请新 EIP、绑定 EIP,一套流程下来手都酸了。

  3. 脚本兼容性差:网上的脚本在 Linux 上能跑,一到 macOS (Bash 3.2) 或者 AWS CloudShell 里就各种报错。

为了解决这些问题,我编写了一个 “AWS 瑞士军刀 (AWS Swiss Army Knife)” 脚本。它是基于原生 Zsh 编写的,无需安装任何依赖,在 MacBook 终端和 AWS CloudShell 上都能完美运行。


🛠️ 脚本核心功能

这个脚本将最常用的功能合二为一,并修复了所有已知的兼容性问题:

  • 🌐 全球区域支持:支持动态获取 AWS 全球所有区域(不仅限于 Wavelength 区域),一键切换。

  • 🚀 一键部署 Wavelength:全自动完成环境搭建。自动启用区域、自动复用/创建网关、智能避让网段冲突、自动配置路由表。

  • 🔄 全能刷 IP 工具

    • Wavelength 实例:自动循环更换 Carrier IP(运营商 IP)。

    • 普通实例:自动循环更换标准 Public IP。

    • 极速交互:按 Enter 键秒换 IP,支持查重(不使用重复 IP)。

  • 🛡️ 防崩溃设计:针对 AWS API 的延迟和偶发错误(如 CreateTags 失败、区域未就绪)做了完善的容错处理,极其稳定。


💻 如何使用

方法一:在 AWS CloudShell 中使用(推荐)

这是最简单的方法,不需要配置本地环境。

  1. 登录 AWS 控制台,点击右上角的终端图标打开 CloudShell

  2. 输入 nano aws_tool.sh 创建文件。

  3. 将下方的脚本代码粘贴进去,按 Ctrl+O 保存,Ctrl+X 退出。

  4. 赋予执行权限并运行:

    Bash
    chmod +x aws_tool.sh
    ./aws_tool.sh
    

方法二:在 macOS 本地终端使用

确保你已经安装了 AWS CLI 并配置了凭证 (aws configure)。

  1. 打开终端 (Terminal)。

  2. 创建并运行脚本(步骤同上)。由于脚本指定了 #!/bin/zsh,它会自动使用 Mac 默认的 Zsh 解释器,无需担心 Bash 版本过旧的问题。


📜 脚本代码

(请复制以下代码保存为 aws_tool.sh)

Bash
#!/bin/zsh
# ============================================================
# AWS 瑞士军刀 (Zsh v72 UI 规范版)
# 调整: 将 "退出 (Exit)" 移动至菜单末尾,符合标准交互习惯
# 核心: 包含 v71 所有功能 (矩阵部署、全网扫描、Socket修复)
# ============================================================

setopt shwordsplit
setopt no_nomatch

# ==============================
# 全局配置 & 颜色
# ==============================
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
RED='\033[0;31m'
NC='\033[0m'
USED_IP_FILE="ip.txt"

export AWS_REGION="${AWS_REGION:-$(aws configure get region 2>/dev/null || echo 'us-east-1')}"
export AWS_DEFAULT_REGION="$AWS_REGION"

# 检查 sshpass
HAS_SSHPASS=0
if command -v sshpass &> /dev/null; then
    HAS_SSHPASS=1
fi

# ==============================
# 辅助函数:动态选择区域
# ==============================
function select_target_region() {
    {
        echo
        echo -e "${BLUE}=== 请选择目标区域 (Target Region) ===${NC}"
        echo "1. US East (N. Virginia)      [us-east-1]"
        echo "2. US East (Ohio)             [us-east-2]"
        echo "3. US West (Oregon)           [us-west-2]"
        echo "4. Asia Pacific (Tokyo)       [ap-northeast-1]"
        echo "5. Asia Pacific (Seoul)       [ap-northeast-2]"
        echo "6. Europe (London)            [eu-west-2]"
        echo "7. Canada (Central)           [ca-central-1]"
        echo -e "${YELLOW}8. 🌐 列出 AWS 所有可用区域 (动态获取)${NC}"
        echo
        echo -n "请选择 [1-8]: "
    } >&2
    
    read -r region_choice
    
    case $region_choice in
        1) echo "us-east-1" ;;
        2) echo "us-east-2" ;;
        3) echo "us-west-2" ;;
        4) echo "ap-northeast-1" ;;
        5) echo "ap-northeast-2" ;;
        6) echo "eu-west-2" ;;
        7) echo "ca-central-1" ;;
        8) 
            echo -n "正在连接 AWS 获取全球区域列表..." >&2
            local RAW_REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text | tr '\t' '\n' | sort)
            echo -e "\r\033[K" >&2
            local -a R_LIST
            R_LIST=($(echo "$RAW_REGIONS"))
            echo -e "${CYAN}=== AWS 全球可用区域 ===${NC}" >&2
            local idx=1
            for r in "${R_LIST[@]}"; do
                printf "%2d. %-18s " "$idx" "$r" >&2
                if (( idx % 3 == 0 )); then echo >&2; fi
                ((idx++))
            done
            if (( (idx-1) % 3 != 0 )); then echo >&2; fi
            echo >&2
            echo -n "请选择区域编号 (1-${#R_LIST[@]}): " >&2
            read -r list_choice
            if [[ "$list_choice" =~ ^[0-9]+$ ]] && (( list_choice >= 1 && list_choice <= ${#R_LIST[@]} )); then
                echo "${R_LIST[$list_choice]}"
            else echo ""; fi
            ;;
        *) echo "" ;;
    esac
}

# ==============================
# UI 统一格式化函数
# ==============================
function print_table_header() {
    printf "%-4s %-24s %-15s %-20s %-12s %-20s %-15s\n" "No." "Availability Zone" "Name" "Instance ID" "State" "Public/Carrier" "Private IP"
    echo "-----------------------------------------------------------------------------------------------------------------"
}

function format_instance_row() {
    local idx="$1"
    local az="$2"
    local name="$3"
    local iid="$4"
    local state="$5"
    local dns="$6"
    local pub="$7"
    local car="$8"
    local pri="$9"

    [[ "$name" == "None" || -z "$name" ]] && name="-"
    if [[ ${#name} -gt 14 ]]; then name="${name:0:13}.."; fi

    local display_ip="-"
    if [[ "$dns" == ec2-* ]]; then display_ip=$(echo "$dns" | cut -d'.' -f1 | sed 's/ec2-//;s/-/./g'); 
    elif [[ "$car" != "None" && -n "$car" ]]; then display_ip="$car"; 
    elif [[ "$pub" != "None" && -n "$pub" ]]; then display_ip="$pub"; fi

    local color_state="$state"
    if [[ "$state" == "running" ]]; then color_state="${GREEN}$state${NC}"; 
    elif [[ "$state" == "stopped" ]]; then color_state="${RED}$state${NC}"; 
    elif [[ "$state" == "pending" ]]; then color_state="${YELLOW}$state${NC}"; fi

    printf "%-4s %-24s %-15s %-20s %-21b %-20s %-15s\n" "$idx" "$az" "$name" "$iid" "$color_state" "$display_ip" "$pri"
}

# ==============================
# 通用函数:选择实例 (单区域)
# ==============================
function select_instance_general() {
    local REGION="$1"
    local STATE_FILTER="${2:-running}"
    local FILTER_ARGS=()
    if [[ "$STATE_FILTER" == "all" ]]; then STATE_FILTER="pending,running,stopping,stopped"; fi
    if [[ -n "$STATE_FILTER" ]]; then FILTER_ARGS+=(--filters "Name=instance-state-name,Values=$STATE_FILTER"); fi
    
    local TMP_LIST="/tmp/aws_ec2_list_$$"
    echo -n "正在连接 AWS 获取实例列表..." >&2
    
    aws ec2 describe-instances "${FILTER_ARGS[@]}" \
        --region "$REGION" \
        --query "Reservations[].Instances[].[InstanceId,State.Name,Placement.AvailabilityZone,PublicDnsName,PublicIpAddress,NetworkInterfaces[0].Association.CarrierIp,PrivateIpAddress,Tags[?Key=='Name'].Value|[0]]" \
        --output text | sed '/^$/d' > "$TMP_LIST"
    
    echo -e "\r\033[K" >&2
    
    if [[ ! -s "$TMP_LIST" ]]; then 
        echo "❌ 未找到实例 (过滤条件: $STATE_FILTER)" >&2
        rm -f "$TMP_LIST"
        return 1
    fi
    
    local max_idx=$(wc -l < "$TMP_LIST" | tr -d ' ')
    
    {
        echo
        echo "=== 实例列表 ($REGION) ==="
        print_table_header
        local idx=0; local line
        while read -r line; do
            idx=$((idx+1))
            read -r IID STATE AZ DNS PUB CAR PRI NAME <<< "$line"
            format_instance_row "$idx" "$AZ" "$NAME" "$IID" "$STATE" "$DNS" "$PUB" "$CAR" "$PRI"
        done < "$TMP_LIST"
        echo
    } >&2
    
    local choice
    while true; do
        echo -n "选择实例编号 (1-$max_idx) [q退出]: " >&2
        if ! read -r choice; then rm -f "$TMP_LIST"; return 1; fi
        if [[ "$choice" == "q" ]]; then rm -f "$TMP_LIST"; return 1; fi
        if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= max_idx )); then
            local picked=$(sed -n "${choice}p" "$TMP_LIST")
            read -r IID STATE AZ DNS PUB CAR PRI NAME <<< "$picked"
            echo "$IID|$AZ"
            rm -f "$TMP_LIST"
            return 0
        fi
    done
}

# ==============================
# 功能 1: 部署环境
# ==============================
function deploy_wavelength() {
    local REGION=$(select_target_region)
    if [[ -z "$REGION" ]]; then echo "未选择区域,取消。"; return; fi
    export AWS_REGION="$REGION"

    echo -e "${BLUE}>>> 进入边缘资源部署模式 ($REGION)...${NC}"
    (
        set -euo pipefail
        echo; echo -e "${YELLOW}[1/5] 深度扫描所有可用区 (Force Discovery)...${NC}"
        RAW_GROUPS=$(aws ec2 describe-availability-zones --region "$REGION" --all-availability-zones --query "AvailabilityZones[].[GroupName, ZoneType]" --output text 2>/dev/null || echo "")
        if [[ -z "$RAW_GROUPS" ]]; then echo -e "${RED}⚠️  警告: 无法获取区域信息。${NC}"; fi
        TARGET_GROUPS=$(echo "$RAW_GROUPS" | grep -E "wavelength-zone|local-zone" | awk '{print $1}' | sort -u || true)
        
        if [[ -z "$TARGET_GROUPS" ]]; then 
            echo -e "${RED}提示: 在 ($REGION) 未发现任何 Wavelength 或 Local Zone 记录。${NC}"
            echo -e "系统将尝试配置标准网络环境..."
        else
            echo -e "发现边缘区域组,正在强制启用..."
            for group in $TARGET_GROUPS; do
                echo -n "启用组 $group ... "
                OUT=$(aws ec2 modify-availability-zone-group --group-name "$group" --opt-in-status opted-in --region "$REGION" 2>&1 || true)
                if echo "$OUT" | grep -q "Information"; then echo -e "${GREEN}已启用${NC}"; elif echo "$OUT" | grep -q "error"; then echo -e "${YELLOW}失败(权限不足?)${NC}"; else echo -e "${GREEN}请求发送${NC}"; fi
            done
            echo -n "⏳ 等待配置生效 (10秒)... "; sleep 10; echo -e "${GREEN}继续${NC}"
        fi

        WL_AZS=$(aws ec2 describe-availability-zones --region "$REGION" --all-availability-zones --filters "Name=zone-type,Values=wavelength-zone" --query "AvailabilityZones[].ZoneName" --output text 2>/dev/null | tr '\t' '\n' | sort || true)
        LZ_AZS=$(aws ec2 describe-availability-zones --region "$REGION" --all-availability-zones --filters "Name=zone-type,Values=local-zone" --query "AvailabilityZones[].ZoneName" --output text 2>/dev/null | tr '\t' '\n' | sort || true)

        echo; echo -e "${YELLOW}[2/5] 获取 VPC 信息...${NC}"
        VPC_INFO=$(aws ec2 describe-vpcs --region "$REGION" --query "Vpcs[].[VpcId, IsDefault, CidrBlock]" --output text)
        if [[ -z "$VPC_INFO" ]]; then echo -e "${RED}无 VPC。${NC}"; exit 1; fi
        TARGET_VPC=$(echo "$VPC_INFO" | grep "True" | head -n 1 || true)
        if [[ -z "$TARGET_VPC" ]]; then TARGET_VPC=$(echo "$VPC_INFO" | head -n 1); fi
        VPC_ID=$(echo "$TARGET_VPC" | awk '{print $1}')
        VPC_CIDR=$(echo "$TARGET_VPC" | awk '{print $3}')
        echo -e "选中 VPC: ${GREEN}$VPC_ID${NC} (CIDR: $VPC_CIDR)"
        CIDR_PREFIX=$(echo "$VPC_CIDR" | cut -d'.' -f1,2)

        echo; echo -e "${YELLOW}[3/5] 配置网络网关...${NC}"
        local CAGW_ID=""; local IGW_ID=""
        if [[ -n "$WL_AZS" ]]; then
            CAGW_ID=$(aws ec2 describe-carrier-gateways --filters "Name=vpc-id,Values=$VPC_ID" "Name=state,Values=available" --region "$REGION" --query "CarrierGateways[0].CarrierGatewayId" --output text)
            if [[ "$CAGW_ID" == "None" || -z "$CAGW_ID" ]]; then
                CAGW_ID=$(aws ec2 create-carrier-gateway --vpc-id "$VPC_ID" --region "$REGION" --query "CarrierGateway.CarrierGatewayId" --output text)
                aws ec2 create-tags --resources "$CAGW_ID" --tags Key=Name,Value="Auto-Wavelength-CAGW" --region "$REGION"
                echo -e "   ✅ 创建 CAGW: ${GREEN}$CAGW_ID${NC}"
            else echo -e "   ✅ 复用 CAGW: ${GREEN}$CAGW_ID${NC}"; fi
        fi
        IGW_ID=$(aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=$VPC_ID" --region "$REGION" --query "InternetGateways[0].InternetGatewayId" --output text)
        if [[ "$IGW_ID" == "None" || -z "$IGW_ID" ]]; then
            IGW_ID=$(aws ec2 create-internet-gateway --region "$REGION" --query "InternetGateway.InternetGatewayId" --output text)
            aws ec2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" --region "$REGION"
            echo -e "   ✅ 创建/绑定 IGW: ${GREEN}$IGW_ID${NC}"
        else echo -e "   ✅ 复用 IGW: ${GREEN}$IGW_ID${NC}"; fi

        echo; echo -e "${YELLOW}[4/5] 配置路由表...${NC}"
        local RT_WL_ID=""; local RT_LZ_ID=""
        if [[ -n "$WL_AZS" && -n "$CAGW_ID" ]]; then
            RT_WL_ID=$(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=Auto-RT-Wavelength" --region "$REGION" --query "RouteTables[0].RouteTableId" --output text)
            if [[ "$RT_WL_ID" == "None" || -z "$RT_WL_ID" ]]; then
                RT_WL_ID=$(aws ec2 create-route-table --vpc-id "$VPC_ID" --region "$REGION" --query "RouteTable.RouteTableId" --output text)
                aws ec2 create-tags --resources "$RT_WL_ID" --tags Key=Name,Value="Auto-RT-Wavelength" --region "$REGION"
                aws ec2 create-route --route-table-id "$RT_WL_ID" --destination-cidr-block 0.0.0.0/0 --carrier-gateway-id "$CAGW_ID" --region "$REGION" >/dev/null
                echo -e "   ✅ 创建 WLZ 路由表: ${GREEN}$RT_WL_ID${NC} -> CAGW"
            else echo -e "   ✅ 复用 WLZ 路由表: ${GREEN}$RT_WL_ID${NC}"; fi
        fi
        RT_LZ_ID=$(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=Auto-RT-LocalZone" --region "$REGION" --query "RouteTables[0].RouteTableId" --output text)
        if [[ "$RT_LZ_ID" == "None" || -z "$RT_LZ_ID" ]]; then
            RT_LZ_ID=$(aws ec2 create-route-table --vpc-id "$VPC_ID" --region "$REGION" --query "RouteTable.RouteTableId" --output text)
            aws ec2 create-tags --resources "$RT_LZ_ID" --tags Key=Name,Value="Auto-RT-LocalZone" --region "$REGION"
            aws ec2 create-route --route-table-id "$RT_LZ_ID" --destination-cidr-block 0.0.0.0/0 --gateway-id "$IGW_ID" --region "$REGION" >/dev/null
            echo -e "   ✅ 创建 LZ/Public 路由表: ${GREEN}$RT_LZ_ID${NC} -> IGW"
        else echo -e "   ✅ 复用 LZ/Public 路由表: ${GREEN}$RT_LZ_ID${NC}"; fi

        # === 5. 批量创建子网 ===
        echo; echo -e "${YELLOW}[5/5] 创建子网并关联路由...${NC}"
        CURRENT_OCTET=200
        ALL_TARGET_AZS="$WL_AZS $LZ_AZS"
        if [[ -z "$ALL_TARGET_AZS" ]]; then echo -e "${YELLOW}⚠️  注意: 暂未发现有效的 Wavelength 或 Local Zone。${NC}"; fi

        for az in $ALL_TARGET_AZS; do
            echo -e "   ----------------------------------------"
            echo -e "   正在处理区域: ${BLUE}$az${NC}"
            local TARGET_RT=""
            if [[ "$az" == *wlz* || "$az" == *wavelength* ]]; then TARGET_RT="$RT_WL_ID"; echo "   类型: Wavelength Zone (使用 CAGW)"; else TARGET_RT="$RT_LZ_ID"; echo "   类型: Local Zone (使用 IGW)"; fi
            if [[ -z "$TARGET_RT" ]]; then echo -e "   ${RED}❌ 错误: 对应类型的路由表未初始化。${NC}"; continue; fi
            TARGET_SUBNET=""
            EXISTING_SUBNET=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" "Name=availability-zone,Values=$az" --region "$REGION" --query "Subnets[0].SubnetId" --output text)
            if [[ "$EXISTING_SUBNET" != "None" && -n "$EXISTING_SUBNET" ]]; then
                echo -e "   ✅ 已存在子网: $EXISTING_SUBNET (复用)"
                TARGET_SUBNET="$EXISTING_SUBNET"
            else
                ATTEMPTS=0; MAX_ATTEMPTS=10; SUCCESS=false
                while [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do
                    NEW_CIDR="${CIDR_PREFIX}.${CURRENT_OCTET}.0/24"
                    if [ $CURRENT_OCTET -gt 254 ]; then CURRENT_OCTET=100; NEW_CIDR="${CIDR_PREFIX}.${CURRENT_OCTET}.0/24"; fi
                    CREATE_OUT=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block "$NEW_CIDR" --availability-zone "$az" --region "$REGION" --output json 2>/dev/null || echo "ERROR")
                    if [[ "$CREATE_OUT" == "ERROR" ]]; then CURRENT_OCTET=$((CURRENT_OCTET+1)); ATTEMPTS=$((ATTEMPTS+1))
                    else
                        TARGET_SUBNET=$(echo "$CREATE_OUT" | grep '"SubnetId":' | cut -d'"' -f4)
                        echo -e "${GREEN}   ✅ 创建成功: $TARGET_SUBNET ($NEW_CIDR)${NC}"
                        SHORT_AZ=${az##*-}
                        aws ec2 create-tags --resources "$TARGET_SUBNET" --tags Key=Name,Value="Auto-$SHORT_AZ" --region "$REGION" 2>/dev/null || true
                        CURRENT_OCTET=$((CURRENT_OCTET+1)); SUCCESS=true; break
                    fi
                done
                if [ "$SUCCESS" = false ]; then echo -e "   ${RED}❌ 无法创建子网(IP耗尽?)${NC}"; continue; fi
            fi
            if [[ -n "$TARGET_SUBNET" ]]; then aws ec2 associate-route-table --route-table-id "$TARGET_RT" --subnet-id "$TARGET_SUBNET" --region "$REGION" >/dev/null 2>&1 || true; fi
        done
        echo; echo -e "${GREEN}🎉 部署完成!${NC}"
    )
    echo -e "${BLUE}按回车键返回主菜单...${NC}"
    read
}

# ==============================
# 功能 2: 创建 SOCKS5 代理实例
# ==============================
function create_proxy_instance() {
    local REGION=$(select_target_region)
    if [[ -z "$REGION" ]]; then echo "未选择区域,取消。"; return; fi
    export AWS_REGION="$REGION"

    echo -e "${BLUE}>>> 创建 Ubuntu SOCKS5 代理实例 ($REGION)...${NC}"

    echo -n "正在搜索最新 Ubuntu 24.04 (AMD64) 镜像... "
    local AMI_INFO=$(aws ec2 describe-images --owners 099720109477 --filters "Name=name,Values=ubuntu/images/hvm-ssd*/ubuntu-noble-24.04-amd64-server-*" "Name=state,Values=available" --query "sort_by(Images, &CreationDate)[-1].[ImageId, RootDeviceName]" --output text --region "$REGION")
    local AMI_ID=$(echo "$AMI_INFO" | awk '{print $1}')
    local ROOT_DEV=$(echo "$AMI_INFO" | awk '{print $2}')

    if [[ -z "$AMI_ID" || "$AMI_ID" == "None" ]]; then
        echo -e "${YELLOW}未找到 24.04,尝试降级 22.04...${NC}"
        AMI_INFO=$(aws ec2 describe-images --owners 099720109477 --filters "Name=name,Values=ubuntu/images/hvm-ssd*/ubuntu-jammy-22.04-amd64-server-*" "Name=state,Values=available" --query "sort_by(Images, &CreationDate)[-1].[ImageId, RootDeviceName]" --output text --region "$REGION")
        AMI_ID=$(echo "$AMI_INFO" | awk '{print $1}')
        ROOT_DEV=$(echo "$AMI_INFO" | awk '{print $2}')
        if [[ -z "$AMI_ID" || "$AMI_ID" == "None" ]]; then echo -e "${RED}失败!无法获取 AMI。${NC}"; return; fi
    fi
    echo -e "${GREEN}$AMI_ID${NC} (Root: $ROOT_DEV)"

    echo; echo -e "${YELLOW}--- 配置账号密码 ---${NC}"
    echo -n "设置 Root 密码 (默认: RootPass123456): "
    read -r SET_ROOT_PASS; SET_ROOT_PASS="${SET_ROOT_PASS:-RootPass123456}"
    echo -n "设置 SOCKS5 密码 (默认: ProxyPass123456): "
    read -r SET_PROXY_PASS; SET_PROXY_PASS="${SET_PROXY_PASS:-ProxyPass123456}"
    local PROXY_USER="admin"; local PROXY_PORT="1080"

    echo; echo -n "检查安全组 Auto-Socks5-SG... "
    local SG_ID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=Auto-Socks5-SG" --query "SecurityGroups[0].GroupId" --output text --region "$REGION" 2>/dev/null)
    if [[ "$SG_ID" == "None" || -z "$SG_ID" ]]; then
        echo -n "创建中... "
        SG_ID=$(aws ec2 create-security-group --group-name "Auto-Socks5-SG" --description "Auto generated by AWS Tool for SOCKS5" --region "$REGION" --query "GroupId" --output text)
        echo -e "${GREEN}完成 ($SG_ID)${NC}"; sleep 2
    else echo -e "${GREEN}已存在 ($SG_ID)${NC}"; fi
    SG_ID=$(echo "$SG_ID" | awk '{print $1}')

    echo -n "添加端口规则... "
    aws ec2 authorize-security-group-ingress --group-id "$SG_ID" --protocol tcp --port 22 --cidr 0.0.0.0/0 --region "$REGION" >/dev/null 2>&1 || true
    aws ec2 authorize-security-group-ingress --group-id "$SG_ID" --protocol tcp --port "$PROXY_PORT" --cidr 0.0.0.0/0 --region "$REGION" >/dev/null 2>&1 || true
    aws ec2 authorize-security-group-ingress --group-id "$SG_ID" --protocol icmp --port -1 --cidr 0.0.0.0/0 --region "$REGION" >/dev/null 2>&1 || true
    echo -e "${GREEN}完成${NC}"

    echo; echo -e "${YELLOW}--- 选择子网 (Subnet) ---${NC}"
    local TMP_SUBNETS="/tmp/aws_subnets_$$"
    aws ec2 describe-subnets --region "$REGION" --query "Subnets[].[SubnetId,AvailabilityZone,CidrBlock,MapPublicIpOnLaunch]" --output text | sort -k2 > "$TMP_SUBNETS"
    if [[ ! -s "$TMP_SUBNETS" ]]; then echo "${RED}当前区域无可用子网。${NC}"; rm "$TMP_SUBNETS"; return; fi
    local max_idx=$(wc -l < "$TMP_SUBNETS" | tr -d ' ')
    printf "%-4s %-24s %-25s %-18s %s\n" "No." "SubnetId" "AZ" "CIDR" "AutoPublicIP"
    local idx=0
    while read -r line; do
        idx=$((idx+1))
        read -r SID AZ CIDR PUB <<< "$line"
        printf "%-4s %-24s %-25s %-18s %s\n" "$idx" "$SID" "$AZ" "$CIDR" "$PUB"
    done < "$TMP_SUBNETS"
    local sub_choice; local TARGET_SUBNET; local TARGET_AZ
    while true; do
        echo -n "请选择子网编号 (1-$max_idx): "
        read -r sub_choice
        if [[ "$sub_choice" =~ ^[0-9]+$ ]] && (( sub_choice >= 1 && sub_choice <= max_idx )); then
            local picked=$(sed -n "${sub_choice}p" "$TMP_SUBNETS")
            TARGET_SUBNET=$(echo "$picked" | awk '{print $1}')
            TARGET_AZ=$(echo "$picked" | awk '{print $2}')
            break
        fi
    done
    rm "$TMP_SUBNETS"

    echo; echo -n "正在分析 $TARGET_AZ 可用机型... "
    local OFFERINGS=$(aws ec2 describe-instance-type-offerings --location-type availability-zone --filters Name=location,Values="$TARGET_AZ" --query "InstanceTypeOfferings[].InstanceType" --output text --region "$REGION")
    local PREFERRED_LIST=("t3.micro" "t3.small" "t3.medium" "t3.large" "t3.xlarge" "t2.micro" "t2.small" "t2.medium" "t2.large" "m5.large" "m5.xlarge")
    local TRY_LIST=()
    for type in "${PREFERRED_LIST[@]}"; do if [[ "$OFFERINGS" == *"$type"* ]]; then TRY_LIST+=("$type"); fi; done
    if [[ ${#TRY_LIST[@]} -eq 0 ]]; then
        echo -e "${YELLOW}未找到常见机型,启用全机型兜底...${NC}"
        echo -n "正在按价格(vCPU)排序并过滤 x86 架构... "
        local SAFE_OFFERINGS=$(aws ec2 describe-instance-types --instance-types ${=OFFERINGS} --filters "Name=vcpu-info.default-vcpus,Values=1,2,4" "Name=processor-info.supported-architecture,Values=x86_64" --query "sort_by(InstanceTypes, &VCpuInfo.DefaultVCpus)[].InstanceType" --output text --region "$REGION" 2>/dev/null)
        if [[ -z "$SAFE_OFFERINGS" ]]; then echo -e "\n${RED}无可用机型。${NC}"; echo -e "${BLUE}按回车键返回...${NC}"; read; return; fi
        TRY_LIST=(${=SAFE_OFFERINGS})
        echo -e "${GREEN}筛选出 ${#TRY_LIST[@]} 个低成本机型${NC}"
    else echo -e "${GREEN}发现 ${#TRY_LIST[@]} 个推荐机型${NC}"; fi

    local VOL_TYPE="gp3"; local IS_EDGE="NO"; local IS_WLZ_MODE="NO"
    if [[ "$TARGET_AZ" == *wlz* || "$TARGET_AZ" == *wavelength* ]]; then IS_WLZ_MODE="YES"; IS_EDGE="YES"; VOL_TYPE="gp2"; echo -e "${YELLOW}检测到 Wavelength: 锁定 gp2, 开启自动 Carrier IP${NC}"
    elif [[ "$TARGET_AZ" == *laz* || "$TARGET_AZ" == *local* ]]; then IS_EDGE="YES"; VOL_TYPE="gp2"; echo -e "${YELLOW}检测到 Local Zone: 锁定 gp2, 使用标准公网 IP${NC}"
    else VOL_TYPE="gp3"; echo -e "${GREEN}检测到普通区域: 使用 gp3, 自动分配动态公网 IP${NC}"; fi
    local MAPPING="[{\"DeviceName\":\"$ROOT_DEV\",\"Ebs\":{\"VolumeType\":\"$VOL_TYPE\"}}]"

    RAW_USER_DATA=$(cat <<EOF
#!/bin/bash
ROOT_PASS="${SET_ROOT_PASS}"
PROXY_USER="${PROXY_USER}"
PROXY_PASS="${SET_PROXY_PASS}"
PROXY_PORT="${PROXY_PORT}"
exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
echo "=== Script Start: \$(date) ==="
echo "Config SSH..."
echo "root:\${ROOT_PASS}" | chpasswd
echo "ssh_pwauth: true" > /etc/cloud/cloud.cfg.d/99-force-pwauth.cfg
systemctl stop ssh.socket; systemctl disable ssh.socket; systemctl mask ssh.socket
rm -rf /etc/ssh/sshd_config.d/*
cat > /etc/ssh/sshd_config <<SSHCONF
PermitRootLogin yes
PasswordAuthentication yes
KbdInteractiveAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding yes
PrintMotd no
Subsystem sftp /usr/lib/openssh/sftp-server
SSHCONF
systemctl stop ssh; sleep 2; systemctl start ssh || systemctl restart sshd
echo "* * * * * root systemctl mask ssh.socket && systemctl is-active ssh || systemctl start ssh" >> /etc/crontab
echo "Firewall..."
ufw disable || true; iptables -F; iptables -X; iptables -P INPUT ACCEPT; iptables -P FORWARD ACCEPT; iptables -P OUTPUT ACCEPT
echo "Dependencies..."
while fuser /var/lib/dpkg/lock >/dev/null 2>&1; do sleep 2; done
apt-get update; apt-get install -y curl gnupg lsb-release net-tools
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ \$(lsb_release -cs) main" | tee /etc/apt/sources.list.d/cloudflare-client.list
apt-get update; apt-get install -y cloudflare-warp
echo "WARP..."
warp-cli --accept-tos registration new || true; warp-cli --accept-tos mode proxy; warp-cli --accept-tos connect
for i in {1..10}; do if netstat -nltp | grep -q "40000"; then break; fi; sleep 2; done
echo "GOST..."
cd /usr/local/bin
curl -L https://github.com/ginuerzh/gost/releases/download/v2.11.5/gost-linux-amd64-2.11.5.gz -o gost.gz
gunzip -f gost.gz; chmod +x gost
echo "Service..."
cat > /etc/systemd/system/gost.service <<SERVICE
[Unit]
Description=GOST Proxy
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/gost -L \${PROXY_USER}:\${PROXY_PASS}@:\${PROXY_PORT} -F socks5://127.0.0.1:40000
Restart=always
User=root
[Install]
WantedBy=multi-user.target
SERVICE
systemctl daemon-reload; systemctl enable gost; systemctl restart gost
echo "=== Done ==="
EOF
)

    local INSTANCE_ID=""; local RUN_OUT=""
    
    function run_instance_cmd() {
        local v_type="$1"; local dev_map="[{\"DeviceName\":\"$ROOT_DEV\",\"Ebs\":{\"VolumeType\":\"$v_type\"}}]"
        local CMD=(aws ec2 run-instances --image-id "$AMI_ID" --instance-type "$INSTANCE_TYPE" --count 1 --user-data "$RAW_USER_DATA" --block-device-mappings "$dev_map" --region "$REGION" --output json --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=Socks5-Proxy}]")
        if [[ "$IS_WLZ_MODE" == "YES" ]]; then
            local NET_IF="DeviceIndex=0,SubnetId=$TARGET_SUBNET,Groups=$SG_ID,AssociateCarrierIpAddress=true"
            CMD+=(--network-interfaces "$NET_IF")
        else CMD+=(--subnet-id "$TARGET_SUBNET" --security-group-ids "$SG_ID" --associate-public-ip-address)
        fi
        if [[ "$IS_EDGE" == "YES" ]]; then if [[ "$INSTANCE_TYPE" == t2* || "$INSTANCE_TYPE" == t3* ]]; then CMD+=(--credit-specification "CpuCredits=standard"); fi; fi
        "${CMD[@]}" 2>&1
    }

    for INSTANCE_TYPE in "${TRY_LIST[@]}"; do
        echo -n "尝试启动 ${YELLOW}$INSTANCE_TYPE${NC} (存储: $VOL_TYPE)... "
        RUN_OUT=$(run_instance_cmd "$VOL_TYPE")
        if [[ $? -eq 0 ]]; then echo -e "${GREEN}成功!${NC}"; INSTANCE_ID=$(echo "$RUN_OUT" | grep '"InstanceId":' | cut -d'"' -f4); break
        else
            if echo "$RUN_OUT" | grep -q "VolumeTypeNotAvailableInZone"; then
                echo -e "${RED}不支持 $VOL_TYPE${NC}"
                if [[ "$VOL_TYPE" == "gp3" ]]; then echo -n "   ↳ 自动降级为 gp2 重试... "; RUN_OUT=$(run_instance_cmd "gp2"); if [[ $? -eq 0 ]]; then echo -e "${GREEN}成功!${NC}"; INSTANCE_ID=$(echo "$RUN_OUT" | grep '"InstanceId":' | cut -d'"' -f4); break; fi; fi
            fi
            if echo "$RUN_OUT" | grep -q "Unsupported"; then echo -e "${RED}不支持${NC} -> 切换下一个"
            elif echo "$RUN_OUT" | grep -q "InsufficientInstanceCapacity"; then echo -e "${RED}无库存${NC} -> 切换下一个"
            elif echo "$RUN_OUT" | grep -q "VolumeTypeNotAvailableInZone"; then echo -e "${RED}存储仍不支持${NC} -> 切换下一个"
            else echo -e "${RED}失败${NC}"; echo "未知错误: $RUN_OUT"; echo -e "${BLUE}按回车键停止尝试...${NC}"; read; return; fi
        fi
    done

    if [[ -z "$INSTANCE_ID" ]]; then echo -e "${RED}所有候选机型均启动失败。${NC}"; echo -e "${BLUE}按回车键返回...${NC}"; read; return; fi
    
    echo -e "实例 ID: ${GREEN}$INSTANCE_ID${NC}"
    echo -n "等待分配 DNS/IP..."
    local FINAL_IP="None"; local ATTEMPTS=0
    while [ $ATTEMPTS -lt 40 ]; do
        sleep 3
        local INFO=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --region "$REGION" --query "Reservations[0].Instances[0].[PublicDnsName,PublicIpAddress,NetworkInterfaces[0].Association.CarrierIp]" --output text)
        local DNS=$(echo "$INFO" | awk '{print $1}'); local PUB=$(echo "$INFO" | awk '{print $2}'); local CAR=$(echo "$INFO" | awk '{print $3}')
        if [[ "$DNS" == ec2-* ]]; then local IP_FROM_DNS=$(echo "$DNS" | cut -d'.' -f1 | sed 's/ec2-//;s/-/./g'); if [[ -n "$IP_FROM_DNS" ]]; then FINAL_IP="$IP_FROM_DNS"; echo -e "${GREEN}已解析 (DNS)!${NC}"; break; fi; fi
        if [[ "$CAR" != "None" && -n "$CAR" ]]; then FINAL_IP="$CAR"; echo -e "${GREEN}已获取 (Carrier)!${NC}"; break; fi
        if [[ "$PUB" != "None" && -n "$PUB" ]]; then FINAL_IP="$PUB"; echo -e "${GREEN}已获取 (Public)!${NC}"; break; fi
        echo -n "."; ATTEMPTS=$((ATTEMPTS+1))
    done
    if [[ "$FINAL_IP" == "None" ]]; then echo -e "\n${RED}获取超时!${NC}"; fi
    
    echo -n "等待 SSH 服务上线 (预计 60秒)..."
    for i in {1..30}; do sleep 3; if nc -z -w 2 "$FINAL_IP" 22 2>/dev/null; then echo -e " ${GREEN}SSH 端口通!${NC}"; break; fi; echo -n "."; done
    echo; echo -e "${GREEN}实例已就绪!${NC}"
    echo
    echo "============================================="
    echo " SSH 连接:    ssh root@$FINAL_IP"
    echo " Root 密码:   $SET_ROOT_PASS"
    echo "---------------------------------------------"
    echo " SOCKS5 代理: $FINAL_IP:$PROXY_PORT"
    echo " 账号:        $PROXY_USER"
    echo " 密码:        $SET_PROXY_PASS"
    echo "============================================="
    
    if [[ $HAS_SSHPASS -eq 1 ]]; then
        echo -e "${YELLOW}🚀 一键自动登录命令:${NC}"
        echo -e "${CYAN}sshpass -p '$SET_ROOT_PASS' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$FINAL_IP${NC}"
        echo "============================================="
    else echo -e "${YELLOW}提示: 安装 sshpass 可实现免密自动登录${NC}"; fi
    
    if [[ "$IS_WLZ_MODE" == "YES" ]]; then
        echo -e "${YELLOW}⚠️  注意: Wavelength 实例通常无法从公网直接访问 SOCKS5。${NC}"
        echo -e "${YELLOW}   此代理通常需要同 VPC 内网的机器(跳板机)才能连接使用。${NC}"
        echo "============================================="
    fi
    
    echo -e "${CYAN}格式化输出: $FINAL_IP:$PROXY_PORT:$PROXY_USER:$SET_PROXY_PASS${NC}"
    echo "============================================="
    echo "注意: 初始化脚本运行需要 2-3 分钟,请稍候连接。"
    echo -e "${BLUE}按回车键返回主菜单...${NC}"; read
}

# ==============================
# 功能 8: 批量部署美国四地代理 (v71)
# ==============================
function deploy_quad_proxies() {
    echo -e "${BLUE}>>> 准备一键部署美国四地 (East-1/2, West-1/2) 代理矩阵...${NC}"
    echo -e "${YELLOW}注意: 此模式将严格筛选 Standard Zone,确保避开 Local/Wavelength Zone。${NC}"
    echo
    echo -n "请设置统一 SOCKS5 密码 [默认: ProxyPass123456]: "
    read -r SET_PROXY_PASS
    SET_PROXY_PASS="${SET_PROXY_PASS:-ProxyPass123456}"
    local SET_ROOT_PASS="RootPass123456"
    local PROXY_USER="admin"
    local PROXY_PORT="1080"

    # 定义目标区域
    local TARGET_REGIONS=("us-east-1" "us-east-2" "us-west-1" "us-west-2")
    
    # 循环部署
    for REGION in "${TARGET_REGIONS[@]}"; do
        echo
        echo -e "${BLUE}>>> 正在部署区域: ${GREEN}$REGION${NC} ..."
        
        # 1. 查找 AMI
        echo -n "   🔍 搜索 AMI (Ubuntu 24.04)... "
        local AMI_ID=$(aws ec2 describe-images --owners 099720109477 --filters "Name=name,Values=ubuntu/images/hvm-ssd*/ubuntu-noble-24.04-amd64-server-*" "Name=state,Values=available" --query "sort_by(Images, &CreationDate)[-1].ImageId" --output text --region "$REGION" 2>/dev/null)
        if [[ -z "$AMI_ID" || "$AMI_ID" == "None" ]]; then
             echo -n "降级 22.04... "
             AMI_ID=$(aws ec2 describe-images --owners 099720109477 --filters "Name=name,Values=ubuntu/images/hvm-ssd*/ubuntu-jammy-22.04-amd64-server-*" "Name=state,Values=available" --query "sort_by(Images, &CreationDate)[-1].ImageId" --output text --region "$REGION" 2>/dev/null)
        fi
        
        if [[ -z "$AMI_ID" || "$AMI_ID" == "None" ]]; then
            echo -e "${RED}失败 (无可用 AMI)${NC}"
            continue
        fi
        echo -e "${GREEN}$AMI_ID${NC}"

        # 2. 准备安全组
        echo -n "   🛡️  配置安全组... "
        local SG_ID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=Auto-Socks5-SG" --query "SecurityGroups[0].GroupId" --output text --region "$REGION" 2>/dev/null)
        if [[ "$SG_ID" == "None" || -z "$SG_ID" ]]; then
            SG_ID=$(aws ec2 create-security-group --group-name "Auto-Socks5-SG" --description "Auto generated SOCKS5 SG" --region "$REGION" --query "GroupId" --output text)
            aws ec2 authorize-security-group-ingress --group-id "$SG_ID" --protocol tcp --port 22 --cidr 0.0.0.0/0 --region "$REGION" >/dev/null 2>&1
            aws ec2 authorize-security-group-ingress --group-id "$SG_ID" --protocol tcp --port "$PROXY_PORT" --cidr 0.0.0.0/0 --region "$REGION" >/dev/null 2>&1
        fi
        echo -e "${GREEN}OK ($SG_ID)${NC}"

        # 3. 寻找标准子网 (关键步骤: 过滤 Local Zone)
        echo -n "   🌐 寻找标准可用区子网... "
        # 先获取该区域所有 Standard AZ 的列表
        local STD_AZS=$(aws ec2 describe-availability-zones --region "$REGION" --filters "Name=zone-type,Values=availability-zone" --query "AvailabilityZones[].ZoneName" --output text)
        
        # 查找属于 Standard AZ 的子网
        # 注意: 这里简化逻辑,找任意一个符合条件的 Public Subnet
        local TARGET_SUBNET=""
        local ALL_SUBNETS=$(aws ec2 describe-subnets --region "$REGION" --query "Subnets[].[SubnetId,AvailabilityZone,MapPublicIpOnLaunch]" --output text)
        
        while read -r SID AZ PUB; do
            # 检查 AZ 是否在标准列表中
            if [[ "$STD_AZS" == *"$AZ"* ]]; then
                TARGET_SUBNET="$SID"
                break
            fi
        done <<< "$ALL_SUBNETS"

        if [[ -z "$TARGET_SUBNET" ]]; then
            echo -e "${RED}失败 (未找到标准区域子网,请检查 VPC)${NC}"
            continue
        fi
        echo -e "${GREEN}OK ($TARGET_SUBNET)${NC}"

        # 4. 启动实例
        echo -n "   🚀 启动实例 (t3/t2.micro)... "
        
        # 构造 UserData
        local RAW_USER_DATA=$(cat <<EOF
#!/bin/bash
ROOT_PASS="${SET_ROOT_PASS}"
PROXY_USER="${PROXY_USER}"
PROXY_PASS="${SET_PROXY_PASS}"
PROXY_PORT="${PROXY_PORT}"
exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
echo "root:\${ROOT_PASS}" | chpasswd
echo "ssh_pwauth: true" > /etc/cloud/cloud.cfg.d/99-force-pwauth.cfg
systemctl stop ssh.socket; systemctl disable ssh.socket; systemctl mask ssh.socket
rm -rf /etc/ssh/sshd_config.d/*
cat > /etc/ssh/sshd_config <<SSHCONF
PermitRootLogin yes
PasswordAuthentication yes
KbdInteractiveAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding yes
PrintMotd no
Subsystem sftp /usr/lib/openssh/sftp-server
SSHCONF
systemctl stop ssh; sleep 2; systemctl start ssh || systemctl restart sshd
ufw disable || true; iptables -F
apt-get update; apt-get install -y curl gnupg lsb-release net-tools
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ \$(lsb_release -cs) main" | tee /etc/apt/sources.list.d/cloudflare-client.list
apt-get update; apt-get install -y cloudflare-warp
warp-cli --accept-tos registration new || true; warp-cli --accept-tos mode proxy; warp-cli --accept-tos connect
for i in {1..10}; do if netstat -nltp | grep -q "40000"; then break; fi; sleep 2; done
cd /usr/local/bin
curl -L https://github.com/ginuerzh/gost/releases/download/v2.11.5/gost-linux-amd64-2.11.5.gz -o gost.gz; gunzip -f gost.gz; chmod +x gost
cat > /etc/systemd/system/gost.service <<SERVICE
[Unit]
Description=GOST Proxy
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/gost -L \${PROXY_USER}:\${PROXY_PASS}@:\${PROXY_PORT} -F socks5://127.0.0.1:40000
Restart=always
User=root
[Install]
WantedBy=multi-user.target
SERVICE
systemctl daemon-reload; systemctl enable gost; systemctl restart gost
EOF
)
        
        # 尝试启动 t3.micro -> t2.micro -> t3.small
        local INSTANCE_ID=""
        for TYPE in "t3.micro" "t2.micro" "t3.small"; do
            local RUN_OUT=$(aws ec2 run-instances --image-id "$AMI_ID" --instance-type "$TYPE" --count 1 --user-data "$RAW_USER_DATA" --subnet-id "$TARGET_SUBNET" --security-group-ids "$SG_ID" --associate-public-ip-address --region "$REGION" --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=Socks5-Proxy}]" --output json 2>&1)
            
            if [[ $? -eq 0 ]]; then
                INSTANCE_ID=$(echo "$RUN_OUT" | grep '"InstanceId":' | cut -d'"' -f4)
                echo -e "${GREEN}成功 ($TYPE)${NC}"
                break
            fi
        done

        if [[ -z "$INSTANCE_ID" ]]; then
            echo -e "${RED}启动失败。${NC}"
        fi
    done

    echo
    echo -e "${GREEN}🎉 矩阵部署完成!正在汇总信息...${NC}"
    sleep 3
    list_proxies
}

# ==============================
# 功能 3: 更换 WARP IP
# ==============================
function rotate_warp_ip() {
    echo -e "${BLUE}>>> 正在全网扫描运行中的实例 (Global Scan)...${NC}"
    echo -n "获取区域列表... "
    local ALL_REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text)
    echo -e "${GREEN}完成${NC}"
    
    local TMP_LIST="/tmp/aws_warp_list_$$"
    rm -f "$TMP_LIST"
    local IDX=0

    # 全网扫描 (仅运行中)
    for r in $ALL_REGIONS; do
        echo -ne "正在扫描 ${CYAN}$r${NC} ...\r"
        local DATA=$(aws ec2 describe-instances --region "$r" --filters "Name=instance-state-name,Values=running" --query "Reservations[].Instances[].[InstanceId,State.Name,Placement.AvailabilityZone,PublicDnsName,PublicIpAddress,NetworkInterfaces[0].Association.CarrierIp,PrivateIpAddress,Tags[?Key=='Name'].Value|[0]]" --output text 2>/dev/null)
        if [[ -n "$DATA" ]]; then
            while read -r line; do
                IDX=$((IDX+1))
                read -r IID STATE AZ DNS PUB CAR PRI NAME <<< "$line"
                echo "$IDX|$r|$IID|$STATE|$AZ|$DNS|$PUB|$CAR|$PRI|$NAME" >> "$TMP_LIST"
            done <<< "$DATA"
        fi
    done
    echo -e "\r\033[K扫描完成。"

    if [[ ! -s "$TMP_LIST" ]]; then echo -e "${RED}未在任何区域发现运行中的实例。${NC}"; echo -e "${BLUE}按回车键返回...${NC}"; read; return; fi

    echo
    print_table_header
    while read -r line; do
        IFS='|' read -r idx r iid state az dns pub car pri name <<< "$line"
        format_instance_row "$idx" "$az" "$name" "$iid" "$state" "$dns" "$pub" "$car" "$pri"
    done < "$TMP_LIST"
    echo "-----------------------------------------------------------------------------------------------------------------"

    echo -n "请选择要更换 WARP IP 的实例编号 [q退出]: "
    read -r choice
    if [[ "$choice" == "q" ]]; then rm -f "$TMP_LIST"; return; fi
    
    local target_line=$(grep "^$choice|" "$TMP_LIST" | head -n 1)
    if [[ -z "$target_line" ]]; then echo "无效编号。"; rm -f "$TMP_LIST"; return; fi
    
    IFS='|' read -r _ TARGET_REGION TARGET_IID _ TARGET_AZ TARGET_DNS TARGET_PUB TARGET_CAR _ _ <<< "$target_line"
    rm -f "$TMP_LIST"
    
    export AWS_REGION="$TARGET_REGION"
    
    # 确定连接IP
    local TARGET_IP="None"
    if [[ "$TARGET_DNS" == ec2-* ]]; then TARGET_IP=$(echo "$TARGET_DNS" | cut -d'.' -f1 | sed 's/ec2-//;s/-/./g'); 
    elif [[ "$TARGET_CAR" != "None" && -n "$TARGET_CAR" ]]; then TARGET_IP="$TARGET_CAR"; 
    elif [[ "$TARGET_PUB" != "None" && -n "$TARGET_PUB" ]]; then TARGET_IP="$TARGET_PUB"; fi
    
    echo
    echo -e "${GREEN}>>> 选中实例: $TARGET_IID ($TARGET_AZ)${NC}"
    echo -e "${GREEN}>>> 管理 IP : $TARGET_IP${NC}"
    
    echo -n "请输入实例 Root 密码 [默认: RootPass123456]: "
    read -r USER_PASS; USER_PASS="${USER_PASS:-RootPass123456}"
    
    local REMOTE_CMD="warp-cli --accept-tos disconnect >/dev/null 2>&1 && warp-cli --accept-tos registration delete >/dev/null 2>&1 && warp-cli --accept-tos registration new >/dev/null 2>&1 && warp-cli --accept-tos connect >/dev/null 2>&1 && sleep 2 && echo -e \"\n\033[0;32m=== WARP 出口 IP (Exit IP) ===\n\$(curl -x socks5h://127.0.0.1:40000 -s ifconfig.me)\n==============================\033[0m\""
    
    if [[ "$TARGET_AZ" == *wlz* || "$TARGET_AZ" == *wavelength* ]]; then
        echo -e "${YELLOW}提示: Wavelength 实例无法直接 SSH,请在内网跳板机上执行以下命令:${NC}"
        echo; if [[ $HAS_SSHPASS -eq 1 ]]; then echo -e "${CYAN}sshpass -p '$USER_PASS' ssh -o StrictHostKeyChecking=no root@$TARGET_IP \"$REMOTE_CMD\"${NC}"; else echo -e "${CYAN}ssh root@$TARGET_IP \"$REMOTE_CMD\"${NC}"; fi
    else
        echo "正在执行更换操作..."
        if [[ $HAS_SSHPASS -eq 1 ]]; then sshpass -p "$USER_PASS" ssh -o ConnectTimeout=8 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@$TARGET_IP "$REMOTE_CMD"
        else echo -e "${YELLOW}⚠️ 未检测到 sshpass,请手动输入密码:${NC}"; ssh -o ConnectTimeout=8 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no root@$TARGET_IP "$REMOTE_CMD"; fi
        if [[ $? -ne 0 ]]; then echo -e "${RED}连接失败。请检查密码或网络。${NC}"; fi
    fi
    echo -e "${BLUE}按回车键返回...${NC}"; read
}

# ==============================
# 功能 4: 删除实例 (Global Scan)
# ==============================
function terminate_instance() {
    echo -e "${BLUE}>>> 正在全网扫描可删除实例 (Global Scan)...${NC}"
    echo -n "获取区域列表... "
    local ALL_REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text)
    echo -e "${GREEN}完成${NC}"
    
    local FOUND_INSTANCES=""
    local IDX=0
    local TMP_LIST="/tmp/aws_global_list_$$"
    rm -f "$TMP_LIST"

    # 并行扫描会更快,这里用串行保证显示顺序稳定
    for r in $ALL_REGIONS; do
        echo -ne "正在扫描 ${CYAN}$r${NC} ...\r"
        local DATA=$(aws ec2 describe-instances --region "$r" --filters "Name=instance-state-name,Values=running,stopped,pending,stopping" --query "Reservations[].Instances[].[InstanceId,State.Name,PublicIpAddress,NetworkInterfaces[0].Association.CarrierIp,PublicDnsName,Tags[?Key=='Name'].Value|[0],Placement.AvailabilityZone,PrivateIpAddress]" --output text 2>/dev/null)
        if [[ -n "$DATA" ]]; then
            while read -r line; do
                IDX=$((IDX+1))
                read -r IID STATE PUB CAR DNS NAME AZ PRI <<< "$line"
                # 智能选择最佳 IP
                local BEST_IP="-"
                if [[ "$DNS" == ec2-* ]]; then BEST_IP=$(echo "$DNS" | cut -d'.' -f1 | sed 's/ec2-//;s/-/./g'); 
                elif [[ "$CAR" != "None" && -n "$CAR" ]]; then BEST_IP="$CAR"; 
                elif [[ "$PUB" != "None" && -n "$PUB" ]]; then BEST_IP="$PUB"; fi
                
                echo "$IDX|$r|$IID|$STATE|$BEST_IP|$NAME|$AZ|$PRI" >> "$TMP_LIST"
            done <<< "$DATA"
        fi
    done
    echo -e "\r\033[K扫描完成。"

    if [[ ! -s "$TMP_LIST" ]]; then echo -e "${RED}未在任何区域发现活跃实例。${NC}"; echo -e "${BLUE}按回车键返回...${NC}"; read; return; fi

    echo
    print_table_header
    while read -r line; do
        IFS='|' read -r idx r iid state ip name az pri <<< "$line"
        format_instance_row "$idx" "$az" "$name" "$iid" "$state" "$ip" "" "$ip" "$pri"
    done < "$TMP_LIST"
    echo "-----------------------------------------------------------------------------------------------------------------"

    echo -n "请选择要删除的实例编号 (支持多选, 如 1,3,5) [q退出]: "
    read -r input_str
    if [[ "$input_str" == "q" ]]; then rm -f "$TMP_LIST"; return; fi
    
    local -a selections
    selections=($(echo "$input_str" | tr ',' ' '))
    
    local -a valid_iids
    local -a valid_regions
    local -a valid_names
    
    for num in "${selections[@]}"; do
        if ! [[ "$num" =~ ^[0-9]+$ ]]; then continue; fi
        local line=$(grep "^$num|" "$TMP_LIST")
        if [[ -n "$line" ]]; then
            IFS='|' read -r _ t_reg t_iid _ _ t_name _ _ <<< "$line"
            valid_iids+=("$t_iid")
            valid_regions+=("$t_reg")
            [[ -z "$t_name" || "$t_name" == "None" ]] && t_name="-"
            valid_names+=("$t_name")
        fi
    done
    
    if [[ ${#valid_iids[@]} -eq 0 ]]; then echo -e "${RED}未选择有效实例。${NC}"; rm -f "$TMP_LIST"; return; fi
    
    echo
    echo -e "${RED}⚠️  警告: 即将删除以下 ${#valid_iids[@]} 个实例:${NC}"
    for (( i=1; i<=${#valid_iids[@]}; i++ )); do
        echo -e "   - [${valid_regions[$i]}] ${valid_iids[$i]} (${valid_names[$i]})"
    done
    
    echo
    echo -n "确认删除? 输入 [y/yes] 继续: "
    read -r confirm
    if [[ "$confirm" == "y" || "$confirm" == "yes" ]]; then
        for (( i=1; i<=${#valid_iids[@]}; i++ )); do
            echo -n "正在终止 ${valid_iids[$i]} ... "
            aws ec2 terminate-instances --instance-ids "${valid_iids[$i]}" --region "${valid_regions[$i]}" >/dev/null 2>&1
            if [[ $? -eq 0 ]]; then echo -e "${GREEN}成功${NC}"; else echo -e "${RED}失败${NC}"; fi
        done
    else
        echo "已取消。"
    fi
    rm -f "$TMP_LIST"
    echo -e "${BLUE}按回车键返回...${NC}"; read
}

# ==============================
# 功能 5: 刷 IP 工具
# ==============================
function run_ip_tool() {
    touch "$USED_IP_FILE"
    echo -e "${BLUE}>>> 正在全网扫描实例 (Global Scan)...${NC}"
    echo -n "获取区域列表... "
    local ALL_REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text)
    echo -e "${GREEN}完成${NC}"
    
    local TMP_LIST="/tmp/aws_ip_tool_list_$$"
    rm -f "$TMP_LIST"
    local IDX=0

    for r in $ALL_REGIONS; do
        echo -ne "正在扫描 ${CYAN}$r${NC} ...\r"
        local DATA=$(aws ec2 describe-instances --region "$r" --filters "Name=instance-state-name,Values=running" --query "Reservations[].Instances[].[InstanceId,State.Name,Placement.AvailabilityZone,PublicDnsName,PublicIpAddress,NetworkInterfaces[0].Association.CarrierIp,PrivateIpAddress,Tags[?Key=='Name'].Value|[0]]" --output text 2>/dev/null)
        if [[ -n "$DATA" ]]; then
            while read -r line; do
                IDX=$((IDX+1))
                read -r IID STATE AZ DNS PUB CAR PRI NAME <<< "$line"
                # IP 优先选择: DNS > Carrier > Public
                local BEST_IP="-"
                if [[ "$DNS" == ec2-* ]]; then BEST_IP=$(echo "$DNS" | cut -d'.' -f1 | sed 's/ec2-//;s/-/./g'); 
                elif [[ "$CAR" != "None" && -n "$CAR" ]]; then BEST_IP="$CAR"; 
                elif [[ "$PUB" != "None" && -n "$PUB" ]]; then BEST_IP="$PUB"; fi
                echo "$IDX|$r|$IID|$STATE|$AZ|$BEST_IP|$PRI|$NAME" >> "$TMP_LIST"
            done <<< "$DATA"
        fi
    done
    echo -e "\r\033[K扫描完成。"

    if [[ ! -s "$TMP_LIST" ]]; then echo -e "${RED}未在任何区域发现运行中的实例。${NC}"; echo -e "${BLUE}按回车键返回...${NC}"; read; return; fi

    echo
    print_table_header
    while read -r line; do
        IFS='|' read -r idx r iid state az ip pri name <<< "$line"
        format_instance_row "$idx" "$az" "$name" "$iid" "$state" "$ip" "" "$ip" "$pri"
    done < "$TMP_LIST"
    echo "-----------------------------------------------------------------------------------------------------------------"

    echo -n "请选择要刷 IP 的实例编号 [q退出]: "
    read -r choice
    if [[ "$choice" == "q" ]]; then rm -f "$TMP_LIST"; return; fi
    
    local target_line=$(grep "^$choice|" "$TMP_LIST")
    if [[ -z "$target_line" ]]; then echo "无效编号。"; rm -f "$TMP_LIST"; return; fi
    
    IFS='|' read -r _ TARGET_REGION TARGET_IID _ TARGET_AZ _ _ _ _ _ <<< "$target_line"
    rm -f "$TMP_LIST"
    
    export AWS_REGION="$TARGET_REGION"
    
    is_wavelength_az() { [[ "$1" == *wlz* ]]; }
    
    local NBG=$(aws ec2 describe-availability-zones --region "$TARGET_REGION" --filters "Name=zone-name,Values=$TARGET_AZ" --query "AvailabilityZones[0].NetworkBorderGroup" --output text)
    
    echo
    echo -e "${GREEN}>>> 选中实例: $TARGET_IID ($TARGET_AZ)${NC}"
    echo -e "${GREEN}>>> 区域: $TARGET_REGION | 边界组: $NBG${NC}"
    echo "-------------------------------------"
    echo " [Enter] 立即更换 IP"
    echo " [q]     退出"
    echo "-------------------------------------"
    
    while true; do
        echo -n "请操作 > "
        if ! read -r input; then break; fi
        if [[ "$input" == "q" ]]; then return; fi
        
        echo -e " [正在执行...]"
        
        local OLD_IP_VAL=$(aws ec2 describe-instances --instance-ids "$TARGET_IID" --region "$TARGET_REGION" --query "Reservations[0].Instances[0].[PublicDnsName,PublicIpAddress,NetworkInterfaces[0].Association.CarrierIp]" --output text)
        
        local ALLOC_ID=$(aws ec2 describe-addresses --filters "Name=instance-id,Values=$TARGET_IID" --query "Addresses[0].AllocationId" --output text --region "$TARGET_REGION")
        if [[ "$ALLOC_ID" != "None" && -n "$ALLOC_ID" ]]; then
            local ASSOC=$(aws ec2 describe-addresses --filters "Name=instance-id,Values=$TARGET_IID" --query "Addresses[0].AssociationId" --output text --region "$TARGET_REGION")
            aws ec2 disassociate-address --association-id "$ASSOC" --region "$TARGET_REGION" 2>/dev/null
            aws ec2 release-address --allocation-id "$ALLOC_ID" --network-border-group "$NBG" --region "$TARGET_REGION" 2>/dev/null
        fi
        
        local NEW_ALLOC=$(aws ec2 allocate-address --domain vpc --network-border-group "$NBG" --query "AllocationId" --output text --region "$TARGET_REGION")
        aws ec2 associate-address --instance-id "$TARGET_IID" --allocation-id "$NEW_ALLOC" --region "$TARGET_REGION" >/dev/null
        
        local NEW_ASSOC=$(aws ec2 describe-addresses --allocation-ids "$NEW_ALLOC" --query "Addresses[0].AssociationId" --output text --region "$TARGET_REGION")
        aws ec2 disassociate-address --association-id "$NEW_ASSOC" --region "$TARGET_REGION" 2>/dev/null
        aws ec2 release-address --allocation-id "$NEW_ALLOC" --network-border-group "$NBG" --region "$TARGET_REGION" 2>/dev/null
        
        echo -n "等待 IP 变更..."
        local NEW_IP="None"
        local retry=0
        while [ $retry -lt 15 ]; do
            sleep 1
            local INFO=$(aws ec2 describe-instances --instance-ids "$TARGET_IID" --region "$TARGET_REGION" --query "Reservations[0].Instances[0].[PublicDnsName,PublicIpAddress,NetworkInterfaces[0].Association.CarrierIp]" --output text)
            read -r DNS PUB CAR <<< "$INFO"
            
            local CUR_IP="None"
            if [[ "$DNS" == ec2-* ]]; then CUR_IP=$(echo "$DNS" | cut -d'.' -f1 | sed 's/ec2-//;s/-/./g'); 
            elif [[ "$CAR" != "None" && -n "$CAR" ]]; then CUR_IP="$CAR"; 
            elif [[ "$PUB" != "None" && -n "$PUB" ]]; then CUR_IP="$PUB"; fi
            
            if [[ "$CUR_IP" != "None" && "$OLD_IP_VAL" != *"$CUR_IP"* ]]; then
                NEW_IP="$CUR_IP"
                break
            fi
            echo -n "."
            retry=$((retry+1))
        done
        echo
        
        if [[ "$NEW_IP" != "None" ]]; then
            echo -e "新 IP: ${GREEN}$NEW_IP${NC}"
            if grep -Fq "$NEW_IP" "$USED_IP_FILE"; then 
                echo -e "${RED}⚠️  检测到重复 IP,建议重试。${NC}"
            else
                echo "$(date +%F_%T) $NEW_IP" >> "$USED_IP_FILE"
            fi
        else
            echo -e "${RED}刷新超时或未变动。${NC}"
        fi
    done
}

# ==============================
# 功能 6: 列出代理 (Global Scan) - v68 统一版
# ==============================
function list_proxies() {
    echo -e "${BLUE}>>> 正在全网扫描已部署的代理实例 (Global Scan)...${NC}"
    echo -n "获取区域列表... "
    local ALL_REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text)
    echo -e "${GREEN}完成${NC}"

    echo
    print_table_header
    
    local -a FORMATTED_LIST=()

    for r in $ALL_REGIONS; do
        local RAW_LIST=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=Socks5-Proxy" "Name=instance-state-name,Values=running" --region "$r" --query "Reservations[].Instances[].[InstanceId, PublicIpAddress, NetworkInterfaces[0].Association.CarrierIp, Placement.AvailabilityZone, PublicDnsName, PrivateIpAddress]" --output text 2>/dev/null)
        
        if [[ -n "$RAW_LIST" ]]; then
            echo "$RAW_LIST" | while read -r IID PUB CAR AZ DNS PRI; do
                local TARGET_IP="None"
                # IP 获取逻辑与全网统一
                if [[ "$DNS" == ec2-* ]]; then
                    TARGET_IP=$(echo "$DNS" | cut -d'.' -f1 | sed 's/ec2-//;s/-/./g')
                elif [[ "$CAR" != "None" && -n "$CAR" ]]; then 
                    TARGET_IP="$CAR"
                elif [[ "$PUB" != "None" && -n "$PUB" ]]; then 
                    TARGET_IP="$PUB"
                fi
                
                local B64_UD=$(aws ec2 describe-instance-attribute --instance-id "$IID" --attribute userData --region "$r" --query "UserData.Value" --output text 2>/dev/null)
                local PASS="Unknown"
                if [[ "$B64_UD" != "None" ]]; then
                    local DECODED=$(echo "$B64_UD" | base64 -d 2>/dev/null)
                    PASS=$(echo "$DECODED" | grep -o 'PROXY_PASS="[^"]*"' | cut -d'"' -f2)
                    if [[ -z "$PASS" ]]; then PASS=$(echo "$DECODED" | grep -o 'PROXY_PASS=[^ ]*' | cut -d'=' -f2); fi
                fi
                [[ -z "$PASS" ]] && PASS="RootPass123456"
                
                # 使用统一的格式化函数 (模拟 State=running)
                format_instance_row "-" "$AZ" "Socks5-Proxy" "$IID" "running" "$DNS" "$PUB" "$CAR" "$PRI"
                
                if [[ "$TARGET_IP" != "None" ]]; then
                    FORMATTED_LIST+=("$TARGET_IP:1080:admin:$PASS")
                fi
            done
        fi
    done
    echo "-----------------------------------------------------------------------------------------------------------------"
    
    if [[ ${#FORMATTED_LIST[@]} -gt 0 ]]; then
        echo
        echo -e "${YELLOW}=== 格式化代理列表 (IP:Port:User:Pass) ===${NC}"
        for proxy in "${FORMATTED_LIST[@]}"; do
            echo "$proxy"
        done
        echo
    fi

    echo -e "${BLUE}按回车键返回...${NC}"; read
}

# ==============================
# 主菜单
# ==============================
while true; do
    clear
    echo -e "${BLUE}========================================${NC}"
    echo -e "${BLUE}    AWS 瑞士军刀 (Zsh v72 UI 规范版)  ${NC}"
    echo -e "${BLUE}========================================${NC}"
    echo
    echo "1. 部署边缘环境 (Wavelength & Local Zone)"
    echo "2. 创建 SOCKS5 代理实例 (Create Proxy)"
    echo "3. 删除实例 (Delete Instance - Global)"
    echo "4. 刷换公网 IP (Public IP Tool - Global)"
    echo "5. 更换 WARP IP (Rotate WARP IP - Global)"
    echo "6. 列出已部署代理 (List Deployed - Global)"
    echo "7. 一键部署美国四地代理 (Deploy US Quad Matrix)"
    echo "8. 退出     (Exit)"
    echo
    echo -n "请输入选项 [1-8]: "
    read -r choice

    case $choice in
        1) deploy_wavelength ;;
        2) create_proxy_instance ;;
        3) terminate_instance ;;
        4) run_ip_tool ;;
        5) rotate_warp_ip ;;
        6) list_proxies ;;
        7) deploy_quad_proxies ;;
        8|q|exit) echo "再见!"; exit 0 ;;
        *) echo "无效选项"; sleep 1 ;;
    esac
done

📸 功能演示

1. 主菜单

脚本启动后,界面清晰明了:

Plaintext
========================================
    AWS 瑞士军刀 (Zsh v7 交互增强版)   
========================================
当前区域: us-east-1

1. 切换区域 (Switch Region)
2. 部署环境 (Wavelength Only)
3. 刷换 IP  (Universal IP Tool)
4. 退出     (Exit)

2. 部署 Wavelength 环境

选择 2,脚本会自动扫描并配置所有网络设施。你只需要看着屏幕上的绿色 OK 即可。针对 InternalError 或网段冲突,脚本会自动重试和避让。

3. 刷 IP 模式

选择 3,脚本会列出当前区域的所有实例。选择一个实例后进入交互模式:

  • Enter:立即更换一个新的 IP。

  • q:返回上级列表选择其他机器。

  • m:直接返回主菜单切换区域。

Plaintext
Wavelength 模式 (i-0xxxxxx)
-------------------------------------
 [Enter] 立即更换 IP
 [q]     返回实例列表
 [m]     返回主菜单
-------------------------------------
请操作 > [正在执行...]
释放旧 IP...
分配新 IP...
解绑并释放...
IP -> 155.146.xx.xx
✅ 已记录:2025-12-23_10:00:00 155.146.xx.xx

💡 常见问题 (FAQ)

Q: 为什么要在 CloudShell 里用?

A: CloudShell 自带了 AWS CLI 和认证信息,网络直通 AWS 内网,延迟极低,刷 IP 速度最快。

Q: 普通 EC2 能用吗?

A: 可以!脚本会自动识别实例所在的子网类型。如果是普通区域,它会使用标准的 Allocate/Release Address 流程更换公网 IP。

Q: 脚本报错 trap: undefined signal: RETURN 怎么办?

A: 请确保使用的是最新版脚本(v3以上),我已经移除了不兼容的信号捕获,改用了标准的 EXIT 信号,完美兼容 macOS。


Next Post Previous Post
No Comment
Add Comment
comment url