2023年10月

我一直在慢慢地撰写关于 Git 工作原理的文章。尽管我曾认为自己对 Git 非常了解,但像往常一样,当我尝试解释某事的时候,我又学到一些新东西。

现在回想起来,这些事情都不算太令人吃惊,但我以前并没有清楚地思考过它们。

事实是:

  • “索引”、“暂存区” 和 -cached 是一回事
  • 隐匿文件就是一堆提交
  • 并非所有引用都是分支或标签
  • 合并提交不是空的

下面我们来详细了解这些内容。

“索引”、“暂存区” 和 -cached 是一回事

当你运行 git add file.txt,然后运行 git status,你会看到类似以下的输出:

$ git add content/post/2023-10-20-some-miscellaneous-git-facts.markdown
$ git status
Changes to be committed:
    (use "git restore --staged <file>..." to unstage)
    new file:   content/post/2023-10-20-some-miscellaneous-git-facts.markdown

人们通常称这个过程为“暂存文件”或“将文件添加到暂存区”。

当你使用 git add 命令来暂存文件时,Git 在后台将文件添加到其对象数据库(在 .git/objects 目录下),并更新一个名为 .git/index 的文件以引用新添加的文件。

Git 中的这个“暂存区”事实上有 3 种不同的名称,但它们都指的是同一个东西(即 .git/index 文件):

  • git diff --cached
  • git diff --staged
  • .git/index 文件

我觉得我早该早点认识到这一点,但我之前并没有,所以在这里提醒一下。

隐匿文件就是一堆提交

当我运行 git stash 命令来保存更改时,我一直对这些更改究竟去了哪里感到有些困惑。事实上,当你运行 git stash 命令时,Git 会根据你的更改创建一些提交,并用一个名为 stash 的引用来标记它们(在 .git/refs/stash 目录下)。

让我们将此博客文章隐匿起来,然后查看 stash 引用的日志:

$ git log stash --oneline
6cb983fe (refs/stash) WIP on main: c6ee55ed wip
2ff2c273 index on main: c6ee55ed wip
... some more stuff

现在我们可以查看提交 2ff2c273 以查看其包含的内容:

$ git show 2ff2c273  --stat
commit 2ff2c273357c94a0087104f776a8dd28ee467769
Author: Julia Evans <[email protected]>
Date:   Fri Oct 20 14:49:20 2023 -0400

    index on main: c6ee55ed wip

    content/post/2023-10-20-some-miscellaneous-git-facts.markdown | 40 ++++++++++++++++++++++++++++++++++++++++

毫不意外,它包含了这篇博客文章。这很合理!

实际上,git stash 会创建两个独立的提交:一个是索引提交,另一个是你尚未暂存的改动提交。这让我感到很振奋,因为我一直在开发一款工具,用于快照和恢复 Git 仓库的状态(也许永远不会发布),而我提出的设计与 Git 的隐匿实现非常相似,所以我对自己的选择感到满意。

显然 stash 中的旧提交存储在 reflog 中。

并非所有引用都是分支或标签

Git 文档中经常泛泛地提到 “引用”,这使得我有时觉得很困惑。就个人而言,我在 Git 中处理 “引用” 的 99% 时间是指分支或 HEAD,而剩下的 1% 时间是指标签。事实上,我以前完全不知道任何不是分支、标签或 HEAD 的引用示例。

但现在我知道了一个例子—— stash 是一种引用,而它既不是分支也不是标签!所以这太酷啦!

以下是我博客的 Git 仓库中的所有引用(除了 HEAD):

$ find .git/refs -type f
.git/refs/heads/main
.git/refs/remotes/origin/HEAD
.git/refs/remotes/origin/main
.git/refs/stash

