Android DNS解析

649 阅读8分钟

概念

DNS全称domain name system。DNS是因特网上域名和IP地址相互映射的一个分布式数据库,使用户更方便去访问互联网而不用记住IP地址。通过域名得到IP地址的过程则是域名解析。

过程

缓存---/system/etc/hosts---DNS服务器

  1. 系统会检查浏览器缓存中有无该域名对应的IP地址,若有,结束解析。
    Android在Java和native层都有缓存,java层缓存16个,时间为2s
    native缓存取决于TTL值
  2. 若用户的浏览器缓存中无,浏览器会查找OS缓存中即本地的Host文件:system/etc/hosts
  3. 若本地Host文件中无,OS发送域名给LocalDNS(本地域名服务器)。LocalDNS通常都提供本地互联网接入的DNS解析服务。专门的域名解析服务器性能都很好,一般都会缓存域名解析结果,缓存时间受域名的失效时间控制,一般缓存空间不是影响域名失效的主要因素。大约90%的域名解析到这里就完成了,所以LDNS承担了主要的域名解析工作。
  4. 若LDNS仍未命中,就直接到Root Server(根域名服务器)请求解析(LDNS 去Root Server 请求)。Root Server是最高层次的域名服务器,所有的Root Server都知道所有的顶级域名服务器的IP地址。根域名服务器有13个域名,由一群服务器组成。
  • 根域名服务器返回给本地域名服务器一个所查询的域的顶级域名服务(gTLD Server)地址。gTLD 是国际顶级域名服务器, 如.com、.cn、.org等

  • 本地域名服务器(Local DNS Server)再向上一步返回的gTLD服务器发送请求。

  • 接受请求的gTLD服务器查找并返回此域名对应的权威域名服务器(又叫权限域名服务器)的地址

  • 权威域名服务器会查询存储的域名和IP的映射关系表,正常情况下都能根据域名得到目标IP记录,连同一个TTL值返回给DNS Server域名服务器。

  • 返回该域名对应的IP和TTL值,Local DNS Server会缓存有该域名和IP的对应关系,缓存的时间由TTL值控制。

  • 把解析结果返回给用户,用户根据TTL值缓存在本地系统缓存中,域名解析过程结束。

另外的描述

业务调用一次getaddrinfo函数进行dns解析请求流程如下:

  1. 首先从文件/system/etc/hosts中查询域名对应地址,若查找成功则结束
  2. 文件中未查询成功,则准备发送dns解析请求,如果ipv6地址存在,则优先发送type为4A的请求
  3. 发送4A请求前先查询DNS缓存,若缓存中查找成功,则结束4A请求,准备进行发送type为A的请求
  4. 若缓存查找失败,则向DNS服务器列表中的第一个DNS服务器地址发出第一次4A请求,收到服务器response且查询成功则结束4A请求,准备发送type为A的请求
  5. 若未接收到服务器response或查询失败,则向DNS服务器列表中的第二个DNS服务器地址发出第一次4A请求,收到服务器response且查询成功则结束4A请求,准备发送type为A的请求
  6. 若DNS服务器列表中3个服务器地址的4A请求都超时或失败,则重新从第一个服务器地址开始发送第二次4A请求,直到查询成功后结束4A请求,若3个服务器地址的第二次4A请求都未成功,则4A请求查询结果为失败,准备发送type未A的请求
  7. A请求过程与4A一样的循环方式

DNS请求流程图

4A和A

Type为4A的dns请求,代表请求域名的IPV6地址

Type为A的dns请求,代表请求域名的IPV4地址

Android设备IPV6单栈的情况(获取了有效的IPV6地址):对域名进行type为4A的dns请求

Android设备IPV4单栈的情况(获取了有效的IPV4地址):对域名进行type为A的dns请求

Android设备IPV6和IPV4双栈的情况(同时获取了有效的IPV6和IPV4地址):对域名分别进行type为4A和A的dns请求,4A的dns请求优先发出

DNS请求的目的地址

DNS请求的目的地址也就是DNS服务器地址,Android设备的DNS服务器地址根据不同的interface(eth0,wlan0等)来管理,以eth0为例:

情景1:

IPV4先连接成功,IPV6后连接成功

这种情况下IPV4的DNS服务器地址会先设置到DNS模块的DNS地址数组中,IPV6的DNS服务器地址则会滞后,保存顺序如下:

DNS_ADDR_LIST[0] = IPV4.DNS1
DNS_ADDR_LIST[1] = IPV4.DNS2
DNS_ADDR_LIST[2] = IPV6.DNS1

情景2:

IPV6先连接成功,IPV4后连接成功

这种情况下IPV6的DNS服务器地址会先设置到DNS模块的DNS地址数组中,IPV4的DNS服务器地址则会滞后,保存顺序如下:

DNS_ADDR_LIST[0] = IPV6.DNS1
DNS_ADDR_LIST[1] = IPV6.DNS2
DNS_ADDR_LIST[2] = IPV4.DNS1

