DNS :

      DNS是一个分层级 (hierarchical ),分布式(decentralized)的网络数据库,完成主机名称和IP地址之间的相互映射。DNS名称空间包含一个树状结构,树根没有命名, 下面是树的最高层为顶级域名, 顶级域名包括: 顶级域名(gTLD),国家代码顶级域名(ccTLD), 国际化国家设施顶级域名(infrastructure TLD), 下面一层是权威域名服务器。其中权威域名服务器对DNS请求进行应答返回IP地址。

图片来自DNS wiki

DNS协议:

dns协议包含两部分:

        1. 对DNS特定名称查询的查询、响应协议

        2. 名称服务器用户交换数据库记录的协议(区域传输)

       应用访问DNS通过地址解析器(resolver), 通常通过TCP,UDP协议通讯是需要将主机名转换为IPv4, IPv6地址。

gethostbname:

        需要socket通过域名方式完成连接 , 可以通过API gethostbyname 函数完成。gethostbyname 函数是 glibc 库中提供一个域名解析的函数。 调用方法也相对简单,存在问题是范围结果是 static 类型变量,存在数据被覆盖的情况。

函数定义:

1
2
3
       #include <netdb.h>
       extern int h_errno;
       struct hostent *gethostbyname(const char *name);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
       The hostent structure is defined in <netdb.h> as follows:

       struct hostent {
               char  *h_name;            /* official name of host */
               char **h_aliases;         /* alias list */
               int    h_addrtype;        /* host address type */
               int    h_length;          /* length of address */
               char **h_addr_list;       /* list of addresses */
           }
           #define h_addr h_addr_list[0] /* for backward compatibility */

DEMO程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <sys/types.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <errno.h>

int main(int argc, char *argv[])
{
	struct hostent *hp=NULL;
	struct in_addr ip_addr;
	char str[64];

	if (argc <= 1){
		printf("argc <1 \n");
		exit(EXIT_FAILURE);
	}
	hp = gethostbyname(argv[1]);
	if (!hp){
		printf("%s was not resolved %d\n", argv[1], errno);
		exit(EXIT_FAILURE);
	}
	char **pptr;
	for (pptr = hp->h_addr_list; *pptr!=NULL; pptr++) {
		printf("HostName: %s was resolved to :%s \n",
				argv[1], inet_ntop(hp->h_addrtype, *pptr, str, sizeof(str)));
	}
	exit(EXIT_SUCCESS);
}

gethostbyname 的glibc2.29版本实现:

下载glibc2.29 版本学习了下gethostbyname 代码:

1. 函数声明

     netdb.h

     #define DECLARE_NSS_PROTOTYPES(service)

      extern enum nss_status _nss_ ## service ## _gethostbyname_r      \

         (const char *name, struct hostent *host, char *buffer, \

            size_t buflen, int *errnop, int *h_errnop); 

2. 函数定义:

     gethostbyname实现比较特殊 使用 nss/getXXbyYY.c, nss/getXXbyYY_r.c  两个文件, 通过宏定义的方式定义了模板,inet/gethstbynm.c ,inet/gethstbynm_r.c  nscd/nscd_gethst_r.c中设置了相关参数信息,与模板组合完成函数定义。

3.函数实现:

getXXbyYY.c :实现函数主题流程

1). 生成锁

2). 申请buffer空间

3). 判断 传入主机名称是否IP地址

4).  调用  INTERNAL,针对gethostbyname内部函数指定为 gethostbyname_r,调用过程中出现ERANGE时, 通过realloc 2倍方式动态扩大内存;

5).  释放锁;

getXXbyYY_r.c : 函数具体实现方法

1). 判断 传入主机名称是否IP地址,并处理

2). 判断是否支持nscd(编译时通过USE_NSCD指定),存在从nscd中获取

3). 初始化处理动作查询表。也是通过宏定义  DB_LOOKUP_FCT,指定第一个处理方法

4). 动态库方法: 通过 DL_CALL_FCT调用方法

                 静态库方法: 通过function.def定义的DEFINE_GETBY

  # define DEFINE_GETBY(h,nm,ky) \
{ #h"_get"#nm"by"#ky"_r", _nss_##h##_get##nm##by##ky##_r },
static struct fct_tbl { const char *fname; void *fp; } *tp, tbl[] =

5).   根据返回结果是否成功,成功 并判断是否需要合并处理,将结果累计存储到mergebuf中,否则进行失败处理。

6).   如果存在多个方法通过__nss_next2 指向一下方法通过4,5调用

4. 域名查询方法初始化与nsswith.conf:

          XXX-lookup.c, nss/nsswitch.c: 进行解析式的具体方法列表创建,查找返回

