diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1f4e26f..99d29009 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,22 +37,36 @@ jobs: node-version: 24 cache: pnpm - # Ubuntu 24.04 的 AppArmor 不许普通进程建 user namespace,云电脑的 bash 沙箱(bwrap)和助理 Chrome 都靠它。 - # 按 Chromium 的 apparmor-userns-restrictions 文档只给这两个程序放开,不整机关 apparmor_restrict_unprivileged_userns。 - # AppArmor 按真实路径匹配,/usr/bin/chromium 是软链,先解析。末尾真起一次,没放开就停在这一步。 + # Ubuntu 23.10 起 AppArmor 默认不许普通进程建 user namespace(kernel.apparmor_restrict_unprivileged_userns=1), + # 云电脑的 bash 沙箱(bwrap)和助理 Chrome 都靠它。按 Chromium 的 apparmor-userns-restrictions 文档只给这两个程序 + # 放开,不整机关这个 sysctl。Ubuntu 26.04 的 apparmor 包自带 bwrap-userns-restrict,已经放行 /usr/bin/bwrap; + # 同一路径再挂一份 profile 两份冲突,exec 时一份都不用,bwrap 建的 user namespace 拿不到 capability, + # 配回环口报 RTM_NEWADDR 失败(#135)。所以先照原样起一次,起不来才补 profile,补完再起一次,还不通就停在这一步。 + # bwrap 用运行器探测沙箱的参数(bot-runner/src/sandbox.ts 的 BWRAP_PROBE_ARGS),探不通 Linux 上的沙箱用例会全跳过。 + # AppArmor 按真实路径匹配,/usr/bin/chromium 是软链,先解析。 # 新 runner 上第一次起 Chrome 要 9 到 45 秒,之后一秒左右,这一次也替后面的用例预热,上限放宽到 120 秒。 - name: 给 bwrap 和 Chrome 放开 user namespace run: | sudo apt-get update -q sudo apt-get install -y -q bubblewrap - for entry in "baize-bwrap $(readlink -f /usr/bin/bwrap)" "baize-chrome $(readlink -f /usr/bin/chromium)"; do - set -- $entry - printf 'abi ,\ninclude \nprofile %s %s flags=(unconfined) {\n userns,\n}\n' "$1" "$2" | - sudo tee "/etc/apparmor.d/$1" >/dev/null - sudo apparmor_parser -r "/etc/apparmor.d/$1" - done - bwrap --ro-bind / / --dev /dev --unshare-all true - timeout 120 /usr/bin/chromium --headless=new --no-first-run --user-data-dir="$(mktemp -d)" --dump-dom about:blank >/dev/null + allow_userns() { + name=$1 + binary=$(readlink -f "$2") + shift 2 + if "$@" >/dev/null 2>&1; then + echo "$binary 不加 profile 就能建 user namespace" + return + fi + printf 'abi ,\ninclude \nprofile %s %s flags=(unconfined) {\n userns,\n}\n' "$name" "$binary" | + sudo tee "/etc/apparmor.d/$name" >/dev/null + sudo apparmor_parser -r "/etc/apparmor.d/$name" + "$@" >/dev/null + echo "$binary 补了 profile $name" + } + allow_userns baize-bwrap /usr/bin/bwrap \ + bwrap --ro-bind / / --unshare-user --unshare-pid --unshare-ipc --unshare-uts --unshare-net true + allow_userns baize-chrome /usr/bin/chromium \ + timeout 120 /usr/bin/chromium --headless=new --no-first-run --user-data-dir="$(mktemp -d)" --dump-dom about:blank # Linux 上助理 Chrome 按远程电脑的方式起,要一块 Xvfb 屏(computer-agent/src/chrome.ts 认 DISPLAY)。 - name: 起 Xvfb diff --git a/deploy/README.md b/deploy/README.md index 2d030477..66416c1c 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -39,21 +39,46 @@ docker run --rm --entrypoint node baize-remote-computer:remote --version ## AppArmor 与 bwrap 探测 -远程电脑上 Bot 的 bash 是否进沙箱,由运行器在进程里探一次决定(`packages/bot-runner/src/sandbox.ts:110-124`): +远程电脑上 Bot 的 bash 是否进沙箱,由运行器在进程里探一次决定(`packages/bot-runner/src/sandbox.ts` 的 `detectSandbox`): -- Linux 上跑 `bwrap --ro-bind / / --unshare-net --unshare-pid true`,退出码 0 就用 bwrap 沙箱。 -- 跑不通就退成 `none`,日志里留一行「bubblewrap 起不来(原因),这台电脑没有 bash 沙箱,Bot 的每条 bash 都要问」。退成 `none` 后每条 bash 都弹审批(`packages/bot-runner/README.md:74`),只读例行里 bash 一律拒绝(`packages/bot-runner/README.md:143`)。服务不会因此起不来,所以探测失败不显眼,要主动查。 +- Linux 上跑 `bwrap --ro-bind / / --unshare-user --unshare-pid --unshare-ipc --unshare-uts --unshare-net true`(同文件的 `BWRAP_PROBE_ARGS`),退出码 0 就用 bwrap 沙箱。 +- 跑不通就退成 `none`,日志里留一行「bubblewrap 起不来(原因),这台电脑没有 bash 沙箱,Bot 的每条 bash 都要问」。退成 `none` 后每条 bash 都弹审批,批了也不进沙箱直接跑;只读例行里 bash 一律拒绝(`packages/bot-runner/src/gate.ts` 的 `NO_SANDBOX_REASON`);云电脑的网页终端不开(`packages/computer-agent/src/agent.ts` 里终端的 `available`)。服务不会因此起不来,所以探测失败不显眼,要主动查。 -部署后按下面的顺序查(未在真机验证)。命令和运行器的探测参数一字不差,和 `remote/README.md:109-113` 那条相同: +宿主机要做什么取决于它的 AppArmor 有没有自带 bwrap 的 profile。下面的结论是在 GitHub Actions 的 `ubuntu-24.04`(apparmor 4.0.1)和 `ubuntu-latest`(26.04,apparmor 5.0.2)runner 上实测的,容器用 `debian:bookworm-slim` 装 bubblewrap,带 compose 里同样的 `seccomp=./seccomp-bwrap.json` 和 `apparmor=unconfined`,以 uid 1000 跑探测(#135): + +| 宿主机 | 什么都不做 | 宿主机加下面那份 profile | +| --- | --- | --- | +| Ubuntu 24.04 | 不通,`bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted` | 通 | +| Ubuntu 26.04 | 通,apparmor 包自带的 `/etc/apparmor.d/bwrap-userns-restrict` 已经放行 `/usr/bin/bwrap` | 不通,报错同左 | + +- 宿主机上的 AppArmor profile 按路径挂,容器里的 `/usr/bin/bwrap` 也挂得上。 +- 自带 `bwrap-userns-restrict` 的宿主机上不要再给 `/usr/bin/bwrap` 加 profile。两份 profile 挂同一个路径时内核记一条 `conflicting profile attachments`,一份都不用,bwrap 落回 `unprivileged_userns`,在新 user namespace 里拿不到配回环口要的 capability。自带的那份只把 capability 留给 bwrap 本身,沙箱里的命令一个 capability 都没有,比下面那份更紧。 +- 看宿主机有没有 `/etc/apparmor.d/bwrap-userns-restrict`(`ls` 一下),不看版本号。没有的(24.04),给 bwrap 单独放开 user namespace,不整机关 `kernel.apparmor_restrict_unprivileged_userns`: + +``` +sudo tee /etc/apparmor.d/baize-bwrap >/dev/null <<'PROFILE' +abi , +include +profile baize-bwrap /usr/bin/bwrap flags=(unconfined) { + userns, +} +PROFILE +sudo apparmor_parser -r /etc/apparmor.d/baize-bwrap +``` + +宿主机升级到带 `bwrap-userns-restrict` 的版本(26.04 实测带,25.04 没实测)时,先删掉这份(`sudo apparmor_parser -R /etc/apparmor.d/baize-bwrap && sudo rm /etc/apparmor.d/baize-bwrap`),不然和自带的冲突。 + +部署后按下面的顺序查(宿主机部分未在真机验证)。命令和运行器的探测参数一字不差: ``` cd deploy/remote sysctl kernel.apparmor_restrict_unprivileged_userns # 宿主机上看,Ubuntu 23.10 以后默认是 1 -docker compose exec remote-computer bwrap --ro-bind / / --unshare-net --unshare-pid true && echo 能用 +docker compose exec remote-computer bwrap --ro-bind / / --unshare-user --unshare-pid --unshare-ipc --unshare-uts --unshare-net true && echo 能用 docker compose logs remote-computer | grep 'bubblewrap 起不来' # 应当没有输出 +sudo dmesg | grep -i 'apparmor.*bwrap' # 不通时看是不是 conflicting profile attachments 或 unprivileged_userns 的 DENIED ``` -不通的两条出路(宿主机 sysctl 设 0,或给 bwrap 配宿主机 AppArmor profile)见 `remote/README.md:117`。 +探测是进程启动时做的,改完宿主机的 AppArmor 要 `docker compose restart remote-computer` 让运行器重探,重启后要重跑 `./put-launch.sh`(`remote/README.md` 的「容器重建或重启后要重新放一次」)。 `apparmor=unconfined`(`remote/docker-compose.yml:87`)是按 `docker-default` profile 的内容推断加上的,开发时用的 OrbStack 没有 AppArmor,没实测过它是否必要(`remote/README.md:107`)。在带 AppArmor 的 Ubuntu 上可以这样判断它能不能去(未在真机验证): diff --git a/deploy/remote/README.md b/deploy/remote/README.md index 5069d87f..0ee1d587 100644 --- a/deploy/remote/README.md +++ b/deploy/remote/README.md @@ -117,12 +117,12 @@ docker compose logs remote-computer | tail Ubuntu 23.10 以后的内核默认开着 `kernel.apparmor_restrict_unprivileged_userns=1`,它在宿主机层面限制非特权 user namespace,容器的 `apparmor=unconfined` 管不到它。部署前在服务器上跑一遍,参数和运行器启动时的探测(`packages/bot-runner/src/sandbox.ts` 的 `detectSandbox`)一字不差: ``` -docker compose exec remote-computer bwrap --ro-bind / / --unshare-net --unshare-pid true && echo 能用 +docker compose exec remote-computer bwrap --ro-bind / / --unshare-user --unshare-pid --unshare-ipc --unshare-uts --unshare-net true && echo 能用 ``` 探测不通时运行器不会起不来,而是退成没有沙箱、每条 bash 都问,日志里留一行「bubblewrap 起不来」。 -不通的话,要么在宿主机上把那个 sysctl 设成 0,要么给 bwrap 单独配一份宿主机的 AppArmor profile。这件事只在 OrbStack(没有 AppArmor)上验过,没在 Ubuntu 服务器上验过。 +宿主机上有 `/etc/apparmor.d/bwrap-userns-restrict`(26.04 自带)就不用设置,也不能再给 bwrap 另加一份 profile,两份冲突反而不通;没有的(24.04)要给 bwrap 单独配一份宿主机的 AppArmor profile。做法和在 GitHub Actions runner 上实测的结果见 [`../README.md`](../README.md) 的「AppArmor 与 bwrap 探测」。 ## 网络