Varnish 限制访问频率

Varnish 4 仍然没有内置的访问频率限制功能,要达到这一目的需要安装 vsthrottle VMOD。

这个 VMOD 可以从这里获取:https://github.com/varnish/libvmod-vsthrottle

可以使用源码目录中的 autogen.sh 利用 autotools 先生成 configure 脚本,然后再按照“./configure; make; make install”步骤安装好这个 VMOD(如果是使用官方 rpm 安装 Varnish 的话,还需要 varnish-libs-devel 这个包才能编译)。

安装好之后就可以在 vcl 配置里面使用它了。

vcl 4.0;

import vsthrottle;

sub vcl_recv {
        set client.identity = client.ip;
        if (vsthrottle.is_denied(client.identity, 15, 10s)) {
                # Client has exceeded 15 reqs per 10s
                return (synth(429, "Too Many Requests"));
        }
}

vsthrottle 的使用比较简单,只有一个“is_denied()”函数。函数原型为:

is_denied(STRING key, INT limit, DURATION period)

第一个 key 参数和 nginx 里面的 limit_req_zone 指令里面的 key 差不多,就是作为限制频率的依据。上面的配置里面我们按照客户端地址进行了访问限制,10s 内如果超过 15 次访问则给它返回 429 状态码——Too Many Requests。但是可以看到并没有直接将 client.ip 传递给 vsthrottle.is_denied,这是因为 key 参数必须是字符串类型,而 client.ip 则是个 IP 地址类型,只能将它先用 client.identity 保存起来后才能进行传递。key 的定义很灵活,我们可以把 req.http.User-Agent 等作为key,也可以利用类型转换,或者是 regsub 函数修改字符来作为 key。

Strict-Transport-Security HTTP 头造成的重定向

Strict-Transport-Security(HSTS) HTTP 头可以告诉浏览器使用强制的更高安全控制,比如当访问到的 SSL/TLS 证书异常时,浏览器会拒绝让用户添加例外(如下图,没有 HSTS 头的时候,Firefox 会有个让用户添加例外的选项,用户可以忽略这个证书异常,继续访问网站。如果是下图的情况,用户登录了这个伪装为 onedrive 的网站,恐怕后果就很严重了),或者是让浏览器以后都默认使用 TLS 协议访问这个网站,而不用再通过 80 端口去重定向(这个时候也是很容易被攻击的)。

但有时候 HSTS 头的不当使用也会造成问题,比如一些站点的莫名无法访问——明明在地址栏输入的是 http:// 但是却老是会自动跳转到 https://,这个时候我们往往会以为在服务器端有什么重定向,但是经过检查却没有!
经过检查,Chrome 或者 Firefox 都有此问题,但是 IE 却没有,所以可以怀疑是浏览器这头做的跳转。
其实就是 HSTS 的 includeSubDomains 这段区域造成的,这段参数使得浏览器在访问每个子站点都会走 TLS 协议

解决办法:服务器上面去掉这个参数,并且在浏览器中也需要清除这个记录,Chrome 可以访问 chrome://net-internal/#hsts 这个地址,查看和删除已经存在的站点,Firefox 则只能选择忘记整个站点的记录来清除。IE 目前还不支持 HSTS(Windows 10 会支持,但还在开发当中),所以没有此问题。

Varnish: 让客户端强制刷新更新服务器缓存

从 RFC2616 的要求来看,缓存服务器是需要对客户端发过来的”Cache-Control”头作出回应的

Sometimes a user agent might want or need to insist that a cache revalidate its cache entry with the origin server (and not just with the next cache along the path to the origin server), or to reload its cache entry from the origin server. End-to-end revalidation might be necessary if either the cache or the origin server has overestimated the expiration time of the cached response. End-to-end reload may be necessary if the cache entry has become corrupted for some reason.

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
其中说到了一种叫做”End-to-end reload”的:

End-to-end reload
The request includes a “no-cache” cache-control directive or, for compatibility with HTTP/1.0 clients, “Pragma: no-cache”. Field names MUST NOT be included with the no-cache directive in a request. The server MUST NOT use a cached copy when responding to such a request.

