栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

【译】Rust 实现一个 DNS 客户端,我从中学到什么

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【译】Rust 实现一个 DNS 客户端,我从中学到什么

  • What I learned from making a DNS client in Rust 译文(Rust 实现一个 DNS 客户端,我从中学到什么)
  • 原文链接:https://blog.adamchalmers.com/making-a-dns-client/
  • 原文作者:Adam Chalmers
  • 译文来自:https://github.com/suhanyujie/article-transfer-rs/
  • 译者:suhanyujie
  • ps:水平有限,翻译不当之处,还请指正。
  • 标签:Rust, DNS, DNS client

在过去的几周里,我构建了我自己的 DNS 客户端。主要是因为我认为 dig(一个标准的 DNS 客户端)有点笨重。另一部分原因是我想了解更多关于 DNS 的知识。这就是我如何创建它,也是你如何创建 dns 客户端的过程。它是一个很棒的“周末项目”,我从完成它中学到了很多。

为什么?

Julia Evans 给我提供了一些思路。她是我最喜欢的技术博客作者之一。她的博客总能给我一些启发,经常激励我去学习新的东西。她也很擅长把复杂的话题总结成简单的小漫画,比如这个:

但我看完这个漫画后,我震惊了 —— DNS 查询协议比我想象的要简单得多。并且,我所在的公司在 DNS 领域算是一个大玩家。我理应更了解这个。

计划

我想构建 DNS 客户端的另一个原因是我可以使用一些优秀的 Rust crate 来快速实现(节省很多步骤)。

  1. 使用 picoargs 解析 CLI 参数

它没有 clap 那么强大,clap 可以视为 Rust 的“企业级” crate,它依赖了很多其他 crate。但我并不需要那么多高级特性,而且 picoargs 的编译速度会快_很多_。

  1. 使用 bitvec 序列化 DNS 查询,它是一个强大且通用的读写“位”值的库。

在做 Advent of Code 时,我学会了使用 Nom 解析“位”协议,我也考虑过使用 deku,但最后还是不使用它了。

  1. 使用 stdlib UdpSocket 类型与 DNS 解析器通信

我不知道怎么使用,但 Rust stdlib 有很详细的相关文档,所以我有信心能够学会它。

  1. 使用 Nom 解析响应的二进制数据

    在做 Advent of Code 的时候,我学会如何使用 Nom 解析位协议。我之前的(博客)(/nom-dns) 中更加详细的描述了解析 DNS 头部指南,使用 Nom 解析位级别的 1 个标志位和 4 个位数值。

  2. 使用 println! 将响应发送给用户

实现的怎么样了?

它用了大概 800 行代码,一个周末就能完成。规范中只有一小部分还没实现:消息的压缩(MC)。不幸的是,我又要花一个周末去完成 —— 详见下方描述。

我把它命名为 Dingo,因为它听起来像 dig,它让我想起了澳大利亚,我的家。无论如何,它可以正常工作!

你可以安装它,也可以在 github 查看源代码。

我学到了什么? 阅读 RFC 文档

我认为很多程序员会被 rfc 吓到。至少我有这种感觉,因为我也被吓到过。可能我的其他同行是喜欢 rfc 的怪物吧,他们在阅读存文本的 ASCII 时会感到兴奋不已。。。但他们从未提过。

RFC 1035 定义了 DNS 消息协议,所以我必须非常仔细地阅读它。这是我第一次真正地从上到下阅读 RFC,文档详细的程度让我感到吃惊。我不断地参考它,并将 RFC 中的关键字和一些关键句子粘贴到源代码注释中,以帮助理解代码的实现原因。也有可能是 RFC 1035 是唯一容易理解的文档,其他都是难理解的。无论如何,我喜欢它。

(作为一个历史留下来的文档,自从 1980 年以来,它发生了很大的变化,了解过去的程序员是如何设计互联网是非常有趣的)

Sockets

一直以来,我对 socket 不是很喜欢。大学的时候,我试着阅读 Beej 的 socket 编程指南,但当时没有合适的操作系统、网络和 c 技能来完成学习。我了解 TCP 和 UDP,但对统一他们的底层抽象却一无所知。

