跳到主要内容

边缘路由与 VM 内守护进程:请求如何抵达具体沙箱

30 秒导读: 你手上有成千上万台随时开关的沙箱(microVM),外部一个普通的 HTTPS 请求怎么"找到"其中某一台、进到它里面去跑命令或读文件?本章把这条链路一次讲完:边缘的 client-proxy 负责"从 URL 解出是哪台沙箱、它在哪个节点、把请求反代过去";如果那台沙箱已经被暂停,边缘层会用一次带鉴权的 gRPC 让控制面即时把它唤醒再转发;请求抵达节点后,由节点内的反向代理打到 guest 里的 envd 守护进程——它才是真正在沙箱内部执行进程、读写文件的那个"手脚"。


1. 这是什么(零基础也能懂)

一句话定义: 这是 E2B 的**"最后一公里"**——把一个来自公网的请求,精确送进某一台正在运行(或需要被唤醒)的沙箱内部。

它要解决的问题,用一个场景说清:

你在沙箱里启动了一个 Web 服务,E2B 给你一个地址,比如 https://3000-ixj4k2m9.e2b.app。你用浏览器打开它。可后台有几万台沙箱、分布在几十台物理机上,而且很多沙箱为省钱**已经被冻结(暂停)**了。这一个请求,凭什么能找到编号 ixj4k2m9 的那台、进到它的 3000 端口?

回答这个"凭什么",正是本章的两个主角:

主角白话职责跑在哪代码位置
client-proxy(边缘路由)看一眼 URL,查出"是哪台沙箱、在哪个节点",把请求反代过去;必要时先唤醒它集群边缘,一个独立服务packages/client-proxy/
envd(VM 内守护进程)沙箱内部监听,替外部把命令跑起来、把文件收发好每台沙箱的 guest 里,一个进程packages/envd/

一句话直觉/类比:client-proxy写字楼前台——它不认识每个人,但看一眼你要找的"房号"(sandbox id)就知道该带你上哪层楼、进哪个房间;envd 则是房间里的助理——你进来后,真正帮你干活(开程序、拿文件)的是它。前台还有个特殊本事:如果那个房间的人"睡着了"(沙箱暂停),前台会先打个电话(gRPC)把人叫醒,再放你进去。

本章只讲"请求怎么进到沙箱";沙箱本身怎么被创建、怎么秒级恢复,见 02-sandbox-lifecycle.md03-fast-resume-uffd.md


2. 顶层全景(它大概怎么转)

怎么读这张图: 从左到右就是一个请求的旅程;每一跳只做一件事,箭头上标了"它靠什么决定下一跳"。

按 host 解出 sandbox_id + port

外部请求 ▼
┌────────┐ ┌──────────────────────────────────┐ catalog 命中: 查到 orchestrator IP
│ 浏览器 │──▶│ client-proxy(边缘) │──────────────┐
│ / SDK │ │ :3002 │ │
└────────┘ │ ① 从 URL/Header 解 id+port │ catalog 未命中(沙箱暂停?)
│ ② 查 Redis 目录 → 节点 IP │──────┐ │
│ ③ 未命中则 gRPC 唤醒 │ │ │
└──────────────────────────────────┘ │ │
▼ │
┌────────────────────┐ │
│ API(控制面) │ │
│ ResumeSandbox gRPC │ │
│ 鉴权→即时 Resume │ │
│ 返回 orchestrator IP│ │
└─────────┬──────────┘ │
│ 唤醒后的 IP │
▼ ▼
┌──────────────────────────────────────┐
│ 目标 orchestrator 节点(物理机) │
│ ┌────────────────────────────────┐ │
│ │ orchestrator-proxy :5007 │ │
│ │ 按 id 找到本机沙箱 → 打到 guest │ │
│ └───────────────┬────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ 沙箱 guest 内: envd :49983 │ │
│ │ chi + Connect RPC + authn │ │
│ │ 进程 / 文件系统 / 上传下载 / init│ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘

