:基于真实应用模式的 API 设计方法与实践)
OpenSSL QUIC 演进中的 Demo-Driven DesignDDD基于真实应用模式的 API 设计方法与实践【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl导读本文围绕 OpenSSL 仓库中的 doc/designs/ddd/README.md 展开系统讲解 OpenSSL 项目为演进其公开 API尤其是为引入 QUIC 支持而提出的Demo-Driven DesignDDD演示驱动设计方法论先调研大量真实开源应用对 libssl API 的使用模式再将最具代表性的模式固化为若干可运行的演示程序demo以此作为评估 API 变更影响的标尺。读者读完本文后将掌握 DDD 的完整流程、OpenSSL 应用使用模式的六类分类体系AOSF / AOSFx / BIOs / BIOx / BIOm / BIOc以及从 TLS 迁移到 QUIC 客户端时六类典型应用各自需要的最小代码改动#ifdef USE_QUIC对比并能在本地编译运行这些 demo 进行验证。DDD 的背景为什么 API 演进需要演示驱动OpenSSL 的公开 API 面public API surface必须不时演进一方面要支持新功能如 QUIC、新的加密算法与 Provider 机制另一方面要淘汰旧功能deprecate。任何 API 变更都必须经过规划、讨论与达成一致。而评估一项 API 变更影响的关键维度是广谱的真实世界 OpenSSL 应用今天究竟如何使用现有 API。这决定了拟议变更会以何种方式影响这些应用影响的范围有多大使用 OpenSSL 的代码库需要做多大改动才能跟上 API 使用的最佳实践。因此OpenSSL 项目需要深刻理解常见代码库中的 API 使用模式以便预判 API 演进的冲击。doc/designs/ddd/目录正是为此而设维护一组API 使用演示API usage demos覆盖真实应用使用 OpenSSL API 的完整谱系。项目可以针对每个 demo 讨论拟议的 API 变更需要怎样修改这个 demo由于 demo 具有代表性就能确保 API 演进既参考真实使用模式也参考对现有应用的影响。这一思路的背景资料可参见 OpenSSL 官方 issue #17939 API long-term evolution。从源码结构看这一以代表性应用驱动 API 设计的取向在 OpenSSL 3.x 的 QUIC 实现中得到了贯彻include/openssl/quic.h中导出了OSSL_QUIC_client_method()等 QUIC 专用 API而 ssl/ 与 ssl/quic/ 下的大量实现则以尽量复用现有 SSL_* 调用约定为原则这正是 DDD 流程的直接产物。DDD 的范围界定当前 DDD 流程的重心是客户端clientdemo。QUIC 的服务端支持被推迟到后续 OpenSSL 版本因此当前设计练习中服务端明确超出范围。demo 刻意聚焦于 libssl 使用中与 QUIC 相关、且必然发生变更的方面例如各式应用如何让 libssl 执行网络 I/O各式应用如何创建 socket 与连接交给 libssl。libssl API 整体范围要大得多包含大量函数与特性DDD 无意全部演示——因为其中大部分不会被 QUIC 触及。例如虽然许多 OpenSSL 用户使用客户端证书或其他 TLS 功能但 QUIC 不太可能对这些 API 产生影响因此演示此类功能的 demo 同样超出范围。背景调研21 个真实开源应用的使用模式分类这些 demo 是在分析下列开源应用、归纳其 libssl API 使用模式之后开发出来的。项目先确定高频出现的使用模式再据此把应用归入若干类别应用BlkFDmuttSAOSFvsftpdSAOSFeximSAOSFxwgetSAOSFFossilSBIOclibrabbitmqABIOxngircdAAOSFstunnelAAOSFxPostfixAAOSFsocatAAOSFHAProxyABIOxDovecotABIOmApache httpdABIOxUnrealIRCdAAOSFwpa_supplicantABIOmicecastAAOSFnginxAAOSFcurlAAOSFAsteriskAAOSFAsterisk (DTLS)ABIOm/xpgbouncerAAOSF, BIOcBlk阻塞 vs 非阻塞 I/OS阻塞SynchronousI/OA非阻塞AsynchronousI/O。FD应用与文件描述符/BIO 的关系AOSF应用自建并持有 FD调用SSL_set_fd交给 libssl应用自己建立连接AOSFx应用自建并持有 FD调用SSL_set_rfd/SSL_set_wfd读写使用不同 FDBIOs应用创建 socket/FD BIO 并调用SSL_set_bio连接由应用建立BIOx应用用自定义 BIO method 创建 BIO 并调用SSL_set_bioBIOm应用创建内存 BIO自行在内存 BIO 与实际 socket 之间搬运数据把 libssl 当作不做任何 I/O 的纯状态机BIOc应用使用BIO_s_connect派生方法如BIO_new_ssl_connect把连接建立交给 OpenSSL。观察表格可以发现一个有意思的分布经典邮件/Web 服务类mutt、vsftpd、exim、wget多为阻塞式 AOSF而高性能服务器nginx、HAProxy、Apache httpd、Postfix、curl几乎全部采用非阻塞 I/ODovecot、wpa_supplicant、Asterisk(DTLS) 等则把 libssl 当作纯状态机BIOm。这正是 DDD 结论QUIC 只需最小改动的事实基础——六类 demo 恰好覆盖了这些真实存在的使用形态。六1 个 Demo 全景目录中的 demo 及其对应分类如下Demo类型描述ddd-01-conn-blocking.cS-BIOc基于BIO_s_connect的阻塞示例展示模范级exemplaryOpenSSL API 用法ddd-02-conn-nonblocking.cA-BIOc基于BIO_s_connect的非阻塞示例使用了缓冲 BIObuffering BIOddd-02-conn-nonblocking-threads.cA-BIOc上述示例的线程辅助thread-assisted变体QUIC 改动面更小ddd-03-fd-blocking.cS-AOSF基于SSL_set_fd的阻塞示例对应上表 S-AOSF 类应用ddd-04-fd-nonblocking.cA-AOSF基于SSL_set_fd的非阻塞示例对应 A-AOSF 类应用ddd-05-mem-nonblocking.cA-BIOm用内存缓冲向 OpenSSL 馈送密文的非阻塞示例对应 A-BIOm 类应用ddd-06-mem-uv.cA-BIOm同上但使用 libuv真实世界的异步 I/O 库注Ubuntu 上可通过安装libuv1-dev软件包获得 libuv。每个 demo 都假设系统存在默认证书存储default certificate store。如运行环境需要可设置SSL_CERT_DIR环境变量来指定证书目录。共性骨架create_ssl_ctx 与连接建立六个 demo 的create_ssl_ctx()骨架几乎完全一致只是SSL_CTX_new的方法参数不同SSL_CTX *create_ssl_ctx(void) { SSL_CTX *ctx; #ifdef USE_QUIC ctx SSL_CTX_new(OSSL_QUIC_client_method()); #else ctx SSL_CTX_new(TLS_client_method()); #endif if (ctx NULL) return NULL; /* Enable trust chain verification. */ SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); /* Load default root CA store. */ if (SSL_CTX_set_default_verify_paths(ctx) 0) { SSL_CTX_free(ctx); return NULL; } return ctx; }这里 TLS 与 QUIC 的差异被压缩到一行TLS_client_method()↔OSSL_QUIC_client_method()。从源码看OSSL_QUIC_client_method在 include/openssl/quic.h 中声明与 TLS 方法一样返回const SSL_METHOD *可直接传给SSL_CTX_new。QUIC 必选配置ALPNQUIC 协议强制要求使用 ALPNRFC 9001 规定 ALPN 必须协商成功这是 DDD 初期规划时遗漏、最终实现时补齐的关键点。demo 中的标准写法为static const unsigned char alpn[] { 5, d, u, m, m, y }; ... /* Configure ALPN, which is required for QUIC. */ if (SSL_set_alpn_protos(ssl, alpn, sizeof(alpn))) { /* Note: SSL_set_alpn_protos returns 1 for failure. */ BIO_free_all(out); return NULL; }注意SSL_set_alpn_protos的返回值语义与大多数 OpenSSL API相反返回 0 表示成功、非 0 表示失败demo 注释中特意强调了这一点。该函数声明于 include/openssl/ssl.h.in。alpn数组的格式是长度前缀协议列表{ 5, d,u,m,m,y }表示一个长度为 5 的dummy协议标识。六类 demo 详解与 QUIC 迁移改动REPORT.mddoc/designs/ddd/REPORT.md记录了 QUIC MVP 完成之后对 DDD 流程结论的复盘。每个 demo 文件都用#ifdef USE_QUIC守卫同时保留了基线版本TLS与 QUIC 版本对照阅读即可直观看到两种协议的 API 差异。ddd-01-conn-blocking最小改动的代表该 demo 展示最简使用BIO_new_ssl_connect一把梭——连接建立、握手全部交给 OpenSSL应用只做BIO_write/BIO_read。最初规划改动仅一行—— ctx SSL_CTX_new(QUIC_client_method()); - ctx SSL_CTX_new(TLS_client_method());实际改动QUIC_client_method因命名空间原因被改名为OSSL_QUIC_client_method新增SSL_set_alpn_protos配置 ALPNQUIC 强制要求DDD 期间未注意到。结论在已配置 ALPN 的前提下启用 QUIC 的字面改动就是一行——这也是整个 DDD 复盘中最亮眼的结论。ddd-02-conn-nonblocking非阻塞 事件循环的完整改造该 demo 演示简单的非阻塞使用名称解析同样由BIO_s_connect管理它还在 BIO 栈上压入了一个BIO_f_buffer缓冲 BIO这是常见的应用使用模式。最初规划改动更换方法同 ddd-01用BIO_f_dgram_buffer取代BIO_f_buffer用BIO_get_poll_fd取得待 poll 的 FD取代BIO_get_fd改变POLLIN/POLLOUT/POLLERR标志的判定方式新增get_conn_pump_timeoutQUIC 事件超时与pump泵动 QUIC 事件循环两个辅助函数增加基于 libssl 报告截止时间deadline合并/比较多个超时并适时调用pump的超时计算代码。线程辅助模式见 ddd-02-conn-nonblocking-threads 变体下其中部分改动并不必要。实际改动方法名变更同 ddd-01使用 ALPN同 ddd-01可轮询资源句柄的暴露策略大幅演变现在用BIO_get_rpoll_descriptor/BIO_get_wpoll_descriptor判定 I/O 就绪而非当初设想的SSL_get_poll_fd何时 pollPOLLIN、何时 pollPOLLOUT的策略已变现在由SSL_net_read_desired/SSL_net_write_desired暴露QUIC 引擎事件处理截止时间的 API 演变为SSL_get_event_timeout替代设想中的BIO_get_timeout/SSL_get_timeoutQUIC 事件处理 API 更名为更具描述性的SSL_handle_events替代设想中的BIO_pump/SSL_pump。以下改动曾被预判为必要、最终证明不必要原以为必须修改在 SSL BIO 之后压入BIO_f_buffer()的代码因为网络侧的缓冲与 QUIC 不兼容。结果并不需要——只需拒绝BIO_push()调用即可。缓冲 BIO 最终会在 SSL BIO 释放时一并释放但该缓冲实际不被使用、纯属多余因此应用仍应尽量删除这段代码。demo 源码中有一段注释明确说明了这一点QUIC SSL 对象与网络之间无法夹缓冲 BIOBIO_push会被忽略代码不删也能工作但若用SSL_set_bio把缓冲 BIO 设为 QUIC SSL 对象底层则不会工作如需在网络侧缓冲应使用BIO_s_dgram_pair。非阻塞收发的核心约定是tx/rx返回-1表示错误、返回-2表示会阻塞对应EWOULDBLOCK并通过BIO_should_retry/BIO_should_read/BIO_should_write记录下次需要读还是写主循环据此构造pollfd。QUIC 版本则改为用SSL_net_read_desired/SSL_net_write_desired直接查询 SSL 状态机当前对 socket 可读/可写事件的需求int get_conn_pending_tx(APP_CONN *conn) { #ifdef USE_QUIC return (SSL_net_read_desired(conn-ssl) ? POLLIN : 0) | (SSL_net_write_desired(conn-ssl) ? POLLOUT : 0) | POLLERR; #else return (conn-tx_need_rx ? POLLIN : 0) | POLLOUT | POLLERR; #endif }超时处理的核心是get_conn_pump_timeout与pumpint get_conn_pump_timeout(APP_CONN *conn) { struct timeval tv; int is_infinite; if (!SSL_get_event_timeout(conn-ssl, tv, is_infinite)) return -1; return is_infinite ? -1 : timeval_to_ms(tv); } void pump(APP_CONN *conn) { SSL_handle_events(conn-ssl); }SSL_get_event_timeout返回此后必须调用一次 libssl任意调用BIO_read/BIO_write/SSL_handle_events皆可的毫秒数-1表示无需调用该值在下次调用 libssl 后可能变化。SSL_handle_events则用于推进 libssl 内部状态机无需执行应用层读写。这两个函数均声明于 include/openssl/ssl.h.inSSL_net_read_desired/SSL_net_write_desired紧随其后ssl.h.inBIO_get_rpoll_descriptor声明于 include/openssl/bio.h.in。ddd-02-conn-nonblocking-threads线程辅助模式这是 ddd-02 的变体基线相同但改动不同。线程辅助模式thread-assisted由内部辅助线程执行 QUIC 事件处理使应用所需改动比 ddd-02 少得多。最初规划改动更换方法这次用QUIC_client_thread_method而非QUIC_client_method用BIO_get_poll_fd取得待 poll 的 FD改变POLLIN/POLLOUT/POLLERR标志判定方式。相比 ddd-02 的改动清单这份清单显著更小。实际改动方法名改为OSSL_QUIC_client_thread_method命名空间原因使用 ALPN同 ddd-01用BIO_get_rpoll_descriptor取代BIO_get_poll_fd使用SSL_net_read_desired/SSL_net_write_desired。注意线程辅助版本的主循环仍然使用简单的poll(pfd, 1, timeout)2000ms 固定超时不需要SSL_get_event_timeout与SSL_handle_events——因为内部辅助线程已经承担了事件泵动职责。这正是线程辅助模式的卖点应用侧代码与 TLS 几乎一致。ddd-03-fd-blocking应用自建 FD 的阻塞模式与 ddd-01 类似但由应用自行创建 socket 并通过SSL_set_fd传给 libssl。最初规划改动更换方法同 ddd-01socket(2)参数从(AF_INET, SOCK_STREAM, IPPROTO_TCP)改为(AF_INET, SOCK_DGRAM, IPPROTO_UDP)。实际改动方法名变更同 ddd-01使用 ALPN同 ddd-01。该 demo 的new_conn展示了 AOSF 模式的典型连接装配序列SSL_new→SSL_set_connect_state→SSL_set_fd→SSL_set1_host→SSL_set_tlsext_host_nameSNIQUIC 下再叠加 ALPN。ddd-04-fd-nonblocking应用自建 FD 的非阻塞模式与 ddd-02 类似但 FD 由应用直接传递。最初规划改动更换方法同 ddd-01socket(2)参数改为 UDP同 ddd-03改变 poll 标志判定方式新增get_conn_pump_timeout与pump增加基于 deadline 的超时合并/比较与pump调用逻辑。实际改动方法名变更同 ddd-01使用 ALPN同 ddd-01SSL_get_timeout换成SSL_get_event_timeoutSSL_pump改名SSL_handle_eventspoll 标志判定改用SSL_net_read_desired/SSL_net_write_desired。ddd-05-mem-nonblocking内存 BIO 纯状态机模式这个 demo 更复杂应用在 libssl 与网络之间自建并管理内存缓冲把 libssl 当作不做网络 I/O 的纯状态机。QUIC 下这种模式更繁琐因为网络通道上必须保持数据报datagram语义。最初规划改动更换方法同 ddd-01BIO_new_bio_pair改为BIO_new_dgram_pair提供带数据报语义的双向内存缓冲 BIO改变 poll 标志判定方式若应用用于缓冲数据报的缓冲小于 1472 字节可能需要调整缓冲大小1472 为常见以太网 MTU 下不产生 IP 分片的 UDP 载荷上界socket(2)参数改为 UDP。实际改动方法名变更同 ddd-01使用 ALPN同 ddd-01构造BIO_s_dgram_pair的 API 最终命名为BIO_new_bio_dgram_pair而非BIO_new_dgram_pair使用SSL_net_read_desired/SSL_net_write_desired。BIO_new_bio_dgram_pair声明于 include/openssl/bio.h.in。该 demo 的连接装配也体现了 BIOm 模式的典型结构SSL_new→BIO_new_bio_pair/BIO_new_bio_dgram_pair生成internal_bio与net_bio→SSL_set_bio(ssl, internal_bio, internal_bio)→ 应用侧再包一层BIO_f_sslBIO_newBIO_set_ssl作为明文侧读写句柄同时把net_bio留给自己在网络侧搬运密文。ddd-06-mem-uv接入真实异步 I/O 反应堆libuv这是整套 demo 中最复杂的一个使用真实世界的异步 I/O 反应堆 libuvNode.js 的底层引擎全程非阻塞在 QUIC 栈两侧用内存缓冲向应用与网络馈送数据以此验证 API 设计在真实异步 I/O 体系下的可行性。最初规划改动更换方法同 ddd-01若干 libuv 用法调整以切换 UDP新增 libuv 定时器事件配置BIO_new_bio_pair改为BIO_new_dgram_pair同 ddd-05因 libuv 设计需要对代码做一定重排。实际改动方法名变更同 ddd-01使用 ALPN同 ddd-01BIO_new_dgram_pair改名BIO_new_bio_dgram_pair同 ddd-05SSL_get_timeout换成SSL_get_event_timeoutSSL_pump改名SSL_handle_events基于对 libuv 运行机制更准确的理解修正用法并随之调整代码。结论复盘改动最小且有限DDD 流程成功达成了既定目标交付一个只需最小 API 改动即可接入的 QUIC API。在原始规划之上、为让这些 demo 用 QUIC 跑通而追加的改动范围极其有限大多只是小改动。每个 demo 在#ifdef USE_QUIC守卫下累计的改动规划 追加既最小又有限。这里最小与有限是两个不同标准即使技术需求不可抗拒应用也可能需要海量改动此时仍可称最小而 DDD demo 证明代表性应用所需的改动不仅最小而且有限——有些 demo 的改动小到令人惊叹例如 ddd-01-conn-blocking 和 ddd-02-conn-nonblocking-threads假设 ALPN 已配置ddd-01 字面上仅需一行改动即可启用 QUIC。构建与运行这些 Demodoc/designs/ddd/Makefile 提供了完整的构建规则每个 demo 会同时编译出-tls与-quic两个可执行文件TESTS_BASE ddd-01-conn-blocking \ ddd-02-conn-nonblocking \ ddd-02-conn-nonblocking-threads \ ddd-03-fd-blocking \ ddd-04-fd-nonblocking \ ddd-05-mem-nonblocking \ ddd-06-mem-uv TESTS $(foreach x,$(TESTS_BASE),$(x)-tls $(x)-quic) CFLAGS -I../../../include -g -Wall -Wsign-compare LDFLAGS -L../../.. LDLIBS -lcrypto -lssl ddd-%-tls: ddd-%.c $(CC_CMD) ddd-%-quic: ddd-%.c $(CC_CMD) -DUSE_QUIC ddd-%-uv-tls: ddd-%-uv.c $(CC_CMD) -luv ddd-%-uv-quic: ddd-%-uv.c $(CC_CMD) -luv -DUSE_QUIC关键要点同一份.c源文件通过-DUSE_QUIC编译宏在 TLS 与 QUIC 两个变体之间切换——这正是阅读从 TLS 迁移到 QUIC 需要改什么的最直接途径与共享库链接时默认运行前需确保 libcrypto 与 libssl 在库搜索路径上例如LD_LIBRARY_PATH../../.. ./ddd-01-conn-blocking-tls构建 ddd-06-mem-uv-tls 与 ddd-06-mem-uv-quic 需要 libuv 库与头文件Ubuntu 的libuv1-dev软件包编译时额外加-luv每个 demo 的用法是demo host port如./ddd-01-conn-blocking-tls openssl.org 443若默认证书存储不可用可设置SSL_CERT_DIR指向 CA 证书目录由于-tls变体执行的是真实 HTTP/1.0 GET 请求也可以借助支持 QUIC 的 HTTPS 站点验证-quic变体需将 demo 中alpn改为站点实际协商的 ALPN 协议如{ 2, h, 3 }。Windows 平台的注意事项doc/designs/ddd/WINDOWS.md 记录了 Windows 支持引入的特殊问题源于 Windows socket API 的若干有趣特性poll(2) 缺失Windows 通常不提供 poll(2)。Vista 引入的WSAPoll(2)曾长期存在 Microsoft 拒绝修复的 bug直到某个 Windows 10 版本才修复因此WSAPoll仅在较新版本 Windows 上可行select() 的差异传统上 Windows 用select()轮询但它与 POSIX 不同——POSIX 的select()接受 FD 位掩码而 Windows 的select()接受一个内嵌固定长度 socket 句柄数组的结构。这是因为 Windows 上 socket 是 NT 内核句柄不会像 FD 那样连续分配。因此 Windows 的select()其实非常接近 POSIX 的poll()是可行的轮询方案高性能轮询的缺失与 IOCPselect()/poll()都不是高性能轮询方案Windows 没有 epoll 或 kqueue高性能网络 I/O 应使用I/O Completion PortsIOCP。对围绕轮询设计的应用来说IOCP 支持很痛苦IOCP 是更高层接口——在轮询之上构建 IOCP 式接口容易但在 IOCP 之上构建轮询式接口基本不可能。因此异步 I/O 库如 libuv、nanomsg内部普遍维护两套实现或相当大一部分代码一套基于轮询的 I/O 反应堆、一套基于 IOCP 的实现以绕过这种阻抗失配模型差异轮询报告的是readiness就绪IOCP 报告的是operation completion操作完成——在 IOCP 模型里你对 socket 发起读写读写完成后事件被投递到 IOCP这是根本不同的模型更接近 libuv 这类高层异步 I/O 库。各 demo 在 Windows/IOCP 下的适用性评估Demo评估结论ddd-01-conn-blocking阻塞示例不适用 IOCPddd-02-conn-nonblockingsocket 由 OpenSSL 管理不支持 IOCPddd-03-fd-blocking阻塞示例不适用 IOCPddd-04-fd-nonblockinglibssl 通过BIO_set_fd拿到 FDBIO_s_sock看起来不支持 overlapped即 IOCP 式I/O因为那需要特殊的WSASend()/WSARecv()而非标准send()/recv()。既然 libssl 的BIO_s_sock本就不支持 IOCP那么任何使用BIO_s_sock的应用显然没打算用 IOCP无需担心该示例对 IOCP 的适配ddd-05-mem-nonblocking应用全权负责在内存 BIO 与网络间搬运数据可以自行使用 IOCPddd-06-mem-uv使用内存 BIO libuv而 libuv 支持 IOCP证明了内存 BIO 可用于支撑 IOCP 式用法文档还指出粗略查看 GitHub 上的代码当人们确实在 libssl 上使用 IOCP 时几乎都是通过传给 libssl 的内存 BIO实现的——ddd-05 与 ddd-06 本质上演示的正是这种用法ddd-06 在 Windows 上内部即使用 IOCP。最终结论是既然 libssl 本就不支持 IOCP无需过度担忧而最坏情况下也总有可行方案如 demo 5、6。延伸阅读方法论文档与 demo 列表doc/designs/ddd/README.mdDDD 流程结论复盘规划 vs 实际改动对照doc/designs/ddd/REPORT.mdWindows/IOCP 专项分析doc/designs/ddd/WINDOWS.mdQUIC API 函数声明OSSL_QUIC_client_methodinclude/openssl/quic.h、SSL_handle_events/SSL_get_event_timeout/SSL_net_read_desired/SSL_net_write_desiredinclude/openssl/ssl.h.in、SSL_set_alpn_protosinclude/openssl/ssl.h.in、BIO_get_rpoll_descriptor/BIO_new_bio_dgram_pairinclude/openssl/bio.h.in、bio.h.inQUIC 整体设计文档doc/designs/quic-design/quic-overview.md 与 doc/designs/quic-design/quic-api.mdQUIC 编程指南README-QUIC.mdREADME-QUIC.md【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考