Last active
March 24, 2022 19:47
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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