当大多数人对 DNS 的认知停留在「域名解析」这一基础功能时,安全研究者已经发现这座运行了近四十五年的协议实际上是一个「免费、全球分布、任意查询」的非结构化存储系统。DOOM Over DNS 项目将这一发现推向了极致:通过近两千条 DNS TXT 记录存储完整的游戏引擎与资产数据,在仅开放 DNS 的受限网络中实现经典游戏的全量流式运行。整个过程不产生任何磁盘写入,所有数据在内存中完成组装与执行,这种技术路径为受限网络环境下的程序分发与游戏流式加载提供了极具参考价值的工程实践。

DNS TXT 记录的非预期使用场景

DNS 协议自 RFC 1035 以来已运行近四十五年,其核心设计目标是建立域名与 IP 地址之间的映射关系。然而,TXT 记录的出现为这个看似单调的协议注入了意外的可能性。TXT 记录最初设计用于电子邮件认证机制(如 SPF、DKIM),协议规范并未对文本内容施加实质性验证,这意味着一理论上可以写入任意字符数据。每个 TXT 记录可容纳约两千字节的文本,而一个 DNS 区域可以承载数千条记录,公共 DNS 服务更是在全球边缘节点缓存这些数据,任何具备网络访问能力的终端都可以发起查询。

这种特性使得 DNS 在某种程度上成为一个免费、全球化、服务器无状态化的键值存储系统。安全研究者很早就注意到这一特性并将其应用于渗透测试场景:将恶意代码的 shellcode 分阶段存储在 TXT 记录中,植入程序在运行时动态查询并组装 Payload,从而绑过传统网络边界防护。这种技术的优势在于 DNS 流量通常不会被深度检测,历史内容的法医分析更是少有人涉足。

DOOM Over DNS 项目将这一思路推进到了全新的维度:既然可以存储代码片段,那么完全可以存储完整的程序与数据。如果一个程序能够从 DNS 记录中加载执行,那么它就具备了真正的游戏逻辑能力,而不仅仅是传输一段 shellcode。

数据分片与记录存储策略

将数兆字节的二进制数据编码为 DNS TXT 记录涉及若干关键的技术决策。首先是编码方式的选择:Base64 是最直接的方案,它将每三个字节转换为四个可打印 ASCII 字符,确保在 DNS 协议层不会因特殊字符导致解析错误。然而原始 Base64 的效率并不足以支撑本项目的规模需求,因此实际实现中需要对游戏引擎二进制与 WAD 资源文件进行预处理压缩。

项目使用的 managed-doom 是纯 C# 实现的 DOOM 引擎移植,经过压缩后 WAD 文件从四兆字节降至约一兆字节,引擎 DLL 捆绑包则从四点四兆字节压缩至约一兆字节。两者合计需要约一千九百六十六条 TXT 记录来承载完整数据,这一数据量恰好可以放入一个 Cloudflare Pro 版本的 DNS 区域。每个分片记录仅存储数据正文,而元数据记录则负责维护索引信息、校验哈希以及多区域分片配置。玩家端的 PowerShell 脚本通过解析元数据记录获知总记录数、分片范围与校验信息,随后发起批量 DNS 查询并依序重组数据流。

对于免费层级的 DNS 服务商,单个区域仅支持一百八十五条数据分片,此时需要跨多个域名实现数据分片。项目提供的 -Zones 参数支持传入域名数组,发布脚本会自动计算分片策略并将数据均匀分布到各域名。对于 Pro 级别或更高权限的域名,单个区域即可容纳三千四百条分片记录,足以承载完整的游戏数据。

内存加载与无文件执行实现

将程序从磁盘加载到内存执行在高级语言层面通常依赖文件系统路径,但要让游戏真正「无文件」运行,需要对既有代码进行实质性改造。managed-doom 原始版本期望从磁盘路径读取 WAD 文件与游戏资源,同时编译为原生可执行文件格式,无法通过。NET 的反射机制从内存字节直接加载。项目的解决方案涉及三个层面的改造:文件流替换为内存流、程序集加载方式调整以及原生窗口库的替代。

