【背景】
在一次问题排查过程中,发现偶现调用"InetAddress.getByName()"无法通过域名解析到IP(实际在容器中都能正确解析到),因此怀疑和容器的DNS解析有问题。但在与容器的开发兄弟沟通过程中,被反问了一句,确定该方法一定触发调用了DNS的域名解析吗?
对此问题一时半会无法准确的答复,因此花了些时间对背后的逻辑原理,相关源码(涉及JDK、glibc源码)进行走读分析,并总结分享。
【准备知识】
1. IP
IP指网络互联协议,即Internet Protocol的缩写,是TCP/IP体系中的网络层协议。设计IP的目的是提高网络的可扩展性:一是解决互联网问题,实现大规模、异构网络的互联互通;二是分割顶层网络应用和底层网络技术之间的耦合关系。
IP规定网络上所有的设备都必须有一个独一无二的地址,即IP地址。
2. 主机名
主机名也就是一个网络设备的别名。是连接到计算机网络中并具有特定IP地址的计算机或任何设备的昵称。
3. 域名
根据百度百科的介绍:
域名(Domain Name),又称网域,是由一串用点分隔的名字组成的Internet上某一台计算机或计算机组的名称,用于在数据传输时对计算机的定位标识。
由于IP地址具有不方便记忆并且不能显示地址组织的名称和性质等缺点,人们设计出了域名,并通过域名名称系统(DNS)来讲域名和IP地址相互映射,使人更方便地访问互联网,而不用去记住能够被机器直接读取的IP地址数串。
域名通常由两部分组成:顶级域名和次级域名,最后一个"."的右边被称为顶级域名(Top-Level Domain),最后一个"."左边部分被称为二级域名,二级域名的左边为三级域名,以此类推。
注:主机名与域名的区别
主机名就是机器本身的名字,而域名是用来解析到IP的。但在局域网中,通过一定配置,主机名也可以解析到IP。
4. FQDN&PQDN
FQDN是"Full Qualified Domain Name"的简称,翻译过来称为完全合规域名 或 完全限定域名。FQDN的组成格式为:
[hostname].[domain].[tld].
# FQDN 由主机名+域名两部分组成, 其中hostname 为主机名; 而域名则是包含了顶级域的全路径
# 注意FQDN以"."结束
与FQDN相对应的就是PQDN(Partially Qualified Domain Name)部分限定域名。通常情况下,仅引用域名的一部分,而没有全部指定的就是PQDN。
5. DNS
域名系统,即Domain Name System的简称,是英特网中作为域名和IP地址互相映射的一个分布式数据库,能够使用用户更方便的访问互联网,而不用记住能够被机器直接读取的IP数串。通过主机名/域名,最终能够得到该主机/域名对应的IP地址的过程称为域名解析(或主机名解析)。
DNS的分布式数据库是以域名为索引的,每个域名实际上就是一颗很大的逆向树中的路径。
【相关的系统配置】
1. /etc/hosts
该配置文件的作用就是配置主机IP以及对应的主机名。一般情况下,该文件的每行为一个主机,且由三部分组成,以空格分隔开。第一部分为IP地址;第二部分为主机名或域名;第三部分为主机名。当然,每行也可以为两部分,即IP地址和主机名。
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.168.3.21 nn-0-hncscwc.network-hncscwc nn-0
2. /etc/resolv.conf
是DNS客户端的配置文件,用于设置DNS服务器的地址,以及主机的域名搜索顺序。其格式很简单,每行以一个关键字开头,后面接一个或多个由空格分隔的参数。
一个典型的配置文件如下所示:
nameserver 10.254.0.2
search hncscwc-1.svc.cluster.local svc.cluster.local cluster.local kube-system.svc.cluster.local
options ndots:5
其中nameserver指明dns服务器的地址,可以有多行,每行指定一个DNS服务器的地址,查询时按照先后顺序,依次进行查询,但是仅当前面一个nameserver查询失败时才从后面nameserver继续进行查询。
search则指明搜索域的顺序,配合options中的ndots选项,对短域名进行补齐,然后再进行查询。
options为DNS的参数选项,可选的参数较多,如下图所示:
这里重点讲解下 ndots。ndots指定的值,表示请求查询的域名中,如果点的个数小于指定的值,则按照search配置的内容,依次添加对应的后缀,然后再进行域名解析,直到获取到解析后的地址。而请求查询的域名中,如果点的个数大于等于指定的值,则不会进行补齐动作。
domain指明本地域名。注:domain与search不能同时并存。
3. /etc/host.conf
该配置文件的作用为指明如何解析主机域名(作用于libresolv.so)。
可选的配置项包括:
- multi:有效值为on/off,当配置为on时,会返回/etc/hosts中出现的主机的所有有效地址,否则仅返回第一个。
- nospoof:表示是否允许服务器对IP地址进行欺骗 on表示不允许 off表示不允许
- reorder:表示是否对查询结果进行重新排序 on表示重新排序 off表示不重新排序
- trim:这个关键字可以多次多次出现,每次出现其后应该跟随单个的以"."开头的域名,如果设置了它,libresolv.so会自动截断通过DNS解析出来的主机名后面的域名。
- spoofalert:有效值为on/off,仅在nospoof配置为on时有效,即两者均配置为on,当发生IP地址欺骗时记录告警或错误日志
注:老的版本里可能还会有order配置项,指明解析顺序,但从man中已经无该配置项的说明,同时从glibc的代码中可以看到,仅解析了该字段但不做任何处理。
4. /etc/nsswitch.conf
名称服务开关(Name Service Switch)配置文件,主要用于指定glibc以及某些应用程序对名称解析的顺序。和主机、域名解析相关的配置项包括:
hosts: files dns
# 用于 gethostbyname 等相关函数
# files表示先读取 /etc/hosts
# dns 表示查询 dns
# 其他可选值还包括 db, nisplus, nis等
networks: files
# 用于 getnetent 等相关函数
# files 表示先读取 /etc/networks
# 其他可选值包括 nisplus
【常用操作】
在我们常见的操作中,就是将一个主机名/域名解析成IP地址,或者是知道IP地址,反查对应的域名。
对于主机名/域名解析成IP地址,最简单的办法就是用ping命令,例如:
[root@nn-0 /]# ping nn-0-hncscwc
PING nn-0-hncscwc (172.168.3.21) 56(84) bytes of data.
64 bytes from 172.168.3.21 (172.168.3.21): icmp_seq=1 ttl=64 time=0.061 ms
而对于IP反查域名,则可以使用nslookup命令,例如:
[root@nn-0 /]# nslookup 172.168.3.21
21.3.168.172.in-addr.arpa name = nn-0-hncscwc.network-hncscwc.
对于ping内部,先通过gethostbyname的系统调用,将非IP地址的主机/域名转换为IP地址,然后发送ICMP报文。
对于"gethostbyname"、"gethostbyaddr"(通过IP地址获取主机/域名)的系统调用,简单示例代码如下所示:
#include <stdio.h>
#include <netdb.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
int main(int argc, char * argv[])
{
if( argc < 2) {
printf("the argc need more two\n");
return 1;
}
struct hostent *host;
const char * addr = argv[1];
char p[30];
// 对于IPv4类型IP地址 通过IP地址获取域名
if(inet_pton(AF_INET,addr, p) == 1) {
host = gethostbyaddr(p, strlen(p), AF_INET);
} else {
// 对于非IPv4地址, 通过主机名/域名获取IP
host = gethostbyname(addr);
}
if(NULL != host) {
printf("hostname: %s\n", host->h_name);
struct sockaddr_in ipaddr;
memcpy((char*)&ipaddr.sin_addr, host->h_addr_list[0], host->h_length);
printf("ipaddr: %s\n", inet_ntoa(ipaddr.sin_addr));
} else {
herror("gethostbyname");
}
return 0;
}
编译的执行效果为:
另外,从man中可以知道gethostbyname,gethostbyaddr已经是过时的方法,正确的方式应该是调用getaddrinfo和getnameinfo。
但这两个方法的内部流程和gethostbyname基本雷同。
而java中InetAddress类的getByName、getByAddress、getAllByName等方法,本质上是调用了系统函数getaddrinfo或gethostbyname来进行主机名/域名到IP之间的转换。相关代码如下所示:
// InetAddress.java
public static InetAddress getByName(String host)
throws UnknownHostException {
return InetAddress.getAllByName(host)[0];
}
public static InetAddress[] getAllByName(String host)
throws UnknownHostException {
return getAllByName(host, null);
}
private static InetAddress[] getAllByName(String host, InetAddress reqAddr)
throws UnknownHostException {
...
return getAllByName0(host, reqAddr, true);
}
private static InetAddress[] getAllByName0 (String host, InetAddress reqAddr, boolean check)
throws UnknownHostException {
...
if (addresses == null) {
addresses = getAddressesFromNameService(host, reqAddr);
}
...
}
// Inet4AddressImpl.c
private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)
throws UnknownHostException {
...
addresses = nameService.lookupAllHostAddr(host);
...
}
JNIEXPORT jobjectArray JNICALL
Java_java_net_Inet4AddressImpl_lookupAllHostAddr(JNIEnv *env, jobject this,
jstring host) {
if (!initializeInetClasses(env)) {
return NULL;
}
#if defined(__GLIBC__)
if (glibc_major_version >= 2 && glibc_minor_version >= 12) {
return lookupAllHostAddrs_getaddrinfo(env, this, host);
} else {
return lookupAllHostAddrs_gethostbyname(env, this, host);
}
#else
return lookupAllHostAddrs_getaddrinfo(env, this, host);
#endif
}
static jobjectArray
lookupAllHostAddrs_getaddrinfo(JNIEnv *env, jobject this, jstring host) {
...
error = getaddrinfo(hostname, NULL, &hints, &res);
...
}
【调用背后发生了什么】
对于"gethostbyname"的系统调用,背后具体又发生了什么呢?从glibc的源码角度来看,总体分为这么两个步骤:
- 初始化这里包括打开/etc/host.conf、/etc/resolv.conf,从配置文件中解析对应的内容。相关配置的值后续需要用到。
- 地址解析打开/etc/nsswitch.conf,读取hosts的内容,并根据其顺序,依次加载对应的动态库(libnss_xxx.so),并调用动态库中的方法完成地址的解析。如果通过某一项能正确进行地址解析,则不进行后续动作。从系统动态库中可以看到,每个配置项都有一个对应的动态库。
对于配置项files而言(libnss_files.so)就是读取/etc/hosts中的内容。而对于dns(libnss_dns.so)自然就是向dns服务器进行查询。
例如在下面配置中执行"gethostbyname":
[root@hdp-hadoop-hdp-namenode-0 opt]# cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-mcastprefix
fe00::1 ip6-allnodes
fe00::2 ip6-allrouters
[root@hdp-hadoop-hdp-namenode-0 opt]# cat /etc/nsswitch.conf | grep hosts
#hosts: db files nisplus nis dns
hosts: files dns myhostname
[root@hdp-hadoop-hdp-namenode-0 opt]# ./main hdp-hadoop-hncscwc-namenode-0
hostname: hdp-hadoop-hncscwc-namenode-0
ipaddr: 172.16.21.104
其strace的分析过程如下所示:
有兴趣的朋友可以通过strace去分析下其调用流程。
好了,这就是本文的全部内容,如果觉得本文对您有帮助,请点赞+转发,也欢迎加我微信交流~
版权归原作者 陈猿解码 所有, 如有侵权,请联系我们删除。