PPysolate SpineREAD THE CODE · UNDERSTAND THE SYSTEM
课程与章节
CODE READING / 31

隔离子进程网络

tools/offline-check.sh

本章目录
配套饭点图解 · 1 节(按需展开)

先回答一个问题:没有网络时,怎样区分“命令没有主动联网”和“命令其实仍能打开外部 socket”?tools/offline-check.sh 把两件事分开验证:先确认本机已有运行所需的工具和 artifact,再在 Linux/Ubuntu 的 user/network namespace 子进程中做 socket 探针,并在同一隔离域运行检查、站点构建和 demo。

make offline
  → 预检 Go / Node / pnpm / Wasm / Astro
  → 设置 Go 与前端离线变量
  → unshare --user --map-root-user --net
       → bash 子进程
          → socket 探针:1.1.1.1:443 应为 ENETUNREACH
          → make check
          → pnpm build
          → PLM / prepared-copy / Linux prepared-COW demo

隔离对象是这个脚本启动的子进程及其后代,不是用户整个终端、SSH 会话或其他程序。脚本外的网络不会因为一次 make offline 自动断开。

1. 参数和平台选择

tools/offline-check.sh · L1–L61 · 打开完整源码

#!/usr/bin/env bash
# Restriction applies only to this check's children, not the user's network.
set -euo pipefail
cd -- "$(dirname -- "$0")/.."
case "${1-}" in
  "") export SPINE_PROBE_ONLY=false ;;
  --probe-only) export SPINE_PROBE_ONLY=true ;;
  *) printf 'Usage: %s [--probe-only]\n' "$0" >&2; exit 2 ;;
esac
case "$(uname -s)" in
  Linux)
    command -v unshare >/dev/null || { printf 'Missing unshare. Linux needs user/network namespaces.\n' >&2; exit 1; }
    isolation=(unshare --user --map-root-user --net)
    ;;
  Darwin)
    command -v sandbox-exec >/dev/null || { printf 'Missing sandbox-exec.\n' >&2; exit 1; }
    isolation=(sandbox-exec -p '(version 1) (allow default) (deny network*)')
    ;;
  *) printf 'Unsupported platform: %s\n' "$(uname -s)" >&2; exit 1 ;;
esac
command -v python3 >/dev/null || { printf 'Missing python3.\n' >&2; exit 1; }
if [[ "$SPINE_PROBE_ONLY" == false ]]; then
  for tool in go node pnpm; do
    command -v "$tool" >/dev/null || { printf 'Missing %s; prepare tools while online.\n' "$tool" >&2; exit 1; }
  done
  [[ -s dist/spine.wasm ]] || { printf 'Missing dist/spine.wasm; run make guest first.\n' >&2; exit 1; }
  [[ -x site/node_modules/.bin/astro ]] || { printf 'Missing site dependencies; run cd site && pnpm install --frozen-lockfile while online.\n' >&2; exit 1; }
fi
export GOPROXY=off GOSUMDB=off GOTOOLCHAIN=local
export npm_config_offline=true ASTRO_TELEMETRY_DISABLED=1
"${isolation[@]}" bash -eu -s <<'OFFLINE'
python3 - <<'PY'
import errno
import platform
import socket

expected = errno.ENETUNREACH if platform.system() == 'Linux' else errno.EPERM
with socket.socket() as sock:
    sock.settimeout(3)
    try:
        sock.connect(('1.1.1.1', 443))
    except OSError as error:
        if error.errno != expected:
            raise SystemExit(f'Unexpected network failure: {error}; expected errno {expected}')
        print(f'Verified: child network is unreachable (errno {error.errno}: {error.strerror})')
    else:
        raise SystemExit('Network unexpectedly available')
PY
if [[ "$SPINE_PROBE_ONLY" == true ]]; then
  printf 'Offline isolation probe passed.\n'
  exit 0
fi
make check
(cd site && pnpm build)
go run ./cmd/demo -source examples/order.py -plm
go run ./cmd/demo -source examples/order.py -prefix -prepared-copy
if [[ "$(uname -s)" == Linux ]]; then
  go run ./cmd/demo -source examples/order.py -prefix -prepared-cow
fi
printf 'Offline check passed with child networking denied.\n'
OFFLINE

脚本先启用 Bash 严格模式,并把当前目录切到仓库根目录。${1-} 在 set -u 下安全读取可能不存在的第一个参数;空参数表示完整检查,--probe-only 表示只验证隔离和网络探针,其他参数以退出码 2 报调用错误。

isolation 是 Bash 数组,保存稍后要执行的隔离命令。Linux 分支要求 unshare 存在,然后使用 --user --map-root-user --net:user namespace 给子进程一套独立的用户映射,--map-root-user 不等于获得宿主机 root;network namespace 给子进程独立的网络设备、路由和连接空间。当前项目面向 Linux x86_64 的 Guest 构建,Ubuntu 上的离线验收走的就是这条 namespace 路径。