1).    宏定义在nss/XXX-lookup.c  nss/hosts-lookup.c完成hosts(域名查询) 方法定义

2). __nss_database_lookup 通过nss_parse_file解析nsswitch.conf文件, 完成 service_user列表初始化,指定开始执行的方法 。

3) .__nss_lookup根据支持的action, 通过动态加载模式,__nss_lookup_function, 找到指定的函数并返回

5. dns,files nscd方式实现:

          域名解析顺序可以通过 /etc/nsswitch.conf 文件中的hosts选项进行设置, 先使用 files方式 (/etc/hosts文件中的配置信息),还是使用dns 方式 (通过/etc/resolv.conf 配置域名服务器通过域名解析进行)

    dns方式:

resolv/nss_dns/dns-host.c  

函数调用链

_nss_dns_gethostbyname3_r ->

gethostbyname3_context->

__res_context_search->       

getanswer_r->

files方式:

   nss/nss_files/files-host.c

函数调用链:

_nss_files_gethostbyname3_r->

gethostbyname3_multi         

   nscd

          nscd/nscd_gethst_r.c

          函数调用链:

          __nscd_gethostbyname_r->

          nscd_gethst_r

sudo apt update
sudo apt install nscd

跟踪gethostbyname的域名解析过程:

环境:

OS:Ubuntu 17.10  (lsb_release -a)  4.13.0-21-generic (uname -a)

glibc :  GNU C Library (Ubuntu GLIBC 2.26-0ubuntu2.1) stable release version 2.26 (libc.so.6 )

gcc: gcc version 7.2.0 (Ubuntu 7.2.0-8ubuntu3.2)

         准备: 

         1)demo编译:

        gcc gethostbyname.c -o gethostbyname

        2) 配置文件:

         /etc/nsswitch.conf 中

         hosts: files dns

         /etc/resolv.conf

         nameserver  223.5.5.5

                  service nscd status: inactive

 strace ./gethostbyname www.baidu.com > gethostbyname.default.log 2>&1  

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
  1 execve("./gethostbyname", ["./gethostbyname", "www.baidu.com"], [/* 22 vars */]) = 0
...
//载入resolv.conf host.conf nsswitch.conf 文件
 26 stat("/etc/resolv.conf", {st_mode=S_IFREG|0644, st_size=241, ...}) = 0
 27 open("/etc/host.conf", O_RDONLY|O_CLOEXEC) = 3
 28 fstat(3, {st_mode=S_IFREG|0644, st_size=92, ...}) = 0
 29 read(3, "# The \"order\" line is only used "..., 4096) = 92
 30 read(3, "", 4096)                       = 0
 31 close(3)                                = 0
 32 open("/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 3
 33 fstat(3, {st_mode=S_IFREG|0644, st_size=241, ...}) = 0
 34 read(3, "# This file is managed by man:sy"..., 4096) = 241
 35 read(3, "", 4096)                       = 0
 36 close(3)                                = 0
...
//ncsd  
uname({sysname="Linux", nodename="vm-134", ...}) = 0
 38 socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 3
 39 connect(3, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
 40 close(3)                                = 0
 41 socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 3
 42 connect(3, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
 43 close(3)                                = 0
...

 44 open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
 45 fstat(3, {st_mode=S_IFREG|0644, st_size=497, ...}) = 0
 46 read(3, "# /etc/nsswitch.conf\n#\n# Example"..., 4096) = 497
 47 read(3, "", 4096)                       = 0
 48 close(3)                                

// 链接动态库根据nsswitch 查找相关函数 这里由于配置了files先查找libnss_files.so.2动态库中指定函数
 54 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 3
 55 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360!\0\0\0\0\0\0"..., 832) = 832
 56 fstat(3, {st_mode=S_IFREG|0644, st_size=47608, ...}) = 0
...
 64 open("/etc/hosts", O_RDONLY|O_CLOEXEC)  = 3
 65 fstat(3, {st_mode=S_IFREG|0644, st_size=348, ...}) = 0
 66 read(3, "127.0.0.1\tlocalhost\n127.0.1.1\twe"..., 4096) = 348
 67 read(3, "", 4096)                       = 0
 68 close(3)    

// 没有结果,通过dns方式查找相关函数, 最终走的是res_search等相关方法
74 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libnss_dns.so.2", O_RDONLY|O_CLOEXEC) = 3
 75 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\20\0\0\0\0\0\0"..., 832) = 832
 76 fstat(3, {st_mode=S_IFREG|0644, st_size=27016, ...}) =  0
...
 82 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libresolv.so.2", O_RDONLY|O_CLOEXEC) = 3
 83 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p8\0\0\0\0\0\0"..., 832) = 832
 84 fstat(3, {st_mode=S_IFREG|0644, st_size=97136, ...}) = 0

// 可以看到基于UDP协议进行了域名查询
 93 socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3
 94 connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("223.5.5.5")}, 16) = 0
 95 poll([{fd=3, events=POLLOUT}], 1, 0)    = 1 ([{fd=3, revents=POLLOUT}])
 96 sendto(3, "B\217\1\0\0\1\0\0\0\0\0\0\3www\5baidu\3com\0\0\1\0\1", 31, MSG_NOSIGNAL, NULL, 0) = 31
 97 poll([{fd=3, events=POLLIN}], 1, 5000)  = 1 ([{fd=3, revents=POLLIN}])
 98 ioctl(3, FIONREAD, [90])                = 0
 99 recvfrom(3, "B\217\201\200\0\1\0\3\0\0\0\0\3www\5baidu\3com\0\0\1\0\1\300"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("223.5.5. 5")}, [28->16]) = 90