在文件流替换层面,原代码中所有涉及文件路径读取的逻辑被修改为接受 MemoryStream 对象,这意味着游戏数据可以在内存中完成解压与组装后直接传递给引擎,无需任何磁盘操作。在程序集加载层面,原生 AOT 编译的二进制被改为框架依赖的 .NET 8 程序集,这样可以利用 Assembly.Load() 方法将原始字节直接加载到当前 AppDomain。窗口管理方面,原方案依赖 GLFW 这一原生 DLL,而 Windows 要求所有原生 DLL 必须存在于文件系统才能被加载,因此项目移除了 GLFW 依赖并改用直接的 Win32 P/Invoke 调用进行窗口创建与渲染。

最终实现的游戏引擎由若干纯托管程序集构成,可完全在内存中运行。唯一的妥协是移除了音频子系统以减少数据量,因此游戏运行在静音状态。这并非技术限制,而是出于分片数量与网络传输效率的工程权衡。

玩家端实现与查询策略

客户端的实现使用约两百五十行 PowerShell 7 脚本完成核心功能。脚本接收命令行参数后可指定主域名、DNS 服务器地址、WAD 文件类型、引擎启动参数以及数据分片前缀等。默认参数适配大多数场景,但 -DnsServer '1.1.1.1' 参数在记录尚未同步到本地解析器时尤为必要。

脚本执行时首先查询元数据记录以获取总记录数、校验哈希与可选的额外域名列表。随后使用 PowerShell 的 Resolve-DnsName cmdlet 并发发起大量 DNS 查询,现代网络环境下通常在十到二十秒内完成全部约两千次查询。获取的原始文本数据经 Base64 解码后重新组装为字节数组,计算哈希值与元数据中的校验值比对以确保数据完整性。校验通过后,字节数组被传递至。NET 的 Assembly.Load() 方法完成程序集加载,最后通过反射调用入口点启动游戏。

整个过程没有任何文件写入操作,系统仅保留 DNS 解析产生的网络流量与内存中的游戏进程。这使得该方案天然适配高度受限的网络环境,只要允许 DNS 查询流量即可运行完整游戏。

受限网络场景的工程参数

将这一技术应用于真实受限网络时,需要关注若干关键参数以确保可用性。首先是单次查询的超时配置,DNS 查询的默认超时通常在一到两秒,但在网络质量不佳的环境中可能需要调整至五秒以上。其次是并发查询数的控制,过高的并发可能触发目标 DNS 服务器的速率限制或本地防火墙的连接数阈值,实践中建议控制在二十至五十个并行查询。

数据分片大小的选择也需要权衡。单条 TXT 记录的最大容量约为两千字符,对应约一千五百字节的 Base64 编码前数据。如果网络丢包率较高,使用较小的单片容量可以降低单次查询失败的影响范围,但会增加元数据管理的复杂度与总查询次数。对于丢包率超过百分之五的网络,建议将单片容量降至一千字节以下并增加重试机制。

缓存策略同样值得关注。公共 DNS 服务会在边缘节点缓存 TXT 记录,这可以显著降低源站压力并提升查询速度,但同时也意味着数据更新后需要等待缓存失效。对于需要频繁更新的场景,应在记录中包含版本号并在客户端实现版本检查逻辑。

技术边界与安全考量

该技术路径的核心价值在于展示了 DNS 协议的非预期使用方式,但这也意味着在生产环境中直接套用需要谨慎评估。首先是可靠性问题,DNS 协议设计目标并非数据传输,丢包、顺序错乱与重复响应等情况在网络异常时更为频繁,客户端必须实现完整的重试与校验机制。其次是合规性问题,部分网络管理员可能在 ACL 层面明确禁止非标准 TXT 记录查询,滥用此技术可能导致网络访问权限被撤销。

从安全防御视角看,该技术揭示了 DNS 作为隐蔽信道的潜力。攻击者可能利用类似技术实现恶意代码的无文件传输与执行,而这种流量的隐蔽性使得传统安全设备难以检测。安全团队应当考虑在 DNS 监控体系中增加对异常大体积 TXT 记录与高频查询行为的告警规则。


资料来源:GitHub resumex/doom-over-dns 项目(https://github.com/resumex/doom-over-dns)、作者 Adam Rice 技术博客(https://blog.rice.is/post/doom-over-dns/)。