1. 程式人生 > >Android DNS之getaddrinfo()的實現

Android DNS之getaddrinfo()的實現

這篇筆記分析了庫函式getaddrinfo()的程式碼實現。

原型解讀

int getaddrinfo(const char *hostname, const char *servname,
    const struct addrinfo *hints, struct addrinfo **res);
  • hostname: 和gethostbyname()的入參hostname相同,要查詢的域名;
  • servname:要查詢的服務名,如果指定,則返回值中還會有服務名對應的埠號;也就是該函式還有getservbyname()的功能;
  • hints:表面意思為暗示,使用該引數可以對查詢結果進行約束,只返回那些滿足條件的查詢結果;後面稱該引數為過濾器
  • res:該引數用於儲存指向查詢結果地址的指標,儲存查詢結果的記憶體有該函式內部分配,使用完畢後呼叫者需要呼叫freeaddrinfo()清除,呼叫者只需要提供儲存struct addrinfo *的地址即可。

關於該函式的更詳細介紹可以參考getaddrinfo(3)。

入口

int getaddrinfo(const char *hostname, const char *servname,
    const struct addrinfo *hints, struct addrinfo **res)
{
	//增加了入參NETID_UNSET和MARK_UNSET
return android_getaddrinfofornet(hostname, servname, hints, NETID_UNSET, MARK_UNSET, res); } int android_getaddrinfofornet(const char *hostname, const char *servname, const struct addrinfo *hints, unsigned netid, unsigned mark, struct addrinfo **res) { //入參中netid和mark值可以影響DNS請求包從哪個網絡卡出去,屬於網路相關的配置,將這些資訊組織成引數
//netcontext繼續向下傳遞 struct android_net_context netcontext = { .app_netid = netid, .app_mark = mark, .dns_netid = netid, .dns_mark = mark, .uid = NET_CONTEXT_INVALID_UID, }; return android_getaddrinfofornetcontext(hostname, servname, hints, &netcontext, res); }

這裡有必要對這兩個函式做個說明:

  • getaddrinfo()是標準的libc介面,系統中Framework以下的C/C++程式碼可以呼叫該介面實現DNS查詢;
  • android_getaddrinfofornet()是Android對libc的擴充套件,當APP在Framework通過InetAddress類提供的介面進行DNS查詢時,最終會通過JNI呼叫到該函式,也就是說該介面是給Framework使用的。

android_getaddrinfofornetcontext()

該函式的作用主要是對輸入引數的合法性進行檢查,以及處理hostname為空或者是純數字格式的情形,如果需要真正發起DNS請求,呼叫explore_fqdn()完成。

/* 定義了faimly、socktype、protocol欄位的有效組合 */
static const struct explore explore[] = {
#ifdef INET6
	{ PF_INET6, SOCK_DGRAM, IPPROTO_UDP, "udp", 0x07 },
	{ PF_INET6, SOCK_STREAM, IPPROTO_TCP, "tcp", 0x07 },
	{ PF_INET6, SOCK_RAW, ANY, NULL, 0x05 },
#endif
	{ PF_INET, SOCK_DGRAM, IPPROTO_UDP, "udp", 0x07 },
	{ PF_INET, SOCK_STREAM, IPPROTO_TCP, "tcp", 0x07 },
	{ PF_INET, SOCK_RAW, ANY, NULL, 0x05 },
	{ PF_UNSPEC, SOCK_DGRAM, IPPROTO_UDP, "udp", 0x07 },
	{ PF_UNSPEC, SOCK_STREAM, IPPROTO_TCP, "tcp", 0x07 },
	{ PF_UNSPEC, SOCK_RAW, ANY, NULL, 0x05 },
	{ -1, 0, 0, NULL, 0 },
};

int android_getaddrinfofornetcontext(const char *hostname, const char *servname,
    const struct addrinfo *hints, const struct android_net_context *netcontext,
    struct addrinfo **res)
{
	//這裡定義了3個addrinfo結構變數,後面對於這些變數的使用非常繞,要注意理解下面的註釋說明
	struct addrinfo sentinel;
	struct addrinfo *cur;
	int error = 0;
	struct addrinfo ai;
	struct addrinfo ai0;
	struct addrinfo *pai;
	const struct explore *ex;

	/* hostname is allowed to be NULL */
	/* servname is allowed to be NULL */
	/* hints is allowed to be NULL */
	assert(res != NULL);
	assert(netcontext != NULL);
    //sentinel清零,並且cur指向該結構
	memset(&sentinel, 0, sizeof(sentinel));
	cur = &sentinel;
    //pai指向ai,並且對ai的成員進行初始化
	pai = &ai;
	pai->ai_flags = 0;
	pai->ai_family = PF_UNSPEC;
	pai->ai_socktype = ANY;
	pai->ai_protocol = ANY;
	pai->ai_addrlen = 0;
	pai->ai_canonname = NULL;
	pai->ai_addr = NULL;
	pai->ai_next = NULL;

    //hostname和servname二者不能同時為空,至少要指定一個
	if (hostname == NULL && servname == NULL)
		return EAI_NONAME;

	//如果指定了過濾器,則對過濾器成員進行檢查,因為作為過濾器,並非addrinfo中的每個成員都是可以指定的
	if (hints) {
		/* error check for hints */
        //作為過濾器,addrinfo的這四個成員是不能指定的,必須保持為0才行
		if (hints->ai_addrlen || hints->ai_canonname ||
		    hints->ai_addr || hints->ai_next)
			ERR(EAI_BADHINTS); /* xxx */
        //ai_flags中有沒有指定非法的標記
		if (hints->ai_flags & ~AI_MASK)
			ERR(EAI_BADFLAGS);
        //ai_faimly成員只可取下面這三個值,其它值均非法
		switch (hints->ai_family) {
		case PF_UNSPEC:
		case PF_INET:
#ifdef INET6
		case PF_INET6:
#endif
			break;
		default:
			ERR(EAI_FAMILY);
		}
        //從這裡可以看出,過濾器引數最終是被儲存在了ai中,這就是ai的作用
		memcpy(pai, hints, sizeof(*pai));

		/*
		 * if both socktype/protocol are specified, check if they
		 * are meaningful combination.
		 */
        //如果同時指定了hints引數中的ai_socktype和ai_protocol欄位(非0),那麼繼續檢查這兩個引數的組合是否合理
        //因為並非所有的組合都是有效的,比如socktype指定為STREAM,但是protocol指定為DGRAM就不合理
		if (pai->ai_socktype != ANY && pai->ai_protocol != ANY) {
        	//expore陣列定義了faimly、socktype、protocol的有效組合
			for (ex = explore; ex->e_af >= 0; ex++) {
				if (pai->ai_family != ex->e_af)
					continue;
				if (ex->e_socktype == ANY)
					continue;
				if (ex->e_protocol == ANY)
					continue;
                //如果是非法組合返回BADHINTS錯誤
				if (pai->ai_socktype == ex->e_socktype && pai->ai_protocol != ex->e_protocol) {
					ERR(EAI_BADHINTS);
				}
			}
		}
	}//end of "if (hints)"

	/*
	 * check for special cases.  (1) numeric servname is disallowed if
	 * socktype/protocol are left unspecified. (2) servname is disallowed
	 * for raw and other inet{,6} sockets.
	 */
    //這段邏輯用於檢查服務名和hints引數是否存在衝突(見註釋)
	if (MATCH_FAMILY(pai->ai_family, PF_INET, 1)
#ifdef PF_INET6
	 || MATCH_FAMILY(pai->ai_family, PF_INET6, 1)
#endif
	    ) {
        //ai0的作用就是備份hints的內容
		ai0 = *pai;	/* backup *pai */

		if (pai->ai_family == PF_UNSPEC) {
#ifdef PF_INET6
			pai->ai_family = PF_INET6;
#else
			pai->ai_family = PF_INET;
#endif
		}
		error = get_portmatch(pai, servname);
		if (error)
			ERR(error);
		//將備份的hints引數內容再賦值回去,保證這步檢查不會修改hints本身的內容
		*pai = ai0;
	}

	ai0 = *pai;
    //處理hostname為空,或者hostname為純數字的情形
	/* NULL hostname, or numeric hostname */
	for (ex = explore; ex->e_af >= 0; ex++) {
		*pai = ai0;

		/* PF_UNSPEC entries are prepared for DNS queries only */
		if (ex->e_af == PF_UNSPEC)
			continue;

		if (!MATCH_FAMILY(pai->ai_family, ex->e_af, WILD_AF(ex)))
			continue;
		if (!MATCH(pai->ai_socktype, ex->e_socktype, WILD_SOCKTYPE(ex)))
			continue;
		if (!MATCH(pai->ai_protocol, ex->e_protocol, WILD_PROTOCOL(ex)))
			continue;

		if (pai->ai_family == PF_UNSPEC)
			pai->ai_family = ex->e_af;
		if (pai->ai_socktype == ANY && ex->e_socktype != ANY)
			pai->ai_socktype = ex->e_socktype;
		if (pai->ai_protocol == ANY && ex->e_protocol != ANY)
			pai->ai_protocol = ex->e_protocol;

		if (hostname == NULL)
        	//處理只請求服務名轉換的情況,轉黃結果儲存到cur->ai_next中。
            //從這裡可以看出sentinel的作用來,它本身不儲存查詢結果,但是查詢結果就儲存在
            //sentinel.ai_next後面的addrinfo連結串列中,而cur指標永遠指向當前連結串列的最後一個成員,
            //這樣便於查詢過程中不斷新增查詢結果,見下面的while迴圈
			error = explore_null(pai, servname, &cur->ai_next);
		else
        	//嘗試按照數字格式解析hostname以及服務名裝換
			error = explore_numeric_scope(pai, hostname, servname,
			    &cur->ai_next);

		if (error)
			goto free;
		//移動cur指標,使其指向當前查詢結果連結串列的最後一個成員
		while (cur->ai_next)
			cur = cur->ai_next;
	}

	/*
	 * XXX
	 * If numeric representation of AF1 can be interpreted as FQDN
	 * representation of AF2, we need to think again about the code below.
	 */
    //如果這裡非空,說明上一個for迴圈中已經解析的結果,請求的hostname就是為空或者是純數字格式,
    //那麼無需再發起DNS查詢,返回結果即可
	if (sentinel.ai_next)
		goto good;

	//hostname為空的場景上面應該已經處理,不應該會執行到這裡
	if (hostname == NULL)
		ERR(EAI_NODATA);
    //hostname為數字格式的情形上面應該已經處理,不應該會執行到這裡
	if (pai->ai_flags & AI_NUMERICHOST)
		ERR(EAI_NONAME);

	//這裡是Android的變化,就是將DNS查詢導向到netd中,然後由netd處理後再回到這裡,
    //這個過程和gethostbyname()中的處理流程一致,可見gethostbyname()實現中的分析
#if defined(__ANDROID__)
	//非netd呼叫會阻塞到這裡,等待netd查詢完後返回
	int gai_error = android_getaddrinfo_proxy(
		hostname, servname, hints, res, netcontext->app_netid);
	//如果是netd呼叫,那麼android_getaddrinfo_proxy()會返回EAI_SYSTEM錯誤,繼續後續的邏輯
    if (gai_error != EAI_SYSTEM) {
		return gai_error;
	}
#endif

	/*
	 * hostname as alphabetical name.
	 * we would like to prefer AF_INET6 than AF_INET, so we'll make a
	 * outer loop by AFs.
	 */
    //按照explore陣列中指定的有效組合,呼叫explore_fqdn()進行DNS查詢
	for (ex = explore; ex->e_af >= 0; ex++) {
		*pai = ai0;

		/* require exact match for family field */
		if (pai->ai_family != ex->e_af)
			continue;

		if (!MATCH(pai->ai_socktype, ex->e_socktype,
				WILD_SOCKTYPE(ex))) {
			continue;
		}
		if (!MATCH(pai->ai_protocol, ex->e_protocol,
				WILD_PROTOCOL(ex))) {
			continue;
		}
		//這步會執行,以為explore中,socktype欄位都是固定的,並非通配
		if (pai->ai_socktype == ANY && ex->e_socktype != ANY)
			pai->ai_socktype = ex->e_socktype;
		if (pai->ai_protocol == ANY && ex->e_protocol != ANY)
			pai->ai_protocol = ex->e_protocol;
		//進行DNS查詢
		error = explore_fqdn(
			pai, hostname, servname, &cur->ai_next, netcontext);
		//查詢結果指標cur前移
		while (cur && cur->ai_next)
			cur = cur->ai_next;
	}
	//sentinel.ai_next不為空,說明有查詢結果,所以設定error為0
	if (sentinel.ai_next)
		error = 0;
	//有錯誤發生,需要呼叫freeaddrinfo()釋放可能已經分配的記憶體空間
	if (error)
		goto free;

	if (error == 0) {
		if (sentinel.ai_next) {
 good:
 			//查詢過程沒有問題 && 查詢到了結果,返回成功
			*res = sentinel.ai_next;
			return SUCCESS;
		} else
        	//查詢過程沒有問題(error=0),但是沒有查到結果(sentinel.ai_next=NULL),也是失敗
			error = EAI_FAIL;
	}
 free:
 bad:
 	//查詢失敗,或者查詢過程出錯,釋放記憶體後返回錯誤碼
	if (sentinel.ai_next)
		freeaddrinfo(sentinel.ai_next);
	*res = NULL;
	return error;
}

explore_fqdn()

看到該函式的程式碼,一定會非常熟悉,在gethostbyname()的實現中,已經見過這種查詢表方式,這裡不再贅述。

/*
 * FQDN hostname, DNS lookup
 */
static int explore_fqdn(const struct addrinfo *pai, const char *hostname,
    const char *servname, struct addrinfo **res,
    const struct android_net_context *netcontext)
{
	struct addrinfo *result;
	struct addrinfo *cur;
	int error = 0;
    //_files_getaddrinfo()完成基於檔案的查詢;_dns_getaddrinfo()完成基於DNS伺服器的查詢;
    //_yp_getaddrinfo()不會被呼叫
	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 }
	};

	assert(pai != NULL);
	/* hostname may be NULL */
	/* servname may be NULL */
	assert(res != NULL);
	result = NULL;

	/*
	 * if the servname does not match socktype/protocol, ignore it.
	 */
    //如果指定了servname,檢查socktype和protocol是否有衝突,Android中不涉及
	if (get_portmatch(pai, servname) != 0)
		return 0;

	//呼叫nsdispatch()進行查詢分發
	switch (nsdispatch(&result, dtab, NSDB_HOSTS, "getaddrinfo",
			default_dns_files, hostname, pai, netcontext)) {
	case NS_TRYAGAIN:
		error = EAI_AGAIN;
		goto free;
	case NS_UNAVAIL:
		error = EAI_FAIL;
		goto free;
	case NS_NOTFOUND:
		error = EAI_NODATA;
		goto free;
	case NS_SUCCESS:
		error = 0;
        //如果hostname查詢成功,如果指定了servname,將對應的埠名轉換為埠號,Android不涉及
		for (cur = result; cur; cur = cur->ai_next) {
			GET_PORT(cur, servname);
			/* canonname should be filled already */
		}
		break;
	}

	*res = result;
	return 0;

