v2rayN 7.22.7 加了 VerifyPeerCertByName 支持,但迁移 allowInsecure 不该从“找一个新开关”开始。先看证书是不是公开 CA 签发、SAN 是否包含你填的 serverName。这两项正常,就不需要额外固定。只有名称分离、自签证书、私有 CA 这类配置,才轮到 verifyPeerCertByName 或 pinnedPeerCertSha256。
不看日志的排查都是玄学。这里的日志,指的是 Xray 核心里的 TLS 证书错误,不是 v2rayN 主窗口里泛泛的“运行失败”。
这篇只按三个来源口径写:github.com/2dust/v2rayN 的 7.22.7 发布说明,xtls.github.io 的 Xray TLS 配置文档,以及 github.com/XTLS/Xray-core 的 #5859 社区讨论。#5859 只当排错背景,不当官方承诺。
为什么不是直接把 allowInsecure 打回去?
allowInsecure 的意思很直白:客户端不验证远端 TLS 证书是否可信。Xray 的 TLS 文档已经把它标为 deprecated,并提示改用 pinnedPeerCertSha256。v2rayN 7.22.7 的发布说明也写了两件跟这篇直接相关的事:添加 VerifyPeerCertByName 支持,以及提醒 Xray 将在 2026.8.1 禁用 allowInsecure。
所以迁移目标不是“继续让错误消失”。迁移目标是让客户端知道自己正在连的证书是谁。
先把几类现象分清楚:
| 你看到的现象 | 更可能原因 | 先检查什么 |
|---|---|---|
关掉 allowInsecure 后提示 certificate verify failed | 证书链不被系统信任 | issuer、系统 CA、是否自签 |
| 日志里出现 hostname、SAN、peer cert name | serverName 和证书 SAN 不匹配 | serverName、verifyPeerCertByName、SAN |
| 同一配置在一台机器能用,另一台不行 | 系统 CA 或时间不同 | 系统时间、根证书包、v2rayN 内核版本 |
填了 verifyPeerCertByName 仍报错 | 证书链本身不可信 | 是否需要 pinnedPeerCertSha256 |
这张表的用法很简单:先修证书校验,再看协议字段。反过来排,会把一个 TLS 问题改成三四个混在一起的问题。
迁移路径怎么选?
默认路线只有一个:公开 CA 签发的真实证书。比如证书 SAN 里有 edge.example.com,你的 serverName 也填 edge.example.com,那就让系统 CA 正常验证,不要额外加 pin。
三种迁移选择可以这样判:
| 证书状态 | 默认动作 | 什么时候换方案 |
|---|---|---|
公开 CA 签发,SAN 包含 serverName | 删除 allowInsecure,保留正常 TLS 校验 | 不需要 verifyPeerCertByName 和 pin |
| 公开 CA 签发,但连接名和证书名有意分离 | 填 verifyPeerCertByName 为证书 SAN 里的域名 | 证书链不可信时,这个字段救不了 |
| 自签证书、私有 CA、临时证书 | 固定 pinnedPeerCertSha256,或把私有 CA 安装进受控设备 | 证书轮换时要同步更新指纹 |
verifyPeerCertByName 处理的是“用哪个名字验”。pinnedPeerCertSha256 处理的是“我只认这个证书指纹”。两者不是同一种东西。
VerifyPeerCertByName 到底校验什么?
Xray 的 TLS 文档把 verifyPeerCertByName 定义为客户端字段。它会覆盖 serverName 用于证书名称验证,多个域名可以用英文逗号分隔,只要证书 SAN 命中其中一个就算通过名称检查。
这句话里最容易漏掉的是“名称检查”。它不是 allowInsecure 的替身。
举个配置口径:
serverName:握手时给远端看的 SNI,也是常规证书校验会用到的名字。verifyPeerCertByName:证书校验时额外指定的名字,适合连接名和证书名分开的特殊部署。pinnedPeerCertSha256:远端证书的 SHA-256 指纹,Xray 文档要求十六进制格式。
如果证书是自签的,系统 CA 不认识它。你只写 verifyPeerCertByName: "localhost",名称可能对了,但信任链还是不对。discussion #5859 里用户遇到的 localhost 证书问题,正好能说明这个坑:日志里看到了证书名,不代表把这个名字填进去就会通过。
怎样用 OpenSSL 看 Subject、SAN 和 sha256?
先用真实连接地址和真实 SNI 抓证书。把下面的 HOST、PORT、SNI 改成你的值:
HOST="edge.example.com"
PORT="443"
SNI="edge.example.com"
openssl s_client \
-connect "${HOST}:${PORT}" \
-servername "${SNI}" \
-showcerts </dev/null 2>/dev/null \
| openssl x509 \
-noout \
-subject \
-issuer \
-ext subjectAltName \
-fingerprint \
-sha256
你要看四行:
subject=CN = edge.example.com
issuer=C = US, O = Example CA, CN = Example CA R3
X509v3 Subject Alternative Name:
DNS:edge.example.com, DNS:www.example.com
sha256 Fingerprint=E8:E2:D3:87:FD:BF:FE:B3:8E:9C:90:65:CF:30:A9:7E:E2:3C:0E:3D:32:EE:6F:78:FF:AE:40:96:6B:EF:CC:C9
verifyPeerCertByName 要从 SAN 里选,不要只看 subject=CN。现在证书名称校验主要看 Subject Alternative Name。SAN 没有你要的名字,优先重签证书。
如果要填 pinnedPeerCertSha256,Xray 文档接受 SHA-256 指纹的十六进制格式。可以把冒号去掉,转成小写,得到这样的值:
openssl s_client \
-connect "${HOST}:${PORT}" \
-servername "${SNI}" \
-showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -fingerprint -sha256 \
| sed 's/^.*=//; s/://g' \
| tr 'A-F' 'a-f'
输出应接近这种形状:
e8e2d387fdbffeb38e9c9065cf30a97ee23c0e3d32ee6f78ffae40966befccc9
别把这条命令用在错误的端口上。某些 Hysteria、Reality 或特殊传输并不是普通 HOST:443 的 TLS 握手,OpenSSL 看到的可能不是 Xray 实际使用的证书。这种情况要从服务端证书文件算指纹,或使用当前 Xray 工具链提供的 TLS ping/hash 能力。
Xray JSON 里怎么写?
下面是完整的 outbound 片段,只展示 TLS 相关字段。第一种是公开 CA 证书,证书 SAN 里有 origin.example.com,但连接时 serverName 需要填另一个名字的情况:
{
"outbounds": [
{
"tag": "proxy",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "edge.example.com",
"port": 443,
"users": [
{
"id": "00000000-0000-0000-0000-000000000000",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "tls",
"tlsSettings": {
"serverName": "edge.example.com",
"verifyPeerCertByName": "origin.example.com",
"fingerprint": "chrome"
}
}
}
]
}
第二种是固定证书指纹。这里不要再写 allowInsecure: true:
{
"outbounds": [
{
"tag": "proxy",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "edge.example.com",
"port": 443,
"users": [
{
"id": "00000000-0000-0000-0000-000000000000",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "tls",
"tlsSettings": {
"serverName": "edge.example.com",
"pinnedPeerCertSha256": "e8e2d387fdbffeb38e9c9065cf30a97ee23c0e3d32ee6f78ffae40966befccc9",
"fingerprint": "chrome"
}
}
}
]
}
如果你手里的配置是订阅生成的,先导出实际运行的 Xray JSON 再判断。订阅链接本身不是证据,核心真正读到的 tlsSettings 才是证据。
v2rayN 里改完怎么确认生效?
不要用“网页能打开”当唯一确认。这个信号太粗,DNS、路由、缓存和系统代理都会把结果搅在一起。
按下面顺序看:
- v2rayN 版本号是
7.22.7或更高。 - 导出的 Xray JSON 里没有
allowInsecure: true。 tlsSettings里只保留你选择的字段:真实 CA 路线不写额外字段;名称分离写verifyPeerCertByName;证书固定写pinnedPeerCertSha256。- 启动核心后,日志里不再出现
allowInsecure、certificate、unknown authority、peer cert invalid这类 TLS 错误。 - 只在 TLS 错误消失后,再看延迟测试、路由命中和应用连接。
如果第 4 步还失败,回到 OpenSSL 输出。把 SAN、issuer、sha256 指纹和 JSON 里的 serverName 摆在一起看,通常比反复导入配置快。
discussion #5859 能说明什么,不能说明什么?
Xray-core discussion #5859 是一个社区排错问题。用户的配置里有 allowInsecure,日志里出现证书名称和 verifyPeerCertByName、pinnedPeerCertSha256 相关错误,并追问特殊传输下怎么拿到原始证书。
它能说明两点:
verifyPeerCertByName不是万能开关,名称对了也可能因为证书链或指纹问题失败。- 特殊传输、混淆层、非标准端口会让“从远端抓证书”这件事变得不直观。
它不能说明两点:
- 不能把提问里的写法当成 Xray 官方推荐配置。
- 不能把“希望日志打印 sha256”当成已经承诺的功能。
社区问题适合当排错背景,不适合当迁移依据。真正该跟的是 v2rayN 发布说明和 Xray TLS 文档。
哪些旧配置别顺手保留?
迁移时最容易留下三个尾巴。
第一,allowInsecure: true。如果核心已经提示这个功能迁移到证书固定,就不要在同一份配置里继续保留它。你要么回到真实 CA 校验,要么明确 pin 证书指纹。
第二,把 serverName、verifyPeerCertByName 和 address 写成三个互不相干的域名。除非你知道证书为什么这样部署,否则先让三者一致,再逐个拆开。
第三,把 SHA-256 指纹当成永久不变。证书续签、重签、换 CA、中间证书调整,都可能让指纹变化。固定指纹后,证书轮换就变成一次配置变更,不是后台自动完成的事。
最后留一个简单的回滚线:保留迁移前的 JSON,但不要回滚到 allowInsecure: true 当长期方案。正确的回滚应该是恢复上一张有效证书、上一条匹配 SAN 的 serverName,或上一枚确认过的 pinnedPeerCertSha256。
同一类 TLS 排查可以接着看 Trojan TLS 证书域名。如果你还在核对客户端入口,先看 v2rayN 客户端;需要服务端配置背景,再看 Xray-core 部署指南。