两层反代,不是一层。 这是最容易忽略、也是理解本章的关键:请求先被边缘的 client-proxy 反代到"正确的物理机",再被那台机器上的 orchestrator-proxy 反代进"正确的沙箱"。分工是:

  • 边缘层解决**"在哪台机器"**(跨节点、跨集群的服务发现与唤醒);
  • 节点层解决**"进哪台沙箱、哪个端口"**(本机沙箱表、连接限流、访问令牌校验)。

主线走一遍(高层、不进代码):

  1. 请求打到 client-proxy,它从 host(或路由头)里解出 sandbox_id 与目标 port
  2. sandbox_idRedis 沙箱目录查:这台沙箱现在跑在哪个 orchestrator 节点?
  3. 查到了 → 直接把请求反代到 节点IP:5007(orchestrator-proxy 的端口)。
  4. 没查到(很可能是被暂停了) → 用一次 gRPC 请求让 API 鉴权并即时 Resume,拿回唤醒后的节点 IP,再反代过去。
  5. 请求抵达节点,orchestrator-proxy 按 id 找到本机那台沙箱,把请求打到 guest 内 envd49983 端口(或用户自己的服务端口)。
  6. envd 在沙箱内部执行:跑进程、读写文件、返回结果。

下面三节把 ①②③ 和 ⑤⑥ 拆开讲原理。


3. 核心原理(逐个机制,由浅入深)

3.1 第一步:从一个 URL 里解出"是哪台沙箱、哪个端口"

要解决的小问题: 边缘代理手上只有一个 HTTP 请求。沙箱的身份藏在哪?

思路: E2B 把身份编码进了域名的最左子域——形如 {port}-{sandboxId}.{域名}。所以解析就是"切最左边那段、按 - 拆开"。另外还支持用请求头直接传 id/port(给 IP 直连、本地开发等域名编不进信息的场景)。

真实实现: 入口是 GetTargetFromRequest(),它返回一个闭包,对每个请求先尝试路由头、再回退到解析 host。

真源码 packages/shared/pkg/proxy/host.go:74 parseHost:切掉第一个 . 之后的域名部分,再按 - 拆出 portsandboxId

// packages/shared/pkg/proxy/host.go:74 parseHost —— 真实源码(节选)
host = host[:dot] // 只留最左子域 "3000-ixj4k2m9"
hostParts := strings.Split(host, "-")
sandboxPortString := hostParts[0] // "3000"
sandboxID = hostParts[1] // "ixj4k2m9"

这段在做的事:3000-ixj4k2m9.e2b.app 解成 (sandboxID=ixj4k2m9, port=3000)

关键细节 / 坑:

  • 两种取值来源,有优先级。 只有当 host 是本地地址或 sandbox. 共享子域时才先看头(shouldParseHeaders in host.go:45);否则一律解 host。头的名字是 E2b-Sandbox-Id / E2b-Sandbox-Port(host.go:110-111)。
  • 解出来立刻校验 id 合法性(id.ValidateSandboxID,host.go:24/37),非法直接 ErrInvalidSandboxID,不进入下一步。
  • 同一份解析逻辑被两层代理复用。 client-proxyorchestrator-proxy 都调 GetTargetFromRequest()——保证"边缘怎么理解这个 URL"和"节点怎么理解"完全一致。

3.2 第二步:按 sandbox id 反代到对应 orchestrator 节点

要解决的小问题: 有了 sandbox_id,怎么知道它此刻跑在哪台物理机?

思路: 不猜、不广播,而是查一张共享目录——沙箱被调度到某节点时,会在 Redis 里登记"sandbox_id → orchestrator_ip"。边缘代理只读这张表。

提示: 仓库自带的 CLAUDE.md 说 client-proxy 用 Consul 做服务发现——但按 sourceCommit 的真实代码,main.go 注入的是 Redis 沙箱目录(e2bcatalog.NewRedisSandboxCatalog),路由查询走 Redis。以源码为准。

