gVisor Go SDKsandboxexec/sandbox详解用 Go 代码创建沙箱并在其中执行命令【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisorgVisor 官方提供了实验性的 Go SDK——gvisor.dev/gvisor/sandboxexec/sandbox包它把runsc的 OCI bundle 生成、沙箱启动、命令执行与资源回收封装成了一套简洁的 Go API。本文基于仓库中 API 参考文档 与 快速上手指南 展开结合 sandboxexec/sandbox/sandbox.go、sandboxexec/sandbox/oci.go 等源码实现带你掌握“用几行 Go 代码创建一个隔离沙箱并安全执行任意命令”的完整方案。[!WARNING]EXPERIMENTAL官方文档与 sandboxexec/README.md 均明确声明该目录下的 API 与工具处于实验阶段不用于生产环境且//sandboxexec目前不是安全审计的有效目标。使用时请以此为前提。1. 前置条件使用 SDK 需要满足两个条件见 快速上手文档已安装 gVisorrunsc系统中必须存在runsc二进制。SDK 底层就是直接调用runsc子进程来驱动沙箱的安装方式参考 安装指南。Go 环境官方建议 Go 1.16。此外源码中还有一个可选的环境变量sandbox.go#L337-L352RUNSC_PATH指定runsc二进制的路径未设置时 SDK 通过exec.LookPath(runsc)在系统PATH中查找找不到会返回runsc binary is not found错误。2. 导入与包定位import gvisor.dev/gvisor/sandboxexec/sandbox包注释自述“Package sandbox provides a simple Go API for creating gVisor sandbox and executing commands in the sandbox”见 sandbox.go#L15-L17。3. 核心对象 Sandbox 与生命周期 API3.1 New创建沙箱func New(ctx context.Context, opts ...Option) (*Sandbox, error)New会以子进程方式拉起一个新沙箱并以detached分离模式启动即调用返回后沙箱常驻运行供后续多次Exec。参数为执行上下文ctx和可变配置项opts ...Option返回*Sandbox句柄失败时返回 error如权限不足、runsc缺失、配置非法等。从源码看sandbox.go#L464-L625New的实际工作流是应用配置newOptions以默认值网络模式none、工作目录/为基底依次套用Option若未指定 ID则用 16 字节随机数生成一个唯一沙箱 IDnewID()sandbox.go#L327-L335。准备目录若未指定 runtime 目录会在临时目录下创建gvisor-sandbox-*state 目录默认位于runtimeDir/state权限要求为0700不满足会直接报错sandbox.go#L502-L519。动态生成 OCI bundle调用newBundleoci.go#L248-L280在runtimeDir/id下写出config.json并创建空的rootfs/目录——也就是说 SDK 不需要你预先准备任何镜像或 bundle。以子进程启动最终执行等价于runsc [全局flags] run --bundle bundleDir --detach sandboxID其中--networknone|host|sandbox、--root、--debug等全局 flag 由runscGlobalArgs()sandbox.go#L400-L424拼装非 root 用户运行时会自动追加--ignore-cgroups。runsc的 stderr 会被捕获进错误信息方便排查启动失败原因。3.2 OCI 配置是怎么生成的newSpecoci.go#L140-L246决定了沙箱的默认形态理解这些默认值有助于理解 SDK 的行为默认 init 进程是/bin/sleep infinitydefaultInitArgs即“保活型”空容器真正的命令全部通过runsc exec注入默认命名空间PID、Mount、UTS、IPC 四个非 root 运行时会额外追加 UserNamespace并自动填充 UID/GID 映射容器内0映射到宿主机当前用户sandbox网络模式下再追加 NetworkNamespaceoci.go#L148-L207默认挂载/procprocfs、/devtmpfs并把宿主机的/bin、/usr、/lib、/lib64、/etc/alternatives以rbind,ro,nosuid,nodev只读绑定进沙箱——注意注释强调这些二进制是在 gVisor 沙箱内执行而不是在宿主机上执行oci.go#L216-L233默认环境变量PATH/bin:/usr/bin:/usr/local/binbaseEnv可用WithoutBaseEnv关闭。3.3 Exec在沙箱内执行命令文档给出的签名为func (s *Sandbox) Exec(ctx context.Context, cmd string, opts ...string) (stdout string, stderr string, err error)说明当前仓库源码中的签名已演化为func (s *Sandbox) Exec(ctx context.Context, argv []string, opts ...ExecOption) (*ExecResult, error)sandbox.go#L673-L728命令以argv []string传入结果封装在ExecResult{ExitCode, Stdout, Stderr}中命令非零退出通过ExitCode表达被信号杀死时为128 信号号与 shell 报告方式一致error仅表示“命令根本没有跑起来”。由于 SDK 处于实验阶段且快速演进建议以源码为准。Exec的语义与实现细节底层执行runsc --root stateDir exec sandboxID argv...并阻塞等待命令退出sandbox.go#L687-L714命令继承沙箱创建时的工作目录、环境变量、用户与 capabilities除启用WithExecSignalRelay外多个Exec调用可以并发内部用execMu读写锁区分sandbox.go#L323-L325。两个ExecOptionsandbox.go#L627-L653// 将命令的 stdin/stdout/stderr 接到给定读写端传 nil 的流会被收集进 ExecResult func WithExecStdio(stdin io.Reader, stdout, stderr io.Writer) ExecOption // 把宿主进程收到的信号转发给沙箱内命令独占式不可并发 func WithExecSignalRelay() ExecOption信号中继的实现是runsc exec时附加--internal-pid-filerelaySignalssandbox.go#L439-L453读取 PID 文件后将本进程收到的信号转成runsc kill --pid pid下发给沙箱内目标进程。3.4 Close关闭与清理func (s *Sandbox) Close(ctx context.Context) errorClose按顺序执行sandbox.go#L731-L759runsc kill id SIGKILL杀掉沙箱进程忽略其报错进程可能已退出runsc delete --force id清理 runsc 侧状态删除 OCI bundle 目录若 state 目录 / runtime 目录是 SDK 自行创建的ownStateDir/ownRuntimeDir则一并卸载并删除通过WithStateDir显式提供的目录会被保留WithStateDir注释明确说明 Close 只删除默认目录多类错误通过errors.Join聚合返回。因此推荐用法是在New成功后立即defer sb.Close(ctx)。3.5 Bundle获取 OCI bundle 路径func (s *Sandbox) Bundle() string返回本沙箱 OCI bundle 目录的绝对路径即含config.json的目录可用于检查生成的 OCI 配置或对接其他工具。4. Option 配置项全集Option是一个函数类型Options结构体持有全部配置sandbox.go#L59-L97。API 文档覆盖了以下基础项源码中还有更多扩展项一并整理如下选项源码签名作用WithRuntimeDirWithRuntimeDir(runtimeDir string) Option指定 bundle 与 state 文件的写入目录不指定时自动创建临时目录并在Close时删除WithIDWithID(id string) Option指定沙箱 ID不设置时自动生成 32 位十六进制随机 ID网络见下节WithNetwork配置沙箱网络模式WithMountWithMount(mounts ...Mount) Option追加自定义挂载按给定顺序应用于默认挂载之后WithEnvWithEnv(envs ...string) Option注入KEYVALUE格式环境变量格式非法直接报错WithWorkingDirWithWorkingDir(cwd string) Option设置沙箱进程 cwd相对路径会拼到/下默认/WithHostnameWithHostname(hostname string) Option设置沙箱主机名WithRootfsWithRootfs(path string, readOnly bool) Option用宿主机已有目录作为 rootfs默认使用 bundle 内空rootfs/WithUserWithUser(uid, gid uint32) Option设置 init 进程 UID/GID默认0:0WithCapabilitiesWithCapabilities(caps *specs.LinuxCapabilities) Option设置 init 进程 capability 集nil 时由运行时用默认值WithNamespacesWithNamespaces(namespaces ...specs.LinuxNamespace) Option完全替换默认命名空间集合传空即无任何命名空间WithIDMappingsWithIDMappings(uidMappings, gidMappings []specs.LinuxIDMapping) Option替换默认 UID/GID 映射仅与 user namespace 搭配有意义WithoutLinuxSystemMountsWithoutLinuxSystemMounts() Option去掉自动的/proc与/dev挂载WithoutHostBinaryMountsWithoutHostBinaryMounts() Option去掉宿主机二进制/库目录的只读绑定WithoutBaseEnvWithoutBaseEnv() Option去掉默认PATH环境变量WithStateDirWithStateDir(dir string) Option自定义 runsc state 目录对应runsc --rootClose 不会删除它WithDebugWithDebug(logPath string) Option打开runsc --debug调试日志写到logPath空则 stderrWithSnapshotWithSnapshot(snapshot *Snapshot) Option从快照恢复启动见第 6 节关于文档中的WithNetworking(enabled bool)文档页面go.md#L124-L133记录的是早期布尔形式“是否启用网络启用需 root”。当前源码已替换为三态的WithNetwork(mode NetworkMode)下节布尔语义被拆分成了none/host两种无 root 需求的模式请以 sandbox.go#L155-L166 为准。5. 网络模式 NetworkModetype NetworkMode string const ( NetworkModeNone NetworkMode none // 完全禁用网络默认 NetworkModeHost NetworkMode host // 共享宿主机网络命名空间支持 rootless NetworkModeSandbox NetworkMode sandbox // 独立网络命名空间 gVisor netstack需要 root ) func WithNetwork(mode NetworkMode) Option要点sandbox.go#L43-L57默认模式为NetworkModeNoneroot / rootless 均可运行适合纯计算类任务sandbox模式要求以 root 运行New中会显式检查os.Geteuid() ! 0否则返回sandbox networking requires running as rootsandbox.go#L498-L500测试用例TestNonRootNetworkingErrorsandbox_test.go#L99-L114验证了该错误非法模式字符串如invalid-mode会被WithNetwork立即标记为错误New直接失败TestInvalidNetworkMode覆盖了这一点模式会翻译成runsc的全局 flag--networknone|host|sandboxrunscGlobalArgs而host模式下 OCI spec 中不会追加 NetworkNamespacesandbox模式会追加oci.go#L159-L161。6. 文件系统挂载与快照6.1 Mount 与 MountTypetype Mount struct { Source, Destination string Type MountType // Bind/Tmpfs/Proc/Sysfs/Devtmpfs/Devpts/Cgroup ReadOnly, Recursive, Private, NoSuid, NoDev bool // 分别对应 ro/rbind/rprivate/nosuid/nodev }Mount会被转换成 OCI mount 条目ociMountoci.go#L107-L138并追加在默认挂载之后——OCI 规范下后者覆盖前者因此可以用自定义挂载覆盖默认路径。测试用例给出了两个可直接复用的模式sandbox_test.go#L128-L191// 宿主目录只读绑定进沙箱验证双向可见性 sb, _ : sandbox.New(ctx, sandbox.WithNetwork(sandbox.NetworkModeNone), sandbox.WithMount(sandbox.Mount{ Type: sandbox.MountTypeBind, Source: tempHostDir, Destination: /mnt/host_share, ReadOnly: true, }), ) res, _ : sb.Exec(ctx, []string{cat, /mnt/host_share/witness.txt}) // 内存 tmpfs sandbox.WithMount(sandbox.Mount{Type: sandbox.MountTypeTmpfs, Source: tmpfs, Destination: /mnt/scratch})6.2 快照与恢复SnapshotSDK 还内置了快照存储抽象storage.gotype SnapshotType string const ( CheckpointRestore SnapshotType CheckpointRestore // 全量进程状态检查点 FilesystemSnapshot SnapshotType FilesystemSnapshot // 文件系统快照 RootfsTarSnapshot SnapshotType RootfsTarSnapshot // rootfs 变更 tar 包 )(s *Sandbox) Snapshot(ctx, snapshotType, storage SnapshotStorage, opts ...SnapshotOption) (*Snapshot, error)把沙箱状态写入存储并自动写metadata.jsonWithLeaveRunning(true)可在快照后保持沙箱运行当前已完整实现的是RootfsTarSnapshot底层执行runsc tar rootfs-upper --file tar id并把 tar 上传到存储sandbox.go#L838-L870FilesystemSnapshot与CheckpointRestore的写出路径在源码中仍是 TODO恢复侧WithSnapshot会在New中读取metadata.json并按类型分流RootfsTarSnapshot下载 tar 并通过--allow-rootfs-tar-annotation注解挂载sandbox.go#L547-L576SnapshotStorage是可插拔接口PutWriter/GetReader/Delete/List/Lookup仓库自带基于本地目录的NewFilesystemStorage(rootDir)实现。测试TestRootfsTarSnapshotsandbox_test.go#L295-L340演示了完整的“沙箱 A 写文件 → 快照 → 沙箱 B 恢复并读到同一文件”的闭环可作为该能力的行为基准。7. 完整实战从创建到销毁下面是与当前源码签名一致的完整示例快速上手文档 中的示例使用了旧的字符串版Exec签名此处按源码调整为argv []string形式package main import ( context fmt log gvisor.dev/gvisor/sandboxexec/sandbox ) func main() { ctx : context.Background() // 创建沙箱。默认网络模式为 nonerootless 可用。 // 如需 gVisor netstack 隔离网络需 root 并改用 // sandbox.WithNetwork(sandbox.NetworkModeSandbox) sb, err : sandbox.New(ctx) if err ! nil { log.Fatalf(Failed to create sandbox: %v, err) } defer func() { if err : sb.Close(ctx); err ! nil { log.Fatalf(Failed to close sandbox: %v, err) } }() // 在沙箱内执行命令 res, err : sb.Exec(ctx, []string{uname, -a}) if err ! nil { log.Fatalf(Exec failed: %v, stderr: %s, err, res.Stderr) } if res.ExitCode ! 0 { log.Fatalf(uname exited %d: %s, res.ExitCode, res.Stderr) } fmt.Printf(Stdout: %s, res.Stdout) }运行步骤与 快速上手文档 一致将代码保存为main.gogo mod init gvisor-quickstart初始化模块添加依赖文档给出的go get gvisor.dev/gvisor/sandboxexec/sandboxgo以实际可用的发布版本为准go run main.go。预期输出形如Stdout: Linux 5.15.0-gvisor内核版本字符串中的gvisor后缀正是命令运行在 gVisor 应用内核emulated Linux kernel中的标志。一个更贴近真实场景的组合示例环境注入 自定义挂载 调试日志sb, err : sandbox.New(ctx, sandbox.WithID(my-sandbox), sandbox.WithRuntimeDir(/tmp/gvisor-sdk), sandbox.WithEnv(APP_ENVprod), sandbox.WithWorkingDir(tmp/app), // 沙箱内解析为 /tmp/app sandbox.WithHostname(worker-01), sandbox.WithMount(sandbox.Mount{ Type: sandbox.MountTypeBind, Source: /data/shared, Destination: /mnt/shared, ReadOnly: true, }), ) // 校验 config.json 已生成 fmt.Println(sb.Bundle()) // 指向 runtimeDir/my-sandbox含 config.json8. 行为验证官方测试用例怎么验证 SDKsandboxexec/sandbox/sandbox_test.go 提供了可直接参照的验证范式TestExecDmesg在默认配置沙箱内执行dmesg输出包含Starting gVisor——这是“命令确实跑在 gVisor 里”的最简证据TestSandboxOptions验证WithID/WithRuntimeDir后Bundle()路径前缀正确TestCustomBindMount/TestCustomTmpfsMount/TestCustomBindMountWrite覆盖 bind 挂载读、tmpfs 写、以及沙箱内写文件回落到宿主目录的反向验证TestSandboxEnv验证重复变量后者生效、PATH默认存在TestSandboxWorkingDir断言 bundle 的config.json中cwd为/tmp/custom相对路径被规范化TestSandboxHostname沙箱内hostname返回WithHostname设置的值TestMain中通过testutil.FindRunsc()定位runsc并设置RUNSC_PATH印证了第 1 节所述环境变量机制。9. 常见问题与排查现象原因与处理runsc binary is not foundrunsc不在PATH中设置RUNSC_PATH或安装 runsc 后重试sandbox networking requires running as root使用了NetworkModeSandbox且当前非 root改用none/host或获取 rootsandbox state directory has incorrect permissions自建 state 目录权限不是0700SDK 只对自建的目录做该检查invalid network mode ...传入的模式不是none/host/sandbox三者之一no snapshot storage configured for restoreWithSnapshot的Snapshot未设置Storage需要看 runsc 侧日志WithDebug(logPath)打开--debug或直接检查sb.Bundle()下的config.json另外从New的清理逻辑sandbox.go#L482-L496可以推断若New中途失败SDK 会自动删除已创建的 bundle、state、runtime 目录因此失败路径一般不留残留但成功创建后务必调用Close否则沙箱进程/bin/sleep infinity保活与目录将持续存在。10. 延伸阅读Go SDK API 参考文档本文的骨架来源Go SDK 快速上手Python SDK同目录下还有一套 Python 实现核心源码sandbox.go生命周期与 Exec/快照、oci.goOCI bundle 生成、storage.go快照存储、debug.go实验性声明与安全范围sandboxexec/README.md【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考