摘要:按照反代正常https网站的方法去反代cloudfalre节点,往往会收到502响应,这是由于没有配置SNI的缘故。

前言

最近在使用cdnplus[1]自建cdn,由于位于美国的cdn节点到位于德国的源站的线路较差,想到了源站 -> cloudflare -> cdnplus这种二层cdn架构,但是在配置完成后访问网站时,cdnplus却返回了502报错。经过查询错误日志,发现报错发生在cdnplus访问cloudflare时。

排查过程

起初,以为问题发生在cloudflare的配置上,开始使用curl对原因进行排查,随后发现了一个奇怪的现象。
1. 使用hosts强指ip到cf节点(104.26.6.250),随后curl https://blog.azlith.com,一切正常。
2. 使用curl -H "Host: blog.azlith.com" https://104.26.6.250,出现502报错,且报错和ssl握手有关。

通过这两个现象的对比,我意识到问题可能和SNI有关。这里摘录一段对SNI的解释[2]

在过去的 HTTP 时代,解决基于名称的主机同一 ip 地址上托管多个网站的问题并不难。当一个客户端请求某特定网站时,把请求的域名作为主机头(host)放在 http header 中,从而服务器根据域名可以知道把该请求引向哪个域名服务,并把匹配的网站传送给客户端。但是此方式到 https 就失效了,因为 SSL 在握手的过程中,不会有 host 信息,所以服务端通常返回配置中的第一个可用证书,这就导致不同虚拟主机上的服务不能使用不同证书(但在实际中,证书通常是与服务对应。)。

为了解决此问题,产生了 SNI,SNI 中文名为服务器名称指示,是对 SSL/TLS 协议的扩展,允许在单个 IP 地址上承载多个 SSL 证书。SNI 的实现方式是将 HTTP 头插入到 SSL 的握手中,提交请求的 Host 信息,使得服务器能够切换到正确的域并返回相应的正确证书。

为了验证猜想,使用curl --resolve blog.azlith.com:443:104.26.6.250 https://blog.azlith.com,发现没有返回502报错,于是判断问题的确和SNI有关。

问题解决

问题的解决是非常容易的,只需要nginx向后端传递SNI即可,可以通过向反代规则中添加如下条目来实现这一点:

proxy_ssl_name $host;
proxy_ssl_server_name on;

思考

一个很有意思的问题是,为何在反代我自己的https站点时没有出现这种情况?这两者的区别是什么?
一个猜测是,不传递SNI,一般不会导致ssl无法使用,只会导致拿到错误的证书,而nginx在反代后端站点时,会忽略证书不匹配的报错,因此即使不传递SNI,也不影响正常使用。
但如果后端服务器存在多个https站点,且开启了HTST,如果不传递SNI,则可能出现错误,这一猜想留待日后有时间时验证。