标签 DNS 下的文章

大家好!前段时间我写了一篇关于“如何用 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中国 荣誉推出

我经常写关于我发现难以学习的技术的文章。不久前,我的朋友 Sumana 向我提出了一个有趣的问题 - 为什么这些东西学起来那么难?为什么它们看起来如此神秘?

以 DNS 为例。我们从 80 年代 开始使用 DNS(已经超过 35 年了!)。它在互联网上的每个网站中都使用。而且它相当稳定 - 在很多方面,它的工作方式与 30 年前完全相同。

但是我花了好几年的时间才弄清楚如何自信地调试 DNS 问题,我也见过很多其他程序员在调试 DNS 问题上苦苦挣扎。那么到底发生了什么呢?

以下是关于为什么学习排除 DNS 问题很困难的几点思考。

(我不会在这篇文章中详细解释 DNS,更多关于 DNS 如何工作的信息,请参阅 《用一个周末实现一个 DNS》 或 我的 DNS 方面的博文

并不是因为 DNS 非常难

当我最终学会如何排除 DNS 问题时,我的反应是“什么,就这样吗???这并不难!”我感觉有点被骗了!我可以在 几个小时 内向你解释关于 DNS 令我感到困惑的一切事情。

那么 - 如果 DNS 并不是那么复杂,为什么我花了这么多年的时间才弄清楚如何排除相当基本的 DNS 问题(比如“即使我已经正确设置了,我的域名仍无法解析”或者“dig 命令和我的浏览器的 DNS 结果不一致,为什么?”)?

而且,在发现 DNS 学习困难方面,我并不孤单!我与许多经验丰富的程序员朋友讨论过多年来的 DNS 问题,其中很多人要么:

  • 不敢轻易对其网站进行简单的 DNS 更改
  • 或对 DNS 工作原理的基本事实感到困惑(比如记录是 拉取的而非推送的
  • 或对 DNS 基础知识了解得很好,但却和我一样存在一些知识盲点(负缓存和 dig 命令及浏览器如何以不同方式进行 DNS 查询的细节)

因此,如果我们都面临着 DNS 的相同困扰,到底发生了什么?为什么对许多人来说学习 DNS 如此困难?

以下是我的一些看法。

很多系统是隐藏的

当你在计算机上发起 DNS 请求时,基本的过程如下:

  1. 你的计算机向一个名为“解析器”的服务器发起请求。
  2. 解析器检查其缓存,并向一些称为“权威名称服务器”的其它服务器发起请求。

以下是你看不到的一些内容:

  • 解析器的缓存。里面有什么内容?
  • 在你的计算机上进行 DNS 请求的库代码是哪个(是否是 libc 的 getaddrinfo 函数?如果是,它是来自 glibc、musl 还是苹果?是你的浏览器的 DNS 代码吗?还是其他自定义的 DNS 实现?)所有这些选项的行为略有不同,并且有不同的配置、缓存方法、可用功能等等。例如,musl DNS 直到 2023 年初 才支持 TCP。
  • 解析器与权威名称服务器之间的对话。如果你能够神奇地获得一个准确记录你的请求期间向下游查询的每个权威名称服务器以及它们的响应的追踪,我认为很多 DNS 问题将变得非常简单。(比如,如果你能运行 dig +debug google.com 并获得一些额外的调试信息会怎么样?)

如何和隐藏系统打交道

以下是几个处理隐藏系统的方法:

  • 向人们传授隐藏系统的知识会产生重大影响。很长一段时间里,我不知道我的计算机有多个不同的 DNS 库,它们在不同情况下使用,我对此感到困惑了好几年。这是我的重要排错方法。
  • 通过 Mess With DNS,我们尝试了一种“鱼缸”的方法,展示了通常隐藏的系统(与解析器和权威名称服务器的对话)的一些部分。
  • 我觉得将 DNS 扩展以包括一个“调试信息”部分会非常酷。(注:似乎这已经有了!它被称为“扩展 DNS 错误”,即 EDE,各种工具正在逐渐添加对它的支持。)

扩展 DNS 错误看起来不错

扩展 DNS 错误是 DNS 服务器提供额外调试信息的一种新方式。以下是一个示例:

$ dig @8.8.8.8 xjwudh.com
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 39830
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
; EDE: 12 (NSEC Missing): (Invalid denial of existence of xjwudh.com/a)
;; QUESTION SECTION:
;xjwudh.com.            IN    A

;; AUTHORITY SECTION:
com.            900    IN    SOA    a.gtld-servers.net. nstld.verisign-grs.com. 1690634120 1800 900 604800 86400

;; Query time: 92 msec
;; SERVER: 8.8.8.8#53(8.8.8.8) (UDP)
;; WHEN: Sat Jul 29 08:35:45 EDT 2023
;; MSG SIZE  rcvd: 161

这里我请求了一个不存在的域名,并收到了扩展错误信息 EDE: 12 (NSEC Missing): (Invalid denial of existence of xjwudh.com/a)。我不太确定这是什么意思(它与 DNSSEC 有关),但能看到这样额外的调试信息真的很酷。

为了能看到上述内容,我确实需要安装更新版本的 dig

令人困惑的工具

尽管很多 DNS 的细节被隐藏起来,但你可以通过使用 dig 工具来找出发生了什么事情。

例如,你可以使用 dig +norecurse 来确定给定的 DNS 解析器是否在其缓存中具有特定的记录。如果响应没有被缓存,8.8.8.8 看起来会返回 SERVFAIL 响应。

以下是对 google.com 进行该操作的示例:

$ dig +norecurse  @8.8.8.8 google.com
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11653
;; flags: qr ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;google.com.            IN  A

;; ANSWER SECTION:
google.com.     21  IN  A   172.217.4.206

;; Query time: 57 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Fri Jul 28 10:50:45 EDT 2023
;; MSG SIZE  rcvd: 55

这是对 homestarrunner.com 的示例:

$ dig +norecurse  @8.8.8.8 homestarrunner.com
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 55777
;; flags: qr ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;homestarrunner.com.        IN    A

;; Query time: 52 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Fri Jul 28 10:51:01 EDT 2023
;; MSG SIZE  rcvd: 47

在这里,你可以看到我们对于 google.com 得到了一个正常的 NOERROR 响应(8.8.8.8 的缓存中有该记录),但对于 homestarrunner.com 得到了 SERVFAIL 响应(没有缓存)。这并不意味着 homestarrunner.com 没有 DNS 记录(实际上有!),它只是没有被缓存。

但如果你不熟悉这样的输出,它确实很难阅读!以下是我认为其中一些奇怪的地方:

  1. 标题很奇怪(有 ->>HEADER<<-flags:OPT PSEUDOSECTION:QUESTION SECTION:ANSWER SECTION:)。
  2. 空格排版很奇怪(OPT PSEUDOSECTIONQUESTION SECTION 之间为什么没有换行符?)。
  3. MSG SIZE rcvd: 47 很奇怪(MSG SIZE 中是否还有其他字段,而不仅仅是 rcvd?它们是什么?)。
  4. 它说有 1 个记录在 ADDITIONAL 部分,但没有显示它,你必须以某种方式神奇地知道OPT PSEUDOSECTION 记录实际上在 ADDITIONAL 部分。

总的来说,dig 的输出给人的感觉是一个以临时方式编写并随着时间的推移逐渐发展起来的脚本,而不是经过有意设计的东西。

处理令人困惑的工具的一些想法:

  • 解释输出结果。例如,我写了一篇 如何使用 dig 的文章,解释了 dig 的输出结果以及如何配置它以默认给出更简短的输出。
  • 创建新的、更友好的工具。例如,在 DNS 方面,有 dogdoggo我的 DNS 查询工具。我认为这些工具非常酷,但我个人不使用它们,因为有时我想做一些稍微高级一点的操作(比如使用 +norecurse),据我所知,无论是 dog 还是 doggo 都不支持 +norecurse。我更愿意使用一个工具来完成所有任务,所以我坚持使用 dig。要替换 dig,其功能广度是一项庞大的工作。
  • 使 dig 的输出更加友好。如果我在 C 编程方面更好一些,我可能会尝试编写一个 dig 的拉取请求,添加一个 +human 标志以以更结构化和易读的方式格式化长格式的输出,可能类似于以下形式:
$ dig +human +norecurse  @8.8.8.8 google.com 
HEADER:
  opcode: QUERY
  status: NOERROR
  id: 11653
  flags: qr ra
  records: QUESTION: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

QUESTION SECTION:
  google.com.            IN    A

ANSWER SECTION:
  google.com.        21    IN    A    172.217.4.206
  
ADDITIONAL SECTION:
  EDNS: version: 0, flags:; udp: 512

EXTRA INFO:
  Time: Fri Jul 28 10:51:01 EDT 2023
  Elapsed: 52 msec
  Server: 8.8.8.8:53
  Protocol: UDP
  Response size: 47 bytes

这样可以更清晰地呈现 DNS 响应的结构-包括标题、问题、答案和附加部分。

而且它并不是“简化”了什么!它是完全相同的信息,只是以更结构化的方式进行了格式化。我对替代的 DNS 工具最大的不满是它们经常为了清晰起见而删除信息。虽然这些工具肯定有其用武之地,但我想要看到所有的信息!我只是希望它能够以清晰明了的方式呈现。

在过去的 40 年中,我们已经学到了很多关于如何设计更用户友好的命令行工具的知识,我认为将其中一些知识应用到我们那些有些陈旧的工具中将会很棒。

dig +yaml

关于 dig 的一个简单备注:较新版本的 dig 支持 +yaml 输出格式,对我来说更加清晰,但有些冗长(一个相当简单的 DNS 响应都无法在屏幕上完整显示)。

一些奇怪的陷阱

DNS 存在一些相对常见但很难通过自学了解到的奇怪问题。以下是一些例子(有更多可在 导致 DNS 中断的一些方式 中找到):

  • 负缓存:我在 这篇演讲 中提到过,我大约花了 5 年时间才意识到不应该访问没有 DNS 记录的域名,因为该记录的 不存在 信息将被缓存,并且该缓存在几个小时内不会被更新,这真的很烦人。
  • getaddrinfo 实现的差异:直到 2023 年初musl 不支持 TCP DNS。
  • 忽略 TTL 的解析器:如果你在 DNS 记录上设置了 TTL(比如“5 分钟”),一些解析器完全会忽略这些 TTL 设置,并将记录缓存更长时间,比如可能是 24 小时。
  • 如果你错误地配置了 Nginx(像这样),它将永久缓存 DNS 记录。
  • ndots 如何导致 Kubernetes DNS 缓慢。

如何应对奇怪的陷阱

对此,我没有像我希望的那样完美的答案。对奇怪陷阱的了解非常难以获得(再次强调,我花了多年的时间才弄清楚负缓存!),对我而言,人们不得不一次又一次地自己重新发现它们感觉很愚蠢。

以下是一些想法:

  • 当有人在解释一个主题时提到了一些棘手的问题,这是非常有帮助的。例如(离开 DNS 一下),Josh Comeau 的 Flexbox 入门解释了这个 最小尺寸的陷阱,在找到解释之前,我多年来遇到过很多次这个问题。
  • 我希望看到更多的社区整理的常见陷阱。比如说,对于 Bash,shellcheck 是一个非常不错的常见陷阱集合。

关于记录 DNS 陷阱的一个棘手问题是,不同的人会遇到不同的陷阱。如果你只是每三年为个人域名配置一次 DNS,你可能会遇到不同的问题,而那些管理高流量域名的人则可能会遇到其他问题。

还有一些更简单的原因:

不经常接触

很多人非常少接触 DNS。如果你只在每三年才处理一次 DNS,学习起来就会更加困难!

我认为备忘单(比如“这是更改你的名称服务器的步骤”)可以在这方面起到很大的帮助。

难以进行实验

DNS 在进行实验时可能会让人感到害怕,因为你不想搞砸自己的域名。我们建立了 Mess With DNS 来使这个过程变得更容易一些。

目前就这些

我很想听听其他关于什么让 DNS(或你最喜欢的神秘技术)难以学习的想法。

(题图:MJ/96c5d8fb-f4a5-4710-8f91-c71617120675)


via: https://jvns.ca/blog/2023/07/28/why-is-dns-still-hard-to-learn/

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

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

当我第一次知道 DNS 时,我想它应该不会很复杂。不就是一些存储在服务器上的 DNS 记录罢了。有什么大不了的?

但是教科书上只是介绍了 DNS 的原理,并没有告诉你实际使用中 DNS 可能会以多少种方式破坏你的系统。这可不仅仅是缓存问题!

所以我 在 Twitter 上发起了一个提问,征集人们遇到的 DNS 问题,尤其是那些一开始看起来与 DNS 没什么关系的问题。(“总是 DNS 问题”这个梗)

我不打算在这篇文章中讨论如何解决或避免这些问题,但我会放一些讨论这些问题的链接,在那里可以找到解决问题的方法。

问题:网络请求缓慢

如果你的网络比预期的要慢,这是因为某些原因导致 DNS 解析器变慢了。这可能是解析器负载过大或者存在内存泄漏等原因导致的。

我的路由器的 DNS 转发器曾遇到过这个问题,导致我的所有 DNS 请求很慢。我通过重启路由器解决了这个问题。

问题:DNS 超时

一些网友提到由于 DNS 查询超时,他们的网络请求需要耗时 2 秒多甚至 30 秒。这跟“网络请求缓慢”问题类似,但情况要更糟糕,因为 DNS 请求就会消耗掉几秒钟时间。

Sophie Haskins 有一篇关于 Kubernete DNS 超时的博客文章 一次 Kube DNS 踩坑经历

问题:ndots 设置

一些网友提到在 /etc/resolv.conf 中设置 ndots:5 时会出现问题。

下面是从 这篇《Kubernetes 容器荚中 /etc/resolv.conf 里设置 ndots:5 为什么会拖慢你的程序性能》中引用的 /etc/resolv.conf文件。

    nameserver 100.64.0.10
    search namespace.svc.cluster.local svc.cluster.local cluster.local eu-west-1.compute.internal
    options ndots:5

如果你用上面的配置文件,想要查询得域名是 google.com,那么你的程序会调用 getaddrinfo 函数,而它会依次查询以下域名:

  1. google.com.namespace.svc.cluster.local.
  2. google.com.svc.cluster.local.
  3. google.com.cluster.local.
  4. google.com.eu-west-1.compute.internal.
  5. google.com.

简单来说,它会检查 google.com 是不是 search 这一行中的某个子域名。

所以每发起一次 DNS 查询,你都得先等待前 4 次查询失败后才能获取到最终查询结果。

问题:难以判断系统使用的 DNS 解析器

这本身并不是一个问题,但当你遇到 DNS 问题时,一般都会跟 DNS 解析器有关。我没有一种判断 DNS 解析器的万能方法。

下面是我知道的方法:

  • 在 Linux 系统上,最常见的是通过 /etc/resolv.conf 来选择 DNS 解析器。但是也有例外,比如浏览器可能会忽略 /etc/resolv.conf,而是使用 基于 HTTPS 的 DNS DNS-over-HTTPS 服务。
  • 如果你使用的是 UDP DNS,你可以通过 sudo tcpdump port 53 来查看 DNS 请求被发送到了哪里。但如果你使用的是基于 HTTPS 的 DNS 或 基于 TLS 的 DNS DNS over TLS ,这个方法就不行了。

我依稀记得这在 MacOS 系统上会更加令人迷惑,我也不清楚原因。

问题:DNS 服务器返回 NXDOMAIN 而不是 NOERROR

这是我曾经遇到过的一个 Nginx 不能解析域名的问题。

  • 我设置 Nginx 使用一个特定的 DNS 服务器来解析 DNS 查询
  • 当访问这个域名时,Nginx 做了两次查询,第一次是对 A 的,第二次是对 AAAA
  • 对于 A 的查询,DNS 服务器返回 NXDOMAIN
  • Nginx 认为这个域名不存在,然后放弃查询
  • 对于 AAAA 的查询 DNS 服务器返回了成功
  • 但 Nginx 忽略了对 AAAA 返回的查询结果,因为它前面已经放弃查询了

问题出在 DNS 服务器本应该返回 NOERROR 的——那个域名确实存在,只是没有关于 A 的记录罢了。我报告了这个问题,然后他们修复了这个问题。

我自己也写出过这个问题,所以我理解为什么会发生这种情况——很容易想当然地认为“没有要查询的记录,就应该返回 NXDOMAIN 错误码”。

问题:自动生效的 DNS 缓存

如果你在生成一个域名的 DNS 记录之前就访问这个域名,那么这个记录的缺失会被缓存起来。当你第一次遇到这个问题时一定会非常吃惊——我也是去年才知道有这个问题。

缓存的 TTL 就是域名的 起始权限记录 Start of Authority (SOA) 记录的 TTL ——比如对于 jvns.ca ,这个值是一个小时。

问题:Nginx 永久缓存 DNS 记录

如果你在 Nginx 中使用下面的配置:

    location / {
        proxy_pass https://some.domain.com;
    }

Nginx 只会在启动的时候解析一次 some.domain.com,以后不会再对其进行解析。这是非常危险的操作,尤其是对于那些 IP 地址经常变动的域名。它可能平安无事地运行几个月,然后突然在某个凌晨两点把你从床上纠起来。

针对这个问题已经有很多众所周知的方法了,但由于本文不是关于 Nginx 的,所以我不打算深入探讨它。但你第一次遇到它时一定会很惊讶。

这是一篇关于这个问题发生在 AWS 负载均衡器上的 博客文章

问题:Java 永久缓存 DNS 记录

跟上面类似的问题,只是出现在 Java 上:据说 这与你 Java 的配置有关。“JVM 的默认 TTL 设置可能会导致只有 JVM 重启时才会刷新 DNS 记录。”

我还没有遇到过这个问题,不过我那些经常写 Java 的朋友遇到过这个问题。

当然,任何软件都可能存在永久缓存 DNS 的问题,但据我所知它经常出现在 Nginx 和 Java 上。

问题:被遗忘的 /etc/hosts 记录

这是另一种缓存问题:/etc/hosts 中的记录会覆盖你的常规 DNS 设置!

让人迷惑的是 dig 命令会忽略 /etc/hosts 文件。所以当你使用 dig whatever.com 来查询 DNS 信息时,它会告诉你一切正常。

问题:电子邮件未发送 / 将成为垃圾邮件

电子邮件是通过 DNS(MX 记录, SPF 记录, DKIM 记录)来发送和验证的,所以有些电子邮件问题其实是 DNS 问题。

问题:对国际化域名无效

你可以使用非 ASCII 字符甚至是表情符来注册域名,比如 拉屎网 https://?.la

DNS 能够处理国际化域名是因为 ?.la 会被用 punycode 编码将转换为 xn--ls8h.la

尽管已经有了 DNS 处理国际化域名的标准,很多软件并不能很好地处理国际化域名。Julian Squires 的 干掉 Chrome 浏览器的表情符!! 就是一个非常有趣的例子。

问题:TCP DNS 被防火墙拦截

有人提到一些防火墙会允许在 53 端口上使用 UDP 协议,但是禁止 TCP 协议。然而很多 DNS 查询需要在 53 端口上使用 TCP,这可能会导致很难排查的间歇性的问题。

问题:musl 不支持 TCP DNS

很多应用程序使用 libcgetaddrinfo 来做 DNS 查询。musl 是用在 Alpine Docker 容器上的 glibc 替代品。而它不支持 TCP DNS。如果你的 DNS 查询的响应数据超过 DNS UDP 数据包的大小(512 字节)就会出现问题。

我对此仍然不太清楚,我下面我的理解也可能是错的:

  1. muslgetaddrinfo 发起一个 DNS 请求
  2. DNS 服务器发现请求的响应数据太大了,没法放入一个 DNS 数据包中
  3. DNS 服务器返回一个 空截断响应 empty truncated response ,并期望客户端通过 TCP DNS 重新用发起查询
  4. musl 不支持 TCP DNS,所以根本不会重试

关于这个问题的文章:在 Alpine Linux 上的 DNS 解析问题

问题:getaddrinfo 不支持轮询 DNS

轮询 round robin DNS 是一种 负载均衡 load balancing 技术,每次 DNS 查询都会获得一个不同的 IP 地址。显然如果你使用 gethostbyname 做 DNS 查询不会有任何问题,但是用 getaddrinfo 就不行了。因为 getaddrinfo 会对获得的 IP 地址进行排序。

在你从 gethostbyname 切换到 getaddrinfo 时可能完全不会意识到这可能会引起负载均衡问题。

这个问题可能会非常隐蔽,如果你不是用 C 语言编程的话,这些函数调用被隐藏在各种调用库背后,你可能完全意识不到发生了这种改变。所以某次看似人畜无害的升级就可能导致你的 DNS 负载均衡失效。

下面是讨论这个的一些文章:

问题:启动服务时的竞争条件

有人 提到 使用 Kubernete DNS 时遇到的问题:他们有两个同时启动的容器,一旦启动就会立即尝试解析对方的地址。由于 Kubernete DNS 还没有改变,所以 DNS 查询会失败。这个失败会被缓存起来,所以后续的查询会一直失败。

写在最后

我所列举的不过是 DNS 问题的冰山一角,期待大家告诉我那些我没有提到的问题和相关链接。我希望了解这些问题在实际中是如何发生的以及如何被解决的。

(题图:MJ/f512f18e-2e1d-4614-bed1-b0a0c373e14d)


via: https://jvns.ca/blog/2022/01/15/some-ways-dns-can-break/

作者:Julia Evans 选题:lujun9972 译者:toknow-gh 校对:wxy

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

通过开源项目 AdGuard Home 运行你自己的 DNS 服务器来控制你的互联网隐私。

域名服务器(DNS)将域名(如 example.com)与 IP 地址(如 93.184.216.34)相关联。这就是当你输入 URL 或搜索引擎返回 URL 供你访问时,你的网络浏览器如何知道在世界的哪个地方寻找数据。DNS 为互联网用户提供了极大的便利,但也并非没有缺点。例如,付费广告会出现在网页上,因为你的浏览器会自然的使用 DNS 来解析这些广告在互联网上“存在”的位置。同样,跟踪你在线活动的软件通常由通过 DNS 解析的服务启用。你不想完全关闭 DNS,因为它非常有用。但是你可以运行自己的 DNS 服务,以便更好地控制它的使用方式。

我相信运行自己的 DNS 服务器至关重要,这样你就可以阻止广告并保持你的浏览隐私,远离试图分析你的在线交互的提供商。我过去用过 Pi-hole,今天仍然推荐它。然而,最近,我一直在我的网络上运行开源项目 AdGuard Home。我发现它有一些值得探索的独特功能。

AdGuard Home

在我使用过的开源 DNS 软件中,AdGuard Home 是最容易设置和维护的。你可以在一个项目中获得许多 DNS 解析解决方案,例如 DNS over TLS、DNS over HTTPS 和 DNS over QUIC。

你可以使用一个脚本将 AdGuard 设置为容器或本地服务:

$ curl -s -S -L \
    https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh

查看这个脚本以便了解它的作用。了解了它的工作过程后,再运行它:

$ sh ./install.sh

我最喜欢 AdGuard Home 的一些功能:

  • 一个简单的管理界面
  • 使用 AdGuard 阻止列表来阻止广告和恶意软件
  • 单独配置网络上每个设备的选项
  • 强制在特定设备上进行安全搜索
  • 为管理界面设置 HTTPS,这样与它的远程交互是完全加密的

我发现 AdGuard Home 为我节省了时间。它的黑名单比 Pi-hole 上的黑名单更强大。你可以快速轻松地将其配置为 DNS over HTTPS。

没有更多的恶意软件

恶意软件是你计算机上不需要的内容。它并不总是对你有直接危险,但它可能会为第三方带来危险活动。这不是互联网最初的目的。我认为你应该托管自己的 DNS 服务,以保护你的互联网历史记录的私密性,并避免被微软、谷歌和亚马逊等已知跟踪器掌握。在你的网络上试用 AdGuard Home 吧。


via: https://opensource.com/article/23/3/open-source-dns-server

作者:Amar Gandhi 选题:lkxed 译者:geekpi 校对:wxy

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

谷歌公共 DNS 服务正在推行大小写混杂查询

在 DNS over TLS 之前,DNS 查询由于技术原因会被投毒,从而导致客户端访问到投毒者设置的假冒地址。为了避免这种安全风险,最好的方式是使用 DNS over TLS 这样的加密查询通道。但是仍然有大量的 DNS 服务器没有或不能使用加密查询。谷歌从 2009 年就在小范围实验一种名为 0x20 的做法,它可以将查询的域名随机改变字母大小写,这可以有效的提高 DNS 投毒的难度。现在谷歌已经扩大了该方案,已经覆盖了 90% 没有使用加密查询的 DNS 服务器。

消息来源:The Register
老王点评:以后在你的权威 DNS 服务器上看到类似 liNUx.cN 这样的查询不要惊奇。

290 款微星主板的安全启动默认被绕过

安全启动是 UEFI 主板固件中的一项安全功能,确保在启动过程中只有受信任的(签名)软件可以执行。研究发现,微星公司在 2021 年 9 月至 2022 年 1 月期间发布的固件更新,改变了微星主板上的一个默认安全启动设置,该设置允许任何操作系统镜像运行,无论其是否有错误或丢失的签名。受影响的主板超过了 290 款,包括最新的用于英特尔和 AMD CPU 的微星主板。

消息来源:Bleeping Computer
老王点评:我觉得讨厌安全启动功能的人会喜欢这个默认设置。

USB4 2.0 规格正式发布,USB-C 可达 120Gbps

USB-C 是令人困惑的,一个 USB-C 端口或电缆可以支持一系列的速度、电源能力和其他功能,它支持的数据传输速率,可以从 USB 2.0 的 0.48Gbps 一直到 USB4 和雷电 3/4 的 40Gbps。USB 实施者论坛(USB-IF)今天发布了 USB4 2.0 规范,它增加了对 80Gbps 双向带宽的可选支持,以及以高达 120Gbps 速度发送或接收数据的可选能力。但预计在至少 12 到 18 个月内不会看到支持新规格的产品出现。

消息来源:USB-IF
老王点评:都是一种样子的接口,但是有的电缆就特别优秀。?

回音

  • 自去年 9 月 宣布 以来,谷歌今天正式 关闭 了其 Stadia 云游戏平台。除了对用户购买的软硬件退款之外,他们还使其游戏手柄能够使用蓝牙,以便在其他游戏平台上使用。

20 个企业控制了一半的域名解析

据分析,来自 20 个域名的 255 台 DNS 服务器控制了 52% 的域名解析,100 个域名的 DNS 服务器控制了 75% 的域名解析,6000 个域名的 DNS 服务器控制了 99% 的域名解析。控制域名解析的前 20 个域名来自互联网巨头,其中 15 个是美国科技巨头,如 GoDaddy、谷歌、Cloudflare 和亚马逊。其中 34% 的域名解析的 IP 地址属于 Cloudflare 的自治系统 AS13335。

消息来源:Netmeister
老王点评:互联网其实不是去中心化的,而是分布式的。

Mastodon 的安全性并不可靠

自从 Twitter 被收购后,Mastodon 引来了大量注意,用户数量激增,两周内总用户达到 870 万。有安全专家发现多个存在安全漏洞的 Mastodon 实例。Mastodon 项目缺乏专门的安全团队,甚至软件也没有自动更新或检查更新的功能。

消息来源:Ars Technica
老王点评:随着用户增多,安全问题就成了大问题,好在 Mastodon 本身并不收集太多用户数据。

Let's Encrypt 签发了逾三十亿证书

目前全球有数以百万计的网站依赖 Let's Encrypt 作为安全保障。其 2022 年年度报告中称,截至 2022 年 11 月 1 日,它为超过 3.09 亿个域名提供了证书,仅 2022 年增加了逾 3300 万个域名。Let's Encrypt 在 2017 年 6 月签发了第一亿个证书。三年后的 2020 年 2 月签发了第 10 亿个证书。

消息来源:Let's Encrypt
老王点评:Let's Encrypt 功莫大焉,要是一直由 CA 公司把持 SSL 证书的签发,HTTPS 的普及可能会慢得多。