我有一个非常简单的客户端服务器,其中一个阻塞套接字进行全双工通信。我已经为应用程序启用了 SSL/TLS。该模型是典型的生产者-消费者模型。客户端生成数据,将其发送到服务器,服务器处理它们。唯一的问题是,服务器偶尔会将数据发送回客户端,客户端会相应地处理这些数据。下面是一个非常简单的应用程序伪代码:
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
事实证明,大约在客户端启动重新协商的同时(第 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:
更新2: 好的。正如 David 所建议的那样,我重新设计了测试应用程序以使用非阻塞套接字,并且始终首先执行 SSL_read 和 SSL_write,然后根据它们返回的内容进行选择,并且在重新协商期间我仍然遇到相同的错误(SSL_write 最终从另一方正在重新谈判中)。问题是,在任何时候,如果 SSL_read 返回 WANT_READ,我是否可以假设这是因为管道中没有任何内容并继续使用 SSL_write,因为我有东西要写?如果没有,那可能就是我最终出现错误的原因。要么,要么我做的重新谈判都错了。注意,如果 SSL_read 返回 WANT_WRITE,我总是选择并再次调用 SSL_read。