Using IPinfo's MMDB database with Rust

If you want to use our IPinfo MMDB database in a Rust environment, here are some high-level notes for you:

  • You need to use the Rust MMDB reader library.
  • The Rust MMDB reader crate does not support IPinfo MMDB IP databases out of the box. It is just an MMDB reader library. You have to deserialize/serialize the response from the library.
  • You have to declare a struct based on the IPinfo IP database schema. You can find the database schema in our documentation.
  • The IPinfo Rust Official Library only supports the IPinfo API and does not work with the MMDB database.

Okay, now that we have the fundamentals covered, let’s go through a basic tutorial. You will need Rust, Cargo and all that stuff. For the IPinfo IP database, we will use the IPinfo free IP database, the IP to Country ASN database.

Initializing the Rust project

Initialize the Rust project with the following command with package name being ipinfo_mmdb:

cargo new ipinfo_mmdb
cd ipinfo_mmdb

In your Cargo.toml file, bring in the required crates:

[dependencies]
maxminddb = "0.24.0"
serde = "1.0.197"

Your crate versions can be different. Check out the latest version from their crates.io pages:

After that, you are ready to write some basic code.

Downloading the IPinfo MMDB database

You can use whatever IPinfo IP database you have access to; just make sure you download the MMDB database. We are going to use the IPinfo IP to Country ASN database.

Run the following code from your terminal to download the IP to Country ASN database:

curl -L https://ipinfo.io/data/free/country_asn.mmdb?token=$TOKEN -o country_asn.mmdb

Make sure to replace the $token placeholder with your IPinfo access token. The above command will download the IPinfo IP to Country ASN MMDB database in your directory with the filename country_asn.mmdb

IP Query code in Rust

The [main.rs](http://main.rs) code

use maxminddb::MaxMindDBError;
use std::net::IpAddr;
use serde::{Deserialize, Serialize};

// Declare the struct based on the IPinfo IP database schema
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct IpinfoCountryASN<'a> {
    pub country: Option<&'a str>,
    pub country_name: Option<&'a str>,
    pub continent: Option<&'a str>,
    pub continent_name: Option<&'a str>,
    pub asn: Option<&'a str>,
    pub as_name: Option<&'a str>,
    pub as_domain: Option<&'a str>,
}

fn main() -> Result<(), MaxMindDBError> {
    // Replace with the actual path to your IPinfo IP database MMDB path
    let mmdb_path = "./country_asn.mmdb";

    // Open the MMDB IP database
    let reader = maxminddb::Reader::open_readfile(&mmdb_path)?;

    // Replace with the IP address you want to look up
    let ip_address: IpAddr = "78.35.248.93".parse().expect("Failed to parse IP");

    // Perform the IP address lookup
    let record: IpinfoCountryASN = reader.lookup(ip_address).unwrap();

    // Return IP query response
    println!("Country: {}", record.country.unwrap());
    println!("Country Name: {}", record.country_name.unwrap());
    println!("Continent: {}", record.continent.unwrap());
    println!("Continent Name: {}", record.continent_name.unwrap());
    println!("ASN: {}", record.asn.unwrap());
    println!("AS Organization Name: {}", record.as_name.unwrap());
    println!("AS Domain/Website: {}", record.as_domain.unwrap());
    
    println!("Payload: {:?}", record);

    Ok(())
}

Let’s go through the code block by block.

use statements

use maxminddb::MaxMindDBError;
use std::net::IpAddr;
use serde::{Deserialize, Serialize};
  • maxminddb is the MMDB reader crate. We will also use the MaxMindDBError function to handle IP addresses that are not present in the MMDB database.
  • std::net::IpAddr is from the standard library. We will use it to parse IP addresses from string format to IpAddr format.
  • serde will be used to deserialize and serialize the response from the MMDB IP query response.

Declaring the struct

#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct IpinfoCountryASN<'a> {
    pub country: Option<&'a str>,
    pub country_name: Option<&'a str>,
    pub continent: Option<&'a str>,
    pub continent_name: Option<&'a str>,
    pub asn: Option<&'a str>,
    pub as_name: Option<&'a str>,
    pub as_domain: Option<&'a str>,
}

We will declare a struct using serde. This is based on the IPinfo IP to Country ASN database schema. In the struct declaration, we will include all the IP metadata columns, which do not contain an IP address, as the MMDB reader only returns the IP metadata as a response.

We are using Option<&'a str> for declaring the values for optional string references. Usually, for all our database values, we return a string, except for our privacy detection database, where we return "" (empty string) or true for privacy detection flags (VPN, hosting, proxy, tor, and relay).

Main function

fn main() -> Result<(), MaxMindDBError>

Our main function runs the core lookup logic with errors being caught by MaxMindDBError error type. If the IP address does not exist in the MMDB database or is a bogon IP address, you will see the following error:

thread 'main' panicked at src/main.rs:29:62:
called `Result::unwrap()` on an `Err` value: AddressNotFoundError("Address not found in database")
let mmdb_path = "./country_asn.mmdb";
let reader = maxminddb::Reader::open_readfile(&mmdb_path)?;

Provide the path to the mmdb IP database. Ensure you have the IP database name and path correct, or you will get an IoError. Using the provided path, the MMDB reader crate will convert the MMDB database and create the reader object that will be used to query IP addresses from the MMDB IP database.

let reader = maxminddb::Reader::open_readfile(&mmdb_path)?;
let ip_address: IpAddr = "78.35.248.93".parse().expect("Failed to parse IP address");

