托管代理和本地 IPC 传输基底
这一阶段是幕后打地基的通信层,让主机、代理、沙箱、IDE、提权进程之间能安全说话。网络代理这边,先把用户和组织规则合成可执行的权限表,再决定能连哪些外部地址、是否走上游代理;需要查看 HTTPS 时,就临时发证书、拆开请求,按规则删头或加密钥,并统一返回结果。另一边,本机通信用 Unix socket、Windows 命名管道等“本地电话线”,还支持把已打开的文件句柄传给别的进程。沙箱路由则像转接线,把宿主机代理接进隔离环境;IDE 通道负责向编辑器插件取当前文件和选区信息。
托管代理配置
这些文件定义有效的托管代理策略,以及决定代理执行在运行时应如何表现的钩子规则。
core/src/config/network_proxy_spec.rs源码 ↗
可以把这个文件理解成网络代理的“配菜师”和“开机师”。原始配置像用户点的菜,组织要求和沙箱权限像厨房红线:有些域名必须允许,有些必须禁止,有些用户能加,有些用户不能改。NetworkProxySpec 会先保存原始配置,再把这些红线套进去,得到一份最终配置和一份约束清单。启动代理时,它会把最终配置做成代理能读的状态,还会按情况接入审批流程:比如在受管沙箱里,访问没被允许的网站时可以询问;但如果是“只准组织白名单”的硬限制,就直接拒绝,不再问。它还支持把执行策略里的网络规则补进来,以及在代理已经跑起来后热更新配置。这样网络代理不会只听某一个来源的话,而是把多层规则合在一起后再行动。
StartedNetworkProxy::new38–43 ↗
fn new(proxy: NetworkProxy, handle: NetworkProxyHandle) -> Self
作用:创建一个“已经启动的网络代理”包装对象。它把代理本体和让代理继续运行的后台句柄绑在一起,避免代理刚启动就没人持有而停掉。
数据流:进去的是一个 NetworkProxy(代理本体)和一个 NetworkProxyHandle(运行句柄)→ 函数把它们放进 StartedNetworkProxy 结构里 → 出来的是一个可保存、可传递的已启动代理对象。
调用关系:NetworkProxySpec::start_proxy 在真正启动代理成功后会调用它,把刚建好的代理和运行句柄封装起来交给上层使用。
调用图:被 1 处调用(start_proxy)。
StartedNetworkProxy::proxy45–47 ↗
fn proxy(&self) -> NetworkProxy
作用:取出已启动代理里的代理本体副本,方便后续对代理发命令。这里返回的是克隆出来的句柄式对象,不是把原来的代理搬走。
数据流:进去的是 StartedNetworkProxy 自己 → 它读取内部保存的 proxy 并克隆一份 → 出来的是可用来操作代理的 NetworkProxy,同时原对象还保留自己的代理。
调用关系:NetworkProxySpec::apply_to_started_proxy 更新已运行代理配置时会先通过它拿到代理对象,再调用代理的替换配置能力。
调用图:被 1 处调用(apply_to_started_proxy);外部调用 1 个(clone)。
StaticNetworkProxyReloader::new56–58 ↗
fn new(state: ConfigState) -> Self
作用:创建一个固定配置重载器。所谓重载器,就是代理问“配置有没有变”时回答的人;这个版本永远拿同一份配置回答。
数据流:进去的是一份 ConfigState(代理可直接使用的配置状态)→ 函数把它保存起来 → 出来的是 StaticNetworkProxyReloader。
调用关系:NetworkProxySpec::build_state_with_audit_metadata 会用它做一个静态重载器,再交给 NetworkProxyState。这样代理框架有统一的重载接口,即使这里的配置不从文件动态读取。
调用图:被 1 处调用(build_state_with_audit_metadata)。
StaticNetworkProxyReloader::maybe_reload62–64 ↗
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>>
作用:回答“现在有没有新配置”。这个静态版本总是说没有,因为它不监控文件或外部来源。
数据流:进去的是重载器自己 → 它不读取新来源,也不改状态 → 异步返回 Ok(None),意思是没有新配置。
调用关系:它实现 ConfigReloader 接口,供网络代理框架在需要检查配置变化时调用。这里用 Box::pin 包起异步结果,是为了符合接口需要的异步形式。
调用图:外部调用 1 个(pin)。
StaticNetworkProxyReloader::reload_now66–68 ↗
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState>
作用:强制返回当前保存的配置。虽然它不会自动发现变化,但别人要求“现在给我一份配置”时,它会给出初始化时那份。
数据流:进去的是重载器自己 → 它克隆内部保存的 ConfigState → 异步返回这份配置,不修改原配置。
调用关系:它实现 ConfigReloader 接口,交给 NetworkProxyState 使用。NetworkProxySpec::build_state_with_audit_metadata 创建的代理状态会带着这个重载器。
调用图:外部调用 2 个(pin, clone)。
StaticNetworkProxyReloader::source_label70–72 ↗
fn source_label(&self) -> String
作用:给这个重载器起一个名字,方便日志或诊断时看出配置来源。这里的名字说明配置来自静态重载器。
数据流:进去的是重载器自己 → 它不读取内部状态 → 出来是字符串 StaticNetworkProxyReloader。
调用关系:它是 ConfigReloader 接口的一部分。代理框架需要展示或记录配置来源时会用到这个标签。
NetworkProxySpec::enabled76–78 ↗
fn enabled(&self) -> bool
作用:告诉调用者最终网络代理是否启用。它看的是合并规则之后的配置,不只是用户原始配置。
数据流:进去的是 NetworkProxySpec → 它读取 self.config.network.enabled → 出来是 true 或 false。
调用关系:这个小查询函数供同一模块或上层流程判断要不要启用网络代理。它不启动代理,也不改配置。
NetworkProxySpec::proxy_host_and_port80–82 ↗
fn proxy_host_and_port(&self) -> String
作用:把代理地址整理成“主机:端口”的形式,方便展示或传给别的组件。默认端口是 3128。
数据流:进去的是 NetworkProxySpec → 它读取最终配置里的 proxy_url → 交给 host_and_port_from_network_addr 解析和补默认端口 → 出来是类似 127.0.0.1:3128 的字符串。
调用关系:它依赖 codex_network_proxy 提供的地址解析工具,供需要知道 HTTP 代理监听地址的地方使用。
调用图:外部调用 1 个(host_and_port_from_network_addr)。
NetworkProxySpec::socks_enabled84–86 ↗
fn socks_enabled(&self) -> bool
作用:告诉调用者 SOCKS5 代理是否启用。SOCKS5 可以理解成另一种常见的代理协议。
数据流:进去的是 NetworkProxySpec → 它读取 self.config.network.enable_socks5 → 出来是 true 或 false。
调用关系:它是一个简单查询口,供启动或展示逻辑判断是否需要关心 SOCKS5 代理。
NetworkProxySpec::from_config_and_constraints88–120 ↗
fn from_config_and_constraints(
config: NetworkProxyConfig,
requirements: Option<NetworkConstraints>,
permission_profile: &PermissionProfile,
) -> std::io::Result<Self>
作用:这是创建 NetworkProxySpec 的主要入口。它把普通配置、组织网络限制和当前权限档位合成一份安全可用的代理规格。
数据流:进去的是 NetworkProxyConfig、可选的 NetworkConstraints、PermissionProfile → 它先保存原始配置,再判断是否是“只准组织白名单”的硬限制;如果有要求,就调用 apply_requirements 把要求写进配置并生成约束;最后调用 validate_policy_against_constraints 检查有没有自相矛盾 → 出来是 NetworkProxySpec,或返回输入无效的错误。
调用关系:构建网络代理规格的上层流程会调用它,很多测试也围绕它确认合并规则是否正确。后续 start_proxy、recompute_for_permission_profile 和 with_exec_policy_network_rules 都建立在它产生的规格之上。
调用图:被 25 处调用(build_network_proxy_spec, allow_only_requirements_do_not_create_deny_constraints_in_full_access, danger_full_access_keeps_managed_allowlist_and_denylist_fixed, deny_only_requirements_do_not_create_allow_constraints_in_full_access, managed_allowed_domains_only_blocks_all_user_domains_in_full_access_without_managed_list, managed_allowed_domains_only_disables_default_mode_allowlist_expansion, managed_allowed_domains_only_ignores_user_allowlist_and_hard_denies_misses, managed_allowed_domains_only_without_managed_allowlist_blocks_all_user_domains, managed_unrestricted_profile_allows_domain_expansion, requirements_allowed_domains_are_a_baseline_for_user_allowlist (+15 more));外部调用 4 个(apply_requirements, validate_policy_against_constraints, clone, default)。
NetworkProxySpec::start_proxy122–151 ↗
async fn start_proxy(
&self,
permission_profile: &PermissionProfile,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
blocked_request_observer: Option<Arc<dyn Blo
作用:按当前规格真正启动网络代理。它还会按权限档位接入审批或拦截观察功能。
数据流:进去的是权限档位、可选的审批决策器、可选的被拦请求观察器、是否开启网络审批流程、审计元数据 → 它先构建带审计信息的代理状态,再创建代理 builder;如果允许审批且不是硬拒绝白名单缺失,就装上外部审批器,或在受管沙箱里装一个默认“询问”的决策器;再装上拦截观察器 → 最后 build 并 run,出来是 StartedNetworkProxy,失败则返回启动错误。
调用关系:start_managed_network_proxy 会调用它完成实际启动。它内部会用 build_state_with_audit_metadata 做状态,用 StartedNetworkProxy::new 包装启动结果,也会参考 managed_sandbox_active 判断默认审批行为。
调用图:调用 3 个内部函数(build_state_with_audit_metadata, new, builder);被 1 处调用(start_managed_network_proxy);外部调用 2 个(new, managed_sandbox_active)。
NetworkProxySpec::recompute_for_permission_profile153–162 ↗
fn recompute_for_permission_profile(
&self,
permission_profile: &PermissionProfile,
) -> std::io::Result<Self>
作用:当权限档位变了时,重新算一遍网络代理规格。因为同一份配置在不同权限档位下,允许用户扩展白名单或黑名单的规则可能不一样。
数据流:进去的是现有 NetworkProxySpec 和新的 PermissionProfile → 它拿出保存的 base_config 和 requirements 克隆一份 → 重新调用 from_config_and_constraints → 出来是按新权限档位计算好的 NetworkProxySpec。
调用关系:它把重算工作交回主构造函数 from_config_and_constraints,避免两处逻辑不一致。权限切换流程会用这种函数保持网络规则跟权限同步。
调用图:外部调用 2 个(from_config_and_constraints, clone)。
NetworkProxySpec::with_exec_policy_network_rules164–177 ↗
fn with_exec_policy_network_rules(
&self,
exec_policy: &Policy,
) -> std::io::Result<Self>
作用:把执行策略里的网络域名规则补进当前代理规格。执行策略可以理解成“这次运行额外带来的允许/禁止名单”。
数据流:进去的是现有 NetworkProxySpec 和 Policy → 它克隆一份规格,调用 apply_exec_policy_network_rules 把策略里的域名加入最终配置,再用 validate_policy_against_constraints 确认没有违反组织约束 → 出来是带执行策略规则的新规格,或错误。
调用关系:start_managed_network_proxy 会在启动前用它把执行策略合进去。它把具体插入域名的活交给 apply_exec_policy_network_rules。
调用图:调用 1 个内部函数(apply_exec_policy_network_rules);被 1 处调用(start_managed_network_proxy);外部调用 1 个(validate_policy_against_constraints)。
NetworkProxySpec::apply_to_started_proxy179–191 ↗
async fn apply_to_started_proxy(
&self,
started_proxy: &StartedNetworkProxy,
) -> std::io::Result<()>
作用:把当前规格应用到一个已经运行的网络代理上,相当于热更新网络规则,不必重启整个代理。
数据流:进去的是 NetworkProxySpec 和 StartedNetworkProxy → 它先用 build_config_state_for_spec 生成代理能读的新状态,再通过 started_proxy.proxy() 拿到代理对象,调用 replace_config_state 替换配置 → 成功返回空结果,失败返回更新错误。
调用关系:它在配置或权限已经变化、但代理进程还在跑时使用。它依赖 StartedNetworkProxy::proxy 取得操作入口。
调用图:调用 2 个内部函数(build_config_state_for_spec, proxy)。
NetworkProxySpec::build_state_with_audit_metadata193–204 ↗
fn build_state_with_audit_metadata(
&self,
audit_metadata: NetworkProxyAuditMetadata,
) -> std::io::Result<NetworkProxyState>
作用:把当前规格做成网络代理运行时需要的完整状态,并带上审计信息。审计信息就是以后记录“谁、在什么上下文下用了这些网络规则”的材料。
数据流:进去的是 NetworkProxySpec 和 NetworkProxyAuditMetadata → 它先调用 build_config_state_for_spec 生成配置状态,再创建 StaticNetworkProxyReloader 保存同一份状态,最后用 NetworkProxyState::with_reloader_and_audit_metadata 打包 → 出来是 NetworkProxyState。
调用关系:NetworkProxySpec::start_proxy 启动代理前会调用它。它连接了配置状态、静态重载器和审计元数据这三个零件。
调用图:调用 3 个内部函数(build_config_state_for_spec, new, with_reloader_and_audit_metadata);被 1 处调用(start_proxy);外部调用 1 个(new)。
NetworkProxySpec::build_config_state_for_spec206–210 ↗
fn build_config_state_for_spec(&self) -> std::io::Result<ConfigState>
作用:把规格里的配置和约束转换成代理内部真正能执行的状态。可以理解成把说明书编译成机器能读的操作表。
数据流:进去的是 NetworkProxySpec → 它克隆最终配置和约束,交给 build_config_state → 出来是 ConfigState,失败时包装成标准 IO 错误。
调用关系:build_state_with_audit_metadata 启动前需要它,apply_to_started_proxy 热更新时也需要它。它是规格和实际代理运行状态之间的转换点。
调用图:被 2 处调用(apply_to_started_proxy, build_state_with_audit_metadata);外部调用 3 个(build_config_state, clone, clone)。
NetworkProxySpec::apply_requirements212–318 ↗
fn apply_requirements(
mut config: NetworkProxyConfig,
requirements: &NetworkConstraints,
permission_profile: &PermissionProfile,
hard_deny_allowlist_misses: bool,
作用:把组织或外部系统给出的硬性网络要求,逐项套到用户配置上,并同时记录哪些地方不能被用户突破。
数据流:进去的是可修改的 NetworkProxyConfig、NetworkConstraints、PermissionProfile,以及是否硬拒绝白名单外域名 → 它先判断白名单和黑名单是否允许用户扩展;然后依次处理启用开关、HTTP/SOCKS 端口、上游代理、本地/Unix socket 权限、允许域名、禁止域名等;需要合并域名时用 merge_domain_lists 去重合并 → 出来是一份最终配置和一份 NetworkProxyConstraints。
调用关系:from_config_and_constraints 在发现有 requirements 时调用它。它又会用 allowlist_expansion_enabled、denylist_expansion_enabled 和 merge_domain_lists 来决定用户配置能不能叠加到组织规则上。
调用图:外部调用 5 个(allowlist_expansion_enabled, denylist_expansion_enabled, merge_domain_lists, format!, default)。
NetworkProxySpec::allowlist_expansion_enabled320–325 ↗
fn allowlist_expansion_enabled(
permission_profile: &PermissionProfile,
hard_deny_allowlist_misses: bool,
) -> bool
作用:判断用户是否可以在组织允许域名名单之外再加自己的允许域名。只有受管沙箱里,并且没有开启“只准组织白名单”的硬模式时才允许。
数据流:进去的是 PermissionProfile 和 hard_deny_allowlist_misses → 它检查 managed_sandbox_active,并确认不是硬拒绝白名单缺失 → 出来是 true 或 false。
调用关系:apply_requirements 处理允许域名时调用它。它把“受管沙箱”和“硬白名单”这两个条件集中到一个判断里。
调用图:外部调用 1 个(managed_sandbox_active)。
NetworkProxySpec::managed_allowed_domains_only327–329 ↗
fn managed_allowed_domains_only(requirements: &NetworkConstraints) -> bool
作用:判断组织要求里是否写了“只能使用受管允许域名”。这是一种更严格的模式,用户的白名单扩展会被忽略。
数据流:进去的是 NetworkConstraints → 它读取 managed_allowed_domains_only 字段;如果没有写,就当作 false → 出来是 true 或 false。
调用关系:from_config_and_constraints 创建规格时会用它决定 hard_deny_allowlist_misses。这个结果会影响 apply_requirements 和 start_proxy 的审批行为。
NetworkProxySpec::denylist_expansion_enabled331–333 ↗
fn denylist_expansion_enabled(permission_profile: &PermissionProfile) -> bool
作用:判断用户是否可以在组织禁止域名名单之外再加自己的禁止域名。当前规则是:只要处在受管沙箱中,就可以扩展黑名单。
数据流:进去的是 PermissionProfile → 它调用 managed_sandbox_active 判断是否受管 → 出来是 true 或 false。
调用关系:apply_requirements 处理禁止域名时调用它,用来决定是只采用组织黑名单,还是把用户黑名单也合进去。
调用图:外部调用 1 个(managed_sandbox_active)。
NetworkProxySpec::managed_sandbox_active335–337 ↗
fn managed_sandbox_active(permission_profile: &PermissionProfile) -> bool
作用:判断当前权限档位是不是受管模式。受管模式可以理解成“系统或组织对运行环境有更强控制”。
数据流:进去的是 PermissionProfile → 它用模式匹配检查是否为 PermissionProfile::Managed → 出来是 true 或 false。
调用关系:start_proxy、allowlist_expansion_enabled 和 denylist_expansion_enabled 都会用它。它是很多网络权限分支的基础判断。
调用图:外部调用 1 个(matches!)。
NetworkProxySpec::merge_domain_lists339–349 ↗
fn merge_domain_lists(mut managed: Vec<String>, user_entries: &[String]) -> Vec<String>
作用:合并两份域名名单,并避免重复。它会忽略大小写来判断重复,比如 Example.com 和 example.com 算同一个。
数据流:进去的是一份 managed 域名列表和一份 user_entries 用户域名列表 → 它逐个检查用户域名是否已经在 managed 里;没有才追加 → 出来是合并后的列表。
调用关系:apply_requirements 在允许白名单或禁止黑名单可扩展时调用它。这样组织给的基础名单保留在前,用户新增项只补不重复。
apply_exec_policy_network_rules352–356 ↗
fn apply_exec_policy_network_rules(config: &mut NetworkProxyConfig, exec_policy: &Policy)
作用:把执行策略里的允许域名和禁止域名写进网络代理配置。它处理的是运行策略带来的临时网络规则。
数据流:进去的是可修改的 NetworkProxyConfig 和 Policy → 它从 Policy 取出编译后的 allowed_domains 和 denied_domains → 分别调用 upsert_network_domains,一个按允许写入,一个按禁止写入 → 配置被原地更新,不直接返回新对象。
调用关系:NetworkProxySpec::with_exec_policy_network_rules 会调用它。它自己不验证约束,验证由调用者随后用 validate_policy_against_constraints 完成。
调用图:调用 1 个内部函数(upsert_network_domains);被 1 处调用(with_exec_policy_network_rules);外部调用 1 个(compiled_network_domains)。
upsert_network_domains358–373 ↗
fn upsert_network_domains(config: &mut NetworkProxyConfig, hosts: Vec<String>, allow: bool)
作用:把一批域名加入网络配置里的允许名单或禁止名单;如果同一批里有重复域名,只处理一次。upsert 的意思是“有就更新,没有就插入”。
数据流:进去的是可修改的 NetworkProxyConfig、一组 host 字符串,以及 allow 标志 → 它先用 HashSet 过滤本次输入里的重复项;然后对每个域名调用 config.network.upsert_domain_permission,allow 为 true 就写允许,false 就写禁止,并用 normalize_host 规范化域名 → 配置里的域名权限被更新。
调用关系:apply_exec_policy_network_rules 会用它分别写入允许域名和禁止域名。它是执行策略域名落到具体网络配置里的最后一步。
调用图:被 1 处调用(apply_exec_policy_network_rules);外部调用 1 个(new)。
network-proxy/src/mitm_hook.rs源码 ↗
MITM 是“中间人”检查的意思,可以理解成代理站在客户端和网站中间,看清请求内容后再决定要不要动手。这个文件做三件事:先检查配置是不是安全、完整;再把配置里的字符串规则编译成运行时好用的匹配器;最后在每个请求到来时判断它是否命中某条规则。规则会看主机名、HTTP 方法(比如 GET、POST)、路径、查询参数和请求头。匹配可以是普通文字,也可以用 pattern: 开头表示通配模式。动作主要是删请求头、注入请求头,注入的秘密只能来自环境变量或绝对路径文件二选一。这里特别谨慎:host 不允许通配,body 匹配暂时直接拒绝,密钥文件必须是绝对路径,避免配置一含糊就泄露凭据。
CompiledGlobMatcher::fmt141–145 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
作用:给通配匹配器生成调试时看的文字。它只展示原始 pattern,不把内部复杂对象展开,方便看日志或测试失败信息。
数据流:进去的是一个已编译的通配匹配器和格式化器 → 它把保存的 pattern 字符串写进调试结构里 → 出来的是格式化结果,不改变匹配器本身。
调用关系:当代码或测试需要打印 CompiledGlobMatcher 时会走到这里;它把具体排版工作交给外部的 debug_struct 工具。
调用图:外部调用 1 个(debug_struct)。
CompiledGlobMatcher::eq149–151 ↗
fn eq(&self, other: &Self) -> bool
作用:判断两个已编译通配匹配器是否算同一个。这里用原始 pattern 字符串比较,而不是比较内部编译对象。
数据流:进去的是两个匹配器 → 它比较两边保存的 pattern → 出来 true 或 false,不改任何数据。
调用关系:这是 PartialEq 的实现,测试和结构体比较时会自动用到;它让包含通配匹配器的配置结果可以被直接比较。
CompiledGlobMatcher::is_match157–159 ↗
fn is_match(&self, candidate: &str) -> bool
作用:用已经编译好的通配规则去检查一个字符串是否符合。比如检查路径或请求头值是不是匹配 pattern: 开头的规则。
数据流:进去的是候选字符串 → 它交给内部 GlobMatcher 做匹配 → 出来一个布尔值,表示命中或不命中。
调用关系:PathMatcher::matches 和 ValueMatcher::matches 会通过它完成真正的通配判断;它把底层匹配工作交给 globset 库。
调用图:外部调用 1 个(is_match)。
validate_mitm_hook_config171–229 ↗
fn validate_mitm_hook_config(config: &NetworkProxyConfig) -> Result<()>
作用:在程序接受 MITM hook 配置前做安全体检。它会拦住空方法、空路径、非法请求头、错误密钥来源等问题,防止代理运行后才出错或误泄露秘密。
数据流:进去的是完整网络代理配置 → 它逐条检查 hook:MITM 是否开启、host 是否合法、方法和路径是否非空、查询参数和请求头规则是否能解析、动作是否合法 → 出来是成功或带上下文的错误,不生成运行时规则。
调用关系:compile_mitm_hooks_with_resolvers 在真正编译前一定先调用它;配置校验流程 validate_policy_against_constraints 和多项测试也直接调用它,确认坏配置会被尽早拒绝。
调用图:调用 7 个内部函数(compile_path_matchers, normalize_hook_host, normalize_methods, validate_header_constraints, validate_injected_headers, validate_query_constraints, validate_strip_request_headers);被 8 处调用(compile_mitm_hooks_with_resolvers, validate_allows_hooks_in_full_mode, validate_rejects_body_matchers_for_now, validate_rejects_dual_secret_sources, validate_rejects_invalid_wildcard_path_pattern, validate_rejects_relative_secret_file, validate_requires_mitm_for_hooks, validate_policy_against_constraints);外部调用 1 个(anyhow!)。
compile_mitm_hooks231–242 ↗
fn compile_mitm_hooks(config: &NetworkProxyConfig) -> Result<MitmHooksByHost>
作用:把用户配置里的 MITM hook 变成代理运行时可以快速使用的规则表。它还会从环境变量或文件里读取要注入的秘密值。
数据流:进去的是网络代理配置 → 它准备两个真实解析器:一个读环境变量,一个读密钥文件并去掉首尾空白 → 交给 compile_mitm_hooks_with_resolvers → 出来按 host 分组的已编译 hook 表。
调用关系:这是生产代码常用入口,build_config_state 和 network_proxy_state_for_policy 会用它建立运行状态;测试文件密钥读取时也会调用它。
调用图:调用 1 个内部函数(compile_mitm_hooks_with_resolvers);被 3 处调用(compile_resolves_file_backed_injected_headers, network_proxy_state_for_policy, build_config_state)。
evaluate_mitm_hooks244–263 ↗
fn evaluate_mitm_hooks(
hooks_by_host: &MitmHooksByHost,
host: &str,
req: &Request,
) -> HookEvaluation
作用:在请求到来时判断这个请求是否命中某个 MITM hook。它会返回三种结果:这个 host 没配置 hook、命中了并给出动作、host 有 hook 但请求条件没对上。
数据流:进去的是按 host 分组的 hook 表、请求目标 host、HTTP 请求 → 它先标准化 host,再取出这个 host 的 hook 列表,按顺序找第一个匹配项 → 出来是 HookEvaluation,命中时会带上要执行的动作副本。
调用关系:请求处理流程 evaluate_mitm_hook_request 会调用它;它自己把单条规则判断交给 hook_matches。
调用图:调用 2 个内部函数(hook_matches, normalize_host);被 2 处调用(evaluate_returns_first_matching_hook, evaluate_mitm_hook_request);外部调用 1 个(get)。
compile_mitm_hooks_with_resolvers265–339 ↗
fn compile_mitm_hooks_with_resolvers(
config: &NetworkProxyConfig,
resolve_env_var: EnvFn,
read_secret_file: FileFn,
) -> Result<MitmHooksByHost>
作用:这是编译 hook 的核心版本,允许调用者自定义“怎么读环境变量”和“怎么读文件”。这样生产代码能读真实系统,测试代码能用假的密钥来源。
数据流:进去的是配置、环境变量解析函数、文件读取函数 → 它先校验配置,再把 host、方法、路径、查询参数、请求头、删除动作、注入动作逐项转换成运行时结构 → 出来按标准化 host 分组的 MitmHooksByHost。
调用关系:compile_mitm_hooks 会用真实解析器调用它;大量测试用假解析器调用它,避免依赖真实环境变量或文件。
调用图:调用 4 个内部函数(compile_path_matchers, normalize_hook_host, normalize_methods, validate_mitm_hook_config);被 9 处调用(compile_mitm_hooks, compile_resolves_env_backed_injected_headers, evaluate_allows_literal_values_with_reserved_prefixes, evaluate_matches_query_and_header_constraints, evaluate_matches_wildcard_path_query_and_header_constraints, evaluate_path_wildcard_does_not_cross_segment_boundaries, evaluate_returns_first_matching_hook, evaluate_returns_hooked_host_no_match_when_query_constraint_fails, evaluate_treats_glob_metacharacters_as_literal_without_glob_prefix);外部调用 1 个(new)。
compile_injected_header341–381 ↗
fn compile_injected_header(
header: &InjectedHeaderConfig,
resolve_env_var: &EnvFn,
read_secret_file: &FileFn,
) -> Result<ResolvedInjectedHeader>
作用:把“要注入一个请求头”的配置变成真实可写入请求的请求头。它负责读取秘密、加前缀,并检查请求头名和值是否合法。
数据流:进去的是注入请求头配置,以及读取环境变量/文件的函数 → 它解析 header 名,确认 secret_env_var 和 secret_file 正好有一个,读出秘密,加上 prefix,再转成 HTTP HeaderValue → 出来 ResolvedInjectedHeader,里面带名字、值和秘密来源记录。
调用关系:compile_mitm_hooks_with_resolvers 在编译每个注入动作时会调用它;它会把 header 名解析交给 parse_header_name,把文件路径检查交给 parse_secret_file。
调用图:调用 2 个内部函数(parse_header_name, parse_secret_file);外部调用 5 个(from_str, anyhow!, format!, EnvVar, File)。
hook_matches383–404 ↗
fn hook_matches(hook: &MitmHook, req: &Request) -> bool
作用:判断一条 hook 规则是否匹配一个具体请求。它像门卫一样逐关检查:方法、路径、查询参数、请求头,任何一关不符合就拒绝。
数据流:进去的是一条已编译 hook 和一个请求 → 它读取请求方法和 URI,依次检查方法列表、路径匹配、查询参数匹配、请求头匹配 → 出来 true 或 false,不改请求。
调用关系:evaluate_mitm_hooks 会对同一 host 下的 hook 逐个调用它;它把细分检查交给 path_matches、query_matches 和 headers_match。
调用图:调用 3 个内部函数(headers_match, path_matches, query_matches);被 1 处调用(evaluate_mitm_hooks);外部调用 2 个(method, uri)。
query_matches406–430 ↗
fn query_matches(query_constraints: &[QueryConstraint], req: &Request) -> bool
作用:检查请求 URL 后面的查询参数是否满足 hook 要求。比如 ?state=open 里 state 必须是 open 或匹配某个通配规则。
数据流:进去的是查询参数约束列表和请求 → 如果没有约束就直接通过;否则解析请求 URI 的 query 字符串,按参数名收集实际值,再逐个确认每个约束至少有一个实际值命中允许规则 → 出来 true 或 false。
调用关系:hook_matches 在路径通过后调用它;它使用 URL 表单解析工具处理 percent-encoding 这类 URL 编码细节。
调用图:被 1 处调用(hook_matches);外部调用 5 个(new, uri, parse, is_empty, iter)。
headers_match432–451 ↗
fn headers_match(header_constraints: &[HeaderConstraint], req: &Request) -> bool
作用:检查请求头是否满足 hook 要求。它既能要求某个请求头存在,也能要求它的值等于某些文字或匹配通配模式。
数据流:进去的是请求头约束列表和请求 → 它逐个读取对应请求头;没有这个头就失败;如果允许值列表为空,只要存在就算通过;否则把头值转成文字再做匹配 → 出来 true 或 false。
调用关系:hook_matches 最后一关会调用它;值的具体判断交给 ValueMatcher::matches。
调用图:被 1 处调用(hook_matches);外部调用 1 个(iter)。
path_matches453–455 ↗
fn path_matches(path_prefixes: &[PathMatcher], path: &str) -> bool
作用:检查请求路径是否命中任意一个配置的路径规则。路径规则可以是普通前缀,也可以是通配模式。
数据流:进去的是路径匹配器列表和实际路径字符串 → 它逐个试,任意一个匹配就算成功 → 出来 true 或 false。
调用关系:hook_matches 在方法检查后调用它;单个匹配器的判断交给 PathMatcher::matches。
调用图:被 1 处调用(hook_matches);外部调用 1 个(iter)。
PathMatcher::matches458–463 ↗
fn matches(&self, candidate: &str) -> bool
作用:让一个路径匹配器检查某条路径。普通 Prefix 是“是否以这个前缀开头”,Glob 是“是否符合通配规则”。
数据流:进去的是匹配器和候选路径 → 如果是 Prefix 就做 starts_with;如果是 Glob 就交给已编译通配匹配器 → 出来 true 或 false。
调用关系:path_matches 会循环调用它;通配分支会继续用 CompiledGlobMatcher::is_match。
ValueMatcher::matches467–472 ↗
fn matches(&self, candidate: &str) -> bool
作用:让一个值匹配器检查查询参数值或请求头值。它支持精确相等,也支持通配模式。
数据流:进去的是匹配器和候选值 → Exact 分支直接比较字符串;Glob 分支用已编译通配规则检查 → 出来 true 或 false。
调用关系:query_matches 和 headers_match 会调用它来判断实际值是否在允许范围内。
compile_path_matchers475–493 ↗
fn compile_path_matchers(path_prefixes: &[String]) -> Result<Vec<PathMatcher>>
作用:把配置里的路径字符串变成可执行的路径匹配器。它会区分普通前缀、literal: 强制文字、pattern: 通配模式。
数据流:进去的是路径规则字符串列表 → 它逐个解析规则类型;普通文字变成 Prefix,pattern: 后面的内容编译成 Glob;空前缀会报错 → 出来 PathMatcher 列表或错误。
调用关系:validate_mitm_hook_config 用它提前发现坏路径规则;compile_mitm_hooks_with_resolvers 用它生成运行时 matcher。
调用图:被 2 处调用(compile_mitm_hooks_with_resolvers, validate_mitm_hook_config)。
compile_value_matchers495–506 ↗
fn compile_value_matchers(values: &[String]) -> Result<Vec<ValueMatcher>>
作用:把查询参数值或请求头值的配置字符串变成可执行的值匹配器。默认按普通文字处理,只有 pattern: 才表示通配。
数据流:进去的是允许值字符串列表 → 它逐个解析 literal:/pattern: 前缀;普通文字变 Exact,通配文字编译成 Glob → 出来 ValueMatcher 列表或错误。
调用关系:validate_query_constraints 和 validate_header_constraints 会调用它,确保值规则写得对;编译查询和请求头约束时也依赖同样规则。
调用图:被 2 处调用(validate_header_constraints, validate_query_constraints)。
parse_matcher_pattern508–519 ↗
fn parse_matcher_pattern(pattern: &str) -> Result<MatcherPattern<'_>>
作用:判断一个配置字符串到底是普通文字还是通配规则。它规定 literal: 表示后面内容必须当普通文字,pattern: 表示后面内容才是通配模式。
数据流:进去的是一段配置字符串 → 如果以 literal: 开头就剥掉前缀并返回 Literal;如果以 pattern: 开头就检查后面非空并返回 Glob;否则整段当 Literal → 出来 MatcherPattern 或错误。
调用关系:compile_path_matchers 和 compile_value_matchers 都靠它统一解释前缀,避免路径和值两套规则不一致。
调用图:外部调用 3 个(anyhow!, Glob, Literal)。
compile_glob_matcher521–533 ↗
fn compile_glob_matcher(pattern: &str, literal_separator: bool) -> Result<CompiledGlobMatcher>
作用:把通配模式字符串编译成真正能快速匹配的对象。通配模式可以理解成带星号的筛选规则,比如 op* 匹配 open。
数据流:进去的是 pattern 字符串和 literal_separator 开关 → 它创建 GlobBuilder,打开反斜杠转义,并设置星号是否能跨路径分隔符 → 编译成功就返回 CompiledGlobMatcher,失败就返回说明哪个 pattern 坏了的错误。
调用关系:compile_path_matchers 和 compile_value_matchers 在遇到 pattern: 时会调用它;路径匹配会把 literal_separator 设为 true,防止 * 随便跨过 /。
调用图:外部调用 1 个(new)。
normalize_hook_host535–546 ↗
fn normalize_hook_host(host: &str) -> Result<String>
作用:把 hook 配置里的 host 统一成标准写法,并拒绝危险写法。MITM hook 的 host 必须是精确主机名,不能写 * 这种通配。
数据流:进去的是 host 字符串 → 它先用 normalize_host 统一大小写等格式,再检查非空、不能包含星号 → 出来标准化 host 或错误。
调用关系:validate_mitm_hook_config 和 compile_mitm_hooks_with_resolvers 都会调用它,保证校验阶段和运行阶段看到的是同一套 host 规则。
调用图:调用 1 个内部函数(normalize_host);被 2 处调用(compile_mitm_hooks_with_resolvers, validate_mitm_hook_config);外部调用 1 个(anyhow!)。
normalize_methods548–559 ↗
fn normalize_methods(methods: &[String]) -> Result<Vec<String>>
作用:把 HTTP 方法配置清理成统一的大写格式。这样用户写 post、POST 或带空格的 POST,后续比较都能一致。
数据流:进去的是方法字符串列表 → 它逐个 trim 去空白并转大写,发现空项就报错 → 出来标准化方法列表。
调用关系:validate_mitm_hook_config 用它检查方法是否合法;compile_mitm_hooks_with_resolvers 用它生成实际匹配时比较的列表。
调用图:被 2 处调用(compile_mitm_hooks_with_resolvers, validate_mitm_hook_config)。
validate_query_constraints561–576 ↗
fn validate_query_constraints(query: &BTreeMap<String, Vec<String>>) -> Result<()>
作用:检查查询参数匹配规则是否合理。它会阻止空参数名、没有允许值的参数,以及写错的通配规则。
数据流:进去的是配置里的 query 映射表 → 它逐个标准化参数名,确认允许值列表非空,并尝试编译这些值匹配器 → 成功返回 Ok,失败返回带参数名的错误。
调用关系:validate_mitm_hook_config 在检查每条 hook 时调用它;它把名称检查交给 normalize_query_name,把值规则检查交给 compile_value_matchers。
调用图:调用 2 个内部函数(compile_value_matchers, normalize_query_name);被 1 处调用(validate_mitm_hook_config);外部调用 1 个(anyhow!)。
normalize_query_name578–583 ↗
fn normalize_query_name(name: &str) -> Result<String>
作用:检查查询参数名不能是空字符串。当前它不改名字,只做最基本的合法性确认。
数据流:进去的是参数名 → 如果为空就返回错误,否则复制成 String 返回 → 不改外部数据。
调用关系:validate_query_constraints 会调用它;compile_mitm_hooks_with_resolvers 在生成 QueryConstraint 时也用同样逻辑保持一致。
调用图:被 1 处调用(validate_query_constraints);外部调用 1 个(anyhow!)。
validate_header_constraints585–592 ↗
fn validate_header_constraints(headers: &BTreeMap<String, Vec<String>>) -> Result<()>
作用:检查请求头匹配规则是否能被 HTTP 规则接受。请求头名必须合法,值匹配规则也必须能编译。
数据流:进去的是 header 约束映射表 → 它逐个解析 header 名,再编译允许值匹配器 → 全部通过返回 Ok,否则返回具体错误。
调用关系:validate_mitm_hook_config 调用它;它依赖 parse_header_name 和 compile_value_matchers。
调用图:调用 2 个内部函数(compile_value_matchers, parse_header_name);被 1 处调用(validate_mitm_hook_config)。
validate_strip_request_headers594–599 ↗
fn validate_strip_request_headers(header_names: &[String]) -> Result<()>
作用:检查“要删除哪些请求头”的名单是否都是真正合法的请求头名。这样运行时删除动作不会因为名字非法而失败。
数据流:进去的是请求头名字符串列表 → 它逐个调用 parse_header_name 试解析 → 全部合法返回 Ok,否则返回错误。
调用关系:validate_mitm_hook_config 在检查 actions.strip_request_headers 时调用它。
调用图:调用 1 个内部函数(parse_header_name);被 1 处调用(validate_mitm_hook_config)。
validate_injected_headers601–624 ↗
fn validate_injected_headers(headers: &[InjectedHeaderConfig]) -> Result<()>
作用:检查“要注入哪些请求头”的配置是否安全完整。它特别要求秘密来源只能二选一:环境变量或密钥文件。
数据流:进去的是注入请求头配置列表 → 它逐个检查 header 名,检查 secret_env_var 非空或 secret_file 是合法绝对路径,并拒绝两个都填或两个都不填 → 成功返回 Ok,失败返回错误。
调用关系:validate_mitm_hook_config 在检查 actions.inject_request_headers 时调用它;真正编译时 compile_injected_header 会再次按这些规则读取秘密。
调用图:调用 2 个内部函数(parse_header_name, parse_secret_file);被 1 处调用(validate_mitm_hook_config);外部调用 1 个(anyhow!)。
parse_header_name626–629 ↗
fn parse_header_name(name: &str) -> Result<HeaderName>
作用:把字符串形式的请求头名转成 HTTP 库认可的 HeaderName。它会拒绝包含非法字符的名字。
数据流:进去的是 header 名字符串 → 它按字节交给 HTTP 库解析 → 出来 HeaderName,或带原名字和原因的错误。
调用关系:请求头相关的校验和编译都会调用它,包括 validate_header_constraints、validate_strip_request_headers、validate_injected_headers 和 compile_injected_header。
调用图:被 4 处调用(compile_injected_header, validate_header_constraints, validate_injected_headers, validate_strip_request_headers);外部调用 1 个(from_bytes)。
parse_secret_file631–641 ↗
fn parse_secret_file(path: &str) -> Result<AbsolutePathBuf>
作用:检查密钥文件路径是否可接受,并转成项目里的绝对路径类型。它强制必须写绝对路径,避免运行目录变化导致读错文件。
数据流:进去的是路径字符串 → 它先拒绝空白路径,再确认是绝对路径,最后转成 AbsolutePathBuf → 出来绝对路径对象或错误。
调用关系:validate_injected_headers 用它提前检查配置;compile_injected_header 用它确认并读取文件来源的秘密。
调用图:调用 1 个内部函数(from_absolute_path);被 2 处调用(compile_injected_header, validate_injected_headers);外部调用 2 个(new, anyhow!)。
tests::base_config653–661 ↗
fn base_config() -> NetworkProxyConfig
作用:为测试生成一份最小可用的网络代理配置。默认打开 MITM,并使用受限网络模式。
数据流:进去没有参数 → 它创建 NetworkProxyConfig,填入测试需要的 network 设置,其余使用默认值 → 出来一份可被各测试修改的配置。
调用关系:几乎所有测试都从它开始,再按测试目标改某些字段,避免每个测试重复写大段配置。
调用图:调用 1 个内部函数(default)。
tests::github_hook663–681 ↗
fn github_hook() -> MitmHookConfig
作用:为测试生成一条典型的 GitHub hook。它匹配 api.github.com 上的 POST/PUT 仓库路径,并把 authorization 头替换成来自环境变量的 Bearer token。
数据流:进去没有参数 → 它构造 MitmHookConfig,包括 host、方法、路径前缀、删除 authorization、注入 authorization 的配置 → 出来一条可复用的测试 hook。
调用关系:很多测试用它作为基础样板,再修改 body、secret_file、query、headers 或路径规则来测试不同情况。
tests::validate_requires_mitm_for_hooks684–694 ↗
fn validate_requires_mitm_for_hooks()
作用:验证只要配置了 MITM hook,就必须打开 MITM。否则代理看不到加密请求内容,也就不能安全执行 hook。
数据流:进去没有外部输入 → 它创建基础配置,关闭 mitm,加入 GitHub hook,然后调用 validate_mitm_hook_config → 期望得到包含指定提示的错误。
调用关系:这个测试直接覆盖 validate_mitm_hook_config 的前置安全检查。
调用图:调用 1 个内部函数(validate_mitm_hook_config);外部调用 3 个(assert!, base_config, vec!)。
tests::validate_allows_hooks_in_full_mode697–703 ↗
fn validate_allows_hooks_in_full_mode()
作用:验证在 Full 网络模式下也允许使用 MITM hook。也就是说 hook 的合法性不被网络模式 Full 本身阻止。
数据流:进去没有外部输入 → 它创建基础配置,把模式改成 Full,加入 GitHub hook,再校验 → 期望成功。
调用关系:这个测试调用 validate_mitm_hook_config,确保配置策略不会误伤 Full 模式。
调用图:调用 1 个内部函数(validate_mitm_hook_config);外部调用 2 个(base_config, vec!)。
tests::validate_rejects_body_matchers_for_now706–716 ↗
fn validate_rejects_body_matchers_for_now()
作用:验证 body 匹配目前还不能用。虽然配置结构里预留了 body,但现在一旦填写就应该明确报错。
数据流:进去没有外部输入 → 它在 GitHub hook 里加入 JSON body 匹配配置,再调用校验 → 期望错误信息说明 body matcher 仍是未来功能。
调用关系:这个测试保护 validate_mitm_hook_config 里的“暂不支持 body”行为,避免半成品功能被悄悄启用。
调用图:调用 1 个内部函数(validate_mitm_hook_config);外部调用 5 个(assert!, base_config, github_hook, json!, vec!)。
tests::validate_rejects_relative_secret_file719–728 ↗
fn validate_rejects_relative_secret_file()
作用:验证密钥文件不能写相对路径。这样可以避免程序从意外目录读取到错误的秘密文件。
数据流:进去没有外部输入 → 它把注入头的秘密来源从环境变量改成 token.txt 这种相对文件路径,再校验 → 期望报“必须是绝对路径”的错误。
调用关系:这个测试通过 validate_mitm_hook_config 间接覆盖 validate_injected_headers 和 parse_secret_file。
调用图:调用 1 个内部函数(validate_mitm_hook_config);外部调用 4 个(assert!, base_config, github_hook, vec!)。
tests::validate_rejects_dual_secret_sources731–739 ↗
fn validate_rejects_dual_secret_sources()
作用:验证同一个注入请求头不能同时配置环境变量和文件两个秘密来源。来源必须清楚,不能含糊。
数据流:进去没有外部输入 → 它让 GitHub hook 同时保留 secret_env_var 并加上 secret_file,再调用校验 → 期望得到“二选一”的错误。
调用关系:这个测试覆盖 validate_injected_headers 的关键安全规则。
调用图:调用 1 个内部函数(validate_mitm_hook_config);外部调用 4 个(assert!, base_config, github_hook, vec!)。
tests::compile_resolves_env_backed_injected_headers742–763 ↗
fn compile_resolves_env_backed_injected_headers()
作用:验证环境变量来源的秘密会在编译 hook 时被读出,并和 prefix 拼成最终请求头值。
数据流:进去没有真实环境依赖 → 它用假的环境变量解析函数返回 ghp-secret,编译配置 → 检查编译结果记录了 EnvVar 来源,且请求头值是 Bearer ghp-secret。
调用关系:这个测试直接调用 compile_mitm_hooks_with_resolvers,覆盖 compile_injected_header 的环境变量分支。
调用图:调用 1 个内部函数(compile_mitm_hooks_with_resolvers);外部调用 3 个(assert_eq!, base_config, vec!)。
tests::compile_resolves_file_backed_injected_headers766–783 ↗
fn compile_resolves_file_backed_injected_headers()
作用:验证文件来源的秘密会被读取,并且文件末尾换行会被去掉。密钥文件常常带换行,这里确保不会把换行塞进请求头。
数据流:进去没有固定文件 → 它创建临时文件并写入 ghp-file-secret 加换行,把 hook 改成从该文件读,调用 compile_mitm_hooks → 检查最终请求头值没有换行。
调用关系:这个测试覆盖生产入口 compile_mitm_hooks 的文件读取行为。
调用图:调用 1 个内部函数(compile_mitm_hooks);外部调用 6 个(new, assert_eq!, base_config, github_hook, write, vec!)。
tests::evaluate_returns_first_matching_hook786–816 ↗
fn evaluate_returns_first_matching_hook()
作用:验证如果同一个 host 下有多条都能匹配的 hook,会使用第一条。顺序在这种配置里很重要。
数据流:进去没有外部输入 → 它配置两条相似 hook,第二条注入不同 prefix,编译后构造一个匹配请求并评估 → 期望拿到第一条的 Bearer abc,而不是第二条的 Token abc。
调用关系:这个测试同时覆盖 compile_mitm_hooks_with_resolvers 和 evaluate_mitm_hooks 的按顺序匹配行为。
调用图:调用 2 个内部函数(compile_mitm_hooks_with_resolvers, evaluate_mitm_hooks);外部调用 7 个(assert_eq!, builder, empty, base_config, github_hook, panic!, vec!)。
tests::evaluate_matches_query_and_header_constraints819–851 ↗
fn evaluate_matches_query_and_header_constraints()
作用:验证 hook 可以同时要求查询参数和请求头满足指定值。只有路径、方法、query、header 都对上才算命中。
数据流:进去没有外部输入 → 它给 hook 加 state 查询参数约束和 x-github-api-version 请求头约束,构造满足这些条件的请求 → 期望 evaluate_mitm_hooks 返回 Matched。
调用关系:这个测试覆盖 hook_matches、query_matches 和 headers_match 协同工作的正常路径。
调用图:调用 1 个内部函数(compile_mitm_hooks_with_resolvers);外部调用 7 个(from, assert_eq!, builder, empty, base_config, github_hook, vec!)。
tests::evaluate_matches_wildcard_path_query_and_header_constraints854–885 ↗
fn evaluate_matches_wildcard_path_query_and_header_constraints()
作用:验证 pattern: 通配模式能用于路径、查询参数值和请求头值。这样配置可以匹配一类相似请求,而不是只能写死一个值。
数据流:进去没有外部输入 → 它配置带星号的路径、query 和 header 规则,构造符合通配模式的请求 → 期望命中 hook。
调用关系:这个测试覆盖 compile_glob_matcher、PathMatcher::matches 和 ValueMatcher::matches 的通配分支。
调用图:调用 1 个内部函数(compile_mitm_hooks_with_resolvers);外部调用 7 个(from, assert_eq!, builder, empty, base_config, github_hook, vec!)。
tests::validate_rejects_invalid_wildcard_path_pattern888–896 ↗
fn validate_rejects_invalid_wildcard_path_pattern()
作用:验证写坏的通配路径会在配置校验时被拒绝。比如不完整的方括号模式不能等到运行时才炸。
数据流:进去没有外部输入 → 它把路径规则改成 pattern:/repos/[,再调用 validate_mitm_hook_config → 期望错误中包含 invalid glob pattern。
调用关系:这个测试通过 validate_mitm_hook_config 覆盖 compile_path_matchers 和 compile_glob_matcher 的错误传播。
调用图:调用 1 个内部函数(validate_mitm_hook_config);外部调用 4 个(assert!, base_config, github_hook, vec!)。
tests::evaluate_path_wildcard_does_not_cross_segment_boundaries899–921 ↗
fn evaluate_path_wildcard_does_not_cross_segment_boundaries()
作用:验证路径里的通配星号不会随便跨过斜杠。这样 /repos/*/codex 不会错误匹配 /repos/openai/private/codex。
数据流:进去没有外部输入 → 它配置路径通配规则,构造多了一层路径段的请求 → 期望结果是 HookedHostNoMatch。
调用关系:这个测试保护 compile_glob_matcher 对路径设置 literal_separator 的行为,防止路径匹配范围过宽。
调用图:调用 1 个内部函数(compile_mitm_hooks_with_resolvers);外部调用 6 个(assert_eq!, builder, empty, base_config, github_hook, vec!)。
tests::evaluate_treats_glob_metacharacters_as_literal_without_glob_prefix924–964 ↗
fn evaluate_treats_glob_metacharacters_as_literal_without_glob_prefix()
作用:验证没有 pattern: 前缀时,星号、方括号这些通配特殊字符都只是普通文字。用户不显式开启通配,就不会被意外解释。
数据流:进去没有外部输入 → 它配置包含 [draft]、op*、[preview] 的普通规则,构造一个完全按文字匹配的请求和一个按通配理解才会匹配的请求 → 期望前者命中,后者不命中。
调用关系:这个测试覆盖 parse_matcher_pattern 的默认 Literal 规则,以及路径、query、header 三处的普通文字匹配。
调用图:调用 1 个内部函数(compile_mitm_hooks_with_resolvers);外部调用 7 个(from, assert_eq!, builder, empty, base_config, github_hook, vec!)。
tests::evaluate_allows_literal_values_with_reserved_prefixes967–1007 ↗
fn evaluate_allows_literal_values_with_reserved_prefixes()
作用:验证如果真实值本身以 pattern: 这样的保留前缀开头,可以用 literal: 明确表示它是普通文字。这样不会失去匹配这类值的能力。
数据流:进去没有外部输入 → 它把 query 和 header 允许值设为 literal:pattern:,构造一个真实值为 pattern: 的请求和一个类似但不同的请求 → 期望只有完全相等的请求命中。
调用关系:这个测试重点覆盖 parse_matcher_pattern 的 literal: 分支,并通过 evaluate_mitm_hooks 验证运行时效果。
调用图:调用 1 个内部函数(compile_mitm_hooks_with_resolvers);外部调用 7 个(from, assert_eq!, builder, empty, base_config, github_hook, vec!)。
tests::evaluate_returns_hooked_host_no_match_when_query_constraint_fails1010–1032 ↗
fn evaluate_returns_hooked_host_no_match_when_query_constraint_fails()
作用:验证 host 有 hook 但查询参数不符合时,结果不是“没配置 hook”,而是“这个 host 配了 hook 但本请求没命中”。
数据流:进去没有外部输入 → 它配置 state 必须是 open,构造 state=closed 的请求 → 期望 evaluate_mitm_hooks 返回 HookedHostNoMatch。
调用关系:这个测试覆盖 evaluate_mitm_hooks 对 query_matches 失败结果的分类。
调用图:调用 1 个内部函数(compile_mitm_hooks_with_resolvers);外部调用 7 个(from, assert_eq!, builder, empty, base_config, github_hook, vec!)。
tests::evaluate_returns_no_hooks_for_unconfigured_host1035–1046 ↗
fn evaluate_returns_no_hooks_for_unconfigured_host()
作用:验证当目标 host 根本没有任何 hook 配置时,会返回 NoHooksForHost。这个结果和“有 hook 但没匹配”要区分开。
数据流:进去没有外部输入 → 它构造一个请求,用空的 hook 表评估 api.github.com → 期望返回 NoHooksForHost。
调用关系:这个测试直接覆盖 evaluate_mitm_hooks 在查不到 host 时的早返回路径。
调用图:外部调用 3 个(assert_eq!, builder, empty)。
代理执行管线
这些文件提供证书、策略、响应、上游和 MITM 机制,用于实现托管 HTTP 和 HTTPS 代理执行。
network-proxy/src/certs.rs源码 ↗
HTTPS 像一封上锁的信。代理如果要做中间人解密,就必须拿出浏览器或工具愿意相信的证书。这个文件做的事就是:先在 Codex 的用户目录里找一套专用的 MITM CA,也就是“中间人根证书机构”;没有就安全地创建,有就校验证书私钥没有被别人随便读。之后每遇到一个主机名或 IP,就用这套根证书临时签一张服务器证书,并装进 rustls 的 TLS 配置里。它还会读取系统自带的可信根证书,加上自己的根证书,生成一个带哈希名字的 PEM 信任包。这样 curl、npm、pip、git 等常见工具可以通过环境变量拿到这个包。文件特别小心:私钥用严格权限写入,不覆盖已有文件,拒绝符号链接,尽量避免写到一半留下坏文件。
ManagedMitmCa::load_or_create43–49 ↗
fn load_or_create() -> Result<Self>
作用:加载这套代理专用的根证书机构;如果还没有,就创建一套新的。调用者用它拿到一个可以继续给网站签证书的对象。
数据流:它先从磁盘读出或新建 CA 证书和私钥文本,再把私钥解析成密钥对象,把证书和私钥合成签发者对象,最后返回 ManagedMitmCa。过程中如果证书或私钥坏了,会返回错误而不是继续冒险使用。
调用关系:它是 ManagedMitmCa 这台“小型发证机器”的启动按钮。外部的 new 流程会调用它;它把找文件、建文件的活交给 load_or_create_ca,再用外部库解析证书和私钥。
调用图:调用 1 个内部函数(load_or_create_ca);被 1 处调用(new);外部调用 2 个(from_ca_cert_pem, from_pem)。
ManagedMitmCa::tls_acceptor_data_for_host51–68 ↗
fn tls_acceptor_data_for_host(&self, host: &str) -> Result<TlsAcceptorData>
作用:给某个具体网站主机生成一份 TLS 服务器配置。简单说,就是让代理能在这个主机名面前扮演一个 HTTPS 服务器。
数据流:输入是主机名或 IP。它先让 issue_host_certificate_pem 生成该主机的证书和私钥,再把 PEM 文本解析成 rustls 能用的格式,创建服务器 TLS 配置,并声明支持 HTTP/2 和 HTTP/1.1,最后包装成 TlsAcceptorData 返回。
调用关系:当代理要接受某个 HTTPS 连接时,外层的 tls_acceptor_data_for_host 会走到这里。它自己不负责签名细节,而是把签证书交给 issue_host_certificate_pem,把 TLS 配置组装交给 rustls。
调用图:调用 1 个内部函数(issue_host_certificate_pem);被 1 处调用(tls_acceptor_data_for_host);外部调用 5 个(from_pem_slice, from_pem_slice, from, builder_with_protocol_versions, vec!)。
issue_host_certificate_pem71–98 ↗
fn issue_host_certificate_pem(
host: &str,
issuer: &Issuer<'_, KeyPair>,
) -> Result<(String, String)>
作用:为一个主机名或 IP 地址临时签发一张服务器证书。它解决的是“每个目标网站都要有匹配证书”的问题。
数据流:输入是 host 和代理自己的 CA 签发者。它判断 host 是 IP 还是域名,放进证书的备用名称里;再设置这张证书只能用于服务器认证,生成一对新的主机私钥,最后用 CA 签名,输出证书 PEM 和私钥 PEM 两段文本。
调用关系:它被 ManagedMitmCa::tls_acceptor_data_for_host 调用,是每次 HTTPS 伪装的核心小步骤。它依赖 rcgen 这样的证书生成库完成密钥生成和签名。
调用图:被 1 处调用(tls_acceptor_data_for_host);外部调用 5 个(new, generate_for, IpAddress, new, vec!)。
managed_ca_paths127–135 ↗
fn managed_ca_paths() -> Result<(PathBuf, PathBuf)>
作用:算出代理 CA 证书和私钥应该放在哪里。这样所有地方都用同一套固定路径,不会各存各的。
数据流:它先找到 Codex 的用户主目录,然后在里面拼出 proxy/ca.pem 和 proxy/ca.key 两个路径,最后返回这两个文件路径。
调用关系:很多流程都要先知道文件位置。load_or_create_ca 用它找证书和私钥;managed_ca_trust_bundle 用它找证书;is_managed_mitm_ca_trust_bundle_path 用它判断某个路径是不是当前这套证书生成的信任包。
调用图:被 3 处调用(is_managed_mitm_ca_trust_bundle_path, load_or_create_ca, managed_ca_trust_bundle);外部调用 1 个(find_codex_home)。
managed_ca_trust_bundle137–143 ↗
fn managed_ca_trust_bundle(
env: &HashMap<&'static str, String>,
) -> Result<ManagedMitmCaTrustBundle>
作用:准备一个给子工具使用的可信证书包。它会确保代理 CA 存在,并记录启动时已有的证书相关环境变量,避免覆盖用户原本设置时毫无记忆。
数据流:输入是一份启动环境变量表。它先调用 load_or_create_ca 确保证书和私钥已经存在,再拿到证书路径,最后交给 managed_ca_trust_bundle_for_cert_path 生成信任包并返回路径和记录下来的环境值。
调用关系:它通常在配置创建流程 from_config 中被调用。它是外部配置层和本文件内部证书包生成流程之间的入口。
调用图:调用 3 个内部函数(load_or_create_ca, managed_ca_paths, managed_ca_trust_bundle_for_cert_path);被 1 处调用(from_config)。
managed_ca_trust_bundle_for_cert_path145–164 ↗
fn managed_ca_trust_bundle_for_cert_path(
cert_path: &Path,
env: &HashMap<&'static str, String>,
) -> Result<ManagedMitmCaTrustBundle>
作用:针对一张指定的代理 CA 证书,生成完整信任包,并记住启动时哪些常见 TLS 环境变量已有值。
数据流:输入是 CA 证书路径和环境变量表。它先从固定的一组环境变量名里挑出非空值保存下来,然后构建包含系统根证书加代理 CA 的信任包,再把信任包写到磁盘,最后返回文件路径和保存的启动环境值。
调用关系:managed_ca_trust_bundle 会调用它;测试 managed_ca_trust_bundle_records_startup_ca_env_values 也直接调用它确认环境变量会被记住。它把“拼内容”交给 build_managed_ca_trust_bundle,把“落盘”交给 persist_managed_ca_trust_bundle。
调用图:调用 2 个内部函数(build_managed_ca_trust_bundle, persist_managed_ca_trust_bundle);被 2 处调用(managed_ca_trust_bundle, managed_ca_trust_bundle_records_startup_ca_env_values)。
build_managed_ca_trust_bundle166–181 ↗
fn build_managed_ca_trust_bundle(managed_ca_cert_path: &Path) -> Result<String>
作用:把系统本来信任的根证书和代理自己的 CA 证书拼成一个 PEM 文件内容。这样子工具既信任正常网站,也信任代理签出来的证书。
数据流:输入是代理 CA 证书路径。它先读取操作系统里的原生根证书,遇到读取错误会警告但继续处理;然后把每张系统证书转成 PEM 文本追加进去,最后把代理 CA 的 PEM 文件也追加进去,输出一整段信任包字符串。
调用关系:它只负责生成信任包内容,被 managed_ca_trust_bundle_for_cert_path 调用。它内部用 push_certificate_pem 转换系统证书,用 append_pem_file 追加代理证书文件。
调用图:调用 2 个内部函数(append_pem_file, push_certificate_pem);被 1 处调用(managed_ca_trust_bundle_for_cert_path);外部调用 3 个(new, load_native_certs, warn!)。
is_current_generated_trust_bundle_path183–206 ↗
fn is_current_generated_trust_bundle_path(path: &Path, managed_ca_cert_path: &Path) -> bool
作用:判断某个文件是不是“当前这套代理 CA 生成出来的信任包”。这可以避免把旧文件或别人伪造的文件误当成可管理文件。
数据流:输入是待检查路径和当前 CA 证书路径。它检查文件是否在同一个 proxy 目录下、文件名是否像 ca-bundle-xxx.pem,然后读取信任包和当前 CA 证书,确认信任包里确实包含当前 CA 内容,最后返回 true 或 false。
调用关系:它是底层判断函数,被 is_managed_mitm_ca_trust_bundle_path 包装后对外使用。相关测试会验证旧信任包会被拒绝。
调用图:被 1 处调用(is_managed_mitm_ca_trust_bundle_path);外部调用 3 个(file_name, parent, read)。
is_managed_mitm_ca_trust_bundle_path209–214 ↗
fn is_managed_mitm_ca_trust_bundle_path(path: &str) -> bool
作用:对外提供一个简单判断:给一个字符串路径,看看它是不是当前 Codex 生成的 MITM CA 信任包。
数据流:输入是路径字符串。它先尝试找到当前 CA 证书路径;如果找不到就直接返回 false。找到了就把字符串变成路径,再交给 is_current_generated_trust_bundle_path 做细查,输出布尔结果。
调用关系:它是给其他模块使用的公开检查口。它不自己读文件细节,而是先用 managed_ca_paths 定位当前 CA,再交给 is_current_generated_trust_bundle_path 判断。
调用图:调用 2 个内部函数(is_current_generated_trust_bundle_path, managed_ca_paths);外部调用 1 个(new)。
persist_managed_ca_trust_bundle216–241 ↗
fn persist_managed_ca_trust_bundle(
managed_ca_cert_path: &Path,
trust_bundle: &str,
) -> Result<PathBuf>
作用:把生成好的信任包安全地写到磁盘,并用内容哈希生成文件名。内容一样时文件名也一样,方便复用。
数据流:输入是 CA 证书路径和信任包文本。它先确认 proxy 目录存在,再对信任包内容算 SHA-256 哈希,也就是内容指纹;然后拼出 ca-bundle-指纹.pem 这样的路径,安全写入或复用已有同内容文件,最后返回文件路径。
调用关系:它被 managed_ca_trust_bundle_for_cert_path 调用,是信任包生成流程的落盘步骤。真正防止覆盖、拒绝符号链接等细节交给 write_atomic_create_new_or_reuse。
调用图:调用 1 个内部函数(write_atomic_create_new_or_reuse);被 1 处调用(managed_ca_trust_bundle_for_cert_path);外部调用 4 个(parent, digest, format!, create_dir_all)。
append_pem_file243–254 ↗
fn append_pem_file(bundle: &mut String, path: &Path) -> Result<()>
作用:把一个 PEM 证书文件追加到已有的证书包字符串后面,并保证换行整齐。
数据流:输入是可修改的字符串和文件路径。它先确保当前字符串以换行分隔,再读取文件内容并追加进去,最后如果末尾没有换行就补一个;修改发生在传入的字符串上。
调用关系:它被 build_managed_ca_trust_bundle 调用,用来把代理自己的 CA 文件拼到系统根证书后面。
调用图:被 1 处调用(build_managed_ca_trust_bundle);外部调用 1 个(read_to_string)。
push_certificate_pem256–264 ↗
fn push_certificate_pem(bundle: &mut String, der: &[u8])
作用:把二进制证书转换成 PEM 文本格式并追加到证书包里。PEM 就是带 BEGIN/END 标记、用 Base64 表示的常见证书文本格式。
数据流:输入是可修改的证书包字符串和一段 DER 二进制证书。它写入证书开头标记,把二进制内容做 Base64 编码,并按每行 64 个字符分行,最后写入结尾标记;结果直接追加在字符串里。
调用关系:它被 build_managed_ca_trust_bundle 调用,专门处理系统根证书,因为系统读出来的常常是二进制格式。
调用图:被 1 处调用(build_managed_ca_trust_bundle);外部调用 1 个(from_utf8_lossy)。
load_or_create_ca266–313 ↗
fn load_or_create_ca() -> Result<(String, String)>
作用:读取或创建代理自己的根证书和私钥。它是整个 MITM 证书系统的地基,没有它就没法签发主机证书。
数据流:它先算出证书和私钥路径。如果两者已存在,就要求两者都在,并校验私钥文件安全,然后读取文本返回。如果都不存在,就创建目录,生成新的 CA 证书和私钥,先用严格权限写私钥,再写证书;如果证书写失败,会删除刚写的私钥,避免留下半套 CA。
调用关系:ManagedMitmCa::load_or_create 和 managed_ca_trust_bundle 都会调用它。它向下调用 generate_ca 生成新证书,调用 validate_existing_ca_key_file 检查旧私钥,调用 write_atomic_create_new 安全写文件。
调用图:调用 4 个内部函数(generate_ca, managed_ca_paths, validate_existing_ca_key_file, write_atomic_create_new);被 2 处调用(load_or_create, managed_ca_trust_bundle);外部调用 5 个(anyhow!, create_dir_all, read_to_string, remove_file, info!)。
generate_ca315–333 ↗
fn generate_ca() -> Result<(String, String)>
作用:生成一套新的代理根证书和对应私钥。根证书相当于这台代理自己的“印章”。
数据流:它创建证书参数,标记这张证书是 CA,也就是可以签别的证书;设置允许签证书、数字签名和密钥加密;再写入通用名称 network_proxy MITM CA。随后生成密钥对,用它自签名,最后输出证书 PEM 和私钥 PEM。
调用关系:它只在 load_or_create_ca 发现磁盘上还没有 CA 时使用。证书生成和签名细节由 rcgen 库完成。
调用图:被 1 处调用(load_or_create_ca);外部调用 5 个(default, new, Ca, generate_for, vec!)。
write_atomic_create_new335–393 ↗
fn write_atomic_create_new(path: &Path, contents: &[u8], mode: u32) -> Result<()>
作用:安全创建一个新文件,绝不悄悄覆盖已有文件。这里的“原子”意思是尽量让别人看到的不是半成品。
数据流:输入是目标路径、要写的字节内容和文件权限。它先在同目录创建一个带进程号和时间的临时文件,写入内容并刷到磁盘;然后尽量用硬链接把临时文件变成最终文件,如果目标已存在就报错;硬链接不支持时才谨慎改用重命名。最后同步父目录,保证目录记录也尽量落盘。
调用关系:load_or_create_ca 用它写 CA 私钥和证书;write_atomic_create_new_or_reuse 用它写信任包。它把平台相关的打开文件交给 open_create_new_with_mode,把目录同步交给 sync_parent_dir。
调用图:调用 2 个内部函数(open_create_new_with_mode, sync_parent_dir);被 2 处调用(load_or_create_ca, write_atomic_create_new_or_reuse);外部调用 10 个(exists, file_name, parent, now, anyhow!, format!, hard_link, remove_file, rename, id)。
sync_parent_dir404–406 ↗
fn sync_parent_dir(_parent: &Path) -> Result<()>
作用:尽量把父目录的变更也刷到磁盘,减少断电或崩溃后文件名丢失的风险。在 Windows 上这个步骤被简化为直接成功。
数据流:输入是父目录路径。在非 Windows 系统上,它打开这个目录并执行同步;成功就返回成功,失败就带着目录路径报错。Windows 版本不做实际操作,直接返回成功。
调用关系:write_atomic_create_new 在文件写完并放到最终位置后调用它。它是安全写文件流程的最后一道保险。
调用图:被 1 处调用(write_atomic_create_new);外部调用 1 个(open)。
write_atomic_create_new_or_reuse408–429 ↗
fn write_atomic_create_new_or_reuse(path: &Path, contents: &[u8], mode: u32) -> Result<()>
作用:安全写入一个文件;如果目标文件已经存在且内容完全一样,就复用它。它主要用于按内容哈希命名的信任包。
数据流:输入是目标路径、内容和权限。它先拒绝符号链接,避免被人把写入引到别处;如果已有文件内容完全相同,就直接成功;如果已有文件但内容不同,就报错。不存在时调用 write_atomic_create_new 创建;如果并发情况下别人刚好创建了同内容文件,也会接受并复用。
调用关系:persist_managed_ca_trust_bundle 用它写信任包。测试 write_atomic_create_new_or_reuse_rejects_matching_symlink_target 会验证即使符号链接指向的内容相同也不能复用。
调用图:调用 1 个内部函数(write_atomic_create_new);被 2 处调用(persist_managed_ca_trust_bundle, write_atomic_create_new_or_reuse_rejects_matching_symlink_target);外部调用 4 个(exists, anyhow!, read, symlink_metadata)。
validate_existing_ca_key_file462–464 ↗
fn validate_existing_ca_key_file(_path: &Path) -> Result<()>
作用:检查已有 CA 私钥文件是否安全。私钥很敏感,如果别人能读,就等于别人也能冒充这套代理签证书。
数据流:输入是私钥路径。在 Unix 系统上,它读取文件元数据,拒绝符号链接,要求它是普通文件,并确认组用户和其他用户没有读写执行权限;通过则返回成功。非 Unix 系统上这个检查简化为直接成功。
调用关系:load_or_create_ca 在使用已有私钥前调用它。多个测试会覆盖权限过宽、符号链接和私有权限这几种情况。
调用图:被 4 处调用(load_or_create_ca, validate_existing_ca_key_file_allows_private_permissions, validate_existing_ca_key_file_rejects_group_world_permissions, validate_existing_ca_key_file_rejects_symlink);外部调用 2 个(anyhow!, symlink_metadata)。
open_create_new_with_mode479–485 ↗
fn open_create_new_with_mode(path: &Path, _mode: u32) -> Result<File>
作用:按指定权限创建一个新文件,并且要求文件原本不存在。它是安全写文件时真正打开临时文件的地方。
数据流:输入是路径和权限模式。在 Unix 系统上,它用给定权限创建新文件;在非 Unix 系统上,权限参数不生效,只要求新建并打开可写文件。成功返回文件句柄,失败返回带路径的错误。
调用关系:write_atomic_create_new 调用它来创建临时文件。它把“不同操作系统怎么设置权限”的差异隔离在一个小函数里。
调用图:被 1 处调用(write_atomic_create_new);外部调用 1 个(new)。
tests::current_generated_trust_bundle_path_rejects_stale_bundle498–508 ↗
fn current_generated_trust_bundle_path_rejects_stale_bundle()
作用:测试旧的、不包含当前 CA 的信任包不会被误认为有效。它保护的是“不要拿过期证书包继续用”的行为。
数据流:它创建临时目录,写入一个当前 CA 文件和一个内容不匹配的 ca-bundle-123.pem,然后调用判断函数,最后断言结果必须是 false。
调用关系:这是 is_current_generated_trust_bundle_path 的单元测试。它不参与正式运行,只在测试时验证识别逻辑不会太宽松。
tests::managed_ca_trust_bundle_records_startup_ca_env_values511–522 ↗
fn managed_ca_trust_bundle_records_startup_ca_env_values()
作用:测试生成信任包时会记录启动时已有的证书环境变量。这样后续代码可以知道用户原本设置过什么。
数据流:它在临时目录写一个 CA 文件,构造一份包含 SSL_CERT_FILE 的环境变量表,调用 managed_ca_trust_bundle_for_cert_path,然后断言返回结果里保存了这个原始值。
调用关系:这是 managed_ca_trust_bundle_for_cert_path 的单元测试。它直接走内部函数,重点检查环境变量记录,不关心正式的 Codex 目录。
调用图:调用 1 个内部函数(managed_ca_trust_bundle_for_cert_path);外部调用 4 个(from, assert_eq!, write, tempdir)。
tests::validate_existing_ca_key_file_rejects_group_world_permissions526–537 ↗
fn validate_existing_ca_key_file_rejects_group_world_permissions()
作用:测试 Unix 上权限太宽的私钥文件会被拒绝。比如 644 表示别人也可能读到,这是不安全的。
数据流:它创建临时私钥文件,把权限设成 0644,然后调用 validate_existing_ca_key_file,最后断言错误信息里提到 group/world accessible。
调用关系:这是 validate_existing_ca_key_file 的安全测试之一,只在 Unix 下运行。它确保私钥权限检查真的会拦住危险文件。
调用图:调用 1 个内部函数(validate_existing_ca_key_file);外部调用 5 个(assert!, from_mode, set_permissions, write, tempdir)。
tests::validate_existing_ca_key_file_rejects_symlink541–555 ↗
fn validate_existing_ca_key_file_rejects_symlink()
作用:测试私钥路径如果是符号链接会被拒绝。符号链接像一个转发牌,可能把程序带到意想不到的文件上。
数据流:它创建一个真实 key 文件,再创建一个指向它的 ca.key 符号链接,然后调用 validate_existing_ca_key_file,最后断言错误信息提到 symlink。
调用关系:这是 validate_existing_ca_key_file 的另一个 Unix 安全测试。它保证程序不会通过链接间接使用私钥。
调用图:调用 1 个内部函数(validate_existing_ca_key_file);外部调用 3 个(assert!, write, tempdir)。
tests::validate_existing_ca_key_file_allows_private_permissions559–566 ↗
fn validate_existing_ca_key_file_allows_private_permissions()
作用:测试权限正确的私钥文件可以通过检查。它确认安全检查不是一味拒绝所有文件。
数据流:它创建临时私钥文件,把权限设为 0600,也就是只有当前用户可读写,然后调用 validate_existing_ca_key_file,预期没有错误。
调用关系:这是 validate_existing_ca_key_file 的正向 Unix 测试。它和拒绝权限过宽的测试配成一组,说明什么情况会被允许。
调用图:调用 1 个内部函数(validate_existing_ca_key_file);外部调用 4 个(from_mode, set_permissions, write, tempdir)。
tests::write_atomic_create_new_or_reuse_rejects_matching_symlink_target570–585 ↗
fn write_atomic_create_new_or_reuse_rejects_matching_symlink_target()
作用:测试即使符号链接指向的文件内容完全匹配,也不能被复用。这样可以防止别人用链接绕过安全写入规则。
数据流:它创建一个真实的 bundle 文件和一个指向它的符号链接,然后让 write_atomic_create_new_or_reuse 往链接路径写同样内容,最后断言返回的错误正是拒绝复用符号链接。
调用关系:这是 write_atomic_create_new_or_reuse 的安全测试。它确保信任包写入流程不会因为内容相同就放松对符号链接的拒绝。
调用图:调用 1 个内部函数(write_atomic_create_new_or_reuse);外部调用 3 个(assert_eq!, write, tempdir)。
network-proxy/src/responses.rs源码 ↗
网络代理在挡住一个请求时,不能只是不回应;它要清楚地告诉对方“为什么不让过”。这个文件就像代理的“回信模板库”:普通文字回复用 text_response,JSON 回复用 json_response,被策略拦截时用 blocked_text_response 或 blocked_text_response_with_policy。它还把内部原因码翻译成人能看懂的句子,比如“域名不在允许名单里”,并放一个 x-proxy-error 头,方便程序也能识别是哪类拦截。PolicyDecisionDetails 保存一次策略判断的上下文,比如协议、来源、主机和端口;目前详细信息没有真正拼进提示语里,但接口已经留好了。这样做的好处是:代理不同地方遇到错误时,不用各写各的回复,用户看到的提示更一致,调用方也更容易判断问题。
text_response26–32 ↗
fn text_response(status: StatusCode, body: &str) -> Response
作用:生成一个最普通的纯文本 HTTP 响应。调用者只要给状态码和文字内容,就能得到可以直接发回客户端的回复。
数据流:进去的是一个 HTTP 状态码和一段文字 → 它创建响应,设置状态码,把 content-type 设成 text/plain,并把文字放进响应体 → 出来的是一个 Response;如果正常构造失败,它会退一步返回一个只带正文的简单响应。
调用关系:它是通用的文字回信工具。evaluate_mitm_policy 和 handle_mitm_request 在处理 MITM,也就是“中间人解密检查 HTTPS 请求”的相关流程时,会用它快速返回说明文字。它自己把真正的响应构造工作交给 HTTP 库的 builder 和 Body::from。
调用图:被 2 处调用(evaluate_mitm_policy, handle_mitm_request);外部调用 2 个(builder, from)。
json_response34–50 ↗
fn json_response(value: &T) -> Response
作用:把一份可序列化的数据包装成 JSON HTTP 响应。JSON 可以理解成程序之间常用的一种结构化文本格式。
数据流:进去的是任意能转成 JSON 的值 → 它先用 serde_json 把值转成字符串,失败时记录错误并用空对象 {} 顶上;然后设置 200 OK 和 application/json → 出来的是一个 Response;如果响应构造也失败,它再记录错误并返回 {}。
调用关系:它主要服务 json_blocked,也就是需要用 JSON 格式说明“请求被拦截”的地方。它依赖 serde_json 做数据转文本,依赖 HTTP 响应构造器做外层包装,出错时用 tracing 的 error! 记日志,方便排查。
调用图:被 1 处调用(json_blocked);外部调用 4 个(builder, error!, from, to_string)。
blocked_header_value52–61 ↗
fn blocked_header_value(reason: &str) -> &'static str
作用:把内部的拦截原因翻译成适合放进 HTTP 头的短标签。这个标签方便机器判断,比如是允许名单拦截、拒绝名单拦截,还是方法限制。
数据流:进去的是一段原因码字符串 → 它逐个匹配已知原因 → 出来的是固定的短英文标签;如果原因不认识,就返回通用的 blocked-by-policy。
调用关系:它是所有“被拦截响应”的小翻译器。json_blocked、blocked_text_response 和 blocked_text_response_with_policy 都会用它来填 x-proxy-error 这个响应头,让客户端不用解析正文也能知道大概原因。
调用图:被 3 处调用(json_blocked, blocked_text_response, blocked_text_response_with_policy)。
blocked_message63–74 ↗
fn blocked_message(reason: &str) -> &'static str
作用:把内部的拦截原因翻译成人能读懂的英文提示语。它解决的是“不能把冷冰冰的原因码直接丢给用户”的问题。
数据流:进去的是原因码字符串 → 它根据原因选择对应的友好句子,比如域名不在允许名单、访问本地地址被沙箱策略禁止、代理被关闭等 → 出来的是一段固定提示文字;未知原因会返回通用拦截提示。
调用关系:它是文本提示的基础来源。blocked_text_response 直接用它生成响应正文,blocked_message_with_policy 也先调用它,再把结果作为带策略上下文版本的提示。
调用图:被 2 处调用(blocked_message_with_policy, blocked_text_response)。
blocked_text_response76–83 ↗
fn blocked_text_response(reason: &str) -> Response
作用:生成一个“请求被拒绝”的纯文本 HTTP 响应。它用于代理决定挡住请求时,给客户端一个统一的 403 Forbidden 回复。
数据流:进去的是拦截原因 → 它把状态码设为 403,正文设成人类可读的拦截说明,同时把 x-proxy-error 设成机器可读的拦截标签 → 出来的是一个 Response;如果构造失败,就返回正文为 blocked 的简化响应。
调用关系:evaluate_mitm_policy 在判断 MITM 相关策略并决定拒绝时会调用它。它内部把“原因转头部标签”的活交给 blocked_header_value,把“原因转提示语”的活交给 blocked_message。
调用图:调用 2 个内部函数(blocked_header_value, blocked_message);被 1 处调用(evaluate_mitm_policy);外部调用 2 个(builder, from)。
blocked_message_with_policy84–87 ↗
fn blocked_message_with_policy(reason: &str, details: &PolicyDecisionDetails<'_>) -> String
作用:生成带策略上下文接口的拦截提示语。现在它实际返回的还是普通拦截提示,但参数里已经接收了本次策略判断的详细信息,方便以后扩展。
数据流:进去的是拦截原因和 PolicyDecisionDetails,后者包含判断结果、来源、协议、主机和端口等信息 → 它目前只读取其中部分字段以保持接口使用,然后调用 blocked_message 得到基础提示 → 出来的是一个 String 类型的提示文字。
调用关系:它是普通提示和“带策略详情提示”之间的中间层。proxy_disabled_response、blocked_text_response_with_policy 和 policy_denied_error 会调用它;测试 tests::blocked_message_with_policy_returns_human_message 也验证它目前会返回友好的人类提示。
调用图:调用 1 个内部函数(blocked_message);被 4 处调用(proxy_disabled_response, blocked_text_response_with_policy, blocked_message_with_policy_returns_human_message, policy_denied_error)。
blocked_text_response_with_policy89–99 ↗
fn blocked_text_response_with_policy(
reason: &str,
details: &PolicyDecisionDetails<'_>,
) -> Response
作用:生成一个带策略详情参数的“请求被拒绝”纯文本响应。它和 blocked_text_response 很像,但调用方可以把本次策略判断的上下文一起传进来。
数据流:进去的是拦截原因和策略详情 → 它设置 403 状态码、text/plain 类型、x-proxy-error 头,并用 blocked_message_with_policy 生成正文 → 出来的是一个 Response;如果构造失败,就退回到正文为 blocked 的简单响应。
调用关系:blocked_text_with_details 会在需要保留策略详情的拦截场景中调用它。它内部继续依赖 blocked_header_value 生成响应头标签,依赖 blocked_message_with_policy 生成正文。
调用图:调用 2 个内部函数(blocked_header_value, blocked_message_with_policy);被 1 处调用(blocked_text_with_details);外部调用 2 个(builder, from)。
tests::blocked_message_with_policy_returns_human_message108–120 ↗
fn blocked_message_with_policy_returns_human_message()
作用:这是一个自动测试,确认带策略详情的拦截提示函数会返回人能看懂的消息,而不是返回内部原因码。
数据流:进去的是测试里手工造出的 PolicyDecisionDetails,以及“不在允许名单”这个原因 → 它调用 blocked_message_with_policy → 然后检查结果是否正好是 “Domain not in allowlist.”;测试本身不产生业务输出,只在结果不对时报错。
调用关系:它只在测试时运行,不参与真实代理处理请求。它专门盯住 blocked_message_with_policy,防止以后改代码时不小心把用户可读提示改坏。
调用图:调用 1 个内部函数(blocked_message_with_policy);外部调用 1 个(assert_eq!)。
network-proxy/src/connect_policy.rs源码 ↗
网络代理收到请求后,常常需要替用户去连接某个目标地址。问题是,这个目标可能不是公网网站,而是 127.0.0.1、本机服务、内网机器这类“非公网地址”。如果不拦一下,别人可能借这个代理去探测或访问服务器内部资源。这个文件就像门卫:在真正拨号连接之前,先看目标地址是不是非公网,再看配置里是否允许“本地绑定/本地访问”。如果不允许,就直接拒绝连接;如果允许,才继续建立 TCP 连接(可以理解成普通的网络插座连接)。它还特别处理了一种情况:如果请求里已经带了 ProxyAddress,说明这是通过上游代理转发的地址,就不在这里做这层目标检查,而交给普通连接器处理。文件里也有测试,确认默认会拒绝本地地址,打开开关后会放行。
TargetCheckedTcpConnector::new24–28 ↗
fn new(state: Arc<NetworkProxyState>) -> Self
作用:创建一个带安全检查的 TCP 连接器,并让它从共享的代理状态里读取策略。外部代码用它来确保每次直连目标前都会看当前配置是否允许访问本地或内网地址。
数据流:进去的是一个共享的 NetworkProxyState(代理运行时状态,里面能读到配置)→ 函数把它包装成 TargetPolicy::State 这种策略来源 → 出来的是 TargetCheckedTcpConnector,之后连接目标时会按这个状态里的配置决定放行还是拒绝。
调用关系:它是很多实际网络流程的入口配件,比如 SOCKS5 处理、CONNECT 隧道和相关测试都会先用它造出连接器。真正收到连接请求后,会进入 TargetCheckedTcpConnector::serve,再在需要时交给 TargetCheckedStreamConnector::connect 做目标地址检查。
调用图:被 10 处调用(direct_connector_allows_non_public_target_when_local_binding_enabled, direct_connector_rejects_non_public_target_when_local_binding_disabled, forward_connect_tunnel, run_socks5_with_listener, handle_socks5_tcp_blocks_hooked_non_https_host_in_full_mode, handle_socks5_tcp_blocks_limited_mode_without_mitm_state, handle_socks5_tcp_uses_mitm_for_hooked_host_in_full_mode, handle_socks5_tcp_uses_mitm_in_limited_mode, direct, from_env_proxy);外部调用 1 个(State)。
TargetCheckedTcpConnector::from_allow_local_binding30–36 ↗
fn from_allow_local_binding(allow_local_binding: bool) -> Self
作用:用一个简单的布尔开关直接创建连接器,不依赖完整的运行状态。适合配置已经提前算好、只需要告诉它“允不允许本地地址”的场景。
数据流:进去的是 allow_local_binding 这个真假值 → 函数把它放进 TargetPolicy::Config → 出来的是 TargetCheckedTcpConnector,后续连接时会直接使用这个固定开关来判断。
调用关系:它被 direct_with_allow_local_binding、from_env_proxy_with_allow_local_binding 这类更偏配置组装的流程使用。和 TargetCheckedTcpConnector::new 不同,它不去读共享状态,而是把策略值直接写死在连接器里。
调用图:被 2 处调用(direct_with_allow_local_binding, from_env_proxy_with_allow_local_binding)。
TargetCheckedTcpConnector::serve47–58 ↗
async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error>
作用:这是连接器真正接到一次“请帮我连这个目标”的请求时执行的函数。它决定这次连接是走普通 TCP 连接,还是先加上目标地址安全检查再连接。
数据流:进去的是一个连接请求 input,里面包含目标信息和扩展信息 → 它先查看请求扩展里有没有 ProxyAddress;如果有,说明可能是交给上游代理的地址,就直接用普通 TcpConnector 连接;如果没有,就创建带检查的 TargetCheckedStreamConnector,再交给 TcpConnector 去完成连接 → 出来的是已建立的 TCP 客户端连接,或者一个连接失败/被策略拒绝的错误。
调用关系:它在 handle_socks5_tcp 这类请求处理流程中被调用,是每次 TCP 转发真正出门前的分岔口。它自己不直接拨网络,而是把拨号工作交给 TcpConnector;当需要安全检查时,又把底层拨号动作交给 TargetCheckedStreamConnector::connect。
调用图:被 1 处调用(handle_socks5_tcp);外部调用 3 个(extensions, new, clone)。
TargetCheckedStreamConnector::connect69–82 ↗
async fn connect(&self, addr: SocketAddr) -> Result<TcpStream, Self::Error>
作用:在真正打开 TCP 连接之前检查目标地址是否被允许。它是这个文件里最像“门卫”的地方:不合规就拒绝,合规才真的连出去。
数据流:进去的是一个 SocketAddr(IP 地址加端口)→ 它先问 TargetPolicy::allow_local_binding 当前是否允许访问本地/内网地址,再用 is_non_public_ip 判断目标 IP 是不是非公网;如果不允许且目标是非公网,就返回“权限不足”的错误;否则调用 tokio 的 TCP connect 真正建立连接 → 出来的是 TcpStream 网络连接,或者错误。
调用关系:它由 TargetCheckedTcpConnector::serve 组装进 TcpConnector 后间接调用。它会先向 TargetPolicy::allow_local_binding 要策略结果,再用 is_non_public_ip 做地址分类,最后才调用系统异步网络连接函数。
调用图:调用 2 个内部函数(allow_local_binding, is_non_public_ip);外部调用 3 个(ip, new, connect)。
TargetPolicy::allow_local_binding92–104 ↗
async fn allow_local_binding(&self) -> Result<bool, BoxError>
作用:读取“是否允许连接本地或内网目标”这个策略值。它把两种来源统一起来:要么来自固定配置,要么来自正在运行的代理状态。
数据流:进去的是 TargetPolicy 自己保存的策略来源 → 如果是 Config,就直接返回里面的 allow_local_binding;如果是 State,就异步读取 NetworkProxyState 里的配置,读取失败时会把错误补上“读取网络代理配置”这层说明 → 出来的是一个真假值,或者读取配置失败的错误。
调用关系:它被 TargetCheckedStreamConnector::connect 调用,用来决定遇到非公网 IP 时该拦还是放。它把“策略从哪里来”的细节藏起来,让连接检查代码只关心最终答案。
调用图:被 1 处调用(connect)。
tests::direct_connector_rejects_non_public_target_when_local_binding_disabled117–136 ↗
async fn direct_connector_rejects_non_public_target_when_local_binding_disabled()
作用:这个测试确认默认情况下,直连本机地址会被拒绝。它防止以后有人改代码时不小心把这道安全门拆掉。
数据流:进去的是测试里临时启动的本机监听端口和默认代理配置 → 测试创建 TargetCheckedTcpConnector,再请求它连接这个本机地址 → 出来的预期结果是错误,并且错误信息里要包含“network target rejected by policy”。
调用关系:它会调用 TargetCheckedTcpConnector::new 创建带状态的连接器,再通过 Service::serve 触发正常连接流程。这个流程最终会走到 TargetCheckedStreamConnector::connect,并因为默认不允许本地目标而返回拒绝错误。
调用图:调用 3 个内部函数(new, default, new);外部调用 6 个(new, from, serve, bind, assert!, network_proxy_state_for_policy)。
tests::direct_connector_allows_non_public_target_when_local_binding_enabled139–156 ↗
async fn direct_connector_allows_non_public_target_when_local_binding_enabled()
作用:这个测试确认当配置明确允许本地连接时,连接本机地址不会被拦。它保证安全开关不是一刀切,而是能按配置放行。
数据流:进去的是测试里临时启动的本机监听端口,以及 allow_local_binding 设为 true 的代理配置 → 测试创建 TargetCheckedTcpConnector,再请求它连接这个本机地址 → 出来的预期结果是连接成功。
调用关系:它同样通过 TargetCheckedTcpConnector::new 和 Service::serve 走完整连接流程。不同的是,TargetPolicy::allow_local_binding 读到允许本地连接,所以 TargetCheckedStreamConnector::connect 不会拒绝,而会继续调用真实 TCP 连接。
调用图:调用 3 个内部函数(new, default, new);外部调用 6 个(new, from, serve, bind, assert!, network_proxy_state_for_policy)。
network-proxy/src/upstream.rs源码 ↗
可以把这个文件想成代理服务器里的“出站司机”。用户的请求进来后,司机要先看路线:目标是普通 HTTP 还是 HTTPS,要不要走 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY 这些环境变量指定的代理。它还会用 TargetCheckedTcpConnector 这类带检查的连接器,避免随便连到不该连的地方。真正出发前,它会搭好一条连接链:可选的上游代理、TLS 加密连接(TLS 就是 HTTPS 用的加密层)、HTTP 客户端连接。请求发送时,它会记录日志:选了直连还是代理、连接花了多久、有没有拿到响应头。如果连接或请求失败,它会把错误包装成统一的错误,方便上层处理。在 macOS 上,它还支持通过 Unix socket(本机文件路径形式的进程间通信通道)转发请求。
ProxyConfig::from_env40–45 ↗
fn from_env() -> Self
作用:从系统环境变量里读取代理设置,生成一份“该怎么走代理”的配置。有人想让出站请求自动遵守 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY 时会用它。
数据流:进去的是当前进程能看到的环境变量名列表 → 它分别尝试读取 HTTP、HTTPS 和通用代理地址 → 出来的是一个 ProxyConfig,里面可能带有 http、https、all 三种代理地址,也可能都是空。
调用关系:它把具体读取和校验环境变量的活交给 read_proxy_env。创建带环境代理的 UpstreamClient 时会调用它,CONNECT 代理流程需要知道上游代理时也会通过 proxy_for_connect 间接用到它。
调用图:调用 1 个内部函数(read_proxy_env);被 3 处调用(from_env_proxy, from_env_proxy_with_allow_local_binding, proxy_for_connect)。
ProxyConfig::proxy_for_protocol47–56 ↗
fn proxy_for_protocol(&self, is_secure: bool) -> Option<ProxyAddress>
作用:根据请求是不是安全连接,挑出最合适的代理地址。简单说,就是帮请求选路线:HTTPS 优先用 HTTPS_PROXY,普通 HTTP 优先用 HTTP_PROXY,不行再看通用代理。
数据流:进去的是一个布尔值,表示目标协议是否安全,比如 HTTPS 就是安全 → 它按优先级查看配置里的代理地址 → 出来的是一个可用的 ProxyAddress,或者没有代理就返回空。
调用关系:UpstreamClient::serve 在每次真正转发请求前都会调用它。它不负责发请求,只负责给 serve 一个“走哪条路”的决定。
调用图:被 1 处调用(serve)。
read_proxy_env59–86 ↗
fn read_proxy_env(keys: &[&str]) -> Option<ProxyAddress>
作用:按顺序读取一组代理环境变量,并把合法的 HTTP 代理地址解析出来。它还会忽略空值、坏地址和非 HTTP 类型的代理,避免错误配置把请求带偏。
数据流:进去的是一组环境变量名字,比如 HTTP_PROXY 和 http_proxy → 它逐个读取变量值,去掉前后空白,尝试解析成 ProxyAddress,并检查协议是不是 HTTP 类代理 → 出来的是第一个可用代理地址;如果都不合格,就返回空,同时对坏配置打警告日志。
调用关系:它是 ProxyConfig::from_env 的底层小帮手。from_env 负责组合整体代理配置,read_proxy_env 负责把某一类环境变量读准、筛干净。
proxy_for_connect88–90 ↗
fn proxy_for_connect() -> Option<ProxyAddress>
作用:专门给 HTTP CONNECT 这类隧道请求查找应该使用的上游代理。CONNECT 常用于把 HTTPS 流量穿过代理,所以这里按“安全连接”的规则选代理。
数据流:进去的是当前环境变量里的代理配置 → 它先调用 ProxyConfig::from_env 读出配置,再按安全协议规则选代理 → 出来的是一个可用于 CONNECT 的代理地址,或者没有。
调用关系:它被 http_connect_proxy 调用,用在 CONNECT 请求处理流程里。它自己不建立连接,只告诉那条流程该不该再套一层上游代理。
调用图:调用 1 个内部函数(from_env);被 1 处调用(http_connect_proxy)。
UpstreamClient::direct103–108 ↗
fn direct(state: Arc<NetworkProxyState>) -> Self
作用:创建一个只直连目标、完全不看环境代理的出站客户端。适合明确不想让系统代理设置影响请求的场景。
数据流:进去的是共享的 NetworkProxyState,里面有连接策略需要的状态 → 它创建默认的空代理配置,并用这个状态创建带目标检查的 TCP 连接器 → 出来的是一个 UpstreamClient,之后会直接连接目标服务器。
调用关系:http_plain_proxy 会在普通 HTTP 代理流程里用它。它把实际组装连接链的工作交给 UpstreamClient::new。
调用图:调用 1 个内部函数(new);被 1 处调用(http_plain_proxy);外部调用 2 个(new, default)。
UpstreamClient::from_env_proxy110–115 ↗
fn from_env_proxy(state: Arc<NetworkProxyState>) -> Self
作用:创建一个会自动读取环境变量代理设置的出站客户端。适合希望代理程序尊重 HTTP_PROXY、HTTPS_PROXY 等系统配置的场景。
数据流:进去的是共享的 NetworkProxyState → 它先从环境变量生成 ProxyConfig,再创建带目标检查的 TCP 连接器 → 出来的是一个 UpstreamClient,请求发出时会按协议决定是否走上游代理。
调用关系:http_plain_proxy 会在需要环境代理支持时调用它。它调用 ProxyConfig::from_env 读配置,再交给 UpstreamClient::new 搭建真实连接器。
调用图:调用 2 个内部函数(new, from_env);被 1 处调用(http_plain_proxy);外部调用 1 个(new)。
UpstreamClient::direct_with_allow_local_binding117–122 ↗
fn direct_with_allow_local_binding(allow_local_binding: bool) -> Self
作用:创建一个直连客户端,但可以显式决定是否允许绑定到本机地址。这里的“绑定到本机地址”可以理解为连接前是否允许使用本地特殊地址,通常是一个安全或隔离相关开关。
数据流:进去的是 allow_local_binding 这个开关 → 它不读取任何代理配置,只根据这个开关创建 TargetCheckedTcpConnector → 出来的是一个不走上游代理、但带指定本地绑定策略的 UpstreamClient。
调用关系:它在某些构建流程的 new 中被调用,用来生成带特殊连接策略的直连客户端。它依赖 TargetCheckedTcpConnector::from_allow_local_binding 准备底层连接规则。
调用图:调用 1 个内部函数(from_allow_local_binding);被 1 处调用(new);外部调用 2 个(new, default)。
UpstreamClient::from_env_proxy_with_allow_local_binding124–129 ↗
fn from_env_proxy_with_allow_local_binding(allow_local_binding: bool) -> Self
作用:创建一个既读取环境代理、又允许调用方指定本地绑定策略的出站客户端。它是“走不走环境代理”和“本地连接限制”两个开关的组合版。
数据流:进去的是 allow_local_binding 开关和当前环境变量 → 它读取代理配置,再按开关创建带检查的 TCP 连接器 → 出来的是一个请求时可能走上游代理、并遵守本地绑定策略的 UpstreamClient。
调用关系:它在某些 new 构建路径中使用。它把代理读取交给 ProxyConfig::from_env,把底层连接策略交给 TargetCheckedTcpConnector::from_allow_local_binding。
调用图:调用 2 个内部函数(from_allow_local_binding, from_env);被 1 处调用(new);外部调用 1 个(new)。
UpstreamClient::unix_socket132–138 ↗
fn unix_socket(path: &str) -> Self
作用:在 macOS 上创建一个通过 Unix socket 发请求的出站客户端。Unix socket 可以理解成本机程序之间用一个文件路径通信,不走普通 TCP 网络端口。
数据流:进去的是 Unix socket 的路径字符串 → 它调用 build_unix_connector 按这个路径建连接器,并使用空代理配置 → 出来的是一个会把 HTTP 请求发到该本机 socket 的 UpstreamClient。
调用关系:proxy_via_unix_socket 会调用它。它不走 build_http_connector 那套 TCP、代理、TLS 链路,而是交给 build_unix_connector 创建本机 socket 连接。
调用图:调用 1 个内部函数(build_unix_connector);被 1 处调用(proxy_via_unix_socket);外部调用 1 个(default)。
UpstreamClient::new140–146 ↗
fn new(proxy_config: ProxyConfig, transport: TargetCheckedTcpConnector) -> Self
作用:把代理配置和底层 TCP 连接器组装成一个真正能发 HTTP 请求的 UpstreamClient。它是多个构造函数背后的共同装配点。
数据流:进去的是 ProxyConfig 和 TargetCheckedTcpConnector → 它用 build_http_connector 搭好 HTTP、代理、TLS 等连接链 → 出来的是包含连接器和代理配置的 UpstreamClient。
调用关系:UpstreamClient::direct 和 UpstreamClient::from_env_proxy 等构造函数都会把准备好的配置交给它。它再调用 build_http_connector 完成底层网络管道的搭建。
调用图:调用 1 个内部函数(build_http_connector)。
UpstreamClient::serve153–219 ↗
async fn serve(&self, mut req: Request<Body>) -> Result<Self::Output, Self::Error>
作用:真正执行一次出站 HTTP 请求:选代理、建立连接、发送请求、返回响应。它是这个文件运行时最核心的函数。
数据流:进去的是一个 HTTP 请求 → 它先从请求里识别目标地址和协议,再根据 ProxyConfig 选择直连或上游代理;如果要走代理,就把代理地址塞进请求的扩展信息;然后通过 connector 建立连接,把连接产生的扩展信息合回请求,最后把请求交给 HTTP 连接发送 → 出来的是目标服务器的响应;如果连接或请求失败,就返回带上下文的错误,并写日志。
调用关系:外部把 UpstreamClient 当作 Service 使用时会调用它。它先调用 ProxyConfig::proxy_for_protocol 做路线选择,再调用底层 connector 的 serve 建连接,最后调用已建立的 HTTP connection 的 serve 发请求。
调用图:调用 1 个内部函数(proxy_for_protocol);外部调用 9 个(serve, now, from_boxed, try_from, extensions_mut, uri, format!, info!, warn!)。
build_http_connector222–240 ↗
fn build_http_connector(
transport: TargetCheckedTcpConnector,
) -> BoxService<
Request<Body>,
EstablishedClientConnection<HttpClientService<Body>, Request<Body>>,
BoxError,
>
作用:搭建一条标准的 HTTP 出站连接管道。它把 TCP 连接、可选 HTTP 代理、TLS 加密和 HTTP 客户端组合成一个可复用的连接器。
数据流:进去的是 TargetCheckedTcpConnector,也就是受规则保护的 TCP 连接能力 → 它先确保 rustls 加密库可用,再套上可选 HTTP 代理层,接着配置 TLS,并加上请求版本适配,最后包装成 HttpConnector → 出来的是一个 boxed 的通用连接服务,可以接收请求并建立 HTTP 客户端连接。
调用关系:UpstreamClient::new 调用它来完成底层装配。之后 UpstreamClient::serve 每次请求都会通过这个连接器去建立到目标或上游代理的连接。
调用图:被 1 处调用(new);外部调用 6 个(new, optional, new, new, auto, ensure_rustls_crypto_provider)。
build_unix_connector243–253 ↗
fn build_unix_connector(
path: &str,
) -> BoxService<
Request<Body>,
EstablishedClientConnection<HttpClientService<Body>, Request<Body>>,
BoxError,
>
作用:在 macOS 上搭建通过 Unix socket 发 HTTP 请求的连接器。它用于本机进程间通信,而不是连远程网络地址。
数据流:进去的是 Unix socket 路径 → 它创建固定路径的 UnixConnector,再把它包装成 HttpConnector → 出来的是一个可接收 HTTP 请求并连接到该 socket 的通用连接服务。
调用关系:UpstreamClient::unix_socket 调用它。它服务于 proxy_via_unix_socket 这类场景,和普通 TCP 出站连接的 build_http_connector 是两条不同路线。
调用图:被 1 处调用(unix_socket);外部调用 2 个(new, fixed)。
network-proxy/src/mitm.rs源码 ↗
这个文件做的是 HTTPS 的中间人代理。所谓中间人代理,就是客户端以为自己在连目标网站,其实先连到这个代理;代理用本地证书把 HTTPS 解开,检查里面的 HTTP 请求,再重新把请求发给真正的网站。它先准备一个本地 CA(可以理解成代理自己的“盖章机构”),给不同网站临时签证书。连接进来后,它从连接信息里拿到目标主机和端口,建立 TLS 接收器,然后用一个小型 HTTP 服务处理解开的请求。每个请求会先过安全规则:不能再套 CONNECT,Host 不能和原目标不一致,要防 DNS 重新绑定到本地地址,还要检查钩子规则和网络模式允许的方法。放行后,它会补好目标 HTTPS 地址和 Host 头,按需要删除或注入请求头,再交给上游客户端转发。文件里还留了请求体/响应体检查日志的机制,不过当前默认关闭。
MitmState::fmt92–98 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
作用:给 MitmState 做调试输出,但故意不把证书、连接器这类敏感内部信息打印出来。这样日志里能看到基本开关状态,又不会泄露秘密材料。
数据流:输入是 MitmState 自身和一个格式化器 → 它只写入 inspect 和 max_body_bytes 这两个安全字段 → 输出是一段调试用文字,同时不会改动状态。
调用关系:当外部日志或调试工具想打印 MitmState 时会走这里。它只把可公开的信息交给标准调试格式化工具,避免把 CA 材料带进日志。
调用图:外部调用 1 个(debug_struct)。
MitmState::new102–123 ↗
fn new(config: MitmUpstreamConfig) -> Result<Self>
作用:创建 HTTPS 中间人代理所需的核心状态。它准备本地证书机构,并决定访问真实网站时是直连还是走系统代理。
数据流:输入是上游连接配置,比如是否允许环境代理、是否允许绑定本地地址 → 它先确保 TLS 加密库准备好,再加载或创建本地 CA,然后创建上游客户端 → 输出一个可共享的 MitmState,里面有证书、上游访问方式和检查开关。
调用关系:系统构建配置状态时由 build_config_state 调用它。后面的 mitm_stream 会依赖这里准备好的证书和上游客户端来接 HTTPS、转发请求。
调用图:调用 3 个内部函数(load_or_create, direct_with_allow_local_binding, from_env_proxy_with_allow_local_binding);被 1 处调用(build_config_state);外部调用 1 个(ensure_rustls_crypto_provider)。
MitmState::tls_acceptor_data_for_host125–127 ↗
fn tls_acceptor_data_for_host(&self, host: &str) -> Result<TlsAcceptorData>
作用:为某个目标网站准备 TLS 接收所需的证书数据。简单说,就是给这个网站现场拿一张代理能用的“假网站证书”。
数据流:输入是目标主机名 → 它把主机名交给本地 CA 生成或取回对应证书材料 → 输出 TLS 接收器能使用的数据,出错时返回错误。
调用关系:mitm_stream 在真正接管客户端 HTTPS 连接前调用它。它把证书准备工作交给 ManagedMitmCa。
调用图:调用 1 个内部函数(tls_acceptor_data_for_host)。
MitmState::inspect_enabled129–131 ↗
fn inspect_enabled(&self) -> bool
作用:告诉调用者当前是否启用了请求体和响应体检查。这里的“体”就是请求或响应里真正的内容部分。
数据流:输入是 MitmState → 它读取 inspect 开关 → 输出 true 或 false,不改任何东西。
调用关系:forward_request 会用它决定是否包装请求体,respond_with_inspection 会按这个决定处理响应体。当前默认配置是关闭检查。
MitmState::max_body_bytes133–135 ↗
fn max_body_bytes(&self) -> usize
作用:告诉调用者检查请求体或响应体时最多关注多少字节。这个值用来避免日志或检查过程无限制地处理大内容。
数据流:输入是 MitmState → 它读取 max_body_bytes → 输出一个字节数,不修改状态。
调用关系:forward_request 和 respond_with_inspection 会把这个限制传给 inspect_body,让后面的流包装器知道超过多少算“太长”。
mitm_tunnel139–141 ↗
async fn mitm_tunnel(upgraded: Upgraded) -> Result<()>
作用:处理 HTTP CONNECT 升级后的隧道,把这条已经建立好的通道交给 HTTPS 中间人逻辑。它像一个转接头,把 CONNECT 场景接到通用的 mitm_stream 上。
数据流:输入是一条升级后的连接 Upgraded → 它直接调用 mitm_stream 继续处理 → 输出成功或失败结果,本身不额外改数据。
调用关系:http_connect_proxy 在决定要解开 HTTPS 隧道时调用它。它不自己干复杂活,而是把后续 TLS 接管和请求转发交给 mitm_stream。
调用图:调用 1 个内部函数(mitm_stream);被 1 处调用(http_connect_proxy)。
mitm_stream144–211 ↗
async fn mitm_stream(stream: S) -> Result<()>
作用:这是接管一条 HTTPS 连接的主流程。它从连接里取出目标网站、代理状态和运行模式,生成证书,启动 TLS 和 HTTP 服务来处理里面的请求。
数据流:输入是一条带扩展信息的原始流 → 它读取 MitmState、NetworkProxyState、ProxyTarget、NetworkMode 和执行器,规范化目标主机名,准备该主机的 TLS 证书,再搭好会移除逐跳头的 HTTP 服务 → 输出是整条连接服务完成的结果;期间会把每个解开的 HTTP 请求送给 handle_mitm_request。
调用关系:mitm_tunnel 和 proxy_socks5_tcp 会调用它。它是连接级别的调度者,往下把单个请求交给 handle_mitm_request,往外接 TLS 接收层和 HTTP 服务器。
调用图:调用 1 个内部函数(normalize_host);被 2 处调用(mitm_tunnel, proxy_socks5_tcp);外部调用 7 个(new, auto, hop_by_hop, hop_by_hop, extensions, new, service_fn)。
handle_mitm_request213–225 ↗
async fn handle_mitm_request(
req: Request,
request_ctx: Arc<MitmRequestContext>,
) -> Result<Response, std::convert::Infallible>
作用:处理一条已经从 HTTPS 里解出来的 HTTP 请求,并保证出错时客户端能收到一个明确的 502 错误,而不是连接莫名断掉。
数据流:输入是 HTTP 请求和共享的请求上下文 → 它调用 forward_request 做真正检查和转发;如果成功就拿到真实响应,如果失败就记录警告并生成“mitm upstream error”的 502 响应 → 输出永远是一个 HTTP 响应包装在成功结果里。
调用关系:mitm_stream 搭建的 HTTP 服务会在每个请求到来时调用它。它把主要工作交给 forward_request,并用 text_response 做兜底错误响应。
调用图:调用 2 个内部函数(forward_request, text_response);外部调用 1 个(warn!)。
forward_request227–275 ↗
async fn forward_request(req: Request, request_ctx: &MitmRequestContext) -> Result<Response>
作用:这是单个 HTTPS 内部请求的核心处理流程:先判定能不能放行,再按钩子修改请求头,最后转发给真正的网站并返回响应。
数据流:输入是客户端请求和 MITM 上下文 → 它先调用 evaluate_mitm_policy;如果被拦截就直接返回拦截响应;如果放行,就整理方法、路径和日志用路径,应用钩子动作,重建 https://目标主机/路径 的 URI,写入 Host 头,必要时包装请求体用于统计日志,然后交给上游客户端发送,最后按需要包装响应体 → 输出是给客户端的 HTTP 响应。
调用关系:handle_mitm_request 调用它。它串起 evaluate_mitm_policy、apply_mitm_hook_actions、build_https_uri、inspect_body、respond_with_inspection 和上游客户端,是请求级别的主干流程。
调用图:调用 8 个内部函数(apply_mitm_hook_actions, authority_header_value, build_https_uri, evaluate_mitm_policy, inspect_body, path_and_query, path_for_log, respond_with_inspection);被 1 处调用(handle_mitm_request);外部调用 5 个(from_str, from_parts, into_parts, method, uri)。
mitm_blocking_response278–286 ↗
async fn mitm_blocking_response(
req: &Request,
policy: &MitmPolicyContext,
) -> Result<Option<Response>>
作用:只检查一条请求会不会被 MITM 策略拦截,并在会拦截时给出对应响应。它适合测试或需要单独问“这条会不会挡”的地方使用。
数据流:输入是请求和策略上下文 → 它调用 evaluate_mitm_policy → 如果策略允许,输出 None;如果策略拦截,输出 Some(response)。
调用关系:它复用 evaluate_mitm_policy 的完整判断逻辑。正常转发路径主要由 forward_request 使用同一套判断;这个函数更像一个轻量包装。
调用图:调用 1 个内部函数(evaluate_mitm_policy)。
evaluate_mitm_policy288–408 ↗
async fn evaluate_mitm_policy(
req: &Request,
policy: &MitmPolicyContext,
) -> Result<MitmPolicyDecision>
作用:判断一条 HTTPS 内部请求能不能通过代理。它是安全门卫,负责挡掉危险、违规或不符合钩子规则的请求。
数据流:输入是请求和策略上下文,包括原 CONNECT 目标、端口、网络模式和全局状态 → 它检查内部不能再发 CONNECT,检查请求里的 Host 是否和隧道目标一致,重新确认目标没有变成本地/私有地址,再执行 MITM 钩子匹配,最后检查当前网络模式是否允许这个 HTTP 方法 → 输出是 Allow,并可能带上要改请求头的动作;或者输出 Block,并带上要返回给客户端的拦截响应。被拦截的情况还会尽量写入 blocked 记录和警告日志。
调用关系:forward_request 和 mitm_blocking_response 都调用它。它会用 extract_request_host、path_for_log、normalize_host 等小工具,也会向 NetworkProxyState 查询主机阻断、钩子规则和记录拦截事件。
调用图:调用 6 个内部函数(extract_request_host, path_for_log, normalize_host, blocked_text_response, text_response, new);被 2 处调用(forward_request, mitm_blocking_response);外部调用 6 个(extensions, method, uri, matches!, Block, warn!)。
apply_mitm_hook_actions410–421 ↗
fn apply_mitm_hook_actions(headers: &mut HeaderMap, actions: Option<&MitmHookActions>)
作用:把钩子规则要求的请求头修改真正应用到请求上。比如删除某些头,或者塞入新的头。
数据流:输入是可修改的请求头集合,以及可选的钩子动作 → 如果没有动作就什么也不做;如果有动作,就逐个删除指定头,再插入指定的新头和值 → 输出体现在传入的 headers 被原地修改。
调用关系:forward_request 在确认请求放行后调用它。钩子动作来自 evaluate_mitm_policy 的判断结果。
调用图:被 1 处调用(forward_request);外部调用 2 个(insert, remove)。
respond_with_inspection423–447 ↗
fn respond_with_inspection(
resp: Response,
inspect: bool,
max_body_bytes: usize,
method: &str,
log_path: &str,
authority: &str,
) -> Result<Response>
作用:在需要检查响应体时,把上游网站返回的响应包装一下,好在响应内容流完后记一条日志。不开检查时,它会原样返回响应。
数据流:输入是上游响应、检查开关、最大字节数、方法、路径和主机 → 如果 inspect 为 false,直接输出原响应;如果为 true,就拆开响应头和响应体,用 inspect_body 包装响应体,再组装回响应 → 输出可能被包装过的响应。
调用关系:forward_request 收到上游响应后调用它。它把真正的流包装交给 inspect_body,并用 ResponseLogContext 记录响应相关信息。
调用图:调用 1 个内部函数(inspect_body);被 1 处调用(forward_request);外部调用 2 个(from_parts, into_parts)。
inspect_body449–460 ↗
fn inspect_body(
body: Body,
max_body_bytes: usize,
ctx: T,
) -> Body
作用:给请求体或响应体套一层“计数器”。内容照常流过,但流完时会记录总长度以及是否超过限制。
数据流:输入是一个 Body、最大字节数和日志上下文 → 它把 Body 转成数据流,包进 InspectStream,并再包装成新的 Body → 输出一个行为基本一样、但会在结束时记日志的 Body。
调用关系:forward_request 用它包装请求体,respond_with_inspection 用它包装响应体。真正逐块计数的工作由 InspectStream::poll_next 完成。
调用图:被 2 处调用(forward_request, respond_with_inspection);外部调用 4 个(new, pin, from_stream, into_data_stream)。
InspectStream::poll_next472–488 ↗
fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>>
作用:这是包装后的内容流每次被读取时执行的逻辑。它一边把数据原样交出去,一边累计已经经过了多少字节。
数据流:输入是流自身和异步任务上下文 → 它向内部真实数据流要下一块数据;拿到数据就累加长度并返回这块数据;拿到错误就原样返回错误;发现流结束时就调用日志上下文记录总长度和是否超限 → 输出是下一块数据、错误、结束信号或等待信号。
调用关系:inspect_body 创建 InspectStream 后,HTTP 框架在发送或读取 Body 时会不断调用它。它结束时会触发 RequestLogContext::log 或 ResponseLogContext::log。
调用图:外部调用 1 个(Ready)。
RequestLogContext::log509–516 ↗
fn log(self, len: usize, truncated: bool)
作用:记录一次被检查的请求体信息。它不会记录正文内容,只记录主机、方法、路径、长度和是否超过限制。
数据流:输入是请求日志上下文、实际字节长度和截断标记 → 它取出 host、method、path 等字段,写入 info 级别日志 → 输出没有返回值,也不改请求。
调用关系:InspectStream::poll_next 在请求体读完时调用它。它是 inspect_body 用在请求方向的日志落点。
调用图:外部调用 1 个(info!)。
ResponseLogContext::log520–528 ↗
fn log(self, len: usize, truncated: bool)
作用:记录一次被检查的响应体信息。它不保存响应正文,只把状态码、长度等摘要写进日志。
数据流:输入是响应日志上下文、实际字节长度和截断标记 → 它取出 host、method、path、status 等字段,写入 info 级别日志 → 输出没有返回值,也不改响应。
调用关系:InspectStream::poll_next 在响应体读完时调用它。它是 inspect_body 用在响应方向的日志落点。
调用图:外部调用 1 个(info!)。
extract_request_host531–537 ↗
fn extract_request_host(req: &Request) -> Option<String>
作用:从请求里找出它声称要访问的主机名。它优先看 Host 请求头,找不到再看 URI 里的 authority 部分。
数据流:输入是一条 HTTP 请求 → 它读取 headers 里的 Host,并尝试转成字符串;如果没有,就读取 URI 的 authority → 输出 Some(host) 或 None。
调用关系:evaluate_mitm_policy 用它检查内部请求的 Host 是否和 CONNECT 时的目标一致。这能防止同一条隧道里偷偷访问另一个主机。
调用图:被 1 处调用(evaluate_mitm_policy);外部调用 1 个(headers)。
authority_header_value539–552 ↗
fn authority_header_value(host: &str, port: u16) -> String
作用:把主机名和端口拼成 HTTP Host 头和 URI authority 需要的格式。它会特别处理 IPv6 地址和默认 443 端口。
数据流:输入是 host 和 port → 如果 host 像 IPv6 地址那样包含冒号,就按规则加方括号;如果端口是 HTTPS 默认的 443,就省略端口;否则追加 :port → 输出格式正确的 authority 字符串。
调用关系:forward_request 在重建发往真实网站的请求时调用它。它的结果会用于 Host 头,也会传给 build_https_uri。
调用图:被 1 处调用(forward_request);外部调用 1 个(format!)。
build_https_uri554–557 ↗
fn build_https_uri(authority: &str, path: &str) -> Result<Uri>
作用:根据目标主机和路径拼出真正要发给上游网站的 HTTPS 地址。
数据流:输入是 authority 和 path → 它拼成 https://authority/path 这种字符串,再解析成 Uri 类型 → 输出 Uri;如果拼出的地址不合法,就返回错误。
调用关系:forward_request 在转发前调用它,把客户端隧道里的相对路径变成上游客户端能发送的完整 HTTPS URI。
调用图:被 1 处调用(forward_request);外部调用 1 个(format!)。
path_and_query559–564 ↗
fn path_and_query(uri: &Uri) -> String
作用:从 URI 里取出路径和问号后面的查询参数。没有路径时,它用 / 作为默认值。
数据流:输入是 URI → 它读取 path_and_query;如果存在就转成字符串,如果不存在就返回 / → 输出路径加查询字符串。
调用关系:forward_request 用它保留客户端原始请求的路径和查询参数,再交给 build_https_uri 拼成完整上游地址。
调用图:被 1 处调用(forward_request);外部调用 1 个(path_and_query)。
path_for_log566–568 ↗
fn path_for_log(uri: &Uri) -> String
作用:取出 URI 的路径部分,专门用于日志显示。它不包含查询参数,避免日志里出现太多可能敏感或冗长的信息。
数据流:输入是 URI → 它读取 path 字段并转成字符串 → 输出仅路径字符串。
调用关系:evaluate_mitm_policy 用它写拦截日志,forward_request 用它写检查日志。真正转发用的完整路径则由 path_and_query 提供。
调用图:被 2 处调用(evaluate_mitm_policy, forward_request);外部调用 1 个(path)。
本地套接字基础
这些文件提供可复用的本地 IPC 传输,供沙盒桥接、shell 提权和更高层的本地通信通道使用。
uds/src/lib.rs源码 ↗
很多程序需要在同一台机器上的两个进程之间通信,就像开一条只在本机里的小管道。Unix 域套接字就是这种管道;问题是它在 Unix 系统上是原生能力,在 Windows 上要靠兼容库,而且细节不一样。这个文件的作用就是把这些差异藏起来,对外只给出 UnixListener 和 UnixStream。前者像“接线员”,在某个路径上等别人连进来;后者像“一根已经接通的线”,可以异步读写数据。文件还提供了创建私有 socket 目录、判断旧 socket 路径是否残留的工具。异步的意思是:读写等待时不会卡死整个程序。这里最重要的安全点是 socket 目录在 Unix 上会被设成只有当前用户能进,避免别人偷偷连接控制通道。Windows 部分还做了额外包装,把阻塞式库接到异步运行环境里。
prepare_private_socket_directory15–17 ↗
async fn prepare_private_socket_directory(socket_dir: impl AsRef<Path>) -> IoResult<()>
作用:准备一个只给当前用户使用的 socket 目录。调用者不用关心当前系统是 Unix 还是 Windows,这个函数会转交给对应平台的实现。
数据流:进去的是一个目录路径 → 它把路径转换成内部能用的引用,然后交给 platform 层 → 出来是成功或失败;成功时目录存在,能用来放 socket 文件,Unix 上权限也会尽量变安全。
调用关系:这是对外入口之一,通常在创建控制 socket 之前调用。它自己不直接碰文件系统,而是把活交给 platform::prepare_private_socket_directory,让不同操作系统按自己的规则办。
调用图:外部调用 2 个(as_ref, prepare_private_socket_directory)。
is_stale_socket_path24–26 ↗
async fn is_stale_socket_path(socket_path: impl AsRef<Path>) -> IoResult<bool>
作用:判断某个 socket 路径是不是旧的、可能需要清掉的残留物。这样程序重启时不会被上一次留下的文件挡住。
数据流:进去的是一个 socket 路径 → 它把路径交给平台实现检查 → 出来是 true 或 false;true 表示这个路径看起来像残留的 socket 占位。
调用关系:这是清理或重试绑定前会用到的对外工具。它把判断逻辑交给 platform::is_stale_socket_path,因为 Unix 和 Windows 能检查到的信息不一样。
调用图:外部调用 2 个(as_ref, is_stale_socket_path)。
UnixListener::bind35–39 ↗
async fn bind(socket_path: impl AsRef<Path>) -> IoResult<Self>
作用:在指定路径上开一个监听器,等别的本机进程连进来。可以把它理解成在门牌号上挂出“这里可以接电话”。
数据流:进去的是 socket 路径 → 它交给 platform::bind_listener 在当前系统上真正绑定 → 出来是 UnixListener;失败时返回系统错误,比如路径被占用或权限不够。
调用关系:这是服务端开始接收本机连接的主要入口。项目里的控制 socket、桥接、测试场景等会调用它;它创建出来的 UnixListener 后续会用 UnixListener::accept 接收客户端。
调用图:被 12 处调用(remote_unix_socket_typed_request_roundtrip_works, disable_remote_control_retries_without_params_for_older_servers, run_enable_remote_control_scenario, start_control_socket_acceptor, run_host_bridge, pipes_stdin_and_stdout_through_socket, fetch_ide_context_uses_unregistered_request_route, validate_unix_socket_path_rejects_unsafe_parent_directory, default_daemon_auto_connect_probes_socket_only, bound_listener_path_is_stale_socket_path (+2 more));外部调用 2 个(as_ref, bind_listener)。
UnixListener::accept42–44 ↗
async fn accept(&mut self) -> IoResult<UnixStream>
作用:等待并接收下一个连进来的客户端。成功后返回一条可读可写的 UnixStream。
数据流:进去的是已经绑定好的监听器自身 → 它让内部平台监听器等一个连接 → 出来是一条 UnixStream,代表这次连接;监听器本身继续保留,可继续接下一个。
调用关系:它在 UnixListener::bind 之后使用,是服务端主循环里“有人来了就接进来”的步骤。实际等待连接的细节由内部 platform::Listener::accept 完成。
调用图:被 1 处调用(accept_initialized_client);外部调用 1 个(accept)。
UnixStream::connect54–58 ↗
async fn connect(socket_path: impl AsRef<Path>) -> IoResult<Self>
作用:连接到某个 socket 路径,作为客户端接入已经在监听的本机服务。可以理解成按门牌号拨通一条本机热线。
数据流:进去的是 socket 路径 → 它交给 platform::connect_stream 去建立连接 → 出来是 UnixStream;成功后调用者可以用它读写数据。
调用关系:这是客户端连接控制 socket 的主要入口。多个上层流程会用它去连守护进程、远程控制端或测试监听器;连接成功后,读写会走 UnixStream 的 AsyncRead 和 AsyncWrite 实现。
调用图:被 8 处调用(connect_unix_socket_endpoint, connect, prepare_control_socket_path, connect_to_socket, run, maybe_probe_default_daemon_socket, stream_round_trips_data_between_listener_and_client, connect_stream);外部调用 2 个(as_ref, connect_stream)。
UnixStream::poll_read62–68 ↗
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<IoResult<()>>
作用:让异步运行时从这条连接里读数据。poll 是异步框架的问法,意思是“现在能不能读,不能读就先别卡住”。
数据流:进去的是连接、任务上下文和一块接收缓冲区 → 它把读取请求转给内部平台流 → 出来是读取完成、还没准备好,或发生错误;缓冲区会被填入收到的数据。
调用关系:上层代码一般不会直接手写调用它,而是通过 Tokio 的异步读接口间接触发。它是 UnixStream 能被当作普通异步输入流使用的关键适配层。
调用图:外部调用 1 个(new)。
UnixStream::poll_write72–74 ↗
fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<IoResult<usize>>
作用:让异步运行时把一段数据写进这条连接。它保证外层 UnixStream 能像普通异步输出流一样被使用。
数据流:进去的是连接、任务上下文和要发送的字节 → 它转给内部平台流尝试写入 → 出来是写了多少字节、暂时不能写,或错误。
调用关系:当上层用异步写方法发送请求或响应时会触发它。它不自己实现传输协议,只负责把写操作转交给平台流。
调用图:外部调用 1 个(new)。
UnixStream::poll_flush76–78 ↗
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>>
作用:要求把已经排队的数据尽量刷出去。就像写完信后确认信真的交给邮差,而不是还压在桌上。
数据流:进去的是连接和任务上下文 → 它要求内部平台流刷新输出 → 出来是刷新成功、还要等,或错误。
调用关系:异步写入流程在需要确认数据送出时会用到它。它把外层统一接口和不同平台的具体刷新动作接起来。
调用图:外部调用 1 个(new)。
UnixStream::poll_shutdown80–82 ↗
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>>
作用:关闭这条连接的写入方向,表示“我这边不再发送了”。这有助于对方知道消息已经结束。
数据流:进去的是连接和任务上下文 → 它让内部平台流执行关闭写入 → 出来是关闭成功、还要等,或错误;连接状态会改变。
调用关系:当上层完成一次通信、需要优雅收尾时会触发它。它把关闭动作转给平台流,Windows 下还有专门处理来确保真的关闭 socket 写端。
调用图:外部调用 1 个(new)。
platform::prepare_private_socket_directory187–189 ↗
async fn prepare_private_socket_directory(socket_dir: &Path) -> IoResult<()>
作用:按当前操作系统的规则准备 socket 目录。Unix 上会特别处理权限;Windows 上主要是确保目录存在。
数据流:进去的是目录路径 → Unix 上先尝试创建目录,若已存在就检查它是不是目录,并把权限修成 0700,也就是只有本人可进入;Windows 上创建完整目录链 → 出来是成功或具体的文件系统错误。
调用关系:它由顶层 prepare_private_socket_directory 调用,是安全准备工作的真正执行者。后面的 platform::bind_listener 会依赖这个目录能安全、可用地存放 socket 路径。
调用图:外部调用 7 个(new, from_mode, format!, new, set_permissions, symlink_metadata, create_dir_all)。
platform::bind_listener193–198 ↗
async fn bind_listener(socket_path: &Path) -> IoResult<Listener>
作用:在当前平台上真正创建监听 socket。外层 UnixListener::bind 只是统一门面,具体怎么绑定由它完成。
数据流:进去的是 socket 路径 → Unix 上直接用 Tokio 的 UnixListener 绑定;Windows 上把路径复制出来,放到阻塞任务里调用兼容库,再包装成异步对象 → 出来是 platform::Listener。
调用关系:它被 UnixListener::bind 调用。创建出的 Listener 之后会被 platform::Listener::accept 使用,形成“先开门,再等人进门”的流程。
调用图:调用 1 个内部函数(bind);外部调用 4 个(new, to_path_buf, from, spawn_blocking_io)。
platform::Listener::accept201–206 ↗
async fn accept(&mut self) -> IoResult<Stream>
作用:在平台监听器上接收一个新连接,并把它变成项目统一使用的流。它藏起了不同系统返回连接的细节。
数据流:进去的是平台监听器 → 它等待底层 socket 接到一个客户端 → 出来是 platform::Stream;Unix 上取出流并丢掉地址信息,Windows 上还会把兼容库的流包装成异步流。
调用关系:它被外层 UnixListener::accept 调用。它接到的 platform::Stream 会继续被外层包成 UnixStream,让后续读写走统一接口。
platform::connect_stream209–216 ↗
async fn connect_stream(socket_path: &Path) -> IoResult<Stream>
作用:在当前平台上真正发起到 socket 路径的连接。它是客户端连接动作的底层实现。
数据流:进去的是 socket 路径 → Unix 上直接异步连接;Windows 上把阻塞连接放进后台阻塞任务,再包装成异步流 → 出来是 platform::Stream 或错误。
调用关系:它被 UnixStream::connect 调用。成功后返回的流会被外层 UnixStream 包住,供上层协议读写。
调用图:调用 1 个内部函数(connect);外部调用 4 个(new, to_path_buf, from, spawn_blocking_io)。
platform::is_stale_socket_path218–220 ↗
async fn is_stale_socket_path(socket_path: &Path) -> IoResult<bool>
作用:用平台能提供的信息判断 socket 路径是否像残留物。Unix 和 Windows 的判断标准不同。
数据流:进去的是路径 → Unix 上读取路径的元数据并看它是不是 socket 文件;Windows 上因为兼容库只留下普通路径,所以只检查路径是否存在 → 出来是布尔值或读取错误。
调用关系:它被顶层 is_stale_socket_path 调用,通常用于绑定前的清理判断。这样外层不用写一堆按系统分支的代码。
调用图:外部调用 2 个(symlink_metadata, try_exists)。
platform::spawn_blocking_io222–231 ↗
async fn spawn_blocking_io(
operation: impl FnOnce() -> IoResult<T> + Send + 'static,
) -> IoResult<T>
作用:把可能会卡住线程的 Windows socket 操作挪到专门的阻塞线程里执行。这样异步主循环不会被一个慢操作拖住。
数据流:进去的是一个会返回 IO 结果的操作 → 它用 Tokio 的 spawn_blocking 放到阻塞线程池运行 → 出来是原操作的结果;如果后台任务本身崩了,会转成普通 IO 错误。
调用关系:Windows 的 platform::bind_listener 和 platform::connect_stream 会用它。它是把阻塞式 uds_windows 库安全接进异步世界的中间垫片。
调用图:外部调用 1 个(spawn_blocking)。
platform::WindowsUnixListener::from236–238 ↗
fn from(listener: uds_windows::UnixListener) -> Self
作用:把 uds_windows 库里的监听器包进本文件自己的 WindowsUnixListener。这个包装让后面能补上异步库需要的接口。
数据流:进去的是 uds_windows::UnixListener → 它放进一个新的 WindowsUnixListener 结构里 → 出来是包装后的监听器,原监听器所有权被转移进去。
调用关系:Windows 的绑定流程会在拿到底层监听器后调用它。随后这个包装会交给 async_io::Async,让监听器能参与异步等待。
platform::WindowsUnixListener::deref244–246 ↗
fn deref(&self) -> &Self::Target
作用:让 WindowsUnixListener 用起来像里面那个原始 uds_windows 监听器。简单说,就是打开包装盒时能直接拿到里面的东西看。
数据流:进去的是包装监听器的引用 → 它返回内部 uds_windows::UnixListener 的引用 → 不改变任何状态。
调用关系:这是 Rust 的 Deref 适配,供底层库或包装层需要访问原监听器方法时使用。它配合 AsSocket 等实现,让 Windows 包装对象能被异步 IO 库识别。
platform::WindowsUnixListener::as_socket250–252 ↗
fn as_socket(&self) -> BorrowedSocket<'_>
作用:把 Windows 监听器暴露成系统 socket 句柄。异步 IO 库需要这个句柄,才能让操作系统通知“什么时候可读”。
数据流:进去的是包装监听器 → 它从内部对象拿到底层 raw socket,再借用成 BorrowedSocket → 出来是一个临时的 socket 视图,不夺走所有权。
调用关系:这是 Windows 适配 async_io::Async 的必要接口。platform::bind_listener 包装监听器后,Async::new 依赖这类能力把它纳入异步等待。
调用图:外部调用 1 个(borrow_raw)。
platform::WindowsUnixStream::from258–260 ↗
fn from(stream: uds_windows::UnixStream) -> Self
作用:把 uds_windows 库里的连接流包成本文件自己的 WindowsUnixStream。这样后面可以给它补齐读写和 socket 句柄接口。
数据流:进去的是 uds_windows::UnixStream → 它被放入 WindowsUnixStream → 出来是包装后的连接流,原流的所有权被移入包装。
调用关系:Windows 的 accept 和 connect 流程都会用到它。包装后再交给 Async 和 compat 层,最终变成外层 UnixStream 可用的异步流。
platform::WindowsUnixStream::deref266–268 ↗
fn deref(&self) -> &Self::Target
作用:让 WindowsUnixStream 可以像内部原始流一样被读取其方法和属性。它是一个方便访问内部对象的适配。
数据流:进去的是包装流的引用 → 返回内部 uds_windows::UnixStream 的引用 → 不读写数据,也不改变状态。
调用关系:它服务于 Windows 包装层的其他接口,比如拿 socket 句柄或调用关闭操作。这样包装不会挡住底层能力。
platform::WindowsUnixStream::as_socket272–274 ↗
fn as_socket(&self) -> BorrowedSocket<'_>
作用:把 Windows 连接流暴露成系统 socket 句柄。没有这个,异步 IO 包装层就不知道该监听哪个系统对象。
数据流:进去的是包装流 → 它借用内部 raw socket → 出来是 BorrowedSocket,一个临时可用但不拥有资源的句柄视图。
调用关系:Windows 的 Async::new 需要它来接管等待通知。platform::connect_stream 和 platform::Listener::accept 包装流之后,会依赖这个接口进入异步读写流程。
调用图:外部调用 1 个(borrow_raw)。
platform::WindowsUnixStream::read278–280 ↗
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize>
作用:给 Windows 包装流补上普通的读取能力。它只是把读取请求转交给内部 uds_windows 流。
数据流:进去的是可写入数据的缓冲区 → 它从内部流读取字节放进去 → 出来是读到的字节数或错误;缓冲区内容会变化。
调用关系:async_io::Async 包装一个对象时需要它像普通读对象一样工作。后续 platform::Stream::poll_read 会间接走到这类读取能力。
调用图:外部调用 1 个(read)。
platform::WindowsUnixStream::write284–286 ↗
fn write(&mut self, buf: &[u8]) -> IoResult<usize>
作用:给 Windows 包装流补上普通的写入能力。它把要发的数据交给内部 uds_windows 流发送。
数据流:进去的是一段字节 → 它调用内部流写入 → 出来是实际写入的字节数或错误;连接的发送状态会前进。
调用关系:这是 Windows 异步写包装的底层普通写接口。platform::Stream::poll_write 最终会通过包装层使用它。
调用图:外部调用 1 个(write)。
platform::WindowsUnixStream::flush288–290 ↗
fn flush(&mut self) -> IoResult<()>
作用:给 Windows 包装流补上刷新能力,确保已写的数据尽量被送出去。
数据流:进去的是包装流自身 → 它调用内部流的 flush → 出来是刷新成功或错误;可能推动缓冲区里的数据发送。
调用关系:platform::Stream::poll_flush 和 platform::Stream::poll_shutdown 需要刷新语义。这个函数让 Windows 的底层流符合普通写接口的期待。
调用图:外部调用 1 个(flush)。
platform::Stream::poll_read294–300 ↗
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<IoResult<()>>
作用:Windows 平台上的异步读取实现。它把项目统一的 platform::Stream 读操作转给兼容层里的异步流。
数据流:进去的是 Windows 平台流、任务上下文和接收缓冲区 → 它让内部 compat 异步流尝试读取 → 出来是读完、暂时等待,或错误;缓冲区可能被填入数据。
调用关系:外层 UnixStream::poll_read 在 Windows 上会一路转到这里。它是 Windows 兼容库和 Tokio 异步读接口之间的桥。
调用图:外部调用 1 个(new)。
platform::Stream::poll_write304–310 ↗
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<IoResult<usize>>
作用:Windows 平台上的异步写入实现。它负责把要发送的字节交给内部兼容层。
数据流:进去的是 Windows 平台流、任务上下文和待发送字节 → 它尝试写入内部流 → 出来是写入字节数、暂时等待,或错误。
调用关系:外层 UnixStream::poll_write 在 Windows 上依赖它。它让上层不用知道 Windows 这里实际经过了 Async 和 Compat 两层包装。
调用图:外部调用 1 个(new)。
platform::Stream::poll_flush312–314 ↗
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>>
作用:Windows 平台上的异步刷新实现。它让上层写完后可以确认内部缓冲尽量清空。
数据流:进去的是 Windows 平台流和任务上下文 → 它调用内部兼容流的刷新 → 出来是成功、等待,或错误。
调用关系:外层 UnixStream::poll_flush 会转到这里。platform::Stream::poll_shutdown 也会先刷新,再做真正的写端关闭。
调用图:外部调用 1 个(new)。
platform::Stream::poll_shutdown316–323 ↗
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>>
作用:Windows 平台上关闭连接写入方向的实现。这里特别绕过了兼容层只刷新不真正关写端的问题,直接调用 socket shutdown。
数据流:进去的是 Windows 平台流和任务上下文 → 它先等待刷新完成,再对底层 socket 执行关闭写端 → 出来是成功、等待,或错误;成功后对方会看到本端不再发送。
调用关系:外层 UnixStream::poll_shutdown 在 Windows 上会用它。它先借助 poll_flush 保证数据不丢,再直接操作底层 socket,是 Windows 适配里一个重要的收尾修正。
调用图:外部调用 2 个(Ready, ready!)。
shell-escalation/src/unix/socket.rs源码 ↗
在 Unix 系统里,两个进程可以用本机 socket 像打电话一样交换数据;更特别的是,还能顺手递交“已打开的文件句柄”。这个文件把这些底层、容易出错的操作包成两个好用的工具:AsyncSocket 用于连续字节流,会先发消息长度,再发 JSON 内容;AsyncDatagramSocket 用于一包一包的短消息。它还处理了异步等待,也就是 socket 暂时不能读写时不会卡死整个程序。文件里有一批小函数专门做底层活:计算控制消息空间、把文件描述符塞进系统控制消息、从收到的控制消息里取出文件描述符。重要的一点是,流式 socket 收消息时必须先读固定长度的“消息头”,否则接收方不知道这一条消息到哪里结束;没有这层封装,大消息、半包、附带文件描述符这些情况都很容易出错。
assume_init26–28 ↗
fn assume_init(buf: &[MaybeUninit<T>]) -> &[T]
作用:把一段“可能还没填好”的内存当成“已经填好的数据”来看。它是内部小工具,只能在调用者已经确认这些字节确实被系统写入后使用。
数据流:输入是一片 MaybeUninit 内存,也就是 Rust 还不敢保证里面有有效值的区域;它不复制数据,只是换个视角把它看成普通切片;输出是可读取的普通切片,本身不改动内存。
调用关系:read_frame_header 和 receive_datagram_bytes 在系统调用读完数据后会用它,把刚刚收到的字节交给后面的长度解析或消息解析。它是底层读 socket 后的“确认收货”步骤。
调用图:被 2 处调用(read_frame_header, receive_datagram_bytes);外部调用 3 个(as_ptr, len, from_raw_parts)。
assume_init_slice30–32 ↗
fn assume_init_slice(buf: &[MaybeUninit<T>; N]) -> &[T; N]
作用:把固定长度数组形式的“可能未初始化内存”转成固定长度的普通数组引用。这里主要用来读取流式消息前面的长度字段。
数据流:输入是一个固定大小的 MaybeUninit 数组;函数相信调用方已经把每个位置都填满;输出是同样大小的普通数组引用,方便后面按字节解释成数字。
调用关系:read_frame_header 在凑齐 4 个长度字节后调用它,然后把这 4 个字节转成消息正文长度。它帮读取流程跨过 Rust 对未初始化内存的安全检查。
调用图:被 1 处调用(read_frame_header)。
assume_init_vec34–42 ↗
fn assume_init_vec(mut buf: Vec<MaybeUninit<T>>) -> Vec<T>
作用:把已经填满的临时缓冲区变成真正的 Vec 数据。它避免再复制一份大消息内容。
数据流:输入是 Vec<MaybeUninit<T>>,也就是一整块刚被写满但类型上还没确认的内存;它接管这块内存的指针、长度和容量;输出是 Vec<T>,表示这些内容现在可以当作普通数据使用。
调用关系:read_frame_payload 在完整读到消息正文后调用它,把缓冲区直接变成返回给上层的字节数组。它处在“读满正文”和“交给 JSON 解析”之间。
调用图:被 1 处调用(read_frame_payload);外部调用 2 个(from_raw_parts, forget)。
control_space_for_fds44–46 ↗
fn control_space_for_fds(count: usize) -> usize
作用:计算要随消息携带若干文件描述符时,系统控制消息需要多大的缓冲区。可以把它理解成先量好快递盒尺寸。
数据流:输入是要携带的文件描述符数量;它调用系统的 CMSG_SPACE 规则计算包括对齐和头部在内的空间;输出是需要分配的字节数。
调用关系:发送和接收文件描述符前都要知道控制区有多大。make_control_message、read_frame_header、receive_datagram_bytes 等流程都会依赖这个尺寸计算。
调用图:外部调用 1 个(CMSG_SPACE)。
extract_fds49–79 ↗
fn extract_fds(control: &[u8]) -> Vec<OwnedFd>
作用:从收到的 Unix socket 控制消息里取出文件描述符。控制消息是系统专门用来夹带这类“额外物品”的区域。
数据流:输入是一段控制消息字节;它按 Unix 的 cmsghdr 格式逐个检查,只认 SCM_RIGHTS,也就是“传递文件描述符”的消息;输出是一组 OwnedFd,表示接收方现在拥有这些文件描述符并负责关闭它们。
调用关系:read_frame_header 和 receive_datagram_bytes 收到消息后会调用它,把底层控制区里的句柄提出来交给上层。它是“系统格式”和“Rust 可管理资源”之间的转换器。
调用图:被 2 处调用(read_frame_header, receive_datagram_bytes);外部调用 8 个(from_raw_fd, new, CMSG_DATA, CMSG_FIRSTHDR, CMSG_LEN, CMSG_NXTHDR, zeroed, try_from)。
read_frame85–89 ↗
async fn read_frame(async_socket: &AsyncFd<Socket>) -> std::io::Result<(Vec<u8>, Vec<OwnedFd>)>
作用:从流式 socket 里读出一整条完整消息。它把“先读长度,再读正文,还可能带文件描述符”这件事合成一步。
数据流:输入是一个异步 socket;它先调用 read_frame_header 读消息长度和随头部来的文件描述符,再调用 read_frame_payload 按长度读正文;输出是正文的字节数组和收到的文件描述符列表。
调用关系:AsyncSocket::receive_with_fds 会用它拿到原始消息,再把字节反序列化成具体 Rust 数据。它是流式接收的主流程。
调用图:调用 2 个内部函数(read_frame_header, read_frame_payload);被 1 处调用(receive_with_fds)。
read_frame_header92–145 ↗
async fn read_frame_header(
async_socket: &AsyncFd<Socket>,
) -> std::io::Result<(usize, Vec<OwnedFd>)>
作用:读取流式消息开头的长度字段,并接收可能跟着这个开头一起送来的文件描述符。没有这个步骤,接收方不知道后面正文该读多少字节。
数据流:输入是异步 socket;它反复等 socket 可读,直到凑齐固定 4 字节长度,同时第一次读取时收集控制消息;最后把 4 字节小端数字转成正文长度,并提取文件描述符;输出是正文长度和文件描述符列表。
调用关系:read_frame 会先调用它。它内部使用 assume_init_slice 解释长度字节,用 assume_init 和 extract_fds 处理控制消息;如果对端提前关闭,就返回 UnexpectedEof,让上层知道消息没收完整。
调用图:调用 3 个内部函数(assume_init, assume_init_slice, extract_fds);被 1 处调用(read_frame);外部调用 7 个(readable, uninit, assert!, new, from_le_bytes, unreachable!, vec!)。
read_frame_payload148–177 ↗
async fn read_frame_payload(
async_socket: &AsyncFd<Socket>,
message_len: usize,
) -> std::io::Result<Vec<u8>>
作用:按照已经知道的长度,继续从流式 socket 里把消息正文读完整。它能处理一次读不完的大消息。
数据流:输入是异步 socket 和要读取的字节数;它循环等待可读并把每次收到的片段填进缓冲区;读满后把缓冲区转成普通 Vec<u8> 返回;如果对端中途关闭,就返回 UnexpectedEof。
调用关系:read_frame 在读完头部后调用它。它用 assume_init_vec 把已填满的缓冲区交给上层,之后 AsyncSocket::receive_with_fds 会把这些字节当 JSON 解析。
调用图:调用 1 个内部函数(assume_init_vec);被 1 处调用(read_frame);外部调用 6 个(readable, new, assert!, new, unreachable!, vec!)。
send_datagram_bytes179–198 ↗
fn send_datagram_bytes(socket: &Socket, data: &[u8], fds: &[OwnedFd]) -> std::io::Result<()>
作用:通过数据报 socket 发送一包字节,并可选地附带文件描述符。数据报像一张明信片,一次发送就是一整包。
数据流:输入是 socket、要发送的字节和文件描述符列表;它先用 make_control_message 做好附带句柄的控制区,再调用 sendmsg 发送;如果系统实际写出的字节数少于整包长度,就返回写入错误。
调用关系:AsyncDatagramSocket::send_with_fds 通过异步写入流程使用它;测试 send_datagram_bytes_rejects_excessive_fd_counts 也直接调用它确认文件描述符过多时会报错。
调用图:调用 1 个内部函数(make_control_message);被 1 处调用(send_datagram_bytes_rejects_excessive_fd_counts);外部调用 5 个(new, new, sendmsg, new, format!)。
encode_length200–208 ↗
fn encode_length(len: usize) -> std::io::Result<[u8; LENGTH_PREFIX_SIZE]>
作用:把消息正文长度编码成固定 4 字节。流式 socket 需要这个长度前缀来区分一条条消息。
数据流:输入是 usize 长度;它尝试转成 u32,因为协议只允许 4 字节长度;成功时输出小端字节数组,太大时输出 InvalidInput 错误。
调用关系:AsyncSocket::send_with_fds 在发送 JSON 正文前调用它生成消息头。测试 encode_length_errors_for_oversized_messages 会验证超大消息不会被悄悄截断。
调用图:被 2 处调用(send_with_fds, encode_length_errors_for_oversized_messages);外部调用 1 个(try_from)。
make_control_message210–233 ↗
fn make_control_message(fds: &[OwnedFd]) -> std::io::Result<Vec<u8>>
作用:把要传给对方的文件描述符打包成 Unix socket 能识别的控制消息。它相当于给普通消息附上一袋“系统句柄”。
数据流:输入是一组 OwnedFd;如果数量超过上限就返回 InvalidInput,如果为空就返回空控制区;否则分配合适大小的字节区,写入 SCM_RIGHTS 头和每个原始 fd 号码;输出是可交给 sendmsg 的控制消息字节。
调用关系:send_datagram_bytes 和 send_stream_chunk 在真正发送前都会调用它。它集中执行数量限制和系统格式拼装,避免每个发送路径各写一遍危险代码。
调用图:被 2 处调用(send_datagram_bytes, send_stream_chunk);外部调用 9 个(is_empty, iter, len, new, new, format!, CMSG_DATA, CMSG_LEN, vec!)。
receive_datagram_bytes235–249 ↗
fn receive_datagram_bytes(socket: &Socket) -> std::io::Result<(Vec<u8>, Vec<OwnedFd>)>
作用:从数据报 socket 接收一整包字节,并取出随包带来的文件描述符。
数据流:输入是 socket;它准备最大数据报缓冲区和控制消息缓冲区,调用 recvmsg 一次收包;然后把实际收到的字节复制成 Vec<u8>,并从控制区提取 OwnedFd;输出是数据和文件描述符列表。
调用关系:AsyncDatagramSocket::receive_with_fds 通过异步读流程使用它。它和 send_datagram_bytes 是数据报通信的一收一发。
调用图:调用 2 个内部函数(assume_init, extract_fds);外部调用 4 个(new, new, recvmsg, vec!)。
AsyncSocket::new256–262 ↗
fn new(socket: Socket) -> std::io::Result<AsyncSocket>
作用:把普通 socket 包装成可被 Tokio 异步运行时使用的 AsyncSocket。Tokio 是这里的异步任务调度器,让程序等待 I/O 时不阻塞整条线程。
数据流:输入是 socket2::Socket;它先把 socket 设为非阻塞模式,再交给 AsyncFd 包装;输出是 AsyncSocket。
调用关系:AsyncSocket::from_fd 和 AsyncSocket::pair 都会调用它。它是创建流式异步通信端点的内部统一入口。
AsyncSocket::from_fd264–266 ↗
fn from_fd(fd: OwnedFd) -> std::io::Result<AsyncSocket>
作用:从一个已有的文件描述符创建 AsyncSocket。适合别的进程或系统调用已经给了一个 socket 句柄的情况。
数据流:输入是 OwnedFd;它把这个 fd 转成 socket2::Socket,再调用 AsyncSocket::new 设置非阻塞并包装;输出是可异步收发的 AsyncSocket。
调用关系:escalate_task 会用它接管外部传入的通信 fd。它把“裸句柄”接入本文件定义的消息协议。
调用图:调用 1 个内部函数(new);被 1 处调用(escalate_task);外部调用 1 个(from)。
AsyncSocket::pair268–277 ↗
fn pair() -> std::io::Result<(AsyncSocket, AsyncSocket)>
作用:创建一对互相连通的本机流式 socket。常用于父子进程或测试里,一端发消息,另一端收消息。
数据流:没有业务输入;它创建 Unix STREAM socket pair,给两端设置 close-on-exec,也就是执行新程序时默认不要泄漏这个 fd;然后分别包装成 AsyncSocket;输出是一对端点。
调用关系:run_shell_escalation_execve_wrapper、多个会话处理测试和大消息测试都会用它搭建通信通道。它是流式通道的便捷工厂。
调用图:调用 1 个内部函数(new);被 10 处调用(run_shell_escalation_execve_wrapper, dropping_session_aborts_intercept_workers_and_kills_spawned_child, handle_escalate_session_accepts_received_fds_that_overlap_destinations, handle_escalate_session_executes_escalated_command, handle_escalate_session_passes_permissions_to_executor, handle_escalate_session_resolves_relative_file_against_request_workdir, handle_escalate_session_respects_run_in_sandbox_decision, async_socket_handles_large_payload, async_socket_round_trips_payload_and_fds, receive_fails_when_peer_closes_before_header);外部调用 1 个(pair_raw)。
AsyncSocket::send_with_fds279–289 ↗
async fn send_with_fds(
&self,
msg: T,
fds: &[OwnedFd],
) -> std::io::Result<()>
作用:发送一条结构化消息,并可附带文件描述符。结构化消息会先变成 JSON 字节,再按本文件的帧格式发出去。
数据流:输入是可序列化的数据和 fd 列表;它把数据转成 JSON,给正文前面加 4 字节长度,再调用 send_stream_frame 发送整帧;输出是成功或 I/O 错误。
调用关系:AsyncSocket::send 会在不带 fd 的普通发送场景调用它;底层发送工作交给 encode_length 和 send_stream_frame。它是流式发送的高级入口。
调用图:调用 2 个内部函数(encode_length, send_stream_frame);被 1 处调用(send);外部调用 2 个(with_capacity, to_vec)。
AsyncSocket::receive_with_fds291–297 ↗
async fn receive_with_fds(
&self,
) -> std::io::Result<(T, Vec<OwnedFd>)>
作用:接收一条结构化消息,并把随消息来的文件描述符一起交出来。适合确实期待对方传句柄的场景。
数据流:输入是当前 socket 和目标数据类型;它调用 read_frame 得到 JSON 字节和 fd,再用 serde_json 解析成目标类型;输出是解析后的消息和 fd 列表。
调用关系:AsyncSocket::receive 会在普通接收场景调用它,然后检查是否意外收到 fd。它是流式接收的高级入口。
调用图:调用 1 个内部函数(read_frame);被 1 处调用(receive);外部调用 1 个(from_slice)。
AsyncSocket::send299–304 ↗
async fn send(&self, msg: T) -> std::io::Result<()>
作用:发送一条不带文件描述符的普通结构化消息。调用者不需要关心底层长度前缀和 JSON 编码。
数据流:输入是可序列化的数据;它把数据借给 send_with_fds,并传入空 fd 列表;输出是发送结果。
调用关系:handle_escalate_session_with_policy 会用它发送普通控制消息。它是 send_with_fds 的简化版。
调用图:调用 1 个内部函数(send_with_fds);被 1 处调用(handle_escalate_session_with_policy)。
AsyncSocket::receive306–312 ↗
async fn receive(&self) -> std::io::Result<T>
作用:接收一条不期待文件描述符的普通结构化消息。如果对方还是传来了 fd,它会记录警告但继续返回消息。
数据流:输入是当前 socket 和目标类型;它调用 receive_with_fds 得到消息和 fd 列表;如果 fd 不为空就写日志提醒;输出是解析后的消息。
调用关系:上层需要普通消息时会用它。它把更通用的 receive_with_fds 包成更安全、更省心的接口。
调用图:调用 1 个内部函数(receive_with_fds);外部调用 1 个(warn!)。
AsyncSocket::into_inner314–316 ↗
fn into_inner(self) -> Socket
作用:拆开 AsyncSocket,取回里面的原始 socket。适合需要把控制权交给别的底层代码时使用。
数据流:输入是 AsyncSocket 本身;它消费这个包装对象;输出是内部 socket,不再由 AsyncSocket 管。
调用关系:这是逃生口式接口。正常收发用 send 或 receive,需要回到底层 socket 操作时才会用它。
调用图:外部调用 1 个(into_inner)。
send_stream_frame319–344 ↗
async fn send_stream_frame(
socket: &AsyncFd<Socket>,
frame: &[u8],
fds: &[OwnedFd],
) -> std::io::Result<()>
作用:把一整帧流式消息可靠地写进 socket。即使系统一次只能写一部分,它也会继续写剩下的。
数据流:输入是异步 socket、完整帧字节和可选 fd;它循环等待 socket 可写,每次调用 send_stream_chunk 写一段;文件描述符只在第一次写时附带;写完全部帧后返回成功。
调用关系:AsyncSocket::send_with_fds 会调用它。它再把每次实际写 socket 的活交给 send_stream_chunk,自己负责异步等待和“写到完成”。
调用图:被 1 处调用(send_with_fds);外部调用 3 个(writable, is_empty, new)。
send_stream_chunk346–364 ↗
fn send_stream_chunk(
socket: &Socket,
frame: &[u8],
fds: &[OwnedFd],
include_fds: bool,
) -> std::io::Result<usize>
作用:向流式 socket 写一次数据块,并在需要时把文件描述符挂在这一次写入上。
数据流:输入是 socket、还没写完的帧片段、fd 列表,以及是否本次附带 fd 的标志;它按需要生成控制消息,再调用 sendmsg;输出是这次实际写入的字节数。
调用关系:send_stream_frame 在循环写帧时调用它;测试 send_stream_chunk_rejects_excessive_fd_counts 直接调用它确认 fd 数量超限会失败。它是流式发送最贴近系统调用的一层。
调用图:调用 1 个内部函数(make_control_message);被 1 处调用(send_stream_chunk_rejects_excessive_fd_counts);外部调用 4 个(new, new, sendmsg, new)。
AsyncDatagramSocket::new371–376 ↗
fn new(socket: Socket) -> std::io::Result<Self>
作用:把普通数据报 socket 包装成异步数据报 socket。数据报 socket 每次收发都是一整包。
数据流:输入是 socket2::Socket;它设置非阻塞,再用 AsyncFd 包装;输出是 AsyncDatagramSocket。
调用关系:AsyncDatagramSocket::from_raw_fd 和 AsyncDatagramSocket::pair 会走这套初始化。它是数据报端点创建的内部统一步骤。
调用图:外部调用 2 个(new, set_nonblocking)。
AsyncDatagramSocket::from_raw_fd378–380 ↗
fn from_raw_fd(fd: RawFd) -> std::io::Result<Self>
作用:从一个原始 fd 号创建异步数据报 socket。这里是 unsafe,因为调用者必须保证这个数字真的是有效 socket,而且所有权可以交给这里。
数据流:输入是 RawFd,也就是一个裸的系统句柄编号;它把编号变成 Socket,再调用 new 设置非阻塞并包装;输出是 AsyncDatagramSocket。
调用关系:get_escalate_client 和相关会话测试会用它接入已经存在的客户端通信 fd。它把外部给来的底层通道纳入本文件的数据报接口。
调用图:被 2 处调用(get_escalate_client, dropping_session_aborts_intercept_workers_and_kills_spawned_child);外部调用 2 个(new, from_raw_fd)。
AsyncDatagramSocket::pair382–391 ↗
fn pair() -> std::io::Result<(Self, Self)>
作用:创建一对互相连通的数据报 socket。适合传短消息,边界清楚,一次发送对应一次接收。
数据流:没有业务输入;它创建 Unix DGRAM socket pair,设置 close-on-exec 防止 fd 泄漏,再包装两端;输出是一对 AsyncDatagramSocket。
调用关系:start_session 和数据报往返测试会调用它。它是会话启动时建立短消息通道的便捷工厂。
调用图:被 2 处调用(start_session, async_datagram_sockets_round_trip_messages);外部调用 2 个(new, pair_raw)。
AsyncDatagramSocket::send_with_fds393–399 ↗
async fn send_with_fds(&self, data: &[u8], fds: &[OwnedFd]) -> std::io::Result<()>
作用:异步发送一包数据报,并可附带文件描述符。调用者只需要给字节和 fd,不用直接碰 sendmsg。
数据流:输入是数据字节和 fd 列表;它等待 socket 可写,然后在异步 I/O 回调里发送整包;输出是发送成功或错误。
调用关系:它使用 Tokio 的 async_io 把阻塞风险交给运行时处理,实际打包和发送由 send_datagram_bytes 完成。它是数据报发送的公开入口。
调用图:外部调用 1 个(async_io)。
AsyncDatagramSocket::receive_with_fds401–405 ↗
async fn receive_with_fds(&self) -> std::io::Result<(Vec<u8>, Vec<OwnedFd>)>
作用:异步接收一包数据报,并返回随包传来的文件描述符。
数据流:输入是当前数据报 socket;它等待 socket 可读,然后在异步 I/O 回调里收取数据和控制消息;输出是收到的字节和 OwnedFd 列表。
调用关系:它使用 Tokio 的 async_io 等待读事件,实际解析由 receive_datagram_bytes 完成。它是数据报接收的公开入口。
调用图:外部调用 1 个(async_io)。
AsyncDatagramSocket::into_inner407–409 ↗
fn into_inner(self) -> Socket
作用:拆开 AsyncDatagramSocket,拿回里面的原始 socket。用于需要离开这层封装、交给别的底层代码时。
数据流:输入是 AsyncDatagramSocket 本身;它消费包装对象;输出是内部 socket。
调用关系:这是数据报封装的退出口。正常通信走 send_with_fds 和 receive_with_fds,只有特殊交接时才需要取回底层 socket。
调用图:外部调用 1 个(into_inner)。
tests::fd_list428–435 ↗
tests::async_socket_round_trips_payload_and_fds438–460 ↗
async fn async_socket_round_trips_payload_and_fds() -> std::io::Result<()>
作用:测试流式 AsyncSocket 能否同时把 JSON 消息和文件描述符从一端传到另一端。
数据流:它创建 socket pair、准备测试 payload 和一个 fd;一边异步接收,另一边发送;最后检查收到的结构体相同、fd 数量正确,而且 fd 仍然有效。
调用关系:这个测试覆盖 AsyncSocket::pair、send_with_fds、receive_with_fds 以及 fd 提取链路。它证明核心的“消息加句柄”能力能跑通。
调用图:调用 1 个内部函数(pair);外部调用 5 个(assert!, assert_eq!, fcntl, fd_list, spawn)。
tests::async_socket_handles_large_payload463–471 ↗
async fn async_socket_handles_large_payload() -> std::io::Result<()>
作用:测试流式 AsyncSocket 能处理一次系统调用读不完的大消息。
数据流:它创建一对 socket,发送 10000 个字节的数组;接收端异步读取并解析回来;最后比较发送和接收内容完全一致。
调用关系:这个测试主要验证 read_frame_payload 和 send_stream_frame 的循环读写逻辑。它防止大消息被截断或读乱。
调用图:调用 1 个内部函数(pair);外部调用 3 个(assert_eq!, spawn, vec!)。
tests::async_datagram_sockets_round_trip_messages474–487 ↗
async fn async_datagram_sockets_round_trip_messages() -> std::io::Result<()>
作用:测试数据报 AsyncDatagramSocket 能一包一包地传数据,并能附带文件描述符。
数据流:它创建数据报 socket pair,准备一段字节和一个 fd;客户端发送,服务端接收;最后检查数据内容相同、fd 数量正确。
调用关系:这个测试覆盖 AsyncDatagramSocket::pair、send_with_fds、receive_with_fds 的基本往返。它证明数据报通道没有丢消息边界或 fd。
调用图:调用 1 个内部函数(pair);外部调用 3 个(assert_eq!, fd_list, spawn)。
tests::send_datagram_bytes_rejects_excessive_fd_counts490–496 ↗
fn send_datagram_bytes_rejects_excessive_fd_counts() -> std::io::Result<()>
作用:测试数据报发送时,如果一次夹带太多文件描述符,会被拒绝。
数据流:它创建原始数据报 socket pair,生成超过上限数量的 fd;调用 send_datagram_bytes;期望返回 InvalidInput 错误。
调用关系:这个测试直接验证 make_control_message 的数量限制能通过数据报发送路径生效,避免构造过大的控制消息。
调用图:调用 1 个内部函数(send_datagram_bytes);外部调用 3 个(pair_raw, assert_eq!, fd_list)。
tests::send_stream_chunk_rejects_excessive_fd_counts499–505 ↗
fn send_stream_chunk_rejects_excessive_fd_counts() -> std::io::Result<()>
作用:测试流式发送时,如果一次夹带太多文件描述符,也会被拒绝。
数据流:它创建原始流式 socket pair,生成超过上限数量的 fd;调用 send_stream_chunk 并要求附带 fd;期望得到 InvalidInput 错误。
调用关系:这个测试验证流式发送路径同样遵守 MAX_FDS_PER_MESSAGE 限制,不会因为换了通道类型就绕过检查。
调用图:调用 1 个内部函数(send_stream_chunk);外部调用 3 个(pair_raw, assert_eq!, fd_list)。
tests::encode_length_errors_for_oversized_messages508–511 ↗
fn encode_length_errors_for_oversized_messages()
作用:测试消息长度太大时不会被错误编码。长度字段只有 4 字节,超出范围必须报错。
数据流:它把 usize::MAX 这种明显过大的长度传给 encode_length;函数返回错误;测试确认错误类型是 InvalidInput。
调用关系:这个测试保护 AsyncSocket::send_with_fds 的长度前缀协议,防止超大消息长度被截断后让接收方读错。
调用图:调用 1 个内部函数(encode_length);外部调用 1 个(assert_eq!)。
tests::receive_fails_when_peer_closes_before_header514–522 ↗
async fn receive_fails_when_peer_closes_before_header()
作用:测试对端还没发完消息头就关闭时,接收方会明确报“意外结束”。
数据流:它创建一对 AsyncSocket 后立刻丢掉客户端;服务端尝试 receive;结果应是 UnexpectedEof 错误。
调用关系:这个测试覆盖 read_frame_header 的断线处理。它确保上层不会把半条消息误当成正常空消息。
调用图:调用 1 个内部函数(pair);外部调用 1 个(assert_eq!)。
沙盒与 IDE IPC 通道
这些文件将本地传输基础应用到跨平台的沙盒代理路由和 IDE 上下文请求/响应通信。
linux-sandbox/src/proxy_routing.rs源码 ↗
很多代理只开在 127.0.0.1,也就是“只允许本机访问”的地址。程序进了 Linux 沙箱后,网络环境变了,原来的 127.0.0.1 不再指向宿主机代理,代理设置就会失效。这个文件专门解决这个问题。它先读取常见的代理环境变量,比如 HTTP_PROXY、HTTPS_PROXY,找出那些确实指向本机代理的地址。然后在宿主机侧创建 Unix 域套接字(一种只在本机文件路径上通信的插座),再启动桥接进程,把这个套接字连到真实代理。进入沙箱网络后,它再开一个 127.0.0.1 的临时 TCP 端口,把流量转回宿主机侧套接字,并把环境变量改写到这个新端口。这样沙箱里的程序以为自己还在连本地代理,实际上流量被安全转送到了宿主机代理。文件还会创建私有临时目录、清理旧套接字目录,并让桥接子进程随父进程退出,避免留下垃圾进程和文件。
prepare_host_proxy_route_spec73–122 ↗
fn prepare_host_proxy_route_spec() -> io::Result<String>
作用:在宿主机这一侧准备代理转发方案。它会找出可用的本机代理,创建临时套接字,并启动宿主机侧的桥接进程。
数据流:输入来自当前进程的环境变量 → 它筛选代理变量、规划要转发的目标、创建私有临时目录和 Unix 套接字路径、为每个真实代理启动桥接进程 → 输出一段 JSON 字符串,里面只写环境变量名和套接字路径,不泄露原始代理 URL。
调用关系:这是主流程 run_main 在宿主机侧调用的准备步骤。它把识别代理、清理旧目录、建目录、启动桥接和安排清理工人这些活串起来,最后把可带进沙箱的转发说明交给后续步骤。
调用图:调用 6 个内部函数(cleanup_stale_proxy_socket_dirs_in, create_proxy_socket_dir, plan_proxy_routes, proxy_socket_parent_dir, spawn_host_bridge, spawn_proxy_socket_dir_cleanup_worker);被 1 处调用(run_main);外部调用 7 个(new, with_capacity, new, other, format!, to_string, vars)。
activate_proxy_routes_in_netns124–170 ↗
fn activate_proxy_routes_in_netns(serialized_spec: &str) -> io::Result<()>
作用:在沙箱的网络空间里启用之前准备好的代理路线。它会开本地端口,并把代理环境变量改成指向这些端口。
数据流:输入是一段 JSON 转发说明 → 它解析出每个环境变量该连哪个 Unix 套接字,为每个套接字启动本地 TCP 桥,再读取原环境变量并改写主机和端口 → 最后修改当前进程环境变量,让即将运行的用户命令走新的本地代理入口。
调用关系:这是 run_main 进入沙箱网络环境后调用的步骤。它接过 prepare_host_proxy_route_spec 生成的说明,把宿主机桥接和沙箱内本地端口接起来。
调用图:调用 2 个内部函数(rewrite_proxy_env_value, spawn_local_bridge);被 1 处调用(run_main);外部调用 7 个(new, new, other, format!, from_str, set_var, var)。
plan_proxy_routes172–201 ↗
fn plan_proxy_routes(env: &HashMap<String, String>) -> ProxyRoutePlan
作用:从一堆环境变量里挑出真正需要转发的代理配置。它只接受指向 localhost、127.0.0.1 或 ::1 的代理。
数据流:输入是一张环境变量表 → 它逐个检查变量名是不是常见代理名,忽略空值,再尝试解析本机代理地址 → 输出一个计划,里面有可转发的环境变量和目标地址,也记录是否曾看到代理配置但无法使用。
调用关系:prepare_host_proxy_route_spec 先调用它做“路线规划”。它把变量名判断交给 is_proxy_env_key,把地址解析交给 parse_loopback_proxy_endpoint。测试也直接调用它确认只保留有效路线。
调用图:调用 2 个内部函数(is_proxy_env_key, parse_loopback_proxy_endpoint);被 2 处调用(prepare_host_proxy_route_spec, plan_proxy_routes_only_includes_valid_loopback_endpoints);外部调用 1 个(new)。
is_proxy_env_key203–206 ↗
fn is_proxy_env_key(key: &str) -> bool
作用:判断一个环境变量名是不是项目认识的代理设置名。它不区分大小写,所以 http_proxy 和 HTTP_PROXY 都算。
数据流:输入一个变量名字符串 → 它转成大写后和内置代理变量名单比较 → 输出 true 或 false,表示要不要把这个变量当代理处理。
调用关系:plan_proxy_routes 用它先筛掉 PATH 之类无关变量,避免把普通环境变量误当成代理。
调用图:被 1 处调用(plan_proxy_routes)。
parse_loopback_proxy_endpoint208–239 ↗
fn parse_loopback_proxy_endpoint(proxy_url: &str) -> Option<SocketAddr>
作用:把代理 URL 解析成本机地址加端口。它只接受本机回环地址,避免把外部代理错误纳入这种特殊桥接模式。
数据流:输入一个代理字符串,可能带协议也可能不带 → 它补齐默认协议、解析 URL、检查主机是不是本机、补默认端口 → 输出 SocketAddr,也就是 IP 加端口;如果不是本机代理或格式不对,就输出空。
调用关系:plan_proxy_routes 用它决定某个代理变量能不能被转发。它内部依赖 is_loopback_host 判断主机名是否安全可接。相关测试会验证本机地址能解析、外部域名会被忽略。
调用图:调用 1 个内部函数(is_loopback_host);被 2 处调用(plan_proxy_routes, parses_loopback_proxy_endpoint);外部调用 4 个(V4, new, parse, format!)。
is_loopback_host241–243 ↗
fn is_loopback_host(host: &str) -> bool
作用:判断主机名是不是明确的本机地址。这里认可 localhost、127.0.0.1 和 ::1。
数据流:输入一个主机名 → 它和几个固定的本机写法比较 → 输出是否是本机地址。
调用关系:parse_loopback_proxy_endpoint 在解析代理地址时调用它,先做最直接的安全筛选。
调用图:被 1 处调用(parse_loopback_proxy_endpoint)。
default_proxy_port245–251 ↗
fn default_proxy_port(scheme: &str) -> u16
作用:在代理 URL 没写端口时,给它补一个常见默认端口。比如 https 默认 443,socks 默认 1080。
数据流:输入协议名,比如 http、https、socks5h → 它按常见约定选择端口 → 输出一个端口号。
调用关系:它服务于代理地址解析这一层,让没有显式端口的代理配置也能被理解。测试会检查这些默认值是否符合预期。
rewrite_proxy_env_value253–279 ↗
fn rewrite_proxy_env_value(proxy_url: &str, local_port: u16) -> Option<String>
作用:把原来的代理地址改写成沙箱内可用的新地址。主机统一改成 127.0.0.1,端口改成本地桥接端口。
数据流:输入原代理字符串和一个本地端口 → 它解析 URL,替换主机和端口,并尽量保留原来有没有协议、末尾有没有斜杠的写法 → 输出改写后的代理字符串;解析失败则输出空。
调用关系:activate_proxy_routes_in_netns 在启动本地桥后调用它,把环境变量改成沙箱内程序能连接的地址。测试会直接验证 socks 代理的端口能被正确替换。
调用图:被 2 处调用(activate_proxy_routes_in_netns, rewrites_proxy_url_to_local_loopback_port);外部调用 2 个(parse, format!)。
create_proxy_socket_dir281–304 ↗
fn create_proxy_socket_dir() -> io::Result<PathBuf>
作用:创建本次运行专用的临时套接字目录。目录权限设为只有当前用户可进入,防止别人偷连这些代理通道。
数据流:输入来自系统的临时目录、当前进程号和用户 ID → 它尝试生成不冲突的目录名,最多试 128 次 → 输出创建好的目录路径;如果一直冲突或创建失败,就返回错误。
调用关系:prepare_host_proxy_route_spec 用它为宿主机侧桥接进程放置 Unix 套接字。它会先通过 proxy_socket_parent_dir 选择合适的父目录。
调用图:调用 1 个内部函数(proxy_socket_parent_dir);被 1 处调用(prepare_host_proxy_route_spec);外部调用 5 个(new, new, format!, geteuid, id)。
proxy_socket_parent_dir306–321 ↗
fn proxy_socket_parent_dir() -> PathBuf
作用:选择放代理套接字目录的父目录。它优先使用 CODEX_HOME/tmp,如果不合适就退回系统临时目录或 /tmp。
数据流:输入主要来自环境变量 CODEX_HOME 和系统临时目录设置 → 它检查路径长度是否适合 Linux 套接字限制,并尝试确保目录是私有权限 → 输出一个可用的父目录路径。
调用关系:prepare_host_proxy_route_spec 用它清理旧目录,create_proxy_socket_dir 用它创建新目录。它把权限准备交给 ensure_private_proxy_socket_parent_dir,把路径长度判断交给 proxy_socket_paths_fit。
调用图:调用 2 个内部函数(ensure_private_proxy_socket_parent_dir, proxy_socket_paths_fit);被 2 处调用(create_proxy_socket_dir, prepare_host_proxy_route_spec);外部调用 3 个(from, temp_dir, var_os)。
proxy_socket_paths_fit323–332 ↗
fn proxy_socket_paths_fit(parent: &Path) -> bool
作用:检查某个父目录下生成的套接字路径会不会超过 Linux 的长度限制。Unix 套接字路径太长会直接绑定失败。
数据流:输入一个父目录路径 → 它用最坏情况拼出可能最长的套接字路径,并计算字节长度 → 输出 true 或 false。
调用关系:proxy_socket_parent_dir 用它判断 CODEX_HOME/tmp 或系统临时目录是否能安全使用。测试会用很长的路径确认这个保护有效。
调用图:被 1 处调用(proxy_socket_parent_dir);外部调用 2 个(join, format!)。
ensure_private_proxy_socket_parent_dir334–344 ↗
fn ensure_private_proxy_socket_parent_dir(path: &Path) -> io::Result<()>
作用:确保某个父目录存在,并且权限是 700。700 的意思是只有当前用户能读、写、进入。
数据流:输入一个路径 → 它递归创建目录,已存在也可以,然后设置权限 → 成功则没有额外输出,失败则返回系统错误。
调用关系:proxy_socket_parent_dir 在选择 CODEX_HOME/tmp 时调用它,保证代理套接字不会放在过于开放的目录里。
调用图:被 1 处调用(proxy_socket_parent_dir);外部调用 3 个(new, from_mode, set_permissions)。
cleanup_stale_proxy_socket_dirs_in346–373 ↗
fn cleanup_stale_proxy_socket_dirs_in(temp_dir: &Path) -> io::Result<()>
作用:清理已经没人使用的旧代理套接字目录。这样可以避免临时目录里堆满上次异常退出留下的垃圾。
数据流:输入一个临时父目录 → 它遍历里面的子目录,挑出名字像本项目代理目录的项,读出拥有者进程号,确认进程已经不在后删除 → 输出清理是否整体成功。
调用关系:prepare_host_proxy_route_spec 在创建新目录前调用它做卫生清理。它依赖 parse_proxy_socket_dir_owner_pid 读目录名里的进程号,依赖 is_pid_alive 判断进程是否还活着,最后用 cleanup_proxy_socket_dir 删除。测试会验证只删死进程目录。
调用图:调用 3 个内部函数(cleanup_proxy_socket_dir, is_pid_alive, parse_proxy_socket_dir_owner_pid);被 2 处调用(prepare_host_proxy_route_spec, cleanup_stale_proxy_socket_dirs_removes_dead_pid_directories);外部调用 1 个(read_dir)。
parse_proxy_socket_dir_owner_pid375–379 ↗
fn parse_proxy_socket_dir_owner_pid(file_name: &str) -> Option<u32>
作用:从代理临时目录名里读出创建它的进程号。目录名里带进程号,是为了以后能判断它是不是过期垃圾。
数据流:输入一个目录名字符串 → 它检查固定前缀,取出第一个短横线前的数字,并排除 0 → 输出进程号;格式不对就输出空。
调用关系:cleanup_stale_proxy_socket_dirs_in 用它识别哪些目录属于本代理路由功能。测试会覆盖正常名字、旧格式名字和无关名字。
调用图:被 1 处调用(cleanup_stale_proxy_socket_dirs_in)。
is_pid_alive381–386 ↗
fn is_pid_alive(pid: u32) -> bool
作用:判断一个 u32 格式的进程号当前是否还存在。它先把数字转成系统调用需要的进程号类型。
数据流:输入一个进程号数字 → 它尝试转换成 libc 的 pid_t → 再交给底层检查函数 → 输出这个进程看起来是否还活着。
调用关系:cleanup_stale_proxy_socket_dirs_in 用它决定某个旧目录能不能删。它把真正的系统探测交给 is_pid_alive_raw。
调用图:调用 1 个内部函数(is_pid_alive_raw);被 1 处调用(cleanup_stale_proxy_socket_dirs_in);外部调用 1 个(try_from)。
is_pid_alive_raw388–395 ↗
fn is_pid_alive_raw(pid: libc::pid_t) -> bool
作用:用系统调用检查进程是否存在。它不会真的杀进程,只是用 kill(pid, 0) 问系统“这个进程还在吗”。
数据流:输入系统格式的进程号 → 它调用 kill(pid, 0),如果成功说明进程存在;如果错误是 ESRCH,说明不存在;其他错误一般按存在处理 → 输出 true 或 false。
调用关系:is_pid_alive 调用它完成底层判断。清理旧目录和清理工人都靠这种判断避免误删正在使用的资源。
调用图:被 1 处调用(is_pid_alive);外部调用 3 个(last_os_error, kill, matches!)。
spawn_proxy_socket_dir_cleanup_worker397–423 ↗
fn spawn_proxy_socket_dir_cleanup_worker(
socket_dir: PathBuf,
host_bridge_pids: Vec<libc::pid_t>,
) -> io::Result<()>
作用:启动一个小的清理子进程,等所有宿主机桥接进程结束后删除本次临时套接字目录。
数据流:输入套接字目录路径和一组桥接进程号 → 它 fork 出子进程,子进程循环检查这些桥是否都退出,退出后删除目录并结束 → 父进程只得到成功或失败结果。
调用关系:prepare_host_proxy_route_spec 在宿主机桥都启动好后调用它。它用 is_pid_alive_raw 等待桥进程消失,再把删除工作交给 cleanup_proxy_socket_dir。
调用图:调用 1 个内部函数(cleanup_proxy_socket_dir);被 1 处调用(prepare_host_proxy_route_spec);外部调用 6 个(from_millis, as_path, last_os_error, _exit, fork, sleep)。
cleanup_proxy_socket_dir425–439 ↗
fn cleanup_proxy_socket_dir(socket_dir: &Path) -> io::Result<()>
作用:删除一个代理套接字目录,里面的套接字文件也一起删掉。它会重试几次,处理短时间内文件还被占用的情况。
数据流:输入目录路径 → 它最多多次尝试 remove_dir_all,遇到不存在就当作成功,遇到临时失败就睡一会儿再试 → 输出删除成功或最后一次错误。
调用关系:cleanup_stale_proxy_socket_dirs_in 用它删旧目录,spawn_proxy_socket_dir_cleanup_worker 用它删本次运行结束后的目录。测试会创建假文件确认它能清干净。
调用图:被 3 处调用(cleanup_stale_proxy_socket_dirs_in, spawn_proxy_socket_dir_cleanup_worker, cleanup_proxy_socket_dir_removes_bridge_artifacts);外部调用 3 个(from_millis, remove_dir_all, sleep)。
spawn_host_bridge441–472 ↗
fn spawn_host_bridge(endpoint: SocketAddr, uds_path: &Path) -> io::Result<libc::pid_t>
作用:启动宿主机侧桥接进程。这个桥监听 Unix 套接字,并把连接转到真实的本机代理端口。
数据流:输入真实代理地址和 Unix 套接字路径 → 它创建一根“就绪通知管道”,fork 子进程运行桥,父进程等待子进程写回准备好了的信号 → 输出桥接子进程的进程号。
调用关系:prepare_host_proxy_route_spec 为每个不同代理端点调用它。它把子进程里的长期监听工作交给 run_host_bridge,并用 create_ready_pipe、close_fd 管好父子进程之间的文件描述符。
调用图:调用 3 个内部函数(close_fd, create_ready_pipe, run_host_bridge);被 1 处调用(prepare_host_proxy_route_spec);外部调用 5 个(from_raw_fd, last_os_error, other, _exit, fork)。
run_host_bridge474–495 ↗
fn run_host_bridge(endpoint: SocketAddr, uds_path: &Path, ready_fd: libc::c_int) -> io::Result<()>
作用:真正运行宿主机侧桥。它接收 Unix 套接字连接,然后连到宿主机上的真实代理。
数据流:输入真实代理地址、套接字路径和就绪通知管道 → 它先加固进程,删除旧套接字文件,绑定新套接字,通知父进程已准备好 → 之后无限接受连接,每个连接开一个线程转发数据。
调用关系:spawn_host_bridge fork 出的子进程调用它。它建立宿主机侧入口,并在每次有连接时借助 proxy_bidirectional 在 Unix 套接字和 TCP 代理之间搬运数据。
调用图:调用 2 个内部函数(harden_bridge_process, bind);被 1 处调用(spawn_host_bridge);外部调用 4 个(from_raw_fd, exists, remove_file, spawn)。
spawn_local_bridge497–523 ↗
fn spawn_local_bridge(uds_path: &Path) -> io::Result<u16>
作用:启动沙箱网络里的本地桥接进程。这个桥会在 127.0.0.1 上开一个临时端口,供沙箱内程序连接。
数据流:输入宿主机侧 Unix 套接字路径 → 它创建就绪管道,fork 子进程运行本地桥,父进程读取子进程写回的端口号 → 输出这个临时 TCP 端口。
调用关系:activate_proxy_routes_in_netns 为每个不同 Unix 套接字调用它。它把长期监听工作交给 run_local_bridge,并把端口号交回给环境变量改写步骤。
调用图:调用 3 个内部函数(close_fd, create_ready_pipe, run_local_bridge);被 1 处调用(activate_proxy_routes_in_netns);外部调用 5 个(from_raw_fd, last_os_error, _exit, fork, from_be_bytes)。
run_local_bridge525–546 ↗
fn run_local_bridge(uds_path: &Path, ready_fd: libc::c_int) -> io::Result<()>
作用:真正运行沙箱内的本地桥。它把沙箱里的 TCP 连接转到宿主机侧的 Unix 套接字。
数据流:输入 Unix 套接字路径和就绪通知管道 → 它加固进程,绑定一个 127.0.0.1 随机端口,把端口号写回父进程 → 之后不断接受沙箱内连接,并为每个连接开线程转发数据。
调用关系:spawn_local_bridge fork 出的子进程调用它。它依赖 bind_local_loopback_listener 拿到可用本地端口,连接到 UnixStream 后用 proxy_bidirectional 搬运数据。
调用图:调用 2 个内部函数(bind_local_loopback_listener, harden_bridge_process);被 1 处调用(spawn_local_bridge);外部调用 4 个(from_raw_fd, clone, to_path_buf, spawn)。
bind_local_loopback_listener548–564 ↗
fn bind_local_loopback_listener() -> io::Result<TcpListener>
作用:在 127.0.0.1 上绑定一个随机可用端口。如果沙箱里的回环网卡没起来,它会尝试把它启动。
数据流:没有业务输入 → 它先尝试绑定 127.0.0.1:0,0 表示让系统挑空闲端口;如果失败原因像是回环网卡不可用,就先修复网卡再重试 → 输出一个 TCP 监听器。
调用关系:run_local_bridge 用它创建沙箱内程序要连接的本地入口。必要时它把网卡修复交给 ensure_loopback_interface_up。
调用图:调用 1 个内部函数(ensure_loopback_interface_up);被 1 处调用(run_local_bridge);外部调用 2 个(bind, matches!)。
ensure_loopback_interface_up566–626 ↗
fn ensure_loopback_interface_up() -> io::Result<()>
作用:确保 Linux 的 lo 回环网卡处于可用状态,并配置 127.0.0.1。没有这个,沙箱里连 127.0.0.1 可能会失败。
数据流:没有业务输入 → 它打开一个控制套接字,读取 lo 的标志位,必要时设置 IFF_UP,再尝试设置回环地址 → 最后关闭文件描述符,成功或返回系统错误。
调用关系:bind_local_loopback_listener 只在绑定本地地址失败且像是网卡未就绪时调用它。它用 close_fd 做收尾,避免底层文件描述符泄漏。
调用图:调用 1 个内部函数(close_fd);被 1 处调用(bind_local_loopback_listener);外部调用 5 个(last_os_error, htonl, ioctl, socket, matches!)。
set_parent_death_signal628–637 ↗
fn set_parent_death_signal() -> io::Result<()>
作用:让桥接子进程在父进程退出时收到 SIGTERM 终止信号。这样主程序没了,桥不会孤零零留在后台。
数据流:没有业务输入 → 它通过 prctl 设置“父进程死亡信号”,然后检查父进程是否已经变成 1 号进程 → 输出成功;如果设置失败或父进程已退出,就返回错误。
调用关系:harden_bridge_process 调用它作为桥接进程加固的一部分。宿主机桥和本地桥都会间接受到这个保护。
调用图:被 1 处调用(harden_bridge_process);外部调用 4 个(last_os_error, other, getppid, prctl)。
harden_bridge_process639–642 ↗
fn harden_bridge_process() -> io::Result<()>
作用:给桥接子进程做基本安全加固。它既让子进程跟随父进程退出,也禁止进程转储,减少敏感信息泄露风险。
数据流:没有业务输入 → 它先设置父进程死亡信号,再调用进程加固库关闭 dump 能力 → 输出成功或错误。
调用关系:run_host_bridge 和 run_local_bridge 一启动就调用它。它把具体的父进程退出保护交给 set_parent_death_signal。
调用图:调用 1 个内部函数(set_parent_death_signal);被 2 处调用(run_host_bridge, run_local_bridge);外部调用 1 个(disable_process_dumping)。
proxy_bidirectional644–655 ↗
fn proxy_bidirectional(mut tcp_stream: TcpStream, mut unix_stream: UnixStream) -> io::Result<()>
作用:在一个 TCP 连接和一个 Unix 套接字连接之间双向搬运数据。简单说,就是两根管子之间互相倒水。
数据流:输入一个 TCP 流和一个 Unix 流 → 它克隆读写端,一边在线程里从 TCP 复制到 Unix,另一边从 Unix 复制到 TCP → 两个方向都结束且没出错时返回成功。
调用关系:宿主机桥和本地桥在每次接受连接后都会用它做实际数据转发。上层负责接入连接,它负责把字节原样来回传。
create_ready_pipe657–664 ↗
fn create_ready_pipe() -> io::Result<(libc::c_int, libc::c_int)>
作用:创建父子进程之间用来通知“我准备好了”的管道。这样父进程不会在桥还没监听好时就继续往下走。
数据流:没有业务输入 → 它调用 pipe2 创建一对文件描述符,并设置执行新程序时自动关闭 → 输出读端和写端两个数字;失败则返回系统错误。
调用关系:spawn_host_bridge 和 spawn_local_bridge 都用它做启动同步。父进程读,子进程写,保证桥接服务真的可用了。
调用图:被 2 处调用(spawn_host_bridge, spawn_local_bridge);外部调用 2 个(last_os_error, pipe2)。
close_fd666–672 ↗
fn close_fd(fd: libc::c_int) -> io::Result<()>
作用:关闭一个底层文件描述符。文件描述符可以理解为系统给打开文件、管道或套接字发的号码牌。
数据流:输入一个文件描述符数字 → 它调用系统 close → 输出成功或关闭失败的错误。
调用关系:spawn_host_bridge、spawn_local_bridge 和 ensure_loopback_interface_up 都用它清理不再需要的底层资源,避免资源泄漏。
调用图:被 3 处调用(ensure_loopback_interface_up, spawn_host_bridge, spawn_local_bridge);外部调用 2 个(last_os_error, close)。
tests::recognizes_proxy_env_keys_case_insensitively694–698 ↗
fn recognizes_proxy_env_keys_case_insensitively()
作用:测试代理环境变量名识别是否不区分大小写。它确保 http_proxy 这种小写写法也能被识别。
数据流:输入几组固定变量名 → 调用判断逻辑并比较结果 → 测试通过表示代理变量名单和大小写处理都正常。
调用关系:它验证 is_proxy_env_key 的基础行为,防止后续规划路线时漏掉用户常见写法。
调用图:外部调用 1 个(assert_eq!)。
tests::parses_loopback_proxy_endpoint701–711 ↗
fn parses_loopback_proxy_endpoint()
作用:测试本机代理 URL 能被正确解析。它用 127.0.0.1 加端口作为样例。
数据流:输入一个固定代理 URL → 调用 parse_loopback_proxy_endpoint → 比较输出是否是预期的 127.0.0.1:43128。
调用关系:它验证代理规划中最关键的地址解析步骤,保证有效本机代理不会被误丢弃。
调用图:调用 1 个内部函数(parse_loopback_proxy_endpoint);外部调用 1 个(assert_eq!)。
tests::ignores_non_loopback_proxy_endpoint714–719 ↗
fn ignores_non_loopback_proxy_endpoint()
作用:测试外部域名代理不会进入本机桥接路线。这个功能只处理宿主机本地代理,不处理 example.com 这类远端代理。
数据流:输入一个 example.com 代理 URL → 解析函数尝试处理后返回空 → 测试确认外部地址被忽略。
调用关系:它保护 parse_loopback_proxy_endpoint 的边界,避免把不该走本机桥的代理纳入特殊路径。
调用图:外部调用 1 个(assert_eq!)。
tests::plan_proxy_routes_only_includes_valid_loopback_endpoints722–744 ↗
fn plan_proxy_routes_only_includes_valid_loopback_endpoints()
作用:测试路线规划只保留有效本机代理。即使环境里有外部代理和普通 PATH,也不会混进去。
数据流:输入一张人工构造的环境变量表 → 调用 plan_proxy_routes → 检查它只生成 HTTP_PROXY 这一条本机路线,同时记录确实看到了代理配置。
调用关系:它覆盖 plan_proxy_routes 对变量名筛选、地址解析和结果排序的组合行为。
调用图:调用 1 个内部函数(plan_proxy_routes);外部调用 2 个(new, assert_eq!)。
tests::rewrites_proxy_url_to_local_loopback_port747–752 ↗
fn rewrites_proxy_url_to_local_loopback_port()
作用:测试代理 URL 能被改写到新的本地端口。这里用 socks5h 代理作为样例。
数据流:输入原代理 URL 和端口 43210 → 调用 rewrite_proxy_env_value → 检查输出保留协议但端口已替换。
调用关系:它验证 activate_proxy_routes_in_netns 最后改写环境变量时依赖的核心函数。
调用图:调用 1 个内部函数(rewrite_proxy_env_value);外部调用 1 个(assert_eq!)。
tests::default_proxy_ports_match_expected_schemes755–759 ↗
fn default_proxy_ports_match_expected_schemes()
作用:测试没有显式端口时使用的默认端口是否符合常识。比如 http 是 80,https 是 443。
数据流:输入几个协议名 → 调用 default_proxy_port → 比较输出端口是否正确。
调用关系:它守住代理地址解析里的默认端口规则,避免配置没写端口时行为突然改变。
调用图:外部调用 1 个(assert_eq!)。
tests::proxy_socket_paths_enforce_linux_path_limit762–771 ↗
fn proxy_socket_paths_enforce_linux_path_limit()
作用:测试 Unix 套接字路径长度限制会被检查。路径太长在 Linux 上会失败,所以要提前挡住。
数据流:输入一个正常 /tmp 路径和一个很长路径 → 调用 proxy_socket_paths_fit → 检查短路径可用、长路径不可用。
调用关系:它验证 proxy_socket_parent_dir 选择临时目录时依赖的安全检查。
调用图:外部调用 1 个(assert_eq!)。
tests::cleanup_proxy_socket_dir_removes_bridge_artifacts774–784 ↗
fn cleanup_proxy_socket_dir_removes_bridge_artifacts()
作用:测试代理临时目录清理函数能删除目录和里面的文件。它模拟桥接留下的套接字文件。
数据流:先创建临时目录、子目录和一个标记文件 → 调用 cleanup_proxy_socket_dir → 检查整个子目录已经不存在。
调用关系:它验证 cleanup_proxy_socket_dir 的实际删除能力,这是旧目录清理和退出后清理都会依赖的函数。
调用图:调用 1 个内部函数(cleanup_proxy_socket_dir);外部调用 4 个(assert_eq!, create_dir, write, tempdir)。
tests::proxy_route_spec_serialization_omits_proxy_urls787–800 ↗
fn proxy_route_spec_serialization_omits_proxy_urls()
作用:测试转发说明序列化时不会包含原始代理 URL。这样带进沙箱的数据更少,也减少泄露代理信息的机会。
数据流:输入一个只含环境变量名和套接字路径的 ProxyRouteSpec → 序列化成 JSON → 检查 JSON 只包含这两类信息。
调用关系:它验证 prepare_host_proxy_route_spec 输出格式的安全边界,确保说明文件只描述桥接路径,不保存代理地址本身。
调用图:外部调用 3 个(assert_eq!, to_string, vec!)。
tests::parse_proxy_socket_dir_owner_pid_reads_owner_pid803–817 ↗
fn parse_proxy_socket_dir_owner_pid_reads_owner_pid()
作用:测试能从代理临时目录名里读出进程号,也能拒绝无效名字。
数据流:输入几个目录名样例 → 调用 parse_proxy_socket_dir_owner_pid → 检查正常名字返回 1234,错误名字返回空。
调用关系:它验证 cleanup_stale_proxy_socket_dirs_in 识别旧目录所有者时使用的小解析器。
调用图:外部调用 1 个(assert_eq!)。
tests::cleanup_stale_proxy_socket_dirs_removes_dead_pid_directories820–840 ↗
fn cleanup_stale_proxy_socket_dirs_removes_dead_pid_directories()
作用:测试旧目录清理只删除死进程留下的目录,不碰当前还活着的目录和无关目录。
数据流:先创建三个目录:死进程样式、当前进程样式、无关名字 → 调用 cleanup_stale_proxy_socket_dirs_in → 检查只有死进程目录被删除。
调用关系:它验证 cleanup_stale_proxy_socket_dirs_in、parse_proxy_socket_dir_owner_pid、is_pid_alive 和 cleanup_proxy_socket_dir 组合起来的行为。
调用图:调用 1 个内部函数(cleanup_stale_proxy_socket_dirs_in);外部调用 4 个(assert_eq!, format!, create_dir, tempdir)。
tui/src/ide_context/windows_pipe.rs源码 ↗
在 Windows 里,命名管道可以理解成两个程序之间的一根本地电话线。这个文件做的事,就是拨通这根电话线、确认电话另一头确实是当前用户开的 IDE 服务,然后提供读和写。它很重要,因为如果不检查对方身份,程序可能连到别的用户或恶意进程开的管道;如果不设置超时,读写时对方没反应就可能一直挂住。核心类型是 WindowsPipeStream,它保存管道句柄和截止时间。读写时它使用 Windows 的“重叠 I/O”(一种异步读写方式,先发起操作,再等完成),由 OverlappedOperation 包装等待、取消和收尾。OwnedHandle 像一个会自动关门的钥匙扣,句柄不用时会自动关闭,避免系统资源泄漏。另一些函数负责读取进程令牌里的用户身份,并比较服务端和当前程序是不是同一个用户。
WindowsPipeStream::connect56–82 ↗
fn connect(pipe_path: PathBuf, deadline: Instant) -> io::Result<Self>
作用:打开指定的 Windows 命名管道,并创建一个可以读写的 WindowsPipeStream。它还会检查管道服务端是不是当前用户启动的,避免连到不该信任的进程。
数据流:进去的是管道路径和一个截止时间。它先把路径转成 Windows API 需要的宽字符格式,再调用系统接口打开管道句柄;打开失败就返回系统错误。打开成功后,它把裸句柄包进 OwnedHandle,并调用 validate_pipe_server_owner 做身份检查,最后产出一个带句柄和截止时间的流对象。
调用关系:它是 Windows 管道连接的入口,被上层的 connect_stream 在需要建立 IDE 上下文连接时调用。它自己负责开门,但会把“确认门后是谁”这一步交给 validate_pipe_server_owner。
调用图:调用 1 个内部函数(validate_pipe_server_owner);被 1 处调用(connect_stream);外部调用 5 个(as_os_str, last_os_error, null, once, CreateFileW)。
WindowsPipeStream::set_deadline84–86 ↗
fn set_deadline(&mut self, deadline: Instant)
作用:更新这个管道流的读写截止时间。调用者用它来告诉后续读写:最多等到什么时候。
数据流:进去的是新的时间点。函数直接把 WindowsPipeStream 里保存的 deadline 换成这个新值;没有返回数据,也不碰管道本身。
调用关系:它通常由上层在一次通信前调整超时策略时使用。之后 WindowsPipeStream::read 和 WindowsPipeStream::write 会按这个新截止时间等待。
WindowsPipeStream::read90–108 ↗
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>
作用:从管道里读取字节,就像从文件或网络连接里读数据一样。它会遵守当前设置的截止时间,避免一直等不到数据。
数据流:进去的是一个可写的字节缓冲区。若缓冲区为空,直接返回 0;否则它创建一次 OverlappedOperation,把缓冲区交给 Windows 的 ReadFile 发起异步读取,然后让 operation.complete 等待读取完成。出来的是实际读到的字节数,或者读失败、超时等错误。
调用关系:它是 Rust 标准 Read 接口的实现,所以外部代码可以把 WindowsPipeStream 当成普通输入流用。它把具体的异步等待、超时和收尾交给 OverlappedOperation::complete。
调用图:调用 2 个内部函数(new, raw);外部调用 3 个(null_mut, try_from, ReadFile)。
WindowsPipeStream::write112–130 ↗
fn write(&mut self, buf: &[u8]) -> io::Result<usize>
作用:把一段字节写进管道,发送给 IDE 上下文服务。它同样带超时,防止对方不接收时程序卡死。
数据流:进去的是要发送的字节切片。若内容为空,直接返回 0;否则它创建一次 OverlappedOperation,调用 Windows 的 WriteFile 发起异步写入,再交给 operation.complete 等待结果。出来的是实际写出的字节数,或者写入失败、超时等错误。
调用关系:它是 Rust 标准 Write 接口的实现,上层发送请求时会走到这里。它和 read 使用同一套 OverlappedOperation 机制,所以读写的等待和取消规则保持一致。
调用图:调用 2 个内部函数(new, raw);外部调用 3 个(null_mut, try_from, WriteFile)。
WindowsPipeStream::flush132–134 ↗
fn flush(&mut self) -> io::Result<()>
作用:满足 Rust Write 接口要求的“刷新”动作。这里没有额外缓存,所以它什么也不用真正刷新。
数据流:没有实际输入数据。函数直接返回成功,不修改任何状态,也不向 Windows 发送额外请求。
调用关系:当上层把 WindowsPipeStream 当成普通输出流并调用 flush 时会到这里。因为写入已经直接交给系统管道,所以不需要再把缓存推送出去。
OverlappedOperation::new143–155 ↗
fn new() -> io::Result<Self>
作用:准备一次 Windows 异步读写操作需要的等待工具。可以把它理解成给这次操作配一个“完成铃”,系统操作结束时会响。
数据流:没有业务输入。它调用 CreateEventW 创建一个事件对象,失败就返回系统错误;成功后创建清零的 OVERLAPPED 结构,并把事件放进去。出来的是一个 OverlappedOperation,里面带事件句柄和异步操作信息。
调用关系:WindowsPipeStream::read 和 WindowsPipeStream::write 每次发起读写前都会调用它。后续 complete 会靠它里面的事件判断操作是否完成。
调用图:被 2 处调用(read, write);外部调用 3 个(last_os_error, null, CreateEventW)。
OverlappedOperation::as_mut_ptr157–159 ↗
fn as_mut_ptr(&mut self) -> *mut OVERLAPPED
作用:把内部的 OVERLAPPED 结构交给 Windows API 使用。Windows API 需要原始指针,所以这里提供这个入口。
数据流:进去的是这次 OverlappedOperation 自己。函数返回内部 OVERLAPPED 的可修改指针,不复制数据,也不完成操作。
调用关系:OverlappedOperation::complete 和 OverlappedOperation::cancel_and_timeout 会用它把同一份异步操作记录传给 Windows。它是这个文件里少数贴近底层系统接口的辅助函数。
调用图:被 2 处调用(cancel_and_timeout, complete)。
OverlappedOperation::complete161–196 ↗
fn complete(
&mut self,
handle: HANDLE,
initial_result: BOOL,
deadline: Instant,
) -> io::Result<usize>
作用:等待一次异步读写真正结束,并取回读了或写了多少字节。它还负责处理“还没完成”“超时”“等待失败”等情况。
数据流:进去的是管道句柄、ReadFile 或 WriteFile 的初始结果,以及截止时间。如果初始结果表示操作还在进行,它会按 remaining_timeout_ms 算出还能等多久,再等待事件;超时就调用 cancel_and_timeout 取消。操作完成后,它调用 GetOverlappedResult 取回传输了多少字节。出来的是字节数,或一个具体错误。
调用关系:read 和 write 发起系统调用后都会把后半段交给它。它是异步 I/O 流程的中心:需要时间计算时找 remaining_timeout_ms,需要超时取消时找 cancel_and_timeout,需要访问句柄时用 OwnedHandle::raw。
调用图:调用 4 个内部函数(as_mut_ptr, cancel_and_timeout, raw, remaining_timeout_ms);外部调用 5 个(last_os_error, other, format!, GetOverlappedResult, WaitForSingleObject)。
OverlappedOperation::cancel_and_timeout198–220 ↗
fn cancel_and_timeout(&mut self, handle: HANDLE) -> io::Error
作用:在读写等太久时,取消还没完成的 Windows 异步操作,并返回一个超时错误。它的重点是安全收尾,避免系统还在用那块操作记录时函数就退出。
数据流:进去的是管道句柄和当前异步操作。它先调用 CancelIoEx 请求取消;如果发现操作其实刚好已经完成,就做一次不等待的结果回收。若取消成功,它再调用 GetOverlappedResult 等系统把取消收尾完成。最后出来的是统一的超时错误。
调用关系:它只由 OverlappedOperation::complete 在等待超时时调用。complete 负责判断“该不该取消”,它负责把取消和清理做干净。
调用图:调用 2 个内部函数(as_mut_ptr, timeout_io_error);被 1 处调用(complete);外部调用 3 个(last_os_error, CancelIoEx, GetOverlappedResult)。
OwnedHandle::raw226–228 ↗
OwnedHandle::drop232–238 ↗
fn drop(&mut self)
作用:当 OwnedHandle 不再使用时,自动关闭 Windows 句柄。这样可以防止管道、事件、进程、令牌等系统资源泄漏。
数据流:进去的是即将被销毁的 OwnedHandle。它检查句柄不是空值也不是无效值后,调用 CloseHandle 关闭;没有返回值,但会释放系统资源。
调用关系:这是 Rust 的 Drop 清理钩子,调用者通常不用手动调用。WindowsPipeStream、OverlappedOperation 以及身份检查中打开的进程和令牌句柄,都靠它在离开作用域时自动收尾。
调用图:外部调用 1 个(CloseHandle)。
TokenUserBuffer::sid246–260 ↗
fn sid(&self) -> io::Result<windows_sys::Win32::Foundation::PSID>
作用:从 Windows 令牌用户信息缓冲区里取出 SID。SID 可以理解成 Windows 给用户账号发的身份证号码。
数据流:进去的是之前由 token_user 填好的字节缓冲区。它先检查缓冲区至少能放下 TOKEN_USER 头部;太小就报数据无效。然后用不要求内存对齐的方式读出 TOKEN_USER,并返回其中的用户 SID 指针。
调用关系:validate_pipe_server_owner 会拿服务端用户和当前用户的 TokenUserBuffer 调用它,然后交给 EqualSid 比较。它把一堆原始字节变成可比较的用户身份指针。
调用图:外部调用 2 个(new, read_unaligned)。
validate_pipe_server_owner263–289 ↗
fn validate_pipe_server_owner(pipe_handle: HANDLE) -> io::Result<()>
作用:确认命名管道另一头的服务进程属于当前 Windows 用户。这样可以避免客户端误连到别的用户或恶意程序开的同名服务。
数据流:进去的是已经打开的管道句柄。它先问系统这个管道服务端的进程 ID,再打开该进程,读取服务端进程令牌和当前进程令牌;接着分别取出用户 SID,并用 EqualSid 比较。相同就返回成功,不同就返回权限拒绝错误。
调用关系:WindowsPipeStream::connect 打开管道后马上调用它。它自己把“打开进程令牌”交给 open_process_token,把“读取令牌用户信息”交给 token_user,把“取出 SID”交给 TokenUserBuffer::sid。
调用图:调用 2 个内部函数(open_process_token, token_user);被 1 处调用(connect);外部调用 6 个(last_os_error, new, EqualSid, GetNamedPipeServerProcessId, GetCurrentProcess, OpenProcess)。
open_process_token291–299 ↗
fn open_process_token(process: HANDLE) -> io::Result<OwnedHandle>
作用:打开某个进程的访问令牌,以便后面查看这个进程是哪个用户在运行。访问令牌可以理解成 Windows 进程随身带的身份牌。
数据流:进去的是一个进程句柄。它调用 OpenProcessToken 请求查询权限;失败就返回系统错误,成功就把得到的令牌句柄包进 OwnedHandle 返回。
调用关系:它由 validate_pipe_server_owner 调用,分别用于服务端进程和当前进程。返回的 OwnedHandle 会在后续 token_user 读取完信息后自动关闭。
调用图:被 1 处调用(validate_pipe_server_owner);外部调用 2 个(last_os_error, OpenProcessToken)。
token_user301–325 ↗
fn token_user(token: HANDLE) -> io::Result<TokenUserBuffer>
作用:从一个进程令牌里读取“这个令牌属于哪个用户”的原始信息。它用两步读取,是因为 Windows 要先告诉程序需要多大的缓冲区。
数据流:进去的是令牌句柄。它先用空缓冲区调用 GetTokenInformation,让系统返回需要的长度;如果长度为 0 就报错。然后按这个长度分配字节数组,再调用一次 GetTokenInformation 把用户信息写进去。出来的是 TokenUserBuffer。
调用关系:validate_pipe_server_owner 用它读取服务端和当前进程的用户信息。之后 TokenUserBuffer::sid 会从这个缓冲区里拿出 SID 供 EqualSid 比较。
调用图:被 1 处调用(validate_pipe_server_owner);外部调用 4 个(last_os_error, null_mut, vec!, GetTokenInformation)。
remaining_timeout_ms327–335 ↗
fn remaining_timeout_ms(deadline: Instant) -> u32
作用:计算从现在到截止时间还剩多少毫秒,给 Windows 的等待函数使用。它把“具体时间点”转换成“还能等多久”。
数据流:进去的是一个截止时间。如果现在已经过了截止时间,就返回 0;否则计算剩余毫秒数,至少返回 1 毫秒,并在数值太大时限制到 u32 能表示的最大值。
调用关系:OverlappedOperation::complete 在等待异步读写完成前调用它。这样 WaitForSingleObject 拿到的是 Windows API 需要的毫秒超时时间。
调用图:被 1 处调用(complete);外部调用 3 个(duration_since, now, try_from)。
timeout_io_error337–339 ↗
fn timeout_io_error() -> io::Error
作用:创建统一的超时错误,表示等待 IDE 上下文服务响应太久。这样上层能清楚知道问题是超时,而不是普通读写失败。
数据流:没有输入。它构造一个 io::ErrorKind::TimedOut 类型的错误,错误信息是“timed out waiting for IDE context”,并返回这个错误对象。
调用关系:OverlappedOperation::cancel_and_timeout 在完成取消和清理后调用它。它让所有超时路径返回一致的错误。
调用图:被 1 处调用(cancel_and_timeout);外部调用 1 个(new)。
tui/src/ide_context/ipc.rs源码 ↗
当用户在 TUI 里想让 Codex 参考 IDE 当前上下文时,程序不能直接读编辑器内部状态,只能通过本机进程间通信(IPC,意思是同一台电脑上两个程序互相说话)去问 IDE 插件。这个文件就是这条通信通道。它先找到默认的通信地址:Unix 上是临时目录里的 socket 文件,Windows 上是命名管道。然后在 5 秒总时限内连上插件,发一条 JSON 请求,并按“前 4 个字节表示长度,后面是 JSON 内容”的格式读回回复。它还会过滤掉广播、发现请求等路上遇到的其他消息,只等待属于自己这次请求的回复。Unix 上它特别小心:检查 socket 文件和对端进程是不是当前用户拥有,避免连到别人伪造的服务。最后,它把 IDE 返回的 JSON 转成项目内部的 IdeContext;如果失败,就给出适合展示给用户或提示词跳过时使用的友好解释。
IdeContextError::user_facing_hint120–122 ↗
fn user_facing_hint(&self) -> String
作用:把内部错误变成用户能看懂的简短建议,比如提示打开 VS Code 或重试 /ide。有人要把错误展示在界面上时会用它。
数据流:进去的是一个具体错误类型 → 它判断是连不上、发送失败、回复太大还是回复格式不对 → 出来是一段适合直接给用户看的文字,不改动其他状态。
调用关系:它是错误处理链的末端之一。前面的连接、读写、解析函数只负责报错;到需要显示给用户时,这个函数负责把技术原因翻译成人话。
调用图:外部调用 1 个(format!)。
IdeContextError::prompt_skip_hint125–127 ↗
fn prompt_skip_hint(&self) -> String
作用:当这次提示词里暂时拿不到 IDE 上下文时,给出一段解释为什么跳过、以后是否还会继续尝试的话。
数据流:进去的是某个 IDE 上下文获取错误 → 它按错误种类选择不同提示,部分情况会补上一句“以后还会继续试” → 出来是一段可以放进提示或日志里的说明。
调用关系:它和 user_facing_hint 类似,但更偏向“这次请求没拿到上下文”的场景。遇到可恢复问题时,它会调用 hint_with_retry 拼上继续重试的说明。
调用图:调用 1 个内部函数(hint_with_retry)。
hint_with_retry131–133 ↗
fn hint_with_retry(message: &str) -> String
作用:给一段错误说明后面统一加上“Codex 以后还会继续尝试”的尾巴。这样多个错误提示能保持同一种口吻。
数据流:进去的是一段基础提示文字 → 它把固定的重试说明接到后面 → 出来是一段完整提示文字。
调用关系:它只服务于 IdeContextError::prompt_skip_hint,用来避免到处重复同一句重试提示。
调用图:被 1 处调用(prompt_skip_hint);外部调用 1 个(format!)。
fetch_ide_context151–153 ↗
fn fetch_ide_context(_workspace_root: &Path) -> Result<IdeContext, IdeContextError>
作用:这是外部调用这个文件最常用的入口:给它一个工作区目录,它就尝试向 IDE 插件要当前上下文。
数据流:进去的是项目根目录路径 → 它先算出默认 IPC 地址,再用固定 5 秒超时去请求 → 成功出来 IdeContext,失败出来 IdeContextError。
调用关系:它把上层 TUI 和底层通信细节隔开。上层只管调用它;它把工作交给 default_ipc_socket_path 和 fetch_ide_context_from_socket。
调用图:调用 2 个内部函数(default_ipc_socket_path, fetch_ide_context_from_socket)。
default_ipc_socket_path169–171 ↗
fn default_ipc_socket_path() -> PathBuf
作用:算出 Codex 默认应该连接哪里,才能找到 IDE 插件提供的本机通信入口。
数据流:进去没有业务参数,只读取系统环境,比如临时目录、当前用户 ID 或 Windows 管道名 → 拼出一个路径 → 返回这个 IPC 地址。
调用关系:fetch_ide_context 在发起请求前会先调用它。后续连接函数会拿这个地址去真正建立连接。
调用图:被 1 处调用(fetch_ide_context);外部调用 5 个(from, new, format!, getuid, temp_dir)。
fetch_ide_context_from_socket174–182 ↗
fn fetch_ide_context_from_socket(
socket_path: PathBuf,
workspace_root: &Path,
timeout: Duration,
) -> Result<IdeContext, IdeContextError>
作用:按指定的 IPC 地址和超时时间完整跑一遍“连接 IDE、发送请求、读取回复”的流程。测试也可以用它传入临时 socket。
数据流:进去的是 socket 或管道路径、工作区目录和超时时长 → 它算出截止时间,先连接,再在同一个时间预算内请求上下文 → 出来是 IdeContext 或错误。
调用关系:它被 fetch_ide_context 调用,也被测试直接调用。它负责把连接阶段交给 connect_stream,把请求阶段交给 fetch_ide_context_from_stream。
调用图:调用 2 个内部函数(connect_stream, fetch_ide_context_from_stream);被 2 处调用(fetch_ide_context, fetch_ide_context_uses_unregistered_request_route);外部调用 1 个(now)。
UnixDeadlineStream::connect200–204 ↗
fn connect(socket_path: PathBuf, deadline: Instant) -> std::io::Result<Self>
作用:在 Unix 系统上建立带截止时间的本地 socket 连接,并确认对方不是可疑进程。
数据流:进去的是 socket 路径和截止时间 → 它先在期限前连上 Unix socket,再检查对端所有者,最后包装成 UnixDeadlineStream → 出来是可读写的安全连接。
调用关系:Unix 版本的 connect_stream 会调用它。它又依赖 connect_unix_stream_before_deadline 做实际连接,依赖 validate_unix_peer_owner 做安全检查。
调用图:调用 2 个内部函数(connect_unix_stream_before_deadline, validate_unix_peer_owner);被 1 处调用(connect_stream);外部调用 1 个(new)。
UnixDeadlineStream::new206–208 ↗
fn new(stream: std::os::unix::net::UnixStream, deadline: Instant) -> Self
作用:把一个普通 UnixStream 包成带截止时间的 UnixDeadlineStream。这样后续读写都会知道不能无限等。
数据流:进去的是已经存在的 UnixStream 和一个截止时间 → 它保存这两样东西 → 出来是一个新的 UnixDeadlineStream。
调用关系:UnixDeadlineStream::connect 成功后会用它包装连接;测试也直接用它造一个流,验证超时读操作是否按预期停止。
调用图:被 1 处调用(unix_deadline_stream_uses_remaining_deadline_for_blocking_reads)。
UnixDeadlineStream::set_deadline210–212 ↗
fn set_deadline(&mut self, deadline: Instant)
作用:更新这条连接的截止时间,让后续读写按新的时间点停止等待。
数据流:进去的是新的截止时间 → 它替换对象里保存的 deadline → 没有返回数据,只改变这个流对象的内部状态。
调用关系:read_response_frame 在每次等回复前会设置同一个总截止时间,确保读取某一帧时不会偷偷超过整体预算。
UnixDeadlineStream::wait_for_ready214–218 ↗
fn wait_for_ready(&self, events: libc::c_short) -> std::io::Result<()>
作用:在真正读或写之前,先等底层 socket 变成“可以读”或“可以写”,并且不会超过截止时间。
数据流:进去的是想等待的事件,比如可读或可写 → 它取出 socket 的文件描述符,调用 wait_for_fd_ready 等待 → 成功表示可以继续读写,失败返回超时或系统错误。
调用关系:UnixDeadlineStream::read、write、flush 都先调用它。它把具体等待工作交给 wait_for_fd_ready。
调用图:调用 1 个内部函数(wait_for_fd_ready);被 3 处调用(flush, read, write);外部调用 1 个(as_raw_fd)。
connect_unix_stream_before_deadline222–262 ↗
fn connect_unix_stream_before_deadline(
socket_path: &Path,
deadline: Instant,
) -> std::io::Result<std::os::unix::net::UnixStream>
作用:用比较底层的系统调用连接 Unix socket,同时严格遵守截止时间。
数据流:进去的是 socket 路径和截止时间 → 它检查路径安全,创建 socket,设置关闭进程时自动关闭、非阻塞,然后发起连接并等待可写 → 出来是已连接的 UnixStream。
调用关系:UnixDeadlineStream::connect 调用它完成真正连接。它会串起 validate_unix_socket_path、unix_socket_addr、set_fd_close_on_exec、set_fd_nonblocking、wait_for_fd_ready、socket_error 等底层辅助函数。
调用图:调用 7 个内部函数(is_in_progress_connect_error, set_fd_close_on_exec, set_fd_nonblocking, socket_error, unix_socket_addr, validate_unix_socket_path, wait_for_fd_ready);被 1 处调用(connect);外部调用 6 个(from_raw_fd, from_raw_os_error, last_os_error, connect, socket, from_raw_fd)。
unix_socket_addr265–314 ↗
fn unix_socket_addr(socket_path: &Path) -> std::io::Result<(libc::sockaddr_un, libc::socklen_t)>
作用:把普通文件路径转换成操作系统能识别的 Unix socket 地址结构。
数据流:进去的是一个路径 → 它检查路径里不能有空字节、长度不能超过系统限制,然后填入 sockaddr_un 结构 → 出来是地址结构和长度。
调用关系:connect_unix_stream_before_deadline 在调用系统 connect 之前需要它。可以把它理解成把门牌号翻译成系统底层能投递的地址格式。
调用图:被 1 处调用(connect_unix_stream_before_deadline);外部调用 4 个(as_os_str, new, try_from, try_from)。
set_fd_close_on_exec317–328 ↗
fn set_fd_close_on_exec(fd: libc::c_int) -> std::io::Result<()>
作用:给 socket 文件描述符加上“启动别的程序时不要继承我”的标记,避免连接被子进程意外带走。
数据流:进去的是文件描述符数字 → 它读取当前标志位,再加上 FD_CLOEXEC → 成功返回空结果,失败返回系统错误。
调用关系:connect_unix_stream_before_deadline 创建 socket 后会马上调用它,是连接安全和资源清理的一步。
调用图:被 1 处调用(connect_unix_stream_before_deadline);外部调用 2 个(last_os_error, fcntl)。
set_fd_nonblocking331–342 ↗
fn set_fd_nonblocking(fd: libc::c_int) -> std::io::Result<()>
作用:把 socket 设成非阻塞模式,意思是操作系统不会让一次 connect 或 read 永远卡住线程。
数据流:进去的是文件描述符 → 它读取现有状态,加上 O_NONBLOCK 标志 → 成功后这个 socket 的后续操作会立即返回或配合 poll 等待。
调用关系:connect_unix_stream_before_deadline 用它来配合自己的截止时间逻辑。否则底层 connect 可能不受这里的 5 秒预算控制。
调用图:被 1 处调用(connect_unix_stream_before_deadline);外部调用 2 个(last_os_error, fcntl)。
is_in_progress_connect_error345–354 ↗
fn is_in_progress_connect_error(error: &std::io::Error) -> bool
作用:判断一次非阻塞连接返回的错误是不是“连接还在进行中”,而不是真失败。
数据流:进去的是系统 IO 错误 → 它看底层错误码是否属于 EINPROGRESS、EALREADY、EWOULDBLOCK、EINTR 等可等待状态 → 出来是真或假。
调用关系:connect_unix_stream_before_deadline 在 connect 没有立刻成功时调用它。若只是进行中,就继续等;若不是,就直接报错。
调用图:被 1 处调用(connect_unix_stream_before_deadline);外部调用 1 个(matches!)。
socket_error357–380 ↗
fn socket_error(fd: libc::c_int) -> std::io::Result<libc::c_int>
作用:在非阻塞连接等完之后,询问操作系统这次 socket 连接最终有没有失败。
数据流:进去的是文件描述符 → 它通过 getsockopt 读取 SO_ERROR → 出来是 0 表示没错,非 0 表示具体系统错误码。
调用关系:connect_unix_stream_before_deadline 在 poll 显示 socket 可写后调用它确认结果。因为“可写”不一定等于“连接成功”。
调用图:被 1 处调用(connect_unix_stream_before_deadline);外部调用 3 个(last_os_error, getsockopt, try_from)。
remaining_timeout383–388 ↗
fn remaining_timeout(deadline: Instant) -> std::io::Result<Duration>
作用:计算离截止时间还剩多久,如果已经到点就返回超时错误。
数据流:进去的是截止时间 → 它拿当前时间相减,并确认剩余时间大于 0 → 出来是剩余 Duration,或一个超时 IO 错误。
调用关系:remaining_timeout_ms 会调用它,把人类可读的时间长度转成 poll 需要的毫秒数。
调用图:被 1 处调用(remaining_timeout_ms);外部调用 2 个(checked_duration_since, now)。
remaining_timeout_ms391–394 ↗
fn remaining_timeout_ms(deadline: Instant) -> std::io::Result<libc::c_int>
作用:把剩余等待时间换成毫秒整数,供底层 poll 系统调用使用。
数据流:进去的是截止时间 → 它先算剩余时长,再转成至少 1 毫秒、最多不超过系统整数上限的值 → 出来是毫秒数。
调用关系:wait_for_fd_ready 每轮等待前都会调用它,保证等待时间一直贴着同一个总截止时间走。
调用图:调用 1 个内部函数(remaining_timeout);被 1 处调用(wait_for_fd_ready);外部调用 1 个(try_from)。
wait_for_fd_ready397–431 ↗
fn wait_for_fd_ready(
fd: libc::c_int,
events: libc::c_short,
deadline: Instant,
) -> std::io::Result<()>
作用:等待某个文件描述符变得可读或可写,同时处理超时、中断和无效 socket。
数据流:进去的是文件描述符、要等的事件、截止时间 → 它循环调用 poll,如果被信号打断就重试,超时或无效就报错 → 成功时表示目标事件已经发生或连接状态已变化。
调用关系:UnixDeadlineStream::wait_for_ready 和连接阶段都会用它。它是 Unix 读写和连接超时控制的核心小齿轮。
调用图:调用 2 个内部函数(deadline_timeout_io_error, remaining_timeout_ms);被 2 处调用(wait_for_ready, connect_unix_stream_before_deadline);外部调用 3 个(last_os_error, new, poll)。
UnixDeadlineStream::read435–448 ↗
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>
作用:从 Unix socket 读取数据,但不会超过对象里保存的截止时间。
数据流:进去的是要填充的字节缓冲区 → 它先等 socket 可读,再尝试读取;遇到暂时不可读或被中断就继续等 → 出来是读到的字节数或错误。
调用关系:read_frame 最终会通过标准 Read 接口触发它。它依赖 wait_for_ready 避免阻塞读卡死。
调用图:调用 1 个内部函数(wait_for_ready);外部调用 1 个(read)。
UnixDeadlineStream::write453–466 ↗
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize>
作用:向 Unix socket 写数据,但在写之前会等到 socket 可写,并遵守截止时间。
数据流:进去的是要发送的字节片段 → 它等待可写后调用底层 write,遇到可重试错误就继续 → 出来是写出的字节数或错误。
调用关系:write_frame 通过标准 Write 接口会间接调用它。它和 read 一样依赖 wait_for_ready 控制等待。
调用图:调用 1 个内部函数(wait_for_ready);外部调用 1 个(write)。
UnixDeadlineStream::flush468–471 ↗
fn flush(&mut self) -> std::io::Result<()>
作用:把已经写入缓冲区的数据真正推送出去,并且同样先确认 socket 可写。
数据流:进去没有额外数据 → 它等待可写,然后调用底层 flush → 成功表示待发送数据已冲刷出去,失败返回 IO 错误。
调用关系:write_frame 写完长度和内容后会刷新流;在 Unix 上这会走到这里,保证请求及时发给 IDE 插件。
调用图:调用 1 个内部函数(wait_for_ready);外部调用 1 个(flush)。
validate_unix_socket_path475–507 ↗
fn validate_unix_socket_path(socket_path: &Path) -> std::io::Result<()>
作用:检查 Unix socket 文件和它所在目录是否安全,防止 Codex 连接到别人伪造的 socket。
数据流:进去的是 socket 路径 → 它检查父目录存在、属于当前用户、不能被其他用户写;再检查 socket 本身确实是 socket 且属于当前用户 → 成功返回空结果,失败返回权限错误。
调用关系:connect_unix_stream_before_deadline 在连接前会调用它。相关测试 validate_unix_socket_path_rejects_unsafe_parent_directory 会验证它能拒绝不安全目录。
调用图:调用 1 个内部函数(permission_denied_io_error);被 2 处调用(connect_unix_stream_before_deadline, validate_unix_socket_path_rejects_unsafe_parent_directory);外部调用 3 个(parent, getuid, symlink_metadata)。
validate_unix_peer_owner569–571 ↗
fn validate_unix_peer_owner(_stream: &std::os::unix::net::UnixStream) -> std::io::Result<()>
作用:连接建立后再确认对面的进程属于当前用户,避免只看文件路径还不够安全。
数据流:进去的是已连接的 UnixStream → 它按操作系统能力读取对端用户 ID,比如 Linux 用 SO_PEERCRED,BSD/macOS 用 getpeereid → 再确认用户 ID 匹配当前用户。
调用关系:UnixDeadlineStream::connect 在连上后调用它。它会把最终判断交给 ensure_peer_uid_matches_current_user。
调用图:调用 1 个内部函数(ensure_peer_uid_matches_current_user);被 1 处调用(connect);外部调用 4 个(last_os_error, getpeereid, getsockopt, as_raw_fd)。
ensure_peer_uid_matches_current_user574–582 ↗
fn ensure_peer_uid_matches_current_user(peer_uid: libc::uid_t) -> std::io::Result<()>
作用:确认对端用户 ID 和当前运行 Codex 的用户 ID 一样。
数据流:进去的是对端用户 ID → 它读取当前用户 ID 比较 → 一样就成功,不一样就返回权限拒绝错误。
调用关系:validate_unix_peer_owner 读取到对端身份后调用它。它用 permission_denied_io_error 生成统一的权限错误。
调用图:调用 1 个内部函数(permission_denied_io_error);被 1 处调用(validate_unix_peer_owner);外部调用 1 个(getuid)。
connect_stream585–591 ↗
fn connect_stream(
socket_path: PathBuf,
deadline: Instant,
) -> Result<IdeContextStream, IdeContextError>
作用:根据平台建立到 IDE 插件的通信流:Unix 用 Unix socket,Windows 用命名管道。
数据流:进去的是 IPC 地址和截止时间 → 它调用对应平台的连接实现,并把连接错误包装成 IdeContextError::Connect → 出来是统一的 IdeContextStream。
调用关系:fetch_ide_context_from_socket 不关心平台差异,只调用它。它是跨平台分岔点。
调用图:调用 2 个内部函数(connect, connect);被 1 处调用(fetch_ide_context_from_socket)。
answer_unsupported_request594–608 ↗
fn answer_unsupported_request(
stream: &mut T,
message: &Value,
) -> Result<(), IdeContextError>
作用:当 IDE 通信通道里反过来收到别人发来的请求时,礼貌地回复“我处理不了”。
数据流:进去的是通信流和收到的 JSON 消息 → 如果消息里有 requestId,就写回一个 error 响应,错误名是 no-handler-for-request → 出来是成功或发送错误。
调用关系:read_response_frame 在等待自己回复时,如果碰到 type 为 request 的消息,就调用它。这能避免对方一直等 TUI 处理一个它不支持的请求。
调用图:调用 1 个内部函数(write_frame);被 1 处调用(read_response_frame);外部调用 2 个(get, json!)。
fetch_ide_context_from_stream611–621 ↗
fn fetch_ide_context_from_stream(
stream: &mut IdeContextStream,
workspace_root: &Path,
deadline: Instant,
) -> Result<IdeContext, IdeContextError>
作用:在已经连好的通信流上完成一次 IDE 上下文请求。
数据流:进去的是通信流、工作区目录和截止时间 → 它生成唯一请求 ID,写出 ide-context 请求,读取匹配这个 ID 的响应,再解析成 IdeContext → 出来是上下文或错误。
调用关系:fetch_ide_context_from_socket 连接成功后把后半段工作交给它。它串起 write_ide_context_request、read_response_frame 和 extract_ide_context。
调用图:调用 3 个内部函数(extract_ide_context, read_response_frame, write_ide_context_request);被 1 处调用(fetch_ide_context_from_socket);外部调用 1 个(new_v4)。
write_ide_context_request624–640 ↗
fn write_ide_context_request(
stream: &mut T,
request_id: &str,
workspace_root: &Path,
) -> std::io::Result<()>
作用:组装并发送一条“请给我 IDE 上下文”的 JSON 请求。
数据流:进去的是通信流、请求 ID、工作区目录 → 它创建包含 type、requestId、sourceClientId、method、workspaceRoot 的 JSON → 交给 write_frame 按协议写出去。
调用关系:fetch_ide_context_from_stream 在读回复前先调用它。它只负责请求内容,实际加长度头和写字节由 write_frame 完成。
调用图:调用 1 个内部函数(write_frame);被 1 处调用(fetch_ide_context_from_stream);外部调用 1 个(json!)。
write_frame643–659 ↗
fn write_frame(stream: &mut T, message: &Value) -> std::io::Result<()>
作用:按这套 IPC 协议发送一帧消息:先发 4 字节长度,再发 JSON 内容。
数据流:进去的是可写流和 JSON 值 → 它把 JSON 序列化成字节,检查长度能放进 u32,写入小端长度、写入内容并刷新 → 出来是写入成功或 IO 错误。
调用关系:发送 IDE 请求、回复不支持请求、回复客户端发现请求、测试写假响应时都会用它。它是所有出站消息的统一出口。
调用图:被 4 处调用(answer_unsupported_request, read_response_frame, write_ide_context_response, write_ide_context_request);外部调用 4 个(flush, write_all, to_vec, try_from)。
read_frame662–677 ↗
fn read_frame(
stream: &mut T,
deadline: Instant,
) -> Result<Value, IdeContextError>
作用:从通信流里读出一整帧 JSON 消息。
数据流:进去的是可读流和截止时间 → 它先读 4 字节长度,拒绝超过最大限制的帧,再按长度读完整内容并解析 JSON → 出来是 JSON 值或协议错误。
调用关系:read_response_frame 用它一帧一帧地读消息。它依赖 read_exact_before_deadline 保证读长度和读内容都不会拖过截止时间。
调用图:调用 1 个内部函数(read_exact_before_deadline);被 1 处调用(read_response_frame);外部调用 3 个(from_slice, from_le_bytes, vec!)。
read_exact_before_deadline680–706 ↗
fn read_exact_before_deadline(
stream: &mut T,
buf: &mut [u8],
deadline: Instant,
) -> Result<(), IdeContextError>
作用:在截止时间前尽量把指定缓冲区读满,弥补标准 read_exact 不方便检查总时限的问题。
数据流:进去的是可读流、目标缓冲区和截止时间 → 它循环读取,读一点就累计一点,每轮先检查是否超时;遇到文件结束或 IO 错误就报错 → 成功时缓冲区被填满。
调用关系:read_frame 用它读取帧头和帧内容。它会调用 ensure_deadline_not_expired 做统一超时判断。
调用图:调用 1 个内部函数(ensure_deadline_not_expired);被 1 处调用(read_frame);外部调用 3 个(read, new, Read)。
read_response_frame709–754 ↗
fn read_response_frame(
stream: &mut IdeContextStream,
request_id: &str,
deadline: Instant,
) -> Result<Value, IdeContextError>
作用:不断读取通道里的消息,直到找到属于当前请求 ID 的 response。
数据流:进去的是通信流、目标请求 ID、截止时间 → 它循环读帧,忽略广播和别人的响应;遇到客户端发现请求就回复自己不能处理;遇到普通请求就回“不支持” → 出来是匹配请求 ID 的响应 JSON,或格式/发送/超时错误。
调用关系:fetch_ide_context_from_stream 写出请求后调用它。它在等待目标回复时还会调用 write_frame 和 answer_unsupported_request 处理路上的杂讯消息。
调用图:调用 4 个内部函数(answer_unsupported_request, ensure_deadline_not_expired, read_frame, write_frame);被 1 处调用(fetch_ide_context_from_stream);外部调用 4 个(set_deadline, format!, json!, InvalidResponse)。
ensure_deadline_not_expired757–763 ↗
fn ensure_deadline_not_expired(deadline: Instant) -> Result<(), IdeContextError>
作用:检查当前时间是否已经超过本次 IDE 请求的截止时间。
数据流:进去的是截止时间 → 它和当前时间比较 → 没到点就成功,到了或超过就返回超时错误。
调用关系:read_exact_before_deadline 和 read_response_frame 在循环里反复调用它,确保整个等待过程不会无限拖延。
调用图:调用 1 个内部函数(timeout_error);被 2 处调用(read_exact_before_deadline, read_response_frame);外部调用 1 个(now)。
timeout_error766–768 ↗
fn timeout_error() -> IdeContextError
作用:生成一个表示“等 IDE 上下文超时”的 IdeContextError。
数据流:进去没有参数 → 它先生成一个标准的超时 IO 错误,再包装成 IdeContextError::Read → 出来是统一的超时错误对象。
调用关系:ensure_deadline_not_expired 发现到点时调用它。这样上层看到的是和读取失败同一类的错误。
调用图:调用 1 个内部函数(deadline_timeout_io_error);被 1 处调用(ensure_deadline_not_expired);外部调用 1 个(Read)。
deadline_timeout_io_error771–776 ↗
fn deadline_timeout_io_error() -> std::io::Error
作用:创建底层 IO 超时错误,错误文本固定为等待 IDE 上下文超时。
数据流:进去没有参数 → 它构造 ErrorKind::TimedOut 的 IO 错误 → 出来是 std::io::Error。
调用关系:timeout_error 和 wait_for_fd_ready 都用它。一个用于通用读取超时,一个用于 Unix poll 等待超时。
调用图:被 2 处调用(timeout_error, wait_for_fd_ready);外部调用 1 个(new)。
permission_denied_io_error779–781 ↗
fn permission_denied_io_error(message: &'static str) -> std::io::Error
作用:创建统一的“权限被拒绝”IO 错误,主要用于 Unix 安全检查失败。
数据流:进去的是静态错误消息 → 它包装成 PermissionDenied 类型的 std::io::Error → 出来是 IO 错误。
调用关系:validate_unix_socket_path 和 ensure_peer_uid_matches_current_user 用它表达安全检查没通过。
调用图:被 2 处调用(ensure_peer_uid_matches_current_user, validate_unix_socket_path);外部调用 1 个(new)。
extract_ide_context784–797 ↗
fn extract_ide_context(response: Value) -> Result<IdeContext, IdeContextError>
作用:从 IDE 返回的响应 JSON 里拿出真正的 IdeContext 数据,并转成内部结构。
数据流:进去的是完整响应 JSON → 它先确认响应是 success,再取 result.ideContext 字段,最后用 JSON 反序列化成 IdeContext → 出来是可供 TUI 使用的上下文对象。
调用关系:fetch_ide_context_from_stream 拿到响应帧后调用它。它把“协议层的 JSON”转换成“业务层的 IDE 上下文”。
调用图:调用 1 个内部函数(ensure_success_response);被 1 处调用(fetch_ide_context_from_stream);外部调用 2 个(get, from_value)。
ensure_success_response800–814 ↗
fn ensure_success_response(response: &Value) -> Result<(), IdeContextError>
作用:确认 IDE 响应表示成功;如果 IDE 明确返回错误,就把错误名保留下来。
数据流:进去的是响应 JSON 引用 → 它查看 resultType 字段:success 就通过,error 就取 error 字段生成 RequestFailed,其他情况算无效响应 → 出来是成功或 IdeContextError。
调用关系:extract_ide_context 在提取 ideContext 前先调用它,避免把失败响应误当正常数据解析。
调用图:被 1 处调用(extract_ide_context);外部调用 3 个(get, InvalidResponse, RequestFailed)。
tests::test_deadline823–825 ↗
tests::write_ide_context_response828–862 ↗
fn write_ide_context_response(
stream: &mut impl std::io::Write,
request_id: &str,
active_selection_content: &str,
)
作用:在测试里伪造一条成功的 IDE 上下文响应,模拟真实 IDE 插件返回数据。
数据流:进去的是可写流、请求 ID、选中文本内容 → 它组装包含 activeFile 和 activeSelectionContent 的 JSON 响应,并用 write_frame 写出 → 写失败就让测试直接失败。
调用关系:tests::fetch_ide_context_uses_unregistered_request_route 的假服务端会调用它,给被测客户端返回最终的 ide-context 响应。
调用图:调用 1 个内部函数(write_frame);外部调用 2 个(json!, panic!)。
tests::unix_deadline_stream_uses_remaining_deadline_for_blocking_reads866–880 ↗
fn unix_deadline_stream_uses_remaining_deadline_for_blocking_reads()
作用:验证 UnixDeadlineStream 读不到数据时会按截止时间超时,而不是一直卡住。
数据流:进去没有外部输入 → 它创建一对本地 UnixStream,只拿客户端读但服务端不写,设置 50 毫秒截止时间 → 结果应当得到 TimedOut 错误,并且耗时不会很久。
调用关系:这个测试直接调用 UnixDeadlineStream::new,并通过标准 read 触发 UnixDeadlineStream::read,保护超时机制不被改坏。
调用图:调用 1 个内部函数(new);外部调用 6 个(from_millis, now, assert!, assert_eq!, read, pair)。
tests::validate_unix_socket_path_rejects_unsafe_parent_directory884–898 ↗
fn validate_unix_socket_path_rejects_unsafe_parent_directory()
作用:验证 Unix socket 放在其他用户也可写的目录里时会被拒绝。
数据流:进去没有外部输入 → 它创建临时目录,把权限改成 777,绑定一个 socket,再调用 validate_unix_socket_path → 期望返回 PermissionDenied。
调用关系:这个测试专门覆盖 validate_unix_socket_path 的安全检查,防止以后误放宽目录权限要求。
调用图:调用 2 个内部函数(validate_unix_socket_path, bind);外部调用 4 个(assert_eq!, from_mode, set_permissions, tempdir)。
tests::fetch_ide_context_uses_unregistered_request_route902–1008 ↗
fn fetch_ide_context_uses_unregistered_request_route()
作用:用一个假的 Unix socket 服务端,验证完整 IDE 上下文请求流程能处理杂讯消息,并最终拿到正确上下文。
数据流:进去没有外部输入 → 它启动假服务端,检查客户端发出的 ide-context 请求内容;服务端先发一个未知请求、一个发现请求、一个大广播,再发真正成功响应 → 客户端应忽略或正确回复这些中间消息,最后得到选中文本为 use 的上下文。
调用关系:这是本文件最接近端到端的测试。它调用 fetch_ide_context_from_socket,服务端侧还使用 read_frame、write_frame 和 write_ide_context_response 来模拟真实 IPC 对话。
调用图:调用 2 个内部函数(fetch_ide_context_from_socket, bind);外部调用 5 个(from_secs, new, assert_eq!, tempdir, spawn)。