free:
	if (result)
		freeaddrinfo(result);
	return error;
}

_dns_getaddrinfo()

我們不關注基於檔案的查詢_files_getaddrinfo()的實現,只看基於DNS伺服器的查詢實現。

static int _dns_getaddrinfo(void *rv, void *cb_data, va_list ap)
{
	struct addrinfo *ai;
	querybuf *buf, *buf2;
	const char *name;
	const struct addrinfo *pai;
	struct addrinfo sentinel, *cur;
	struct res_target q, q2;
	res_state res;
	const struct android_net_context *netcontext;

	//從explore_fqdn()傳入的三個引數
	name = va_arg(ap, char *);
	pai = va_arg(ap, const struct addrinfo *);
	netcontext = va_arg(ap, const struct android_net_context *);

	memset(&q, 0, sizeof(q));
	memset(&q2, 0, sizeof(q2));
	memset(&sentinel, 0, sizeof(sentinel));
	cur = &sentinel;
	//buf和buf2用於儲存查詢結果,見下文的getanswer()
	buf = malloc(sizeof(*buf));
	if (buf == NULL) {
		h_errno = NETDB_INTERNAL;
		return NS_NOTFOUND;
	}
	buf2 = malloc(sizeof(*buf2));
	if (buf2 == NULL) {
		free(buf);
		h_errno = NETDB_INTERNAL;
		return NS_NOTFOUND;
	}

	//這段邏輯非常重要,決定了是否發起AAAA與A查詢以及它們的順序
	switch (pai->ai_family) {
	case AF_UNSPEC:
		//當沒有指定具體的failmy時,優先IPv6
		/* prefer IPv6 */
		q.name = name;
		q.qclass = C_IN;
		q.answer = buf->buf;
		q.anslen = sizeof(buf->bu