Skip to Content
SpikesS13 · microsandbox on WSL

Spike S13 · Microsandbox 在 WSL2 上跑通(私有化 Plan B 候选)

状态:✅ 通过(2026-05-20)— msb run alpine 端到端 530ms 冷起(含 OCI image cold load + μVM 启动 + exec + stdout 回收 + tear-down),决策依据 = 推进 OpenSpec change add-microsandbox-private-fallback

环境

  • hostdouglas-wsl(Windows 11 · WSL 2 · kernel 6.6.114-microsoft-standard-WSL2 · Ubuntu 24.04.3 LTS)
  • 资源:Intel x86_64 · 48 vCPU · 31 GiB RAM · 939 GiB free(同 s12 host)
  • CPU virt flagsvmx × 48
  • nested-virt:intel = Y
  • /dev/kvmcrw-rw---- root:kvm 已就位 · kvm_intel 模块已加载
  • Microsandbox:v0.4.6(msb-linux-x86_64 24.5 MB binary + libkrunfw.so.5.2.1 21.4 MB)
  • Tailscale:mirror mode(Windows 端 daemon · WSL eth1 共享 Tailscale tailnet IP 100.116.83.96)
  • Node:v22.22.0(≥ 22 是 microsandbox npm SDK 最低要求)

为什么记录这份 spike