Android系统DNS模块中默认MAXNS(最多保存DNS服务器地址个数)为3

#define MAXNS			4	/* max # name servers we'll track */

最多向4个DNS服务器发起域名解析请求(前3个DNS服务器都无法成功解析时)

请求过程如下:

{Send request to DNS_ADDR_LIST[0]If(ok)returnelse{Send request to DNS_ADDR_LIST[1]If(ok)returnelseSend request to DNS_ADDR_LIST[2]}}

DNS请求retry和timeout机制

Android dns每次请求等待DNS服务器响应都有timeout时间,超时或接收到的response不正确则会有retry机制

#define RES_DFLRETRY		2

默认的retry设置是2

#define RES_TIMEOUT		5

每次等待DNS服务器响应的timeout时间基数是5s,实际的timeout时间会根据这个基数和当前请求的dns服务器地址index做一个计算来求出本次等待的timeout时间

Android 13的源码

Android Dns模块代码:bionic/libc/netbsd/*

java层

网络请求都会经此方法,查询IP地址(发送一个https请求,在此方法打断点)。

  1. 从缓存中拿

  2. 获取不到去请求

  3. 请求后的结果放在缓存中

    private static InetAddress[] lookupHostByName(String host, int netId) throws UnknownHostException { BlockGuard.getThreadPolicy().onNetwork(); 1. Object cachedResult = addressCache.get(host, netId);

      try {
          StructAddrinfo hints = new StructAddrinfo();
          hints.ai_flags = AI_ADDRCONFIG;
          hints.ai_family = AF_UNSPEC;
          hints.ai_socktype = SOCK_STREAM;
          2.
          InetAddress[] addresses = Libcore.os.android_getaddrinfo(host, hints, netId);
          for (InetAddress address : addresses) {
              address.holder().hostName = host;
              address.holder().originalHostName = host;
          }
          3.
          addressCache.put(host, netId, addresses);
          return addresses;
      } 
     
    

    }

缓存结果

addressCache.put(host, netId, addresses);将请求结果放到缓存中

class AddressCache {
     
    private static final int MAX_ENTRIES = 16;


    // The TTL for the Java-level cache is short, just 2s.
    private static final long TTL_NANOS = 2 * 1000000000L;


    private final BasicLruCache<AddressCacheKey, AddressCacheEntry> cache
            = new BasicLruCache<AddressCacheKey, AddressCacheEntry>(MAX_ENTRIES);


    static class AddressCacheEntry {
        final Object value;
        final long expiryNanos;
        AddressCacheEntry(Object value) {
            this.value = value;
            this.expiryNanos = System.nanoTime() + TTL_NANOS;
        }
    }


    public void put(String hostname, int netId, InetAddress[] addresses) {
        cache.put(new AddressCacheKey(hostname, netId), new AddressCacheEntry(addresses));
    }
    
}

java层的TTL值固定2s,网络DNS请求包中的TTL值存在哪里?

是否可修改Java层的值TTL_NANOS 和 MAX_ENTRIES ,缓存ip的值,以加快网络速度

发起DNS请求

Libcore.os.android_getaddrinfo()

public final class Libcore {
    private Libcore() { }
    public static Os rawOs = new Linux();
    public static Os os = new BlockGuardOs(rawOs);
}


//BlockGuardOs.java
public InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException {
       return os.android_getaddrinfo(node, hints, netId);
}

public final class Linux implements Os {
    Linux() { }
  、、、
    public native InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException;
  、、、
    }

所以最终调用了native的方法

Java层流程图

JNI 层

关注 /libcore/luni/src/main/native/libcore_io_Linux.cpp

Native层

Native层(客户端进程)

Libcore.os.android_getaddrinfo() 最终调用到getaddrinfo.c中的android_getaddrinfo_proxy方法

static int
android_getaddrinfo_proxy(
    const char *hostname, const char *servname,
    const struct addrinfo *hints, struct addrinfo **res, unsigned netid)

Native层流程图

netd进程

init进程解析init.rc文件,创建zygote进程,netd进程,serviceManager服务,surfaceFlinger等一系类服务和进程(通过adb shell ps 命令可查看进程名称和id);

DNS的解析是通过Netd代理的方式进行的。Android监听/dev/socket/dnsproxyd,若系统需解析DNS,那么就需打开dnsproxyd,然后按照一定的格式写入命令,然后监听等待目标回答。

这里是netd进程,已跨进程;

void DnsProxyListener::GetAddrInfoHandler::run() {
    、、、
    if (queryLimiter.start(uid)) {
        const char* host = mHost.starts_with('^') ? nullptr : mHost.c_str();
        const char* service = mService.starts_with('^') ? nullptr : mService.c_str();
        if (evaluate_domain_name(mNetContext, host)) {
            rv = resolv_getaddrinfo(host, service, mHints.get(), &mNetContext, &result, &event);
        } else {
            rv = EAI_SYSTEM;
        }
        queryLimiter.finish(uid);
    }
    、、、
}

static int explore_fqdn(const struct addrinfo *pai, const char *hostname,
    const char *servname, struct addrinfo **res,
    const struct android_net_context *netcontext)
{
	static const ns_dtab dtab[] = {
		NS_FILES_CB(_files_getaddrinfo, NULL)
		{ NSSRC_DNS, _dns_getaddrinfo, NULL },	/* force -DHESIOD */
		NS_NIS_CB(_yp_getaddrinfo, NULL)
		{ 0, 0, 0 }
	};
  、、、
  nsdispatch(&result, dtab, NSDB_HOSTS, "getaddrinfo",
			default_dns_files, hostname, pai, netcontext)
	
	}
}

