Skip to content

Varnish: Reject or delay requests after given tresholds are reached. (Think API rate limit, or per-ip MISS rate limit)

License

Unknown, Unknown licenses found

Licenses found

Unknown
LICENSE
Unknown
COPYING
Notifications You must be signed in to change notification settings

jennytoo/libvmod-throttle

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

77 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vmod_throttle

Varnish Throttling Module

Author: Nicolas Deschildre
Updated by:Jennifer Lang
Date: 2015-10-22
Version: 4.0.0
Manual section:3

SYNOPSIS

import throttle;

DESCRIPTION

This vmod most obvious uses are to handle denial of services by a single user (or bot) punching through the cache, or to set rate limits to API calls you provide.

It will allow you to set rate limiting, on several different time windows, per path/IP/whatever you want. If a time window limit was reached, this vmod will return you the time to wait before this call will be authorized. With this return information, you can handle rate limit overflow by either abruptly returning an error, or by actually waiting the necessary time if you deems it reasonable, for a smoother rate limit overflow handling.

Please note that at the moment, there is no native way (AFAIK) to wait within Varnish (and no, using sleep() in the VCL is a bad idea), so you'll have to hack your way to it, e.g. redirect to a separate server that will wait for you and come back later.

Please read carefully the "Memory management and denial of service" section below to avoid misuses that could lead to denial of services.

"This is far better than any result I've seen from either HAProxy or Nginx doing layer 7 inspection on such modest [test] hardware."

Happy user rate-limiting on the X-Forwarded-For header.

FUNCTIONS

is_allowed

Prototype
is_allowed(STRING key, STRING rate_limits)
Arguments

key: A unique key that will identify what you are throttling. Can be used in may ways. See the examples below.

rate_limits: A list of different rate limits you want to put on this call. The syntax is the following: "[nb_of_calls]req/[duration][durations_size], ...". Example: "3req/s, 10req/30s, 30req/5m, 100req/h". Please note that we are using the Varnish duration size identifiers: s, m (and not mn/min), h, d. WARNING: You cannot define different rate_limits for a same key. If you do, only the one given in the first call will be used. See API rate limiting example below.

Return value
DURATION
Description
Increments the call counters. Returns 0.0 if the call was authorized, or the time to wait if one of the time window limit was reached.
Example

Prevent a single user (or crazy googlebot) to make a denial of service by punching through the cache: limit MISS calls on non-static assets by IP, max 3 req/s, 10 req/30s, 30 req/5m:

sub vcl_miss {
  if (req.url !~ "\.(jpg|jpeg|png|gif|ico|swf|css|js|html|htm)$") {
    if (throttle.is_allowed("ip:" + client.ip, "3req/s, 10req/30s, 30req/5m") > 0s) {
      return(synth(429, "Calm down"));
    }
  }
}

API rate limiting: limit calls by IP and API call: max 2 req/s, 100 req/2h, 1000 req/d. If an API-key is given, allow more.:

