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

发布时间 2023-07-02 16:32:40作者: SkyOnSky

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 <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

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 生成此执行文件。下面是执行此函数的结果图片:

rep_img

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

  • 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.53
nameserver 8.8.8.8
options edns0

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

//打开resolve.conf
openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=736, ...}) = 0
read(3, "# This file is managed by man:sy"..., 4096) = 736
read(3, "", 4096)                       = 0
close(3)

//通过resolve.conf的dns服务器,查询dns
//这里有个小知识:dns服务器的默认端口是53,详细可以通过/etc/protocol,/etc/services查看
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, 16) = 0
poll([{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) = 38
poll([{fd=3, events=POLLIN}], 1, 5000)  = 1 ([{fd=3, revents=POLLIN}])
ioctl(3, FIONREAD, [70])                = 0
recvfrom(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]) = 70
close(3)                                = 0

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

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

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

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

gdb ./test
b 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=<optimized out>, 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=<optimized out>, service=<optimized out>, 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=<synthetic pointer>, 
    v_circuit=<synthetic pointer>, ns=0, terrno=0x7fffffffc0a8, anssizp=0x7fffffffc1d0, ansp=0x7fffffffc098, buflen2=0, buf2=0x0, buflen=38, 
    buf=0x7fffffffc200 "\254\212\001", statp=<optimized out>) 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=<optimized out>, ans@entry=0x7fffffffcd10 "cxaPfi", anssiz=<optimized out>, ansp=<optimized out>, ansp2=<optimized out>, nansp2=<optimized out>, 
    resplen2=<optimized out>, ansp2_malloced=<optimized out>) 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=<optimized out>, 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=<optimized out>, service=<optimized out>, 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地址,端口号)如下图:

rep_img

  因此我们得到了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的解析过程是有了一个比较清晰的了解了,以后遇到类似的问题也能够有一个好的思路来解决问题。

参考文献




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

PS: 请尊重原创,不喜勿喷。

PS: 要转载请注明出处,本人版权所有。

PS: 有问题请留言,看到后我会第一时间回复。