Geoblocking when you can't Geoblock

Published: 2022-03-01
Last Updated: 2022-03-01 19:52:37 UTC
by Rob VandenBrink (Version: 1)
2 comment(s)

Given recent events, I've gotten a flood of calls from clients who want to start blocking egress traffic to specific countries, or block ingress traffic from specific countries (or both).  This seems like something the more "aware" organizations have tried quite a while back, and in many cases have tried it and given it up as not so effective.  But just this last week we've been seeing a flood of folks who are thinking about it as something they need to do NOW.  In many cases, depending on your hardware and licensing it's as simple as a few tickboxes or lines in an ACL.  Even freely available firewalls such as pfSense do a good job of this, using MaxMind (look at pfBlockerNG for pfSense)

However, if your hardware doesn't support using a feed or an API interface for a tool like MaxMind, what can you do?  The tricky part in geo-blocking is that it's an ever-shifting landscape, your list of subnets that are "assigned" to any given country will change daily.  Also, saying "block Russia" is not terribly effective.  If you want to block any given country, you should consider that any target country will have a list of allies that might host attacks or "phone home" servers.  More importantly, if an attacker is any good, they simply won't source any of their attacks from their own personal or corporate addresses, or any IP's that are in their country.  Really you can host most attacks for pennies on most cloud platforms.

All that being said, we still need to deal with these requests from Sr Managment to "block Russia".  Understand going in that you likely won't be able to convince them it's a bad idea.  So to save time, let's script this so you can get it off your list quick!  We'll do this in Windows / PowerShell since that's a bit more accessible than Linux and/or Python - - sorry, I didn't mean to bring religion into this :-)  , but you can run the PowerShell script in your Linux desktop too if you want.

With everything discussed, let's say you're going to proceed with blocking country X.  MaxMind still has free lists of subnets-per-country that you can download as CSVs (their GeoLite2 list).  The files are dated so you can easily tell how fresh your data is - in this example I'm working with GeoLite2-Country-CSV_20220215.csv

The file header is:

network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,is_anonymous_proxy,is_satellite_provider

Each line in the file looks like:

1.2.3.0/24,2077456,2077456,,0,0

 

How do we tell which line is which country?  Look at the companion file GeoLite2-Country-Locations-en.csv -  For instance, we can get Russia's ID from this:

type GeoLite2-Country-Locations-en.csv | findstr "RU"
2017370,en,EU,Europe,RU,Russia,0

Going back to the first file, the subnet mask is in binary (bitmask format) - for instance "/16". For an ACL you'll likely want to work that back to decimal values such as 255.255.0.0 (dotted-netmask representation) or 0.0.255.255 (dotted-wildcard representation), depending on your platform.
Let's look at a bitmask of /17 and convert it to a netmask format (in PowerShell):

$MaskLength = 17

[ipaddress] $mask = ([Math]::Pow(2, $MaskLength) - 1) * [Math]::Pow(2, (32 - $MaskLength))

$mask

Address            : 131071

AddressFamily      : InterNetwork

ScopeId            :

IsIPv6Multicast    : False

IsIPv6LinkLocal    : False

IsIPv6SiteLocal    : False

IsIPv6Teredo       : False

IsIPv4MappedToIPv6 : False

IPAddressToString  : 255.255.1.0

you can see that our answer is in $mask.IpAddressToString

If you need the inverse (wildcard representation) for deploying to an IOS device, take your $mask and invert it:

$wildcard = [ipaddress] (-bnot([uint32] $mask.address))

$wildcard

 

Address            : 4279173120

AddressFamily      : InterNetwork

ScopeId            :

IsIPv6Multicast    : False

IsIPv6LinkLocal    : False

IsIPv6SiteLocal    : False

IsIPv6Teredo       : False

IsIPv4MappedToIPv6 : False

IPAddressToString  : 0.0.15.255

$wildcard.ipaddresstostring gives you a wildcard of "0.0.15.255"

Let's process the GeoLite2 database, looking for country ID of 2017370.  First, let's import the file and look at one entry in the resulting list:

$GL2 = Import-Csv .\GeoLite2-Country-Blocks-IPv4.csv

$GL2[0]

network                        : 1.0.0.0/24

geoname_id                     : 2077456

registered_country_geoname_id  : 2077456

represented_country_geoname_id :

is_anonymous_proxy             : 0

is_satellite_provider          : 0

Let's pull our target entries:

$target = "2017370"

$target_list = $gl2 | where {($_.geoname_id -eq $target) -or ($_.registered_country_geoname_id -eq $target) -or ($_.represented_country_geoname_id -eq $target)}

This gives us enough to create our final script:

$GL2 = Import-Csv .\GeoLite2-Country-Blocks-IPv4.csv

# target country is Russian Federation

$target = "2017370"

$target_list = $gl2 | where {($_.geoname_id -eq $target) -or ($_.registered_country_geoname_id -eq $target) -or ($_.represented_country_geoname_id -eq $target)}

# start by declaring the two lists

$aclinbound = @()

$acloutbound = @()

# start the list by deleting the existing list so we can start over with current values

$aclinbound += "no access-list ingressfilter-geo"

$acloutbound += "no access-list egressfilter-geo"

# compute the line items

