一、网络层的任务
数据包进入网络层后主要做以下几个处理,无论是接受数据包还是发送数据包。
1、数据包校验和检查
数据包如有错误就会被丢掉,错误包括检验和不正确、协议头草除了边界等等。
2、防火墙对数据包做过滤
网络防火墙就是大名鼎鼎的netfiler框架,比如我们应用层通过iptable命令配置的规则,获取我们写的内核模块也就是在netfilter中加入钩子函数,防火墙主要有四个表五条链子,四个表:filter表、nat表、mangle表、raw表。五条链:PREROUTING、INPUT、FORWARD、OUTPUT。五条链上的钩子 处理函数可能对数据包做改动、或者丢包。
3、IP选项处理
网络数据包负载数据中的IP协议头包含选项,说明IP协议应对数据包做何种处理,有的信息应用层也要使用。
4、数据包分片和重组
IP数据包的最大长度可以达到64KB,这时IP协议数据包长度的数据域占用的位数决定的,当一个数据包从一个网络传输到另一个网络MTU可能不一样,所以在IP层要包数据包分割成一个小于或者等于对端MTU在发送,对端收到后在把小的数据包重组。
5、接受、发送、前送
接受数据包是从链路层上传到网络层后肉IP层的接受函数处理,外传数据包是本机产生的数据包,经过TCP/IP协议栈从上层下传到网络层,前送数据包和外传数据包相关,但数据包是冲网络中接受的而不是本机产生的。
二、网络层数据结构
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4, //使用小端字节排序
version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4, //使用大端字节排序
ihl:4; //ip协议长度4位 最大值15,15*4=60,ip协议头最大60字节
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos; //服务类型
__be16 tot_len; //ip数据包总长度 16位,2的16次方,最大64kbyte
__be16 id; //ip数据包标识符
__be16 frag_off; //分片数据在数据包中的偏移量
__u8 ttl; //数据包的生成周期
__u8 protocol; //上层协议
__sum16 check; //校验和
__be32 saddr; //原地址
__be32 daddr; //目的地址
/*The options start here. */
};
三、ip协议初始化
IP协议初始化的前提是PF_INET协议族的初始化,初始化的文件是net/ipv4/af_inet.c,由inet_init函数实现,调用协议族中各层协议的初始化函数建立起tcp/ip协议栈主要功能:
(1)、注册协议栈各协议实例、如TCP、UDP、ICMP等
(2)、初始化IP协议
(3)、为TCP/UDP套接字分配内存槽,以便在打开套接字时为其分配所需要的内存。
(4)、调用dev_add_pack注册IP数据包的接受处理函数ip_rcv,将IP协议数据包处理函数ip_rcv插入到网络层和链路层的接口链表ptype_base中
(4)、初始化AF_INET协议族在/proc/文件系统的入口。
inet_init函数代码:
static int __init inet_init(void)
{
struct sk_buff *dummy_skb;
struct inet_protosw *q;
struct list_head *r;
int rc = -EINVAL;
BUILD_BUG_ON(sizeof(struct inet_skb_parm) > sizeof(dummy_skb->cb));
sysctl_local_reserved_ports = kzalloc(65536 / 8, GFP_KERNEL);
if (!sysctl_local_reserved_ports)
goto out;
//注册tcp协议实例
rc = proto_register(&tcp_prot, 1);
if (rc)
goto out_free_reserved_ports;
//注册udp协议
rc = proto_register(&udp_prot, 1);
if (rc)
goto out_unregister_tcp_proto;
rc = proto_register(&raw_prot, 1);
if (rc)
goto out_unregister_udp_proto;
/*
* Tell SOCKET that we are alive...
*/
(void)sock_register(&inet_family_ops);
#ifdef CONFIG_SYSCTL
ip_static_sysctl_init();
#endif
/*
* Add all the base protocols.
*/
//注册传输层的处理函数到inet_protos全局数组中
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n");
//注册udp处理函数
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n");
#ifdef CONFIG_IP_MULTICAST
if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)
printk(KERN_CRIT "inet_init: Cannot add IGMP protocol\n");
#endif
...
//建立arp模块
arp_init();
/*
* Set the IP module up
*/
//建立ip模块
ip_init();
tcp_v4_init();
//为tcp套接字建立内存槽
/* Setup TCP slab cache for open requests. */
tcp_init();
//为udp套接字建立内存槽
/* Setup UDP memory threshold */
udp_init();
...
if (icmp_init() < 0)
panic("Failed to create the ICMP control socket.\n");
/*
* Initialise the multicast router
*/
#if defined(CONFIG_IP_MROUTE)
if (ip_mr_init())
printk(KERN_CRIT "inet_init: Cannot init ipv4 mroute\n");
#endif
/*
* Initialise per-cpu ipv4 mibs
*/
if (init_ipv4_mibs())
printk(KERN_CRIT "inet_init: Cannot init ipv4 mibs\n");
//初始化proc文件系统和应用层交互
ipv4_proc_init();
ipfrag_init();
//注册ip数据包接受处理函数ip_rcv,将ip协议数据包的处理函数ip_rcv插入到网络
//层和数据链路层的接口链表ptype_base中
dev_add_pack(&ip_packet_type);
...
}
添加和链路层的接口ip_rcv:
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv, //ip层从数据链路层的接受函数
.gso_send_check = inet_gso_send_check,
.gso_segment = inet_gso_segment,
.gro_receive = inet_gro_receive,
.gro_complete = inet_gro_complete,
};
ip_init(void)初始化ip协议,ip层接受到数据后最重要的工作是如何转发数据包,所以ip_init的主要工作如下:
a、初始化路由子系统,建立独立于协议的路由缓冲表。路由表中保存数据包发送的目的地址和下一跳的地址信息。
b、初始化管理裸ip的基本功能。
c、如果配置了ip组发送功能,初始化组发送功能使用的/proc文件系统。
代码如下:
void __init ip_init(void)
{
//初始化路由子系统
ip_rt_init();
//初始化裸ip
inet_initpeers();
//初始化组发送的/proc文件系统
#if defined(CONFIG_IP_MULTICAST) && defined(CONFIG_PROC_FS)
igmp_mc_proc_init();
#endif
}
四、与网络过滤子系统的交互
数据包到达网络层后需要做一些安全处理,也就是netfiler的过滤,网络子系统在tcp/ip协议栈多个地方被调用
a.接受数据包时
b.前送数据包(路由决策前)
c.前送数据包(路由决策后)
c.发送数据包
也就是netfiler的四个链子,相应的处理函数也分为两个阶段,第一阶段通常为do_something(比如ip_rcv),主要做相应的安全检查和为数据包预留必要内存空间,第二阶段函数通常命名为do_something_finish(比如do_rcv_finish),网络子系统的回到函数是NF_HOOK
static inline int
NF_HOOK(uint8_t pf, unsigned int hook, struct sk_buff *skb,
struct net_device *in, struct net_device *out,
int (*okfn)(struct sk_buff *))
{
return NF_HOOK_THRESH(pf, hook, skb, in, out, okfn, INT_MIN);
}
参数:
pf----协议比如NFPROTO_IPV4
hook--钩子函数的位置如:NF_INET_PRE_ROUTING、NF_INET_LOCAL_IN....
skb ----数据包函数
in----接受数据包网络设备
out---发送数据包网络设备
okfn---处理函数
我们看ip_rcv处理完成后就调用网络子系统的钩子函数,此时还没有选路由所以out传的是NULL,钩子的位置是NF_INET_PRE_ROUTING是在选路由之前,处理函数是ip_rcv_finish.
NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
五、与路由子系统交互
数据包在ip层最主要的工作是决定数据包的去向,无论是转发还是本地产生的数据包,所以要个路由子系统交互,ip层首先要获取数据包的目的地址,ip层需要用到查路由表的有三个API:
a. ip_route_input:确定数据包的目的地址,数据包可能由本机接受、前送、或者扔掉。
b. ip_route_output_flow: 在发送一个数据包之前使用,该函数返回数据包发送路径中下一跳网关ip和发送数据包的网络设备。
c. dst_pmtu: 该函数的输入参数为路由表的入口,返回的数据包发送路径上的最大传输单元PMTU。
在IP层查询路由表时,主要依据以下五个参数来路由决策
1)、IP数据包的目的地址。
2)、IP数据包的源地址。
3)、服务了类型(ToS)。
4)、接受数据包的网络设备。
5)、可发送的网络设备。
路由决策后的结果存放在skb->dst数据域中。
六、ip_rcv函数
ip_rcv函数只要是对skb中的数据域做各种合法性检查,比如协议头长度、协议版本、数据包长度、校验和等。
1、当前skb个数据域的状态
ip_rcv函数由链路层技术函数netif_receive_skb调用执行,此时skb的网络层信息地址指针skb->network_header设置到网络层(IP层)协议头信息的起始地址,此时IP层处理函数可以直接将IP的协议头信息从Socket Buffer数据缓冲区中复制到iphdr数据结构中,在调用ip_rcv函数之前,skb数据域大部分已经初始化了,skb->data是指向整个数据起始地址的指针,此时链路层已经处理完成,skb->data指向了IP层写协议头信息的起始地址。
2、skb->pkt_type
如果接受的数据包的MAC地址不是本机网络设备的MAC地址,数据类型skb->pkt_type就会被设置为PACKET_OUTHERHOST,正常情况下这中类型的数据包会被丢弃。如果网络设备工作模式是混杂模式,它就不会管数据包的MAC地址是否与本机MAC相同,接受所有数据包,在将数据包上传给TCP/IP协议栈。这里收到的主机数据包和将数据包按路由前送给其他主机不是一个概念,前者是MAC地址不是本机,后者是IP地址不是本机,但MAC地址是本机。
/*
* Main IP Receive routine.
*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
struct iphdr *iph;
u32 len;
/* When the interface is in promisc. mode, drop all the crap
* that it receives, do not try to analyse it.
*/
//数据包的目的mac不是本机扔掉
if (skb->pkt_type == PACKET_OTHERHOST)
goto drop;
...
3、数据包共享处理
skb_share_check检查数据包引用计数是否大于1,如果是表名该数据包还被其他进程使用,ip_rcv之前的处理函数是二层的netif_receive_skb已经把引用计数加1了,ip协议处理函数看到引用计数大于1就会拷贝一个信的数据包,对拷贝的数据包可以做任何修改处理
//引用数大于1就拷贝一份数据包
if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
goto out;
}
static inline struct sk_buff *skb_share_check(struct sk_buff *skb,
gfp_t pri)
{
might_sleep_if(pri & __GFP_WAIT);
if (skb_shared(skb)) {
//引用计数大于1拷贝数据包
struct sk_buff *nskb = skb_clone(skb, pri);
kfree_skb(skb);
skb = nskb;
}
return skb;
}
4、IP协议头信息检查
1)、判断IP协议头长度是否正确
pskb_may_pull的作用是是检查skb->data指向的区域至少包含一个数据块,他的长度和IP协议头信息的长度一致,每个IP数据包必须包含完整的IP协议头信息,如果skb_may_pull检查出错就直接扔掉数据包,检查成功要再次初始化iphdr因为pskb_may_pull可以改变头信息存放缓冲区的结构
static inline int pskb_may_pull(struct sk_buff *skb, unsigned int len)
{
if (likely(len <= skb_headlen(skb)))
return 1;
if (unlikely(len > skb->len))
return 0;
return __pskb_pull_tail(skb, len - skb_headlen(skb)) != NULL;
}
2)判断IP协议头信息是否正确
IP协议头的长度应该是20字节,描述ip协议长度是4位,每个单元4字节,所有iph->ihl不能小于5(20字节),然后判断是ipv4还是ipv6。然后再次调用pskb_may_pull检查整个IP协议头的长度是否正确,确保在从协议头读取需要的数据之前,基本的协议头信息没有被破坏,接着调用ip_fast_csum计算校验和。
//ip头必须大于或者等于20个字节,
//表示ip协议头长长度是4为,最大值是15,所以ip头最大60字节
if (iph->ihl < 5 || iph->version != 4)
goto inhdr_error;
//对整个IP协议信息的长度做合法性检查
if (!pskb_may_pull(skb, iph->ihl*4))
goto inhdr_error;
//再次初始化iphdr,因为pskb_may_pull会改变头信息存储结构
iph = ip_hdr(skb);
5、数据包正确性检查
ip_rcv要对负载数据本身做正确性检查,检查主要包括两方面。
1)、确保缓冲区的长度大于或者等于IP协议头信息中保到的数据包长度,因为链路层协议可能在负载数据后加补丁,来保证发送的数据包长度不小于数据链路层的最小数据包长度,数据包中报告的长度skb->len要大于或者等于数据包实际长度(len=ntohs(iph->tot_len))。
2)、确保每个数据包是至少包含一个IP协议头,IP协议头的长度是(iph->ihl*4),每个单元是32位也就是4字节。
//包的总长度要大于实际数据的长度
//因为二层为了长度达到二层的长度可能会填充0
len = ntohs(iph->tot_len);
if (skb->len < len) {
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INTRUNCATEDPKTS);
goto drop;
//整个数据包的长度不能小于IP协议头的长度20字节,必须包含一个ip头
} else if (len < (iph->ihl*4))
goto inhdr_error;
6、清除工作,获取一个干净的数据包
数据包在IP层要清除数据链路层在数据包包尾部添加的补丁,把Socket Buffer中的数据缓冲区裁减成实际大小,清空Socket BUffer中的控制缓冲区,最后调用网络过滤子系统的回调函数,首先是PRE_ROUTEING链,网络过滤子系统可能对数据包进行修改、丢包,如果通过网络过滤系统的就继续执行ip_rcv_finish。
//去掉二层的填充
if (pskb_trim_rcsum(skb, len)) {
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
goto drop;
}
//清空缓冲区
/* Remove any debris in the socket control block */
memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
/* Must drop socket now because of tproxy. */
skb_orphan(skb);
//调用网络过滤子系统的回调函数,对数据包进行安全过滤
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
总体来看ip_rcv只要对数据包做全方面的检查,确保数据包的正确。这时对系统至关重要的,因为数据包错误可能造成访问到无效的内存,造成系统崩溃,越是底层的越要保证稳定性,以下是ip_rcv的全部代码:
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
struct iphdr *iph;
u32 len;
//数据包的目的mac不是本机扔掉
if (skb->pkt_type == PACKET_OTHERHOST)
goto drop;
IP_UPD_PO_STATS_BH(dev_net(dev), IPSTATS_MIB_IN, skb->len);
//引用数大于1就拷贝一份数据包
if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
goto out;
}
//判断ip协议头是否正确
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
goto inhdr_error;
iph = ip_hdr(skb);
//ip头必须大于或者等于20个字节,
//表示ip协议头长长度是4为,最大值是15,所以ip头最大60字节
if (iph->ihl < 5 || iph->version != 4)
goto inhdr_error;
//对整个IP协议信息的长度做合法性检查
if (!pskb_may_pull(skb, iph->ihl*4))
goto inhdr_error;
//再次初始化iphdr,因为pskb_may_pull会改变头信息存储结构
iph = ip_hdr(skb);
//校验和检查
if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
goto inhdr_error;
//包的总长度要大于实际数据的长度
//因为二层为了长度达到二层的长度可能会填充0
len = ntohs(iph->tot_len);
if (skb->len < len) {
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INTRUNCATEDPKTS);
goto drop;
//整个数据包的长度不能小于IP协议头的长度20字节,必须包含一个ip头
} else if (len < (iph->ihl*4))
goto inhdr_error;
//去掉二层的填充
if (pskb_trim_rcsum(skb, len)) {
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
goto drop;
}
//清空缓冲区
/* Remove any debris in the socket control block */
memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
/* Must drop socket now because of tproxy. */
skb_orphan(skb);
//进入网络过滤子系统的回调函数,首先是PRE_ROUTING链如果没有
//扔掉就调用ip_rcv_finish函数继续处理
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
inhdr_error:
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
drop:
kfree_skb(skb);
out:
return NET_RX_DROP;
}