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

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

  • 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地址,端口号)如下图:

  因此我们得到了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: 有问题请留言,看到后我会第一时间回复。

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

  1. [大数据从入门到放弃系列教程]第一个spark分析程序

    [大数据从入门到放弃系列教程]第一个spark分析程序 原文链接:http://www.cnblogs.com/blog5277/p/8580007.html 原文作者:博客园--曲高终和寡 **** ...

  2. OpenStack从入门到放弃

    OpenStack从入门到放弃 目录: 为何选择云计算/云计算之前遇到的问题 什么是云计算 云服务模式 云应用形式 传统应用与云感知应用 openstack及其相关组件介绍 flat/vlan/gre ...

  3. Android -- 带你从源码角度领悟Dagger2入门到放弃

    1,以前的博客也写了两篇关于Dagger2,但是感觉自己使用的时候还是云里雾里的,更不谈各位来看博客的同学了,所以今天打算和大家再一次的入坑试试,最后一次了,保证最后一次了. 2,接入项目 在项目的G ...

  4. Android -- 带你从源码角度领悟Dagger2入门到放弃(二)

    1,接着我们上一篇继续介绍,在上一篇我们介绍了简单的@Inject和@Component的结合使用,现在我们继续以老师和学生的例子,我们知道学生上课的时候都会有书籍来辅助听课,先来看看我们之前的Stu ...

  5. Struts2入门到放弃

    写在前面------------------------------------------------------------------------- 本文章主要从三个方面来学习Struts2框架 ...

  6. OkHttp框架从入门到放弃,解析图片使用Picasso裁剪,二次封装OkHttpUtils,Post提交表单数据

    OkHttp框架从入门到放弃,解析图片使用Picasso裁剪,二次封装OkHttpUtils,Post提交表单数据 我们这片博文就来聊聊这个反响很不错的OkHttp了,标题是我恶搞的,本篇将着重详细的 ...

  7. Android -- 带你从源码角度领悟Dagger2入门到放弃(一)

    1,以前的博客也写了两篇关于Dagger2,但是感觉自己使用的时候还是云里雾里的,更不谈各位来看博客的同学了,所以今天打算和大家再一次的入坑试试,最后一次了,保证最后一次了. 2,接入项目 在项目的G ...

  8. [Python 从入门到放弃] 6. 文件与异常(二)

    本章所用test.txt文件可以在( [Python 从入门到放弃] 6. 文件与异常(一))找到并自行创建 现在有个需求,对test.txt中的文本内容进行修改: (1)将期间的‘:’改为‘ sai ...

  9. 30分钟Git命令“从入门到放弃”

    git 现在的火爆程度非同一般,它被广泛地用在大型开源项目中,但是初学者非常容易“从入门到放弃”,各种命令各种参数,天哪,宝宝要吓哭了.实际上新手并不需要了解所有命令的用途,学习是需要一个循序渐进的过 ...

  10. Java性能测试从入门到放弃-概述篇

    Java性能测试从入门到放弃-概念篇 辅助工具 Jmeter: Apache JMeter是Apache组织开发的基于Java的压力测试工具.用于对软件做压力测试.JMeter 可以用于对服务器.网络 ...

随机推荐

  1. 一句话总结Docker与K8S的关系

    一句话总结:Docker只是容器的一种,它面向的是单体,K8S可以管理多种容器,它面向的是集群,Docker可以作为一种容器方案被K8S管理.下文继续具体介绍. 1.容器的核心概念 介绍这几个核心概念 ...

  2. 使用SecureCRT的按钮实现快速查询

    背景:有这么个日常运维场景,客户因管理需求,不允许在服务器上部署任何自动化的脚本,需要人工登录到机器上查询ASM磁盘组的使用率情况,有上百套环境. 使用的工具是SecureCRT,如何提升一些效率呢? ...

  3. .NET Core开发实战(第7课:用Autofac增强容器能力)--学习笔记(下)

    07 | 用Autofac增强容器能力:引入面向切面编程(AOP)的能力 如何获取没有命名的服务呢? // Autofac 容器获取实例的方式是一组 Resolve 方法 var service = ...

  4. 深入浅出 Application Insights--学习笔记

    摘要 介绍如何将 Application Insights 用于生产上实践,并透过它发现/诊断问题.同时也会介绍如何将 Application Insighs 与其他体系相集成实现 Devops(与发 ...

  5. JS leetcode 有多少小于当前数字的数字 解题分析,你应该了解的桶排序

    壹 ❀ 引 刷题其实一直没断,只是这两天懒得整理题目...那么今天来记录一道前天做的题,题目本身不难,不过拓展起来还是有些东西可以讲,题目来自leetcode有多少小于当前数字的数字,题目描述如下: ...

  6. NC14704 美味菜肴

    题目链接 题目 题目描述 小明是个大厨,早上起来他开始一天的工作.他所在的餐厅每天早上都会买好 \(n\) 件食材(每种食材的数量可以视为无限),小明从到达餐厅开始就连续工作 \(T\) 时间.每道菜 ...

  7. pip指定镜像安装

    清华大学开源软件镜像站

  8. USB至串口TTL转接设备及Console线

    USB转串口常见芯片方案 FT232, FTDI(英国) 公认稳定可靠, 传输速率3Mbps, 功能最强, 单芯片内置SPI,TWI,JTAG,GPIO等功能. FT232BM为较早型号, FT232 ...

  9. Oracle设置和删除不可用列

    Oracle设置和删除不可用列 1.不可用列是什么? 就是表中的1个或多个列被ALTER TABLE-SET UNUSED 语句设置为无法再被程序利用的列. 2.使用场景? If you are co ...

  10. uber-go guide,uber的go编码规范

    uber-go guide,uber的go语言编码规范 感谢翻译者和原作们 本文转自:https://github.com/xxjwxc/uber_go_guide_cn (特此感谢作者的翻译,感谢他 ...