☰
OpenSSL 实战:用 sslecho/echecho 示例从零构建 TLS 回显客户端与服务端(含 ECH 加密 ClientHello)
2026/9/29 23:09:42 网站建设 项目流程

OpenSSL 实战:用 sslecho/echecho 示例从零构建 TLS 回显客户端与服务端(含 ECH 加密 ClientHello)

【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl

导读

本文以 OpenSSL 仓库中的官方示例 demos/sslecho 为主线,完整讲解如何用 OpenSSL 的 C API 编写一个最简单的 TLS 回显(echo)客户端与服务端:从裸 TCP 连接、升级为 SSL 连接、到SSL_write/SSL_read收发数据,全程不依赖任何第三方封装库。在此基础上,文章进一步剖析该示例提供的 Encrypted Client Hello(ECH)变体echecho,展示启用 ECH 所需的最小代码改动。读完本文,你将掌握 TLS 客户端/服务端的标准编程范式(TCP connect → SSL_connect → SSL_read/write)、自签名证书的生成与配置,以及 OpenSSL 3.x 新增 ECH API(OSSL_ECHSTORE_*、SSL_CTX_set1_echstore、SSL_ech_get1_status)的用法和状态码语义。

示例概览:一个控制台程序,两种运行模式

sslecho是一个控制台应用程序,通过命令行参数决定运行模式:

  • sslecho s:以服务端模式运行,监听 TCP 端口4433(源码中static const int server_port = 4433;,见 main.c),等待客户端接入并回显收到的每一行文本;
  • sslecho c hostname:以客户端模式运行,连接指定主机名的服务端,把键盘输入逐行发给服务端,并把回显结果显示在终端。

不带任何参数直接运行程序,会打印用法说明(usage()函数,见 main.c):

Usage: sslecho s --or-- sslecho c hostname c=client, s=server, hostname=hostname of server

该示例的服务端代码改编自 OpenSSL Wiki 上的 Simple TLS Server 教程,在原有基础上增加了回显逻辑;客户端代码则是全新编写,用于与服务端建立连接并把键盘输入发送过去。

程序启动流程

主函数(main.c)的启动逻辑清晰:

  1. 打印启动横幅(sslecho : Simple Echo Client/Server);
  2. 检查参数数量与首字母,确定是客户端还是服务端;客户端还需第二个参数(服务端主机名);
  3. 调用create_context()创建SSL_CTX:服务端用TLS_server_method(),客户端用TLS_client_method(),再由SSL_CTX_new()实例化(见 main.c);
  4. 按模式分别执行服务端或客户端的连接与回显循环。

在非 Windows 平台,程序启动时还会执行signal(SIGPIPE, SIG_IGN)(见 main.c),忽略SIGPIPE信号,这样当对端异常断开导致管道破裂时,服务端进程不会因信号被终止,而是由SSL_read/SSL_write返回错误码来处理。

构建与运行:Makefile 与共享库路径

示例自带 Makefile,同时构建sslecho和echecho两个二进制:

TESTS = sslecho echecho CFLAGS = -I../../include -g -Wall LDFLAGS = -L../.. LDLIBS = -lssl -lcrypto all: $(TESTS)

关键点:

  • 头文件搜索路径-I../../include指向仓库根目录下的 include(即 OpenSSL 的头文件目录);
  • 链接库为-lssl -lcrypto,对应libssl.so与libcrypto.so;
  • 当以共享库方式链接(默认情况)时,运行前必须确保libcrypto与libssl在动态库搜索路径上,Makefile 注释中给出了示例命令:
LD_LIBRARY_PATH=../.. ./sslecho

本地联调运行

先启动服务端:

$ LD_LIBRARY_PATH=../.. ./sslecho s

再另开一个终端启动客户端(localhost可换成实际主机名或 IP):

$ LD_LIBRARY_PATH=../.. ./sslecho c localhost

服务端会依次打印Client TCP connection accepted与Client SSL connection accepted;客户端会打印TCP connection to server successful与SSL connection to server successful。此后在客户端输入任意文本并回车,服务端打印Received: ...并原样回显,客户端随即打印回显内容。客户端输入kill会触发服务端内置的"关机指令"(源码注释为Terminate...with extreme prejudice),服务端退出;客户端直接按回车或 Ctrl-D(EOF)则正常结束本次连接。

