这次复盘记录的是我们“Codex-Windows沙箱”项目的一次典型网络故障,从出现症状到定位根因花了六个多小时。如果你也在用Windows Sandbox跑自动化服务,那这个真实案例的排查脉络和坑点应该能帮你省下不少时间。
故障本身并不复杂,但极具迷惑性:Windows沙箱能正常启动,网络也通,可沙箱里的Codex服务就是起不来,一直卡在“连接内部模型网关”那一步。我们反复重启、改配置、看日志,最后才找到真正的问题出在宿主机安全软件更新后对Hyper-V虚拟交换机的“过度保护”上。
这个案例的每一步排查思路和每一行命令,我都整理在下面了。
1. 故障背景与环境搭建
1.1 为什么用Windows沙箱跑Codex
先说背景。我们在维护一个代号为“Codex”的代码生成服务,它需要在一个隔离的Windows环境里执行一次性任务。这个服务会读取临时脚本、调用内部模型网关、编译并运行用户提交的代码片段,安全要求比较高,不允许直接跑在宿主机上。
当时可选方案有三个:
- 传统虚拟机(Hyper-V):隔离性最好,但启动要几十秒甚至几分钟,每次还要维护快照、处理系统更新,运维成本高。
- Docker容器:启动快、资源省,但我们的Windows容器镜像里缺一些底层API,而且容器共享宿主内核,对“代码生成后还要调用Windows系统接口”这种场景不够干净。
- Windows Sandbox:微软自带的一次性轻量虚拟机,秒级启动,无状态,关掉即销毁,天然适合“跑一次、扔一次”的临时任务。
我们最终选了Windows Sandbox。主要看重它三点:启动快、全隔离、没有License成本。Windows Sandbox基于Hyper-V底层能力,但比传统VM更轻,因为它使用一组动态差异磁盘,每次启动都是全新系统。
下面是我们当时的环境配置:
| 项目 | 配置 |
|---|---|
| 宿主机 | Windows 11 Pro 22H2,8核16线程,32GB内存,SSD剩余150GB |
| 沙箱规格 | CPU 2核,内存4GB,启用网络,映射只读目录 |
| Codex服务 | 沙箱内的.NET服务,监听5000端口,启动时从模型网关拉取模型清单 |
| 模型网关地址 | model-gw.local,通过内部DNS解析 |
| 调度器 | 宿主机上的Windows计划任务,调用沙箱执行任务 |
| 共享目录 | 宿主机D:\codex_share映射到沙箱C:\share |
沙箱的启动配置由一个.wsb文件控制,核心内容是这样的:
<Configuration> <MappedFolders> <MappedFolder> <HostFolder>D:\codex_share</HostFolder> <SandboxFolder>C:\share</SandboxFolder> </MappedFolder> </MappedFolders> <LogonCommand> <CommandFile>C:\share\bootstrap.cmd</CommandFile> </LogonCommand> <MemoryInMB>4096</MemoryInMB> <VirtualProcessorCount>2</VirtualProcessorCount> <Networking>true</Networking> </Configuration>bootstrap.cmd会调用一个PowerShell脚本,等待网络就绪,然后启动Codex服务。
这个架构看起来没什么问题,但我们忽略了一个关键细节:沙箱内的DNS解析完全依赖宿主机的转发机制。一旦宿主机侧对沙箱的DNS请求做手脚,整个服务就瘫了。这次故障恰恰就出在这。
1.2 Codex服务为什么必须访问模型网关
Codex服务并不是一个纯本地程序。它启动时要做三件事:
- 加载本地模型配置。
- 连接内部模型网关
model-gw.local,获取最新的模型版本、许可证校验、算法参数。 - 服务就绪后,监听TCP 5000端口,等待宿主机调度器下发任务。
如果第2步失败,服务不会立即退出,而是会重试几次,每次间隔5秒,最终抛出一个“上游连接失败”的异常。这个重试设计本意是好的,结果在故障发生时反而掩盖了根因,让我们一度以为是服务代码或网络延迟的问题。
现在想想,如果Codex服务在启动时能把“依赖DNS解析”变成“依赖IP直连”,这次故障可能压根不会影响业务。但当时没有这种意识,后面会详细讲。
2. 故障现象与第一现场
2.1 故障具体表现
10月8日早上9点15分左右,调度器反馈连续三次任务都失败了,错误信息都是“Codex握手超时”。我马上手动启动了一个沙箱,进去以后发现Codex服务进程还活着,但5000端口没有监听。
查看服务日志,发现它卡在同一个地方:
[09:21:03] INFO Connecting to model gateway: https://model-gw.local/api/v1/models [09:21:08] WARN Connection attempt 1 timed out [09:21:13] WARN Connection attempt 2 timed out [09:21:18] WARN Connection attempt 3 timed out [09:21:23] ERROR Failed to initialize model gateway, shutting down.这个日志看起来像是网关不可达,但我马上在沙箱里做了一个简单测试:
ping model-gw.local结果直接显示找不到主机。然后我又用IP地址ping了一下模型网关的内网IP:
ping 10.3.0.45结果立刻通了。也就是说,沙箱的网络链路是好的,TCP/IP能走通,问题纯粹出在DNS解析上。
为了验证,我执行了:
nslookup model-gw.local等待了大约10秒,返回的是:
DNS request timed out. timeout was 2 seconds.当时心里基本有数了:这不是Codex的问题,也不是网络断了,而是沙箱里的域名解析不可用。
2.2 初步判断与影响范围
一开始我们猜测是不是沙箱的网络配置漂移了,于是尝试重启沙箱,而且重启了两次。
第二次重启时,我特意在沙箱桌面加载完成后,立刻执行nslookup model-gw.local。有趣的现象来了:第一次解析成功了,但紧接着再执行一次,又超时了。
这个细节很关键。它说明沙箱从宿主机继承的初始DNS配置是生效的,但生效状态只能维持几秒钟,之后就莫名失效。我当时怀疑是宿主机上某个服务在动态修改网络策略,于是把排查重点转向了宿主机的网络转发环节。
影响范围方面,因为Codex服务启动失败,当天上午所有依赖代码生成的任务都无法执行。调度器上没有做“自动重试N次”的策略,只有一次性触发,所以任务队列全部阻塞。最直接的损失是项目进度延误,但也让我们有机会彻底检查一遍这套沙箱架构的稳定性。
3. 排查过程实录
3.1 第一轮:瞎忙活与表面检查
我们最开始做的是一些常规检查,现在回想起来基本属于“瞎忙活”。
先看了Hyper-V相关服务是否正常:
Get-Service vmcompute, hvhost, vmms三者的状态都是Running。再看Windows Sandbox的进程:
Get-Process WindowsSandbox -ErrorAction SilentlyContinue进程也在。接着去事件查看器翻日志:
Microsoft-Windows-Hyper-V-VmSwitch-OperationalMicrosoft-Windows-WindowsSandbox/Operational
日志里只有一些常规信息,没有任何红色错误。于是我们想通过“重新安装Windows Sandbox功能”来修复,但这个操作必须重启系统,而且影响面大,没有立刻执行。
这半小时属于典型的“症状驱动排查”——看到什么查什么,没有抓住“IP能通但DNS不通”这个明确的矛盾点。真正高效的排查应该是先区分“服务没起来”还是“服务起来但依赖问题”,然后逐层下沉到网络协议栈。
3.2 第二轮:从网络层开刀
第二轮我开始认真看沙箱内的网络配置。在沙箱里执行:
ipconfig /all关键信息如下:
- 沙箱IP:
172.25.192.5 - 网关:
172.25.192.1 - DNS服务器:
172.25.192.1
这个172.25.192.1是宿主机的“vEthernet (Default Switch)”接口。Windows Sandbox默认使用Hyper-V的Default Switch,所以沙箱的网关和DNS都是同一个地址。
在沙箱里ping网关:
ping 172.25.192.1通。这说明沙箱到宿主机NAT网关之间没有问题。但为什么DNS请求没响应?
用Resolve-DnsName再测试一次:
Resolve-DnsName -Name "model-gw.local" -Type A结果没有任何返回,最后抛出了超时错误。
这里也踩了一个小坑:很多人会用Test-NetConnection model-gw.local -Port 53来测DNS,但 TCP/53 根本不是默认DNS服务。DNS查询走的是UDP/53,而Test-NetConnection默认只支持TCP,所以它给出的结果没有参考价值。正确测法是直接Resolve-DnsName,或者用nslookup。
3.3 第三轮:抓包定位DNS
既然沙箱内的测试已经表明“有IP、有网关、但DNS无响应”,那问题要么出在沙箱的DNS客户端,要么出在宿主机侧的转发环节。
为了区分这两者,我在沙箱内用系统自带的抓包工具开了一个追踪会话:
netsh trace start capture=yes scenario=dns然后执行nslookup model-gw.local触发一次DNS查询。抓包完成后,把生成的ETL文件拷出来分析。
结果很明确:沙箱已经发出了DNS查询请求,源地址是172.25.192.5:54321,目的地址是172.25.192.1:53,但这个请求发出去之后,再也没有任何回应包。DNS请求反复重传了几次,最终超时。
这说明沙箱的DNS客户端没有问题,问题出在宿主机“收到这个请求并转发到真实DNS服务器”这个环节。
接着我在宿主机上也开启一个网络监听,过滤UDP 53端口的流量,结果发现来自172.25.192.5的DNS请求根本没有出现在宿主机的物理网卡上。也就是说,请求在Default Switch内部就被丢弃了。
到这一步,我基本确定是宿主机防火墙或第三方安全软件拦截了虚拟交换机上的DNS转发流量。
3.4 转折点:防火墙规则与系统补丁
为了验证这个判断,我尝试暂时退出宿主机上的第三方安全软件,然后再回到沙箱里执行一次:
Resolve-DnsName model-gw.local结果立刻成功。然后我重新启动安全软件,再测一次,又超时了。反复验证了两次,结论完全一致。
打开安全软件的管理界面,发现它把“vEthernet (Default Switch)”这个网络接口标记成了“不信任网络”,并且自动启用了一条“DNS过滤规则”。这条规则的生效时间正好是10月8日凌晨,也就是安全软件自动更新完之后。
为什么普通Hyper-V虚拟机不受影响,而Windows Sandbox会被拦?因为Sandbox使用的Default Switch是系统自动创建的特殊虚拟交换机,安全软件对它进行了单独的网络分类,一旦它被归入“不信任网络”,默认就会丢弃所有DNS查询,以此作为一种防护策略。
这个行为设计上是为了防止恶意虚拟网络间解析,防止DNS劫持,但在我们的内网环境里反而成了误伤。
4. 根因分析与修复步骤
4.1 根因最终解释
一句话总结:沙箱的DNS请求通过Default Switch转发到宿主机时,宿主机上的安全软件更新后把Default Switch网卡划为“不信任网络”,并启用了DNS过滤策略,导致沙箱发出的UDP 53请求被静默丢弃。
为什么是“静默丢弃”,而不是“拒绝”?
安全软件对“不信任网络”的默认策略通常是DROP而不是REJECT。DROP会让请求方一直等待到超时,表现成“间歇性网络故障”。如果当时是REJECT,沙箱会立刻收到“网络不可达”的ICMP消息,反而更容易定位。
另一个诱因是Codex服务的设计:模型网关地址使用域名model-gw.local,并且没有做IP直连或hosts兜底。一旦DNS失效,服务就必死无疑。
如果当初服务配置里直接用https://10.3.0.45/api/v1/models,那么这次故障根本不会影响业务。这也给我们敲了个警钟:高可用不是只靠重试就能实现的,关键依赖要尽量减少单点。
4.2 临时快速恢复方案
确认根因之后,我们立刻采用一个无侵入的临时方案:在沙箱启动脚本里,强制将DNS配置为内部DNS服务器,绕开宿主机的Default Switch转发。
在沙箱内执行以下命令即可:
Get-NetAdapter -Name "Ethernet" | Set-DnsClientServerAddress -ServerAddresses ("10.3.0.100", "10.3.0.101")这里的10.3.0.100/101是我们内部网段的权威DNS服务器。沙箱访问这两个地址的路径是:
沙箱 → Default Switch NAT → 宿主机物理网卡 → 内部DNS服务器安全软件虽然拦截了 Default Switch 的DNS转发代理,但不影响沙箱经过NAT后直接访问业务段的DNS服务。所以这个方案能立刻生效。
但要注意,Windows Sandbox每次启动都是全新环境,必须把这一步写进启动脚本,不能只在当前打开的沙箱里手动执行。
修改后的bootstrap.cmd内容如下:
@echo off powershell.exe -ExecutionPolicy Bypass -File "C:\share\bootstrap.ps1"bootstrap.ps1开头改成这样:
$dnsServers = @("10.3.0.100", "10.3.0.101") $adapter = Get-NetAdapter -Name "Ethernet" Set-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex -ServerAddresses $dnsServers # 等待网络就绪 Start-Sleep -Seconds 2 # 验证DNS $resolved = Resolve-DnsName "model-gw.local" -ErrorAction SilentlyContinue if (-not $resolved) { Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "10.3.0.45 model-gw.local" } # 启动Codex服务 Start-Process -FilePath "C:\share\codex-service.exe" -ArgumentList "--config", "C:\share\config.json"这里用了一个if (-not $resolved)的逻辑兜底:如果DNS手动配置后仍然解析不了,就直接写hosts,保证Codex服务能启动。Write-Host这些调试输出可以根据需要加上。
4.3 永久加固:三层方案
临时方案虽然能恢复业务,但我并不想长期依赖它。所以后续做了三层永久加固:
第一层:调整安全软件策略。在安全软件中把“vEthernet (Default Switch)”这个虚拟接口重新标记为“信任网络”,并且放行DNS转发流量。这个操作需要与安全运维人员确认,不能自己乱改,因为涉及到整体安全基线。
第二层:沙箱启动脚本标准化。把“自定义DNS + hostst兜底 + 网络自检”做成标准模板,以后凡是跑Windows Sandbox的调度任务,都自动加入这个启动逻辑。这样即使宿主机的安全策略再发生变化,沙箱内部仍然具备自愈能力。
第三层:应用配置改造。把Codex服务的主机配置从域名改成IP直连,域名只保留在日志里做展示用途。以后即使DNS彻底挂了,服务也能启动。这一步是治本,但涉及代码改动和发布流程,所以安排在下个版本上线。
最终.wsb文件也做了小调整,比如把映射目录设为只读,避免沙箱内意外写入损坏宿主机文件:
<Configuration> <MappedFolders> <MappedFolder> <HostFolder>D:\codex_share</HostFolder> <SandboxFolder>C:\share</SandboxFolder> <ReadOnly>true</ReadOnly> </MappedFolder> </MappedFolders> <LogonCommand> <CommandFile>C:\share\bootstrap.cmd</CommandFile> </LogonCommand> <MemoryInMB>4096</MemoryInMB> <VirtualProcessorCount>2</VirtualProcessorCount> <Networking>true</Networking> </Configuration>4.4 验证与监控
修复后,我们连续跑了三个真实任务,全部成功。后面又专门做了一个DNS断网演练:在沙箱启动时手动把安全软件的DNS过滤打开,确认启动脚本能够自动切换到hosts兜底,Codex服务依然正常启动。
同时给调度器加了一个健康检查:沙箱启动30秒后,如果Codex服务的5000端口没有监听,就自动标记失败并轮询重启。这避免以后再次出现“沙箱进程在跑但服务起不来”时,任务队列静默卡住的情况。
监控项我整理成了表:
| 监控点 | 检查方式 | 告警条件 |
|---|---|---|
| DNS解析 | Resolve-DnsName | 连续3次超时 |
| 服务监听 | Test-NetConnection -Port 5000 | 15秒内未监听 |
| 沙箱进程 | Get-Process WindowsSandbox | 进程退出 |
| 安全软件规则 | 读取安全软件日志 | 防御规则变更时产生通知 |
5. 常见问题排查速查表
这次故障之后,我趁热整理了一份Windows Sandbox网络问题速查表,之后排查类似问题可以直接对照。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 沙箱内DNS解析超时,但IP可ping通 | 宿主机DNS转发被拦截 | 检查Default Switch网卡的防火墙/安全软件策略 |
| 沙箱无法访问外网,但宿主机正常 | 宿主机防火墙拦截虚拟交换机出站流量 | 临时关闭Windows防火墙测试,确认规则 |
| 沙箱启动后黑屏或卡登录 | Hyper-V服务异常或内存不足 | 查看Hyper-V事件日志,检查可用内存 |
| 共享文件夹不可见 | .wsb映射路径错误或UNC路径不支持 | 使用本地绝对路径,避免带空格 |
| 沙箱启动失败,报错0x80070005 | 权限不足或防病毒拦截 | 以管理员运行,检查安全软件隔离区 |
另外有两个容易被忽略的坑,单独说一下:
第一个坑:沙箱内DNS一开始正常,过一会儿才超时。这通常是宿主机安全策略或DNS转发进程在沙箱运行后才开始生效,不是沙箱配置问题。别浪费时间反复重启沙箱,直接看宿主机侧的规则变更。
第二个坑:Test-NetConnection测不了DNS。我见过有人用Test-NetConnection 10.0.0.1 -Port 53去测DNS,这是不对的。TCP/53不是标准DNS端口,测出来的结果没意义。测DNS就用:
Resolve-DnsName -Name "model-gw.local" -Type A -DnsOnly-DnsOnly参数可以忽略hosts文件,只走实打实的DNS协议,排查更干净。
6. 经验总结
这次故障真正的问题不在Codex,也不在Windows沙箱,而是“网络依赖链路里被忽略的一环”。我们一直默认沙箱的DNS是可用的,结果一个安全软件更新就能让整个系统停下来。复盘完之后,我最大的改变有两个。
第一个改变是:凡是关键服务之间的调用,尽量用IP地址或本地hosts兜底,不要依赖“动态DNS”这种外部变量。尤其在这种一次性的沙箱环境里,系统生成得快,网络策略却不受你控制,能用静态解析决不用动态解析。
第二个改变是:对于Windows Sandbox这类动态环境,启动脚本里必须先做“自检 + 兜底”。网络通了再启动服务,不通就自动走到hosts逻辑,让异常在沙箱内部消化掉,而不是反馈到上层调度器。
如果你也在维护类似的Windows沙箱任务,我建议你回头看看自己的启动脚本里有没有这三行:等待网络就绪、测试DNS、写入hosts兜底。没有这三行,哪怕今天不出事,明天也可能栽在某个安全软件更新上。