边缘路由与 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.md 与 03-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 反代进"正确的沙箱"。分工是:
- 边缘层解决**"在哪台机器"**(跨节点、跨集群的服务发现与唤醒);
- 节点层解决**"进哪台沙箱、哪个端口"**(本机沙箱表、连接限流、访问令牌校验)。
主线走一遍(高层、不进代码):
- 请求打到
client-proxy,它从host(或路由头)里解出sandbox_id与目标port。 - 拿
sandbox_id去 Redis 沙箱目录查:这台沙箱现在跑在哪个 orchestrator 节点? - 查到了 → 直接把请求反代到
节点IP:5007(orchestrator-proxy 的端口)。 - 没查到(很可能是被暂停了) → 用一次 gRPC 请求让 API 鉴权并即时 Resume,拿回唤醒后的节点 IP,再反代过去。
- 请求抵达节点,
orchestrator-proxy按 id 找到本机那台沙箱,把请求打到 guest 内envd的49983端口(或用户自己的服务端口)。 envd在沙箱内部执行:跑进程、读写文件、返回结果。
下面三节把 ①②③ 和 ⑤⑥ 拆开讲原理。
3. 核心原理(逐个机制,由浅入深)
3.1 第一步:从一个 URL 里解出"是哪台沙箱、哪个端口"
要解决的小问题: 边缘代理手上只有一个 HTTP 请求。沙箱的身份藏在哪?
思路: E2B 把身份编码进了域名的最左子域——形如 {port}-{sandboxId}.{域名}。所以解析就是"切最左边那段、按 - 拆开"。另外还支持用请求头直接传 id/port(给 IP 直连、本地开发等域名编不进信息的场景)。
真实实现: 入口是 GetTargetFromRequest(),它返回一个闭包,对每个请求先尝试路由头、再回退到解析 host。
真源码 packages/shared/pkg/proxy/host.go:74 parseHost:切掉第一个 . 之后的域名部分,再按 - 拆出 port 和 sandboxId。
// 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.共享子域时才先看头(shouldParseHeadersinhost.go:45);否则一律解 host。头的名字是E2b-Sandbox-Id/E2b-Sandbox-Port(host.go:110-111)。 - 解出来立刻校验 id 合法性(
id.ValidateSandboxID,host.go:24/37),非法直接ErrInvalidSandboxID,不进入下一步。 - 同一份解析逻辑被两层代理复用。
client-proxy和orchestrator-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)、envd640s(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.authorizeat: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 + org | proxy_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分别映射成"拒绝恢复 / 当作不存在 / 仍在转换中 / 资源耗尽"四类用户可见错误(handlePausedSandboxproxy.go:112-124)。 - "envd 流量"是特判的。 API 用
MetadataSandboxRequestPort是否等于49983来判断这次是不是发往 envd 本身(isNonEnvdTrafficRequestproxy_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 / List | Connect RPC(流式) | internal/services/process/*.go |
| 文件系统(元数据) | Stat / ListDir / MakeDir / Move / Remove / WatchDir / CreateWatcher … | Connect RPC | internal/services/filesystem/*.go |
| 文件(数据面) | GET /files(下载) / POST /files(上传) | REST(多段/裸流) | internal/api/download.go, upload.go |
| 沙箱初始化 | POST /init(设环境变量、时间、令牌、CA、NFS 挂载) | REST | internal/api/init.go |
| 冻结/解冻 | POST /freeze / POST /unfreeze(配合暂停) | REST | internal/api/init.go:257/290 |
| 健康 | GET /health | REST | internal/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 服务的挂载方式一致(Handle 里 spec.NewXxxHandler(service, interceptors) + server.Mount(path, h),见 process/service.go:35、filesystem/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;进程 Start 时 GetAuthUser(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/health、GET/files、POST/files、POST/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 时写入(
checkMMDSHashinit.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支持multipart与application/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:34、orchestrator proxy.go:32、envd/main.go:34)。
5. 边界与局限(诚实说明)
- 本章不展开 envd 的版本约定与构建/模板细节。 envd 的版本在
packages/envd/pkg/version.go,仓库约定"任何行为变更都要 bump",但这属于发布纪律,不影响本章的请求路径;沙箱镜像/模板如何把 envd 打进 guest 也不在此处。 - 服务发现的真相以代码为准。 仓库
CLAUDE.md描述的是 Consul,而sourceCommit的client-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.go | run, NewRedisSandboxCatalog, NewGRPCPausedSandboxResumer |
| 边缘路由决策 / 组装反代目标 :5007 | packages/client-proxy/internal/proxy/proxy.go | NewClientProxy, catalogResolution, orchestratorProxyPort |
| 目录未命中 → 触发恢复的旁路 | packages/client-proxy/internal/proxy/proxy.go | handlePausedSandbox, autoResumeResult |
| 恢复 gRPC 客户端 | packages/client-proxy/internal/proxy/paused_sandbox_resumer_grpc.go | grpcPausedSandboxResumer, Resume |
| 恢复 gRPC 的 OAuth 鉴权 | packages/client-proxy/internal/proxy/grpc_resume_auth.go | oauthGrpcResumeAuth, grpcResumeAuthScope |
| 恢复器接口 | packages/client-proxy/internal/proxy/paused_resumer.go | PausedSandboxResumer |
| 边缘配置(gRPC 地址、Redis、OAuth) | packages/client-proxy/internal/cfg/model.go | Config, APIInternalGRPCAddress, APIEdgeGRPCAddress |
| 边缘健康状态机(draining/unhealthy) | packages/client-proxy/internal/info.go | ServiceInfo, ServiceHealth |
| 从 URL/Header 解 id+port(两层复用) | packages/shared/pkg/proxy/host.go | GetTargetFromRequest, parseHost, parseHeaders |
| Redis 沙箱目录 / 节点 IP | packages/shared/pkg/sandbox-catalog/catalog.go | SandboxInfo, OrchestratorIP, GetSandbox |
| gRPC metadata / scope 常量 | packages/shared/pkg/grpc/proxy/metadata.go | MetadataEnvdHTTPAccessToken, ScopeSandboxLifecycle |
| 控制面即时恢复处理器 | packages/api/internal/handlers/proxy_grpc.go | ResumeSandbox, getAutoResumeSnapshot, HandleExistingSandboxAutoResume |
| 节点内反代(id→本机沙箱→guest) | packages/orchestrator/pkg/proxy/proxy.go | NewSandboxProxy, SandboxProxyRetries |
| envd 主服务(chi + Connect + authn) | packages/envd/main.go | run, withCORS, defaultPort |
| envd 访问令牌鉴权 + 签名 | packages/envd/internal/api/auth.go | WithAuthorization, authExcludedPaths, validateSigning |
envd /init 与 MMDS 哈希校验 | packages/envd/internal/api/init.go | PostInit, validateInitAccessToken, checkMMDSHash |
| 令牌安全存储 | packages/envd/internal/api/secure_token.go | SecureToken |
| 文件上传(多段/裸流 + xattr 元数据) | packages/envd/internal/api/upload.go | PostFiles, processFile, extractMetadataHeaders |
| 文件下载(gzip/Range) | packages/envd/internal/api/download.go | GetFiles |
| 进程 RPC 服务 | packages/envd/internal/services/process/service.go | Handle, Service, getProcess |
| 文件系统 RPC 服务 | packages/envd/internal/services/filesystem/service.go | Handle, Service |
| RPC 用户身份(Basic-Auth → user.User) | packages/envd/internal/permissions/authenticate.go | AuthenticateUsername, GetAuthUser |
同组其它章:index.md · 01-architecture.md · 02-sandbox-lifecycle.md · 03-fast-resume-uffd.md · 04-cow-block-storage.md · 05-network-isolation.md