基于OpenSSL的TLS代理纯代理模式优化及连接关闭问题咨询
TLS代理优化方案及SIGSEGV问题解决
需求与当前问题
实现一款TLS代理应用:
- 监听TLS客户端连接,处理初始若干请求后转发修改后的数据到后端服务器,同时修改后端响应返回给客户端
- 完成初始处理后进入纯代理模式,双向转发客户端与后端的数据
当前采用双线程阻塞BIO方案,存在两个问题:
- 双线程方案存在线程切换开销,且易引发线程安全问题
- 双线程下执行
SSL_shutdown/SSL_free时间歇性出现SIGSEGV错误
一、更优的纯代理实现方案:单线程非阻塞IO
方案优势
- 避免线程切换开销,提升性能
- 消除线程同步带来的复杂度与潜在问题
- 更适合高并发场景,可通过epoll/select批量处理连接
实现步骤
- 将SSL对象的BIO设置为非阻塞模式
- 使用select/epoll监听两个SSL连接的读写事件
- 单线程内循环处理双向数据转发
代码示例
#include <openssl/ssl.h> #include <sys/select.h> #include <errno.h> #define BUF_LEN 4096 #define MAX(a,b) ((a) > (b) ? (a) : (b)) void single_thread_proxy(SSL *client_ssl, SSL *backend_ssl) { fd_set read_fds; int client_fd = SSL_get_fd(client_ssl); int backend_fd = SSL_get_fd(backend_ssl); int max_fd = MAX(client_fd, backend_fd); char buf[BUF_LEN]; int ret; // 设置非阻塞模式 SSL_set_mode(client_ssl, SSL_MODE_AUTO_RETRY); SSL_set_mode(backend_ssl, SSL_MODE_AUTO_RETRY); BIO_set_nbio(SSL_get_rbio(client_ssl), 1); BIO_set_nbio(SSL_get_wbio(client_ssl), 1); BIO_set_nbio(SSL_get_rbio(backend_ssl), 1); BIO_set_nbio(SSL_get_wbio(backend_ssl), 1); while (1) { FD_ZERO(&read_fds); FD_SET(client_fd, &read_fds); FD_SET(backend_fd, &read_fds); // 等待可读事件 ret = select(max_fd + 1, &read_fds, NULL, NULL, NULL); if (ret < 0) { if (errno == EINTR) continue; break; } // 处理客户端到后端的转发 if (FD_ISSET(client_fd, &read_fds)) { ssize_t len = SSL_read(client_ssl, buf, BUF_LEN); if (len > 0) { SSL_write(backend_ssl, buf, len); } else if (len == 0 || SSL_get_error(client_ssl, len) == SSL_ERROR_ZERO_RETURN) { // 客户端关闭连接,触发后端关闭 SSL_shutdown(backend_ssl); break; } else if (SSL_get_error(client_ssl, len) != SSL_ERROR_WANT_READ) { // 发生其他错误,退出循环 break; } } // 处理后端到客户端的转发 if (FD_ISSET(backend_fd, &read_fds)) { ssize_t len = SSL_read(backend_ssl, buf, BUF_LEN); if (len > 0) { SSL_write(client_ssl, buf, len); } else if (len == 0 || SSL_get_error(backend_ssl, len) == SSL_ERROR_ZERO_RETURN) { // 后端关闭连接,触发客户端关闭 SSL_shutdown(client_ssl); break; } else if (SSL_get_error(backend_ssl, len) != SSL_ERROR_WANT_READ) { // 发生其他错误,退出循环 break; } } } // 统一清理资源 SSL_shutdown(client_ssl); SSL_shutdown(backend_ssl); SSL_free(client_ssl); SSL_free(backend_ssl); }
二、双线程模式下SIGSEGV错误的解决办法
错误原因
- 多线程同时操作同一个SSL对象,违反OpenSSL的线程安全要求(SSL对象本身并非线程安全)
- 一个线程已执行
SSL_free释放资源,另一个线程仍在访问该SSL对象
解决措施
- 线程同步:用互斥锁保护所有SSL对象的操作(
SSL_read/SSL_write/SSL_shutdown/SSL_free) - 连接关闭协调:设置全局退出标志,当任意线程检测到连接关闭时,通知另一线程停止操作
- 资源原子释放:统一在主线程等待子线程结束后,再执行SSL资源的关闭与释放
修改后的代码示例
#include <openssl/ssl.h> #include <pthread.h> #define BUF_LEN 4096 typedef struct proxy_args_t { SSL* source; SSL* target; pthread_mutex_t* ssl_mutex; volatile int* stop_flag; } proxy_args_t; void do_proxy(SSL *source, SSL *target, pthread_mutex_t* ssl_mutex, volatile int* stop_flag) { char buf[BUF_LEN]; ssize_t len; while (!*stop_flag) { pthread_mutex_lock(ssl_mutex); len = SSL_read(source, buf, BUF_LEN); if (len > 0) { SSL_write(target, buf, len); pthread_mutex_unlock(ssl_mutex); } else { pthread_mutex_unlock(ssl_mutex); // 检测到连接关闭,设置停止标志 *stop_flag = 1; break; } } } void *proxy_handler(void *args) { proxy_args_t *proxy_args = (proxy_args_t *)args; do_proxy(proxy_args->source, proxy_args->target, proxy_args->ssl_mutex, proxy_args->stop_flag); return NULL; } int pure_proxy(SSL *source, SSL *target) { pthread_t pthread; proxy_args_t proxy_args; pthread_mutex_t ssl_mutex; volatile int stop_flag = 0; // 初始化互斥锁 if (pthread_mutex_init(&ssl_mutex, NULL) != 0) { return -1; } proxy_args.source = source; proxy_args.target = target; proxy_args.ssl_mutex = &ssl_mutex; proxy_args.stop_flag = &stop_flag; // 创建子线程 if (pthread_create(&pthread, NULL, proxy_handler, &proxy_args) != 0) { pthread_mutex_destroy(&ssl_mutex); return -1; } // 主线程处理反向转发 do_proxy(target, source, &ssl_mutex, &stop_flag); // 等待子线程结束 pthread_join(pthread, NULL); // 统一在主线程执行SSL关闭与资源释放 pthread_mutex_lock(&ssl_mutex); SSL_shutdown(source); SSL_shutdown(target); SSL_free(source); SSL_free(target); pthread_mutex_unlock(&ssl_mutex); // 销毁互斥锁 pthread_mutex_destroy(&ssl_mutex); return 0; }
内容的提问来源于stack exchange,提问作者Nitin Mathur
相关产品推荐
相关产品推荐

