验证与连接测试

s_client 测试 TLS 连接:SNI、STARTTLS 与双向认证

作者: · 最后核实 实测 macOS · OpenSSL 3.6.1

openssl s_client 是排查 TLS 问题最通用的客户端:任何 TLS 端点(包括用 STARTTLS 升级的邮件、数据库服务)的证书链、协议版本、密码套件、密钥交换组和验证结果,它都能显示出来。下面的输出来自 OpenSSL 3.6.1,除注明版本的选项外,3.0 到 3.6 的用法相同。只想在浏览器里看结果,可以用 SSL 检测工具。

一屏看完连接概况

-brief 只输出协议、套件、证书、验证结果和密钥交换组。

openssl s_client -connect www.example.com:443 -servername www.example.com -brief < /dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=www.example.com
Hash used: SHA256
Signature type: ecdsa_secp256r1_sha256
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE

Negotiated TLS1.3 group: X25519MLKEM768 表示用了混合后量子密钥交换,OpenSSL 3.5 起默认就会提供。对方不支持时会显示 Peer Temp Key: X25519, 253 bits。去掉 -brief 能看到完整的证书链、会话参数和会话票据。

用 -servername 发送正确的域名(SNI)

从 OpenSSL 1.1.1 起,-connect 写域名时会自动发送 SNI;写 IP 时必须自己加 -servername。

openssl s_client -connect 203.0.113.10:443 -servername www.example.com -brief < /dev/null
openssl s_client -connect 203.0.113.10:443 -noservername -brief < /dev/null
Peer certificate: CN=www.example.com
Verification: OK
...
Peer certificate: CN=localhost
Verification error: self-signed certificate

第二条没有发送 SNI,服务器只好返回默认证书,这正是不支持 SNI 的老客户端看到的情形。1.1.0 及更早版本只有加了 -servername 才发 SNI,所以很多老教程每条命令都带着它。一台服务器托管多个站点(宝塔面板建的多个站点就是这样)时,SNI 决定了返回哪张证书,见 SNI。

指定源站 IP 测试,同时保留域名

连 IP、SNI 和 HTTP Host 头都写真实域名,适合在 CDN 或负载均衡后面直接测源站。

printf 'HEAD / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n' \
  | openssl s_client -quiet -connect 203.0.113.10:443 -servername www.example.com
depth=2 C=US, O=Example CA, CN=Example Root R1
verify return:1
depth=1 C=US, O=Example CA, CN=Example TLS Issuing CA
verify return:1
depth=0 CN=www.example.com
verify return:1
HTTP/1.1 200 OK

-quiet 隐藏会话信息并隐含 -ign_eof,s_client 会等服务器主动断开;Connection: close 让服务器响应完立刻断开。接入阿里云、腾讯云 CDN 前,先用这种方式确认源站证书没问题,再切换 DNS。

查看服务器发送的完整证书链

-showcerts 按顺序以 PEM 格式打印服务器发来的每一张证书。

openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts < /dev/null
Certificate chain
 0 s:CN=www.example.com
   i:C=CN, O=Example Corp, CN=Example Intermediate CA
   a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256
   v:NotBefore: Nov  9 18:16:48 2026 GMT; NotAfter: Dec 11 18:16:48 2027 GMT
-----BEGIN CERTIFICATE-----
MIICXzCCAgWgAwIBAgICEAAwCgYIKoZIzj0EAwIw...
-----END CERTIFICATE-----
 1 s:C=CN, O=Example Corp, CN=Example Intermediate CA
   i:C=CN, O=Example Corp, CN=Example Root CA
...

s: 是主体,i: 是签发者;较新的版本还会打印 a:(密钥和签名算法)和 v:(有效期)。每一行 i: 都应等于下一张证书的 s:。只有一张证书,说明缺了中间证书,处理方法见验证证书链。

强制使用 TLS 1.2 或 TLS 1.3

-tls1_2、-tls1_3 锁定协议版本,可以分别确认两个版本都已启用。

openssl s_client -connect www.example.com:443 -tls1_2 -brief < /dev/null
openssl s_client -connect www.example.com:443 -tls1_3 -brief < /dev/null
Protocol version: TLSv1.2
Ciphersuite: ECDHE-ECDSA-AES256-GCM-SHA384
...
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384

TLS 1.2 的套件名是 OpenSSL 风格(ECDHE-ECDSA-AES256-GCM-SHA384),TLS 1.3 用的是 IANA 名称。在 OpenSSL 3.x 上测试 TLS 1.0/1.1 还要降低安全级别,方法见密码套件与协议版本。

