Skip to content

Commit dfb83bd

Browse files
committed
Add openssl_io.md
1 parent 279f94d commit dfb83bd

File tree

1 file changed

+104
-0
lines changed

1 file changed

+104
-0
lines changed

openssl_io.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# OpenSSL IO读写超时踩坑小记
2+
3+
## 一、背景
4+
5+
最近一个线上服务项目需要开启 SSL 功能,该项目使用了Acl库中的 SSL、HTTP及协程模块、,Acl SSL 模块封装了OpenSSL库,使用户编写 SSL 服务更加简便快捷。当该服务程序开启 SSL 功能后,发现有 TCP 连接句柄较长时间堵在 IO 读上,所设置的 SSL IO 读写超时失去作用,造成的后果是服务程序内存不断增长。因为在线连接比较大,同时再加上该服务程序采用了协程模式,使问题排查过程比较困难。
6+
7+
## 二、问题分析
8+
9+
- 项目负责人通过记录引用计数方式给出了问题可能出在 HTTPS 的读 Body 数据体部分。于是先查看 ACL 库 HTTP 模块的超时是否有效,结论正常;同时对比 HTTPS 加密 及 HTTP 明文两种方式的运行状态,结果发现 HTTPS 有连接挂起问题,而 HTTP 方式则正常,所以初步分析可能与 SSL 模块有关。
10+
11+
- 分析 Acl 库中 openssl_io.cpp 源码(封装了OpenSSL IO)的 read() 读部分,里面会在调用 SSL_read() 前先调用 acl_read_wait() 检查 socket 是否可读,如果读超时则返回,如果有数据可读才会调用 SSL_read() 读取数据,感觉应该没有问题,于是想借助 ChatGPT 来协助分析问题,下面是 GPT4 给出的解决 SSL 读写超时的方法:
12+
13+
```
14+
OpenSSL库的IO操作本身并没有内建的超时机制,但你可以通过其它方法实现类似的效果。具体的实现方式会因你使用的环境和代码的结构而异,但以下是一种常见的方法:
15+
16+
将底层的socket设置为非阻塞模式,并且使用系统的函数(如`select()`, `poll()` 或者 `epoll()`)监听这些socket上的事件,并在这些函数中设置超时。
17+
18+
以下是一个使用`select`的示例:
19+
```
20+
21+
```c
22+
// 假设你已经设置了SSL *ssl和连接的socket fd
23+
24+
// 将socket设置为非阻塞模式
25+
fcntl(fd, F_SETFL, O_NONBLOCK);
26+
27+
// 待超时时间设置为5秒
28+
struct timeval timeout;
29+
timeout.tv_sec = 5;
30+
timeout.tv_usec = 0;
31+
32+
// 对文件描述符进行监听
33+
fd_set read_fds;
34+
FD_ZERO(&read_fds);
35+
FD_SET(fd, &read_fds);
36+
37+
// 使用select,等待socket有数据可读或者超时
38+
int ready_for_reading = select(fd + 1, &read_fds, NULL, NULL, &timeout);
39+
if (ready_for_reading == 1)
40+
{
41+
// socket有数据可读,执行SSL_read
42+
int ret = SSL_read(ssl, buffer, sizeof(buffer));
43+
44+
// 处理读取结果...
45+
}
46+
else if (ready_for_reading == 0)
47+
{
48+
// select超时
49+
printf("SSL_read timeout.\n");
50+
}
51+
else
52+
{
53+
// select出错
54+
printf("Error: select.\n");
55+
56+
// 执行错误处理...
57+
}
58+
```
59+
60+
该方法与 Acl 库中先调用 acl_read_wait() 再调用 SSL_read() 的方式相似,另外又对比了网上查询的一些方案,思路也基本类似,
61+
62+
- 显然上面的方式无法在实践中解决 OpenSSL 读导致的 socket 阻塞问题,于是想,如果每次 SSL_read 时函数内部只调用一次 read 的话,上面设置的超时读方法应该是有效的,但如果 SSL_read 内部在某种情况下有多次 read 操作则前面所设置的读等待只对第一次有效,对后面的就无效了;于是又回头仔细查询 SSL_read 的帮助文档,其中有这么一段话:
63+
```
64+
If necessary, a read function will negotiate a TLS/SSL session, if not already explicitly performed by SSL_connect(3) or SSL_accept(3). If the peer requests a re-negotiation, it will be performed transparently during the read function operation. The behaviour of the read functions depends on the underlying BIO.
65+
```
66+
通过这段话,隐约感觉 SSL_read 内部可能存在多次 read 过程,但上面的话还有点晦涩,接下来只能去看 SSL_read 源码了,从 ssl_lib.c 中找到 SSL_read,其读数据的大体流程为:
67+
`SSL_read` >> `ssl_read_internal` >> `s->method->ssl_read`,然后再顺藤摸瓜,找到 ssl_read 的具体位置在 s3_lib.c 的 ssl3_read_internal 函数中,如下:
68+
```c
69+
static int ssl3_read_internal(SSL *s, void *buf, size_t len, int peek,
70+
size_t *readbytes)
71+
{
72+
int ret;
73+
74+
clear_sys_error();
75+
if (s->s3->renegotiate)
76+
ssl3_renegotiate_check(s, 0);
77+
s->s3->in_read_app_data = 1;
78+
ret =
79+
s->method->ssl_read_bytes(s, SSL3_RT_APPLICATION_DATA, NULL, buf, len,
80+
peek, readbytes);
81+
if ((ret == -1) && (s->s3->in_read_app_data == 2)) {
82+
/*
83+
* ssl3_read_bytes decided to call s->handshake_func, which called
84+
* ssl3_read_bytes to read handshake data. However, ssl3_read_bytes
85+
* actually found application data and thinks that application data
86+
* makes sense here; so disable handshake processing and try to read
87+
* application data again.
88+
*/
89+
ossl_statem_set_in_handshake(s, 1);
90+
ret =
91+
s->method->ssl_read_bytes(s, SSL3_RT_APPLICATION_DATA, NULL, buf,
92+
len, peek, readbytes);
93+
ossl_statem_set_in_handshake(s, 0);
94+
} else
95+
s->s3->in_read_app_data = 0;
96+
97+
return ret;
98+
}
99+
```
100+
从上面代码可以看出存在两处读操作(调用ssl_read_bytes),再跟踪一下 ssl_read_bytes 应该就可以找到系统读 API了,懒得跟踪了。即然坐实了可能存在两次 read 的过程,则就可以认定在 SSL_read 设置的读等待超时对于第二次读是无效的。
101+
102+
## 三、解决方案
103+
104+
问题原因分析出来了,但必须得要有解决方案才成,毕竟生产环境中的项目需要等米下锅。

0 commit comments

Comments
 (0)