Mapping the 1.111B XYZ Universe

Generation XYZ, the provider of the TLD .xyz, is running a promotion for a special class of .xyz domains. For just $0.99 a year, you can register any .xyz domain from 000000.xyz up to 999999999.xyz. They call this the “1.111B class.”

They suggest some possible uses for these domains:

… Internet of Things device connectivity, vanity identification, digital currencies, and any other creative uses…

But what’s actually on these domains?

The Results

Out of the total 1.111 billion domains, I scanned over 5 million and found that about 70,000 had active DNS records.

That’s kind of interesting, but what does it look like when we plot them on a grid?

You can explore the results with this interactive map! Try zooming out, right-click and drag to move around. Hover over a domain to see its address and page title (if available). Each highlighted pixel represents a domain with a website!

Some fun patterns emerge, revealing which numbers people seem to value. We see people registering:

Querying DNS Records Quickly

Making a DNS query can be a slow process. I explored a few methods on how to query millions of domains.

Digging

dig is a simple utility to send DNS queries.

dig +short A example.com

would yield example.com’s A records. But while simple, bash is a bit slow.

DNS over HTTPS

Another solution was to use Google’s JSON API for DNS over HTTPS. It’s a public API that let’s you resolve domains and get a simple JSON resonse of records.

You can see it in action here: https://dns.google/resolve?name=example.com.

Unfortunately Google’s public APIs are subject to some strict rate limiting and spamming domains as fast as my computer could would certainly leave me with nothing but a bunch of 429 responses.

Go to the Rescue

Go has replaced a lot of my Python usage when I need a quick script or I’m doing anything network intensive. It’s standard library and concurrency model have been widely regarded as very good to say the least and I could not agree more.

To get started let’s create a goroutine that generates domains.

func xyzDomains() <-chan string {
    out := make(chan string)
    go func() {
		for i := 0; i <= 999999999; i++ {
			var format string
			switch {
			case i <= 999999:
				format = "%06d.%s"
			case i <= 9999999:
				format = "%07d.%s"
			case i <= 99999999:
				format = "%08d.%s"
			default: // i <= 999999999
				format = "%09d.%s"
			}
			out <- fmt.Sprintf(format, i, "xyz")
		}
		close(out)
	}()
	return out
}

We can use github.com/miekg/dns to make DNS queries like this:

m4 := new(dns.Msg)
m4.SetQuestion(dns.Fqdn(domain), dns.TypeA)
in4, _, err4 := client.Exchange(m4, "8.8.8.8:53")

Where 8.8.8.8:53 is Google’s DNS server.

Keeping everything in goroutines is critical for performance. So let’s create a WaitGroup and worker goroutine that will do all the work.

func worker(domains <-chan string, results chan<-string, wg *sync.WaitGroup) {
    defer wg.Done()

    client := new(dns.Client)

    for domain := range domains {
        m4 := new(dns.Msg)
        m4.SetQuestion(dns.Fqdn(domain), dns.TypeA)
        in4, _, err4 := client.Exchange(m4, "8.8.8.8:53")

        m6 := new(dns.Msg)
        m6.SetQuestion(dns.Fqdn(domain), dns.TypeAAAA)
        in6, _, err6 := client.Exchange(m6, "8.8.8.8:53")
        
        result := fmt.Sprintf("%s - ", domain)
        hasRecords := false

        if err4 == nil && len(in4.Answer) > 0 {
            hasRecords = true
            for _, ans := range in4.Answer {
                if a, ok := ans.(*dns.A); ok {
                    result += fmt.Sprintf("%s ", a.A.String())
                }
            }
        } else if err4 != nil {

        }
        
        if err6 == nil && len(in6.Answer) > 0 {
            hasRecords = true
            for _, ans := range in6.Answer {
                if aaaa, ok := ans.(*dns.AAAA); ok {
                    result += fmt.Sprintf("%s ", aaaa.AAAA.String())
                }
            }
        } else if err6 != nil {

        }

        if !hasRecords {
            result = ""
        }

        results <- result
    }
}

Putting everything together we can build a pretty fast client:

package main

import (
    "fmt"
    "log"
    "sync"

    "github.com/miekg/dns"
)

func xyzDomains() <-chan string {
    out := make(chan string)
    go func() {
		for i := 0; i <= 999999999; i++ {
			var format string
			switch {
			case i <= 999999:
				format = "%06d.%s"
			case i <= 9999999:
				format = "%07d.%s"
			case i <= 99999999:
				format = "%08d.%s"
			default: // i <= 999999999
				format = "%09d.%s"
			}
			out <- fmt.Sprintf(format, i, "xyz")
		}
		close(out)
	}()
	return out
}
func worker(domains <-chan string, results chan<-string, wg *sync.WaitGroup) {
    defer wg.Done()

    client := new(dns.Client)

    for domain := range domains {
        m4 := new(dns.Msg)
        m4.SetQuestion(dns.Fqdn(domain), dns.TypeA)
        in4, _, err4 := client.Exchange(m4, "8.8.8.8:53")

        m6 := new(dns.Msg)
        m6.SetQuestion(dns.Fqdn(domain), dns.TypeAAAA)
        in6, _, err6 := client.Exchange(m6, "8.8.8.8:53")
        
        result := fmt.Sprintf("%s - ", domain)
        hasRecords := false

        if err4 == nil && len(in4.Answer) > 0 {
            hasRecords = true
            for _, ans := range in4.Answer {
                if a, ok := ans.(*dns.A); ok {
                    result += fmt.Sprintf("%s ", a.A.String())
                }
            }
        } else if err4 != nil {

        }
        
        if err6 == nil && len(in6.Answer) > 0 {
            hasRecords = true
            for _, ans := range in6.Answer {
                if aaaa, ok := ans.(*dns.AAAA); ok {
                    result += fmt.Sprintf("%s ", aaaa.AAAA.String())
                }
            }
        } else if err6 != nil {

        }

        if !hasRecords {
            result = ""
        }

        results <- result
    }
}

func main() {
    outputFile := "xyz_results.txt"

    domainChan := xyzDomains()
    resultChan := make(chan string)

    file, err := os.Create(outputFile)
	if err != nil {
		log.Fatalf("Error creating output file: %v", err)
	}
	defer file.Close()


    var wg sync.WaitGroup
    for i := 1; i <= 100; i++ {
        wg.Add(1)
        go worker(domainChan, resultChan, &wg)
    }

    go func() {
        wg.Wait()
        close(resultChan)
    }()

    for result := range resultChan {
        _, err := file.WriteString(result)
		if err != nil {
			log.Printf("Error writing to file: %v", err)
		}
    }
}