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