This is an old revision of the document!
Dezvoltarea Internet-ului a dus la creșterea exponențială a aplicațiilor de rețea și, drept consecință, la creșterea cerințelor de viteza și productivitate a subsistemului de rețea (networking) al unui sistem de operare. Subsistemul de networking nu este o componentă esențială a nucleului unui sistem de operare (kernel-ul de Linux poate fi compilat fără suport de rețea). Este, însă, destul de puțin probabil pentru un sistem de calcul (sau chiar un dispozitiv embedded) să conțină un sistem de operare fără suport de rețea, datorită nevoii de conectivitate. Atât sistemele Linux (Unix), cât și Windows folosesc stiva TCP/IP. Nucleul acestora va conține implementate protocoalele pâna la nivelul transport inclusiv, urmând ca protocoalele de nivel aplicație să fie implementate în user-space (HTTP, FTP, SSH, etc.).
În user-space abstracția comunicației în rețea este socket-ul. Socket-ul abstractizează un canal de comunicație și este interfața de interacțiune cu stiva TCP/IP implementată în kernel. Unui socket IP i se asociază o adresă IP, protocolul de nivel transport utilizat (TCP, UDP etc) și un port. Apelurile uzuale pe un socket sunt : apelul de creare (socket
), de inițializare (bind
), de conectare (connect
), de așteptare de conexiuni (listen
, accept
) și de închidere (close
).
Comunicația în rețea se realizează prin intermediul apelurilor read/write
sau recv/send
pentru socket-i TCP, respectiv recvfrom/sendto
pentru socket-i UDP. Operațiile de transmitere și recepție sunt transparente aplicației, lăsând la latitudinea nucleului încapsularea și transmiterea acestora în rețea. Este, însă, posibilă implementarea stivei TCP/IP în user-space folosind socket-i raw (opțiunea PF_PACKET
la crearea unui socket), sau implementarea unui protocol de nivel aplicație în kernel (TUX web server).
Pentru mai multe detalii despre programarea în user-space folosind socket-i, consultați Beej's Guide to Network Programming Using Internet Sockets pentru Linux și Windows Sockets pentru Windows.
Kernel-ul Linux oferă trei structuri fundamentale pentru lucrul cu pachetele de rețea: struct socket, struct sock și struct sk_buff.
Primele două reprezintă abstracții ale unui socket:
Cele doua structuri sunt corelate: struct socket
conține un câmp de INET socket, iar struct sock
are un BSD socket care îl deține.
Structura struct sk_buff este reprezentarea unui pachet de rețea și a stării acestuia. O astfel de structură este creată la sosirea unui pachet în kernel, fie din user-space, fie de la placa de rețea.
Structura struct socket este reprezentarea în kernel a unui socket BSD, operațiile care pot fi executate asupra acestuia fiind similare cu cele expuse de kernel aplicațiilor (prin apeluri de sistem). Operațiile comune de lucru cu socket-ii (creare, inițializare/bind, închidere, etc.), rezultă în apeluri de sistem specifice; acestea operează asupra unei structuri de tipul struct socket
.
Operațiile asupra struct socket sunt descrise în net/socket.c și sunt independente de tipul de protocoale de mai jos. Structura struct socket
este, astfel, o interfață generică peste implementări particulare de operații de rețea. De obicei, numele acestor operații încep cu șirul sock_
.
Operații asupra unui socket sunt:
Crearea este asemănătoare apelului socket
din user-space, dar socket-ul creat struct socket, va fi întors în parametrul res
:
socket
;Parametrii acestor apeluri sunt următorii:
family
reprezintă familia protocoalelor utilizate în transferul informației; de obicei, acestea încep cu șirul PF_
(Protocol Family); constantele care reprezintă familia de protocoale utilizate se găsesc în linux/socket.h, dintre care cea mai utilizată este PF_INET
, pentru protocoalele TCP/IP.type
reprezintă tipul de socket; constantele utilizate pentru acest parametru se găsesc în linux/net.h, dintre care cele mai utilizate sunt SOCK_STREAM
pentru o comunicație bazată pe conexiune între sursă și destinație și SOCK_DGRAM
pentru o comunicație fără conexiune.protocol
reprezintă protocolul utilizat și este în strânsă legătură cu parametrul type; constatele utilizate pentru acest parametru se găsesc în linux/in.h, dintre care cele mai folosite sunt IPPROTO_TCP
pentru TCP și IPPROTO_UDP
pentru UDP.Pentru crearea unui socket TCP în kernel se va apela:
struct socket *sock; int err; err = sock_create_kern(PF_INET, SOCK_STREAM, IPPROTO_TCP, &sock); if(err < 0) { /* handle error */ }
iar pentru crearea unui socket UDP:
struct socket *sock; int err; err = sock_create_kern(PF_INET, SOCK_DGRAM, IPPROTO_UDP, &sock); if(err < 0) { /* error */ }
Un exemplu de utilizare poate fi urmărit în codul handler-ului pentru apelul de sistem sys_socket:
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { int retval; struct socket *sock; int flags; /* Check the SOCK_* constants for consistency. */ BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC); BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK); flags = type & ~SOCK_TYPE_MASK; if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) return -EINVAL; type &= SOCK_TYPE_MASK; if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK)) flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; retval = sock_create(family, type, protocol, &sock); if (retval < 0) goto out; retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); if (retval < 0) goto out_release; out: /* It may be already another descriptor 8) Not kernel problem. */ return retval; out_release: sock_release(sock); return retval; }
Închiderea conexiunii (pentru socket cu conexiune) și eliberarea resurselor asociate:
void
sock_release(struct socket *sock)
- această funcție va apela funcția release
din câmpul ops
al structurii socket-ului:void sock_release(struct socket *sock) { if (sock->ops) { struct module *owner = sock->ops->owner; sock->ops->release(sock); sock->ops = NULL; module_put(owner); } //... }
Transmiterea/recepția mesajelor se face cu ajutorul funcțiilor:
int
kernel_recvmsg(struct socket *sock, struct msghdr *msg, struct kvec *vec, size_t num, size_t size, int flags);
int
kernel_sendmsg(struct socket *sock, struct msghdr *msg, struct kvec *vec, size_t num, size_t size);
Funcțiile de transmitere/recepție de mesaj vor apela ulterior funcția sendmsg/recvmsg
din câmpul ops
al socket-ului. Funcțiile ce conțin kernel_
ca prefix sunt folosite în cazul în care socket-ul este utilizat în cadrul kernel-ului.
Parametrii acestor funcții sunt următorii:
msg
, o structura struct msghdr, ce conține mesajul de transmis/recepționat. Dintre componentele importante ale acestei structuri avem:msg_name
, msg_namelen
, care pentru socket-i UDP
trebuie completați cu adresa la care se transmite mesajul (struct sockaddr_in)msg_iov
, msg_iovlen
, datele de transmis, într-un vector de structuri struct iovec. O structură iovec
conține un pointer către buffer-ul ce conține datele și dimensiunea acestuia.vec
, o structură kvec, ce conține un pointer către buffer-ul ce conține datele și dimensiunea acestuia; după cum se poate observa, are o structură similară cu structura struct iovecModul de lucru cu funcțiile de transmitere poate fi urmărit în cadrul handler-ului pentru apelul de sistem sys_sendto:
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len, unsigned, flags, struct sockaddr __user *, addr, int, addr_len) { struct socket *sock; char address[MAX_SOCK_ADDR]; int err; struct msghdr msg; struct iovec iov; int fput_needed; struct file *sock_file; sock_file = fget_light(fd, &fput_needed); err = -EBADF; if (!sock_file) goto out; sock = sock_from_file(sock_file, &err); if (!sock) goto out_put; iov.iov_base = buff; iov.iov_len = len; msg.msg_name = NULL; msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = NULL; msg.msg_controllen = 0; msg.msg_namelen = 0; if (addr) { err = move_addr_to_kernel(addr, addr_len, address); if (err < 0) goto out_put; msg.msg_name = address; msg.msg_namelen = addr_len; } if (sock->file->f_flags & O_NONBLOCK) flags |= MSG_DONTWAIT; msg.msg_flags = flags; err = sock_sendmsg(sock, &msg, len); out_put: fput_light(sock_file, fput_needed); out: return err; }
Structura struct socket:
/** * struct socket - general BSD socket * @state: socket state (%SS_CONNECTED, etc) * @type: socket type (%SOCK_STREAM, etc) * @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc) * @ops: protocol specific socket operations * @fasync_list: Asynchronous wake up list * @file: File back pointer for gc * @sk: internal networking protocol agnostic socket representation * @wait: wait queue for several uses */ struct socket { socket_state state; short type; unsigned long flags; /* * Please keep fasync_list & wait fields in the same cache line */ struct fasync_struct *fasync_list; wait_queue_head_t wait; struct file *file; struct sock *sk; const struct proto_ops *ops; };
Câmpuri importante sunt:
ops
- structura ce conține pointeri la funcțiile specifice protocolului implementat;sk
- INET socket-ul asociat.
Structura struct proto_ops conține implementările operațiilor specifice protocolului implementat (TCP
, UDP
, etc.); funcțiile de aici vor fi apelate din funcțiile generice de lucru cu struct socket (sock_release, sock_sendmsg, etc.)
Structura struct proto_ops conține, așadar, o serie de pointeri de funcții pentru implementări specifice de protocol:
struct proto_ops { int family; struct module *owner; int (*release) (struct socket *sock); int (*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len); int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags); int (*socketpair)(struct socket *sock1, struct socket *sock2); int (*accept) (struct socket *sock, struct socket *newsock, int flags); //...
Inițializarea câmpului ops
din struct socket se realizează în funcția __sock_create, prin apelul funcției create specifică protocolului; un apel echivalent este următorul:
//... if ((err = net_families[family]->create(net, sock, protocol)) < 0) { sock->ops = NULL; goto out_module_put; } //...
Se va realiza astfel instanțierea pointerilor de funcții cu apeluri specifice tipului de protocol asociat socket-ului. Apelurile sock_register și sock_unregister sunt folosite pentru completarea vectorului net_families.
Pentru restul operațiilor cu socketi (în afară de creare, închidere și transmitere/recepție mesaj, prezentate mai sus, în secțiunea Operații asupra structurii socket), se vor apela funcțiile date de pointerii din această structură. Spre exemplu, pentru operația bind
, care asociază unui socket un port pe mașina locală, vom avea următoarea secvență de cod:
#define MY_PORT 60000 struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons (MY_PORT), .sin_addr = { htonl (INADDR_LOOPBACK) } }; //... err = sock->ops->bind (sock, (struct sockaddr *) &addr, sizeof(addr)); if (err < 0) { /* handle error */ } //...
După cum se poate observa, pentru transmiterea informațiilor legate de adresa și portul care se vor asocia socket-ului, se completează o structură struct sockaddr_in. 1)
Structura struct sock descrie un INET socket. O astfel de structură este asociată unui socket creat în user-space și, implicit, unei structuri struct socket. Structura este folosită pentru a menține informații despre starea unei conexiuni. Câmpurile structurii și operațiile asociate încep, de obicei, cu șirul sk_
. Câteva câmpuri sunt prezentate mai jos:
struct sock { //... unsigned char sk_protocol; unsigned short sk_type; //... struct socket *sk_socket; //... struct sk_buff *sk_send_head; //... void (*sk_state_change)(struct sock *sk); void (*sk_data_ready)(struct sock *sk, int bytes); void (*sk_write_space)(struct sock *sk); void (*sk_error_report)(struct sock *sk); int (*sk_backlog_rcv)(struct sock *sk, struct sk_buff *skb); void (*sk_destruct)(struct sock *sk); };
sk_protocol
este tipul de protocol utilizat de socket;sk_type
este tipul de socket (SOCK_STREAM
, SOCK_DGRAM
, etc.)sk_socket
este socket-ul BSD care îl deține;sk_send_head
este lista de structuri sk_buff
pentru transmitere;
Inițializarea struct sock
și atașarea acesteia la un socket BSD se face cu ajutorul callback-ului create din net_families (apelat in __sock_create). Mai jos este prezentat modul de
inițializare a structurii struct sock
pentru protocolul IP, în cadrul funcției inet_create:
/* * Create an inet socket. */ static int inet_create(struct net *net, struct socket *sock, int protocol) { struct sock *sk; //... err = -ENOBUFS; sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot); if (sk == NULL) goto out; err = 0; sk->sk_no_check = answer_no_check; if (INET_PROTOSW_REUSE & answer_flags) sk->sk_reuse = 1; //... sock_init_data(sock, sk); sk->sk_destruct = inet_sock_destruct; sk->sk_family = PF_INET; sk->sk_protocol = protocol; sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv; //... }
Structura struct sk_buff (socket buffer) descrie un pachet de rețea. Câmpurile structurii conțin informații atât despre antetele și conținutul pachetelor cât și protocoalele utilizate, dispozitivul de rețea utilizat, pointeri către celelalte structuri struct sk_buff
. O descriere sumară a conținutului structurii este prezentată mai jos:
struct sk_buff { /* These two members must be first. */ struct sk_buff *next; struct sk_buff *prev; struct sock *sk; ktime_t tstamp; struct net_device *dev; struct dst_entry *dst; struct sec_path *sp; /* * This is the control buffer. It is free to use for every * layer. Please put your private variables there. If you * want to keep them across layers you have to do a skb_clone() * first. This is owned by whoever has the skb queued ATM. */ char cb[48]; unsigned int len, data_len; __u16 mac_len, hdr_len; /* ... */ void (*destructor)(struct sk_buff *skb); /* ... */ sk_buff_data_t transport_header; sk_buff_data_t network_header; sk_buff_data_t mac_header; /* These elements must be at the end, see alloc_skb() for details. */ sk_buff_data_t tail; sk_buff_data_t end; unsigned char *head, *data; unsigned int truesize; atomic_t users; };
unde:
next
și prev
sunt pointeri către următorul, respectiv precedentul element din lista de buffer-e;dev
este device-ul care transmite sau primește buffer-ul;sk
este socket-ul asociat buffer-ului;destructor
este apelul callback de dealocare a buffer-ului;transport_header
, network header
și mac_header
sunt offset-uri între începutul pachetului si începutul diverselor headere din pachet. Ele sunt menținute intern de diversele niveluri de procesare prin care trece pachetul. Pentru a obține pointeri către headere, folosiți una din următoarele funcții: tcp_hdr, udp_hdr, ip_hdr, etc. În principiu, fiecare protocol oferă o funcție de a obține o referință la header-ul respectivului protocol din cadrul unui pachet primit. De reținut: câmpul network_header
nu este setat decât după ce pachetul ajunge la nivelul rețea, iar câmpul transport_header
nu este setat decât după ce pachetul ajunge la nivelul transport.Structura unui antet IP (struct iphdr) are următoarele câmpuri:
struct iphdr { #if defined(__LITTLE_ENDIAN_BITFIELD) __u8 ihl:4, version:4; #elif defined (__BIG_ENDIAN_BITFIELD) __u8 version:4, ihl:4; #else #error "Please fix <asm/byteorder.h>" #endif __u8 tos; __be16 tot_len; __be16 id; __be16 frag_off; __u8 ttl; __u8 protocol; __sum16 check; __be32 saddr; __be32 daddr; /*The options start here. */ };
unde:
protocol
reprezintă protocolul de nivel transport utilizat;saddr
reprezintă adresa IP a nodului sursă;daddr
reprezintă adresa IP a nodului destinație.Structura unui antet TCP (struct tcphdr) are următoarele câmpuri:
struct tcphdr { __be16 source; __be16 dest; __be32 seq; __be32 ack_seq; #if defined(__LITTLE_ENDIAN_BITFIELD) __u16 res1:4, doff:4, fin:1, syn:1, rst:1, psh:1, ack:1, urg:1, ece:1, cwr:1; #elif defined(__BIG_ENDIAN_BITFIELD) __u16 doff:4, res1:4, cwr:1, ece:1, urg:1, ack:1, psh:1, rst:1, syn:1, fin:1; #else #error "Adjust your <asm/byteorder.h> defines" #endif __be16 window; __sum16 check; __be16 urg_ptr; };
source
reprezintă portul sursădest
reprezintă portul destinațieStructura unui antet UDP (struct udphdr) are următoarele câmpuri:
struct udphdr { __be16 source; __be16 dest; __be16 len; __sum16 check; };
source
reprezintă portul sursădest
reprezintă portul destinațieUn exemplu de accesare a informațiilor prezente în antetele unui pachet de rețea este următorul:
struct sk_buff *skb; struct iphdr *iph = ip_hdr(skb); /* IP header */ /* iph->saddr - source IP address */ /* iph->daddr - destination IP address */ if (iph->protocol == IPPROTO_TCP) { /* TCP protocol */ struct tcphdr *tcph = tcp_hdr(skb); /* TCP header */ /* tcph->source - source TCP port */ /* tcph->dest - destination TCP port */ } else if (iph->protocol == IPPROTO_UDP) { /* UDP protocol */ struct udphdr *udph = udp_hdr(skb); /* UDP header */ /* udph->source - source UDP port */ /* udph->dest - destination UDP port */ }
În sisteme diferite, există mai multe variante pentru ordonarea octeților într-un cuvânt (Endianness), printre care: Big Endian (cel mai semnificativ octet primul) și Little Endian (cel mai puțin semnificativ octet primul). Având în vedere că o rețea interconectează sisteme cu platforme diferite, Internet-ul a impus o secvență standard pentru stocarea datelor numerice, numită network byte-order. Spre deosebire, secvența octeților pentru reprezentarea datelor numerice pe calculatorul gazdă se numește host byte-order. Datele primite/trimise din/în rețea sunt în formatul network byte-order și trebuie facută conversia între acest format și host byte-order.
Pentru conversie există urmatoarele macrodefiniții:
Netfilter este denumirea interfeței de kernel pentru captura pachetelor de rețea cu scopul de modificare/analiză a acestora (pentru filtrare, NAT, etc.). Interfața netfilter este utilizată în user-space de iptables.
În kernel-ul Linux, captura de pachete folosind netfilter se realizează prin atașarea unor hook-uri. Hook-urile pot fi precizate în diferite locații din traseul urmat de un pachet de rețea în kernel, în funcție de necesitate. O organigramă cu traseul urmat de un pachet și zonele posibile de plasare a unui hook găsiți aici.
Header-ul inclus atunci când se folosește netfilter este linux/netfilter.h.
Un hook se definește prin intermediul structurii struct nf_hook_ops:
struct nf_hook_ops { struct list_head list; /* User fills in from here down. */ nf_hookfn *hook; struct module *owner; int pf; int hooknum; /* Hooks are ordered in ascending priority. */ int priority; };
unde:
pf
este tipul pachetului (PF_INET, etc.);hooknum
este tipul de hook utilizat; pentru IP, acestea sunt definite în linux/netfilter_ipv4.h: /* IP Hooks */ /* After promisc drops, checksum checks. */ #define NF_INET_PRE_ROUTING 0 /* If the packet is destined for this box. */ #define NF_INET_LOCAL_IN 1 /* If the packet is destined for another interface. */ #define NF_INET_FORWARD 2 /* Packets coming from a local process. */ #define NF_INET_LOCAL_OUT 3 /* Packets about to hit the wire. */ #define NF_INET_POST_ROUTING 4 #define NF_INET_NUMHOOKS 5
priority
este prioritatea; prioritățile sunt definite în linux/netfilter_ipv4.h: enum nf_ip_hook_priorities { NF_IP_PRI_FIRST = INT_MIN, NF_IP_PRI_CONNTRACK_DEFRAG = -400, NF_IP_PRI_RAW = -300, NF_IP_PRI_SELINUX_FIRST = -225, NF_IP_PRI_CONNTRACK = -200, NF_IP_PRI_MANGLE = -150, NF_IP_PRI_NAT_DST = -100, NF_IP_PRI_FILTER = 0, NF_IP_PRI_NAT_SRC = 100, NF_IP_PRI_SELINUX_LAST = 225, NF_IP_PRI_CONNTRACK_HELPER = INT_MAX - 2, NF_IP_PRI_NAT_SEQ_ADJUST = INT_MAX - 1, NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX, NF_IP_PRI_LAST = INT_MAX, };
nhook_fn
este handler-ul apelat in momentul capturării unui pachet de rețea (în forma unei structuri struct sk_buff
); prototipul este definit în linux/netfilter.h: typedef unsigned int nf_hookfn(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *));
skb
este pointer la pachet-ul de rețea capturat.hooknum
./* Responses from hook functions. */ #define NF_DROP 0 #define NF_ACCEPT 1 #define NF_STOLEN 2 #define NF_QUEUE 3 #define NF_REPEAT 4 #define NF_STOP 5
NF_DROP
este folosit pentru a filtra (ignora) un pachet, iar NF_ACCEPT
este folosit pentru a accepta un pachet și a-l transmite mai departe.Înregistrarea/deînregistrarea unui hook se realizează cu ajutorul funcțiilor definite în linux/netfilter.h:
/* Function to register/unregister hook points. */ int nf_register_hook(struct nf_hook_ops *reg); void nf_unregister_hook(struct nf_hook_ops *reg); int nf_register_hooks(struct nf_hook_ops *reg, unsigned int n); void nf_unregister_hooks(struct nf_hook_ops *reg, unsigned int n);
struct sk_buff
dat ca parametru într-un hook netfilter. În timp ce antetul IP poate fi obținut de fiecare dată folosind ip_hdr()
, antetele TCP și UDP pot fi obținute cu tcp_hdr()
, respectiv udp_hdr()
numai pentru pachete care pornesc dinspre sistem, și nu pentru cele care intră. În cazul din urmă, trebuie calculat manual offset-ul antetelor în pachet: // Pentru pachete TCP (iph->protocol == IPPROTO_TCP) tcph = (struct tcphdr*)((__u32*)iph + iph->ihl); // Pentru pachete UDP (iph->protocol == IPPROTO_UDP) udph = (struct udphdr*)((__u32*)iph + iph->ihl);
Acest cod funcționează în toate situațiile de filtrare, și astfel este recomandată folosirea lui în locul funcțiilor de acces la antete.
Un exemplu de utilizare a unui hook netfilter este prezentat mai jos:
#include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> static unsigned int my_nf_hookfn(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { /* process packet */ //... return NF_ACCEPT; } static struct nf_hook_ops my_nfho = { .owner = THIS_MODULE, .hook = my_nf_hookfn, .hooknum = NF_INET_LOCAL_IN, .pf = PF_INET, .priority = NF_IP_PRI_FIRST }; int __init my_hook_init(void) { return nf_register_hook(&my_nfho); } void __exit my_hook_exit(void) { nf_unregister_hook(&my_nfho); } module_init(my_hook_init); module_exit(my_hook_exit);
În kernel-ul Windows lucrul cu rețeaua se realizează prin intermediul NPI (Network Programming Interface). NPI definește interfața între module de networking care doresc să comunice între ele (clienți și provideri).
În Windows 2000/XP/2003, NPI este reprezentată de TDI (Transport Driver Interface). TDI expune o interfață peste care rulează așa numiții clienți TDI care implementează operațiile specifice. Socket-ii Windows (winsockets) sunt implementați peste clienți TDI.
TDI folosește obiecte fișier pentru comunicație și pentru controlul acesteia. Astfel, TDI folosește obiecte fișier pentru a descrie adrese de transport deschise, conexiuni asociate unei adrese de transport și canale de control cu scopul de stabilire și consultare a parametrilor de comunicație.
Arhitectura de rețea pentru Windows este una modulară. TDI este construit peste NDIS (Network Driver Interface Specification). NDIS este o bibliotecă de funcții kernel care abstractizează diferitele niveluri ale rețelei; fiecărui astfel de nivel i se pot asocia drivere specifice. Folosind interfețele NDIS se pot construi:
Începând cu Windows Vista, se recomandă utilizarea WSK (Winsock Kernel) în locul TDI. WSK vine cu un API nou pentru o performanță mai bună și programarea mai ușoara a driver-elor. WSK NPI suportă operații pe socket-i asemănătoare cu cele din user-space.
Operațiile TDI sunt apropiate operațiilor disponibile în user-space pentru lucrul cu socket-i:
API-ul furnizat de TDI pentru implementarea acestor operații este unul destul de stufos iar documentația asociată poate fi pe alocuri confuză și greu de urmărit; drept urmare, nu vom insista.
O descriere foarte bună a modului de utilizare a API-ului TDI (împreună cu un exemplu cod sursa) găsiți la The Code Project.
Captura de pachete în kernel-ul Windows se realizează prin intermediul TDI Drivers, NDIS Intermediate Drivers, sau așa numitelor Hook Drivers. Deși primele doua tipuri oferă mai multă flexibilitate și sunt recomandate în locul hook drivers, nu vom insista pe acestea deoarece au un API complex.
Kernel-ul de Windows 2000/XP/2003 oferă API pentru două tipuri de hook drivers:
Un filter-hook driver înregistrează un callback (denumit filter hook) în cadrul IP filter driver. Funcția de callback este apoi utilizată pentru a prelucra pachetul.
La fel cum în Linux, tipul funcției de filtrare era nf_hook_fn
, în cazul filter-hook, funcția de callback trebuie să fie PacketFilterExtensionPtr:
typedef PF_FORWARD_ACTION (*PacketFilterExtensionPtr)( IN unsigned char *PacketHeader, IN unsigned char *Packet, IN unsigned int PacketLength, IN unsigned int RecvInterfaceIndex, IN unsigned int SendInterfaceIndex, IN IPAddr RecvLinkNextHop, IN IPAddr SendLinkNextHop );
unde:
PacketHeader
este pointer la antetul IP al pachetului; de obicei se va face cast la un pointer de tip IPHeader: typedef struct IPHeader { UCHAR iph_verlen; UCHAR iph_tos; USHORT iph_length; USHORT iph_id; USHORT iph_offset; UCHAR iph_ttl; UCHAR iph_protocol; USHORT iph_xsum; ULONG iph_src; ULONG iph_dest; } IPHeader;
Packet
este un pointer la informația conținută de pachetul IP fără antetul acestuia; de obicei, se vor crea structuri UDPHeader
și TCPHeader
și se va face un cast corespunzător;PacketLength
este lungimea, în octeți, a conținutului pachetului (fără antet).Funcția de callback (hook-ul) poate întoarce trei valori:
PF_FORWARD
: IP filter driver va transmite imediat pachetul următorului nivel din stiva de rețea;PF_DROP
: IP filter driver va filtra (ignora) pachetul;PF_PASS
: IP filter driver va procesa pachetul și apoi îl va transmite următorului nivel din stiva de rețea.Un exemplu de astfel de funcție:
static PF_FORWARD_ACTION cbFilterFunction ( unsigned char *PacketHeader, unsigned char *Packet, unsigned int PacketLength, unsigned int RecvInterfaceIndex, unsigned int SendInterfaceIndex, unsigned long RecvLinkNextHop, unsigned long SendLinkNextHop) { IPHeader *iph = (IPHeader *) PacketHeader; if (iph->iph_protocol == IPPROTO_TCP) { TCPHeader *tcph = (TCPHeader*) Packet; /* process tcp packet from IP address iph->iph_src */ } else if (iph->protocol == IPPROTO_UDP) { UDPHeader *udph = (UDPHeader*) Packet; /* process tcp packet from IP address iph->iph_src */ } return PF_FORWARD; }
Hook-ul se înregistrează în cadrul IP filter driver prin intermediul rutinei ioctl IOCTL_PF_SET_EXTENSION_POINTER; se creează un IRP care apoi este transmis către IP filter driver. Pașii urmați sunt:
NTSTATUS status = STATUS_SUCCESS; UNICODE_STRING filterName; PDEVICE_OBJECT ipDeviceObject = NULL; PFILE_OBJECT ipFileObject = NULL; RtlInitUnicodeString(&filterName, DD_IPFLTRDRVR_DEVICE_NAME); status = IoGetDeviceObjectPointer(&filterName, STANDARD_RIGHTS_ALL, &ipFileObject, &ipDeviceObject);
PDEVICE_OBJECT ipDeviceObject = NULL; PF_SET_EXTENSION_HOOK_INFO filterData; KEVENT event; IO_STATUS_BLOCK ioStatus; PIRP irp; /* get pointer to IP filter driver device object */ //... /* specify the callback filter function */ filterData.ExtensionPointer = cbFilterFunction; /* init event that will notify us of the completion of the request */ KeInitializeEvent(&event, NotificationEvent, FALSE); irp = IoBuildDeviceIoControlRequest( IOCTL_PF_SET_EXTENSION_POINTER, ipDeviceObject, (PVOID) &filterData, sizeof (PF_SET_EXTENSION_HOOK_INFO), NULL, 0, FALSE, &event, &ioStatus);
ExtensionPointer
al acestei structuri specifică callback-ul; pentru a elibera hook-ul din sistem, aici se poate transmite NULL
).status = IoCallDriver (ipDeviceObject, irp);
Pentru deînregistare trebuie construit un IRP pentru care pointerul la structura PF_SET_EXTENSION_HOOK_INFO
să fie NULL
.
Înainte de încărcarea în kernel a unui filter-hook driver trebuie pornit serviciul IpFilterDriver
:
net start IpFilterDriver
O implementare de Filter-Hook driver găsiți la The Code Project: Developing Firewalls for Windows 2000/XP.
Avantajul utilizării Firewall-Hook driver este posibilitatea de inserare a mai multor funcții de filtrare. Fiecare funcție are asociată o anumită prioritate. Sistemul va apela funcțiile în ordinea priorității pâna în momentul în care una din ele întoarce DROP_PACKET
.
La fel ca la filter-hook, filtrarea se realizează prin intermediul unui callback. Tipul acestuia este IPPacketFirewallPtr
:
FORWARD_ACTION cbFilterFunction(VOID **pData, UINT RecvInterfaceIndex, UINT *pSendInterfaceIndex, UCHAR *pDestinationType, VOID *pContext, UINT ContextLength, struct IPRcvBuf **pRcvBuf);
unde:
pData
este un pointer la un buffer de pachet de tipul struct IPRcvBuf;pContext
este un pointer la o structură FIREWALL_CONTEXT_T
de unde se pot afla informații despre pachet.Funcția poate întoarce trei valori:
FORWARD
: pachetul este admis și este transmis următorului callback;DROP
: pachetul este filtrat (ignorat);ICMP_ON_DROP
: pachetul este filtrat și se transmite un pachet ICMP
sistemului de la distanță.Structura struct IPRcvBuf are următorul conținut:
struct IPRcvBuf { /* point to the next buffer in the chain */ struct IPRcvBuf *ipr_next; /* always 0 */ UINT ipr_owner; /* buffer data */ UCHAR *ipr_buffer; /* buffer data size */ UINT ipr_size; //... };
Spre deosebire de filter-hook, pachetul nu mai este complet asamblat. El va trebui reasamblat urmând pointerul ipr_next
la următorul buffer:
FORWARD_ACTION cbFilterFunction(VOID **pData, UINT RecvInterfaceIndex, UINT *pSendInterfaceIndex, UCHAR *pDestinationType, VOID *pContext, UINT ContextLength, struct IPRcvBuf **pRcvBuf) { char *pPacket = NULL; int iBufferSize; struct IPRcvBuf *pBuffer = (struct IPRcvBuf *) *pData; /* total packet size */ iBufferSize = buffer->ipr_size; while (pBuffer->ipr_next != NULL) { pBuffer = pBuffer->ipr_next; iBufferSize += pBuffer->ipr_size; } /* allocate memory for entire packet */ pPacket = (char *) ExAllocatePool(NonPagedPool, iBufferSize); if (pPacket != NULL) { unsigned int iOffset = 0; pBuffer = (struct IPRcvBuf *) *pData; /* copy all the buffers in the entire packet buffer */ memcpy(pPacket, pBuffer->ipr_buffer, pBuffer->ipr_size); while(pBuffer->ipr_next != NULL) { iOffset += pBuffer->ipr_size; pBuffer = pBbuffer->ipr_next; memcpy(pPacket + iOffset, pBuffer->ipr_buffer, pBbuffer->ipr_size); } } //... }
Înregistrarea/deînregistrarea unui callback se realizează la fel ca la filter-driver. Acum, însa, codul ioctl folosit la crearea IRP-ului este IOCTL_IP_SET_FIREWALL_HOOK
, iar structura utilizată este de tip IP_SET_FIREWALL_HOOK_INFO
:
typedef struct _IP_SET_FIREWALL_HOOK_INFO { /* filter callback */ IPPacketFirewallPtr FirewallPtr; /* priority of the hook */ UINT Priority; /* if TRUE then ADD else DELETE */ BOOLEAN Add; } IP_SET_FIREWALL_HOOK_INFO, *PIP_SET_FIREWALL_HOOK_INFO;
Pentru înregistarea unui callback câmpul Add
al structurii va fi inițializat la TRUE
, iar pentru deînregistrare la FALSE
:
PDEVICE_OBJECT ipDeviceObject = NULL; IP_SET_FIREWALL_HOOK_INFO filterData; KEVENT event; IO_STATUS_BLOCK ioStatus; PIRP irp; /* get pointer to IP filter driver device object */ //... /* specify the callback filter function */ filterData.FirewallPtr = filterFunction; filterData.Priority = 1; filterData.Add = load; KeInitializeEvent(&event, NotificationEvent, FALSE); /* build IRP */ irp = IoBuildDeviceIoControlRequest( IOCTL_IP_SET_FIREWALL_HOOK, ipDeviceObject, (PVOID) &filterData, sizeof (IP_SET_FIREWALL_HOOK_INFO), NULL, 0, FALSE, &event, &ioStatus);
O implementare de Firewall-Hook driver găsiți la The Code Project: An Adventure: How to implement a Firewall-Hook Driver?
Începând cu Windows Vista, implementarea de drivere de filtrare de pachete se va realiza utilizând Windows Filtering Platform Callout Drivers. Aceste drivere sunt construite în cadrul WFP (Windows Filtering Platform) pentru a prelucra pachetele de rețea.
La fel ca și pentru Linux, pentru datele primite/transmise din/în rețea trebuie facută conversia între network byte-order și host byte-order.
Pentru conversie există urmatoarele funcții:
USHORT
RtlUshortByteSwap(USHORT Source)
– convertește un întreg pe 16 biți între formatul host byte-order și network byte-order (echivalent pentru ntohs
și htons
)ULONG
RtlUlongByteSwap(ULONG Source)
– convertește un întreg pe 32 de biți între formatul host byte-order și network byte-order order (echivalent pentru ntohl
și htonl
)Cand se dezvolta aplicații care includ o parte de networking, una din cele mai folosite unelte este netcat. Supranumit și “Swiss-army knife for TCP/IP”, netcat permite printre altele:
Pentru a iniția o conexiune TCP:
nc hostname port
Pentru a asculta pe un port TCP:
nc -l -p port
Primirea și trimiterea pachetelor UDP se realizează adăugând opțiunea -u
în linia de comandă.
Observație: numele comenzii este nc
; de multe ori netcat
este un alias pentru această comandă. Există și alte implementări ale comenzii netcat, unele având parametrii puțin diferiți față de implementarea clasică. Consultați man nc
sau rulați nc -h
pentru a vedea modul de utilizare.
Pentru mai multe informații despre netcat, citiți acest tutorial.
/usr/src/linux
.
spook.local
(Linux) și chooch.local
Windows.root
cu parola student
; conectare folosind comanda ssh root@spook.local
), student/student (ssh student@spook.local
)ssh Administrator@spook.local
), student/student (ssh student@chooch.local
)root
respectiv Administrator
).ssh linux
ssh windows
/root/share/
de pe mașina virtuală Linux în /home/student/linux-share/
pe sistemul local se face folosind comanda ~/bin/mount-linux
./home/Administrator/share/
(Cygwin) de pe mașina virtuală Windows în /home/student/windows-share/
pe sistemul local se face folosind comanda ~/bin/mount-windows
.
lin/
din arhiva de sarcini a laboratorului.
ssh linux
.wget
pentru descărcarea arhivei de sarcini a laboratorului.
lin/1-2-netfilter/
.TODO 1
.NF_INET_LOCAL_OUT
.%pI4
al funcției printk.lin/1-2-netfilter/test-1.sh
.MY_IOCTL_FILTER_ADDRESS
.TODO 2
.my_ioctl
.ioctl_set_addr
.test_daddr
test_addr
în handler-ul netfilter.lin/1-2-netfilter/test-2.sh
.lin/1-2-netfilter/test.c
.make -f Makefile.test
.Connection from: 127.0.0.1:60001
Connection from: 127.0.0.2:60001
60000
pe interfața de loopback (în init_module
).lin/3-4-tcp-sock/
din arhiva de sarcini a laboratorului.TODO 1
.bind
, listen
în kernel-space, consultați handler-ele de apel de sistem sys_bind și sys_listen.sock→ops→…
.lin/3-4-tcp_sock/test-3.sh
.TODO 2
.accept
în kernel-space, consultați handler-ul de apel de sistem sys_accept (care este implementat folosind sys_accept4).0
ca valoarea pentru ultimul argument (flags
).accept
în funcția de inițializare a modulului, operația de insmod
se va bloca până se va realiza o conexiune.lin/3-4-tcp_sock/test-4.sh
.60001
.lin/5-udp-sock/
din arhiva de sarcini a laboratorului..win/
din arhiva de sarcini a laboratorului.
Windows Server 2003 Checked x86 Build Environment
).x86 Checked Build Environment
din partea dreaptă a Desktop-ului pentru a deschide consola DDK.C:\Cygwin\home\Administrator\so2
sau C:\Cygwin\home\Administrator\share
.vim-nox
.WinDbg
și din acesta dump-ul de memorie C:\Windows\MEMORY.DMP
.!analyze -v
pentru depanare.
IpFilterDriver
(net start IpFilterDriver
).win/1-2-filter/
, respectiv win/1-2-firewall/
din arhiva de sarcini a laboratorului.cbFilterFunction
, respectiv FilterPacket
.SYN
în antetul TCP și au dezactivat flag-ul ACK
.win/include/NetHeaders.h
.iph
pentru a referi antetul IP.Packet
. Antetul TCP este reprezentat de tipul de date TCPHeader
descris în include/NetHeaders.h
.DbgPrint
pentru afișarea adresei sursă și a portului sursă.NIPQUAD
și NIPQUAD_FMT
pentru afișarea adresei IP/”…” NIPQUAD_FMT ”…”, NIPQUAD(address)
unde address
este în network byte-order.NIPQUAD_FMT
este un șir de caractere, nu se pun virgule la concatenarea cu alte siruri de caractere.nc
.nc -l -p 5000
.nc localhost 5000
.MY_IOCTL_FILTER_ADDRESS
.testDestinationAddress
și apelați-o în cadrul funcțiilor cbFilterFunction
, respectiv FilterPacket
pentru testarea adresei destinație a unui pachet.DeviceIoControl
pentru a permite stocarea adresei destinație de test (în cadrul variabilei ioctlSetAddresss
.win/test/
din arhiva de sarcini a laboratorului.nc -l -p 5000
.nc $IP_ADDRESS 5000
(unde $IP_ADDRESS
este adresa IP a mașinii virtuale).nc localhost 5000
.