ping的时候unknown host
前几天有个前同事出了个题目考我们, 在linux上面ping item.jd.hk, 或者是curl的时候, 报unknown host的错误. 但是windows, mac上面正常. 让我们想一下原因是什么.
先host看一下,
% host item.jd.hk
item.jd.hk is an alias for *.jd.hk.gslb.qianxun.com.
*.jd.hk.gslb.qianxun.com has address 106.39.164.182
*.jd.hk.gslb.qianxun.com has address 120.52.148.32
先是Cname到 *.jd.hk.gslb.qianxun.com, 然后 *.jd.hk.gslb.qianxun.com是指向两个IP. 看起来好像没有问题.
但ping的时候, 的确会报错
% ping item.jd.hk
ping: unknown host item.jd.hk
网上搜索一下ping的源码, 很快就可以定位, 是gethostbyname这个函数返回了Null
hp = gethostbyname(target);
if (!hp) {
(void)fprintf(stderr,
"ping: unknown host %s\n", target);
exit(2);
}
然后找gethostbyname的代码, 这个是glibc的函数, google了好一番,终于找到这里:https://fossies.org/dox/glibc-2.23/gethnamaddr_8c_source.html#l00486 (当前还是2.23, 以后版本更新后, 可能需要相应的修改URL)
gethostbyname调用了gethostbyname2, gethostbyname2最后是调用了2个函数. 先是querybuf发送一个dns查询的请求,然后getanswer解析dns请求的返回.
struct hostent *
gethostbyname (const char *name)
{
struct hostent *hp;
if (__res_maybe_init (&_res, 0) == -1) {
__set_h_errno (NETDB_INTERNAL);
return (NULL);
}
if (_res.options & RES_USE_INET6) {
hp = gethostbyname2(name, AF_INET6);
if (hp)
return (hp);
}
return (gethostbyname2(name, AF_INET));
}
struct hostent *
gethostbyname2 (const char *name, int af)
{
...
...
buf.buf = origbuf = (querybuf *) alloca (1024);
if ((n = __libc_res_nsearch(&_res, name, C_IN, type, buf.buf->buf, 1024,
&buf.ptr, NULL, NULL, NULL, NULL)) < 0) {
if (buf.buf != origbuf)
free (buf.buf);
Dprintf("res_nsearch failed (%d)\n", n);
if (errno == ECONNREFUSED)
return (_gethtbyname2(name, af));
return (NULL);
}
ret = getanswer(buf.buf, n, name, type);
if (buf.buf != origbuf)
free (buf.buf);
return ret;
通过tcpdump抓包, 可以看到请求正常的发出了, 也收到了正确的返回(tpcumpd里面可以看到完整的记录和解析)
192.168.0.1.53 > 192.168.0.110.37612: 37604 3/0/0 item.jd.hk. CNAME *.jd.hk.gslb.qianxun.com., *.jd.hk.gslb.qianxun.com. A 120.52.148.32, *.jd.hk.gslb.qianxun.com. A 106.39.164.182 (98)
所以querybuf可以不用看, 直接看getanswer.
getanswer, 简单来说, 就是解析dns response里面的内容, 一一检查分析.
如果当前记录是A记录(不明白当前记录是A记录什么意思的,以及看不懂上面的tcpdump内容的, 可以搜索一下dns返回的格式),就会调用name_ok(也就是res_hnok这个函数). 在res_hnok中, 如果认为response中格式有错, 直接跳出解析的while循环, 然后返回Null, 随后gethostbyname也返回Null. (errno在哪里设置的, 找不到了, 印象中那天还找到了).
res_hnok函数的定义在https://fossies.org/dox/glibc-2.23/res__comp_8c_source.html
int
res_hnok(const char *dn) {
int pch = PERIOD, ch = *dn++;
while (ch != '\0') {
int nch = *dn++;
if (periodchar(ch)) {
(void)NULL;
} else if (periodchar(pch)) {
if (!borderchar(ch))
return (0);
} else if (periodchar(nch) || nch == '\0') {
if (!borderchar(ch))
return (0);
} else {
if (!middlechar(ch))
return (0);
}
pch = ch, ch = nch;
}
return (1);
}
京东把.jd.hk Cname到了.jd.hk.gslb.qianxun.com, .jd.hk.gslb.qianxun.com又A记录到了IP. 这样的话, gethostbyname在解析到.jd.hk.gslb.qianxun.com的时候, 当做A记录解析, 但第一个字母是*, 报错返回Null.
实际上, 把一个特定的域名, 比如 item.jd.hk Cname到了*.jd.hk.gslb.qianxun.com,然后再到A记录,也是同样的问题. 本质上就是,dns返回的所有回答里面,A记录的host要符合res_hnok函数的检查. 域名的规范可以参考rfc 1035 2.3.1章节.
解决方案
和前同事也确认了一下他们的解决方案, 就是把*.jd.hk指向star.jd.hk.gslb.qianxun.com, star.jd.hk.gslb.qianxun.com再配置A记录.
后续
-
如果配置了.jk.hk A XXX.XXX.XXX.XXX, 可以直接查询.jk.hk这个东西, 也是可以返回这个IP的, 但同样的原因, host tcpdump都可以看到,但ping curl会报错. 如果是查询item.jd.hk, 会返回item.jd.hk A XXX.XXX.XXX.XXX这种回答,完全符合规范.
-
我回家之后继续测试的时候, 发现随便找一个host, 比如abcdfadsf.jh.hk,第一次是unknown host,第二次就OK了. 抓包分析发现, 是我的路由器把这条记录缓存了, 第二次ping的时候, 直接返回了如abcdfadsf.jh.hk A XXX.XXX.XXX.