如何判断SSL_read是否已经接收并处理了来自单个消息的所有记录



下面是困境,SSL_read,在成功时返回读取的字节数,SSL_pending用于判断处理后的记录是否有更多要读取的字节,这意味着提供的缓冲区可能不足以包含该记录。

SSL_read可能返回n>0,但如果在处理了第一条记录并且消息实际上是多记录通信时发生这种情况,该怎么办。

问题:我使用epoll发送/接收消息,这意味着我必须对事件进行排队,以防我需要更多数据。什么检查可以确保从单个消息中读取所有记录,并且是时候删除此事件并将响应写入客户端的响应事件排队了?

附言:这个代码还没有经过测试,所以它可能是不正确的。代码的目的是分享我试图实现的想法。

以下是读取的代码片段-

//read whatever is available.
while (1)
{
auto n = SSL_read(ssl_, ptr_ + tail_, sz_ - tail_);
if (n <= 0)
{
int ssle = SSL_get_error(ch->ssl_, rd);
auto old_ev = evt_.events;
if (ssle == SSL_ERROR_WANT_READ)
{
//need more data to process, wait for epoll notification again
evt_.events = EPOLLIN | EPOLLERR;
}
else if (err == SSL_ERROR_WANT_WRITE)
{
evt_.events = EPOLLOUT | EPOLLERR;
}
else
{
/*  connection closed by peer, or
some irrecoverable error */
done_ = true;
tail_ = 0; //invalidate the data
break;
}
if (old_ev != evt_.events)
if (epoll_ctl(epoll_fd_, EPOLL_CTL_MOD, socket_fd_, &evt_) < 0)
{
perror("handshake failed at EPOLL_CTL_MOD");
SSL_free(ssl_);
ssl_ = nullptr;
return false;
}
}
else //some data has been read
{
tail_ = n;
if (SSL_pending(ssl_) > 0)
//buffer wasn't enough to hold the content. resize and reread
resize();
else
break;
}
}
```
enter code here

SSL_read()返回调用方缓冲区中返回的解密字节数,而不是连接上接收的字节数。这模拟了recv()read()的返回值。

SSL_pending()返回仍在SSL的缓冲区中且调用方尚未读取的解密字节数。这相当于在套接字上调用ioctl(FIONREAD)

无法知道有多少SSL/TLS记录构成了一个"应用程序消息",也就是说,由解密的协议数据来决定。协议需要指定消息的结束位置和新消息的开始位置。例如,通过在消息数据中包含消息长度。或者用终止符分隔消息。

无论哪种方式,SSL/TLS层都没有"消息"的概念,只有一个任意的字节流,它根据需要进行加密和解密,并在自己选择的"记录"中传输。类似于TCP如何将任意字节流分解为IP帧等。

因此,当您的循环从OpenSSL读取任意字节时,它需要处理这些字节以检测协议消息之间的分离,这样它就可以根据每条消息采取相应的行动。

什么检查将确保从单个消息中读取所有记录,并且是时候删除此事件并将响应写回客户端的响应事件排队了?

我希望您的消息有一个包含记录数的标头。否则,您的协议可能无法解析。

您需要的是有一个有状态的解析器,它会消耗所有可用的字节,并在记录完成后输出记录。一旦到达解密输入的最后一个字节,这样的解析器就需要暂停其状态,然后在有更多数据可供读取时必须再次调用。但在所有情况下,如果你不能提前预测预期的数据量,你就无法判断消息何时完成——也就是说,除非你使用的是自同步协议。ATM头之类的东西将是一个起点。但是,当您所需要的只是正确地界定数据,以便数据包解析器能够准确地知道它是否得到了所需的所有数据时,这种复杂性是没有必要的。

这就是发送消息的问题:发送接收方无法解码的内容非常容易,因为发送方完全可以处理数据丢失的问题——它根本不在乎。但接收器肯定需要知道预期有多少字节或记录——不知何故。它可以通过发送包括字节计数或固定大小记录计数(只是以不同的单位表示相同大小的信息)的报头来先验地告知这一点,或者通过使用唯一的记录分隔符来进行后验。例如,当发送拆分成行的可打印文本时,这样的分隔符可以是Unicode段落分隔符(U+2029)。

确保记录分隔符不会出现在记录数据本身中是非常重要的。因此,您需要某种";填充物";机制,如果有效负载中出现分隔符序列,您可以对其进行更改,使其不再是有效的分隔符。您还需要一个";取消填充";机制,以便可以检测到这种改变的定界符序列并将其转换回其原始形式,当然不会被解释为定界符。这种定界过程的一个非常简单的例子是PPP协议中的八位字节填充帧。这是HDLC框架的一种形式。记录分隔符为0x7E。只要在有效负载中检测到该字节,就会对其进行转义,并由0x7D 0x5E序列替换。在接收端,0x7D被解释为意味着"0";下面的字符已经与0x20"异或";。因此,接收器首先将0x7D 0x5E转换为0x5E(它移除转义字节),然后将其与0x20异或,从而产生原始0x7E。这样的成帧很容易实现,但可能比使用较长的定界符序列或甚至动态定界符(其形式因流中的每个位置而不同)的成帧具有更多的开销。这可用于防止拒绝服务攻击,因为攻击者可能会恶意提供会导致大量转义开销的有效负载。动态分隔符序列——尤其是在不可预测的情况下,例如通过为每个连接协商一个新的序列——可以防止这种服务降级。

最新更新