本章主要介绍Linux支持IP的数据结构和基本活动,如入口IP包如何传递至IP接收函数,校验和如何验证,以及IP选项如何处理。
主要的IPv4数据结构:
struct iphdr{
};//ip报头
struct ip_options{
};//此结构代表必须被传输或转发的封包选项,那些选项存储在此结构中,而不是struct iphdr结构中
struct ipcm_cookie{
};//此结构包含了传输封包所需的各种信息
struct ipq{
};//IP封包的片段集,参见第二十二章IP片段hash表的组织一节
struct inet_peer{
};//内核会为最近连接过的每个远程主机都保留一个这一结构的实例
struct ipstats_mib{
};//SNMP(简单网络管理协议)采用一种名为MIB(Management Information Base ,管理信息库)的对象来收集系统的相关统计数据。此结构会保存关于IP层的统计资料。
struct in_device{
};//此结构存储了一个网络设备所有与IPv4相关的配置内容。net_device结构中的ip_ptr指针指向该结构
struct in_ifaddr{
};//当在接口山配置一个IPv4地址时,内核会建立一个in_faddr结构
struct ipv4_devconf{
};//该结构用于调整网络设备的行为。每个设备都有一个实例。其字段通过/proc/sys/net/ipv4/conf输出
struct ipv4_config{
};//不同于ipv4_devconf存储每个设备的配置,该结构存储每个主机的配置
struct cork{
};//该结构用于存储套接字选项CORK选项。第二十一章会看到其字段如何应用
sk_buff和net_device结构里与校验和有关的字段:
net_device->features字段表明设备的能力,其中和控制检验和计算的一些标志如下:
NETIF_F_NO_SUM:此设备很可靠,不需要使用任何L4校验和。回绕设备就开启了此c功能。
NETIF_F_IP_CSUM:此设备可以在硬件中计算L4检验和,但是只针对使用IPv4的TCP,UDP。
NETIF_F_HW_CSUM:此设备可以为任何协议在硬件中计算L4校验和。
skb_buff中主要有两个字段跟校验和有关:skb->csum和skb->ip_summed。
当一个封包被接收时,skb->csum可能包含其L4校验和,skb->ip_summed字段则会记录L4校验和的状态,这些状态代表设备驱动程序要告诉L4层的事,状态有下列这些值:
CHECKSUM_NONE:csum中的校验和无效,需要L4层自己来计算。为社么csum中的校验和无效,因为①设备不提供硬件校验和计算。②校验和必须重新计算并验证。如第十八章“对L4校验和所做的修改”一节提到的情况。
CHECKSUM_HW:NIC以L4报头和有效载荷计算了校验和,然后把校验和拷贝到skb->csum字段。软件(L4接收函数)需要把伪报头的校验和添加到skb->csum,并验证最后所得的校验和。
CHECKSUM_UNNECESSARY:NIC已经计算了L4报头以及伪报头的校验和(伪报头的校验和可以由设备驱动程序在软件中计算),所以,软件(L4接收函数)无需再计算L4的校验和。
当一个封包被传输时,skb->csum不再是校验和本是,而是指向NIC要把它计算的校验和即将存放的地方,也就是说,封包传输期间,只有当校验和是在硬件中计算时,才会用到此字段。skb->ip_summed依然表示L4校验和状态:
CHECKSUM_NONE:协议已经处理了校验和,设备不需要做任何事。
CHECKSUM_HW:协议只把伪报头的校验和存储在报头中,设备应该添加L4报头和有效载荷的校验和。
封包的一般性处理:
协议初始化:
ipv4协议由ip_init函数初始化,该函数完成以下主要任务:
为ip封包注册处理函数ip_rcv。(参见第十三章)
初始化路由子系统,包括与协议无关的缓存。(参见第三十二章)
初始化用于管理ip端点的基础架构(参见二十三章“长效ip端点信息”一节)
开机期间,ip_init会由inet_init调用。inet_init会处理所有与ipv4有关的子系统的初始化。
和Netfilter交互:
基本上,防火墙在网络堆栈程序中的某些地方都有钩子函数。当符合某些条件时,封包就会通过那些钩子函数。
与路由子系统的交互:
ip层在好几个地方必须和路由表交互。本章只简单说明ip层用于查询路由表的三个函数:
ip_route_input:决定封包是被本地传递,转发或丢弃。
ip_route_output_flow:传输封包前使用,此函数会返回下个跳点以及要使用的出口设备。
dst_pmtu:给定一个路由表缓存项目,返回相关的PMTU。
上述函数会把路由表的查询结构存储在skb->dst中。
处理输入ip封包:
下面从ip_rcv函数开始分析内核网络协议栈内ip封包的路径。ip_rcv函数原型如下:
int ip_rcv(struct sk_buff *skb,struct net_device *dev,struct packet_type *pt);
在第十章和第十三章已经知道,NIC驱动如何设定L3协议标识符skb->protocol和封包类型skb->pkt_type。当L2的目的地址和接收接口的地址不同时,skb->pkt_type = PACKET_OTHERHOST,通常这些包会被抛弃去,若接口处于混杂模式,会把这些包传给相关的嗅探器。但是,到了L3,ip_rcv只会将其丢弃:
if(skb->pkt_type == PACKET_OTHERHOST)
goto drop;
skb_share_check会检查封包的引用计数是否大于1,若处理程序看见引用计数大于1,就会自己建立一份缓冲区副本,使其可以修改封包。
if((skb = skb_share_check(skb,GFP_ATOMIC)) == NULL){
IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
goto out;
}
pskb_may_pull的工作是确保skb->data所指区域包含的数据至少和ip头一样大。如果条件符合,则无事可做,否则缺漏的部分就会从skb_shinfo(skb)->frags[]里的数据片段(如果有的话)拷贝过来 ,之后再次初始话iph(struct iphdr)。
if(!pskb_may_pull(skb,sizeof(struct iphdr)))
goto inhdr_error;
iphdr = skb->nh.iph;
接着对ip报头做一些健康检查。
if(iph->ihl < 5 || iph->version !=4)
goto inhdr_error;
现在,重复先前做过的检查,但是这次是完整的ip头(包括选项)。
if(!pskb_may_pull(skb,iph->ihl*4))
goto inhdr_error;
iph = skb->nh.iph;
接着计算校验和。
if(ip_fast_csum((u8 *)iph,iph->ihl) != 0)
goto inhdr_error;
然后继续检查,确保接收的封包长度大于或等于ip报头中记录的长度(因为L2层可能为了满足最小传输尺寸而做了填充,所以封包长度可能大于ip头中记录的长度)。同时确保封包的此少和ip报头一样大。
{
_ _u32 len = ntohs(iph->tot_len);
if(skb->len < len || len < iph->ihl<<2))//<<2是因为报头的大小以4字节为单位,所以需要先乘以4
goto inhdr_error;
//pskb_trim_rcsum用于检查L2是否填充了封包使其达到特定的最小长度,如果有,将其剪裁成正确的大小
if(pskb_trim_rcsum(skb,len)){
IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
goto drop;
}
}
最后来到函数的尾端,调用Netfilter子系统。
return NF_HOOL(PF_INET,NF_IP_PRE_ROUTING, skb , dev ,NULL,ip_rcv_finish);
Netfilter子系统(确切的说,是PF_INET,NF_IP_PRE_ROUTING位置)不决定丢弃封包,则后续会执行ip_rcv_finish函数。
ip_rcv_finish函数(static inline int ip_rcv_finish(struct sk_buff *skb) ):
ip_rcv主要做一些基本的健康检查,ip_rcv_finish会处理主要工作:
决定封包传给本地还是转发,若转发,还要找到出口设备和下个跳点。
分析和处理一些ip选项,并非所有选项都在这处理。
skb->nh字段是在netif_receive_skb里初始化的,当时,还不知道L3协议,所以用nh.raw初始化,现在,可以取得指向IP报头的指针了。
struct net_device *dev = skb->dev;
struct iphdr *iph = skb->nh.iph;
skb->dst可能包含封包通往其目的的路由信息,如果没有,询问路由子系统。
if(skb->dst == NULL){
if(ip_route_input(skb,iph->daddr,iph->saddr,iph->tos,dev))
goto drop;
}
接着,更新Traffic Control(Qos层)所用的统计数据。
#ifdef CONFIG_NET_CLS_ROUTE
...
#endif
当ip报头的长度大于20字节时,有一些IP选项要处理。
if(iph->ipl > 5){
struct ip_options *opt;
if(skb_cow(skb,skb_headroom(skb))){//skb_cow,如果缓冲区和别人共享,就会做出缓冲区副本
IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
goto drop;
}
iph = skb->nh.iph;
//解析报头中的IP选项,将分析结果放在中skb->cb所指的私有数据域字段的ip_option结构内。
if(ip_options_compile(NULL,skb))
goto inhdr_error;
}
在封包是来源地路由的情况下,内核必须检查该设备的配置是否允许使用该选项。一般情况下,默认是允许的。若设备配置不允许来源地路由选项,则该封包就被丢弃(但不会产生ICMP消息)。当设备允许IP源路由时,调用ip_options_rcv_srr函数设置skb->dst,决定使用哪个设备把该封包转发至来源地路由列表中的下一个跳点。
if(opt->srr){
....
if(ip_options_rcv_srr(skb))
goto drop;
}
ip_rcv_finish函数最后会调用dst_input,完成封包的处理。
IP选项处理:
并非一个封包的所有ip选项都必须在其所有片段中重复,下面是选项相关的主要API:
ip_options_compile:分析IP报头中的一群选项,然后对一个ip_options结构的实例初始化。
ip_options_build:对IP报头中选项部分做初始化,传输本地封包时会用到该函数。
ip_options_fragment:第一个片段时唯一继承了原有封包所有选项的片段,其他片段则不会,但是会以空选项填充,使所有片段尺寸一样,这样可以简化分片流程。
ip_forward_options:转发一个封包时,有些选项必须被处理。
ip_options_get:此函数会接收一群选项,用ip_options_compile 解析,然后把结果存储在其分配的ip_options结构中。
ip_options_echo:指定入口IP封包及其IP选项后,此函数就可以建立用于回复传送者的IP选项。
ip_options_compile函数:
原型:int ip_options_compile(struct ip_options *opt ,struct sk_buff *skb)
当skb不为NULL时(本例中opt为NULL),表示正在处理入口封包。当skb为NULL时(本例中,opt不为NULL),表示正在处理本地传输的封包。
在传输一个本地封包时,opt不为NULL,opt->data包含一个指向IP报头的指针。处理入口封包时,opt为NULL,报头包含在skb中,ip_options结构存储在skb->cb。ip_options_compile函数会根据IP报头位于何处而对本地变量做初始化。