Linux下的地址解析函数应用实例

作者: 默难 ( monnand@gmail.com )

0        引言
域名系统(DNS)是一种用于TCP/IP应用程序的分布式数据库, 它提供主机名字和IP地址之间的转换及有关电子邮件的选路信息.[1] 目前, 它已经在全球范围内被广泛应用. 从应用的角度上看, 对DNS的访问是通过一个地址解析器(resolver)来完成的. 本文通过讲解一些常用的地址解析函数, 并利用精简后的部分qmail代码, 让不熟悉DNS相关函数的程序员了解并掌握常用的地址解析函数.

1        概述
DNS查询中, 最常用的两类分别是A类查询(A query)和指针查询(PTR query). 前者是已知主机名, 询问IP; 后者是已知IP, 询问主机名. 对于这些查询, 在Unix主机中可以直接调用基本DNS函数: gethostbyname(3)和gethostbyaddr(3)来实现. 但是对于其他类型的查询(例如MX查询), 则没有专门的函数来负责处理. 此时, 程序员不得不依赖地址解析函数来亲自处理这些问题. 这需要对DNS报文格式有基本的了解, 这些将在下面几节进行说明. 关于gethostbyname(3)和gethostbyaddr(3)两个函数, 读者可以查阅自己系统上的man手册.

2        DNS报文格式
在对地址解析函数讲解之前, 有必要先了解一下DNS报文格式. 之后的几节会频繁地涉及到本节所讲的内容. 如果想对DNS相关协议有更深的了解, 可以阅读参考文献[1] [2] [3].

DNS定义了一个用于查询和响应的报文格式, 图1 显示了这个报文的总体格式.

[图1]

每个DNS查询(或响应)报文都包含有一个12字节长的首部和四个变长的字段组成.

对于本文来说, 首部中主要关心的是问题数和资源记录数两个字段. 这两个字段分别用于说明各自对应的变长字段中的条目数. 问题数说明查询问题字段中的条目数; 资源记录数则说明回答字段中的条目数. 对于一个DNS查询报文, 问题数通常是1. 对于应答报文, 回答数至少是1.

首部以下是四个变长字段, 本文所关心的是查询问题字段和回答字段.

查询问题字段可以包含多个查询问题, 每个问题的格式如图2 所示.

[图2]

其中, 查询名一项存储着要查找的名字. 它长度可变并以一种特殊的格式存储. 程序可以通过其中存储的内容确定其长度. 具体获得其中存储内容的方法, 将在下一节中进行详细讲解. 每一个问题有一个查询类型, 每个响应(下文中将会提到)也同样有一个类型. 常用的类型有: A类型---表示期望获得查询名的IP地址; PTR查询---表示期望获得一个IP地址对应的域名; MX查询---邮件交换查询(关于MX查询的具体内容, 下文会提到). 查询类指定了所使用的协议簇, 通常是1, 表示Internet地址.

回答字段可以包含多个条目. 每个回答字段是以一种叫做资源记录(Resource Record, RR)的格式存储的. ( 授权字段和额外信息字段也同样以资源记录的格式存储信息). 资源记录的格式如图3 所示.

[图3]

域名是记录中资源数据对应的名字. 它的格式和前面介绍的查询名字段格式相同. 类型和类字段和前面介绍的查询类型, 查询类字段的功能一样. 类字段的取值通常是1, 表示Internet地址. 生存时间字段是客户程序保留该资源记录的秒数. 资源数据长度说明资源数据包含的字节数. 资源数据则根据类型字段的值有不同的格式. 对于A类型, 资源数据是IP地址. 对于MX查询, 资源数据是优先值和域名, 域名的格式与查询名字段格式相同(MX记录的具体内容下文会有介绍).

至此, DNS中用到的报文格式已经基本介绍完. 下一节中将会介绍一些常用的地址解析函数. 阅读下文时, 最好随时翻阅本节所讲的内容以便于理解.

3        地址解析函数

除了经常用到的gethostbyname(3)和gethostbyaddr(3)函数以外, Linux(以及其它UNIX/UNIX-like系统)还提供了一套用于在底层处理DNS相关问题的函数(这里所说的底层仅是相对gethostbyname和gethostbyaddr两个函数而言). 这套函数被称为地址解析函数(resolver functions). 用户可以通过键入man resolver来了解其中的具体信息. 这里将对其中常用到的函数做一个解释. 常用的地址解析函数原型如下:

       #include <netinet/in.h>
       #include <arpa/nameser.h>
       #include <resolv.h>
       extern struct state _res;

       int res_init(void);

       int res_query(const char *dname, int class, int type,
              unsigned char *answer, int anslen);

       int res_search(const char *dname, int class, int type,
              unsigned char *answer, int anslen);

       int dn_expand(unsigned char *msg, unsigned char *eomorig,
              unsigned char *comp_dn, unsigned char *exp_dn,
              int length);
