MX and Host lookup using the trust-dns-resolver Crate

I used to often work with DNS in one of my previous jobs. So I have always had a long running interesting in DNS. Most recently I have been looking into Rust. I wanted to see how easy it might be to use rust to access DNS records, one; because rust is said to be fast, and also because it’s a safe programming language.

This will be a brief write up at my attempt to use trust-dns-resolver to do MX record lookups and subsequently host address lookups.

Warning: This code is not intended to be used in production. You should review and adjust to your own needs.

Getting Started

First we will need to create our development environment.

cargo new trust-dns-resolver && cd $_

This will give us our standard rust directly structure. We need to add our crate to the Cargo.toml

-snip-
[dependencies]
trust-dns-resolver = "0.20.1"

Next edit the src/main.rs as follows. This code was taken from the crate documentation with a couple of minor edits to get to compile.

use std::net::*;
use trust_dns_resolver::config::*;
use trust_dns_resolver::Resolver;

fn main() {
  // Construct a new Resolver with default configuration options
  let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default()).unwrap();

  // Lookup the IP addresses associated with a name.
  let response = resolver.lookup_ip("www.example.com.").unwrap();

  // There can be many addresses associated with the name,
  //  this can return IPv4 and/or IPv6 addresses
  let address = response.iter().next().expect("no addresses returned!");
  if address.is_ipv4() {
      assert_eq!(address, IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34)));
  } else {
      assert_eq!(address, IpAddr::V6(Ipv6Addr::new(0x2606, 0x2800, 0x220, 0x1, 0x248, 0x1893, 0x25c8, 0x1946)));
  }

}

Compile and run the code to make sure everything is ok. There should be no output.

cargo run

Assuming this has worked and we got a clean build and run we can move onto digging up MX records.

Changing the code to get MX records

Lets replace the entire code with the following:

use trust_dns_resolver::config::*;
use trust_dns_resolver::Resolver;

fn main() {
    // Construct a new Resolver with default configuration options
    let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default()).unwrap();

    // Lookup the IP addresses associated with a name.
    // The final dot forces this to be an FQDN, otherwise the search rules as specified
    //  in `ResolverOpts` will take effect. FQDN's are generally cheaper queries.
    let mx_response = resolver.mx_lookup("hotmail.com.");

    // There can be many addresses associated with the name,
    //  this can return IPv4 and/or IPv6 addresses
    //let address = response.iter().next().expect("no addresses returned!");
    //println!("{}", address);

    match mx_response {
        Err(_) => println!("No Records"),
        Ok(mx_response) => {
            let addresses = mx_response.iter();
            for record in addresses {
                println!("{} {}", record.preference(), record.exchange());
                let host_name = record.exchange();
                let lookup_response = resolver.lookup_ip(host_name.to_string().as_str()).unwrap();
                let addr_list = lookup_response.iter();
                for addr in addr_list {
                    if addr.is_ipv4() {
                        println!("\tip4: {}", addr)
                    } else {
                        println!("\tip6: {}", addr)
                    }
                }
            }
        }
    }
}

Explaining the code

Let’s take a look at what we are doing here.
First we create a resolver which will do the work of doing the DNS lookups.
Next we use the resolver to call mx_lookup() and store the result into mx_response
mx_response will contain either and MXLookup or an Err. For this reason we need to handle these two cases. Here I will use match.
In the case of Err. I do nothing and just report there were no records.
In the case of MXLookup, I will need to do some more processing.

  • First lets make an iter() out of mx_response and loop over it.
  • In the loop we get the preference() or MX weight and the exchange() or DNS record for the host.
  • I am taking a short cut in the next step where I look up the ip address. I am assuming that because there is an MX host record, there will be an ip address. This could be a false assumption due to misconfiguration, so I will address that later. For now I will simply do a lookup_ip() and trust that unwrap() will no panic.
  • Again I convert the lookup_response to an iter() and loop over the result.

The output if this code is:

Finished dev [unoptimized + debuginfo] target(s) in 0.28s
 Running `target/debug/trust-dns`  
2 hotmail-com.olc.protection.outlook.com.  
ip4: 104.47.55.33  
ip4: 104.47.58.33  

Preference: 2
Exchange: hotmail-com.olc.protection.outlook.com.
This exchange host resolves to two ip addresses. Both of which are IPv4
If we change the code a little and replace hotmail.com with gmail.com we get:

10 alt1.gmail-smtp-in.l.google.com.
	ip4: 74.125.137.26
5 gmail-smtp-in.l.google.com.
	ip4: 108.177.125.27
20 alt2.gmail-smtp-in.l.google.com.
	ip4: 142.250.138.27
30 alt3.gmail-smtp-in.l.google.com.
	ip4: 173.194.199.26
40 alt4.gmail-smtp-in.l.google.com.
	ip4: 209.85.145.26

Though I know gmail has IPv6 addresses, I am currently not getting them. I will have to see if I am missing something. I do actually expect to get them.

Improving on the Code

Let’s make things a little safer by remove the unwrap() To keep things simpler, I will just provide the updated match code here.

match mx_response {
    Err(_) => println!("No Records"),
    Ok(mx_response) => {
        let records = mx_response.iter();
        for record in records {
            println!("{} {}", record.preference(), record.exchange());
            let lookup_response = resolver.lookup_ip(record.exchange().to_string().as_str());
            match lookup_response {
                Err(_) => println!("This exchange host has no address."),
                Ok(lookup_response) => {
                    let ip_addrs = lookup_response.iter();
                    for ip_addr in ip_addrs {
                        if ip_addr.is_ipv4() {
                            println!("   ip4: {}", ip_addr)
                        } else {
                            println!("   ip6: {}", ip_addr)
                        }
                    }
                }
            }
        }
    }
}

Please note I have update some of the variable names and we now have nested match statements. But the code code generates the same output as in the previous example.

I hope you find this interesting and useful.


See also