Use Agent for native, container, systemd and OpenWrt installations and signed updates. Remove retired Probe deployment templates and smoke scripts.
155 lines
9.0 KiB
Bash
155 lines
9.0 KiB
Bash
#!/bin/sh
|
||
# MoShiro 节点一键安装(监听和拨号两种模式共用同一份,**静态**文件:不替换任何值,所以它的 SHA-256 在一个版本内固定不变)。
|
||
# 面板给的命令先下载这份文件、核对 SHA-256(CI 构建时算出,写进签名的发布清单,也公布在发布页),一致才执行:
|
||
# f=$(mktemp) && curl -fsSL 'https://panel/install' -o "$f" && echo "<sha256> $f" | sha256sum -c - && MOSHIRO_SERVER=… MOSHIRO_ROLE=… MOSHIRO_CODE=… sh "$f"
|
||
# 必需的环境变量:MOSHIRO_SERVER(面板地址)、MOSHIRO_ROLE(默认 agent,监听并拨号)、MOSHIRO_CODE(一次性的加入码)。
|
||
# 做的事:用 Docker 或 Podman 起一个容器,身份和配置存在命名卷里(重启、换新镜像重建都不用再给参数),节点名字默认取主机名。
|
||
# 可选环境变量:MOSHIRO_NAME=节点名字(名字被占用时用)、MOSHIRO_ENGINE=docker|podman、MOSHIRO_DRY_RUN=1(只打印命令)、
|
||
# MOSHIRO_GENESIS / MOSHIRO_CONTROLLERS(命令里带着:你的信任根指纹和这个节点的属主 passkey 凭证 id,节点在注册时验证面板的操作员集合并把它们固定在本地;都是公开值)、
|
||
# MOSHIRO_ALLOW_UNVERIFIED=1 加 MOSHIRO_IMAGE=镜像(面板还没有已签名的发布清单时,不验签、按这个镜像安装;默认拒绝)。
|
||
# 镜像:向面板取 CI 签名的发布清单,用下面内置的发布公钥验签,再按清单里的 digest 拉取(镜像按内容寻址,换不了)。
|
||
set -eu
|
||
|
||
SERVER="${MOSHIRO_SERVER:-}"
|
||
ROLE="${MOSHIRO_ROLE:-agent}"
|
||
CODE="${MOSHIRO_CODE:-}"
|
||
IMAGE="${MOSHIRO_IMAGE:-}"
|
||
RELEASE_KEY='-----BEGIN PUBLIC KEY-----
|
||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfw7EkNcl0FZqOETK+Vt55RMZ9K0q
|
||
YOrtO3Vso/QNdSMmepQlx1Q3v+KkN7ro3AY3I6DT2Q2txJu5pS7qzutSQA==
|
||
-----END PUBLIC KEY-----'
|
||
|
||
say() { printf '%s\n' "$*"; }
|
||
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
||
|
||
# 这些值来自命令行环境:只允许正常的字符,因为它们会进到 URL 和容器参数里
|
||
case "$ROLE" in agent) ;; *) die "MOSHIRO_ROLE must be agent" ;; esac
|
||
case "$SERVER" in http://*|https://*) ;; *) die "MOSHIRO_SERVER must be the panel address (http:// or https://)" ;; esac
|
||
case "$SERVER$CODE" in *[!A-Za-z0-9._:/@~%+\[\]-]*|'') die "MOSHIRO_SERVER or MOSHIRO_CODE has unexpected characters" ;; esac
|
||
[ -n "$CODE" ] || die "MOSHIRO_CODE is required (copy the whole command from the panel)"
|
||
|
||
name="${MOSHIRO_NAME:-$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo node)}"
|
||
name="$(printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '-' | cut -c1-64)"
|
||
[ -n "$name" ] || name=node
|
||
|
||
engine="${MOSHIRO_ENGINE:-}"
|
||
if [ -z "$engine" ]; then
|
||
if command -v docker >/dev/null 2>&1; then engine=docker
|
||
elif command -v podman >/dev/null 2>&1; then engine=podman
|
||
else die "Docker or Podman is required (https://docs.docker.com/engine/install/)"; fi
|
||
fi
|
||
command -v "$engine" >/dev/null 2>&1 || die "$engine is not installed"
|
||
|
||
# 当前用户连不上引擎(不在 docker 组、rootful podman)就用 sudo
|
||
use_sudo=0
|
||
if ! "$engine" info >/dev/null 2>&1 </dev/null; then
|
||
[ "$(id -u)" -ne 0 ] || die "$engine is installed but not running"
|
||
command -v sudo >/dev/null 2>&1 || die "$engine needs root: run this as root or install sudo"
|
||
use_sudo=1
|
||
fi
|
||
eng() { if [ "$use_sudo" -eq 1 ]; then sudo "$engine" "$@"; else "$engine" "$@"; fi; }
|
||
|
||
container="moshiro-$ROLE"
|
||
volume="moshiro-$ROLE-config"
|
||
linux=0
|
||
[ "$(uname -s)" = Linux ] && linux=1
|
||
|
||
if [ "${MOSHIRO_DRY_RUN:-}" != 1 ] && eng container inspect "$container" >/dev/null 2>&1 </dev/null; then
|
||
eng start "$container" >/dev/null </dev/null
|
||
say "$container is already installed and running."
|
||
say "reinstall: $engine rm -f $container && $engine volume rm $volume (then run the install command again)"
|
||
exit 0
|
||
fi
|
||
|
||
# 验发布清单:签名对不上、组件不对、digest 格式不对都直接拒绝,不装
|
||
resolve_image() {
|
||
if [ "${MOSHIRO_DRY_RUN:-}" = 1 ]; then IMAGE="${IMAGE:-<the image of the signed release>}"; return 0; fi
|
||
tmp="$(mktemp -d)"
|
||
trap 'rm -rf "$tmp"' EXIT
|
||
have_manifest=1
|
||
curl -fsS "$SERVER/install/manifest?code=$CODE" -o "$tmp/manifest" 2>/dev/null </dev/null || have_manifest=0
|
||
curl -fsS "$SERVER/install/manifest.sig?code=$CODE" -o "$tmp/sig.b64" 2>/dev/null </dev/null || have_manifest=0
|
||
if [ "$have_manifest" -eq 0 ]; then
|
||
[ "${MOSHIRO_ALLOW_UNVERIFIED:-}" = 1 ] && [ -n "$IMAGE" ] || die "the panel has no signed release yet, so the image cannot be verified. To install anyway set MOSHIRO_ALLOW_UNVERIFIED=1 and MOSHIRO_IMAGE=<image>"
|
||
say "warning: installing $IMAGE without signature verification"
|
||
return 0
|
||
fi
|
||
command -v openssl >/dev/null 2>&1 || die "openssl is required to verify the release signature"
|
||
printf '%s\n' "$RELEASE_KEY" > "$tmp/key.pem"
|
||
openssl base64 -d -A -in "$tmp/sig.b64" -out "$tmp/sig.bin" 2>/dev/null || die "the release signature is not valid base64"
|
||
openssl dgst -sha256 -verify "$tmp/key.pem" -signature "$tmp/sig.bin" "$tmp/manifest" >/dev/null 2>&1 \
|
||
|| die "the release signature does not verify against the built-in release key; refusing to install"
|
||
field() { sed -n "s/.*\"$1\":\"\([^\"]*\)\".*/\1/p" "$tmp/manifest" | head -n 1; }
|
||
[ "$(field component)" = agent ] || die "the signed release is for '$(field component)', not 'agent'"
|
||
repo="$(field image)"
|
||
digest="$(field digest)"
|
||
case "$digest" in sha256:????????????????????????????????????????????????????????????????) ;; *) die "the signed release has a malformed digest" ;; esac
|
||
case "$repo" in ''|*[!A-Za-z0-9._/:-]*) die "the signed release has a malformed image name" ;; esac
|
||
IMAGE="$repo@$digest"
|
||
say "verified release $(field version) ($(field tag)): $IMAGE"
|
||
}
|
||
resolve_image
|
||
|
||
restart=unless-stopped
|
||
[ "$engine" = podman ] && restart=always
|
||
set -- run -d --name "$container" --restart "$restart" \
|
||
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m --cap-drop all --security-opt no-new-privileges \
|
||
-v "$volume:/etc/moshiro"
|
||
[ "$engine" = podman ] && set -- "$@" --security-opt label=disable
|
||
if [ "$linux" -eq 1 ]; then
|
||
# 宿主网络:监听的节点对外暴露端口,两种模式都靠它识别本机的公网 IPv4 和 IPv6 地址
|
||
set -- "$@" --network host
|
||
# 主机资源占用(只读),管理端的节点详情页显示 CPU、内存、磁盘、网络
|
||
set -- "$@" -v /etc/os-release:/etc/os-release:ro -v /proc:/host/proc:ro -v /sys:/host/sys:ro -v /:/host/root:ro \
|
||
-e MOSHIRO_NETWORK_SCOPE=host -e HOST_PROC=/host/proc -e HOST_SYS=/host/sys -e HOST_ROOT=/host/root
|
||
elif [ "$ROLE" = agent ]; then
|
||
# macOS 上的 Docker 没有宿主网络:把监听节点的端口发布出来(面板里的地址需要指向这台机器的公网地址)
|
||
set -- "$@" -p 7443:7443/tcp -p 7443:7443/udp -p 7444:7444/tcp
|
||
fi
|
||
set -- "$@" -e "MOSHIRO_SERVER=$SERVER" -e "MOSHIRO_JOIN=$CODE" -e "MOSHIRO_NAME=$name" -e "MOSHIRO_MODE=$ROLE"
|
||
# 每个节点都必须有属主(passkey):命令里的信任根指纹和属主缺一个就不装(面板的命令总是带着;只有没有信任根的老面板才需要 MOSHIRO_ALLOW_NO_OWNER=1)
|
||
if [ -z "${MOSHIRO_GENESIS:-}" ] || [ -z "${MOSHIRO_CONTROLLERS:-}" ]; then
|
||
[ "${MOSHIRO_ALLOW_NO_OWNER:-}" = 1 ] || die "this node has no owner: MOSHIRO_GENESIS and MOSHIRO_CONTROLLERS are required (copy the complete command from the panel; set up the trust root first)"
|
||
fi
|
||
if [ -n "${MOSHIRO_GENESIS:-}" ]; then
|
||
case "$MOSHIRO_GENESIS${MOSHIRO_CONTROLLERS:-}" in *[!A-Za-z0-9_,-]*) die "MOSHIRO_GENESIS or MOSHIRO_CONTROLLERS has unexpected characters" ;; esac
|
||
set -- "$@" -e "MOSHIRO_GENESIS=$MOSHIRO_GENESIS" -e "MOSHIRO_CONTROLLERS=${MOSHIRO_CONTROLLERS:-}"
|
||
fi
|
||
set -- "$@" "$IMAGE"
|
||
|
||
if [ "${MOSHIRO_DRY_RUN:-}" = 1 ]; then
|
||
say "$engine $*"
|
||
exit 0
|
||
fi
|
||
|
||
say "pulling $IMAGE"
|
||
if ! eng pull "$IMAGE" >/dev/null </dev/null; then
|
||
die "could not pull $IMAGE (private registry? run: $engine login ${IMAGE%%/*})"
|
||
fi
|
||
|
||
say "starting $container as '$name'"
|
||
eng "$@" >/dev/null </dev/null
|
||
|
||
# Podman 的容器开机自启要靠 podman-restart.service
|
||
if [ "$engine" = podman ] && command -v systemctl >/dev/null 2>&1; then
|
||
if [ "$(id -u)" -eq 0 ]; then systemctl enable --now podman-restart >/dev/null 2>&1 || true
|
||
elif [ "$use_sudo" -eq 1 ]; then sudo systemctl enable --now podman-restart >/dev/null 2>&1 || true; fi
|
||
fi
|
||
|
||
# 等注册完成:第一次启动会用上面的令牌注册,成功之后才算装好
|
||
i=0
|
||
while [ "$i" -lt 30 ]; do
|
||
state="$(eng container inspect -f '{{.State.Running}}' "$container" 2>/dev/null </dev/null || echo false)"
|
||
logs="$(eng logs "$container" 2>&1 </dev/null || true)"
|
||
case "$logs" in
|
||
*"link up"*) say "done: $ROLE '$name' is online. It shows up in the panel now."; say "logs: $engine logs -f $container"; exit 0 ;;
|
||
esac
|
||
if [ "$state" != true ]; then
|
||
printf '%s\n' "$logs" | tail -n 8 >&2
|
||
die "$container stopped. If the name '$name' is taken, run again with MOSHIRO_NAME=<other-name>. To start over: $engine rm -f $container && $engine volume rm $volume"
|
||
fi
|
||
i=$((i + 1))
|
||
sleep 1
|
||
done
|
||
say "$container is running, but has not reported online yet. Check: $engine logs $container"
|