最新要闻

广告

手机

光庭信息跌4.57% 2021上市超募11亿2022扣非降74% 时快讯

光庭信息跌4.57% 2021上市超募11亿2022扣非降74% 时快讯

搜狐汽车全球快讯 | 大众汽车最新专利曝光:仪表支持拆卸 可用手机、平板替代-环球关注

搜狐汽车全球快讯 | 大众汽车最新专利曝光:仪表支持拆卸 可用手机、平板替代-环球关注

家电

LinuxDNS分析从入门到放弃(记一次有趣的dns问题排查记录,ping 源码分析,getaddrinfo源码分析)

来源:博客园

PS:要转载请注明出处,本人版权所有。PS: 这个只是基于《我自己》的理解,如果和你的原则及想法相冲突,请谅解,勿喷。

环境说明

ubuntu 18.04


(资料图)

前言

我们这里有一块嵌入式板卡,当我们通过PING测试内网IP时,发现外网IP访问正常,但是测试域名访问一直报unknown host。一般来说,在ubuntu里面,我们遇到域名不能访问,反手就是去设置/etc/resolv.conf文件里面的nameserver为114.114.114.114。但是万万没有想到,这次我们这样改了之后,还是访问报unknown host。

当时我就人麻了,但是我仿佛灵光一闪,想到了一个问题:为何以前我们修改了/etc/resolv.conf文件就DNS就正常了,但是这里同样修改了这个文件,就不正常工作?此外,/etc/resolv.conf文件是干嘛的,为何改了DNS就有效了?

要回答以上问题,根本原因就是要了解Linux平台里面DNS的详细工作流程,于是我踏上了看源码之旅。

探索之旅

在这里,我们第一件事情就是想到,当我们ping一个域名,或者我们在浏览器里面输入了一个域名,然后访问的那一刻发生了什么?

首先,当我们进行网络编程的时候,我们不管是send udp msg还是send tcp msg,我们都是需要构建一个struct sockaddr结构,这个结构里面包含了访问的地址,端口等等信息。但是这里的地址是ip地址,并不是域名地址,因此可以猜测,在浏览器访问或者ping域名之前,肯定做了dns查询工作,然后将域名转换之后,得到ip地址才构建出struct sockaddr结构,然后才开始网络访问的操作的。

下面我去翻阅了ping的源码,在https://git.savannah.gnu.org/cgit/inetutils.git/tree/ping/libping.c?h=v2.4 中的ping_set_dest 函数中,有一个将host转换为struct sockaddr的方法引起了我的注意,他就是:getaddrinfo / gethostbyname。下面是ping的简要工作流程:

  1. 在https://git.savannah.gnu.org/cgit/inetutils.git/tree/ping/ping.c?h=v2.4中,main函数中调用了argp_parse,这里面会调用parse_opt,在这里有一个重要的ping_type函数指针在这里设置了,如果我们不配置任何参数的话,那么就是调用的ping_echo函数。
  2. 在https://git.savannah.gnu.org/cgit/inetutils.git/tree/ping/ping.c?h=v2.4中,main函数中调用了ping_init,初始化了ICMP的socket,并记录到句柄PING中。
  3. 在经历各种初始化后,就会调用ping_type,后面我们默认分析ping_echo。
  4. 在https://git.savannah.gnu.org/cgit/inetutils.git/tree/ping/ping_echo.c?h=v2.4中,ping_echo中回去调用ping_set_dest设置参数,同时调用ping_run循环通过send_to发送ping icmp报文。
  5. 在https://git.savannah.gnu.org/cgit/inetutils.git/tree/ping/libping.c?h=v2.4中,ping_set_dest 调用getaddrinfo解析域名。

getaddrinfo / gethostbyname

首先,我们在查这两个方法之前,可以猜测他们的一个功能就是:这两个函数可能拿着域名,去访问了域名服务器,然后解析出ip地址,并且构造了struct sockaddr结构并返回。

因此我们去看看这两个函数相关的man介绍和源码:根据man手册:https://man7.org/linux/man-pages/man3/getaddrinfo.3.html ,我们可以知道,getaddrinfo已经包含了gethostbyname的工作,因此我们就只需要分析这个方法就行。

getaddrinfo 参数分析

我们先来看看这个函数的参数含义

