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

2022-10-19  本文已影响0人  重庆八怪

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

本文主要对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主要如下:

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

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

如果解析出现错误比如/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表中密码的。实际上这个动作,会根据输入的用户和其(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:空字符?

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

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

(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这种类型

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

因此即便我们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相关,这个前面已经说过了。

mysql> select * from performance_schema.host_cache \G
*************************** 1. row ***************************
                                        IP: 192.168.1.101
                                      HOST: NULL
                            HOST_VALIDATED: YES
...

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

开启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

以上。。

上一篇 下一篇

猜你喜欢

热点阅读