HAProxy 区分流量特征
无意之中发现博客会间歇性无法访问,具体表现为部分请求长时间被 pengding ,一直处于 stalled 状态,复现条件不明确,时而出现时而正常。一开始还以为是这个评论中描述的因为OCSP 封装配置导致的问题,检查了一下配置和在服务器本地测试发现Nginx的配置并没有问题,经过日志分析将问题锁定到在 Nginx 前部的 Haproxy 上。
本博按照之前443端口共用的方案一文中提到的方法已经跑了几年了,以往都没有出现这种间歇性无法访问的问题。仔细回想一下几个月之前貌似手贱升级过组件,是不是之前升级后的 Haproxy 版本功能差异导致了这个问题呢?
尝试原方案
带着这个疑问进一步地进行了测试,目前 HA-Proxy version 2.0.7 ,较 1.X 时代的版本确实有不小的变化,但是整体来说并没有什么过多的变化。随后发现,不仅 HTTPS 服务会出现间歇性失败的问题,复用的数据同样也会,于是我将焦点放到了Haproxy的流量区分规则上。
listen https-in
bind :443
tcp-request inspect-delay 5s
acl is_ssl req_ssl_ver 3:4
tcp-request content accept if is_ssl
server server-https :4431 check send-proxy
use_backend ss-out if !is_ssl
acl is_ssl req_ssl_ver 3:4,之前是通过req_ssl_ver版本判断指令来确定流量是否属于 HTTPS 流量,然后再根据判断结果将非 HTTPS 流量转发到指定后端。
查阅资料发现 req_ssl_ver 已经废弃了,取而代之的是 req.ssl_ver 指令(注意 . 变成了 _)
req.ssl_ver : integer
req_ssl_ver : integer (deprecated)
Returns an integer value containing the version of the SSL/TLS protocol of a
stream present in the request buffer. Both SSLv2 hello messages and SSLv3
messages are supported. TLSv1 is announced as SSL version 3.1. The value is
composed of the major version multiplied by 65536, added to the minor
version. Note that this only applies to raw contents found in the request
buffer and not to contents deciphered via an SSL data layer, so this will not
work with "bind" lines having the "ssl" option. The ACL version of the test
matches against a decimal notation in the form MAJOR.MINOR (e.g. 3.1). This
fetch is mostly used in ACL.
但是修改重试之后发现问题依旧…… 说明在这一版本里,光靠识别 SSL/TLS 协议的版本已经不能很好(或者说可靠)地区分流量特征了。
协议特征区分流量
有没有长久可靠一点的方法来区分流量呢,经过一番折腾找到了 req.payload 指令:
req.payload(<offset>,<length>) : binary
This extracts a binary block of <length> bytes and starting at byte <offset>
in the request buffer. As a special case, if the <length> argument is zero,
the the whole buffer from <offset> to the end is extracted. This can be used
with ACLs in order to check for the presence of some content in a buffer at
any location.
ACL alternatives :
payload(<offset>,<length>) : hex binary match
req.payload 是用来对数据流进行查找和标记的指令,和 req.ssl_ver 一样都是对 Haproxy 收到的原始数据流进行分析。
因为 HTTPS 协议的数据包的头部数据都是完全相同的(16 03 01),如果协议不发生大变化的话这个特征一直会稳定下去,所以优化后的完整配置如下:
listen https-in
bind :443 tfo
tcp-request inspect-delay 2s
acl is_https req.payload(0,3) -m bin 160301
tcp-request content accept if is_https
use_backend ss-out if !is_https
retry-on empty-response conn-failure
server server-https :4431 send-proxy-v2 tfo
其中头尾的 tfo 和中间的 retry-on empty-response conn-failure 是为了开启 TCP Fast Open,非必须的。
再拓展一点
至此根据协议特征区分流量的目的已经达到,然而之前有一个网友留言问如何识别指定的特征,这其实就是自己提取和总结流量特征了。上面提到的协议特征是其他网友已经总结好了的,倘若你知道一个协议具有某些特征(例如 头部开头相同),或者是想故意制造某些特征(例如 某无特征流量 伪装成 HTTP/HTTPS 流量),可以按以下的方法自己尝试自己进行提取:
假设要提取特征的服务运行在1234端口上,在运行服务端执行:
nc -l -p 1234 | hexdump -C
观察协议的特征。
然后在客户端尝试连接 1234 端口,观察服务端 nc 的输出。
以ssh服务为例,客户端使用 PuTTY 得到的输出
53 53 48 2d 32 2e 30 2d 50 75 54 54 59 5f 52 65 |SSH-2.0-PuTTY_Re|
头部的 53 53 48 便是特征值(根据偏移量的设置,可以挑取更长的,或者全局的)。
同理 HTTPS 服务的特征值是 16 03 01 。
HTTP 由于有 POST/GET、DEL、PUT 等不同的方法,头部的特征也不尽相同。
因此这三种协议的识别特征如下:
#HTTPS
acl is_https req.payload(0,3) -m bin 160301
#GET POS(T) PUT DEL(ETE) OPT(IONS) HEA(D) CON(NECT) TRA(CE)
acl is_http req.payload(0,3) -m bin 474554 504f53 505554 44454c 4f5054 484541 434f4e 545241
#SSH
acl is_ssh req.payload(0,3) -m bin 535348
好了,根据协议特征区分流量的就结束了,但是这些还不能解决网友在评论中提出的区分“tls1.2_ticket_auth”问题,这里推荐使用神器 haproxy 的内置针对原始流量中 SNI 信息进行筛选的 req.ssl_snile 指令,根据自定义的 SNI 区分流量。
req.ssl_sni : string
req_ssl_sni : string (deprecated)
Returns a string containing the value of the Server Name TLS extension sent
by a client in a TLS stream passing through the request buffer if the buffer
contains data that parse as a complete SSL (v3 or superior) client hello
message. Note that this only applies to raw contents found in the request
buffer and not to contents deciphered via an SSL data layer, so this will not
work with "bind" lines having the "ssl" option. SNI normally contains the
name of the host the client tries to connect to (for recent browsers). SNI is
useful for allowing or denying access to certain hosts when SSL/TLS is used
by the client. This test was designed to be used with TCP request content
inspection. If content switching is needed, it is recommended to first wait
for a complete client hello (type 1), like in the example below. See also
"ssl_fc_sni".
ACL derivatives :
req_ssl_sni : exact string match