真实实现: 启动时 main.go:128 构造 catalog := e2bcatalog.NewRedisSandboxCatalog(redisClient),把它传进 NewClientProxy。每个请求的路由决策在 proxy.go 的路由闭包里:

  • catalogResolution(packages/client-proxy/internal/proxy/proxy.go:76)调 catalog.GetSandbox(ctx, sandboxId);
  • 命中就取 SandboxInfo.OrchestratorIP(packages/shared/pkg/sandbox-catalog/catalog.go:13);
  • 组装目标 URL,反代到 节点IP:5007(orchestratorProxyPort,proxy.go:29)。
// packages/client-proxy/internal/proxy/proxy.go:188 —— 真实源码(节选)
url := &url.URL{
Scheme: "http",
Host: net.JoinHostPort(nodeIP, strconv.Itoa(orchestratorProxyPort)), // nodeIP:5007
}

这段在做的事:把"哪台机器"变成一个具体的反代目标——边缘层的活到此为止,剩下"进哪台沙箱"交给节点上的 orchestrator-proxy

关键细节 / 坑:

  • 重试分工。 边缘代理用 ClientProxyRetries,注释明说"端口转发延迟的重试由 orchestrator-proxy 负责"(proxy.go:142-143);节点代理才用 SandboxProxyRetries(5 次)去扛 envd 端口刚转发过来的抖动(orchestrator/pkg/proxy/proxy.go:63-64)。
  • 超时逐层放大。 边缘 610s(proxy.go:34)、节点 620s(orchestrator proxy.go:32)、envd 640s(envd/main.go:34)——刻意让下游比上游长,躲开 GCP LB 的 600s 空闲超时竞态。
  • 优雅下线是三段式。 client-proxy 收到 SIGTERM 后先标 Draining、等健康检查器发现、再标 Unhealthy、再关代理(main.go:250-302,状态定义在 internal/info.go:14-18)——保证正在传输的连接不被硬切。

3.3 巧妙的一步:请求打到"已暂停"的沙箱,边缘层如何即时唤醒

要解决的小问题: 为省钱,沙箱常被暂停(内存快照存盘、从节点上撤走)。这时 Redis 目录里查不到它。请求难道就 404?

思路: 不。目录未命中被当作"可能是暂停了"的信号——边缘层用一次 gRPC 问控制面 API:"这台能不能自动恢复?能就帮我拉起来,并告诉我它落在哪个节点。"整个"边缘发现暂停 → 触发恢复 → 转发"对调用方是透明的。

图示(怎么读:虚线是仅在目录未命中时才走的旁路):

catalogResolution
│ GetSandbox(id)

命中? ──是──▶ 用 OrchestratorIP 直接反代 :5007
│否(ErrSandboxNotFound)

handlePausedSandbox ┈┈┈┈▶ gRPC ResumeSandbox(id) ┈┈┈▶ API 控制面
│ (带 OAuth Bearer) │ ① 校验 client-proxy 身份(scope/org)
│◀┈┈┈┈ 返回 orchestrator_ip ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤ ② 查快照的 auto-resume 策略
▼ │ ③ 校验 traffic / envd 访问令牌
反代到唤醒后的 IP:5007 │ ④ startSandboxInternal → 拿节点 IP

边缘侧实现(client-proxy):

  • 目录未命中即调 handlePausedSandbox(proxy.go:97),内部调用 pausedChecker.Resume(...)(接口在 internal/proxy/paused_resumer.go:5)。
  • gRPC 客户端 grpcPausedSandboxResumer.Resume(packages/client-proxy/internal/proxy/paused_sandbox_resumer_grpc.go:73)把原始请求端口、以及可能存在的 traffic / envd 访问令牌塞进 gRPC metadata,再调 ResumeSandbox
  • 谁允许 client-proxy 触发恢复? 鉴权在 grpc_resume_auth.go:跨集群边缘拓扑下用 OAuth 客户端凭证换 Bearer token,scope 固定为 sandboxes:lifecycle(grpc_resume_auth.go:16/45-52,oauthGrpcResumeAuth.authorize at :59 注入 Authorization: Bearer ...)。若没配 OAuth 则是 noop(内网直连场景)。
