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的简要工作流程:
- 在https://git.savannah.gnu.org/cgit/inetutils.git/tree/ping/ping.c?h=v2.4中,main函数中调用了argp_parse,这里面会调用parse_opt,在这里有一个重要的ping_type函数指针在这里设置了,如果我们不配置任何参数的话,那么就是调用的ping_echo函数。
- 在https://git.savannah.gnu.org/cgit/inetutils.git/tree/ping/ping.c?h=v2.4中,main函数中调用了ping_init,初始化了ICMP的socket,并记录到句柄PING中。
- 在经历各种初始化后,就会调用ping_type,后面我们默认分析ping_echo。
- 在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报文。
- 在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源码分析)的更多相关文章
- [大数据从入门到放弃系列教程]第一个spark分析程序
[大数据从入门到放弃系列教程]第一个spark分析程序 原文链接:http://www.cnblogs.com/blog5277/p/8580007.html 原文作者:博客园--曲高终和寡 **** ...
- OpenStack从入门到放弃
OpenStack从入门到放弃 目录: 为何选择云计算/云计算之前遇到的问题 什么是云计算 云服务模式 云应用形式 传统应用与云感知应用 openstack及其相关组件介绍 flat/vlan/gre ...
- Android -- 带你从源码角度领悟Dagger2入门到放弃
1,以前的博客也写了两篇关于Dagger2,但是感觉自己使用的时候还是云里雾里的,更不谈各位来看博客的同学了,所以今天打算和大家再一次的入坑试试,最后一次了,保证最后一次了. 2,接入项目 在项目的G ...
- Android -- 带你从源码角度领悟Dagger2入门到放弃(二)
1,接着我们上一篇继续介绍,在上一篇我们介绍了简单的@Inject和@Component的结合使用,现在我们继续以老师和学生的例子,我们知道学生上课的时候都会有书籍来辅助听课,先来看看我们之前的Stu ...
- Struts2入门到放弃
写在前面------------------------------------------------------------------------- 本文章主要从三个方面来学习Struts2框架 ...
- OkHttp框架从入门到放弃,解析图片使用Picasso裁剪,二次封装OkHttpUtils,Post提交表单数据
OkHttp框架从入门到放弃,解析图片使用Picasso裁剪,二次封装OkHttpUtils,Post提交表单数据 我们这片博文就来聊聊这个反响很不错的OkHttp了,标题是我恶搞的,本篇将着重详细的 ...
- Android -- 带你从源码角度领悟Dagger2入门到放弃(一)
1,以前的博客也写了两篇关于Dagger2,但是感觉自己使用的时候还是云里雾里的,更不谈各位来看博客的同学了,所以今天打算和大家再一次的入坑试试,最后一次了,保证最后一次了. 2,接入项目 在项目的G ...
- [Python 从入门到放弃] 6. 文件与异常(二)
本章所用test.txt文件可以在( [Python 从入门到放弃] 6. 文件与异常(一))找到并自行创建 现在有个需求,对test.txt中的文本内容进行修改: (1)将期间的‘:’改为‘ sai ...
- 30分钟Git命令“从入门到放弃”
git 现在的火爆程度非同一般,它被广泛地用在大型开源项目中,但是初学者非常容易“从入门到放弃”,各种命令各种参数,天哪,宝宝要吓哭了.实际上新手并不需要了解所有命令的用途,学习是需要一个循序渐进的过 ...
- Java性能测试从入门到放弃-概述篇
Java性能测试从入门到放弃-概念篇 辅助工具 Jmeter: Apache JMeter是Apache组织开发的基于Java的压力测试工具.用于对软件做压力测试.JMeter 可以用于对服务器.网络 ...
随机推荐
- 面试官:你能简单聊聊MyBatis执行流程
本文分享自华为云社区<面试必问|聊聊MyBatis执行流程?>,作者: 冰 河. MyBatis源码解析 大家应该都知道Mybatis源码也是对Jbdc的再一次封装,不管怎么进行包装,还是 ...
- spring声明式事务(@Transactional)开发常犯的几个错误及解决办法
spring声明式事务(@Transactional)开发常犯的几个错误及解决办法 目前JAVA的微服务项目基本都是SSM结构(即:springCloud +springMVC+Mybatis),而其 ...
- python-比较两个列表中的相同元素和不同元素
由于两个列表(List)中可能会存在重复的元素,所以我们可以将列表转化成集合(set)进行去重,然后计算交集和差集. A = ['1','2','3'] B = ['2','4','5'] ##计算交 ...
- ABC 311
前四题过水 E 枚举正方形的上边界所在行.对于第 \(i\) 行一个没洞的位置 \((i,j)\),我们尝试求出以它为右上角的无洞正方形个数. 结论:设以 \((i,j-1)\) 为右上角的无洞正方形 ...
- 提高Java开发生产力,我选Stream API,真香啊
Java 8 引入的Stream API提供了一种新的数据处理方式,它以声明式.函数式的编程模型,极大地简化了对集合.数组或其他支持数据源的操作.Stream可以被看作是一系列元素的流水线.允许你高效 ...
- Activiti7 多实例子流程
顾名思义,子流程是一个包含其他活动.网关.事件等的活动,这些活动本身形成了一个流程,该流程是更大流程的一部分. 使用子流程确实有一些限制: 一个子流程只能有一个none类型的启动事件,不允许有其他类型 ...
- STC8H8K64U 的 USB 功能测试(续)
对 STC8H8K64U 的USB测试昨天没搞定, 判断可能是供电的问题, 直接用5V不行, 从USB2TTL上采电3.3V时存在一个问题, 就是 D-/D+ 在上电前就已经连接了, 不符合 USB ...
- STC12硬件SPI驱动MAX7219点阵LED
max7219是一个用于驱动8位7段数字LED或者8x8点阵LED的驱动芯片, 以列扫描的方式, 用16个pin管理64个发光点, 显示8个数字时刷新率为500-1300Hz, 典型值为800Hz. ...
- Swoole从入门到入土(8)——协程初探
这一章节"协程"话题的讨论是为了让我们对之后协程风格服务端有更全面的了解.所以我们需要先一起了解一下什么是协程?协程有什么作用? 当大家第一次看到"协程"这个词 ...
- VS2019 配置 Qt 库
如标题所说,在 VS 编译器中调用 Qt 库 首先安装一个适合 VS 的 Qt 版本 不知道怎么安装的,可以参考:Visual Studio+Qt配置开发环境 Qt 安装之后,需要在 VS 编译器中 ...