也就是说,当客户端(通常来说是浏览器)发过来”Cache-Control: no-cache”头(通常当我们按 Ctrl+f5 或者 Shift+鼠标点击刷新按钮的时候就会发出这个 HTTP 头)的时候,缓存服务器绝不能响应之前被缓存的拷贝,而是要重新去从源服务器获取一份发送给客户端。
但是在现实中往往不是这样:当我们在浏览器进行强制刷新的时候,只是将浏览器本地的缓存给清除掉了,缓存服务器的可能并没有更新。造成这个现象的原因主要有两个:

  1. 缓存服务器的性能可能满足不了这样的要求。
    在一个繁忙的缓存服务器,尤其是以磁盘作为主要缓存存储设备的服务器上面,限于磁盘IO,如果每次都必须将缓存对象彻底清除(实际上应该是可以讨巧的不去碰旧的缓存对象,然后去后端请求一个新的,然后替代旧的),即使再好的算法恐怕也无法应对大量的刷新缓存需求。
  2. 忽略客户端发过来的”Cache-Control”头,可以达到更高的缓存命中率。
    之所以要做缓存服务器,就是要让缓存率越高越好,开放让用户端去刷新缓存会降低缓存命中率。

但是现在也许该用新一点的思维来思考这个问题了:如果我们的服务器可以承受这样的刷新量,我们也许就可以让用户拥有刷新缓存服务器的能力。毕竟这是个信息迅速过期的时代,”Cache-Control: max-age=31536000″ 的作用也许已并不重要了。
我们可以用 Varnish 缓存服务器来做到这点:
前提要求是最好用纯内存作为缓存。添加如下的配置

vcl 4.0;

acl purge {
    "127.0.0.1";
}

sub vcl_recv {
    if (req.http.Cache-Control ~ "no-cache" && client.ip ~ purge) {
        # Force a cache miss
        set req.hash_always_miss = true;
    }
}

req.hash_always_miss 让 Varnish 去后端取新的内容,在 hash 里面替代旧的缓存对象,但是旧的并不会被立即清除,要等待它的 ttl 到了或者其它方法才会清除掉。这个做法的缺点是会在内存里面留下大量的旧的无用的缓存对象拷贝。
另一个方法是使用 ban 规则去清除缓存:

vcl 4.0;

acl purge {
    "127.0.0.1";
}

sub vcl_backend_response {
    # 后端响应添加到缓存时,把 Host 和 Url 添加到缓存里面,以方便后续可以根据 obj. 对象进行缓存清除。
    set beresp.http.X-Cache-Host = bereq.http.Host;
    set beresp.http.X-Cache-Url = bereq.url;
}

sub vcl_recv {
    # purge 地址内的客户端发过来"Cache-Control: no-cache"头,则添加一条 ban 规则,清除缓存
    if (req.http.Cache-Control ~ "no-cache" && client.ip ~ purge) {
        ban ("obj.http.X-Cache-Host == " + req.http.Host + " && obj.http.X-Cache-Url == " + req.url);
    }
}

sub vcl_backend_response {
        # 设置 Varnish 最长缓存保留时间为一天
        if (beresp.ttl > 1d) {
                set beresp.ttl = 1d;
        }
}

sub vcl_deliver {
    # 在发送给客户端之前,我们可以将只用于清除缓存的头去掉,没必要发给客户。
    unset resp.http.X-Cache-Host;
    unset resp.http.X-Cache-Url;
}

可以根据需求修改要不要对可以进行刷缓存的客户端 IP 进行限制。
最后,为什么要设置 Varnish 的最长保留缓存时间为一天?这是因为如果刷新请求量一旦比较大的话,会累积大量的 ban 规则(ban 规则只有当缓存里面最老的缓存对象都比他要新的时候才会被删除,否则就会一直累积着。因此,即使只有一个缓存对象保存时间为一年的话,都会导致 Varnish 累积一年的 ban 规则),这仍然还是会对性能产生很大的影响,所以为了不至于累积过多的 ban 规则,拖累 ban lurker 线程和整个 Varnish 的性能,直接设置最多保存一天就够了。(实际上,需要被缓存一天以上的东西本来就不多,可以通过 varnishtop -i TTL 查看到。)
更新:后面使用 ban 规则的方法似乎设置过最长 ttl 为一天后,还是会出现大量 ban 规则累积的情况,暂时没时间研究了。。。 🙁

貌似被 GFW 认证了

最近这段时间在自己 VPS 上面搭建的 PPTP VPN 连接每过几分钟都会中断,试验了下,发现是 1723 端口每隔一段时间都会被屏蔽。
这应该就是 GFW 在屏蔽 VPN 吧,也许我该换个 IP 地址了(我不想换做用别的类型 VPN,因为 PPTP 的客户端是最容易的,基本上任何 OS 上面都是内置支持的)。
最后我想说:真鸡巴恶心,肏你妈屄的,早点去死吧!