This section accepts the input IP address as a string and converts it to the IpAddr data type using the std::net standard library. In this example, we are using the random IP address 78.35.248.93

Then, using the reader object, we look up the input IP address from the MMDB database and store the MMDB response IP metadata in the record variable, which is the type IpinfoCountryASN

    println!("Country: {}", record.country.unwrap());
    println!("Country Name: {}", record.country_name.unwrap());
    println!("Continent: {}", record.continent.unwrap());
    println!("Continent Name: {}", record.continent_name.unwrap());
    println!("ASN: {}", record.asn.unwrap());
    println!("AS Organization Name: {}", record.as_name.unwrap());
    println!("AS Domain/Website: {}", record.as_domain.unwrap());

    println!("Payload: {:?}", record);

Then, we print out the individual values from the record response and the entire payload.

Country: DE
Country Name: Germany
Continent: EU
Continent Name: Europe
ASN: AS8422
AS Organization Name: NetCologne Gesellschaft fur Telekommunikation mbH
AS Domain/Website: netcologne.de

Payload: IpinfoCountryASN { country: Some("DE"), country_name: Some("Germany"), continent: Some("EU"), continent_name: Some("Europe"), asn: Some("AS8422"), as_name: Some("NetCologne Gesellschaft fur Telekommunikation mbH"), as_domain: Some("netcologne.de") }

That is the gist of using our MMDB dataset in Rust.

1 Like

Thanks Abdullah for the amazing guide.
It’ll be helpful for sure.

I’ll come back here in case of further doubts with the implementation.

1 Like

Thank you very much. Really appreciate it!

Hi @Abdullah, I wanted to let you know that I’ve finally implemented this feature in a PR, and that from the next version Sniffnet will finally support IPinfo in addition to standard MMDBs.

I’m writing some unit tests to assert that everything works properly and I’m including sample IPinfo DBs taken from this folder.

I’d like to test both the Country+ASN DB as well as the Country and ASN taken separately.
This is to verify that there is consistency between the two approaches.
However, at least from a first sight, I cannot seem to find an IP address in common between the three different DBs (IP to ASN, IP to Country, IP to Country ASN).
Having the same IP available would allow me to more easily verify the aforementioned consistency.

I was expecting that such databases were featuring the same entries, but apparently that’s not the case.
I think that it’d be nice to include at least one IPv4 and one IPv6 address in common between all the DBs for ease of testing (choosing for example from the most commonly used for samples such as 8.8.8.8).
Maybe it could be useful for other folks as well.

What do you think?

1 Like

Hey Gyuly :wave:, it is so nice to hear from you!

I’ve finally implemented this feature in a PR, and that from the next version Sniffnet will finally support IPinfo in addition to standard MMDBs.

Awesome stuff, dude! Thank you very much!

I’m writing some unit tests to assert that everything works properly and I’m including sample IPinfo DBs taken from this folder.

Perfect. I update this repo weekly. Our IP database, including the free ones, is updated daily. But I think there should be no issues.

I cannot seem to find an IP address in common between the three different DBs (IP to ASN, IP to Country, IP to Country ASN).

Hmmmm… All three IP databases are aggregated, there should be at least something common. I will report back with sample IPs. Off the top of my hand, the DOD IP addresses ( AS721 DoD Network Information Center details - IPinfo.io) are fairly stable in the terms of IP metadata. They never change ASN and they are always based in US.

What do you think?

I think having these “stable universal” IP addresses could be useful. I will look into it and report back.

1 Like

Thank you Abdullah for your prompt response.

My problem isn’ the stability of a given IP in time, since anyway I wouldn’t update the test database files.

My idea was more toward including some IP ranges in common between all the sample databases.
Maybe there’s already some, but at a first sight I wasn’t able to find them.

Anyway this isn’t a blocking issue for me, so don’t worry.

Hey GyulyVGC,

I apologize for the delayed response. I was OOO last week.

However, at least from a first sight, I cannot seem to find an IP address in common between the three different DBs (IP to ASN, IP to Country, IP to Country ASN).
Having the same IP available would allow me to more easily verify the aforementioned consistency.
[…]
My idea was more toward including some IP ranges in common between all the sample databases.

Trying to figure out a solution for it. :thinking:

Here, I download all 3 databases:

I ran a query that looked for the common IP ranges across these three (full) databases, and then I also looked into the ratio between their row counts.

SELECT
  count(*) as ip_count,
  ip_count/(SELECT count(*) FROM country_asn) as country_asn_ratio,
  ip_count/(SELECT count(*) FROM country) as country_ratio,
  ip_count/(SELECT count(*) FROM ipasn) as ipasn_ratio
FROM 
  country c
JOIN 
  country casn ON c.start_ip = casn.start_ip AND c.end_ip = casn.end_ip
JOIN 
  ipasn ipasn ON c.start_ip = ipasn.start_ip AND c.end_ip = ipasn.end_ip
WHERE 
  c.start_ip LIKE '%.%'
ip_count 97884
country_asn_ratio 0.0425198
country_ratio 0.0491014
ipasn_ratio 0.231768

So, the number of common rows between all 3 databases is 4% on the low end. Now, as the sample databases contain only 100 rows, statistically, there are not going to be many, if any, common rows between all three sample databases.

I can inject a common amount of rows in the sample for these queries, but it will complicate the data pipeline for the sample database. Let me know what you think. I will try to think of other options as well.