static void _sethtent(FILE **hostf)
{
	if (!*hostf)
		*hostf = fopen(_PATH_HOSTS, "re");
	else
		rewind(*hostf);
}
#define	_PATH_HOSTS	"/system/etc/hosts"
  1. 从文件/system/etc/hosts获取

  1. 从dns服务器获取

netd进程到底是怎么向DNS服务发起请求的,在res_send.c文件send_dg()真正发起网络请求的地方,有无一种熟悉的感觉,创建socket,建立连接,发送请求,等待结果。

static int
send_dg(res_state statp,
	const u_char *buf, int buflen, u_char *ans, int anssiz,
	int *terrno, int ns, int *v_circuit, int *gotsomewhere,
	time_t *at, int *rcode, int* delay)
{


	if (EXT(statp).nssocks[ns] == -1) {
		EXT(statp).nssocks[ns] = socket(nsap->sa_family, SOCK_DGRAM | SOCK_CLOEXEC, 0);


		if (random_bind(EXT(statp).nssocks[ns], nsap->sa_family) < 0) {
			Aerror(statp, stderr, "bind(dg)", errno, nsap,
			    nsaplen);
			res_nclose(statp);
			return (0);
		}
		if (__connect(EXT(statp).nssocks[ns], nsap, (socklen_t)nsaplen) < 0) {
			Aerror(statp, stderr, "connect(dg)", errno, nsap,
			    nsaplen);
			res_nclose(statp);
			return (0);
		}
	if (sendto(s, (const char*)buf, buflen, 0, nsap, nsaplen) != buflen)
	{
		Aerror(statp, stderr, "sendto", errno, nsap, nsaplen);
		res_nclose(statp);
		return (0);
	}
	/*
	 * Wait for reply.
	 */
	seconds = get_timeout(statp, ns);
	now = evNowTime();
	timeout = evConsTime((long)seconds, 0L);
	finish = evAddTime(now, timeout);
	n = retrying_select(s, &dsmask, NULL, &finish);
  }
}

  1. java层的TTL值是一个固定的2s,那网络DNS请求包中的TTL值存在那里了?当发送DNS请求,请求回来以后,将请求结果保存

    void _resolv_cache_add(const void* answer)//这里省略部分参数 { //从响应报文中获取本次查询结果中指定的查询结果的有效期 ttl = answer_getTTL(answer, answerlen); if (ttl > 0) { //ttl大于0,表示该地址可保留一段时间,那么创建一个新的cache项, //然后设定其有效期,并将其加入到cache中 e = entry_alloc(key, answer, answerlen); if (e != NULL) { e->expires = ttl + _time_now(); _cache_add_p(cache, lookup, e); } } }

在res_nsend()真正向DNS服务器发起DNS查询请求之前,会首先向自己的cache查询,若cache可命中,那么直接返回,否则才继续向DNS服务器查询。该查询过程是通过_resolv_cache_lookup()完成的。

ResolvCacheStatus _resolv_cache_lookup(unsigned netid,const void* query,int querylen,void*  answer,intanswersize,int   *answerlen )
{
  、、、
    lookup = _cache_lookup_p(cache, key);
    e      = *lookup;
    now = _time_now();
    //查询结果无效,返回没有查询到结果,向DNS服务器发起查询请求
    if (now >= e->expires) {
        XLOG( " NOT IN CACHE (STALE ENTRY %p DISCARDED)", *lookup );
        XLOG_QUERY(e->query, e->querylen);
        _cache_remove_p(cache, lookup);
        goto Exit;
    }
    //ok,到这里说明cache中的结果没问题
    memcpy( answer, e->answer, e->answerlen );
    //返回查询成功
    XLOG( "FOUND IN CACHE entry=%p", e );
    result = RESOLV_CACHE_FOUND;
    、、、
    return result;
}

Android系统中通过这种方式来管理DNS的好处是,所有解析后得到的 DNS 记录都将缓存在 Netd 进程中,从而使这些信息成为一个公共的资源,最大程度做到信息共享。

Netd进程流程图

流程图

参考

source.android.com/docs/core/a…