- 下载基址统一 RELEASE_BASE 指向 Gitea 发行 V1.0.0:agent 二进制(原在旧 tag v3.7.0-agent.1)、双架构 xray zip、geoip/geosite 全部走自建镜像, 不再依赖 GitHub CDN - 未传 --master/--token/--name 时逐字段沿用本机 /etc/x-ui/agent.json, 显式参数优先;日志打明沿用的节点名与主控,绝不打印 token;文件不可 解析且必填仍缺则明确报错退出 - 每步下载/解压显式判失败(拆掉旧 && 链的静默吞错),下载验非空,前置 检查 curl/unzip;收尾 self_check 真跑 xray version、验 geo 非空与 agent 可执行,任一不过报错退出且不写配置不起服务 - fake-root 模拟 9 场景 58 断言全 PASS(报告 NEXT-SCRIPTS-REPORT.md)
326 lines
14 KiB
Bash
Executable File
326 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
||
# 3x-ui-agent 节点一键安装脚本
|
||
# 用法: bash install-agent.sh --master http://MASTER_IP:2053 --token YOUR_AGENT_TOKEN [--name my-node]
|
||
# 要求: Debian/Ubuntu/Alpine/CentOS (amd64/arm64), root 运行
|
||
#
|
||
# 无参沿用(第 16 条):已装过 agent 的机器重跑本脚本升级时,可省略全部参数——
|
||
# 脚本会从本机 /etc/x-ui/agent.json 沿用 master_url/token/node_name;显式传入的
|
||
# 参数逐字段覆盖沿用值。日志只打明沿用的节点名与主控地址,绝不打印 token。
|
||
#
|
||
# 测试用环境变量(一般无需设置):
|
||
# XUI_ROOT 安装路径前缀(假根目录验证用),默认空 = 真实路径
|
||
# XUI_INIT_SYSTEM 强制 init 系统: systemd | openrc | none,默认 auto 探测
|
||
# XUI_URL agent 二进制下载地址覆盖
|
||
#
|
||
# 二进制命名(P12 改名):agent 独立装为 /usr/local/x-ui/xui-agent,不再与主控共用
|
||
# x-ui 文件——①ps 里进程名即 xui-agent,与主控一眼可辨;②同机部署时 agent 的
|
||
# 安装/升级永不写主控文件,不再撞 Text file busy;③进程清理只按 "xui-agent agent"
|
||
# 精确匹配 agent 自身。存量旧 agent 的进程形态是 "x-ui agent"(带 agent 子命令),
|
||
# 与主控的裸 "x-ui"(无参数)天然可区分,迁移清理只杀前者、绝不误杀主控。
|
||
|
||
set -e
|
||
|
||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
|
||
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||
|
||
[[ $EUID -ne 0 ]] && error "请用 root 运行"
|
||
command -v curl >/dev/null 2>&1 || error "缺少 curl,请先安装 curl 后重跑"
|
||
command -v unzip >/dev/null 2>&1 || error "缺少 unzip,请先安装 unzip 后重跑"
|
||
|
||
MASTER=""; TOKEN=""; NAME=""
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--master) MASTER="$2"; shift 2;;
|
||
--token) TOKEN="$2"; shift 2;;
|
||
--name) NAME="$2"; shift 2;;
|
||
*) error "未知参数: $1";;
|
||
esac
|
||
done
|
||
|
||
# 无参沿用(第 16 条):参数缺项时,从本机已有 agent.json 逐字段补默认值,
|
||
# 显式参数优先。agent.json 由本脚本以固定格式写出,这里用 sed 提取字段,
|
||
# 不依赖 jq。文件不存在或解析不出可用字段、且必填值仍缺 → 下方校验明确报错。
|
||
AGENT_JSON_PATH="${XUI_ROOT:-}/etc/x-ui/agent.json"
|
||
json_get() { # $1=文件 $2=键名 → 打印值(取不到则空)
|
||
sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$1" 2>/dev/null | head -n 1
|
||
}
|
||
if [[ -f "$AGENT_JSON_PATH" ]]; then
|
||
INHERITED=()
|
||
f_master=$(json_get "$AGENT_JSON_PATH" master_url)
|
||
f_token=$(json_get "$AGENT_JSON_PATH" token)
|
||
f_name=$(json_get "$AGENT_JSON_PATH" node_name)
|
||
if [[ -z "$MASTER" && -n "$f_master" ]]; then MASTER="$f_master"; INHERITED+=("主控地址"); fi
|
||
if [[ -z "$TOKEN" && -n "$f_token" ]]; then TOKEN="$f_token"; INHERITED+=("token"); fi
|
||
if [[ -z "$NAME" && -n "$f_name" ]]; then NAME="$f_name"; INHERITED+=("节点名"); fi
|
||
if [[ ${#INHERITED[@]} -gt 0 ]]; then
|
||
info "沿用本机已有配置 ${AGENT_JSON_PATH}(${INHERITED[*]}):节点名=${NAME},主控=${MASTER}(token 已沿用,不显示)"
|
||
elif [[ -z "$MASTER" || -z "$TOKEN" ]]; then
|
||
warn "发现已有配置 ${AGENT_JSON_PATH},但未能解析出可用字段"
|
||
fi
|
||
fi
|
||
|
||
[[ -z "$MASTER" ]] && error "缺少 --master 参数(本机亦无可沿用的 agent.json)"
|
||
[[ -z "$TOKEN" ]] && error "缺少 --token 参数 (从主控面板 API 获取)"
|
||
[[ -z "$NAME" ]] && NAME="agent-node"
|
||
|
||
ARCH=$(uname -m)
|
||
case "$ARCH" in
|
||
x86_64|amd64) BIN_ARCH="amd64";;
|
||
aarch64|arm64) BIN_ARCH="arm64";;
|
||
*) error "不支持的架构: $ARCH (仅支持 amd64/arm64)";;
|
||
esac
|
||
|
||
# 发行下载基址(本仓 Gitea Releases,发行 tag V1.0.0):agent 二进制、xray、geo
|
||
# 全部走自建镜像,不再依赖 GitHub CDN(部分节点极慢甚至僵死,见第 15 条)。
|
||
RELEASE_BASE="https://git.881218.xyz/rogue/3x-ui-agent/releases/download/V1.0.0"
|
||
XUI_URL="${XUI_URL:-${RELEASE_BASE}/x-ui-linux-${BIN_ARCH}}"
|
||
XRAY_VER="26.6.27"
|
||
|
||
ROOT="${XUI_ROOT:-}"
|
||
XUI_HOME="${ROOT}/usr/local/x-ui"
|
||
CONF_DIR="${ROOT}/etc/x-ui"
|
||
AGENT_BIN="${XUI_HOME}/xui-agent"
|
||
|
||
info "创建目录..."
|
||
mkdir -p "${XUI_HOME}/bin" "${CONF_DIR}"
|
||
|
||
info "下载 agent 二进制..."
|
||
# 先下到 .new 再改名覆盖:改名是原子替换,正在运行的旧 agent 进程不受影响,
|
||
# 也不会因目标文件正被执行而报 Text file busy;全程只写 xui-agent,不碰主控的 x-ui。
|
||
curl -fL -o "${AGENT_BIN}.new" "$XUI_URL" || error "agent 下载失败: $XUI_URL"
|
||
[[ -s "${AGENT_BIN}.new" ]] || error "agent 下载得到空文件: $XUI_URL"
|
||
chmod +x "${AGENT_BIN}.new"
|
||
mv -f "${AGENT_BIN}.new" "${AGENT_BIN}"
|
||
|
||
info "下载 xray v${XRAY_VER}..."
|
||
cd "${XUI_HOME}/bin"
|
||
case "$BIN_ARCH" in
|
||
amd64) XRAY_ZIP="Xray-linux-64.zip";;
|
||
arm64) XRAY_ZIP="Xray-linux-arm64-v8a.zip";;
|
||
esac
|
||
# 每一步都显式判失败并报错退出(第 17 条):历史教训——下载/解压失败曾被
|
||
# && 链条吞掉,脚本带病走完报成功,结果 bin 里没有 xray、入站全断而面板显在线。
|
||
curl -fL -o "$XRAY_ZIP" "${RELEASE_BASE}/${XRAY_ZIP}" || error "xray 下载失败: ${RELEASE_BASE}/${XRAY_ZIP}"
|
||
[[ -s "$XRAY_ZIP" ]] || error "xray 下载得到空文件: ${RELEASE_BASE}/${XRAY_ZIP}"
|
||
unzip -o -q "$XRAY_ZIP" xray || error "xray 解压失败: $XRAY_ZIP"
|
||
mv xray "xray-linux-${BIN_ARCH}" || error "xray 落位失败(解压产物缺失)"
|
||
chmod +x "xray-linux-${BIN_ARCH}"
|
||
rm -f "$XRAY_ZIP"
|
||
|
||
info "下载 geo 文件..."
|
||
curl -fL -o geoip.dat "${RELEASE_BASE}/geoip.dat" || error "geoip 下载失败: ${RELEASE_BASE}/geoip.dat"
|
||
[[ -s geoip.dat ]] || error "geoip 下载得到空文件: ${RELEASE_BASE}/geoip.dat"
|
||
curl -fL -o geosite.dat "${RELEASE_BASE}/geosite.dat" || error "geosite 下载失败: ${RELEASE_BASE}/geosite.dat"
|
||
[[ -s geosite.dat ]] || error "geosite 下载得到空文件: ${RELEASE_BASE}/geosite.dat"
|
||
cd - >/dev/null
|
||
|
||
# 安装收尾自检(第 17 条):文件在不等于能用——xray 本体须存在、可执行、
|
||
# version 能真实跑通;geo 须存在且非空;agent 本体须可执行。任一不过即
|
||
# 报错退出并指出缺什么,不许带病进入写配置/起服务阶段。
|
||
self_check() {
|
||
local xray_bin="${XUI_HOME}/bin/xray-linux-${BIN_ARCH}"
|
||
[[ -f "$xray_bin" ]] || error "自检失败:缺少 xray 本体 ${xray_bin}"
|
||
[[ -x "$xray_bin" ]] || error "自检失败:xray 本体不可执行 ${xray_bin}"
|
||
local ver_out
|
||
if ! ver_out=$("$xray_bin" version 2>/dev/null); then
|
||
error "自检失败:xray 本体无法运行(${xray_bin} version 退出非零)"
|
||
fi
|
||
[[ -n "$ver_out" ]] || error "自检失败:xray version 无输出(${xray_bin})"
|
||
info "自检通过:$(printf '%s' "$ver_out" | head -n 1)"
|
||
[[ -s "${XUI_HOME}/bin/geoip.dat" ]] || error "自检失败:geoip.dat 缺失或为空(${XUI_HOME}/bin/geoip.dat)"
|
||
[[ -s "${XUI_HOME}/bin/geosite.dat" ]] || error "自检失败:geosite.dat 缺失或为空(${XUI_HOME}/bin/geosite.dat)"
|
||
[[ -x "$AGENT_BIN" ]] || error "自检失败:agent 二进制不可执行 ${AGENT_BIN}"
|
||
}
|
||
self_check
|
||
|
||
info "写入 agent 配置..."
|
||
cat > "${CONF_DIR}/agent.json" <<EOF
|
||
{
|
||
"master_url": "$MASTER",
|
||
"token": "$TOKEN",
|
||
"node_name": "$NAME"
|
||
}
|
||
EOF
|
||
chmod 600 "${CONF_DIR}/agent.json"
|
||
|
||
# 探测主控可达性:拿假 token 打一次注册接口,在启动前把地址问题拦下来,
|
||
# 别让 agent 带着必死的配置装完再无声无息地死掉(主控上一直显示"未知")。
|
||
# 404 → 注册路由不存在,主控地址大概率少了面板基础路径
|
||
# 连不上 → 网络/域名/TLS 问题
|
||
# 其他(401等) → 路由存在(假 token 被拒正是预期),路径正确
|
||
probe_master() {
|
||
local url="${MASTER%/}/panel/api/agent/register"
|
||
info "探测主控注册接口: $url"
|
||
local code
|
||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST "$url" \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Authorization: Bearer install-probe-invalid-token' \
|
||
-d '{"token":"install-probe-invalid-token","name":"install-probe"}' 2>/dev/null) || code="000"
|
||
case "$code" in
|
||
404)
|
||
echo ""
|
||
echo -e "${RED}============================================================${NC}"
|
||
echo -e "${RED} 主控返回 404:注册接口不存在,安装中止。${NC}"
|
||
echo -e "${RED} 主控地址大概率少了面板基础路径(如 /rogue)。${NC}"
|
||
echo -e "${RED} 当前地址: ${MASTER}${NC}"
|
||
echo -e "${RED} 若面板实际地址带基础路径,请改用 --master ${MASTER%/}/rogue${NC}"
|
||
echo -e "${RED} 之类的完整地址(以面板真实基础路径为准)改对后重跑本脚本。${NC}"
|
||
echo -e "${RED}============================================================${NC}"
|
||
exit 1
|
||
;;
|
||
000)
|
||
echo ""
|
||
echo -e "${RED}============================================================${NC}"
|
||
echo -e "${RED} 连不上主控,安装中止。${NC}"
|
||
echo -e "${RED} 请检查:网络是否通、域名解析是否正常、主控地址与端口是否写对、${NC}"
|
||
echo -e "${RED} 以及主控 HTTPS 证书是否有效(agent 不接受自签证书)。${NC}"
|
||
echo -e "${RED} 目标: $url${NC}"
|
||
echo -e "${RED}============================================================${NC}"
|
||
exit 1
|
||
;;
|
||
*)
|
||
info "主控探测通过 (HTTP $code,注册路由存在)"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
probe_master
|
||
|
||
# 存量迁移清理:旧版脚本把 agent 装在主控文件 x-ui 上(进程形态 "x-ui agent"),
|
||
# 重跑本脚本升级时先停掉这类旧进程,服务定义随后会被新单元覆盖。
|
||
# 安全边界:只精确匹配带 agent 子命令的旧路径进程;主控是裸 x-ui(无 agent 参数),
|
||
# 同机部署时绝不误杀。本脚本任何地方都不写、不删主控文件 x-ui 本身——agent-only
|
||
# 节点上它在迁移后成为无用残留,确认该机不跑主控后可人工删除。
|
||
stop_legacy_agent() {
|
||
pkill -f "${XUI_HOME}/x-ui agent" 2>/dev/null || true
|
||
}
|
||
|
||
detect_init_system() {
|
||
case "${XUI_INIT_SYSTEM:-auto}" in
|
||
systemd|openrc|none) echo "${XUI_INIT_SYSTEM}"; return;;
|
||
esac
|
||
if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then
|
||
echo systemd; return
|
||
fi
|
||
if command -v rc-update >/dev/null 2>&1 && command -v rc-service >/dev/null 2>&1; then
|
||
echo openrc; return
|
||
fi
|
||
echo none
|
||
}
|
||
|
||
# 以 systemd 服务方式安装并启动(开机自启)
|
||
start_via_systemd() {
|
||
local unit_dir="${ROOT}/etc/systemd/system"
|
||
mkdir -p "$unit_dir"
|
||
cat > "${unit_dir}/x-ui-agent.service" <<'EOF'
|
||
[Unit]
|
||
Description=3x-ui agent
|
||
After=network.target
|
||
|
||
[Service]
|
||
ExecStart=/usr/local/x-ui/xui-agent agent
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
info "已生成 systemd 服务文件: ${unit_dir}/x-ui-agent.service"
|
||
if [ -n "$ROOT" ]; then
|
||
info "(XUI_ROOT 模式:跳过 systemctl 实际操作)"
|
||
return 0
|
||
fi
|
||
systemctl stop x-ui-agent 2>/dev/null || true
|
||
stop_legacy_agent
|
||
pkill -f "${AGENT_BIN} agent" 2>/dev/null || true; sleep 1
|
||
systemctl daemon-reload
|
||
systemctl enable x-ui-agent
|
||
systemctl restart x-ui-agent
|
||
sleep 2
|
||
if systemctl is-active --quiet x-ui-agent; then
|
||
info "agent 已由 systemd 托管启动(开机自启)"
|
||
echo ""
|
||
echo " 主控: $MASTER"
|
||
echo " 节点名: $NAME"
|
||
echo " 日志: journalctl -u x-ui-agent -f"
|
||
echo " agent 会在 30 秒内从主控拉取配置"
|
||
else
|
||
error "agent 服务启动失败,查看: systemctl status x-ui-agent"
|
||
fi
|
||
}
|
||
|
||
# 以 OpenRC 服务方式安装并启动(开机自启,Alpine 等)
|
||
start_via_openrc() {
|
||
local init_dir="${ROOT}/etc/init.d"
|
||
mkdir -p "$init_dir"
|
||
cat > "${init_dir}/x-ui-agent" <<'EOF'
|
||
#!/sbin/openrc-run
|
||
|
||
name="x-ui-agent"
|
||
description="3x-ui lightweight node agent"
|
||
command="/usr/local/x-ui/xui-agent"
|
||
command_args="agent"
|
||
command_background="yes"
|
||
pidfile="/run/x-ui-agent.pid"
|
||
output_log="/var/log/x-ui-agent.log"
|
||
error_log="/var/log/x-ui-agent.log"
|
||
|
||
depend() {
|
||
need net
|
||
}
|
||
EOF
|
||
chmod +x "${init_dir}/x-ui-agent"
|
||
info "已生成 OpenRC 服务文件: ${init_dir}/x-ui-agent"
|
||
if [ -n "$ROOT" ]; then
|
||
info "(XUI_ROOT 模式:跳过 rc-update/rc-service 实际操作)"
|
||
return 0
|
||
fi
|
||
rc-service x-ui-agent stop 2>/dev/null || true
|
||
stop_legacy_agent
|
||
pkill -f "${AGENT_BIN} agent" 2>/dev/null || true; sleep 1
|
||
rc-update add x-ui-agent default
|
||
rc-service x-ui-agent restart
|
||
sleep 2
|
||
if rc-service x-ui-agent status >/dev/null 2>&1; then
|
||
info "agent 已由 OpenRC 托管启动(开机自启)"
|
||
echo ""
|
||
echo " 主控: $MASTER"
|
||
echo " 节点名: $NAME"
|
||
echo " 日志: tail -f /var/log/x-ui-agent.log"
|
||
echo " agent 会在 30 秒内从主控拉取配置"
|
||
else
|
||
error "agent 服务启动失败,查看: rc-service x-ui-agent status"
|
||
fi
|
||
}
|
||
|
||
# 无服务管理器时回落 nohup(无开机自启,明确警告)
|
||
start_via_nohup() {
|
||
warn "未检测到 systemd / OpenRC,退回 nohup 方式启动"
|
||
warn "注意:nohup 方式没有开机自启,机器重启后 agent 不会自动恢复,需手动重跑本脚本!"
|
||
stop_legacy_agent
|
||
pkill -f "${AGENT_BIN} agent" 2>/dev/null || true; sleep 1
|
||
# -config 显式给出(生产环境即其默认路径 /etc/x-ui/agent.json,行为不变),
|
||
# 这样 XUI_ROOT 假根验证时 agent 也能找到刚写下的配置。
|
||
nohup "${AGENT_BIN}" agent -config "${CONF_DIR}/agent.json" > /tmp/agent.log 2>&1 &
|
||
sleep 8
|
||
if grep -q "Starting 3x-ui agent" /tmp/agent.log 2>/dev/null; then
|
||
info "agent 启动成功!"
|
||
echo ""
|
||
echo " 主控: $MASTER"
|
||
echo " 节点名: $NAME"
|
||
echo " 日志: tail -f /tmp/agent.log"
|
||
echo " agent 会在 30 秒内从主控拉取配置"
|
||
else
|
||
error "启动失败,查看日志: cat /tmp/agent.log"
|
||
fi
|
||
}
|
||
|
||
info "启动 agent..."
|
||
case "$(detect_init_system)" in
|
||
systemd) start_via_systemd;;
|
||
openrc) start_via_openrc;;
|
||
*) start_via_nohup;;
|
||
esac
|