这个项目使我第一次不得不打开一个 UDP socket —— 在常规编程中,我都是依赖于一些网络库来处理底层细节。所以,我阅读了 Rust 的 UDP socket 文档,它写的很清楚。UdpSocket 上的很多方法直接对应于 Linux 系统调用。当我再回过头来重新阅读 Beej 的 socket 编程指南时,发现很容易懂了。所有的系统调用都比较熟悉 —— 它们只是 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 ID
socket(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 socket
setsockopt(0x3, 0xFFFF, 0x1022)  = 0 0 # Set the options on the UDP socket
bind(0x3, 0x7FF7BE87346C, 0x10)  = 0 0 # Bind the UDP socket to a local address
setsockopt(0x3, 0xFFFF, 0x1006)  = 0 0 # Set more options, dunno why it needs more...
connect(0x3, 0x7FF7BE873584, 0x10)  = 0 0 # Connect to the remote DNS resolver
sendto(0x3, 0x7FAE060041F0, 0x21)  = 33 0 # Send the request to the remote DNS resolver
recvfrom(0x3, 0x7FAE060043F0, 0x200)  = 79 0 # Get the response from the remote DNS resolver
close_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

我真的很喜欢 Bitvec。它结合了 Vec 类型的可用性和可读性,并具备“位”的可操作性。而这正是 Rust 毫不妥协的“人体工程学、速度和正确性”的体现。

标准库公开了 BitArray,BitVec 和 BitSlice 类型。它们的工作原来基本相同,但我发现了两个小问题,它们的工作方式不同。这些很容易通过单元测试发现,所以我想这是一个不错的学习方式。作者希望在 Rust 发布 const generics后,会发布 Bitvec 2.0,使这些类型都能以类似的方式使用。

Dig 的奇怪输出

我之前提到过,我讨厌使用 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: 77

但在实现了一个 DNS 客户端之后,我根进一步了解了这些相关知识。例如,“IN”并不是英文单词“in”的意思,它是“Internet”的缩写,因为从技术上讲,DNS 支持许多名称空间(只是我们基本上只使用 dig 来进行 Internet DNS 查询)。

现在看 dig 输出有点酷,因为它提醒我学到了多少东西。是的,我还了解到,dig 查询很容易就能得到你想要的信息:

$ dig +short adamchalmers.com
104.19.238.120
104.19.237.120

。。。如果我早在一月份就知道这些,我可能永远不会开始这个项目 

我仍然喜欢 enum

枚举(Enum)是表示域逻辑(domain logic)的一种很好的方式。函数式程序员谈论联合类型已经有几十年了,我很高兴它们终于出现在其他语言中。在 Rust/Swift/Haskell 中使用枚举之后,很难对 Go 中的域逻辑进行建模。

消息压缩(MC)

MC 是一种整洁的功能,DNS 服务器用它减小响应消息的大小。MC 不会把相同的主机名放在同一个响应中,而是用指向先前引用的主机名的指针替换主机名。RFC 参考 explains it really well。MC 将 DNS 响应消息放入单个 UDP 数据包中,这一点很重要,由于 UDP 是不可靠的,并且不关心被截断问题。MC 只查看前面已解析的字节,而 Nom 只能查看剩余的未解析字节。我尝试了好几次以一种更好的、惯用的方式支持 ([code][msgcmprcode]),所以我又花了一个周末的时间完成它。

结语

这是我做过的最喜欢的项目之一。我今年的职业目标是学习更多关于 Linux 和网络的相关知识。编写 dingo 让我了解到互联网的基本组成,以及如何处理操作系统的调用。如果你页正在学习一些底层编程的知识,那么实现一个 DNS 客户端是一个学习挑战。它能让你学到很多东西比如按位运算、解析、UDP 网络、IP 地址和 DNS 主机名等。事实上,在我写了 dingo 后,我的朋友 Jesse Li 写了一个 Python 版本的 DNS 客户端。显然,编写一个 DNS 客户端是你必须要掌握的热门新趋势了。如果你也有类似想法,请在下面评论区讨论 


脚注

1.我试过 deku,它很棒!我喜欢它在结构体字段上生成序列化反序列化的注解,这样不会发生冲突,并且你也不必学习使用两个库(bitvec nom)。

但我希望更有针对性的练习使用 bitvec 并熟悉它的 api —— 毕竟,我做这个项目就是为了学习新东西。

此外,deku 使用 serde 和 syn 来支持序列化反序列化的注解。这些 crate 真的很不错,可以减少很多模板代码。但是这也增加了项目构建时的编译时间。这在工作的时候不是问题,但我的 Rust 项目比较大,并且仓库中也包含了 serde/syn。但是 dingo 没有使用它们,所以添加 deku 会让构建时间从 5s 增加到 15s。我希望以后再使用 deku,它可能更适合其他特定的项目。

2.系统调用是类似于操作系统定义的函数,因此操作系统可以管理 I/O 等有风险的操作。Dtruss 是 Linux 工具 strace 的 MacOS 版本。我从 Julia Evans 的 非常棒的strace comics 那里学到了如何使用 strace,这是我_非常推荐_的,我从中学到了很多。现在,当我编写程序时,我可以窥探编译后的代码在运行我的函数时到底在做什么。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/869934.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号