摘要

  • ALPN 让客户端按顺序提出不透明协议字节,再由服务器从双方共同支持的集合中选出一项。该选择只管这条 TLS 连接上的应用数据,不能直接升级为主机、源站或集群属性。
  • TLS 终止点同时拥有下游服务器和上游客户端两个角色。下游使用 HTTP/2、上游使用 HTTP/1.1 完全可以同时成立;可靠证据必须逐段保存协商和应用启动结果。
  • 注册表、配置项和已安装回调都只是候选状态。运行证明来自具体连接上的 offer、selection、告警、回调调用、握手完成和协议首帧;证书身份与业务授权仍是另外两次判定。

一条真实记录如何变成了错误资产属性

事件现场的第一段链路很干净。ClientHello 按顺序携带 h2、http/1.1;服务器在 TLS 1.3 的 EncryptedExtensions 中返回 h2;Finished 把握手记录纳入认证;随后双方发送 HTTP/2 connection preface 与 SETTINGS。边缘节点确实在这一条连接上提供了 HTTP/2。

但边缘节点并未把这条加密连接原样送到源站。它读取请求后,另开 TCP 和 TLS,会使用不同证书、不同策略代际和另一组密钥。上游连接可能只得到 http/1.1,也可能根本没有 ALPN,代理再按明确规则回退到 HTTP/1.1。

监控系统省略了两个连接 ID 和中间的翻译事件,只把 selected=h2 连接到“源站服务”这一行。于是“边缘与浏览器在连接 A 上使用 h2”被改写成“源站集群支持 h2”。从强而窄的运行事实,变成弱而宽的拓扑推测。

正确修复不是降低 ALPN 的可信度,而是恢复它的作用域。它的价值正在于边界清楚:哪两个端点,在哪次握手中,就哪组精确字节达成一致,并在同一连接上开始说对应协议。

协商对象是字节,不是方便展示的名称

IANA 把扩展值 16 分配给 application_layer_protocol_negotiation。RFC 7301 规定,客户端发送一个协议名称列表;其中每一项都是带一字节长度前缀、长度为 1 到 255 的不透明字节串。空项与截断项无效。

“不透明”意味着观察系统不能擅自去空格、统一大小写或模糊匹配。TLS 上的 HTTP/2 标识正好是两个字节 h2。RFC 9113 还明确禁止客户端在 TLS ALPN 中发送、服务器选择 h2c。看起来相近的字符串没有相同权力。

客户端顺序表达偏好,却不独占最后选择。服务器要在交集中按自己的偏好挑一项。因此,“客户端把 h2 放在第一位”不是 h2 已选中的证据;“配置里包含 h2”也不是该连接真正执行了那份配置的证据。

如果没有交集,RFC 7301 要求服务器发出致命 no_application_protocol。RFC 9325 再次强调严格处理,因为接受客户端没有提出的协议,会打开跨协议混淆空间。无交集不是选择任意默认值的许可。

同一连接内权威明确,连接之外没有继承

一旦协商成功,选择结果对该连接是决定性的。服务器不能先选 h2,然后在同一字节流上按 HTTP/1.1 解析。HTTP/2 还在 TLS 完成后增加双方 connection preface;SETTINGS 的出现为“应用协议确实启动”提供了独立证据。

这形成一条证据阶梯:ClientHello 证明提出过什么;EncryptedExtensions 证明服务器返回什么;Finished 证明握手记录受到认证;preface 和解析器证明应用协议已经运行;成功的请求与响应再证明业务交换发生。

每一级都不能向上借权。只抓到 ClientHello,不能声称协商完成;握手完成却没有 HTTP/2 preface,不能声称 h2 已正常运行;某个实例成功一次,也不能证明下一条连接或另一个实例仍然支持。

RFC 9113 直接提醒,过去支持 HTTP/2 不是未来连接的强信号,因为配置、集群实例和网络条件会变化。因此,连接粒度不是日志设计上的麻烦,而是规范本身所要求的归属粒度。

终止代理必须保存两份账

代理面对浏览器时是 TLS 服务器,面对启用 TLS 的源站时又是 TLS 客户端。两种角色分别产生 offer、selection、告警和应用启动结果。它可以保持协议,可以转换,也可以在上游无法协商时回退。

Envoy 的官方上游协议选项正好说明这道边界:可以明确沿用下游协议,也可以对上游独立使用 ALPN 在 HTTP/1.1 与 HTTP/2 之间选择;上游没有 ALPN 时还能按配置回退 HTTP/1.1。“沿用下游”本身就是一项必须配置且必须执行的策略,不是所有代理天然具备的事实。

真正的隧道则不同。中间设备若只转发字节,可能根本看不到内层握手结果。RFC 7639 定义的 ALPN HTTP 头可以在 CONNECT 中表达准备在隧道里使用的协议,但“准备使用”仍不是内层握手已完成。

