WebMap – Teil 1: Das Internet kartografieren

Die Idee

Nachdem ich vor kurzem herausfinden wollte, was für Domains auf einem Shared Hosting Server liegen und feststellen musste, dass das nicht ohne Weiteres möglich ist, stieß ich bei der Recherche auf viele Seiten (z. B.: https://www.whoisxmlapi.com/) welche kuratierte Daten von Domains zum Kauf anbieten.

Somit kam die Idee auf, das Internet zu crawlen und eine Liste aller registrierten Domains zu erstellen. Was genau dabei gesammelt wird, ob es nur Domains, Sub-Domains oder auch Inhalte der Webseiten sind, lässt sich beliebig ausbauen. Mit dem Beginn dieser Artikelserie soll jeder Blogbeitrag den Fortschritt des Projektes dokumentiert. Dabei kommt, wie in den vorherigen Artikeln, Golang zum Einsatz.

Wichtiger Hinweis

Es gilt darauf zu achten, Programme wie Web Crawler, verantwortungsvoll und unter großer Sorgfalt einzusetzen. Das Massenhafte aufrufen von Webseiten kann zu hohen Lasten und erhöhtem Traffic führen, Log-Einträge fluten oder andere Ressourcen belasten, welche auch zu einem Denial-of-Service führen können.

Das Projekt einrichten

In einem Artikel wurden bereits Go Modules vorgestellt. Mittlerweile wurde auch der "How to Write Go Code"-Beitrag auf der golang.org Seite angepasst. Das Projekt wird dementsprechend unter Verwendung von Go Modules aufgesetzt. Der Quellcode steht auf Github zur Verfügung.

Ansätze

Um eine Liste aller Domains zu erstellen bedarf es zunächst eines Startpunkts, an dem begonnen werden kann. Hierfür wurden drei Ansätze ausgewählt, die verfolgt werden sollen:

  1. Domains automatisch generieren (a.de, b.de, c.de, …)
  2. Domain Seeder-Liste (händisch) erstellen
  3. Suchmaschinen mit Wörtern befüllen & Ergebnisseiten auslesen

Ansatz 1
Hierbei werden aus allen möglichen Buchstaben-Kombinationen Domainnamen erzeugt. Es ist davon auszugehen, dass gerade Domains mit einer Länge von 1–3 Buchstaben in allen möglichen Kombinationen vorkommen.

Vorteile Nachteile
Ist schnell umzusetzen. Erzeugt ab einer gewissen Länge keine zuverlässigen Domainnamen mehr.
Erzeugt nur einen Request je Domain.

Ansatz 2
Bei einer Seeder-Liste handelt es sich um eine händisch zusammengestellte Liste aus Domainnamen. Alle Domains aus dieser Liste können anschließend gecrawlt werden. Domains, welche wiederum auf diesen Seiten vorkommen, können ebenfalls gecrawlt werden, um so den Bestand von Domains sukzessiv zu erweitern.

Vorteile Nachteile
Verwendet nur Domains, die es auch gibt. Eine Domain wird unter Umständen vielfach aufgerufen.
Seeder-Liste muss möglichst groß sein.
Domains auf der Seeder-Liste müssen auf ihren Seiten möglichst viele Verweise auf andere Domains haben.

Ansatz 3
Ähnlich wie bei der Seeder-Liste wird hier eine Liste mit Wörtern oder Sätzen zusammengestellt. Wörter aus dieser Liste werden in eine Suchmaschine eingetragen. Als Nächstes müssen die Ergebnisseiten gecrawlt werden, um die Domains zu extrahieren. Diese Domains können zusätzlich für den zweiten Ansatz verwendet werden.

Vorteile Nachteile
Kombination aus zwei Ansätzen sorgt für mehr Diversität. Suchmaschine wird mit vielen Requests befeuert.
Generische Wörter sorgen in Suchmaschinen für Millionen von Einträgen und führen somit zu vielen Domains. Etwas aufwendiger, da nicht nur die erste Ergebnisseite der Suchmaschine gecrawlt werden sollte. Es müssen die Gegebenheiten der Suchmaschine beachtet werden.
Generische Wörter sind einfacher zu finden als gute Domains für Ansatz 2.

Umsetzung

Um eine möglichst große Anzahl an Domains abzudecken, macht die Kombination aus mehreren Ansätzen am meisten Sinn. Gestartet wird mit dem ersten Ansatz, da dieser am einfachsten umzusetzen ist und somit schnell zu ersten Ergebnissen führt. Des Weiteren wird sich zunächst auf .de-Domains beschränkt, welche nur aus Buchstaben von a bis z bestehen. Die Generation der Domainnamen kann später beliebig erweitert werden.

Aufbau des Projektes

Das Projekt besteht im Wesentlichen aus zwei Teilen:

  1. Generators
    Während der Umsetzung des ersten Ansatzes sorgt der Generator dafür, alle möglichen Kombinationen aus Buchstaben zu erzeugen und daraus Domainnamen zu generieren. Für Ansatz 3 könnte ein Generator zum Beispiel URLs für Suchmaschinen erzeugen.
  2. Fetchers
    Diese laden Informationen zu einer gegebenen Domain, was im ersten Schritt die IP-Adresse ist.

Ein bisschen Code

Domain Generator
Zu Testzwecken wird das Alphabet, aus denen Domains generiert werden sollen, auf drei Buchstaben (a, b, c) und eine maximale Domainlänge von drei Buchstaben reduziert. Im folgenden Code-Ausschnitt ist der Domain-Generator zu sehen. Der Einstiegspunkt ist die Funktion GenerateDomains. Da die Funktion mit einem Großbuchstaben beginnt, wird sie exportiert und kann von außen aufgerufen werden. Darauf folgt die Ausführung der Funktion combinations, welche Rekursiv arbeitet und alle möglichen Kombinationen aus dem Alphabet erstellt. Die Abbruch-Bedingung für den rekursiven Durchlauf wird anhand der Zeichenlänge der generierten Domains bestimmt. Innerhalb der combination-Funktion wird jede generierte Domain in den mitgegebenen Channel geschrieben. Dieser kann an anderer Stelle (siehe Main) ausgelesen werden, um die generierte Domain dort weiterzuverarbeiten.

package generators

var alphabet = []string{"a", "b", "c"}

var tlds = [...]string{"de"}

var maxDomainLength = 3

func GenerateDomains(domainsChannel chan string) {
    combinations("", domainsChannel)
    close(domainsChannel)
}

func combinations(prefix string, ch chan string) {
    for i := 0; i < len(alphabet); i++  {
        domain := prefix + alphabet[i]

        if len(domain) > maxDomainLength {
            return
        }

        for j := 0; j < len(tlds); j++ {
            ch <- domain + "." + tlds[j]
        }

        combinations(domain, ch)
    }
}

In der folgenden Abbildung ist zu sehen, welche Domains bei einem Alphabet von a, b und c, sowie einer maximalen Länge von drei Buchstaben erzeugt werden.

Zur besseren Verständlichkeit ist dargestellt, an welcher Stelle das Präfix zum Einsatz kommt. Auch wird angeben, wie viele Kombinationen an Domains erzeugt werden. Wichtig ist dabei, dass anders, als zum Beispiel beim Heap’s Algorithmus, nicht nur alle Kombinationen des gesamten Alphabets im Array erzeugt werden, sondern auch jede Teilmenge. So werden auch Domainnamen wie a.de oder aa.de erzeugt. Auch handelt es sich bei den beiden Domains ab.de und ba.de nicht um Duplikate, sondern um zwei unterschiedliche Domains.

Wird nun das gesamte Alphabet verwendet, sowie alle Ziffern, lässt sich schnell errechnen, dass viele Million Kombinationsmöglichkeiten existieren. Wobei einer Domain zwischen jedem Buchstaben und jeder Zahl zusätzlich Bindestriche eingesetzt werden können. (Bei 26 Buchstaben [a-z], 10 Ziffern [0-9] und einer Domainlänge von maximal 5 Buchstaben sind das 36^1+36^2+36^3+36^4+36^5 = 62.193.780 Kombinationen, welche generiert werden. Wobei hier sogar noch die möglichen Bindestriche vernachlässigt wurden.)

Main
In dem main-File wird das Programm gestartet, wie der folgende Code darstellt.

package main

import (
    "github.com/timoisik/web-map/fetchers"
    "github.com/timoisik/web-map/generators"
)

func main() {
    domainsChannel := make(chan string)
    go generators.GenerateDomains(domainsChannel)

    for domain := range domainsChannel {
        fetchers.FetchDomainIp(domain)
    }
}

Zunächst wird der Channel erstellt, in diesen werden die generierten Domains innerhalb des Generators geschrieben. Nachdem der Generator als Goroutine gestartet wurde, werden die Domains so lange aus dem Channel gelesen, bis dieser im Generator geschlossen wird. Die Domain wird nun an den Fetcher weitergeleitet, wo die IP-Adresse geladen werden soll.

Fetcher
Im Fetcher wird zunächst nur ein Lookup der IP-Adresse durchgeführt. Dies reicht allerdings noch nicht aus, um alle registrierten Domains zu speichern, da nicht für jede Domain A oder AAAA Records in den DNS Einstellungen hinterlegt sein müssen. Für den Beginn werden jedoch keine weiteren Überprüfungen durchgeführt.

package fetchers

import (
    "fmt"
    "net"
)

func FetchDomainIp(domain string) {
    ip, err := net.LookupIP(domain)

    if err != nil {
        fmt.Printf("No ip for domain %v\n", domain)
        return
    }

    fmt.Printf("Domain %v exists at %v\n", domain, ip)
}

Ausführung des Programms
Die Ausführung wird mittels go run main.go gestartet und führt zur folgenden Ausgabe.

Domain a.de exists at [137.74.127.233]
Domain aa.de exists at [138.201.192.58]
Domain aaa.de exists at [103.224.182.245]
Domain aab.de exists at [91.222.46.20]
Domain aac.de exists at [23.236.62.147]
Domain ab.de exists at [81.28.232.71]
Domain aba.de exists at [194.126.208.59]
Domain abb.de exists at [138.225.2.74]
Domain abc.de exists at [213.238.32.179]
No ip for domain ac.de
Domain aca.de exists at [81.169.145.85 2a01:238:20a:202:1085::]
No ip for domain acb.de
Domain acc.de exists at [88.198.52.112]
Domain b.de exists at [95.130.17.35]
Domain ba.de exists at [128.1.131.238]
Domain baa.de exists at [62.116.175.65]
Domain bab.de exists at [178.250.10.144 212.53.129.42]
Domain bac.de exists at [62.220.18.183]
Domain bb.de exists at [62.91.40.140]
No ip for domain bba.de
Domain bbb.de exists at [194.153.190.185]
Domain bbc.de exists at [212.58.249.206 212.58.249.207 212.58.244.210 212.58.244.129 2001:41c1:4007::bbc:7 2001:41c1:4007::bbc:5 2001:41c1:4007::bbc:8 2001:41c1:4007::bbc:6 2001:41c1:4008::bbc:5 2001:41c1:4008::bbc:6]
Domain bc.de exists at [217.111.9.230]
No ip for domain bca.de
Domain bcb.de exists at [212.77.224.36]
No ip for domain bcc.de
Domain c.de exists at [137.74.127.233]
Domain ca.de exists at [62.93.246.131 195.246.174.134]
Domain caa.de exists at [212.86.204.177]
Domain cab.de exists at [213.144.1.197]
Domain cac.de exists at [103.224.182.245]
Domain cb.de exists at [217.160.233.197]
No ip for domain cba.de
Domain cbb.de exists at [176.28.33.83 2a01:488:42:1000:b01c:2153:ffd5:a66d]
Domain cbc.de exists at [194.36.43.40]
Domain cc.de exists at [213.221.105.180]
Domain cca.de exists at [80.86.80.53]
Domain ccb.de exists at [212.77.229.174]
Domain ccc.de exists at [195.54.164.39 2001:67c:20a0:2:0:164:0:39]

Hier ist zu sehen, dass für die meisten Domains eine IP-Adresse gefunden wird. Wird keine IP-Adresse gefunden, kann das mehrere Gründe haben. Einer ist natürlich, dass die Domain noch nicht vergeben ist. Das dürfte bei sehr kurzen Domainnamen, wie hier, nicht der Fall sein. Stattdessen können die DNS-Einstellungen für eine Domain so gesetzt sein, dass der Aufruf mit vorangestelltem www unterschiedliche Resultate liefert. Dies lässt sich am Beispiel von ac.de mithilfe des Programms dig zeigen.

$ dig -t A ac.de

; <<>> DiG 9.10.6 <<>> -t A ac.de
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58617
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;ac.de.				IN	A

;; AUTHORITY SECTION:
ac.de.			1799	IN	SOA	dns11.netcologne.de. hostmaster.netcologne.de. 2018012501 10800 3600 3600000 3600

;; Query time: 78 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Mon Jan 20 19:42:14 CET 2020
;; MSG SIZE  rcvd: 98

Wie in der Ausgabe zu sehen ist, führt die Abfrage nach einem A-Record für die Domain ac.de zu keinem Ergebnis. Wohingegen die Abfrage mit vorangestelltem www einen A-Record liefert.

$ dig -t A www.ac.de

; <<>> DiG 9.10.6 <<>> -t A www.ac.de
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12539
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;www.ac.de.			IN	A

;; ANSWER SECTION:
www.ac.de.		21599	IN	A	212.66.130.10

;; Query time: 66 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Mon Jan 20 19:43:53 CET 2020
;; MSG SIZE  rcvd: 54

Es ist durchaus sinnvoll, alle Domains mit und ohne vorangestelltem www zu prüfen, da dies, je nach DNS-Einstellungen, zu unterschiedlichen Resultaten führen kann. Wird weder bei der einen, noch der anderen Variante ein Ergebnis für eine Domain geliefert, kann es sein, dass keine A-Records für eine Domain gesetzt sind. In diesem Fall kann zusätzlich geprüft werden, ob ein SOA Eintrag gesetzt ist. Können für eine Domain gar keine DNS-Einträge gefunden werden, besteht die Möglichkeit, einen Domainanbieter zu Crawlen. Dieser Ansatz ist zwar aufwendiger, führt aber zu einer genauen Antwort, da die Domainanbieter zur Überprüfung der Domain die Schnittstellen der Domain-Registrare Anfragen.

Schlusswort

Um ein Projekt wie dieses umzusetzen ist es notwendig, mehrere Ansätze zu Entwickeln. Jeder Ansatz für sich generiert oder findet Domains, welche zusätzlich einem weiteren Ansatz als Startpunkt dienen können.
Dabei muss stets darauf geachtet werden, dass nicht durch eine vielfache Anzahl von Requests ein Server derart belastet wird, dass an irgendeiner Stelle ein Schaden entsteht.

Kommentare anzeigen