C# .NET DNS 查询组件






4.89/5 (81投票s)
一个用于执行 DNS 查询的可重用组件。
引言
本文档演示了 DNS 查询消息的格式,以及如何向 DNS 服务器提交请求并解析结果。我已将此功能封装到一个小型易于使用的 C# 程序集中,您可以轻松地将其部署到自己的应用程序中,以进行 ANAME、MX、NS 和 SOA 查询。该程序集完全是安全的托管代码,在 Linux(使用 Mono CLR)和 Windows 上都能正常工作。本文档及随附的代码均基于 RFC1035 - Domain names - implementation and specification 中的信息编写。
背景
直到最近,垃圾邮件还只是困扰别人的事情,我从未收到过。不知何故,我的电子邮件地址出现在了一个邮件列表中,垃圾邮件开始出现在我的收件箱中。起初这只是一个小麻烦,但我预计我的电子邮件地址与其他数千个地址一起被卖给了其他垃圾邮件发送者,现在我每天收到大约 100 封垃圾邮件——这真是太烦人了。他们为什么如此确信我需要伟哥?我还没那么老...
值得称赞的是,Outlook 在检测和清除这些垃圾邮件方面表现出色,但我仍然不喜欢它必须接收然后被丢弃的事实。原本预示着新重要邮件到达的略微延迟的发送和接收变得越来越令人失望,我想知道如果用户仍然使用拨号连接会怎样。那将是无法容忍的。
我的解决方案是创建一个自己的 SMTP/POP3 服务器组合,该服务器可以在垃圾邮件对 Outlook 造成麻烦之前就将其检测并清除,这为我提供了一个机会,让我能够提高我的 .NET 套接字编程技能,因为我过去很少使用它。我可以使用我在伦敦 Red Bus 的 Linux 服务器之一来托管邮件服务器(通过 Mono 的伟大贡献),但我必须完全使用托管的 C#,而不能使用 P/Invoke 调用。我开始着手了。
SMTP 和 POP3 是相对简单的基于文本的标准,很容易实现,但我遇到了一个问题。为了让我的 SMTP 服务器将我的消息中继到其他服务器,需要进行 MX 查询。MX 查询是指检索处理域电子邮件的一个或多个邮件服务器的主机名。快速查看 System.Net.Dns
后发现,该框架不支持此功能,而且如前所述,我无法走互操作(Interop)路线。唯一的其他选择是昂贵的商业组件,这完全不在我的考虑范围内,因为我当然不会因为一些垃圾邮件发送者而损失金钱。我的项目就这样变得更大了。
所以你想问 DNS 服务器一个问题...
表面上看,DNS 似乎很简单,就是将名称转换为数字,或者检索域的其他信息。实际上,这是一个庞大而复杂的主题,相关的书籍往往内容非常丰富。幸运的是,为了我们正在做的事情,我们不需要理解太多——只需要知道如何创建查询、将其发送到服务器以及解析响应。DNS 服务器处理的最常见查询是 ANAME 查询,它将域名映射到 IP 地址(例如,将 *codeproject.com* 映射到 209.171.52.99)。System.Net.Dns.GetHostByName
执行 ANAME 查询。下一个最常见的查询类型可能是 MX 查询。
与许多基于文本的互联网协议不同,DNS 是一个二进制协议。DNS 服务器是互联网上最繁忙的计算机之一,而字符串解析的开销会使这样的协议变得不可行。为了保持快速和精简,UDP 是传输的首选,因为它轻量、无连接且与 TCP 相比速度更快。要与 DNS 服务器通信,您只需向它发送一个 UDP 数据包,它就会返回一个。哦,而且这些数据包不能超过 512 字节。 (顺便说一句,许多防火墙会阻止大于 512 字节的 UDP 数据包。)
下图显示了我发送给我的 DNS 服务器以查找 *microsoft.com* 域的 MX 记录的二进制请求以及我收到的相应响应。为了做到这一点,我向我的 DNS 服务器的 53 端口发送了一个 31 字节的 UDP 数据包,如下所示。它通过 UDP 端口 53 回复了一个 97 字节的响应。
请求和响应都具有相同的格式,开头是一个 12 字节的报头块。报头以一个 2 字节的消息标识符开始。它可以是任何 16 位值,并在响应的前 2 个字节中回显,这很有用,因为它允许我们匹配请求和响应,因为 UDP 不保证事物到达的顺序。接下来是一个 2 字节的状态字段,在我们的请求中,只有一个位被设置,即 *递归请求(recursion desired)* 位。然后是一个 2 字节的值,表示请求中的问题数量,在本例中为 1。之后是三个 2 字节的值,分别表示答案数量、名称服务器记录数量和附加记录数量。由于这是请求,所有这些值都为零。
请求的其余部分是我们单个问题。一个问题由一个可变长度的域名、一个 2 字节的 QTYPE
和一个 2 字节的 QCLASS
组成。域名被视为一系列 *标签(labels)*,标签是点之间的单词。在我们的示例 *microsoft.com* 中,包含两个标签:*microsoft* 和 *com*。每个标签前面都有一个字节指定其长度。QTYPE
表示要检索的记录类型,在本例中为 MX。QCLASS
为 *Internet*。
我们收到的响应告诉我们,*microsoft.com* 域有三个入站邮件服务器:*maila.microsoft.com*、*mailb.microsoft.com* 和 *mailc.microsoft.com*。所有三个服务器的优先级都相同,为 10。在发送邮件到某个域时,应首先尝试优先级最低的邮件服务器,然后是次低的,依此类推。在这种情况下,没有优先级差异,任何一个都可以使用。让我们更仔细地看看响应。
您可能已经注意到,响应的前 31 个字节与请求非常相似,唯一的区别在于状态字段(字节 2 和 3)和答案计数(字节 6 和 7)。答案计数告诉我们响应中有三个答案。对于状态字段的构成,我将参考上述 RFC 第 4.1.1 节,此处不再赘述。您还会注意到问题在响应中被回显,这在我看来似乎效率不高,但这就是标准。第一个答案从字节 31 (0x1F) 开始。
任何答案的第一部分都嵌入了问题,因此如果您提出多个问题,您就知道答案与哪个问题相关。这里使用了一种简化的形式——而不是明确重复域名 *microsoft.com*,这在我们只有 512 字节的空间时非常浪费。我们引用了第 12 字节 (0x0C) 处已定义的域。在我们的示例中,这只需要 2 个字节而不是 15 个字节。检查在标签前缀的标签长度字节时,如果设置了最高有效两位,则表示引用了先前定义的域名,并且标签不在此处。下一个字节告诉您消息中现有域名的位置。同样,后面是 QTYPE
和 QCLASS
,然后我们开始看到答案部分。
接下来的四个字节表示记录的 TTL(生存时间)。当 DNS 服务器无法明确回答问题时,它会知道(或可以找到)另一个可以回答的服务器并向其发出请求。它会将此答案缓存一段时间以提高效率。缓存中的每条记录都有一个 TTL,之后如果需要,它将被删除并从别处重新获取。
接下来的两个字节表示记录的大小,再接下来的两个字节表示 MX 优先级,然后是可变长度的域名。这里我们只指定域名的 *mailc* 部分,然后再次引用第 12 字节处的其余域名(以生成 *mailc.microsoft.com*)。对于 *maila.microsoft.com* 和 *mailb.microsoft.com*,会出现两个几乎相同的记录。
使用组件
现在一切都清晰可见了,让我们看看如何使用提供的组件来为您执行域名查找。您需要引用程序集并导入 Bdev.Net.Dns
命名空间。以下代码演示了上述示例
// Shameful hardcoding of my DNS server
IPAddress dnsServerAddress = IPAddress.Parse("194.74.65.68");
// Retrieve the MX records for the domain microsoft.com
MXRecord[] records = Resolver.MXLookup("microsoft.com",
dnsServerAddress);
// iterate through all the records and display the output
foreach (MXRecord record in records)
{
Console.WriteLine("{0}, preference {1}",
record.HostName, record.Preference);
}
这使用了简化的接口形式,用于 MX 记录。您也可以使用以下代码执行相同的查询
// Further shameful hardcoding of my DNS server
IPAddress dnsServerAddress = IPAddress.Parse("194.74.65.68");
// create a request
Request request = new Request();
// add the question
request.AddQuestion(new Question("microsoft.com",
DnsType.MX, DnsClass.IN));
// send the query and collect the response
Response response = Resolver.Lookup(request, dnsServerAddress);
// iterate through all the answers and display the output
foreach (Answer answer in Answers)
{
MXRecord record = (MXRecord)answer.Record;
Console.WriteLine("{0}, preference {1}",
record.HostName, record.Preference);
}
杂项
此组件的一个令人烦恼之处在于,您必须在每次执行查找时显式地将 DNS 服务器的 IP 地址告诉解析器。理想情况下,它应该使用其中一个默认 DNS 服务器,这样您就可以省略此参数,但我一直无法找到以编程方式获取此信息的方法。如果您知道方法,请*告知我*。这让我很困扰。
关于 DNS 服务器有两点需要注意。首先,一些服务器除了 UDP 外还支持 53 端口上的 TCP 连接,这可以用来绕过 512 字节的限制。许多服务器不支持,我也没有提供 TCP 实现。其次,虽然协议允许每个请求有多个问题,但许多服务器不支持,可能是为了尽量将内容保持在 512 字节的限制内。我建议您每个请求只使用一个问题。如果响应不适合 512 字节,状态字段中会设置一个 *截断(truncation)* 位。
请向我报告任何错误/增强功能。
历史
- 2005-10-07:
初始发布。