域名随机大小写导致libevent2的异步DNS解析失败

libevent内置的异步DNS解析器采用了一种叫做DNS-0x20 encoding的hacking手段来防止DNS投毒攻击。但是这种做法是有问题的,在某些环境下会导致DNS解析全部失败。

DNS协议对域名大小写的规定

数据库中,域名应该是不分大小写的。

DNS协议的RFC标准是http://tools.ietf.org/html/rfc1034。在它的3.5节中特别有一段话

"Note that while upper and lower case letters are allowed in domain names, no significance is attached to the case. That is, two names with the same spelling but different case are to be treated as if identical."

DNS答复中,应保持域名大小写不变。DNS消息有三种类型 请求、答复、更新。请求包中域名是怎样的大小写,答复包中也应该保持一致。例如,我要查www.baidU.com,那收到的答复消息中域名也应该是www.baidU.com,而不是www.baidu.com或www.Baidu.com。而且在数据库中,www.baidU.com、www.baidu.com和www.Baidu.com应该是同一条记录。

实际上,有些DNS Server并不遵守标准。

下面是我做的实验。我在我的windows 7上用nslookup www.Baidu.com 202.106.0.20向联通的DNS(202.106.0.20)发起了请求对www.Baidu.com的查询请求,并用wireshark抓取来往的包。

请求:

0000   00 02 01 00 00 01 00 00 00 00 00 00 03 77 77 77  .............www
0010   05 42 61 69 64 75 03 63 6f 6d 00 00 01 00 01     .Baidu.com.....

答复:

0000   00 02 80 00 00 01 00 01 00 00 00 00 03 77 77 77  .............www
0010   05 62 61 69 64 75 03 63 6f 6d 00 00 01 00 01 c0  .baidu.com......
0020   0c 00 01 00 01 00 00 01 f4 00 04 77 4b d9 38     ...........wK.8

可以很明显的看出答复的包中,域名被全小写了。据我猜测,不是联通DNS Server的问题,而是中途被我的ISP搞了鬼。

什么是DNS-0x20 encoding?

它是一种未被标准化的防伪机制。DNS尽管本来就有TransactionID,但是很容易被伪造。于是就有人想,如果我们把请求包中的域名进行随机大小写,并且DNS server确实遵守rfc 1034规范,按照原样返回,那么这样就增加了中途投毒的难度。因为中途如果有人想要篡改,就得维持一个session来记录当初发过去的包是大写还是小写。ascii表中,大写字母从0x41开始,小写字母从0x61开始,恰好相差0x20。即 'A' | 0x20 = 'a'; 所以这种随机大小写的方式被称为0x20 encoding。但是我觉得,到底是你太笨还是你把别人想的太笨了?

实际上,google在构建它的public dns server(8.8.8.8)时确实也采用了这种方式。但是它深知现实与理想是不一致的,所以采用了一套白名单机制。只有位于白名单中的DNS server,才用0x20 encoding。google说这样的请求占了它70%以上的流量("The whitelisted nameservers comprise more than 70% of our traffic." from https://developers.google.com/speed/public-dns/docs/security

Libevent如何实现DNS-0x20 encoding

Libevent是一套非常流行的网络库,比如Chrome、memcached就是依靠它实现非阻塞IO。在编写非阻塞IO程序时,很重要的一点是连域名解析也必须是异步的,也就是说glibc里面的那些域名解析函数都不能用。为此,libevent内置了一套异步DNS解析器,叫evdns。但是evdns的实现就要简单粗暴的很多。它强行对所有请求都采用了0x20 encoding。

在evdns_base结构体内部有一个字段:

/* true iff we will use the 0x20 hack to prevent poisoning attacks. */

int global_randomize_case;

这个字段在evdns_base_new这个函数中被初始化为1,所以这个功能默认是打开的。

global_randomize_case这个开关会影响如何构造DNS request。

在evdns.c的request_new这个函数中,

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 Server收到reply之后,client会调用reply_parse函数来解析。下面是函数栈。

 reply_parse(evdns_base * base, unsigned char * packet, int length)
 nameserver_read(nameserver * ns) 
 nameserver_ready_callback(int fd, short events, void * arg)
 event_persist_closure(event_base * base, event * ev) 
 event_process_active_single_queue(event_base * base, event_list * activeq) 
 event_process_active(event_base * base) 
 event_base_loop(event_base * base, int flags)
 event_base_dispatch(event_base * event_base)

在 int reply_parse(struct evdns_base *base, u8 *packet, int length)这个函数中,它要在请求包和答复包中按照DNS lable做查找匹配。

下面的tmp_name来自reply,cmp_name来自request。

int name_matches=0;
//...
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;
}

如果全找完后name_matches是0。那么就是DNS解析失败了。

在libevent的samples目录下有一个小程序,你可以试下

# ./dns-example -v www.baidu.com

INFO: Parsing resolv.conf file /etc/resolv.conf

INFO: Added nameserver 219.239.26.42:53 as 0xbf86d0

INFO: Added nameserver 202.106.0.20:53 as 0xbf88a0

EVUTIL_AI_CANONNAME in example = 2

resolving (fwd) www.baidu.com...

INFO: Resolve requested for www.baidu.com

INFO: Setting timeout for request 0xbf8a70, sent to nameserver 0xbf86d0

INFO: Removing timeout for request 0xbf8a70

www.baidu.com: No answer (66)

不应该是No answser啊!

如果你好奇,既然chrome用的也是libevent,那么为何没这个问题呢?答案是:chrome没用libevent内置的DNS解析器,它只用了基础IO部分。

最后,我给libevent提了一个patch请求它们修改默认行为。不知道会不会被merge。https://github.com/libevent/libevent/pull/97

此博客中的热门博文

在windows下使用llvm+clang

少写代码,多读别人写的代码

tensorflow distributed runtime初窥