_res: 这个结构体用于保存相关的状态信息. 它的定义在<resolv.h>中.

res_init: 读取配置文件并修改环境变量:LOCALDOMAIN. 在调用其他地址解析函数前通常要先调用res_init. 如果执行成功, 函数返回0; 否则返回-1.

res_query:  用来发出一个指定类(由参数class指定)和类型(由参数type指定)的DNS询问. dname是要查询的主机名. 返回信息被存储在answser指向的内存区域中. 信息的长度不能大于anslen个字节. 这个函数会创建一个DNS查询报文并把它发送到指定的DNS服务器.

res_search: 和res_query的行为类似, 与res_query不同的是, 当域名中不包含点时, 会在域名后面加上默认域名; 同时, 支持递归查询(即当一个服务器没有存储询问的信息时, 会继续向其他服务器询问). 一般情况下尽量使用res_search. 因为它的成功几率会比较大.

res_query和res_search函数返回值是响应报文的长度; 如果发生错误则返回-1.

dn_expand: 上一节中已经说到, DNS报文中主机名是以一种特殊格式存储的. dn_expand函数则是将这种特殊格式存储的字符串还原成一般格式. msg参数值是整个DNS报文的首地址; eomorig参数指向DNS报文的最后一个字节后的一字节, 用于指定报文的结束位置; comp_dn参数指向报文中需要被还原的主机名的首地址; 还原后的主机名被存储在exp_dn指向的内存区域中, 长度不大于length个字节. 函数返回主机名在DNS报文中的长度(即被还原前的长度); 如果发生错误则返回-1.

需要注意的是, 如果程序中用到了这些地址解析函数, 那么在编译的时候需要加上-lresolv选项才能正常编译.

利用这些地址解析函数, 不仅可以完成A类查询或PTR查询, 还可以进行其他类型的询问. 下一节将给出利用地址解析函数进行MX查询的实例.

4        地址解析函数应用实例---MX查询
在发送电子邮件时, 需要用到MX(Mail eXchange)记录. 一个电子邮箱的地址是 "用户名@域名" 的格式. 当要给某个用户发送电子邮件时, 首先需要从这个用户的电子邮箱地址中得到域名; 然后向DNS服务器发出一个MX查询, 询问该域名由哪些服务器负责处理.DNS服务器会返回处理该域名的服务器的主机名. 每个主机名对应一个16bit的整数值, 该值称为优先值(preference value), 如果一个域存在多条MX记录, 则首先使用优先值较小的主机名. 之后, 就是利用SMTP协议与相应的主机进行连接并发送邮件.

如果要发出一个MX查询, 可以利用host命令:

        [monnand@monnand-host ~]$ host -t mx gmail.com
        gmail.com mail is handled by 50 gsmtp183.google.com.
        gmail.com mail is handled by 5 gmail-smtp-in.l.google.com.
        gmail.com mail is handled by 10 alt1.gmail-smtp-in.l.google.com.
        gmail.com mail is handled by 10 alt2.gmail-smtp-in.l.google.com.
        gmail.com mail is handled by 50 gsmtp163.google.com.
-t 选项用于指明查询类型, -t mx表示发起一个MX查询. 后面的参数是要查询的域名(这里以gmail.com为例).
显示出的是关于查询域名的MX记录. 这里关于gmail.com的MX记录共5条, 每条记录都有相应的优先值(显示在主机名前面), 例如第一条记录的优先值是50.

下面, 我们就利用前面讲到的地址解析函数来实现一个类似功能的程序. 即指定查询域名, 打印出关于这个域名的MX记录. 该程序的代码是从qmail的代码中精简出来的, 其中去掉了一些错误检测, 并修改了与qmail其他部分相关联的代码, 使整个程序能够独立出来(当然, 因为去掉了很多错误检测, 程序也失去了原有的健壮性和安全性). 但是整体的思路基本没有大的改动. 有兴趣的读者可以自己阅读qmail的源代码. 相信会有更多的收获.

整个程序被写到了两个文件中, 分别名为dns.h和dns.c.

以下是dns.h中的内容:

