在 OpenSSL 编程中,SSL_get_error() 是排查 SSL/TLS 连接问题的核心函数。当 SSL_read() 或 SSL_write() 返回负数时,需要调用它来确定具体的错误类型。本文详细介绍各错误码的含义及处理方法。
函数原型#
1
2
3
|
#include <openssl/ssl.h>
int SSL_get_error(const SSL *ssl, int ret);
|
ret 是上次 SSL 操作的返回值(通常为负数),函数返回以下错误码之一:
| 错误码 |
常量 |
含义 |
| 0 |
SSL_ERROR_NONE |
操作成功完成 |
| 1 |
SSL_ERROR_SSL |
协议层面的内部错误 |
| 2 |
SSL_ERROR_WANT_READ |
操作未完成,需要更多数据 |
| 3 |
SSL_ERROR_WANT_WRITE |
操作未完成,需要写入更多数据 |
| 4 |
SSL_ERROR_WANT_X509_LOOKUP |
等待 X509 回调完成 |
| 5 |
SSL_ERROR_SYSCALL |
系统调用错误,检查 errno |
| 6 |
SSL_ERROR_ZERO_RETURN |
对端关闭连接 |
| 7 |
SSL_ERROR_WANT_CONNECT |
等待连接完成 |
| 8 |
SSL_ERROR_WANT_ACCEPT |
等待 Accept 完成 |
| 9 |
SSL_ERROR_WANT_ASYNC |
等待异步操作 |
| 10 |
SSL_ERROR_WANT_ASYNC_JOB |
等待异步任务 |
| 11 |
SSL_ERROR_WANT_CLIENT_HELLO_CB |
等待 ClientHello 回调 |
| 12 |
SSL_ERROR_WANT_RETRY_VERIFY |
等待重试验证 |
常见错误码详解#
SSL_ERROR_SSL (1)#
这是最常见的错误,表示协议层面的内部错误。通常由以下原因导致:
- 证书验证失败
- 不支持的协议版本
- 密码套件不匹配
- 握手失败
使用 ERR_get_error() 可获取详细错误信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <openssl/err.h>
void print_ssl_error(SSL *ssl, int ret) {
int err = SSL_get_error(ssl, ret);
printf("SSL error: %d\n", err);
unsigned long e;
while ((e = ERR_get_error()) != 0) {
char err_buf[256];
ERR_error_string_n(e, err_buf, sizeof(err_buf));
printf(" -> %s\n", err_buf);
}
}
|
实际示例 - 证书过期:
1
2
3
4
|
# 连接一个证书过期的服务器,观察错误
$ openssl s_client -connect expired.badssl.com:443 -servername expired.badssl.com </dev/null 2>&1 | grep -E "verify error|notAfter"
verify error:num=10:certificate has expired
notAfter=Apr 12 23:59:59 2015 GMT
|
SSL_ERROR_SYSCALL (5)#
表示底层系统调用失败,可能是:
- 网络连接被重置(ECONNRESET)
- 超时(ETIMEDOUT)
- 资源耗尽(EMFILE, ENOMEM)
需要检查 errno 确定具体原因:
1
2
3
4
5
6
7
8
9
10
|
if (err == SSL_ERROR_SYSCALL) {
int sock_err = errno;
if (sock_err == ECONNRESET) {
printf("Connection reset by peer\n");
} else if (sock_err == ETIMEDOUT) {
printf("Connection timeout\n");
} else {
printf("System error: %s\n", strerror(sock_err));
}
}
|
实际测试 - 连接拒绝:
1
2
3
|
# 连接一个不存在的服务,观察 SYSCALL 错误
$ openssl s_client -connect 127.0.0.1:19999 -servername test </dev/null 2>&1
connect:errno=111
|
errno=111 是 ECONNREFUSED。
SSL_ERROR_ZERO_RETURN (6)#
对端正常关闭了 TLS 连接。这通常发生在:
- 服务器主动关闭连接
- 连接空闲超时
- 正常的连接关闭流程
1
2
3
4
|
if (err == SSL_ERROR_ZERO_RETURN) {
printf("Connection closed gracefully\n");
// 可以尝试重新连接
}
|
实际观察:
1
2
3
4
|
# 使用 s_server 测试连接关闭
$ openssl s_server -cert server.pem -key key.pem -accept 8443 &
$ openssl s_client -connect localhost:8443 </dev/null 2>&1
# 服务器端 Ctrl+C 关闭后,客户端会看到 SSL_ERROR_ZERO_RETURN
|
SSL_ERROR_WANT_READ / WANT_WRITE (2/3)#
这些是非阻塞 I/O 下的正常状态,表示操作未完成:
- WANT_READ:需要更多数据才能继续,等待下次可读事件
- WANT_WRITE:缓冲区已满,等待下次可写事件
在非阻塞模式下应当这样处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
int ret = SSL_read(ssl, buf, len);
int err = SSL_get_error(ssl, ret);
switch (err) {
case SSL_ERROR_WANT_READ:
// 等待 socket 可读,然后重试 SSL_read()
break;
case SSL_ERROR_WANT_WRITE:
// 等待 socket 可写,然后重试 SSL_read()
break;
case SSL_ERROR_SYSCALL:
// 处理系统错误
break;
case SSL_ERROR_SSL:
// 处理协议错误
break;
}
|
使用 openssl errstr 分析错误#
OpenSSL 提供了 errstr 工具将错误码转换为可读信息:
1
2
3
4
5
6
7
8
9
10
|
# 查看错误码的详细信息
$ openssl errstr 0x14094410
error:14094410:UI routines::reason(607248)
# 查看更多 SSL 错误码
$ openssl errstr 0x14009065
error:14009065:SSL routines::certificate verify failed
$ openssl errstr 0x1408A0C5
error:1408A0C5:SSL routines::ssl3_get_record:wrong version number
|
完整错误处理示例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <errno.h>
#include <string.h>
int handle_ssl_error(SSL *ssl, int ret) {
int err = SSL_get_error(ssl, ret);
switch (err) {
case SSL_ERROR_NONE:
return 0; // 成功
case SSL_ERROR_SSL:
fprintf(stderr, "SSL protocol error\n");
{
unsigned long e;
while ((e = ERR_get_error()) != 0) {
fprintf(stderr, " %s\n", ERR_error_string(e, NULL));
}
}
return -1;
case SSL_ERROR_SYSCALL:
fprintf(stderr, "System call error: %s\n", strerror(errno));
if (errno == 0 && ret == 0) {
fprintf(stderr, " (EOF)\n");
}
return -1;
case SSL_ERROR_ZERO_RETURN:
fprintf(stderr, "Connection closed\n");
return -1;
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
// 非阻塞 I/O,需要重试
return 1;
default:
fprintf(stderr, "Unknown SSL error: %d\n", err);
return -1;
}
}
|
常见错误场景对照表#
| 场景 |
错误码 |
原因 |
解决方法 |
| 证书过期 |
SSL_ERROR_SSL + X509_V_ERR_CERT_HAS_EXPIRED |
证书有效期已过 |
更新证书 |
| 主机名不匹配 |
SSL_ERROR_SSL + X509_V_ERR_HOSTNAME_MISMATCH |
SAN 与连接域名不符 |
检查域名或证书配置 |
| 自签名证书 |
SSL_ERROR_SSL + X509_V_ERR_self_signed_cert |
证书不在信任链 |
配置 CA 证书或跳过验证(仅测试) |
| 协议不支持 |
SSL_ERROR_SSL |
服务器不支持 TLS 版本 |
调整客户端协议版本 |
| 连接超时 |
SSL_ERROR_SYSCALL + ETIMEDOUT |
网络延迟或阻塞 |
检查网络或增加超时 |
| 连接被拒绝 |
SSL_ERROR_SYSCALL + ECONNREFUSED |
服务未启动 |
检查目标服务 |
| 密码套件不匹配 |
SSL_ERROR_SSL |
无共同密码套件 |
启用兼容的密码套件 |
使用 openssl s_client 模拟错误#
1
2
3
4
5
6
7
8
9
|
# 模拟证书验证失败(自签名)
$ openssl s_client -CAfile /dev/null -connect self-signed.badssl.com:443 </dev/null 2>&1 | grep "verify"
verify error:num=19:self signed certificate
# 模拟主机名不匹配
$ openssl s_client -connect badssl.com -servername wrong.host.badssl.com </dev/null 2>&1 | head -5
# 模拟协议版本不匹配(TLS 1.0)
$ openssl s_client -tls1 -connect google.com:443 </dev/null 2>&1 | head -3
|
SSL_get_error() 是诊断 SSL/TLS 连接问题的关键函数:
- 先调用
SSL_get_error() 确定错误类型
- 对于 SSL_ERROR_SSL,用
ERR_get_error() 获取详细原因
- 对于 SSL_ERROR_SYSCALL,检查 errno
- 对于 WANT_READ/WANT_WRITE,在非阻塞模式下重试
- 使用
openssl errstr 快速查询错误码含义
掌握这些错误码,能让你在调试 SSL/TLS 连接时事半功倍。
参考来源: