容器环境下https请求诡异错误修复一例

某生产环境Docker 17.03.1 + Rancher 1.x
因为发现直接通过公网调用某度api错误率 15% 以上,故做了一个到百度云的隧道,通过squid代理 http/https 请求进行中转,中转后错误率降低到 2‰ 左右,出错原因仍然很诡异,python requests报错:

Caused by ProxyError('Cannot connect to proxy.', timeout('The handshake operation timed out',)

心想既然隧道都挖了,何不直接走隧道直接发http/https请求呢?
一顿转发配置以后,请求顺利过去了,然而问题又来了:

# curl -v https://api.baidu.com
* Rebuilt URL to: https://api.baidu.com/
*   Trying 123.125.115.11...
* Connected to api.baidu.com (123.125.115.11) port 443 (#0)
* found 148 certificates in /etc/ssl/certs/ca-certificates.crt
* found 592 certificates in /etc/ssl/certs
* ALPN, offering http/1.1
* gnutls_handshake() failed: Error in the pull function.
* Closing connection 0
curl: (35) gnutls_handshake() failed: Error in the pull function.

放狗一搜得到各种千奇百怪的答案,关注到一篇 https://meta.discourse.org/t/fresh-install-failing-gnutls-handshake-failed-error-in-the-pull-function/49867 说因为MTU问题导致请求无法被正确处理,仔细一看,现在的docker接口MTU是1500,似乎没有问题啊。转念一想,因为访问百度走的是一个隧道接口,MTU只有1300,会不会是因为没有自动整MTU导致拆包拆傻了?尝试

iptables -t mangle -I POSTROUTING -o TUNNEL_INTERFACE -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

后解决问题!
然而百度2‰的错误率好像还是存在,这个锅我不背了…

关于MTU和MSS

TCP在建立连接时会协商MSS(Maximum Segment Size,最大分段大小)值,即MTU(Maximum Transmission Unit,最大传输单元)。为达到最佳的传输性能,就是每次多带点东西嘛,当然希望这值越大越好,当然这是不现实的,一般IEEE 802.3标准以太网的MTU最大为1500;当然,千兆的二般网络打开巨帧(Jumbo Frame)支持后可以达到9000+。

从家用宽带的PPPoE拨号开始说起,因为PPPoE连接中PPP包头占用8字节,所以MTU为1492字节,再减去IP数据包包头20字节和TCP数据包头20字节,MSS为1452字节。其他PPTP、L2TP等协议因为各种原因,MTU会被减少到1200~1300,这个值不一定固定,但是如果遇到了过大的包进入较小的管道,就像把15寸的电脑装机15寸包可能正好还有余,但是装机12寸内胆包就塞不下了。此时不管是iptables还是路由器,都会提供一项功能:为目标端口调整MTU的大小以避免产生错误。
因为隧道的特殊性,MTU常常会远低于1500,所以Clamp TCP MSS变得非常重要。
另外例如上海电信精品网,因为多地址出口的技术实现原因,MTU只有1442,如果路由器不协商且不设置为小于等于1442的MTU可能导致PPPoE直接无法连接。同时对PPTP、L2TP和IPSec等产生隧道连接都同理需要clamp。