Asynchronous Domain Enumerator in Rust
In this post I will show you a Rust application to enumerate domains asynchronously using DNS queries with the A and AAAA DNS record types to identify if the domain is resolving or not. This can be useful to identify if an domain is available or enumerate domains or subdomains via brute forcing.
This also could be performed with tools like massdns or bash scripts using the dig or host commands, but I wanted to learn how to do a basic version of that kind of tools with Rust.
Example to look up for the example.com domain:
1
2
3
4
5
dig +short example.com
93.184.216.34
dig +short asdfasdf.io # No IP address found
Using the dig tool is possible to program a shell script to produce the next json output:
1
2
3
4
[
{ "domain": "example.io", "resolved": true },
{ "domain": "asdfasdf.io", "resolved": false }
]
However, I will do this with Rust performing two DNS queries with the following DNS Record types:
- A: IPv4 address
- AAAA: IPv6 address
The code is available on github.
Sequence Diagram
The application is composed of following parts:
- Domain: Stores the domain and its resolved status.
- DomainNames: Stores the resolved domains and serialize them to JSON.
- DomainGenerator: Reads names from a file and combine them with the top level domain to build a list of domain names.
- AsyncDomainEnumerator: Enumerates the domain names asynchronously in batches of 20 and serializes the results to JSON.
The following sequence diagram shows the components and how they interact with each other.