Darwin 分支改用 sandbox-exec 和拒绝网络操作的 profile,其他系统直接失败。无论平台,脚本都要求 python3,因为网络探针由 Python socket 实现。平台工具的存在检查只确认命令在 PATH 中,不把“命令可找到”扩大成“完整工具链已验证”。

2. 预检和离线变量

完整模式才检查 go、node、pnpm、非空的 dist/spine.wasm 以及 site/node_modules/.bin/astro。这组检查的含义是“后面的消费方有机会启动”:Go module cache 是否完整、站点所有依赖是否可用、Wasm 是否能运行,仍要由后续命令验证。

预检提示用户在线准备依赖,但脚本不会在这里执行安装或下载。它也不会调用 make setup-build 或 make guest。因此完整模式要求以下输入已经存在:Go module cache、Node/pnpm 依赖、站点源码所需的构建依赖、dist/spine.wasm,以及 Guest 重建所需的 build/inputs(后者由被调用的运行路径按需消费)。缺少输入应当导致失败,而不是暂时解除隔离。

外层为子进程设置两组环境变量:

  • GOPROXY=off、GOSUMDB=off、GOTOOLCHAIN=local 约束 Go,不访问模块代理、外部 checksum database,也不自动切换远程 toolchain;
  • npm_config_offline=true、ASTRO_TELEMETRY_DISABLED=1 约束 npm/Astro 的离线和 telemetry 行为。

这些变量是工具层约束。它们不能替代操作系统的网络隔离,所以脚本仍必须通过 ${isolation[@]} 启动子 shell。

3. socket 探针怎样证明子进程被阻断

隔离子 shell 从标准输入读取 here-document 中的脚本。外层用单引号保护 body,避免在启动隔离环境前展开变量;子 shell 自己执行探针和后续命令。探针用 Python 创建 socket,设置 3 秒超时,连接固定 IP 1.1.1.1 的 443 端口。直接使用 IP 是为了避免把 DNS 失败误判成网络隔离。

Linux 预期 errno 是 ENETUNREACH(网络不可达),Darwin 预期是 EPERM(操作不允许)。探针只捕获 OSError,并严格比较 errno:超时、其他错误码或连接成功都算失败。连接成功会报告网络意外可用;收到预期错误才打印验证通过。这个探针证明的是该隔离子进程访问一个固定外部地址的结果,不是覆盖所有潜在通信通道的完整安全审计。

--probe-only 在探针通过后立即退出 0,不要求 Go、Node、pnpm,也不运行测试或站点构建。完整模式则继续在同一隔离子 shell 中执行后续命令,保证被检查的命令继承相同的网络边界。

4. 同一隔离域里检查什么

完整路径先执行 make check,再在子 shell 中进入 site 执行 pnpm build,然后运行 PLM demo 和 prefix 加 prepared full-copy demo。Linux 额外运行 prepared-COW demo;Darwin 跳过它,因为当前 COW 真实路径是 Linux 专用。所有命令都受 bash -eu 和外层严格模式约束,任意一个返回非零都会停止脚本,最终通过消息不会打印。

这条顺序体现产物依赖:Go/Guest 检查消费现有 dist/spine.wasm,站点构建消费已安装的 Node 依赖,demo 消费 Go module cache 和 Guest artifact。离线脚本验收的是“已有输入能否在禁网条件下闭合”,不是“禁网环境能否从零准备输入”。

5. 失败结果怎样解释

  • 参数错误退出 2;系统不支持、缺 unshare/sandbox-exec/python3 或完整模式预检缺项,退出 1。
  • 探针得到超时、错误 errno 或成功连接,都不能算离线通过。
  • make check、pnpm build 或任一 demo 失败时,脚本停止,不打印最终通过消息。
  • 只有 --probe-only 通过,只能证明这一轮固定地址探针通过;它不证明 Go、站点或 Guest 检查通过。

所以 GOPROXY=off 失败通常说明 Go cache 或 toolchain 输入不完整;namespace 探针失败说明子进程的网络边界没有达到脚本要求;后续构建失败则说明已有输入与源码组合仍不能完成该阶段。不要把这些原因合并成一个笼统的“离线失败”。

6. 读完后应能回答

  1. 为什么 GOPROXY=off 之后还必须做 socket 探针?
  2. 为什么 Ubuntu 的 unshare --net 只影响检查子进程,不会断开脚本外的 SSH?
  3. 为什么 probe-only 通过不等于完整离线验收通过?

下一步读本地阅读服务:它不参与 namespace 验收,只提供已经生成的 site/dist。本章覆盖 tools/offline-check.sh 的全部非空源码行,没有执行 namespace、网络探针、测试、pnpm 构建或 demo;当前机器的实际隔离权限和缓存状态仍未现场验证。