// packages/client-proxy/internal/proxy/grpc_resume_auth.go:59 —— 真实源码(节选)
func (a oauthGrpcResumeAuth) authorize(ctx context.Context) (context.Context, error) {
token, err := a.tokenSource.Token() // 客户端凭证换来的短期 token
if err != nil { return ctx, ... }
return metadata.AppendToOutgoingContext(ctx,
proxygrpc.MetadataAuthorization, "Bearer "+token.AccessToken), nil
}

这段在做的事:给"唤醒沙箱"这个高权限操作盖上身份戳——不是任何人打个 gRPC 就能拉起别人的沙箱。

控制面侧实现(API ResumeSandbox,packages/api/internal/handlers/proxy_grpc.go:127): 收到请求后按顺序把关,任何一关不过就返回对应 gRPC 错误码(边缘会翻译成 403/404/503 等页面):

关卡做什么代码
身份若启用边缘鉴权,校验 client-proxy 的 OAuth claims + scope + orgproxy_grpc.go:131-140,159-173
策略查快照的 auto-resume 配置,非 Any 策略拒绝;纯文件系统快照拒绝隐式唤醒getAutoResumeSnapshot :99-125
复用若沙箱其实还在(转换中),走 HandleExistingSandboxAutoResume 直接返回,不重复拉起:185-207
令牌私网入口的非 envd 流量校验 traffic token;安全沙箱的 envd 流量校验 envd token(常数时间比较):234-256
拉起startSandboxInternal 真正恢复,再取节点路由 IP 回传:259-278

关键细节 / 坑:

  • 错误码是有语义的。 边缘把 gRPC 的 PermissionDenied / NotFound / FailedPrecondition / ResourceExhausted 分别映射成"拒绝恢复 / 当作不存在 / 仍在转换中 / 资源耗尽"四类用户可见错误(handlePausedSandbox proxy.go:112-124)。
  • "envd 流量"是特判的。 API 用 MetadataSandboxRequestPort 是否等于 49983 来判断这次是不是发往 envd 本身(isNonEnvdTrafficRequest proxy_grpc.go:63),据此选择校验 traffic token 还是 envd token。

3.4 抵达沙箱内部:envd 暴露了哪些能力

要解决的小问题: 请求终于进到 guest 了。谁在里面接?它能替我干什么?

思路: guest 里常驻一个 HTTP 服务 envd,监听 49983(packages/envd/main.go:37)。它用 chi 路由器 挂两类接口:一类是 Connect RPC(进程、文件系统),一类是普通 REST(健康、init、文件上传下载)。SDK / orchestrator 对沙箱的一切操作,最终都落到这里。

一个 envd 提供的能力清单:

