vlambda博客
学习文章列表

Redis 6.0 系列 | TLS源码分析

Redis 6.0 系列 | TLS源码分析

作者:Wenhui

TLS 是 Redis 6.0 支持的新特性。在本文中将简单介绍 Redis 如何通过 OpenSSL 库来支持 TLS 连接的。


前提条件

当前,Redis 内部使用 OpenSSL 开发库来实现TLS功能的。因此,需要在 Redis 编译之前预先安装 OpenSSL 套件库,同时,在编译 Redis 源代码时需要链接 OpenSSL 库。


初始化

在 Redis config.c 的源代码中,configs 结构体数组中,定义了 TLS 相关的配置如下:

#ifdef USE_OPENSSL  createIntConfig("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); }  
tlsConfigure 函数的详细信息在 tls.c 中定义。这个函数所做的主要工作是在服务器启动时创建新的 SSL 上下文并根据 TLS 配置参数配置上下文。然后将新生成的 SSL 上下文保存到 redis_tls_ctx 全局变量中。
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_FRAGMENTS SSL_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 

 

Redis 6.0 系列 | TLS源码分析

往期精华



中间件小哥

中间件技术、IT咨询的快递小哥  

Redis 6.0 系列 | TLS源码分析

点击“阅读原文”可查看英文版。