检查 ALPN 与 HTTP/2

-alpn 声明客户端支持的应用层协议,服务器选中的结果显示在 ALPN protocol 一行。

openssl s_client -connect www.example.com:443 -alpn h2,http/1.1 < /dev/null 2>/dev/null | grep -i alpn
ALPN protocol: h2

显示 No ALPN negotiated 说明服务器没有处理 ALPN,浏览器只能用 HTTP/1.1。Nginx 要开启 HTTP/2:1.25.1 起用 http2 on;,更早的版本写成 listen 443 ssl http2;。HTTP/3 走的是 QUIC 而不是 TCP,s_client 从 OpenSSL 3.2 起有 -quic 选项,但一般用 curl --http3 测试更方便。

在脚本里干净地退出

给 s_client 一个空的标准输入,它就会在握手完成后退出,而不是等你输入。

openssl s_client -connect www.example.com:443 < /dev/null
echo | openssl s_client -connect www.example.com:443
openssl s_client -connect www.example.com:443 -no-interactive   # OpenSSL 3.2+
...
Verify return code: 0 (ok)
---
DONE

看到 DONE 表示 s_client 自己关闭了连接。在管道里加 2>/dev/null 可以去掉 depth= 那些行。交互模式下,以 Q 开头的一行表示退出、R 开头表示重新协商(仅 TLS 1.2),手工敲 HTTP 请求时容易误触发,这时加 -ign_eof 或 -quiet。

测试 SMTP 的 STARTTLS

-starttls smtp 会先发 EHLO 和 STARTTLS,再在同一连接上开始 TLS 握手。

openssl s_client -connect smtp.example.com:587 -starttls smtp -brief < /dev/null
openssl s_client -connect smtp.example.com:25 -starttls smtp -name mx-test.example.com < /dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=smtp.example.com
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE

-name 指定 EHLO 里使用的主机名(LMTP、XMPP 也用它)。465 端口(SMTPS)是直接 TLS,不要加 -starttls。国内邮箱服务商大多提供 465 端口的 SSL 连接,部分也支持 587 STARTTLS,以各家帮助文档为准。阿里云、腾讯云的云服务器通常默认封禁出方向 25 端口,从 ECS 上测 25 端口失败未必是对方的问题;也可以用 STARTTLS 检测工具从外部测试。

测试 IMAP、POP3、FTP、数据库和 LDAP

其他常见协议也用 -starttls;直接 TLS 端口则不用。

openssl s_client -connect mail.example.com:143  -starttls imap     < /dev/null
openssl s_client -connect mail.example.com:110  -starttls pop3     < /dev/null
openssl s_client -connect ftp.example.com:21    -starttls ftp      < /dev/null
openssl s_client -connect db.example.com:5432   -starttls postgres < /dev/null
openssl s_client -connect db.example.com:3306   -starttls mysql    < /dev/null
openssl s_client -connect ldap.example.com:389  -starttls ldap     < /dev/null
openssl s_client -connect mail.example.com:993  -brief             < /dev/null
Didn't find STARTTLS in server response, trying anyway...
...:error:0A000126:SSL routines::unexpected eof while reading:...

上面的输出是服务器没有宣告 STARTTLS 时的样子:s_client 硬着头皮继续,结果被服务器断开。OpenSSL 3.x 支持的关键字有 smtp、pop3、imap、ftp、xmpp、xmpp-server、irc、postgres、mysql、lmtp、nntp、sieve 和 ldap。测试云数据库(如阿里云 RDS、腾讯云 MySQL)的 TLS 时,要先在控制台开启 SSL 并下载对应的 CA 证书,再用 -CAfile 指定。

协议 STARTTLS 端口 直接 TLS 端口
SMTP 提交 587 465
IMAP 143 993
POP3 110 995
LDAP 389 636

检查 OCSP 装订

-status 请求服务器装订 OCSP 响应,收到的话会打印出来。

openssl s_client -connect www.example.com:443 -servername www.example.com -status < /dev/null 2>/dev/null \
  | sed -n '/OCSP response/,/Next Update/p'
OCSP responses: number of responses: 1
======================================
OCSP Response Data:
    OCSP Response Status: successful (0x0)
    Response Type: Basic OCSP Response
    ...
    Cert Status: good
    This Update: Nov  9 04:50:45 2026 GMT
    Next Update: Nov 16 04:50:45 2026 GMT

