Skip to content

Instantly share code, notes, and snippets.

@Integralist
Last active March 24, 2022 19:47
Show Gist options
  • Save Integralist/50833024cf073e804e1243baa0c690e8 to your computer and use it in GitHub Desktop.
Save Integralist/50833024cf073e804e1243baa0c690e8 to your computer and use it in GitHub Desktop.
[Simple Fastly CDN Rate Limiting Logic using Vary behaviour for cacheable GET requests] #fastly #varnish #vcl #ratelimit #dos
# Example:
# https://fiddle.fastlydemo.net/fiddle/4ac44faf
#
# the principle of this design is that we want
# requests to be cached using a key that varies
# on the "Fastly-Client-IP" and "User-Agent" headers
# but only for responses that indicate "429 Too Many Requests"
#
# whenever we get a "429 Too Many Requests" response, we'll
# we'll ensure it is cacheable, and that will result in the Vary headers
# being applied as part of the cache lookup process (because a different
# client request will provide a different value for the IP and User-Agent headers
# which are used for the varying logic. and so they'll have empty values and
# this will result in (hopefully) a cached "200 OK". or at the very least they'll
# get a cache MISS and go to origin to fetch the content which will then be cached).
#
# caveat:
# this example doesn't work with POST requests.
# the solution to that is to `return(lookup)` in vcl_recv for POST|PUT|DELETE methods
# then skip caching for anything other than the 429 status code (e.g. if '200 OK' then don't cache)
# as we only want to cache a 429 with Vary behaviour
#
# see this fiddle: https://fiddle.fastlydemo.net/fiddle/47871720
# which fixes the issue and implements the following changes
# + additional changes to support otherwise uncacheable method types
#
# the code for that latter fiddle is also copied at the bottom of this gist
# just in case the fastly fiddle url is no longer available.
#
# one caveat to this approach (according to fastly)...
#
# > As far as the caveat is considered, Fastly have told me…
# > POST requests are also ineligible for clustering, ie transferring the request to a consistently-hashed storage node.
# > Since we don’t expect POSTs to be cached, this is an optimisation and we unfortunately don’t provide a way for you to override it.
# > The effect of this is that even if you do enable POSTs to be cached, your cache hit ratio will be poor.
# > This may be fine if your intention is just to use it for rate limiting.
#
# So yeah it’s not the end of the world, but the fact we lose fastly’s clustering behaviour isn’t ideal either.
if (req.restarts == 0) {
# the User-Agent isn't provided by all clients (e.g. pentesting tools)
# so we'll provide a genericized value if no header is found
if (!req.http.User-Agent) {
set req.http.UserAgent = "Foo-Bar";
}
}
# at this point we've gone to the origin and we've received a 429
# and so we set the 429 to be cacheable and we give it a TTL via Cache-Control header
# and we state this content needs to utilize Vary so that other clients don't get the cached 429
# if no caching directives are provided, then we'll set a default of one hour
if (beresp.status == 429) {
# ensure all responses have the required Vary header values
# for the sake of this test fiddle we simply hardcode the value
# but in our real service we would check the Vary header and append to it
if (!beresp.http.Vary) {
set beresp.http.Vary = "Fastly-Client-IP, User-Agent";
}
set beresp.cacheable = true;
if (!beresp.http.Surrogate-Control && !beresp.http.Cache-Control) {
set beresp.ttl = 1h; # 1 hour
}
}
sub vcl_recv {
if (req.restarts == 0) {
if (!req.http.User-Agent) {
set req.http.UserAgent = "Fallback";
}
}
// force caching for methods that are otherwise normally uncached
// and mimic the standard behaviour of disabling 'request collapsing'
if (req.method ~ "(POST|PUT|DELETE)") {
set req.http.X-PassMethod = "true";
set req.hash_ignore_busy = true;
return(lookup);
}
}
sub vcl_fetch {
// this conditional is used for testing the logic
// and isn't required for actual implementation
if (req.http.X-429) {
set beresp.status = 429;
}
declare local var.vary STRING;
set var.vary = "Fastly-Client-IP, User-Agent";
if (beresp.status == 429) {
if (!beresp.http.Vary) {
set beresp.http.Vary = var.vary;
} else {
set beresp.http.Vary = beresp.http.Vary + ", " + var.vary;
}
set beresp.cacheable = true;
if (!beresp.http.Surrogate-Control:max-age && !beresp.http.Cache-Control:max-age) {
set beresp.ttl = 1m; // 1 minute
}
}
if (req.http.X-PassMethod) {
if (beresp.status != 429) {
set beresp.cacheable = false;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment