INTRODUZIONE ALLE SOCKET RAW Un modo comune per descrivere gli strati (Layers), attraverso i quali é possibile la comunicazione tra computer, in una rete, É dato dal modello ISO OSI a 7 strati. Ogni layer fornisce dei servizi allo strato soprastante e utilizza quelli dello strato sottostante, rendendo indipendente (rispetto agli altri layers), la sua implementazione (e.g. implementazione di un nuovo strato - Ipv6). É possibile applicare tale modello alla suite di protocolli TCP/IP, ma é necessaria una restrizione, una ``mappatura'', da 7 a 4 strati, essendo il TCP/IP antecedente, e quindi ideato e sviluppato NON basandosi su tale rappresentazione teorica. Application ----------------\ Presentation ----------------- Application Session ----------------/ Transport ----------------- Transport Network ----------------- Network Datalink ----------------\ Datalink/ Physical ----------------/ Physical Modello ISO/OSI TCP/IP Mappatura 4 layer / http Application < ftp \ telnet Transport / TCP \ UDP Network / IP \ ARP, RARP / BPF Datalink < SOCK_PACKET \ PF_PACKET Applicazione Modello ISO/OSI alla suite TCP/IP Con la parola socket[1] ci riferiamo all'interfaccia (derivata dall'implementazione BSD) che rende possibile l'implementazione di applicazioni Network Oriented. Generalmente si sviluppano applicazioni che utilizzano TCP/UDP come protocollo di trasporto. La gestione di tali protocolli É a carico del kernel, mentre al programmatore É lasciata la gestione ed implementazione del 4^o layer: Il protocollo Applicativo (e.g. http, ftp, telnet, ssh, etc). Per poter creare il ``nostro'' punto di comunicazione, utilizziamo la syscall socket(2), della quale ricordiamo il prototipo: int socket(int family, int type, int proto); Nel caso piú comune, ovvero lo sviluppo di applicazioni TCP/UDP, ci ``posizioniamo'' al 4^o layer nel modello ISO/OSI. Le socket raw sono un tipo (int type) di socket che ci permettono di accedere, posizionarsi ai livelli sottostanti il 4^o. A seconda del parametro proto specificato nella syscall socket(2), accederemo al 3^o, 2^o o 1^o layer, preoccupandoci di implementare gli header relativi ai protocolli ``associati'' a quei layers. In realtá per accedere al 1^o layer, il datalink, non si possono usare le socket raw, ma bisogna utilizzare l'API fornita dall'OS che é dipendente da quest'ultimo; ad esempio per accedere al datalink layer utilizzando il kernel della serie 2.0.X di linux é necessario creare una socket di tipo SOCK_PACKET, famiglia AF_INET, mentre nei nuovi kernel (serie 2.2.X) e' necessario creare una socket di tipo SOCK_RAW, famiglia PF_PACKET, specificando in entrambi i casi il protocollo che il datalink layer incapsulerá (e.g. ETH_P_IP si riferisce ad un frame ethernet che incapsula un datagrammo IP). Le socket raw forniscono tre servizi che non possono essere ottenuti attraverso l'utilizzo di socket ``normale'' quali SOCK_STREAM e SOCK_DGRAM: Leggere/Scrivere pacchetti ICMP. Questo ci permette ad esempio, di eliminare codice dal kernel per la gestione di alcuni tipi di ICMP, implementando tale gestione in userland. Leggere/Scrivere datagrammi IP con il campo ``proto'' dell'IP header non gestito dal kernel. Costruire il proprio IP header. Creazione La creazione di socket raw é praticamente identica alla creazione di socket di altri tipi, ovviamente cambiano il secondo e terzo parametro. fd = socket(AF_INET, SOCK_RAW, IPPROTO_XXX); In questo modo creiamo una socket raw, che ``utilizzerá'' il protocollo IPPROTO_XXX. La definizione di IPPROTO_XXX si trova in /usr/include/netinet/in.h IPPROTO_RAW gioca un ruolo particolare nella creazione di socket raw che permetteranno la creazione del proprio IP header. se si vuole costruire il proprio IP header (e quindi ``dire'' al kernel di non costruirlo lui), bisogna impostare l'opzione socket IP_HDRINCL[2] int on = 1; setsockopt(fd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(int)); dove fd é il descrittore di socket raw precedentemente creato con proto IPPROTO_RAW. Si possono chiamare le syscall bind() e connect() su una socket raw anche se questo é raro. Solo il superuser o un processo che abbia CAP_NET_RAW capability puó creare socket raw. Output La spedizione di dati attraverso socket raw non presenta particolari differenze rispetto alla spedizione di dati attraverso socket ``normali''. La syscall sendto() puó essere usate per spedire dati attraverso socket raw. Eventualmente anche la syscall send() puó essere chiamata, nel caso si sia chiamata anche connect() sulla socket. Se IP_HDRINCL é stato settato, allora il buffer che spediamo dovrá contenere, ovviamente, anche l'IP Header (e i relativi header dei layer soprastanti, se ce ne sono). Il kernel frammenta i pacchetti se la dimensione del pacchetto in uscita é maggiore dell'MTU dell'interfaccia di uscita. Su linux questo non é vero [Limitazione]. Il kernel riempie, comunque, alcuni campi dell'IP header. Tabella riassuntiva dei campi dell'IP header modificati dal kernel IP checksum SI Source IP Si se riempito con 0 Packet ID Si se riempito con 0 Total length SI Input Il kernel passa la copia dei datagrammi ricevuti alle socket raw aperte, seguente i seguenti criteri: Datagrammi UDP e segmenti TCP non sono mai passati alle socket raw. Per riceverli é necessario accedere al datalink layer. Linux permette, comunque, di ricevere datagrammi UDP e segmenti TCP anche attraverso socket raw, senza quindi, necessariamente, accedere al datalink layer. Tutti gli ICMP e IGMP sono passati dal kernel, dopo che il kernel ha finito di processarli. Tutti i datagrammi IP con un campo proto dell'IP header non gestito dal kernel I frammenti non vengono mai passati alle socket raw. Il datagramma deve essere riassemblato prima (OS dependent). Dopo che i criteri sopra elencati sono stati soddisfatti, il kernel passa la copia dei datagrammi ricevuti a tutte le socket che sono state associate al protocollo del pacchetto ricevuto. Esempio, per ricevere tutti i pacchetti ICMP apro semplicemente una socket raw in questo modo: socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); e infine uso la syscall recvfrom() per ricevere effettivamente dati da quella socket. Se volessi ricevere tutti i protocolli attraverso una socket (solo sotto linux, perché TCP e UDP non possono esser ricevuti sugli altri OS in questo modo) basta specificare ``0'' al posto di IPPROTO_ICMP nella creazione della socket. Quando il datagrammo IP viene passato alla socket raw, viene passato anche l'IP header. Lorenzo ``Gigi Sullivan'' Cavallaro References: Unix Network Programming, 2nd edition, Volume 1 Author W. Richard Stevens, ed. Prentice Hall Programming Esempio 1: Costruzione di un segmento TCP con il flag SYN attivato. /* * $sfcs-mark$ * $filename: tcp_ex.c$ * $description: simple tcp segment$ * $authors: Lorenzo Cavallaro 'Gigi Sullivan'$ * $copyright: Copyright (C) 1999 by Lorenzo Cavallaro$ * $license: This source file is under LGPL$ * $creation time: Mon Mar 13 20:12:40 CET 2000$ * $last modification time: Mon Mar 13 20:12:40 CET 2000$ * $revision: 1$ */ #include #include #include #include #include #include u_short in_cksum(u_short *, int); struct phdr { struct in_addr src, dst; u_char pad; u_char proto; u_short len; struct tcphdr tcp; }; int main(int argc, char **argv) { struct sockaddr_in sin; struct in_addr src; struct ip *ip; struct tcphdr *tcp; struct phdr phdr; int fd, ret; int on = 1; char buf[sizeof(struct ip) + sizeof(struct tcphdr)]; u_short sport, dport; if (argc != 5) { fprintf(stderr, "Usage: %s " \ "\n", argv[0]); exit(1); } memset(&sin, 0, sizeof(struct sockaddr_in)); src.s_addr = inet_addr(argv[1]); sport = atoi(argv[2]); sin.sin_family = AF_INET; sin.sin_addr.s_addr = inet_addr(argv[3]); sin.sin_port = dport = atoi(argv[4]); fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if (fd == -1) { perror("socket"); exit(1); } ret = setsockopt(fd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(int)); if (ret == -1) { perror("setsockopt"); exit(1); } memset(&phdr, 0, sizeof(struct phdr)); memset(buf, 0, sizeof(buf)); ip = (struct ip *) buf; ip->ip_hl = 5; ip->ip_v = 4; ip->ip_tos = 0; ip->ip_len = htons(sizeof(struct ip) + sizeof(struct tcphdr)); ip->ip_id = htons(242); ip->ip_off = htons(IP_DF); ip->ip_ttl = 255; ip->ip_p = IPPROTO_TCP; ip->ip_sum = 0; ip->ip_src.s_addr = src.s_addr; ip->ip_dst.s_addr = sin.sin_addr.s_addr; tcp = (struct tcphdr *) ((char *) ip + sizeof(struct ip)); tcp->th_sport = htons(sport); tcp->th_dport = htons(dport); tcp->th_seq = htonl(0x12345678); tcp->th_ack = 0; tcp->th_x2 = 0; tcp->th_off = 5; tcp->th_flags = TH_SYN; tcp->th_win = htons(1024); tcp->th_urp = 0; tcp->th_sum = 0; phdr.src.s_addr = ip->ip_src.s_addr; phdr.dst.s_addr = ip->ip_dst.s_addr; phdr.pad = 0; phdr.proto = IPPROTO_TCP; phdr.len = htons(sizeof(struct tcphdr)); memcpy(&phdr.tcp, tcp, sizeof(struct tcphdr)); tcp->th_sum = in_cksum((u_short *) &phdr, sizeof(struct phdr)); ret = sendto(fd, buf, sizeof(buf), 0, (struct sockaddr *) &sin, sizeof(sin)); if (ret == -1) { perror("sendto"); exit(1); } } u_short in_cksum(u_short *addr,int len) { int sum = 0; u_short answer = 0; u_short *w = addr; int nleft = len; /* * Our algorithm is simple, using a 32 bit accumulator (sum), we add * sequential 16 bit words to it, and at the end, fold back all the * carry bits from the top 16 bits into the lower 16 bits. */ while (nleft > 1) { sum += *w++; nleft -= 2; } /* mop up an odd byte, if necessary */ if (nleft == 1) { *(u_char *)(&answer) = *(u_char *)w ; sum += answer; } /* add back carry outs from top 16 bits to low 16 bits */ sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */ sum += (sum >> 16); /* add carry */ answer = ~sum; /* truncate to 16 bits */ return(answer); } Esempio 2: Segmento TCP con il flag SYN attivato usando la libreria librnet /* * $sfcs-mark$ * $filename: ip+tcp+data.c$ * $description: Simple tcp segment.$ * $authors: Lorenzo Cavallaro 'Gigi Sullivan'$ * $copyright: Copyright (C) 1999 by Lorenzo Cavallaro$ * $license: This source file is under LGPL$ * $creation time: Tue Feb 22 13:30:57 CET 2000$ * $last modification time: Tue Feb 22 13:30:57 CET 2000$ * $revision: 1$ */ /* * Simple TCP packet assembly. * We'll build up the both ip and tcp hdr, plus data. */ /* * A lot of stuff is temp stuff. * There will be wrapper function however. * This is a pre pre example. */ #include "../../include/librnet.h" int main(int argc, char **argv) { int ret; int pktlen; struct rnet_packet *tcp; struct rnet_layers layers = { NULL, rnet_build_ip, rnet_build_tcp }; struct in_addr src, dst; int c; char *buf = "Hello world"; while((c = getopt(argc, argv, "s:d:")) != EOF) { switch(c) { case 's': src.s_addr = inet_addr(optarg); break; case 'd': dst.s_addr = inet_addr(optarg); break; case '?': fprintf(stderr, "Usage: %s -s -d \n", argv[0]); exit(1); } } if (!src.s_addr || !sin.sin_addr.s_addr) { fprintf(stderr, "Usage: %s -s -d \n", argv[0]); exit(1); } pktlen = RNET_IP_SIZE + RNET_TCP_SIZE; + strlen(buf); tcp = rnet_init(&layers, IPPROTO_RAW, pktlen); if (!(tcp)) { rnet_perror("rnet_init"); exit(1); } /* IP hdr args */ RNET_PUTP(tcp, rnet_ip_proto, IPPROTO_TCP); RNET_PUTP(tcp, rnet_ip_id, 242); RNET_PUTP(tcp, rnet_ip_source, src.s_addr, rnet_ip_dest, dst.s_addr); RNET_PUTP(tcp, rnet_ip_len, pktlen); RNET_PUTP(tcp, rnet_ip_frag, IP_DF, rnet_ip_ttl, 255); /* TCP args */ RNET_PUTP(tcp, rnet_tcp_source, 1024, rnet_tcp_dest, 80); RNET_PUTP(tcp, rnet_tcp_seq, 0x12345678, rnet_tcp_ack, 0); RNET_PUTP(tcp, rnet_tcp_win, 1024); RNET_PUTP(tcp, rnet_tcp_hl, RNET_TCP_SIZE >> 2); RNET_PUTP(tcp, rnet_tcp_flags, TH_SYN); /* Data */ RNET_PUTP(tcp, rnet_data, buf, rnet_dsize, strlen(buf)); ret = rnet_process(tcp); if (ret == -1) { rnet_perror("rnet_process"); exit(1); } ret = rnet_send(tcp); (ret == -1) ? rnet_perror("rnet_send") : \ printf("Wrote %d bytes of %d.\n", ret, tcp->nlen); rnet_destroy(tcp); exit(0); } [1] In realtá il termine socket puó assumere piú significati: API, syscall socket(2), socket inteso come punto di ingresso, canale di comunicazione. [2] L'opzione socket IP_HDRINCL fu introdotta da Van Jacobson nel 1998 per permettere la modifica del campo TTL dell'Header IP, necessario per il traceroute.