因此,每个终止点要分别保留:下游连接 ID、客户端 offer、边缘 selection、TLS 结果、应用 preface;上游连接 ID、边缘 offer、源站 selection、TLS 结果、应用 preface。中间用“终止并翻译”事件关联,绝不能合并成一条结果。

配置存在与运行回调之间还有距离

OpenSSL 把这段距离暴露得很具体。客户端用长度前缀列表配置 ALPN;长度设为零会清空列表,不再发送扩展。服务器安装选择回调;如果 ClientHello 没有 ALPN,回调就不会运行。

服务器名称回调先执行,而且可以替换本连接的 SSL_CTX;随后 ALPN 回调才在新上下文下运行。只盘点一份全局配置,可能完全错过 SNI 选中的虚拟主机策略。

回调输出必须来自客户端列表中的一项。SSL_get0_alpn_selected 返回的是带明确长度、非 NUL 结尾、由库持有的内存。若日志把它当普通 C 字符串,可能截断、越界读取或改写字节;这些都会破坏协议证据。

还有一个很适合做负向测试的陷阱:SSL_select_next_proto 在双方没有交集时,仍把客户端第一项放进输出,同时返回 OPENSSL_NPN_NO_OVERLAP。OpenSSL 明确要求 ALPN 路径忽略此时的输出。代码若先写日志、后检查返回码,就会制造一次从未发生的协商。

OpenSSL 也区分致命无交集与 NOACK:后者表示本连接没有选择协议,例如没有配置。没有 offer、编码错误、没有交集、策略不选择、回调失败、成功选中,必须是六种不同结果,不能压成一个 alpn=false。

恢复会话也要重新划定连接边界

RFC 7301 明确说 ALPN 属于连接,不属于会话。使用会话票据或恢复握手时,旧连接的扩展内容不再有效;新握手里的值决定新连接。

这意味着,曾在启用 h2 时签发的 ticket 不能让以后连接自动拥有 h2。新的边缘实例、SNI 策略或发布代际都可能改变结果。指标必须从新握手取得 selection,而不是从票据或前一次日志复制。

TLS 1.3 的 0-RTT 不是例外,反而约束更严。PSK 关联着早期数据参数,其中包括 ALPN。服务器只有在新选择与 PSK 关联的 ALPN 相同情况下才能接受早期数据。如果早期数据被拒绝,而最终连接选择了另一协议,应用可能要重新构造消息。

RFC 9846 因此禁止 TLS 实现自动重发早期数据,除非最终 ALPN 相同。不同协议的帧结构、语义和可重放动作可能不同;是否重建、重发或放弃,只能由理解业务语义的应用决定。

ALPN 不负责证明身份,也不负责批准动作

ALPN 决定使用哪个应用协议,证书路径与服务身份匹配则决定对端是否为目标服务。HTTP/3 把两者写得很清楚:通常在 QUIC 上通过 TLS 选择 h3,客户端仍必须验证证书是否匹配 URI 的源站。h3 选中而证书不合格时,协议选择是真实的,源站权威却没有建立。

即使身份正确,业务权限仍要另判。已认证的客户端或服务可以被拒绝访问某个租户、路由、方法或资源。因此生产记录至少要保留三个命名结果:ALPN、服务身份、应用授权,并分别绑定各自策略版本。

IANA 注册也不能替代运行证据。注册表回答“这些字节的公共含义是什么”,不回答“哪台机器何时真正使用”。源代码支持、构建特性、配置对象和库存标签都只能解释连接结果,不能代替连接结果。

用负向测试建立诚实的协议台账

让客户端按 h2,http/1.1 提出,服务器偏好 http/1.1,验证监控保存实际 selection,而不是假设客户端第一项获胜。尝试选择未被提出的值,确认客户端终止握手。

发送不含 ALPN 的 ClientHello,确认 OpenSSL 回调没有被调用;再制造无交集并捕获 no_application_protocol;最后在明确兼容策略下返回 NOACK,确保三者不混淆。

用无交集列表触发 SSL_select_next_proto 的输出陷阱,证明系统检查返回码后忽略伪输出。让 SNI 回调切换 SSL_CTX,记录后续 ALPN 到底使用哪个代际。

在代理上分别构造下游 h2、上游 HTTP/1.1,以及下游 h2、上游无 ALPN 后回退 HTTP/1.1 两种场景。两份连接证据都应完整,源站资产行不得继承下游结果。

恢复连接时改变策略,确认旧 selection 不被复用。让 0-RTT 关联一个 ALPN、最终选择另一个,确认早期数据被拒绝且 TLS 不自动重发。最后分别制造 ALPN 成功但身份失败、ALPN 与身份成功但授权失败。一个可靠系统会把它们呈现为分层结果,而不是相互冲突的绿灯。

来源