#!/bin/bash # 3x-ui-agent 节点一键安装脚本 # 用法: bash install-agent.sh --master http://MASTER_IP:2053 --token YOUR_AGENT_TOKEN [--name my-node] [--probe-only] # --probe-only(V2.1.0):纯探针安装——跳过 sing-box 核心下载与自检, # agent.json 写入 probeOnly=true;agent 只跑探针通道、不运行任何核心。 # 要求: 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 | procd | 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 后重跑" MASTER=""; TOKEN=""; NAME=""; PROBE_ONLY="" while [[ $# -gt 0 ]]; do case $1 in --master) MASTER="$2"; shift 2;; --token) TOKEN="$2"; shift 2;; --name) NAME="$2"; shift 2;; --probe-only) PROBE_ONLY=1; shift;; *) 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 # probeOnly 是布尔字段,json_get 只取字符串值,这里单独按 true 沿用。 if [[ -z "$PROBE_ONLY" ]] && grep -q '"probeOnly"[[:space:]]*:[[:space:]]*true' "$AGENT_JSON_PATH"; then PROBE_ONLY=1; 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 V2.0.0):agent 二进制走 # 自建镜像,不再依赖 GitHub CDN(部分节点极慢甚至僵死,见第 15 条)。 # sing-box 核心在 NexusGate 仓的专用发行里(见下方 SINGBOX_BASE)。 # 注意:本脚本是 V2 版——只装 sing-box 运行时;主控未升 V2 前不要用它 # 重装节点(节点升级走面板一键更新,不需要本脚本)。 RELEASE_BASE="https://git.881218.xyz/rogue/3x-ui-agent/releases/download/V2.2.1" XUI_URL="${XUI_URL:-${RELEASE_BASE}/x-ui-linux-${BIN_ARCH}}" 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}" # 第 6 条 B5 正式切换:agent 只装 sing-box 核心(自编 v1.13.21, # with_v2ray_api with_utls with_quic),xray 与 geo 下载已移除—— # sing-box 配置零 geo 引用,节点不再需要 geo 文件;xray 本体亦不再 # 随装。存量节点 bin 里已有的 xray/geo 文件本脚本不删(回滚旧主控时 # 仍可用)。sing-box 资产在 NexusGate 仓的专用发行里(与 NexusGate # 共用同一自编件)。其 version 输出为 unknown(自编件无版本戳), # 故另写版本记录文件供 agent 遥测读取。 # 每一步都显式判失败并报错退出(第 17 条):历史教训——下载失败曾被 # && 链条吞掉,脚本带病走完报成功,结果核心缺失、入站全断而面板显在线。 SINGBOX_VER="1.13.21" SINGBOX_BASE="https://git.881218.xyz/rogue/NexusGate/releases/download/sing-box-v${SINGBOX_VER}" SINGBOX_URL="${SINGBOX_URL:-${SINGBOX_BASE}/nexusgate-sing-box-linux-${BIN_ARCH}}" if [[ "$PROBE_ONLY" == "1" ]]; then # 纯探针节点不运行核心(V2.1.0):不下载、不落地 sing-box; # 本机已有的核心文件不删(关掉纯探针后可直接复用)。 info "纯探针模式:跳过 sing-box 核心下载" else info "下载 sing-box v${SINGBOX_VER}..." curl -fL -o "${XUI_HOME}/bin/sing-box-linux-${BIN_ARCH}.new" "$SINGBOX_URL" || error "sing-box 下载失败: $SINGBOX_URL" [[ -s "${XUI_HOME}/bin/sing-box-linux-${BIN_ARCH}.new" ]] || error "sing-box 下载得到空文件: $SINGBOX_URL" chmod +x "${XUI_HOME}/bin/sing-box-linux-${BIN_ARCH}.new" mv -f "${XUI_HOME}/bin/sing-box-linux-${BIN_ARCH}.new" "${XUI_HOME}/bin/sing-box-linux-${BIN_ARCH}" printf '%s\n' "$SINGBOX_VER" > "${XUI_HOME}/bin/sing-box.version" fi # 安装收尾自检(第 17 条):文件在不等于能用——sing-box 本体须存在、 # 可执行、version 能真实跑通;若已有生效配置,须过 sing-box check; # agent 本体须可执行。任一不过即报错退出并指出缺什么,不许带病进入 # 写配置/起服务阶段。 self_check() { if [[ "$PROBE_ONLY" == "1" ]]; then # 纯探针节点没有核心可检(V2.1.0),只验 agent 本体。 [[ -x "$AGENT_BIN" ]] || error "自检失败:agent 二进制不可执行 ${AGENT_BIN}" info "纯探针模式:跳过 sing-box 自检" return 0 fi local sb_bin="${XUI_HOME}/bin/sing-box-linux-${BIN_ARCH}" [[ -x "$sb_bin" ]] || error "自检失败:sing-box 本体缺失或不可执行 ${sb_bin}" local sb_out if ! sb_out=$("$sb_bin" version 2>/dev/null); then error "自检失败:sing-box 本体无法运行(${sb_bin} version 退出非零)" fi [[ -n "$sb_out" ]] || error "自检失败:sing-box version 无输出(${sb_bin})" info "sing-box 自检通过:$(printf '%s' "$sb_out" | head -n 1)(版本记录 ${SINGBOX_VER})" local sb_cfg="${XUI_HOME}/bin/config-singbox.json" if [[ -s "$sb_cfg" ]]; then "$sb_bin" check -c "$sb_cfg" >/dev/null 2>&1 || error "自检失败:现行 sing-box 配置未过 check(${sb_cfg})" info "sing-box 配置自检通过:${sb_cfg}" fi [[ -x "$AGENT_BIN" ]] || error "自检失败:agent 二进制不可执行 ${AGENT_BIN}" } self_check info "写入 agent 配置..." PROBE_ONLY_JSON="" if [[ "$PROBE_ONLY" == "1" ]]; then PROBE_ONLY_JSON=', "probeOnly": true'; fi cat > "${CONF_DIR}/agent.json" </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|procd|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 if [ -f /etc/rc.common ] && command -v procd >/dev/null 2>&1; then echo procd; 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" # supervise-daemon 守护(第 23 条):进程崩溃后按 respawn 参数自动拉起, # 对齐 systemd 支的 Restart=on-failure + RestartSec=5;旧版 command_background # 只负责后台化、崩溃不重启。respawn_max=0 = 不限次(同 systemd 的持续重试)。 # respawn_period 必须为正数:supervise-daemon 启动时校验该值,0 直接报 # "Invalid respawn-period value '0'" 拒绝拉起(V2.2.1 ⑤,Alpine 真机实证); # 取 1800(真机上其他 supervise-daemon 服务同值)。max=0 不限次时 period # 仅是计数窗口,不影响持续重试语义。 supervisor="supervise-daemon" respawn_delay=5 respawn_max=0 respawn_period=1800 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 } # 以 procd 服务方式安装并启动(开机自启,OpenWrt) start_via_procd() { local init_dir="${ROOT}/etc/init.d" mkdir -p "$init_dir" # 服务脚本与 2026-10-23 AX6600 真机手工验证版逐字一致(V2.2.1 ④ # 收编):路径按脚本变量展开,XUI_ROOT 为空时即验证版的 # /usr/local/x-ui/xui-agent 与 /etc/x-ui/agent.json。procd 托管 # + respawn 守护,stdout/stderr 进 syslog(logread 查看)。 cat > "${init_dir}/xui-agent" </dev/null || true stop_legacy_agent pkill -f "${AGENT_BIN} agent" 2>/dev/null || true; sleep 1 /etc/init.d/xui-agent enable /etc/init.d/xui-agent start sleep 2 if pgrep -f "${AGENT_BIN} agent" >/dev/null 2>&1 || ps w 2>/dev/null | grep "[x]ui-agent agent" >/dev/null 2>&1; then info "agent 已由 procd 托管启动(开机自启)" echo "" echo " 主控: $MASTER" echo " 节点名: $NAME" echo " 日志: logread -f" echo " agent 会在 30 秒内从主控拉取配置" else error "agent 服务启动失败,查看: logread | tail -n 50" fi } # 无服务管理器时回落 nohup(无开机自启,明确警告) start_via_nohup() { # BusyBox 精简系统可能连 nohup 都没有(AX6600 实证):先讲清再退, # 不要假装启动、走到最后才报一个看不懂的失败。 command -v nohup >/dev/null 2>&1 || error "未检测到 systemd / OpenRC / procd,且本机无 nohup 命令,无法启动 agent" warn "未检测到 systemd / OpenRC / procd,退回 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;; procd) start_via_procd;; *) start_via_nohup;; esac