Introduction
Fastly is a popular CDN based on the open-source Varnish. Since it supports VCL, a lot of custom “logic” to handle incoming requests can be added, right at the edge. It also provides a host of geolocation identification options.
In this post, we’ll build on the service we created in a previous post Fastly Meta Service to implement a simple API which returns details about a particular public IP.
This service can be nice value add, whether for use by machines (json output helps!) or by non-technical folks to gather useful info tech teams often need to solve/debug “Does not work from my home. I get an error, please fix!” issues.
Solution
The service will rely entirely on Fastly’s infrastructure and will not interact with the configured origin at all! Sadly, you cannot launch a service without any origin at this point, so you would need to add some dummy host. httpbin.org maybe a good choice here!
Such an isolated request flow is possible, thanks to the synthetic responses that can be served directly from Fastly using the vcl constructs.
The request flow will look like this (no requests are sent to the origin) -
Geolocation Database
Fastly uses 2 vendors to fetch the geolocation details - Maxmind and Digital Element. This Fastly docs page lists out the different variables that are available for use. Broadly these variables are available under 2 distinct namespaces, each provided by a different vendor. client.geo.*
namespace variables are provided by Digital Element, while geoip.*
namespace variables are provided by Maxmind.
Note that Fastly has deprecated Maxmind, so any future implementations should only be using the client.geo.*
variables as outlined in this post.
API Endpoints
We will build the following endpoints
Request
GET /
Response
1{
2 "date": "Timestamp in GMT",
3 "ipv6": "true|false",
4 "public_ip": "public ip",
5 "de_city": "city from DE database",
6 "de_country": "country from DE database",
7 "de_continent": "continent from DE database",
8 "de_proxy" : "proxy type and description from DE database",
9 "mx_city": "city from Maxmind database",
10 "mx_country": "city from Maxmind database",
11 "mx_continent": "countinent from Maxmind database"
12}
Request
GET /?ip=ip_v4_add or ip_v6_add
Response
1{
2 "date": "Timestamp in GMT",
3 "ipv6": "true|false",
4 "public_ip": "provided ip_v4_add or ip_v6_add",
5 "de_city": "city from DE database",
6 "de_country": "country from DE database",
7 "de_continent": "continent from DE database",
8 "de_proxy" : "proxy type and description from DE database",
9 "mx_city": "city from Maxmind database",
10 "mx_country": "city from Maxmind database",
11 "mx_continent": "countinent from Maxmind database"
12}
Request
GET /random
Response
301 Redirect to /
VCL Snippets
We will modify the vcl snippets recv
and error
created previously to extend the service and provide more details.
Update the recv
snippet with this code -
1if (!req.url.path ~ "^/$" || !req.http.Fastly-SSL){
2 error 618;
3}
4
5set req.url = querystring.filter_except(req.url, "ip");
6
7if (fastly.ff.visits_this_service == 0 && req.restarts == 0) {
8 set req.http.Fastly-Client-IP = client.ip;
9 set req.http.ipv6 = "false";
10 if (req.is_ipv6){
11 set req.http.ipv6 = "true";
12 }
13}
14
15if (req.url.qs ~ "ip"){
16 set req.http.Fastly-Client-IP = querystring.get(req.url, "ip");
17 set client.geo.ip_override = req.http.Fastly-Client-IP;
18 set geoip.ip_override = req.http.Fastly-Client-IP;
19}
20
21error 620;
Update the error
snippet with this code -
1if (obj.status == 618) {
2 set obj.status = 301;
3 set obj.response = "Moved Permanently";
4 set obj.http.Location = "https://" req.http.host "/";
5 set obj.http.cache-control = "private, no-store, no-cache, max-age=0";
6 return (deliver);
7}
8
9if (obj.status == 620) {
10 set obj.status = 200;
11 set obj.response = "OK";
12 set obj.http.Content-Type = "application/json";
13 set obj.http.cache-control = "private, no-store, no-cache, max-age=0";
14 synthetic
15{"{
16 "date": ""} now {"",
17 "ipv6": ""} req.http.ipv6 {"",
18 "public_ip": ""} req.http.Fastly-Client-IP {"",
19 "de_city": ""} client.geo.city {"",
20 "de_country": ""} client.geo.country_code {"",
21 "de_continent": ""} client.geo.continent_code {"",
22 "de_proxy" : ""} client.geo.proxy_type " - " client.geo.proxy_description {"",
23 "mx_city": ""} geoip.city {"",
24 "mx_country": ""} geoip.country_code {"",
25 "mx_continent": ""} geoip.continent_code {""
26}"};
27 return (deliver);
28}
You can see/test this flow in a fiddle here
Snippets also committed to the repo
Deployment
The service, configurations and vcls can be deployed to Fastly using Terraform. The required Terraform code is similar to what we used in the last post and is available here
Outputs
Once the service is ready, a simple request via curl
or browser will return the details like below. I am using the test domain, hence need to use the -k
flag (for more uses of curl
, you can check this post). For a production level deployment, you would obviously map a custom domain to the service and also use SSL (Fastly provided or self procured).
Let’s check a few curl responses.
Proton Public IP
If you are connected to the ProtonVPN free NL server, this is what you should see. Note the difference of output between the Maxmind and DE outputs. Maxmind says it’s a US IP!
1$ curl -k https://meta.dane-example.com.global.prod.fastly.net/
2
3{
4 "date": "Fri, 05 Mar 2021 16:23:53 GMT",
5 "ipv6": "false",
6 "public_ip": "34.66.60.237",
7 "de_city": "HUISSEN",
8 "de_country": "NL",
9 "de_continent": "EU",
10 "de_proxy" : "HOSTING - VPN",
11 "mx_city": "COUNCIL BLUFFS",
12 "mx_country": "US",
13 "mx_continent": "NA"
14}
Google Public IP
Let’s inspect a public IP, maybe the Google DNS? This time both vendors show the correct country, but Maxmind is off the mark on city.
1$ curl -k https://meta.dane-example.com.global.prod.fastly.net/?ip=8.8.8.8
2
3{
4 "date": "Fri, 05 Mar 2021 16:23:53 GMT",
5 "ipv6": "false",
6 "public_ip": "8.8.8.8",
7 "de_city": "MOUNTAIN VIEW",
8 "de_country": "US",
9 "de_continent": "NA",
10 "de_proxy" : "ANONYMOUS - ?",
11 "mx_city": "NEW YORK",
12 "mx_country": "US",
13 "mx_continent": "NA"
14}
Public IPv6
Let’s try the IPv6 address now, if that works? Google DNS again. Maxmind doesn’t have any details about IPv6, DE does.
1$ curl -k https://meta.dane-example.com.global.prod.fastly.net/?ip=2001:4860:4860::8888
2
3{
4 "date": "Fri, 05 Mar 2021 16:23:53 GMT",
5 "ipv6": "false",
6 "public_ip": "2001:4860:4860::8888",
7 "de_city": "MOUNTAIN VIEW",
8 "de_country": "US",
9 "de_continent": "NA",
10 "de_proxy" : "? - ?",
11 "mx_city": "",
12 "mx_country": "",
13 "mx_continent": ""
14}
Private IP
What will it return for a private IP? Maxmind is trumped again, however, DE does tell you it’s a private IP. Nice.
1$ curl -k https://meta.dane-example.com.global.prod.fastly.net/?ip=192.168.1.1
2
3{
4 "date": "Fri, 05 Mar 2021 16:23:53 GMT",
5 "ipv6": "false",
6 "public_ip": "192.168.1.1",
7 "de_city": "PRIVATE",
8 "de_country": "**",
9 "de_continent": "**",
10 "de_proxy" : "? - ?",
11 "mx_city": "",
12 "mx_country": "",
13 "mx_continent": ""
14}
What if someone hits a random path?
Since the body doesn’t return anything for redirects, we will either have to follow redirect using -L
or inspect the headers to confirm the response.
Let’s do the latter.
1$ curl -k -I https://meta.dane-example.com.global.prod.fastly.net/random
2
3HTTP/2 301 Moved Permanently
4accept-ranges: bytes
5cache-control: private, no-store, no-cache, max-age=0
6content-length: 0
7date: Fri, 05 Mar 2021 16:23:53 GMT
8location: https://meta.dane-example.com.global.prod.fastly.net/
9retry-after: 0
Conclusion
We enhanced the previous basic version considerably, by returning output from both vendors as well as making the response more machine friendly. It is possible to further enhance this service to put out even more information, as needed. It removes any reliance on 3rd party services which may or may not be hosted in secure environments.
The simpler service with only txt output is desribed in a previous post - Fastly Meta Service.
Note: Code mentioned above is here