在 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=111ECONNREFUSED

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 连接问题的关键函数:

  1. 先调用 SSL_get_error() 确定错误类型
  2. 对于 SSL_ERROR_SSL,用 ERR_get_error() 获取详细原因
  3. 对于 SSL_ERROR_SYSCALL,检查 errno
  4. 对于 WANT_READ/WANT_WRITE,在非阻塞模式下重试
  5. 使用 openssl errstr 快速查询错误码含义

掌握这些错误码,能让你在调试 SSL/TLS 连接时事半功倍。


参考来源: