跳转至

当客户端输入 mysql -h x.x.x.x -u root -p 时,服务端在做什么(中)

Conventions

  • 每一个代码块的顶部都有它所属的文件(相对)路径,如果代码块属于某个函数(方法),那么顶部会有函数(方法)的声明。
  • 代码块中的对象,如果很重要,会在行尾补充该对象的声明。
  • 我使用的MySQL 版本是 8.0.41。你看到这篇时,可能有了更新的版本,比如 8.0.42,区别不大的。

身份认证

sql/sql_connect.cc
bool thd_prepare_connection(THD *thd) {
  thd->enable_mem_cnt();

  bool rc;
  lex_start(thd);
  rc = login_connection(thd);

  if (rc) return rc;

  prepare_new_connection_state(thd);
  return false;
}

上一篇结尾,我们讲到 thd_prepare_connection 函数会验证用户名和密码。

thd_prepare_connection 方法很简短,一眼就看到第 893 行的 login_connection 是关键。

login_connection 方法也很简短,但如果每个方法都完整列出来,代码块会占很大的篇幅,所以接下来的几个方法,直接展示调用链,因为真的不重要,重要的肯定会贴出来的。

sql/sql_connect.cc
  error = check_connection(thd);
sql/sql_connect.cc
  auth_rc = acl_authenticate(thd, COM_CONNECT);
sql/auth/sql_authentication.cc
    res = do_auth_once(thd, auth_plugin_name, &mpvio);
sql/auth/sql_authentication.cc
    res = auth->authenticate_user(mpvio, &mpvio->auth_info); // (1)!
  1. st_mysql_auth *auth

login_connection -> check_connection -> acl_authenticate -> do_auth_once -> authenticate_user 这么一路调用下来,走到上面这行停住。

auth 是指向结构类型的指针,表示身份认证插件,authenticate_user 是该结构的成员,对应类型 int (*)(MYSQL_PLUGIN_VIO *vio,MYSQL_SERVER_AUTH_INFO *info),这是指向身份认证函数的指针类型。

不同的身份认证插件会有不同的实现,我们只看默认插件 caching_sha2_password 的实现。caching_sha2_password 在文件 sql/auth/sha2_password.cc 中。caching_sha2_password_authenticate 函数是身份认证函数在该插件中的具体实现,也就是说 authenticate_user 指向了这个函数,所以接下来我们会看这个函数。

Tip

MySQL 有 3 种身份认证插件,分别是 mysql_native_passwordsha256_passwordcaching_sha2_password

握手

sql/auth/sha2_password.cc
static int caching_sha2_password_authenticate(MYSQL_PLUGIN_VIO *vio,
                                              MYSQL_SERVER_AUTH_INFO *info) {}

caching_sha2_password_authenticate 这个函数比较长,就不贴出来了。这个函数中我认为我们要关注的是两件事,一件是服务端与客户端的握手,另一件就是验证密码。下面我们先看握手,验证密码得放到下一篇。

TCP 的握手大家肯定都听过,这儿讲的是 TCP 握手之后,MySQL 实现的服务端与客户端的握手。下面我们通过 Wireshark 查看这个过程。

握手

Tip

单击图片可以放大图片,看得更清楚。

1、2、3 号包是 TCP 的握手,4 号和 6 号是 MySQL 的握手。

4 号包是 MySQL 服务端发出的,说明握手是服务端发起。上图中左下方有包内容,可以看到服务端通过握手传递了版本号、协议版本、盐、身份认证的插件等信息,对应下面函数:

sql/auth/sha2_password.cc static int caching_sha2_password_authenticate(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info)
  if (vio->write_packet(vio, (unsigned char *)scramble, SCRAMBLE_LENGTH + 1))

write_packet 是指向函数的指针,指向下面的函数 server_mpvio_write_packet

