From 016dbc2bbf8f3d23a4425f124801b31cecd173e2 Mon Sep 17 00:00:00 2001 From: justme Date: Wed, 21 Oct 2026 11:55:41 +0800 Subject: [PATCH] scripts: consolidate Agent deployment and remove Probe packages Use Agent for native, container, systemd and OpenWrt installations and signed updates. Remove retired Probe deployment templates and smoke scripts. --- packaging/compose/agent/compose.yaml | 2 +- packaging/compose/probe/compose.yaml | 50 -- packaging/container/Agent.Containerfile | 4 +- .../container/Agent.NativeBuild.Containerfile | 1 - packaging/container/Probe.Containerfile | 21 - .../container/Probe.NativeBuild.Containerfile | 60 -- packaging/container/deploy-node.sh | 23 +- packaging/container/entrypoint.sh | 13 +- packaging/install.sh | 29 +- packaging/install/install.ps1 | 16 +- packaging/install/install.sh | 26 +- packaging/openwrt/install.sh | 34 +- ...{moshiro-probe.init => moshiro-agent.init} | 8 +- .../{native-probe.init => native-agent.init} | 10 +- packaging/quadlet/moshiro-backend.container | 2 +- packaging/quadlet/moshiro-probe.container | 55 -- .../systemd/moshiro-probe-container.service | 16 - .../systemd/moshiro-probe-docker.service | 16 - .../systemd/moshiro-probe-rootless.service | 16 - packaging/systemd/moshiro-probe.service | 44 -- packaging/update/install-updater.sh | 2 +- packaging/update/moshiro-update.sh | 12 +- packaging/update/tests/feed-test.sh | 2 +- scripts/package.sh | 14 +- scripts/publish.sh | 7 +- scripts/smoke-linux.sh | 86 --- scripts/smoke-systemd.sh | 31 - scripts/smoke-wsl.py | 77 --- scripts/stability_wsl.py | 613 ------------------ scripts/test-openwrt-updater.py | 12 +- 30 files changed, 96 insertions(+), 1206 deletions(-) delete mode 100644 packaging/compose/probe/compose.yaml delete mode 100644 packaging/container/Probe.Containerfile delete mode 100644 packaging/container/Probe.NativeBuild.Containerfile rename packaging/openwrt/{moshiro-probe.init => moshiro-agent.init} (69%) rename packaging/openwrt/{native-probe.init => native-agent.init} (69%) delete mode 100644 packaging/quadlet/moshiro-probe.container delete mode 100644 packaging/systemd/moshiro-probe-container.service delete mode 100644 packaging/systemd/moshiro-probe-docker.service delete mode 100644 packaging/systemd/moshiro-probe-rootless.service delete mode 100644 packaging/systemd/moshiro-probe.service delete mode 100644 scripts/smoke-linux.sh delete mode 100644 scripts/smoke-systemd.sh delete mode 100644 scripts/smoke-wsl.py delete mode 100644 scripts/stability_wsl.py diff --git a/packaging/compose/agent/compose.yaml b/packaging/compose/agent/compose.yaml index 5b35034..f41085f 100644 --- a/packaging/compose/agent/compose.yaml +++ b/packaging/compose/agent/compose.yaml @@ -14,7 +14,7 @@ services: container_name: moshiro-agent restart: unless-stopped init: true - # Probe 要直连 Agent 的 7443(TCP+UDP)和 7444(TCP),用宿主网络,不做端口映射 + # 其他 Agent 要直连此节点 的 7443(TCP+UDP)和 7444(TCP),用宿主网络,不做端口映射 network_mode: host volumes: - agent-config:/etc/moshiro diff --git a/packaging/compose/probe/compose.yaml b/packaging/compose/probe/compose.yaml deleted file mode 100644 index cd7e27b..0000000 --- a/packaging/compose/probe/compose.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# MoShiro Probe。Docker 和 rootless/rootful Podman 共用。 -# -# 用法(面板「添加 Probe」会给出带好参数的 .env 内容): -# mkdir moshiro-probe && cd moshiro-probe -# 把面板给的 .env 内容存成 .env,再下载本文件为 compose.yaml -# podman compose up -d # Docker 用 docker compose -# 更新:podman compose pull && podman compose up -d (身份和配置在命名卷里,不会动) -# 固定版本:在 .env 里写 MOSHIRO_VERSION=<版本号>;想要更严,写成 MOSHIRO_VERSION=<版本号>@sha256:。 -# macOS、Windows 的 Docker Desktop 没有宿主网络:面板给的 .env 里已经写了 MOSHIRO_NETWORK_MODE=bridge。 -# .env 里的 MOSHIRO_SERVER、MOSHIRO_JOIN(只在第一次启动用来注册,之后可以删掉)、MOSHIRO_GENESIS、MOSHIRO_CONTROLLERS、MOSHIRO_NAME 原样传进容器。 -# 说明见 docs/ops/deployment.md。 -services: - probe: - image: ${MOSHIRO_IMAGE:-registry.oeo.one/moshiro/moshiro-probe}:${MOSHIRO_VERSION:-latest} - container_name: moshiro-probe - restart: unless-stopped - init: true - network_mode: ${MOSHIRO_NETWORK_MODE:-host} - # .env 里的变量原样传进容器(不用带默认值的变量替换:有的 compose 提供者,比如 podman-compose,不替换环境变量里的默认值) - env_file: - - .env - environment: - HOST_PROC: /host/proc - HOST_SYS: /host/sys - HOST_ROOT: /host/root - volumes: - - probe-config:/etc/moshiro - - probe-status:/etc/moshiro/update-status:ro - - /etc/os-release:/etc/os-release:ro - # 主机资源占用(CPU、内存、磁盘、网卡流量、运行时长,只有汇总数字,没有型号和 IP):只读挂载宿主机的 /proc、/sys 和根目录; - # 不想上报:在 probe.json 里写 reporting.host_stats=false,再删掉下面三行挂载。Docker Desktop 上看到的是它的虚拟机,不是 Windows 或 macOS 本身。 - - /proc:/host/proc:ro - - /sys:/host/sys:ro - - /:/host/root:ro - read_only: true - tmpfs: - - /tmp:rw,noexec,nosuid,size=64m,mode=1777 - security_opt: - - label=disable - - no-new-privileges:true - cap_drop: - - ALL - # Linux ICMP 使用 ping sockets;ping_group_range 需允许容器映射后的 GID。 - stop_grace_period: 20s - -volumes: - probe-config: - name: moshiro-probe-config - probe-status: - name: moshiro-probe-status diff --git a/packaging/container/Agent.Containerfile b/packaging/container/Agent.Containerfile index 6b4c2bf..6013b22 100644 --- a/packaging/container/Agent.Containerfile +++ b/packaging/container/Agent.Containerfile @@ -2,7 +2,7 @@ FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine ARG APP_VERSION=unknown LABEL org.opencontainers.image.title="MoShiro Agent" \ - org.opencontainers.image.description="MoShiro node agent for Linux" \ + org.opencontainers.image.description="MoShiro node for Linux: listens and dials (MOSHIRO_MODE=agent) or only dials (MOSHIRO_MODE=dial)" \ org.opencontainers.image.licenses="Apache-2.0" \ org.opencontainers.image.version="$APP_VERSION" @@ -12,10 +12,8 @@ COPY --chown=1654:1654 app/fleet-enroll /usr/local/bin/fleet-enroll COPY --chown=1654:1654 config/ /etc/moshiro/ COPY --chown=1654:1654 --chmod=0755 entrypoint.sh /usr/local/bin/moshiro-entrypoint -ENV MOSHIRO_ROLE=agent ENV LD_LIBRARY_PATH=/usr/local/bin USER 1654:1654 WORKDIR /etc/moshiro VOLUME ["/etc/moshiro"] ENTRYPOINT ["/usr/local/bin/moshiro-entrypoint"] -CMD ["--config", "/etc/moshiro/agent.json"] diff --git a/packaging/container/Agent.NativeBuild.Containerfile b/packaging/container/Agent.NativeBuild.Containerfile index 37ee5f1..b1084d4 100644 --- a/packaging/container/Agent.NativeBuild.Containerfile +++ b/packaging/container/Agent.NativeBuild.Containerfile @@ -52,7 +52,6 @@ COPY --chown=1654:1654 --from=build /out/enroll/fleet-enroll /usr/local/bin/flee COPY --chown=1654:1654 config/ /etc/moshiro/ COPY --chown=1654:1654 --chmod=0755 entrypoint.sh /usr/local/bin/moshiro-entrypoint -ENV MOSHIRO_ROLE=agent USER 1654:1654 WORKDIR /etc/moshiro VOLUME ["/etc/moshiro"] diff --git a/packaging/container/Probe.Containerfile b/packaging/container/Probe.Containerfile deleted file mode 100644 index 484e46a..0000000 --- a/packaging/container/Probe.Containerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine - -ARG APP_VERSION=unknown -LABEL org.opencontainers.image.title="MoShiro Probe" \ - org.opencontainers.image.description="MoShiro network measurement probe for Linux" \ - org.opencontainers.image.licenses="Apache-2.0" \ - org.opencontainers.image.version="$APP_VERSION" - -COPY --chown=1654:1654 app/moshiro-probe /usr/local/bin/moshiro-probe -COPY --chown=1654:1654 app/lib*.so.* /usr/local/bin/ -COPY --chown=1654:1654 app/fleet-enroll /usr/local/bin/fleet-enroll -COPY --chown=1654:1654 config/ /etc/moshiro/ -COPY --chown=1654:1654 --chmod=0755 entrypoint.sh /usr/local/bin/moshiro-entrypoint - -ENV MOSHIRO_ROLE=probe -ENV LD_LIBRARY_PATH=/usr/local/bin -USER 1654:1654 -WORKDIR /etc/moshiro -VOLUME ["/etc/moshiro"] -ENTRYPOINT ["/usr/local/bin/moshiro-entrypoint"] -CMD ["--config", "/etc/moshiro/probe.json"] diff --git a/packaging/container/Probe.NativeBuild.Containerfile b/packaging/container/Probe.NativeBuild.Containerfile deleted file mode 100644 index 1586904..0000000 --- a/packaging/container/Probe.NativeBuild.Containerfile +++ /dev/null @@ -1,60 +0,0 @@ -# AotAnywhere cross-links both Alpine architectures on the native amd64 runner. -ARG NATIVEAOT_BUILDER_IMAGE=localhost/moshiro-dotnet10-nativeaot-aotanywhere:ci -FROM --platform=linux/amd64 ${NATIVEAOT_BUILDER_IMAGE} AS build - -WORKDIR /src/backend -COPY restore/backend/ ./ -ARG NODE_RID=linux-musl-x64 -ENV NUGET_PACKAGES=/root/.nuget/packages -RUN dotnet restore probe/src/MoShiro.Probe -r "$NODE_RID" -p:PublishAot=true -p:UseAotAnywhere=true \ - -p:StaticMsQuic=true -p:StaticExecutable=true -p:RuntimeFrameworkVersion=10.0.12 \ - -p:InvariantGlobalization=true --artifacts-path /artifacts \ - && dotnet restore fleet/tools/fleet-enroll -r "$NODE_RID" -p:PublishAot=true -p:UseAotAnywhere=true \ - -p:StaticMsQuic=true -p:StaticExecutable=true -p:RuntimeFrameworkVersion=10.0.12 -p:InvariantGlobalization=true \ - --artifacts-path /artifacts - -COPY source/backend/ ./ -COPY source/packaging/native/msquic/ /src/packaging/native/msquic/ -# 构建信息:CI 通过 --build-arg 传入;不传就是空(放在 restore 之后,每次构建都变的时间不会让依赖还原的缓存失效) -ARG MOSHIRO_TAG= -ARG MOSHIRO_COMMIT= -ARG MOSHIRO_BUILT_AT= -ENV MOSHIRO_TAG=$MOSHIRO_TAG MOSHIRO_COMMIT=$MOSHIRO_COMMIT MOSHIRO_BUILT_AT=$MOSHIRO_BUILT_AT -RUN dotnet publish probe/src/MoShiro.Probe -m:3 -c Release -r "$NODE_RID" \ - --self-contained true --no-restore -p:PublishAot=true -p:UseAotAnywhere=true \ - -p:StaticMsQuic=true -p:StaticExecutable=true -p:RuntimeFrameworkVersion=10.0.12 \ - -p:InvariantGlobalization=true -p:UseAppHost=true -p:StripSymbols=true \ - -p:DebugType=none -p:DebugSymbols=false --artifacts-path /artifacts -o /out/probe \ - && dotnet publish fleet/tools/fleet-enroll -m:3 -c Release -r "$NODE_RID" \ - --self-contained true --no-restore -p:PublishAot=true -p:UseAotAnywhere=true \ - -p:StaticMsQuic=true -p:StaticExecutable=true -p:RuntimeFrameworkVersion=10.0.12 -p:InvariantGlobalization=true \ - -p:UseAppHost=true -p:StripSymbols=true \ - -p:DebugType=none -p:DebugSymbols=false --artifacts-path /artifacts -o /out/enroll \ - && rm -rf /artifacts -RUN test -x /out/probe/moshiro-probe && test -x /out/enroll/fleet-enroll \ - && test "$(stat -c%s /out/probe/moshiro-probe)" -gt 2000000 \ - && test "$(stat -c%s /out/enroll/fleet-enroll)" -gt 2000000 \ - && ! readelf -l /out/probe/moshiro-probe | grep -q INTERP \ - && ! readelf -d /out/probe/moshiro-probe 2>&1 | grep -q NEEDED \ - && ! readelf -l /out/enroll/fleet-enroll | grep -q INTERP \ - && ! readelf -d /out/enroll/fleet-enroll 2>&1 | grep -q NEEDED - -FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine - -ARG APP_VERSION=main -LABEL org.opencontainers.image.title="MoShiro Probe" \ - org.opencontainers.image.description="MoShiro network measurement probe for Linux" \ - org.opencontainers.image.licenses="Apache-2.0" \ - org.opencontainers.image.version="$APP_VERSION" - -COPY --chown=1654:1654 --from=build /out/probe/moshiro-probe /usr/local/bin/moshiro-probe -COPY --chown=1654:1654 --from=build /out/enroll/fleet-enroll /usr/local/bin/fleet-enroll -COPY --chown=1654:1654 config/ /etc/moshiro/ -COPY --chown=1654:1654 --chmod=0755 entrypoint.sh /usr/local/bin/moshiro-entrypoint - -ENV MOSHIRO_ROLE=probe -USER 1654:1654 -WORKDIR /etc/moshiro -VOLUME ["/etc/moshiro"] -ENTRYPOINT ["/usr/local/bin/moshiro-entrypoint"] -CMD ["--config", "/etc/moshiro/probe.json"] diff --git a/packaging/container/deploy-node.sh b/packaging/container/deploy-node.sh index 31094c8..0549936 100644 --- a/packaging/container/deploy-node.sh +++ b/packaging/container/deploy-node.sh @@ -1,24 +1,22 @@ #!/bin/sh -# 部署或更新容器服务(Agent、Probe;Backend 用 Quadlet,见 packaging/quadlet/moshiro-backend.container)。可以重复运行:每次都会拉取镜像、把配置迁移到当前版本需要的样子、 +# 部署或更新容器服务(Agent;Backend 用 Quadlet,见 packaging/quadlet/moshiro-backend.container)。可以重复运行:每次都会拉取镜像、把配置迁移到当前版本需要的样子、 # 重新安装单元和更新器,然后重启节点;已有的身份和配置不会被覆盖。 # # 首次部署 Agent:sudo sh deploy-node.sh agent --image-repo REPO --server https://panel --join jt_xxx --name hk-1 -# 首次部署 Probe:sudo sh deploy-node.sh probe --image-repo REPO --server https://panel --join jt_xxx --name home-1 # 更新(配置迁移、单元和更新器、镜像):sudo sh deploy-node.sh agent (其余参数沿用上次的更新器配置) -# 旧版 Probe(probe.json 里没有 fleet 段):加上 --server --join --name 重新运行即可,旧文件备份为 probe.json.old # # 需要 root、systemd,以及 Podman 或 Docker;脚本要和 packaging/ 里的 systemd/、update/ 放在一起。 # rootless Podman 见 docs/ops/deployment.md。 set -eu usage() { - echo "usage: deploy-node.sh [--engine podman|docker] [--image-repo R] [--channel dev|release] [--tag T] [--image REF]" >&2 + echo "usage: deploy-node.sh [--engine podman|docker] [--image-repo R] [--channel dev|release] [--tag T] [--image REF]" >&2 echo " [--server URL --join TOKEN --name NAME] [--policy auto|manual] [--authfile F]" >&2 exit 2 } ROLE="${1:-}"; shift || true -case "$ROLE" in agent|probe) ;; *) usage ;; esac +case "$ROLE" in agent) ;; *) usage ;; esac ENGINE=""; REPO=""; TAG=""; IMAGE=""; SERVER=""; JOIN=""; NAME=""; POLICY=""; AUTHFILE=""; CHANNEL="" while [ $# -gt 0 ]; do case "$1" in @@ -121,7 +119,7 @@ in_volume() { } in_volume 'mkdir -p /etc/moshiro/update' &2; exit 1; } @@ -139,18 +137,7 @@ if [ "$ROLE" = agent ]; then in_volume 'sed -i "s/\"bind\" *: *\"0\.0\.0\.0\"/\"bind\": \"::\"/" /etc/moshiro/agent.json' ::" fi -elif [ "$ROLE" = probe ]; then - # 旧格式(面板生成的 probe.json:没有 fleet 段)备份之后按新格式重写 - if in_volume 'test -f /etc/moshiro/probe.json' &2; exit 1; } - printf '{"fleet":{"server":"%s","identity_dir":"/etc/moshiro/probe-id"}}\n' "$SERVER" \ - | in_volume 'umask 077; cat > /etc/moshiro/probe.json' - echo "config: created probe.json" - fi + fi # 4. 单元和更新器(保持原来的更新策略) diff --git a/packaging/container/entrypoint.sh b/packaging/container/entrypoint.sh index 934dce6..d009f15 100644 --- a/packaging/container/entrypoint.sh +++ b/packaging/container/entrypoint.sh @@ -1,9 +1,10 @@ #!/bin/sh -# 容器入口(Agent 和 Probe 共用):第一次启动用 MOSHIRO_SERVER / MOSHIRO_JOIN(MOSHIRO_NAME 不写就用主机名)自己注册并写配置, +# 容器入口(监听和拨号两种模式共用,镜像里只有 moshiro-agent):第一次启动用 MOSHIRO_SERVER / MOSHIRO_JOIN(MOSHIRO_NAME 不写就用主机名)自己注册并写配置, # 身份和配置都在命名卷 /etc/moshiro 里,之后重启(或换新镜像重建容器)直接启动,不需要再给参数。 set -eu -role="${MOSHIRO_ROLE:?MOSHIRO_ROLE is set by the image}" +# 所有节点默认同时监听和拨号,身份与配置统一保存。 +role=agent # Quadlet 单元和 compose 默认用宿主网络;Docker Desktop(没有宿主网络)的 .env 里写 MOSHIRO_NETWORK_SCOPE=container export MOSHIRO_NETWORK_SCOPE="${MOSHIRO_NETWORK_SCOPE:-host}" dir=/etc/moshiro @@ -39,11 +40,7 @@ if [ ! -f "$config" ]; then echo "$config is missing: set MOSHIRO_SERVER to create it" >&2 exit 1 fi - if [ "$role" = agent ]; then - printf '{"fleet":{"server":"%s","identity_dir":"%s"},"listen":{"bind":"::"}}\n' "$MOSHIRO_SERVER" "$identity" > "$config" - else - printf '{"fleet":{"server":"%s","identity_dir":"%s"}}\n' "$MOSHIRO_SERVER" "$identity" > "$config" - fi + printf '{"fleet":{"server":"%s","identity_dir":"%s"},"listen":{"bind":"::"}}\n' "$MOSHIRO_SERVER" "$identity" > "$config" fi -exec "/usr/local/bin/moshiro-$role" "$@" +exec /usr/local/bin/moshiro-agent --config "$config" "$@" diff --git a/packaging/install.sh b/packaging/install.sh index 299bcec..1daed8f 100644 --- a/packaging/install.sh +++ b/packaging/install.sh @@ -1,10 +1,9 @@ #!/bin/sh -# Installs moshiro-agent or moshiro-probe from an extracted release directory (the directory this script lives in). +# Installs moshiro-agent or moshiro-agent from an extracted release directory (the directory this script lives in). # POSIX sh: runs on Debian/Ubuntu/Alpine/OpenWrt (busybox). # # ./install.sh agent --server https://panel.example.com --join --name hk-1 -# ./install.sh probe --server https://panel --join jt_xxx --name home-1 -# ./install.sh uninstall agent|probe [--purge] +# ./install.sh uninstall agent [--purge] # # Binaries go to /usr/lib/moshiro (root-owned: the collector cannot rewrite its own executable); # config and identity go to /etc/moshiro (owned by the unprivileged `moshiro` user). @@ -80,7 +79,7 @@ write_config() { } JSON else - cat >"$CONFIG/probe.json" <"$CONFIG/agent.json" <&2 ;; @@ -145,14 +143,14 @@ install_role() { if [ "$ready" -eq 1 ] && [ "$no_start" -eq 0 ]; then case "$init" in systemd) systemctl enable --now "moshiro-$role" ;; - procd) /etc/init.d/moshiro-probe enable; /etc/init.d/moshiro-probe restart ;; + procd) /etc/init.d/moshiro-agent enable; /etc/init.d/moshiro-agent restart ;; esac say "moshiro-$role is installed" else say "installed, not started yet. Next:" echo " $PREFIX/fleet-enroll --server --join --name --identity-dir $CONFIG (as the '$USER_NAME' user)" echo " write $CONFIG/$role.json (see README.md), then: systemctl enable --now moshiro-$role" - [ "$role" = agent ] || echo " (OpenWrt: /etc/init.d/moshiro-probe enable && /etc/init.d/moshiro-probe start)" + echo " (OpenWrt: /etc/init.d/moshiro-agent enable && /etc/init.d/moshiro-agent start)" fi } @@ -165,15 +163,13 @@ uninstall() { rm -f "/etc/systemd/system/moshiro-$role.service" systemctl daemon-reload ;; procd) - /etc/init.d/moshiro-probe stop 2>/dev/null || true - /etc/init.d/moshiro-probe disable 2>/dev/null || true - rm -f /etc/init.d/moshiro-probe /lib/upgrade/keep.d/moshiro ;; + /etc/init.d/moshiro-agent stop 2>/dev/null || true + /etc/init.d/moshiro-agent disable 2>/dev/null || true + rm -f /etc/init.d/moshiro-agent /lib/upgrade/keep.d/moshiro ;; esac rm -f "$PREFIX/moshiro-$role" rm -f "$PREFIX/fleet-enroll" - other_role=agent - [ "$role" = agent ] && other_role=probe - [ -x "$PREFIX/moshiro-$other_role" ] || rm -f "$PREFIX"/lib*.so.* + rm -f "$PREFIX"/lib*.so.* rmdir "$PREFIX" 2>/dev/null || true if [ "$purge" -eq 1 ]; then rm -rf "$CONFIG" @@ -185,12 +181,11 @@ uninstall() { cmd="${1:-}" case "$cmd" in - agent|probe) shift; install_role "$cmd" "$@" ;; + agent) shift; install_role "$cmd" "$@" ;; uninstall) shift role="${1:-}" - [ "$role" = agent ] || [ "$role" = probe ] || die "usage: install.sh uninstall agent|probe [--purge]" shift uninstall "$role" "$@" ;; - *) die "usage: install.sh agent|probe [options] | uninstall agent|probe [--purge]" ;; + *) die "usage: install.sh agent [options] | uninstall agent [--purge]" ;; esac diff --git a/packaging/install/install.ps1 b/packaging/install/install.ps1 index a288911..889808a 100644 --- a/packaging/install/install.ps1 +++ b/packaging/install/install.ps1 @@ -1,6 +1,6 @@ -# MoShiro 节点一键安装(Windows,Agent 和 Probe 共用同一份,**静态**文件:不替换任何值,所以它的 SHA-256 在一个版本内固定不变)。 +# MoShiro 节点一键安装(Windows,监听和拨号两种模式共用同一份,**静态**文件:不替换任何值,所以它的 SHA-256 在一个版本内固定不变)。 # 面板给的命令先下载这份文件、核对 SHA-256(CI 构建时算出,写进签名的发布清单,也公布在发布页),一致才执行。 -# 必需的环境变量:MOSHIRO_SERVER(面板地址)、MOSHIRO_ROLE(agent 或 probe)、MOSHIRO_CODE(一次性的加入码)。 +# 必需的环境变量:MOSHIRO_SERVER(面板地址)、MOSHIRO_ROLE(默认 agent,监听并拨号)、MOSHIRO_CODE(一次性的加入码)。 # 做的事:用 Docker Desktop 起一个容器,身份和配置存在命名卷里(重启、换新镜像重建都不用再给参数),节点名字默认取计算机名。 # 可选环境变量:MOSHIRO_NAME=节点名字(名字被占用时用)、MOSHIRO_GENESIS / MOSHIRO_CONTROLLERS(命令里带着:信任根指纹和这个节点的属主 passkey 凭证 id,都是公开值)、 # MOSHIRO_ALLOW_UNVERIFIED=1 加 MOSHIRO_IMAGE=镜像(面板还没有已签名的发布清单时,不验签、按这个镜像安装;默认拒绝)。 @@ -31,7 +31,7 @@ function Test-MoShiroSignature([byte[]]$data, [byte[]]$derSignature, [string]$pe function Install-MoShiro { $ErrorActionPreference = 'Stop' - $role = $env:MOSHIRO_ROLE + $role = if ($env:MOSHIRO_ROLE) { $env:MOSHIRO_ROLE } else { 'agent' } $server = $env:MOSHIRO_SERVER $code = $env:MOSHIRO_CODE $image = $env:MOSHIRO_IMAGE @@ -43,7 +43,7 @@ YOrtO3Vso/QNdSMmepQlx1Q3v+KkN7ro3AY3I6DT2Q2txJu5pS7qzutSQA== '@ # 这些值来自命令行环境:只允许正常的字符,因为它们会进到 URL 和容器参数里 - if ($role -notin @('agent', 'probe')) { Write-Error 'MOSHIRO_ROLE must be agent or probe'; return } + if ($role -notin @('agent')) { Write-Error 'MOSHIRO_ROLE must be agent'; return } if ($server -notmatch '^https?://[A-Za-z0-9._:/@~%+\[\]-]+$') { Write-Error 'MOSHIRO_SERVER must be the panel address (http:// or https://)'; return } if ($code -notmatch '^[A-Za-z0-9._~%+-]+$') { Write-Error 'MOSHIRO_CODE is required (copy the whole command from the panel)'; return } @@ -83,7 +83,7 @@ YOrtO3Vso/QNdSMmepQlx1Q3v+KkN7ro3AY3I6DT2Q2txJu5pS7qzutSQA== } if ($null -eq $manifest) { if ($env:MOSHIRO_ALLOW_UNVERIFIED -ne '1' -or -not $image) { - Write-Error "the panel has no signed release for $role yet, so the image cannot be verified. To install anyway set `$env:MOSHIRO_ALLOW_UNVERIFIED='1' and `$env:MOSHIRO_IMAGE" + Write-Error "the panel has no signed release yet, so the image cannot be verified. To install anyway set `$env:MOSHIRO_ALLOW_UNVERIFIED='1' and `$env:MOSHIRO_IMAGE" return } Write-Host "warning: installing $image without signature verification" @@ -93,7 +93,7 @@ YOrtO3Vso/QNdSMmepQlx1Q3v+KkN7ro3AY3I6DT2Q2txJu5pS7qzutSQA== return } $release = [Text.Encoding]::UTF8.GetString($manifest) | ConvertFrom-Json - if ($release.component -ne $role) { Write-Error "the signed release is for '$($release.component)', not '$role'"; return } + if ($release.component -ne 'agent') { Write-Error "the signed release is for '$($release.component)', not 'agent'"; return } if ($release.digest -notmatch '^sha256:[0-9a-f]{64}$') { Write-Error 'the signed release has a malformed digest'; return } if ($release.image -notmatch '^[A-Za-z0-9._/:-]+$') { Write-Error 'the signed release has a malformed image name'; return } $image = "$($release.image)@$($release.digest)" @@ -110,9 +110,9 @@ YOrtO3Vso/QNdSMmepQlx1Q3v+KkN7ro3AY3I6DT2Q2txJu5pS7qzutSQA== $arguments = @('run', '-d', '--name', $container, '--restart', 'unless-stopped', '--read-only', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m', '--cap-drop', 'all', '--security-opt', 'no-new-privileges', '-v', "${volume}:/etc/moshiro") - # Docker Desktop 没有宿主网络:Agent 把端口发布出来(面板里的地址需要指向这台机器的公网地址) + # Docker Desktop 没有宿主网络:监听的节点把端口发布出来(面板里的地址需要指向这台机器的公网地址) if ($role -eq 'agent') { $arguments += @('-p', '7443:7443/tcp', '-p', '7443:7443/udp', '-p', '7444:7444/tcp') } - $arguments += @('-e', "MOSHIRO_SERVER=$server", '-e', "MOSHIRO_JOIN=$code", '-e', "MOSHIRO_NAME=$name") + $arguments += @('-e', "MOSHIRO_SERVER=$server", '-e', "MOSHIRO_JOIN=$code", '-e', "MOSHIRO_NAME=$name", '-e', "MOSHIRO_MODE=$role") # 每个节点都必须有属主(passkey):命令里的信任根指纹和属主缺一个就不装(面板的命令总是带着;只有没有信任根的老面板才需要 MOSHIRO_ALLOW_NO_OWNER=1) if ((-not $env:MOSHIRO_GENESIS -or -not $env:MOSHIRO_CONTROLLERS) -and $env:MOSHIRO_ALLOW_NO_OWNER -ne '1') { Write-Error '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)' diff --git a/packaging/install/install.sh b/packaging/install/install.sh index a98ab9f..3634e1d 100644 --- a/packaging/install/install.sh +++ b/packaging/install/install.sh @@ -1,8 +1,8 @@ #!/bin/sh -# MoShiro 节点一键安装(Agent 和 Probe 共用同一份,**静态**文件:不替换任何值,所以它的 SHA-256 在一个版本内固定不变)。 +# MoShiro 节点一键安装(监听和拨号两种模式共用同一份,**静态**文件:不替换任何值,所以它的 SHA-256 在一个版本内固定不变)。 # 面板给的命令先下载这份文件、核对 SHA-256(CI 构建时算出,写进签名的发布清单,也公布在发布页),一致才执行: # f=$(mktemp) && curl -fsSL 'https://panel/install' -o "$f" && echo " $f" | sha256sum -c - && MOSHIRO_SERVER=… MOSHIRO_ROLE=… MOSHIRO_CODE=… sh "$f" -# 必需的环境变量:MOSHIRO_SERVER(面板地址)、MOSHIRO_ROLE(agent 或 probe)、MOSHIRO_CODE(一次性的加入码)。 +# 必需的环境变量: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,节点在注册时验证面板的操作员集合并把它们固定在本地;都是公开值)、 @@ -11,7 +11,7 @@ set -eu SERVER="${MOSHIRO_SERVER:-}" -ROLE="${MOSHIRO_ROLE:-}" +ROLE="${MOSHIRO_ROLE:-agent}" CODE="${MOSHIRO_CODE:-}" IMAGE="${MOSHIRO_IMAGE:-}" RELEASE_KEY='-----BEGIN PUBLIC KEY----- @@ -23,7 +23,7 @@ say() { printf '%s\n' "$*"; } die() { printf 'error: %s\n' "$*" >&2; exit 1; } # 这些值来自命令行环境:只允许正常的字符,因为它们会进到 URL 和容器参数里 -case "$ROLE" in agent|probe) ;; *) die "MOSHIRO_ROLE must be agent or probe" ;; esac +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)" @@ -70,7 +70,7 @@ resolve_image() { curl -fsS "$SERVER/install/manifest?code=$CODE" -o "$tmp/manifest" 2>/dev/null /dev/null " + [ "${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=" say "warning: installing $IMAGE without signature verification" return 0 fi @@ -80,7 +80,7 @@ resolve_image() { 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)" = "$ROLE" ] || die "the signed release is for '$(field component)', not '$ROLE'" + [ "$(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 @@ -97,18 +97,16 @@ set -- run -d --name "$container" --restart "$restart" \ -v "$volume:/etc/moshiro" [ "$engine" = podman ] && set -- "$@" --security-opt label=disable if [ "$linux" -eq 1 ]; then - # 宿主网络:Agent 对外暴露端口,也靠它识别本机的公网 IPv4 和 IPv6 地址 + # 宿主网络:监听的节点对外暴露端口,两种模式都靠它识别本机的公网 IPv4 和 IPv6 地址 set -- "$@" --network host - if [ "$ROLE" = agent ]; then - # 主机资源占用(只读),管理端的 Agent 详情页显示 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 - fi + # 主机资源占用(只读),管理端的节点详情页显示 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 没有宿主网络:把 Agent 的端口发布出来(面板里的地址需要指向这台机器的公网地址) + # 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" +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)" diff --git a/packaging/openwrt/install.sh b/packaging/openwrt/install.sh index 58d3c4c..cb57284 100644 --- a/packaging/openwrt/install.sh +++ b/packaging/openwrt/install.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Native OpenWrt Probe installer/updater. No container engine or .NET runtime. +# Native OpenWrt installer/updater for an Agent. No container engine or .NET runtime. # Bootstrap: verify this script's published hash before running `sh install.sh install`. # Trust/configuration live outside the collector's writable identity directory. set -eu @@ -9,7 +9,7 @@ ROOT=${MOSHIRO_NATIVE_ROOT:-/usr/lib/moshiro-native} CONF=${MOSHIRO_NATIVE_CONF:-/etc/moshiro-native.conf} STATE=${MOSHIRO_NATIVE_STATE:-/etc/moshiro-native-state} STATUS=${MOSHIRO_NATIVE_STATUS:-/var/run/moshiro-update} -RUNTIME=${MOSHIRO_NATIVE_RUNTIME:-/var/run/moshiro-probe} +RUNTIME=${MOSHIRO_NATIVE_RUNTIME:-/var/run/moshiro-agent} ACTION=${1:-install} FEED=https://git.oeo.one/api/packages/justme/generic/moshiro-openwrt CHANNEL=${MOSHIRO_CHANNEL:-release} @@ -26,7 +26,7 @@ die() { echo "moshiro-openwrt: $*" >&2; exit 1; } log() { echo "moshiro-openwrt: $*"; } ctl() { if [ "${MOSHIRO_NATIVE_TEST:-}" = 1 ]; then "$MOSHIRO_NATIVE_SERVICE" "$@" - else /etc/init.d/moshiro-probe "$@"; fi + else /etc/init.d/moshiro-agent "$@"; fi } field() { jsonfilter -i "$WORK/manifest.json" -e "@.$1"; } download() { @@ -121,7 +121,7 @@ verify() { download "$FEED/$CHANNEL/manifest-$RID.sig" 1024 "$WORK/manifest.sig" || return 1 base64 -d "$WORK/manifest.sig" > "$WORK/signature" 2>/dev/null || return 1 openssl dgst -sha256 -verify "$STATE/release-key.pem" -signature "$WORK/signature" "$WORK/manifest.json" >/dev/null 2>&1 || return 1 - [ "$(field schema)" = 1 ] && [ "$(field component)" = probe ] \ + [ "$(field schema)" = 1 ] && [ "$(field component)" = agent ] \ && [ "$(field channel)" = "$CHANNEL" ] && [ "$(field rid)" = "$RID" ] || return 1 HASH=$(field sha256); SIZE=$(field size); SEQ=$(field seq); VERSION=$(field version); PACKAGE=$(field package) case "$HASH" in ''|*[!0-9a-f]*) return 1 ;; esac @@ -130,7 +130,7 @@ verify() { [ -n "$SEQ" ] && [ "${#SEQ}" -le 15 ] && [ -n "$SIZE" ] && [ "${#SIZE}" -le 9 ] && [ "$SIZE" -gt 0 ] && [ "$SIZE" -le "$MAX_BYTES" ] || return 1 case "$VERSION" in ''|*[!A-Za-z0-9.+_-]*) return 1 ;; esac [ "${#VERSION}" -le 100 ] && printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?(\+[A-Za-z0-9.-]+)?$' || return 1 - [ "$PACKAGE" = "probe-$RID-$HASH.tar.gz" ] || return 1 + [ "$PACKAGE" = "agent-$RID-$HASH.tar.gz" ] || return 1 } switch_to() { @@ -168,7 +168,7 @@ stage() { [ "$(sort -u "$WORK/members" | wc -l)" -eq "$(wc -l < "$WORK/members")" ] || return 1 while IFS= read -r member; do case "$member" in - moshiro-probe|fleet-enroll|openwrt.sh|probe.init|updater.init) ;; + moshiro-agent|fleet-enroll|openwrt.sh|agent.init|updater.init) ;; lib*.so.*) case "$member" in *[!A-Za-z0-9._+-]*) return 1 ;; esac ;; *) return 1 ;; esac @@ -185,7 +185,7 @@ stage() { if ! tar -xzf "$WORK/package.tar.gz" -C "$STAGED"; then return 1 fi - for required in moshiro-probe fleet-enroll openwrt.sh probe.init updater.init; do + for required in moshiro-agent fleet-enroll openwrt.sh agent.init updater.init; do [ -s "$STAGED/$required" ] || return 1 chmod 755 "$STAGED/$required" || return 1 done @@ -206,7 +206,7 @@ healthy() { version=$(jsonfilter -i "$health" -e '@.version' 2>/dev/null || true) pid=$(jsonfilter -i "$health" -e '@.pid' 2>/dev/null || true) at=$(jsonfilter -i "$health" -e '@.at' 2>/dev/null || echo 0) - active=$(ubus call service list '{"name":"moshiro-probe"}' 2>/dev/null | jsonfilter -e '@["moshiro-probe"].instances.*.pid' 2>/dev/null || true) + active=$(ubus call service list '{"name":"moshiro-agent"}' 2>/dev/null | jsonfilter -e '@["moshiro-agent"].instances.*.pid' 2>/dev/null || true) case "$pid:$at" in ''|*[!0-9:]*) ;; *) now=$(date +%s) if [ "$version" = "$VERSION" ] && [ -n "$pid" ] && [ "$pid" = "$active" ] && [ "$at" -ge "$started" ] && [ $((now - at)) -le 30 ]; then @@ -280,9 +280,9 @@ install() { : "${MOSHIRO_SERVER:?panel URL required}" "${MOSHIRO_CODE:?join token required}" "${MOSHIRO_GENESIS:?trust root required}" "${MOSHIRO_CONTROLLERS:?owner required}" case "$MOSHIRO_SERVER" in https://*) ;; *) die "panel must use HTTPS" ;; esac case "$MOSHIRO_SERVER" in *[!A-Za-z0-9:/._-]*) die "unsupported panel URL characters" ;; esac - [ ! -e /etc/moshiro/node.json ] && [ ! -e /etc/moshiro/probe.json ] || die "existing native identity/config found; migrate explicitly instead of overwriting it" + [ ! -e /etc/moshiro/node.json ] && [ ! -e /etc/moshiro/agent.json ] || die "existing native identity/config found; migrate explicitly instead of overwriting it" else - [ -f /etc/moshiro/node.json ] && [ -f /etc/moshiro/trust.json ] && [ -f /etc/moshiro/probe.json ] || die "saved identity/config incomplete; restore a backup before reinstalling" + [ -f /etc/moshiro/node.json ] && [ -f /etc/moshiro/trust.json ] && [ -f /etc/moshiro/agent.json ] || die "saved identity/config incomplete; restore a backup before reinstalling" fi acquire || die "another installation is running" if [ ! -f "$STATE/release-key.pem" ]; then @@ -301,7 +301,7 @@ KEY uid=900 while awk -F: -v uid="$uid" '$3 == uid {found=1} END {exit !found}' /etc/passwd /etc/group; do uid=$((uid + 1)); done echo "moshiro:x:$uid:" >> /etc/group - echo "moshiro:x:$uid:$uid:MoShiro:/var/run/moshiro-probe:/bin/false" >> /etc/passwd + echo "moshiro:x:$uid:$uid:MoShiro:/var/run/moshiro-agent:/bin/false" >> /etc/passwd } mkdir -p /etc/moshiro "$RUNTIME" chmod 750 /etc/moshiro "$RUNTIME" @@ -310,7 +310,7 @@ KEY LD_LIBRARY_PATH="$ROOT/versions/$HASH" "$ROOT/versions/$HASH/fleet-enroll" \ --server "$MOSHIRO_SERVER" --join "$MOSHIRO_CODE" --name "${MOSHIRO_NAME:-$(uname -n)}" \ --identity-dir /etc/moshiro --genesis "$MOSHIRO_GENESIS" --controllers "$MOSHIRO_CONTROLLERS" - printf '{"fleet":{"server":"%s","identity_dir":"/etc/moshiro"},"limits":{"max_test_mbps":20,"max_total_pps":2000}}\n' "$MOSHIRO_SERVER" > /etc/moshiro/probe.json + printf '{"fleet":{"server":"%s","identity_dir":"/etc/moshiro"},"limits":{"max_test_mbps":20,"max_total_pps":2000}}\n' "$MOSHIRO_SERVER" > /etc/moshiro/agent.json fi chown -R moshiro:moshiro /etc/moshiro "$RUNTIME" chmod 600 /etc/moshiro/node.key /etc/moshiro/tls.pfx @@ -321,9 +321,9 @@ KEY switch_to "versions/$HASH" mkdir -p /usr/libexec ln -sf "$ROOT/current/openwrt.sh" /usr/libexec/moshiro-openwrt - cp "$ROOT/current/probe.init" /etc/init.d/moshiro-probe - cp "$ROOT/current/updater.init" /etc/init.d/moshiro-probe-update - chmod 755 /etc/init.d/moshiro-probe /etc/init.d/moshiro-probe-update + cp "$ROOT/current/agent.init" /etc/init.d/moshiro-agent + cp "$ROOT/current/updater.init" /etc/init.d/moshiro-agent-update + chmod 755 /etc/init.d/moshiro-agent /etc/init.d/moshiro-agent-update mkdir -p /lib/upgrade/keep.d printf '/etc/moshiro/\n%s\n%s/\n' "$CONF" "$STATE" > /lib/upgrade/keep.d/moshiro printf '%s\n' "$SEQ" > "$STATE/$CHANNEL.seq" @@ -336,8 +336,8 @@ KEY die "installed but not healthy; inspect logread -e moshiro, then rerun install (identity is preserved)" fi status up_to_date - /etc/init.d/moshiro-probe-update enable - /etc/init.d/moshiro-probe-update restart + /etc/init.d/moshiro-agent-update enable + /etc/init.d/moshiro-agent-update restart log "installed $VERSION ($CHANNEL/$RID); automatic signed updates enabled" } diff --git a/packaging/openwrt/moshiro-probe.init b/packaging/openwrt/moshiro-agent.init similarity index 69% rename from packaging/openwrt/moshiro-probe.init rename to packaging/openwrt/moshiro-agent.init index 2f9539e..1b6aaed 100644 --- a/packaging/openwrt/moshiro-probe.init +++ b/packaging/openwrt/moshiro-agent.init @@ -1,16 +1,16 @@ #!/bin/sh /etc/rc.common -# procd service for moshiro-probe (OpenWrt). Installed as /etc/init.d/moshiro-probe. +# procd service for moshiro-agent (OpenWrt). Installed as /etc/init.d/moshiro-agent. START=95 STOP=10 USE_PROCD=1 -PROG=/usr/lib/moshiro/moshiro-probe -CONF=/etc/moshiro/probe.json +PROG=/usr/lib/moshiro/moshiro-agent +CONF=/etc/moshiro/agent.json start_service() { [ -f "$CONF" ] || { - logger -t moshiro-probe "$CONF not found: enroll first (install.sh probe --server URL --join TOKEN --name NAME)" + logger -t moshiro-agent "$CONF not found: enroll first (install.sh agent --server URL --join TOKEN --name NAME)" return 1 } procd_open_instance diff --git a/packaging/openwrt/native-probe.init b/packaging/openwrt/native-agent.init similarity index 69% rename from packaging/openwrt/native-probe.init rename to packaging/openwrt/native-agent.init index 2df9963..5380791 100644 --- a/packaging/openwrt/native-probe.init +++ b/packaging/openwrt/native-agent.init @@ -4,12 +4,12 @@ STOP=10 USE_PROCD=1 start_service() { - [ -f /etc/moshiro/probe.json ] || return 1 - mkdir -p /var/run/moshiro-probe - chown moshiro:moshiro /var/run/moshiro-probe - chmod 750 /var/run/moshiro-probe + [ -f /etc/moshiro/agent.json ] || return 1 + mkdir -p /var/run/moshiro-agent + chown moshiro:moshiro /var/run/moshiro-agent + chmod 750 /var/run/moshiro-agent procd_open_instance - procd_set_param command /usr/lib/moshiro-native/current/moshiro-probe --config /etc/moshiro/probe.json + procd_set_param command /usr/lib/moshiro-native/current/moshiro-agent --config /etc/moshiro/agent.json procd_set_param env LD_LIBRARY_PATH=/usr/lib/moshiro-native/current MOSHIRO_UPDATE=openwrt procd_set_param user moshiro procd_set_param group moshiro diff --git a/packaging/quadlet/moshiro-backend.container b/packaging/quadlet/moshiro-backend.container index 9383132..cf86986 100644 --- a/packaging/quadlet/moshiro-backend.container +++ b/packaging/quadlet/moshiro-backend.container @@ -1,4 +1,4 @@ -# MoShiro Backend(面板)的 Podman Quadlet 单元(Podman 4.6 及以上),更新方式和 Agent、Probe 完全一样: +# MoShiro Backend(面板)的 Podman Quadlet 单元(Podman 4.6 及以上),更新方式和 Agent 完全一样: # systemd 每次启动这个单元时 Pull=always 先从镜像仓库拉最新的镜像,Backend 在管理端确认更新之后有序退出(先把数据库备份到数据卷的 backups/), # Restart=always 把它拉起来,退出再被拉起就是更新(Backend 自己不下载、不替换任何东西)。 # 不会自动重启:Backend 指挥所有节点,更新要管理员在管理端点「更新并重启」。 diff --git a/packaging/quadlet/moshiro-probe.container b/packaging/quadlet/moshiro-probe.container deleted file mode 100644 index 33147c4..0000000 --- a/packaging/quadlet/moshiro-probe.container +++ /dev/null @@ -1,55 +0,0 @@ -# MoShiro Probe 的 Podman Quadlet 单元(Podman 4.6 及以上)。节点的更新也靠它:systemd 每次启动这个单元时 Pull=always 先从镜像仓库拉最新的镜像, -# 所以节点退出再被拉起就是更新(节点自己不下载、不替换任何东西)。默认自动更新:面板发现新版本后,节点运行满 10 分钟就自己有序退出; -# 想改成在管理端点「更新」才更新,在 env 文件里写 MOSHIRO_UPDATE_POLICY=manual。 -# -# 安装(rootless,普通用户,不用 sudo): -# mkdir -p ~/.config/containers/systemd ~/.config/moshiro -# 把面板「添加 Probe」给的 .env 内容存成 ~/.config/moshiro/probe.env,chmod 600 -# 把本文件放进 ~/.config/containers/systemd/ -# systemctl --user daemon-reload && systemctl --user start moshiro-probe # Quadlet 会自动生成服务,不用 enable;[Install] 让它开机自启 -# loginctl enable-linger $USER # 退出登录后继续运行(需要管理员执行一次) -# rootful:本文件放进 /etc/containers/systemd/,.env 内容存成 /etc/moshiro/probe.env,把下面的 EnvironmentFile 改成这个路径、 -# WantedBy 改成 multi-user.target,用 systemctl(不加 --user)。 -# 开发通道:把 Image 的 moshiro 换成 moshiro-dev。固定版本:把 :latest 换成 :<版本号>(固定之后更新不会换版本)。 -# 想要完全自动、不用在管理端点:另外启用 podman-auto-update.timer,并在 [Container] 里加 AutoUpdate=registry(Pull=always 也保留)。 -# 说明见 docs/ops/deployment.md。 -[Unit] -Description=MoShiro Probe -After=network-online.target -Wants=network-online.target - -[Container] -Image=registry.oeo.one/moshiro/moshiro-probe:latest -ContainerName=moshiro-probe -Pull=always -Network=host -RunInit=true -ReadOnly=true -NoNewPrivileges=true -DropCapability=ALL -SecurityLabelDisable=true -Tmpfs=/tmp:rw,noexec,nosuid,size=64m,mode=1777 -StopTimeout=20 -Volume=moshiro-probe-config:/etc/moshiro -Volume=/etc/os-release:/etc/os-release:ro -# 主机资源占用(只有汇总数字):只读挂载宿主机的 /proc、/sys 和根目录;不想上报就删掉这三行和下面三个 HOST_ 变量 -Volume=/proc:/host/proc:ro -Volume=/sys:/host/sys:ro -Volume=/:/host/root:ro -Environment=HOST_PROC=/host/proc -Environment=HOST_SYS=/host/sys -Environment=HOST_ROOT=/host/root -Environment=MOSHIRO_NETWORK_SCOPE=host -Environment=LD_LIBRARY_PATH=/usr/local/bin -# 告诉节点:退出之后外面会重新拉取,所以「更新」就是退出(没有这一行,节点不报告更新状态) -Environment=MOSHIRO_UPDATE=restart -EnvironmentFile=%h/.config/moshiro/probe.env - -[Service] -Restart=always -RestartSec=5 -# 第一次拉取镜像可能要一会儿 -TimeoutStartSec=600 - -[Install] -WantedBy=default.target diff --git a/packaging/systemd/moshiro-probe-container.service b/packaging/systemd/moshiro-probe-container.service deleted file mode 100644 index 3124b41..0000000 --- a/packaging/systemd/moshiro-probe-container.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=MoShiro Probe (Podman container) -After=network-online.target -Wants=network-online.target - -[Service] -EnvironmentFile=/etc/moshiro-update/probe.image -Restart=always -RestartSec=5 -TimeoutStopSec=30 -ExecStartPre=-/usr/bin/podman rm -f moshiro-probe -ExecStart=/usr/bin/podman run --rm --name moshiro-probe --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -e MOSHIRO_NETWORK_SCOPE=host -v /etc/os-release:/etc/os-release:ro -v moshiro-probe-config:/etc/moshiro -v moshiro-probe-status:/etc/moshiro/update-status:ro ${IMAGE} -ExecStop=/usr/bin/podman stop -t 20 moshiro-probe - -[Install] -WantedBy=multi-user.target diff --git a/packaging/systemd/moshiro-probe-docker.service b/packaging/systemd/moshiro-probe-docker.service deleted file mode 100644 index d1b8c3a..0000000 --- a/packaging/systemd/moshiro-probe-docker.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=MoShiro Probe (Docker container) -After=network-online.target -Wants=network-online.target - -[Service] -EnvironmentFile=/etc/moshiro-update/probe.image -Restart=always -RestartSec=5 -TimeoutStopSec=30 -ExecStartPre=-/usr/bin/docker rm -f moshiro-probe -ExecStart=/usr/bin/docker run --rm --name moshiro-probe --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -e MOSHIRO_NETWORK_SCOPE=host -v /etc/os-release:/etc/os-release:ro -v moshiro-probe-config:/etc/moshiro -v moshiro-probe-status:/etc/moshiro/update-status:ro ${IMAGE} -ExecStop=/usr/bin/docker stop -t 20 moshiro-probe - -[Install] -WantedBy=multi-user.target diff --git a/packaging/systemd/moshiro-probe-rootless.service b/packaging/systemd/moshiro-probe-rootless.service deleted file mode 100644 index d7d5462..0000000 --- a/packaging/systemd/moshiro-probe-rootless.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=MoShiro Probe (rootless Podman container) -After=network-online.target -Wants=network-online.target - -[Service] -EnvironmentFile=%h/.config/moshiro/probe.image -Restart=always -RestartSec=5 -TimeoutStopSec=30 -ExecStartPre=-/usr/bin/podman rm -f moshiro-probe -ExecStart=/usr/bin/podman run --rm --name moshiro-probe --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -e MOSHIRO_NETWORK_SCOPE=host -v /etc/os-release:/etc/os-release:ro -v moshiro-probe-config:/etc/moshiro -v moshiro-probe-status:/etc/moshiro/update-status:ro ${IMAGE} -ExecStop=/usr/bin/podman stop -t 20 moshiro-probe - -[Install] -WantedBy=default.target diff --git a/packaging/systemd/moshiro-probe.service b/packaging/systemd/moshiro-probe.service deleted file mode 100644 index 10c6e05..0000000 --- a/packaging/systemd/moshiro-probe.service +++ /dev/null @@ -1,44 +0,0 @@ -[Unit] -Description=MoShiro probe (network line quality measurement origin) -After=network-online.target -Wants=network-online.target - -[Service] -Type=exec -User=moshiro -Group=moshiro -Environment=LD_LIBRARY_PATH=/usr/lib/moshiro -ExecStart=/usr/lib/moshiro/moshiro-probe --config /etc/moshiro/probe.json -Restart=always -RestartSec=5 - -# The probe is a pure collector: it never starts processes and writes no measurement data. -# The only file the probe writes is agents.json (the latest agent list) next to probe.json. -ReadWritePaths=/etc/moshiro -NoNewPrivileges=yes -ProtectSystem=strict -ProtectHome=yes -PrivateTmp=yes -PrivateDevices=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectKernelLogs=yes -ProtectControlGroups=yes -ProtectClock=yes -ProtectHostname=yes -RestrictAddressFamilies=AF_INET AF_INET6 -RestrictNamespaces=yes -RestrictRealtime=yes -LockPersonality=yes -# NativeAOT does not JIT, so writable+executable memory is never needed. -MemoryDenyWriteExecute=yes -SystemCallArchitectures=native -# No capabilities needed: the probe only dials out. -CapabilityBoundingSet= - -MemoryMax=64M -TasksMax=64 -LimitNOFILE=4096 - -[Install] -WantedBy=multi-user.target diff --git a/packaging/update/install-updater.sh b/packaging/update/install-updater.sh index e5a9b4e..5e16c71 100644 --- a/packaging/update/install-updater.sh +++ b/packaging/update/install-updater.sh @@ -5,7 +5,7 @@ set -eu COMPONENT="${1:-}"; shift || true -case "$COMPONENT" in agent|probe) ;; *) echo "usage: install-updater.sh [--user] [--engine podman|docker] --data-dir D --image-repo R --unit U [--policy auto|manual] [--channel dev|release] [--health-url URL] [--authfile F] [--no-enable]" >&2; exit 2 ;; esac +case "$COMPONENT" in agent) ;; *) echo "usage: install-updater.sh [--user] [--engine podman|docker] --data-dir D --image-repo R --unit U [--policy auto|manual] [--channel dev|release] [--health-url URL] [--authfile F] [--no-enable]" >&2; exit 2 ;; esac DATA_DIR=""; IMAGE_REPO=""; UNIT=""; POLICY=manual; AUTHFILE=""; ENGINE=podman; SERVICE_SCOPE=system; ENABLE=true; DATA_VOLUME=""; STATUS_VOLUME=""; HELPER_IMAGE="" CHANNEL=""; HEALTH_URL="" while [ $# -gt 0 ]; do diff --git a/packaging/update/moshiro-update.sh b/packaging/update/moshiro-update.sh index 9d6aae4..c684e18 100644 --- a/packaging/update/moshiro-update.sh +++ b/packaging/update/moshiro-update.sh @@ -1,12 +1,12 @@ #!/bin/sh -# MoShiro 容器节点(Agent、Probe)的宿主机更新器。用法:moshiro-update <组件> 组件 = agent 或 probe +# MoShiro 容器节点(Agent)的宿主机更新器。用法:moshiro-update <组件> 组件 = agent # # 信任模型(防篡改、防中间人): # - 只接受用发布私钥签名的清单(ECDSA P-256 / SHA-256,对清单文件的原始字节签名);公钥放在本机,面板和 Registry 都改不了它。 # - 清单里写的是镜像的 digest:拉取时用 repo@sha256:…,内容寻址,Registry 或链路上的人换不出另一个镜像。 # - 镜像仓库、组件、服务名都来自本机的配置(/etc/moshiro-update/<组件>.conf),清单里写什么都不会改变拉哪个仓库。 # - 清单的 seq 必须比已安装的大(防止旧清单被重放回滚)。 -# - 业务进程(Agent、Probe)只能往数据目录里写「清单」和「请求」文件,更新由这个宿主机脚本完成,业务进程不执行任何命令。 +# - 业务进程(Agent)只能往数据目录里写「清单」和「请求」文件,更新由这个宿主机脚本完成,业务进程不执行任何命令。 # Shell snippets below execute inside helper containers. # shellcheck disable=SC2016 set -eu @@ -14,8 +14,8 @@ umask 077 CMD="${1:-}" COMPONENT="${2:-}" -case "$CMD" in check|apply) ;; *) echo "usage: moshiro-update " >&2; exit 2 ;; esac -case "$COMPONENT" in agent|probe) ;; *) echo "component must be agent or probe" >&2; exit 2 ;; esac +case "$CMD" in check|apply) ;; *) echo "usage: moshiro-update " >&2; exit 2 ;; esac +case "$COMPONENT" in agent) ;; *) echo "component must be agent" >&2; exit 2 ;; esac if [ "$(id -u)" -eq 0 ]; then DEFAULT_CONF_DIR=/etc/moshiro-update @@ -55,7 +55,7 @@ pull_image() { "$ENGINE" pull --quiet "$1" fi } -# 业务进程(容器里的 Agent、Probe)可写的目录只有 update/:它在里面放 offer.manifest、offer.sig、apply.request。 +# 业务进程(容器里的 Agent)可写的目录只有 update/:它在里面放 offer.manifest、offer.sig、apply.request。 # 更新器在容器外运行:只读取普通文件(不跟符号链接)、读取之后先复制到自己的临时目录再验证和使用(防止验证之后被换掉), # 状态目录在 rootless 容器里另行只读挂载;更新器配置和已安装版本状态不挂入容器。 if [ -n "$DATA_VOLUME" ]; then @@ -139,7 +139,7 @@ fetch_mailbox() { now() { date +%s; } -# 状态文件:更新器写,Agent / Probe 从只读挂载中读取并随心跳上报。 +# 状态文件:更新器写,Agent 从只读挂载中读取并随心跳上报。 write_status() { # state available_json error tmp="$WORK/status.json" printf '{"state":"%s","current":"%s","available":%s,"error":"%s","at":%s}\n' \ diff --git a/packaging/update/tests/feed-test.sh b/packaging/update/tests/feed-test.sh index cb5d78f..00010eb 100644 --- a/packaging/update/tests/feed-test.sh +++ b/packaging/update/tests/feed-test.sh @@ -108,7 +108,7 @@ resolve() { # 签名私钥 组件 镜像仓库 } expect "first install: signed manifest pins the digest" "$REPO@$DIGEST" "$(resolve "$T/b.pem" agent "$REPO")" expect "first install: wrong signing key is refused" "REFUSED" "$(resolve "$T/c.pem" agent "$REPO")" -expect "first install: manifest for another component is refused" "REFUSED" "$(resolve "$T/b.pem" probe "$REPO")" +expect "first install: manifest for another component is refused" "REFUSED" "$(resolve "$T/b.pem" backend "$REPO")" expect "first install: manifest naming another repository is refused" "REFUSED" "$(resolve "$T/b.pem" agent "registry.evil/moshiro-dev/moshiro-agent")" # --- 第一次部署:正在跑的镜像就是清单里的这个 digest → 记成已安装(不当作有更新) diff --git a/scripts/package.sh b/scripts/package.sh index 1407151..d2f03b6 100644 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -1,12 +1,13 @@ #!/usr/bin/env bash -# 打包:scripts/package.sh → backend/artifacts/packages/moshiro---.tar.gz(附 .sha256) +# 打包:scripts/package.sh → backend/artifacts/packages/moshiro---.tar.gz(附 .sha256) # 内容:节点二进制(agent 还带 fleet-enroll)、systemd 单元、OpenWrt procd 脚本、install.sh。 # 发布产物要先用 scripts/publish.sh 生成(agent 需要 agent 和 fleet-enroll 两个)。 set -euo pipefail -role="${1:?usage: package.sh }" -rid="${2:?usage: package.sh }" +role="${1:?usage: package.sh }" +rid="${2:?usage: package.sh }" +[[ "$role" == agent ]] || { echo "only agent packages are supported" >&2; exit 2; } root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -version="$(sed -n 's#.*\(.*\).*#\1#p' "$root/Directory.Build.props" | head -1)" +version="$(sed -n 's#.*\(.*\).*#\1#p' "$root/backend/Directory.Build.props" | head -1)" name="moshiro-$role-$version-$rid" stage="$root/backend/artifacts/packages/stage/$name" quic_runtime="$root/backend/artifacts/packages/quic-runtime/$rid" @@ -27,10 +28,11 @@ fi cp "$root/backend/artifacts/publish/$role/$rid/moshiro-$role" "$stage/" cp "$root/packaging/install.sh" "$stage/" cp "$root/packaging/systemd/moshiro-$role.service" "$stage/systemd/" -cp "$root/$role/README.md" "$stage/README.md" +cp "$root/backend/$role/README.md" "$stage/README.md" if [ "$role" = agent ]; then cp "$root/backend/artifacts/publish/fleet-enroll/$rid/fleet-enroll" "$stage/" -else +fi +if [[ "$rid" == linux-* ]]; then mkdir -p "$stage/openwrt" cp "$root/packaging/openwrt/"* "$stage/openwrt/" fi diff --git a/scripts/publish.sh b/scripts/publish.sh index f772815..39c88f6 100644 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -1,16 +1,15 @@ #!/usr/bin/env bash -# 发布一个节点程序:scripts/publish.sh +# 发布一个节点程序:scripts/publish.sh # 输出到 backend/artifacts/publish///。linux-musl-x64(Alpine)和 linux-x64(Ubuntu noble,glibc)在 NativeAOT SDK 容器里构建(任何主机都可以,包括 Windows), # 其他 rid 用本机的 dotnet(NativeAOT 不能交叉编译到别的操作系统,所以 win-x64 要在 Windows 上、linux-x64 要在 glibc 的 Linux 上构建)。 set -euo pipefail -project="${1:?usage: publish.sh }" -rid="${2:?usage: publish.sh }" +project="${1:?usage: publish.sh }" +rid="${2:?usage: publish.sh }" root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" case "$project" in agent) csproj="backend/agent/src/MoShiro.Agent" ;; - probe) csproj="backend/probe/src/MoShiro.Probe" ;; backend) csproj="backend/src/MoShiro.Backend" ;; fleet-enroll) csproj="backend/fleet/tools/fleet-enroll" ;; *) echo "unknown project: $project" >&2; exit 2 ;; diff --git a/scripts/smoke-linux.sh b/scripts/smoke-linux.sh deleted file mode 100644 index da91a1d..0000000 --- a/scripts/smoke-linux.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/sh -# 在 Linux 容器里用发布出来的 NativeAOT 二进制跑一遍完整链路:后端 → 注册 Agent → Agent → Probe → 数据落库。 -# -# RID=linux-musl-x64 IMAGE=alpine:3.22 (默认) -# RID=linux-x64 IMAGE=debian:bookworm-slim -# MODE=install 用打包产物里的 install.sh 安装(agent 注册、probe 身份、以 moshiro 用户运行),而不是直接运行二进制 -# -# 用法(在仓库根目录;Windows 下先 export MSYS_NO_PATHCONV=1;需先运行 scripts/package.sh): -# docker run --rm -e RID -e MODE -v "$PWD/backend/artifacts:/art:ro" -v "$PWD/scripts/smoke-linux.sh:/smoke.sh:ro" $IMAGE sh /smoke.sh [秒数] -set -eu -seconds="${1:-60}" -rid="${RID:-linux-musl-x64}" -mode="${MODE:-direct}" -pub=/art/publish - -if command -v apk >/dev/null 2>&1; then - apk add --no-cache curl jq libssl3 libstdc++ >/dev/null -else - apt-get update >/dev/null && apt-get install -y --no-install-recommends curl jq ca-certificates libssl3 >/dev/null -fi - -work=/tmp/smoke -mkdir -p "$work/data" "$work/probe" -base=http://127.0.0.1:18080 - -export ASPNETCORE_URLS="$base" MoShiro__DataDirectory="$work/data" MoShiro__InitialAdminUser=admin \ - MoShiro__InitialAdminPassword="correct horse battery" MoShiro__SecureCookies=false -(cd "$pub/backend/$rid" && ./moshiro-backend >"$work/backend.log" 2>&1 &) -for i in $(seq 1 60); do curl -fs "$base/healthz" >/dev/null 2>&1 && break; sleep 1; done -curl -fs "$base/healthz" >/dev/null || { cat "$work/backend.log"; echo "backend did not start"; exit 1; } -echo "backend up" - -login=$(curl -si -X POST "$base/api/admin/auth/login" -H 'Content-Type: application/json' -d '{"username":"admin","password":"correct horse battery"}') -cookie=$(echo "$login" | grep -i '^set-cookie' | head -1 | sed 's/^[^:]*: //; s/;.*//' | tr -d '\r') -csrf=$(echo "$login" | tail -1 | jq -r .csrf_token) -token=$(curl -fs -X POST "$base/api/admin/api-tokens" -H "Cookie: $cookie" -H "X-CSRF-Token: $csrf" -H 'Content-Type: application/json' -d '{"name":"smoke","scope":"admin"}' | jq -r .plaintext) -auth="Authorization: Bearer $token" -join=$(curl -fs -X POST "$base/api/admin/join-tokens" -H "$auth" -H 'Content-Type: application/json' -d '{"auto_approve":true,"max_uses":1,"expires_in_hours":1}' | jq -r .plaintext) -probe_join=$(curl -fs -X POST "$base/api/admin/join-tokens" -H "$auth" -H 'Content-Type: application/json' -d '{"role":"probe","max_uses":1,"expires_in_hours":1}' | jq -r .plaintext) - -if [ "$mode" = install ]; then - # 安装到 /usr/lib/moshiro 和 /etc/moshiro,以 moshiro 用户运行(没有 systemd:自己启动) - mkdir -p /tmp/pkg && for role in agent probe; do tar -C /tmp/pkg -xzf /art/packages/moshiro-$role-*-$rid.tar.gz; done - (cd /tmp/pkg/moshiro-agent-* && ./install.sh agent --server "$base" --join "$join" --name smoke-agent --no-start) - ls -l /usr/lib/moshiro /etc/moshiro - run_as() { su -s /bin/sh moshiro -c "exec $*"; } - run_as /usr/lib/moshiro/moshiro-agent --config /etc/moshiro/agent.json >"$work/agent.log" 2>&1 & - agent_pid=$! - sleep 3 - (cd /tmp/pkg/moshiro-probe-* && ./install.sh probe --server "$base" --join "$probe_join" --name smoke-probe --no-start) - run_as /usr/lib/moshiro/moshiro-probe --config /etc/moshiro/probe.json >"$work/probe.log" 2>&1 & - probe_pid=$! -else - for role in agent probe; do - package_dir="$work/$role-bin" - mkdir -p "$package_dir" - set -- /art/packages/moshiro-"$role"-*-"$rid".tar.gz - [ -f "$1" ] || { echo "missing QUIC-bundled package: $1 (run scripts/package.sh first)" >&2; exit 1; } - tar -xzf "$1" -C "$package_dir" - done - mkdir -p "$work/agent-id" - "$work/agent-bin/fleet-enroll" --server "$base" --join "$join" --name smoke-agent --identity-dir "$work/agent-id" - printf '{"fleet":{"server":"%s","identity_dir":"%s"},"listen":{"bind":"127.0.0.1"}}' "$base" "$work/agent-id" >"$work/agent.json" - "$work/agent-bin/moshiro-agent" --config "$work/agent.json" >"$work/agent.log" 2>&1 & - agent_pid=$! - sleep 3 - mkdir -p "$work/probe-id" - "$work/probe-bin/fleet-enroll" --server "$base" --join "$probe_join" --name smoke-probe --identity-dir "$work/probe-id" - printf '{"fleet":{"server":"%s","identity_dir":"%s"}}' "$base" "$work/probe-id" >"$work/probe/probe.json" - "$work/probe-bin/moshiro-probe" --config "$work/probe/probe.json" >"$work/probe.log" 2>&1 & - probe_pid=$! -fi - -rss() { awk '/VmRSS/ {printf "%.1f", $2/1024}' "/proc/$1/status"; } -i=0 -while [ "$i" -lt "$seconds" ]; do - sleep 10; i=$((i + 10)) - echo "t+${i}s probe=$(rss "$probe_pid")MB agent=$(rss "$agent_pid")MB" -done - -echo "agents: $(curl -fs "$base/api/admin/agents" -H "$auth" | jq -c '[.items[] | {name, status, probe_connected}]')" -echo "chunks: $(curl -fs "$base/api/admin/raw/chunks" -H "$auth" | jq -c '[.items[].kind] | group_by(.) | map({(.[0]): length}) | add')" -agent_id=$(curl -fs "$base/api/admin/agents" -H "$auth" | jq -r '.items[0].id') -echo "tcp summary: $(curl -fs "$base/api/public/lines/$agent_id/series?range=5m&step=1s&metrics=tcp.rtt.p50,tcp.rtt.p999,tcp.rtt.max,tcp.rtt.tail,tcp.slow,tcp.retrans,tcp.srtt.p95" -H "$auth" | jq -c '.summary.metrics')" -echo "--- probe.log"; tail -4 "$work/probe.log" -echo "--- agent.log"; tail -4 "$work/agent.log" diff --git a/scripts/smoke-systemd.sh b/scripts/smoke-systemd.sh deleted file mode 100644 index 3ee744a..0000000 --- a/scripts/smoke-systemd.sh +++ /dev/null @@ -1,31 +0,0 @@ -# 在带 systemd 的 Debian 容器里用 install.sh 安装并以 systemd 单元运行(验证沙箱指令与 NativeAOT 兼容)。 -# docker run -d --name sysd --privileged --cgroupns=host -v /sys/fs/cgroup:/sys/fs/cgroup:rw -v "$PWD/backend/artifacts:/art:ro" jrei/systemd-debian:12 -# docker cp scripts/smoke-systemd.sh sysd:/s.sh && docker exec sysd bash /s.sh -set -eu -apt-get update >/dev/null && apt-get install -y --no-install-recommends curl jq ca-certificates libssl3 >/dev/null -base=http://127.0.0.1:18080 -mkdir -p /tmp/data -cd /art/publish/backend/linux-x64 -ASPNETCORE_URLS=$base MoShiro__DataDirectory=/tmp/data MoShiro__InitialAdminUser=admin MoShiro__InitialAdminPassword="correct horse battery" MoShiro__SecureCookies=false nohup ./moshiro-backend >/tmp/backend.log 2>&1 & -for i in $(seq 1 60); do curl -fs $base/healthz >/dev/null 2>&1 && break; sleep 1; done -login=$(curl -si -X POST $base/api/admin/auth/login -H 'Content-Type: application/json' -d '{"username":"admin","password":"correct horse battery"}') -cookie=$(echo "$login" | grep -i '^set-cookie' | head -1 | sed 's/^[^:]*: //; s/;.*//' | tr -d '\r') -csrf=$(echo "$login" | tail -1 | jq -r .csrf_token) -token=$(curl -fs -X POST $base/api/admin/api-tokens -H "Cookie: $cookie" -H "X-CSRF-Token: $csrf" -H 'Content-Type: application/json' -d '{"name":"t","scope":"admin"}' | jq -r .plaintext) -auth="Authorization: Bearer $token" -join=$(curl -fs -X POST $base/api/admin/join-tokens -H "$auth" -H 'Content-Type: application/json' -d '{"auto_approve":true,"max_uses":1,"expires_in_hours":1}' | jq -r .plaintext) -mkdir -p /tmp/pkg; for r in agent probe; do tar -C /tmp/pkg -xzf /art/packages/moshiro-$r-*-linux-x64.tar.gz; done -(cd /tmp/pkg/moshiro-agent-* && ./install.sh agent --server $base --join "$join" --name sysd-agent) -sleep 3 -probe_join=$(curl -fs -X POST $base/api/admin/join-tokens -H "$auth" -H 'Content-Type: application/json' -d '{"role":"probe","max_uses":1,"expires_in_hours":1}' | jq -r .plaintext) -(cd /tmp/pkg/moshiro-probe-* && ./install.sh probe --server $base --join "$probe_join" --name sysd-probe) -sleep 25 -systemctl is-active moshiro-agent moshiro-probe -echo "agents: $(curl -fs $base/api/admin/agents -H "$auth" | jq -c '[.items[] | {name, status, probe_connected}]')" -echo "chunks: $(curl -fs $base/api/admin/raw/chunks -H "$auth" | jq -c '[.items[].kind] | length')" -systemctl show moshiro-agent -p MemoryCurrent -p NRestarts | tr '\n' ' '; systemctl show moshiro-probe -p MemoryCurrent -p NRestarts | tr '\n' ' '; echo -ps -o user,rss,comm -C moshiro-agent,moshiro-probe -journalctl -u moshiro-agent -u moshiro-probe --no-pager | tail -8 -echo "--- sandbox check: agent cannot write /etc/moshiro"; nsenter -t $(systemctl show moshiro-agent -p MainPID --value) -m sh -c 'touch /etc/moshiro/x 2>&1; ls /proc/self/status >/dev/null' 2>&1 | head -2 -grep -E "NoNewPrivs|CapEff|Seccomp:" /proc/$(systemctl show moshiro-agent -p MainPID --value)/status -grep -E "NoNewPrivs|CapEff|Seccomp:" /proc/$(systemctl show moshiro-probe -p MainPID --value)/status diff --git a/scripts/smoke-wsl.py b/scripts/smoke-wsl.py deleted file mode 100644 index f4a55d8..0000000 --- a/scripts/smoke-wsl.py +++ /dev/null @@ -1,77 +0,0 @@ -"""跨机器的真实链路验证:Windows 上的 Probe(NativeAOT exe)→ WSL2 里的 Agent 和 Backend(真实的虚拟网络、不同的单调时钟原点)。 - -前提:WSL 里有 Debian(root)、tc(netem 注入丢包/延迟/黑洞用);已经用 scripts/publish.sh 发布了 linux-x64 的 agent、backend、fleet-enroll, -并且 Windows 上发布了 probe(dotnet publish backend/probe/src/MoShiro.Probe -c Release -r win-x64 -p:PublishAot=true -o backend/artifacts/aot-probe)。 -把 linux-x64 的产物复制到 WSL 的 /root/mon/,在 WSL 里用 setsid -f nohup 启动 moshiro-backend(监听 0.0.0.0:29181),再运行本脚本。 -故障注入见 docs/ops/deployment.md 第 3 节。 -""" -import json, os, subprocess, sys, time, urllib.request, urllib.error - -# 要连的是本机的虚拟网络(WSL),不能走环境变量里的 HTTP(S)_PROXY -OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({})) - -ROOT = r"C:\Users\JustMe\Desktop\moshiro-proj" -PROBE_EXE = os.path.join(ROOT, "backend", "artifacts", "aot-probe", "moshiro-probe.exe") -WORK = r"C:\Users\JustMe\AppData\Local\Temp\realnet" -PORT = 29181 - - -def wsl(cmd, check=True): - r = subprocess.run(["wsl", "-d", "Debian", "-u", "root", "-e", "bash", "-c", cmd], capture_output=True, text=True) - if check and r.returncode != 0: - print("WSL FAILED:", cmd, r.stdout, r.stderr) - raise SystemExit(1) - return r.stdout.strip() - - -WSLIP = wsl("ip -4 addr show eth0 | grep -oP '(?<=inet )[0-9.]+'") -BASE = f"http://{WSLIP}:{PORT}" - - -def req(method, path, body=None, headers=None, ctype="application/json"): - data = None if body is None else json.dumps(body).encode() - h = {"Content-Type": ctype} - h.update(headers or {}) - r = urllib.request.Request(BASE + path, data=data, method=method, headers=h) - try: - with OPENER.open(r, timeout=30) as resp: - text = resp.read().decode() - return resp, (json.loads(text) if text else None) - except urllib.error.HTTPError as e: - print("HTTP", e.code, e.read().decode()) - raise - - -def main(): - os.makedirs(WORK, exist_ok=True) - print("wsl ip", WSLIP) - resp, login = req("POST", "/api/admin/auth/login", {"username": "admin", "password": os.environ.get("MOSHIRO_ADMIN_PASSWORD", "correct horse battery")}) - cookie = resp.headers["Set-Cookie"].split(";")[0] - _, tok = req("POST", "/api/admin/api-tokens", {"name": "net", "scope": "admin"}, {"Cookie": cookie, "X-CSRF-Token": login["csrf_token"]}) - auth = {"Authorization": "Bearer " + tok["plaintext"]} - json.dump({"base": BASE, "token": tok["plaintext"], "wslip": WSLIP}, open(WORK + r"\state.json", "w")) - _, join = req("POST", "/api/admin/join-tokens", {"auto_approve": True, "max_uses": 1, "expires_in_hours": 1}, auth) - - wsl("pkill moshiro-agent; cd /root/mon && rm -rf id agent.json") - print(wsl(f"cd /root/mon && ./fleet-enroll --server http://127.0.0.1:{PORT} --join {join['plaintext']} --name wsl-agent --identity-dir /root/mon/id")) - cfg = {"fleet": {"server": f"http://127.0.0.1:{PORT}", "identity_dir": "/root/mon/id"}, "listen": {"bind": "0.0.0.0"}} - wsl("cat > /root/mon/agent.json <<'EOF'\n" + json.dumps(cfg) + "\nEOF") - wsl("cd /root/mon && (setsid nohup ./moshiro-agent --config /root/mon/agent.json > agent.log 2>&1 < /dev/null &)") - time.sleep(3) - _, agents = req("GET", "/api/admin/agents", None, auth) - agent = agents["items"][0] - print("agent", agent["id"], agent["status"], "online", agent.get("online")) - # the backend only sees 127.0.0.1 as the agent's address: the panel needs the address the probe can reach - req("PATCH", "/api/admin/agents/" + agent["id"], {"public_host": WSLIP}, auth, "application/merge-patch+json") - _, ident = req("POST", "/api/admin/probes", {"name": "home"}, auth) - print("bootstrap", ident["bootstrap"]) - os.makedirs(WORK + r"\probe", exist_ok=True) - json.dump(ident, open(WORK + r"\probe\probe.json", "w")) - json.dump({"base": BASE, "token": tok["plaintext"], "wslip": WSLIP, "agent_id": agent["id"]}, open(WORK + r"\state.json", "w")) - log = open(WORK + r"\probe.log", "w") - p = subprocess.Popen([PROBE_EXE, "--config", WORK + r"\probe\probe.json"], stdout=log, stderr=subprocess.STDOUT) - open(WORK + r"\probe.pid", "w").write(str(p.pid)) - print("probe pid", p.pid) - - -main() diff --git a/scripts/stability_wsl.py b/scripts/stability_wsl.py deleted file mode 100644 index 69dac5b..0000000 --- a/scripts/stability_wsl.py +++ /dev/null @@ -1,613 +0,0 @@ -""" -稳定性实测:真实的初始化、正常关闭、崩溃关闭、链路故障与恢复。 - -拓扑:Windows 上的 Probe(NativeAOT exe)→ WSL2 里的 Agent → 同一个 WSL 里的 Backend。 -Probe↔Agent 走真实的虚拟网络(eth0),Agent↔Backend 走 WSL 里的回环(故障用 tc 注入在 lo 上)。 - -前提:WSL 里有 Debian(root)和 tc;已发布 linux-x64 的 agent、backend、fleet-enroll(scripts/publish.sh), -以及 Windows 的 probe(dotnet publish backend/probe/src/MoShiro.Probe -c Release -r win-x64 -p:PublishAot=true -o backend/artifacts/aot-probe)。 - -用法:python scripts/stability_wsl.py [场景名 ...] (不带参数运行全部) -场景:init graceful crash probe_link backend_link silent soak -""" -import ctypes -import json -import os -import subprocess -import sys -import time -import urllib.error -import urllib.request - -# 要连的是本机的虚拟网络(WSL),不能走环境变量里的 HTTP(S)_PROXY -OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({})) - -ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -PROBE_EXE = os.path.join(ROOT, "backend", "artifacts", "aot-probe", "moshiro-probe.exe") -PUB = "/mnt/" + ROOT[0].lower() + ROOT[2:].replace("\\", "/") + "/backend/artifacts/publish" -WORK = os.path.join(os.environ.get("TEMP", "."), "stability") -PORT = 29181 -ADMIN_PASSWORD = os.environ.get("MOSHIRO_ADMIN_PASSWORD", "correct horse battery") # 测试用后端的初始管理员密码 -RESULTS = [] -HIDDEN = subprocess.CREATE_NO_WINDOW # 所有子进程都不弹窗 - - -# ------------------------------------------------------------------ WSL / process helpers - -def wsl(cmd, check=True, timeout=120): - r = subprocess.run(["wsl", "-d", "Debian", "-u", "root", "-e", "bash", "-c", cmd], capture_output=True, text=True, timeout=timeout, creationflags=HIDDEN) - if check and r.returncode != 0: - raise RuntimeError(f"wsl failed ({r.returncode}): {cmd}\n{r.stdout}\n{r.stderr}") - return r.stdout.strip() - - -WSLIP = wsl("ip -4 addr show eth0 | grep -oP '(?<=inet )[0-9.]+'") -BASE = f"http://{WSLIP}:{PORT}" -TOKEN = "" - -LAUNCHER = r"""#!/bin/sh -# start.sh : run detached, append to .log, record the exit code in .exit -name=$1; shift -rm -f /root/mon/$name.exit -setsid -f sh -c 'cd /root/mon; "$@" >> /root/mon/'$name'.log 2>&1; echo $? > /root/mon/'$name'.exit' sh "$@" < /dev/null -""" - - -def now(): - return time.time() - - -def check(name, ok, detail=""): - RESULTS.append((name, bool(ok), detail)) - print(f" [{'PASS' if ok else 'FAIL'}] {name}" + (f" - {detail}" if detail else ""), flush=True) - return ok - - -def wait_until(cond, timeout, step=0.5): - start = now() - while now() - start < timeout: - try: - if cond(): - return now() - start - except Exception: - pass - time.sleep(step) - return None - - -def api(method, path, body=None, ctype="application/json", timeout=15): - headers = {"Content-Type": ctype} - if TOKEN: - headers["Authorization"] = "Bearer " + TOKEN - data = None if body is None else json.dumps(body).encode() - req = urllib.request.Request(BASE + path, data=data, method=method, headers=headers) - with OPENER.open(req, timeout=timeout) as resp: - text = resp.read().decode() - return json.loads(text) if text else None - - -def backend_up(): - try: - OPENER.open(BASE + "/healthz", timeout=2) - return True - except Exception: - return False - - -# ------------------------------------------------------------------ components - -def pids(name): - out = wsl(f"pgrep -x {name} || true") - return [int(x) for x in out.split()] - - -def rss_mb(name): - p = pids(name) - if not p: - return None - return int(wsl(f"ps -o rss= -p {p[0]}")) / 1024 - - -def start_backend(): - wsl(f"cd /root/mon && /root/mon/start.sh backend env ASPNETCORE_URLS=http://0.0.0.0:{PORT} MoShiro__DataDirectory=/root/mon/data " - f"MoShiro__InitialAdminUser=admin 'MoShiro__InitialAdminPassword={ADMIN_PASSWORD}' MoShiro__SecureCookies=false ./moshiro-backend") - return wait_until(backend_up, 30) - - -def start_agent(): - wsl("/root/mon/start.sh agent ./moshiro-agent --config /root/mon/agent.json") - return wait_until(lambda: bool(pids("moshiro-agent")), 10) - - -def stop_proc(name, sig="TERM", timeout=30): - """Send a signal and wait for the process to exit; returns (seconds, exit code or None).""" - start = now() - wsl(f"pkill -{sig} -x {name} || true") - gone = wait_until(lambda: not pids(name), timeout, 0.2) - code = wsl(f"cat /root/mon/{name.replace('moshiro-', '')}.exit 2>/dev/null || true") - return (now() - start if gone is not None else None), (int(code) if code.strip().lstrip('-').isdigit() else None) - - -def send_ctrl_break(pid): - """ - Ctrl+Break to a process that owns its own (hidden) console: attach to that console, broadcast the event, detach. - Ctrl+Break rather than Ctrl+C: a console process can inherit "ignore Ctrl+C" from its parent (it does under most job runners), - Ctrl+Break cannot be ignored that way. .NET reports it as SIGQUIT. - """ - from ctypes import wintypes - k = ctypes.WinDLL("kernel32", use_last_error=True) - handler = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD)(lambda event: True) # swallow the event in this process - k.FreeConsole() - if not k.AttachConsole(pid): - raise OSError(f"AttachConsole({pid}) failed: {ctypes.get_last_error()}") - k.SetConsoleCtrlHandler(handler, True) - k.GenerateConsoleCtrlEvent(1, 0) - time.sleep(0.3) - k.FreeConsole() - k.SetConsoleCtrlHandler(handler, False) - - -class Probe: - def __init__(self): - self.proc = None - self.log = os.path.join(WORK, "probe.log") - - def start(self): - os.makedirs(WORK, exist_ok=True) - handle = open(self.log, "a") - # 自己的隐藏控制台:这样才能从外面给它发 Ctrl+Break - flags = subprocess.CREATE_NO_WINDOW - self.proc = subprocess.Popen([PROBE_EXE, "--config", os.path.join(WORK, "probe", "probe.json")], stdout=handle, stderr=subprocess.STDOUT, creationflags=flags) - return self.proc.pid - - def alive(self): - return self.proc is not None and self.proc.poll() is None - - def stop(self, mode="graceful", timeout=20): - if not self.alive(): - return 0.0, self.proc.poll() if self.proc else None - start = now() - if mode == "graceful": - send_ctrl_break(self.proc.pid) - else: - self.proc.kill() - try: - code = self.proc.wait(timeout) - except subprocess.TimeoutExpired: - self.proc.kill() - return None, None - return now() - start, code - - def suspend(self): - """Freeze the process (its TCP connections stay open and go silent, like a hung machine).""" - handle = ctypes.windll.kernel32.OpenProcess(0x0800, False, self.proc.pid) # PROCESS_SUSPEND_RESUME - ctypes.windll.ntdll.NtSuspendProcess(handle) - ctypes.windll.kernel32.CloseHandle(handle) - - def resume(self): - handle = ctypes.windll.kernel32.OpenProcess(0x0800, False, self.proc.pid) - ctypes.windll.ntdll.NtResumeProcess(handle) - ctypes.windll.kernel32.CloseHandle(handle) - - def working_set_mb(self): - if not self.alive(): - return None - out = subprocess.run(["powershell", "-NoProfile", "-Command", f"(Get-Process -Id {self.proc.pid}).WorkingSet64"], capture_output=True, text=True, creationflags=HIDDEN).stdout.strip() - return int(out) / 1048576 if out else None - - def log_text(self): - return open(self.log, encoding="utf-8", errors="replace").read() if os.path.exists(self.log) else "" - - -PROBE = Probe() -AGENT_ID = "" - - -# ------------------------------------------------------------------ environment setup - -def setup(fresh=True): - """Clean state, copy the artifacts, start backend/agent, create the probe identity. The probe is not started.""" - global TOKEN, AGENT_ID - PROBE.stop("kill") - wsl("pkill -9 -x moshiro-agent; pkill -9 -x moshiro-backend; tc qdisc del dev eth0 ingress 2>/dev/null; tc qdisc del dev eth0 root 2>/dev/null; tc qdisc del dev lo root 2>/dev/null; tc qdisc del dev lo ingress 2>/dev/null; true") - if fresh: - wsl(f"rm -rf /root/mon && mkdir -p /root/mon && cp {PUB}/backend/linux-x64/* {PUB}/agent/linux-x64/* {PUB}/fleet-enroll/linux-x64/* /root/mon/ && chmod +x /root/mon/moshiro-* /root/mon/fleet-enroll") - wsl("cat > /root/mon/start.sh <<'EOF'\n" + LAUNCHER + "EOF\nchmod +x /root/mon/start.sh") - if os.path.isdir(WORK): - subprocess.run(["cmd", "/c", "rmdir", "/s", "/q", WORK], capture_output=True, creationflags=HIDDEN) - os.makedirs(os.path.join(WORK, "probe"), exist_ok=True) - assert start_backend() is not None, "backend did not start" - if fresh: - req = urllib.request.Request(BASE + "/api/admin/auth/login", data=json.dumps({"username": "admin", "password": ADMIN_PASSWORD}).encode(), headers={"Content-Type": "application/json"}, method="POST") - with OPENER.open(req, timeout=15) as resp: - cookie = resp.headers["Set-Cookie"].split(";")[0] - csrf = json.loads(resp.read())["csrf_token"] - req = urllib.request.Request(BASE + "/api/admin/api-tokens", data=json.dumps({"name": "stab", "scope": "admin"}).encode(), headers={"Content-Type": "application/json", "Cookie": cookie, "X-CSRF-Token": csrf}, method="POST") - TOKEN = json.loads(OPENER.open(req, timeout=15).read())["plaintext"] - join = api("POST", "/api/admin/join-tokens", {"auto_approve": True, "max_uses": 5, "expires_in_hours": 1})["plaintext"] - wsl(f"cd /root/mon && ./fleet-enroll --server http://127.0.0.1:{PORT} --join {join} --name wsl-agent --identity-dir /root/mon/id") - cfg = {"fleet": {"server": f"http://127.0.0.1:{PORT}", "identity_dir": "/root/mon/id"}, "listen": {"bind": "0.0.0.0"}} - wsl("cat > /root/mon/agent.json <<'EOF'\n" + json.dumps(cfg) + "\nEOF") - assert start_agent() is not None - assert wait_until(lambda: api("GET", "/api/admin/agents")["items"][0]["online"], 20) is not None, "agent did not come online" - agent = api("GET", "/api/admin/agents")["items"][0] - AGENT_ID = agent["id"] - if fresh: - api("PATCH", "/api/admin/agents/" + AGENT_ID, {"public_host": WSLIP}, "application/merge-patch+json") - ident = api("POST", "/api/admin/probes", {"name": "home"}) - json.dump(ident, open(os.path.join(WORK, "probe", "probe.json"), "w")) - PROBE.start() - assert wait_until(agent_view_probe_connected, 30) is not None, "probe did not connect" - - -def agent_view(): - return api("GET", "/api/admin/agents/" + AGENT_ID) - - -def agent_view_probe_connected(): - return agent_view()["probe_connected"] - - -def agent_online(): - return agent_view()["online"] - - -def metrics(rng="2m", names="loss.total,loss.up,loss.down,rtt.p50,tcp.rtt.p50,tcp.rtt.max"): - s = api("GET", f"/api/public/lines/{AGENT_ID}/series?range={rng}&step=1s&metrics={names}") - return s["summary"]["metrics"], s["summary"]["samples"] - - -def gaps(minutes=3): - """Buckets (10 s) in the last N minutes, after the first data, whose udp sample count is below half of the expected 100.""" - s = api("GET", f"/api/public/lines/{AGENT_ID}/series?range={minutes}m&step=10s&metrics=n.udp_echo,n.tcp_echo") - udp = s["values"]["n.udp_echo"] - first = next((i for i, v in enumerate(udp) if v is not None and v >= 50), len(udp)) # buckets before the first data are not gaps - return [i for i, v in enumerate(udp[:-1]) if i > first and (v is None or v < 50)], len(udp) - - -def stored_chunks(): - return len(api("GET", f"/api/admin/raw/chunks?agent_id={AGENT_ID}&limit=1000")["items"]) - - -def healthy(): - try: - return agent_online() and agent_view_probe_connected() - except Exception: - return False - - -def ensure_healthy(timeout=60): - if not backend_up(): - start_backend() - if not pids("moshiro-agent"): - start_agent() - if not PROBE.alive(): - PROBE.start() - return wait_until(healthy, timeout) - - -# ------------------------------------------------------------------ scenarios - -def scenario_init(): - print("== init: cold starts in every order, bad configuration") - # 1. everything down, then start in the opposite order: probe -> agent -> backend (each must retry until its peer is up) - PROBE.stop("kill") - wsl("pkill -9 -x moshiro-agent; pkill -9 -x moshiro-backend; true") - time.sleep(1) - PROBE.start() - time.sleep(3) - start_agent() - time.sleep(3) - t0 = now() - start_backend() - took = wait_until(healthy, 90) - check("probe -> agent -> backend start order converges", took is not None, f"{took:.1f}s after the backend came up" if took else "never became healthy") - chunks_flow = wait_until(lambda: stored_chunks() > 0, 40) - check("data flows after the cold start", chunks_flow is not None) - - # 2. backend first, then agent, then probe (the normal order) after a full stop - PROBE.stop("kill") - wsl("pkill -9 -x moshiro-agent; pkill -9 -x moshiro-backend; true") - start_backend() - start_agent() - PROBE.start() - took = wait_until(healthy, 60) - check("backend -> agent -> probe start order converges", took is not None, f"{took:.1f}s" if took else "") - - # 3. the probe starts when its agent list is only a stale cache (agents.json wrong): bootstrap is still tried - # 4. bad configuration: clear errors and a non-zero exit code, no crash dump - bad = os.path.join(WORK, "bad.json") - open(bad, "w").write("{ not json") - r = subprocess.run([PROBE_EXE, "--config", bad], capture_output=True, text=True, timeout=20, creationflags=HIDDEN) - check("probe: malformed config exits 1 with a readable error", r.returncode == 1 and "startup failed" in r.stderr + r.stdout, (r.stderr + r.stdout).strip()[:120]) - r = subprocess.run([PROBE_EXE, "--config", os.path.join(WORK, "missing.json")], capture_output=True, text=True, timeout=20, creationflags=HIDDEN) - check("probe: missing config exits 1 with the file name", r.returncode == 1 and "missing.json" in r.stderr + r.stdout, (r.stderr + r.stdout).strip()[:120]) - out = wsl("cd /root/mon && echo '{ not json' > bad.json && ./moshiro-agent --config bad.json 2>&1; echo exit=$?", check=False) - check("agent: malformed config exits 1", "exit=1" in out and "startup failed" in out, out.strip()[-120:]) - out = wsl("cd /root/mon && printf '{\"fleet\":{\"server\":\"http://127.0.0.1:1\",\"identity_dir\":\"/nonexistent\"}}' > noid.json && ./moshiro-agent --config noid.json 2>&1; echo exit=$?", check=False) - check("agent: no identity says to run fleet-enroll first", "exit=1" in out and "fleet-enroll" in out, out.strip()[-140:]) - out = wsl(f"cd /root/mon && ./fleet-enroll --server http://127.0.0.1:{PORT} --join bogus --name x --identity-dir /tmp/enroll-x 2>&1; echo exit=$?", check=False) - check("fleet-enroll: a bad join token fails clearly (non-zero exit)", "exit=0" not in out, out.strip()[-140:]) - out = wsl(f"cd /root/mon && ./fleet-enroll --server http://127.0.0.1:{PORT} --join bogus --name x --identity-dir /root/mon/id 2>&1; echo exit=$?", check=False) - check("fleet-enroll: refuses to overwrite an existing identity", "exit=0" not in out and "already enrolled" in out, out.strip()[-140:]) - ensure_healthy() - - -def scenario_graceful(): - print("== graceful: SIGTERM / Ctrl+Break of each component, then restart") - ensure_healthy() - # probe (Ctrl+Break in its own console) - secs, code = PROBE.stop("graceful") - check("probe exits on Ctrl+Break within 5 s with code 0", secs is not None and secs < 5 and code == 0, f"{secs and round(secs, 2)}s code={code}") - seen = wait_until(lambda: not agent_view_probe_connected(), 10) - check("backend sees the probe disconnect", seen is not None, f"{seen and round(seen, 1)}s") - PROBE.start() - check("probe restarts and reconnects", wait_until(healthy, 30) is not None) - - # agent - secs, code = stop_proc("moshiro-agent", "TERM") - log = wsl("tail -5 /root/mon/agent.log") - check("agent exits on SIGTERM within 5 s with code 0", secs is not None and secs < 5 and code == 0, f"{secs and round(secs, 2)}s code={code}") - check("agent logs a clean stop (no crash trace)", "stopping" in log and "Unhandled" not in log, log.splitlines()[-1][:100] if log else "") - seen = wait_until(lambda: not agent_online(), 10) - check("backend sees the agent go offline", seen is not None, f"{seen and round(seen, 1)}s") - start_agent() - check("agent restarts, reconnects to backend and probe", wait_until(healthy, 40) is not None) - - # backend - secs, code = stop_proc("moshiro-backend", "TERM", 30) - log = wsl("tail -5 /root/mon/backend.log") - check("backend exits on SIGTERM within 15 s with code 0", secs is not None and secs < 15 and code == 0, f"{secs and round(secs, 2)}s code={code}") - check("backend log has no unhandled exception", "Unhandled" not in log and "Exception" not in log, log.replace(chr(10), ' | ')[-160:]) - time.sleep(5) - start_backend() - check("backend restarts, agent and probe converge again", wait_until(healthy, 60) is not None) - ensure_healthy() - - -def scenario_crash(): - print("== crash: kill -9 / taskkill /F of each component, data durability") - ensure_healthy() - time.sleep(25) - before = stored_chunks() - - # backend crash: the agent keeps its outbox; after restart nothing acknowledged is lost - t = now() - wsl("pkill -9 -x moshiro-backend") - time.sleep(25) # agent outbox accumulates (2-3 chunk rounds) - start_backend() - back = wait_until(healthy, 60) - check("backend kill -9: agent reconnects and everything converges", back is not None, f"{back and round(back, 1)}s after restart") - time.sleep(20) - after = stored_chunks() - g, n = gaps(3) - check("backend kill -9: chunks buffered by the agent are delivered after the restart (no gap)", not g, f"gaps in {n} buckets: {g}; chunks {before}->{after}") - - # agent crash - wsl("pkill -9 -x moshiro-agent") - time.sleep(5) - start_agent() - check("agent kill -9: recovers", wait_until(healthy, 40) is not None) - - # probe crash - PROBE.stop("kill") - time.sleep(5) - PROBE.start() - check("probe kill: recovers", wait_until(healthy, 40) is not None) - time.sleep(25) - m, samples = metrics("30s") - check("after all three crashes the line reports correct data (no phantom loss)", (m.get("loss.up") or 0) < 1 and (m.get("loss.down") or 0) < 1, f"loss.up={m.get('loss.up')} loss.down={m.get('loss.down')}") - - # crash in the middle of an on-demand test - test = api("POST", f"/api/admin/lines/{AGENT_ID}/tests", {"plan": "hy2_ladder", "params": {"size": 1250}}) - time.sleep(12) - PROBE.stop("kill") - state = wait_until(lambda: api("GET", "/api/public/tests/" + test["id"])["status"] in ("failed", "aborted"), 30) - final = api("GET", "/api/public/tests/" + test["id"]) - check("probe crash mid-test: the test ends (failed/aborted), it does not hang", state is not None, f"status={final['status']} error={final.get('error_code')}") - PROBE.start() - wait_until(healthy, 40) - test2 = api("POST", f"/api/admin/lines/{AGENT_ID}/tests", {"plan": "hy2_ladder", "params": {"size": 1250}}) - done = wait_until(lambda: api("GET", "/api/public/tests/" + test2["id"])["status"] in ("succeeded", "failed", "aborted"), 90, 2) - check("a new test runs to completion after the crash (the agent is not stuck busy)", done is not None and api("GET", "/api/public/tests/" + test2["id"])["status"] == "succeeded", api("GET", "/api/public/tests/" + test2["id"])["status"]) - ensure_healthy() - - -def netem(device, spec, direction="egress", ports=(7443, 7444)): - """Impair only the data/control ports of the agent (so the rest of WSL is unaffected).""" - clear_netem(device) - if direction == "egress": - wsl(f"tc qdisc add dev {device} root handle 1: prio && tc qdisc add dev {device} parent 1:3 handle 30: netem {spec}") - for p in ports: - wsl(f"tc filter add dev {device} protocol ip parent 1:0 prio 3 u32 match ip sport {p} 0xffff flowid 1:3") - else: - wsl(f"tc qdisc add dev {device} handle ffff: ingress") - for p in ports: - action = "action drop" if spec == "drop" else "" - wsl(f"tc filter add dev {device} parent ffff: protocol ip u32 match ip dport {p} 0xffff {action}") - - -def clear_netem(device): - wsl(f"tc qdisc del dev {device} root 2>/dev/null; tc qdisc del dev {device} ingress 2>/dev/null; true") - - -def scenario_probe_link(): - print("== probe<->agent link: flapping, long blackhole (half-open), reconnect storms") - ensure_healthy() - time.sleep(20) - mem0 = (PROBE.working_set_mb(), rss_mb("moshiro-agent")) - - # 1. flapping: 3 s blackholes every 10 s, 8 times - for _ in range(8): - netem("eth0", "drop", "ingress") - time.sleep(3) - clear_netem("eth0") - time.sleep(7) - check("flapping link: probe still connected afterwards", wait_until(healthy, 30) is not None) - - # 2. a 70 s blackhole: both ends must notice or survive, and recover once the path returns - t = now() - netem("eth0", "drop", "ingress") - time.sleep(70) - clear_netem("eth0") - back = wait_until(healthy, 120) - check("70 s blackhole: converges back to healthy", back is not None, f"{back and round(back, 1)}s after the path returned") - time.sleep(35) - m, _ = metrics("30s") - check("after the blackhole the line is measured correctly again", (m.get("loss.total") or 0) < 2, f"loss.total={m.get('loss.total')}") - - # 3. reconnect storm: kill the agent 12 times in a row - for _ in range(12): - wsl("pkill -9 -x moshiro-agent; true") - time.sleep(0.5) - start_agent() - time.sleep(2) - check("12 agent restarts in a row: converges", wait_until(healthy, 60) is not None) - time.sleep(30) - g, n = gaps(1) - check("... and the data is flowing again", not g, f"gaps: {g}") - - # 4. restart storm of the probe - for _ in range(8): - PROBE.stop("kill") - time.sleep(0.3) - PROBE.start() - time.sleep(2) - check("8 probe restarts in a row: converges", wait_until(healthy, 60) is not None) - time.sleep(40) - mem1 = (PROBE.working_set_mb(), rss_mb("moshiro-agent")) - check("memory stays bounded after the storms", mem1[0] < mem0[0] + 15 and mem1[1] < mem0[1] + 15, f"probe {mem0[0]:.1f}->{mem1[0]:.1f} MB, agent {mem0[1]:.1f}->{mem1[1]:.1f} MB") - ensure_healthy() - - -def scenario_backend_link(): - print("== agent<->backend link: outage buffering, replay, duplicates") - ensure_healthy() - time.sleep(30) - # an outage on loopback (tc on lo: the agent<->backend link) shorter than the buffering capacity - netem("lo", "drop", "ingress", ports=(PORT,)) - t = now() - time.sleep(45) - clear_netem("lo") - check("45 s agent<->backend outage: agent stays up and reconnects", wait_until(agent_online, 90) is not None, f"{now() - t:.0f}s") - time.sleep(35) - g, n = gaps(4) - check("data produced during the outage is replayed from the agent's outbox (no gap)", not g, f"gaps in {n} buckets: {g}") - - # duplicates: chunk ids are idempotent. The count of stored chunks grows by the expected 2 per 10 s, not more - c0 = stored_chunks() - time.sleep(30) - c1 = stored_chunks() - check("steady state: ~2 chunks / 10 s (udp + tcp), no duplicates", 4 <= c1 - c0 <= 8, f"{c1 - c0} chunks in 30 s") - - # the backend is stopped for a while (graceful): the agent keeps collecting, delivers after - stop_proc("moshiro-backend", "TERM", 30) - time.sleep(40) - start_backend() - check("backend down 40 s then up: everything converges", wait_until(healthy, 90) is not None) - time.sleep(35) - g, n = gaps(4) - check("... with no gap in the data", not g, f"gaps: {g}") - ensure_healthy() - - -def scenario_silent(): - print("== silent death: a frozen process keeps its sockets open and says nothing (hung machine, lost power)") - ensure_healthy() - time.sleep(10) - - # a frozen probe: the agent must notice and tell the backend, instead of keeping a dead session forever - PROBE.suspend() - t = now() - seen = wait_until(lambda: not agent_view_probe_connected(), 150, 1) - check("frozen probe: the backend learns the probe is gone", seen is not None, f"{seen and round(seen)}s" if seen else "still shown as connected after 150 s") - PROBE.resume() - check("... and when the probe thaws it comes back", wait_until(healthy, 90) is not None) - - # a frozen agent: the probe must drop the session and keep retrying; the backend must mark the agent offline - wsl("pkill -STOP -x moshiro-agent") - seen_off = wait_until(lambda: not agent_online(), 150, 1) - check("frozen agent: the backend marks it offline", seen_off is not None, f"{seen_off and round(seen_off)}s" if seen_off else "still online after 150 s") - wsl("pkill -CONT -x moshiro-agent") - check("... and when the agent thaws everything converges", wait_until(healthy, 90) is not None) - - # a frozen backend: the agent must notice (WebSocket ping timeout) and reconnect once the backend thaws - wsl("pkill -STOP -x moshiro-backend") - time.sleep(70) - wsl("pkill -CONT -x moshiro-backend") - check("frozen backend (70 s): converges after it thaws", wait_until(healthy, 90) is not None) - time.sleep(30) - g, n = gaps(3) - check("... and the data produced while it was frozen is delivered", not g, f"gaps: {g}") - ensure_healthy() - - -def scenario_soak(): - print("== soak: 8 minutes with random faults; memory, convergence and data continuity") - ensure_healthy() - time.sleep(20) - mem0 = (PROBE.working_set_mb(), rss_mb("moshiro-agent"), rss_mb("moshiro-backend")) - import random - rnd = random.Random(7) - end = now() + 8 * 60 - faults = 0 - while now() < end: - fault = rnd.choice(["blackhole", "loss", "agent_kill", "probe_kill", "none", "none"]) - faults += fault != "none" - if fault == "blackhole": - netem("eth0", "drop", "ingress") - time.sleep(rnd.uniform(1, 6)) - clear_netem("eth0") - elif fault == "loss": - netem("eth0", "delay 20ms 5ms loss 5%", "egress") - time.sleep(rnd.uniform(5, 12)) - clear_netem("eth0") - elif fault == "agent_kill": - wsl("pkill -9 -x moshiro-agent; true") - time.sleep(1) - start_agent() - elif fault == "probe_kill": - PROBE.stop("kill") - time.sleep(0.5) - PROBE.start() - time.sleep(rnd.uniform(8, 20)) - clear_netem("eth0") - check(f"after {faults} random faults everything converges", ensure_healthy(90) is not None) - time.sleep(45) - mem1 = (PROBE.working_set_mb(), rss_mb("moshiro-agent"), rss_mb("moshiro-backend")) - check("memory bounded", mem1[0] < mem0[0] + 15 and mem1[1] < mem0[1] + 20 and mem1[2] < mem0[2] + 40, f"probe {mem0[0]:.0f}->{mem1[0]:.0f}, agent {mem0[1]:.0f}->{mem1[1]:.0f}, backend {mem0[2]:.0f}->{mem1[2]:.0f} MB") - m, _ = metrics("30s") - check("the final minute is measured correctly", (m.get("loss.up") or 0) < 1 and (m.get("loss.down") or 0) < 1, f"{m.get('loss.up')} {m.get('loss.down')}") - - -SCENARIOS = { - "init": scenario_init, - "graceful": scenario_graceful, - "crash": scenario_crash, - "probe_link": scenario_probe_link, - "backend_link": scenario_backend_link, - "silent": scenario_silent, - "soak": scenario_soak, -} - - -def main(): - names = sys.argv[1:] or list(SCENARIOS) - print(f"wsl ip {WSLIP}") - setup(fresh=True) - for name in names: - try: - SCENARIOS[name]() - except Exception as ex: - check(f"{name} ran to completion", False, repr(ex)[:300]) - PROBE.stop("kill") - wsl("pkill -9 -x moshiro-agent; pkill -9 -x moshiro-backend; tc qdisc del dev eth0 root 2>/dev/null; tc qdisc del dev eth0 ingress 2>/dev/null; tc qdisc del dev lo ingress 2>/dev/null; true") - failed = [r for r in RESULTS if not r[1]] - print(f"\n{len(RESULTS) - len(failed)}/{len(RESULTS)} checks passed") - for name, _, detail in failed: - print(f"FAILED: {name} - {detail}") - sys.exit(1 if failed else 0) - - -if __name__ == "__main__": - main() diff --git a/scripts/test-openwrt-updater.py b/scripts/test-openwrt-updater.py index 5cc1284..65641c1 100644 --- a/scripts/test-openwrt-updater.py +++ b/scripts/test-openwrt-updater.py @@ -44,11 +44,11 @@ elif name == 'jsonfilter': args = sys.argv[1:] data = json.loads(pathlib.Path(args[args.index('-i') + 1]).read_text() if '-i' in args else sys.stdin.read()) query = args[args.index('-e') + 1] - value = data['moshiro-probe']['instances']['default']['pid'] if query.startswith('@[') else data.get(query[2:]) + value = data['moshiro-agent']['instances']['default']['pid'] if query.startswith('@[') else data.get(query[2:]) if value is None: sys.exit(1) print(value) elif name == 'ubus': - print(json.dumps({'moshiro-probe': {'instances': {'default': {'pid': 1234}}}})) + print(json.dumps({'moshiro-agent': {'instances': {'default': {'pid': 1234}}}})) elif name == 'service': with (base / 'restarts').open('a') as f: f.write('restart\n') if os.environ.get('HEALTH') != 'good': sys.exit(0) @@ -93,7 +93,7 @@ class UpdaterTests(unittest.TestCase): def publish(self, *, seq=100, version="1.0.1-dev.1", members=None, changes=None): payload = io.BytesIO() - members = members or {name: b"executable\n" for name in ("moshiro-probe", "fleet-enroll", "openwrt.sh", "probe.init", "updater.init")} + members = members or {name: b"executable\n" for name in ("moshiro-agent", "fleet-enroll", "openwrt.sh", "agent.init", "updater.init")} with tarfile.open(fileobj=payload, mode="w:gz") as archive: for name, content in members.items(): entry = tarfile.TarInfo(name) @@ -101,11 +101,11 @@ class UpdaterTests(unittest.TestCase): archive.addfile(entry, io.BytesIO(content)) data = payload.getvalue() self.hash = hashlib.sha256(data).hexdigest() - package = f"probe-linux-musl-arm64-{self.hash}.tar.gz" + package = f"agent-linux-musl-arm64-{self.hash}.tar.gz" version_dir = self.base / "feed" / version version_dir.mkdir(exist_ok=True) (version_dir / package).write_bytes(data) - self.manifest = dict(schema=1, component="probe", channel="dev", rid="linux-musl-arm64", version=version, + self.manifest = dict(schema=1, component="agent", channel="dev", rid="linux-musl-arm64", version=version, package=package, sha256=self.hash, size=len(data), seq=seq) self.manifest.update(changes or {}) self.sign() @@ -144,7 +144,7 @@ class UpdaterTests(unittest.TestCase): def test_ci_publisher_produces_a_signed_package_the_updater_can_install(self): binaries = self.base / "binaries" binaries.mkdir() - for name in ("moshiro-probe", "fleet-enroll"): + for name in ("moshiro-agent", "fleet-enroll"): (binaries / name).write_bytes(b"native executable fixture\n") env = dict(self.env, CI_COMMIT_TAG="", CI_PIPELINE_EVENT="push", CI_COMMIT_BRANCH="main", CI_COMMIT_MESSAGE="[CI] fixture", GITEA_BASE="https://test.invalid", GITEA_TOKEN="test-token", RELEASE_SIGNING_KEY=self.key.read_text())