1. 程式人生 > >android上libevent dns解析的一個bug修復

android上libevent dns解析的一個bug修復

    在測試我們開發的一個 APK(使用了 libevent-2.1.3-alpha 作為網路庫) 時發現一個奇怪的問題,域名解析有時報錯 Non-recoverable name resolution failure 。在公司偶爾報錯,後來程式改動了一下,出錯時重試幾次,問題沒再出現,以為好了。昨天換了個網路環境,結果報錯機率變得非常大。

    網際網路搜尋到這個錯誤的一個處理辦法,說在使用 getnameinfo() 函式時需要顯式指定其第二個引數 salen 為 sizeof(struct sockaddr_in) 或者 sizeof(struct sockaddr_in6) ,說是 Solaris 和 Android 上的 getnameinfo() 實現不會檢視 saddr 中的 sin_family 來計算出真正 salen 。我嘗試了一下,沒有解決問題,後來想想, libevent 根本就沒有使用系統的域名解析函式,完全是自己實現的,於是只好自己跟程式碼了。

    由於遠端除錯的環境沒有搭建起來,只能不斷地新增日誌,反覆檢視,非常耗時。最後還真給我找到了問題所在。

    libevent 的 dns 解析實現就在 evdns.c 這個檔案中,不過如果不懂得 DNS 協議,程式碼看起來可能比較難懂,我重溫了 DNS 協議,然後開始跟程式碼。

    libevent 在處理 DNS 解析時,針對域名引入了一個隨機大小寫的概念,在 evdns_base_new() 中把 global_randomize_case 預設設定為 1 ,然後在讀取域名伺服器配置檔案時根據裡面的 options 來修改。安卓上沒有 resolv.conf ,這些選項就沒有修正的機會,於是最終 global_randomize_case 還是 1。

    在 libevent 構造 DNS 請求( request_new() 函式)時,會根據 global_randomize_case 來決定是否對發起 dns 請求時傳入的域名進行大小寫隨機轉換,程式碼如下:

	if (base->global_randomize_case) {
		unsigned i;
		char randbits[(sizeof(namebuf)+7)/8];
		strlcpy(namebuf, name, sizeof(namebuf));
		evutil_secure_rng_get_bytes(randbits, (name_len+7)/8);
		for (i = 0; i < name_len; ++i) {
			if (EVUTIL_ISALPHA_(namebuf[i])) {
				if ((randbits[i >> 3] & (1<<(i & 7))))
					namebuf[i] |= 0x20;
				else
					namebuf[i] &= ~0x20;
			}
		}
		name = namebuf;
	}

    然後在處理 DNS 伺服器返回的結果時,從 DNS 請求列表中根據 trans_id 找到對應的 request ,拿 DNS 結果中解析出來的名字和 request 中的名字比較,如果不一致,就認為出錯了。詳情參考 reply_parse() 函式,其中 TESTNAME 巨集實現名字比對,原始程式碼如下:

#define TEST_NAME	 \
 do { tmp_name[0] = '\0';	 \
  cmp_name[0] = '\0';	 \
  k = j;	 \
  if (name_parse(packet, length, &j, tmp_name,	 \
   sizeof(tmp_name))<0)	 \
   goto err;	 \
  if (name_parse(req->request, req->request_len, &k,	\
   cmp_name, sizeof(cmp_name))<0)	 \
   goto err;	 \
  if (base->global_randomize_case) {	 \
   if (strcmp(tmp_name, cmp_name) == 0)	 \
    name_matches = 1;	 \
  } else {	 \
   if (evutil_ascii_strcasecmp(tmp_name, cmp_name) == 0) \
    name_matches = 1;	 \
  }	 \
 } while (0)

    這段程式碼是有問題的,global_randomize_case 標記和字串比較函式沒有匹配上,顛倒了。所以比對就出了問題,有時候正確,有時候不正確。修改成下面的程式碼就好了:
#define TEST_NAME							\
	do { tmp_name[0] = '\0';					\
		cmp_name[0] = '\0';					\
		k = j;							\
		if (name_parse(packet, length, &j, tmp_name,		\
			sizeof(tmp_name))<0)				\
			goto err;					\
		if (name_parse(req->request, req->request_len, &k,	\
			cmp_name, sizeof(cmp_name))<0)			\
			goto err;					\
		if (base->global_randomize_case) {			\
            if (evutil_ascii_strcasecmp(tmp_name, cmp_name) == 0)		\
				name_matches = 1;			\
		} else {						\
            if (strcmp(tmp_name, cmp_name) == 0) \
				name_matches = 1;			\
		}							\
	} while (0)

    這個問題我花費了4個多小時來跟,最後總算解決了,心終於放下了(之前出錯重試的修補方式總讓人不踏實)。