套接字编程:DIG DNS 查询消息:标头长度不正确?

问题描述

RFC 参考

我正在从事一个项目,该项目涉及套接字编程和解释 DIG DNS 查询输出

我使用 RFC 1035 作为参考。尽管这已经很老了(1987 年),但据我从后来的 RFC(例如 8490)中可以看出,DNS 标头仍然相同。

https://tools.ietf.org/html/rfc1035

代码概览:IPv6 TCP 查询

我用 C 语言编写了一个从 IPv6 TCP 套接字读取的短程序。我使用 DIG 将数据发送到这个套接字。 (我的程序只是读取它在套接字上看到的所有数据,并将其打印到 stdout。)

请注意,这里有两件不寻常的事情:

  • 首先是IPv6的使用
  • 其次使用 TCP(DNS 消息通常是 UDP)

这是使用的命令:

dig @::1 -p 8053 duckduckgo.com +tcp

我正在 Debian 测试中运行 dig 版本 DiG 9.16.13-Debian。 (cera 2021-5 月)

输出、讨论和问题

这是从套接字读取的十六进制和可打印字符输出

Hex:
00 37 61 78 01 20 00 01 00 00 00 00 00 01 0A 64 75 63 6B 64 75 63 6B 67 6F 03 63 6F 6D 00 00 01 00 01 00 00 29 10 00 00 00 00 00 00 0C 00 0A 00 08 00 7A 4* 48 2C 16 0* 33 
Char:
00  7 61  x 01 20 00 01 00 00 00 00 00 01 0A  d  u  c  k  d  u  c  k  g  o 03  c  o  m 00 00 01 00 01 00 00  ) 10 00 00 00 00 00 00 0C 00 0A 00 08 00  z 4*  H,16 0* 33 

如果遇到不可打印的字符,则打印十六进制值。

虽然这是一个相当长的数据流,但问题与头部的长度有关。


根据 RFC 1035,标头的长度应为 12 个字节。

4.1.1. Header section format

The header contains the following fields:

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      ID                       |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    QDCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ANCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    NSCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ARCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

标头后跟一个 QUESTION SECTION。问题部分以指定长度的单个字节开始。


检查上面的数据流,我们看到偏移量 12 处的字节的值为 0。我在下面用偏移量数字重复它以使其清楚。数据在中间行,上下一行是字节偏移量

 0  1  2  3  4  5  6  7  8  9 10 11 <- byte 12
00 37 61 78 01 20 00 01 00 00 00 00 00 01 0A 64 75 63 6B ...
 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 <- byte 15

这显然没有任何意义。

再次查看流,我们可以看到“duckduckgo”前面有字节 0A。这是十进制的 10,对应于“duckduckgo”的 10 个字符。此字符串后跟一个字节 03,对应于“com”的 3 个字节。

字节 0A 的偏移量为 15。不是 12

我一定误解了 RFC 规范。但我误解了什么?标头本身是否以与我认为的不同的偏移量开始? (字节零。)或者在标题的结尾和第一个问题部分的开头之间是否有一些填充?

本网站现有问题:

评论:下面的链接声明没有填充。这是这个问题的唯一答案。问题是关于DNS 响应而不是查询,并且不询问查询的标头部分。 (虽然其中一方的信息应该适用于另一方,但可能不适用。)

Do DNS messages pad names to an even number of bytes?

评论:以下链接询问了构建数据结构以处理 DNS 数据的最佳方法。此外,答案指出必须注意网络字节顺序和机器字节顺序。我已经意识到这一点,并且在将信息打印到 ntohs() 之前,我使用 x86_64 从网络字节顺序转换为 stdout 字节顺序。这不是问题,也不能解释为什么我看到有关 dns 查询的信息从字节 15 而不是 12 开始,而标头应该是 12 字节的固定大小。

Implementing a DNS Query in c++ according to RFC 1035

解决方法

感谢@SteffenUllrich 在评论中提出了解决方案。

RFC 1035 4.2.2 状态

4.2.2. TCP usage

Messages sent over TCP connections use server port 53 (decimal).  The
message is prefixed with a two byte length field which gives the message



Mockapetris                                                    [Page 32]
 
RFC 1035        Domain Implementation and Specification    November 1987


length,excluding the two byte length field.  This length field allows
the low-level processing to assemble a complete message before beginning
to parse it.

我曾在某个时候删除了结构开头的 2 字节字段。

这是重新启用 2 字节长度字段后的结构。

struct __attribute__((__packed__)) dns_header
{

    unsigned short ID;
    union
    {
        unsigned short FLAGS;

        struct
        {
            unsigned short QR : 1;
            unsigned short OPCODE : 4;
            unsigned short AA : 1;
            unsigned short TC : 1;
            unsigned short RD : 1;
            unsigned short RA : 1;
            unsigned short Z : 3;
            unsigned short RCODE : 4;
        };
    };
    unsigned short QDCOUNT;
    unsigned short ANCOUNT;
    unsigned short NSCOUNT;
    unsigned short ARCOUNT;

};


struct __attribute__((__packed__)) dns_struct_tcp
{

    unsigned short length; // length excluding 2 bytes for length field

    struct dns_header header;

};

例如:我收到一个长度为 53 字节的 TCP 数据包。 length 的值设置为 51。

要将数据读入该结构体:

memcpy(&dnsdata,buf,sizeof(struct dns_struct_tcp));

解释这个数据(因为它是按网络字节顺序存储的):

void dns_header_print(FILE *file,const struct dns_header *header)
{

    fprintf(file,"ID: %u\n",ntohs(header->ID));
    char str_FLAGS[8 * sizeof(unsigned short) + 1];
    str_FLAGS[8 * sizeof(unsigned short)] = '\0';
    print_binary_16_fixed_width(str_FLAGS,header->FLAGS);
    fprintf(file,"FLAGS: %s\n",str_FLAGS);
    fprintf(file,"FLAGS: QOP  ATRRZZZR   \n");
    fprintf(file,"       RCODEACDA   CODE\n");
    fprintf(file,"QDCOUNT: %u\n",ntohs(header->QDCOUNT));
    fprintf(file,"ANCOUNT: %u\n",ntohs(header->ANCOUNT));
    fprintf(file,"NSCOUNT: %u\n",ntohs(header->NSCOUNT));
    fprintf(file,"ARCOUNT: %u\n",ntohs(header->ARCOUNT));
    
}

请注意,标志不变,因为标志的每个字段的长度都小于 8 位。但是在 x86_64 系统上,unsigned short 以小端格式存储,因此 ntohs() 用于将大端(网络)字节顺序的数据转换为小端(主机) 字节顺序。