admin管理员组

文章数量:1530085

Android DNS解析的过程

DNS解析概念

DNS的全称是domain name system,即域名系统。DNS是因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的去访问互联网而不用去记住能够被机器直接读取的IP地址。通过域名最终得到该域名对应的IP地址的过程则是域名解析的过程。

DNS解析过程

  1. 系统会检查浏览器缓存中有没有这个域名对应的解析过的IP地址,如果缓存中有,这个解析过程就将结束。
    Android在Java层和native都有缓存,java层缓存16个,时间为2秒
    native的缓存取决于TTL值

  2. 如果用户的浏览器缓存中没有,浏览器会查找操作系统缓存中即为本地的Host文件。 Android的Host文件路径(/system/etc/hosts)

  3. 如果本地Host文件中没有那么操作系统会把这个域名发送给LocalDNS(本地域名服务器)

       这个LocalDNS通常都提供给你本地互联网接入的一个DNS解析服务。这个专门的域名解析服务器性能都会很好,它们一般都会缓存域名解析结果,当然缓存时间是受域名的失效时间控制的,一般缓存空间不是影响域名失效的主要因素。大约90%的域名解析都到这里就已经完成了,所以LDNS主要承担了域名的解析工作。

  4. 如果LDNS仍然没有命中,就直接到Root Server(根域名服务器)请求解析(LDNS 去Root Server 请求)

       根域名服务器是最高层次的域名服务器,所有的根域名服务器都知道所有的顶级域名服务器的IP地址。根域名服务器有13个域名,由一群服务器组成。

  1. 根域名服务器返回给本地域名服务器一个所查询的域的顶级域名服务(gTLD Server)地址。

       gTLD 是国际顶级域名服务器, 如、、等

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

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

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

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

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

DNS请求流程图

Android的DNS解析的过程(Android 9.0的源码)

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));
    }
    
}
  1. java层的TTL值是一个固定的2s,那网络DNS请求包中的TTL值存在那里了?
  2. 是不是可以修改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)
{
	FILE* proxy = android_open_proxy();
	// Send the request.
	if (fprintf(proxy, "getaddrinfo %s %s %d %d %d %d %u",
		    hostname == NULL ? "^" : hostname,
		    servname == NULL ? "^" : servname,
		    hints == NULL ? -1 : hints->ai_flags,
		    hints == NULL ? -1 : hints->ai_family,
		    hints == NULL ? -1 : hints->ai_socktype,
		    hints == NULL ? -1 : hints->ai_protocol,
		    netid) < 0) {
		goto exit;
	}
}
1.android_open_proxy()
__LIBC_HIDDEN__ FILE* android_open_proxy() {
    、、、
  	int s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);  
  	const int one = 1;
  	setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    
  	struct sockaddr_un proxy_addr;
  	memset(&proxy_addr, 0, sizeof(proxy_addr));
  	proxy_addr.sun_family = AF_UNIX;
  	strlcpy(proxy_addr.sun_path, "/dev/socket/dnsproxyd", sizeof(proxy_addr.sun_path));
  
  	if (TEMP_FAILURE_RETRY(connect(s, (const struct sockaddr*) &proxy_addr, sizeof(proxy_addr))) != 0) {
  		close(s);
  		return NULL;
  	}
  	return fdopen(s, "r+");
}
  1. 创建socket套接字并初始化
  2. 将netd进程的socket拷贝到 proxy_addr.sun_path(strlcpy() 字符串的拷贝(比Strcpy多了一个‘\0’))
  3. 建立连接 这里就涉及到socket网络编程了,就不去探索了,感兴趣的可以关注connet,accept 等函数

.

2.fprintf(proxy, “getaddrinfo”)

Linux下一切皆文件,将命令“getaddrinfo” 写入文件中,这样就与net进程进行通信;

Native层流程图

netd进程

先储备一点点netd进程的知识

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

DNS的解析是通过Netd代理的方式进行的。Netd是Network Daemon的缩写,Netd在Android中负责物理端口的网络操作相关的实现,如Bandwidth,NAT,PPP,soft-ap等。Netd为Framework隔离了底层网络接口的差异,提供了统一的调用接口,简化了整个网络逻辑的使用。简单来说就是Android将监听/dev/socket/dnsproxyd,如果系统需要DNS解析服务,那么就需要打开dnsproxyd,然后安装一定的格式写入命令,然后监听等待目标回答(CDNS 对netd进程比较好的解释)。

这里省略netd进程创建和初始化的过程,假设我们现在都了解这些知识,并且已经走到该函数中。
这里是netd进程,已经跨进程;

void DnsProxyListener::GetAddrInfoHandler::run() {
    、、、
    if (queryLimiter.start(uid)) {
        rv = android_getaddrinfofornetcontext(mHost, mService, mHints, &mNetContext, &result);
        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()真正发起网络请求的地方,有没有一种熟悉的感觉,创建套接字,建立连接,发送请求,等待结果。(详细流程和调用链,关注流程图)

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进程流程图

流程图

最后

Android DNS请求解析,缓存机制不仅仅是这么简单,这里只是大概的流程,路漫漫其修远兮!!

本文标签: 过程androiddns