我有一个非常简单的客户端服务器,其中一个阻塞套接字进行全双工通信.我已经为应用程序启用了SSL/TLS.该模型是典型的生产者 - 消费者的模型.客户端生成数据,将其发送到服务器,服务器处理它们.唯一的问题是,服务器偶尔会将数据发送回客户端,客户端会相应地处理这些数据.下面是一个非常简单的应用程序伪代码:
Run Code Online (Sandbox Code Playgroud)1 Client: 2 ------- 3 while (true) 4 { 5 if (poll(pollin, timeout=0) || 0 < SSL_pending(ssl)) 6 { 7 SSL_read(); 8 // Handle WANT_READ or WANT_WRITE appropriately. 9 // If no error, handle the received control message. 10 } 11 // produce data. 12 while (!poll(pollout)) 13 ; // Wait until the pipe is ready for a send(). 14 SSL_write(); 15 // Handle WANT_READ or WANT_WRITE appropriately. 16 if (time to renegotiate) 17 SSL_renegotiate(ssl); 18 } 19 20 Server: 21 ------- 22 while (true) 23 { 24 if (poll(pollin, timeout=1s) || 0 < SSL_pending(ssl)) 25 { 26 SSL_read(); 27 // Handle WANT_READ or WANT_WRITE appropriately. 28 // If no error, consume data. 29 } 30 if (control message needs to be sent) 31 { 32 while (!poll(pollout)) 33 ; // Wait until the pipe is ready for a send(). 34 SSL_write(); 35 // Handle WANT_READ or WANT_WRITE appropriately. 36 } 37 }
出于测试目的,我强制进行SSL重新协商(第16-17行).会话开始很简单,但过了一段时间,我得到以下错误:
Client:
-------
error:140940F5:SSL routines:SSL3_READ_BYTES:unexpected record
Server:
-------
error:140943F2:SSL routines:SSL3_READ_BYTES:sslv3 alert unexpected message
Run Code Online (Sandbox Code Playgroud)
事实证明,在客户端启动重新协商的同时(第14行),服务器最终将应用程序数据发送到客户端(第34行).作为重新协商过程的一部分的客户端接收此应用程序数据并发出"意外记录"错误的炸弹.类似地,当服务器进行后续接收时(第26行),当它期待应用数据时,它最终会收到重新协商数据.
我究竟做错了什么?我应该如何使用全双工通道处理/测试SSL重新协商.请注意,没有涉及线程.它是一个简单的单线程模型,在套接字的任何一端都有读/写.
更新:为了验证我编写的应用程序没有任何问题,我甚至可以使用OpenSSL的s_client和s_server实现轻松地重现这一点.我启动了一个s_server,一旦s_client连接到服务器,我就会以编程方式将一堆应用程序数据从服务器发送到客户端,并从客户端向服务器发送一堆"R"(重新协商请求).最终,它们都以与上述完全相同的方式失败.
s_client:
RENEGOTIATING
4840:error:140940F5:SSL routines:SSL3_READ_BYTES:unexpected record:s3_pkt.c:1258:
s_server:
Read BLOCK
ERROR
4838:error:140943F2:SSL routines:SSL3_READ_BYTES:sslv3 alert unexpected message:s3_pkt.c:1108:SSL alert number 10
4838:error:140940E5:SSL routines:SSL3_READ_BYTES:ssl handshake failure:s3_pkt.c:1185:
Run Code Online (Sandbox Code Playgroud)
更新2: 好的.正如David所建议的那样,我重新设计了测试应用程序以使用非阻塞套接字,并且总是首先执行SSL_read和SSL_write并根据它们返回的内容进行选择,并且在重新协商期间仍然会遇到相同的错误(SSL_write最终从应用程序中获取应用程序数据)在重新谈判中的另一边).问题是,在任何时候,如果SSL_read返回WANT_READ,我可以假设它是因为管道中没有任何东西并继续使用SSL_write,因为我有东西要写吗?如果没有,那可能就是我最终出错的原因.要不然,或者我正在进行重新谈判.注意,如果SSL_read返回WANT_WRITE,我总是进行选择并再次调用SSL_read.
你正试图"浏览"SSL黑盒子.这是一个巨大的错误.
if (poll(pollin, timeout=0) || 0 < SSL_pending(ssl))
{
SSL_read();
Run Code Online (Sandbox Code Playgroud)
您假设为了SSL_read进行前进,它需要从套接字读取数据.这是一个可能是错误的假设.例如,如果正在进行重新协商,则SSL引擎可能需要接下来发送数据,而不是读取数据.
while (!poll(pollout))
; // Wait until the pipe is ready for a send().
SSL_write();
Run Code Online (Sandbox Code Playgroud)
您如何知道SSL引擎想要将数据写入管道?它给你一个WANT_WRITE指示吗?如果没有,可能需要读取重新协商数据才能发送.
要在非阻塞模式下使用SSL,只需尝试您要执行的操作.如果要读取解密数据,请致电SSL_read.如果要发送加密数据,请致电SSL_write.仅poll在SSL引擎通过或指示告诉您时调用.WANT_READWANT_WRITE
更新 ::在阻塞和非阻塞方法之间,您有"混合的一半".这不可行.问题很简单:在你打电话之前SSL_read,你不知道它是否需要从套接字中读取.如果poll先调用,即使SSL_read不需要从套接字读取,也会阻塞.如果SSL_read先调用它,它将阻止它是否需要从套接字读取.SSL_pending不会帮到你.如果SSL_read需要写入套接字以进行前进,SSL_pending将返回零,但调用poll将永远阻塞.
你有两个明智的选择:
阻塞.将套接字设置为阻塞.只需SSL_read在您想要阅读时以及SSL_write何时想要书写时打电话.他们会阻止.阻塞套接字可能会阻塞,这就是它们的工作方式.
非阻塞.将套接字设置为非阻塞.只需SSL_read在您想要阅读时以及SSL_write何时想要书写时打电话.他们不会阻止.如果您收到WANT_READ指示,请在读取方向上进行轮询.如果得到WANT_WRITE指示,则在写入方向上轮询.请注意,SSL_read返回是完全正常的WANT_WRITE,然后在写入方向上进行轮询.同样,SSL_write可以返回WANT_READ,然后在读取方向上轮询.
如果SSL_read的实现基本上是"读取一些数据然后解密"并且SSL_write"加密一些数据并发送它",那么你的代码(大部分)都会工作.问题是,这些函数实际上运行了一个复杂的状态机,可以根据需要读取和写入套接字,并最终导致为您提供解密数据或加密数据并发送数据.