文/杰瑞曲
最近有几个朋友遇到了打开HTTP/2后无法访问网站的问题。有些网站只是火狐无法访问,请求可以中止;通过控制台网络面板;有些网站Firefox无法访问,甚至Chrome也会跳转到错误页面,错误代码为“err_spdy_impulsive_transport_security”。奇怪的是,只要去掉对HTTP/2的支持(比如去掉Nginxlisten配置中的http2),就万事大吉了。也就是说,无法访问的现象只存在于HTTPS+HTTP/2的组合中,单独提供HTTPS服务就不错了。
这个问题有意思。本文不仅会告诉你如何解决,还会帮你找出问题的前因后果。如果你只关心结论,就看最后的总结。
首先,网站无法访问的可能性有很多。一般来说,你应该从基本项目开始检查:
如果可以建立TCP连接,则至少Web服务器正在运行,并且Web服务器的本地网络正常。如果仍有问题,我们应该开始排除应用层故障,例如:
对于HTTPS网站,在HTTP和TCP之间还有一层TLS。浏览器在发送HTTP消息之前,要和服务器建立TLS连接,非常复杂,容易出问题。例如:
关于部署HTTPS时的一些注意事项,可以参考我之前的文章《关于启用HTTPS的一些经验分享(二)》,这不是本文的重点,就不讨论了。
总之,前面列出的这么多可能性,基本都和本文要解决的问题无关!如果响应被延迟,或者证书是非法的,就不可能访问它。没有理由不启用HTTP/2。
其实Chrome的错误代码“err_spdy_impulsive_transport_security”已经给出了两个提示:
从Wireshark的数据包捕获中可以看到,在这种情况下,浏览器在TLS握手过程中发送了“加密警报”,然后主动断开TCP。TLS连接未成功建立,因此无法访问该页面。
之前看HTTP/2RFC的时候,了解到HTTP/2协议中对TLS有更严格的限制:比如HTTP/2中只能使用TLSv1.2+,禁用了上百个CipherSuite(详见:TLS1.2CipherSuite黑名单)。此时,您可以确定该错误的原因是服务器上没有启用TLSv1.2,或者CipherSuite的配置有问题。在这种情况下,服务器支持TLSv1.2,只有后者可能会有问题。
CipherSuite,即加密套件,在整个TLS协议中至关重要。
建立TLS连接时,浏览器需要在客户端Hello握手中提供其支持的CipherSuite列表和应用协议列表(由TLSALPN扩展),服务器通过服务器Hello握手返回选择的CipherSuite和应用协议。如果服务器选择的应用协议是HTTP/2,浏览器需要检查CipherSuite是否在HTTP/2的黑名单中,如果存在就终止TLS握手。
当然,如果浏览器本身不支持HTTP/2,客户端Hello握手中的ALPN扩展就不会包含h2(实际上ALPN扩展可能不存在),服务器也不会选择HTTP/2作为后续的应用协议。其实这个过程就是HTTP/2协议协商机制。
HTTP/2对CipherSuite有更严格的限制。用于传输HTTP/1.1加密流量的CipherSuite不能用于传输HTTP/2加密流量。这也导致HTTPS网站以前工作得很好。启用HTTP/2后,可能无法通过HTTP/2访问,因为CipherSuite被禁用。
了解了原理之后,我们来看一个具体案例(注:此案例来源于本博客用户的评论,via):
在Nginx中配置以下CipherSuite并启用HTTP/2,在最新的Firefox中无法访问:
SSL_ciphersECDhe-RSA-chacha20-poly1305:ECDhe-RSA-AES256-GCM-SHA384:ECDhe-RSA-AES256-SHA384:ECDhe-RSA-AES256-SHA:ECDhe-RSA-AES128-GCM-SHA256:ECDhe-RSA-AES128-SHA256:ECDhe-RSA-AES128-SHA:AES256-GCM-SHA阿努尔:伊努尔!导出:!山茶花:!德斯:!MD5:!PSK:!RC4;
注:以上配置中的CHACHA20/POLY1305是Google开发的。以前需要LibreSSL、BoringSSL或者CloudFlare的OpenSSL补丁来支持,最新版本的OpenSSL已经内置了对它的支持。
我们来看看上面配置中指定的具体CipherSuite(注意:下面命令中的openssl版本是libresl2.3.1):
OpenSSLciphers-V'ECDhe-RSA-chacha20-poly1305:ECDhe-RSA-AES256-GCM-SHA384:ECDhe-RSA-AES256-SHA:ECDhe-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDhe-RSA-AES128-SHA:AES256-SHA阿努尔:伊努尔!导出:!山茶花:!德斯:!MD5:!PSK:!RC4“|column-t”
运行结果如下:
0xCC,0x13-ECDHE-RSA-CHACHA20-POLY1305TLSv1.2Kx=ECDHAu=RSAEnc=ChaCha20-Poly1305Mac=AEAD 0xC0,0x30-ECDHE-RSA-AES256-GCM-SHA384TLSv1.2Kx=ECDHAu=RSAEnc=AESGCM(256)
Mac=AEAD 0xC0,0x28-ECDHE-RSA-AES256-SHA384TLSv1.2Kx=ECDHAu=RSAEnc=AES(256)
Mac=SHA384 0xC0,0x14-ECDHE-RSA-AES256-SHASSLv3Kx=ECDHAu=RSAEnc=AES(256)
Mac=SHA1 0xC0,0x2F-ECDHE-RSA-AES128-GCM-SHA256TLSv1.2Kx=ECDHAu=RSAEnc=AESGCM(128)
Mac=AEAD 0xC0,0x27-ECDHE-RSA-AES128-SHA256TLSv1.2Kx=ECDHAu=RSAEnc=AES(128)
Mac=SHA256 0xC0,0x13-ECDHE-RSA-AES128-SHASSLv3Kx=ECDHAu=RSAEnc=AES(128)
Mac=SHA1 0x00,0x9D-AES256-GCM-SHA384TLSv1.2Kx=RSAAu=RSAEnc=AESGCM(256)
Mac=AEAD 0x00,0x3D-AES256-SHA256TLSv1.2Kx=RSAAu=RSAEnc=AES(256)
Mac=SHA256 0x00,0x35-AES256-SHASSLv3Kx=RSAAu=RSAEnc=AES(256)
Mac=SHA1 0x00,0x9C-AES128-GCM-SHA256TLSv1.2Kx=RSAAu=RSAEnc=AESGCM(128)
Mac=AEAD 0x00,0x3C-AES128-SHA256TLSv1.2Kx=RSAAu=RSAEnc=AES(128)
Mac=SHA256 0x00,0x2F-AES128-SHASSLv3Kx=RSAAu=RSAEnc=AES(128)
Mac=SHA1 0xC0,0x12-ECDHE-RSA-DES-CBC3-SHASSLv3Kx=ECDHAu=RSAEnc=3DES(168)
Mac=SHA1 0x00,0x0A-DES-CBC3-SHASSLv3Kx=RSAAu=RSAEnc=3DES(168)
Mac=SHA1
然后通过Wireshark获取客户端Hello中Firefox发送的CipherSuite列表,如下:
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256(0xC0,0x2B) TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xC0,0x2F) TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA(0xC0,0x0A) TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA(0xC0,0x09) TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA(0xC0,0x13) TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA(0xC0,0x14) TLS_DHE_RSA_WITH_AES_128_CBC_SHA(0x00,0x33) TLS_DHE_RSA_WITH_AES_256_CBC_SHA(0x00,0x39) TLS_RSA_WITH_AES_128_CBC_SHA(0x00,0x2F) TLS_RSA_WITH_AES_256_CBC_SHA(0x00,0x35) TLS_RSA_WITH_3DES_EDE_CBC_SHA(0x00,0x0A)CipherSuite协商的目的是找出两端支持的Suite,也就是取出两者的交集:
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xC0,0x2F) TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA(0xC0,0x13) TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA(0xC0,0x14) TLS_RSA_WITH_AES_128_CBC_SHA(0x00,0x2F) TLS_RSA_WITH_AES_256_CBC_SHA(0x00,0x35) TLS_RSA_WITH_3DES_EDE_CBC_SHA(0x00,0x0A)乍一看,选择还挺多的,但是别忘了,HTTP/2协议中也禁止了上百种。移除此零件后,仅:
TLS_ECDhe_RSA_WITH_AES_128_GCM_sha256(0xc0,0x2F)
奇怪的是,至少有一个满足所有条件的套件。为什么还是握手失败?通过Wireshark查看ServerHello,你会发现在这种情况下,通过Firefox访问,服务器选择的包是0xC0,0x14,而不是0xC0,0x2F。
Nginx有一个SSL_prefer_server_cipher配置。如果设置为on,表示协商CipherSuite时,计算交集后,按照服务器配置的Suite列表顺序返回第一个,可以提高安全性。在ssl_ciphers配置中,0xc0和0x14位于0xc0和0x2f之前。当ssl_prefer_server_ciphers打开时,将选择被HTTP/2禁用的0xc0和0x14,导致最终的HTTPS+HTTP/2握手失败。
那为什么这个配置在Chrome里很正常?Chrome支持的CipherSuite如下,大家可以自己分析一下。
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256(0xC0,0x2B) TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xC0,0x2F) TLS_DHE_RSA_WITH_AES_128_GCM_SHA256(0x00,0x9E) TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256(0xCC,0x14) TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256(0xCC,0x13) TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA(0xC0,0x0A) TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA(0xC0,0x14) TLS_DHE_RSA_WITH_AES_256_CBC_SHA(0x00,0x39) TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA(0xC0,0x09) TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA(0xC0,0x13) TLS_DHE_RSA_WITH_AES_128_CBC_SHA(0x00,0x33) TLS_RSA_WITH_AES_128_GCM_SHA256(0x00,0x9C) TLS_RSA_WITH_AES_256_CBC_SHA(0x00,0x35) TLS_RSA_WITH_AES_128_CBC_SHA(0x00,0x2F) TLS_RSA_WITH_3DES_EDE_CBC_SHA(0x00,0x0A)根据这个案例,把Nginx配置中的0xc0,0x2f(ECDHE-RSA-AES128-GCM-SHA256)移到0xC0,0x14(ECDHE-RSA-AES256-SHA)可以解决最新Firefox下无法访问的问题。当然,正如我在之前的文章中多次强调的,配置TLS时一定要参考权威文档,比如Mozilla的推荐配置和CloudFlare使用的配置。经过测试,使用这两种配置的HTTPS站点在启用HTTP/2后没有问题。
简单总结一下,如果HTTPS网站启用了HTTP/2可以正常工作,请检查服务器的这两个配置:
1)是否TLSv1.2已启用;
2)密码套件的配置是否正确。
本文写到这里。您通常会遇到关于HTTP(S)和HTTP/2的问题。请给我留言或发邮件讨论。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)