109 lines
6.6 KiB
PowerShell
109 lines
6.6 KiB
PowerShell
# setup-wsl-sandbox.ps1 — 创建 astrion WSL2+bwrap 专用沙箱发行版
|
||
#
|
||
# 用法(PowerShell,普通用户即可,无需管理员):
|
||
# powershell -ExecutionPolicy Bypass -File scripts\setup-wsl-sandbox.ps1
|
||
# 可选参数:
|
||
# -DistroName 发行版名称(默认 astrion-sandbox,需与 HOST_SANDBOX_WSL_DISTRO 一致)
|
||
# -InstallDir 发行版 VHDX 安装目录(默认 ~\.astrion\wsl-sandbox)
|
||
# -RootfsUrl Alpine minirootfs 下载地址(默认阿里云镜像)
|
||
# -ApkMirror apk 软件源镜像(默认阿里云)
|
||
#
|
||
# 设计依据:wsl2-sandbox-poc-report.md
|
||
# - 必须是专用发行版并关闭 interop,否则沙箱内可经 cmd.exe 逃逸到宿主机;
|
||
# - 固化公共 DNS,规避 localhost 代理导致的 WSL NAT DNS 失效;
|
||
# - 内置国内 apk 镜像,避免官方源大包下载卡死。
|
||
|
||
param(
|
||
[string]$DistroName = "astrion-sandbox",
|
||
[string]$InstallDir = (Join-Path $env:USERPROFILE ".astrion\wsl-sandbox"),
|
||
[string]$RootfsUrl = "https://mirrors.aliyun.com/alpine/v3.21/releases/x86_64/alpine-minirootfs-3.21.3-x86_64.tar.gz",
|
||
[string]$ApkMirror = "https://mirrors.aliyun.com/alpine/v3.21"
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
$env:WSL_UTF8 = "1"
|
||
# 控制台与输出流统一 UTF-8:后端以管道读取本脚本输出,默认 GBK 会导致中文乱码
|
||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||
|
||
function Invoke-Wsl {
|
||
# PS 5.1 下 ValueFromRemainingArguments 会把 -d/-e 等 wsl 参数误绑定到函数通用参数
|
||
# (-e 前缀匹配 -ErrorAction/-ErrorVariable 报二义性错误),故改为单个数组传参,
|
||
# 调用处一律:Invoke-Wsl @('-d', $Name, '-e', 'true')
|
||
param([string[]]$WslArgs)
|
||
# PS 5.1 坑:全局 EAP=Stop 时原生命令写 stderr(如 wsl 的 localhost 代理警告)
|
||
# 会直接抛 NativeCommandError 终止脚本,Where 过滤来不及生效。
|
||
# 函数内降为 Continue(函数作用域,不影响全局 Stop 对其它命令的保护)。
|
||
$ErrorActionPreference = 'Continue'
|
||
$output = & wsl.exe @WslArgs 2>&1 | Where-Object { $_ -notmatch "localhost 代理|localhost proxy" }
|
||
return @{ Code = $LASTEXITCODE; Output = ($output -join "`n") }
|
||
}
|
||
|
||
Write-Host "==> [1/6] 检查 WSL 环境"
|
||
$wslStatus = Invoke-Wsl @('--status')
|
||
if ($wslStatus.Code -ne 0) {
|
||
throw "WSL 不可用。请先启用 WSL2(wsl --install --no-distribution 或安装 Docker Desktop 后重试)。"
|
||
}
|
||
|
||
Write-Host "==> [2/6] 检查发行版 '$DistroName' 是否已存在"
|
||
$probe = Invoke-Wsl @('-d', $DistroName, '-e', 'true')
|
||
if ($probe.Code -eq 0) {
|
||
Write-Host " 发行版已存在,跳过导入(如需重建:wsl --unregister $DistroName)"
|
||
} else {
|
||
Write-Host "==> [3/6] 下载 Alpine rootfs"
|
||
$rootfs = Join-Path $env:TEMP "astrion-alpine-minirootfs.tar.gz"
|
||
if (-not (Test-Path $rootfs)) {
|
||
Invoke-WebRequest -Uri $RootfsUrl -OutFile $rootfs -UseBasicParsing
|
||
}
|
||
Write-Host " rootfs: $rootfs ($([math]::Round((Get-Item $rootfs).Length/1MB,1)) MB)"
|
||
|
||
Write-Host "==> [4/6] 导入为 WSL2 发行版"
|
||
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
|
||
$null = Invoke-Wsl @('--import', $DistroName, $InstallDir, $rootfs, '--version', '2')
|
||
$check = Invoke-Wsl @('-d', $DistroName, '-e', 'true')
|
||
if ($check.Code -ne 0) { throw "发行版导入失败: $($check.Output)" }
|
||
}
|
||
|
||
Write-Host "==> [5/6] 写入沙箱配置(关闭 interop / 固定 DNS / 国内镜像)"
|
||
# 关闭 Windows 互操作:防止沙箱内经 cmd.exe 逃逸宿主机(PoC 已验证该逃逸路径)
|
||
$conf = Invoke-Wsl @('-d', $DistroName, '--', 'sh', '-c', "printf '[network]\ngenerateResolvConf = false\n\n[interop]\nenabled = false\nappendWindowsPath = false\n' > /etc/wsl.conf")
|
||
if ($conf.Code -ne 0) { throw "写入 wsl.conf 失败: $($conf.Output)" }
|
||
# 坑:首次导入的发行版内 /etc/resolv.conf 是 WSL 生成的 bind mount,直接 rm 会 EBUSY 失败
|
||
# (旧脚本未检查返回码被静默吞掉,terminate 后 generateResolvConf=false 生效,
|
||
# 不再自动生成 resolv.conf → 文件缺失 → DNS 完全失效)。必须先 umount 再重建真实文件。
|
||
$resolv = Invoke-Wsl @('-d', $DistroName, '--', 'sh', '-c', "umount /etc/resolv.conf 2>/dev/null; rm -f /etc/resolv.conf; printf 'nameserver 223.5.5.5\nnameserver 119.29.29.29\n' > /etc/resolv.conf && cat /etc/resolv.conf")
|
||
if ($resolv.Code -ne 0) { throw "写入 resolv.conf 失败: $($resolv.Output)" }
|
||
$apkRepo = Invoke-Wsl @('-d', $DistroName, '--', 'sh', '-c', "printf '$ApkMirror/main\n$ApkMirror/community\n' > /etc/apk/repositories")
|
||
if ($apkRepo.Code -ne 0) { throw "写入 apk 镜像源失败: $($apkRepo.Output)" }
|
||
|
||
# 使 wsl.conf 生效
|
||
$null = Invoke-Wsl @('--terminate', $DistroName)
|
||
Start-Sleep -Seconds 2
|
||
|
||
Write-Host "==> [6/6] 安装沙箱工具链(bubblewrap / bash / python3 / git)"
|
||
$apk = Invoke-Wsl @('-d', $DistroName, '--', 'sh', '-c', "apk update && apk add bubblewrap bash ncurses-libs python3 git")
|
||
if ($apk.Code -ne 0) { throw "apk 安装失败: $($apk.Output)" }
|
||
|
||
# 与 Windows 端 Git for Windows 默认的 core.autocrlf=true 对齐:
|
||
# Windows 检出的工作区文件是 CRLF,沙箱 git 不开 autocrlf 会把全部文件误判为已修改
|
||
# (全量 +/-),且全量内容读取+xdiff 会让 git 命令慢到触发执行超时。
|
||
$null = Invoke-Wsl @('-d', $DistroName, '-e', 'git', 'config', '--system', 'core.autocrlf', 'true')
|
||
# 9P 挂载的 inode/ctime 语义与 Windows git 写入的索引不匹配,会导致沙箱 git 对全部
|
||
# 文件做内容重读(全树 status/diff >20s);放宽 stat 校验后只需秒级。
|
||
$null = Invoke-Wsl @('-d', $DistroName, '-e', 'git', 'config', '--system', 'core.checkStat', 'minimal')
|
||
$null = Invoke-Wsl @('-d', $DistroName, '-e', 'git', 'config', '--system', 'core.trustctime', 'false')
|
||
|
||
# 验收
|
||
$verify = Invoke-Wsl @('-d', $DistroName, '--', 'sh', '-c', "bwrap --version && bash -c 'echo bash-ok' && python3 --version && git --version")
|
||
Write-Host $verify.Output
|
||
$escape = Invoke-Wsl @('-d', $DistroName, '--', 'bwrap', '--die-with-parent', '--new-session', '--unshare-all', '--ro-bind', '/', '/', '--proc', '/proc', '--dev', '/dev', '--', 'bash', '-c', "cmd.exe /c echo escape 2>&1 || echo interop-blocked")
|
||
if ($escape.Output -match "interop-blocked|not found") {
|
||
Write-Host " interop 逃逸封堵验证: OK"
|
||
} else {
|
||
Write-Warning " interop 可能未关闭,请检查 /etc/wsl.conf 后执行 wsl --terminate $DistroName"
|
||
}
|
||
|
||
Write-Host ""
|
||
Write-Host "完成。沙箱发行版 '$DistroName' 就绪。"
|
||
Write-Host "若使用了非默认名称,请设置环境变量 HOST_SANDBOX_WSL_DISTRO=$DistroName"
|