Domain
The Domain struct defines two fields: name and resolved. Additionally, this also provides a new function to create a Domain instance and uses the Serialize and Deserialize traits from the serde crate.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[derive(Serialize, Deserialize)]
struct Domain {
name: String,
resolved: bool,
}
impl Domain {
fn new(name: String, resolved: bool) -> Self {
Self {
name: name,
resolved: resolved,
}
}
}
DomainNames
The DomainNames struct has a single field called domains of type Vec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct DomainNames {
domains: Vec<Domain>,
}
impl DomainNames {
fn new() -> Self {
Self {
domains: Vec::<Domain>::new(),
}
}
fn add(&mut self, domain: Domain) {
self.domains.push(domain);
}
fn to_json(&mut self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&self.domains)
}
}
DomainGenerator
This struct has methods for creating a new instance as well as generating valid domains from a file with names combined with the provided top level domain. As result, this returns a vector storing the generated domains.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
impl DomainGenerator {
fn new(path: String, top_level: String) -> Self {
Self {
path: path,
top_level: top_level,
}
}
fn generate_domains(&self) -> io::Result<Vec<String>> {
let file_path = Path::new(&self.path);
let file = File::open(&file_path)?;
self.words_to_domains(BufReader::new(file))
}
fn words_to_domains<R: BufRead>(&self, reader: R) -> io::Result<Vec<String>> {
let mut domains = Vec::new();
for line in reader.lines() {
let name = line?;
if name.is_empty() {
continue;
}
let domain = format!("{}.{}", name, self.top_level);
if self.valid_domain(&domain) {
domains.push(domain);
}
}
Ok(domains)
}
fn valid_domain(&self, domain: &String) -> bool {
let regex_pattern = r"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$";
let regex = Regex::new(regex_pattern).unwrap();
regex.is_match(domain)
}
}
AsyncDomainEnumerator
This Rust code defines a struct used for asynchronously resolving multiple domain names based on the rsdns crate. It uses the list of generated domains, the maximum number of asynchronous dns queries, and the data structure called DomainNames to store domain and its status once it has been validated.
The provided functions will perform the following actions:
- Initializing a new instance of AsyncDomainEnumerator.
- Resolving domains using asynchronous operations in batches of 20.
- Returns the results as a JSON string.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
struct AsyncDomainEnumerator {
domains: Vec<String>,
max_async_queries: u32,
resolved_domains: DomainNames,
}
impl AsyncDomainEnumerator {
fn new(domains: Vec<String>) -> Self {
Self {
domains: domains,
max_async_queries: 20,
resolved_domains: DomainNames::new(),
}
}
fn resolve_domains(&mut self) {
let rt = tokio::runtime::Runtime::new().unwrap();
for domains in self.domains.chunks(self.max_async_queries as usize) {
let now = Local::now();
println!("--- Verifying {} domains at {:?} ---", domains.len(), now);
let verified_domains = rt.block_on(self.async_resolve_domains(domains));
for domain in verified_domains {
println!("domain: {}, resolved:{}", domain.name, domain.resolved);
self.resolved_domains.add(domain);
}
}
}
fn as_json(&mut self) -> Result<String, serde_json::Error> {
self.resolved_domains.to_json()
}
async fn async_resolve_domains(&self, domains: &[String]) -> Vec<Domain> {
let mut futures = Vec::new();
for domain in domains {
let f = self.resolve_domain(domain.to_string());
futures.push(f);
}
let results = join_all(futures).await;
results
}
async fn resolve_domain(&self, qname: String) -> Domain {
let ip_addr_and_port = "8.8.8.8:53";
let nameserver: SocketAddr = ip_addr_and_port
.parse()
.expect("Unable to parse socket address");
let config = ClientConfig::with_nameserver(nameserver);
let mut client = Client::new(config)
.await
.expect("Unable to create DNS client");
let rrset = client.query_rrset::<A>(qname.as_str(), Class::In).await;
let rrset_ipv6 = client.query_rrset::<Aaaa>(qname.as_str(), Class::In).await;
Domain::new(qname, rrset.is_ok() || rrset_ipv6.is_ok())
}
}
Main
The main function defines a command-line interface using the clap crate. It expects two or three arguments:
- Path of file with names
- Top level domain of your choice
- Output path (optional)
The main function parses the command-line arguments, initializes a DomainGenerator, generates domains, resolves them asynchronously using AsyncDomainResolver, then converts the results to JSON, and writes it to the specified output file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let args = Args::parse();
let generator = DomainGenerator::new(args.names_path, args.top_level);
match generator.generate_domains() {
Ok(domains) => {
let mut async_resolver = AsyncDomainResolver::new(domains);
async_resolver.resolve_domains();
if let Ok(json) = async_resolver.as_json() {
let output_path = args.output_path;
fs::write(output_path.clone(), json).expect("Unable to write file");
println!("Output file: {}", output_path);
} else if let Err(e) = async_resolver.as_json() {
eprintln!("Failed to convert to JSON: {}", e);
}
}
Err(e) => eprintln!("Error occurred: {}", e),
}
}
How to get the code
1
git clone https://github.com/karmatr0n/domain_enumerator.git
How to compile the application
1
cargo build --release
How to install it
1
cargo install --path .
How to run it
1
2
3
4
5
6
7
8
9
10
$HOME/.cargo/bin/domain_enumerator -n dicts/wordlist -t io -o results.json
or
$HOME/.cargo/bin/domain_enumerator -n dicts/wordlist -t io
or
$HOME/.cargo/bin/domain_enumerator --help
Example output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[
{
"name": "qwerty.io",
"resolved": true
},
{
"name": "iloveu.io",
"resolved": true
},
{
"name": "michelle.io",
"resolved": true
},
{
"name": "tigger.io",
"resolved": true
},
{
"name": "sunshinexabc.io",
"resolved": false
},
{
"name": "chocolate.io",
"resolved": true
},
{
"name": "password1.io",
"resolved": false
}
]
Conclusion
This article demonstrates how to resolve domains asynchronously using Rust, and it has been one of my favorite recent personal projects so far. Writing this small application has taught me about asynchronous programming in Rust, which is one of the concurrent programming models supported by this language. It also allow me to combine this knowledge with my interest in network protocols.
I hope you enjoyed reading it and learned something new.
References
- rust-lang for the Rust programming language.
- Asynchronous Programming in Rust for asynchronous programming in Rust.
- clap for the command-line interface.
- tokio for the asynchronous runtime.
- rsdns for the Asynchronous DNS client.
- chrono for working with date and time.
- serde for serializing the domain names to JSON.