While scrolling through the visitor’s report of my blog, I noticed visitors from a “strange” ISP: “Magistrat der Stadt Wien, Magistratsabteilung 01”, which is the IT department of the magistrate of the City of Vienna. Digging further through the logs, I found out they are a regular visitor, and so are multiple ministries, the Austrian unemployment service, and multiple other public bodies (Maybe more, but due to IP anonymization I only know about the ones that have their own subnets). So I’ve decided to give them a special greeting when visiting. I wanted it to look like this:

Screenshot of my blog, that shows the modal with the greeting Hallo Bundesministerium für Inneres

Preparation #

Find the relevant IP ranges #

There are multiple ways to do this. Since I already knew before that the City of Vienna has their own ASN, I went over to Hurricane Electric’s BGP Toolkit (after I had to look up their ASN again) to see what IP ranges they announce. For this example, at the moment those are as follows:

Prefix Description
138.22.0.0/17 Zentralanstalt fuer Meteorologie und Geodynamik
138.22.128.0/17 Zentralanstalt fuer Meteorologie und Geodynamik
141.203.0.0/16 Magistrat der Stadt Wien, Magistratsabteilung 01
144.65.0.0/17 Austrian Federal Ministry of Science, Research and Economy
144.65.128.0/17 Austrian Federal Ministry of Science, Research and Economy
193.170.66.0/24 Bundesministerium fuer Nachhaltigkeit und Tourismus
193.170.168.0/21 Zentralanstalt fuer Meteorologie und Geodynamik
193.170.182.0/23 Office of the Federal Austrian President
193.171.152.0/24  
193.171.153.0/24 Bundesministerium fuer Landesverteidigung
193.171.154.0/24 Bundesministerium fuer Landesverteidigung
194.37.10.0/24 Bundesministerium fuer Nachhaltigkeit und Tourismus
217.149.224.0/20 Magistrat der Stadt Wien, Magistratsabteilung 01

What we notice is that one range doesn’t have a description. A quick glance at the whois information reveals that it also belongs to the Austrian Ministry of Defense.

inetnum:  193.171.152.0 - 193.171.154.255
netname:  BMLV-HDVA
descr:    Bundesministerium fuer Landesverteidigung
descr:    Heeres-Datenverarbeitungsamt
country:  AT

If we repeat this procedure, we find out all the IP ranges that we want to use. A good tip to do so is to look at who the first target is peering with if they belong to the same type of organization. For example, one of the magistrate’s peers is the Ministry of the Interior, another one is the federal data center, the parliament and so on. We end up with a list of IPs.

Create the list #

For later use, the list should have two columns, that have the subnet in the first column and an abbreviation of the target without spaces in the second one, separated by tabs or spaces. For example like this:

141.203.0.0/16    wien
217.149.224.0/20  wien
2a00:1ba0:2::/48  wien
2a00:1ba0:3::/48  wien
217.116.64.0/20   wien

The problem #

I use a static site generator to deploy this blog (Jekyll), and I can’t have serverside code.

nginx to the rescue! #

Nginx has a module called ngx_http_sub_module, which can replace strings in the response body before sending it to the client.

Prepare the modal #

I added this to the top of my default template

<div class="is-hidden-iprange">
    Hallo %%NAME%%!
</div>

and to make sure it is hidden for visitors that are not in the list, we also need this class definition in the stylesheet:

.is-hidden-iprange {
  display: none;
}

What we also notice is the placeholder %%NAME%%, which will be replaced by the name of the visitor’s institution.

nginx configuration #

As mentioned before, we need the ngx_http_sub_module and ngx_http_geo_module compiled into nginx. The version Debian distributes includes those modules. We basically need to add three things to our config.
Disclaimer: I’m not 100% sure if this is a good way to do this.

Prepare the list #

First, we prepare our list and add them to a variable of the “geo” type. This is later matched to the $remote_addr variable, but we could also use other ones, for example if your webserver is behind a reverse proxy (See the documentation for the ngx_http_geo_module to find out more).

The finished list looks like this in my case:

geo $ranges {
  default              nothing;
  78.41.148.0/24       bmi;
  78.41.149.0/24       bmi;
  78.41.150.0/24       bmi;
  193.187.2.0/23       bmi;
  193.187.2.0/24       bmi;
  193.187.3.0/24       bmi;
  2a03:f080:1800::/48  bmi;
  141.203.0.0/16       wien;
  217.149.224.0/20     wien;
  2a00:1ba0:2::/48     wien;
  2a00:1ba0:3::/48     wien;
  217.116.64.0/20      wien;
  194.37.104.0/21      ams;
  193.171.154.0/24     bmlv;
  193.171.152.0/24     bmlv;
  193.171.153.0/24     bmlv;
  213.185.172.0/26     wienit;
  193.178.171.0/24     wienit;
  91.219.68.0/22       fhspt;
  2001:678:1d8::/48    fhspt;
}

So our variable is called “ranges”, and we add a default value “nothing”, that always matches if none of the others does. This has to be outside of out server {} block.

Just as a side story, the short names I’m using here are bmi, short for Bundesministerium für Inneres, the Ministry for the Interior; wien for the magistrate of the City of Vienna; ams, short for Arbeitsmarktservice, the unemployment agency, bmlv for Bundesministerium für Landesverteidigung, the Austrian defense ministry; wienit, which is the ISP of something called Wiener Stadtwerke which is the Viennese public utility company and fhstp, the University of Applied Sciences in St. Pölten, as they are all very loyal visitors, among other public authorities grinning face with smiling eyes emoji.

Define replacement strings #

Here we need two maps. One maps the placeholder in the modal to the name, and it looks like this:

map $ranges $stelle_filter {
  bmi 'Bundesministerium für Inneres';
  wien 'Stadt Wien';
  ams 'Arbeitsmarktservice';
  bmlv 'Bundesministerium für Landesverteidigung';
  wienit 'Wiener Stadtwerke';
  fhspt 'FH St. Pölten';
}

So we have the short name from above mapped to what we want to show the user. The other one is the one that removes the class that hides the modal itself, for this we replace the .is-hidden-iprange class with an empty string, like this:

map $ranges $class_filter {
  bmi '';
  wien '';
  ams '';
  bmlv '';
  wienit '';
  fhspt '';
  nothing 'is-hidden-iprange';
}

For some reason that I’ve not discovered yet, we also need to replace the is-hidden-iprange class with itself.

Add the filter #

In the server block, we need to tell nginx what to replace with what. This is done with the following three lines:

sub_filter '%%NAME%%' $stelle_filter;
sub_filter 'is-hidden-iprange' $class_filter;
sub_filter_once on;

The first one replaces the placeholder with the name, the second removes the class, and the third statement tells nginx to only replace the first occurrence of the strings we search for.