Tour 8: "rawsock"

Überblick

Bei raw sockets handelt es sich um Sockets, mit denen man Zugriff auf die Rohdaten der IP-Datagramme erhält. Das benötigt man um eigene Protokolle zu implementieren, ohne dafür extra Kernel-Module zu schreiben, aber auch um z.B. ICMP-Datagramme zu empfangen. Das Beispiel-Programm zu dieser Tour ist auch ein sehr einfaches Ping-Programm.

Die Unterstützung von Raw Sockets ist unter UNIX relativ gleichförmig, während ich bisher unter Windows eher wenig Erfolg damit hatte. Grundsätzlich muss man zwischen Senden und Empfangen unterscheiden:

Einen Raw-Socket erzeugen

Ein Raw-Socket wird ebenfalls mit der Funktion socket() erzeugt, wobei als Typ SOCK_RAW angegeben wird:

int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sock == -1)
{
        fprintf(stderr, "Couldn't create socket: %s\n", strerror(errno));
}

Als Protokoll kann entweder ein bekanntes Protokoll wie z.B. hier IPPROTO_ICMP angegeben werden, oder IPPROTO_RAW um irgendetwas komplett eigenes zu erzeugen. Wird ein dem System bekanntes Protokoll angegeben, so werden nur Datagramme dieses Protokolls an den Socket übergeben. Beim Beispiel ICMP kann man aber gut sehen, dass alle ICMP-Datagramme an den Socket übergeben werden, d.h. man muss selbst dafür sorgen dass in einem Ping-Programm nur Datagramme vom Typ ICMP_ECHOREPLY ausgewertet werden, und zur Unterscheidung von Datagrammen anderer Instanzen das dafür vorgesehene ID-Feld nutzen.

Für das Erzeugen eines Raw-Sockets benötigt man in der Regel "root-Rechte" oder zumindest eine entsprechende Capability (unter Linux: CAP_NET_RAW).

Daten empfangen

Das Empfangen von Daten ist sehr einfach. Als erstes Experiment kann man einen ICMP-Socket anlegen und alles, was empfangen wird, auf der Konsole ausgeben (am besten als Hex-Dump). Dann auf einer anderen Konsole etwas mit den vorhandenen Programmen ping oder traceroute spielen und schauen, was so vorbei kommt.

Empfangene Datagramme enthalten immer den vollständigen IP-Header. Um diesen zu untersuchen kann man einen Zeiger auf eine Struktur iphdr (aus netinet/ip.h) darüberlegen. Die Länge des IP-Headers mitsamt der Optionen ermittelt man über das Feld ihl (IP Header Length, Größe des IP-Headers in 32-Bit-Worten), das ist robuster als nur sizeof(struct iphdr) zu nehmen. Dran denken: mehrbytige Werte liegen immer in Network Byte Order vor.

Jede Menge Details zur Funktionsweise der Protokolle und dem Aufbau ihrer Header findet man in den dazugehörigen RFCs, z.B. RFC 791 für IP, RFC 793 für TCP, RFC 768 für UDP und RFC 792 für ICMP.

Daten senden

Beim Senden von Daten muss unterschieden werden, ob das System den IP-Header hinzufügen soll (Default-Einstellung) oder ob man das selbst tun möchte. Im zweiten Fall muss die Socket-Option IP_HDRINCL gesetzt werden:

int opt = 1;
int result = setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &opt, sizeof(opt));
if (result != 0)
{
        fprintf(stderr, "Setting socket option failed: %s\n", strerror(errno));
}

An dieser Stelle sei noch einmal ausdrücklich gesagt, dass für das Senden von ICMP-Echo-Requests die Option IP_HDRINCL nicht notwendig ist und sie im Beispiel-Programm nur genutzt wird, um das Erzeugen eigener IP-Header zu demonstrieren.

Die Checksumme

Bei diversen Protokollen der Internet-Protokoll-Familie wird eine Checksumme berechnet. Diese ist in RFC 791 einigermaßen kryptisch spezifiziert:

The checksum field is the 16 bit one's complement of the one's complement sum of all 16 bit words in the header.
For purposes of computing the checksum, the value of the checksum field is zero.

In RFC 1071 wird dies noch mal etwas deutlicher gemacht und eine Beispiel-Implementierung in C beigefügt. Diese (etwas überarbeitet) sieht so aus:

uint16_t computeInternetChecksum(const void *addr, int count)
{
        register long sum = 0;
        const uint16_t *word = addr;

        while (count > 1)
        {
                sum += *word++;
                count -= 2;
        }

        if (count > 0)
                sum += *(uint8_t*) word;

        while (sum >> 16)
                sum = (sum & 0xffff) + (sum >> 16);

        return ~sum;
}

Vor dem Berechnen der Checksumme muss das entsprechende Header-Feld auf 0 gesetzt werden. Danach wird die Checksumme dort eingesetzt. Das Hübsche an diesem Algorithmus ist, dass er zur Prüfung auf das fertige IP-Datagramm angewendet werden kann; hier ist das Ergebnis gleich 0, wenn die Checksumme stimmt. Außerdem lässt sich eine solche Checksumme inkrementell erweitern und ist unabhängig von der Byte Order der beteiligten Systeme.

Die Checksumme für den IP-Header selbst wird von Linux übrigens immer und automatisch eingetragen (ebenso wie die Gesamtgröße des Datagramms). Aber für das Senden eigener ICMP, UDP- oder TCP-Datagramme wird die Checksumme benötigt. Bei TCP und UDP wird noch ein Pseudo-Header hinzugenommen, der Teil des IP-Headers wiederholt (Quell-/Zieladresse, Protokoll). Dies ist in den jeweiligen RFCs beschrieben.


Zurück