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.

The 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) - fastly-geo-service

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
{
    "date": "Timestamp in GMT",
    "ipv6": "true|false",
    "public_ip": "public ip",
    "de_city": "city from DE database",
    "de_country": "country from DE database",
    "de_continent": "continent from DE database",
    "de_proxy" : "proxy type and description from DE database",
    "mx_city": "city from Maxmind database",
    "mx_country": "city from Maxmind database",
    "mx_continent": "countinent from Maxmind database"
}

Request

GET /?ip=ip_v4_add or ip_v6_add
Response
{
    "date": "Timestamp in GMT",
    "ipv6": "true|false",
    "public_ip": "provided ip_v4_add or ip_v6_add",
    "de_city": "city from DE database",
    "de_country": "country from DE database",
    "de_continent": "continent from DE database",
    "de_proxy" : "proxy type and description from DE database",
    "mx_city": "city from Maxmind database",
    "mx_country": "city from Maxmind database",
    "mx_continent": "countinent from Maxmind database"
}

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 -

if (!req.url.path ~ "^/$" || !req.http.Fastly-SSL){
  error 618;
}

set req.url = querystring.filter_except(req.url, "ip");

if (fastly.ff.visits_this_service == 0 && req.restarts == 0) {
  set req.http.Fastly-Client-IP = client.ip;
  set req.http.ipv6 = "false";
  if (req.is_ipv6){
    set req.http.ipv6 = "true";
  }
}

if (req.url.qs ~ "ip"){
  set req.http.Fastly-Client-IP = querystring.get(req.url, "ip");
  set client.geo.ip_override = req.http.Fastly-Client-IP;
  set geoip.ip_override = req.http.Fastly-Client-IP;
}

error 620;

Update the error snippet with this code -

if (obj.status == 618) {
    set obj.status = 301;
    set obj.response = "Moved Permanently";
    set obj.http.Location = "https://" req.http.host "/";
    set obj.http.cache-control = "private, no-store, no-cache, max-age=0";
    return (deliver);
}

if (obj.status == 620) {
    set obj.status = 200;
    set obj.response = "OK";
    set obj.http.Content-Type = "application/json";
    set obj.http.cache-control = "private, no-store, no-cache, max-age=0";
    synthetic 
{"{
    "date": ""} now {"",
    "ipv6": ""} req.http.ipv6 {"",
    "public_ip": ""} req.http.Fastly-Client-IP {"",
    "de_city": ""} client.geo.city {"",
    "de_country": ""} client.geo.country_code {"",
    "de_continent": ""} client.geo.continent_code {"",
    "de_proxy" : ""} client.geo.proxy_type  " - " client.geo.proxy_description {"",
    "mx_city": ""} geoip.city {"",
    "mx_country": ""} geoip.country_code {"",
    "mx_continent": ""} geoip.continent_code {""
}"};
    return (deliver);
}

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.

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!

curl -k https://meta.dane-example.com.global.prod.fastly.net/
{
    "date": "Fri, 05 Mar 2021 16:23:53 GMT",
    "ipv6": "false",
    "public_ip": "34.66.60.237",
    "de_city": "HUISSEN",
    "de_country": "NL",
    "de_continent": "EU",
    "de_proxy" : "HOSTING - VPN",
    "mx_city": "COUNCIL BLUFFS",
    "mx_country": "US",
    "mx_continent": "NA"
}

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.

curl -k https://meta.dane-example.com.global.prod.fastly.net/?ip=8.8.8.8 
{
    "date": "Fri, 05 Mar 2021 16:23:53 GMT",
    "ipv6": "false",
    "public_ip": "8.8.8.8",
    "de_city": "MOUNTAIN VIEW",
    "de_country": "US",
    "de_continent": "NA",
    "de_proxy" : "ANONYMOUS - ?",
    "mx_city": "NEW YORK",
    "mx_country": "US",
    "mx_continent": "NA"
}

Let’s try the IPv6 address now, if that works? Google DNS again. Maxmind doesn’t have any details about IPv6, DE does.

curl -k https://meta.dane-example.com.global.prod.fastly.net/?ip=2001:4860:4860::8888
{
    "date": "Fri, 05 Mar 2021 16:23:53 GMT",
    "ipv6": "false",
    "public_ip": "2001:4860:4860::8888",
    "de_city": "MOUNTAIN VIEW",
    "de_country": "US",
    "de_continent": "NA",
    "de_proxy" : "? - ?",
    "mx_city": "",
    "mx_country": "",
    "mx_continent": ""
}

What will it return for a private IP? Maxmind is trumped again, however, DE does tell you it’s a private IP. Nice.

curl -k https://meta.dane-example.com.global.prod.fastly.net/?ip=192.168.1.1
{
    "date": "Fri, 05 Mar 2021 16:23:53 GMT",
    "ipv6": "false",
    "public_ip": "192.168.1.1",
    "de_city": "PRIVATE",
    "de_country": "**",
    "de_continent": "**",
    "de_proxy" : "? - ?",
    "mx_city": "",
    "mx_country": "",
    "mx_continent": ""
}

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.

Let’s do the latter.

curl -k -I https://meta.dane-example.com.global.prod.fastly.net/random
HTTP/2 301 Moved Permanently
accept-ranges: bytes
cache-control: private, no-store, no-cache, max-age=0
content-length: 0
date: Fri, 05 Mar 2021 16:23:53 GMT
location: https://meta.dane-example.com.global.prod.fastly.net/
retry-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 

References (1)

  1. Geolocation