LEZIONE 25 I router, come abbiamo visto, alla fine "sono dei computer". Packet processing with libpcap/WinPcap Fulvio Risso http://fulvio.frisso.net 1 Libpcap/WinPcap n Libpcap is an open source C library that allows to capture/send raw packets from a Network Interface Card (NIC) n Libpcap gives you access to exactly the packets that are being received/sent by your NIC n n WinPcap is the Windows version Be carefully that some packets may be filtered by your network switch! n Main target are C programmers, but other languages (e.g., Java) may be supported through additional components n This presentation will focus on the capturing capabilities of libpcap/WinPcap Iniziamo da libpcap, librerie open source scritte in C che servono per interagire direttamente con la NIC. Non è formalmente corretto, ma possiamo immaginare che l'interazione del nostro programma con la rete sia sostanzialmente diretta e non si passi dal SO. Dal punto di vista funzionale noi possiamo generare e ricevere ESATTAMENTE gli stessi pacchetti che verranno trasmessi sulla rete. L'interfaccia nativa è appunto in C. Dopodichè esistono altre librerie di adattamento (ad esempio per Java, Python e PHP). libpcap e winpcap possono sia inviare che ricevere. 2 Libpcap/Wincap do and don’t Does not Does n Capture raw packets n Filter traffic n n Parse packets n Capture a subset of traffic n Supports reading traffic from physical interfaces and files n Saves (filtered) traffic on file E.g., easy access to protocol fields (e.g., ip.src) n Visualize packet content n Support for post-capture filters n Unless data is saved on a file, which is then opened with a filter Cosa fanno? - catturano pacchetti crudi - filtrano il traffico ("cattura solo i pacchetti TCP") - lettura di traffico sia da network interface che da un file - salvare il traffico filtrato sul file Cosa non fanno? - parsificazione del pacchetto (viene restituita una stringa di caratteri, winpcap non ha la più pallida idea del significato) - non visualizza il contenuto del pacchetto (perchè comunque ci va del parsing) - se voglio specificare un filtro lo devo dire prima della cattura (non posso cambiarlo se non fermando una cattura e farne partire un altra. Quindi eventualmente catturate tutto su file e fare più filtri a posteriori). 3 tcpdump/WinDump n Basic sniffer based on libpcap/WinDump n Command line interface n Often the only sniffer present when you install your Linux OS C:\Users\Fulvio>windump -n -r sample.cap reading from file sample.cap, link-type EN10MB (Ethernet) 13:55:56.141474 IP 130.192.3.76.138 > 130.192.3.255.138: UDP, length 201 13:55:56.141725 arp who-has 130.192.43.107 tell 130.192.4.34 13:55:56.239625 IP 130.192.25.132.138 > 130.192.25.255.138: UDP, length 232 13:55:56.338967 arp who-has 208.137.254.2 tell 130.192.3.24 13:55:56.439478 arp who-has 130.192.24.8 (ff:ff:ff:ff:ff:ff) tell 130.192.24.254 13:55:56.440093 arp who-has 130.192.24.16 (ff:ff:ff:ff:ff:ff) tell 130.192.24.254 13:55:56.440670 arp who-has 130.192.24.11 (ff:ff:ff:ff:ff:ff) tell 130.192.24.254 13:55:56.443275 router-hello l2rout vers 2 eco 0 ueco 0 src 60.29 blksize 1498 pri 64 hello 15 13:55:56.539253 IP 130.192.4.32.138 > 130.192.4.255.138: UDP, length 237 13:55:57.441384 IP 130.192.16.105.138 > 130.192.16.255.138: UDP, length 178 13:55:57.443861 IP 130.192.3.24.2301 > 255.255.255.255.2301: UDP, length 12 13:55:57.908574 IP 130.192.16.81.1210 > 130.192.3.21.53: 79+[|domain] 13:55:57.911728 IP 130.192.3.21.53 > 130.192.16.81.1210: 79*[|domain] 13:55:57.929764 IP 130.192.16.81 > 130.192.28.6: ICMP echo request, id 512, seq 432, length 40 13:55:57.931675 IP 130.192.28.6 > 130.192.16.81: ICMP echo reply, id 512, seq 432, length 40 4 tcpdump è un programmino che sfrutta libpcap per catturare il traffico. Aggiunge un minimo di parsing in cui ci fa vedere un timestamp del pacchetto e aggiunge un sommario dello stesso. In due parole è una versione scarnissima di Wireshark. tcpdump/Windump basics n Some suggestions for the command line: n -D: prints the list of network interfaces available in the system n -i [interface]: captures from that network interface n -s [snaplen]: captures only the first [snaplen] bytes of each packet n n Often modern NICs implement TCP reassembly, returning to the user very large packet (also 64KB) n -r [filename]: reads packets from [filename] instead of NIC n -w [filename]: saves the read packet to [filename] n n Please set it to a large number to be sure that all the payload gets captured Qui ci sono alcune indicazioni: -i [interface]: lancio da una specifica interfaccia (su Windows ho dei nomi lunghissimi, si può usare il numero dell'interfaccia) -D: numero e nome delle interfacce -r [filename]: legge da file -w [filename]: scrive su file -s: per questioni di privacy/velocità tcpdump cattura solo i primi byte del pacchetto (96 bytes o roba del genere), con -s [tot] posso specificare quanti byte leggo ("meglio" mettere un NUMERO INCREDIBILE in modo da riuscire a prendere pacchetti messi su più parti di una sessione TCP). Can be used also to read from a first file, apply a filter, and save the result on a second filter Example tcpdump –i eth0 –s 10000 – w sample.cap tcp port 80 5 Istruzioni per ottenere le librerie. Per sviluppare serve qualcosa in più. Getting and installing the library n n n Linux/UNIX n Binaries (libpcap + tcpdump) usually included in your distribution n May require administrative privileges (e.g., sudo) Windows n Need to download and install the binaries from http://www.winpcap.org n Two separate downloads for the library binary (a couple of DLL files) and tcpdump (a standalone executable) For developers n Need to install the developer version, which includes headers n Linux: often called libpcap-dev n Windows: install the “Developer’s Pack” 6 6 CMake n Cross-platform generator for project files (e.g., makefiles, Windows Studio projects, etc) n Not strictly needed, but definitely useful in order to write cross-platform software n Download and install from: n Linux: use your package manager n Windows: http://www.cmake.org CMake non è obbligatorio, ma siccome siamo in un mondo di N sistemi operativi posso fare dei file C portabili (se scritti bene). E' un tool con un suo linguaggio: si scrive il Makefile con un certo linguaggio semplice e crea il Makefile con il linguaggio opportuno. 7 Create your first libpcap program n Objective: open a capture file and print the hex dump of the packets on screen C:\Users\Fulvio\test-libpcap\debug> 1387179358:265840 (186) 00 0d 29 0b a4 3f 20 cf 30 14 c9 0d 00 ac 20 18 40 00 40 06 2e 00 82 c0 05 5d 00 16 c3 25 f2 8c 0b 21 f9 1a 00 50 ec c2 00 00 b2 76 79 0b 39 8f d0 ae ec 6e b0 e7 a7 97 d1 66 da b5 e0 d0 d3 c6 37 e1 85 ea ca cf 2f da 25 1c 90 63 e4 68 cc ca d6 42 32 9b 59 41 55 74 e6 28 01 15 8d d8 c0 99 c3 ea c4 66 b6 b7 ed 79 2d a2 fc 7d 76 0f 37 50 93 fd 89 33 ba 31 39 2b a1 d1 76 53 c5 a7 60 7f ca dd fd d5 9f 11 b7 b5 5e 15 11 12 df 3c 1387179358:267072 20 cf 30 14 c9 0d 00 28 7a f6 40 00 e1 46 c3 25 00 16 3f 79 c4 8c 00 00 8 (60) 00 0d 7e 06 f9 1a 00 00 29 95 05 b1 0b b5 22 27 a4 82 f2 f5 readfile-ex sample.cap 08 e1 05 e8 b4 2d df 58 20 f5 97 00 46 22 00 46 5e e6 c2 0b ba fa 45 82 50 c2 ab 10 f1 95 a1 13 76 10 c0 18 c3 fd 7b d6 45 72 35 4a 3f 08 00 45 00 c0 05 5d 82 c0 8c 0b a5 50 10 f1 Piccolo programma su libcap che legge un file e stampa il timestamp, tra parentesi la lunghezza del pacchetto e l'esadecimale del pacchetto. Sul sito c'è uno zip. Lo scomprimiamo. Create your first libpcap program n Unpack the sample n Create the project files: n Compile the executable n C:\Users\Fulvio\test-libpcap> cmake . n Windows: open the Microsoft Studio Solution file (.sln) and compile the solution n Linux: fulvio@zerg:~/test-libpcap$ make Launch the executable n Windows: C:\Users\Fulvio\test-libpcap\debug> readfile-ex ..\sample.cap n Linux: fulvio@zerg:~/test-libpcap$ ./readfile-ex sample.cap 9 Il sorgente è semplicissimo. Ci sono un paio di include, la seconda è <pcap.h> ed è l'header di winpcap. Prima funzione chiamata: apertura di una cattura offline. error buffer viene riempito in caso di fallimento. La funzione ritorna un handler a una struttura dati interna per lavorare successivamente su quella cattura. The source code (1) // Standard C include file for I/O functions #include <stdio.h> // Include files for libpcap/WinPcap functions #include <pcap.h> #define LINE_LEN 16 int main(int argc, char **argv) { pcap_t *fp; char errbuf[PCAP_ERRBUF_SIZE]; struct pcap_pkthdr *header; const u_char *pkt_data; u_int i=0; int res; if (argc != 2) { printf("usage: %s filename\n\n", argv[0]); return -1; } /* Open the capture file */ if ((fp = pcap_open_offline(argv[1], // name of the device to open errbuf // error buffer )) == NULL) { fprintf(stderr,"Unable to open the file %s.\n\n", argv[1]); return -1; } 10 The source code (2) /* Retrieve the packets from the file */ while((res = pcap_next_ex(fp, &header, &pkt_data)) >= 0) { /* print pkt timestamp and pkt len */ printf("%ld:%ld (%d)\n", header->ts.tv_sec, header->ts.tv_usec, header->len); pcap_next_ex serve per leggere il prossimo pacchetto. Parametri: puntatore, header passato per puntatore (quando la funzione ritorna c'è l'header del pacchetto) e data passato per puntatore (idem come sopra). L'header non è l'header del pacchetto, ma è un header con alcune informazioni utili per analizzarlo (timestamp, lunghezza...) Il for interno cicla sui bytes del pacchetto e stampa byte per byte. /* Print the packet */ for (i=1; (i < header->caplen + 1 ) ; i++) { printf("%.2x ", pkt_data[i-1]); if ( (i % LINE_LEN) == 0) printf("\n"); } printf("\n\n"); } if (res == -1) { printf("Error reading the packets: %s\n", pcap_geterr(fp)); } pcap_close(fp); return 0; Devo essere "veloce abbastanza" a fare processing affinchè il buffer circolare che sta sotto non finisca, saturato dai nuovi pacchetti in arrivo. La dimensione del buffer si può chiamare tramite opportune chiamate che permettono di interagire col kernel. Il traffico in uscita non ha padding, a differenza di quello in ingresso. } 11 Qui c'è qualcosa al riguardo del file di configurazione per creare i Makefile. Questa roba si può anche non toccare. The project file PROJECT(READFILE-SAMPLE) CMAKE_MINIMUM_REQUIRED(VERSION 2.4) # Set source files SET(SOURCES readfile_ex.c ) # Default directories for include files IF(WIN32) INCLUDE_DIRECTORIES ( ${READFILE-SAMPLE_SOURCE_DIR} ${READFILE-SAMPLE_SOURCE_DIR}/../WPdPack/Include ) ELSE(WIN32) INCLUDE_DIRECTORIES ( ${READFILE-SAMPLE_SOURCE_DIR} ) ENDIF(WIN32) # Default directories for linking # (in Linux those are already in the system path) IF(WIN32) LINK_DIRECTORIES(${READFILE-SAMPLE_SOURCE_DIR}/../WPdPack/Lib) ENDIF(WIN32) Assume the “WPdPack” folder is at the same level of our sample 12 # Platform-specific definitions IF(WIN32) ADD_DEFINITIONS( -D_CRT_SECURE_NO_WARNINGS -D_CRT_SECURE_NO_DEPRECATE -DWIN32_LEAN_AND_MEAN ) ENDIF(WIN32) # Create executable ADD_EXECUTABLE( readfile-ex ${SOURCES} ) # Link the executable to the # required libraries IF(WIN32) TARGET_LINK_LIBRARIES( readfile-ex wpcap ) ELSE(WIN32) TARGET_LINK_LIBRARIES( readfile-ex pcap ) ENDIF(WIN32) Libpcap programming basics: basic workflow Start Chosen interface Chosen filter Read available adapters pcap_findalldevs() Open capture interface pcap_open() pcap_open_live() pcap_open_offline() Compile packet filter pcap_compile() Set packet filter pcap_setfilter() Vediamo alcune funzioni principali in modo da creare un programma più elaborato: - guardo quali sono le schede di rete (findalldevs) - scelgo l'interfaccia da aprire (open apre file o adapter) - compilare un filtro (passo una stringa a un minicompilatore) - settare il filtro sull'interfaccia - ricevere i pacchetti pcap_next_ex() Read packets 13 Relevant source code for the “callback” mode /* Open the capture file */ if ((fp = pcap_open_offline(argv[1], // name of the device errbuf // error buffer )) == NULL) { fprintf(stderr,"Unable to open the file %s.\n\n", argv[1]); return -1; } /* read and dispatch packets until EOF is reached */ pcap_loop(fp, 0, dispatcher_handler, NULL); Vedi prima slide 16. Confrontando questo codice con quello visto in precedenza, notiamo che non c'è un while ma una pcap_loop. Tra i parametri c'è un dispatcher_handler, funzione che viene poi chiamata dal sistema operativo quando ho un pacchetto. E' esattamente il codice che c'era in precedenza che una volta avevo nella while. Ho un parametro in più (temp1) per sapere da quale interfaccia è stata chiamata la funzione se uso un solo dispatcher_handler. pcap_close(fp); return 0; } void dispatcher_handler(u_char *temp1, const struct pcap_pkthdr *header, const u_char *pkt_data) { u_int i=0; /* print pkt timestamp and pkt len */ printf("%ld:%ld (%ld)\n", header->ts.tv_sec, header->ts.tv_usec, header->len); /* Print the packet */ for (i=1; (i < header->caplen + 1 ) ; i++) { printf("%.2x ", pkt_data[i-1]); if ( (i % LINE_LEN) == 0) printf("\n"); } printf("\n\n"); } 14 Libpcap programming basics: data structures Most common function for reading packets (polling model) pcap_next_ex( Pcap handler (identifies the source currently in use) fp, Pcap header &header, Raw packet dump (may include Ethernet padding) &pkt_data Le strutture principali le abbiamo già viste intuitivamente: - fp: handler usato in tutte le funzioni per specificare dove devo operare - header: composto da timestamp, lunghezza del pacchetto e lunghezza del pacchetto catturato. Il timestamp è a sua volta una struct. ) struct pcap_pkthdr { struct timeval ts; // time stamp bpf_u_int32 caplen; // captured packet length bpf_u_int32 len; // total packet length } struct timeval { long tv_sec; long tv_usec; }; // seconds // and microseconds 15 Polling vs. event-driven libpcap programming Start Start Read available adapters Read available adapters Open capture interface Open capture interface Compile packet filter Compile packet filter Set packet filter Set packet filter Read packets pcap_next_ex() Polling 16 Stop in a blocking call Call from the operating system User-defined callback function Return to the Operating system pcap_loop() Event-based libpcap ha due modalità di lettura dei dati. Polling è il procedimento che abbiamo visto prima. La seconda modalità è event-driven: il programma si ferma su una call di tipo bloccante. Il processamento dei pacchetti viene fatto con un handler chiamato dal sistema operativo. Modalità originaria. Polling è più facile da capire, il problema è che la read ogni tanto rimane "appesa" perchè non arriva niente (c'è un timeout, bisogna vedere per quale motivo lo raggiungo!) Uno snapshot di codice "callback" è alla slide 14. Parsing packets: protocols struct ether_header { u_int8_t ether_dhost[6]; u_int8_t ether_shost[6]; u_int16_t ether_type; }; Define a structure that helps parsing the Ethernet header; this is possible only because the Ethernet header has fixed length /* 6 bytes destination address */ /* 6 bytes source address */ /* 2 bytes ethertype */ Cast the raw packet (starting at offset zero) into an ether_header structure Una volta catturati i pacchetti bisogna parsificarli. Cerchiamo di migliorarlo un po': sarebbe bello interpretare i byte crudi come byte Ethernet. Definiamo dunque una struttura ether_header composta da tre campi (destination host, source host, ether type). Il fatto che uso uint è buono se voglio fare roba cross-platform. Ho definito dunque una struttura che "scimmiotta" quella dell'Ethernet. int main(int argc, char **argv) { ... } void dispatcher_handler(u_char *temp1, const struct pcap_pkthdr *header, const u_char *pkt_data) { /* Pointer to the ether_header structure */ struct ether_header *eptr; /* Cast the raw packet to the ether_header structure, in order to facilitate the parsing */ eptr = (struct ether_header *) pkt_data; /* Print on screen the MAC addresses of each packet */ printf("%02x:%02x:%02x:%02x:%02x:%02x --> %02x:%02x:%02x:%02x:%02x:%02x\n\n", eptr->ether_shost[0], eptr->ether_shost[1], eptr->ether_shost[2], eptr->ether_shost[3], eptr->ether_shost[4], eptr->ether_shost[5], eptr->ether_dhost[0], eptr->ether_dhost[1], eptr->ether_dhost[2], eptr->ether_dhost[3], eptr->ether_dhost[4], eptr->ether_dhost[5]); Per usarla: - alloco un puntatore a questa struttura - casto pkt_data a quel puntatore Tutto il parsing è delegato all'utente. Tra l'altro, tutto il parsing aiuta nel caso in cui il protocollo ha dimensioni prefissate. Altrimenti si scava a manina. } Print the value in hex using two digits and prepend ‘0’ if needed 17 Parsing packets: accessing to protocol fields #ifdef WIN32 #include <winsock2.h> /* Needed for ntohs() in Windows */ #endif struct ether_header { u_int8_t ether_dhost[6]; u_int8_t ether_shost[6]; u_int16_t ether_type; }; /* 6 bytes destination address */ /* 6 bytes source address */ /* 2 bytes ethertype */ Warning! In Windows, remember to add the library “ws2_32” in CMakeLists.txt in order to resolve the linking for function ntohs() Altro problema grosso sul parsing dei protocolli. Qui voglio stampare il valore di ether_type. E' un casino. I 16 bit sulla rete sono in network byte order, Intel memorizza in senso opposto. Vedi sotto. int main(int argc, char **argv) { ... } void dispatcher_handler(u_char *temp1, const struct pcap_pkthdr *header, const u_char *pkt_data) { struct ether_header *eptr; /* Pointer to the ether_header structure */ u_int16_t ethertype; /* Variable that keeps the ethertype in host-based format */ /* Cast the raw packet to the ether_header structure, in order to facilitate the parsing */ eptr = (struct ether_header *) pkt_data; /* Print on screen the MAC addresses of each packet */ printf("%02x:%02x:%02x:%02x:%02x:%02x --> %02x:%02x:%02x:%02x:%02x:%02x", eptr->ether_shost[0], eptr->ether_shost[1], eptr->ether_shost[2], eptr->ether_shost[3], eptr->ether_shost[4], eptr->ether_shost[5], eptr->ether_dhost[0], eptr->ether_dhost[1], eptr->ether_dhost[2], eptr->ether_dhost[3], eptr->ether_dhost[4], eptr->ether_dhost[5]); ethertype= ntohs(eptr->ether_type); /* Converting ethertype from network to host byte order */ printf("(ethertype: 0x%04x)\n\n", ethertype); Function that converts the ethertype from network byte order to host byte order } 18 Endianess Need to use ntohX() and htonX() to convert data Host-based order for Intel CPU 0x0D 0x0C 0x0B 0x0A Offset 3 0x0D Offset 2 0x0C Offset 1 0x0B Offset 0 0x0A Little-endian Tutte le volte che devo accedere a qualcosa che non è 8 bit ma 16 o 32 devo usare: - ntoh: da network a host - hton: da host a network (voglio scrivere dei dati all'interno del pck) X = s (short), l (long) Su Windows devo aggiungere una libreria per poter usare queste due funzioni. 0x0A 0x0B 0x0C 0x0D Big-endian Network byte order (raw packet data) 19 Esempio un po' più complesso in cui ho due strutture dati. Occhio al campo lenver: è 8 bit, ma ho comunque problemi di byte ordering. Parsing packets: a more complex example (1) #ifdef WIN32 #include <winsock2.h> /* Needed for ntohs() in Windows */ #endif struct ether_header { u_int8_t ether_dhost[6]; u_int8_t ether_shost[6]; u_int16_t ether_type; }; struct ip_header { u_int8_t lenver; u_int8_t ip_tos; u_int16_t ip_len; u_int16_t ip_id; u_int16_t ip_off; u_int8_t ip_ttl; u_int8_t ip_p; u_int16_t ip_sum; u_int8_t ip_src[4]; u_int8_t ip_dst[4]; }; /* /* /* /* /* /* /* /* /* /* /* 6 bytes destination address*/ /* 6 bytes source address */ /* 2 bytes ethertype */ Warning! Byte ordering problems are present here as well header length + version*/ type of service */ total length */ identification */ fragment offset field */ time to live */ protocol */ checksum */ source and dest address */ source and dest address */ int main(int argc, char **argv) { ... } This definition makes easy to get access to each single digit, but is not very efficient when we need to compare addresses; in that case, better to transform the IP address in a u_int32_t 20 Parsing packets: a more complex example (2) Abbastanza simile a prima. Se ethertype = 800 allora uso la struttura ipptr. void dispatcher_handler(u_char *temp1, const struct pcap_pkthdr *header, const u_char *pkt_data) { struct ether_header *eptr; u_int16_t ethertype; eptr = (struct ether_header *) pkt_data; ethertype= ntohs(eptr->ether_type); if (ethertype == 0x800) { struct ip_header *ipptr; Even trivial protocol parsing usually requires several checks on the packets; for instance, our code does not support VLANs, does not check if the IP has options, etc. ipptr = (struct ip_header *) &pkt_data[14]; /* Print on screen the IP addresses of each packet */ printf("%d.%d.%d.%d --> %d.%d.%d.%d (length %d)\n\n", ipptr->ip_src[0], ipptr->ip_src[1], ipptr->ip_src[2], ipptr->ip_src[3], ipptr->ip_dst[0], ipptr->ip_dst[1], ipptr->ip_dst[2], ipptr->ip_dst[3], ntohs(ipptr->ip_len)); } else printf("Not an IP packet (ethertype: 0x%04x)\n\n", ethertype); } Although it works (in this case), better to avoid fixed offsets in the code 21 Filtraggio di traffico. La sintassi di filtering è comune a tutti i tool che si basano su libpcap e si trova sulla pagina indicata. Filtering traffic n The setfilter() function allows to filter incoming traffic n The filtering syntax is common among all the libpcap-based tools (e.g., tcpdump/WinDump) n Some sample filters n n ip n host 1.2.3.4 n src host 1.1.1.1 and dst port 80 tcpdump/WinDump man page: http://www.winpcap.org/windump/docs/manual.htm 22 Documentation n Full list of (working) samples and a step-by-step tutorial to libpcap/WinPcap programming http://www.winpcap.org n n (section “Documentation” – “Manual”) A small example (slightly modified from the WinPcap site) available on the course website 23 Caveats n Creating packet processing code is easy n Creating good packet processing code is hard n 24 n Efficiency matters, when you have a few clock cycles per packet n Avoid copying memory n Avoid memory allocations n Avoid function calls n Consider CPU-level issues (cache alignment, cache and memory latencies, CPU cores bindings, etc) Necessity to deal with tons of low-level details n Complex protocol encapsulations n IP segmentation n TCP session handling and payload reassembly Alcuni warning. Creare codice che lavora sui pacchetti è relativamente semplice (a limite un attimo noioso perchè scrivo tonnellate di codice preparatorio a risolvere il problema, e poi risolverlo è easy). Scrivere codice efficiente è un altro paio di maniche per tutti i motivi riportati: siccome ogni tanto me la gioco su 100 colpi di clock e per accedere a memoria ce ne metto 180 sono panato. Poi devo gestire tonnellate di problemi di basso livello, tra cui la segmentazione IP e gestire una tabella di sessioni (i pacchetti arrivano interlacciati) Esistono versioni customizzate di libpcap che permettono di andare più veloce. Conclusions n De-facto standard library for packet sniffing and raw-packet creation n Rather easy primitives n Leaves a lot of code under the responsibility of the programmer n Customized versions available (through accelerated drivers) if performance are an issue 25 NIC-specific
© Copyright 2024 ExpyDoc