在过去几周里,我建立了自己的DNS客户端,主要是因为我认为dig(标准的DNS客户端)有点笨拙。部分原因是我想学习更多关于DNS的知识。因此,这里是我如何建立它,以及你如何建立你自己的。这是一个伟大的周末项目,我从完成它中学到了很多。
为什么要这么做?
Julia Evans给了我做一个DNS客户端的想法。她是我最喜欢的技术博主之一。她的博客总是能教给我一些东西,并经常激励我去学习新的东西。她还非常善于把复杂的话题总结成非常简单的小漫画,比如这幅。
当我读到这幅漫画时,我感到很震惊—DNS查询协议比我想象的要简单得多。另外,我在一家公司工作,该公司是,呃,在DNS世界中的一个大玩家。我也许应该更了解它。

计划列表
我想做一个DNS客户端的另一个原因是,我知道我可以用一些伟大的Rust板块来简化每一个步骤。该计划。
- 使用picoargs解析CLI的args。
它没有clap那么强大,后者是标准的Rust “企业级 “CLI crate,而且需要更多的模板。但我并不真的需要高级的CLI功能,而且picoargs的编译速度要快很多。
- 使用bitvec序列化DNS查询,这是一个用于读取或写入单个比特的很棒的通用工具箱。
我在做Advent of Code的时候学会了如何用Nom解析位数协议。我考虑过用deku来代替,但决定不这样做。
- 用stdlib UdpSocket类型与DNS解析器通信
我不知道它是如何工作的,但Rust的stdlib真的有很好的文档,所以我确信我可以把它捡起来。
- 用Nom解析二进制响应
我在做Advent of Code的时候学会了如何用Nom解析位数协议,使用位级Nom来解析一比特标志和四比特数字。
- 使用普通的println!来向用户打印响应信息
我是怎么做的?
它花了大约800行代码,我在一个周末就基本完成了。只有规范中的一个部分我没有实现:消息压缩(MC)。不幸的是,我又花了一个周末才完成—更多细节见下文。
我把它命名为Dingo,因为它听起来像Dig,而且它让我想起了澳大利亚,我的家。无论如何,它是有效的

