Nameserver-Abfragen, UDP mit Perl und mit C

Anfragen an Nameserver heißt senden eines User Datagram mit einer Binary und auch die Antwort ist eine Bytesequenz

Entwicklung mit Wireshark

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 Request in Wireshark

Anfrage NS

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 in Wireshark

Antwort NS

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.

Ein einfaches Perl Script

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.

Unser kleines C-Programm

#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;
}

Serialize Algorithmus für das Datagram

Der bytegenaue Aufbau eines Request

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
);

Wiederherstellen der Daten aus der Response

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. s​os­@rolf­rost.de.