gethostbyname_r() 无法解析域名的探索

1. 背景介绍

博主无意间在测试已经撸好的 ZigBee 网关项目代码的时候,发现在某种情况下,把网关所连接的网络在有线网络和无线网络之间进行切换之后(不能访问外网的网络切换到能够访问外网的网络),调用 gethostbyname_r() 居然无法解析域名,使的网关与服务器不能建立连接。这可是非常严重的 Bug。

Linux 内核版本: 2.6.36


2. 探索过程

网关每次切换网络,都会重新 DHCP 申请 IP 地址,打开或者创建 resolv.conf 文件配置 DNS 服务器 IP 。之前也进行过类似测试,为什么之前没有测试出来?测试环境也没有变化(真的没有变化吗?细节很重要)

解决问题的思路大致如下:

  1. 百度/谷歌……
  2. 请教同事/同行……
  3. 硬着头皮上啊,在痛苦中坚持,在折磨中前进。
    也许在公司,被折磨的没有任何思绪,而这一切看似做的是无用功,可冥冥中不断的尝试,就不断的缩近了到达成功的距离。也许在你下班路上,灵感一来,想到某个点,第二天迫不及待的进行验证就解决了!博主就经常这样解决了问题。

一开始遇到这个问题也是百思不得其解,因为大部分的情况下都是正常的,而大部分情况下正常的这种现实很容易让人在寻找问题根源的时候迷失方向

首先是检查代码,不断的尝试,不断的排除。。。。。。
没辙只能有求于百度/谷歌了,可多次搜索无果。(或许我提问的关键字没有匹配,以至于搜索不出来。)
无奈之下,抱着试一试的态度请教了一下做路由器的同事,他说以前也遇见过类似的情况,还建议我去看下 gethostbyname_r() 的实现。

寻找问题的答案,要探索问题的根源,既然问题出在 gethostbyname_r() ,那何不去 gethostbyname_r() 函数内部探个究竟呢。当初我觉的该系统函数不会有问题,却不曾想到是配置有问题。

gethostbyname_r() —> __open_nameservers()

/*
 *  we currently read formats not quite the same as that on normal
 *  unix systems, we can have a list of nameservers after the keyword.
 */

int __open_nameservers()
{
    FILE *fp;
    int i;
#define RESOLV_ARGS 5
    char szBuffer[128], *p, *argv[RESOLV_ARGS];
    int argc;

    BIGLOCK;
    //跟踪代码到这里,问题似乎就明朗了。
    if (__nameservers > 0) {
        BIGUNLOCK;
        return 0;
    }

    if ((fp = fopen("/etc/resolv.conf", "r")) ||
            (fp = fopen("/etc/config/resolv.conf", "r")))
    {

        while (fgets(szBuffer, sizeof(szBuffer), fp) != NULL) {

            for (p = szBuffer; *p && isspace(*p); p++)
                /* skip white space */;
            if (*p == '\0' || *p == '\n' || *p == '#') /* skip comments etc */
                continue;
            argc = 0;
            while (*p && argc < RESOLV_ARGS) {
                argv[argc++] = p;
                while (*p && !isspace(*p) && *p != '\n')
                    p++;
                while (*p && (isspace(*p) || *p == '\n')) /* remove spaces */
                    *p++ = '\0';
            }

            if (strcmp(argv[0], "nameserver") == 0) {
                for (i = 1; i < argc && __nameservers < MAX_SERVERS; i++) {
                    __nameserver[__nameservers++] = strdup(argv[i]);
                    DPRINTF("adding nameserver %s\n", argv[i]);
                }
            }

            /* domain and search are mutually exclusive, the last one wins */
            if (strcmp(argv[0],"domain")==0 || strcmp(argv[0],"search")==0) {
                while (__searchdomains > 0) {
                    free(__searchdomain[--__searchdomains]);
                    __searchdomain[__searchdomains] = NULL;
                }
                for (i=1; i < argc && __searchdomains < MAX_SEARCH; i++) {
                    __searchdomain[__searchdomains++] = strdup(argv[i]);
                    DPRINTF("adding search %s\n", argv[i]);
                }
            }
        }
        fclose(fp);
        DPRINTF("nameservers = %d\n", __nameservers);
        BIGUNLOCK;
        return 0;
    }
    DPRINTF("failed to open %s\n", "resolv.conf");
    h_errno = NO_RECOVERY;
    BIGUNLOCK;
    return -1;
}

gethostbyname_r() 函数内部调用了 __open_nameservers()。
当首次调用 gethostbyname_r() 时,通过 __open_nameservers() 函数得知,如果 __nameservers 值为 0,则会去读取 resolv.conf 的内容,一旦获取到 DNS 服务器 IP 记录后,__nameservers 就会变为非 0,后续再次调用 gethostbyname_r() 时,都不会去读取 resolv.conf 文件了,如果首次读取 resolv.conf 文件中记录的 DNS 服务器 IP 地址是无效的,那么后续 gethostbyname_r() 所使用的 DNS 服务器 IP 都将是无效的 IP 地址。

分析到这里,一切就都明朗了。