你可以安装它或在GitHub(https://github.com/adamchalmers/dingo)上阅读成品代码。
我学到了什么?
阅读RFCs
我认为很多程序员都被RFCs吓到了。至少,我愿意这么想,因为我确实是这样。也许我所有的同行都是暗地里喜欢RFC的小精灵,他们从阅读纯文本ASCII图中获得了令人心跳加速的感觉……但他们从未提及。
RFC 1035定义了DNS消息协议,所以我不得不仔细阅读。这是我第一次真正从上到下地阅读RFC,我对它的可读性感到惊讶。我不断地参考它,并把RFC中的关键定义和引语粘贴到源代码的注释中,以帮助理解所有的部分是如何结合在一起的。也许RFC 1035是不寻常的好,其余的实际上都是可怕的和不可理解的。但我喜欢它。
把它作为历史文件、主要资料来读特别有意思—自20世纪80年代以来发生了很多变化,在现代互联网真正存在之前,了解当时的程序员在想什么是很吸引人的)
套接字
我对套接字一直不是很适应。我在大学时曾试着读过Beej的《套接字编程指南》,但我并不具备必要的操作系统、网络或C语言技能来完成它。我知道TCP和UDP,但我对统一它们的底层抽象一无所知。
这个项目是我第一次需要打开一个UDP套接字–在我平时的编程中,我只是依靠一些网络库来处理这个低层次的细节。因此,我阅读了Rust关于UDP套接字的文档,其中的内容非常清晰。UdpSocket上的很多方法都直接对应于Linux的系统调用。
当我后来回去好好阅读Beej的套接字指南时,真的很容易。所有这些系统调用都很熟悉–它们只是Rust stdlib的网络方法。
事实上,如果我使用dtruss(一个MacOS工具,用于检查你的程序使用哪些系统调用),我可以准确地看到我的程序正在使用哪些系统调用。
$ sudo dtruss dingo -t A www.twitter.com# Skipping lots of syscalls just for starting a process on MacOS...getentropy(0x7FF7BE8734E0, 0x20, 0x0) = 0 0 # Used by `rand` to generate a random DNS request IDsocket(0x2, 0x2, 0x0) = 3 0 # Create the UDP socket, aka "file descriptor 3"ioctl(0x3, 0x20006601, 0x0) = 0 0 # Not sure, something about the UDP socketsetsockopt(0x3, 0xFFFF, 0x1022) = 0 0 # Set the options on the UDP socketbind(0x3, 0x7FF7BE87346C, 0x10) = 0 0 # Bind the UDP socket to a local addresssetsockopt(0x3, 0xFFFF, 0x1006) = 0 0 # Set more options, dunno why it needs more...connect(0x3, 0x7FF7BE873584, 0x10) = 0 0 # Connect to the remote DNS resolversendto(0x3, 0x7FAE060041F0, 0x21) = 33 0 # Send the request to the remote DNS resolverrecvfrom(0x3, 0x7FAE060043F0, 0x200) = 79 0 # Get the response from the remote DNS resolverclose_nocancel(0x3) = 0 0 # Close the UDP socket
# Skipping lots of syscalls just for ending a process on MacOS
系统调用connect、sendto和recvfrom都来自于调用Rust方法UdpSocket::{connect, send_to, recv_from}—它们以1:1的方式转换为系统调用! 这真是太酷了。
我真的很喜欢Bitvec。它结合了Vec<bool>的可用性和可读性,以及使用位的技巧的速度。它是Rust “工效、速度和正确性 “理想的完美范例。
该库提供了BitArray、BitVec和BitSlice的类型。它们的工作原理基本相同,但我发现有两个小问题令人困惑,它们的工作原理不同。不过这些问题很容易被单元测试发现,所以我想这只是一种学习经验。作者希望在Rust推出更多的常量泛型功能后,他们可以发布Bitvec 2.0,让这些类型的工作方式保持一致。
奇怪输出
我早些时候提到,我讨厌使用Dig。它有这么多混乱的字段和奇怪的无关信息。我只想看看某个主机名的解析结果,而Dig强迫我阅读所有这些我不关心的额外信息。
$ dig adamchalmers.com
; <<>> DiG 9.10.6 <<>> adamchalmers.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51459
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;adamchalmers.com. IN A
;; ANSWER SECTION:
adamchalmers.com. 300 IN A 104.19.237.120
adamchalmers.com. 300 IN A 104.19.238.120
;; Query time: 80 msec
;; SERVER: 2600:1700:280:1f40::1#53(2600:1700:280:1f40::1)
;; WHEN: Sun Apr 10 17:48:43 CDT 2022
;; MSG SIZE rcvd: $ dig +short adamchalmers.com
104.19.238.120
104.19.237.12077
但在实现了一个DNS客户端后,我真的知道这些都是什么意思了! 比如,”IN “并不是指英语中的 “in”,它是 “internet “的缩写,因为DNS在技术上支持许多可能的命名空间(只是我们基本上只在互联网DNS上使用dig)。
现在读dig的输出结果有点酷,因为它提醒了我学到了多少东西。哦,对了,我还了解到,要让dig提供你想要的信息是很容易的。
$ dig +short adamchalmers.com
104.19.238.120
104.19.237.120
…如果我在一月份就知道这一点,我可能永远不会开始这个项目 🙂
我还是喜欢枚举
枚举是一种表达领域逻辑的好方法。功能性程序员讨论联合类型已经有几十年了,我很高兴它们终于在其他语言中出现了。在Rust/Swift/Haskell中使用枚举后,很难再在Go中建立领域逻辑模型。
消息压缩 (MC)
MC是一个整洁的功能,DNS服务器可以用它来减少其响应的大小。MC不是在响应中多次重复同一个主机名,而是用一个指向先前引用的主机名的指针来替换一个主机名。RFC实际上解释得非常好。MC帮助服务器将他们的DNS响应放入一个UDP数据包中,这很重要,因为UDP是不可靠的,不关心截断的数据包。MC需要回看之前解析过的字节,但Nom只让你回看剩余的、未解析过的字节。我花了好几次时间才能够以一种漂亮的、习惯性的Nom方式支持MC,所以这又花了我一个周末的功夫。
总结
这是我做过的最喜欢的项目之一。我今年的职业目标是学习更多的Linux和网络知识。编写dingo让我学到了很多关于互联网的基本构件之一,以及真正的操作系统如何处理它。如果你想学习更多关于低级别的编程,DNS客户端是一个完美的挑战。它有位数运算、解析、UDP网络、IP地址和DNS主机名。你会学到很多东西。事实上,在我写完dingo之后,我的朋友Jesse Li用Python写了他自己的DNS客户端。很明显,编写DNS客户端是新的热门趋势,你必须要加入进来。如果你尝试了,应该在下面评论 🙂
我试过deku,它非常好!我喜欢它使用你的结构体字段上的注解来生成序列化和反序列化的方法,这样它们就不会冲突。我喜欢它使用结构体字段上的注解来生成序列化和反序列化的方法,所以它们永远不会冲突,而且你不必学习两个独立的库(bitvec和nom)。
但我想练习明确地使用bitvec并适应它的API—毕竟,我做这个项目只是为了学习东西。
另外,deku使用serde和syn来支持其(非常有用的)序列化注释。这些板块真的很强大,可以真正减少你代码库中的模板。但它们确实给你的构建时间增加了相当多的开销。这在工作中不是问题,因为我的Rust项目相当大,而且已经在树中包含了serde/syn。但是dingo没有使用它们中的任何一个,所以加入Deku会使构建时间从5秒增加到15秒。我希望将来能使用Deku,但它并不适合这个特定的项目。
Syscalls就像操作系统定义的函数,所以操作系统可以管理像I/O这样的风险操作。Dtruss是一个MacOS版本的strace,一个Linux工具。我从Julia Evans的伟大的strace漫画中学到了如何使用strace,我强烈推荐这个漫画,我从里面学到了很多。现在当我写一个程序时,我可以窥探到编译后的代码在运行我的函数时究竟在做什么。