终于解掉 nginx 在 SSL 下 400 的错误,蠢死了

本文发布已超过一年,其中的内容可能已经过时。

这个问题持续了得有超过一年了,症状很简单,配置了 https 访问之后,使用 IPv6 访问的时候会报 400 Bad Request 错误,强制使用 IPv4 或者强行指定 https 协议都能解决。说到这里应该很明显了对吧,首先就应该想到是不是 80 端口上也开着 ssl 来着。但这个讨厌的地方在于用 IPv4 就一切正常了,我一直没想那么多……就因为这我的博客很长时间都没办法强制 https 访问,不然 IPv6 过来的用户没法看。

前两天搞着搞着调另一个什么反向代理相关的设置的时候,顺手开了 debug 日志,然后就想顺便测一把,结果一不小心就找到了原因_(:зゝ∠)_

长话短说,问题的根本原因是普通 http 协议和加密的 https 协议配置在了一个 server{} 块内。并不是说不支持这样搞,而是这样配置过后服务器的行为会和分开配置成两个有所不同。

假如配置成如下样子,就可以复现出我遇到的这个问题:

其中的 spdy_detect 是 Tengine 的功能,用来自动判断链接是否为 SPDY 协议,原生 nginx 应该是不支持的。

在这个配置下使用 IPv6 访问 80 端口的时候就会出现 400 错误,看 debug 日志里面的报错是说普通 http 流量发到了 https 端口上,但特么的 80 就是 http 端口啊!所以这是服务器解析配置之后的行为有差异造成的。

当把 http 和 https 的监听语句分别写到两个不同的 server{} 块里面的时候,这两个虚拟主机相互独立,用这样的配置不会有问题。在配置到同一个虚拟主机的情况下,服务器会通过 listen 语句后面指定的不同标签判断对应的协议情况,比如加上 ssl 就意味着这是一个开启 https 的端口,不加就按照默认的 http 来。到这里为止至少在 IPv4 下服务器的行为还是符合预期的。

问题在于对 IPv6 的设置,下面的 ssl on; 会覆盖掉监听语句的配置,即将整个虚拟主机都开启 https 协议,所以这个时候在 [::]:80 上面监听的是一个 https 服务器,这特么…

于是解决方法也很简单了,去掉 ssl on; 这一行就搞定。所以说蠢死了我。

比较奇怪的是这个问题只在 IPv6 下存在,上面的配置放在 IPv4 环境中没有任何问题,当初这个配置也是不知道从哪里抄了一份一直反复修改用到现在的,从来没出过问题所以直觉想不到这里去,直到我尝试在 IPv6 上也配置 ssl 访问…

— UPDATE 2017-09-26 —
今天看到了这个issue, 里面具体解释了这一现象的原理。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注