1        #ifndef DNS_H
2        #define        DNS_H
3         
4        #define        DNS_MSG_END        -2
5         
6        #define        dns_mx_query(str)        dns_resolve((str),T_MX)
7        #define        dns_mx_expand()                dns_findmx(T_MX)
8         
9        #define        foreach_mxrr(p,dn)        while(dns_mx_expand()!=DNS_MSG_END        /
10                                                &&(!dns_get_mxrr(&p,dn,MAXDNAME)))
11         
12        void dns_init(void);
13        int dns_get_mxrr(unsigned short *,unsigned char *,unsigned int);
14        int dns_resolve(char *,int);
15        int dns_findmx(int);
16         
17        #endif /* #ifndef MONNAND_DNS_H */
该文件中声明了4个函数. 为了便于操作, 定义了三个宏. 关于其中具体的用法, 之后会有介绍. 下面给出dns.c中的源代码:

1        #include <stdio.h>
2        #include <stdlib.h>
3        #include <string.h>
4        #include <netdb.h>
5        #include <sys/types.h>
6        #include <netinet/in.h>
7        #include <arpa/nameser.h>
8        #include <resolv.h>
9        #include <errno.h>
10         
11        #include "dns.h"
12         
13        extern int res_query();
14        extern int res_search();
15        extern int errno;
16        extern int h_errno;
17         
18        static unsigned short getshort(unsigned char *c) { unsigned short u; u = c[0]; return (u << 8) + c[1]; }
19         
20        static union { HEADER hdr; unsigned char buf[PACKETSZ]; } response;
21        static int responselen;
22        static unsigned char *responseend;
23        static unsigned char *responsepos;
24        static int numanswers;
25        static char name[MAXDNAME];
26        unsigned short pref;
27         
28        int dns_resolve(char *domain,int type)
29        {
30                int n;
31                int i;
32                errno=0;
33                if(NULL == domain)
34                        return -1;
35                responselen = res_search(domain,C_IN,type,response.buf,sizeof(response));
36                if(responselen <= 0)
37                        return -1;
38                if(responselen >= sizeof(response))
39                        responselen = sizeof(response);
40                responseend = response.buf + responselen;
41                responsepos = response.buf + sizeof(HEADER);
42                n = ntohs(response.hdr.qdcount);
43                while(n-->0)
44                {
45                        i = dn_expand(response.buf,responseend,responsepos,name,MAXDNAME);
46                        responsepos += i;
47                        i = responseend - responsepos;
48                        if(i < QFIXEDSZ) return -1;
49                        responsepos += QFIXEDSZ;
50                }
51                numanswers = ntohs(response.hdr.ancount);
52                return numanswers;
53        }
54         
55        int dns_findmx(int wanttype)
56        {
57                unsigned short rrtype;
58                unsigned short rrdlen;
59                int i;
60         
61                if(numanswers <=0) return DNS_MSG_END;
62                numanswers--;
63                if(responsepos == responseend) return -1;
64                i = dn_expand(response.buf,responseend,responsepos,name,MAXDNAME);
65                if(i < 0) return -1;
66                responsepos += i;
67                i = responseend - responsepos;
68                if(i < 10) return -1;
69                rrtype = getshort(responsepos);
70                rrdlen = getshort(responsepos + 8);
71                responsepos += 10;
72                if(rrtype == wanttype)
73                {
74                        if(rrdlen < 3)
75                                return -1;
76                        pref = (responsepos[0] << 8) + responsepos[1];
77                        memset(name,0,MAXDNAME);
78                        if(dn_expand(response.buf,responseend,responsepos + 2,name,MAXDNAME) < 0)
79                                return -1;
80                        responsepos += rrdlen;
81                        return strlen(name);
82                }
83                responsepos += rrdlen;
84                return 0;
85        }
86         
87        void dns_init()
88        {
89                res_init();
90                memset(name,0,MAXDNAME);
91        }
92         
93        int dns_get_mxrr(unsigned short *p,unsigned char *dn,unsigned int len)
94        {
95                *p = pref;
96                strncpy(dn,name,len);
97                if(len < (strlen(name)+1))
98                        return -1;
99                return 0;
100        }
101         
102        int main(int argc, char *argv[])
103        {
104                char dname[MAXDNAME];
105                int i;
106                unsigned short p;
107                dns_init();
108                if(argc!=2)
109                {
110                        fprintf(stderr,"bad argument/n");
111                        exit(-1);
112                }
113                i = dns_mx_query(argv[1]);
114                if(i<0)
115                {
116                        fprintf(stderr,"err/n");
117                        return 0;
118                }
119                printf("pref/tdomain name/n");
120                foreach_mxrr(p,dname)
121                {
122                        printf("%d/t%s/n",p,dname);
123                }
124                return 0;
125        }
注释:
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[/code]



__________________________________

生活日志:
http://monnand.linuxsky.net
技术文章:
http://blog.csdn.net/monnand/
联系方式:
MSN:monnand@yahoo.com.cn
E-mail:monnand@yahoo.com.cn
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值