人们在本帖回复中提到的其他一些参考资料:

  • refs/notes/*,来自 git notes
  • refs/pull/123/headrefs/pull/123/head` 用于 GitHub 拉取请求(可通过 git fetch origin refs/pull/123/merge 获取)
  • refs/bisect/*,来自 git bisect

合并提交不是空的

这是一个示例 Git 仓库,其中我创建了两个分支 xy,每个分支都有一个文件(x.txty.txt),然后将它们合并。让我们看看合并提交。

$ git log --oneline
96a8afb (HEAD -> y) Merge branch 'x' into y
0931e45 y
1d8bd2d (x) x

如果我运行 git show 96a8afb,合并提交看起来是“空的”:没有差异!

git show 96a8afb
commit 96a8afbf776c2cebccf8ec0dba7c6c765ea5d987 (HEAD -> y)
Merge: 0931e45 1d8bd2d
Author: Julia Evans <[email protected]>
Date:   Fri Oct 20 14:07:00 2023 -0400

    Merge branch 'x' into y

但是,如果我单独比较合并提交与其两个父提交之间的差异,你会发现当然差异:

$ git diff 0931e45 96a8afb   --stat
    x.txt | 1 +
    1 file changed, 1 insertion(+)
$ git diff 1d8bd2d 96a8afb   --stat
    y.txt | 1 +
    1 file changed, 1 insertion(+)

现在回想起来,合并提交并不是实际上“空的”(它们是仓库当前状态的快照,就像任何其他提交一样),这一点似乎很明显,只是我以前从未思考为什么它们看起来为空。

显然,这些合并差异为空的原因是合并差异只显示冲突 —— 如果我创建一个带有合并冲突的仓库(一个分支在同一文件中添加了 x,而另一个分支添加了 y),然后查看我解决冲突的合并提交,它看起来会像这样:

$ git show HEAD
commit 3bfe8311afa4da867426c0bf6343420217486594
Merge: 782b3d5 ac7046d
Author: Julia Evans <[email protected]>
Date:   Fri Oct 20 15:29:06 2023 -0400

    Merge branch 'x' into y

diff --cc file.txt
index 975fbec,587be6b..b680253
--- a/file.txt
+++ b/file.txt
@@@ -1,1 -1,1 +1,1 @@@
- y
    -x
++z

这似乎是在告诉我,一个分支添加了 x,另一个分支添加了 y,合并提交通过将 z 替代冲突解决了它。但在前面的示例中,没有冲突,所以 Git 并未显示任何差异。

(感谢 Jordi 告诉我合并差异的工作原理)

先这样吧

些写到这里吧,也许我将在学到更多 Git 知识时撰写另一篇关于 Git 的知识的博客文章。

(题图:MJ/03bfecc3-944e-47a0-a4fd-575293d2ba92)


via: https://jvns.ca/blog/2023/10/20/some-miscellaneous-git-facts/

作者:Julia Evans 选题:lujun9972 译者:KaguyaQiang 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

通过这个全新的应用可以实现所有蓝牙功能!

一款适用于 Linux 的新应用已经出现,它可能是满足你所有蓝牙需求的一站式应用。

这款名为 “Overskride” 的开源应用首次发布。尽管它还处于开发阶段,但已经提供了一些不错的功能。

请允许我带你看一下。

Overskride:可以期待什么?

Overskride 将会吸引 Rust 爱好者,因为它主要是用 Rust 语言编写的,带有 GTK4/libadwaita 风格

根据开发人员的说法,它是一个简单的蓝牙和 Obex 客户端 (未来计划),无论使用什么桌面环境或窗口管理器都可以工作。

一些主要功能包括:

  • 信任/阻止设备。
  • 能够发送/接收文件。
  • 设置连接超时时间。
  • 支持配置适配器。

查看上面的截图,你可以看到自定义蓝牙设备和连接的所有基本选项,包括适配器名称。

当然,考虑到这是该应用的第一次发布,人们不应该抱有太高的期望。因此,还有改进的空间。

以下是 Overskride 的一些预览,以查看它提供的功能。

我在 Ubuntu 22.04 LTS 和 GNOME 42.9 上使用提供的 Flatpak 包进行安装。在此安装上运行似乎没有任何问题。

Overskride 能够检测到我的智能手机,并提供多种配置选项。

你可以将设备添加到受信任列表或阻止列表、重命名并发送文件。

我尝试了文件传输功能,但在此之前,我必须使用 Flatseal 允许访问用户文件,以便它可以读取我系统上的文件。

我在手机上接受文件传输后,传输开始。速度还可以,文件确实完整地到达那里,没有任何问题。

我必须说,在其首次发布时,开发人员为我们提供了一个有用的实用程序。我很高兴看到其未来版本将提供什么样的改进。

一位 Reddit 用户 询问 是否有任何计划支持显示无线耳机的电池百分比。对此,开发人员提到这样做很棘手,因为 每个设备都遵循不同的规范,这使得这一目标更难实现。

? 获得 Overskride

目前,Overskride 只能通过 GitHub 仓库 以 Flatpak 软件包的形式提供。或者,你也可以从源代码开始编译。

Overskride (GitHub)

我希望开发者在发布稳定版本后将其发布在 Flathub 上,以便用户可以使用。


via: https://news.itsfoss.com/bluetooth-app-linux-overskride/

作者:Sourav Rudra 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

艺术家使用图像投毒工具反击生成式人工智能

一种名为 “Nightshade” 的新工具可以让艺术家们在将作品上传到网络之前,对作品中的像素点进行不可见的修改,这样一来,如果作品被抓取到人工智能训练集中,就会导致生成的模型以混乱和不可预测的方式崩溃,使它们的一些输出结果变得毫无用处,比如狗变成猫,汽车变成牛等等。开发此工具的团队还开发了一个名为 “Glaze” 的工具,允许艺术家 “屏蔽” 自己的个人风格,过微妙的方式改变图像的像素,这种改变人眼看不见,但却能操纵机器学习模型,将图像解释为与实际显示不同的东西。

消息来源:Technology Review
老王点评:我欣赏这样的举动,但是我认为这样的工具并不能根除问题,就像那些用来判断是否是 AI 生成的文字的工具一样,终究会被 AI 打败。

科学家们可以放心使用 Excel 了

2020 年,科学家们决定重新设计他们用来表示基因的字母数字符号,以避免在输入到 Excel 中时自动转换。Excel 的自动转换功能旨在让某些类型的常用输入数据(例如数字和日期)的输入变得更简单、更快捷。但对于使用快速速记来使事物清晰易读的科学家来说,这可能会毁掉已发表的、经过同行评审的数据,比如将基因名称解释为日期,并(毫无)帮助地自动重新排版。上周,Excel 团队发布消息称,他们修复了这一问题,添加了一个启用默认自动转换功能的复选框。

消息来源:The Verge
老王点评:不知道有没有做科研的同学被微软 Excel 的自动转换坑过。

英伟达和 AMD 计划推出基于 Arm 的 PC 芯片

PC 行业正面临来自苹果公司愈来愈大的竞争压力。自苹果 Mac 电脑采用基于 Arm 的芯片以来,它的市场份额三年内几乎翻了一番。苹果的成功吸引了微软的注意,微软希望 Windows 平台也有类似的高性能芯片。分析家称,“微软从上世纪 90 年代学到,不能再依赖单一供应商。如果 Arm 真的在 PC 领域起飞,他们绝不会让高通成为唯一的供应商。”高通早在 2016 年就开始为笔记本电脑提供 Arm 芯片。英伟达和 AMD 预计最早将在 2025 年销售运行 Windows 的 Arm 芯片。

消息来源:路透社
老王点评:看来 Arm 能在 PC 市场获得一席之地。不过我还是更看好 RISC-V 的未来。

成都,别称“蓉城”,是我国西南的一颗璀璨的明珠,热辣的四川火锅,憨态可掬的大熊猫,都是人们对于成都的理解和认识。而我们的 Linux 爱好者线下沙龙,也在这个冬天,来到了美丽蓉城,与蓉城的开发者们线下相见。

2023 年 10 月 29 日下午,我们将在成都市菁蓉汇举行 LLUG 2023 · 成都场,欢迎广大的 Linux 爱好者来到现场,与我们一同交流技术,分享自己工作过程中的所思所想。同期还在举行中国开源年会 COSCON,欢迎来参与活动的同时,参加 COSCON,了解中国开源项目,助力中国开源产业

本次活动依然由 Linux 中国和 龙蜥社区(OpenAnolis)联合主办,异步图书、COSCON 中国开源年会提供了支持。

龙蜥社区(OpenAnolis)是国内的顶尖 Linux 发行版社区,我们希望在普及 Linux 知识的同时,也能让中国的 Linux 发行版,为更多人知晓,推动国产发行版的发展和进步。
异步社区(www.epubit.com) 是由人民邮电出版社创办的IT专业图书社区。异步社区于2015年8月上线运营,依托于人民邮电出版社20余年的IT专业优质出版资源和编辑策划团队,致力于优质学习内容的出版和分享,为读者提供优质学习内容,为作译者提供优质出版服务,实现作者与读者在线交流互动,实现传统出版与数字出版的融合发展。
中国开源年会(COSCON)是由开源社主办的,中国大陆开源界最高规格的年度盛会。大家可以访问 https://kaiyuanshe.cn/activity/coscon-2023 了解活动详情。

活动议程

时间议题分享简介分享者
14:00~14:20签到
14:20~14:30LLUG 活动介绍白宦成
14:30~15:00技术之旅:从结识Linux到创业——我的Linux历程作为一个创业公司的 CEO,我是如何从 2010 年开始了解 Linux,又一步步从 嵌入式开发,走上创业的道路孙康,
成都不周山文化发展有限公司 CEO。
15:00~15:30如何高效参与开源作为新人,如何学习上手使用开源,参与开源贡献wxy
Linux 中国创始人
15:30~16:00大模型开发经验分享
作为一个大模型开发者,分享一些自己在使用 LLMs 开发应用的经验,帮助大家上手使用 LLMs。开发者也要使用 LLMs 优化工作流!白宦成
Linux 中国技术组组长
16:30~17:00神秘议题
17:00~17:30闪电演讲(短分享)

《技术之旅:从结识 Linux 到创业——我的 Linux 历程》

孙康,成都不周山文化发展有限公司 CEO

作为一个 Linuxer, 我是如何与 Linux 结缘,又是如何一步步从嵌入式开发,深入 Linux 生态,并最终走到创业路的故事。

《如何高效参与开源》

 title=

老王(wxy),Linux 中国技术社区创始人

老王的经典分享,聊聊作为一个社区新人,应该如何参与到开源社区当中,并逐步成长为社区的中坚力量的。

《大模型开发经验分享》

白宦成,Linux 中国技术组组长

作为一个大模型开发者,分享一些自己在使用 LLMs 开发应用的经验,帮助大家上手使用 LLMs。开发者也要使用 LLMs 优化工作流!

⚡️闪电演讲

本次线下活动依旧保留闪电演讲环节,作为最受欢迎的线下活动,本次活动依旧继续举办闪电演讲。每位演讲者有 5 分钟时间参与现场活动,可以提前报名,也可即兴上台演讲。时间一满,马上结束~强制大家控制自己的分享时间,用最短的时间,向大家发出你的声音~

上海场闪电演讲照片:

 title=

胡张治分享自己从 GNU/Linux 小白到 ArchLinuxCN 贡献者的旅程

 title=

李伟光现场介绍 neovim 的使用

活动地点及到达信息

四川省成都市武侯区天府五街200号 菁蓉汇 7 栋 5 号会议室

如果你因为有事,没办法来到线下,那也没问题,我们的活动也会在 Linux 中国视频号、Linux 中国 B 站、龙蜥 B 站、龙蜥钉钉群等开启同步直播。

当然,我们更希望你能亲自来到线下,和我们一起聊聊开源,聊聊技术~

点此报名

并在活动前收到我们的提醒~此外,也可以在问卷中反馈你想听的内容,我们将竭尽所能,邀请行业专家,针对大家感兴趣的话题进行分享。

本次活动免费参与,如果你希望参加同期举行的中国开源年会 COSCON 2023,欢迎大家入群后联系小助手,分享活动推文,即可领取赠票。
中国开源年会 COSCON 2023

(题图:MJ/ff7ab7b7-35ac-452b-901f-d8369f89b067)

大家好!前段时间我写了一篇关于“如何用 Go 语言建立一个简易的 DNS 解析器”的帖子。

那篇帖子里我没写有关“如何生成以及解析 DNS 查询请求”的内容,因为我觉得这很无聊,不过一些伙计指出他们不知道如何解析和生成 DNS 查询请求,并且对此很感兴趣。

我开始好奇了——解析 DNS 花多大功夫?事实证明,编写一段 120 行精巧的 Ruby 语言代码组成的程序就可以做到,这并不是很困难。

所以,在这里有一个如何生成 DNS 查询请求,以及如何解析 DNS 响应报文的速成教学!我们会用 Ruby 语言完成这项任务,主要是因为不久以后我将在一场 Ruby 语言大会上发表观点,而这篇博客帖的部分内容是为了那场演讲做准备的。?

(我尽量让不懂 Ruby 的人也能读懂,我只使用了非常基础的 Ruby 语言代码。)

最后,我们就能制作一个非常简易的 Ruby 版本的 dig 工具,能够查找域名,就像这样:

$ ruby dig.rb example.com
example.com    20314    A    93.184.216.34

整个程序大概 120 行左右,所以 并不 算多。(如果你想略过讲解,单纯想去读代码的话,最终程序在这里:dig.rb。)

我们不会去实现之前帖中所说的“一个 DNS 解析器是如何运作的?”,因为我们已经做过了。

那么我们开始吧!

如果你想从头开始弄明白 DNS 查询是如何格式化的,我将尝试解释如何自己弄明白其中的一些东西。大多数情况下的答案是“用 Wireshark 去解包”和“阅读 RFC 1035,即 DNS 的规范”。

生成 DNS 查询请求

步骤一:打开一个 UDP 套接字

我们需要实际发送我们的 DNS 查询,因此我们就需要打开一个 UDP 套接字。我们会将我们的 DNS 查询发送至 8.8.8.8,即谷歌的服务器。

下面是用于建立与 8.8.8.8 的 UDP 连接,端口为 53(DNS 端口)的代码。

require 'socket'
sock = UDPSocket.new

sock.bind('0.0.0.0', 12345)
sock.connect('8.8.8.8', 53)

关于 UDP 的说明

关于 UDP,我不想说太多,但是我要说的是,计算机网络的基础单位是“ 数据包 packet ”(即一串字节),而在这个程序中,我们要做的是计算机网络中最简单的事情:发送 1 个数据包,并接收 1 个数据包作为响应。

所以 UDP 是一个传递数据包的最简单的方法。

它是发送 DNS 查询最常用的方法,不过你还可以用 TCP 或者 DNS-over-HTTPS。

步骤二:从 Wireshark 复制一个 DNS 查询

下一步:假设我们都不知道 DNS 是如何运作的,但我们还是想尽快发送一个能运行的 DNS 查询。获取 DNS 查询并确保 UDP 连接正常工作的最简单方法就是复制一个已经正常工作的 DNS 查询!

所以这就是我们接下来要做的,使用 Wireshark (一个绝赞的数据包分析工具)。

我的操作大致如下:

  1. 打开 Wireshark,点击 “ 捕获 capture ” 按钮。
  2. 在搜索栏输入 udp.port == 53 作为筛选条件,然后按下回车。
  3. 在我的终端运行 ping example.com(用来生成一个 DNS 查询)。
  4. 点击 DNS 查询(显示 “Standard query A example.com”)。 (“A”:查询类型;“example.com”:域名;“Standard query”:查询类型描述)
  5. 右键点击位于左下角面板上的 “ 域名系统(查询) Domain Name System (query) ”。
  6. 点击 “ 复制 Copy ” ——> “ 作为十六进制流 as a hex stream ”。
  7. 现在 b96201000001000000000000076578616d706c6503636f6d0000010001 就放到了我的剪贴板上,之后会用在我的 Ruby 程序里。好欸!

步骤三:解析 16 进制数据流并发送 DNS 查询

现在我们能够发送我们的 DNS 查询到 8.8.8.8 了!就像这样,我们只需要再加 5 行代码:

hex_string = "b96201000001000000000000076578616d706c6503636f6d0000010001"
bytes = [hex_string].pack('H*')
sock.send(bytes, 0)

# get the reply
reply, _ = sock.recvfrom(1024)
puts reply.unpack('H*')

[hex_string].pack('H*') 意思就是将我们的 16 位字符串转译成一个字节串。此时我们不知道这组数据到底是什么意思,但是很快我们就会知道了。

我们还可以借此机会运用 tcpdump ,确认程序是否正常进行以及发送有效数据。我是这么做的:

  1. 在一个终端选项卡下执行 sudo tcpdump -ni any port 53 and host 8.8.8.8 命令
  2. 在另一个不同的终端指标卡下,运行 这个程序ruby dns-1.rb

以下是输出结果:

$ sudo tcpdump -ni any port 53 and host 8.8.8.8
08:50:28.287440 IP 192.168.1.174.12345 > 8.8.8.8.53: 47458+ A? example.com. (29)
08:50:28.312043 IP 8.8.8.8.53 > 192.168.1.174.12345: 47458 1/0/0 A 93.184.216.34 (45)

非常棒 —— 我们可以看到 DNS 请求(”这个 example.com 的 IP 地址在哪里?“)以及响应(“在93.184.216.34”)。所以一切运行正常。现在只需要(你懂的)—— 搞清我们是如何生成并解析这组数据的。

步骤四:学一点点 DNS 查询的格式

现在我们有一个关于 example.com 的 DNS 查询,让我们了解它的含义。

下方是我们的查询(16 位进制格式):

b96201000001000000000000076578616d706c6503636f6d0000010001

如果你在 Wireshark 上搜索,你就能看见这个查询它由两部分组成:

  • 请求头b96201000001000000000000
  • 语句本身076578616d706c6503636f6d0000010001

步骤五:制作请求头

我们这一步的目标就是制作字节串 b96201000001000000000000(借助一个 Ruby 函数,而不是把它硬编码出来)。

(LCTT 译注: 硬编码 hardcode 指在软件实现上,将输出或输入的相关参数(例如:路径、输出的形式或格式)直接以常量的方式撰写在源代码中,而非在运行期间由外界指定的设置、资源、数据或格式做出适当回应。)

那么:请求头是 12 个字节。那些个 12 字节到底意味着什么呢?如果你在 Wireshark 里看看(亦或者阅读 RFC-1035),你就能理解:它是由 6 个 2 字节大小的数字串联在一起组成的。

这六个数字分别对应查询 ID、标志,以及数据包内的问题计数、回答资源记录数、权威名称服务器记录数、附加资源记录数。

我们还不需要在意这些都是些什么东西 —— 我们只需要把这六个数字输进去就行。

但所幸我们知道该输哪六位数,因为我们就是为了直观地生成字符串 b96201000001000000000000

所以这里有一个制作请求头的函数(注意:这里没有 return,因为在 Ruby 语言里,如果处在函数最后一行是不需要写 return 语句的):

def make_question_header(query_id)
  # id, flags, num questions, num answers, num auth, num additional
  [query_id, 0x0100, 0x0001, 0x0000, 0x0000, 0x0000].pack('nnnnnn')
end

上面内容非常的短,主要因为除了查询 ID ,其余所有内容都由我们硬编码写了出来。

什么是 nnnnnn?

可能能想知道 .pack('nnnnnn') 中的 nnnnnn 是个什么意思。那是一个向 .pack() 函数解释如何将那个 6 个数字组成的数据转换成一个字节串的一个格式字符串。

.pack 的文档在 这里,其中描述了 n 的含义其实是“将其表示为” 16 位无符号、网络(大端序)字节序’”。

(LCTT 译注: 大端序 Big-endian :指将高位字节存储在低地址,低位字节存储在高地址的方式。)

16 个位等同于 2 字节,同时我们需要用网络字节序,因为这属于计算机网络范畴。我不会再去解释什么是字节序了(尽管我确实有 一幅自制漫画尝试去描述它)。

测试请求头代码

让我们快速检测一下我们的 make_question_header 函数运行情况。

puts make_question_header(0xb962) == ["b96201000001000000000000"].pack("H*")

这里运行后输出 true 的话,我们就成功了。

好了我们接着继续。

步骤六:为域名进行编码

下一步我们需要生成 问题本身(“example.com 的 IP 是什么?”)。这里有三个部分:

  • 域名(比如说 example.com
  • 查询类型(比如说 A 代表 “IPv4 Address”)
  • 查询类(总是一样的,1 代表 INternet)

最麻烦的就是域名,让我们写个函数对付这个。

example.com 以 16 进制被编码进一个 DNS 查询中,如 076578616d706c6503636f6d00。这有什么含义吗?

如果我们把这些字节以 ASCII 值翻译出来,结果会是这样:

076578616d706c6503636f6d00
 7 e x a m p l e 3 c o m 0

因此,每个段(如 example)的前面都会显示它的长度(7)。

下面是有关将 example.com 翻译成 7 e x a m p l e 3 c o m 0 的 Ruby 代码:

def encode_domain_name(domain)
  domain
    .split(".")
    .map { |x| x.length.chr + x }
    .join + "\0"
end

除此之外,,要完成问题部分的生成,我们只需要在域名结尾追加上(查询)的类型和类。

步骤七:编写 make\_dns\_query

下面是制作一个 DNS 查询的最终函数:

def make_dns_query(domain, type)
  query_id = rand(65535)
  header = make_question_header(query_id)
  question =  encode_domain_name(domain) + [type, 1].pack('nn')
  header + question
end

这是目前我们写的所有代码 dns-2.rb —— 目前仅 29 行。

接下来是解析的阶段

现在我尝试去解析一个 DNS 查询,我们到了硬核的部分:解析。同样的,我们会将其分成不同部分:

  • 解析一个 DNS 的请求头
  • 解析一个 DNS 的名称
  • 解析一个 DNS 的记录

这几个部分中最难的(可能跟你想的不一样)就是:“解析一个 DNS 的名称”。

步骤八:解析 DNS 的请求头

让我们先从最简单的部分开始:DNS 的请求头。我们之前已经讲过关于它那六个数字是如何串联在一起的了。

那么我们现在要做的就是:

  • 读其首部 12 个字节
  • 将其转换成一个由 6 个数字组成的数组
  • 为方便起见,将这些数字放入一个类中

以下是具体进行工作的 Ruby 代码:

class DNSHeader
  attr_reader :id, :flags, :num_questions, :num_answers, :num_auth, :num_additional
  def initialize(buf)
    hdr = buf.read(12)
    @id, @flags, @num_questions, @num_answers, @num_auth, @num_additional = hdr.unpack('nnnnnn')
  end
end

注: attr_reader 是 Ruby 的一种说法,意思是“使这些实例变量可以作为方法使用”。所以我们可以调用 header.flags 来查看@flags变量。

我们也可以借助 DNSheader(buf) 调用这个,也不差。

让我们往最难的那一步挪挪:解析一个域名。

步骤九:解析一个域名

首先,让我们写其中的一部分:

def read_domain_name_wrong(buf)
  domain = []
  loop do
    len = buf.read(1).unpack('C')[0]
    break if len == 0
    domain << buf.read(len)
  end
  domain.join('.')
end

这里会反复读取一个字节的数据,然后将该长度读入字符串,直到读取的长度为 0。

这里运行正常的话,我们在我们的 DNS 响应头第一次看见了域名(example.com)。

关于域名方面的麻烦:压缩!

但当 example.com 第二次出现的时候,我们遇到了麻烦 —— 在 Wireshark 中,它报告上显示输出的域的值为含糊不清的 2 个字节的 c00c

这种情况就是所谓的 DNS 域名压缩,如果我们想解析任何 DNS 响应我们就要先把这个实现完。

幸运的是,这没那么难。这里 c00c 的含义就是:

  • 前两个比特(0b11.....)意思是“前面有 DNS 域名压缩!”
  • 而余下的 14 比特是一个整数。这种情况下这个整数是 120x0c),意思是“返回至数据包中的第 12 个字节处,使用在那里找的域名”

如果你想阅读更多有关 DNS 域名压缩之类的内容。我找到了相关更容易让你理解这方面内容的文章: 关于 DNS RFC 的释义

步骤十:实现 DNS 域名压缩

因此,我们需要一个更复杂的 read_domain_name 函数。

如下所示:

domain = []
loop do
  len = buf.read(1).unpack('C')[0]
  break if len == 0
  if len & 0b11000000 == 0b11000000
    # weird case: DNS compression!
    second_byte = buf.read(1).unpack('C')[0]
    offset = ((len & 0x3f) << 8) + second_byte
    old_pos = buf.pos
    buf.pos = offset
    domain << read_domain_name(buf)
    buf.pos = old_pos
    break
  else
    # normal case
    domain << buf.read(len)
  end
end
domain.join('.')

这里具体是:

  • 如果前两个位为 0b11,那么我们就需要做 DNS 域名压缩。那么:

    • 读取第二个字节并用一点儿运算将其转化为偏移量。
    • 在缓冲区保存当前位置。
    • 在我们计算偏移量的位置上读取域名
    • 在缓冲区存储我们的位置。

可能看起来很乱,但是这是解析 DNS 响应的部分中最难的一处了,我们快搞定了!

一个关于 DNS 压缩的漏洞

有些人可能会说,有恶意行为者可以借助这个代码,通过一个带 DNS 压缩条目的 DNS 响应指向这个响应本身,这样 read_domain_name 就会陷入无限循环。我才不会改进它(这个代码已经够复杂了好吗!)但一个真正的 DNS 解析器确实会更巧妙地处理它。比如,这里有个 能够避免在 miekg/dns 中陷入无限循环的代码

如果这是一个真正的 DNS 解析器,可能还有其他一些边缘情况会造成问题。

步骤十一:解析一个 DNS 查询

你可能在想:“为什么我们需要解析一个 DNS 查询?这是一个响应啊!”

但每一个 DNS 响应包含它自己的原始查询,所以我们有必要去解析它。

这是解析 DNS 查询的代码:

class DNSQuery
  attr_reader :domain, :type, :cls
  def initialize(buf)
    @domain = read_domain_name(buf)
    @type, @cls = buf.read(4).unpack('nn')
  end
end

内容不是太多:类型和类各占 2 个字节。

步骤十二:解析一个 DNS 记录

最让人兴奋的部分 —— DNS 记录是我们的查询数据存放的地方!即这个 “rdata 区域”(“记录数据字段”)就是我们会在 DNS 查询对应的响应中获得的 IP 地址所驻留的地方。

代码如下:

class DNSRecord
  attr_reader :name, :type, :class, :ttl, :rdlength, :rdata
  def initialize(buf)
    @name = read_domain_name(buf)
    @type, @class, @ttl, @rdlength = buf.read(10).unpack('nnNn')
    @rdata = buf.read(@rdlength)
  end

我们还需要让这个 rdata 区域更加可读。记录数据字段的实际用途取决于记录类型 —— 比如一个“A” 记录就是一个四个字节的 IP 地址,而一个 “CNAME” 记录则是一个域名。

所以下面的代码可以让请求数据更可读:

def read_rdata(buf, length)
  @type_name = TYPES[@type] || @type
  if @type_name == "CNAME" or @type_name == "NS"
    read_domain_name(buf)
  elsif @type_name == "A"
    buf.read(length).unpack('C*').join('.')
  else
    buf.read(length)
  end
end

这个函数使用了 TYPES 这个哈希表将一个记录类型映射为一个更可读的名称:

TYPES = {
  1 => "A",
  2 => "NS",
  5 => "CNAME",
  # there are a lot more but we don't need them for this example
}

read.rdata 中最有趣的一部分可能就是这一行 buf.read(length).unpack('C*').join('.') —— 像是在说:“嘿!一个 IP 地址有 4 个字节,就将它转换成一组四个数字组成的数组,然后数字互相之间用 ‘.’ 联个谊吧。”

步骤十三:解析 DNS 响应的收尾工作

现在我们正式准备好解析 DNS 响应了!

工作代码如下所示:

class DNSResponse
  attr_reader :header, :queries, :answers, :authorities, :additionals
  def initialize(bytes)
    buf = StringIO.new(bytes)
    @header = DNSHeader.new(buf)
    @queries = ([email protected]_questions).map { DNSQuery.new(buf) }
    @answers = ([email protected]_answers).map { DNSRecord.new(buf) }
    @authorities = ([email protected]_auth).map { DNSRecord.new(buf) }
    @additionals = ([email protected]_additional).map { DNSRecord.new(buf) }
  end
end

这里大部分内容就是在调用之前我们写过的其他函数来协助解析 DNS 响应。

如果 @header.num_answers 的值为 2,代码会使用了 ([email protected]_answers).map 这个巧妙的结构创建一个包含两个 DNS 记录的数组。(这可能有点像 Ruby 魔法,但我就是觉得有趣,但愿不会影响可读性。)

我们可以把这段代码整合进我们的主函数中,就像这样:

sock.send(make_dns_query("example.com", 1), 0) # 1 is "A", for IP address
reply, _ = sock.recvfrom(1024)
response = DNSResponse.new(reply) # parse the response!!!
puts response.answers[0]

尽管输出结果看起来有点辣眼睛(类似于 #<DNSRecord:0x00000001368e3118>),所以我们需要编写一些好看的输出代码,提升它的可读性。

步骤十四:对于我们输出的 DNS 记录进行美化

我们需要向 DNS 记录增加一个 .to_s 字段,从而让它有一个更良好的字符串展示方式。而者只是做为一行方法的代码在 DNSRecord 中存在。

def to_s
  "#{@name}\t\t#{@ttl}\t#{@type_name}\t#{@parsed_rdata}"
end

你可能也注意到了我忽略了 DNS 记录中的 class 区域。那是因为它总是相同的(IN 表示 “internet”),所以我觉得它是个多余的。虽然很多 DNS 工具(像真正的 dig)会输出 class

大功告成!

这是我们最终的主函数:

def main
  # connect to google dns
  sock = UDPSocket.new
  sock.bind('0.0.0.0', 12345)
  sock.connect('8.8.8.8', 53)

  # send query
  domain = ARGV[0]
  sock.send(make_dns_query(domain, 1), 0)

  # receive & parse response
  reply, _ = sock.recvfrom(1024)
  response = DNSResponse.new(reply)
  response.answers.each do |record|
    puts record
  end

我不觉得我们还能再补充什么 —— 我们建立连接、发送一个查询、输出每一个回答,然后退出。完事儿!

$ ruby dig.rb example.com
example.com   18608   A   93.184.216.34

你可以在这里查看最终程序:dig.rb。可以根据你的喜好给它增加更多特性,就比如说:

  • 为其他查询类型添加美化输出。
  • 输出 DNS 响应时增加“授权”和“可追加”的选项
  • 重试查询
  • 确保我们看到的 DNS 响应匹配我们的查询(ID 信息必须是对的上的!)

另外如果我在这篇文章中出现了什么错误,就 在推特和我聊聊吧。(我写的比较赶所以可能还是会有些错误)

(题图:MJ/449d049d-6bdd-448b-a61d-17138f8551bc)


via: https://jvns.ca/blog/2022/11/06/making-a-dns-query-in-ruby-from-scratch/

作者:Julia Evans 选题:lujun9972 译者:Drwhooooo 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

美国 NASA 向 1977 年发射的两个旅行者探测器发送补丁

自 1977 年发射以来,美国国家航空航天局(NASA)的旅行者 1 号和 2 号探测器已经飞行了超过 120 亿英里,并且仍在从太阳系外发回数据。然而由于一些问题,NASA 需要跨越整个太阳系向它们发送补丁。其中一项工作是解决飞船上一些推进器的窄管内似乎积聚了燃料残渣的问题。推进器用于使每个航天器的天线对准地球。这次任务已经开始让两个航天器在每个方向上稍微旋转远一点,差不多 1 度。预计这个预防措施,可也让飞船推进器推进剂入口管至少五年内不会完全堵塞。另外一个补丁是为了解决一个姿态衔接和控制系统的错误。

消息来源:NASA
老王点评:就说为了维护一个几十年前的计算机设备得有多困难,而且可能没有试错的机会。

大多数人在车内还是只听收音机

现在是 2023 年,你可能会认为 AM/FM 收音机即将被淘汰,但新的数据显示,对很多人来说并非如此。市场调研发现,没有车载娱乐系统的人有 67% 的时间在收听 AM/FM 广播,其余时间在收听流媒体服务或播客等。但是,在有车载娱乐系统的人中,46% 的人仍在收听 AM/FM 广播。

消息来源:9to5mac
老王点评:这是国外的调查数据,国内如何,大家在开车时听什么?

Mozilla 首次发布年度数字隐私“惊悚指数”:今年“非常可怕”

Mozilla 首次发布 “ 年度消费者惊悚指数 Annual Consumer Creep-o-Meter ,该指数试图为数字隐私设定基准并识别趋势。2023 年,我们的数字隐私状况是:非常可怕。 2017 年以来,2023 年,产品和公司正在收集比以往更多的个人数据,用于有针对性的广告和人工智能训练。越来越多的产品无法离线使用,在很多情况下,这已不再是一种选择。

消息来源:Mozilla
老王点评:迎接新的时代吧,无隐私时代 —— 我已经对保持数字隐私放弃了期望。