Redis 6.0 系列 | TLS源码分析
TLS 是 Redis 6.0 支持的新特性。在本文中将简单介绍 Redis 如何通过 OpenSSL 库来支持 TLS 连接的。
前提条件
当前,Redis 内部使用 OpenSSL 开发库来实现TLS功能的。因此,需要在 Redis 编译之前预先安装 OpenSSL 套件库,同时,在编译 Redis 源代码时需要链接 OpenSSL 库。
初始化
在 Redis config.c 的源代码中,configs 结构体数组中,定义了 TLS 相关的配置如下:
#ifdef USE_OPENSSLcreateIntConfig("tls-port", NULL, IMMUTABLE_CONFIG, 0, 65535, server.tls_port, 0, INTEGER_CONFIG, NULL, NULL), /* TCP port. */createBoolConfig("tls-cluster", NULL, MODIFIABLE_CONFIG, server.tls_cluster, 0, NULL, NULL),createBoolConfig("tls-replication", NULL, MODIFIABLE_CONFIG, server.tls_replication, 0, NULL, NULL),createBoolConfig("tls-auth-clients", NULL, MODIFIABLE_CONFIG, server.tls_auth_clients, 1, NULL, NULL),createBoolConfig("tls-prefer-server-ciphers", NULL, MODIFIABLE_CONFIG, server.tls_ctx_config.prefer_server_ciphers, 0, NULL, updateTlsCfgBool),createStringConfig("tls-cert-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.cert_file, NULL, NULL, updateTlsCfg),createStringConfig("tls-key-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.key_file, NULL, NULL, updateTlsCfg),createStringConfig("tls-dh-params-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.dh_params_file, NULL, NULL, updateTlsCfg),createStringConfig("tls-ca-cert-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.ca_cert_file, NULL, NULL, updateTlsCfg),createStringConfig("tls-ca-cert-dir", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.ca_cert_dir, NULL, NULL, updateTlsCfg),createStringConfig("tls-protocols", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.protocols, NULL, NULL, updateTlsCfg),createStringConfig("tls-ciphers", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.ciphers, NULL, NULL, updateTlsCfg),createStringConfig("tls-ciphersuites", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.ciphersuites, NULL, NULL, updateTlsCfg),
当服务器加载配置时,与 TLS 相关的参数将加载到 server 结构体的相应字段中。例如,tls-port 将被加载到 server.tls_port 字段中。
在 server.c 的 InitServer 函数中,提供的 TLS 参数将被用于配置 OpenSSL 库的参数:
if (server.tls_port && tlsConfigure(&server.tls_ctx_config) == C_ERR) {serverLog(LL_WARNING, "Failed to configure TLS. Check logs for more info.");exit(1);}if (server.tls_port && tlsConfigure(&server.tls_ctx_config) == C_ERR) {serverLog(LL_WARNING, "Failed to configure TLS. Check logs for more info.");exit(1);}
int tlsConfigure(redisTLSContextConfig *ctx_config) {char errbuf[256];SSL_CTX *ctx = NULL;if (!ctx_config->cert_file) {serverLog(LL_WARNING, "No tls-cert-file configured!");goto error;}if (!ctx_config->key_file) {serverLog(LL_WARNING, "No tls-key-file configured!");goto error;}if (!ctx_config->ca_cert_file && !ctx_config->ca_cert_dir) {serverLog(LL_WARNING, "Either tls-ca-cert-file or tls-ca-cert-dir must be configured!");goto error;}ctx = SSL_CTX_new(SSLv23_method());SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3);SSL_CTX_set_options(ctx, SSL_OP_SINGLE_DH_USE);#ifdef SSL_OP_DONT_INSERT_EMPTY_FRAGMENTSSSL_CTX_set_options(ctx, SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS);#endif............SSL_CTX_set_mode(ctx, SSL_MODE_ENABLE_PARTIAL_WRITE|SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);SSL_CTX_set_ecdh_auto(ctx, 1);if (SSL_CTX_use_certificate_file(ctx, ctx_config->cert_file, SSL_FILETYPE_PEM) <= 0) {ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));serverLog(LL_WARNING, "Failed to load certificate: %s: %s", ctx_config->cert_file, errbuf);goto error;}if (SSL_CTX_use_PrivateKey_file(ctx, ctx_config->key_file, SSL_FILETYPE_PEM) <= 0) {ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));serverLog(LL_WARNING, "Failed to load private key: %s: %s", ctx_config->key_file, errbuf);goto error;}if (SSL_CTX_load_verify_locations(ctx, ctx_config->ca_cert_file, ctx_config->ca_cert_dir) <= 0) {ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));serverLog(LL_WARNING, "Failed to configure CA certificate(s) file/directory: %s", errbuf);goto error;}.................SSL_CTX_free(redis_tls_ctx);redis_tls_ctx = ctx;return C_OK;error:if (ctx) SSL_CTX_free(ctx);return C_ERR;}
处理客户端连接
ACCEPT(接受客户端连接):
Redis 使用 io 多路复用架构来处理文件事件。在 initServer 函数中,我们可以找到以下部分进行套接字绑定和 Redis 服务器的 TLS 端口侦听。
if (server.tls_port != 0 &&listenToPort(server.tls_port,server.tlsfd,&server.tlsfd_count) == C_ERR)exit(1);
listenToPort 函数是套接字绑定和侦听的封装。成功调用后,它将文件描述符保存在 server.tlsfd 文件描述符数组中。之后,在 server.tlsfd 中为文件描述符注册了 AE_READABLE 文件事件。并将回调函数设置为 acceptTLSHandler。
for (j = 0; j < server.tlsfd_count; j++) {if (aeCreateFileEvent(server.el, server.tlsfd[j], AE_READABLE,acceptTLSHandler,NULL) == AE_ERR){serverPanic("Unrecoverable error creating server.tlsfd file event.");}}
acceptTLSHandler 函数如下所示:
void acceptTLSHandler(aeEventLoop *el, int fd, void *privdata, int mask) {int cport, cfd, max = MAX_ACCEPTS_PER_CALL;char cip[NET_IP_STR_LEN];UNUSED(el);UNUSED(mask);UNUSED(privdata);while(max--) {cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);if (cfd == ANET_ERR) {if (errno != EWOULDBLOCK)serverLog(LL_WARNING,"Accepting client connection: %s", server.neterr);return;}serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);acceptCommonHandler(connCreateAcceptedTLS(cfd, server.tls_auth_clients),0,cip);}
该函数调用 anetTcpAccept 函数接受套接字连接,并将使用新创建的套接字文件描述符传递给 connCreateAcceptedTLS 函数。此函数的作用如下:
connection *connCreateAcceptedTLS(int fd, int require_auth) {tls_connection *conn = (tls_connection *) connCreateTLS();conn->c.fd = fd;conn->c.state = CONN_STATE_ACCEPTING;if (!require_auth) {/* We still verify certificates if provided, but don't require them.*/SSL_set_verify(conn->ssl, SSL_VERIFY_PEER, NULL);}SSL_set_fd(conn->ssl, conn->c.fd);SSL_set_accept_state(conn->ssl);return (connection *) conn;}connection *connCreateAcceptedTLS(int fd, int require_auth) {tls_connection *conn = (tls_connection *) connCreateTLS();conn->c.fd = fd;conn->c.state = CONN_STATE_ACCEPTING;if (!require_auth) {/* We still verify certificates if provided, but don't require them.*/SSL_set_verify(conn->ssl, SSL_VERIFY_PEER, NULL);}SSL_set_fd(conn->ssl, conn->c.fd);SSL_set_accept_state(conn->ssl);return (connection *) conn;}
首先,它初始化一个 tls_connection 结构体实例。在 tls.c 中,tls_connection结构的定义如下:
typedef struct tls_connection {connection c;int flags;SSL *ssl;char *ssl_error;listNode *pending_list_node;} tls_connection;
其中,connection 类型定义了 Redis 中常见的套接字连接参数:
struct connection {ConnectionType *type;ConnectionState state;short int flags;short int refs;int last_errno;void *private_data;ConnectionCallbackFunc conn_handler;ConnectionCallbackFunc write_handler;ConnectionCallbackFunc read_handler;int fd;};
然后,connCreateAcceptedTLS 函数将文件描述符保存到 conn.fd 字段中,还将 SSL 上下文与此文件描述符关联,SSL_set_accept_state 函数使用服务器模式初始化ssl上下文对象。最后,函数返回 tls_connection 连接对象。此连接对象用于传递到 acceptCommonHandler 函数,以便在服务器端创建 redis 客户端实例对象并调用 connTLSAccept 函数以处理接受 tls 客户端连接,函数定义如下所示:
static int connTLSAccept(connection *_conn, ConnectionCallbackFunc accept_handler) {tls_connection *conn = (tls_connection *) _conn;int ret;if (conn->c.state != CONN_STATE_ACCEPTING) return C_ERR;ERR_clear_error();/* Try to accept */conn->c.conn_handler = accept_handler;ret = SSL_accept(conn->ssl);if (ret <= 0) {WantIOType want = 0;if (!handleSSLReturnCode(conn, ret, &want)) {registerSSLEvent(conn, want); /* We'll fire back */return C_OK;} else {conn->c.state = CONN_STATE_ERROR;return C_ERR;}}conn->c.state = CONN_STATE_CONNECTED;if (!callHandler((connection *) conn, conn->c.conn_handler)) return C_OK;conn->c.conn_handler = NULL;return C_OK;}
该函数的主要部分是调用 SSL_accept 函数以等待客户端来初始化 tls 握手,如果成功返回,连接状态将设置为 CONN_STATE_CONNECTED。
READ/WRITE(读写操作)
在 tls.c 文件中,当多路复用 io 返回 AE_READABLE 文件事件时,connTLSRead 函数被注册为回调函数。该函数定义如下:
static int connTLSRead(connection *conn_, void *buf, size_t buf_len) {tls_connection *conn = (tls_connection *) conn_;int ret;int ssl_err;if (conn->c.state != CONN_STATE_CONNECTED) return -1;ERR_clear_error();ret = SSL_read(conn->ssl, buf, buf_len);if (ret <= 0) {WantIOType want = 0;if (!(ssl_err = handleSSLReturnCode(conn, ret, &want))) {if (want == WANT_WRITE) conn->flags |= TLS_CONN_FLAG_READ_WANT_WRITE;updateSSLEvent(conn);errno = EAGAIN;return -1;} else {if (ssl_err == SSL_ERROR_ZERO_RETURN ||((ssl_err == SSL_ERROR_SYSCALL) && !errno)) {conn->c.state = CONN_STATE_CLOSED;return 0;} else {conn->c.state = CONN_STATE_ERROR;return -1;}}}return ret;}
它是 SSL_read 函数的一个封装,该函数从 tls 连接读取 buf_len 个字节到 buf 缓冲区中。
同样,对于写操作,回调是在 connTLSWrite 函数中定义的:
static int connTLSWrite(connection *conn_, const void *data, size_t data_len) {tls_connection *conn = (tls_connection *) conn_;int ret, ssl_err;if (conn->c.state != CONN_STATE_CONNECTED) return -1;ERR_clear_error();ret = SSL_write(conn->ssl, data, data_len);if (ret <= 0) {WantIOType want = 0;if (!(ssl_err = handleSSLReturnCode(conn, ret, &want))) {if (want == WANT_READ) conn->flags |= TLS_CONN_FLAG_WRITE_WANT_READ;updateSSLEvent(conn);errno = EAGAIN;return -1;} else {if (ssl_err == SSL_ERROR_ZERO_RETURN ||((ssl_err == SSL_ERROR_SYSCALL && !errno))) {conn->c.state = CONN_STATE_CLOSED;return 0;} else {conn->c.state = CONN_STATE_ERROR;return -1;}}}return ret;}
它调用 SSL_write 函数将 data_len 字节写入套接字。
参考资料:
https://www.openssl.org/docs/man1.1.1/man3/
https://github.com/antirez/redis
往期精华
中间件小哥
中间件技术、IT咨询的快递小哥
点击“阅读原文”可查看英文版。
