Anfragen an Nameserver heißt senden eines User Datagram mit einer Binary und auch die Antwort ist eine Bytesequenz
Ein interessantes Entwicklerwerkzeug ist das bekannte Programm Wireshark, damit können wir kontrollieren ob unser Perl-Script oder das C-Programm richtig arbeitet. Werfen wir als Erstes einen Blick in den Sniffer und machen uns mit ein paar Kenngrößen vertraut.
Der Screenshot zeigt die Anfrage von der Transaction ID 0x0ebc bis hin zur Query, das ist der Header, der 6 Felder mit jeweils 16 Bit umfasst. Zum Header gehören weiterhin die Flags, siehe Bild. Sämtliche Flags sind in einem 16-Bit Headerfeld zusammengefasst, als Zahl ausgedrückt 0x0100 Standard Query.
Des Weiteren gehören zum Header die Felder Questions: 1, Answer RRs: 0, Authority RRs: 0 und Additional RRs: 0. All diese Zahlen finden wir in untestehendem Perlcode wieder.
Die Anfrage (Query) nun enthält den abzufragenden Namen (example.org), ist von Type A, Class IN, das heißt, es wird der A-Record abgefragt, also die IP-Adresse soll ermittelt werden. Während Type und Class auf 16-Bit-Integer abgebildet werden, wird mit dem Namen anders verfahren, mehr dzu später im Code.
Die Response ist im Prinzip genauso aufgebaut wie der Request, nur mit dem Unterschied, daß die Flags anders ausfallen und daß an Header und Anfrage (Query) eine Antwort (Answer) angehängt ist. Ganz oben findet sich auch die ID wieder welche beim Request in den Header eingebaut wurde.
Des Weiteren ist kodiert, wieviele Antworten es gibt, im Beispiel gibt es eine, Answer RRs: 1.
Im Folgenden sei ein einfaches Perlscript vorgestellt, welches die Anfrage an den Nameserver sendet und die Antwort, also die IP-Adresse ausgibt. Wir benötigen eine IP-Adresse für den Nameserver, im LAN können wir dafür die IP-Adresse des Gateway verwenden, der die Anfrage ins öffentliche Netz weiterreicht.
use strict; use warnings; use IO::Socket; my $ns = "10.0.0.1"; # Nameserver my $qname = "example.org"; # Erstelle ein UDP Socket für den NS und Port my $sh = IO::Socket::INET->new( PeerAddr => $ns, PeerPort => 53, Proto => "udp", ) or die "Kein Socket!\n"; # Angaben für den DNS Header my $id = $$; my $flags = 0x0100; # Recursion in Query my $header = pack("n6", $id, $flags, 1, 0, 0, 0); # Query erzeugen my $req = packreq($qname); # Request senden $sh->print($header.$req); # Response empfangen und Ergebnis ausgeben my $res; recv($sh, $res, length($header.$req) + 24, 0); my @octs = unpack "C*", $res; # Ergebnis dezimal und hexadezimal printf( "%d.%d.%d.%d\n %02X %02X %02X %02X", $octs[-4], $octs[-3], $octs[-2], $octs[-1], $octs[-4], $octs[-3], $octs[-2], $octs[-1] ) ; # Diese Funktion erzeugt die Query # split into labels example com # Längen 7 3 sub packreq{ my $qname = shift; my $req = ""; my @q = split /\./, $qname; foreach my $p(@q){ $req .= pack("Ca*", length($p), $p); } # ein Nullbyte, type A, class IN anhängen $req .= pack("Cnn", 0,1,1); return $req; }
Zu den Längenangaben kommen wir später.
#define NAMESERVER "10.0.0.1" #define PORT 53 #include <stdio.h> #include <stdint.h> #include <string.h> #include <stdlib.h> #include <fcntl.h> #include <windows.h> #include <winsock2.h> typedef uint8_t Oct; typedef uint32_t Vax; typedef uint16_t Sho; // IP Addr Port SOCK_DRAM int getSocket(char *host, int port, const int type){ WSADATA wsa; if (WSAStartup(MAKEWORD(2,2),&wsa) != 0){ fprintf(stderr, "Failed. Error Code : %d",WSAGetLastError()); return -1; } struct sockaddr_in server; // IPAddr, Addressfamily und Port server.sin_addr.s_addr = inet_addr(host); server.sin_family = AF_INET; server.sin_port = htons( PORT ); SOCKET sh; if((sh = socket(AF_INET , SOCK_DGRAM , 0 )) == INVALID_SOCKET){ fprintf(stderr, "Could not create socket : %d" , WSAGetLastError()); return -1; } if (connect(sh , (struct sockaddr *)&server , sizeof(server)) < 0){ fprintf(stderr, "connect error"); return -1; } return sh; } // Serialize Nameserver Query int SerializeNameserverQuery(char *query, char *qname, Sho type, Sho class){ char *tok = "."; char *pch = NULL; pch = strtok(qname, tok); int i = 0; for( ; pch != NULL; pch = strtok(NULL, tok)){ Oct len = strlen(pch); query[i] = len; memcpy(&query[i+1], pch, len); i += 1 + len; } query[i] = 0; // Terminieren // Appending Type, Class query[i+1] = type >> 8 & 0xFF; query[i+2] = type & 0xFF; query[i+3] = class >> 8 & 0xFF; query[i+4] = class & 0xFF; int qlen = i + 5; // Länge der Query return qlen; } int main(int argc , char *argv[]){ SOCKET Sock = getSocket(NAMESERVER, PORT, SOCK_DGRAM); Sho h[6] = { htons(0xACDC), // Identyfier htons(0x0100), // Standard Query htons(0x0001), // One Question 0, 0, 0 // Answer RRs, Authority RRs, Additional RRs }; char query[512]; char qname[] = "example.org"; int qlen = SerializeNameserverQuery(query, qname, 1, 1); char req[qlen + 12]; memcpy(&req[0], (char*)h, 12); memcpy(&req[12], query, qlen); int xsend = send(Sock, req, qlen + 12, 0); // Response auswerten Oct res[512]; int xres = recv(Sock, res, 512, 0); // Server failure in header erkennen // z.B. 0x8582 in Flags ist ein Fehler // 0x8180 ist OK if( (res[2] != 0x81) || (res[3] != 0x80)) { fprintf(stderr, "Server failure! Flags 0x%02X%02X", res[2], res[3]); return 1; } Oct ip[4]; // in res[12+qlen+10] und res[12+qlen+11] // steht die Länge der IPv4 Addr also eine 4 ip[0] = res[12+qlen+12]; ip[1] = res[12+qlen+13]; ip[2] = res[12+qlen+14]; ip[3] = res[12+qlen+15]; printf("%d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]); return 0; }
Schauen wir uns nun den Aufbau eines Request-Datagram an den Nameserver etwas genauer an. Der DNS Header hat eine Länge von genau 12 Bytes, das sind 6 Zahlen vom Type Short als Big Endian. Dieser Header hat also stets dieselbe Länge von 12 Bytes. Daran schließt sich eine Bytefolge mit variabler Länge an, die sogenannte Query. Damit der Nameserver die Query, z.B. example.com
wiederherstellen kann, wird dieser String in sogenannte Labels gesplittet und jedem Label die Länge vorangestellt:
Query: exampl.com Labels: example com Längen: 7 3 Zusammen: 7example3com0 (die 0 wird angehängt)
Wobei jede Längenangabe als Byte gesendet wird. So erklärt sich die Maximale Länge eines Labels mit 255 Bytes aber das nurmal so nebenbei. Der NS liest also nach dem Header das 1. Byte und weiß damit daß das 1. Label eine Länge von 7 Bytes hat welche zu lesen sind. Danach erkennt er, daß für das nächste Label genau 3 Bytes zu lesen sind und wenn das Nullbyte erscheint, ist die Wiederherstellung der Query abgeschlossen. Schließlich werden weitere 4 Bytes angehängt welche als 16-Bit-Integer Type und Class spezifizieren.
In Perl lässt sich die gesamte Anfrage mit einer einzigen pack()-Anweisung erzeugen, welche Schablonen da zu verwenden sind, siehe untenstehend:
pack("n6a*n2", $id, $flags, 1, 0, 0, 0, $query, $type, $class );
Wie bereits festgestellt, der Responseheader hat dieslbe Länge wie der Requestheader: 12 Bytes. Die sich daran anschließende sog. Question-Section ist die Originalanfrage, hat also auch genau dieselbe Länge wie die Original Anfrage. Im Anschluß daran lesen wir die Answer-Section:
C0 0C - NAME (komprimiertes Format, 2 Bytes) 00 01 - TYPE 2 Bytes 00 01 - CLASS 2 Bytes 00 00 18 4C - TTL 4 Bytes 00 04 - RDLENGTH 2 bytes 4D B7 D7 21 - RDDATA
Wobei der Algorithmus darin besteht, als Erstes RDLENGTH zu ermitteln, um damit die eigentlichen Daten mit der exakten Länge lesen zu können. Wenn wir also die Länge unseres Request kennen, gehen wir in der Response 10 Bytes weiter und lesen in den folgenden 2 Bytes wieviele Bytes bis zum Ende zu lesen sind in denen die Response steht. Im Beispiel sind 4 Bytes zu lesen.
Umsetzung in Perl:
# Response empfangen und Deserialisieren $, = "\n"; read($sh, my $header_, 12); // Fehlerbehandlung if( (unpack "n6", $header_)[1] != 0x8180 ){ printf "Server failure! Flags: 0x%04X", (unpack "n6", $header_)[1]; exit 1; } print map{ sprintf "%02X", $_ } unpack "n6", $header_; # Die Query übergehen wir read($sh, my $query_, 10 + length($req)); # Ermitteln welche Länge die eigentliche Antwort hat read($sh, my $rdlength, 2); print "\n-----\n"; print unpack "n", $rdlength; # Lese die Antwort read($sh, my $answer, 4); print "\n-----\n"; print unpack "C4", $answer;
Das ergibt untenstehende Ausgabe:
1E0 Die gesendete ID 8180 Flags 01 Questions 01 Answer RRs 00 Authority RRs 00 Additional RRs ----- 4 Die Antwort hat diese Länge ----- 93 Hier die Antwort 184 216 34
Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. sos@rolfrost.de.