Files
NodeFlare/agent/install-freebsd.sh
2026-09-10 17:19:38 +08:00

317 lines
10 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
set -eu
SERVICE_NAME="nodeflare-agent"
RC_NAME="nodeflare_agent"
INSTALL_DIR="/usr/local/libexec/nodeflare"
AGENT_FILE="$INSTALL_DIR/agent"
STATE_ROOT="/var/db/nodeflare"
STATE_DIR="$STATE_ROOT/agent"
SERVICE_FILE="/usr/local/etc/rc.d/$SERVICE_NAME"
temporary=""
backup_agent=""
backup_service=""
rollback_agent=false
had_agent=false
had_service=false
log() {
printf '%s\n' "$1"
}
fail() {
printf '错误:%s\n' "$1" >&2
exit 1
}
print_install_result() {
if [ "$update" = true ] || [ "$had_agent" = true ]; then
printf '\n更新完成(v%s)\n' "$installed_version"
return
fi
printf '\n安装完成(v%s)\n' "$installed_version"
printf ' 服务:%s(FreeBSD rc.d)\n' "$SERVICE_NAME"
printf ' 查看状态:service %s status\n' "$SERVICE_NAME"
printf ' 查看日志:grep nodeflare_agent /var/log/messages\n'
}
cleanup_agent_install() {
if [ "$rollback_agent" = true ]; then
rollback_agent=false
log "安装未完成,正在恢复上一版本"
stop_agent || { printf '错误:旧服务未停止,回滚备份已保留在 %s\n' "$STATE_DIR" >&2; return 1; }
if [ "$had_agent" = true ]; then
cp -p "$backup_agent" "$AGENT_FILE" || return 1
else
rm -f "$AGENT_FILE" || return 1
fi
if [ "$had_service" = true ]; then
cp -p "$backup_service" "$SERVICE_FILE" || return 1
service "$SERVICE_NAME" start || return 1
else
rm -f "$SERVICE_FILE" || return 1
fi
fi
[ -z "$temporary" ] || rm -f "$temporary"
[ -z "$backup_agent" ] || rm -f "$backup_agent"
[ -z "$backup_service" ] || rm -f "$backup_service"
}
stop_agent() {
[ -f "$SERVICE_FILE" ] || return 0
service "$SERVICE_NAME" onestatus >/dev/null 2>&1 || return 0
service "$SERVICE_NAME" onestop || return 1
! service "$SERVICE_NAME" onestatus >/dev/null 2>&1
}
usage() {
cat <<'EOF'
NodeFlare Agent FreeBSD 安装脚本
用法:
install-freebsd.sh -e <NodeFlare URL> -t <Agent Token> [-i <历史保存间隔>] [-m <下载加速前缀>]
install-freebsd.sh --update [-m <下载加速前缀>]
install-freebsd.sh --status
install-freebsd.sh --uninstall
支持 FreeBSD amd64 和 arm64。Agent Token 请勿泄露。
-m 为可选的 GitHub 下载加速前缀(如 https://ghproxy.net)。
--update 手动更新 Agent。
EOF
}
safe_value() {
case "$1" in *[!A-Za-z0-9_./:@-]*|'') return 1 ;; esac
}
load_installed_agent_config() {
[ -f "$SERVICE_FILE" ] || fail "未找到已安装 Agent 的服务配置,请先安装"
endpoint=$(sed -n 's/^command_args=".* -e \([^ ]*\) -i [0-9]*"$/\1/p' "$SERVICE_FILE")
interval=$(sed -n 's/^command_args=".* -e [^ ]* -i \([0-9]*\)"$/\1/p' "$SERVICE_FILE")
token=$(sed -n 's/^export NODEFLARE_AGENT_TOKEN="\([^"]*\)"$/\1/p' "$SERVICE_FILE")
[ -n "$endpoint" ] && [ -n "$token" ] && [ -n "$interval" ] || fail "已安装 Agent 的服务配置不完整"
}
download_stdout() {
url="$1"
if command -v curl >/dev/null 2>&1; then
curl --fail --location --silent --show-error --max-time 30 \
-H 'Accept: application/vnd.github+json' \
-H 'User-Agent: nodeflare-installer' \
"$url"
else
fetch -q -T 30 -o - "$url"
fi
}
download_file() {
url="$1"
destination="$2"
if command -v curl >/dev/null 2>&1; then
curl --fail --location --silent --show-error --max-time 120 \
"$url" -o "$destination"
else
fetch -q -T 120 -o "$destination" "$url"
fi
}
parse_agent_args() {
token=""
endpoint=""
interval=60
interval_set=false
mirror=""
mirror_set=false
update=false
if [ "${1:-}" = "--update" ]; then
update=true
shift
fi
while [ "$#" -gt 0 ]; do
option="$1"
if [ "$update" = true ] && [ "$option" != "-m" ]; then
fail "--update 仅接受可选的 -m 下载加速前缀"
fi
case "$option" in
-t|-e|-i|-m)
[ "$#" -ge 2 ] && [ -n "$2" ] || fail "参数 $option 缺少值"
value="$2"
shift 2
;;
*) fail "未知参数:$option" ;;
esac
case "$option" in
-t) [ -z "$token" ] || fail "参数 $option 重复"; token="$value" ;;
-e) [ -z "$endpoint" ] || fail "参数 $option 重复"; endpoint="$value" ;;
-i) [ "$interval_set" = false ] || fail "参数 $option 重复"; interval="$value"; interval_set=true ;;
-m) [ "$mirror_set" = false ] || fail "参数 $option 重复"; mirror="$value"; mirror_set=true ;;
esac
done
if [ "$update" = true ]; then
load_installed_agent_config
fi
}
if [ "${1:-}" = "--uninstall" ]; then
[ "$#" -eq 1 ] || fail "--uninstall 不接受其它参数"
[ "$(id -u)" -eq 0 ] || fail "请使用 root 权限执行卸载"
log "正在停止并移除 Agent 服务"
service "$SERVICE_NAME" stop 2>/dev/null || true
sysrc -x "${RC_NAME}_enable" >/dev/null 2>&1 || true
rm -f "$SERVICE_FILE" "$AGENT_FILE"
rm -rf "$STATE_DIR"
rmdir "$STATE_ROOT" 2>/dev/null || true
rmdir "$INSTALL_DIR" 2>/dev/null || true
echo "Agent 已卸载"
exit 0
fi
if [ "${1:-}" = "--status" ]; then
[ "$#" -eq 1 ] || fail "--status 不接受其它参数"
service "$SERVICE_NAME" status
exit $?
fi
[ "${1:-}" != "-h" ] && [ "${1:-}" != "--help" ] && [ "$#" -gt 0 ] || {
usage
exit 0
}
[ "$(id -u)" -eq 0 ] || fail "请使用 root 权限运行安装"
[ "$(uname -s)" = "FreeBSD" ] || fail "此脚本仅支持 FreeBSD"
case "$(uname -m)" in
amd64|x86_64) arch=x64 ;;
arm64|aarch64) arch=aarch64 ;;
*) fail "仅支持 FreeBSD amd64 和 arm64" ;;
esac
command -v fetch >/dev/null 2>&1 || command -v curl >/dev/null 2>&1 || fail "缺少 fetch 或 curl"
command -v sha256 >/dev/null 2>&1 || fail "缺少 sha256,无法校验下载文件"
command -v service >/dev/null 2>&1 || fail "缺少 service"
command -v sysrc >/dev/null 2>&1 || fail "缺少 sysrc"
log "正在检查运行环境"
parse_agent_args "$@"
[ -n "$token" ] && [ -n "$endpoint" ] || { usage; exit 1; }
endpoint=${endpoint%/}
[ ${#token} -le 512 ] && [ ${#endpoint} -le 2048 ] || fail "安装参数长度超出限制"
if ! safe_value "$token" || ! safe_value "$endpoint"; then
fail "服务地址或 Agent Token 格式无效"
fi
case "$endpoint" in
https://?*|http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
*) fail "服务地址必须使用 HTTPS;仅本机调试可使用 HTTP" ;;
esac
case "$endpoint" in *@*) fail "服务地址不能包含用户信息" ;; esac
case "$interval" in ''|*[!0-9]*) fail "历史保存间隔必须是整数" ;; esac
[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || fail "历史保存间隔必须在 15-3600 秒之间"
mirror=${mirror%/}
if [ -n "$mirror" ]; then
[ ${#mirror} -le 2048 ] || fail "下载加速前缀长度超出限制"
safe_value "$mirror" || fail "下载加速前缀格式无效"
case "$mirror" in
https://?*) ;;
http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
*) fail "下载加速前缀必须使用 HTTPS;仅本机调试可使用 HTTP" ;;
esac
case "$mirror" in *@*) fail "下载加速前缀不能包含用户信息" ;; esac
fi
mkdir -p "$INSTALL_DIR" "$STATE_ROOT" "$STATE_DIR"
chmod 755 "$INSTALL_DIR"
chmod 750 "$STATE_ROOT"
chmod 750 "$STATE_DIR"
temporary="$INSTALL_DIR/.agent.$$.download"
trap cleanup_agent_install EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
artifact="agent-freebsd-$arch"
release_api="https://api.github.com/repos/elysia62/NodeFlare/releases/latest"
log "正在获取 GitHub 最新正式版本($artifact)"
release_json=$(download_stdout "$release_api")
release_tag=$(printf '%s\n' "$release_json" | tr ',' '\n' | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
printf '%s\n' "$release_tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' || fail "GitHub 最新 Release 标签无效:${release_tag:-未找到}"
expected=$(printf '%s\n' "$release_json" | tr '{' '\n' | awk -v name="$artifact" '
{
compact = $0
gsub(/[[:space:]]/, "", compact)
if (index(compact, "\"name\":\"" name "\"") > 0) selected = 1
else if (index(compact, "\"name\":") > 0) selected = 0
}
selected {
marker = "\"digest\":\"sha256:"
position = index(compact, marker)
if (position == 0) next
digest = substr(compact, position + length(marker), 64)
if (length(digest) == 64 && digest !~ /[^0-9a-fA-F]/) {
print tolower(digest)
exit
}
}
')
[ -n "$expected" ] || fail "Release 缺少 $artifact 的 SHA-256 摘要"
release_base="https://github.com/elysia62/NodeFlare/releases/download/$release_tag"
download_url="$release_base/$artifact"
if [ -n "$mirror" ]; then
download_url="$mirror/$release_base/$artifact"
log "正在通过下载加速前缀拉取 Agent $release_tag"
else
log "正在下载 Agent $release_tag"
fi
download_file "$download_url" "$temporary"
actual=$(sha256 -q "$temporary")
[ "$actual" = "$expected" ] || fail "Agent SHA-256 校验失败,已停止安装"
chmod 755 "$temporary"
log "下载校验通过,正在验证可执行文件"
installed_version=$("$temporary" --version) || fail "下载的 Agent 无法在当前 FreeBSD 运行"
installed_version=${installed_version##* }
[ "$installed_version" = "${release_tag#v}" ] || fail "Release $release_tag 与 Agent 版本 $installed_version 不一致"
backup_agent="$STATE_DIR/.agent.$$.previous"
backup_service="$STATE_DIR/.service.$$.previous"
if [ -f "$AGENT_FILE" ]; then
cp -p "$AGENT_FILE" "$backup_agent"
had_agent=true
fi
if [ -f "$SERVICE_FILE" ]; then
cp -p "$SERVICE_FILE" "$backup_service"
had_service=true
fi
log "正在启动 FreeBSD rc.d 服务"
stop_agent || fail "无法停止已有 Agent 服务,未替换程序"
rollback_agent=true
mv "$temporary" "$AGENT_FILE"
cat > "$SERVICE_FILE" <<EOF
#!/bin/sh
# PROVIDE: nodeflare_agent
# REQUIRE: NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
export NODEFLARE_STATE_DIR="$STATE_DIR"
export NODEFLARE_AGENT_TOKEN="$token"
name="$RC_NAME"
rcvar="${RC_NAME}_enable"
pidfile="/var/run/\${name}.pid"
command="/usr/sbin/daemon"
command_args="-P \${pidfile} -r -R 10 -S -T \${name} $AGENT_FILE -e $endpoint -i $interval"
load_rc_config "\${name}"
: \${nodeflare_agent_enable:="NO"}
run_rc_command "\$1"
EOF
chmod 700 "$SERVICE_FILE"
sysrc "${RC_NAME}_enable=YES" >/dev/null
service "$SERVICE_NAME" start
service "$SERVICE_NAME" status >/dev/null || {
service "$SERVICE_NAME" status >&2 || true
fail "服务启动失败,请查看 /var/log/messages"
}
rollback_agent=false
cleanup_agent_install
trap - EXIT HUP INT TERM
print_install_result