../wolfssl

wolfSSL 使用

背景

c++ 写 TLS 客户端,openssl 实在是有些丑,另外也希望优化加解密性能,遂研究了一下 wolfSSL 的使用,因为看它:

安装及编译选项

源码拉取

官网有个 Download 页面,可以下载源码,其 github release 页也能拉源码,二者在 configure 时略有不同:前者可以直接 configure,后者需要先 ./autogen.sh

以后者为例

wget https://github.com/wolfSSL/wolfssl/archive/refs/tags/v5.7.4-stable.tar.gz -O wolfssl-5.7.4-stable.tar.gz
tar -xf wolfssl-5.7.4-stable.tar.gz
cd wolfssl-5.7.4-stable && ./autogen.sh

编译

大概是因为 wolfSSL 在设计之初是考虑给嵌入式设备用的,支持非常多的编译选项,所以非常可定制,当然也会导致刚开始用的时候也会比较 confusing,所有选项参考 Building wolfSSL

根据我的用例,我选择了如下选项,它们的作用分别是

上面的选项里,性能相关的选项,除了 enable-intelasm 测试后有明显提升,其他选项均是看文档写了可能提升 performance 才启用的,但实际测试改进可能不显著,建议自己写个 benchmark 然后每启用一个选项测试一遍

另外我只关心 TLS Application 相关的性能,即握手完成后对称加解密的过程,所以只测了这个

完整的 configure 命令如下,包含了编译安装

./configure --enable-tls13 \
    --disable-harden \
    --enable-intelasm \      
    --enable-aesni \
    --enable-sp --enable-sp-asm \
    --enable-singlethreaded \
    --enable-opensslall \
    --enable-ed25519 \
    --libdir=/usr/local/lib64 \
    CFLAGS="-DLARGE_STATIC_BUFFERS"

make -j8 && make install

使用

参考 wolfssl-examples,用例非常全

在我的使用场景中,基本上只要把 openssl 头文件换成

#include <wolfssl/options.h> // 必须在所有 wolfssl include 之前
#include <wolfssl/ssl.h>

然后编译链接时把 -lcrypto -lssl 换成 -lwolfssl 即可,由于启用了 --enable-opensslall,所以基本上绝大多数 openssl 的 symbol 都可以直接使用,其会被替换成 wolfssl 的,例如

#define SSL_CTX WOLFSSL_CTX
#define SSL_new wolfSSL_new

I/O Callback

原本的 openssl 的代码里,I/O 用了 BIO,在启用 --enable-opensslall 后,wolfSSL 也提供 BIO 接口,也是可以无缝迁移的

但是看文档发现它还提供了 I/O Callback 接口,参考 Portability 的 Custom Input/Output Abstraction Layer 章节

此外,在看源码时发现,其所有 I/O 都是以 I/O Callback 实现的,例如设置 BIO 其实只是设置了 BIO callback,在需要读写时将数据写到 BIO 层。直接用 callback 相比 BIO 应该少了一次 memcpy,出于性能的考量打算使用这个接口

使用也比较简单,可以参考 wolfssl-examples/custom-io-callbacks,它实现了通过文件而非 socket 作为 I/O 进行 SSL 通信的例子

两个 callback 的定义如下

typedef int (*CallbackIORecv)(WOLFSSL *ssl, char *buf, int sz, void *ctx);
typedef int (*CallbackIOSend)(WOLFSSL *ssl, char *buf, int sz, void *ctx);

这两个 callback 的语义是

ctx 则是用户自己设置的 userdata

在实现了自己的 callback 后,通过如下代码设置即可

wolfSSL_SSLSetIORecv(ssl, CBIORecv);
wolfSSL_SSLSetIOSend(ssl, CBIOSend);
// wolfSSL_SetIOReadCtx(ssl, userdata);
// wolfSSL_SetIOWriteCtx(ssl, userdata);

减少 malloc

wolfSSL 支持自定义 allocator(malloc, free, realloc),参考 Portability 的 Memory Use 章节

int wolfSSL_SetAllocators(wolfSSL_Malloc_cb  malloc_function,
                         wolfSSL_Free_cb    free_function,
                         wolfSSL_Realloc_cb realloc_function);

也因此,我尝试用一个包含调用计数的 malloc 来观察 malloc 的次数,然后发现在不启用 LARGE_STATIC_BUFFERS 的情况下,除了握手阶段以外,每次读或者写 SSL 都会出现一次 malloc(和相应的 free)

如果启用 LARGE_STATIC_BUFFERS,每个 SSL 都会有一个固定大小的 staticBuffer,其大小应该是 MAX_RECORD_SIZE,即 16KB,在读写数据时只要这个 buffer 够用,就不会出现 malloc

另一个方法则复杂一些,参考 Features 的 Static Buffer Allocation Option 章节,大概流程是:

不过其实 staticmemory 本质上还是要每次都分配内存(只是在预分配的区域上分配),相比 LARGE_STATIC_BUFFERS 在理想情况下可以完全不分配内存,后者应该更符合我的需求

在我的测试里,在收发数据包较小(因此单次 malloc 成本很低)的情况下,性能表现 LARGE_STATIC_BUFFERS > malloc > staticmemory

Benchmark

简单测试了 TLS Client 端,在 TLS1.3 用 TLS_AES_128_GCM_SHA256 cipher 的情况下,连续收发 256B payload,加解密的耗时(ns)

openssl 作为 baseline, 在使用 BIO 的情况下

encryption_costs: min: 357, max: 11197, avg: 388, p50: 370, p99: 546
decryption_costs: min: 316, max: 16816, avg: 350, p50: 330, p99: 532

wolfSSL 不启用 intelasm 且使用 BIO

encryption_costs: min: 1074, max: 16198, avg: 1132, p50: 1098, p99: 1335
decryption_costs: min: 1143, max: 16839, avg: 1205, p50: 1169, p99: 1427

启用 --disable-harden,没有观察到提升

encryption_costs: min: 1067, max: 11969, avg: 1127, p50: 1092, p99: 1338
decryption_costs: min: 1139, max: 21007, avg: 1206, p50: 1168, p99: 1433

启用 --enable-intelasm,性能提升显著

encryption_costs: min: 200, max: 10915, avg: 228, p50: 214, p99: 304
decryption_costs: min: 263, max: 8928, avg: 309, p50: 296, p99: 407

启用 --enable-sp-asm,没有观察到提升

encryption_costs: min: 205, max: 16980, avg: 244, p50: 235, p99: 319
decryption_costs: min: 258, max: 15187, avg: 300, p50: 286, p99: 388

启用 --enable-singlethreaded,提升不大,但似乎是有些提升

encryption_costs: min: 211, max: 10392, avg: 249, p50: 240, p99: 331
decryption_costs: min: 238, max: 12050, avg: 281, p50: 261, p99: 373

在发送方向上用 Callback,接收方向上仍然用 BIO,且启用 LARGE_STATIC_BUFFERS,提升较大

encryption_costs: min: 124, max: 10720, avg: 131, p50: 131, p99: 138
decryption_costs: min: 172, max: 11071, avg: 184, p50: 183, p99: 208

再启用 fast-mathfast-huge-math,没有观察到提升

encryption_costs: min: 124, max: 17766, avg: 132, p50: 131, p99: 147
decryption_costs: min: 172, max: 16989, avg: 184, p50: 183, p99: 208

总结就是使用 IOCallback,启用 LARGE_STATIC_BUFFERS,启用 intelasmsinglethreaded(如果确认不会多线程使用),能达到比较好的加解密性能,比 openssl 快