# 前言
在近期的mysql学习过程中,一直想做的一件事就是分析下mysql的协议。了解Mysql协议也是深入了解Mysql的过程。本章的内容由于篇幅过大,会拆分成两章来讲述。
Mysql版本:8.0.20
抓包工具:Wireshark
1.Mysql协议分析
在分析mysql之前我们首先要把mysql的ssl关掉,抓包过程中TLS解析比较麻烦。
1.1 关闭Mysql SSL
可以通过配置my.cnf文件加入skip_ssl指令关闭ssl,配置如下:
[mysqld]
skip_ssl
<center>图1-1-1 查询SSL关闭</center>
关闭SSL之后重启mysql服务,可以通过“show global variables like 'have_ssl%';” 查看ssl关闭情况,如图1-1-1。
通过客户端直接连接mysql的话可能会报“ERROR 2061 (HY000): Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.”错误,此时可以通过加入“--connect-expired-password” 参数进行访问。默认情况下Mysql8.0.20会采用caching_sha2_password插件进行密码交互,会存在缓存。此参数是密码的沙箱模式缓存过期。Mysql8.0以下版本采用mysql_native_password插件进行交互。
其次如果要抓包的话,连接客户端需要使用参数“--protocol=tcp”进行访问,这样方便抓包。
1.2 Mysql 抓包
抓包之前我们明确一下我们要抓包的内容,抓包内容如下:
$bin/mysql -h localhost -u root -p'123456' --protocol=tcp
mysql>use t1;
mysql>select * from t1;
1.2.1 Mysql 抓包
语句中我们只有两条语句,执行过程如图1-2-1:
<center>图1-2-1 查询过程</center>
我们直接使用Wireshark进行抓包,抓包格式如下:
No. | Source | Sport | Dport | Protocol | Length | Info |
---|---|---|---|---|---|---|
1 | localhost | 56933 | 3306 | TCP | 68 | 56933 → 3306 [SYN] Seq=0 Win=65535 Len=0 MSS=16344 WS=64 TSval=282542188 TSecr=0 SACK_PERM=1 |
2 | localhost | 3306 | 56933 | TCP | 68 | 3306 → 56933 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=16344 WS=64 TSval=282542188 TSecr=282542188 SACK_PERM=1 |
3 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=1 Ack=1 Win=408256 Len=0 TSval=282542188 TSecr=282542188 |
4 | localhost | 3306 | 56933 | TCP | 56 | [TCP Window Update] 3306 → 56933 [ACK] Seq=1 Ack=1 Win=408256 Len=0 TSval=282542188 TSecr=282542188 |
5 | localhost | 3306 | 56933 | MySQL | 134 | Server Greeting proto=10 version=8.0.20 |
6 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=1 Ack=79 Win=408192 Len=0 TSval=282542188 TSecr=282542188 |
7 | localhost | 56933 | 3306 | MySQL | 268 | Login Request user=root |
8 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=79 Ack=213 Win=408064 Len=0 TSval=282542188 TSecr=282542188 |
9 | localhost | 3306 | 56933 | MySQL | 62 | Auth Switch Request |
10 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=213 Ack=85 Win=408192 Len=0 TSval=282542188 TSecr=282542188 |
11 | localhost | 3306 | 56933 | MySQL | 67 | Response OK |
12 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=213 Ack=96 Win=408192 Len=0 TSval=282542188 TSecr=282542188 |
13 | localhost | 56933 | 3306 | MySQL | 93 | Request Query |
14 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=96 Ack=250 Win=408000 Len=0 TSval=282542188 TSecr=282542188 |
15 | localhost | 3306 | 56933 | MySQL | 139 | Response |
16 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=250 Ack=179 Win=408064 Len=0 TSval=282542188 TSecr=282542188 |
17 | localhost | 56933 | 3306 | MySQL | 78 | Request Query |
18 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=179 Ack=272 Win=408000 Len=0 TSval=282558403 TSecr=282558403 |
19 | localhost | 3306 | 56933 | MySQL | 113 | Response |
20 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=272 Ack=236 Win=408064 Len=0 TSval=282558403 TSecr=282558403 |
21 | localhost | 56933 | 3306 | MySQL | 63 | Request Use Database |
22 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=236 Ack=279 Win=408000 Len=0 TSval=282558403 TSecr=282558403 |
23 | localhost | 3306 | 56933 | MySQL | 74 | Response OK |
24 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=279 Ack=254 Win=408000 Len=0 TSval=282558403 TSecr=282558403 |
25 | localhost | 56933 | 3306 | MySQL | 75 | Request Query |
26 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=254 Ack=298 Win=408000 Len=0 TSval=282558404 TSecr=282558404 |
27 | localhost | 3306 | 56933 | MySQL | 214 | Response |
28 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=298 Ack=412 Win=407872 Len=0 TSval=282558407 TSecr=282558407 |
29 | localhost | 56933 | 3306 | MySQL | 72 | Request Query |
30 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=412 Ack=314 Win=407936 Len=0 TSval=282558407 TSecr=282558407 |
31 | localhost | 3306 | 56933 | MySQL | 152 | Response |
32 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=314 Ack=508 Win=407744 Len=0 TSval=282558409 TSecr=282558409 |
33 | localhost | 56933 | 3306 | MySQL | 67 | Request Show Fields |
34 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=508 Ack=325 Win=407936 Len=0 TSval=282558409 TSecr=282558409 |
35 | localhost | 3306 | 56933 | MySQL | 275 | Response |
36 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=325 Ack=727 Win=407552 Len=0 TSval=282558415 TSecr=282558415 |
37 | localhost | 56933 | 3306 | MySQL | 64 | Request Show Fields |
38 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=727 Ack=333 Win=407936 Len=0 TSval=282558415 TSecr=282558415 |
39 | localhost | 3306 | 56933 | MySQL | 140 | Response |
40 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=333 Ack=811 Win=407488 Len=0 TSval=282558415 TSecr=282558415 |
41 | localhost | 56933 | 3306 | MySQL | 64 | Request Show Fields |
42 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=811 Ack=341 Win=407936 Len=0 TSval=282558415 TSecr=282558415 |
43 | localhost | 3306 | 56933 | MySQL | 140 | Response |
44 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=341 Ack=895 Win=407360 Len=0 TSval=282558416 TSecr=282558416 |
45 | localhost | 56933 | 3306 | MySQL | 77 | Request Query |
46 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=895 Ack=362 Win=407936 Len=0 TSval=282563656 TSecr=282563656 |
47 | localhost | 3306 | 56933 | MySQL | 166 | Response |
48 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=362 Ack=1005 Win=407296 Len=0 TSval=282563658 TSecr=282563658 |
49 | localhost | 56933 | 3306 | MySQL | 61 | Request Quit |
50 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=1005 Ack=367 Win=407872 Len=0 TSval=282585990 TSecr=282585990 |
51 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [FIN, ACK] Seq=367 Ack=1005 Win=407296 Len=0 TSval=282585990 TSecr=282585990 |
52 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [ACK] Seq=1005 Ack=368 Win=407872 Len=0 TSval=282585990 TSecr=282585990 |
53 | localhost | 3306 | 56933 | TCP | 56 | 3306 → 56933 [FIN, ACK] Seq=1005 Ack=368 Win=407872 Len=0 TSval=282585990 TSecr=282585990 |
54 | localhost | 56933 | 3306 | TCP | 56 | 56933 → 3306 [ACK] Seq=368 Ack=1006 Win=407232 Len=0 TSval=282585990 TSecr=282585990 |
<center>表1-2-2 抓包数据</center>
如表1-2-2所示,“No.”编号 1~3 为TCP的三次握手。“No.”编号 51~54为TCP的四次挥手部分。其余部分为Mysql的通信数据部分。
<center>图1-2-3 数据包交互流程</center>
根据表1-2-2中抓包数据,我们可以得到如图1-2-3,我们的一个数据包交互流程。
在分析数据包时,会有Request Command的Server Command编码该编码在/mysql-8.0.20/include/my_command.h文件中:
enum enum_server_command {
COM_SLEEP, /** 0 */
COM_QUIT, /** 1 退出,可以看page_protocol_com_quit */
COM_INIT_DB, /** 2 初始化数据库, 可以看page_protocol_com_init_db */
COM_QUERY, /** 3 查询,可以看 page_protocol_com_query */
COM_FIELD_LIST, /** 4 列举字段,可以看 page_protocol_com_field_list */
COM_CREATE_DB, /** 5 创建数据库,可以看 ::dispatch_command */
COM_DROP_DB, /** 6 删除数据库,可以看 ::dispatch_command */
COM_REFRESH, /** 7 刷新,可以看 page_protocol_com_refresh */
COM_DEPRECATED_1, /** 8 < Deprecated, used to be COM_SHUTDOWN */
COM_STATISTICS, /** 9 查询服务端内部统计,可以看 page_protocol_com_statistics */
COM_PROCESS_INFO, /** 10 查看processs list 可以看 page_protocol_com_process_info */
COM_CONNECT, /** 11 当服务器被拒绝,连接 */
COM_PROCESS_KILL, /** 12 process kill,可以看 page_protocol_com_process_kill */
COM_DEBUG, /** 13 debug,可以看 page_protocol_com_debug */
COM_PING, /** 14 ping,可以看page_protocol_com_ping */
COM_TIME, /** 15 当服务器被拒绝 */
COM_DELAYED_INSERT, /** 16 功能已删除 */
COM_CHANGE_USER, /** 17 修改用户,可以看 page_protocol_com_change_user */
COM_BINLOG_DUMP, /** 18 binlog dump,可以看 page_protocol_com_binlog_dump */
COM_TABLE_DUMP, /** 19 dumptable */
COM_CONNECT_OUT, /** 20 服务器中的内部命令 */
COM_REGISTER_SLAVE, /** 21 在主服务器上注册一个从服务器。应该在使用COM_BINLOG_DUMP请求binlog事件之前发送 。 */
COM_STMT_PREPARE, /** 22 根据传递的查询字符串创建准备好的语句,例如“SELECT CONC
41 54 28 3f 2c 20 3f 29 20 41 53 20 63 6f 6c 31 AT(?, ?) AS col1”,可以看 page_protocol_com_stmt_prepare */
COM_STMT_EXECUTE, /** 23 要求服务器执行由标识的准备好的语句stmt-id,可以看 page_protocol_com_stmt_execute */
COM_STMT_SEND_LONG_DATA, /** 24 发送列的数据。重复发送,将数据追加到参数中。可以看 page_protocol_com_stmt_send_long_data */
COM_STMT_CLOSE, /** 25 取消分配准备好的语句、可以看 page_protocol_com_stmt_close */
COM_STMT_RESET, /** 26 重置通过COM_STMT_SEND_LONG_DATA 命令累积的准备好的语句的数据, 如果使用COM_STMT_EXECUTE打开了该游标,则关闭该游标、可以看 page_protocol_com_stmt_reset */
COM_SET_OPTION, /** 27 设置参数,比如启用和禁用当前连接的功CLIENT_MULTI_STATEMENTS、可以设置MYSQL_OPTION_MULTI_STATEMENTS_ON和MYSQL_OPTION_MULTI_STATEMENTS_OFF,可以看 page_protocol_com_set_option */
COM_STMT_FETCH, /** 28 在COM_STMT_EXECUTE之后从现有结果集中获取行 。可以看。 page_protocol_com_stmt_fetch */
/**
Currently refused by the server. See ::dispatch_command.
Also used internally to mark the session as a "daemon",
i.e. non-client THD. Currently the scheduler and the GTID
code does use this state.
These threads won't be killed by `KILL`
@sa Event_scheduler::start, ::init_thd, ::kill_one_thread,
::Find_thd_with_id
*/
COM_DAEMON, /** 29 服务器中的内部命令 */
COM_BINLOG_DUMP_GTID, /** 30 如果binlog-filename为空,则服务器将发送第一个已知二进制日志的二进制日志流。 */
COM_RESET_CONNECTION, /** 31 重置会话状态;比COM_CHANGE_USER它不会关闭并重新打开连接,并且不会重新进行身份验证 更轻巧。可以看 page_protocol_com_reset_connection */
COM_CLONE, /** 32 克隆 */
/* 别忘了在sql中更新 sql_parse.cc 的const char *command_name[] 中 */
/* 最后一个 */
COM_END /** 33 不是真正的命令. */
};
1.2.2 Mysql 授权版本信息数据包分析
我们首先从数据包第5条开始看,如图1-2-4。
<center>图1-2-4 Server Greeting数据包</center>
图1-2-4内容可以清晰看到我们Mysql Protocol协议体结构,结构如表1-2-5。
Type | Name | Description |
---|---|---|
int<3> | payload_length | 有效数据包的长度。数据包中超过组成数据包头的4个字节的字节数。 |
int<1> | sequence_id | Sequence ID |
string<var> | payload | 有效的数据包 |
<center>表1-2-5 数据包结构</center>
数据结构可以参照官方文档:
https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_packets.html 。我们可以看到图1-2-3中Mysql Protocol结构,payload_length长度为74,sequence_id为1。Server Greeting中可以获取到mysql到版本号,协议,线程ID,Salt等等,该数据包是服务端回复客户端数据。
1.2.3 Mysql 授权数据包分析
接下来我们继续看一下,下一个Mysql数据协议包,登陆数据包。
<center>图1-2-6 Login Request </center>
如图1-2-6客户端往服务端发送了一个Login Request,Login Request 对应Username为root,对应的Client Auth Plugin插件名称为caching_sha2_password。
<center>图1-2-7 Auth Switch Request</center>
身份验证开关数据包。
<center>图1-2-8 Response OK</center>
授权ok数据包。
<center>图1-2-9 查询@@version_comment</center>
在使用use t1之前,先调用了“select @@version_comment”,查询对应版本备注。Command指令为3,对应1.2.1小节中的COM_QUERY参数。
1.2.4 Mysql 选择数据库数据包分析
**<center>图1-2-10 SELECT DATABASE</center>**
查询当前数据库,当前在那个数据实例下。,Command指令为3,对应1.2.1小节中的COM_QUERY参数。
<center>图1-2-11 use database </center>
Command指令为2,对应1.2.1小节中的COM_INIT_DB参数。初始化数据空间。
<center>图1-2-12 show databases 和 show tables </center>
use t1; 数据包中除了上面数据包以外还会发起几个数据包,分别是show databases、show tables、以及Show Fields。几个数据包。Show Fields总数据包对应demo1、t1、t2三张表。也就是show tables返回数据包返回的表。
1.2.5 Mysql 查询数据包分析
<center>图1-2-13 select查询t1表</center>
如图1-2-13,Command指令为3,对应1.2.1小节中的COM_QUERY参数,Statement对应“select * from t1”,客户端往服务端去送查询数据包。
<center>图1-2-14 select查询返回(一) 字段</center>
如图1-2-14中,Number of fileds为显示的字段个数。 MySQL Protocol为返回显示字段名称,其中包含数据包长度,数据库名称,表明此,类型等,服务端回复的数据包。
<center>图1-2-15 select查询返回(二)数据 </center>
如图1-2-15中返回字段对应的数据,总共返回了3行数据。和图1-2-14为同一个数据包。
1.2.6 Mysql 退出数据包分析
<center>图1-2-16 退出数据包 </center>
根据图1-2-16所示、“No”号为49的Mysql发送了一个Request Quit。Command的值等于1,1刚好就是对应1.2.1小节中的COM_QUIT参数。往服务端发送了退出指令。
2.Mysql协议源码分析
经过之前数据包分析之后,我又进一步研究了一下源码。把源码的研究过程,整理成了图 2-1-1。根据图中内容可以通过gdb或lldb下一些断点调试下。
<center>图2-1-1 通信协议</center>
2.1 客户端部分解析
在客户端中,我们主要看一下run_plugin_auth函数,其他的的内容可以根据图2-1-1 进行调试。
run_plugin_auth函数原型如下:
/**
插件驱动程序的客户端身份验证。
@注意,mysql_real_connect和mysql_change_用户都使用此选项
@param mysql mysql
@param data 插件中的身份验证数据,握手包
@param data_len 数据长度
@param data_plugin 为数据准备的插件如果是mysql_change_ user()则为0
@param db 要使用的初始数据库,可以是0
@retval 0 ok
@retval 1 error
*/
int run_plugin_auth(MYSQL *mysql, char *data, uint data_len,
const char *data_plugin, const char *db) {
DBUG_TRACE;
mysql_state_machine_status status;
mysql_async_auth ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.mysql = mysql; //mysql结构、内部包含连接信息,如:host、user、passwd、unix_socket、server_version等等
ctx.data = data; //验证数据
ctx.data_len = data_len; //数据长度
ctx.data_plugin = data_plugin; //插件数据mysql8.0.20默认为caching_sha2_password
ctx.db = db; //初始化数据库
ctx.non_blocking = false;
ctx.state_function = authsm_begin_plugin_auth;
do {
status = ctx.state_function(&ctx); //调用插件处理函数
} while (status != STATE_MACHINE_FAILED && status != STATE_MACHINE_DONE);
return status == STATE_MACHINE_FAILED;
}
根据如上代码我们可以看到run_plugin_auth函数是调用授权插件,授权插件Mysql8.0.20是默认使用caching_sha2_password,图2-1-2。
<center>图2-1-2 run_plugin_auth函数调试</center>
caching_sha2_password在authsm_begin_plugin_auth函数中设置,ctx->auth_plugin对应的内容中的caching_sha2_password_plugin_name宏,/mysql-8.0.20/sql-common/client.cc 文件中。
#define caching_sha2_password_plugin_name "caching_sha2_password"
<center>图2-1-3 caching_sha2_password_auth_client函数调用</center>
在运行run_plugin_auth函数后,调用插件处理函数最终会调用caching_sha2_password_auth_client进行授权处理。caching_sha2_password_auth_client函数也是在authsm_begin_plugin_auth函数中设置,对应ctx->auth_plugin。
总结
(1)Mysql 8.0.20 采用caching_sha2_password插件进行密码验证。Mysql8.0之前是使用mysql_native_password插件进行密码验证。
(2)caching_sha2_password密码验证方式有缓存。
(3)“use 数据库; ”语句会发多条查询包。
- 1.select DATABASE()
- 2.use t1
- 3.show databases
- 4.show tables
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。