gethostbyname函数实现分析

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 类型变量,存在数据被覆盖的情况。

函数定义:

       #include <netdb.h>
       extern int h_errno;
       struct hostent *gethostbyname(const char *name);
       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程序:

#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 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)

 //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.
....
       
          nswitch.conf merge目前只支持 group database
 

 

参考文档

DNS wiki

man gethostbyname

man  nsswitch.conf

探探gethostbyname

gethostbyname与DNS

 

Be First to Comment

发表回复