sql/auth/sql_authentication.cc
/**
  vio->write_packet() callback method for server authentication plugins

  This function is called by a server authentication plugin, when it wants
  to send data to the client.

  It transparently wraps the data into a handshake packet,
  and handles plugin negotiation with the client. If necessary,
  it escapes the plugin data, if it starts with a mysql protocol packet byte.
*/
static int server_mpvio_write_packet(MYSQL_PLUGIN_VIO *param,
                                     const uchar *packet, int packet_len) {
  MPVIO_EXT *mpvio = (MPVIO_EXT *)param;
  int res;
  Protocol_classic *protocol = mpvio->protocol;

  DBUG_TRACE;
  /*
    Reset cached_client_reply if not an old client doing mysql_change_user,
    as this is where the password from COM_CHANGE_USER is stored.
  */
  if (!((!(protocol->has_client_capability(CLIENT_PLUGIN_AUTH))) &&
        mpvio->status == MPVIO_EXT::RESTART &&
        mpvio->cached_client_reply.plugin ==
            ((st_mysql_auth *)(plugin_decl(mpvio->plugin)->info))
                ->client_auth_plugin))
    mpvio->cached_client_reply.pkt = nullptr;
  /* for the 1st packet we wrap plugin data into the handshake packet */
  if (mpvio->packets_written == 0)
    res = send_server_handshake_packet(
        mpvio, pointer_cast<const char *>(packet), packet_len);
  else if (mpvio->status == MPVIO_EXT::RESTART) {
    /*
      Inject error here for testing purpose.
      See auth_sec.server_send_client_plugin
    */
    DBUG_EXECUTE_IF("assert_authentication_roundtrips", {
      return -1;  // Crash here.
    });
    res = send_plugin_request_packet(mpvio, packet, packet_len);
  } else if (mpvio->status == MPVIO_EXT::START_MFA) {
    res = send_auth_next_factor_packet(mpvio, packet, packet_len);
    /*
      reset the status to avoid sending AuthNextFactor again for the
      same factor authentication.
    */
    mpvio->status = MPVIO_EXT::FAILURE;
  } else
    res = wrap_plguin_data_into_proper_command(protocol->get_net(), packet,
                                               packet_len);
  mpvio->packets_written++;
  return res;
}

关键在第 3241 行 send_server_handshake_packet 函数,函数名翻译成中文就是发送服务端握手包,对应 Wireshark 截图中的 4 号包。

详解服务端握手包

---
title: "MySQL Server Greeting Packet"
---
packet-beta
0-23: "Packet Length: 80"
24-31: "Packet Number: 0"
32-39: "Protocol: 10"
40-143: "Version: 8.0.41-debug"
144-175: "Thread ID: 8"
176-247: "Salt: C@o\\x13\\x14IeR"
248-263: "Server Capabilities: ffff"
264-271: "Server Language: utf8mb4"
272-287: "Server Status: 0002"
288-303: "Extended Server Capabilities: dfff"
304-311: "Authentication Plugin Length: 21"
312-391: "Unused: 00 00 00 00 00 00 00 00 00 00"
392-495: "Salt: tB0.9HRb,Zj/"
496-671: "Authentication Plugin: caching_sha2_password"

上面这张图是 4 号包的样子,我们按照代码一个一个过。