TLS 编程范式的三个关键步骤

README 用四点高度概括了客户端代码所展示的核心要点,这正是 OpenSSL 常规 TLS 编程的骨架:

  1. 与 SSL 服务器的连接,起始于一次标准的 TCPconnect;
  2. TCP 连接建立后,客户端通过SSL_connect()将连接"升级"为 SSL;
  3. SSL 握手完成后,通过SSL_write()与SSL_read()收发数据;
  4. 整体流程相当简单。

第一步:TCP 层建连(socket/connect)

socket()创建套接字时使用AF_INET + SOCK_STREAM(见 main.c)。服务端额外执行setsockopt(SO_REUSEADDR)(便于快速重启)、bind()、listen();客户端则直接调用connect()连接server_port对应的地址。

客户端在 TCP 层使用的地址转换函数是inet_pton(AF_INET, rem_server_name, &addr.sin_addr.s_addr)(见 main.c),因此传入的主机名需要是点分十进制 IP(如127.0.0.1)——这是示例的一个简化,真实程序中通常会先用getaddrinfo()做域名解析。

第二步:把 TCP 连接升级为 SSL

关键 API 序列如下(客户端侧,见 main.c):

ssl = SSL_new(ssl_ctx); /* 基于 SSL_CTX 创建 SSL 对象 */ SSL_set_fd(ssl, (int)client_skt); /* 把已连接的 TCP 套接字绑定到 SSL 对象 */ SSL_set_tlsext_host_name(ssl, rem_server_name); /* 设置 SNI */ SSL_set1_dnsname(ssl, rem_server_name); /* 配置证书主机名校验 */ SSL_connect(ssl); /* 执行 TLS 握手,成功返回 1 */

服务端侧则对应为(见 main.c):

ssl = SSL_new(ssl_ctx); SSL_set_fd(ssl, (int)client_skt); /* 绑定 accept() 返回的客户端套接字 */ SSL_accept(ssl); /* 等待客户端发起 TLS 握手 */

SSL_set1_dnsname()与SSL_set1_ipaddr()的声明位于 include/openssl/ssl.h.in,二者分别用于在证书校验时按 DNS 名称或 IP 地址核对对端身份,防止中间人冒充。

第三步:用 SSL_read/SSL_write 收发数据

连接建立后,数据读写与普通 socket 读写体验一致:

  • 服务端循环SSL_read(),把收到的内容用SSL_write()原样写回,若读到kill\n则终止(见 main.c);
  • 客户端用fgets()从stdin取一行,SSL_write()发送,再SSL_read()等待回显(见 main.c)。

注意SSL_read()的返回值语义:返回 0 表示对端已关闭连接,返回负数表示出错,此时需用ERR_print_errors_fp(stderr)打印错误栈定位问题——这是示例中反复出现的错误处理模式。

收尾:SSL_shutdown 与资源释放

程序退出路径(见 main.c)依次调用SSL_shutdown()(发送关闭通知)、SSL_free()(释放 SSL 对象)、SSL_CTX_free()(释放上下文)以及closesocket()(关闭套接字)。服务端在回显循环结束后还会执行SSL_shutdown(ssl); SSL_free(ssl);为下一个客户端做好准备(见 main.c)。

证书与密钥:自签名证书的生成与加载

示例随附的 cert.pem 与 key.pem 是自签名证书,其 "Common Name" 为localhost。README 建议:最好使用真实主机名来生成 pem 文件,这样客户端按主机名校验证书时才不会因名称不匹配而失败。

生成自己的证书(来自 A-SSL-Docs.txt)

示例目录下的 A-SSL-Docs.txt 给出了生成 RSA-4096 自签名证书的命令(有效期 10 年):

openssl req -newkey rsa:4096 -x509 -sha256 -days 3650 -nodes -out cert.pem -keyout key.pem

交互提示中可以一路回车接受默认值,唯一需要认真填写的是 "Common Name",应输入localhost或实际主机名。同一对密钥可以同时供通信双方使用,无论它们是否在同一台机器上。

服务端与客户端的证书加载方式对比

  • 服务端通过SSL_CTX_use_certificate_chain_file(ctx, "cert.pem")加载证书链、SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM)加载私钥(见 main.c);
  • 客户端通过SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL)开启对服务端证书的强制校验,并用SSL_CTX_load_verify_locations(ctx, "cert.pem", NULL)把自签名证书加入信任列表(见 main.c)。