add-microsandbox-private-fallback change 决策前,需要把”上游真能跑”+“WSL2 部署有几个坎”这两点先实测确认,避免重蹈 §11.6 spike s12(Cube 因 upstream #159 阻塞)的 ROI 陷阱。

实测踩了 3 个 WSL2 quirk(与 s12 部分重叠,但本 spike 是 Microsandbox 视角的复现),全部固化到 design D5 + spec ADDED Requirement “WSL2 quirk 合规约束”。

端到端 spike 结果

$ sg kvm -c "~/microsandbox-spike/msb --warn run \ 10.255.255.254:5000/alpine:3.21 -- echo HelloFromMicroVM" HelloFromMicroVM real 0m0.530s # 总耗时 530ms

包含:

  1. msb 子进程 fork + libkrun init
  2. OCI image manifest + 3.5 MiB layer 从本机 local registry 拉
  3. erofs rootfs 构建 + μVM cold boot(KVM hardware accel)
  4. agentd PID 1 init(mount fs + run.exec)
  5. echo 执行 + stdout 经 agent socket 回传
  6. sandbox tear-down + 资源回收

健康度对照(vs Cube · s12):

维度Cube v0.2.2Microsandbox v0.4.6
GitHub Stars~50 (2026-04-20 开源)6162
Release 节奏4 个 release / 30 天,含 plugin bug regression4-5 个 release / 周 · 当日 PR 即合
开源 licenseApache-2.0Apache-2.0
维护方Tencent · 商业开源早期Zerocore AI · YC backed
Upstream open issues#159(plugin loading)跨环境阻塞 OPEN59 active,maintainer 当日处理
我们的 spike 端到端△ 协议层 OK · sandbox 真起被 upstream bug 阻塞✅ 真起

3 个 WSL2 quirk 完整复盘

Quirk 1 · WSL2 mirror mode loopback 必须用 10.255.255.254

WSL2 在 mirrored networking mode 下,127.0.0.1 映射到 Windows host loopback,但 host loopback 进程(包括 dockerd container port mapping)不通过 127.0.0.1 转发回 WSL 内部进程

实测:

# 在 douglas-wsl 上 docker run -p 5000:5000 registry:2 之后 $ curl --max-time 5 http://127.0.0.1:5000/v2/ HTTP 000 t=5.062s (timeout) $ curl --max-time 5 http://192.168.10.202:5000/v2/ HTTP 000 t=5.062s (timeout) $ curl --max-time 5 http://10.255.255.254:5000/v2/ HTTP 200 t=0.001s $ curl --max-time 5 http://172.17.0.1:5000/v2/ HTTP 200 t=0.002s (docker0 bridge IP 也可)

含义

  • 所有本机 client → 本机 docker container port 的通信必须用 10.255.255.254(WSL global lo IP,在 /etc/resolv.conf nameserver 同 IP)或 172.17.0.1(docker0 bridge)
  • 不能用 127.0.0.1localhost、LAN IP 192.168.10.202

对 Microsandbox 部署的影响

# ✗ msb pull --insecure localhost:5000/... (resolver 走 127.0.0.1 → fail) # ✗ msb pull --insecure 127.0.0.1:5000/... (5s timeout) # ✓ msb pull --insecure 10.255.255.254:5000/... (instant)

s12 spike 的 cube-api/cubelet 也踩过同一个坎(cube-api 默认监听 127.0.0.1 改为 10.255.255.254)。

Quirk 2 · 用户态 HTTPS 出口被 Tailscale CGNAT 劫持

实测:

# 在 douglas-wsl 上: $ curl -s -o /dev/null -w "HTTP %{http_code} t=%{time_total}s\n" \ --max-time 10 https://github.com HTTP 000 t=10.015s $ curl ... https://registry.npmjs.org HTTP 000 t=10.018s $ curl ... https://ghcr.io HTTP 000 t=10.034s $ curl ... https://docker.io HTTP 000 t=10.024s $ curl ... https://install.microsandbox.dev HTTP 000 t=15.000s $ ping -c 2 8.8.8.8 50% packet loss $ getent hosts github.com 198.18.1.79 github.com # ← 198.18.x.x = Tailscale CGNAT 劫持 IP

根因

douglas-wsl 上 DNS 走 10.255.255.254(WSL host loopback)→ Windows 端 systemd-resolved-equivalent → Tailscale MagicDNS 接管 → 返回 CGNAT 假 IP(198.18.0.0/15 范围)。这套机制原本是为 Tailscale magic 主机服务的,但拦了所有 DNS 解析;用户态进程拿到 CGNAT IP 后没路由可达,全部 timeout。

例外dockerd 本身有 magic path(在 docker network namespace + 自己的 DNS 配置)能正常 pull image。这是 docker pull alpine:3.21 在 spike 中能成功的原因。

对 Microsandbox 部署的影响

  • curl -fsSL https://install.microsandbox.dev | sh(msb 官方 install 脚本不可用)
  • npm install microsandbox(NPM registry HTTP 000)
  • msb pull ghcr.io/openspec/task-runner:latest(msb 自己的 HTTPS client 也走系统 DNS)

解法:所有 binary / image / npm package 走 mac-side 中转:

┌─────────────────────────────────────────────────────────────────┐ │ mac (有公网) ──gh release download──▶ msb-linux-x86_64.tar.gz │ │ + docker pull task-runner:latest │ │ + docker save tarball │ │ mac ──ssh ControlMaster scp──▶ douglas-wsl:/tmp/ │ │ │ │ douglas-wsl: │ │ tar xzf /tmp/microsandbox-linux-x86_64.tar.gz │ │ docker load < /tmp/task-runner.tar │ │ docker push 10.255.255.254:5000/task-runner:latest │ │ msb pull --insecure 10.255.255.254:5000/task-runner:latest │ └─────────────────────────────────────────────────────────────────┘

包装脚本:scripts/bridge-task-runner-image-to-wsl.sh + scripts/deploy-microsandbox-shim.sh

Quirk 3 · usermod -aG kvm 不立即生效,需 systemd 注入 group

实测:

$ sudo usermod -aG kvm douglasdong $ getent group kvm kvm:x:993:douglasdong # ← /etc/group 已更新 $ groups douglasdong adm cdrom sudo dip plugdev users docker # ← 但当前 session group set 没有 kvm(PAM 注入早于 usermod) $ ~/microsandbox-spike/msb run alpine -- echo hello Error: failed to open /dev/kvm: Permission denied # ← 当前 session 无权限 $ sg kvm -c "~/microsandbox-spike/msb run alpine -- echo hello" hello # ← sg 临时切组生效

含义

  • 交互式 shell 加 group 后必须 newgrp kvm / sg kvm -c / 重新登录才生效
  • 但 systemd 启的服务进程不走 PAM,可以直接 SupplementaryGroups=kvm 注入

对 Microsandbox 部署的影响

shim 服务的 systemd unit 必须显式声明 group:

[Service] User=douglasdong SupplementaryGroups=kvm Environment=MSB_PATH=/opt/microsandbox-shim/msb ExecStart=/usr/bin/node /opt/microsandbox-shim/shim.mjs

不依赖 douglasdong 的 active session group set。

验证步骤复现(30 分钟内可全跑)

# 1. mac 端下载 binary gh release download v0.4.6 -R superradcompany/microsandbox \ -p 'microsandbox-linux-x86_64.tar.gz' \ -p 'checksums.sha256' \ -D /tmp/ # 2. mac 端校验 + scp 到 douglas-wsl cd /tmp && grep microsandbox-linux-x86_64.tar.gz checksums.sha256 | sha256sum -c - scp -o ControlPath=~/.ssh/cm/douglas-wsl -o ControlMaster=no \ /tmp/microsandbox-linux-x86_64.tar.gz \ /tmp/checksums.sha256 \ douglas-wsl:/tmp/ # 3. douglas-wsl 端解压 + 加 kvm group ssh -o ControlPath=~/.ssh/cm/douglas-wsl -o ControlMaster=no -T douglas-wsl ' mkdir -p ~/microsandbox-spike ~/.microsandbox/{bin,lib} cd ~/microsandbox-spike && tar xzf /tmp/microsandbox-linux-x86_64.tar.gz ln -sf ~/microsandbox-spike/msb ~/.microsandbox/bin/msb ln -sf ~/microsandbox-spike/libkrunfw.so.5.2.1 ~/.microsandbox/lib/libkrunfw sudo usermod -aG kvm douglasdong ' # 4. douglas-wsl 启 local registry + 装 alpine ssh ... 'docker run -d --restart unless-stopped --name spike-registry -p 5000:5000 registry:2 docker pull alpine:3.21 docker tag alpine:3.21 localhost:5000/alpine:3.21 docker push localhost:5000/alpine:3.21' # 5. msb cold start ssh ... 'sg kvm -c "~/microsandbox-spike/msb --warn run \ 10.255.255.254:5000/alpine:3.21 -- echo HelloFromMicroVM"' # 期望输出:HelloFromMicroVM(~500ms 内)

与 sandbank 抽象的对位(提前定义)

sandbank capabilityMicrosandbox SDK API备注
provider.create()Sandbox.builder(name).image(...).create()spawn msb 子进程
provider.exec()sandbox.exec(cmd, args) 同步收 stdout/stderr
exec.streamsandbox.execStream(cmd, args)AsyncIterable<ExecEvent>event.kind = stdout / stderr / exited
provider.destroy()sandbox.stopAndWait()也可以走 msb stop / msb rm CLI
port.exposebuilder .publishPort(host, guest)
sleepmsb stop + msb start (detached mode)
snapshotSandbox.builder(...).snapshot()beta API
terminalsandbox.openShell()通过 shim SSE 转发

上游 events API(暂未 GA · 不阻塞本期 change)

Microsandbox 计划的 on_event(name, handler) / emit(name, data) host↔guest 结构化 RPC 在 v0.4.6 标注 “coming soon”。我们的 task-runner 内 stage 切换走 PostToolUse hook 写 stdout + shim execStream parse + HTTP callback 到主 API(与 BoxLite/AIO 同模式),不依赖 events API。

后续 follow-up(不在本 change scope)

  • F1 Microsandbox 版本升级走单独 change(锁 v0.4.6 baseline · 因为 README 标 beta + 周 release 节奏要求审慎)
  • F2 WSL2 出口劫持根治(修 Windows Tailscale daemon 配置)— 不假设修,bridge 脚本保留
  • F3 events API GA 后切换 stage 回传到结构化 RPC(减少 stdout parsing 复杂度)
  • F4 μVM 长时间运行的 metrics 接入(msb metrics 输出已经是结构化的,可对接 Prometheus)