sql/auth/sql_authentication.cc
static bool send_server_handshake_packet(MPVIO_EXT *mpvio, const char *data,
                                         uint data_len) {
  assert(mpvio->status == MPVIO_EXT::FAILURE);
  assert(data_len <= 255);
  Protocol_classic *protocol = mpvio->protocol;

  char *buff = (char *)my_alloca(1 + SERVER_VERSION_LENGTH + data_len + 64);
  char scramble_buf[SCRAMBLE_LENGTH];
  char *end = buff;

  DBUG_TRACE;
  *end++ = protocol_version;

  protocol->set_client_capabilities(CLIENT_BASIC_FLAGS);

  if (opt_using_transactions)
    protocol->add_client_capability(CLIENT_TRANSACTIONS);

  protocol->add_client_capability(CAN_CLIENT_COMPRESS);

  bool have_ssl = false;
  if (current_thd->is_admin_connection() && g_admin_ssl_configured == true) {
    Lock_and_access_ssl_acceptor_context context(mysql_admin);
    have_ssl = context.have_ssl();
  } else {
    Lock_and_access_ssl_acceptor_context context(mysql_main);
    have_ssl = context.have_ssl();
  }

  if (have_ssl) {
    protocol->add_client_capability(CLIENT_SSL);
    protocol->add_client_capability(CLIENT_SSL_VERIFY_SERVER_CERT);
  }

  if (opt_protocol_compression_algorithms &&
      opt_protocol_compression_algorithms[0] != 0) {
    /* turn off the capability flag as the global variable might have changed */
    protocol->remove_client_capability(CLIENT_COMPRESS);
    protocol->remove_client_capability(CLIENT_ZSTD_COMPRESSION_ALGORITHM);
    std::vector<std::string> list;
    parse_compression_algorithms_list(opt_protocol_compression_algorithms,
                                      list);
    auto it = list.begin();
    NET_SERVER *ext = static_cast<NET_SERVER *>(protocol->get_net()->extension);
    struct compression_attributes *compression = &(ext->compression);
    compression->compression_optional = false;
    while (it != list.end()) {
      std::string value = *it;
      switch (get_compression_algorithm(value)) {
        case enum_compression_algorithm::MYSQL_ZSTD:
          protocol->add_client_capability(CLIENT_ZSTD_COMPRESSION_ALGORITHM);
          break;
        case enum_compression_algorithm::MYSQL_ZLIB:
          protocol->add_client_capability(CLIENT_COMPRESS);
          break;
        case enum_compression_algorithm::MYSQL_UNCOMPRESSED:
          compression->compression_optional = true;
          break;
        case enum_compression_algorithm::MYSQL_INVALID:
          assert(false);
          break;
      }
      it++;
    }
  }

  if (data_len) {
    mpvio->cached_server_packet.pkt =
        (char *)memdup_root(mpvio->mem_root, data, data_len);
    mpvio->cached_server_packet.pkt_len = data_len;
  }

  if (data_len < SCRAMBLE_LENGTH) {
    if (data_len) {
      /*
        the first packet *must* have at least 20 bytes of a scramble.
        if a plugin provided less, we pad it to 20 with zeros
      */
      memcpy(scramble_buf, data, data_len);
      memset(scramble_buf + data_len, 0, SCRAMBLE_LENGTH - data_len);
      data = scramble_buf;
    } else {
      /*
        if the default plugin does not provide the data for the scramble at
        all, we generate a scramble internally anyway, just in case the
        user account (that will be known only later) uses a
        mysql_native_password plugin (which needs a scramble). If we don't send
        a scramble now - wasting 20 bytes in the packet - mysql_native_password
        plugin will have to send it in a separate packet, adding one more round
        trip.
      */
      generate_user_salt(mpvio->scramble, SCRAMBLE_LENGTH + 1);
      data = mpvio->scramble;
    }
    data_len = SCRAMBLE_LENGTH;
  }

  end = my_stpnmov(end, server_version, SERVER_VERSION_LENGTH) + 1;

  assert(sizeof(my_thread_id) == 4);
  int4store((uchar *)end, mpvio->thread_id);
  end += 4;

  /*
    Old clients does not understand long scrambles, but can ignore packet
    tail: that's why first part of the scramble is placed here, and second
    part at the end of packet.
  */
  end = (char *)memcpy(end, data, AUTH_PLUGIN_DATA_PART_1_LENGTH);
  end += AUTH_PLUGIN_DATA_PART_1_LENGTH;
  *end++ = 0;

  int2store(end, static_cast<uint16>(protocol->get_client_capabilities()));
  /* write server characteristics: up to 16 bytes allowed */
  end[2] = (char)default_charset_info->number;
  int2store(end + 3, mpvio->server_status[0]);
  int2store(end + 5, protocol->get_client_capabilities() >> 16);
  end[7] = data_len;
  DBUG_EXECUTE_IF("poison_srv_handshake_scramble_len", end[7] = -100;);
  DBUG_EXECUTE_IF("increase_srv_handshake_scramble_len", end[7] = 50;);
  memset(end + 8, 0, 10);
  end += 18;
  /* write scramble tail */
  end = (char *)memcpy(end, data + AUTH_PLUGIN_DATA_PART_1_LENGTH,
                       data_len - AUTH_PLUGIN_DATA_PART_1_LENGTH);
  end += data_len - AUTH_PLUGIN_DATA_PART_1_LENGTH;
  end = strmake(end, client_plugin_name(mpvio->plugin),
                strlen(client_plugin_name(mpvio->plugin)));

  int res = protocol->write((uchar *)buff, (size_t)(end - buff + 1)) ||
            protocol->flush();
  return res;
}

