当你询问「DNS 还能做什么?」时,答案可能是运行 DOOM。DOOM Over DNS 是一个极具创意的工程实验:将整个 DOOM 游戏引擎和 WAD 文件拆分存储在近两千条 DNS TXT 记录中,客户端通过 PowerShell 脚本和标准 DNS 查询在运行时动态获取数据,最终在内存中完整加载并运行这款经典游戏。整个过程不涉及任何文件下载,WAD 文件从未触碰磁盘,.NET 引擎程序集直接加载到内存中执行。

这个项目背后涉及一系列工程挑战:如何在 DNS 协议的限制下实现可靠的数据分片、如何设计高效的编码方案、以及如何处理网络传输带来的延迟问题。本文将从技术实现角度深入分析这一创意项目的核心设计。

DNS 协议作为传输层的基础限制

DNS 协议传统上仅用于域名到 IP 地址的解析,但其 TXT 记录类型允许存储任意文本字符串,这一特性为构建隐蔽数据传输通道提供了可能。DOOM Over DNS 项目的核心思路是将 DNS 作为一种低带宽、分布式且全球可访问的存储层来使用。Cloudflare 免费提供全球边缘缓存的 DNS 服务,单个免费域名可承载 185 个数据分片,而 Pro 版本及以上则支持 3400 个分片。

这种架构带来了独特的优势:无需自建服务器、无需配置 CDN、无需考虑带宽成本,任何能发起 DNS 查询的客户端都可以获取数据。然而,DNS 协议的设计初衷并非文件存储,因此必须面对若干硬性限制。首先是单条 TXT 记录的长度限制,RFC 1035 并未强制规定 TXT 记录的最大长度,但实际部署中通常受限于 UDP 数据报文的 512 字节限制,超过此长度的响应会被截断或切换至 TCP 模式。其次是查询延迟,每次 DNS 解析都会产生网络往返延迟,在游戏这种对实时性有要求的场景下,必须通过缓存策略和预取机制来平滑体验。

数据分片与编码方案设计

DOOM 游戏包含两部分核心数据:游戏引擎的可执行代码(DLL 形式)和游戏资源文件(WAD 文件)。Shareware 版本的 DOOM1.WAD 体积较小,但仍需要约 1199 个数据分片来存储。完整的游戏引擎程序集则需要额外的 765 个分片,总计约 1964 条 TXT 记录才能完整承载整个游戏。

分片策略采用固定大小的数据块,每块包含特定序号的游戏数据。客户端在启动时首先查询元数据记录获取总分片数和校验信息,然后按顺序并行或串行获取各个分片。为了确保数据传输的可靠性,每个分片都包含 CRC32 校验码,客户端在接收后立即验证数据完整性,若校验失败则触发重传机制。这种设计虽然增加了网络开销,但保证了在不稳定网络环境下仍能可靠地恢复完整数据。

在数据压缩方面,项目在上传阶段使用 Deflate 算法对游戏引擎程序集和 WAD 文件进行预处理,将原始二进制数据转换为更紧凑的格式。考虑到 DNS 查询的文本特性,分片数据还需要进行 Base32 或类似编码,确保输出为可打印 ASCII 字符,避免特殊字符导致的解析问题。

运行时加载与内存管理机制

DOOM Over DNS 的客户端实现基于 PowerShell 7 和 .NET 8 运行时。核心脚本 Start-DoomOverDNS.ps1 负责解析命令行参数、构建 DNS 查询请求、处理数据重组,并最终启动游戏引擎。整个过程的执行流程分为三个阶段:元数据获取阶段、分片下载阶段和内存加载阶段。

在元数据获取阶段,客户端首先查询特定的元数据 TXT 记录(如 stripe-meta),该记录包含总分片数、分片大小、校验信息和游戏配置参数。这一设计使得客户端可以动态适应不同的游戏版本和配置,无需硬编码任何游戏相关参数。获取元数据后,客户端进入分片下载阶段,此时会并发发起多个 DNS 查询以提高传输效率。

内存加载阶段是整个项目最关键的技术难点。项目使用的 managed-doom 是原生 Doom 引擎的 .NET 移植版本,原版使用 Native AOT 编译,无法通过 Assembly.Load() 动态加载。DOOM Over DNS 项目组为此创建了一个专用分支,将引擎改造为框架依赖的 .NET 8 程序集,支持从流直接加载。同时,游戏资源的加载方式也被修改为流式读取,数据从 DNS 获取后直接传入内存流,省去了任何磁盘写入操作。

关键参数配置与监控要点

实际部署和运行 DOOM Over DNS 时,需要关注若干关键参数。启动脚本 Start-DoomOverDNS.ps1 提供以下可配置项:-PrimaryZone 指定 DNS 区域名称(必需参数);-DnsServer 允许指定特定 DNS 解析器 IP,默认为系统解析器;-WadName 用于选择 WAD 文件类型,支持 doom1、doom、doom2、plutonia、tnt 等变体;-DoomArgs 允许传递游戏启动参数,例如 -warp 1 3 -skill 5 可直接进入特定关卡和难度;-WadPrefix-LibsPrefix 分别定义 WAD 数据和引擎 DLL 数据的 DNS 记录前缀。

对于上传部署,Publish-DoomOverDNS.ps1 脚本需要以下参数:-PublishDir 指向 dotnet publish 输出的目录;-WadPath 指定 WAD 文件路径;-Zones 数组定义目标 DNS 区域;-Force 参数可在覆盖已有记录时跳过确认提示。特别值得注意的是免费用户的多区域分片策略:由于免费版每个域名仅支持 185 个分片,而 WAD 文件需要约 1199 个分片,因此必须配置多个域名并在 -Zones 参数中传递域名数组,系统会自动进行分片分发。

上传中断处理是该项目的另一个工程亮点。Publish-TXTStripe 函数支持 -Resume 参数,执行时会自动验证已有分片的哈希值、定位最后一个成功上传的分片位置,并从断点处继续上传。这对于大量分片的批量上传场景至关重要,避免了网络波动导致的全部重传。

性能考量与适用边界

尽管 DOOM Over DNS 展示了 DNS 协议的非传统用法,但在实际应用中必须清醒认识到其性能边界。DNS 查询的延迟通常在数十毫秒到数百毫秒不等,取决于网络拓扑和 DNS 解析器的负载情况。对于 DOOM 这种需要 60 FPS 流畅运行的游戏而言,通过 DNS 实时传输每一帧游戏数据是不可能的。

项目的实际工作模式是将游戏完整下载到内存后再运行,而非通过 DNS 实时传输游戏画面。这意味着只有在首次启动时需要等待分片下载完成,之后的游戏体验完全在本地内存中执行。这种设计巧妙地规避了 DNS 的实时性限制,将其仅用作一种创意的大规模数据传输通道。

从安全角度来看,大量异常的 DNS 查询可能触发某些网络设备的告警或限流机制。此外,Cloudflare 等 DNS 提供商对 TXT 记录的滥用行为有相应的服务条款限制,生产环境中的类似应用需谨慎评估合规性风险。

DOOM Over DNS 项目展示了工程师在资源受限条件下进行创造性问题解决的能力,同时也为理解 DNS 协议的深层特性和边界提供了有趣的实践案例。

资料来源:GitHub reposumex/doom-over-dns