Use Geocoder and MaxMind DB to Geocode IP Addresses
Reading time: 6 minutes
Utilizing IP addresses for communication is a fundamental aspect of many web applications employing HTTP(S) as their protocol, particularly those interconnected to the internet. However, a multitude of IP addresses are also generated through protocols such as SSH, SMTP, or IMAP. In this article, we’ll explore how to determine their origins and extract valuable data using the geocoder gem in conjunction with the MaxMind GeoLite2 Database.
Requirements
To proceed with this, it’s assumed that you have a Ruby on Rails project. It’s more advantageous to create something reusable, and the setup for the geocoder gem is simplified.
Get the GeoLite2 Database from MaxMind
To utilize offline geolocation via the geocoder gem, you’ll need to download the GeoLite2 City database from MaxMind. Simply follow the steps outlined on the developer page and download the ZIP containing the mmdb version of the database. While the country-only version is an option, I suggest opting for the city database for a richer dataset, making the exploration of IP addresses worldwide more intriguing.
Setup your Ruby on Rails Project
The setup process is straightforward within a Ruby on Rails project. Let’s walk through the steps to initiate the exploration of the world through IP addresses.
Add the geocoder gem
To begin, you’ll need to add the geocoder gem into your project. We’ll utilize it to search for an IP address and delve into the geographical information surrounding it. Simply add it to your Gemfile using your preferred text editor:
# Gemfile
+gem "geocoder"
Once you’ve added the geocoder gem to your Gemfile, proceed to run bundle install and then restart your Ruby on Rails application or the Rails Console.
bundle install
You can accomplish this in a single step by executing the command bundle add <name>
within your Ruby on Rails directory.
$ bundle add geocoder
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
...
Fetching geocoder 1.8.2
...
Installing geocoder 1.8.2
$ git diff Gemfile
+
+gem "geocoder", "~> 1.8"
The geocoder gem has a Ruby on Rails integration. Just restart your server and you can start using it.
Add maxminddb gem for the geocoder gem
Geocoder utilizes the maxminddb gem to query an mmdb file for information. Simply include it in your Gemfile as you did for the geocoder gem:
# Gemfile
+ gem "maxminddb"
Run bundle install
to install the current version of maxminddb
gem.
Or just run:
$ bundle add maxminddb
Don’t forget to restart your Ruby on Rails project to load the newly added gem.
Create the geocoder configuration file
To enable the geocoder gem to utilize a MaxMind database for offline geolocation of IP addresses, we need to configure it. You can generate a basic configuration file for this purpose by running the following command in your Ruby on Rails project:
# generate config/initializers/geocoder.rb
rails generate geocoder:config
The generated configuration file provides various options, but we only need to focus on a few lines of it. I’ve streamlined it by removing all the optional setups, resulting in this concise version:
Geocoder.configure(
ip_lookup: :geoip2,
geoip2: {
file: "vendor/maxmind/GeoLite2-City.mmdb"
}
)
Since I’ve predetermined the path for the mmdb file, it should be located within
your Ruby on Rails project at vendor/maxmind/GeoLite2-City.mmdb
. You have the
option to choose a different location, but you can also simply use the one I’ve
designated.
Test the setup using Ruby on Rails Console
To verify the setup, let’s begin by testing it in the Ruby on Rails console. However, for testing purposes, we require an IP address. I’ve opted to use the IP address of an old friend, ugu.com, for this test:
% ping -c 1 ugu.com
PING ugu.com (76.50.19.84): 56 data bytes
We will use the IP 76.50.19.84
for the first test:
% irb
irb(main):001> geo = Geocoder.search("76.50.19.84")
irb(main):002> geo
=>
[#<Geocoder::Result::Geoip2:0x000000013abe6230
@cache_hit=nil,
@data=
#<MaxMindDB::Result:0x000000013abe6a28
@raw=
{"city"=>{"geoname_id"=>5405288, "names"=>{"en"=>"Valencia"}},
"continent"=>
{"code"=>"NA",
"geoname_id"=>6255149,
"names"=>{"de"=>"Nordamerika", "en"=>"North America", "es"=>"Norteamérica", "fr"=>"Amérique du Nord", "ja"=>"北アメリカ", "pt-BR"=>"América do Norte", "ru"=>"Северная Америка", "zh-CN"=>"北美洲"}},
"country"=>
{"geoname_id"=>6252001,
"iso_code"=>"US",
"names"=>{"de"=>"Vereinigte Staaten", "en"=>"United States", "es"=>"Estados Unidos", "fr"=>"États Unis", "ja"=>"アメリカ", "pt-BR"=>"EUA", "ru"=>"США", "zh-CN"=>"美国"}},
"location"=>{"accuracy_radius"=>10, "latitude"=>34.4463, "longitude"=>-118.5356, "metro_code"=>803, "time_zone"=>"America/Los_Angeles"},
"postal"=>{"code"=>"91354"},
"registered_country"=>
{"geoname_id"=>6252001,
"iso_code"=>"US",
"names"=>{"de"=>"Vereinigte Staaten", "en"=>"United States", "es"=>"Estados Unidos", "fr"=>"États Unis", "ja"=>"アメリカ", "pt-BR"=>"EUA", "ru"=>"США", "zh-CN"=>"美国"}},
"subdivisions"=>
[{"geoname_id"=>5332921,
"iso_code"=>"CA",
"names"=>{"de"=>"Kalifornien", "en"=>"California", "es"=>"California", "fr"=>"Californie", "ja"=>"カリフォルニア州", "pt-BR"=>"Califórnia", "ru"=>"Калифорния", "zh-CN"=>"加州"}}],
"network"=>"76.50.18.0/23"}>>]
The result of the query, geo
, is an ruby Array. By default, the geocoder gem might return multiple locations for a query, which is common for searches such as a city name like “York” (which could result in multiple places like York, New York, Yorkshire, etc.). However, for an IP address, we simply utilize the first result, as there is only one possible location.
We can proceed by selecting the first element in the array:
irb(main):002> geo.first
=>
#<Geocoder::Result::Geoip2:0x000000013abe6230
@cache_hit=nil,
@data=
#<MaxMindDB::Result:0x000000013abe6a28
...
As Geocoder::Result::Geoip2 has predefined functions, we can use them to query the data:
irb(main):004> geo.first.city
=> "Valencia"
irb(main):005> geo.first.country
=> "United States"
irb(main):006> geo.first.country_code
=> "US"
This is highly beneficial for many use cases developers encounter. You can simply utilize the provided data and construct the application accordingly. However, MaxMind offers additional features within the database.
irb(main):007> data = geo.first.data
=>
{"city"=>{"geoname_id"=>5405288, "names"=>{"en"=>"Valencia"}},
...
irb(main):011> data
=>
{"city"=>{"geoname_id"=>5405288, "names"=>{"en"=>"Valencia"}},
"continent"=>
{"code"=>"NA",
"geoname_id"=>6255149,
"names"=>
{"de"=>"Nordamerika",
"en"=>"North America",
"es"=>"Norteamérica",
"fr"=>"Amérique du Nord",
"ja"=>"北アメリカ",
"pt-BR"=>"América do Norte",
"ru"=>"Северная Америка",
"zh-CN"=>"北美洲"}},
"country"=>
{"geoname_id"=>6252001,
"iso_code"=>"US",
"names"=>
{"de"=>"Vereinigte Staaten",
"en"=>"United States",
"es"=>"Estados Unidos",
"fr"=>"États Unis",
"ja"=>"アメリカ",
"pt-BR"=>"EUA",
"ru"=>"США",
"zh-CN"=>"美国"}},
"location"=>
{"accuracy_radius"=>10,
"latitude"=>34.4463,
"longitude"=>-118.5356,
"metro_code"=>803,
"time_zone"=>"America/Los_Angeles"},
"postal"=>{"code"=>"91354"},
"registered_country"=>
{"geoname_id"=>6252001,
"iso_code"=>"US",
"names"=>
{"de"=>"Vereinigte Staaten",
"en"=>"United States",
"es"=>"Estados Unidos",
"fr"=>"États Unis",
"ja"=>"アメリカ",
"pt-BR"=>"EUA",
"ru"=>"США",
"zh-CN"=>"美国"}},
"subdivisions"=>
[{"geoname_id"=>5332921,
"iso_code"=>"CA",
"names"=>
{"de"=>"Kalifornien",
"en"=>"California",
"es"=>"California",
"fr"=>"Californie",
"ja"=>"カリフォルニア州",
"pt-BR"=>"Califórnia",
"ru"=>"Калифорния",
"zh-CN"=>"加州"}}],
"network"=>"76.50.18.0/23"}
If your data lacks information regarding the continent to which the country associated with the IP address belongs, you can query it as follows:
irb(main):015> data["continent"]
=>
{"code"=>"NA",
"geoname_id"=>6255149,
"names"=>{"de"=>"Nordamerika", "en"=>"North America", "es"=>"Norteamérica", "fr"=>"Amérique du Nord", "ja"=>"北アメリカ", "pt-BR"=>"América do Norte", "ru"=>"Северная Америка", "zh-CN"=>"北美洲"}}
irb(main):016> data["continent"]["names"]["en"]
=> "North America"
If you are interested in the networks associated with the IP address, you can also query them in this manner:
irb(main):017> data["network"]
=> "76.50.18.0/23"
Since the data object is a Ruby Hash, we can access any data within it.
Explore Google DNS Server
To demonstrate that the data may vary, let’s examine one of Google’s Public DNS servers and observe what MaxMind displays as its location:
irb(main):019> google = Geocoder.search("8.8.8.8")
=>
[#<Geocoder::Result::Geoip2:0x000000011925b740
...
irb(main):020> google.first
=>
#<Geocoder::Result::Geoip2:0x000000011925b740
@cache_hit=nil,
@data=
#<MaxMindDB::Result:0x000000011925bb00
@raw=
{"continent"=>
{"code"=>"NA",
"geoname_id"=>6255149,
"names"=>{"de"=>"Nordamerika", "en"=>"North America", "es"=>"Norteamérica", "fr"=>"Amérique du Nord", "ja"=>"北アメリカ", "pt-BR"=>"América do Norte", "ru"=>"Северная Америка", "zh-CN"=>"北美洲"}},
"country"=>
{"geoname_id"=>6252001,
"iso_code"=>"US",
"names"=>{"de"=>"Vereinigte Staaten", "en"=>"United States", "es"=>"Estados Unidos", "fr"=>"États Unis", "ja"=>"アメリカ", "pt-BR"=>"EUA", "ru"=>"США", "zh-CN"=>"美国"}},
"location"=>{"accuracy_radius"=>1000, "latitude"=>37.751, "longitude"=>-97.822, "time_zone"=>"America/Chicago"},
"registered_country"=>
{"geoname_id"=>6252001,
"iso_code"=>"US",
"names"=>{"de"=>"Vereinigte Staaten", "en"=>"United States", "es"=>"Estados Unidos", "fr"=>"États Unis", "ja"=>"アメリカ", "pt-BR"=>"EUA", "ru"=>"США", "zh-CN"=>"美国"}},
"network"=>"8.8.8.0/23"}>>
irb(main):021> google.first.city
=> ""
irb(main):022> google.first.country
=> "United States"
As we can see, there are IP addresses, which don’t have further information except the country.
Summary
MaxMind offers a wealth of intriguing information about IP addresses through their GeoIP2 Lite database. There are numerous use cases, such as enhancing security, gaining a deeper understanding of service usage, or simply exploring the world through IPs.
Depending on your specific use case, I advise validating the data before relying on it blindly. While it may be sufficiently accurate for one scenario, it could be inaccurate for another, as this data is built on certain assumptions that may not align with your needs.
Newsletter
See Also
- Working with Legacy Ruby on Rails: spring.gem fork() Crash
- Analyzing SassC::SyntaxError in Ruby on Rails 7.0
- Deploy Ruby on Rails 7.0 to Dokku micro PaaS
- Fixing require LoadError as an example for the matrix gem
- Fix Flaky Rails System Tests caused by slow scrolling or animations