证书本身没有 OCSP 地址时,OCSP responses: no responses sent 是正常的,2025 年以后的 Let's Encrypt 证书都属于这种情况。老版本 OpenSSL 显示的是单数形式 OCSP response: no response sent。详见 OCSP 与 CRL 命令。

出示客户端证书(双向 TLS)

服务器要求客户端证书时,用 -cert、-key 传入,中间证书用 -cert_chain。

openssl s_client -connect api.example.com:443 -servername api.example.com \
  -cert client.crt -key client.key -cert_chain intermediate.pem -brief < /dev/null
Acceptable client certificate CA names
C=CN, O=Example Corp, CN=Example Intermediate CA
C=CN, O=Example Corp, CN=Example Root CA
...
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE

完整输出里的 Acceptable client certificate CA names 列出服务器接受哪些 CA 签发的客户端证书。不带证书时,TLS 1.3 服务器会在握手后报 tlsv13 alert certificate required ... SSL alert number 116;证书不是它认可的 CA 签发的,则是 tlsv1 alert unknown ca ... SSL alert number 48。原理见 mTLS。

测试会话复用

用 -sess_out 保存会话,再用 -sess_in 重连,输出 Reused 即复用成功。

sleep 1 | openssl s_client -connect www.example.com:443 -sess_out sess.pem 2>/dev/null | grep -E '^(New|Reused)'
sleep 1 | openssl s_client -connect www.example.com:443 -sess_in sess.pem 2>/dev/null | grep -E '^(New|Reused)'
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Reused, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

TLS 1.3 的票据在握手之后才到,sleep 1 让连接多停留一秒好收下它。-reconnect(用同一会话重连 5 次)在 TLS 1.2 下很好用,在 TLS 1.3 下却常常每次都显示 New,原因相同。参见会话复用。

通过 HTTP 代理连接

-proxy host:port 通过 HTTP CONNECT 代理建立隧道,常见于企业内网出口。

openssl s_client -proxy proxy.example.com:3128 -connect www.example.com:443 \
  -servername www.example.com -brief < /dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=www.example.com

需要认证的代理加 -proxy_user 和 -proxy_pass。如果对端证书来自你不认识的企业 CA,说明代理在做 TLS 解密审计,内网里出现未知签发机构错误往往就是这个原因。

逐条查看握手消息

-msg 列出每条握手消息,-trace 逐字段解码,-keylogfile 导出 Wireshark 可用的密钥。

openssl s_client -connect www.example.com:443 -msg < /dev/null 2>&1 | grep -E '^(>>>|<<<)'
openssl s_client -connect www.example.com:443 -keylogfile keys.log < /dev/null
>>> TLS 1.3, Handshake [length 0608], ClientHello
<<< TLS 1.3, Handshake [length 04ba], ServerHello
<<< TLS 1.3, Handshake [length 000a], EncryptedExtensions
<<< TLS 1.3, Handshake [length 0d7d], Certificate
<<< TLS 1.3, Handshake [length 004f], CertificateVerify
<<< TLS 1.3, Handshake [length 0034], Finished

这里的 ClientHello 有 1544 字节(0x608),因为它带了 1216 字节的 X25519MLKEM768 密钥份额。-trace 需要编译时启用 trace 支持(Homebrew 版已启用),能看到 key_share、server_name 等每个扩展。keys.log 和私钥一样敏感,用完就删。每条消息的作用见 TLS 握手过程。

常见问题

为什么 s_client 看到的证书和浏览器不一样?

通常是 SNI 的原因。浏览器总会发送域名,而 s_client 只有在 -connect 后面写域名或者加了 -servername 时才发。直接连 IP 又不加 -servername,很多服务器和 CDN 会返回默认证书。加上 -servername www.example.com 再对比一次。

s_client 连上之后一直卡住不退出怎么办?

不要给它交互输入:命令末尾加 < /dev/null,或者用 echo | 管道;OpenSSL 3.2 及以上也可以加 -no-interactive。注意 -quiet 会自动打开 -ign_eof,会一直等服务器先断开。

s_client 会校验域名吗?

默认不会。它校验证书链,但不校验主机名,除非加 -verify_hostname www.example.com。想让它像真正的客户端一样遇错即停,再加 -verify_return_error。

资料来源

  1. OpenSSL 手册:openssl-s_client
  2. OpenSSL 3.0 手册:openssl-s_client
  3. RFC 6066:TLS 扩展(SNI、status_request)
  4. RFC 3207:SMTP STARTTLS 扩展
  5. RFC 7301:TLS ALPN 扩展