Lookup MX Records using Rust & trust-dns-resolver
Tags: rust, dns, mx, crate
Published: 2021-03-25 13:19:54

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](https://en.wikipedia.org/wiki/Rust_(programming_language)). 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](https://crates.io/crates/trust-dns-resolver) to do [MX](https://en.wikipedia.org/wiki/MX_record) 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.
```zsh
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`  
```zsh
-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.

```rust
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.
```zsh
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:
```rust
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:
```zsh
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:  
```zsh
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.

```rust
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.