源码注释特别说明:真实应用中客户端通常会直接调用SSL_CTX_set_default_verify_paths(ctx)使用系统默认证书信任库;本例因为用的是自签名证书,才必须显式信任它。

ECH 变体:echecho 如何用最小改动启用加密 ClientHello

Encrypted Client Hello(ECH)是 TLS 领域保护隐私的重要扩展,它把 ClientHello 中的敏感字段(尤其是 SNI 服务器名称)加密起来,防止网络中间人窥探用户访问的目标站点。OpenSSL 在该仓库中提供了共享模式(shared-mode)的 ECH 实现,相关头文件为 include/openssl/ech.h。

echecho.c 与sslecho功能完全一致,但演示了启用 ECH 所需的最小代码改动。echecho二进制与sslecho拥有相同的用户界面(echecho s起服务端,echecho c ip起客户端),不同之处在于:

  • 它基于硬编码的 ECH 配置数据启用 ECH(源码中的echconfig是 base64 编码的 ECHConfigList,echprivbuf是包含 ECH 私钥与 ECHCONFIG 的 PEM 块,见 echecho.c);
  • 真实的服务端应改为从文件加载这些配置,真实的客户端则应通过 DNS 获取 ECHConfigList。

ECH 的"三步启用法"

README 明确指出,使用 ECH 只需做两件事:

  1. 通过OSSL_ECHSTORE_read_*系列 API 把 ECH 数据加载到OSSL_ECHSTORE;
  2. 通过SSL_CTX_set1_echstore()把它挂到 SSL 上下文上。

至于用SSL_ech_get1_status()查询并打印 ECH 状态,属于可选步骤。示例中的configure_ech()函数(见 echecho.c)完整展示了这个过程:

static int configure_ech(SSL_CTX *ctx, int server, unsigned char *buf, size_t len) { OSSL_ECHSTORE *es = NULL; BIO *es_in = BIO_new_mem_buf(buf, (int)len); if (es_in == NULL || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL) goto err; if (server && OSSL_ECHSTORE_read_pem(es, es_in, 1) != 1) goto err; if (!server && OSSL_ECHSTORE_read_echconfiglist(es, es_in) != 1) goto err; if (SSL_CTX_set1_echstore(ctx, es) != 1) goto err; BIO_free_all(es_in); return 1; err: OSSL_ECHSTORE_free(es); BIO_free_all(es_in); return 0; }

这里的关键差异在于加载方式:

  • 服务端调用OSSL_ECHSTORE_read_pem(es, es_in, 1),从 PEM 块中读取 ECH 私钥与配置(第三个参数1对应OSSL_ECH_FOR_RETRY,即该配置用于 ECH 重试,见 ech.h);
  • 客户端调用OSSL_ECHSTORE_read_echconfiglist(es, es_in),只读取公开的 ECHConfigList(不含私钥)。

OSSL_ECHSTORE是一个对外不透明的类型,用于管理 ECHConfigList、ECH 私钥及相关元数据;完整的 API 列表与签名可参考 doc/man3/SSL_set1_echstore.pod,其中包括OSSL_ECHSTORE_new_config()(生成新配置与密钥)、OSSL_ECHSTORE_write_pem()(导出 PEM)、OSSL_ECHSTORE_downselect()(选择条目)、SSL_ech_set1_server_names()(设置内外层 SNI)等。该文档还说明:当前版本仅支持 ECH 的共享模式,不支持 split-mode,且 ECH 版本为 RFC 9849(OSSL_ECH_RFC9849_VERSION,见 ech.h)。

运行 echecho

按 README 给出的命令启动服务端与客户端:

$ LD_LIBRARY_PATH=../.. ./echecho s $ LD_LIBRARY_PATH=../.. ./echecho c localhost

一切顺利时,双方在每次连接建立后都会打印 ECH 状态:

ECH worked (status: 1, inner: localhost, outer: example.com)

含义:ECH 加密成功,内层(真实)SNI 是localhost,外层(公开)SNI 是配置中的公共名称example.com。

看懂 ECH 状态码

SSL_ech_get1_status()返回的状态码定义在 ech.h:

