#!/usr/bin/env bash # # argo.sh — 一键部署 VLESS + WS + TLS(Cloudflare Tunnel / Argo) # # 用法: # bash <(curl -sL https://example.com/argo.sh) # UUID=xxx ARGO_AUTH=xxx ARGO_DOMAIN=xxx.example.com bash <(curl -sL https://example.com/argo.sh) # bash <(curl -sL https://example.com/argo.sh) -u # 卸载:杀进程并删除安装目录 # # 环境变量参数(全部可选,有默认值): # UUID VLESS 用户 UUID(默认随机生成) # WS_PATH WebSocket 路径,如 /a1b2c3(默认随机 16 位 hex) # ARGO_PORT 本地 xray 监听端口(默认 18200;注意:某些受限容器只能绑定 >=10000 的端口) # ARGO_AUTH Cloudflare Tunnel Token(为空则使用 quick tunnel 临时域名) # ARGO_DOMAIN 命名隧道对外域名(ARGO_AUTH 非空时必填; # 需在 CF 后台给该域名加 Public Hostname -> http://127.0.0.1:${ARGO_PORT}) # CFIP 客户端连接地址,支持填 Cloudflare 优选 IP(默认等于对外域名) # CFPORT 客户端连接端口(默认 443) # INSTALL_DIR 安装目录(默认 $HOME/argo) # # 位置参数: # KEY=VALUE 与同名环境变量等效,如 UUID=xxx ARGO_PORT=18300 # -u, --uninstall # 卸载模式:停止该 INSTALL_DIR 下的 xray / cloudflared 进程, # 删除整个 INSTALL_DIR 后退出(INSTALL_DIR 为空、/ 或 $HOME 时拒绝执行) # # 产物(供 nodejs / java 等程序集成): # $INSTALL_DIR/result.json 机器可读结果:{"uuid":..,"domain":..,"address":.., # "port":..,"path":..,"link":"vless://.."} # 标准输出会打印 vless:// 链接;失败时非零退出,错误信息打到 stderr。 # set -u # ---------------- 参数 ---------------- UUID="${UUID:-}" WS_PATH="${WS_PATH:-}" ARGO_PORT="${ARGO_PORT:-18200}" ARGO_AUTH="${ARGO_AUTH:-}" ARGO_DOMAIN="${ARGO_DOMAIN:-}" CFIP="${CFIP:-}" CFPORT="${CFPORT:-443}" INSTALL_DIR="${INSTALL_DIR:-$HOME/argo}" UNINSTALL=0 log() { echo "[argo] $*" >&2; } die() { echo "[argo][ERROR] $*" >&2; exit 1; } kill_pat() { # $1 = 匹配串,$2 = 信号(默认 TERM) local sig="${2:-TERM}" for p in $(ps -eo pid,args 2>/dev/null | grep -F "$1" | grep -v grep | awk '{print $1}'); do kill "-$sig" "$p" 2>/dev/null && log "停止旧进程 $p ($1)" done } # 解压工具检测:unzip > python3 > perl(需 IO::Uncompress::Unzip 模块) have_unzip() { command -v unzip >/dev/null 2>&1; } have_py3() { command -v python3 >/dev/null 2>&1; } have_perlzip() { perl -MIO::Uncompress::Unzip -e '1' >/dev/null 2>&1; } # 三者都没有时,尝试用包管理器自动安装 unzip(非 root 用 sudo -n,非交互避免卡住) try_install_unzip() { local sudo="" if [ "$(id -u)" != "0" ]; then command -v sudo >/dev/null 2>&1 || return 1 sudo="sudo -n" fi log "未检测到解压工具,尝试自动安装 unzip..." if command -v apt-get >/dev/null 2>&1; then $sudo apt-get update -qq && $sudo apt-get install -y -qq unzip elif command -v dnf >/dev/null 2>&1; then $sudo dnf install -y -q unzip elif command -v yum >/dev/null 2>&1; then $sudo yum install -y -q unzip elif command -v apk >/dev/null 2>&1; then $sudo apk add --no-cache -q unzip elif command -v pacman >/dev/null 2>&1; then $sudo pacman -Sy --noconfirm -q unzip else return 1 fi command -v unzip >/dev/null 2>&1 } # 允许 KEY=VALUE 形式的位置参数:bash <(curl ..) UUID=xxx ARGO_PORT=18300 # -u / --uninstall:卸载模式 for arg in "$@"; do case "$arg" in -u|--uninstall) UNINSTALL=1 ;; *=*) k="${arg%%=*}"; v="${arg#*=}" case "$k" in UUID|WS_PATH|ARGO_PORT|ARGO_AUTH|ARGO_DOMAIN|CFIP|CFPORT|INSTALL_DIR) eval "$k=\"\$v\"";; *) log "忽略未知参数: $k";; esac;; *) log "忽略未知参数: $arg";; esac done # ---------------- 卸载 ---------------- if [ "$UNINSTALL" = 1 ]; then case "$INSTALL_DIR" in ""|"/"|"$HOME") die "拒绝删除危险目录: ${INSTALL_DIR:-<空>}" ;; esac log "卸载 argo,目标目录: $INSTALL_DIR" kill_pat "$INSTALL_DIR/xray run" TERM kill_pat "$INSTALL_DIR/cloudflared tunnel" TERM sleep 2 kill_pat "$INSTALL_DIR/xray run" KILL kill_pat "$INSTALL_DIR/cloudflared tunnel" KILL if [ -d "$INSTALL_DIR" ]; then rm -rf "$INSTALL_DIR" && log "已删除目录 $INSTALL_DIR" || die "删除 $INSTALL_DIR 失败" else log "目录不存在,跳过删除: $INSTALL_DIR" fi log "卸载完成" exit 0 fi # ---------------- 校验 ---------------- command -v curl >/dev/null 2>&1 || die "需要 curl,请先安装" [ -n "$UUID" ] || UUID="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "")" [[ "$UUID" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]] \ || die "UUID 格式非法: $UUID" [ -n "$WS_PATH" ] || WS_PATH="/$(head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n')" [[ "$WS_PATH" == /* ]] || WS_PATH="/$WS_PATH" [[ "$ARGO_PORT" =~ ^[0-9]+$ ]] || die "ARGO_PORT 非法: $ARGO_PORT" if [ -n "$ARGO_AUTH" ] && [ -z "$ARGO_DOMAIN" ]; then die "使用命名隧道时必须提供 ARGO_DOMAIN" fi ARCH="$(uname -m)" case "$ARCH" in x86_64) CF_ARCH=amd64; XRAY_ARCH=64 ;; aarch64|arm64) CF_ARCH=arm64; XRAY_ARCH=arm64 ;; *) die "不支持的架构: $ARCH" ;; esac mkdir -p "$INSTALL_DIR" || die "无法创建目录 $INSTALL_DIR" cd "$INSTALL_DIR" || die "无法进入目录 $INSTALL_DIR" # ---------------- 下载 ---------------- CF_BIN="$INSTALL_DIR/cloudflared" XRAY_BIN="$INSTALL_DIR/xray" if [ ! -x "$CF_BIN" ]; then log "下载 cloudflared ($CF_ARCH)..." curl -fsSL --retry 3 -o "$CF_BIN" \ "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${CF_ARCH}" \ || die "cloudflared 下载失败" chmod +x "$CF_BIN" fi if [ ! -x "$XRAY_BIN" ]; then if ! have_unzip && ! have_py3 && ! have_perlzip; then try_install_unzip || die "未找到解压工具且自动安装失败,请手动安装后重试:apt-get install -y unzip(debian/ubuntu)或 yum install -y unzip(rhel 系)" fi log "下载 xray..." XRAY_VER="$(curl -fsSL --max-time 10 https://api.github.com/repos/XTLS/Xray-core/releases/latest 2>/dev/null \ | grep -m1 '"tag_name"' | cut -d'"' -f4)" [ -n "$XRAY_VER" ] || XRAY_VER="v26.3.27" # API 失败时的兜底版本 curl -fsSL --retry 3 -o xray.zip \ "https://github.com/XTLS/Xray-core/releases/download/${XRAY_VER}/Xray-linux-${XRAY_ARCH}.zip" \ || die "xray 下载失败" if have_unzip; then unzip -o -q xray.zip xray || die "xray 解压失败" elif have_py3; then python3 -c "import zipfile; zipfile.ZipFile('xray.zip').extract('xray')" || die "xray 解压失败" elif have_perlzip; then # perl 兜底(极简容器常用;已预检 IO::Uncompress::Unzip 模块可用) perl -MIO::Uncompress::Unzip -e ' my $z = new IO::Uncompress::Unzip "xray.zip" or die "open: $!"; while ($z->nextStream()) { my $n = $z->getHeaderInfo()->{Name}; if ($n eq "xray") { open my $o, ">", "xray" or die "write: $!"; binmode $o; my $buf; while ($z->read($buf, 65536) > 0) { print $o $buf; } close $o; } }' || die "xray 解压失败" else die "xray 解压失败(未找到可用的解压工具)" fi chmod +x "$XRAY_BIN" rm -f xray.zip fi log "二进制就绪: $($XRAY_BIN version 2>/dev/null | head -1)" # ---------------- 清理旧进程(幂等) ---------------- kill_pat "$INSTALL_DIR/xray run" kill_pat "$INSTALL_DIR/cloudflared tunnel" sleep 1 # ---------------- xray 配置 ---------------- printf '%s\n' \ '{' \ ' "log": {"loglevel": "warning"},' \ ' "inbounds": [' \ ' {' \ " \"port\": ${ARGO_PORT}," \ ' "listen": "127.0.0.1",' \ ' "protocol": "vless",' \ ' "settings": {' \ " \"clients\": [{\"id\": \"${UUID}\"}], " \ ' "decryption": "none"' \ ' },' \ ' "streamSettings": {' \ ' "network": "ws",' \ " \"wsSettings\": {\"path\": \"${WS_PATH}\"}" \ ' }' \ ' }' \ ' ],' \ ' "outbounds": [{"protocol": "freedom"}]' \ '}' > xray.json wait_port() { # $1=port, $2=超时秒 local i=0 while [ $i -lt "$2" ]; do if (exec 3<>/dev/tcp/127.0.0.1/$1) 2>/dev/null; then return 0; fi sleep 1; i=$((i+1)) done return 1 } log "启动 xray (127.0.0.1:${ARGO_PORT})..." setsid nohup "$XRAY_BIN" run -c xray.json >>xray.log 2>&1 < /dev/null & wait_port "$ARGO_PORT" 15 || { tail -20 xray.log >&2; die "xray 启动失败,详见 $INSTALL_DIR/xray.log"; } log "xray 运行正常" # ---------------- cloudflared 隧道 ---------------- rm -f cloudflared.log if [ -z "$ARGO_AUTH" ]; then log "启动 cloudflared quick tunnel..." setsid nohup "$CF_BIN" tunnel --no-autoupdate --url "http://127.0.0.1:${ARGO_PORT}" \ >>cloudflared.log 2>&1 < /dev/null & PUB_DOMAIN="" for _ in $(seq 1 30); do if grep -aq 'failed to request quick Tunnel' cloudflared.log 2>/dev/null; then tail -20 cloudflared.log >&2 die "quick tunnel 创建失败(网络可能无法直连 Cloudflare API),详见 $INSTALL_DIR/cloudflared.log" fi cand="$(grep -ao 'https://[a-z0-9][a-z0-9.-]*\.trycloudflare\.com' cloudflared.log 2>/dev/null \ | grep -v '^https://api\.' | head -1 | sed 's#https://##')" if [ -n "$cand" ] && grep -aq 'Registered tunnel connection' cloudflared.log 2>/dev/null; then PUB_DOMAIN="$cand"; break fi sleep 2 done [ -n "$PUB_DOMAIN" ] || { tail -20 cloudflared.log >&2; die "quick tunnel 未能获取域名"; } log "quick tunnel 域名: $PUB_DOMAIN" else log "启动 cloudflared 命名隧道..." PUB_DOMAIN="$ARGO_DOMAIN" setsid nohup "$CF_BIN" tunnel --no-autoupdate run --token "$ARGO_AUTH" \ >>cloudflared.log 2>&1 < /dev/null & ok=0 for _ in $(seq 1 30); do grep -aq 'Registered tunnel connection' cloudflared.log 2>/dev/null && { ok=1; break; } grep -aqi 'ERR\|error' cloudflared.log 2>/dev/null && break sleep 2 done [ "$ok" = 1 ] || { tail -20 cloudflared.log >&2; die "命名隧道注册失败"; } log "命名隧道已注册: $PUB_DOMAIN" fi # ---------------- 生成客户端链接 ---------------- CLIENT_ADDR="${CFIP:-$PUB_DOMAIN}" ENC_PATH="$(printf '%s' "${WS_PATH#/}" | sed 's#/#%2F#g')" LINK="vless://${UUID}@${CLIENT_ADDR}:${CFPORT}?encryption=none&security=tls&sni=${PUB_DOMAIN}&type=ws&host=${PUB_DOMAIN}&path=%2F${ENC_PATH}#argo-vless" cat > result.json <