nginx 1.31.0 拒收 h2/h3 里的 Connection、Keep-Alive:先排查前面那台 1.1 网关
nginx.org 的 CHANGES 记录 1.31.0(2026-05-13)已经发布。这一版里最容易被忽略、但线上影响最直接的一条,是对 HTTP/2、HTTP/3 请求头的强制校验。
官方变更记录:nginx.org/en/CHANGES | nginx.org/2026.html
1.31.0 改了什么
原文变更条目:
- Security:使用
proxy_set_body指令时存在安全问题(CVE-2026-42926)。CHANGES 中该段描述不完整,受影响范围与修复版本以 nginx 安全公告为准。 - Change:nginx 现在会拒绝携带
Connection、Proxy-Connection、Keep-Alive、Transfer-Encoding、Upgrade头行的 HTTP/2 与 HTTP/3 请求,以及TE值不是trailers的请求。 - Change:
ngx_http_dav_module拒绝源与目标相同、或两者存在父子集合关系的 COPY / MOVE 请求。 - Change:
invalid alert、record layer failure以及SSL alert number N(任意 alert 编号)的日志级别从crit降为info。 - Change:
sticky模块可用--without-http_upstream_sticky_module关闭;旧的--without-http_upstream_sticky已弃用。 - Feature:新增
ngx_http_tunnel_module;代理认证支持auth_basic、satisfy、auth_delay。 - Feature:
upstream块内支持least_time指令。 - Feature:stream 模块支持
proxy_ssl_alpn。 - Bugfix:使用
proxy_set_body或proxy_pass_request_body时,到 HTTP/2 后端的连接可能不被缓存。 - Bugfix:代理 HTTP/0.9、SCGI、uWSGI 响应时,如果首行未被完整读取,响应可能传输错误。
以前能通过、现在会失败的原因
HTTP/1.1 里,Connection、Keep-Alive、Transfer-Encoding、Upgrade、TE、Proxy-Connection 都是逐跳(hop-by-hop)头,只对当前这一跳有意义。Connection: keep-alive 决定这条 TCP 连接怎么复用,Transfer-Encoding: chunked 描述这一段报文怎么分帧。到了 HTTP/2,这些语义全部由帧层承担:连接复用靠 SETTINGS,消息边界靠 END_STREAM 和 DATA 帧长度,协议升级靠 ALPN 在握手阶段完成。
RFC 9113 §8.2.2 因此直接规定:Connection、Proxy-Connection、Keep-Alive、Transfer-Encoding、Upgrade 出现在 HTTP/2 请求里即视为 malformed,TE 只允许取值 trailers。RFC 9114 对 HTTP/3 沿用同一规则。
1.31.0 之前,nginx 对 h2/h3 请求里的这些字段没有做这项强制校验:HEADERS 帧照常解析,字段被忽略,或者通过 $http_* 变量暴露给配置,请求继续进入 location 处理。所以一个把 HTTP/1.1 header 表原样塞进 h2 的客户端,跑了很久也不会报错,问题是潜伏的。
1.31.0 起,按 RFC 判为 malformed 的请求会被直接拒绝。客户端侧通常表现为 RST_STREAM,或者收到 4xx 响应;对应的 error_log 文案、确切的响应码与是否可配置,需要看安全公告和实测结果。
三类高概率会触发的链路
把 1.1 头表直接复用到 h2 的自研 SDK / RPC 框架。 典型写法是维护一份默认 header map(Connection: keep-alive、Keep-Alive: timeout=60、需要上传时再加 Transfer-Encoding: chunked),换协议时只换了传输层,header 构造逻辑没动。这类客户端此前一直“能跑”,升级后会被拦截。
前面还有 HTTP/1.1 网关的链路。 边缘网关收 1.1,回源时用 h2,转发时把客户端原始 header 直接透传,只在末尾追加几个自定义头。这是最常见的一种:出问题的不是终端客户端,而是中间那一跳。nginx 自己在 1.29.7 起默认不再向上游发送 Connection 代理头,但自研网关、老版本封装未必做了同样的清理。
在已建立的 h2 连接里再发 Upgrade: h2c,或发送 TE: gzip 之类的客户端。 前者是 h2 里套用 HTTP/1.1 升级语义,后者是 TE 取值不是 trailers,两者都落在新校验范围里。
具体到某个语言或某个库是否受影响,用自己的流量判断更可靠,不要依据版本号猜测。
复现与排查
先确认入站 h2/h3 请求里到底带了什么。抓包看 HEADERS 帧最直接:
tcpdump -i any -s 0 -w h2.pcap 'tcp port 443'
在 Wireshark 里按 http2.headers 过滤,逐条检查是否含上述六个字段。
用 curl 构造请求时要注意,部分版本在 h2 模式下会屏蔽或改写 Connection 一类头,构造出来未必是你要验证的报文:
curl --http2 -sv \
-H 'Connection: keep-alive' \
-H 'Keep-Alive: timeout=5' \
https://example.com/ -o /dev/null
发不出去就用 nghttp2 的客户端,或者直接拿业务 SDK 打预发环境。抓包工具的 header 构造行为以各自文档为准。
预发环境把日志开到 info,方便对齐时间点:
error_log /var/log/nginx/error.log info;
如果链路上 nginx 自己是回源方,检查历史配置里有没有显式写过代理头:
location /api/ {
proxy_pass https://backend;
# 1.29.7 起 nginx 默认不再向上游发 Connection;
# 历史配置若显式设过,这里清掉
proxy_set_header Connection "";
}
升级检查清单
- 抓一遍现网 h2/h3 入站流量,统计带
Connection、Proxy-Connection、Keep-Alive、Transfer-Encoding、Upgrade、非trailers的TE的请求占比,以及来源 IP / UA。 - 逐个检查 HTTP/1.1 → h2 的网关转发实现,确认它是否透传原始 header。
- 检查自研 SDK 的 header 构造逻辑,特别是“默认 header 表 + 复用”的写法。
- 灰度时先上边缘层,观察被拒请求的 error_log 与客户端错误率。
- 业务侧若有依赖
$http_connection、$http_keep_alive取值的配置,确认新版本下的行为。 - DAV 的 COPY / MOVE 自动化脚本:同源复制、父子目录整体移动会被拒,提前改脚本。
- 日志告警规则:SSL alert 与 record layer failure 从
crit降到info,原先挂在 crit 上的告警会静默失效。 - 打包脚本里用到
--without-http_upstream_sticky的,换成--without-http_upstream_sticky_module。 - 涉及
proxy_set_body/proxy_pass_request_body且后端是 h2 的,验证连接缓存行为是否变化。 - 准备回滚预案,并确认 CVE-2026-42926 的修复细节后再决定上线窗口。
相邻版本背景
- 1.29.6(2026-03-10):sticky 会话保持开源;upstream 块支持
sticky指令,server 支持route/drain。 - 1.29.7(2026-03-24):
listen支持 multipath(MPTCP);upstream keepalive 默认启用,proxy_http_version默认 1.1,默认不再发送Connection代理头。 - 1.29.8(2026-04-07):
max_headers指令;OpenSSL 4.0 兼容。 - 1.30.0(2026-04-14):进入 stable 分支。
- 1.29.4(2025-12-09):proxy 模块支持 HTTP/2;ECH(
ssl_ech_file);chunked 编码里的裸 LF 视为错误(400)。
1.29.7 已经在客户端侧做了清理,1.31.0 把服务端侧的校验补齐,两条改动方向是一致的。链路上只要有一环还是老逻辑,就会在 1.31.0 上暴露出来。
链接
- 变更记录:
- 2026 年发布公告:
- 协议依据:RFC 9113 §8.2.2(HTTP/2 连接专用头字段),RFC 9114(HTTP/3)沿用同一规则
CVE-2026-42926 的触发条件、影响版本与是否有临时缓解手段,以 nginx 安全公告为准。