foreach ($t in $target_list) {

    # get the network and bitmask

    $n = ($t.network).split("/")[0]

    $MaskLength = ($t.network).split("/")[1]

    # compute the netmask:

    $mask = [ipaddress]  $mask = ([Math]::Pow(2, $MaskBits) - 1) * [Math]::Pow(2, (32 - $MaskBits))

    # create the ACL entry - inbound

    $aclinbound += "access-list ingressfilter-geo extended deny ip "+$n+" "+$mask.IPAddressToString+" any"

    # ditto for the Egress filter list

    $acloutbound += "access-list egresssfilter-geo extended deny ip any "+$n+" "+$mask.IPAddressToString

    }

# Output the ACLs to text files

$aclinbound | out-file "./ingressfilter.txt"

$acloutbound | out-file "./egressfilter.txt"

Taking a quick look, those are pretty hefty ACLs.  You can certainly apply this on most reasonable gear, but it's going to make your config files a bit unweildy, and while it will run fine, it'll certainly affect your memory and cpu.  Especially given the caveats we discussed earlier - this isn't going to be terribly effective!

#subtract one to account for the header line

$aclinbound.length -1

13399

 

Where to go from here?  You can cut/paste the ACLs as-is into your ASA, then apply it to the appropriate inbound/outbound interface(s).  To streamline it, you could easily script the download using MaxMind's API (dev.maxmind.com), and while you're at it you could update to the more accurate GeoIP2 list.

At the other end, you can apply the ACL using common automation tools like (among other tools) Solarwinds' CATTOOLs, in PowerShell using Posh-SSH or in Python using netmiko or paramiko.  EXPECT is another tried-and-true option.  Frameworks like Ansible, SALT, Puppet, Chef or Terraform can allow you to expand your automation to more complex functions - these will also tend to protect your firewall credentials better than a plain text script.

If you're looking for a more useful way to build your egress list, we discussed this all the way back in 2014:
https://isc.sans.edu/forums/diary/Egress+Filtering+What+do+we+have+a+bird+problem/18379/
In short, letting your internal workstations and people trust all hosts and protocols on the internet is a really bad idea!  Trust what you need to, then wrap up your egress filter with a deny any/any/log line.

If you're looking for lists of malicious hosts to build a block list, that's more easy to come by
ISC Block list: https://isc.sans.edu/block.txt
Tor Project Exit nodes: https://github.com/SecOps-Institute/Tor-IP-Addresses/blob/master/tor-exit-nodes.lst

More on working with IP addresses in powershell:
https://isc.sans.edu/diary/Using+AD+to+find+hosts+that+aren%27t+in+AD+-+fun+with+the+%5BIPAddress%5D+construct%21/24762
https://isc.sans.edu/diary/Sorting+Things+Out+-+Sorting+Data+by+IP+Address/27916

If you've got an active / regularly updated "block list" that you use to protect your infrastructure, please share in our comment form!

===============
Rob VandenBrink
rob <at> coherentsecurity.com

Keywords:
2 comment(s)

Comments

There is an issue with part of the script... $MaskLength should be $maskBits

foreach ($t in $target_list) {

# get the network and bitmask

$n = ($t.network).split("/")[0]

$maskBits = ($t.network).split("/")[1]

# compute the netmask with Cisco iOS conversion:

$mask = [ipaddress] $mask = ([Math]::Pow(2, $maskBits) - 1) * [Math]::Pow(2, (32 - $maskBits))
$mask = [ipaddress] (-bnot([uint32] $mask.Address))

# create the ACL entry - inbound

$aclinbound += "access-list ingressfilter-geo extended deny ip "+$n+" "+$mask.IPAddressToString+" any"

# ditto for the Egress filter list

$acloutbound += "access-list egresssfilter-geo extended deny ip any "+$n+" "+$mask.IPAddressToString

}
Full disclosure: Commenting on this publicly as one of the core developers behind IPFire (https://www.ipfire.org/), after being asked by Rob to do so.

Indeed, we observe a lot of "let's block RU, and the cyber threat issue is solved" requests those days as well. Which is sad indeed, as blocking a country virtually always causes more harm than good, and it pins a reputation to a country code where there really is none. To quote from the netfilter documentation, geoblocking is racist routing after all.

On the other hand, some countries (such as US, NL, DE, CH, et al.) are virtually unblockable, despite also being the location (whether the term "location" is referring to the physical location of the infrastructure or the jurisdiction a person behind an IP address would be affected by - both information have legitimate use-cases, yet it is rarely clarified which database outputs which kind of "location") of some pretty dangerous networks.

In IPFire, we use the Spamhaus DROP lists by default (see https://blog.ipfire.org/post/introducing-elementary-network-protection-dropping-all-traffic-from-and-to-hostile-networks-by-default for an announcement of this), to (a) try to get rid of the "country == reputation" mentality and (b) provide a basic protection against the "baddest of the bad" by default.

Also, it might be interesting to some readers that there is an open-source friendly alternative to MaxMind's GeoIP database available: IPFire Location (https://location.ipfire.org/)

We use this database for country-based (and hopefully soon ASN-based) firewalling, and fetching a list of IP networks belonging to a certain country is quite easy (see the "export" command in https://man-pages.ipfire.org/libloc/location.html), even in different formats necessary for ipset & co., which might be a bit more comfortable to use than parsing a CSV file.

Should you be interested, further information on IPFire Location is available here:
- https://blog.ipfire.org/post/on-retiring-the-maxmind-geoip-database
- https://blog.ipfire.org/post/libloc-or-what-is-working-inside-it

(Crap, this reads like an advertisement by now. It really shouldn't; just wanted to elaborate on our perspective in the hope it is useful. :-)

Diary Archives