//重要的参数结构体struct addrinfo {    int              ai_flags;    int              ai_family;    int              ai_socktype;    int              ai_protocol;    socklen_t        ai_addrlen;    struct sockaddr *ai_addr;    char            *ai_canonname;    struct addrinfo *ai_next;};int getaddrinfo (const char *name, const char *service, const struct addrinfo *hints, struct addrinfo **pai);//根据https://man7.org/linux/man-pages/man3/getaddrinfo.3.html,我们可以知道://name: 域名//service: 服务,注意这里这个服务会让我们重新复习一遍计算机网络。//hints: 就是填写struct addrinfo里面的属性,然后getaddrinfo根据这些特殊指定的属性,将对应的struct addrinfo返回回来。//pai: 此函数返回的一个链表,其中包含了符合要求的struct addrinfo信息。//其实我们从pai可以知道,当返回有值时,意味着我们知道了一个域名和服务对应的网络地址,然后我们就可以在connect,bind中使用pai->ai_addr了。
getaddrinfo 例子调试分析

注意:我这里是事后写本文的时候,才发现需要下面的内容。由于getaddrinfo的实现特殊性,其不是很方便查看源码,因此采取strace + gdb + simple example方式来分析此函数的实现。

首先下面是我的simple example

#include #include #include #include #include #include #include #include int main(int argc, char * argv[]){struct addrinfo hints, *res;  memset (&hints, 0, sizeof (hints));  hints.ai_family = AF_INET;  hints.ai_flags = AI_CANONNAME;if (0 > getaddrinfo("baidu.com", NULL, &hints, &res)){perror("get error:");return -1;}struct sockaddr_in * get_addr = res->ai_addr;printf("ip for baidu: %s\n", inet_ntoa(get_addr->sin_addr));return 0;}

然后gcc test.c -o test -g 生成此执行文件。下面是执行此函数的结果图片:

下面是一些读取源码需要的单词简写:

  • IDNA: Internationalizing Domain Names in Applications
  • NSCD: name service cache daemon
  • NSS: Name Services Switch
  • CNAME: canonical name (规范化名字)

下面是resolv.conf的文件内容:

# resolve.conf 内容# This file is managed by man:systemd-resolved(8). Do not edit.## This is a dynamic resolv.conf file for connecting local clients to the# internal DNS stub resolver of systemd-resolved. This file lists all# configured search domains.## Run "systemd-resolve --status" to see details about the uplink DNS servers# currently in use.## Third party programs must not access this file directly, but only through the# symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way,# replace this symlink by a static file or a different symlink.## See man:systemd-resolved.service(8) for details about the supported modes of# operation for /etc/resolv.conf.# nameserver 127.0.0.53nameserver 8.8.8.8options edns0

下面是strace ./test 输出的部分节选(包含了/etc/resolv.conf的内容)

//打开resolve.confopenat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=736, ...}) = 0read(3, "# This file is managed by man:sy"..., 4096) = 736read(3, "", 4096)                       = 0close(3)//通过resolve.conf的dns服务器,查询dns//这里有个小知识:dns服务器的默认端口是53,详细可以通过/etc/protocol,/etc/services查看socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, 16) = 0poll([{fd=3, events=POLLOUT}], 1, 0)    = 1 ([{fd=3, revents=POLLOUT}])sendto(3, "5\244\1\0\0\1\0\0\0\0\0\1\5baidu\3com\0\0\1\0\1\0\0)\4\260"..., 38, MSG_NOSIGNAL, NULL, 0) = 38poll([{fd=3, events=POLLIN}], 1, 5000)  = 1 ([{fd=3, revents=POLLIN}])ioctl(3, FIONREAD, [70])                = 0recvfrom(3, "5\244\201\200\0\1\0\2\0\0\0\1\5baidu\3com\0\0\1\0\1\300\f\0\1\0"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 70close(3)                                = 0

首先我们要调试glibc的话,需要安装glibc的调试符号:

sudo apt-get install libc6-dbgcd /usr/src/glibctar -xf glibc-{version}.tar.xz

下面通过GDB 结合 GLIBC的debug info开始调试程序,分析出getaddrinfo中我想关注的部分。

首先我们调试得到打开/etc/resolv.conf的实现:

