注释:
18: getshort函数从指定地址读取16bit网络字节顺序的数据, 并将其转换成little-endian的顺序返回.
20: 定义了一个联合体变量名为response, 用于存储DNS响应报文. HEADER是用于存储DNS首部的结构体, 定义在<arpa/nameser_compat.h>中(通常在/usr/include/arpa/nameser_compat.h 中. <arpa/nameser_compat.h>这个头文件则在<arpa/nameser.h>中被include. 后面用到的很多宏都定义在<arpa/nameser_compat.h>这个头文件中). HEADER结构体中本文会用到的成员是dncount和ancout, 分别表示问题数和资源记录数(参见图1).PACKETSZ也定义在<arpa/nameser_compat.h>中, 这个宏表示一个报文的最大长度.
21-26: responselen是响应报文的长度. responseend指向了响应报文最后一个字节之后的一字节, 即response.buf+responselen. responsepos指向了即将处理的字段. numanswers是还未处理的回答数. name用来存储主机名, 长度是MAXDNAME个字节. MAXDNAME定义在<arpa/nameser.h>中, 表示域名的最大长度. pref用来存储MX记录的优先值.
28: dns_resolve函数发起一个指定查询名(domain)和查询类型(type)的DNS查询. type可取的值在<arpa/nameser_compat.h>中定义成了宏. 这些宏都是以T_开头. 例如, A类查询的值是T_A. 对于MX查询, 则可以调用dns_resolve(domain,T_MX)(这也就是dns.h中, dns_mx_query这个宏所做的). 如果发生错误, 函数返回-1, 否则返回资源记录数(参见图1).
35: 利用res_search函数发起一条指定类型的DNS查询. 其中的参数C_IN表示Internet地址. C_IN这个宏也定义在<arpa/nameser_compat.h>中. 返回值赋给responselen, 即响应报文长度.
36: 检查是否有错误发生, 如果发生返回-1. 实际上这种错误检测是不完善的. 关于详细的错误检测方法可以参考qmail源代码中的dns.c文件.
40-42: 调整responseend和responsepos的值. 并将报文中的问题数存储在临时变量n中, 用于之后的处理. 由于响应报文中的数据都是网络字节顺序, 因此需要调用ntohs函数进行转换. ntohs函数的具体内容可以参考man手册.调整后, responsepos指向第一个查询问题的首地址.
43-50: 跳过所有的查询问题, 让responsepos指向第一个回答的首地址. 此时, 需要参考第二节中的内容来帮助理解. 首先利用dn_expand函数获得查询问题字段中查询名的长度, 并把该值赋给i. 之后, responsepos+=i使得responsepos指向了查询类型字段的首地址(参见图2). QFIXEDSZ宏定义在<arpa/nameser_compat.h>中, 其值等于4. 它表示DNS查询报文中问题部分的定长字段的字节数, 即查询类型和查询类两个字段的总长度. i = responseend - responsepos令i的值等于responsepos和responseend之间的距离, 由于此时responsepos指向问题部分查询类型的第一个字节, 因此, 对于一个正常的报文来说, i的值应该至少等于4. 因此, 在第48行进行了检测, 若i < QFIXEDSZ, 表明该报文格式有错. 则返回-1. 否则, responsepos+=QFIXEDSZ. 此时, responsepos指向了下一个查询问题字段(或第一个回答字段)的首地址. 如此反复, 直至跳过全部的查询问题字段. 则循环执行完, responsepos指向第一个回答字段的首地址.
51-52: 将资源记录数的值赋给numanswers. 返回numansers.
55: dns_findmx函数用于在调用了dns_resolve函数之后, 分析DNS响应报文中的回答字段. 如果当前responsepos指向的回答字段的类型是参数wanttype指定的类型, 则对该回答字段进行处理. 参数wanttype可取的值与dns_resolve函数中的type参数可取值一样. 对于MX记录, 值为T_MX. dns.h中dns_mx_expand宏的定义就是dns_findmx(T_MX). 该函数若发生错误则返回-1, 若报文中已经没有可以处理的字段, 则返回DNS_MSG_END. 否则返回name数组存储的字符串长度(不包括结尾的'\0').
61-62: 检查未处理的回答字段数目, 若达到或小于零, 则返回DNS_MSG_END. 否则numanswers值减一.
64: 回答字段是以资源记录格式存储的. 第一项是域名(参见图3). 因此用dn_expand函数将该字段还原为普通字符串格式. 并将返回的该字段长度赋值给i.
66: 调整responsepos的值, 使其跳过域名部分, 指向类型字段(参见图3).
67-68: 资源记录中, 定长字段的总长度为10字节,即类型, 类, 生存时间和资源数据长度字段的总长度(参见图3). 若responseend-responsepos的值小于10, 则表明该报文非法.
69-71: 此时responsepos指向了资源记录中类型字段的首地址. 利用getshort函数分别获得类型和资源数据长度字段的值, 存储在rrtype和rrdlen变量中. 之后,调整responsepos的值, 使其指向资源数据的首地址(参见图3). 对于一个MX查询, 资源数据中的内容是优先值和主机名.
72-82: 如果这条资源记录的类型是函数参数所指定的类型, 则对其进行处理. MX记录中, 资源数据内第一项存储着16bit的优先值, 之后存储着主机名. 优先值长度为2字节, 主机名长度至少1字节, 则资源数据长度至少要3字节. 否则报文格式为非法. 第74, 75行就是利用这种方法检测资源数据是否为非法. 第76行从资源数据中获得优先值存储在pref中. 由于报文中的优先值是按照网络字节顺序存储的, 因此需要将其转换成little-endian顺序后存到pref中. 接着对name数组清零. 之后从资源数据中提取主机名. responsepos+=rrdlen令responsepos指向了下一条资源数据. 最后利用strlen函数返回主机名的字符串长度.
83: 该资源记录的类型与参数指定类型不符, 则跳过该条记录, 将responsepos调整至下一条资源记录.
87-91: 调用res_init函数执行相关初始化操作. 并对name数组清零. 此处没有对res_init的返回值进行检测.
93-100: dns_get_mxrr函数将pref的值(优先值)存储到p指向的地址中, 并将name中存储的主机名复制到dn指向的地址中, 复制的长度不超过len个字节. 如果len个字节不足以存储整个主机名, 则返回-1, 否则返回0.
102: main函数, argv[1]中会存储着要查询的域名.
104-106: dname用于存储查找到的主机名. 变量i用于存储相关函数的返回值. p用于存储优先值.
108-112: 用户提供的参数有错误.
113: 利用dns.h中定义的宏, 发起MX查询. 该语句相当与i = dns_resolve(argv[1],T_MX);
120: 利用dns.h中定义的宏, 对响应报文中的每条资源记录进行处理. foreach_mxrr宏在dns.h中定义, 该语句相当于while(dns_findmx(T_MX)!=DNS_MSG_END&&(!dns_get_mxrr(& p,dname,MAXDNAME)))
121-124: 打印每条资源记录后函数返回.
该程序在Magic Linux 2.0正式版上编译成功( 内核版本2.6.15.3; gcc版本3.4.4; i386体系结构 ).编译和运行结果如下:
[monnand@monnand-host src]$ gcc -lresolv dns.c -o dns
[monnand@monnand-host src]$ ./dns gmail.com
pref domain name
50 gsmtp163.google.com
50 gsmtp183.google.com
5 gmail-smtp-in.l.google.com
10 alt1.gmail-smtp-in.l.google.com
10 alt2.gmail-smtp-in.l.google.com
5 后记
本文简要介绍了DNS报文格式, 讲解了常用的地址解析函数并给出了这些函数的应用实例. 第4节中的代码可以应用在其他程序内, 但是使用前最好对其进行一定的修改从而增强健壮性和安全性. 修改时可以参考qmail的源代码中dns.c和dns.h两个文件. 如果想了解更多的地址解析函数, 可以查阅man resolver. 如果想对DNS相关的协议及报文格式有更深的了解, 可以查阅附录中的参考文献.
附录 参考文献
[1]W. Richard Stevens.TCP/IP详解 卷1:协议.北京:机械工业出版社.1993.
[2]Mockapetris, P. V. "Domain Names: Concepts and Facilities," RFC 1034. 1987
[3]Mockapetris, P. V. "Domain Names: Implementation and Specification," RFC1035. 1987
Linux下的地址解析函数应用实例(3)
内容版权声明:除非注明,否则皆为本站原创文章。