转:MySQL:DNS反解析和用户密码比对方式
原文: mp.weixin.qq.com
在 MySQL 中存在一个 DNS 反解析的功能,也就是通过客户端的 IP 地址反解析为 hostname,涉及的设置和参数包含如下:
- –skip-name-resolve
- –skip-host-cache
- host_cache_size
本文主要对 DNS 反解析进行说明,仅供参考。代码版本 5.7.22.
一、本地连接和远端连接
实际上本地连接使用的是 unix 本地 socket(unix domain socket)如下,
1 |
|
而远端连接使用的 TCP 连接(TCP socket)如下,
1 |
|
实际上这两种连接方式在确认连接方式的时候是有区别的。在 MySQL 中判定这个也显得比较简单,如果连接属性中给定的是 hostname 就是本地连接,如果没有给定就是 TCP 连接。这个在 check_connection 函数的开头就在确认如下:
1 |
|
接下面进行描述。
二、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)
1 |
|
- 不管 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 中没有写相关信息,则报错
1 |
|
如果反解析到了 hostname,还会设置上下文的 hostname 为解析到 hostname,并且设置 host_or_ip 为解析到 hostname。
而本地连接就简单多了,没有什么解析不解析的,直接指定 hostname 为 localhost 就可以了,并且设置 host_or_ip 为 localhost。
随后反解析的 hostname 和 ip 地址都会供密码插件使用。我们最关心可能是如果反解析失败是否会影响到登录,这也是我最担心的。
三、native_password 插件如何验证密码
实际上这部分和密码插件有很大的关系,我们就看常用的 native_password 插件。经过前面的 DNS 反解析过后,可能解析到 hostname,接下来就是和 user 表中的信息进行匹配了。内部存储的时候会有 3 个变量存在一个叫做 MPVIO_EXT 的 mpvio 上下文中,当然这里面还有很多元素,比如在 user 表中找到的密码串(加盐后)也会存储在其中,我们关注的如下:
1 |
|
其中 native_password_authenticate 就是 native_password 插件密码验证的内容,主要完成的工作如下:
- 连接握手
- 根据 user 表中的信息匹配用户,查询密码
- 验证密码
这里我们需要关注的是其如何查询 user 表中密码的。实际上这个动作,会根据输入的用户和其(hostname 或者 ip)进行验证,因此即便是没有反解析到 hostname,客户端的 ip 是一定有的,但是查找 user 信息的时候就是 @ip 这种形式,而不是 @hostname,言外之意如果你的用户为 test@hostname,但是由于 DNS 反解析失败,那么只能根据 ip 进行查找了。我们来看看这部分,实际上在函数 find_mpvio_user 中重点如下:
1 |
|
这里我们明显看到在循环 acl_users,这个信息就是 user 表的内存信息,并且做了排序,排序的规则没去仔细看,但是来自 sql_auth_cache.cc:get_sort 函数,其排列的顺序在函数注释中有如下,
1 |
|
这也是我们查找匹配用户的规则。
在这个循环中我们看到条件为(先不考虑空用户):
!strcmp(mpvio->auth_info.user_name, acl_user_tmp->user):如果输入的用户名和 user 表中的用户名相等。
acl_user_tmp->host.compare_hostname(mpvio->host, mpvio->ip):不考虑 user 表中的空 hostname,那么判定如下:
1 |
|
根据断路原则:
如果 DNS 反解析没有解析到 hostname 则 host_arg 为 NULL,直接用 ip 进行判定
如果 DNS 反解析解析到 hostname 则优先比较 user@hostname 这种用户(当然这个还要看排序规则),如果不对才进行 ip 的判定,也就是是否为 user@ip 这种类型
因此我们知道这里有如下结论:
如果 DNS 没有反解析到 hostname,直接用客户端的 ip 和 user 表中的信息进行匹配
如果 user 表中压根就不存在 user@hostname 这种用户,那么还是会通过 user@ip 这种用户进行匹配的。
因此即便我们 MySQL DNS 反解析有问题,通过 user@ip 这种用户登录是没有问题的,但是前提是你建立的用户是 user@ip 这种形式的。这里还需要注意一点如果 user 表中没有用户匹配到,那么内存信息中是一个没有密码的用户,这种用户在进行密码校验的时候依旧报错密码不对,也就是如下代码:
1 |
|
一旦用户匹配到了密码也就定下来了,那么需要对输入的密码进行判定,这密码判定实际上在 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 已经缓存了,如下,
1 |
|
然后根据流程如果缓存命中了,就不会进行实际的解析了,依旧报错,需要 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。如下:
1 |
|
其次,需要注意的是,即便是用户不存在,我们在上面解析中,发现用户没找到的情况,是虚构的一个没有密码的用户,那么在验证密码的时候肯定是错误的,因此也是密码错误,并且返回的错误中如果 DNS 反解析成功了返回的是 hostname,如果失败返回的是 IP 地址如下(这来自前面我们说的 host_or_ip 这个变量):
1 |
|
注意 @后面的部分就是表名是否 DNS 反解析成功了。
五、总结
为什么需要看看这个东西呢,因为虽然我自己在使用时候是直接 –skip-host-cache 和 –skip-name-resolve 的设置,但是很多朋友不是的,是开启了 DNS 反解析功能的,因此做了一些学习。总而言之,这个 DNS 反解析真的麻烦(吐血狂喷),跳过反解析就是最好的安慰。唯一的好处我觉得就是能够让用户 user@hostname 这种用户登录到数据库。但是这一般不是必须的,因此建议直接全部跳过 DNS 反解析这部分,建立的用户全部写 IP 或者通配符,也不会有很多很多的歧义。另外如果是开启了反解析,我们依旧可以使用 user@IP 这种用户登录(建议都是这种类型的用户),因为从流程上看,即便反解析失败或者没有 user@hostname 这种用户依旧会通过 IP 进行用户查找。但是解析失败可能出现 DNS 反解析比较慢的问题,因此还是建议在 / etc/hosts 配置所有客户端的地址。
六、部分代码流程
1 |
|
以上。
感谢关注 “MySQL 解决方案工程师”