sub vcl_recv {
  if (req.url ~ "^/my_api_path/my_api_name") {
    // Consider using libvmod-redis to share apikey infos between Varnish && your api key management app
    if (req.http.X-apikey *is valid*) {
      if (throttle.is_allowed("ip:" + client.ip" + ":api:[my_api_name]:auth", "5req/s, 10000req/d") > 0s) {
        return(synth(429, "Calm down"));
      } else if (throttle.is_allowed("ip:" + client.ip" + ":api:[my_api_name]" + req.url, "2req/s, 100req/2h, 1000req/d") > 0s) {
        return(synth(429, "Calm down"));
      }
    }
  }
}

Please note: You cannot set 2 differents set of rate limits for a same key. (If you do, only one will be used, and the other will be ignored). In this example, simply add some extra text to the key to differentiate the authentificated calls from the non-authentificated ones.

is_allowed_with_time

Prototype
is_allowed_with_time(STRING key, STRING rate_limits, REAL seconds)
Arguments

key: Same as in is_allowed

rate_limits: Same as in is_allowed

seconds: Time for the check in seconds since the epoch

Return value
DURATION
Description
Identical to is_allowed except that the request time is passed in instead of taken from the request. This can be used directly but is exposed for unit testing.

calls

Prototype
calls(STRING key)
Argments
key: The unique key that will identify what you are throttling, used with the is_allowed() function.
Return value
INT
Description
Return the total number of calls being tracked for the given key.

calls_for_rate_limit

Prototype
calls_for_rate_limit(STRING key, STRING rate_limit)
Argments

key: The unique key that will identify what you are throttling, used with the is_allowed() function.

rate_limit: A single rate limit, with the same syntax than in the is_allowed() function.

Return value
INT
Description
Return the total number of calls being tracked for the given key and rate limit; returns -1 if rate_limit is invalid.

memory_usage

Prototype
memory_usage()
Return value
INT
Description
Return the amount of memory in use by this module.

remaining_calls

Prototype
remaining_calls(STRING key, STRING rate_limit)
Arguments

key: The unique key that will identify what you are throttling, used with the is_allowed() function.

rate_limit: A single rate limit, with the same syntax than in the is_allowed() function. It has to be one of the rate limits you defined in the is_allowed() function.

Return value
INT
Description
Return the number of remaining allowed calls for a given key, and for a given rate limitation; returns -1 if the key is not found or rate_limit is invalid.
Example

In the API example above, show in a header the remaining calls for the hour:

sub vcl_recv {
  if (req.url ~ "^/my_api_path/my_api_name") {
    if (throttle.is_allowed("ip:" + client.ip" + ":api:[my_api_name]", "5req/s, 100req/h") > 0s) {
      return(synth(429, "Calm down"));
    }
  }
}

sub vcl_deliver {
  if (req.url ~ "^/my_api_path/my_api_name") {
    set resp.http.X-throttle-remaining-calls = throttle.remaining_calls("ip:" + client.ip" + ":api:[my_api_name]", "100req/h");
  }
}

MEMORY MANAGEMENT AND DENIAL OF SERVICE

If used incorrectly, this tool could let an attacker force Varnish to consume all available memory and crash. It would be too bad to be DoS'ed by a tool that prevents DoS! What you need to know is that this vmod will keep in memory the time of the revelant last requests for each key you provide. And this memory is outside of the memory you specify to Varnish for caching. (So if you specify 4G of RAM to varnish, this vmod memory will be on top of it.)

For a given key, the amount of necessary memory is at its maximum fixed to the maximum number of request limit you give to this key, multiplied by 16 bytes. For example:

if(throttle.is_allowed("pouet", "2req/s, 100req/h, 1000req/d") > 0s)

For the key "pouet", the maximum memory usage will be 1000 (the maximum number between 2, 100, and 1000) multiplied by 16 bytes = 16 kbytes. Now, with a more advanced key:

if(throttle.is_allowed("ip:" + client.ip, "2req/s, 100req/3h, 1000req/d") > 0s)

We now have one key per client IP, which will each consume 16kbytes maximum. That is potentially unlimited. So what you also need to know is that the request times are kept in memory until they get older than the biggest time window: here one day (the biggest between 1s, 3 hours and 1 day). So if you take an average of 10,000 differents IP per day, that would cost at the maximum (if every IP was making 1000 calls), 10,000 * 16kbytes = 160 mbytes. That begins to be quite a number. So one can reduce this number by keeping request limits lower. For example:

if(throttle.is_allowed("ip:" + client.ip, "2req/s, 30req/h") > 0s)

This would reduce the maximum memory consumption, with 10,000 differents IP per day, to 10,000 * 30 * 16 = 4.8 mbytes. Much better. But wait! Now that we no longer have the 1 day window, the request times will only be kept for the new largest window, 1 hour. So if we have around 1,000 different IP per hour, that makes a maximum memory consumption of 1,000 * 30 * 16 = 480 kbytes. Muuch better! So we see that the time window sizes and lengths has a big impact on memory consumption.

With the following example, we are theorically still open to distributed denial of service due to this vmod, but with the required number of necessary clients to consume all memory, it is much more likely that your backend services will fall and crash first. (And remember, we only use at maximum a fixed amount of memory per key, whatever the number of calls for this key).

When we begin to be vulnerable to denial of service by a single user is when a single user can have an unlimited number of keys:

if(throttle.is_allowed("ip:" + client.ip + ":path:" + req.url, "2req/s, 30req/h") > 0s)

With this example, you would limit the request rate per IP and per URL. A single user can thus create an unlimited number of keys, and thus consume an unlimited amount of memory, and make a denial of service by crashing varnish. So if you are in a case when you want to have different rate limits per path, it is a good idea to normalize the paths, and have a limited number of them only. For example:

if(req.url ~ "^/my_api_path/my_api_name") {
    if(throttle.is_allowed("ip:" + client.ip + ":api:api_name", "2req/s, 30req/h") > 0s)

Finally, if you want to want to track the memory usage of this throttle vmod, you can use this command:

if(req.url == "/my_admin_page") {
    set resp.http.X-throttle-memusage = throttle.memory_usage();
}

INSTALLATION

The source tree is based on autotools to configure the building, and does also have the necessary bits in place to do functional unit tests using the varnishtest tool.

Usage:

./configure VARNISHSRC=DIR [VMODDIR=DIR]

VARNISHSRC is the directory of the Varnish source tree for which to compile your vmod. Both the VARNISHSRC and VARNISHSRC/include will be added to the include search paths for your module. (currently this is not true as this will always use the system varnish include directories)

Optionally you can also set the vmod install directory by adding VMODDIR=DIR (defaults to the pkg-config discovered directory from your Varnish installation).

Make targets:

  • make - builds the vmod
  • make install - installs your vmod in VMODDIR

In your VCL you could then use this vmod along the following lines:

import throttle;

sub vcl_miss {
        # This sets resp.http.hello to "Hello, World"
        set resp.http.X-throttle-wait = throttle.is_allowed("ip:" + client.ip + ":api:/path", 2, 20, 200);
}

HISTORY

This module use libvmod-example as a base and has been updated to work with varnish 4.0

COPYRIGHT

This document is licensed under the same license as the libvmod-example project. See LICENSE for details.

  • Copyright (c) 2012 Nicolas Deschildre
  • Copyright (c) 2011 Varnish Software

TODO

  • Test files

About

Varnish: Reject or delay requests after given tresholds are reached. (Think API rate limit, or per-ip MISS rate limit)

Resources

License

Unknown, Unknown licenses found

Licenses found

Unknown
LICENSE
Unknown
COPYING

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C 95.7%
  • Shell 4.3%