gdb ./testb fopen# 设置gdb搜索源文件目录,注意你下断点时,对应源码的相对路径(gdb) set directories /usr/src/glibc/glibc-2.27/xxxx

在多次运行后,抓到打开/etc/resolv.conf的地方,其堆栈如下:

#0  _IO_new_fopen (filename=filename@entry=0x7ffff7b98cea "/etc/resolv.conf", mode=mode@entry=0x7ffff7b9578c "rce") at iofopen.c:88#1  0x00007ffff7b25c0f in __resolv_conf_load (preinit=preinit@entry=0x0) at res_init.c:553#2  0x00007ffff7b28459 in __resolv_conf_get_current () at resolv_conf.c:162#3  0x00007ffff7b26cad in __res_vinit (statp=0x7ffff7dd1bc0 <_res>, preinit=preinit@entry=0) at res_init.c:609#4  0x00007ffff7b27e50 in maybe_init (preinit=false, ctx=0x555555756530) at resolv_context.c:122#5  context_get (preinit=false) at resolv_context.c:184#6  __GI___resolv_context_get () at resolv_context.c:195#7  0x00007ffff7ae790e in gaih_inet (name=name@entry=0x555555554924 "baidu.com", service=, req=req@entry=0x7fffffffdeb0, pai=pai@entry=0x7fffffffd9c8,     naddrs=naddrs@entry=0x7fffffffd9c4, tmpbuf=tmpbuf@entry=0x7fffffffda30) at ../sysdeps/posix/getaddrinfo.c:767#8  0x00007ffff7ae9c84 in __GI_getaddrinfo (name=, service=, hints=0x7fffffffdeb0, pai=0x7fffffffdea0) at ../sysdeps/posix/getaddrinfo.c:2300#9  0x000055555555483b in main (argc=1, argv=0x7fffffffdfd8) at test.c:17

因此我们得到了getaddrinfo的打开resolv.conf的调用路径,其值如下:getaddrinfo-->gaih_inet->__resolv_context_get()->context_get()->maybe_init()->__res_vinit()->__resolv_conf_get_current()->__resolv_conf_load() 这里面会打开resolv.conf

注意,当真正的打开了resolv.conf后,其还会解析此文件,这个部分就不在本文进行分析了。

用同样的方法,对connect下断点,得到了访问dns服务器的堆栈信息:

