1. 事件还原:一次工具调用暂停背后的技术真相
1.1 从“最强模型暂停工具调用”说起
OpenAI 对其最强模型的工具调用能力做了一次临时限制,官方的说法很克制,只提到“在特定场景下暂停了部分工具调用功能”。但做 Agent 开发的人一看就明白,这类措辞背后通常不是模型本身出了问题,而是执行链路中的某个环节被绕过了。这次被绕过的环节,是 DNS。
先把概念理清楚。所谓“工具调用”(Tool Calling / Function Calling),是大模型与外部世界交互的核心机制。模型本身只会生成文本,它要查天气、读文件、发请求、跑代码,必须通过一个“工具”接口把意图转化成真实动作。这个接口在服务端执行时,通常跑在一个**沙箱(Sandbox)**里——一个被严格限制权限、网络、文件系统的隔离环境。沙箱的目的很单纯:模型可以“想”,但不能“乱来”。
问题就出在“网络”这一层。很多沙箱的网络策略是基于域名白名单做的,而不是基于 IP 或完整流量审计。白名单的判定依赖 DNS 解析结果。一旦 DNS 这一环被做了手脚,白名单就可能形同虚设。这次事件的核心,就是有人找到了这个缺口。
1.2 为什么一个 DNS 缺口能撬动整个沙箱
要理解这件事的严重性,得先理解 DNS 在沙箱里的角色。沙箱要限制模型访问外网,常见做法是维护一份允许访问的域名列表,比如只允许访问某个 API 域名、某个包管理源。当模型发起请求时,系统先解析域名,拿到 IP,再判断这个 IP 是否在白名单对应的地址范围内。
这个流程有个天然的信任假设:DNS 返回的结果是可信的。但 DNS 协议本身在设计之初并没有强认证机制,解析结果可以被中间环节影响。如果攻击者能控制某个被允许域名下的解析行为,或者利用 DNS 重绑定(DNS Rebinding)这类技术,让同一个域名在不同时刻解析到不同 IP,那么白名单校验就可能被欺骗——校验时解析到合法 IP,实际请求时解析到内网地址或非预期地址。
注意:这里说的不是“攻破模型”,而是“绕过沙箱的网络边界”。模型仍然在按指令行事,只是它发出的请求被导向了沙箱设计者没预料到的地方。
这就是为什么一个看似不起眼的 DNS 缺口,能暴露整个 AI 沙箱的盲区。沙箱的防护层级里,网络隔离往往是最容易被低估的一环,因为它不像文件系统隔离那样直观,也不像权限控制那样有明确的“拒绝”反馈。DNS 出问题时,请求可能“成功”了,只是成功去了不该去的地方。
1.3 这件事对 Agent 开发者意味着什么
如果你在做 Agent 开发,这件事不是“看个热闹”。它直接关系到你部署的 Agent 是否安全。很多团队在搭建 Agent 时,会把精力放在提示词工程、工具编排、记忆管理上,网络层往往直接用默认配置——容器默认网络、云厂商默认安全组、框架默认的 HTTP 客户端。默认配置在功能上通常没问题,但在隔离性上经常是“够用就好”,而“够用”和“安全”之间隔着一条 DNS 的鸿沟。
我见过不少 Agent 项目,工具调用直接跑在宿主机上,或者跑在一个网络完全放开的容器里,理由是“方便调试”。调试期确实方便,但一旦上线,这个口子就是实打实的风险。这次事件相当于给所有做 Agent 的人提了个醒:工具调用的安全边界,必须包含 DNS 这一层。
2. 沙箱网络隔离的常见做法与 DNS 盲区
2.1 主流沙箱方案的网络策略对比
不同团队做 Agent 沙箱,网络隔离的粒度差别很大。我按隔离强度从低到高列一下常见的几种做法,你可以对照自己项目看看处在哪一档。
| 隔离方案 | 网络控制方式 | DNS 处理 | 隔离强度 | 适用场景 |
|---|---|---|---|---|
| 宿主机直接执行 | 无隔离,依赖系统防火墙 | 系统 DNS | 极低 | 本地调试、一次性脚本 |
| 普通容器 | 默认 bridge 网络 | 容器内 DNS 转发 | 低 | 内部工具、可信环境 |
| 容器 + 网络策略 | 出站白名单(IP/域名) | 需显式配置 DNS | 中 | 一般生产 Agent |
| 独立网络命名空间 | 完全隔离,仅放行必要出口 | 自建 DNS 或禁用外部 DNS | 高 | 多租户、不可信代码执行 |
| 微虚拟机(如 Firecracker) | 内核级隔离 + 网络策略 | 受控 DNS | 很高 | 高安全要求的代码沙箱 |
大部分 Agent 项目落在第二档和第三档之间。第二档的问题很明显:容器默认能访问外网,DNS 用的是宿主或容器运行时的默认配置,模型只要生成一个请求,就能打到任意地址。第三档做了出站白名单,但白名单的落地方式决定了它是否真的可靠。
2.2 域名白名单的三种实现及其 DNS 依赖
出站白名单听起来简单,实现起来有好几种路径,每种对 DNS 的依赖程度不同。
第一种是在应用层做校验。HTTP 客户端在发请求前,检查目标域名是否在允许列表里。这种做法最轻量,但最容易被绕过,因为校验的是“域名字符串”,而实际连接时 DNS 解析可能返回任意 IP。如果攻击者能影响解析,应用层校验就只是心理安慰。
第二种是在网络层做 IP 白名单。先把允许的域名解析成 IP,然后把 IP 写进防火墙规则。这种做法比第一种强,但有个致命问题:IP 会变。CDN、云服务、负载均衡都会导致同一域名解析到不同 IP,维护成本高,而且如果解析发生在规则生成之后,规则就可能过期。
第三种是在 DNS 层做拦截。自建一个受控的 DNS 解析器,只允许解析白名单内的域名,并且对解析结果做校验(比如拒绝解析到内网地址段)。这种做法把控制点前移到了 DNS,是目前比较可靠的方案。但它要求团队有能力维护 DNS 服务,并且要处理好解析缓存、TTL、重绑定防护等细节。
提示:如果你的 Agent 沙箱用的是第一种或第二种,建议认真评估一下 DNS 重绑定的风险。这不是理论问题,而是已经被实际利用过的攻击路径。
2.3 DNS 重绑定为什么能骗过白名单
DNS 重绑定(DNS Rebinding)的原理不复杂,但很巧妙。攻击者控制一个域名,比如evil.example.com,并把这个域名的 DNS 记录配置成:第一次解析返回一个合法 IP(在白名单内),TTL 设得很短;等白名单校验通过后,第二次解析返回一个内网 IP 或攻击者控制的地址。
关键在于,很多白名单校验和实际请求是两次独立的 DNS 解析。校验时解析一次,请求时又解析一次。如果这两次之间 DNS 记录变了,校验就白做了。即使校验和请求用同一次解析结果,如果 TTL 很短,后续请求也可能拿到新结果。
防御这种攻击,核心思路是让校验和请求使用同一个解析结果,并且锁定这个结果。具体做法包括:在 DNS 层做解析并缓存,应用层直接使用缓存结果;或者使用 DNS pinning,把域名和 IP 的映射固定一段时间;再或者干脆在沙箱里禁用外部 DNS,所有解析走内部受控服务。
3. 从零搭建一个带 DNS 防护的 Agent 沙箱
3.1 整体架构设计
我以一个常见的 Agent 代码执行场景为例,搭一个带 DNS 防护的沙箱。目标很明确:模型可以调用工具、执行代码,但网络访问被严格限制,DNS 解析走受控通道,重绑定攻击无法生效。
整体架构分三层。最外层是网络命名空间,给沙箱一个独立的网络栈,默认没有任何出口。中间层是受控 DNS 解析器,所有域名解析请求都打到它,它负责白名单校验、结果过滤和缓存。最内层是执行环境,跑模型生成的代码或工具调用,只能通过受控 DNS 和代理访问外部。
这个架构的关键在于:执行环境不能直接访问外部 DNS。它的/etc/resolv.conf指向受控解析器,而受控解析器只监听内部地址。这样即使模型生成的代码想自己解析域名,也只能走受控通道。
3.2 网络命名空间与出口控制
先创建独立的网络命名空间。用 Linux 的ip netns就能做,不需要额外工具。
# 创建命名空间 ip netns add agent-sandbox # 创建一对 veth 接口,一端在宿主机,一端在命名空间 ip link add veth-host type veth peer name veth-sandbox ip link set veth-sandbox netns agent-sandbox # 配置宿主机侧 ip addr add 10.200.0.1/24 dev veth-host ip link set veth-host up # 配置命名空间侧 ip netns exec agent-sandbox ip addr add 10.200.0.2/24 dev veth-sandbox ip netns exec agent-sandbox ip link set veth-sandbox up ip netns exec agent-sandbox ip link set lo up # 默认路由指向宿主机侧,但宿主机侧默认不转发 ip netns exec agent-sandbox ip route add default via 10.200.0.1这时候命名空间里能 ping 通宿主机侧,但出不了外网,因为宿主机没有开启转发,也没有 NAT 规则。这是故意的——默认拒绝比默认允许安全得多。
接下来只放行必要的出口。假设沙箱需要访问一个包管理源和一个内部 API,我们在宿主机侧用 iptables 做精细控制。
# 开启转发 sysctl -w net.ipv4.ip_forward=1 # 只允许命名空间访问受控 DNS(假设受控 DNS 跑在 10.200.0.1:5353) iptables -A FORWARD -i veth-host -o veth-host -p udp --dport 5353 -j ACCEPT iptables -A FORWARD -i veth-host -o veth-host -p tcp --dport 5353 -j ACCEPT # 放行到内部 API 的流量(假设 API 在 10.200.0.1:8080) iptables -A FORWARD -i veth-host -o veth-host -p tcp --dport 8080 -j ACCEPT # 其余全部拒绝 iptables -A FORWARD -i veth-host -o veth-host -j DROP这样命名空间里的进程只能访问受控 DNS 和内部 API,其他一律不通。注意这里没有放行 53 端口的常规 DNS,就是为了强制所有解析走受控通道。
3.3 受控 DNS 解析器的实现要点
受控 DNS 解析器可以用现成的工具,比如dnsmasq配合自定义配置,也可以用 Python 的dnslib自己写一个。自己写的好处是逻辑完全可控,方便加白名单校验和重绑定防护。
核心逻辑分几步。收到解析请求后,先检查域名是否在白名单内。不在白名单的直接返回 NXDOMAIN 或一个黑洞地址。在白名单内的,向上游解析,拿到结果后做过滤:如果解析结果是内网地址段(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8),直接拒绝,防止重绑定到内网。过滤通过后,把结果缓存起来,并设置一个合理的 TTL,在 TTL 内后续请求直接返回缓存结果,不再向上游查询。
from dnslib import DNSRecord, RR, QTYPE, A from dnslib.server import DNSServer, BaseResolver import ipaddress WHITELIST = {"pypi.org", "api.internal.example.com"} BLOCKED_NETS = [ ipaddress.ip_network("10.0.0.0/8"), ipaddress.ip_network("172.16.0.0/12"), ipaddress.ip_network("192.168.0.0/16"), ipaddress.ip_network("127.0.0.0/8"), ] class SafeResolver(BaseResolver): def resolve(self, request, handler): qname = str(request.q.qname).rstrip(".") reply = request.reply() if qname not in WHITELIST: reply.header.rcode = 3 # NXDOMAIN return reply # 向上游解析(这里简化处理,实际应调用上游 DNS) upstream_ip = self._query_upstream(qname) ip = ipaddress.ip_address(upstream_ip) for net in BLOCKED_NETS: if ip in net: reply.header.rcode = 3 return reply reply.add_answer(RR(qname, QTYPE.A, rdata=A(upstream_ip), ttl=60)) return reply def _query_upstream(self, qname): # 实际实现中调用上游 DNS 并返回 IP ...这个解析器跑在宿主机的 10.200.0.1:5353 上,命名空间里的/etc/resolv.conf指向它。这样所有解析都经过白名单和地址过滤,重绑定到内网的路径被堵死。
注意:TTL 设置很关键。设得太长,白名单变更后生效慢;设得太短,缓存防护效果弱。60 秒是个比较平衡的值,具体可以根据你的白名单更新频率调整。
3.4 执行环境的 DNS 配置与验证
命名空间里的执行环境需要显式配置 DNS。创建/etc/netns/agent-sandbox/resolv.conf,内容指向受控解析器。
mkdir -p /etc/netns/agent-sandbox echo "nameserver 10.200.0.1" > /etc/netns/agent-sandbox/resolv.conf echo "options timeout:2 attempts:1" >> /etc/netns/agent-sandbox/resolv.conf注意端口问题。标准 DNS 走 53 端口,但我们的受控解析器跑在 5353。有两个选择:一是让解析器监听 53,但需要 root 权限且可能和系统 DNS 冲突;二是用 iptables 做端口重定向,把命名空间发往 10.200.0.1:53 的流量转到 5353。
iptables -t nat -A PREROUTING -s 10.200.0.2 -p udp --dport 53 -j REDIRECT --to-port 5353 iptables -t nat -A PREROUTING -s 10.200.0.2 -p tcp --dport 53 -j REDIRECT --to-port 5353配置完成后,进命名空间验证。
# 应该能解析白名单域名 ip netns exec agent-sandbox nslookup pypi.org # 应该解析失败 ip netns exec agent-sandbox nslookup evil.example.com # 应该无法直接访问外部 IP ip netns exec agent-sandbox curl --max-time 3 http://1.1.1.1如果白名单域名能解析、非白名单解析失败、直接 IP 访问超时,说明基础防护到位了。
4. 实操中踩过的坑与排查清单
4.1 DNS 相关问题的典型表现
搭这套东西的过程中,DNS 问题是最磨人的,因为它经常表现为“时好时坏”或者“莫名其妙”。我整理了几种典型表现和对应的排查方向。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 白名单域名偶尔解析失败 | 上游 DNS 不稳定或 TTL 过期 | 检查上游 DNS 可用性,适当增大缓存 TTL |
| 解析结果和预期不符 | 缓存污染或上游返回了 CDN 节点 | 对比多次解析结果,检查是否有异常 IP |
| 命名空间内无法解析 | resolv.conf 未生效或端口未重定向 | 检查 /etc/netns 配置和 iptables NAT 规则 |
| 解析成功但连接超时 | 出口防火墙未放行目标 IP | 检查 FORWARD 链规则,确认目标 IP 在放行范围 |
| 重启后配置丢失 | netns 和 iptables 规则未持久化 | 用 systemd 或启动脚本固化配置 |
4.2 几个容易忽略的细节
第一个细节是IPv6。很多团队只配了 IPv4 的防护,但容器和命名空间默认可能启用 IPv6。如果 IPv6 出口没控制,模型可以通过 IPv6 绕过所有 IPv4 规则。建议在沙箱里直接禁用 IPv6,或者对 IPv6 做同样的白名单控制。
ip netns exec agent-sandbox sysctl -w net.ipv6.conf.all.disable_ipv6=1第二个细节是DNS over HTTPS 和 DNS over TLS。如果执行环境里的程序自己实现了 DoH/DoT,它可能绕过系统 DNS 配置,直接通过 HTTPS 查询解析结果。这种情况下,受控 DNS 就失效了。防御办法是在出口防火墙上只放行受控 DNS 的端口,其他 443 端口的流量如果目标是已知 DoH 服务商,也要拦截。更彻底的做法是只允许通过内部代理访问外网,代理层做域名校验。
第三个细节是解析结果的二次校验。即使 DNS 层做了过滤,应用层在拿到 IP 后最好再校验一次,确认目标 IP 不在内网段。这是纵深防御的思路,DNS 层被绕过时还有一道防线。
4.3 上线前的安全检查清单
在把 Agent 沙箱推向生产前,我建议过一遍这个清单。每一条都对应一个实际出现过的风险点。
- 沙箱是否有独立的网络命名空间,默认出口是否为拒绝
- DNS 解析是否走受控通道,是否禁用了外部 DNS
- 白名单是否同时覆盖域名和 IP,是否有定期更新机制
- 是否防护了 DNS 重绑定,解析结果是否过滤内网地址段
- IPv6 是否被禁用或同等管控
- 是否拦截了 DoH/DoT 等绕过系统 DNS 的通道
- 应用层是否有二次 IP 校验
- 所有网络规则是否持久化,重启后是否自动恢复
- 是否有日志记录所有解析请求和出站连接,便于事后溯源
- 是否定期做绕过测试,验证防护有效性
这份清单不是一次性的,建议每次沙箱配置变更后都重新过一遍。网络隔离这东西,配好之后很容易被后续的“临时放行”慢慢侵蚀,定期复查很有必要。
5. 工具调用安全的延伸思考
5.1 从 DNS 缺口看沙箱设计的信任边界
这次事件真正值得琢磨的,不是 DNS 重绑定这个具体技术,而是沙箱设计中对“信任”的假设。沙箱的每一层防护,都建立在某个东西“可信”的基础上。文件系统隔离信任内核的权限检查,网络隔离信任 DNS 解析结果,工具调用信任参数校验。一旦某个被信任的环节出问题,整条防线就可能被穿透。
做 Agent 安全,本质上是在管理这些信任假设。你需要明确知道:哪些环节是你信任的,为什么信任,如果它失效了会怎样。DNS 之所以成为盲区,就是因为很多团队默认“DNS 是基础设施,应该没问题”,而没有把它纳入安全设计的考量范围。
5.2 Agent 工具调用的最小权限原则
顺着这个思路,工具调用的权限设计应该遵循最小权限原则。每个工具只给完成它功能所必需的最小权限,不多给一分。读文件的工具就只给读权限,不要给写;发 HTTP 请求的工具就只给特定域名的访问权,不要给全网访问;执行代码的工具就只给受限的运行时,不要给完整 shell。
这个原则说起来简单,做起来需要克制。开发期为了方便,很容易把权限放大,上线时又忘了收回来。我的做法是:开发环境和生产环境用两套完全独立的沙箱配置,开发环境可以宽松,生产环境必须严格,并且两套配置的差异要显式记录,避免“临时放宽”被带到线上。
5.3 监控与溯源:出问题时怎么查
再好的防护也可能被绕过,所以监控和溯源能力是必须的。沙箱里所有 DNS 解析请求、所有出站连接尝试,都应该有日志。日志要包含时间、来源、目标、结果,并且集中收集,避免沙箱被清理后日志丢失。
排查问题时,我通常按这个顺序看:先看 DNS 解析日志,确认解析请求是否走了受控通道,有没有异常域名;再看防火墙日志,确认出站连接是否被正确放行或拒绝;最后看应用日志,确认工具调用的实际行为是否符合预期。这三层日志对上了,基本能定位问题出在哪一环。
提示:日志本身也要注意安全,不要记录敏感数据,并且要防止日志被沙箱内的代码篡改。日志应该写到沙箱外部,由宿主机或独立服务收集。
5.4 给 Agent 开发者的几条实操建议
最后分享几条我在实际项目里总结的建议,都是踩过坑之后才明白的。
第一条,不要把安全寄托在模型身上。模型的输出是不可控的,它可能被提示词影响,也可能自己“发挥”。安全边界必须由沙箱和基础设施来保证,而不是靠模型“自觉”。
第二条,默认拒绝,显式放行。无论是网络、文件还是系统调用,默认都应该是拒绝的,只有明确需要的才放行。这个原则会增加一些配置成本,但能避免绝大多数意外。
第三条,定期做绕过测试。自己写几个测试用例,尝试从沙箱里访问非白名单域名、访问内网地址、用 DoH 绕过 DNS,看看防护是否有效。这种测试不用很复杂,但能发现配置疏漏。
第四条,保持对基础设施层的关注。Agent 开发者容易把注意力全放在模型和框架上,但 DNS、网络、容器运行时这些“底层”东西,往往才是安全的关键。这次事件就是个例子,一个 DNS 缺口,影响的是整个工具调用链路。
这套沙箱方案我在几个项目里用过,稳定性没问题,配置也不算复杂。真正花时间的是想清楚信任边界在哪、哪些环节需要防护、防护失效时怎么发现。技术实现反而是后面的事。如果你也在做 Agent 工具调用的安全隔离,希望这些经验能帮你少走点弯路。