100 close(3)                                = 0

                            

启动 nscd 后(service nscd)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 //nscd 走的是TCP协议
38 socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 3
 39 connect(3, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = 0
 40 sendto(3, "\2\0\0\0\r\0\0\0\6\0\0\0hosts\0", 18, MSG_NOSIGNAL, NULL, 0) = 18
 41 poll([{fd=3, events=POLLIN|POLLERR|POLLHUP}], 1, 5000) = 1 ([{fd=3, revents=POLLIN|POLLHUP}])
 42 recvmsg(3, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="hosts\0", iov_len=6}, {iov_base="\310O\3\0\0\0\0\0", iov_len=8}], msg_iovlen=2, msg_control=[{    cmsg_len=20, cmsg_level=SOL_SOCKET, cmsg_type=SCM_RIGHTS, cmsg_data=[4]}], msg_controllen=20, msg_flags=MSG_CMSG_CLOEXEC}, MSG_CMSG_CLOEXEC) = 14
 43 mmap(NULL, 217032, PROT_READ, MAP_SHARED, 4, 0) = 0x7fb454ef8000
 44 close(4)                                = 0
 45 close(3)                                = 0
 46 socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 3
 47 connect(3, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = 0
 48 sendto(3, "\2\0\0\0\4\0\0\0\16\0\0\0www.baidu.com\0", 26, MSG_NOSIGNAL, NULL, 0) = 26
 49 poll([{fd=3, events=POLLIN|POLLERR|POLLHUP}], 1, 5000) = 1 ([{fd=3, revents=POLLIN|POLLHUP}])
 50 read(3, "\2\0\0\0\1\0\0\0\21\0\0\0\1\0\0\0\2\0\0\0\4\0\0\0\2\0\0\0\0\0\0\0", 32) = 32
 51 readv(3, [{iov_base="www.a.shifen.com\0", iov_len=17}, {iov_base="\16\0\0\0", iov_len=4}, {iov_base="o\rd[o\rd\\", iov_len=8}], 3) = 29
 52 read(3, "www.baidu.com\0", 14)          = 14
 53 close(3)                                = 0
 54 fstat(1, {st_mode=S_IFREG|0664, st_size=3702, ...}) = 0
 55 write(1, "HostName: www.baidu.com was reso"..., 112HostName: www.baidu.com was resolved to :111.13.100.91

nsswitch.conf merge action:

        概述及疑问:

          nsswitch.conf 文件大致格式如下:

           database1:  service1[STATUS=ACTION], service2

           …..

          可以配置 database , 如hosts ,passwd等,通过调整它们的service 顺序,来设置哪个services先被查询, 如果存在多个services,可以通过service查询结果状态设置不通的action 目前包含 return, continue, merge。 

        这个验证了下merge action

        验证过程:

调整   /etc/nsswitch.conf 

hosts: files [SUCCESS=merge] dns

./gethostbyname www.baidu.com
www.baidu.com was not resolved 22 (EINVAL)

         问题分析: 

         1)源码:

在getXXbyYY_r. c 处理逻辑中是有merge 操作的, 调用了

__merge_einval函数, 目前这个函数直接返回  EINVAL。 

         2) 文档:

merge [SUCCESS=merge] is used between two database entries. When a group is located in the first of the two group entries, processing will continue on to the next one.

1
....

          nswitch.conf merge目前只支持 group database

参考文档

DNS wiki

man gethostbyname

man  nsswitch.conf

探探gethostbyname

gethostbyname与DNS