能力域典型 RPC / 端点传输代码
进程Start / Connect / SendInput / StreamInput / SendSignal / Update / ListConnect RPC(流式)internal/services/process/*.go
文件系统(元数据)Stat / ListDir / MakeDir / Move / Remove / WatchDir / CreateWatcherConnect RPCinternal/services/filesystem/*.go
文件(数据面)GET /files(下载) / POST /files(上传)REST(多段/裸流)internal/api/download.go, upload.go
沙箱初始化POST /init(设环境变量、时间、令牌、CA、NFS 挂载)RESTinternal/api/init.go
冻结/解冻POST /freeze / POST /unfreeze(配合暂停)RESTinternal/api/init.go:257/290
健康GET /healthRESTinternal/api/store.go

服务是怎么挂起来的(packages/envd/main.go:159 run):

// packages/envd/main.go —— 真实源码(节选,标注行号)
m := chi.NewRouter()
filesystemRpc.Handle(m, &fsLogger, defaults) // :189 挂文件系统 Connect RPC
processRpc.Handle(m, &processLogger, defaults, cgroupManager)// :200 挂进程 Connect RPC
service := api.New(...) // :202 REST API (init/files/health...)
handler := api.HandlerFromMux(service, m) // :203 REST 路由并入同一个 mux
middleware := authn.NewMiddleware(permissions.AuthenticateUsername) // :204 Basic-Auth 解出目标用户
s := &http.Server{ Handler: withCORS(
service.WithAuthorization(middleware.Wrap(handler)), // :207-211 令牌鉴权 → 用户解析 → 业务
), Addr: ":49983" }

这段在做的事:把三层洋葱套好——最外层 CORS,中间 WithAuthorization(校验访问令牌),再中间 authn(把 HTTP Basic 里的用户名解析成沙箱内的 user.User),最里才是进程/文件系统/REST 的真正处理。

两个 Connect RPC 服务的挂载方式一致(Handlespec.NewXxxHandler(service, interceptors) + server.Mount(path, h),见 process/service.go:35filesystem/service.go:21),都带一个统一日志拦截器;文件系统还多挂一个 legacy.Convert() 兼容旧协议。

为什么 orchestrator-proxy 能打到用户自己的端口(如 3000)? 因为 envd 在后台跑了一个端口转发器:扫描 guest 内绑定在 127.0.0.1/localhost 的端口,把它们桥接到 eth0,这样节点代理才能从沙箱外访问到(main.go:219-227,NewScanner / NewForwarder / StartForwarding)。

用户身份怎么定? RPC 走 HTTP Basic 的用户名字段:AuthenticateUsername(packages/envd/internal/permissions/authenticate.go:14)把用户名查成 user.User 塞进 context;进程 StartGetAuthUser(authenticate.go:30)取出来,没给就回退到模板默认用户。envd 是以"沙箱内某个 Linux 用户"的身份执行你的命令的,不是一律 root。

3.5 envd 的鉴权:令牌、MMDS 哈希与 init 的特殊路径

要解决的小问题: envd 就在沙箱里、端口也暴露了,凭什么别的沙箱或攻击者不能随便连上来跑命令?

思路: envd 自带一层访问令牌(access token)校验。令牌一旦设置,除少数白名单路径外,所有请求都得带对 X-Access-Token(或对文件收发用签名参数)。

主校验: WithAuthorization(packages/envd/internal/api/auth.go:33):

// packages/envd/internal/api/auth.go:33 WithAuthorization —— 真实源码(节选)
if a.accessToken.IsSet() {
authHeader := req.Header.Get(accessTokenHeader) // "X-Access-Token"
allowedPath := slices.Contains(authExcludedPaths, req.Method+req.URL.Path)
if !a.accessToken.Equals(authHeader) && !allowedPath {
jsonError(w, http.StatusUnauthorized, ...); return
}
}

这段在做的事:令牌没设时全放行(初始/非安全沙箱),一旦设了就强制校验——除了白名单 GET/healthGET/filesPOST/filesPOST/init(auth.go:26)。文件收发之所以在白名单,是因为它们改用基于令牌的 HMAC 签名做鉴权(validateSigning auth.go:74,generateSignature :55),便于生成带时效的预签名 URL。

POST /init 为什么特殊? 它是 orchestrator 在恢复/启动沙箱时打进来设初始状态(环境变量、系统时间、访问令牌本身、CA、NFS 挂载)的入口——鸡生蛋:此刻令牌可能还没设好。所以 init 不走普通令牌校验,而是校验 MMDS 哈希:

  • validateInitAccessToken(packages/envd/internal/api/init.go:46):请求令牌等于现有令牌、或等于 MMDS 里登记的哈希,则放行;两者都无则视为首次装配放行。
  • MMDS 哈希由 orchestrator 在 Resume 时写入(checkMMDSHash init.go:76 的注释讲清了 hash(token) / hash("") / "" 三种含义),envd 从 microVM 的元数据服务读取比对。

令牌本身存得很小心: SecureToken(packages/envd/internal/api/secure_token.go:19)用 memguard 的锁定缓冲区存,带守护页、销毁时安全清零,比较用常数时间——避免令牌在内存里被读走或被计时侧信道猜出。

关键细节 / 坑:

  • 令牌在链路上的接力。 发往 envd(端口 49983)的流量,client-proxy 把 envd 令牌放在 X-Access-Token(MetadataEnvdHTTPAccessToken,packages/shared/pkg/grpc/proxy/metadata.go:15)里带下来;orchestrator-proxy 对 envd 端口跳过自己的 traffic-token 校验(isNonEnvdTraffic 为假,orchestrator/pkg/proxy/proxy.go:80-84),因为"envd 有它自己的校验机制"。校验最终落在 envd 的 WithAuthorization
  • 上传会连带写 xattr 元数据。 POST /files 支持 multipartapplication/octet-stream 两种(upload.go:239,332-340),并把 X-Metadata-* 头写成文件的 user.e2b.* 扩展属性(extractMetadataHeaders :35,processFile :139)。
  • 下载支持 gzip 与 Range。 GET /files(download.go:19)按 Accept-Encoding 决定裸传还是 gzip,遇到 Range/条件请求回退到 identity 以保留 206/304 语义(:120-136)。

4. 巧妙之处(可借鉴的技术)

  • "目录未命中 = 可能暂停"这一等价,把冷启动藏进了路由。 边缘代理不需要单独的"这台是不是睡了"探测——查不到就顺势走恢复旁路(proxy.go:79-88)。对调用方,访问一台暂停沙箱和访问一台运行沙箱看不出区别,只是慢一点。
  • 高权限操作(唤醒)用 OAuth 客户端凭证 + scope 收口。 唤醒别人的沙箱是敏感动作,边缘用短期 Bearer(scope sandboxes:lifecycle)自证身份,API 端再校验 scope/org(grpc_resume_auth.go + proxy_grpc.go:131-173)——横向越权在协议层就被挡住。
  • 同一份 host 解析,两层代理复用。 GetTargetFromRequest() 被边缘和节点同时使用,消除"两端对同一个 URL 理解不一致"的整类 bug(host.go:16)。
  • 鉴权分级白名单 + 预签名。 令牌校验对文件收发让路、改用带时效的 HMAC 签名(auth.go:26,74),既保住安全,又能生成可直接分享的下载/上传 URL。
  • 令牌用 memguard 常数时间比较。 把"令牌"当机密对待——锁页、销毁清零、防计时侧信道(secure_token.go)。
  • 超时逐层放大躲 LB 竞态。 610 < 620 < 640 秒的刻意梯度(边缘/节点/envd),避免上游先于下游超时导致的连接半关闭(proxy.go:34orchestrator proxy.go:32envd/main.go:34)。

5. 边界与局限(诚实说明)

  • 本章不展开 envd 的版本约定与构建/模板细节。 envd 的版本在 packages/envd/pkg/version.go,仓库约定"任何行为变更都要 bump",但这属于发布纪律,不影响本章的请求路径;沙箱镜像/模板如何把 envd 打进 guest 也不在此处。
  • 服务发现的真相以代码为准。 仓库 CLAUDE.md 描述的是 Consul,而 sourceCommitclient-proxy/main.go:128 实际注入 Redis 目录。文档随上游更新时,应以 RedisSandboxCatalog 这个符号为锚重新核对。
  • 恢复能否成功不由边缘决定。 边缘只发起 gRPC;能不能唤醒取决于 API 端的快照策略(如非 Any 的 auto-resume、纯文件系统快照拒绝隐式恢复)、团队是否被封禁、资源是否耗尽(proxy_grpc.go:113-124,175-177)。这些都会以特定错误码返回给用户。
  • 恢复本身的机制(内存快照 + userfaultfd 惰性缺页)不在本章。03-fast-resume-uffd.md;块存储层见 04-cow-block-storage.md
  • 网络隔离与出站管控不在本章。 每台沙箱的独立网络栈、eth0 桥接的更底层细节见 05-network-isolation.md;本章只用到"envd 把 localhost 端口转发到 eth0"这一结果。

6. 代码地图(导航索引)

用符号名 grep 比用行号更抗漂移(上游更新后行号会变,符号名通常还在)。

主题文件路径符号名
边缘代理启动、注入 Redis 目录与恢复器packages/client-proxy/main.gorun, NewRedisSandboxCatalog, NewGRPCPausedSandboxResumer
边缘路由决策 / 组装反代目标 :5007packages/client-proxy/internal/proxy/proxy.goNewClientProxy, catalogResolution, orchestratorProxyPort
目录未命中 → 触发恢复的旁路packages/client-proxy/internal/proxy/proxy.gohandlePausedSandbox, autoResumeResult
恢复 gRPC 客户端packages/client-proxy/internal/proxy/paused_sandbox_resumer_grpc.gogrpcPausedSandboxResumer, Resume
恢复 gRPC 的 OAuth 鉴权packages/client-proxy/internal/proxy/grpc_resume_auth.gooauthGrpcResumeAuth, grpcResumeAuthScope
恢复器接口packages/client-proxy/internal/proxy/paused_resumer.goPausedSandboxResumer
边缘配置(gRPC 地址、Redis、OAuth)packages/client-proxy/internal/cfg/model.goConfig, APIInternalGRPCAddress, APIEdgeGRPCAddress
边缘健康状态机(draining/unhealthy)packages/client-proxy/internal/info.goServiceInfo, ServiceHealth
从 URL/Header 解 id+port(两层复用)packages/shared/pkg/proxy/host.goGetTargetFromRequest, parseHost, parseHeaders
Redis 沙箱目录 / 节点 IPpackages/shared/pkg/sandbox-catalog/catalog.goSandboxInfo, OrchestratorIP, GetSandbox
gRPC metadata / scope 常量packages/shared/pkg/grpc/proxy/metadata.goMetadataEnvdHTTPAccessToken, ScopeSandboxLifecycle
控制面即时恢复处理器packages/api/internal/handlers/proxy_grpc.goResumeSandbox, getAutoResumeSnapshot, HandleExistingSandboxAutoResume
节点内反代(id→本机沙箱→guest)packages/orchestrator/pkg/proxy/proxy.goNewSandboxProxy, SandboxProxyRetries
envd 主服务(chi + Connect + authn)packages/envd/main.gorun, withCORS, defaultPort
envd 访问令牌鉴权 + 签名packages/envd/internal/api/auth.goWithAuthorization, authExcludedPaths, validateSigning
envd /init 与 MMDS 哈希校验packages/envd/internal/api/init.goPostInit, validateInitAccessToken, checkMMDSHash
令牌安全存储packages/envd/internal/api/secure_token.goSecureToken
文件上传(多段/裸流 + xattr 元数据)packages/envd/internal/api/upload.goPostFiles, processFile, extractMetadataHeaders
文件下载(gzip/Range)packages/envd/internal/api/download.goGetFiles
进程 RPC 服务packages/envd/internal/services/process/service.goHandle, Service, getProcess
文件系统 RPC 服务packages/envd/internal/services/filesystem/service.goHandle, Service
RPC 用户身份(Basic-Auth → user.User)packages/envd/internal/permissions/authenticate.goAuthenticateUsername, GetAuthUser

同组其它章:index.md · 01-architecture.md · 02-sandbox-lifecycle.md · 03-fast-resume-uffd.md · 04-cow-block-storage.md · 05-network-isolation.md