1606 行声明并初始化了一个缓冲区 buff,1608 行 end 指向缓冲区 buff

接着,就是往缓冲区中写入 ProtocolVersionThread ID等 4 号包所示的字段值。

1611 行写入 Protocol,占 1 个字节。

1697 行写入 Version,占用字节数不固定,以 0 结尾,对于当前例子是 8.0.41-debug 12 字节外加结尾的 0 共占 13 个字节。

1700 行写入 Thread ID,占 4 个字节,当我们使用 mysql 命令登录时,欢迎语中会显示这个值。

1708 行写入 Salt,占 8 个字节,这是整个盐的前面部分。

1710 行写入 0,占 1 个字节。

1712 行写入 Server Capabilities,占 2 个字节,每一位都是一个标识位,表示服务端支持的功能。

1714 行写入 Server Language,占 1 个字节。

1715 行写入 Server Status,占 2 个字节。

1716 行写入 Extended Server Capabilities,占 2 个字节,每一位都是一个标识位,表示服务端支持的功能。

1717 行写入 Authentication Plugin Length,占 1 个字节,表示盐的总字节数。

1720 行写入 Unused,占 10 个字节,全部以 0 填充。

1723 和 1724 行写入 Salt,占 13 个字节,与 1708 行的盐拼在一起,是完整的盐。

1726 和 1727 行写入 Authentication Plugin,占用字节数不固定,以 0 结尾,对于当前例子是 caching_sha2_password 21 字节外加结尾的 0 共占 22 个字节。

1729 行会计算 buff 缓冲区的字节数,作为 Packet Length,占用 3 个字节,外加 1 个字节的 Packet Number。这样,整个服务端握手包就生成好了。

sql/auth/sha2_password.cc
  if ((pkt_len = vio->read_packet(vio, &pkt)) == -1) return CR_AUTH_HANDSHAKE;

完成发送服务端握手包后,函数返回到 caching_sha2_password_authenticate。接着,往下走到第 962 行的 read_packet 函数,这也是指向函数的指针,指向 sql/auth/sql_authentication.ccserver_mpvio_read_packet 函数,这个函数稍稍有点长,不贴出来了。我们只需要关心,在该函数中调用了下面的 parse_client_handshake_packet 函数,即解析客户端握手包,这个包对应 Wireshark 截图中的 6 号包。

sql/auth/sql_authentication.cc
static size_t parse_client_handshake_packet(THD *thd, MPVIO_EXT *mpvio,
                                            uchar **buff, size_t pkt_len) {
// ignore
  /*
    If client requested SSL then we must stop parsing, try to switch to SSL,
    and wait for the client to send a new handshake packet.
    The client isn't expected to send any more bytes until SSL is initialized.
  */
  if (protocol->has_client_capability(CLIENT_SSL)) { // line 2882
// ignore
    if (sslaccept(*(context.get()), protocol->get_vio(), // line 2900
                  protocol->get_net()->read_timeout, &errptr)) {
      DBUG_PRINT("error", ("Failed to accept new SSL connection"));
      return packet_error;
    }

    DBUG_PRINT("info", ("Reading user information over SSL layer"));
    int rc = protocol->read_packet(); // line 2907
    pkt_len = protocol->get_packet_length();
  }
// ignore
  char *user = get_string(&end, &bytes_remaining_in_packet, &user_len); // line 2999
// ignore
  passwd =
      get_length_encoded_string(&end, &bytes_remaining_in_packet, &passwd_len); // line 3011

在解析客户端端握手包过程中,如果发现客户端支持 TLS(第 2882 行),那么会进行 TLS 握手(第 2900 行),TLS 握手完成后客户端会发送用户名和密码,服务端阻塞读取(第 2907 行),然后解析(第 2999 和 3011 行)得到用户名和密码。

小结

MySQL 服务端和客户端在 TCP 握手完成后,并没有立刻进行 TLS 握手,而是先做了基础信息的交换,比如版本号、是否支持 TLS 等信息,这是 MySQL 自身实现的握手,如果支持 TLS,那么才进行 TLS 握手,全部完成后,客户端发送用户名和密码,服务端接收到后解析。

下一篇,讲讲服务端如何验证用户名和密码。