那什么时候通过 DHCP 获取到的 DNS 服务器 IP 地址会被当作是无效的呢?

  1. 没连外网的路由器重启后,默认会把路由器本身的 IP 地址当作 DNS 服务器 IP 地址,如果设备从该路由器的网络中切换到另一个能够访问外网的路由器所在的网络,并且两个路由器不在同一个网段的情况下(如果在同一个网段,并且两个路由器自身 IP 地址一样的话,那切换后解析域名仍旧是正常的),此时设备在没连外网的路由器网络中通过 DHCP 获取到的 DNS 服务器 IP 地址就会认为是无效的。
  2. 用户手动设置了路由器中的 DNS 配置,并且配置了一个无效的 DNS 服务器 IP(这种可能性,做产品时无需考虑的)

现在看来,要出现以上 Bug,需满足以下条件

  1. 路由器必须是重启后的;
  2. 路由器重启后没有连过外网的(不能是一开始有外网,后续把路由器外网掐掉,因为这种情况下,路由器记录的 DNS 服务器 IP 将会是一个有效的 公网 IP 地址了。)
  3. 不同网络的路由器自身 IP 地址不能是一样的;
  4. 设备必须是重启后的;
  5. 设备重启后首次必须连接到该路由器的网络中。

很多时候条件一多,要想满足 Bug 出现的所有条件是有一定难度的。


3. 解决方案

解决方案固然重要,可最重要的仍旧是探索的过程。已知解决方案,推导探索过程很容易,倘若未知解决方案,是否仍能轻松的推导出探索过程,这需要打个大大的问号。

一切没有解决的小问题都是大问题;一切已经解决的大问题都是小问题。

3.1. 优化 DHCP 客户端代码

DHCP 在更新 resolve.conf 配置文件后,调用 res_init() 函数,重新初始化域名解析模块。该函数在头文件 “resolv.h” 中声明。

int res_init(void)
{
    struct __res_state *rp = &(_res);

    __close_nameservers();//清除已有的 DNS 服务器 IP 地址记录
    __open_nameservers();//重新获取 resolv.conf 文件中记录的 DNS 服务器 IP 地址
    rp->retrans = RES_TIMEOUT;
    rp->retry = 4;
    rp->options = RES_INIT;
    rp->id = (u_int) random();
    rp->nsaddr.sin_addr.s_addr = INADDR_ANY;
    rp->nsaddr.sin_family = AF_INET;
    rp->nsaddr.sin_port = htons(NAMESERVER_PORT);
    rp->ndots = 1;
    /** rp->pfcode = 0; **/
    rp->_vcsock = -1;
    /** rp->_flags = 0; **/
    /** rp->qhook = NULL; **/
    /** rp->rhook = NULL; **/
    /** rp->_u._ext.nsinit = 0; **/

    BIGLOCK;
    if(__searchdomains) {
        int i;
        for(i=0; i<__searchdomains; i++) {
            rp->dnsrch[i] = __searchdomain[i];
        }
    }

    if(__nameservers) {
        int i;
        struct in_addr a;
        for(i=0; i<__nameservers; i++) {
            if (inet_aton(__nameserver[i], &a)) {
                rp->nsaddr_list[i].sin_addr = a;
                rp->nsaddr_list[i].sin_family = AF_INET;
                rp->nsaddr_list[i].sin_port = htons(NAMESERVER_PORT);
            }
        }
    }
    rp->nscount = __nameservers;
    BIGUNLOCK;

    return(0);
}

调用以上函数后,使得后续调用 gethostbyname_r() 时,能够使用更新后的resolv.conf 文件中所记录的 DNS 服务器 IP 地址。

3.2. 修改内核代码

int gethostbyname_r(const char * name,
                struct hostent * result_buf,
                char * buf, size_t buflen,
                struct hostent ** result,
                int * h_errnop)
{
    ......
    ......

    __close_nameservers();//增加该行代码
    __open_nameservers();

    ......
    ......
}

void __close_nameservers(void)
{
    BIGLOCK;
    while (__nameservers > 0) {
        free(__nameserver[--__nameservers]);
        __nameserver[__nameservers] = NULL;
    }
    while (__searchdomains > 0) {
        free(__searchdomain[--__searchdomains]);
        __searchdomain[__searchdomains] = NULL;
    }
    BIGUNLOCK;
}

每次调用 gethostbyname_r() 时,函数内部都先调用 __close_nameservers() 再调用 __open_nameservers(),这样就能避免更新 resolve.conf 文件之后,因为 __nameservers 变量值大于 0,而不再去读取 resolve.conf 内容,继而无法获取到有效的 DNS 服务器 IP 地址,最终无法解析域名的情况发生。

本解决方案总感觉美中不足:
一个是每次都必须调用 __close_nameservers(),来解决 resolve.conf 文件更新而 gethostbyname_r() 无法同步其内容的问题,虽然此举并不会对性能造成很大影响,谁会有事没事一直进行 DNS 解析呢,但是看着就是不爽;
二个是需要改写内核代码,重新编译内核,并更新镜像到设备上,麻烦。

追求方便简单还是使用第一种方案。

3.3. service network restart

不过 service 命令到底执行了什么,请点击查看

由于内核不支持该命令,本方案只能放弃。


4. 相关延伸

以下延伸已有相关博客进行了阐述,博主就不重复造轮子了。请点击标题进入相关博客进行阅读。

4.1. 为何使用 gethostbyname_r() 替代 gethostbyname()

4.2. gethostbyname_r() 参数介绍

4.2. 函数可重入和不可重入的介绍


以上若有出入,欢迎指正。

猜你喜欢

转载自blog.csdn.net/QQ1452008/article/details/81222254