美文网首页
MySQL:DNS反解析和用户密码比对方式

MySQL:DNS反解析和用户密码比对方式

作者: 重庆八怪 | 来源:发表于2022-10-19 16:21 被阅读0次

    在MySQL中存在一个DNS反解析的功能,也就是通过客户端的IP地址反解析为hostname,涉及的设置和参数包含如下:

    • --skip-name-resolve
    • --skip-host-cache
    • host_cache_size

    本文主要对DNS反解析进行说明,仅供参考。代码版本5.7.22.


    一、本地连接和远端连接

    实际上本地连接使用的是unix本地socket(unix domain socket)如下,

    -S'/tmp/mysql3325.sock'
    

    而远端连接使用的TCP连接(TCP socket)如下,

    -u mytest -p'gelc123' -h 192.168.1.63 -P 3325
    

    实际上这两种连接方式在确认连接方式的时候是有区别的。在MySQL中判定这个也显得比较简单,如果连接属性中给定的是hostname就是本地连接,如果没有给定就是TCP 连接。这个在check_connection函数的开头就在确认如下:

     if (!thd->m_main_security_ctx.host().length)     // If TCP/IP connection
      { //如果没有 主机名就是TCP 连接
    ...
    else /* Hostname given means that the connection was on a socket */
    //如果
    

    接下面进行描述。

    二、DNS反解析相关内容

    这部分和我们host cache有关,判定也稍微复杂一些,我们来看看大概流程

    如果没有设置--skip-name-resolve 则进行,则调用函数ip_to_hostname进行DNS反解析,在ip_to_hostname主要如下:

    • 如果是127.0.0.1 则说明是回环地址,强制反解析为localhost,然后结束流程
    • 如果没有设置--skip-name-resolve 则进行,主要是在host_cache中进行寻找,如果找到了进行max_connect_errors的判定,如果超过了不允许登录,如果找到了当然就结束了。
    • 如果host_cache也没找到(或者设置了--skip-host-cache),则进行实际的DNS反解析,实际上核心就是调用的Linux api getnameinfo,其主要和/etc/hosts、 /etc/resolv.conf、/etc/nsswitch.conf 等文件相关,其api带入的flag为NI_NAMEREQD,那么如果找不到就会返回错误EAI_NONAME,但是任何getnameinfo的报错都会打印日志(warnings)
    IP address '%s' could not be resolved: %s
    
    • 不管Linux api getnameinfo解析是否成功还会将这条信息放入到host_cache中,以便下次直接在host_cache中就能找到。如果解析失败插入到host_cache中的hostname为NULL(add_hostname(ip_key, NULL, validated, &errors);)

    总的说来DNS反解析host_cache的作用,就是避免在没有设置--skip-name-resolve 的情况下,避免重复的调用Linux api getnameinfo进行反解析的代价,结合--skip-host-cache或者host_cache_size=0那么就有如下一些情况发生:

    • --skip-name-resolve设置了并且--skip-host-cache或者host_cache_size=0
      由于--skip-name-resolve设置了直接跳过一切的反解析步骤
    • --skip-name-resolve设置了但是没有设置--skip-host-cache或者host_cache_size=0
      由于--skip-name-resolve设置了直接跳过一切的反解析步骤
    • --skip-name-resolve没有设置但是设置--skip-host-cache或者host_cache_size=0
      这种情况,虽然用不到使用不到host_cache,但是每次的反解析直接使用是Linux api getnameinfo进行反解析,并且127.0.0.1也会反解析为localhost
    • 都没有设置
      那么就严格按照上面的流程进行,127.0.0.1也会反解析为localhost,并且在host cache中查找,找不到就调用Linux api getnameinfo进行反解析

    如果解析出现错误比如/etc/hosts中没有写相关信息,则报错

    IP address '%s' could not be resolved: %s
    

    如果反解析到了hostname,还会设置上下文的hostname为解析到hostname,并且设置host_or_ip为解析到hostname。

    而本地连接就简单多了,没有什么解析不解析的,直接指定hostname为localhost就可以了,


    image.png

    并且设置host_or_ip为localhost。

    随后反解析的hostname和ip地址都会供密码插件使用。我们最关心可能是如果反解析失败是否会影响到登录,这也是我最担心的。

    三、native_password 插件如何验证密码

    实际上这部分和密码插件有很大的关系,我们就看常用的native_password插件。
    经过前面的DNS反解析过后,可能解析到hostname,接下来就是和user表中的信息进行匹配了。
    内部存储的时候会有3个变量存在一个叫做MPVIO_EXT的mpvio上下文中,当然这里面还有很多元素,比如在user表中找到的密码串(加盐后)也会存储在其中,我们关注的如下:

    ip:客户端的IP地址
    host:客户端经过DNS反解析后的hostname
    auth_info::host_or_ip :如果DNS反解析到host就是hostname,如果没有就是ip,这个和我们报错信息有关
    
    主要的接口为
    check_connection
     ->acl_authenticate
      ->do_auth_once
       ->native_password_authenticate(插件相关)
    

    其中native_password_authenticate就是native_password 插件密码验证的内容,主要完成的工作如下:

    • 连接握手
    • 根据user表中的信息匹配用户,查询密码
    • 验证密码

    这里我们需要关注的是其如何查询user表中密码的。实际上这个动作,会根据输入的用户和其(hostname或者ip)进行验证,因此即便是没有反解析到hostname,客户端的ip是一定有的,但是查找user信息的时候就是@ip这种形式,而不是@hostname,言外之意如果你的用户为test@hostname,但是由于DNS反解析失败,那么只能根据ip进行查找了。我们来看看这部分,实际上在函数find_mpvio_user中重点如下:

    find_mpvio_user:
      for (ACL_USER *acl_user_tmp= acl_users->begin();
           acl_user_tmp != acl_users->end(); ++acl_user_tmp)//循环acl users
      {
        if ((!acl_user_tmp->user ||  //用户名为空
             !strcmp(mpvio->auth_info.user_name, acl_user_tmp->user)) && //用户名
            acl_user_tmp->host.compare_hostname(mpvio->host, mpvio->ip)) //IP和hostname
        {
          mpvio->acl_user= acl_user_tmp->copy(mpvio->mem_root); //拿到了user中的密码
    ...
        }
      }
      if (!mpvio->acl_user) //如果查找到的用户为空  假设用户存在 但是密码为空
      {
        /*
          Pretend the user exists; let the plugin decide how to handle
          bad credentials.
        */
        LEX_STRING usr= { mpvio->auth_info.user_name, //传入的用户 
                          mpvio->auth_info.user_name_length };
        mpvio->acl_user= decoy_user(usr, mpvio->mem_root);
    ...
      }
    

    这里我们明显看到在循环acl_users,这个信息就是user表的内存信息,并且做了排序,排序的规则没去仔细看,但是来自sql_auth_cache.cc:get_sort函数,其排列的顺序在函数注释中有如下,

       1. no wildcards:没有通配符
       2.strings containg wildcards and non-wildcard characters:包含部分通配符
       3.single muilt-wildcard character('%'):通配符%
       4.empty string:空字符?
    

    这也是我们查找匹配用户的规则。

    在这个循环中我们看到条件为(先不考虑空用户):

    • !strcmp(mpvio->auth_info.user_name, acl_user_tmp->user):如果输入的用户名和user表中的用户名相等。
    • acl_user_tmp->host.compare_hostname(mpvio->host, mpvio->ip):不考虑user表中的空hostname,那么判定如下:
    (host_arg &&
           !wild_case_compare(system_charset_info, host_arg, hostname)) ||
          (ip_arg && !wild_compare(ip_arg, hostname, 0))
    

    根据断路原则:

    1. 如果DNS反解析没有解析到hostname则host_arg为NULL,直接用ip进行判定
    2. 如果DNS反解析解析到hostname则优先比较 user@hostname这种用户(当然这个还要看排序规则),如果不对才进行ip的判定,也就是是否为user@ip这种类型

    因此我们知道这里有如下结论:

    • 如果DNS没有反解析到hostname,直接用客户端的ip和user表中的信息进行匹配
    • 如果user表中压根就不存在user@hostname这种用户,那么还是会通过user@ip这种用户进行匹配的。

    因此即便我们MySQL DNS反解析有问题,通过user@ip这种用户登录是没有问题的,但是前提是你建立的用户是user@ip这种形式的。
    这里还需要注意一点如果user表中没有用户匹配到,那么内存信息中是一个没有密码的用户,这种用户在进行密码校验的时候依旧报错密码不对,也就是如下代码:

    native_password_authenticate:
      info->password_used= PASSWORD_USED_YES; //是否使用了密码
      if (pkt_len == SCRAMBLE_LENGTH)
      {
        if (!mpvio->acl_user->salt_len)
          DBUG_RETURN(CR_AUTH_USER_CREDENTIALS); //如果收到的有密码 ,但是user中没有,则报错
        DBUG_RETURN(check_scramble(pkt, mpvio->scramble, mpvio->acl_user->salt) ?
                    CR_AUTH_USER_CREDENTIALS : CR_OK); //验证密码
      }
    

    一旦用户匹配到了密码也就定下来了,那么需要对输入的密码进行判定,这密码判定实际上在check_scramble中(如上),它输入的刚好就是通过socket读取到了密码和在user表中找到的密码,然后进行密码的比对,如果密码不对就会报错。

    四、相关场景和报错信息

    有了前面的分析,我们来看看几个相关的场景。DNS反解析成功还是失败通常和主机的/etc/hosts相关,这个前面已经说过了。

    • DNS反解析失败,用户是user@hostname的定义
      这种情况首先日志报警为IP address could not be resolved,主机名为NULL,并且插入到host_cache中,密码验证使用ip进行查询,但是用户为hostname,因此找不到相关的信息,直接按没有密码进行处理,也就是密码错误。
      这种情况下,如果接着在主机的/etc/hosts中进行添加响应的IP和主机名,再次登录依旧不行,因为host_cache已经缓存了,如下,
    mysql> select * from performance_schema.host_cache \G
    *************************** 1. row ***************************
                                            IP: 192.168.1.101
                                          HOST: NULL
                                HOST_VALIDATED: YES
    ...
    

    然后根据流程如果缓存命中了,就不会进行实际的解析了,依旧报错,需要flush hosts一次。

    • DNS反解析失败,用户是user@IP的定义
      这种情况下日志报警为IP address could not be resolved,主机名为NULL,并且插入到host_cache中,密码验证使用ip进行查询,发现用户存在,校验密码后,登录成功。

    • DNS反解析成功,用户是user@IP的定义
      这种情况下当然也没有任何问题,因为校验用户的时候也会校验user@IP这种用户,只是在user@hostname校验过后。

    • DNS反解析成功,用户是user@hostname的定义
      这种就是正常的情况了,没啥说的,肯定没问题的。

    • 本地登录使用 -h 127.0.0.1 -P 3306 这种方式,用户为root@localhost
      这也是最常见的一种登录方式,如果发现这种能登录上去,那么说明至少没有设置--skip-name-resolve,因为一旦设置了,TCP连接下的127.0.0.1 这个回环地址不会解析为localhost,因此登录是失败的。我们需要做的就是用-S'' 的方式登录就可以了,因为本地连接始终为localhost。如下:

    开启DNS反解析的时候
    [root@mgr3 ~]# /opt/my_mysql/bin/mysql -uroot  -h 127.0.0.1 -P 3325
    Welcome to the MySQL monitor.  Commands end with ; or \g.
    ...
    mysql> exit
    Bye
    关闭DNS反解析的时候
    [root@mgr3 ~]# /opt/my_mysql/bin/mysql -uroot  -h 127.0.0.1 -P 3325
    ERROR 1045 (28000): Access denied for user 'root'@'127.0.0.1' (using password: NO)
    

    其次,需要注意的是,即便是用户不存在,我们在上面解析中,发现用户没找到的情况,是虚构的一个没有密码的用户,那么在验证密码的时候肯定是错误的,因此也是密码错误,并且返回的错误中如果DNS反解析成功了返回的是hostname,如果失败返回的是IP地址如下(这来自前面我们说的host_or_ip这个变量):

    解析失败:
    [root@mgr1 tmp]# /opt/mysql/mysql3310/install/mysql31/bin/mysql -u mytest -p'gelc1234' -h 192.168.1.63 -P 3325
    mysql: [Warning] Using a password on the command line interface can be insecure.
    ERROR 1045 (28000): Access denied for user 'mytest'@'192.168.1.101' (using password: YES)
    解析成功:
    [root@mgr1 tmp]# /opt/mysql/mysql3310/install/mysql31/bin/mysql -u mytest111 -p'gelc1234' -h 192.168.1.63 -P 3325
    mysql: [Warning] Using a password on the command line interface can be insecure.
    ERROR 1045 (28000): Access denied for user 'mytest111'@'mgr10' (using password: YES)
    
    日志如下:
    2022-10-19T07:58:56.571803Z 79 [Note] Access denied for user 'mytest111'@'192.168.1.101' (using password: YES)
    2022-10-19T07:59:11.041723Z 80 [Note] Access denied for user 'mytest111'@'mgr10' (using password: YES)
    

    注意@后面的部分就是表名是否DNS反解析成功了。

    五、总结

    为什么需要看看这个东西呢,因为虽然我自己在使用时候是直接--skip-host-cache和--skip-name-resolve的设置,但是很多朋友不是的,是开启了DNS反解析功能的,因此做了一些学习。
    总而言之,这个DNS反解析真的麻烦(吐血狂喷)。唯一的好处我觉得就是能够让用户user@hostname这种用户登录到数据库。但是这一般不是必须的,因此建议直接全部跳过DNS反解析这部分,建立的用户全部写IP或者通配符,也不会有很多很多的歧义。
    另外如果是开启了反解析,我们依旧可以使用user@IP这种用户登录(建议都是这种类型的用户),因为从流程上看,即便反解析失败或者没有user@hostname这种用户依旧会通过IP进行用户查找。但是解析失败可能出现DNS反解析比较慢的问题,因此还是建议在/etc/hosts配置所有客户端的地址。

    六、部分代码流程

    check_connection
    
     -> if (!thd->m_main_security_ctx.host().length)
       如果是TCP连接
       ->if (!(specialflag & SPECIAL_NO_RESOLVE))
         没有指定了选项 --skip-name-resolve
         ->ip_to_hostname  
          (ip_storage=0x7fff80000a28, ip_string=0x7fff80007f90 "192.168.1.101", hostname=0x7fff9f211cf8, connect_errors=0x7fff9f211d1c)
           -> is_ip_loopback(ip)
              如果是回环地址127.0.0.1
              ->*hostname= (char *) my_localhost;
              直接将hostname设置为localhost,直接return 0 
           -> 定义一个ip_key的内存,并且将IP地址传入到这个内存
              prepare_hostname_cache_key(ip_string, ip_key)
              将ip的字符串传入到这个内存中
           -> 如果没有跳过 skip host cache ,设置参数 
              (specialflag & SPECIAL_NO_HOST_CACHE)
              -> 在缓存中查找
                 hostname_cache_search(ip_key)
                -> 如果找到,找到的对象为entry
                   if (entry) ....
                   
                   返回得到的hostname
                  
           如果没有找到,则进行实际的解析,注意这里即便是设置了skip host cache也会进行实际的解析。
           -> 定义hostname_buffer 用于存储解析到的hostname
           -> err_code= vio_getnameinfo(ip, hostname_buffer, NI_MAXHOST, NULL, 0, NI_NAMEREQD); 
             通过IP反解析hostname
              -> vio_getnameinfo  getnameinfo 主要是通过/etc/hosts和/etc/service等进行域名解析,解析到登入IP的域名
                 ->getnameinfo 带入 NI_NAMEREQD
                   如果找不到hosts配置则像错误一样对待,返回errno
                   如果找到则进入hostname_buffer
           -> 如果err_code存在
              报出warnings
              sql_print_warning("Host name '%s' could not be resolved: %s",
                               hostname_buffer,
                               gai_strerror(err_code));
             如果返回错误为 EAI_NONAME ,就是没解析到,为getnameinfo的返回值,设置validated为ture
             如果返回错误为其他,则设置validated为false
             add_hostname
             -> 加入到host cache中如下,
               mysql> select *from host_cache \G
                                                 IP: 192.168.1.101
                                               HOST: NULL
                                     HOST_VALIDATED: YES
             直接返回0,这里导致一个异常,即便密码和/etc/hosts 加入后依旧存在,见下文
         ->thd->m_main_security_ctx.assign_host(host, host? strlen(host) : 0) 
           将解析到的 hostname写入到THD的属性m_host中
         ->main_sctx_host= thd->m_main_security_ctx.host();
           将hostname和长度封装到main_sctx_host中
         ->解决hostname超长的问题 最大长度为60(HOSTNAME_LENGTH)字节一般不一超出  
     ->如果是本地连接,主机名为localhost
       连接认证走的是localhost
    

    以上。。

    相关文章

      网友评论

          本文标题:MySQL:DNS反解析和用户密码比对方式

          本文链接:https://www.haomeiwen.com/subject/ofarzrtx.html