Table des matières
Bonjour à tous. Suite à mes essais d'uml[1], et quelques cheveux en moins, je suis enfin parvenu à disposer d'un noyau Linux (2.4.27) que je peux utiliser avec gdb. Voici le suivi de ma découverte - par la pratique - de la pile tcp/ip de Linux. Notez que dans cet exemple, il est question uniquement de TCP/IP version 4. Je ne traite pas, du moins au moment ou j'écris ces lignes, de la couche hardware (par exemple le driver réseau).
Table des matières
Dans ce paragraphe, nous verrons les différentes étapes (fonction) lorsque la machine recoit un paquet réseau. Notez que je passe totalement outre les couches bas niveaux (niveau 2). L'exemple choisi est la réception d'un paquet icmp echo-request (un ping). Le noyau se chargera donc de renvoyer un icmp echo-reply, sans que le mode utilisateur n'intervienne.
Une fois les diverses opérations relatives à la couche 2 (liaison) effectuées, si le paquet reçu est du type IP en version 4, alors cette fonction est la première appelée.
struct sk_buff *skb : (liste doublement chaînée)
struct net_device *dev : pointeur vers le périphérique d'entrée
struct packet_type *pt
On lit skb->nh.iph qui correspond à l'en-tête IP, puis on vérifie quelques champs pour la validation RFC. On renvoie une erreur si : iph->ihl (longueur de l'en-tête IP) est inférieur à 5 ou encore si iph->version != 4 ou si la somme de contrôle (checksum [2]) est incorrecte, ou finalement si la longueur est incorrecte. Ensuite on utilise le système de hook de netfilter (NF_HOOK) pour passer à la fonction suivante, à savoir ip_rcv_finish.
Si iptables n'est pas utilisé, ou si les règles n'ont pas supprimé le paquet, on appel cette fonction. En premier lieu elle se charge de calculer la route du paquet, via l'appel de la fonction ip_route_input. En effet, si le noyau agit en tant que routeur, il est normal, et c'est ce que l'on attend, que si le paquet n'est pas à destination de la machine, alors elle le renvoie sur la bonne interface. En revanche, si la destination est locale, alors il faut traiter le paquet. Si le paquet est considéré comme local, la fonction ip_route_input définie le pointeur de fonction struct rtable->u.dst.input à ip_local_deliver. Il en existe cependant d'autres, comme ip_forward quand le noyau agit en tant que routeur, ip_error, et ip_mr_input pour le multicasting.
Dans un premier temps, on recompose les paquets. Puis NF_HOOK vers la fonction ip_local_deliver_finish avec les paramètres :
PF_INET
NF_IP_LOCAL_IN
skb
skb->dev (interface d'entrée)
NULL (interface de sortie)
Paramètres :
struct sk_buff *skb
Cette fonction se charge de délivrer à la coupe supérieure (par exemple TCP, UDP ou bien encore ICMP comme dans cet exemple) le paquet, prêt à être traiter, et contenant toutes les informations relatives à celui-ci. Pour cela, on lit le champ protocole du paquet IP (skb->nh.iph->protocol). Avant tout, on vérifie que le paquet ne correspond pas à une socket de type RAW. Sinon, on utilise ce même numéro pour indexer un tableau (en vérifiant cependant qu'il ne dépasse pas MAX_INET_PROTOCOLS - 1, soit 255). Celui-ci contient, entre autres, un membre handler qui est en fait l'adresse de la fonction qui assurera le traitement de la couche supérieure. Si celle ci renvoie une erreur (valeur négative), on tente de retraiter le paquet. Dans notre cas (icmp echo-request) c'est la fonction icmp_echo qui est appelée.
Paramètres :
struct sk_buff *skb
Cette fonction, comme son nom ne l'indique peut être pas, se charge de traiter les paquets du type ICMP echo request. En fait, elle fait un test sur la variable noyau sysctl_icmp_echo_ignore_all. Si la valeur est égale à 0, alors on définie une structure de type icmp_bxm, qu'on remplie avec les éléments suivants :
Un en tête icmp
Le type ICMP, ici echo reply (définie via une constante préprocesseur)
Un pointeur vers la structure struct sk_buff*skb
Définition de l'offset à 0
Longueur des données (skb->len)
Longuer de l'en tête (sizeof (struct icmphdr))
On appel finalement la fonction icmp_reply.
[2] Notez que sur architecture x86, c'est une fonction écrite en assembleur pour des raisons de performances
Pour cet exemple, je prend le cas d'une connexion TCP (socket de type STREAM) vers un hôte accessible via IPv4.
Elle effectue simplement un appel système vers la section intitulée « Appel système sys_socketcall ».
Tout échange entre le mode applicatif (userland, comprendre les applications) et le noyau (la pile TCP/IP) se fait au travers de l'appel système générique sys_socketcall. Celui-ci prend en premier paramètre un numéro correspondant à la fonction « réelle », par exemple sys_connect, sys_read, sys_write, etc..
Pour commencer, on associe une structure *sock en fonction du numéro fd via la fonction sockfd_lookup. Celle-ci se charge de remplir les différentes structures, comme par exemple le membre ops, qui contient un ensemble de pointeurs vers les fonctions à appeler en fonction des divers états possibles (connect, accept, bind, etc..). Ces fonctions sont elles mêmes des wrapper vers des fonctions plus bas niveau. Par exemple, on a, pour une socket toujours de type STREAM, la valeur connect qui pointe vers la fonction inet_stream_connect.
Table des matières
Pour bien comprendre la fonction de recherche de route décrite plus tard, il faut commencer par comprendre comment les routes sont définies dans le noyau. Nous verrons dans un premier temps, l'outil mode utilisateur (userland) qui permet de modifier la table de routage, à savoir la commande (8) route. Puis le traitement des IOCTL[3] par la pile TCP/IP.
La fonction principale se charge quasi exclusivement de traiter les paramètres puis d'appeler une des fonctions suivantes :
route_info
route_edit
Cette fonction, est en fait un simple wrapper vers une autre, définie en fonction de la famille de la route (par exemple inet6, ipx, par défaut inet (IPv4)). Pour une route de type inet, la fonction appelée est INET_setroute.
En premier lieu, on déclare rt comme étant une structure de type struct rtentry, celle-ci sera utilisée en argument à la fonction ioctl. Elle contient en fait toutes les informations relative à la route, relevons les champs principaux :
struct sockaddr rt_dst
struct sockaddr rt_gateway
struct sockaddr rt_genmask
short rt_metric
char *rt_dev
En fait, le rôle de la fonction INET_setroute est tout simplement de remplir ces divers membres avec les valeurs passées en argument de la commande, après avoir bien entendu défini l'ensemble des champs à 0 via un appel à la fonction memset. Ensuite on créer une socket de type AF_INET, SOCK_DGRAM, et le protocole 0. On utilise le descripteur de fichier retourné pour appeler la fonction ioctl. Comme noté précedemment, la structure rt est utilisé en argument final, représentant les données qui seront utilisées par la fonction noyau (à savoir ip_rt_ioctl, définie dans le fichier fib_frontend).
Paramètres :
unsigned int cmd
void *arg
Les valeurs attendues pour cmd sont :
SIOCADDRT (ajout d'une route)
SIOCDELRT (suppression d'une route)
On attend que arg soit en fait un pointeur vers une structure de type rtentry.
Cette fonction est chargée de traiter les IOCTL relatifs aux modifications de la table de routage (SIOCADDRT et SIOCDELRT). En premier lieu, on regarde si l'appelant à effectivement les droits de rajouter une route (via les mécanismes des capacities, qui ne sont pas (encore ?) traités dans cet ouvrage). La première action notable de cette fonction est l'appel de fib_convert_rtentry. Si aucune erreur n'est renvoyée, on dispose alors de toute les informations prêt a être utilisée par les fonctions de plus bas niveau.
Si cmd est égale à SIOCDELRT, on fait un appel à fib_get_table avec comme argument req.rtm.rtm_table, puis si aucune erreur n'est renvoyée on utilise le pointeur de fonction tb->tb_delete avec les arguments tb, &.rtm, &rta, &, &req.nlh, NULL) (celui ci pointe en fait vers fn_hash_delete.
Si cmd est égale à SIOCADDRT, on déclare une structure de type fib_table qu'on initialise via l'appel de la fonction fib_new_table avec toujours le paramètre req.rtm.rtm_table. Enfin, si aucunne erreur n'est retournée, on appelle le pointeur de fonction tb->tb_insert avec les même paramètre que pour tb->tb_insert (tb_insert pointe vers la fonction fn_hash_insert).
Paramètres :
int cmd
struct nlmsghdr *nl
struct rtmsg *rtm
struct kern_rta *rta
struct rtentry *r
Cette fonction se charge de préparée une structure de type nlmsghdr, une autre de type rtsmg et d'une troisième de type kern_rta à partir de l'argument de type struct rtentry.
struct nlmsghdr { __u32 nlmsg_len; /* Length of message including header */ __u16 nlmsg_type; /* Message content */ __u16 nlmsg_flags; /* Additional flags */ __u32 nlmsg_seq; /* Sequence number */ __u32 nlmsg_pid; /* Sending process PID */ };
struct rtmsg { unsigned char rtm_family; unsigned char rtm_dst_len; unsigned char rtm_src_len; unsigned char rtm_tos; unsigned char rtm_table; /* Routing table id */ unsigned char rtm_protocol; /* Routing protocol; see below */ unsigned char rtm_scope; /* See below */ unsigned char rtm_type; /* See below */ unsigned rtm_flags; };
struct kern_rta { void *rta_dst; /* destination de la route */ void *rta_src; /* source eventuelle de la route */ int *rta_iif; /* interface d'entrée */ int *rta_oif; /* interface de sortie */ void *rta_gw; /* gateway*/ u32 *rta_priority; /* metric */ void *rta_prefsrc; /* */ struct rtattr *rta_mx; struct rtattr *rta_mp; unsigned char *rta_protoinfo; u32 *rta_flow; struct rta_cacheinfo *rta_ci; struct rta_session *rta_sess; };
L'affectation s'effectue de la manière suivante :
Mise à zero des structures rtm et rta
Si r->rt_dst.sa_family est différent de AF_INET on renvoie une erreur
Déclaration et affectation d'une variable de type int nommée plen à 32 (en fait, c'est le masque par défaut de la route)
On attribue à ptr (un pointeur vers u32) l'adresse de destination de la structure rtentry
Si r->rt_flags est différent de RTF_HOST (si c'est une route vers un réseau et non une seule machine) on redéfinie plen avec la valeur numérique (0 à 32) du masque de la route
On définie nl->nlmsg_flags à NLM_F_REQUEST (définie via instruction préprocesseur à 1)
On définie nl->nlmsg_pid et nl->nmlsg_seq à 0
On définie nl->nlmsg_len à NLMSG_LENGTH[4](sizeof(*rtm))
Si on supprime une route, on redéfinie nlmsg_flags à 0 et nlmsg_type à RTM_DELROUTE
Sinon on positionne nlmsg_type à RTM_NEWROUTE, nmlsg_flags à NLM_F_REQUEST | NLM_F_CREATE et rtm->rtm_protocol à RTPROT_BOOT (3)
On affecte plen (le masque) à rtm->rtm_dst_len
On affecte rta->rta_dst avec la variable ptr
On définie rta->rta_prioriry avec une eventuelle valeur de metric
Si la valeur de r->rt_flags est égale à RTF_REJECT on définie rtm->rtm_scope à RT_SCOPE_HOST et rtm->rtm_type à RTN_UNREACHABLE (injoignable) puis la fonction retourne 0
Si r->dev n'est pas nulle (donc elle contient un nom (char *) d'un périphérique) on affecte le numéro correspondant à rta->rta_oif
On fait maintenant pointer ptr vers l'adresse source de la passerelle
Si ptr est vrai et que le type de la passerelle est bien AF_INET, on définie rta->rta_gw à ptr
Si cmd == SIOCDELRT (suppression d'une route), la fonction s'arrête là retourne 0 (pas d'erreur)
Si r->rt_flags correspond à RTF_GATEWAY mais que rta->rta_gw ne correspond à rien, on retourne l'erreur EINVAL
Si rtm->rtm_scope égal à RT_SCOPE_NOWHERE, on rédéfinie cette variable à RT_SCOPE_LINK
MTU ou WINDOW ou IRTT
On retourne 0 (pas d'erreur)
Si CONFIG_IP_MULTIPLE_TABLES n'est pas définie, c'est une simple macro vers fib_get_table.
Si CONFIG_IP_MULTIPE_TABLES n'est pas activé, renvoie local_table ou main_table.
Cette fonction vérifie dans une table de routage déjà calculée (qui sert donc de cache) s'il n'existe pas une entrée correspondante. Notez que la fonction utilisant un hash (index calculé en fonction de l'adresse de destination, de source et du champ ToS), elle peut entraîner des collisions. C'est pourquoi la valeur associée correspond en fait à une liste chaînée. Finalement, si une entrée valide est détectée, on vérifie que les points suivants correspondent :
l'adresse de destination
l'adresse source
l'interface d'entrée
l'interface de sortie (nulle)
le champ ToS (Type of Service)
Si c'est le cas, alors on remplit le membre dst, un pointeur vers une structure de type dst_entry. Celle-ci contient, entre autre, un pointeur vers la fonction à utiliser. Par exemple ip_local_deliver pour un paquet devant être traité localement. Si aucune entrée est trouvée, alors on calcule la route à l'aide de la fonction ip_route_input_slow.
Définition des paramètres
struct sk_buff *sk;
__u32 daddr;
__u32 saddr;
__u8 tos;
struct net_device *dev; (Interface d'entrée)
Si la route n'a pas déjà été calculée, c'est ici que l'opération s'effectue. Dans un premier temps, on vérifie - via un test sur le pointeur (struct net_device) in_dev - que le protocole IP est activé sur l'interface. Ensuite, on effectue les tests suivants sur l'adresse source :
adresse source différent de 0xff000000
de même pour l'adresse de destination :
masque de 0xf0000000 (BADCLASS), qui correspond à l'intervalle de 240.0.0.0 à 240.255.255.255
masque de 0x7f000000 (LOOPBACK), de 127.0.0.0 à 127.255.255.255
masque de 0xff000000 (ZERONET), de 255.0.0.0 à 255.255.255.255
[Now, we are ready to route packet.]
Pour calculer la route du paquet, on fait appel à la fonction fib_lookup avec les paramètres suivants :
struct flowi &fl
struct fib_result res
Cette fonction se charge de calculer la route à partir des quelques éléments passés en argument. La structure flowi est définie de la manière suivante :
struct flowi fl = { .nl_u = { .ip4_u = { .daddr = daddr, .saddr = saddr, .tos = tos, .scope = RT_SCOPE_UNIVERSE, #ifdef CONFIG_IP_ROUTE_FWMARK .fwmark = skb->nfmark #endif } }, .iif = dev->ifindex };
Une fois l'appel de la fonction fib_lookup effectué, on applique les éventuelles règles de translation d'adresse[5]. Puis, en fonction de la valeur res.type, on effectue divers traitements.
RTN_BROADCAST
RTN_LOCAL
FORWARD (implicite)
On valide la source, on définie res.type à RTN_BROADCAST puis on continue le point suivant comme pour un paquet de destination locale.
Définition des paramètres :
const struct flowi *flp
struct fib_result *res
Si CONFIG_IP_MULTIPLE_TABLES est faux, c'est une fonction (déclarée en static inline) qui se charge de parcourir les deux tables de routage local et main à l'aide de la fonction pointée par le membre tb_lookup (en l'occurence fn_hash_loopkup). Sinon, c'est une fonction qui va parcourir l'ensemble des tables disponibles.
Paramètres :
struct fibtable *tb
const struct flowi *flp
struct fib_result *res
On peut dire que nous voici dans la fonction du calcul de le route à proprement parler.
Table des matières
L'intégration de netfilter, le firewall de Linux, se fait au travers de hook [6], de la façon suivante. Les différents passages de fonctions s'effectuent via l'appel de NF_HOOK, une constante préprocesseur, avec les paramètres suivants :
protocole du HOOK, exemple : PF_INET pour IPv4
chaîne, exemple : NF_IP_PRE_ROUTING, NF_IP_LOCAL_IN, etc
pointeur vers une structure struct sk_buff, qui contient en fait des données relative au paquet
interface d'entrée
interface de sortie (peut être NULL)
la fonction à appeler si le paquet n'est pas supprimé
Ainsi, si CONFIG_NETFILTER, n'est pas définie, la constante NF_HOOK est définie à :
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb)
se résumant à un simple appel de la fonction okfn, avec le paramètre sbk.
Table des matières
struct sk_buff { /* These two members must be first. */ struct sk_buff * next; /* Next buffer in list */ struct sk_buff * prev; /* Previous buffer in list */ struct sk_buff_head * list; /* List we are on */ struct socket *sk; /* Socket we are owned by */ struct timeval stamp; /* Time we arrived */ struct net_device *dev; /* Device we arrived on/are leaving by */ struct net_device *real_dev; /* For support of point to point protocols (e.g. 802.3ad) over bonding, we must save the physical device that got the packet before replacing skb->dev with the virtual device. */ /* Transport layer header */ union { struct tcphdr *th; struct udphdr *uh; struct icmphdr *icmph; struct igmphdr *igmph; struct iphdr *ipiph; struct ipv6hdr *ipv6h; struct spxhdr *spxh; unsigned char *raw; } h; /* Network layer header */ union { struct iphdr *iph; struct ipv6hdr *ipv6h; struct arphdr *arph; struct ipxhdr *ipxh; unsigned char *raw; } nh; /* Link layer header */ union { struct ethhdr *ethernet; unsigned char *raw; } mac; 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; /* Length of actual data */ unsigned int data_len; unsigned int csum; /* Checksum */ unsigned char local_df, cloned, /* head may be cloned (check refcnt to be sure). */ pkt_type, /* Packet class */ ip_summed; /* Driver fed us an IP checksum */ __u32 priority; /* Packet queueing priority */ atomic_t users; /* User count - see datagram.c,tcp.c */ unsigned short protocol; /* Packet protocol from driver. */ unsigned short security; /* Security level of packet */ unsigned int truesize; /* Buffer size */ unsigned char *head; /* Head of buffer */ unsigned char *data; /* Data head pointer */ unsigned char *tail; /* Tail pointer */ unsigned char *end; /* End pointer */ void (*destructor)(struct sk_buff *); /* Destruct function */ #ifdef CONFIG_NETFILTER /* Can be used for communication between hooks. */ unsigned long nfmark; /* Cache info */ __u32 nfcache; /* Associated connection, if any */ struct nf_ct_info *nfct; #ifdef CONFIG_NETFILTER_DEBUG unsigned int nf_debug; #endif #endif /*CONFIG_NETFILTER*/ #if defined(CONFIG_HIPPI) union{ __u32 ifield; } private; #endif #ifdef CONFIG_NET_SCHED __u32 tc_index; /* traffic control index */ #endif };
struct socket { socket_state state; unsigned long flags; struct proto_ops *ops; struct inode *inode; struct fasync_struct *fasync_list; /* Asynchronous wake up list */ struct file *file; /* File back pointer for gc */ struct sock *sk; wait_queue_head_t wait; short type; unsigned char passcred; };
struct proto_ops { int family; int (*release) (struct socket *sock); int (*bind) (struct socket *sock, struct sockaddr *umyaddr, int sockaddr_len); int (*connect) (struct socket *sock, struct sockaddr *uservaddr, int sockaddr_len, int flags); int (*socketpair) (struct socket *sock1, struct socket *sock2); int (*accept) (struct socket *sock, struct socket *newsock, int flags); int (*getname) (struct socket *sock, struct sockaddr *uaddr, int *usockaddr_len, int peer); unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait); int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); int (*listen) (struct socket *sock, int len); int (*shutdown) (struct socket *sock, int flags); int (*setsockopt) (struct socket *sock, int level, int optname, char *optval, int optlen); int (*getsockopt) (struct socket *sock, int level, int optname, char *optval, int *optlen); int (*sendmsg) (struct socket *sock, struct msghdr *m, int total_len, struct scm_cookie *scm); int (*recvmsg) (struct socket *sock, struct msghdr *m, int total_len, int flags, struct scm_cookie *scm); int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct * vma); ssize_t (*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags); };