状态码宏定义含义
4SSL_ECH_STATUS_BACKEND后端感知到ech_is_inner标记
3SSL_ECH_STATUS_GREASE_ECH发送了 GREASE 且收到了 ECH 响应
2SSL_ECH_STATUS_GREASE发生了 ECH GREASE
1SSL_ECH_STATUS_SUCCESSECH 成功
0SSL_ECH_STATUS_FAILED内部或协议错误
-100SSL_ECH_STATUS_BAD_CALL传入参数为 NULL
-101SSL_ECH_STATUS_NOT_TRIED未尝试 ECH
-102SSL_ECH_STATUS_BAD_NAMEECH 成功但服务端证书校验失败
-103SSL_ECH_STATUS_NOT_CONFIGURED未配置 ECH
-105SSL_ECH_STATUS_FAILED_ECH尝试失败但收到来自正常名称的 ECH
-106SSL_ECH_STATUS_FAILED_ECH_BAD_NAME尝试失败且来自异常名称的 ECH

混合联调时的三种典型输出

README 给出了三种具有诊断价值的联调场景:

场景一:echecho 客户端 + echecho 服务端

双方都输出ECH worked (status: 1, ...),如上文所示,这是最理想的结果。

场景二:普通 sslecho 客户端 + echecho 服务端

服务端检测到客户端没有尝试 ECH,输出:

ECH failed/not-tried (status: -101, inner: (null), outer: (null))

状态码-101(SSL_ECH_STATUS_NOT_TRIED)表示对端根本未发起 ECH 流程。

场景三:echecho 客户端 + 普通 sslecho 服务端

由于客户端尝试了 ECH 但服务端不支持,客户端会以错误退出。在 debug 构建下,错误栈类似于:

80EBEE54227F0000:error:0A000163:SSL routines:tls_process_initial_server_flight:ech required:ssl/statem/statem_clnt.c:3274:

该错误(ech required)发生在 TLS 1.3 客户端状态机处理首条服务器消息的阶段,源码位置在 ssl/statem/statem_clnt.c。README 特别说明:真实客户端在这种情况下大概率会回退到不使用 ECH 继续连接,只是这个示例为演示目的选择了直接报错。

与此同时,服务端也会因为收到客户端发出的 ECH alert(SSL alert number 121)而退出:

403787A8307F0000:error:0A000461:SSL routines:ssl3_read_bytes:reason(1121):../ssl/record/rec_layer_s3.c:1588:SSL alert number 121

这两条错误信息分别指向 ssl/statem/statem_clnt.c 与 ssl/record/rec_layer_s3.c,可作为排查 ECH 握手失败时的错误定位参考。

从示例到真实应用:可以借鉴的改造方向

综合 README 与源码,可以总结出将本示例落地为真实程序的几条路径:

  1. 证书信任链:把SSL_CTX_load_verify_locations()换成SSL_CTX_set_default_verify_paths()以使用系统信任库,或接入自己的 CA 证书链;
  2. 域名解析:客户端目前用inet_pton()只接受点分 IP,真实程序应改用getaddrinfo()支持主机名解析,并结合SSL_set1_dnsname()做证书名称校验;
  3. ECH 配置来源:echecho的 ECH 数据是硬编码的,真实服务端应从文件(或密钥管理设施)加载 PEM,真实客户端应从 DNS HTTPS 记录获取 ECHConfigList;
  4. 超时与并发:源码注释(见 main.c)指出,程序尚未为 TCP/SSL 的 accept/read 加入超时机制,且服务端是串行处理连接的,生产环境需要引入超时控制与并发模型(如线程、事件循环或非阻塞 socket 配合SSL_get_error())。

小结

sslecho以不到 400 行 C 代码,完整演示了 OpenSSL TLS 编程的最小闭环:SSL_CTX创建 → 证书加载 → TCP 建连 →SSL_new/SSL_set_fd→SSL_connect/SSL_accept握手 →SSL_read/SSL_write通信 →SSL_shutdown清理。而它的 ECH 变体echecho进一步展示了 ECH 与普通 TLS 代码之间的最小差异——只需加载 ECH 数据并调用SSL_CTX_set1_echstore(),配合SSL_ech_get1_status()即可验证加密 ClientHello 是否生效。对于想快速上手 OpenSSL 编程或调研 ECH 落地细节的开发者而言,这两个示例与 include/openssl/ech.h、doc/man3/SSL_set1_echstore.pod 构成了一套完整、可直接对照学习的参考材料。

【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询