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:
- Increasing and descending digit sequences
- Repeating digits
- Palindromes
- Binary numbers
- The first million domains
- …and probably more!
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)
}
}
}