#0  __libc_connect (fd=3, addr=addr@entry=..., len=16) at ../sysdeps/unix/sysv/linux/connect.c:26#1  0x00007ffff71b4b20 in reopen (statp=statp@entry=0x7ffff7dd1bc0 <_res>, terrno=terrno@entry=0x7fffffffc0a8, ns=ns@entry=0) at res_send.c:977#2  0x00007ffff71b5e0b in send_dg (ansp2_malloced=0x0, resplen2=0x0, anssizp2=0x0, ansp2=0x0, anscp=0x7fffffffd168, gotsomewhere=,     v_circuit=, ns=0, terrno=0x7fffffffc0a8, anssizp=0x7fffffffc1d0, ansp=0x7fffffffc098, buflen2=0, buf2=0x0, buflen=38,     buf=0x7fffffffc200 "\254\212\001", statp=) at res_send.c:1078#3  __res_context_send (ctx=ctx@entry=0x555555756530, buf=buf@entry=0x7fffffffc200 "\254\212\001", buflen=buflen@entry=38, buf2=buf2@entry=0x0, buflen2=buflen2@entry=0,     ans=, ans@entry=0x7fffffffcd10 "cxaPfi", anssiz=, ansp=, ansp2=, nansp2=,     resplen2=, ansp2_malloced=) at res_send.c:522#4  0x00007ffff71b34d1 in __GI___res_context_query (ctx=ctx@entry=0x555555756530, name=name@entry=0x555555554924 "baidu.com", class=class@entry=1, type=type@entry=1,     answer=answer@entry=0x7fffffffcd10 "cxaPfi", anslen=anslen@entry=1024, answerp=0x7fffffffd168, answerp2=0x0, nanswerp2=0x0, resplen2=0x0, answerp2_malloced=0x0)    at res_query.c:216#5  0x00007ffff71b428d in __res_context_querydomain (domain=0x0, answerp2_malloced=0x0, resplen2=0x0, nanswerp2=0x0, answerp2=0x0, answerp=0x7fffffffd168, anslen=1024,     answer=0x7fffffffcd10 "cxaPfi", type=1, class=1, name=0x555555554924 "baidu.com", ctx=0x555555756530) at res_query.c:601#6  __GI___res_context_search (ctx=ctx@entry=0x555555756530, name=name@entry=0x555555554924 "baidu.com", class=class@entry=1, type=type@entry=1,     answer=answer@entry=0x7fffffffcd10 "cxaPfi", anslen=anslen@entry=1024, answerp=0x7fffffffd168, answerp2=0x0, nanswerp2=0x0, resplen2=0x0, answerp2_malloced=0x0)    at res_query.c:370#7  0x00007ffff73c7f0c in gethostbyname3_context (ctx=ctx@entry=0x555555756530, name=name@entry=0x555555554924 "baidu.com", af=af@entry=2,     result=result@entry=0x7fffffffd7d0, buffer=buffer@entry=0x7fffffffda40 "\377\002", buflen=buflen@entry=1024, errnop=0x7ffff7fca440, h_errnop=0x7ffff7fca4a4, ttlp=0x0,     canonp=0x7fffffffd7c8) at nss_dns/dns-host.c:218#8  0x00007ffff73c8928 in _nss_dns_gethostbyname3_r (name=name@entry=0x555555554924 "baidu.com", af=af@entry=2, result=result@entry=0x7fffffffd7d0,     buffer=0x7fffffffda40 "\377\002", buflen=1024, errnop=errnop@entry=0x7ffff7fca440, h_errnop=0x7ffff7fca4a4, ttlp=0x0, canonp=0x7fffffffd7c8) at nss_dns/dns-host.c:164#9  0x00007ffff7ae7e6f in gaih_inet (name=name@entry=0x555555554924 "baidu.com", service=, req=req@entry=0x7fffffffdeb0, pai=pai@entry=0x7fffffffd9c8,     naddrs=naddrs@entry=0x7fffffffd9c4, tmpbuf=tmpbuf@entry=0x7fffffffda30) at ../sysdeps/posix/getaddrinfo.c:885#10 0x00007ffff7ae9c84 in __GI_getaddrinfo (name=, service=, hints=0x7fffffffdeb0, pai=0x7fffffffdea0) at ../sysdeps/posix/getaddrinfo.c:2300#11 0x000055555555483b in main (argc=1, argv=0x7fffffffdfd8) at test.c:17

注意,这里connect的堆栈信息由于struct sockaddr的原因,无法判断是否真实访问的是8.8.8.8,因此我们可以尝试将其转换回(通过inet_ntoa转换ip,通过ntohs转换端口)struct sockaddr_in来分析,分析结果(IP地址,端口号)如下图:

因此我们得到了getaddrinfo的访问8.8.8.8 dns服务器的调用路径,其值如下:getaddrinfo-->gaih_inet->_nss_dns_gethostbyname3_r->gethostbyname3_context->__res_context_search->__res_context_querydomain->__res_context_query->__res_context_send->send_dg->reopen->__libc_connect 连接 dns服务器,解析dns

特别注意:

这里仅仅是描述了通过dns服务器得到ip的过程,其实相对于getaddrinfo的dns解析来说,这只是其中的一部分,还有通过类似nscd(有兴趣可以去看看man systemd-resolved)等等来解析的方式,这些内容感兴趣可以去看看。

后记

最后,回到了本文的开始,经过上述的分析后,我找到了我的问题的本质原因。我遇到的问题的本质原因是:由于某些特殊原因导致了connect 114.114.114.114:53 服务器的时候报错了,导致解析域名失败,当我换一个域名服务器(8.8.8.8)就解决了。

总的来说,本文从ping baidu.com入手,然后分析了ping的源码,得到了我们要分析的重点getaddrinfo。然后我们由于构造一个小例子来分析getaddrinfo的源码,最终得到了/etc/resolv.conf是因为什么原因生效的。

通过本文的分析,相信我们对DNS的解析过程是有了一个比较清晰的了解了,以后遇到类似的问题也能够有一个好的思路来解决问题。

参考文献

打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)

PS: 请尊重原创,不喜勿喷。PS: 要转载请注明出处,本人版权所有。PS: 有问题请留言,看到后我会第一时间回复。

关键词: