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

> openssl s_client 实用命令：-servername 与 SNI、-showcerts、指定 TLS 1.2/1.3、ALPN、邮件和数据库的 STARTTLS、OCSP 装订、双向 TLS 与会话复用。

Source: https://ssl.sh/zh/cookbook/test-connections/
Last updated: 2026-11-10

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

## 一屏看完连接概况

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

```bash
openssl s_client -connect www.example.com:443 -servername www.example.com -brief < /dev/null
```

```text
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`。

```bash
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
```

```text
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](https://ssl.sh/zh/glossary/sni/)。

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

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

```bash
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
```

```text
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 格式打印服务器发来的每一张证书。

```bash
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts < /dev/null
```

```text
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:`。只有一张证书，说明缺了中间证书，处理方法见[验证证书链](https://ssl.sh/zh/cookbook/verify-chains/)。

## 强制使用 TLS 1.2 或 TLS 1.3

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

```bash
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
```

```text
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 还要降低安全级别，方法见[密码套件与协议版本](https://ssl.sh/zh/cookbook/ciphers-and-protocols/)。

## 检查 ALPN 与 HTTP/2

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

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

```text
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 一个空的标准输入，它就会在握手完成后退出，而不是等你输入。

```bash
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+
```

```text
...
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 握手。

```bash
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
```

```text
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 检测工具](https://ssl.sh/zh/tools/starttls-checker/)从外部测试。

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

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

```bash
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
```

```text
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 响应，收到的话会打印出来。

```bash
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'
```

```text
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 命令](https://ssl.sh/zh/cookbook/ocsp-crl/)。

## 出示客户端证书（双向 TLS）

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

```bash
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
```

```text
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](https://ssl.sh/zh/learn/mtls/)。

## 测试会话复用

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

```bash
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)'
```

```text
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`，原因相同。参见[会话复用](https://ssl.sh/zh/glossary/session-resumption/)。

## 通过 HTTP 代理连接

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

```bash
openssl s_client -proxy proxy.example.com:3128 -connect www.example.com:443 \
  -servername www.example.com -brief < /dev/null
```

```text
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=www.example.com
```

需要认证的代理加 `-proxy_user` 和 `-proxy_pass`。如果对端证书来自你不认识的企业 CA，说明代理在做 TLS 解密审计，内网里出现[未知签发机构错误](https://ssl.sh/zh/errors/x509-certificate-signed-by-unknown-authority/)往往就是这个原因。

## 逐条查看握手消息

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

```bash
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
```

```text
>>> 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 握手过程](https://ssl.sh/zh/learn/tls-handshake/)。
